@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/dist/tools/videogen.js
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 fs from 'node:fs';
|
|
11
22
|
import path from 'node:path';
|
|
@@ -13,23 +24,38 @@ import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, creat
|
|
|
13
24
|
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
14
25
|
import { ModelClient } from '../agent/llm.js';
|
|
15
26
|
import { analyzeMediaRequest, renderProposalForAskUser } from '../agent/media-router.js';
|
|
27
|
+
import { recordUsage } from '../stats/tracker.js';
|
|
28
|
+
import { findModel, estimateCostUsd } from '../gateway-models.js';
|
|
16
29
|
const DEFAULT_MODEL = 'xai/grok-imagine-video';
|
|
17
30
|
const DEFAULT_DURATION = 8;
|
|
18
31
|
const PRICE_PER_SECOND_USD = 0.05;
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
32
|
+
// POST submit is fast (~3-20s). Generation is async upstream (60-300s for
|
|
33
|
+
// Seedance, 20-90s for Grok). We poll until completed, then download. The
|
|
34
|
+
// server signs authorizations for 600s — keep the overall budget below that.
|
|
35
|
+
const SUBMIT_TIMEOUT_MS = 30_000;
|
|
36
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
37
|
+
const POLL_MAX_WAIT_MS = 480_000; // 8 min — covers Seedance worst case
|
|
22
38
|
const DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
23
39
|
function estimateVideoCostUsd(durationSeconds = DEFAULT_DURATION) {
|
|
24
40
|
return Math.max(1, durationSeconds) * PRICE_PER_SECOND_USD;
|
|
25
41
|
}
|
|
26
42
|
function buildExecute(deps) {
|
|
27
43
|
return async function execute(input, ctx) {
|
|
28
|
-
const
|
|
29
|
-
|
|
44
|
+
const rawInput = input;
|
|
45
|
+
const { output_path, model, image_url, duration_seconds, contentId } = rawInput;
|
|
46
|
+
if (!rawInput.prompt)
|
|
30
47
|
return { output: 'Error: prompt is required', isError: true };
|
|
48
|
+
// One-shot refinement opt-out: leading `///` tells Franklin "don't
|
|
49
|
+
// refine this prompt." Strip the prefix and pass skipRefine through.
|
|
50
|
+
let prompt = rawInput.prompt;
|
|
51
|
+
let skipRefine = false;
|
|
52
|
+
if (prompt.trimStart().startsWith('///')) {
|
|
53
|
+
prompt = prompt.replace(/^\s*\/\/\/\s?/, '');
|
|
54
|
+
skipRefine = true;
|
|
55
|
+
}
|
|
31
56
|
let videoModel = model || DEFAULT_MODEL;
|
|
32
57
|
let duration = duration_seconds ?? DEFAULT_DURATION;
|
|
58
|
+
let chosenPrompt = prompt;
|
|
33
59
|
// ── Media router + AskUser flow (video bills per second, always ask) ──
|
|
34
60
|
const autoApprove = process.env.FRANKLIN_MEDIA_AUTO_APPROVE_ALL === '1';
|
|
35
61
|
if (!model && !autoApprove && ctx.onAskUser) {
|
|
@@ -42,6 +68,7 @@ function buildExecute(deps) {
|
|
|
42
68
|
durationSeconds: duration_seconds,
|
|
43
69
|
client,
|
|
44
70
|
signal: ctx.abortSignal,
|
|
71
|
+
skipRefine,
|
|
45
72
|
});
|
|
46
73
|
if (proposal) {
|
|
47
74
|
const { question, options } = renderProposalForAskUser(proposal, prompt);
|
|
@@ -59,9 +86,15 @@ function buildExecute(deps) {
|
|
|
59
86
|
return {
|
|
60
87
|
output: `## Video generation cancelled\n\nNo USDC was spent.`,
|
|
61
88
|
};
|
|
89
|
+
case 'use-raw':
|
|
90
|
+
videoModel = proposal.recommended.model;
|
|
91
|
+
// chosenPrompt stays as the raw input
|
|
92
|
+
break;
|
|
62
93
|
case 'recommended':
|
|
63
94
|
default:
|
|
64
95
|
videoModel = proposal.recommended.model;
|
|
96
|
+
if (proposal.refinedPrompt)
|
|
97
|
+
chosenPrompt = proposal.refinedPrompt;
|
|
65
98
|
}
|
|
66
99
|
// Use the proposal's duration — the router honored the user's
|
|
67
100
|
// duration_seconds or filled in the model's default.
|
|
@@ -96,7 +129,7 @@ function buildExecute(deps) {
|
|
|
96
129
|
: path.resolve(ctx.workingDir, `generated-${Date.now()}.mp4`);
|
|
97
130
|
const body = JSON.stringify({
|
|
98
131
|
model: videoModel,
|
|
99
|
-
prompt,
|
|
132
|
+
prompt: chosenPrompt,
|
|
100
133
|
...(image_url ? { image_url } : {}),
|
|
101
134
|
...(duration_seconds ? { duration_seconds } : {}),
|
|
102
135
|
});
|
|
@@ -104,26 +137,32 @@ function buildExecute(deps) {
|
|
|
104
137
|
'Content-Type': 'application/json',
|
|
105
138
|
'User-Agent': `franklin/${VERSION}`,
|
|
106
139
|
};
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
140
|
+
const onAbort = (ctrl) => () => ctrl.abort();
|
|
141
|
+
// Phase 1: submit the job. First POST triggers a 402; we sign and retry.
|
|
142
|
+
// The signed paymentHeaders must be reused on every GET poll — the server
|
|
143
|
+
// uses the authorization to verify identity on each poll and settles on
|
|
144
|
+
// the first completed response.
|
|
145
|
+
const submitCtrl = new AbortController();
|
|
146
|
+
const submitTimeout = setTimeout(() => submitCtrl.abort(), SUBMIT_TIMEOUT_MS);
|
|
147
|
+
const submitAbort = onAbort(submitCtrl);
|
|
148
|
+
ctx.abortSignal.addEventListener('abort', submitAbort, { once: true });
|
|
149
|
+
let paymentHeaders = null;
|
|
150
|
+
let submitResult;
|
|
112
151
|
try {
|
|
113
152
|
let response = await fetch(endpoint, {
|
|
114
153
|
method: 'POST',
|
|
115
|
-
signal:
|
|
154
|
+
signal: submitCtrl.signal,
|
|
116
155
|
headers,
|
|
117
156
|
body,
|
|
118
157
|
});
|
|
119
158
|
if (response.status === 402) {
|
|
120
|
-
|
|
159
|
+
paymentHeaders = await signPayment(response, chain, endpoint);
|
|
121
160
|
if (!paymentHeaders) {
|
|
122
161
|
return { output: 'Payment failed. Check wallet balance with: franklin balance', isError: true };
|
|
123
162
|
}
|
|
124
163
|
response = await fetch(endpoint, {
|
|
125
164
|
method: 'POST',
|
|
126
|
-
signal:
|
|
165
|
+
signal: submitCtrl.signal,
|
|
127
166
|
headers: { ...headers, ...paymentHeaders },
|
|
128
167
|
body,
|
|
129
168
|
});
|
|
@@ -131,22 +170,71 @@ function buildExecute(deps) {
|
|
|
131
170
|
if (!response.ok) {
|
|
132
171
|
const errText = await response.text().catch(() => '');
|
|
133
172
|
return {
|
|
134
|
-
output: `Video
|
|
173
|
+
output: `Video submit failed (${response.status}): ${errText.slice(0, 300)}`,
|
|
135
174
|
isError: true,
|
|
136
175
|
};
|
|
137
176
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
177
|
+
submitResult = await response.json();
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
const msg = err.message || '';
|
|
181
|
+
if (msg.includes('abort')) {
|
|
182
|
+
return {
|
|
183
|
+
output: `Video submit timed out or was aborted after ${Math.round(SUBMIT_TIMEOUT_MS / 1000)}s.`,
|
|
184
|
+
isError: true,
|
|
185
|
+
};
|
|
142
186
|
}
|
|
187
|
+
return { output: `Error submitting video job: ${msg}`, isError: true };
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
clearTimeout(submitTimeout);
|
|
191
|
+
ctx.abortSignal.removeEventListener('abort', submitAbort);
|
|
192
|
+
}
|
|
193
|
+
if (!submitResult.poll_url || !paymentHeaders) {
|
|
194
|
+
return { output: 'API did not return a poll_url for the video job', isError: true };
|
|
195
|
+
}
|
|
196
|
+
// Phase 2: poll GET /v1/videos/generations/{id} with the SAME signed
|
|
197
|
+
// x-payment header until the job completes. Server settles on the first
|
|
198
|
+
// completed poll and returns the backed-up video URL.
|
|
199
|
+
const origin = new URL(apiUrl).origin;
|
|
200
|
+
const pollEndpoint = submitResult.poll_url.startsWith('http')
|
|
201
|
+
? submitResult.poll_url
|
|
202
|
+
: `${origin}${submitResult.poll_url}`;
|
|
203
|
+
const outcome = await pollUntilReady(pollEndpoint, { ...headers, ...paymentHeaders }, ctx.abortSignal);
|
|
204
|
+
if (outcome.kind === 'timed_out') {
|
|
205
|
+
return {
|
|
206
|
+
output: `Video generation did not complete within ${Math.round(POLL_MAX_WAIT_MS / 1000)}s. ` +
|
|
207
|
+
`No USDC was charged (settlement only fires on completion).`,
|
|
208
|
+
isError: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (outcome.kind === 'failed') {
|
|
212
|
+
return {
|
|
213
|
+
output: `Video generation failed upstream: ${outcome.error ?? 'unknown error'}. No USDC was charged.`,
|
|
214
|
+
isError: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const videoData = outcome.data;
|
|
218
|
+
const videoUrl = videoData.url;
|
|
219
|
+
if (!videoUrl) {
|
|
220
|
+
return { output: 'No video URL returned from API', isError: true };
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
143
223
|
// Download the MP4
|
|
144
224
|
const dlCtrl = new AbortController();
|
|
145
225
|
const dlTimeout = setTimeout(() => dlCtrl.abort(), DOWNLOAD_TIMEOUT_MS);
|
|
146
|
-
const
|
|
147
|
-
|
|
226
|
+
const dlAbort = onAbort(dlCtrl);
|
|
227
|
+
ctx.abortSignal.addEventListener('abort', dlAbort, { once: true });
|
|
228
|
+
let vidResp;
|
|
229
|
+
try {
|
|
230
|
+
vidResp = await fetch(videoUrl, { signal: dlCtrl.signal });
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
clearTimeout(dlTimeout);
|
|
234
|
+
ctx.abortSignal.removeEventListener('abort', dlAbort);
|
|
235
|
+
}
|
|
148
236
|
if (!vidResp.ok) {
|
|
149
|
-
return { output: `Video fetched URL but download failed (${vidResp.status}): ${
|
|
237
|
+
return { output: `Video fetched URL but download failed (${vidResp.status}): ${videoUrl}`, isError: true };
|
|
150
238
|
}
|
|
151
239
|
const buffer = Buffer.from(await vidResp.arrayBuffer());
|
|
152
240
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
@@ -154,6 +242,21 @@ function buildExecute(deps) {
|
|
|
154
242
|
const fileSize = fs.statSync(outPath).size;
|
|
155
243
|
const sizeMB = (fileSize / 1_048_576).toFixed(1);
|
|
156
244
|
const dur = videoData.duration_seconds ?? duration;
|
|
245
|
+
// Stats: record this generation so it shows up in `franklin insights`
|
|
246
|
+
// alongside chat spend. Before this, media generations bypassed
|
|
247
|
+
// recordUsage entirely, so the insights panel under-reported total
|
|
248
|
+
// spend and never surfaced video models in its "top models" list.
|
|
249
|
+
// Prefer the live gateway price when the model is in the catalog;
|
|
250
|
+
// fall back to the legacy $0.05/s estimate otherwise. Fire-and-
|
|
251
|
+
// forget — stats write must not fail a user-visible generation.
|
|
252
|
+
void (async () => {
|
|
253
|
+
try {
|
|
254
|
+
const m = await findModel(videoModel);
|
|
255
|
+
const estCost = m ? estimateCostUsd(m, { duration_seconds: dur }) : estimateVideoCostUsd(dur);
|
|
256
|
+
recordUsage(videoModel, 0, 0, estCost, 0);
|
|
257
|
+
}
|
|
258
|
+
catch { /* ignore stats errors */ }
|
|
259
|
+
})();
|
|
157
260
|
let contentSummary = '';
|
|
158
261
|
if (contentId && deps.library) {
|
|
159
262
|
const rec = deps.library.addAsset(contentId, {
|
|
@@ -191,18 +294,63 @@ function buildExecute(deps) {
|
|
|
191
294
|
const msg = err.message || '';
|
|
192
295
|
if (msg.includes('abort')) {
|
|
193
296
|
return {
|
|
194
|
-
output: `Video
|
|
297
|
+
output: `Video download timed out or was aborted after ${Math.round(DOWNLOAD_TIMEOUT_MS / 1000)}s.`,
|
|
195
298
|
isError: true,
|
|
196
299
|
};
|
|
197
300
|
}
|
|
198
301
|
return { output: `Error: ${msg}`, isError: true };
|
|
199
302
|
}
|
|
200
|
-
finally {
|
|
201
|
-
clearTimeout(timeout);
|
|
202
|
-
ctx.abortSignal.removeEventListener('abort', onAbort);
|
|
203
|
-
}
|
|
204
303
|
};
|
|
205
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Poll the GET /v1/videos/generations/{id} endpoint until the job reaches a
|
|
307
|
+
* terminal state. Reuses the caller's signed x-payment header verbatim on
|
|
308
|
+
* every request — the server verifies the same authorization each poll and
|
|
309
|
+
* settles on the first completed response.
|
|
310
|
+
*/
|
|
311
|
+
async function pollUntilReady(pollEndpoint, headers, userAbort) {
|
|
312
|
+
const deadline = Date.now() + POLL_MAX_WAIT_MS;
|
|
313
|
+
while (Date.now() < deadline) {
|
|
314
|
+
if (userAbort.aborted)
|
|
315
|
+
throw new Error('aborted');
|
|
316
|
+
const resp = await fetch(pollEndpoint, { method: 'GET', headers, signal: userAbort });
|
|
317
|
+
// 202 = still queued/in_progress; 200 = completed or failed.
|
|
318
|
+
if (resp.status === 202 || resp.status === 200) {
|
|
319
|
+
const body = (await resp.json().catch(() => ({})));
|
|
320
|
+
if (body.status === 'completed' && body.data?.[0]?.url) {
|
|
321
|
+
return { kind: 'completed', data: body.data[0] };
|
|
322
|
+
}
|
|
323
|
+
if (body.status === 'failed') {
|
|
324
|
+
return { kind: 'failed', error: body.error };
|
|
325
|
+
}
|
|
326
|
+
// queued / in_progress — sleep and try again.
|
|
327
|
+
}
|
|
328
|
+
else if (resp.status === 429 || resp.status >= 500) {
|
|
329
|
+
// Transient — back off briefly. Fall through to the sleep below.
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
const text = await resp.text().catch(() => '');
|
|
333
|
+
throw new Error(`Poll failed (${resp.status}): ${text.slice(0, 300)}`);
|
|
334
|
+
}
|
|
335
|
+
await sleep(POLL_INTERVAL_MS, userAbort);
|
|
336
|
+
}
|
|
337
|
+
return { kind: 'timed_out' };
|
|
338
|
+
}
|
|
339
|
+
function sleep(ms, signal) {
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
if (signal.aborted)
|
|
342
|
+
return reject(new Error('aborted'));
|
|
343
|
+
const t = setTimeout(() => {
|
|
344
|
+
signal.removeEventListener('abort', onAbort);
|
|
345
|
+
resolve();
|
|
346
|
+
}, ms);
|
|
347
|
+
const onAbort = () => {
|
|
348
|
+
clearTimeout(t);
|
|
349
|
+
reject(new Error('aborted'));
|
|
350
|
+
};
|
|
351
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
352
|
+
});
|
|
353
|
+
}
|
|
206
354
|
// ─── Payment ───────────────────────────────────────────────────────────────
|
|
207
355
|
async function signPayment(response, chain, endpoint) {
|
|
208
356
|
try {
|
|
@@ -218,7 +366,9 @@ async function signPayment(response, chain, endpoint) {
|
|
|
218
366
|
const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
|
|
219
367
|
resourceUrl: details.resource?.url || endpoint,
|
|
220
368
|
resourceDescription: details.resource?.description || 'Franklin video generation',
|
|
221
|
-
|
|
369
|
+
// Video poll can take up to 8 min; honor the server's advertised
|
|
370
|
+
// value (blockrun sends 600s) and fall back to 600 not 300.
|
|
371
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 600,
|
|
222
372
|
extra: details.extra,
|
|
223
373
|
});
|
|
224
374
|
return { 'PAYMENT-SIGNATURE': payload };
|
package/dist/tools/webhook.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* session.
|
|
16
16
|
*/
|
|
17
17
|
import { isIP } from 'node:net';
|
|
18
|
+
import { VERSION } from '../config.js';
|
|
18
19
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
19
20
|
const MAX_BODY_BYTES = 512 * 1024; // 512 KB is generous for a chat push.
|
|
20
21
|
function isPrivateHost(hostname) {
|
|
@@ -101,7 +102,7 @@ async function execute(input, ctx) {
|
|
|
101
102
|
}
|
|
102
103
|
const finalHeaders = {
|
|
103
104
|
'Content-Type': contentType,
|
|
104
|
-
'User-Agent':
|
|
105
|
+
'User-Agent': `franklin/${VERSION} (webhook)`,
|
|
105
106
|
...(headers ?? {}),
|
|
106
107
|
};
|
|
107
108
|
const ctrl = new AbortController();
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* cooldown, user-agent, timeout, and in-memory cache.
|
|
7
7
|
*/
|
|
8
8
|
import { recordFetch } from '../telemetry.js';
|
|
9
|
+
import { VERSION } from '../../../config.js';
|
|
9
10
|
const BASE = 'https://api.coingecko.com/api/v3';
|
|
10
|
-
const UA =
|
|
11
|
+
const UA = `franklin/${VERSION} (trading)`;
|
|
11
12
|
const TIMEOUT_MS = 10_000;
|
|
12
13
|
// Ticker → CoinGecko slug. Not exhaustive; unknown tickers fall through to
|
|
13
14
|
// lowercase and let CoinGecko either accept the slug or 404.
|
package/dist/ui/app.js
CHANGED
|
@@ -509,7 +509,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
509
509
|
}, []);
|
|
510
510
|
// Expose event handler, balance updater, and permission bridge
|
|
511
511
|
useEffect(() => {
|
|
512
|
-
globalThis.
|
|
512
|
+
globalThis.__franklin_ui = {
|
|
513
513
|
updateModel: (model) => { setCurrentModel(model); },
|
|
514
514
|
updateBalance: (bal) => {
|
|
515
515
|
setBalance(bal);
|
|
@@ -703,7 +703,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
703
703
|
setQueuedInputs((prev) => prev.slice(1));
|
|
704
704
|
// Small delay so React can flush the ready=true state first
|
|
705
705
|
setTimeout(() => {
|
|
706
|
-
const fn = globalThis.
|
|
706
|
+
const fn = globalThis.__franklin_submit;
|
|
707
707
|
if (typeof fn === 'function')
|
|
708
708
|
fn(queued);
|
|
709
709
|
}, 50);
|
|
@@ -713,12 +713,12 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
713
713
|
}
|
|
714
714
|
},
|
|
715
715
|
};
|
|
716
|
-
globalThis.
|
|
716
|
+
globalThis.__franklin_submit = (msg) => {
|
|
717
717
|
handleSubmit(msg);
|
|
718
718
|
};
|
|
719
719
|
return () => {
|
|
720
|
-
delete globalThis.
|
|
721
|
-
delete globalThis.
|
|
720
|
+
delete globalThis.__franklin_ui;
|
|
721
|
+
delete globalThis.__franklin_submit;
|
|
722
722
|
};
|
|
723
723
|
}, [handleSubmit, commitResponse, showStatus]);
|
|
724
724
|
// ── Render ──
|
|
@@ -781,7 +781,7 @@ export function launchInkUI(opts) {
|
|
|
781
781
|
let pendingInput = null; // Queue for inputs that arrive before waitForInput
|
|
782
782
|
let exiting = false;
|
|
783
783
|
let abortCallback = null;
|
|
784
|
-
const instance = render(_jsx(RunCodeApp, { initialModel: opts.model, workDir: opts.workDir, walletAddress: opts.walletAddress || 'not set — run:
|
|
784
|
+
const instance = render(_jsx(RunCodeApp, { initialModel: opts.model, workDir: opts.workDir, walletAddress: opts.walletAddress || 'not set — run: franklin setup', walletBalance: opts.walletBalance || 'unknown', chain: opts.chain || 'base', startWithPicker: opts.showPicker, onSubmit: (value) => {
|
|
785
785
|
if (resolveInput) {
|
|
786
786
|
resolveInput(value);
|
|
787
787
|
resolveInput = null;
|
|
@@ -799,19 +799,19 @@ export function launchInkUI(opts) {
|
|
|
799
799
|
} }));
|
|
800
800
|
return {
|
|
801
801
|
handleEvent: (event) => {
|
|
802
|
-
const ui = globalThis.
|
|
802
|
+
const ui = globalThis.__franklin_ui;
|
|
803
803
|
ui?.handleEvent(event);
|
|
804
804
|
},
|
|
805
805
|
updateModel: (model) => {
|
|
806
|
-
const ui = globalThis.
|
|
806
|
+
const ui = globalThis.__franklin_ui;
|
|
807
807
|
ui?.updateModel(model);
|
|
808
808
|
},
|
|
809
809
|
updateBalance: (bal) => {
|
|
810
|
-
const ui = globalThis.
|
|
810
|
+
const ui = globalThis.__franklin_ui;
|
|
811
811
|
ui?.updateBalance(bal);
|
|
812
812
|
},
|
|
813
813
|
onTurnDone: (cb) => {
|
|
814
|
-
const ui = globalThis.
|
|
814
|
+
const ui = globalThis.__franklin_ui;
|
|
815
815
|
ui?.onTurnDone(cb);
|
|
816
816
|
},
|
|
817
817
|
waitForInput: () => {
|
|
@@ -828,11 +828,11 @@ export function launchInkUI(opts) {
|
|
|
828
828
|
onAbort: (cb) => { abortCallback = cb; },
|
|
829
829
|
cleanup: () => { mouse.disable(); instance.unmount(); },
|
|
830
830
|
requestPermission: (toolName, description) => {
|
|
831
|
-
const ui = globalThis.
|
|
831
|
+
const ui = globalThis.__franklin_ui;
|
|
832
832
|
return ui?.requestPermission(toolName, description) ?? Promise.resolve('no');
|
|
833
833
|
},
|
|
834
834
|
requestAskUser: (question, options) => {
|
|
835
|
-
const ui = globalThis.
|
|
835
|
+
const ui = globalThis.__franklin_ui;
|
|
836
836
|
return ui?.requestAskUser(question, options) ?? Promise.resolve('(no response)');
|
|
837
837
|
},
|
|
838
838
|
};
|
package/dist/ui/model-picker.js
CHANGED
|
@@ -19,9 +19,11 @@ export const MODEL_SHORTCUTS = {
|
|
|
19
19
|
'opus-4.6': 'anthropic/claude-opus-4.6',
|
|
20
20
|
haiku: 'anthropic/claude-haiku-4.5-20251001',
|
|
21
21
|
// OpenAI
|
|
22
|
-
gpt
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
// `gpt` / `gpt5` / `gpt-5` follow the gateway's flagship — currently 5.5.
|
|
23
|
+
gpt: 'openai/gpt-5.5',
|
|
24
|
+
gpt5: 'openai/gpt-5.5',
|
|
25
|
+
'gpt-5': 'openai/gpt-5.5',
|
|
26
|
+
'gpt-5.5': 'openai/gpt-5.5',
|
|
25
27
|
'gpt-5.4': 'openai/gpt-5.4',
|
|
26
28
|
'gpt-5.4-pro': 'openai/gpt-5.4-pro',
|
|
27
29
|
'gpt-5.3': 'openai/gpt-5.3',
|
|
@@ -107,7 +109,8 @@ export const PICKER_CATEGORIES = [
|
|
|
107
109
|
{ id: 'anthropic/claude-opus-4.7', shortcut: 'opus', label: 'Claude Opus 4.7', price: '$5/$25', highlight: true },
|
|
108
110
|
{ id: 'anthropic/claude-sonnet-4.6', shortcut: 'sonnet', label: 'Claude Sonnet 4.6', price: '$3/$15' },
|
|
109
111
|
{ id: 'anthropic/claude-opus-4.6', shortcut: 'opus-4.6', label: 'Claude Opus 4.6', price: '$5/$25' },
|
|
110
|
-
{ id: 'openai/gpt-5.
|
|
112
|
+
{ id: 'openai/gpt-5.5', shortcut: 'gpt', label: 'GPT-5.5', price: '$5/$30', highlight: true },
|
|
113
|
+
{ id: 'openai/gpt-5.4', shortcut: 'gpt-5.4', label: 'GPT-5.4', price: '$2.5/$15' },
|
|
111
114
|
{ id: 'openai/gpt-5.4-pro', shortcut: 'gpt-5.4-pro', label: 'GPT-5.4 Pro', price: '$30/$180' },
|
|
112
115
|
{ id: 'google/gemini-2.5-pro', shortcut: 'gemini', label: 'Gemini 2.5 Pro', price: '$1.25/$10' },
|
|
113
116
|
{ id: 'google/gemini-3.1-pro', shortcut: 'gemini-3', label: 'Gemini 3.1 Pro', price: '$2/$12' },
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public wallet surface for `@blockrun/franklin/wallet`.
|
|
3
|
+
*
|
|
4
|
+
* Thin pass-through over Franklin's wallet/manager helpers plus the
|
|
5
|
+
* lower-level primitives from `@blockrun/llm` that downstream code
|
|
6
|
+
* typically needs (setup, address, load/save, funding messages, types).
|
|
7
|
+
*
|
|
8
|
+
* Modelled after the flat wallet surface in `@blockrun/clawrouter` —
|
|
9
|
+
* but exposed under a subpath because Franklin's `.` entry is a CLI
|
|
10
|
+
* (side-effectful shebang) and can't double as a library.
|
|
11
|
+
*/
|
|
12
|
+
export { walletExists, setupWallet, setupSolanaWallet, getAddress, } from './manager.js';
|
|
13
|
+
export { getOrCreateWallet, getOrCreateSolanaWallet, setupAgentWallet, setupAgentSolanaWallet, createWallet, createSolanaWallet, } from '@blockrun/llm';
|
|
14
|
+
export { getWalletAddress, scanWallets, scanSolanaWallets, loadWallet, loadSolanaWallet, saveWallet, saveSolanaWallet, } from '@blockrun/llm';
|
|
15
|
+
export { formatWalletCreatedMessage, formatNeedsFundingMessage, formatFundingMessageCompact, getEip681Uri, getPaymentLinks, } from '@blockrun/llm';
|
|
16
|
+
export { WALLET_DIR_PATH, WALLET_FILE_PATH, SOLANA_WALLET_FILE_PATH, } from '@blockrun/llm';
|
|
17
|
+
export type { WalletInfo, SolanaWalletInfo, PaymentLinks, } from '@blockrun/llm';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public wallet surface for `@blockrun/franklin/wallet`.
|
|
3
|
+
*
|
|
4
|
+
* Thin pass-through over Franklin's wallet/manager helpers plus the
|
|
5
|
+
* lower-level primitives from `@blockrun/llm` that downstream code
|
|
6
|
+
* typically needs (setup, address, load/save, funding messages, types).
|
|
7
|
+
*
|
|
8
|
+
* Modelled after the flat wallet surface in `@blockrun/clawrouter` —
|
|
9
|
+
* but exposed under a subpath because Franklin's `.` entry is a CLI
|
|
10
|
+
* (side-effectful shebang) and can't double as a library.
|
|
11
|
+
*/
|
|
12
|
+
export { walletExists, setupWallet, setupSolanaWallet, getAddress, } from './manager.js';
|
|
13
|
+
// ─── Re-exports from @blockrun/llm ────────────────────────────────────────
|
|
14
|
+
// So callers only need one import: `@blockrun/franklin/wallet`.
|
|
15
|
+
// Setup / create
|
|
16
|
+
export { getOrCreateWallet, getOrCreateSolanaWallet, setupAgentWallet, setupAgentSolanaWallet, createWallet, createSolanaWallet, } from '@blockrun/llm';
|
|
17
|
+
// Query / load / save
|
|
18
|
+
export { getWalletAddress, scanWallets, scanSolanaWallets, loadWallet, loadSolanaWallet, saveWallet, saveSolanaWallet, } from '@blockrun/llm';
|
|
19
|
+
// Funding / payment link helpers
|
|
20
|
+
export { formatWalletCreatedMessage, formatNeedsFundingMessage, formatFundingMessageCompact, getEip681Uri, getPaymentLinks, } from '@blockrun/llm';
|
|
21
|
+
// File-system paths (useful for dotfiles / migration scripts)
|
|
22
|
+
export { WALLET_DIR_PATH, WALLET_FILE_PATH, SOLANA_WALLET_FILE_PATH, } from '@blockrun/llm';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/franklin",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.36",
|
|
4
4
|
"description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
"types": "./dist/plugin-sdk/index.d.ts",
|
|
13
13
|
"default": "./dist/plugin-sdk/index.js"
|
|
14
14
|
},
|
|
15
|
+
"./wallet": {
|
|
16
|
+
"types": "./dist/wallet/index.d.ts",
|
|
17
|
+
"default": "./dist/wallet/index.js"
|
|
18
|
+
},
|
|
15
19
|
"./package.json": "./package.json"
|
|
16
20
|
},
|
|
17
21
|
"bin": {
|
|
18
|
-
"franklin": "dist/index.js"
|
|
19
|
-
"runcode": "dist/index.js"
|
|
22
|
+
"franklin": "dist/index.js"
|
|
20
23
|
},
|
|
21
24
|
"files": [
|
|
22
25
|
"dist",
|
|
@@ -47,8 +50,7 @@
|
|
|
47
50
|
"ai-marketing",
|
|
48
51
|
"ai-trading",
|
|
49
52
|
"crypto-native",
|
|
50
|
-
"blockrun"
|
|
51
|
-
"runcode"
|
|
53
|
+
"blockrun"
|
|
52
54
|
],
|
|
53
55
|
"license": "Apache-2.0",
|
|
54
56
|
"repository": {
|