← back to Linkedin Agent
src/lib/compose.ts
84 lines
/**
* compose.ts — merges Steve's bio (templates/STEVE_BIO.md) into post drafts.
*
* The point: every post the agent generates carries a tasteful pointer back
* to Steve's current product/agent inventory. NOT spammy CTA — folded into
* the natural close of the post.
*/
import { readFileSync } from 'fs';
import { join } from 'path';
const ROOT = process.cwd();
let cached: { bio: string; mtime: number } | null = null;
function readBio(): string {
const path = join(ROOT, 'templates/STEVE_BIO.md');
try {
const stat = (require('fs') as typeof import('fs')).statSync(path);
if (cached && cached.mtime === stat.mtimeMs) return cached.bio;
const bio = readFileSync(path, 'utf8');
cached = { bio, mtime: stat.mtimeMs };
return bio;
} catch {
return '';
}
}
// Pull just the "Active products" + "Autonomous agents" sections, sample N
// items at random, format as a 1-line tagline.
function extractTagline(bio: string, sample = 3): string {
const sections = bio.split(/^## /m);
const products = (sections.find((s) => s.startsWith('Active products'))?.match(/^- \*\*([^*]+)\*\*/gm) || [])
.map((l) => l.replace(/^- \*\*([^*]+)\*\*.*/, '$1'));
const agents = (sections.find((s) => s.startsWith('Autonomous agents'))?.match(/^- \*\*([^*]+)\*\*/gm) || [])
.map((l) => l.replace(/^- \*\*([^*]+)\*\*.*/, '$1'));
const pool = [...products, ...agents];
if (pool.length === 0) return '';
// Deterministic-ish sample by current minute (so re-running same minute = same output)
const seed = Math.floor(Date.now() / 60_000);
const picked: string[] = [];
for (let i = 0; i < Math.min(sample, pool.length); i++) {
const idx = (seed * (i + 1) * 31) % pool.length;
if (!picked.includes(pool[idx])) picked.push(pool[idx]);
}
return picked.join(' · ');
}
/**
* Compose a final post: takes a draft (LLM-generated body) and weaves in
* Steve's bio tagline at the end. If the draft is already > 2800 chars
* (LinkedIn limit is 3000), skip the tagline.
*/
export function composePost(draft: string, opts?: { tagline?: boolean }): string {
const wantTagline = opts?.tagline !== false;
const bio = readBio();
if (!bio || !wantTagline) return draft.trim();
const tagline = extractTagline(bio, 3);
if (!tagline) return draft.trim();
const body = draft.trim();
if (body.length > 2800) return body;
// Naturalize: if draft already mentions Steve's product names, skip
for (const name of tagline.split(' · ')) {
if (body.toLowerCase().includes(name.toLowerCase())) return body;
}
return `${body}\n\n—\nBuilding lately: ${tagline}.`;
}
/**
* Read a named template from templates/ and return its body.
*/
export function readTemplate(name: string): string {
try {
return readFileSync(join(ROOT, 'templates', `${name}.md`), 'utf8');
} catch {
return '';
}
}