@blockrun/franklin 3.8.35 → 3.8.37

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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/commands.js +1 -1
  3. package/dist/agent/compact.js +1 -1
  4. package/dist/agent/evaluator.d.ts +3 -1
  5. package/dist/agent/evaluator.js +44 -8
  6. package/dist/agent/llm.js +2 -2
  7. package/dist/agent/loop.js +19 -0
  8. package/dist/agent/optimize.js +1 -0
  9. package/dist/agent/permissions.js +10 -1
  10. package/dist/agent/tokens.js +4 -0
  11. package/dist/agent/types.d.ts +22 -1
  12. package/dist/commands/balance.js +1 -1
  13. package/dist/commands/daemon.js +23 -16
  14. package/dist/commands/plugin.d.ts +1 -1
  15. package/dist/commands/plugin.js +10 -10
  16. package/dist/commands/stats.d.ts +1 -1
  17. package/dist/commands/stats.js +2 -2
  18. package/dist/index.js +2 -2
  19. package/dist/panel/server.js +7 -6
  20. package/dist/plugin-sdk/index.d.ts +2 -2
  21. package/dist/plugin-sdk/index.js +2 -2
  22. package/dist/plugin-sdk/plugin.d.ts +4 -4
  23. package/dist/plugins/registry.d.ts +3 -3
  24. package/dist/plugins/registry.js +6 -6
  25. package/dist/pricing.js +1 -0
  26. package/dist/proxy/server.js +148 -26
  27. package/dist/router/index.js +3 -3
  28. package/dist/session/storage.js +2 -2
  29. package/dist/tools/imagegen.d.ts +14 -0
  30. package/dist/tools/imagegen.js +154 -22
  31. package/dist/tools/read.js +29 -2
  32. package/dist/tools/videogen.d.ts +14 -3
  33. package/dist/tools/videogen.js +161 -28
  34. package/dist/tools/webhook.js +2 -1
  35. package/dist/trading/providers/coingecko/client.js +2 -1
  36. package/dist/ui/app.js +12 -12
  37. package/dist/ui/model-picker.js +7 -4
  38. package/dist/wallet/index.d.ts +17 -0
  39. package/dist/wallet/index.js +22 -0
  40. package/package.json +7 -5
package/README.md CHANGED
@@ -398,7 +398,7 @@ That economic loop is the product.
398
398
 
399
399
  ```text
400
400
  src/
401
- ├── index.ts CLI entry (franklin + runcode alias)
401
+ ├── index.ts CLI entry (franklin)
402
402
  ├── banner.ts Ben Franklin portrait + FRANKLIN gradient text
403
403
  ├── agent/ Agent loop, LLM client, compaction, commands
404
404
  ├── tools/ 20+ built-in tools (Read/Write/Edit/Bash/Glob/Grep/
@@ -289,7 +289,7 @@ const DIRECT_COMMANDS = {
289
289
  }
290
290
  const hasWallet = fs.existsSync(path.join(BLOCKRUN_DIR, 'wallet.json'))
291
291
  || fs.existsSync(path.join(BLOCKRUN_DIR, 'solana-wallet.json'));
292
- checks.push(hasWallet ? '✓ wallet configured' : '⚠ no wallet — run: runcode setup');
292
+ checks.push(hasWallet ? '✓ wallet configured' : '⚠ no wallet — run: franklin setup');
293
293
  checks.push(fs.existsSync(path.join(BLOCKRUN_DIR, 'franklin-config.json')) || fs.existsSync(path.join(BLOCKRUN_DIR, 'runcode-config.json')) ? '✓ config file exists' : '⚠ no config — using defaults');
294
294
  // Check MCP
295
295
  const { listMcpServers } = await import('../mcp/client.js');
@@ -434,7 +434,7 @@ function pickCompactionModel(primaryModel) {
434
434
  if (primaryModel.includes('opus') || primaryModel.includes('pro')) {
435
435
  return 'anthropic/claude-sonnet-4.6';
436
436
  }
437
- if (primaryModel.includes('sonnet') || primaryModel.includes('gpt-5.4') || primaryModel.includes('gemini-2.5-pro')) {
437
+ if (primaryModel.includes('sonnet') || primaryModel.includes('gpt-5.4') || primaryModel.includes('gpt-5.5') || primaryModel.includes('gemini-2.5-pro')) {
438
438
  return 'anthropic/claude-haiku-4.5-20251001';
439
439
  }
440
440
  if (primaryModel.includes('haiku') || primaryModel.includes('mini') || primaryModel.includes('nano')) {
@@ -31,7 +31,9 @@ export interface GroundingResult {
31
31
  }
32
32
  /**
33
33
  * Decide whether this turn warrants a grounding check. Principles:
34
- * - Non-trivial user input (not a greeting, not a slash command)
34
+ * - Non-trivial user input (not a greeting, not a slash command), OR
35
+ * the assistant answer contains specific factual claims (numbers + units,
36
+ * currency, dates, times) regardless of input length
35
37
  * - Non-trivial assistant text output (not just a tool-result echo)
36
38
  *
37
39
  * Intentionally NOT gating on tool-type (read vs write) — the whole point
@@ -71,11 +71,19 @@ If not GROUNDED, list each issue on its own line starting with "- " and the tool
71
71
 
72
72
  Empty line between verdict and list. No other text. No preamble. No apology. Be terse.`;
