@blockrun/franklin 3.21.0 → 3.21.2
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/dist/panel/html.js +2 -2
- package/dist/tools/phone.js +15 -9
- package/dist/tools/videogen.js +44 -1
- package/dist/tools/voice.js +22 -7
- package/package.json +1 -1
package/dist/panel/html.js
CHANGED
|
@@ -10,7 +10,7 @@ export function getHTML() {
|
|
|
10
10
|
<head>
|
|
11
11
|
<meta charset="utf-8">
|
|
12
12
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
13
|
-
<title>Franklin Panel</title>
|
|
13
|
+
<title>Franklin Agent Panel</title>
|
|
14
14
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='30' y='20' width='55' height='60' rx='14' stroke='white' stroke-width='8' fill='none'/%3E%3Cpath d='M15 35 L25 35' stroke='white' stroke-width='6' stroke-linecap='round'/%3E%3Cpath d='M10 50 L25 50' stroke='white' stroke-width='6' stroke-linecap='round'/%3E%3Cpath d='M15 65 L25 65' stroke='white' stroke-width='6' stroke-linecap='round'/%3E%3C/svg%3E">
|
|
15
15
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
16
16
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -489,7 +489,7 @@ a:hover { text-decoration:underline; }
|
|
|
489
489
|
<div class="sidebar-header">
|
|
490
490
|
<div class="sidebar-brand">
|
|
491
491
|
<div class="icon"><img src="/assets/franklin-portrait.jpg" alt="F"></div>
|
|
492
|
-
<h1>Franklin</h1>
|
|
492
|
+
<h1>Franklin Agent</h1>
|
|
493
493
|
</div>
|
|
494
494
|
<div class="sidebar-sub">by <span style="color:var(--success)">BlockRun.ai</span></div>
|
|
495
495
|
<div class="sidebar-status">
|
package/dist/tools/phone.js
CHANGED
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
15
15
|
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
16
16
|
import { logger } from '../logger.js';
|
|
17
|
+
import { recordUsage } from '../stats/tracker.js';
|
|
17
18
|
const PHONE_TIMEOUT_MS = 30_000;
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
async function postWithPayment(path, body, ctx, meta) {
|
|
20
|
+
const startMs = Date.now();
|
|
20
21
|
const chain = loadChain();
|
|
21
22
|
const apiUrl = API_URLS[chain];
|
|
22
23
|
const endpoint = `${apiUrl}${path}`;
|
|
@@ -51,7 +52,12 @@ async function postWithPayment(path, body, ctx) {
|
|
|
51
52
|
const errText = await response.text().catch(() => '');
|
|
52
53
|
throw new Error(`Phone ${path} failed (${response.status}): ${errText.slice(0, 300)}`);
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
const data = (await response.json());
|
|
56
|
+
try {
|
|
57
|
+
recordUsage(meta.tool, 0, 0, meta.priceUsd, Date.now() - startMs);
|
|
58
|
+
}
|
|
59
|
+
catch { /* telemetry best-effort */ }
|
|
60
|
+
return data;
|
|
55
61
|
}
|
|
56
62
|
finally {
|
|
57
63
|
clearTimeout(timeout);
|
|
@@ -117,7 +123,7 @@ export const listPhoneNumbersCapability = {
|
|
|
117
123
|
},
|
|
118
124
|
execute: async (_input, ctx) => {
|
|
119
125
|
try {
|
|
120
|
-
const res = await postWithPayment('/v1/phone/numbers/list', {}, ctx);
|
|
126
|
+
const res = await postWithPayment('/v1/phone/numbers/list', {}, ctx, { tool: 'ListPhoneNumbers', priceUsd: 0.001 });
|
|
121
127
|
return {
|
|
122
128
|
output: `## Phone numbers (wallet-owned)\n\n` +
|
|
123
129
|
'```json\n' + JSON.stringify(res, null, 2) + '\n```',
|
|
@@ -150,7 +156,7 @@ export const buyPhoneNumberCapability = {
|
|
|
150
156
|
if (typeof input.area_code === 'string')
|
|
151
157
|
body.areaCode = input.area_code;
|
|
152
158
|
try {
|
|
153
|
-
const res = await postWithPayment('/v1/phone/numbers/buy', body, ctx);
|
|
159
|
+
const res = await postWithPayment('/v1/phone/numbers/buy', body, ctx, { tool: 'BuyPhoneNumber', priceUsd: 5.0 });
|
|
154
160
|
return {
|
|
155
161
|
output: `## Number provisioned ($5 USDC charged)\n\n` +
|
|
156
162
|
'```json\n' + JSON.stringify(res, null, 2) + '\n```',
|
|
@@ -180,7 +186,7 @@ export const renewPhoneNumberCapability = {
|
|
|
180
186
|
return { output: 'phone_number (E.164) required', isError: true };
|
|
181
187
|
}
|
|
182
188
|
try {
|
|
183
|
-
const res = await postWithPayment('/v1/phone/numbers/renew', { phoneNumber: input.phone_number }, ctx);
|
|
189
|
+
const res = await postWithPayment('/v1/phone/numbers/renew', { phoneNumber: input.phone_number }, ctx, { tool: 'RenewPhoneNumber', priceUsd: 5.0 });
|
|
184
190
|
return {
|
|
185
191
|
output: `## Lease renewed (+30 days, $5 USDC charged)\n\n` +
|
|
186
192
|
'```json\n' + JSON.stringify(res, null, 2) + '\n```',
|
|
@@ -210,7 +216,7 @@ export const releasePhoneNumberCapability = {
|
|
|
210
216
|
return { output: 'phone_number (E.164) required', isError: true };
|
|
211
217
|
}
|
|
212
218
|
try {
|
|
213
|
-
const res = await postWithPayment('/v1/phone/numbers/release', { phoneNumber: input.phone_number }, ctx);
|
|
219
|
+
const res = await postWithPayment('/v1/phone/numbers/release', { phoneNumber: input.phone_number }, ctx, { tool: 'ReleasePhoneNumber', priceUsd: 0 });
|
|
214
220
|
return {
|
|
215
221
|
output: `## Number released (free)\n\n` +
|
|
216
222
|
'```json\n' + JSON.stringify(res, null, 2) + '\n```',
|
|
@@ -241,7 +247,7 @@ export const phoneLookupCapability = {
|
|
|
241
247
|
return { output: 'phone_number (E.164) required', isError: true };
|
|
242
248
|
}
|
|
243
249
|
try {
|
|
244
|
-
const res = await postWithPayment('/v1/phone/lookup', { phoneNumber: input.phone_number }, ctx);
|
|
250
|
+
const res = await postWithPayment('/v1/phone/lookup', { phoneNumber: input.phone_number }, ctx, { tool: 'PhoneLookup', priceUsd: 0.01 });
|
|
245
251
|
return {
|
|
246
252
|
output: `## Phone lookup ($0.01 USDC charged)\n\n` +
|
|
247
253
|
'```json\n' + JSON.stringify(res, null, 2) + '\n```',
|
|
@@ -271,7 +277,7 @@ export const phoneFraudCheckCapability = {
|
|
|
271
277
|
return { output: 'phone_number (E.164) required', isError: true };
|
|
272
278
|
}
|
|
273
279
|
try {
|
|
274
|
-
const res = await postWithPayment('/v1/phone/lookup/fraud', { phoneNumber: input.phone_number }, ctx);
|
|
280
|
+
const res = await postWithPayment('/v1/phone/lookup/fraud', { phoneNumber: input.phone_number }, ctx, { tool: 'PhoneFraudCheck', priceUsd: 0.05 });
|
|
275
281
|
return {
|
|
276
282
|
output: `## Fraud check ($0.05 USDC charged)\n\n` +
|
|
277
283
|
'```json\n' + JSON.stringify(res, null, 2) + '\n```',
|
package/dist/tools/videogen.js
CHANGED
|
@@ -28,6 +28,13 @@ import { ModelClient } from '../agent/llm.js';
|
|
|
28
28
|
import { analyzeMediaRequest, renderProposalForAskUser } from '../agent/media-router.js';
|
|
29
29
|
import { recordUsage } from '../stats/tracker.js';
|
|
30
30
|
import { findModel, estimateCostUsd } from '../gateway-models.js';
|
|
31
|
+
// BytePlus RealFace asset IDs from token360 Asset UI (after H5 verification).
|
|
32
|
+
// Format: `ta_` + alphanumeric.
|
|
33
|
+
const REAL_FACE_ASSET_ID_REGEX = /^ta_[A-Za-z0-9]+$/;
|
|
34
|
+
const REAL_FACE_MODELS = new Set([
|
|
35
|
+
'bytedance/seedance-2.0',
|
|
36
|
+
'bytedance/seedance-2.0-fast',
|
|
37
|
+
]);
|
|
31
38
|
const DEFAULT_MODEL = 'xai/grok-imagine-video';
|
|
32
39
|
const DEFAULT_DURATION = 8;
|
|
33
40
|
const PRICE_PER_SECOND_USD = 0.05;
|
|
@@ -44,9 +51,33 @@ function estimateVideoCostUsd(durationSeconds = DEFAULT_DURATION) {
|
|
|
44
51
|
function buildExecute(deps) {
|
|
45
52
|
return async function execute(input, ctx) {
|
|
46
53
|
const rawInput = input;
|
|
47
|
-
const { output_path, model, image_url, duration_seconds, contentId, aspect_ratio } = rawInput;
|
|
54
|
+
const { output_path, model, image_url, duration_seconds, contentId, aspect_ratio, real_face_asset_id } = rawInput;
|
|
48
55
|
if (!rawInput.prompt)
|
|
49
56
|
return { output: 'Error: prompt is required', isError: true };
|
|
57
|
+
// RealFace asset client-side validations (the gateway 400s on the same
|
|
58
|
+
// conditions but a local check is friendlier — and the rejected request
|
|
59
|
+
// doesn't burn an x402 round-trip).
|
|
60
|
+
if (real_face_asset_id !== undefined) {
|
|
61
|
+
if (typeof real_face_asset_id !== 'string' || !REAL_FACE_ASSET_ID_REGEX.test(real_face_asset_id)) {
|
|
62
|
+
return {
|
|
63
|
+
output: `Error: real_face_asset_id must match "ta_<alphanumeric>" (e.g. ta_abc123). Got: ${JSON.stringify(real_face_asset_id)}`,
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const chosenModel = model || DEFAULT_MODEL;
|
|
68
|
+
if (!REAL_FACE_MODELS.has(chosenModel)) {
|
|
69
|
+
return {
|
|
70
|
+
output: `Error: real_face_asset_id is only supported on Seedance 2.0 variants (${[...REAL_FACE_MODELS].join(', ')}). Current model: ${chosenModel}.`,
|
|
71
|
+
isError: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (image_url) {
|
|
75
|
+
return {
|
|
76
|
+
output: 'Error: real_face_asset_id and image_url both seed the first frame — pick one. Drop image_url to use RealFace, or drop real_face_asset_id to use the image.',
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
50
81
|
// Resolve image_url before sending. The gateway requires a URL (http(s)
|
|
51
82
|
// or data: URI), but agents naturally pass a local file path —
|
|
52
83
|
// verified 2026-05-04 in a live session: agent passed
|
|
@@ -162,6 +193,10 @@ function buildExecute(deps) {
|
|
|
162
193
|
// value, the 400 body surfaces via 3.15.45 diagnostic so the agent
|
|
163
194
|
// can drop the param and retry.
|
|
164
195
|
...(aspect_ratio ? { aspect_ratio } : {}),
|
|
196
|
+
// RealFace (BytePlus, Seedance 2.0 only) — seeds the first frame from
|
|
197
|
+
// a real-person asset for cross-frame character consistency. Client
|
|
198
|
+
// already validated the ID + model gate above; just pass through.
|
|
199
|
+
...(real_face_asset_id ? { real_face_asset_id } : {}),
|
|
165
200
|
});
|
|
166
201
|
const headers = {
|
|
167
202
|
'Content-Type': 'application/json',
|
|
@@ -502,6 +537,14 @@ export function createVideoGenCapability(deps = {}) {
|
|
|
502
537
|
'error body surfaces — drop the param and retry.',
|
|
503
538
|
},
|
|
504
539
|
contentId: { type: 'string', description: 'Optional Content id to attach and budget against.' },
|
|
540
|
+
real_face_asset_id: {
|
|
541
|
+
type: 'string',
|
|
542
|
+
description: 'Optional BytePlus RealFace asset id (format `ta_<alphanumeric>`) for cross-frame ' +
|
|
543
|
+
'character consistency. Users get asset IDs from token360\'s Asset UI after H5 ' +
|
|
544
|
+
'verification. Seedance 2.0 variants only (bytedance/seedance-2.0, ' +
|
|
545
|
+
'bytedance/seedance-2.0-fast). Mutually exclusive with image_url — both seed the ' +
|
|
546
|
+
'first frame; pick one.',
|
|
547
|
+
},
|
|
505
548
|
},
|
|
506
549
|
required: ['prompt'],
|
|
507
550
|
},
|
package/dist/tools/voice.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
18
18
|
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
19
19
|
import { logger } from '../logger.js';
|
|
20
|
+
import { recordUsage } from '../stats/tracker.js';
|
|
20
21
|
import { CallLog } from '../phone/call-log.js';
|
|
21
22
|
/** Singleton, lazy — paths are computed at first use so tests can stub homedir. */
|
|
22
23
|
let _callLog = null;
|
|
@@ -52,8 +53,8 @@ function normalizeStatus(raw) {
|
|
|
52
53
|
return 'queued';
|
|
53
54
|
}
|
|
54
55
|
const VOICE_TIMEOUT_MS = 30_000;
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
async function postWithPayment(path, body, ctx, meta) {
|
|
57
|
+
const startMs = Date.now();
|
|
57
58
|
const chain = loadChain();
|
|
58
59
|
const apiUrl = API_URLS[chain];
|
|
59
60
|
const endpoint = `${apiUrl}${path}`;
|
|
@@ -88,14 +89,22 @@ async function postWithPayment(path, body, ctx) {
|
|
|
88
89
|
const errText = await response.text().catch(() => '');
|
|
89
90
|
throw new Error(`Voice ${path} failed (${response.status}): ${errText.slice(0, 400)}`);
|
|
90
91
|
}
|
|
91
|
-
|
|
92
|
+
const data = (await response.json());
|
|
93
|
+
// Telemetry — record the cost so the status bar's per-turn delta reflects
|
|
94
|
+
// real x402 spend (not just LLM cost). Best-effort; never block on failure.
|
|
95
|
+
try {
|
|
96
|
+
recordUsage(meta.tool, 0, 0, meta.priceUsd, Date.now() - startMs);
|
|
97
|
+
}
|
|
98
|
+
catch { /* telemetry best-effort */ }
|
|
99
|
+
return data;
|
|
92
100
|
}
|
|
93
101
|
finally {
|
|
94
102
|
clearTimeout(timeout);
|
|
95
103
|
ctx.abortSignal.removeEventListener('abort', onAbort);
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
|
-
async function getNoPayment(path, ctx) {
|
|
106
|
+
async function getNoPayment(path, ctx, meta) {
|
|
107
|
+
const startMs = Date.now();
|
|
99
108
|
const chain = loadChain();
|
|
100
109
|
const apiUrl = API_URLS[chain];
|
|
101
110
|
const endpoint = `${apiUrl}${path}`;
|
|
@@ -113,7 +122,13 @@ async function getNoPayment(path, ctx) {
|
|
|
113
122
|
const errText = await resp.text().catch(() => '');
|
|
114
123
|
throw new Error(`Voice ${path} failed (${resp.status}): ${errText.slice(0, 300)}`);
|
|
115
124
|
}
|
|
116
|
-
|
|
125
|
+
const data = (await resp.json());
|
|
126
|
+
// Record even free calls so the audit tab shows the activity (cost 0).
|
|
127
|
+
try {
|
|
128
|
+
recordUsage(meta.tool, 0, 0, meta.priceUsd, Date.now() - startMs);
|
|
129
|
+
}
|
|
130
|
+
catch { /* telemetry best-effort */ }
|
|
131
|
+
return data;
|
|
117
132
|
}
|
|
118
133
|
finally {
|
|
119
134
|
clearTimeout(timeout);
|
|
@@ -255,7 +270,7 @@ export const voiceCallCapability = {
|
|
|
255
270
|
if (typeof input.wait_for_greeting === 'boolean')
|
|
256
271
|
body.wait_for_greeting = input.wait_for_greeting;
|
|
257
272
|
try {
|
|
258
|
-
const res = await postWithPayment('/v1/voice/call', body, ctx);
|
|
273
|
+
const res = await postWithPayment('/v1/voice/call', body, ctx, { tool: 'VoiceCall', priceUsd: 0.54 });
|
|
259
274
|
const callId = (res.call_id || res.id);
|
|
260
275
|
// Persist a "queued" row so the panel sees the call before VoiceStatus polls.
|
|
261
276
|
// Best-effort — if disk write fails we still surface the call_id to the agent.
|
|
@@ -314,7 +329,7 @@ export const voiceStatusCapability = {
|
|
|
314
329
|
return { output: 'call_id required', isError: true };
|
|
315
330
|
}
|
|
316
331
|
try {
|
|
317
|
-
const res = await getNoPayment(`/v1/voice/call/${encodeURIComponent(input.call_id)}`, ctx);
|
|
332
|
+
const res = await getNoPayment(`/v1/voice/call/${encodeURIComponent(input.call_id)}`, ctx, { tool: 'VoiceStatus', priceUsd: 0 });
|
|
318
333
|
// Patch the local journal with whatever fields the gateway returned —
|
|
319
334
|
// transcript / recording / duration / disposition. Append-only schema
|
|
320
335
|
// means we just write a new row; CallLog.summary() picks the latest.
|
package/package.json
CHANGED