@blockrun/franklin 3.8.0 → 3.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.8.0",
3
+ "version": "3.8.2",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,5 +0,0 @@
1
- interface HistoryOptions {
2
- n?: string;
3
- }
4
- export declare function historyCommand(options: HistoryOptions): void;
5
- export {};
@@ -1,31 +0,0 @@
1
- import chalk from 'chalk';
2
- import { loadStats } from '../stats/tracker.js';
3
- export function historyCommand(options) {
4
- const { history } = loadStats();
5
- const limit = Math.min(parseInt(options.n || '20', 10), history.length);
6
- console.log(chalk.bold(`
7
- 📜 Last ${limit} Requests\n`));
8
- console.log('─'.repeat(55));
9
- if (history.length === 0) {
10
- console.log(chalk.gray('\n No history recorded yet.\n'));
11
- console.log('─'.repeat(55) + '\n');
12
- return;
13
- }
14
- const recent = history.slice(-limit).reverse();
15
- for (const record of recent) {
16
- const time = new Date(record.timestamp).toLocaleString();
17
- const model = record.model.split('/').pop() || record.model;
18
- const cost = '$' + record.costUsd.toFixed(5);
19
- const tokens = `${record.inputTokens}+${record.outputTokens}`.padEnd(10);
20
- const latency = `${record.latencyMs}ms`.padEnd(8);
21
- const fallbackMark = record.fallback ? chalk.yellow(' ↺') : '';
22
- console.log(chalk.gray(`[${time}]`) +
23
- ` ${model.padEnd(20)}${fallbackMark} ` +
24
- chalk.cyan(tokens) +
25
- chalk.magenta(latency) +
26
- chalk.green(cost));
27
- }
28
- console.log('\n' + '─'.repeat(55));
29
- console.log(chalk.gray(` Showing ${limit} of ${history.length} total records.`));
30
- console.log(chalk.gray(' Run `runcode stats` for more detailed statistics.\n'));
31
- }
@@ -1,10 +0,0 @@
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 type { Plugin } from '../../plugin-sdk/index.js';
9
- declare const plugin: Plugin;
10
- export default plugin;
@@ -1,363 +0,0 @@
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;
@@ -1,14 +0,0 @@
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
- }
@@ -1,19 +0,0 @@
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;
@@ -1,67 +0,0 @@
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
- }
@@ -1,58 +0,0 @@
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;
@@ -1,16 +0,0 @@
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
- };