@blockrun/franklin 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +256 -0
- package/dist/agent/commands.d.ts +27 -0
- package/dist/agent/commands.js +659 -0
- package/dist/agent/compact.d.ts +31 -0
- package/dist/agent/compact.js +366 -0
- package/dist/agent/context.d.ts +11 -0
- package/dist/agent/context.js +184 -0
- package/dist/agent/error-classifier.d.ts +10 -0
- package/dist/agent/error-classifier.js +61 -0
- package/dist/agent/llm.d.ts +63 -0
- package/dist/agent/llm.js +448 -0
- package/dist/agent/loop.d.ts +12 -0
- package/dist/agent/loop.js +346 -0
- package/dist/agent/optimize.d.ts +53 -0
- package/dist/agent/optimize.js +262 -0
- package/dist/agent/permissions.d.ts +39 -0
- package/dist/agent/permissions.js +226 -0
- package/dist/agent/reduce.d.ts +49 -0
- package/dist/agent/reduce.js +317 -0
- package/dist/agent/streaming-executor.d.ts +36 -0
- package/dist/agent/streaming-executor.js +149 -0
- package/dist/agent/tokens.d.ts +53 -0
- package/dist/agent/tokens.js +185 -0
- package/dist/agent/types.d.ts +125 -0
- package/dist/agent/types.js +5 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +27 -0
- package/dist/commands/balance.d.ts +1 -0
- package/dist/commands/balance.js +40 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.js +107 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +117 -0
- package/dist/commands/history.d.ts +5 -0
- package/dist/commands/history.js +31 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +92 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +89 -0
- package/dist/commands/models.d.ts +1 -0
- package/dist/commands/models.js +56 -0
- package/dist/commands/plugin.d.ts +14 -0
- package/dist/commands/plugin.js +176 -0
- package/dist/commands/proxy.d.ts +13 -0
- package/dist/commands/proxy.js +106 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +292 -0
- package/dist/commands/stats.d.ts +10 -0
- package/dist/commands/stats.js +94 -0
- package/dist/commands/uninit.d.ts +1 -0
- package/dist/commands/uninit.js +63 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +179 -0
- package/dist/mcp/client.d.ts +44 -0
- package/dist/mcp/client.js +147 -0
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.js +138 -0
- package/dist/plugin-sdk/channel.d.ts +100 -0
- package/dist/plugin-sdk/channel.js +10 -0
- package/dist/plugin-sdk/index.d.ts +14 -0
- package/dist/plugin-sdk/index.js +9 -0
- package/dist/plugin-sdk/plugin.d.ts +87 -0
- package/dist/plugin-sdk/plugin.js +7 -0
- package/dist/plugin-sdk/search.d.ts +13 -0
- package/dist/plugin-sdk/search.js +4 -0
- package/dist/plugin-sdk/tracker.d.ts +27 -0
- package/dist/plugin-sdk/tracker.js +5 -0
- package/dist/plugin-sdk/workflow.d.ts +126 -0
- package/dist/plugin-sdk/workflow.js +11 -0
- package/dist/plugins/registry.d.ts +33 -0
- package/dist/plugins/registry.js +155 -0
- package/dist/plugins/runner.d.ts +21 -0
- package/dist/plugins/runner.js +453 -0
- package/dist/plugins-bundled/social/index.d.ts +10 -0
- package/dist/plugins-bundled/social/index.js +363 -0
- package/dist/plugins-bundled/social/plugin.json +14 -0
- package/dist/plugins-bundled/social/prompts.d.ts +19 -0
- package/dist/plugins-bundled/social/prompts.js +67 -0
- package/dist/plugins-bundled/social/types.d.ts +58 -0
- package/dist/plugins-bundled/social/types.js +16 -0
- package/dist/pricing.d.ts +21 -0
- package/dist/pricing.js +91 -0
- package/dist/proxy/fallback.d.ts +38 -0
- package/dist/proxy/fallback.js +144 -0
- package/dist/proxy/server.d.ts +18 -0
- package/dist/proxy/server.js +576 -0
- package/dist/proxy/sse-translator.d.ts +29 -0
- package/dist/proxy/sse-translator.js +270 -0
- package/dist/router/index.d.ts +22 -0
- package/dist/router/index.js +269 -0
- package/dist/session/search.d.ts +33 -0
- package/dist/session/search.js +229 -0
- package/dist/session/storage.d.ts +48 -0
- package/dist/session/storage.js +173 -0
- package/dist/stats/insights.d.ts +55 -0
- package/dist/stats/insights.js +195 -0
- package/dist/stats/tracker.d.ts +54 -0
- package/dist/stats/tracker.js +165 -0
- package/dist/tools/askuser.d.ts +6 -0
- package/dist/tools/askuser.js +76 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +336 -0
- package/dist/tools/edit.d.ts +5 -0
- package/dist/tools/edit.js +148 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/glob.js +158 -0
- package/dist/tools/grep.d.ts +5 -0
- package/dist/tools/grep.js +194 -0
- package/dist/tools/imagegen.d.ts +6 -0
- package/dist/tools/imagegen.js +172 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.js +30 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.js +90 -0
- package/dist/tools/subagent.d.ts +5 -0
- package/dist/tools/subagent.js +116 -0
- package/dist/tools/task.d.ts +5 -0
- package/dist/tools/task.js +91 -0
- package/dist/tools/webfetch.d.ts +5 -0
- package/dist/tools/webfetch.js +166 -0
- package/dist/tools/websearch.d.ts +5 -0
- package/dist/tools/websearch.js +103 -0
- package/dist/tools/write.d.ts +5 -0
- package/dist/tools/write.js +114 -0
- package/dist/ui/app.d.ts +26 -0
- package/dist/ui/app.js +545 -0
- package/dist/ui/model-picker.d.ts +14 -0
- package/dist/ui/model-picker.js +161 -0
- package/dist/ui/terminal.d.ts +35 -0
- package/dist/ui/terminal.js +337 -0
- package/dist/wallet/manager.d.ts +10 -0
- package/dist/wallet/manager.js +23 -0
- package/package.json +79 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Workflow Plugin.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: This file ONLY imports from `../../plugin-sdk/`.
|
|
5
|
+
* It does NOT import from `src/agent/`, `src/commands/`, `src/social/`, etc.
|
|
6
|
+
* This is the boundary that keeps plugins decoupled from core internals.
|
|
7
|
+
*/
|
|
8
|
+
import { DEFAULT_MODEL_TIERS } from '../../plugin-sdk/index.js';
|
|
9
|
+
import { DEFAULT_REPLY_STYLE } from './types.js';
|
|
10
|
+
import { FILTER_SYSTEM, LEAD_SCORE_SYSTEM, buildReplyPrompt, buildKeywordPrompt, buildSubredditPrompt, } from './prompts.js';
|
|
11
|
+
// ─── Workflow Implementation ──────────────────────────────────────────────
|
|
12
|
+
const socialWorkflow = {
|
|
13
|
+
id: 'social',
|
|
14
|
+
name: 'Social Growth',
|
|
15
|
+
description: 'AI-powered social engagement on Reddit/X',
|
|
16
|
+
defaultConfig() {
|
|
17
|
+
return {
|
|
18
|
+
name: 'social',
|
|
19
|
+
models: { ...DEFAULT_MODEL_TIERS },
|
|
20
|
+
products: [],
|
|
21
|
+
platforms: {},
|
|
22
|
+
replyStyle: { ...DEFAULT_REPLY_STYLE },
|
|
23
|
+
targetUsers: '',
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
onboardingQuestions: [
|
|
27
|
+
{
|
|
28
|
+
id: 'product',
|
|
29
|
+
prompt: "What's your product? (name + one-line description)",
|
|
30
|
+
type: 'text',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'targetUsers',
|
|
34
|
+
prompt: 'Who are your target users? (be specific)',
|
|
35
|
+
type: 'text',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'platform',
|
|
39
|
+
prompt: 'Which platforms?',
|
|
40
|
+
type: 'select',
|
|
41
|
+
options: ['X/Twitter', 'Reddit', 'Both'],
|
|
42
|
+
default: 'Both',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'handle',
|
|
46
|
+
prompt: "What's your social media handle/username?",
|
|
47
|
+
type: 'text',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
async buildConfigFromAnswers(answers, llm) {
|
|
51
|
+
const [productName, ...descParts] = (answers.product || '').split('—').map(s => s.trim());
|
|
52
|
+
const productDesc = descParts.join(' — ') || productName;
|
|
53
|
+
const targetUsers = answers.targetUsers || '';
|
|
54
|
+
const platform = answers.platform || 'Both';
|
|
55
|
+
const handle = answers.handle || '';
|
|
56
|
+
// Auto-generate keywords using LLM
|
|
57
|
+
let keywords = [];
|
|
58
|
+
let subreddits = [];
|
|
59
|
+
try {
|
|
60
|
+
const kwResponse = await llm(buildKeywordPrompt(productName, productDesc, targetUsers));
|
|
61
|
+
const parsed = JSON.parse(kwResponse.replace(/```json?\n?/g, '').replace(/```/g, '').trim());
|
|
62
|
+
if (Array.isArray(parsed))
|
|
63
|
+
keywords = parsed;
|
|
64
|
+
}
|
|
65
|
+
catch { /* use empty */ }
|
|
66
|
+
if (platform === 'Reddit' || platform === 'Both') {
|
|
67
|
+
try {
|
|
68
|
+
const srResponse = await llm(buildSubredditPrompt(productName, productDesc, targetUsers));
|
|
69
|
+
const parsed = JSON.parse(srResponse.replace(/```json?\n?/g, '').replace(/```/g, '').trim());
|
|
70
|
+
if (Array.isArray(parsed))
|
|
71
|
+
subreddits = parsed;
|
|
72
|
+
}
|
|
73
|
+
catch { /* use empty */ }
|
|
74
|
+
}
|
|
75
|
+
const config = {
|
|
76
|
+
name: 'social',
|
|
77
|
+
models: { ...DEFAULT_MODEL_TIERS },
|
|
78
|
+
products: [{
|
|
79
|
+
name: productName,
|
|
80
|
+
description: productDesc,
|
|
81
|
+
keywords: keywords.slice(0, 10),
|
|
82
|
+
}],
|
|
83
|
+
platforms: {},
|
|
84
|
+
replyStyle: { ...DEFAULT_REPLY_STYLE },
|
|
85
|
+
targetUsers,
|
|
86
|
+
};
|
|
87
|
+
if (platform === 'X/Twitter' || platform === 'Both') {
|
|
88
|
+
config.platforms.x = {
|
|
89
|
+
username: handle.startsWith('@') ? handle : `@${handle}`,
|
|
90
|
+
dailyTarget: 20,
|
|
91
|
+
minDelaySeconds: 300,
|
|
92
|
+
searchQueries: keywords.slice(0, 10),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (platform === 'Reddit' || platform === 'Both') {
|
|
96
|
+
config.platforms.reddit = {
|
|
97
|
+
username: handle.replace('@', ''),
|
|
98
|
+
dailyTarget: 10,
|
|
99
|
+
minDelaySeconds: 600,
|
|
100
|
+
subreddits: subreddits.slice(0, 8),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return config;
|
|
104
|
+
},
|
|
105
|
+
steps: [
|
|
106
|
+
{
|
|
107
|
+
name: 'search',
|
|
108
|
+
modelTier: 'none',
|
|
109
|
+
execute: searchStep,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'filter',
|
|
113
|
+
modelTier: 'cheap',
|
|
114
|
+
execute: filterStep,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'score',
|
|
118
|
+
modelTier: 'cheap',
|
|
119
|
+
execute: scoreStep,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'draft',
|
|
123
|
+
modelTier: 'dynamic',
|
|
124
|
+
execute: draftStep,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'preview',
|
|
128
|
+
modelTier: 'none',
|
|
129
|
+
execute: previewStep,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'post',
|
|
133
|
+
modelTier: 'none',
|
|
134
|
+
execute: postStep,
|
|
135
|
+
skipInDryRun: true,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'track',
|
|
139
|
+
modelTier: 'none',
|
|
140
|
+
execute: trackStep,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
// ─── Step Implementations ─────────────────────────────────────────────────
|
|
145
|
+
async function searchStep(ctx) {
|
|
146
|
+
const sc = ctx.config;
|
|
147
|
+
const allResults = [];
|
|
148
|
+
// Search using configured queries
|
|
149
|
+
const queries = sc.platforms?.x?.searchQueries ?? sc.products?.[0]?.keywords ?? [];
|
|
150
|
+
for (const query of queries.slice(0, 5)) {
|
|
151
|
+
const results = await ctx.search(query, {
|
|
152
|
+
maxResults: 5,
|
|
153
|
+
sources: ['reddit', 'x', 'web'],
|
|
154
|
+
});
|
|
155
|
+
allResults.push(...results);
|
|
156
|
+
}
|
|
157
|
+
if (allResults.length === 0) {
|
|
158
|
+
return { summary: 'No posts found (search returned empty — channel plugins may not be installed)', abort: true };
|
|
159
|
+
}
|
|
160
|
+
// Dedup by URL
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
const unique = allResults.filter(r => {
|
|
163
|
+
if (seen.has(r.url))
|
|
164
|
+
return false;
|
|
165
|
+
seen.add(r.url);
|
|
166
|
+
return true;
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
data: { searchResults: unique, itemCount: unique.length },
|
|
170
|
+
summary: `Found ${unique.length} posts`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async function filterStep(ctx) {
|
|
174
|
+
const results = (ctx.data.searchResults ?? []);
|
|
175
|
+
const sc = ctx.config;
|
|
176
|
+
const product = sc.products?.[0];
|
|
177
|
+
if (!product)
|
|
178
|
+
return { summary: 'No product configured', abort: true };
|
|
179
|
+
const relevant = [];
|
|
180
|
+
for (const post of results) {
|
|
181
|
+
if (await ctx.isDuplicate(post.url))
|
|
182
|
+
continue;
|
|
183
|
+
const prompt = `Product: ${product.name} — ${product.description}\n\nPost:\nTitle: ${post.title}\nBody: ${post.snippet}\n\nIs this post relevant?`;
|
|
184
|
+
try {
|
|
185
|
+
const response = await ctx.callModel('cheap', prompt, FILTER_SYSTEM);
|
|
186
|
+
const parsed = JSON.parse(response.replace(/```json?\n?/g, '').replace(/```/g, '').trim());
|
|
187
|
+
if (parsed.relevant && parsed.score >= 5) {
|
|
188
|
+
relevant.push({ ...post, relevanceScore: parsed.score });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch { /* skip parse failures */ }
|
|
192
|
+
}
|
|
193
|
+
if (relevant.length === 0) {
|
|
194
|
+
return { summary: 'No relevant posts after filtering', abort: true };
|
|
195
|
+
}
|
|
196
|
+
relevant.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
197
|
+
return {
|
|
198
|
+
data: { filteredPosts: relevant, itemCount: relevant.length },
|
|
199
|
+
summary: `${relevant.length}/${results.length} posts are relevant`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async function scoreStep(ctx) {
|
|
203
|
+
const posts = (ctx.data.filteredPosts ?? []);
|
|
204
|
+
const sc = ctx.config;
|
|
205
|
+
const product = sc.products[0];
|
|
206
|
+
const scored = [];
|
|
207
|
+
for (const post of posts) {
|
|
208
|
+
const prompt = `Product: ${product.name} — ${product.description}\n\nPost:\nTitle: ${post.title}\nBody: ${post.snippet}\nAuthor: ${post.author ?? 'unknown'}`;
|
|
209
|
+
try {
|
|
210
|
+
const response = await ctx.callModel('cheap', prompt, LEAD_SCORE_SYSTEM);
|
|
211
|
+
const parsed = JSON.parse(response.replace(/```json?\n?/g, '').replace(/```/g, '').trim());
|
|
212
|
+
scored.push({
|
|
213
|
+
title: post.title,
|
|
214
|
+
url: post.url,
|
|
215
|
+
snippet: post.snippet,
|
|
216
|
+
platform: post.source.includes('reddit') ? 'reddit' : 'x',
|
|
217
|
+
author: post.author,
|
|
218
|
+
timestamp: post.timestamp,
|
|
219
|
+
commentCount: post.commentCount,
|
|
220
|
+
relevanceScore: post.relevanceScore,
|
|
221
|
+
leadScore: parsed.leadScore ?? 5,
|
|
222
|
+
urgency: parsed.urgency ?? 'medium',
|
|
223
|
+
painPoints: parsed.painPoints ?? [],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
scored.push({
|
|
228
|
+
title: post.title,
|
|
229
|
+
url: post.url,
|
|
230
|
+
snippet: post.snippet,
|
|
231
|
+
platform: post.source.includes('reddit') ? 'reddit' : 'x',
|
|
232
|
+
relevanceScore: post.relevanceScore,
|
|
233
|
+
leadScore: 5,
|
|
234
|
+
urgency: 'medium',
|
|
235
|
+
painPoints: [],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Track high-score leads
|
|
240
|
+
for (const s of scored.filter(s => s.leadScore >= 7)) {
|
|
241
|
+
await ctx.track('lead', {
|
|
242
|
+
url: s.url,
|
|
243
|
+
title: s.title,
|
|
244
|
+
leadScore: s.leadScore,
|
|
245
|
+
urgency: s.urgency,
|
|
246
|
+
painPoints: s.painPoints,
|
|
247
|
+
platform: s.platform,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
data: { scoredPosts: scored },
|
|
252
|
+
summary: `${scored.filter(s => s.leadScore >= 7).length} high-value leads, ${scored.length} total`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function draftStep(ctx) {
|
|
256
|
+
const posts = (ctx.data.scoredPosts ?? []);
|
|
257
|
+
const sc = ctx.config;
|
|
258
|
+
const product = sc.products[0];
|
|
259
|
+
const drafts = [];
|
|
260
|
+
for (const post of posts) {
|
|
261
|
+
const tier = post.leadScore >= 7 ? 'premium' : 'cheap';
|
|
262
|
+
const maxLength = post.platform === 'reddit' ? sc.replyStyle.maxLengthReddit : sc.replyStyle.maxLengthX;
|
|
263
|
+
const prompt = buildReplyPrompt({ title: post.title, body: post.snippet, platform: post.platform }, { name: product.name, description: product.description }, { tone: sc.replyStyle.tone, maxLength, rules: sc.replyStyle.rules });
|
|
264
|
+
try {
|
|
265
|
+
const text = await ctx.callModel(tier, prompt);
|
|
266
|
+
drafts.push({
|
|
267
|
+
post,
|
|
268
|
+
text: text.trim(),
|
|
269
|
+
model: tier,
|
|
270
|
+
tier,
|
|
271
|
+
estimatedCost: 0, // Cost tracked at runner level
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
ctx.log(`Failed to draft reply for ${post.url}: ${err.message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
data: { drafts, itemCount: drafts.length },
|
|
280
|
+
summary: `${drafts.length} draft replies generated`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function previewStep(ctx) {
|
|
284
|
+
const drafts = (ctx.data.drafts ?? []);
|
|
285
|
+
if (drafts.length === 0)
|
|
286
|
+
return { summary: 'No drafts to preview' };
|
|
287
|
+
const high = drafts.filter(d => d.post.leadScore >= 7);
|
|
288
|
+
const medium = drafts.filter(d => d.post.leadScore < 7);
|
|
289
|
+
ctx.log('\n' + '═'.repeat(50));
|
|
290
|
+
ctx.log('DRAFT REPLIES');
|
|
291
|
+
ctx.log('═'.repeat(50));
|
|
292
|
+
if (high.length > 0) {
|
|
293
|
+
ctx.log(`\n🎯 HIGH VALUE (${high.length} posts)`);
|
|
294
|
+
for (const d of high) {
|
|
295
|
+
ctx.log(`\n ${d.post.platform}: "${d.post.title.slice(0, 60)}"`);
|
|
296
|
+
ctx.log(` ⭐ Lead: ${d.post.leadScore}/10 | Tier: ${d.tier}`);
|
|
297
|
+
ctx.log(` Reply: "${d.text.slice(0, 120)}..."`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (medium.length > 0) {
|
|
301
|
+
ctx.log(`\n📋 MEDIUM (${medium.length} posts)`);
|
|
302
|
+
for (const d of medium) {
|
|
303
|
+
ctx.log(` ${d.post.platform}: "${d.post.title.slice(0, 50)}" | Lead: ${d.post.leadScore}/10`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
ctx.log('═'.repeat(50));
|
|
307
|
+
return { summary: `${high.length} high + ${medium.length} medium drafts` };
|
|
308
|
+
}
|
|
309
|
+
async function postStep(ctx) {
|
|
310
|
+
const drafts = (ctx.data.drafts ?? []);
|
|
311
|
+
let posted = 0;
|
|
312
|
+
for (const draft of drafts) {
|
|
313
|
+
await ctx.track('reply', {
|
|
314
|
+
url: draft.post.url,
|
|
315
|
+
platform: draft.post.platform,
|
|
316
|
+
tier: draft.tier,
|
|
317
|
+
leadScore: draft.post.leadScore,
|
|
318
|
+
replyLength: draft.text.length,
|
|
319
|
+
});
|
|
320
|
+
// Post via channel if available
|
|
321
|
+
if (ctx.sendMessage) {
|
|
322
|
+
try {
|
|
323
|
+
await ctx.sendMessage(draft.post.platform, {
|
|
324
|
+
text: draft.text,
|
|
325
|
+
inReplyTo: draft.post.url,
|
|
326
|
+
});
|
|
327
|
+
posted++;
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
ctx.log(`Failed to post to ${draft.post.platform}: ${err.message}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
ctx.log(`✓ Would post to ${draft.post.platform}: ${draft.post.url}`);
|
|
335
|
+
posted++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
data: { postedCount: posted },
|
|
340
|
+
summary: `${posted} replies ${ctx.dryRun ? 'drafted' : 'posted'}`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async function trackStep(ctx) {
|
|
344
|
+
const drafts = (ctx.data.drafts ?? []);
|
|
345
|
+
return {
|
|
346
|
+
summary: `${drafts.length} replies tracked`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
// ─── Plugin Export ────────────────────────────────────────────────────────
|
|
350
|
+
const plugin = {
|
|
351
|
+
manifest: {
|
|
352
|
+
id: 'social',
|
|
353
|
+
name: 'Social Growth',
|
|
354
|
+
description: 'AI-powered social engagement on Reddit/X',
|
|
355
|
+
version: '1.0.0',
|
|
356
|
+
provides: { workflows: ['social'] },
|
|
357
|
+
entry: 'index.js',
|
|
358
|
+
},
|
|
359
|
+
workflows: {
|
|
360
|
+
social: () => socialWorkflow,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
export default plugin;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "social",
|
|
3
|
+
"name": "Social Growth",
|
|
4
|
+
"description": "AI-powered social engagement — find relevant posts on Reddit/X, generate quality replies with multi-model routing",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"provides": {
|
|
7
|
+
"workflows": ["social"]
|
|
8
|
+
},
|
|
9
|
+
"entry": "index.js",
|
|
10
|
+
"author": "BlockRun",
|
|
11
|
+
"homepage": "https://github.com/BlockRunAI/runcode",
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"runcodeVersion": ">=2.6.0"
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social workflow prompts.
|
|
3
|
+
*/
|
|
4
|
+
export declare const FILTER_SYSTEM = "You are a social media relevance filter. Given a post and a product description, determine if the post is relevant for engagement.\n\nRespond with a JSON object:\n{\"relevant\": true/false, \"score\": 1-10, \"reason\": \"one line explanation\"}\n\nScore guide:\n- 9-10: Directly asking for what the product does, or complaining about the exact problem it solves\n- 7-8: Discussing the product's domain, comparing alternatives\n- 5-6: Tangentially related, could be relevant with a creative angle\n- 1-4: Not relevant enough to engage\n\nOnly mark as relevant if score >= 5.";
|
|
5
|
+
export declare const LEAD_SCORE_SYSTEM = "You are a lead qualification analyst. Given a social media post and product info, score the poster as a potential customer.\n\nRespond with a JSON object:\n{\"leadScore\": 1-10, \"urgency\": \"high\"|\"medium\"|\"low\", \"painPoints\": [\"point1\", \"point2\"], \"businessType\": \"description\"}\n\nLead score guide:\n- 9-10: Actively looking for a solution, has budget, decision maker\n- 7-8: Has the problem, open to solutions\n- 5-6: In the right space but not actively looking\n- 1-4: Low intent or wrong audience";
|
|
6
|
+
export declare function buildReplyPrompt(post: {
|
|
7
|
+
title: string;
|
|
8
|
+
body: string;
|
|
9
|
+
platform: string;
|
|
10
|
+
}, product: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
}, style: {
|
|
14
|
+
tone: string;
|
|
15
|
+
maxLength: number;
|
|
16
|
+
rules: string[];
|
|
17
|
+
}): string;
|
|
18
|
+
export declare function buildKeywordPrompt(productName: string, productDesc: string, targetUsers: string): string;
|
|
19
|
+
export declare function buildSubredditPrompt(productName: string, productDesc: string, targetUsers: string): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social workflow prompts.
|
|
3
|
+
*/
|
|
4
|
+
export const FILTER_SYSTEM = `You are a social media relevance filter. Given a post and a product description, determine if the post is relevant for engagement.
|
|
5
|
+
|
|
6
|
+
Respond with a JSON object:
|
|
7
|
+
{"relevant": true/false, "score": 1-10, "reason": "one line explanation"}
|
|
8
|
+
|
|
9
|
+
Score guide:
|
|
10
|
+
- 9-10: Directly asking for what the product does, or complaining about the exact problem it solves
|
|
11
|
+
- 7-8: Discussing the product's domain, comparing alternatives
|
|
12
|
+
- 5-6: Tangentially related, could be relevant with a creative angle
|
|
13
|
+
- 1-4: Not relevant enough to engage
|
|
14
|
+
|
|
15
|
+
Only mark as relevant if score >= 5.`;
|
|
16
|
+
export const LEAD_SCORE_SYSTEM = `You are a lead qualification analyst. Given a social media post and product info, score the poster as a potential customer.
|
|
17
|
+
|
|
18
|
+
Respond with a JSON object:
|
|
19
|
+
{"leadScore": 1-10, "urgency": "high"|"medium"|"low", "painPoints": ["point1", "point2"], "businessType": "description"}
|
|
20
|
+
|
|
21
|
+
Lead score guide:
|
|
22
|
+
- 9-10: Actively looking for a solution, has budget, decision maker
|
|
23
|
+
- 7-8: Has the problem, open to solutions
|
|
24
|
+
- 5-6: In the right space but not actively looking
|
|
25
|
+
- 1-4: Low intent or wrong audience`;
|
|
26
|
+
export function buildReplyPrompt(post, product, style) {
|
|
27
|
+
return `Write a reply to this ${post.platform} post. You are NOT writing an ad. You are a real user sharing your experience.
|
|
28
|
+
|
|
29
|
+
POST:
|
|
30
|
+
Title: ${post.title}
|
|
31
|
+
Body: ${post.body}
|
|
32
|
+
|
|
33
|
+
YOUR PRODUCT (mention naturally, not as an ad):
|
|
34
|
+
${product.name}: ${product.description}
|
|
35
|
+
|
|
36
|
+
REPLY STYLE:
|
|
37
|
+
- Tone: ${style.tone}
|
|
38
|
+
- Max length: ${style.maxLength} characters
|
|
39
|
+
- Rules:
|
|
40
|
+
${style.rules.map(r => ` - ${r}`).join('\n')}
|
|
41
|
+
|
|
42
|
+
Write ONLY the reply text. No quotation marks, no meta-commentary, no "Here's my reply:".`;
|
|
43
|
+
}
|
|
44
|
+
export function buildKeywordPrompt(productName, productDesc, targetUsers) {
|
|
45
|
+
return `Given this product and target audience, generate 10 search queries that would find relevant social media posts to engage with.
|
|
46
|
+
|
|
47
|
+
Product: ${productName}
|
|
48
|
+
Description: ${productDesc}
|
|
49
|
+
Target users: ${targetUsers}
|
|
50
|
+
|
|
51
|
+
Return a JSON array of 10 search queries. Mix specific and broad queries.
|
|
52
|
+
Example: ["claude code rate limit alternative", "ai coding agent comparison 2026", ...]
|
|
53
|
+
|
|
54
|
+
Return ONLY the JSON array.`;
|
|
55
|
+
}
|
|
56
|
+
export function buildSubredditPrompt(productName, productDesc, targetUsers) {
|
|
57
|
+
return `Given this product and target audience, suggest 5-8 subreddits where the target users hang out.
|
|
58
|
+
|
|
59
|
+
Product: ${productName}
|
|
60
|
+
Description: ${productDesc}
|
|
61
|
+
Target users: ${targetUsers}
|
|
62
|
+
|
|
63
|
+
Return a JSON array of subreddit names (without r/ prefix).
|
|
64
|
+
Example: ["programming", "MachineLearning", "LocalLLaMA", ...]
|
|
65
|
+
|
|
66
|
+
Return ONLY the JSON array.`;
|
|
67
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social plugin types — extends WorkflowConfig with social-specific fields.
|
|
3
|
+
*/
|
|
4
|
+
import type { WorkflowConfig } from '../../plugin-sdk/index.js';
|
|
5
|
+
export interface SocialProduct {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
keywords: string[];
|
|
9
|
+
url?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SocialPlatformConfig {
|
|
12
|
+
username: string;
|
|
13
|
+
dailyTarget: number;
|
|
14
|
+
minDelaySeconds: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SocialReplyStyle {
|
|
17
|
+
tone: string;
|
|
18
|
+
maxLengthReddit: number;
|
|
19
|
+
maxLengthX: number;
|
|
20
|
+
rules: string[];
|
|
21
|
+
imageForHighValue: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface SocialConfig extends WorkflowConfig {
|
|
24
|
+
name: 'social';
|
|
25
|
+
products: SocialProduct[];
|
|
26
|
+
platforms: {
|
|
27
|
+
reddit?: SocialPlatformConfig & {
|
|
28
|
+
subreddits: string[];
|
|
29
|
+
};
|
|
30
|
+
x?: SocialPlatformConfig & {
|
|
31
|
+
searchQueries: string[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
replyStyle: SocialReplyStyle;
|
|
35
|
+
targetUsers: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ScoredPost {
|
|
38
|
+
title: string;
|
|
39
|
+
url: string;
|
|
40
|
+
snippet: string;
|
|
41
|
+
platform: 'reddit' | 'x';
|
|
42
|
+
author?: string;
|
|
43
|
+
timestamp?: string;
|
|
44
|
+
commentCount?: number;
|
|
45
|
+
relevanceScore: number;
|
|
46
|
+
leadScore: number;
|
|
47
|
+
urgency: 'high' | 'medium' | 'low';
|
|
48
|
+
painPoints: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface DraftReply {
|
|
51
|
+
post: ScoredPost;
|
|
52
|
+
text: string;
|
|
53
|
+
model: string;
|
|
54
|
+
tier: 'cheap' | 'premium';
|
|
55
|
+
estimatedCost: number;
|
|
56
|
+
imageUrl?: string;
|
|
57
|
+
}
|
|
58
|
+
export declare const DEFAULT_REPLY_STYLE: SocialReplyStyle;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social plugin types — extends WorkflowConfig with social-specific fields.
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_REPLY_STYLE = {
|
|
5
|
+
tone: 'knowledgeable developer sharing experience',
|
|
6
|
+
maxLengthReddit: 400,
|
|
7
|
+
maxLengthX: 260,
|
|
8
|
+
rules: [
|
|
9
|
+
'Lead with a genuine insight or question about the post',
|
|
10
|
+
'Mention product naturally as "what I use/built" — not as an ad',
|
|
11
|
+
'Never start with "Great post!" or "I agree!"',
|
|
12
|
+
'Sound like a real developer who has faced this problem',
|
|
13
|
+
'If post is not directly relevant, skip — do not force a mention',
|
|
14
|
+
],
|
|
15
|
+
imageForHighValue: true,
|
|
16
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for model pricing (per 1M tokens).
|
|
3
|
+
* Used by agent loop, proxy server, stats tracker, and router.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MODEL_PRICING: Record<string, {
|
|
6
|
+
input: number;
|
|
7
|
+
output: number;
|
|
8
|
+
perCall?: number;
|
|
9
|
+
}>;
|
|
10
|
+
/** Opus pricing for savings calculations */
|
|
11
|
+
export declare const OPUS_PRICING: {
|
|
12
|
+
input: number;
|
|
13
|
+
output: number;
|
|
14
|
+
perCall?: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Estimate cost in USD for a request.
|
|
18
|
+
* Falls back to $2/$10 per 1M for unknown models.
|
|
19
|
+
* For per-call models (perCall > 0), uses flat per-call pricing instead of per-token.
|
|
20
|
+
*/
|
|
21
|
+
export declare function estimateCost(model: string, inputTokens: number, outputTokens: number, calls?: number): number;
|
package/dist/pricing.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for model pricing (per 1M tokens).
|
|
3
|
+
* Used by agent loop, proxy server, stats tracker, and router.
|
|
4
|
+
*/
|
|
5
|
+
export const MODEL_PRICING = {
|
|
6
|
+
// Routing profiles (blended averages)
|
|
7
|
+
'blockrun/auto': { input: 0.8, output: 4.0 },
|
|
8
|
+
'blockrun/eco': { input: 0.2, output: 1.0 },
|
|
9
|
+
'blockrun/premium': { input: 3.0, output: 15.0 },
|
|
10
|
+
'blockrun/free': { input: 0, output: 0 },
|
|
11
|
+
// FREE - NVIDIA models
|
|
12
|
+
'nvidia/gpt-oss-120b': { input: 0, output: 0 },
|
|
13
|
+
'nvidia/gpt-oss-20b': { input: 0, output: 0 },
|
|
14
|
+
'nvidia/nemotron-ultra-253b': { input: 0, output: 0 },
|
|
15
|
+
'nvidia/nemotron-3-super-120b': { input: 0, output: 0 },
|
|
16
|
+
'nvidia/nemotron-super-49b': { input: 0, output: 0 },
|
|
17
|
+
'nvidia/deepseek-v3.2': { input: 0, output: 0 },
|
|
18
|
+
'nvidia/mistral-large-3-675b': { input: 0, output: 0 },
|
|
19
|
+
'nvidia/qwen3-coder-480b': { input: 0, output: 0 },
|
|
20
|
+
'nvidia/devstral-2-123b': { input: 0, output: 0 },
|
|
21
|
+
'nvidia/glm-4.7': { input: 0, output: 0 },
|
|
22
|
+
'nvidia/llama-4-maverick': { input: 0, output: 0 },
|
|
23
|
+
// Anthropic
|
|
24
|
+
'anthropic/claude-sonnet-4.6': { input: 3.0, output: 15.0 },
|
|
25
|
+
'anthropic/claude-opus-4.6': { input: 5.0, output: 25.0 },
|
|
26
|
+
'anthropic/claude-haiku-4.5': { input: 1.0, output: 5.0 },
|
|
27
|
+
'anthropic/claude-haiku-4.5-20251001': { input: 1.0, output: 5.0 },
|
|
28
|
+
// OpenAI
|
|
29
|
+
'openai/gpt-5-nano': { input: 0.05, output: 0.4 },
|
|
30
|
+
'openai/gpt-4.1-nano': { input: 0.1, output: 0.4 },
|
|
31
|
+
'openai/gpt-4o-mini': { input: 0.15, output: 0.6 },
|
|
32
|
+
'openai/gpt-5-mini': { input: 0.25, output: 2.0 },
|
|
33
|
+
'openai/gpt-4.1-mini': { input: 0.4, output: 1.6 },
|
|
34
|
+
'openai/gpt-5.2': { input: 1.75, output: 14.0 },
|
|
35
|
+
'openai/gpt-5.3': { input: 1.75, output: 14.0 },
|
|
36
|
+
'openai/gpt-5.3-codex': { input: 1.75, output: 14.0 },
|
|
37
|
+
'openai/gpt-4.1': { input: 2.0, output: 8.0 },
|
|
38
|
+
'openai/o3': { input: 2.0, output: 8.0 },
|
|
39
|
+
'openai/gpt-4o': { input: 2.5, output: 10.0 },
|
|
40
|
+
'openai/gpt-5.4': { input: 2.5, output: 15.0 },
|
|
41
|
+
'openai/o1-mini': { input: 1.1, output: 4.4 },
|
|
42
|
+
'openai/o3-mini': { input: 1.1, output: 4.4 },
|
|
43
|
+
'openai/o4-mini': { input: 1.1, output: 4.4 },
|
|
44
|
+
'openai/o1': { input: 15.0, output: 60.0 },
|
|
45
|
+
'openai/gpt-5.2-pro': { input: 21.0, output: 168.0 },
|
|
46
|
+
'openai/gpt-5.4-pro': { input: 30.0, output: 180.0 },
|
|
47
|
+
// Google
|
|
48
|
+
'google/gemini-2.5-flash-lite': { input: 0.1, output: 0.4 },
|
|
49
|
+
'google/gemini-2.5-flash': { input: 0.3, output: 2.5 },
|
|
50
|
+
'google/gemini-3-flash-preview': { input: 0.5, output: 3.0 },
|
|
51
|
+
'google/gemini-2.5-pro': { input: 1.25, output: 10.0 },
|
|
52
|
+
'google/gemini-3-pro-preview': { input: 2.0, output: 12.0 },
|
|
53
|
+
'google/gemini-3.1-pro': { input: 2.0, output: 12.0 },
|
|
54
|
+
// xAI
|
|
55
|
+
'xai/grok-4-fast': { input: 0.2, output: 0.5 },
|
|
56
|
+
'xai/grok-4-fast-reasoning': { input: 0.2, output: 0.5 },
|
|
57
|
+
'xai/grok-4-1-fast': { input: 0.2, output: 0.5 },
|
|
58
|
+
'xai/grok-4-1-fast-reasoning': { input: 0.2, output: 0.5 },
|
|
59
|
+
'xai/grok-4-0709': { input: 0.2, output: 1.5 },
|
|
60
|
+
'xai/grok-3-mini': { input: 0.3, output: 0.5 },
|
|
61
|
+
'xai/grok-2-vision': { input: 2.0, output: 10.0 },
|
|
62
|
+
'xai/grok-3': { input: 3.0, output: 15.0 },
|
|
63
|
+
// DeepSeek
|
|
64
|
+
'deepseek/deepseek-chat': { input: 0.28, output: 0.42 },
|
|
65
|
+
'deepseek/deepseek-reasoner': { input: 0.28, output: 0.42 },
|
|
66
|
+
// Minimax
|
|
67
|
+
'minimax/minimax-m2.7': { input: 0.3, output: 1.2 },
|
|
68
|
+
'minimax/minimax-m2.5': { input: 0.3, output: 1.2 },
|
|
69
|
+
// Others
|
|
70
|
+
'moonshot/kimi-k2.5': { input: 0.6, output: 3.0 },
|
|
71
|
+
'nvidia/kimi-k2.5': { input: 0.55, output: 2.5 },
|
|
72
|
+
// PROMOTION (active ~2026-04): flat $0.001/call. Normal pricing: input:1.00, output:3.20
|
|
73
|
+
'zai/glm-5.1': { input: 0, output: 0, perCall: 0.001 },
|
|
74
|
+
// PROMOTION (active ~2026-04): flat $0.001/call. Normal pricing: input:1.20, output:4.00
|
|
75
|
+
'zai/glm-5.1-turbo': { input: 0, output: 0, perCall: 0.001 },
|
|
76
|
+
};
|
|
77
|
+
/** Opus pricing for savings calculations */
|
|
78
|
+
export const OPUS_PRICING = MODEL_PRICING['anthropic/claude-opus-4.6'];
|
|
79
|
+
/**
|
|
80
|
+
* Estimate cost in USD for a request.
|
|
81
|
+
* Falls back to $2/$10 per 1M for unknown models.
|
|
82
|
+
* For per-call models (perCall > 0), uses flat per-call pricing instead of per-token.
|
|
83
|
+
*/
|
|
84
|
+
export function estimateCost(model, inputTokens, outputTokens, calls = 1) {
|
|
85
|
+
const pricing = MODEL_PRICING[model] || { input: 2.0, output: 10.0 };
|
|
86
|
+
if (pricing.perCall) {
|
|
87
|
+
return pricing.perCall * calls;
|
|
88
|
+
}
|
|
89
|
+
return ((inputTokens / 1_000_000) * pricing.input +
|
|
90
|
+
(outputTokens / 1_000_000) * pricing.output);
|
|
91
|
+
}
|