@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
|
@@ -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
|
package/dist/proxy/server.js
CHANGED
|
@@ -67,9 +67,11 @@ const MODEL_SHORTCUTS = {
|
|
|
67
67
|
'opus-4.6': 'anthropic/claude-opus-4.6',
|
|
68
68
|
haiku: 'anthropic/claude-haiku-4.5',
|
|
69
69
|
// OpenAI
|
|
70
|
-
gpt
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// `gpt` / `gpt5` / `gpt-5` follow the gateway's flagship — currently 5.5.
|
|
71
|
+
gpt: 'openai/gpt-5.5',
|
|
72
|
+
gpt5: 'openai/gpt-5.5',
|
|
73
|
+
'gpt-5': 'openai/gpt-5.5',
|
|
74
|
+
'gpt-5.5': 'openai/gpt-5.5',
|
|
73
75
|
'gpt-5.4': 'openai/gpt-5.4',
|
|
74
76
|
'gpt-5.4-pro': 'openai/gpt-5.4-pro',
|
|
75
77
|
'gpt-5.3': 'openai/gpt-5.3',
|
package/dist/router/index.js
CHANGED
|
@@ -44,11 +44,11 @@ const AUTO_TIERS = {
|
|
|
44
44
|
},
|
|
45
45
|
MEDIUM: {
|
|
46
46
|
primary: 'anthropic/claude-sonnet-4.6',
|
|
47
|
-
fallback: ['openai/gpt-5.
|
|
47
|
+
fallback: ['openai/gpt-5.5', 'google/gemini-3.1-pro', 'moonshot/kimi-k2.6'],
|
|
48
48
|
},
|
|
49
49
|
COMPLEX: {
|
|
50
50
|
primary: 'anthropic/claude-sonnet-4.6',
|
|
51
|
-
fallback: ['openai/gpt-5.
|
|
51
|
+
fallback: ['openai/gpt-5.5', 'anthropic/claude-opus-4.7', 'moonshot/kimi-k2.6'],
|
|
52
52
|
},
|
|
53
53
|
REASONING: {
|
|
54
54
|
// Opus 4.7: step-change improvement in agentic coding over 4.6 per
|
|
@@ -93,7 +93,7 @@ const PREMIUM_TIERS = {
|
|
|
93
93
|
},
|
|
94
94
|
COMPLEX: {
|
|
95
95
|
primary: 'anthropic/claude-opus-4.7',
|
|
96
|
-
fallback: ['anthropic/claude-opus-4.6', 'openai/gpt-5.
|
|
96
|
+
fallback: ['anthropic/claude-opus-4.6', 'openai/gpt-5.5', 'anthropic/claude-sonnet-4.6'],
|
|
97
97
|
},
|
|
98
98
|
REASONING: {
|
|
99
99
|
primary: 'anthropic/claude-opus-4.7',
|
package/dist/session/storage.js
CHANGED
|
@@ -13,7 +13,7 @@ function getSessionsDir() {
|
|
|
13
13
|
if (resolvedSessionsDir)
|
|
14
14
|
return resolvedSessionsDir;
|
|
15
15
|
const preferred = path.join(BLOCKRUN_DIR, 'sessions');
|
|
16
|
-
const fallback = path.join(os.tmpdir(), '
|
|
16
|
+
const fallback = path.join(os.tmpdir(), 'franklin', 'sessions');
|
|
17
17
|
for (const dir of [preferred, fallback]) {
|
|
18
18
|
try {
|
|
19
19
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -41,7 +41,7 @@ function metaPath(id) {
|
|
|
41
41
|
}
|
|
42
42
|
function withWritableSessionDir(action) {
|
|
43
43
|
const preferred = path.join(BLOCKRUN_DIR, 'sessions');
|
|
44
|
-
const fallback = path.join(os.tmpdir(), '
|
|
44
|
+
const fallback = path.join(os.tmpdir(), 'franklin', 'sessions');
|
|
45
45
|
try {
|
|
46
46
|
action();
|
|
47
47
|
}
|
package/dist/tools/imagegen.d.ts
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { CapabilityHandler } from '../agent/types.js';
|
|
6
6
|
import type { ContentLibrary } from '../content/library.js';
|
|
7
|
+
/**
|
|
8
|
+
* Models that accept a reference image via /v1/images/image2image. Currently
|
|
9
|
+
* limited to OpenAI's edit endpoint — Gemini Nano Banana Pro and Grok Imagine
|
|
10
|
+
* Image Pro need gateway-side support before they can be wired in here.
|
|
11
|
+
*/
|
|
12
|
+
export declare const EDIT_SUPPORTED_MODELS: Set<string>;
|
|
13
|
+
export declare const REFERENCE_IMAGE_MAX_BYTES = 4000000;
|
|
14
|
+
/**
|
|
15
|
+
* Normalize a reference image into a base64 data URI for the gateway. The
|
|
16
|
+
* /v1/images/image2image endpoint validates `image` against /^data:image\//,
|
|
17
|
+
* so http(s) URLs and local paths both have to be inlined client-side before
|
|
18
|
+
* posting. Already-formed data URIs pass through.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveReferenceImage(input: string, workingDir: string): Promise<string>;
|
|
7
21
|
export interface ImageGenDeps {
|
|
8
22
|
/** Optional Content library for auto-recording generations into a piece. */
|
|
9
23
|
library?: ContentLibrary;
|
package/dist/tools/imagegen.js
CHANGED
|
@@ -9,21 +9,124 @@ import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
|
9
9
|
import { checkImageBudget, recordImageAsset } from '../content/record-image.js';
|
|
10
10
|
import { ModelClient } from '../agent/llm.js';
|
|
11
11
|
import { analyzeMediaRequest, renderProposalForAskUser } from '../agent/media-router.js';
|
|
12
|
+
import { recordUsage } from '../stats/tracker.js';
|
|
13
|
+
import { findModel, estimateCostUsd } from '../gateway-models.js';
|
|
14
|
+
/**
|
|
15
|
+
* Models that accept a reference image via /v1/images/image2image. Currently
|
|
16
|
+
* limited to OpenAI's edit endpoint — Gemini Nano Banana Pro and Grok Imagine
|
|
17
|
+
* Image Pro need gateway-side support before they can be wired in here.
|
|
18
|
+
*/
|
|
19
|
+
export const EDIT_SUPPORTED_MODELS = new Set([
|
|
20
|
+
'openai/gpt-image-1',
|
|
21
|
+
'openai/gpt-image-2',
|
|
22
|
+
]);
|
|
23
|
+
export const REFERENCE_IMAGE_MAX_BYTES = 4_000_000;
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a reference image into a base64 data URI for the gateway. The
|
|
26
|
+
* /v1/images/image2image endpoint validates `image` against /^data:image\//,
|
|
27
|
+
* so http(s) URLs and local paths both have to be inlined client-side before
|
|
28
|
+
* posting. Already-formed data URIs pass through.
|
|
29
|
+
*/
|
|
30
|
+
export async function resolveReferenceImage(input, workingDir) {
|
|
31
|
+
if (input.startsWith('data:image/'))
|
|
32
|
+
return input;
|
|
33
|
+
if (/^https?:\/\//i.test(input)) {
|
|
34
|
+
const ctrl = new AbortController();
|
|
35
|
+
const timeout = setTimeout(() => ctrl.abort(), 30_000);
|
|
36
|
+
try {
|
|
37
|
+
const resp = await fetch(input, { signal: ctrl.signal });
|
|
38
|
+
if (!resp.ok) {
|
|
39
|
+
throw new Error(`Reference image fetch failed: ${resp.status} ${resp.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
const contentType = (resp.headers.get('content-type') || '').toLowerCase().split(';')[0].trim();
|
|
42
|
+
if (!contentType.startsWith('image/')) {
|
|
43
|
+
throw new Error(`Reference image URL returned non-image content-type: ${contentType || '(none)'}`);
|
|
44
|
+
}
|
|
45
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
46
|
+
if (buf.byteLength > REFERENCE_IMAGE_MAX_BYTES) {
|
|
47
|
+
throw new Error(`Reference image too large: ${(buf.byteLength / 1_000_000).toFixed(1)}MB > ${(REFERENCE_IMAGE_MAX_BYTES / 1_000_000).toFixed(1)}MB cap.`);
|
|
48
|
+
}
|
|
49
|
+
return `data:${contentType};base64,${buf.toString('base64')}`;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Treat as local file path.
|
|
56
|
+
const resolved = path.isAbsolute(input) ? input : path.resolve(workingDir, input);
|
|
57
|
+
const stat = fs.statSync(resolved);
|
|
58
|
+
if (stat.size > REFERENCE_IMAGE_MAX_BYTES) {
|
|
59
|
+
throw new Error(`Reference image too large: ${(stat.size / 1_000_000).toFixed(1)}MB > ${(REFERENCE_IMAGE_MAX_BYTES / 1_000_000).toFixed(1)}MB cap. Resize or crop first.`);
|
|
60
|
+
}
|
|
61
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
62
|
+
const mimeMap = {
|
|
63
|
+
'.png': 'image/png',
|
|
64
|
+
'.jpg': 'image/jpeg',
|
|
65
|
+
'.jpeg': 'image/jpeg',
|
|
66
|
+
'.gif': 'image/gif',
|
|
67
|
+
'.webp': 'image/webp',
|
|
68
|
+
};
|
|
69
|
+
const mime = mimeMap[ext];
|
|
70
|
+
if (!mime) {
|
|
71
|
+
throw new Error(`Unsupported reference image extension ${ext || '(none)'}. Use .png/.jpg/.jpeg/.gif/.webp.`);
|
|
72
|
+
}
|
|
73
|
+
const bytes = fs.readFileSync(resolved);
|
|
74
|
+
return `data:${mime};base64,${bytes.toString('base64')}`;
|
|
75
|
+
}
|
|
12
76
|
function buildExecute(deps) {
|
|
13
77
|
return async function execute(input, ctx) {
|
|
14
|
-
const
|
|
15
|
-
|
|
78
|
+
const rawInput = input;
|
|
79
|
+
const { output_path, size, model, contentId, image_url } = rawInput;
|
|
80
|
+
if (!rawInput.prompt) {
|
|
16
81
|
return { output: 'Error: prompt is required', isError: true };
|
|
17
82
|
}
|
|
83
|
+
// Resolve the reference image (if any) before any paid call so we fail
|
|
84
|
+
// cheaply on bad paths / oversize attachments. Holds the resolved data URI
|
|
85
|
+
// / http URL that gets posted to /v1/images/image2image.
|
|
86
|
+
let referenceImage;
|
|
87
|
+
if (image_url) {
|
|
88
|
+
try {
|
|
89
|
+
referenceImage = await resolveReferenceImage(image_url, ctx.workingDir);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
return { output: `Error: ${err.message}`, isError: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// One-shot refinement opt-out: leading `///` tells Franklin "don't
|
|
96
|
+
// refine this prompt, I wrote it the way I want it." Strip the prefix
|
|
97
|
+
// and pass skipRefine through to the router.
|
|
98
|
+
let prompt = rawInput.prompt;
|
|
99
|
+
let skipRefine = false;
|
|
100
|
+
if (prompt.trimStart().startsWith('///')) {
|
|
101
|
+
prompt = prompt.replace(/^\s*\/\/\/\s?/, '');
|
|
102
|
+
skipRefine = true;
|
|
103
|
+
}
|
|
18
104
|
// ── Media router + AskUser flow ────────────────────────────────────
|
|
19
105
|
// If the caller explicitly named a model, or the env auto-approves, or
|
|
20
106
|
// no AskUser bridge exists (batch / --prompt mode), skip the proposal
|
|
21
107
|
// step and use the old default. Otherwise: classifier picks a fitting
|
|
22
|
-
// model,
|
|
23
|
-
|
|
108
|
+
// model + rewrites the prompt, the preview goes to AskUser, user
|
|
109
|
+
// chooses or cancels.
|
|
110
|
+
// Reference-image mode forces an edit-capable model. If the caller named
|
|
111
|
+
// an unsupported one, fail loudly so we don't silently downgrade their
|
|
112
|
+
// request to text-only generation.
|
|
113
|
+
if (referenceImage && model && !EDIT_SUPPORTED_MODELS.has(model)) {
|
|
114
|
+
return {
|
|
115
|
+
output: `Error: model ${model} does not support reference images. ` +
|
|
116
|
+
`Use one of: ${[...EDIT_SUPPORTED_MODELS].join(', ')}.`,
|
|
117
|
+
isError: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
let imageModel = model || (referenceImage ? 'openai/gpt-image-2' : 'openai/gpt-image-1');
|
|
24
121
|
const imageSize = size || '1024x1024';
|
|
122
|
+
let chosenPrompt = prompt;
|
|
123
|
+
// Skip the proposal flow when a reference image is set: the media router
|
|
124
|
+
// doesn't know which models support image-to-image, so its suggestions
|
|
125
|
+
// would frequently be unusable (text-only models). Default to gpt-image-1
|
|
126
|
+
// for now; a future router upgrade can pick between the four edit-capable
|
|
127
|
+
// models based on the prompt.
|
|
25
128
|
const autoApprove = process.env.FRANKLIN_MEDIA_AUTO_APPROVE_ALL === '1';
|
|
26
|
-
if (!model && !autoApprove && ctx.onAskUser) {
|
|
129
|
+
if (!model && !autoApprove && ctx.onAskUser && !referenceImage) {
|
|
27
130
|
try {
|
|
28
131
|
const chain = loadChain();
|
|
29
132
|
const client = new ModelClient({ apiUrl: API_URLS[chain], chain });
|
|
@@ -33,6 +136,7 @@ function buildExecute(deps) {
|
|
|
33
136
|
quantity: 1,
|
|
34
137
|
client,
|
|
35
138
|
signal: ctx.abortSignal,
|
|
139
|
+
skipRefine,
|
|
36
140
|
});
|
|
37
141
|
if (proposal) {
|
|
38
142
|
const { question, options } = renderProposalForAskUser(proposal, prompt);
|
|
@@ -51,9 +155,15 @@ function buildExecute(deps) {
|
|
|
51
155
|
return {
|
|
52
156
|
output: `## Image generation cancelled\n\nNo USDC was spent. Ask again when ready, or pass an explicit \`model\` to skip the confirmation step.`,
|
|
53
157
|
};
|
|
158
|
+
case 'use-raw':
|
|
159
|
+
imageModel = proposal.recommended.model;
|
|
160
|
+
// chosenPrompt stays as the raw input
|
|
161
|
+
break;
|
|
54
162
|
case 'recommended':
|
|
55
163
|
default:
|
|
56
164
|
imageModel = proposal.recommended.model;
|
|
165
|
+
if (proposal.refinedPrompt)
|
|
166
|
+
chosenPrompt = proposal.refinedPrompt;
|
|
57
167
|
}
|
|
58
168
|
}
|
|
59
169
|
}
|
|
@@ -76,18 +186,30 @@ function buildExecute(deps) {
|
|
|
76
186
|
}
|
|
77
187
|
const chain = loadChain();
|
|
78
188
|
const apiUrl = API_URLS[chain];
|
|
79
|
-
|
|
189
|
+
// Reference-image mode hits the dedicated /v1/images/image2image endpoint;
|
|
190
|
+
// otherwise stay on text-to-image generations.
|
|
191
|
+
const endpoint = referenceImage
|
|
192
|
+
? `${apiUrl}/v1/images/image2image`
|
|
193
|
+
: `${apiUrl}/v1/images/generations`;
|
|
80
194
|
// Default output path
|
|
81
195
|
const outPath = output_path
|
|
82
196
|
? (path.isAbsolute(output_path) ? output_path : path.resolve(ctx.workingDir, output_path))
|
|
83
197
|
: path.resolve(ctx.workingDir, `generated-${Date.now()}.png`);
|
|
84
|
-
const body = JSON.stringify(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
198
|
+
const body = JSON.stringify(referenceImage
|
|
199
|
+
? {
|
|
200
|
+
model: imageModel,
|
|
201
|
+
prompt: chosenPrompt,
|
|
202
|
+
image: referenceImage,
|
|
203
|
+
size: imageSize,
|
|
204
|
+
n: 1,
|
|
205
|
+
}
|
|
206
|
+
: {
|
|
207
|
+
model: imageModel,
|
|
208
|
+
prompt: chosenPrompt,
|
|
209
|
+
n: 1,
|
|
210
|
+
size: imageSize,
|
|
211
|
+
response_format: 'b64_json',
|
|
212
|
+
});
|
|
91
213
|
const headers = {
|
|
92
214
|
'Content-Type': 'application/json',
|
|
93
215
|
'User-Agent': `franklin/${VERSION}`,
|
|
@@ -106,7 +228,7 @@ function buildExecute(deps) {
|
|
|
106
228
|
if (response.status === 402) {
|
|
107
229
|
const paymentHeaders = await signPayment(response, chain, endpoint);
|
|
108
230
|
if (!paymentHeaders) {
|
|
109
|
-
return { output: 'Payment failed. Check wallet balance with:
|
|
231
|
+
return { output: 'Payment failed. Check wallet balance with: franklin balance', isError: true };
|
|
110
232
|
}
|
|
111
233
|
response = await fetch(endpoint, {
|
|
112
234
|
method: 'POST',
|
|
@@ -124,12 +246,23 @@ function buildExecute(deps) {
|
|
|
124
246
|
if (!imageData) {
|
|
125
247
|
return { output: 'No image data returned from API', isError: true };
|
|
126
248
|
}
|
|
127
|
-
// Save image
|
|
249
|
+
// Save image. The /v1/images/image2image endpoint returns Gemini results
|
|
250
|
+
// as a data URI in `url`, so decode those locally instead of going through
|
|
251
|
+
// fetch — saves a network round-trip and avoids data:-URI fetch quirks.
|
|
128
252
|
if (imageData.b64_json) {
|
|
129
253
|
const buffer = Buffer.from(imageData.b64_json, 'base64');
|
|
130
254
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
131
255
|
fs.writeFileSync(outPath, buffer);
|
|
132
256
|
}
|
|
257
|
+
else if (imageData.url && imageData.url.startsWith('data:')) {
|
|
258
|
+
const match = imageData.url.match(/^data:[^;]+;base64,(.+)$/);
|
|
259
|
+
if (!match) {
|
|
260
|
+
return { output: 'Malformed data URI in response', isError: true };
|
|
261
|
+
}
|
|
262
|
+
const buffer = Buffer.from(match[1], 'base64');
|
|
263
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
264
|
+
fs.writeFileSync(outPath, buffer);
|
|
265
|
+
}
|
|
133
266
|
else if (imageData.url) {
|
|
134
267
|
// Download from URL (with 30s timeout)
|
|
135
268
|
const dlCtrl = new AbortController();
|
|
@@ -146,6 +279,20 @@ function buildExecute(deps) {
|
|
|
146
279
|
const fileSize = fs.statSync(outPath).size;
|
|
147
280
|
const sizeKB = (fileSize / 1024).toFixed(1);
|
|
148
281
|
const revisedPrompt = imageData.revised_prompt ? `\nRevised prompt: ${imageData.revised_prompt}` : '';
|
|
282
|
+
// Stats: record this generation so it shows up in `franklin insights`
|
|
283
|
+
// alongside chat spend. Before this, media generations bypassed
|
|
284
|
+
// recordUsage entirely (only LLM chat calls were tracked), so the
|
|
285
|
+
// insights panel under-reported total spend and never surfaced
|
|
286
|
+
// image-generation models in its "top models" list. Fire-and-forget —
|
|
287
|
+
// stats write must not fail a user-visible generation.
|
|
288
|
+
void (async () => {
|
|
289
|
+
try {
|
|
290
|
+
const m = await findModel(imageModel);
|
|
291
|
+
const estCost = m ? estimateCostUsd(m, { quantity: 1 }) : 0;
|
|
292
|
+
recordUsage(imageModel, 0, 0, estCost, 0);
|
|
293
|
+
}
|
|
294
|
+
catch { /* ignore stats errors */ }
|
|
295
|
+
})();
|
|
149
296
|
let contentSummary = '';
|
|
150
297
|
if (contentId && deps.library) {
|
|
151
298
|
const rec = recordImageAsset(deps.library, {
|
|
@@ -206,7 +353,7 @@ async function signPayment(response, chain, endpoint) {
|
|
|
206
353
|
const feePayer = details.extra?.feePayer || details.recipient;
|
|
207
354
|
const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
|
|
208
355
|
resourceUrl: details.resource?.url || endpoint,
|
|
209
|
-
resourceDescription: details.resource?.description || '
|
|
356
|
+
resourceDescription: details.resource?.description || 'Franklin image generation',
|
|
210
357
|
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
|
|
211
358
|
extra: details.extra,
|
|
212
359
|
});
|
|
@@ -218,7 +365,7 @@ async function signPayment(response, chain, endpoint) {
|
|
|
218
365
|
const details = extractPaymentDetails(paymentRequired);
|
|
219
366
|
const payload = await createPaymentPayload(wallet.privateKey, wallet.address, details.recipient, details.amount, details.network || 'eip155:8453', {
|
|
220
367
|
resourceUrl: details.resource?.url || endpoint,
|
|
221
|
-
resourceDescription: details.resource?.description || '
|
|
368
|
+
resourceDescription: details.resource?.description || 'Franklin image generation',
|
|
222
369
|
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
|
|
223
370
|
extra: details.extra,
|
|
224
371
|
});
|
|
@@ -253,13 +400,16 @@ export function createImageGenCapability(deps = {}) {
|
|
|
253
400
|
return {
|
|
254
401
|
spec: {
|
|
255
402
|
name: 'ImageGen',
|
|
256
|
-
description: "Generate an image from a text prompt
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
"
|
|
403
|
+
description: "Generate an image from a text prompt — optionally with a reference " +
|
|
404
|
+
"image for style transfer / character consistency / edits. Costs USDC " +
|
|
405
|
+
"from the user's wallet — confirm before generating. Saves to a local " +
|
|
406
|
+
"file. Default size: 1024x1024. Do NOT call repeatedly to iterate on " +
|
|
407
|
+
"style — ask the user first. Pass contentId to attach the result to " +
|
|
408
|
+
"an existing Content piece: the content's budget is checked BEFORE " +
|
|
409
|
+
"paying, and on success the image is recorded as an asset with its " +
|
|
410
|
+
"estimated cost. Skipping contentId generates a one-off image with no " +
|
|
411
|
+
"budget tracking. When image_url is set, only edit-capable models " +
|
|
412
|
+
"(openai/gpt-image-1, openai/gpt-image-2) are accepted.",
|
|
263
413
|
input_schema: {
|
|
264
414
|
type: 'object',
|
|
265
415
|
properties: {
|
|
@@ -267,6 +417,7 @@ export function createImageGenCapability(deps = {}) {
|
|
|
267
417
|
output_path: { type: 'string', description: 'Where to save the image. Default: generated-<timestamp>.png in working directory' },
|
|
268
418
|
size: { type: 'string', description: 'Image size: 1024x1024, 1792x1024, or 1024x1792. Default: 1024x1024' },
|
|
269
419
|
model: { type: 'string', description: 'Image model to use. Default: openai/gpt-image-1' },
|
|
420
|
+
image_url: { type: 'string', description: 'Optional reference image (image-to-image / style transfer). Accepts an http(s) URL, a data URI, or a local file path. Only works with edit-capable models.' },
|
|
270
421
|
contentId: { type: 'string', description: 'Optional Content id to attach this generation to. Pre-flight budget check + auto-record on success.' },
|
|
271
422
|
},
|
|
272
423
|
required: ['prompt'],
|
package/dist/tools/read.js
CHANGED
|
@@ -84,7 +84,34 @@ async function execute(input, ctx) {
|
|
|
84
84
|
// (some binaries have no extension: `.env.enc`, `.data`, compiled tools
|
|
85
85
|
// without suffixes, etc. Content sniff catches those.)
|
|
86
86
|
const ext = path.extname(resolved).toLowerCase();
|
|
87
|
-
|
|
87
|
+
// Image extensions → load as vision content so models with vision (Sonnet,
|
|
88
|
+
// GPT-4o, Gemini) actually see the bytes instead of a "Binary file" stub.
|
|
89
|
+
// The agent loop wraps `images` into tool_result.content for provider APIs.
|
|
90
|
+
const IMAGE_MEDIA_TYPES = {
|
|
91
|
+
'.png': 'image/png',
|
|
92
|
+
'.jpg': 'image/jpeg',
|
|
93
|
+
'.jpeg': 'image/jpeg',
|
|
94
|
+
'.gif': 'image/gif',
|
|
95
|
+
'.webp': 'image/webp',
|
|
96
|
+
};
|
|
97
|
+
if (IMAGE_MEDIA_TYPES[ext]) {
|
|
98
|
+
const sizeStr = stat.size >= 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
|
|
99
|
+
// Anthropic accepts up to 5MB base64; cap raw bytes at ~3.75MB to be safe.
|
|
100
|
+
const IMAGE_MAX_BYTES = 3_750_000;
|
|
101
|
+
if (stat.size > IMAGE_MAX_BYTES) {
|
|
102
|
+
return {
|
|
103
|
+
output: `Image file: ${resolved} (${ext}, ${sizeStr}). Too large to inline for vision (>${Math.round(IMAGE_MAX_BYTES / 1_000_000)}MB). Resize or crop first.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const bytes = fs.readFileSync(resolved);
|
|
107
|
+
const base64 = bytes.toString('base64');
|
|
108
|
+
fileReadTracker.set(resolved, { mtimeMs: stat.mtimeMs, readAt: Date.now() });
|
|
109
|
+
return {
|
|
110
|
+
output: `Image file: ${resolved} (${ext}, ${sizeStr}). Rendered below for vision-capable models.`,
|
|
111
|
+
images: [{ mediaType: IMAGE_MEDIA_TYPES[ext], base64 }],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const binaryExts = new Set(['.ico', '.bmp', '.pdf', '.zip', '.tar', '.gz', '.woff', '.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.wav', '.avi', '.mov', '.exe', '.dll', '.so', '.dylib']);
|
|
88
115
|
if (binaryExts.has(ext)) {
|
|
89
116
|
const sizeStr = stat.size >= 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
|
|
90
117
|
return { output: `Binary file: ${resolved} (${ext}, ${sizeStr}). Cannot display contents.` };
|
|
@@ -163,7 +190,7 @@ Usage:
|
|
|
163
190
|
- This tool can only read files, not directories. To list a directory, use Glob or ls via Bash.
|
|
164
191
|
- If you read a file that exists but has empty contents you will receive a warning.
|
|
165
192
|
- Reads over 2MB are rejected — use offset/limit to read portions.
|
|
166
|
-
-
|
|
193
|
+
- Image files (.png, .jpg, .jpeg, .gif, .webp) are loaded as vision content — vision-capable models see the actual image. Other binary files (PDFs, archives, fonts) cannot be displayed.
|
|
167
194
|
- You will regularly be asked to read screenshots or images. If the user provides a path, ALWAYS use this tool to view it.
|
|
168
195
|
|
|
169
196
|
IMPORTANT: Always use Read instead of cat, head, or tail via Bash. This tool provides line numbers and integrates with Edit's read-before-edit enforcement.`,
|
package/dist/tools/videogen.d.ts
CHANGED
|
@@ -3,9 +3,20 @@
|
|
|
3
3
|
* /v1/videos/generations endpoint. Uses x402 payment (Base or Solana).
|
|
4
4
|
*
|
|
5
5
|
* Default model `xai/grok-imagine-video` returns an 8-second clip for ~$0.42.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Seedance 2.0 (bytedance/seedance-2.0 and -fast) runs longer — up to a few
|
|
7
|
+
* minutes for a 10s clip.
|
|
8
|
+
*
|
|
9
|
+
* Flow (async since blockrun@654cd35):
|
|
10
|
+
* 1. POST /v1/videos/generations with signed x-payment header. The server
|
|
11
|
+
* verifies payment (does NOT settle), submits the upstream job, and
|
|
12
|
+
* returns 202 { id, poll_url, status: "queued" }.
|
|
13
|
+
* 2. GET the poll_url with the SAME x-payment header every ~5s until
|
|
14
|
+
* status=completed. On the first completed poll the server backs up
|
|
15
|
+
* the MP4 to GCS, settles payment, and returns the video URL.
|
|
16
|
+
* 3. Download the MP4 and write it locally.
|
|
17
|
+
*
|
|
18
|
+
* If the upstream job fails, the server returns status=failed and no USDC
|
|
19
|
+
* is ever transferred. If the client never polls, no charge either.
|
|
9
20
|
*/
|
|
10
21
|
import type { CapabilityHandler } from '../agent/types.js';
|
|
11
22
|
import type { ContentLibrary } from '../content/library.js';
|