@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.
Files changed (40) hide show
  1. package/README.md +4 -2
  2. package/dist/agent/commands.js +1 -1
  3. package/dist/agent/compact.js +1 -1
  4. package/dist/agent/context.js +1 -1
  5. package/dist/agent/loop.js +19 -0
  6. package/dist/agent/media-router.d.ts +33 -0
  7. package/dist/agent/media-router.js +87 -9
  8. package/dist/agent/optimize.js +1 -0
  9. package/dist/agent/permissions.js +10 -1
  10. package/dist/agent/tokens.js +4 -0
  11. package/dist/agent/types.d.ts +22 -1
  12. package/dist/commands/balance.js +1 -1
  13. package/dist/commands/daemon.js +23 -16
  14. package/dist/commands/plugin.d.ts +1 -1
  15. package/dist/commands/plugin.js +10 -10
  16. package/dist/commands/stats.d.ts +1 -1
  17. package/dist/commands/stats.js +2 -2
  18. package/dist/index.js +2 -2
  19. package/dist/panel/server.js +7 -6
  20. package/dist/plugin-sdk/index.d.ts +2 -2
  21. package/dist/plugin-sdk/index.js +2 -2
  22. package/dist/plugin-sdk/plugin.d.ts +4 -4
  23. package/dist/plugins/registry.d.ts +3 -3
  24. package/dist/plugins/registry.js +6 -6
  25. package/dist/pricing.js +1 -0
  26. package/dist/proxy/server.js +5 -3
  27. package/dist/router/index.js +3 -3
  28. package/dist/session/storage.js +2 -2
  29. package/dist/tools/imagegen.d.ts +14 -0
  30. package/dist/tools/imagegen.js +175 -24
  31. package/dist/tools/read.js +29 -2
  32. package/dist/tools/videogen.d.ts +14 -3
  33. package/dist/tools/videogen.js +181 -31
  34. package/dist/tools/webhook.js +2 -1
  35. package/dist/trading/providers/coingecko/client.js +2 -1
  36. package/dist/ui/app.js +12 -12
  37. package/dist/ui/model-picker.js +7 -4
  38. package/dist/wallet/index.d.ts +17 -0
  39. package/dist/wallet/index.js +22 -0
  40. package/package.json +7 -5
@@ -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
- * The endpoint is synchronous-over-polling: the HTTP connection stays open
7
- * until the upstream xAI job finishes (typically 20–60s, timeout 180s), so
8
- * the caller only needs to issue a single POST.
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
- // Long ceiling the endpoint synchronously waits for xAI's async job (up to
20
- // ~180s). Give ourselves a bit of headroom for the GCS backup + settle step.
21
- const GEN_TIMEOUT_MS = 210_000;
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 { prompt, output_path, model, image_url, duration_seconds, contentId } = input;
29
- if (!prompt)
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 controller = new AbortController();
108
- const timeout = setTimeout(() => controller.abort(), GEN_TIMEOUT_MS);
109
- // Abort on user cancel too
110
- const onAbort = () => controller.abort();
111
- ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
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: controller.signal,
154
+ signal: submitCtrl.signal,
116
155
  headers,
117
156
  body,
118
157
  });
119
158
  if (response.status === 402) {
120
- const paymentHeaders = await signPayment(response, chain, endpoint);
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: controller.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 generation failed (${response.status}): ${errText.slice(0, 300)}`,
173
+ output: `Video submit failed (${response.status}): ${errText.slice(0, 300)}`,
135
174
  isError: true,
136
175
  };
137
176
  }
138
- const result = (await response.json());
139
- const videoData = result.data?.[0];
140
- if (!videoData?.url) {
141
- return { output: 'No video URL returned from API', isError: true };
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 vidResp = await fetch(videoData.url, { signal: dlCtrl.signal });
147
- clearTimeout(dlTimeout);
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}): ${videoData.url}`, isError: true };
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 generation timed out or was aborted (limit ${Math.round(GEN_TIMEOUT_MS / 1000)}s).`,
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
- maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
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 };
@@ -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': 'franklin/3.8.9 (webhook)',
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 = 'franklin/3.8.9 (trading)';
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.__runcode_ui = {
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.__runcode_submit;
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.__runcode_submit = (msg) => {
716
+ globalThis.__franklin_submit = (msg) => {
717
717
  handleSubmit(msg);
718
718
  };
719
719
  return () => {
720
- delete globalThis.__runcode_ui;
721
- delete globalThis.__runcode_submit;
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: runcode setup', walletBalance: opts.walletBalance || 'unknown', chain: opts.chain || 'base', startWithPicker: opts.showPicker, onSubmit: (value) => {
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.__runcode_ui;
802
+ const ui = globalThis.__franklin_ui;
803
803
  ui?.handleEvent(event);
804
804
  },
805
805
  updateModel: (model) => {
806
- const ui = globalThis.__runcode_ui;
806
+ const ui = globalThis.__franklin_ui;
807
807
  ui?.updateModel(model);
808
808
  },
809
809
  updateBalance: (bal) => {
810
- const ui = globalThis.__runcode_ui;
810
+ const ui = globalThis.__franklin_ui;
811
811
  ui?.updateBalance(bal);
812
812
  },
813
813
  onTurnDone: (cb) => {
814
- const ui = globalThis.__runcode_ui;
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.__runcode_ui;
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.__runcode_ui;
835
+ const ui = globalThis.__franklin_ui;
836
836
  return ui?.requestAskUser(question, options) ?? Promise.resolve('(no response)');
837
837
  },
838
838
  };
@@ -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: 'openai/gpt-5.4',
23
- gpt5: 'openai/gpt-5.4',
24
- 'gpt-5': 'openai/gpt-5.4',
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.4', shortcut: 'gpt', label: 'GPT-5.4', price: '$2.5/$15' },
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.34",
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": {