73
73
  // ─── Trigger policy ──────────────────────────────────────────────────────
74
- const MIN_USER_CHARS = 20; // Short inputs are greetings/acks, not questions
74
+ const MIN_USER_CHARS = 3; // "hi"/"ok"/"no" skip; "BTC"/"21044" do not
75
75
  const MIN_ANSWER_CHARS = 50; // Short answers are acks, not factual claims
76
+ // Factual-content patterns: digits paired with units, currency, dates, or
77
+ // percent/temperature/time signs. If the assistant emitted any of these in
78
+ // a >= MIN_ANSWER_CHARS reply, we check grounding regardless of how short
79
+ // the user's input was — a 5-char ZIP code "21044" can elicit a fabricated
80
+ // weather paragraph, and the original user-length gate let that through.
81
+ const FACTUAL_PATTERN = /(\$\s*\d|\d[\d,]*\s*(?:°[CF]?|%|km|mi|miles?|mph|kph|kg|lbs?|ft|in|cm|hours?|hrs?|minutes?|mins?|seconds?|secs?|GB|MB|KB|TB|USD|EUR|CNY|JPY|BTC|ETH|SOL)|\b(?:19|20)\d{2}-\d{1,2}-\d{1,2}\b|\b\d{1,2}:\d{2}\s*(?:AM|PM|am|pm)?\b|\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2}\b)/;
76
82
  /**
77
83
  * Decide whether this turn warrants a grounding check. Principles:
78
- * - Non-trivial user input (not a greeting, not a slash command)
84
+ * - Non-trivial user input (not a greeting, not a slash command), OR
85
+ * the assistant answer contains specific factual claims (numbers + units,
86
+ * currency, dates, times) regardless of input length
79
87
  * - Non-trivial assistant text output (not just a tool-result echo)
80
88
  *
81
89
  * Intentionally NOT gating on tool-type (read vs write) — the whole point
@@ -85,11 +93,17 @@ export function shouldCheckGrounding(userInput, assistantText) {
85
93
  if (process.env.FRANKLIN_NO_EVAL === '1')
86
94
  return false;
87
95
  const ui = userInput.trim();
88
- if (ui.length < MIN_USER_CHARS)
89
- return false;
90
96
  if (ui.startsWith('/'))
91
97
  return false;
92
- if (assistantText.trim().length < MIN_ANSWER_CHARS)
98
+ const at = assistantText.trim();
99
+ if (at.length < MIN_ANSWER_CHARS)
100
+ return false;
101
+ // If the answer looks factual (numbers + units, dates, prices), check
102
+ // even when the user's prompt was a single token. The 21044 zip-code
103
+ // case lived here.
104
+ if (FACTUAL_PATTERN.test(at))
105
+ return true;
106
+ if (ui.length < MIN_USER_CHARS)
93
107
  return false;
94
108
  return true;
95
109
  }
@@ -294,15 +308,37 @@ export function renderGroundingFollowup(result) {
294
308
  * history; we only need to name the gap + the tools to use.
295
309
  */
