@blockrun/franklin 3.8.34 → 3.8.36
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 +4 -2
- package/dist/agent/commands.js +1 -1
- package/dist/agent/compact.js +1 -1
- package/dist/agent/context.js +1 -1
- package/dist/agent/loop.js +19 -0
- package/dist/agent/media-router.d.ts +33 -0
- package/dist/agent/media-router.js +87 -9
- 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 +5 -3
- 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 +175 -24
- package/dist/tools/read.js +29 -2
- package/dist/tools/videogen.d.ts +14 -3
- package/dist/tools/videogen.js +181 -31
- 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/
|
|
@@ -480,10 +480,12 @@ cd franklin
|
|
|
480
480
|
npm install
|
|
481
481
|
npm run build
|
|
482
482
|
npm test # deterministic local tests — no API calls
|
|
483
|
-
npm run test:e2e # live e2e tests —
|
|
483
|
+
npm run test:e2e # live e2e tests — free smoke works unfunded; paid tools need network + funded wallet
|
|
484
484
|
node dist/index.js --help
|
|
485
485
|
```
|
|
486
486
|
|
|
487
|
+
For the recommended live validation order and failure triage, see [docs/live-e2e-checklist.md](docs/live-e2e-checklist.md).
|
|
488
|
+
|
|
487
489
|
**Contributing:** open an issue first to discuss meaningful changes. PRs welcome on bugs, docs, new models in pricing, and new tools.
|
|
488
490
|
|
|
489
491
|
---
|
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')) {
|
package/dist/agent/context.js
CHANGED
|
@@ -189,7 +189,7 @@ Your training data is frozen in the past. Live-world questions MUST be answered
|
|
|
189
189
|
|
|
190
190
|
If you find yourself about to emit one of these, stop and call the tool instead. If you don't know which ticker the user means, call ExaSearch or AskUser — never deflect.
|
|
191
191
|
|
|
192
|
-
**Media generation (ImageGen / VideoGen).** Pass just the user's descriptive prompt and the output path — do NOT pass \`model\`. The harness picks the right model for the requested style + budget and surfaces a cost proposal through AskUser before spending. Only pass \`model\` explicitly if the user named one specifically.`;
|
|
192
|
+
**Media generation (ImageGen / VideoGen).** Pass just the user's descriptive prompt and the output path — do NOT pass \`model\`. The harness picks the right model for the requested style + budget, refines loose prompts using a 5-slot template (scene / subject / details / use case / constraints), and surfaces both the refinement and a cost proposal through AskUser before spending. If the user wants their prompt left exactly as written, prefix it with \`///\` to skip refinement. Only pass \`model\` explicitly if the user named one specifically.`;
|
|
193
193
|
}
|
|
194
194
|
function getTokenEfficiencySection() {
|
|
195
195
|
return `# Token Efficiency
|
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,
|
|
@@ -33,9 +33,35 @@ export interface MediaProposal {
|
|
|
33
33
|
style: MediaStyle;
|
|
34
34
|
priority: MediaPriority;
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* A fuller rewrite of the user's prompt, following the 5-slot template
|
|
38
|
+
* (scene/subject/details/use-case/constraints). Null when the classifier
|
|
39
|
+
* judged the input already well-specified, or when the env opt-out is set,
|
|
40
|
+
* or when the rewrite was identical to the raw input. When non-null, the
|
|
41
|
+
* AskUser layout surfaces it as "Refined:" with a "Use ORIGINAL" option.
|
|
42
|
+
*/
|
|
43
|
+
refinedPrompt: string | null;
|
|
44
|
+
refinementSummary: string;
|
|
36
45
|
totalCostUsd: number;
|
|
37
46
|
}
|
|
38
47
|
export declare function clearMediaRouterCache(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Normalize a refined prompt: trim, cap length, reject obvious junk.
|
|
50
|
+
* Returns null when the value should be treated as absent (missing,
|
|
51
|
+
* non-string, empty after trim).
|
|
52
|
+
*
|
|
53
|
+
* Exported for testability — invariants matter more here than elsewhere
|
|
54
|
+
* because the output is user-visible and paid for.
|
|
55
|
+
*/
|
|
56
|
+
export declare function validateRefined(raw: unknown, maxChars: number): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Whitespace-insensitive, case-insensitive identity check — if the
|
|
59
|
+
* classifier's "refinement" is just the input with different spacing,
|
|
60
|
+
* don't bother the user with a "Refined:" block.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isEffectivelyIdentical(a: string, b: string): boolean;
|
|
63
|
+
export declare const REFINED_PROMPT_MAX_CHARS = 500;
|
|
64
|
+
export declare const REFINEMENT_SUMMARY_LIMIT = 80;
|
|
39
65
|
export interface AnalyzeMediaOpts {
|
|
40
66
|
kind: MediaKind;
|
|
41
67
|
prompt: string;
|
|
@@ -43,6 +69,13 @@ export interface AnalyzeMediaOpts {
|
|
|
43
69
|
quantity?: number;
|
|
44
70
|
durationSeconds?: number;
|
|
45
71
|
signal?: AbortSignal;
|
|
72
|
+
/**
|
|
73
|
+
* One-shot opt-out — caller stripped a `///` prefix from the user's input
|
|
74
|
+
* and wants the proposal rendered without a Refined block or Use-original
|
|
75
|
+
* option. The classifier still runs (for model selection), but the
|
|
76
|
+
* refinement is discarded at parse time.
|
|
77
|
+
*/
|
|
78
|
+
skipRefine?: boolean;
|
|
46
79
|
}
|
|
47
80
|
/**
|
|
48
81
|
* Pick the best model + alternatives for this media request. Returns null
|
|
@@ -16,7 +16,9 @@ import { getModelsByCategory, estimateCostUsd, defaultDurationSeconds, maxDurati
|
|
|
16
16
|
// ─── Classifier ─────────────────────────────────────────────────────────
|
|
17
17
|
const CLASSIFIER_MODEL = process.env.FRANKLIN_MEDIA_ROUTER_MODEL || 'nvidia/llama-4-maverick';
|
|
18
18
|
const TIMEOUT_MS = 3_500; // slightly more lenient than the turn-analyzer — we're asking for JSON with reasoning
|
|
19
|
-
const MAX_TOKENS = 256
|
|
19
|
+
const MAX_TOKENS = 384; // bumped from 256 to leave room for refined_prompt (≤500 chars) + refinement_summary (≤80)
|
|
20
|
+
const REFINED_MAX_CHARS = 500;
|
|
21
|
+
const REFINEMENT_SUMMARY_MAX_CHARS = 80;
|
|
20
22
|
function buildSystemPrompt(kind, catalog) {
|
|
21
23
|
const catalogLines = catalog.map(m => {
|
|
22
24
|
const p = m.pricing;
|
|
@@ -25,7 +27,7 @@ function buildSystemPrompt(kind, catalog) {
|
|
|
25
27
|
: `$${(p.per_second ?? 0).toFixed(2)}/s (default ${p.default_duration_seconds ?? 8}s, max ${p.max_duration_seconds ?? 8}s)`;
|
|
26
28
|
return ` - ${m.id} · ${price} · ${m.description || m.name}`;
|
|
27
29
|
}).join('\n');
|
|
28
|
-
return `You pick the best ${kind} model for a user's Franklin request. Output ONE LINE of compact JSON. No markdown, no code fences, no explanation.
|
|
30
|
+
return `You pick the best ${kind} model for a user's Franklin request AND refine the user's prompt. Output ONE LINE of compact JSON. No markdown, no code fences, no explanation.
|
|
29
31
|
|
|
30
32
|
## Catalog (${catalog.length} available ${kind} models)
|
|
31
33
|
${catalogLines}
|
|
@@ -34,6 +36,8 @@ ${catalogLines}
|
|
|
34
36
|
|
|
35
37
|
{"style":"photoreal|illustration|anime|logo|concept|other",
|
|
36
38
|
"priority":"cost|quality|balanced",
|
|
39
|
+
"refined_prompt":"<rewritten prompt in the user's language, <=${REFINED_MAX_CHARS} chars, or null if already well-specified>",
|
|
40
|
+
"refinement_summary":"<one short sentence, <=${REFINEMENT_SUMMARY_MAX_CHARS} chars, user-visible>",
|
|
37
41
|
"recommended":{"model":"<id from catalog>","rationale":"<one sentence, <=140 chars>"},
|
|
38
42
|
"cheaper":{"model":"<id from catalog | null>","rationale":"<one sentence>"},
|
|
39
43
|
"premium":{"model":"<id from catalog | null>","rationale":"<one sentence>"}}
|
|
@@ -45,16 +49,33 @@ Rules:
|
|
|
45
49
|
- Match style → model: anime/illustration prefers CogView, photoreal prefers Nano Banana Pro / Grok Imagine Pro, budget-conscious picks cheapest-acceptable.
|
|
46
50
|
- One sentence rationale, user-visible.
|
|
47
51
|
|
|
52
|
+
## Refinement (emit refined_prompt + refinement_summary)
|
|
53
|
+
|
|
54
|
+
If the user's prompt is missing ≥3 of these 5 slots, rewrite to fill them. If it already has ≥3 covered, set refined_prompt to null and refinement_summary to "Already well-specified".
|
|
55
|
+
|
|
56
|
+
1. Scene — location, time of day, environment, background
|
|
57
|
+
2. Subject — primary focus (who / what), preserved EXACTLY from the user's input (no substitution)
|
|
58
|
+
3. Details — materials, textures, lighting, camera/lens feel, composition, mood (concrete visual facts, not praise)
|
|
59
|
+
4. Use Case — editorial photo, product mockup, UI screen, logo, storyboard frame, social-media cover, etc.
|
|
60
|
+
5. Constraints — aspect ratio, what must not drift (no watermark, preserve face, no text), hard asks
|
|
61
|
+
|
|
62
|
+
Anti-slop rules:
|
|
63
|
+
- Concrete visual facts ("overcast daylight", "brushed aluminum") beat vague praise ("stunning", "cinematic masterpiece").
|
|
64
|
+
- Wrap literal text that must appear in the image in double quotes. Spell difficult words letter-by-letter.
|
|
65
|
+
- One revision per turn — do not combine conflicting asks.
|
|
66
|
+
- Natural language, not keyword-tag format.
|
|
67
|
+
- refined_prompt stays in the same language as the user input. Chinese in → Chinese out.
|
|
68
|
+
|
|
48
69
|
Examples:
|
|
49
70
|
|
|
50
71
|
Input: "a photo of a cat on Mars, photoreal"
|
|
51
|
-
Output: {"style":"photoreal","priority":"balanced","recommended":{"model":"google/nano-banana-pro","rationale":"Photoreal scenes — Nano Banana Pro has strong realism at moderate cost."},"cheaper":{"model":"google/nano-banana","rationale":"Same family, lower cost, slightly less detail."},"premium":{"model":"openai/gpt-image-2","rationale":"Best photoreal fidelity when budget allows."}}
|
|
72
|
+
Output: {"style":"photoreal","priority":"balanced","refined_prompt":"Eye-level photograph of a cat standing on the rust-colored Martian surface, late-afternoon low sun casting long shadows, distant canyon rim in the background, 50mm feel, shallow depth of field, editorial photo use, no watermark.","refinement_summary":"Added scene, lighting, lens, use case, constraint.","recommended":{"model":"google/nano-banana-pro","rationale":"Photoreal scenes — Nano Banana Pro has strong realism at moderate cost."},"cheaper":{"model":"google/nano-banana","rationale":"Same family, lower cost, slightly less detail."},"premium":{"model":"openai/gpt-image-2","rationale":"Best photoreal fidelity when budget allows."}}
|
|
52
73
|
|
|
53
74
|
Input: "赛博朋克风格的动漫角色"
|
|
54
|
-
Output: {"style":"anime","priority":"balanced","recommended":{"model":"zai/cogview-4","rationale":"CogView-4 specializes in stylized/anime imagery."},"cheaper":{"model":"google/nano-banana","rationale":"Cheaper but less stylized."},"premium":{"model":"xai/grok-imagine-image-pro","rationale":"Premium detail for complex scenes."}}
|
|
75
|
+
Output: {"style":"anime","priority":"balanced","refined_prompt":"赛博朋克风格的动漫角色,站在霓虹灯映照的雨夜街道上,身穿合成纤维夹克与金属反光饰件,头顶全息广告牌漂浮,低角度视角,强烈青粉对比,海报用,居中构图。","refinement_summary":"补全了场景、光线、材质、用途、构图。","recommended":{"model":"zai/cogview-4","rationale":"CogView-4 specializes in stylized/anime imagery."},"cheaper":{"model":"google/nano-banana","rationale":"Cheaper but less stylized."},"premium":{"model":"xai/grok-imagine-image-pro","rationale":"Premium detail for complex scenes."}}
|
|
55
76
|
|
|
56
77
|
Input: "a 10-second cinematic drone shot over Tokyo at night"
|
|
57
|
-
Output: {"style":"concept","priority":"quality","recommended":{"model":"bytedance/seedance-2.0","rationale":"Seedance 2.0 delivers the best cinematic quality."},"cheaper":{"model":"bytedance/seedance-2.0-fast","rationale":"Faster + cheaper, minor quality trade-off."},"premium":{"model":null,"rationale":"2.0 is already the top tier."}}
|
|
78
|
+
Output: {"style":"concept","priority":"quality","refined_prompt":null,"refinement_summary":"Already well-specified.","recommended":{"model":"bytedance/seedance-2.0","rationale":"Seedance 2.0 delivers the best cinematic quality."},"cheaper":{"model":"bytedance/seedance-2.0-fast","rationale":"Faster + cheaper, minor quality trade-off."},"premium":{"model":null,"rationale":"2.0 is already the top tier."}}
|
|
58
79
|
|
|
59
80
|
Output JSON only, single line.`;
|
|
60
81
|
}
|
|
@@ -82,6 +103,33 @@ function validateChoice(raw, catalog) {
|
|
|
82
103
|
const rationale = typeof raw.rationale === 'string' ? raw.rationale.slice(0, 240) : '';
|
|
83
104
|
return { model, rationale };
|
|
84
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Normalize a refined prompt: trim, cap length, reject obvious junk.
|
|
108
|
+
* Returns null when the value should be treated as absent (missing,
|
|
109
|
+
* non-string, empty after trim).
|
|
110
|
+
*
|
|
111
|
+
* Exported for testability — invariants matter more here than elsewhere
|
|
112
|
+
* because the output is user-visible and paid for.
|
|
113
|
+
*/
|
|
114
|
+
export function validateRefined(raw, maxChars) {
|
|
115
|
+
if (typeof raw !== 'string')
|
|
116
|
+
return null;
|
|
117
|
+
const trimmed = raw.trim();
|
|
118
|
+
if (trimmed.length === 0)
|
|
119
|
+
return null;
|
|
120
|
+
return trimmed.slice(0, maxChars);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Whitespace-insensitive, case-insensitive identity check — if the
|
|
124
|
+
* classifier's "refinement" is just the input with different spacing,
|
|
125
|
+
* don't bother the user with a "Refined:" block.
|
|
126
|
+
*/
|
|
127
|
+
export function isEffectivelyIdentical(a, b) {
|
|
128
|
+
const normalize = (s) => s.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
129
|
+
return normalize(a) === normalize(b);
|
|
130
|
+
}
|
|
131
|
+
export const REFINED_PROMPT_MAX_CHARS = REFINED_MAX_CHARS;
|
|
132
|
+
export const REFINEMENT_SUMMARY_LIMIT = REFINEMENT_SUMMARY_MAX_CHARS;
|
|
85
133
|
/**
|
|
86
134
|
* Pick the best model + alternatives for this media request. Returns null
|
|
87
135
|
* on any failure path (classifier timeout, parse error, empty catalog) so
|
|
@@ -100,12 +148,18 @@ export async function analyzeMediaRequest(opts) {
|
|
|
100
148
|
if (catalog.length === 0)
|
|
101
149
|
return null;
|
|
102
150
|
// Cache check — classifier output is stable for a given prompt + catalog
|
|
103
|
-
// version, so re-asking within 30s is waste.
|
|
151
|
+
// version, so re-asking within 30s is waste. Cache stores the FULL
|
|
152
|
+
// classifier response (refinement + model picks); the per-call mask for
|
|
153
|
+
// skipRefine / FRANKLIN_NO_MEDIA_PROMPT_REFINE is applied on the way out
|
|
154
|
+
// so the same cache entry serves callers with different opt-out flags.
|
|
104
155
|
const quantity = Math.max(1, Math.floor(opts.quantity ?? 1));
|
|
156
|
+
const globalOptOut = process.env.FRANKLIN_NO_MEDIA_PROMPT_REFINE === '1';
|
|
157
|
+
const shouldDiscard = globalOptOut || opts.skipRefine === true;
|
|
158
|
+
const maskRefinement = (p) => shouldDiscard ? { ...p, refinedPrompt: null, refinementSummary: '' } : p;
|
|
105
159
|
const key = hashKey([kind, prompt.trim().slice(0, 500), String(quantity), String(opts.durationSeconds ?? '')]);
|
|
106
160
|
const hit = cache.get(key);
|
|
107
161
|
if (hit && hit.expiresAt > Date.now())
|
|
108
|
-
return hit.value;
|
|
162
|
+
return maskRefinement(hit.value);
|
|
109
163
|
// Call the classifier.
|
|
110
164
|
const catalogMap = new Map(catalog.map(m => [m.id, m]));
|
|
111
165
|
const system = buildSystemPrompt(kind, catalog);
|
|
@@ -152,6 +206,15 @@ export async function analyzeMediaRequest(opts) {
|
|
|
152
206
|
return null;
|
|
153
207
|
const cheaperChoice = validateChoice(parsed.cheaper, catalogMap);
|
|
154
208
|
const premiumChoice = validateChoice(parsed.premium, catalogMap);
|
|
209
|
+
// Refinement fields. The cache stores the full classifier output; the
|
|
210
|
+
// per-call mask is applied on the way out via maskRefinement() above, so
|
|
211
|
+
// here we just normalize + discard rewrites that are effectively the
|
|
212
|
+
// same as the raw input (drift-proof).
|
|
213
|
+
let refinedPrompt = validateRefined(parsed.refined_prompt, REFINED_MAX_CHARS);
|
|
214
|
+
const refinementSummary = validateRefined(parsed.refinement_summary, REFINEMENT_SUMMARY_MAX_CHARS) ?? '';
|
|
215
|
+
if (refinedPrompt !== null && isEffectivelyIdentical(refinedPrompt, prompt)) {
|
|
216
|
+
refinedPrompt = null;
|
|
217
|
+
}
|
|
155
218
|
// Build proposal with live cost estimates.
|
|
156
219
|
const durationSeconds = kind === 'video'
|
|
157
220
|
? (opts.durationSeconds ?? defaultDurationSeconds(rec.model))
|
|
@@ -180,6 +243,8 @@ export async function analyzeMediaRequest(opts) {
|
|
|
180
243
|
cheaper: toChoice(cheaperChoice),
|
|
181
244
|
premium: toChoice(premiumChoice),
|
|
182
245
|
intent: { style, priority },
|
|
246
|
+
refinedPrompt,
|
|
247
|
+
refinementSummary,
|
|
183
248
|
totalCostUsd: recommended.estimatedCostUsd,
|
|
184
249
|
};
|
|
185
250
|
// Evict oldest if bounded
|
|
@@ -189,7 +254,7 @@ export async function analyzeMediaRequest(opts) {
|
|
|
189
254
|
cache.delete(firstKey);
|
|
190
255
|
}
|
|
191
256
|
cache.set(key, { value: proposal, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
192
|
-
return proposal;
|
|
257
|
+
return maskRefinement(proposal);
|
|
193
258
|
}
|
|
194
259
|
// ─── Presentation ───────────────────────────────────────────────────────
|
|
195
260
|
/**
|
|
@@ -202,6 +267,13 @@ export function renderProposalForAskUser(p, userPrompt) {
|
|
|
202
267
|
lines.push(`*Media generation proposal*`);
|
|
203
268
|
lines.push('');
|
|
204
269
|
lines.push(`Prompt: "${userPrompt.trim().slice(0, 200)}"`);
|
|
270
|
+
if (p.refinedPrompt) {
|
|
271
|
+
lines.push('');
|
|
272
|
+
lines.push(`Refined: ${p.refinedPrompt}`);
|
|
273
|
+
if (p.refinementSummary) {
|
|
274
|
+
lines.push(` (${p.refinementSummary})`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
205
277
|
if (p.kind === 'video' && p.durationSeconds) {
|
|
206
278
|
const maxNote = p.maxDurationSeconds ? ` (max ${p.maxDurationSeconds}s)` : '';
|
|
207
279
|
lines.push(`Duration: ${p.durationSeconds}s${maxNote}`);
|
|
@@ -220,11 +292,17 @@ export function renderProposalForAskUser(p, userPrompt) {
|
|
|
220
292
|
lines.push('');
|
|
221
293
|
lines.push(` (prices include the 5% gateway fee)`);
|
|
222
294
|
const options = [];
|
|
223
|
-
|
|
295
|
+
const recLabel = p.refinedPrompt
|
|
296
|
+
? `Continue with refined prompt + ${p.recommended.model}`
|
|
297
|
+
: `Continue with ${p.recommended.model}`;
|
|
298
|
+
options.push({ id: 'recommended', label: recLabel });
|
|
224
299
|
if (p.cheaper)
|
|
225
300
|
options.push({ id: 'cheaper', label: `Use cheaper (${p.cheaper.model})` });
|
|
226
301
|
if (p.premium)
|
|
227
302
|
options.push({ id: 'premium', label: `Use premium (${p.premium.model})` });
|
|
303
|
+
if (p.refinedPrompt) {
|
|
304
|
+
options.push({ id: 'use-raw', label: `Use ORIGINAL prompt + ${p.recommended.model}` });
|
|
305
|
+
}
|
|
228
306
|
options.push({ id: 'cancel', label: 'Cancel (no charge)' });
|
|
229
307
|
return { question: lines.join('\n'), options };
|
|
230
308
|
}
|
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 {
|