@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.
- package/README.md +1 -1
- package/dist/agent/commands.js +1 -1
- package/dist/agent/compact.js +1 -1
- package/dist/agent/evaluator.d.ts +3 -1
- package/dist/agent/evaluator.js +44 -8
- package/dist/agent/llm.js +2 -2
- package/dist/agent/loop.js +19 -0
- package/dist/agent/optimize.js +1 -0
- package/dist/agent/permissions.js +10 -1
- package/dist/agent/tokens.js +4 -0
- package/dist/agent/types.d.ts +22 -1
- package/dist/commands/balance.js +1 -1
- package/dist/commands/daemon.js +23 -16
- package/dist/commands/plugin.d.ts +1 -1
- package/dist/commands/plugin.js +10 -10
- package/dist/commands/stats.d.ts +1 -1
- package/dist/commands/stats.js +2 -2
- package/dist/index.js +2 -2
- package/dist/panel/server.js +7 -6
- package/dist/plugin-sdk/index.d.ts +2 -2
- package/dist/plugin-sdk/index.js +2 -2
- package/dist/plugin-sdk/plugin.d.ts +4 -4
- package/dist/plugins/registry.d.ts +3 -3
- package/dist/plugins/registry.js +6 -6
- package/dist/pricing.js +1 -0
- package/dist/proxy/server.js +148 -26
- package/dist/router/index.js +3 -3
- package/dist/session/storage.js +2 -2
- package/dist/tools/imagegen.d.ts +14 -0
- package/dist/tools/imagegen.js +154 -22
- package/dist/tools/read.js +29 -2
- package/dist/tools/videogen.d.ts +14 -3
- package/dist/tools/videogen.js +161 -28
- package/dist/tools/webhook.js +2 -1
- package/dist/trading/providers/coingecko/client.js +2 -1
- package/dist/ui/app.js +12 -12
- package/dist/ui/model-picker.js +7 -4
- package/dist/wallet/index.d.ts +17 -0
- package/dist/wallet/index.js +22 -0
- 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
|
|
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/
|
package/dist/agent/commands.js
CHANGED
|
@@ -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:
|
|
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');
|
package/dist/agent/compact.js
CHANGED
|
@@ -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
|
package/dist/agent/evaluator.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
90_000);
|
|
24
24
|
}
|
|
25
25
|
function linkAbortSignal(parent, child) {
|
|
26
26
|
if (!parent)
|
package/dist/agent/loop.js
CHANGED
|
@@ -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,
|
package/dist/agent/optimize.js
CHANGED
|
@@ -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, '
|
|
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'));
|
package/dist/agent/tokens.js
CHANGED
|
@@ -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,
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -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 |
|
|
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;
|
package/dist/commands/balance.js
CHANGED
|
@@ -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 `
|
|
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'}`));
|
package/dist/commands/daemon.js
CHANGED
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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(`✓
|
|
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
|
|
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(`✓
|
|
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('
|
|
124
|
+
console.log(chalk.dim('franklin daemon: not running'));
|
|
118
125
|
return;
|
|
119
126
|
}
|
|
120
127
|
if (isRunning(pid)) {
|
|
121
|
-
console.log(chalk.green(`✓
|
|
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('
|
|
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:
|
|
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
|
-
* `
|
|
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 {
|
package/dist/commands/plugin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generic plugin command dispatcher.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
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 "
|
|
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 "
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 "
|
|
169
|
+
console.log(chalk.dim(` Run "franklin ${workflow.id} run --dry" to preview.\n`));
|
|
170
170
|
return config;
|
|
171
171
|
}
|
|
172
172
|
catch (err) {
|
package/dist/commands/stats.d.ts
CHANGED
package/dist/commands/stats.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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 `
|
|
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.
|
|
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: `
|
|
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']);
|
package/dist/panel/server.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
*
|
|
2
|
+
* Franklin Plugin SDK — public surface for plugins.
|
|
3
3
|
*
|
|
4
|
-
* Plugins import ONLY from '@blockrun/
|
|
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.
|
package/dist/plugin-sdk/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Franklin Plugin SDK — public surface for plugins.
|
|
3
3
|
*
|
|
4
|
-
* Plugins import ONLY from '@blockrun/
|
|
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
|
|
30
|
-
|
|
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
|
-
/**
|
|
58
|
-
|
|
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: <
|
|
7
|
-
* 2. User: ~/.blockrun/plugins/*
|
|
8
|
-
* 3. Local dev: $
|
|
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;
|
package/dist/plugins/registry.js
CHANGED
|
@@ -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: <
|
|
7
|
-
* 2. User: ~/.blockrun/plugins/*
|
|
8
|
-
* 3. Local dev: $
|
|
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
|
-
|
|
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
|
|
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
|