296
310
  export function buildGroundingRetryInstruction(result, originalUserQuestion) {
311
+ // Pull the named missing tools out of the evaluator's issue list so we
312
+ // can name them in the imperative. The evaluator outputs lines like
313
+ // Claim: "..." → missing tool: WebSearch
314
+ // grab the bit after "missing tool:" / "should have called:".
315
+ const namedTools = new Set();
316
+ for (const issue of result.issues) {
317
+ const m = issue.match(/(?:missing tool|should have called):\s*([A-Za-z][\w| ,/-]*)/i);
318
+ if (m) {
319
+ for (const tok of m[1].split(/[|,/]/)) {
320
+ const t = tok.trim().split(/\s+/)[0];
321
+ if (t && t !== '...' && t !== '(or')
322
+ namedTools.add(t);
323
+ }
324
+ }
325
+ }
326
+ const toolList = namedTools.size > 0
327
+ ? Array.from(namedTools).join(', ')
328
+ : '(see the missing-tool fields in the issues above)';
297
329
  const lines = [
298
- '[GROUNDING CHECK FAILED]',
299
- 'Your previous answer stated facts without calling the relevant tools. Specifically:',
330
+ '[GROUNDING CHECK FAILED — RETRY ROUND]',
331
+ 'Your previous answer stated facts without calling tools. Specifically:',
300
332
  ];
301
333
  for (const issue of result.issues) {
302
334
  lines.push(`- ${issue}`);
303
335
  }
304
336
  lines.push('');
305
- lines.push('Retry: call the missing tools first, then give a concise final answer based on the tool results. Only claim what the tool outputs actually say. If a tool fails, say so rather than falling back to memory.');
337
+ lines.push('## What you must do this round');
338
+ lines.push(`1. **Call these tools first**, before any prose: ${toolList}.`);
339
+ lines.push('2. **Do not write a single factual sentence until the tool results return.** No restatement of the prior answer, no hedging, no "based on general knowledge".');
340
+ lines.push('3. **Do NOT invent source names** (no fake URLs, no fabricated citation domains, no "per Trippy" / "per drivvin.com" — if you cite a source, it must come from a tool result you just ran).');
341
+ lines.push('4. After tools return, write a concise answer that ONLY restates what the tool outputs say. If a result is partial or a tool failed, say so explicitly — do not paper over with memory.');
306
342
  lines.push('');
307
343
  lines.push(`Original user question: ${originalUserQuestion.trim().slice(0, 500)}`);
308
344
  return lines.join('\n');
package/dist/agent/llm.js CHANGED
@@ -15,12 +15,12 @@ function parseTimeoutEnv(name) {
15
15
  function getModelRequestTimeoutMs() {
16
16
  return (parseTimeoutEnv('FRANKLIN_MODEL_REQUEST_TIMEOUT_MS') ??
17
17
  parseTimeoutEnv('FRANKLIN_MODEL_IDLE_TIMEOUT_MS') ??
18
- 8_000);
18
+ 45_000);
19
19
  }
20
20
  function getModelStreamIdleTimeoutMs() {
21
21
  return (parseTimeoutEnv('FRANKLIN_MODEL_STREAM_IDLE_TIMEOUT_MS') ??
22
22
  parseTimeoutEnv('FRANKLIN_MODEL_IDLE_TIMEOUT_MS') ??
23
- 25_000);
23
+ 90_000);
24
24
  }
25
25
  function linkAbortSignal(parent, child) {
26
26
  if (!parent)
@@ -1208,6 +1208,25 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
1208
1208
  };
1209
1209
  }
1210
1210
  }
