Skip to content

Commit b1dd1f2

Browse files
committed
feat: add growth distribution tooling (launch, listen, reply)
Add launch command that generates Show HN, Reddit, social, and dev.to drafts from GROWTH.md config. Add listen command for intent monitoring (HN, Reddit, hotmention). Add reply command for AI-assisted engagement. Add HN and directory generators, hotmention integration, Telegram platform, growth-config loader, and cli-utils. Extend schema.prisma with growth tables.
1 parent db37442 commit b1dd1f2

14 files changed

Lines changed: 1237 additions & 1 deletion

File tree

schema.prisma

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,38 @@ model ScheduledPost {
8585
@@map("scheduled_posts")
8686
}
8787

88+
model IntentSignal {
89+
id String @id @default(cuid())
90+
appName String
91+
platform String // 'reddit' | 'x' | 'linkedin' | 'quora'
92+
url String @unique
93+
title String
94+
excerpt String
95+
intentScore Int @default(0) // 0-100
96+
status String @default("new") // 'new' | 'drafting' | 'replied' | 'ignored'
97+
createdAt DateTime @default(now())
98+
drafts Draft[]
99+
100+
@@index([appName, status])
101+
@@index([intentScore])
102+
@@map("intent_signals")
103+
}
104+
105+
model Draft {
106+
id String @id @default(cuid())
107+
appName String
108+
platform String // 'reddit' | 'hackernews' | 'bluesky' | 'mastodon' | 'telegram' | 'devto'
109+
content String
110+
metadata String @default("{}") // JSON: subreddit, title, tags, threadUrl, etc.
111+
status String @default("draft") // 'draft' | 'approved' | 'posted'
112+
signalId String?
113+
signal IntentSignal? @relation(fields: [signalId], references: [id], onDelete: SetNull)
114+
createdAt DateTime @default(now())
115+
postedAt DateTime?
116+
117+
@@index([appName, status])
118+
@@map("drafts")
119+
}
120+
88121
// Indexes for performance
89122
// Note: Indexes are automatically created for @unique and foreign key fields

src/HyperPost.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
RedditPlatform,
1414
DevtoPlatform,
1515
MediumPlatform,
16+
TelegramPlatform,
1617
BasePlatform
1718
} from './platforms';
1819
import * as crypto from 'crypto';
@@ -39,7 +40,8 @@ export class HyperPost {
3940
{ name: 'reddit', displayName: 'Reddit' },
4041
{ name: 'discord', displayName: 'Discord' },
4142
{ name: 'devto', displayName: 'Dev.to' },
42-
{ name: 'medium', displayName: 'Medium' }
43+
{ name: 'medium', displayName: 'Medium' },
44+
{ name: 'telegram', displayName: 'Telegram' }
4345
];
4446

4547
for (const platform of platformData) {
@@ -185,6 +187,11 @@ export class HyperPost {
185187
if (credentials.medium) {
186188
this.platforms.set('medium', new MediumPlatform(credentials.medium));
187189
}
190+
191+
// Telegram
192+
if (credentials.telegram) {
193+
this.platforms.set('telegram', new TelegramPlatform(credentials.telegram));
194+
}
188195
}
189196

190197
/**

src/cli-utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { SignupManager } from './signup-manager';
2+
3+
export function loadCredentials(): Record<string, Record<string, string>> {
4+
const credentials: Record<string, Record<string, string>> = {};
5+
const signupManager = new SignupManager();
6+
const completedAccounts = signupManager.getAllCompletedAccounts();
7+
8+
for (const [platform, accountData] of Object.entries(completedAccounts)) {
9+
credentials[platform] = accountData as Record<string, string>;
10+
}
11+
12+
return credentials;
13+
}

src/cli.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,4 +1442,102 @@ program
14421442
}
14431443
});
14441444

1445+
// === Distribution commands ===
1446+
1447+
program
1448+
.command('launch')
1449+
.description('Generate launch drafts for an app (HN, Reddit, social, directories)')
1450+
.argument('<app-name>', 'App name (must match GROWTH.md app field)')
1451+
.option('--app-dir <path>', 'Path to app directory (default: ~/dev/hyperdrift/apps/<app-name>)')
1452+
.option('--dry-run', 'Preview drafts without saving to DB')
1453+
.action(async (appName, options) => {
1454+
try {
1455+
const { generateLaunchDrafts } = await import('./commands/launch');
1456+
await generateLaunchDrafts(appName, {
1457+
appDir: options.appDir,
1458+
dryRun: options.dryRun,
1459+
});
1460+
} catch (err) {
1461+
console.error(`Error: ${(err as Error).message}`);
1462+
process.exit(1);
1463+
}
1464+
});
1465+
1466+
program
1467+
.command('listen')
1468+
.description('Fetch intent signals for an app and generate reply drafts')
1469+
.argument('<app-name>', 'App name')
1470+
.option('--app-dir <path>', 'Path to app directory')
1471+
.option('--min-score <n>', 'Minimum intent score 0-100 (default: 50)', '50')
1472+
.option('--dry-run', 'Show signals without saving')
1473+
.action(async (appName, options) => {
1474+
try {
1475+
const { listenForSignals } = await import('./commands/listen');
1476+
await listenForSignals(appName, {
1477+
appDir: options.appDir,
1478+
minScore: parseInt(options.minScore, 10),
1479+
dryRun: options.dryRun,
1480+
});
1481+
} catch (err) {
1482+
console.error(`Error: ${(err as Error).message}`);
1483+
process.exit(1);
1484+
}
1485+
});
1486+
1487+
const replyCmd = program
1488+
.command('reply')
1489+
.description('Manage reply drafts from intent signals');
1490+
1491+
replyCmd
1492+
.command('list')
1493+
.description('List pending reply drafts')
1494+
.option('--app <name>', 'Filter by app name')
1495+
.option('--min-score <n>', 'Minimum intent score', '0')
1496+
.option('--status <status>', 'Filter by status (draft|approved|posted)', 'draft')
1497+
.action(async (options) => {
1498+
const { listDrafts } = await import('./commands/reply');
1499+
await listDrafts({ app: options.app, status: options.status });
1500+
});
1501+
1502+
replyCmd
1503+
.command('view <id>')
1504+
.description('Show full content of a draft')
1505+
.action(async (id) => {
1506+
const { viewDraft } = await import('./commands/reply');
1507+
await viewDraft(id);
1508+
});
1509+
1510+
replyCmd
1511+
.command('approve <id>')
1512+
.description('Mark a draft as approved for posting')
1513+
.action(async (id) => {
1514+
const { approveDraft } = await import('./commands/reply');
1515+
await approveDraft(id);
1516+
});
1517+
1518+
replyCmd
1519+
.command('post <id>')
1520+
.description('Post an approved draft to its platform')
1521+
.action(async (id) => {
1522+
const { postDraft } = await import('./commands/reply');
1523+
await postDraft(id);
1524+
});
1525+
1526+
replyCmd
1527+
.command('post-all')
1528+
.description('Post all approved drafts for an app')
1529+
.requiredOption('--app <name>', 'App name')
1530+
.action(async (options) => {
1531+
const { postAllApproved } = await import('./commands/reply');
1532+
await postAllApproved(options.app);
1533+
});
1534+
1535+
replyCmd
1536+
.command('ignore <id>')
1537+
.description('Ignore a draft and its signal')
1538+
.action(async (id) => {
1539+
const { ignoreDraft } = await import('./commands/reply');
1540+
await ignoreDraft(id);
1541+
});
1542+
14451543
program.parse();

0 commit comments

Comments
 (0)