@blockrun/franklin 3.21.1 → 3.21.3

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.
@@ -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">
@@ -1990,6 +1990,22 @@ function formatDuration(sec) {
1990
1990
  return m > 0 ? m + 'm ' + s + 's' : s + 's';
1991
1991
  }
1992
1992
 
1993
+ function escapeHtml(value) {
1994
+ return String(value == null ? '' : value).replace(/[&<>"']/g, ch => ({
1995
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
1996
+ }[ch]));
1997
+ }
1998
+
1999
+ function safeHttpUrl(value) {
2000
+ if (typeof value !== 'string') return '';
2001
+ try {
2002
+ const u = new URL(value);
2003
+ return (u.protocol === 'http:' || u.protocol === 'https:') ? u.href : '';
2004
+ } catch (e) {
2005
+ return '';
2006
+ }
2007
+ }
2008
+
1993
2009
  function renderCallsList(calls) {
1994
2010
  const list = document.getElementById('calls-list');
1995
2011
  if (!list) return;
@@ -2007,13 +2023,14 @@ function renderCallsList(calls) {
2007
2023
  const fromHuman = formatPhoneNumber(c.from);
2008
2024
  const when = new Date(c.timestamp).toLocaleString();
2009
2025
  const cost = c.paid_usd ? '$' + c.paid_usd.toFixed(2) : '—';
2010
- const safeTask = (c.task || '').replace(/[<>&]/g, ch => ({'<':'&lt;','>':'&gt;','&':'&amp;'}[ch]));
2026
+ const safeTask = escapeHtml(c.task || '');
2027
+ const recordingUrl = safeHttpUrl(c.recording_url);
2011
2028
  const transcriptHtml = c.transcript
2012
2029
  ? '<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--text-dim);font-size:12px">Transcript</summary><pre style="white-space:pre-wrap;background:oklch(0 0 0 / 25%);padding:10px;border-radius:8px;font-size:12px;margin-top:6px;max-height:400px;overflow:auto">' +
2013
- c.transcript.replace(/[<>&]/g, ch => ({'<':'&lt;','>':'&gt;','&':'&amp;'}[ch])) + '</pre></details>'
2030
+ escapeHtml(c.transcript) + '</pre></details>'
2014
2031
  : '';
2015
- const recordingHtml = c.recording_url
2016
- ? '<a href="' + c.recording_url + '" target="_blank" rel="noopener" style="font-size:11px;color:var(--brand);text-decoration:none;margin-left:8px">▶ recording</a>'
2032
+ const recordingHtml = recordingUrl
2033
+ ? '<a href="' + escapeHtml(recordingUrl) + '" target="_blank" rel="noopener" style="font-size:11px;color:var(--brand);text-decoration:none;margin-left:8px">▶ recording</a>'
2017
2034
  : '';
2018
2035
  return ''
2019
2036
  + '<div class="phone-row" style="grid-template-columns:auto 1fr auto;align-items:start">'
@@ -2023,12 +2040,12 @@ function renderCallsList(calls) {
2023
2040
  + ' </svg>'
2024
2041
  + ' </div>'
2025
2042
  + ' <div class="phone-main">'
2026
- + ' <div class="phone-num">' + human + ' <span style="font-size:11px;color:var(--text-dim);font-weight:400">from ' + fromHuman + '</span></div>'
2043
+ + ' <div class="phone-num">' + escapeHtml(human) + ' <span style="font-size:11px;color:var(--text-dim);font-weight:400">from ' + escapeHtml(fromHuman) + '</span></div>'
2027
2044
  + ' <div class="phone-meta">'
2028
- + ' <span class="chip ' + st.cls + '">' + st.label + '</span>'
2045
+ + ' <span class="chip ' + st.cls + '">' + escapeHtml(st.label) + '</span>'
2029
2046
  + ' <span class="chip">' + formatDuration(c.duration_sec) + '</span>'
2030
2047
  + ' <span class="chip">' + cost + '</span>'
2031
- + ' <span style="font-size:11px;color:var(--text-dim)">' + when + '</span>'
2048
+ + ' <span style="font-size:11px;color:var(--text-dim)">' + escapeHtml(when) + '</span>'
2032
2049
  + recordingHtml
2033
2050
  + ' </div>'
2034
2051
  + ' <div style="font-size:12px;color:var(--text-muted);margin-top:4px;line-height:1.5">' + (safeTask.slice(0, 200) + (safeTask.length > 200 ? '…' : '')) + '</div>'
@@ -2074,6 +2091,7 @@ document.querySelector('[data-tab="markets"]')?.addEventListener('click', loadMa
2074
2091
  const initialHash = (location.hash || '').replace(/^#/, '');
2075
2092
  if (initialHash && initialHash !== 'overview' && document.getElementById('tab-' + initialHash)) {
2076
2093
  activateTab(initialHash);
2094
+ if (initialHash === 'calls') loadCalls();
2077
2095
  }
2078
2096
  }
2079
2097
 
@@ -100,24 +100,30 @@ export class CallLog {
100
100
  summary(limit = 50) {
101
101
  const all = this.all();
102
102
  const latest = new Map();
103
+ const paidByCall = new Map();
103
104
  for (const e of all) {
105
+ paidByCall.set(e.call_id, Math.max(paidByCall.get(e.call_id) ?? 0, e.paid_usd));
104
106
  const cur = latest.get(e.call_id);
105
107
  // Keep the row with the FRESHEST timestamp per call_id (status updates).
106
108
  if (!cur || e.timestamp >= cur.timestamp)
107
109
  latest.set(e.call_id, e);
108
110
  }
109
111
  // Sort newest-first by the latest-row timestamp.
110
- const list = Array.from(latest.values()).sort((a, b) => b.timestamp - a.timestamp);
112
+ const list = Array.from(latest.values())
113
+ .map(e => ({ ...e, paid_usd: paidByCall.get(e.call_id) ?? e.paid_usd }))
114
+ .sort((a, b) => b.timestamp - a.timestamp);
111
115
  return list.slice(0, limit);
112
116
  }
113
117
  byCallId(callId) {
114
118
  let best = null;
119
+ let paidUsd = 0;
115
120
  for (const e of this.all()) {
116
121
  if (e.call_id !== callId)
117
122
  continue;
123
+ paidUsd = Math.max(paidUsd, e.paid_usd);
118
124
  if (!best || e.timestamp >= best.timestamp)
119
125
  best = e;
120
126
  }
121
- return best;
127
+ return best ? { ...best, paid_usd: paidUsd } : null;
122
128
  }
123
129
  }
@@ -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
  },
@@ -346,7 +346,7 @@ export const voiceStatusCapability = {
346
346
  callLog().append({
347
347
  ...prior,
348
348
  timestamp: Date.now(),
349
- paid_usd: 0, // status polls are free; only the initial POST charges
349
+ paid_usd: prior.paid_usd, // status polls are free; preserve the per-call total
350
350
  status: normalizeStatus(res.status ?? res.queue_status ?? res.disposition),
351
351
  duration_sec: duration ?? prior.duration_sec,
352
352
  transcript: transcript ?? prior.transcript,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.21.1",
3
+ "version": "3.21.3",
4
4
  "description": "Franklin Agent — 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": {