1211
+ // Vision attachments: if a tool returned image bytes (e.g. Read on a
1212
+ // .png), wrap them into Anthropic-native tool_result.content so
1213
+ // vision-capable models can actually see the image. The gateway
1214
+ // preserves these blocks end-to-end via the tool_result side channel.
1215
+ if (result.images && result.images.length > 0) {
1216
+ const content = [{ type: 'text', text: result.output }];
1217
+ for (const img of result.images) {
1218
+ content.push({
1219
+ type: 'image',
1220
+ source: { type: 'base64', media_type: img.mediaType, data: img.base64 },
1221
+ });
1222
+ }
1223
+ return {
1224
+ type: 'tool_result',
1225
+ tool_use_id: inv.id,
1226
+ content,
1227
+ is_error: result.isError,
1228
+ };
1229
+ }
1211
1230
  return {
1212
1231
  type: 'tool_result',
1213
1232
  tool_use_id: inv.id,
@@ -29,6 +29,7 @@ const MODEL_MAX_OUTPUT = {
29
29
  'anthropic/claude-opus-4.6': 32_000,
30
30
  'anthropic/claude-sonnet-4.6': 64_000,
31
31
  'anthropic/claude-haiku-4.5-20251001': 16_384,
32
+ 'openai/gpt-5.5': 32_768,
32
33
  'openai/gpt-5.4': 32_768,
33
34
  'openai/gpt-5-mini': 16_384,
34
35
  'google/gemini-2.5-pro': 65_536,
@@ -151,7 +151,16 @@ export class PermissionManager {
151
151
  }
152
152
  // ─── Internal ──────────────────────────────────────────────────────────
153
153
  loadRules() {
154
- const configPath = path.join(BLOCKRUN_DIR, 'runcode-permissions.json');
154
+ const configPath = path.join(BLOCKRUN_DIR, 'franklin-permissions.json');
155
+ const legacyPath = path.join(BLOCKRUN_DIR, 'runcode-permissions.json');
156
+ // One-shot migration from the old name. If the user only has the legacy
157
+ // file, rename it so future writes/reads land on the franklin path.
158
+ try {
159
+ if (!fs.existsSync(configPath) && fs.existsSync(legacyPath)) {
160
+ fs.renameSync(legacyPath, configPath);
161
+ }
162
+ }
163
+ catch { /* best effort */ }
155
164
  try {
156
165
  if (fs.existsSync(configPath)) {
157
166
  const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
@@ -166,6 +166,10 @@ const MODEL_CONTEXT_WINDOWS = {
166
166
  'anthropic/claude-haiku-4.5': 200_000,
167
167
  'anthropic/claude-haiku-4.5-20251001': 200_000,
168
168
  // OpenAI
169
+ // gpt-5.5 advertises 1.05M context at the gateway, but Franklin keeps the
170
+ // conservative 128k baseline matching every other gpt-5.x line — bump in
171
+ // a separate change once a real >128k call has been verified end-to-end.
172
+ 'openai/gpt-5.5': 128_000,
169
173
  'openai/gpt-5.4': 128_000,
170
174
  'openai/gpt-5.4-pro': 128_000,
171
175
  'openai/gpt-5.3': 128_000,
@@ -18,10 +18,21 @@ export interface ThinkingSegment {
18
18
  thinking: string;
19
19
  signature?: string;
20
20
  }
21
+ export interface ImageSegment {
22
+ type: 'image';
23
+ source: {
24
+ type: 'base64';
25
+ media_type: string;
26
+ data: string;
27
+ } | {
28
+ type: 'url';
29
+ url: string;
30
+ };
31
+ }
21
32
  export interface CapabilityOutcome {
22
33
  type: 'tool_result';
23
34
  tool_use_id: string;
24
- content: string | ContentPart[];
35
+ content: string | Array<TextSegment | ImageSegment>;
25
36
  is_error?: boolean;
26
37
  }
27
38
  export type ContentPart = TextSegment | CapabilityInvocation | ThinkingSegment;
@@ -60,6 +71,16 @@ export interface CapabilityResult {
60
71
  };
61
72
  /** Full tool output for expandable display — separate from truncated preview. */
62
73
  fullOutput?: string;
74
+ /**
75
+ * Optional image attachments emitted by a tool (e.g. Read on a .png).
76
+ * The agent loop wraps these into an Anthropic-native tool_result.content
77
+ * array so vision-capable models can actually see the bytes instead of
78
+ * getting a "Binary file" stub.
79
+ */
80
+ images?: Array<{
81
+ mediaType: string;
82
+ base64: string;
83
+ }>;
63
84
  }
64
85
  export interface ExecutionScope {
65
86
  workingDir: string;
@@ -30,7 +30,7 @@ export async function balanceCommand() {
30
30
  catch (err) {
31
31
  const msg = err instanceof Error ? err.message : '';
32
32
  if (msg.includes('ENOENT') || msg.includes('wallet') || msg.includes('key')) {
33
- console.log(chalk.red('No wallet found. Run `runcode setup` first.'));
33
+ console.log(chalk.red('No wallet found. Run `franklin setup` first.'));
34
34
  }
35
35
  else {
36
36
  console.log(chalk.red(`Error checking balance: ${msg || 'unknown error'}`));
@@ -1,4 +1,4 @@
1
- import { spawn, execSync } from 'node:child_process';
1
+ import { spawn, execFileSync, execSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import chalk from 'chalk';
@@ -42,6 +42,17 @@ function isRunning(pid) {
42
42
  return true;
43
43
  }
44
44
  }
45
+ function findDaemonBinary() {
46
+ for (const name of ['franklin', 'runcode']) {
47
+ try {
48
+ return execFileSync('which', [name], { encoding: 'utf-8' }).trim();
49
+ }
50
+ catch {
51
+ // Try the legacy alias next.
52
+ }
53
+ }
54
+ return null;
55
+ }
45
56
  export async function daemonCommand(action, options) {
46
57
  const port = parseInt(options.port || String(DEFAULT_PROXY_PORT));
47
58
  if (isNaN(port) || port < 1 || port > 65535) {
@@ -56,24 +67,20 @@ export async function daemonCommand(action, options) {
56
67
  console.log(chalk.dim(` Proxy: http://localhost:${port}/api`));
57
68
  return;
58
69
  }
59
- // Find runcode binary
60
- let runcodeBin;
61
- try {
62
- runcodeBin = execSync('which runcode', { encoding: 'utf-8' }).trim();
63
- }
64
- catch {
65
- console.log(chalk.red('runcode binary not found in PATH.'));
70
+ const daemonBin = findDaemonBinary();
71
+ if (!daemonBin) {
72
+ console.log(chalk.red('franklin binary not found in PATH.'));
66
73
  return;
67
74
  }
68
75
  fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
69
- const child = spawn(runcodeBin, ['proxy', '--port', String(port)], {
76
+ const child = spawn(daemonBin, ['proxy', '--port', String(port)], {
70
77
  detached: true,
71
78
  // stdout → /dev/null (banner + startup messages), stderr → log file (debug/errors only)
72
79
  stdio: ['ignore', 'ignore', fs.openSync(LOG_FILE, 'a')],
73
80
  });
74
81
  child.unref();
75
82
  fs.writeFileSync(PID_FILE, String(child.pid));
76
- console.log(chalk.green(`✓ runcode daemon started (PID ${child.pid})`));
83
+ console.log(chalk.green(`✓ franklin daemon started (PID ${child.pid})`));
77
84
  console.log(chalk.dim(` Proxy: http://localhost:${port}/api`));
78
85
  console.log(chalk.dim(` Logs: ${LOG_FILE}`));
79
86
  break;
@@ -81,7 +88,7 @@ export async function daemonCommand(action, options) {
81
88
  case 'stop': {
82
89
  const pid = readPid();
83
90
  if (!pid) {
84
- console.log(chalk.yellow('No runcode daemon found.'));
91
+ console.log(chalk.yellow('No franklin daemon found.'));
85
92
  return;
86
93
  }
87
94
  if (!isRunning(pid)) {
@@ -104,7 +111,7 @@ export async function daemonCommand(action, options) {
104
111
  fs.unlinkSync(PID_FILE);
105
112
  }
106
113
  catch { /* already gone */ }
107
- console.log(chalk.green(`✓ runcode daemon stopped (PID ${pid})`));
114
+ console.log(chalk.green(`✓ franklin daemon stopped (PID ${pid})`));
108
115
  }
109
116
  catch (e) {
110
117
  console.log(chalk.red(`Failed to stop daemon: ${e.message}`));
@@ -114,23 +121,23 @@ export async function daemonCommand(action, options) {
114
121
  case 'status': {
115
122
  const pid = readPid();
116
123
  if (!pid) {
117
- console.log(chalk.dim('runcode daemon: not running'));
124
+ console.log(chalk.dim('franklin daemon: not running'));
118
125
  return;
119
126
  }
120
127
  if (isRunning(pid)) {
121
- console.log(chalk.green(`✓ runcode daemon running`));
128
+ console.log(chalk.green(`✓ franklin daemon running`));
122
129
  console.log(` PID: ${chalk.bold(pid)}`);
123
130
  console.log(` Proxy: ${chalk.cyan(`http://localhost:${port}/api`)}`);
124
131
  console.log(chalk.dim(` Logs: ${LOG_FILE}`));
125
132
  }
126
133
  else {
127
134
  fs.unlinkSync(PID_FILE);
128
- console.log(chalk.yellow('runcode daemon: not running (stale PID cleaned up)'));
135
+ console.log(chalk.yellow('franklin daemon: not running (stale PID cleaned up)'));
129
136
  }
130
137
  break;
131
138
  }
132
139
  default:
133
140
  console.log(chalk.red(`Unknown daemon action: ${action}`));
134
- console.log('Usage: runcode daemon <start|stop|status>');
141
+ console.log('Usage: franklin daemon <start|stop|status>');
135
142
  }
136
143
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Generic plugin command dispatcher.
3
3
  *
4
- * `runcode <plugin-id> <action>` works for ANY plugin that registers a workflow.
4
+ * `franklin <plugin-id> <action>` works for ANY plugin that registers a workflow.
5
5
  * Core stays plugin-agnostic — adding a new plugin requires zero changes here.
6
6
  */
7
7
  export interface PluginCommandOptions {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Generic plugin command dispatcher.
3
3
  *
4
- * `runcode <plugin-id> <action>` works for ANY plugin that registers a workflow.
4
+ * `franklin <plugin-id> <action>` works for ANY plugin that registers a workflow.
5
5
  * Core stays plugin-agnostic — adding a new plugin requires zero changes here.
6
6
  */
7
7
  import chalk from 'chalk';
@@ -49,7 +49,7 @@ export async function pluginCommand(pluginId, action, options) {
49
49
  // No action and already configured: show stats + dry-run hint
50
50
  const stats = getStats(workflow.id);
51
51
  console.log(formatWorkflowStats(workflow, stats));
52
- console.log(chalk.dim(`Run "runcode ${pluginId} run --dry" to preview.\n`));
52
+ console.log(chalk.dim(`Run "franklin ${pluginId} run --dry" to preview.\n`));
53
53
  }
54
54
  break;
55
55
  }
@@ -73,7 +73,7 @@ export async function pluginCommand(pluginId, action, options) {
73
73
  case 'leads': {
74
74
  const leads = getByAction(workflow.id, 'lead');
75
75
  if (leads.length === 0) {
76
- console.log(chalk.dim(`\nNo leads found yet. Run "runcode ${pluginId} run" first.\n`));
76
+ console.log(chalk.dim(`\nNo leads found yet. Run "franklin ${pluginId} run" first.\n`));
77
77
  break;
78
78
  }
79
79
  console.log(chalk.bold(`\n LEADS (${leads.length})\n`));
@@ -94,12 +94,12 @@ export async function pluginCommand(pluginId, action, options) {
94
94
  console.log(chalk.red(`Unknown action: ${action}`));
95
95
  console.log(chalk.dim(`
96
96
  Usage:
97
- runcode ${pluginId} # show stats / first-run setup
98
- runcode ${pluginId} init # interactive setup
99
- runcode ${pluginId} run # execute workflow
100
- runcode ${pluginId} run --dry # dry run (no side effects)
101
- runcode ${pluginId} stats # show statistics
102
- runcode ${pluginId} leads # show tracked leads (if applicable)
97
+ franklin ${pluginId} # show stats / first-run setup
98
+ franklin ${pluginId} init # interactive setup
99
+ franklin ${pluginId} run # execute workflow
100
+ franklin ${pluginId} run --dry # dry run (no side effects)
101
+ franklin ${pluginId} stats # show statistics
102
+ franklin ${pluginId} leads # show tracked leads (if applicable)
103
103
  `));
104
104
  }
105
105
  }
@@ -166,7 +166,7 @@ async function runOnboarding(workflow, client) {
166
166
  const config = await workflow.buildConfigFromAnswers(answers, llm);
167
167
  console.log(chalk.green(' ✓ Configuration saved!\n'));
168
168
  console.log(chalk.dim(` Config: ~/.blockrun/workflows/${workflow.id}.config.json\n`));
169
- console.log(chalk.dim(` Run "runcode ${workflow.id} run --dry" to preview.\n`));
169
+ console.log(chalk.dim(` Run "franklin ${workflow.id} run --dry" to preview.\n`));
170
170
  return config;
171
171
  }
172
172
  catch (err) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * runcode stats command
2
+ * franklin stats command
3
3
  * Display usage statistics and cost savings
4
4
  */
5
5
  interface StatsOptions {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * runcode stats command
2
+ * franklin stats command
3
3
  * Display usage statistics and cost savings
4
4
  */
5
5
  import chalk from 'chalk';
@@ -90,5 +90,5 @@ export function statsCommand(options) {
90
90
  }
91
91
  }
92
92
  console.log('\n' + '─'.repeat(55));
93
- console.log(chalk.gray(' Run `runcode stats --clear` to reset statistics\n'));
93
+ console.log(chalk.gray(' Run `franklin stats --clear` to reset statistics\n'));
94
94
  }
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ program
38
38
  program
39
39
  .command('start')
40
40
  .description('Start the franklin agent')
41
- .option('-m, --model <model>', 'Model to use (e.g. openai/gpt-5.4, anthropic/claude-sonnet-4.6). Default from config or claude-sonnet-4.6')
41
+ .option('-m, --model <model>', 'Model to use (e.g. openai/gpt-5.5, anthropic/claude-sonnet-4.6). Default from config or claude-sonnet-4.6')
42
42
  .option('--debug', 'Enable debug logging')
43
43
  .option('--trust', 'Trust mode — skip permission prompts for all tools')
44
44
  .option('-r, --resume [sessionId]', 'Resume a session by ID (or show picker if omitted)')
@@ -252,7 +252,7 @@ function parseStartFlags(argv, startIdx = 0) {
252
252
  }
253
253
  return opts;
254
254
  }
255
- // Handle chain shortcuts: `runcode solana` or `runcode base`
255
+ // Handle chain shortcuts: `franklin solana` or `franklin base`
256
256
  if (firstArg === 'solana' || firstArg === 'base') {
257
257
  if (hasAnyFlag(args, HELP_FLAGS)) {
258
258
  program.parse(['node', 'franklin', 'start', '--help']);
@@ -6,8 +6,8 @@
6
6
  import http from 'node:http';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
- import { BLOCKRUN_DIR, loadChain, saveChain } from '../config.js';
10
- import { getStatsSummary } from '../stats/tracker.js';
9
+ import { loadChain, saveChain } from '../config.js';
10
+ import { getStatsSummary, getStatsFilePath } from '../stats/tracker.js';
11
11
  import { generateInsights } from '../stats/insights.js';
12
12
  import { listSessions, loadSessionHistory } from '../session/storage.js';
13
13
  import { searchSessions } from '../session/search.js';
@@ -413,10 +413,11 @@ export function createPanelServer(port) {
413
413
  console.error('[panel] client error:', err.message);
414
414
  }
415
415
  });
416
- // Watch stats file for changes → push to SSE clients
417
- const statsFile = fs.existsSync(path.join(BLOCKRUN_DIR, 'franklin-stats.json'))
418
- ? path.join(BLOCKRUN_DIR, 'franklin-stats.json')
419
- : path.join(BLOCKRUN_DIR, 'runcode-stats.json');
416
+ // Watch stats file for changes → push to SSE clients.
417
+ // getStatsFilePath() also handles the runcode-stats.json franklin-stats.json
418
+ // migration on first call, so users coming from the old binary keep their
419
+ // history without an extra cleanup step.
420
+ const statsFile = getStatsFilePath();
420
421
  if (fs.existsSync(statsFile)) {
421
422
  fs.watchFile(statsFile, { interval: 2000 }, () => {
422
423
  try {
@@ -1,7 +1,7 @@
1
1
  /**
2
- * RunCode Plugin SDK — public surface for plugins.
2
+ * Franklin Plugin SDK — public surface for plugins.
3
3
  *
4
- * Plugins import ONLY from '@blockrun/runcode/plugin-sdk' (or this barrel).
4
+ * Plugins import ONLY from '@blockrun/franklin/plugin-sdk' (or this barrel).
5
5
  * They MUST NOT import from src/** of core or other plugins.
6
6
  *
7
7
  * Core stays plugin-agnostic: adding a plugin should never require editing core.
@@ -1,7 +1,7 @@
1
1
  /**
2
- * RunCode Plugin SDK — public surface for plugins.
2
+ * Franklin Plugin SDK — public surface for plugins.
3
3
  *
4
- * Plugins import ONLY from '@blockrun/runcode/plugin-sdk' (or this barrel).
4
+ * Plugins import ONLY from '@blockrun/franklin/plugin-sdk' (or this barrel).
5
5
  * They MUST NOT import from src/** of core or other plugins.
6
6
  *
7
7
  * Core stays plugin-agnostic: adding a plugin should never require editing core.
@@ -26,8 +26,8 @@ export interface PluginManifest {
26
26
  homepage?: string;
27
27
  /** License */
28
28
  license?: string;
29
- /** Required runcode version (semver range) */
30
- runcodeVersion?: string;
29
+ /** Required Franklin version (semver range) */
30
+ franklinVersion?: string;
31
31
  }
32
32
  export interface PluginProvides {
33
33
  /** This plugin contributes one or more workflows (e.g. "social", "trading") */
@@ -54,8 +54,8 @@ export interface Plugin {
54
54
  }
55
55
  /** Context passed to plugin lifecycle hooks */
56
56
  export interface PluginContext {
57
- /** RunCode version */
58
- runcodeVersion: string;
57
+ /** Franklin version */
58
+ franklinVersion: string;
59
59
  /** Plugin's own data directory (~/.blockrun/plugins/<id>/) */
60
60
  dataDir: string;
61
61
  /** Path to plugin's installation directory */
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Core stays plugin-agnostic: it knows about the *interface*, not specific plugins.
5
5
  * Plugins are discovered from:
6
- * 1. Bundled: <runcode>/plugins-bundled/* (ships with runcode)
7
- * 2. User: ~/.blockrun/plugins/* (installed via `runcode plugin install`)
8
- * 3. Local dev: $RUNCODE_PLUGINS_DIR/* (env var for development)
6
+ * 1. Bundled: <franklin>/plugins-bundled/* (ships with Franklin)
7
+ * 2. User: ~/.blockrun/plugins/*
8
+ * 3. Local dev: $FRANKLIN_PLUGINS_DIR/* (or legacy $RUNCODE_PLUGINS_DIR/*)
9
9
  */
10
10
  import type { Plugin, PluginManifest } from '../plugin-sdk/plugin.js';
11
11
  export declare function getBundledPluginsDir(): string;
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Core stays plugin-agnostic: it knows about the *interface*, not specific plugins.
5
5
  * Plugins are discovered from:
6
- * 1. Bundled: <runcode>/plugins-bundled/* (ships with runcode)
7
- * 2. User: ~/.blockrun/plugins/* (installed via `runcode plugin install`)
8
- * 3. Local dev: $RUNCODE_PLUGINS_DIR/* (env var for development)
6
+ * 1. Bundled: <franklin>/plugins-bundled/* (ships with Franklin)
7
+ * 2. User: ~/.blockrun/plugins/*
8
+ * 3. Local dev: $FRANKLIN_PLUGINS_DIR/* (or legacy $RUNCODE_PLUGINS_DIR/*)
9
9
  */
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
@@ -22,7 +22,7 @@ export function getUserPluginsDir() {
22
22
  return path.join(os.homedir(), '.blockrun', 'plugins');
23
23
  }
24
24
  function getDevPluginsDir() {
25
- return process.env.RUNCODE_PLUGINS_DIR || null;
25
+ return process.env.FRANKLIN_PLUGINS_DIR || process.env.RUNCODE_PLUGINS_DIR || null;
26
26
  }
27
27
  const loaded = new Map();
28
28
  // ─── Discovery ────────────────────────────────────────────────────────────
@@ -119,7 +119,7 @@ export async function loadAllPlugins() {
119
119
  if (plugin.onLoad) {
120
120
  try {
121
121
  await plugin.onLoad({
122
- runcodeVersion: getRuncodeVersion(),
122
+ franklinVersion: getFranklinVersion(),
123
123
  dataDir: path.join(os.homedir(), '.blockrun', 'plugins', manifest.id),
124
124
  pluginDir: dir,
125
125
  log: (msg) => process.stderr.write(`[${manifest.id}] ${msg}\n`),
@@ -149,7 +149,7 @@ export function listChannelPlugins() {
149
149
  return listPlugins().filter(p => p.plugin.channels && Object.keys(p.plugin.channels).length > 0);
150
150
  }
151
151
  // ─── Helpers ──────────────────────────────────────────────────────────────
152
- function getRuncodeVersion() {
152
+ function getFranklinVersion() {
153
153
  try {
154
154
  const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
155
155
  return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version || '0.0.0';
package/dist/pricing.js CHANGED
@@ -46,6 +46,7 @@ export const MODEL_PRICING = {
46
46
  'openai/o3-mini': { input: 1.1, output: 4.4 },
47
47
  'openai/o4-mini': { input: 1.1, output: 4.4 },
48
48
  'openai/o1': { input: 15.0, output: 60.0 },
49
+ 'openai/gpt-5.5': { input: 5.0, output: 30.0 },
49
50
  'openai/gpt-5.2-pro': { input: 21.0, output: 168.0 },
50
51
  'openai/gpt-5.4-pro': { input: 30.0, output: 180.0 },
51
52
  // Google