@blockrun/franklin 3.8.35 → 3.8.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/commands.js +1 -1
  3. package/dist/agent/compact.js +1 -1
  4. package/dist/agent/evaluator.d.ts +3 -1
  5. package/dist/agent/evaluator.js +44 -8
  6. package/dist/agent/llm.js +2 -2
  7. package/dist/agent/loop.js +19 -0
  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 +148 -26
  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 +154 -22
  31. package/dist/tools/read.js +29 -2
  32. package/dist/tools/videogen.d.ts +14 -3
  33. package/dist/tools/videogen.js +161 -28
  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 type { CapabilityHandler } from '../agent/types.js';
11
22
  import type { ContentLibrary } from '../content/library.js';
@@ -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,12 +24,17 @@ 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;
@@ -121,26 +137,32 @@ function buildExecute(deps) {
121
137
  'Content-Type': 'application/json',
122
138
  'User-Agent': `franklin/${VERSION}`,
123
139
  };
124
- const controller = new AbortController();
125
- const timeout = setTimeout(() => controller.abort(), GEN_TIMEOUT_MS);
126
- // Abort on user cancel too
127
- const onAbort = () => controller.abort();
128
- 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;
129
151
  try {
130
152
  let response = await fetch(endpoint, {
131
153
  method: 'POST',
132
- signal: controller.signal,
154
+ signal: submitCtrl.signal,
133
155
  headers,
134
156
  body,
135
157
  });
136
158
  if (response.status === 402) {
137
- const paymentHeaders = await signPayment(response, chain, endpoint);
159
+ paymentHeaders = await signPayment(response, chain, endpoint);
138
160
  if (!paymentHeaders) {
139
161
  return { output: 'Payment failed. Check wallet balance with: franklin balance', isError: true };
140
162
  }
141
163
  response = await fetch(endpoint, {
142
164
  method: 'POST',
143
- signal: controller.signal,
165
+ signal: submitCtrl.signal,
144
166
  headers: { ...headers, ...paymentHeaders },
145
167
  body,
146
168
  });
@@ -148,22 +170,71 @@ function buildExecute(deps) {
148
170
  if (!response.ok) {
149
171
  const errText = await response.text().catch(() => '');
150
172
  return {
151
- output: `Video generation failed (${response.status}): ${errText.slice(0, 300)}`,
173
+ output: `Video submit failed (${response.status}): ${errText.slice(0, 300)}`,
152
174
  isError: true,
153
175
  };
154
176
  }
155
- const result = (await response.json());
156
- const videoData = result.data?.[0];
157
- if (!videoData?.url) {
158
- 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
+ };
159
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 {
160
223
  // Download the MP4
161
224
  const dlCtrl = new AbortController();
162
225
  const dlTimeout = setTimeout(() => dlCtrl.abort(), DOWNLOAD_TIMEOUT_MS);
163
- const vidResp = await fetch(videoData.url, { signal: dlCtrl.signal });
164
- 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
+ }
165
236
  if (!vidResp.ok) {
166
- 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 };
167
238
  }
168
239
  const buffer = Buffer.from(await vidResp.arrayBuffer());
169
240
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
@@ -171,6 +242,21 @@ function buildExecute(deps) {
171
242
  const fileSize = fs.statSync(outPath).size;
172
243
  const sizeMB = (fileSize / 1_048_576).toFixed(1);
173
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
+ })();
174
260
  let contentSummary = '';
175
261
  if (contentId && deps.library) {
176
262
  const rec = deps.library.addAsset(contentId, {
@@ -208,18 +294,63 @@ function buildExecute(deps) {
208
294
  const msg = err.message || '';
209
295
  if (msg.includes('abort')) {
210
296
  return {
211
- 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.`,
212
298
  isError: true,
213
299
  };
214
300
  }
215
301
  return { output: `Error: ${msg}`, isError: true };
216
302
  }
217
- finally {
218
- clearTimeout(timeout);
219
- ctx.abortSignal.removeEventListener('abort', onAbort);
220
- }
221
303
  };
222
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
+ }
223
354
  // ─── Payment ───────────────────────────────────────────────────────────────
224
355
  async function signPayment(response, chain, endpoint) {
225
356
  try {
@@ -235,7 +366,9 @@ async function signPayment(response, chain, endpoint) {
235
366
  const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
236
367
  resourceUrl: details.resource?.url || endpoint,
237
368
  resourceDescription: details.resource?.description || 'Franklin video generation',
238
- 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,
239
372
  extra: details.extra,
240
373
  });
241
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.35",
3
+ "version": "3.8.37",
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": {