@blockrun/franklin 3.8.8 → 3.8.10
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/agent/error-classifier.js +1 -0
- package/dist/agent/llm.d.ts +7 -0
- package/dist/agent/llm.js +48 -7
- package/dist/agent/loop.js +66 -3
- package/dist/agent/permissions.js +2 -2
- package/dist/agent/types.d.ts +7 -0
- package/dist/banner.js +15 -0
- package/dist/commands/start.d.ts +4 -0
- package/dist/commands/start.js +72 -2
- package/dist/index.js +11 -3
- package/dist/panel/html.js +111 -21
- package/dist/panel/server.js +15 -4
- package/dist/tools/activate.d.ts +29 -0
- package/dist/tools/activate.js +96 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/tool-categories.d.ts +22 -0
- package/dist/tools/tool-categories.js +44 -0
- package/dist/tools/trading-execute.d.ts +11 -21
- package/dist/tools/trading-execute.js +43 -130
- package/dist/tools/trading-views.d.ts +64 -0
- package/dist/tools/trading-views.js +115 -0
- package/dist/tools/trading.js +86 -7
- package/dist/tools/webhook.d.ts +18 -0
- package/dist/tools/webhook.js +185 -0
- package/dist/trading/data.d.ts +24 -1
- package/dist/trading/data.js +67 -102
- package/dist/trading/providers/blockrun/client.d.ts +48 -0
- package/dist/trading/providers/blockrun/client.js +253 -0
- package/dist/trading/providers/blockrun/price.d.ts +24 -0
- package/dist/trading/providers/blockrun/price.js +110 -0
- package/dist/trading/providers/coingecko/client.d.ts +20 -0
- package/dist/trading/providers/coingecko/client.js +87 -0
- package/dist/trading/providers/coingecko/markets.d.ts +3 -0
- package/dist/trading/providers/coingecko/markets.js +25 -0
- package/dist/trading/providers/coingecko/ohlcv.d.ts +3 -0
- package/dist/trading/providers/coingecko/ohlcv.js +29 -0
- package/dist/trading/providers/coingecko/price.d.ts +11 -0
- package/dist/trading/providers/coingecko/price.js +41 -0
- package/dist/trading/providers/coingecko/trending.d.ts +3 -0
- package/dist/trading/providers/coingecko/trending.js +22 -0
- package/dist/trading/providers/fetcher.d.ts +43 -0
- package/dist/trading/providers/fetcher.js +45 -0
- package/dist/trading/providers/registry.d.ts +45 -0
- package/dist/trading/providers/registry.js +82 -0
- package/dist/trading/providers/standard-models.d.ts +94 -0
- package/dist/trading/providers/standard-models.js +21 -0
- package/dist/trading/providers/telemetry.d.ts +51 -0
- package/dist/trading/providers/telemetry.js +115 -0
- package/dist/ui/app.js +28 -2
- package/package.json +1 -1
package/dist/agent/llm.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export interface LLMClientOptions {
|
|
|
27
27
|
chain: Chain;
|
|
28
28
|
debug?: boolean;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract the most human-readable message from an error body.
|
|
32
|
+
* Some gateways wrap provider errors multiple times, e.g.
|
|
33
|
+
* `{"error":{"message":"{\"error\":{\"message\":\"...\"}}"}}`.
|
|
34
|
+
* Peel those layers so the UI doesn't show raw nested JSON.
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractApiErrorMessage(errorBody: string): string;
|
|
30
37
|
/**
|
|
31
38
|
* Apply Anthropic prompt caching using the `system_and_3` strategy.
|
|
32
39
|
* Pattern from nousresearch/hermes-agent `agent/prompt_caching.py`.
|
package/dist/agent/llm.js
CHANGED
|
@@ -6,6 +6,53 @@
|
|
|
6
6
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
7
7
|
import { USER_AGENT } from '../config.js';
|
|
8
8
|
import { ThinkTagStripper } from './think-tag-stripper.js';
|
|
9
|
+
/**
|
|
10
|
+
* Extract the most human-readable message from an error body.
|
|
11
|
+
* Some gateways wrap provider errors multiple times, e.g.
|
|
12
|
+
* `{"error":{"message":"{\"error\":{\"message\":\"...\"}}"}}`.
|
|
13
|
+
* Peel those layers so the UI doesn't show raw nested JSON.
|
|
14
|
+
*/
|
|
15
|
+
export function extractApiErrorMessage(errorBody) {
|
|
16
|
+
const visited = new Set();
|
|
17
|
+
const walk = (value, depth = 0) => {
|
|
18
|
+
// Some providers wrap the real message under error.message as a JSON
|
|
19
|
+
// string, which adds another object/string hop. Allow a few layers of
|
|
20
|
+
// nesting without risking runaway recursion.
|
|
21
|
+
if (depth > 8 || visited.has(value))
|
|
22
|
+
return null;
|
|
23
|
+
if (value && (typeof value === 'object' || typeof value === 'string')) {
|
|
24
|
+
visited.add(value);
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'string') {
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (trimmed) {
|
|
29
|
+
try {
|
|
30
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
31
|
+
const parsed = JSON.parse(trimmed);
|
|
32
|
+
const nested = walk(parsed, depth + 1);
|
|
33
|
+
if (nested)
|
|
34
|
+
return nested;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch { /* plain string — use as-is below */ }
|
|
38
|
+
}
|
|
39
|
+
return trimmed || null;
|
|
40
|
+
}
|
|
41
|
+
if (!value || typeof value !== 'object')
|
|
42
|
+
return null;
|
|
43
|
+
const obj = value;
|
|
44
|
+
for (const key of ['error', 'message', 'detail', 'reason']) {
|
|
45
|
+
if (key in obj) {
|
|
46
|
+
const nested = walk(obj[key], depth + 1);
|
|
47
|
+
if (nested)
|
|
48
|
+
return nested;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
const extracted = walk(errorBody) ?? errorBody;
|
|
54
|
+
return extracted.replace(/\s+/g, ' ').trim();
|
|
55
|
+
}
|
|
9
56
|
// ─── Anthropic Prompt Caching ─────────────────────────────────────────────
|
|
10
57
|
/**
|
|
11
58
|
* Apply Anthropic prompt caching using the `system_and_3` strategy.
|
|
@@ -265,13 +312,7 @@ export class ModelClient {
|
|
|
265
312
|
}
|
|
266
313
|
if (!response.ok) {
|
|
267
314
|
const errorBody = await response.text().catch(() => 'unknown error');
|
|
268
|
-
|
|
269
|
-
let message = errorBody;
|
|
270
|
-
try {
|
|
271
|
-
const parsed = JSON.parse(errorBody);
|
|
272
|
-
message = parsed?.error?.message || parsed?.message || errorBody;
|
|
273
|
-
}
|
|
274
|
-
catch { /* not JSON — use raw text */ }
|
|
315
|
+
const message = extractApiErrorMessage(errorBody);
|
|
275
316
|
yield {
|
|
276
317
|
kind: 'error',
|
|
277
318
|
payload: { status: response.status, message },
|
package/dist/agent/loop.js
CHANGED
|
@@ -13,6 +13,8 @@ import { optimizeHistory, CAPPED_MAX_TOKENS, ESCALATED_MAX_TOKENS, getMaxOutputT
|
|
|
13
13
|
import { classifyAgentError } from './error-classifier.js';
|
|
14
14
|
import { SessionToolGuard } from './tool-guard.js';
|
|
15
15
|
import { resetToolSessionState } from '../tools/index.js';
|
|
16
|
+
import { CORE_TOOL_NAMES, dynamicToolsEnabled } from '../tools/tool-categories.js';
|
|
17
|
+
import { createActivateToolCapability } from '../tools/activate.js';
|
|
16
18
|
import { recordUsage } from '../stats/tracker.js';
|
|
17
19
|
import { recordSessionUsage } from '../stats/session-tracker.js';
|
|
18
20
|
import { appendAudit, extractLastUserPrompt } from '../stats/audit.js';
|
|
@@ -317,11 +319,36 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
317
319
|
chain: config.chain,
|
|
318
320
|
debug: config.debug,
|
|
319
321
|
});
|
|
322
|
+
// ── Dynamic tool visibility ──
|
|
323
|
+
// Register ActivateTool before building the capability map so the agent
|
|
324
|
+
// can always reach the meta-tool. When FRANKLIN_DYNAMIC_TOOLS=0 is set,
|
|
325
|
+
// `activeTools` is seeded with every registered name — behaves as the
|
|
326
|
+
// pre-3.8.9 static registry.
|
|
320
327
|
const capabilityMap = new Map();
|
|
321
328
|
for (const cap of config.capabilities) {
|
|
322
329
|
capabilityMap.set(cap.spec.name, cap);
|
|
323
330
|
}
|
|
324
|
-
const
|
|
331
|
+
const activeTools = new Set();
|
|
332
|
+
const dynamicTools = dynamicToolsEnabled();
|
|
333
|
+
if (dynamicTools) {
|
|
334
|
+
for (const name of CORE_TOOL_NAMES) {
|
|
335
|
+
if (capabilityMap.has(name))
|
|
336
|
+
activeTools.add(name);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
for (const cap of config.capabilities)
|
|
341
|
+
activeTools.add(cap.spec.name);
|
|
342
|
+
}
|
|
343
|
+
const activateToolCap = createActivateToolCapability({ activeTools, allTools: capabilityMap });
|
|
344
|
+
capabilityMap.set(activateToolCap.spec.name, activateToolCap);
|
|
345
|
+
if (dynamicTools)
|
|
346
|
+
activeTools.add(activateToolCap.spec.name);
|
|
347
|
+
const allToolDefs = [...capabilityMap.values()].map(c => c.spec);
|
|
348
|
+
const buildCallToolDefs = () => dynamicTools ? allToolDefs.filter(t => activeTools.has(t.name)) : allToolDefs;
|
|
349
|
+
const buildActiveCapabilityMap = () => dynamicTools
|
|
350
|
+
? new Map([...capabilityMap.entries()].filter(([name]) => activeTools.has(name)))
|
|
351
|
+
: capabilityMap;
|
|
325
352
|
const maxTurns = config.maxTurns ?? 15;
|
|
326
353
|
const workDir = config.workingDir ?? process.cwd();
|
|
327
354
|
const permissions = new PermissionManager(config.permissionMode ?? 'default', config.permissionPromptFn);
|
|
@@ -576,6 +603,20 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
576
603
|
'4. Think step by step — show your reasoning explicitly when it adds value\n' +
|
|
577
604
|
'Prioritize correctness and thoroughness over speed.');
|
|
578
605
|
}
|
|
606
|
+
// ── Dynamic tool visibility hint ──
|
|
607
|
+
// When the core/on-demand split is active, tell every model up front
|
|
608
|
+
// that its tool list is intentionally small and that extras can be
|
|
609
|
+
// pulled via ActivateTool. Kept byte-stable across turns (no tool
|
|
610
|
+
// names inlined) so the prompt cache still holds.
|
|
611
|
+
if (dynamicTools && allToolDefs.length > activeTools.size) {
|
|
612
|
+
systemParts.push('# Tool Inventory\n' +
|
|
613
|
+
'Your current tool list is intentionally minimal. Additional tools ' +
|
|
614
|
+
'(web search, image/video/music generation, trading, content, brain ' +
|
|
615
|
+
'recall, etc.) are available on demand. Call `ActivateTool()` with ' +
|
|
616
|
+
'no arguments to see what is available, then call `ActivateTool({ ' +
|
|
617
|
+
'"names": ["<name>"] })` to enable the ones you need. Activated ' +
|
|
618
|
+
'tools become visible on the next turn.');
|
|
619
|
+
}
|
|
579
620
|
// ── Context awareness injection ──
|
|
580
621
|
// Tell the model how full its context window is so it can self-regulate.
|
|
581
622
|
// At high usage, nudge it to be concise and avoid unnecessary tool calls.
|
|
@@ -611,8 +652,9 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
611
652
|
let usage;
|
|
612
653
|
let stopReason;
|
|
613
654
|
// Create streaming executor for concurrent tool execution
|
|
655
|
+
const activeCapabilityMap = buildActiveCapabilityMap();
|
|
614
656
|
const streamExec = new StreamingExecutor({
|
|
615
|
-
handlers:
|
|
657
|
+
handlers: activeCapabilityMap,
|
|
616
658
|
scope: {
|
|
617
659
|
workingDir: workDir,
|
|
618
660
|
abortSignal: abort.signal,
|
|
@@ -669,7 +711,10 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
669
711
|
}
|
|
670
712
|
// Build per-call tool defs, max_tokens, and system prompt
|
|
671
713
|
// (planning calls get no tools + short output + planning prompt)
|
|
672
|
-
|
|
714
|
+
// Dynamic visibility: `buildCallToolDefs()` returns only the active set
|
|
715
|
+
// (core + any the agent pulled via ActivateTool). Re-evaluated every
|
|
716
|
+
// turn so newly activated tools take effect immediately.
|
|
717
|
+
let callToolDefs = buildCallToolDefs();
|
|
673
718
|
let callMaxTokens = maxTokens;
|
|
674
719
|
let callSystemPrompt = systemPrompt;
|
|
675
720
|
if (planActive && loopCount === 1) {
|
|
@@ -918,6 +963,24 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
918
963
|
const opusCost = (inputTokens / 1_000_000) * OPUS_PRICING.input
|
|
919
964
|
+ (usage.outputTokens / 1_000_000) * OPUS_PRICING.output;
|
|
920
965
|
sessionSavedVsOpus += Math.max(0, opusCost - costEstimate);
|
|
966
|
+
// ── Max-spend guard ──
|
|
967
|
+
// Session-level cost ceiling. Cron/daily drivers pass this to bound a
|
|
968
|
+
// single run ("spend at most $0.50 for today's digest"); interactive
|
|
969
|
+
// users can pass it to feel safe walking away. Hits as soon as accumulated
|
|
970
|
+
// cost crosses the cap — the last call that tipped us over still runs,
|
|
971
|
+
// but no further API calls are made.
|
|
972
|
+
const maxSpend = config.maxSpendUsd;
|
|
973
|
+
if (typeof maxSpend === 'number' && Number.isFinite(maxSpend) && maxSpend > 0 &&
|
|
974
|
+
sessionCostUsd >= maxSpend) {
|
|
975
|
+
onEvent({
|
|
976
|
+
kind: 'text_delta',
|
|
977
|
+
text: `\n\n_Max-spend reached: $${sessionCostUsd.toFixed(4)} ≥ cap $${maxSpend.toFixed(2)}. ` +
|
|
978
|
+
`Stopping session — further calls would exceed the budget._\n`,
|
|
979
|
+
});
|
|
980
|
+
persistSessionMeta();
|
|
981
|
+
onEvent({ kind: 'turn_done', reason: 'budget' });
|
|
982
|
+
return history;
|
|
983
|
+
}
|
|
921
984
|
// ── Max output tokens recovery ──
|
|
922
985
|
if (stopReason === 'max_tokens' && recoveryAttempts < MAX_RECOVERY_ATTEMPTS) {
|
|
923
986
|
recoveryAttempts++;
|
|
@@ -33,10 +33,10 @@ function isCommonDevCommand(cmd) {
|
|
|
33
33
|
return COMMON_DEV_PATTERNS.some(p => p.test(trimmed));
|
|
34
34
|
}
|
|
35
35
|
// ─── Default Rules ─────────────────────────────────────────────────────────
|
|
36
|
-
const READ_ONLY_TOOLS = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'Task', 'AskUser', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX']);
|
|
36
|
+
const READ_ONLY_TOOLS = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'Task', 'AskUser', 'ActivateTool', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX']);
|
|
37
37
|
const DESTRUCTIVE_TOOLS = new Set(['Write', 'Edit', 'Bash']);
|
|
38
38
|
const DEFAULT_RULES = {
|
|
39
|
-
allow: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'AskUser', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX'],
|
|
39
|
+
allow: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'AskUser', 'ActivateTool', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX'],
|
|
40
40
|
deny: [],
|
|
41
41
|
ask: ['Write', 'Edit', 'Bash', 'Agent', 'PostToX'],
|
|
42
42
|
};
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -155,4 +155,11 @@ export interface AgentConfig {
|
|
|
155
155
|
* unset. Format: "<driver>:<owner-or-chat-id>", e.g. "telegram:12345".
|
|
156
156
|
*/
|
|
157
157
|
sessionChannel?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Hard cap on total USD spend for this session. When accumulated API cost
|
|
160
|
+
* crosses the cap, the loop stops with `reason: 'budget'`. Zero/negative
|
|
161
|
+
* values disable the cap. Primary use case: cron/daily drivers that must
|
|
162
|
+
* bound a single run to keep autonomous execution inside a known envelope.
|
|
163
|
+
*/
|
|
164
|
+
maxSpendUsd?: number;
|
|
158
165
|
}
|
package/dist/banner.js
CHANGED
|
@@ -82,6 +82,21 @@ function padVisible(s, targetWidth) {
|
|
|
82
82
|
return s + '\x1b[0m' + ' '.repeat(targetWidth - current);
|
|
83
83
|
}
|
|
84
84
|
export function printBanner(version) {
|
|
85
|
+
const style = process.env.FRANKLIN_BANNER?.toLowerCase();
|
|
86
|
+
if (style === 'full' || style === 'legacy') {
|
|
87
|
+
printLegacyBanner(version);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
printCompactBanner(version);
|
|
91
|
+
}
|
|
92
|
+
function printCompactBanner(version) {
|
|
93
|
+
const title = chalk.bold.hex(GOLD_START)('FRANKLIN');
|
|
94
|
+
const meta = chalk.dim(` · blockrun.ai · v${version}`);
|
|
95
|
+
console.log(`${title}${meta}`);
|
|
96
|
+
console.log(chalk.dim('The AI agent with a wallet'));
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
|
99
|
+
function printLegacyBanner(version) {
|
|
85
100
|
const termWidth = process.stdout.columns ?? 80;
|
|
86
101
|
const useSideBySide = termWidth >= MIN_WIDTH_FOR_PORTRAIT;
|
|
87
102
|
if (useSideBySide) {
|
package/dist/commands/start.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ interface StartOptions {
|
|
|
7
7
|
resume?: string | boolean | 'picker';
|
|
8
8
|
/** Continue: resume most recent session matching the current working directory */
|
|
9
9
|
continue?: boolean;
|
|
10
|
+
/** Hard USD cap on total session spend. Stops the loop when exceeded. */
|
|
11
|
+
maxSpend?: string | number;
|
|
12
|
+
/** Run a single prompt non-interactively, then exit. For cron/scripted use. */
|
|
13
|
+
prompt?: string;
|
|
10
14
|
}
|
|
11
15
|
export declare function startCommand(options: StartOptions): Promise<void>;
|
|
12
16
|
export {};
|
package/dist/commands/start.js
CHANGED
|
@@ -74,6 +74,49 @@ export async function startCommand(options) {
|
|
|
74
74
|
// Default: free NVIDIA model — zero wallet charges until user explicitly switches
|
|
75
75
|
model = 'nvidia/nemotron-ultra-253b';
|
|
76
76
|
}
|
|
77
|
+
const workDir = process.cwd();
|
|
78
|
+
// --prompt batch mode: skip all interactive startup UI/side effects so
|
|
79
|
+
// stdout stays clean for cron/scripts. Keep the capability surface to the
|
|
80
|
+
// built-ins only — no panel, no MCP autoconnect, no wallet/banner chatter.
|
|
81
|
+
if (options.prompt) {
|
|
82
|
+
if (options.resume === true || options.resume === 'picker') {
|
|
83
|
+
console.error(chalk.red('`--prompt` requires `--resume` to include an explicit session id.'));
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const systemInstructions = assembleInstructions(workDir, model);
|
|
88
|
+
const subAgent = createSubAgentCapability(apiUrl, chain, allCapabilities, model);
|
|
89
|
+
const { registerMoAConfig } = await import('../tools/moa.js');
|
|
90
|
+
registerMoAConfig(apiUrl, chain, model);
|
|
91
|
+
const capabilities = [...allCapabilities, subAgent];
|
|
92
|
+
if (options.debug) {
|
|
93
|
+
const issues = validateToolDescriptions(capabilities);
|
|
94
|
+
for (const issue of issues) {
|
|
95
|
+
console.error(`[validate] ${issue.severity}: ${issue.toolName} — ${issue.issue}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const agentConfig = {
|
|
99
|
+
model,
|
|
100
|
+
apiUrl,
|
|
101
|
+
chain,
|
|
102
|
+
systemInstructions,
|
|
103
|
+
capabilities,
|
|
104
|
+
maxTurns: 100,
|
|
105
|
+
workingDir: workDir,
|
|
106
|
+
permissionMode: 'trust',
|
|
107
|
+
debug: options.debug,
|
|
108
|
+
resumeSessionId: (typeof options.resume === 'string' && options.resume !== 'picker')
|
|
109
|
+
? options.resume
|
|
110
|
+
: continueResolvedId,
|
|
111
|
+
...(options.maxSpend != null
|
|
112
|
+
? { maxSpendUsd: Number(options.maxSpend) }
|
|
113
|
+
: {}),
|
|
114
|
+
};
|
|
115
|
+
const exitCode = await runOneShot(agentConfig, options.prompt);
|
|
116
|
+
flushStats();
|
|
117
|
+
process.exitCode = exitCode;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
77
120
|
// Warn when a paid model is active so users know they'll be charged
|
|
78
121
|
const FREE_MODELS = new Set([
|
|
79
122
|
'nvidia/nemotron-ultra-253b',
|
|
@@ -114,7 +157,6 @@ export async function startCommand(options) {
|
|
|
114
157
|
catch { /* migration is optional */ }
|
|
115
158
|
}
|
|
116
159
|
printBanner(version);
|
|
117
|
-
const workDir = process.cwd();
|
|
118
160
|
// Auto-start panel in background unless explicitly disabled.
|
|
119
161
|
// Binds loopback-only (wallet secrets on /api/wallet/secret — never expose on LAN).
|
|
120
162
|
let panelUrl;
|
|
@@ -240,9 +282,13 @@ export async function startCommand(options) {
|
|
|
240
282
|
workingDir: workDir,
|
|
241
283
|
// Non-TTY (piped) input = scripted mode → trust all tools automatically.
|
|
242
284
|
// Interactive TTY = default mode (prompts for Bash/Write/Edit).
|
|
243
|
-
|
|
285
|
+
// --prompt is also scripted; the cron driver never sees a TTY.
|
|
286
|
+
permissionMode: (options.trust || options.prompt || !process.stdin.isTTY) ? 'trust' : 'default',
|
|
244
287
|
debug: options.debug,
|
|
245
288
|
resumeSessionId,
|
|
289
|
+
...(options.maxSpend != null
|
|
290
|
+
? { maxSpendUsd: Number(options.maxSpend) }
|
|
291
|
+
: {}),
|
|
246
292
|
};
|
|
247
293
|
// Bootstrap learnings from existing CLAUDE.md on first run (async, non-blocking)
|
|
248
294
|
Promise.all([
|
|
@@ -262,6 +308,30 @@ export async function startCommand(options) {
|
|
|
262
308
|
await runWithBasicUI(agentConfig, model, workDir);
|
|
263
309
|
}
|
|
264
310
|
}
|
|
311
|
+
// ─── One-shot mode (franklin --prompt "...") ──────────────────────────────
|
|
312
|
+
// Used by cron drivers. Non-interactive, prints text deltas to stdout as
|
|
313
|
+
// they stream, honors --max-spend, exits 0 on completion / 1 on error.
|
|
314
|
+
async function runOneShot(agentConfig, prompt) {
|
|
315
|
+
let delivered = false;
|
|
316
|
+
let exitCode = 0;
|
|
317
|
+
const getInput = async () => {
|
|
318
|
+
if (delivered)
|
|
319
|
+
return null;
|
|
320
|
+
delivered = true;
|
|
321
|
+
return prompt;
|
|
322
|
+
};
|
|
323
|
+
await interactiveSession(agentConfig, getInput, (event) => {
|
|
324
|
+
if (event.kind === 'text_delta') {
|
|
325
|
+
process.stdout.write(event.text);
|
|
326
|
+
}
|
|
327
|
+
else if (event.kind === 'turn_done') {
|
|
328
|
+
if (event.reason === 'error')
|
|
329
|
+
exitCode = 1;
|
|
330
|
+
process.stdout.write('\n');
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
return exitCode;
|
|
334
|
+
}
|
|
265
335
|
// ─── Ink UI (interactive terminal) ─────────────────────────────────────────
|
|
266
336
|
async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, onBalanceReady, fetchBalance) {
|
|
267
337
|
const startSnapshot = snapshotStats();
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,8 @@ program
|
|
|
43
43
|
.option('--trust', 'Trust mode — skip permission prompts for all tools')
|
|
44
44
|
.option('-r, --resume [sessionId]', 'Resume a session by ID (or show picker if omitted)')
|
|
45
45
|
.option('-c, --continue', 'Continue the most recent session in this directory')
|
|
46
|
+
.option('--max-spend <usd>', 'Hard USD cap on total session API spend — session stops when exceeded')
|
|
47
|
+
.option('-p, --prompt <text>', 'Run a single prompt non-interactively (for cron/scripted use)')
|
|
46
48
|
.action((options) => startCommand({ ...options, version }));
|
|
47
49
|
program
|
|
48
50
|
.command('resume [sessionId]')
|
|
@@ -209,7 +211,7 @@ const args = process.argv.slice(2);
|
|
|
209
211
|
const firstArg = args[0];
|
|
210
212
|
const HELP_FLAGS = new Set(['-h', '--help']);
|
|
211
213
|
const VERSION_FLAGS = new Set(['-V', '--version']);
|
|
212
|
-
const START_ONLY_FLAGS = new Set(['--trust', '--debug', '-m', '--model', '-r', '--resume', '-c', '--continue']);
|
|
214
|
+
const START_ONLY_FLAGS = new Set(['--trust', '--debug', '-m', '--model', '-r', '--resume', '-c', '--continue', '-p', '--prompt', '--max-spend']);
|
|
213
215
|
function hasAnyFlag(argv, flags) {
|
|
214
216
|
return argv.some(arg => flags.has(arg));
|
|
215
217
|
}
|
|
@@ -227,6 +229,12 @@ function parseStartFlags(argv, startIdx = 0) {
|
|
|
227
229
|
else if ((arg === '-m' || arg === '--model') && argv[i + 1]) {
|
|
228
230
|
opts.model = argv[++i];
|
|
229
231
|
}
|
|
232
|
+
else if ((arg === '-p' || arg === '--prompt') && argv[i + 1]) {
|
|
233
|
+
opts.prompt = argv[++i];
|
|
234
|
+
}
|
|
235
|
+
else if (arg === '--max-spend' && argv[i + 1]) {
|
|
236
|
+
opts.maxSpend = argv[++i];
|
|
237
|
+
}
|
|
230
238
|
else if (arg === '-c' || arg === '--continue') {
|
|
231
239
|
opts.continue = true;
|
|
232
240
|
}
|
|
@@ -257,7 +265,7 @@ if (firstArg === 'solana' || firstArg === 'base') {
|
|
|
257
265
|
saveChain(firstArg);
|
|
258
266
|
const startOpts = parseStartFlags(args, 1);
|
|
259
267
|
await startCommand(startOpts);
|
|
260
|
-
process.exit(0);
|
|
268
|
+
process.exit(process.exitCode ?? 0);
|
|
261
269
|
}
|
|
262
270
|
else if (!firstArg || firstArg.startsWith('-')) {
|
|
263
271
|
if (hasAnyFlag(args, HELP_FLAGS) && hasStartOnlyFlag(args)) {
|
|
@@ -273,7 +281,7 @@ else if (!firstArg || firstArg.startsWith('-')) {
|
|
|
273
281
|
// No subcommand or only flags — treat as 'start' with flags
|
|
274
282
|
const startOpts = parseStartFlags(args, 0);
|
|
275
283
|
await startCommand(startOpts);
|
|
276
|
-
process.exit(0);
|
|
284
|
+
process.exit(process.exitCode ?? 0);
|
|
277
285
|
}
|
|
278
286
|
else {
|
|
279
287
|
program.parse();
|
package/dist/panel/html.js
CHANGED
|
@@ -371,14 +371,14 @@ a:hover { text-decoration:underline; }
|
|
|
371
371
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4z"/></svg>
|
|
372
372
|
Wallet
|
|
373
373
|
</button>
|
|
374
|
+
<button class="nav-item" data-tab="markets">
|
|
375
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 14l4-4 4 4 5-5"/></svg>
|
|
376
|
+
Markets
|
|
377
|
+
</button>
|
|
374
378
|
<button class="nav-item" data-tab="sessions">
|
|
375
379
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
376
380
|
Sessions
|
|
377
381
|
</button>
|
|
378
|
-
<button class="nav-item" data-tab="social">
|
|
379
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4l11.733 16h4.267l-11.733-16z"/><path d="M4 20l6.768-6.768M15.232 11.232L20 4"/></svg>
|
|
380
|
-
Social
|
|
381
|
-
</button>
|
|
382
382
|
<button class="nav-item" data-tab="learnings">
|
|
383
383
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
|
384
384
|
Learnings
|
|
@@ -542,16 +542,34 @@ a:hover { text-decoration:underline; }
|
|
|
542
542
|
<div class="session-detail" id="session-detail" style="display:none"></div>
|
|
543
543
|
</div>
|
|
544
544
|
|
|
545
|
-
<!--
|
|
546
|
-
<div class="tab" id="tab-
|
|
545
|
+
<!-- Markets -->
|
|
546
|
+
<div class="tab" id="tab-markets">
|
|
547
547
|
<div class="content-header">
|
|
548
|
-
<h2>
|
|
549
|
-
<p>
|
|
548
|
+
<h2>Markets</h2>
|
|
549
|
+
<p>How Franklin gets trading data — and what it costs.</p>
|
|
550
550
|
</div>
|
|
551
|
-
|
|
552
|
-
<div class="
|
|
553
|
-
<h3>
|
|
554
|
-
<div
|
|
551
|
+
|
|
552
|
+
<div class="grid grid-4">
|
|
553
|
+
<div class="card"><h3>Calls today</h3><div class="metric" id="mk-calls">—</div></div>
|
|
554
|
+
<div class="card"><h3>Spend today</h3><div class="metric gold" id="mk-spend">—</div></div>
|
|
555
|
+
<div class="card"><h3>p50 latency</h3><div class="metric" id="mk-p50">—</div></div>
|
|
556
|
+
<div class="card"><h3>Payment chain</h3><div class="metric" id="mk-chain">—</div></div>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<div style="display:grid;grid-template-columns:1.1fr 1fr;gap:14px;margin-top:14px">
|
|
560
|
+
<div class="card">
|
|
561
|
+
<h3>Data pipeline</h3>
|
|
562
|
+
<p style="color:var(--text-dim);font-size:12px;margin:4px 0 14px">
|
|
563
|
+
Each asset class routes through the provider registry to the active upstream.
|
|
564
|
+
</p>
|
|
565
|
+
<div id="mk-pipeline" style="font-family:var(--mono);font-size:12px;line-height:1.75"></div>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="card">
|
|
568
|
+
<h3>Providers</h3>
|
|
569
|
+
<div id="mk-providers" style="margin-top:6px"></div>
|
|
570
|
+
<h3 style="margin-top:18px">Recent paid calls</h3>
|
|
571
|
+
<div id="mk-paid" class="empty" style="margin-top:6px">No paid calls yet</div>
|
|
572
|
+
</div>
|
|
555
573
|
</div>
|
|
556
574
|
</div>
|
|
557
575
|
|
|
@@ -588,6 +606,7 @@ a:hover { text-decoration:underline; }
|
|
|
588
606
|
</div>
|
|
589
607
|
<div id="audit-list" style="font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;"></div>
|
|
590
608
|
</div>
|
|
609
|
+
|
|
591
610
|
</div>
|
|
592
611
|
|
|
593
612
|
<script>
|
|
@@ -716,14 +735,84 @@ document.getElementById('session-search').addEventListener('input', (e) => {
|
|
|
716
735
|
}, 300);
|
|
717
736
|
});
|
|
718
737
|
|
|
719
|
-
async function
|
|
720
|
-
const
|
|
721
|
-
if (!
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
738
|
+
async function loadMarkets() {
|
|
739
|
+
const data = await api('markets');
|
|
740
|
+
if (!data) return;
|
|
741
|
+
|
|
742
|
+
const calls = (data.totals && data.totals.callsToday) || 0;
|
|
743
|
+
const spend = (data.totals && data.totals.spendUsdToday) || 0;
|
|
744
|
+
const p50 = data.totals && data.totals.p50LatencyMs;
|
|
745
|
+
document.getElementById('mk-calls').textContent = String(calls);
|
|
746
|
+
document.getElementById('mk-spend').textContent = usd(spend);
|
|
747
|
+
document.getElementById('mk-p50').textContent = (p50 == null) ? '—' : (p50 + ' ms');
|
|
748
|
+
document.getElementById('mk-chain').textContent = (data.chain || 'base').toUpperCase();
|
|
749
|
+
|
|
750
|
+
// Pipeline: Franklin → registry → per-asset-class provider → endpoint
|
|
751
|
+
const rows = (data.wiring || []).filter(function(r){ return r.kind === 'price'; });
|
|
752
|
+
const singletonRows = (data.wiring || []).filter(function(r){ return r.kind !== 'price'; });
|
|
753
|
+
const providerLabel = function(name) {
|
|
754
|
+
if (name === 'coingecko') return '<span style="color:var(--success)">CoinGecko</span>';
|
|
755
|
+
if (name === 'blockrun') return '<span style="color:var(--gold)">BlockRun Gateway</span>';
|
|
756
|
+
return esc(name);
|
|
757
|
+
};
|
|
758
|
+
const pipeLines = [
|
|
759
|
+
'<div>Franklin agent</div>',
|
|
760
|
+
'<div style="color:var(--text-dim);padding-left:8px">↓</div>',
|
|
761
|
+
'<div>Provider registry</div>',
|
|
762
|
+
'<div style="color:var(--text-dim);padding-left:8px">↓</div>',
|
|
763
|
+
];
|
|
764
|
+
rows.forEach(function(r, i){
|
|
765
|
+
const last = i === rows.length - 1;
|
|
766
|
+
const branch = last ? '└' : '├';
|
|
767
|
+
const paid = r.paid ? ' <span style="color:var(--gold);font-size:10px">◆ x402</span>' : '';
|
|
768
|
+
pipeLines.push(
|
|
769
|
+
'<div> ' + branch + '─ ' + esc(r.assetClass).padEnd(9, ' ') +
|
|
770
|
+
' → ' + providerLabel(r.provider) + paid + '</div>'
|
|
771
|
+
);
|
|
772
|
+
});
|
|
773
|
+
pipeLines.push('<div style="margin-top:10px;color:var(--text-dim);font-size:11px">Other singleton kinds:</div>');
|
|
774
|
+
singletonRows.forEach(function(r){
|
|
775
|
+
pipeLines.push(
|
|
776
|
+
'<div style="color:var(--text-dim);font-size:11px"> ' +
|
|
777
|
+
esc(r.kind) + ' → ' + providerLabel(r.provider) + '</div>'
|
|
778
|
+
);
|
|
779
|
+
});
|
|
780
|
+
document.getElementById('mk-pipeline').innerHTML = pipeLines.join('');
|
|
781
|
+
|
|
782
|
+
// Providers health
|
|
783
|
+
const statusChip = function(s){
|
|
784
|
+
if (s === 'ok') return '<span class="dot on"></span> <span style="color:var(--success)">OK</span>';
|
|
785
|
+
if (s === 'degraded') return '<span class="dot off"></span> <span style="color:var(--danger)">degraded</span>';
|
|
786
|
+
return '<span class="dot" style="background:var(--text-dim)"></span> <span style="color:var(--text-dim)">cold</span>';
|
|
787
|
+
};
|
|
788
|
+
const providers = data.providers || [];
|
|
789
|
+
document.getElementById('mk-providers').innerHTML = providers.length === 0 ? '<div class="empty">No calls recorded yet.</div>' : providers.map(function(p){
|
|
790
|
+
const since = p.lastOkAt ? Math.round((Date.now() - p.lastOkAt) / 1000) + 's ago' : '—';
|
|
791
|
+
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid var(--border);font-size:12px">' +
|
|
792
|
+
'<span>' + statusChip(p.status) + ' <strong>' + esc(p.name) + '</strong></span>' +
|
|
793
|
+
'<span style="color:var(--text-dim);font-family:var(--mono);font-size:11px">' +
|
|
794
|
+
p.calls + ' calls · p50 ' + (p.p50LatencyMs == null ? '—' : p.p50LatencyMs + 'ms') + ' · last ' + since +
|
|
795
|
+
'</span>' +
|
|
796
|
+
'</div>';
|
|
797
|
+
}).join('');
|
|
798
|
+
|
|
799
|
+
// Recent paid calls
|
|
800
|
+
const paid = data.recentPaidCalls || [];
|
|
801
|
+
const paidBox = document.getElementById('mk-paid');
|
|
802
|
+
if (paid.length === 0) {
|
|
803
|
+
paidBox.className = 'empty';
|
|
804
|
+
paidBox.textContent = 'No paid calls yet — stocks ship in the next release.';
|
|
805
|
+
} else {
|
|
806
|
+
paidBox.className = '';
|
|
807
|
+
paidBox.innerHTML = paid.map(function(r){
|
|
808
|
+
const age = Math.round((Date.now() - r.ts) / 1000) + 's ago';
|
|
809
|
+
return '<div style="display:flex;justify-content:space-between;padding:4px 0;font-family:var(--mono);font-size:12px">' +
|
|
810
|
+
'<span>' + esc(r.endpoint) + '</span>' +
|
|
811
|
+
'<span class="gold">' + usd(r.costUsd) + '</span>' +
|
|
812
|
+
'<span style="color:var(--text-dim)">' + age + '</span>' +
|
|
813
|
+
'</div>';
|
|
814
|
+
}).join('');
|
|
815
|
+
}
|
|
727
816
|
}
|
|
728
817
|
|
|
729
818
|
async function loadLearnings() {
|
|
@@ -909,9 +998,10 @@ document.querySelector('[data-tab="audit"]')?.addEventListener('click', loadAudi
|
|
|
909
998
|
|
|
910
999
|
loadOverview();
|
|
911
1000
|
loadSessions();
|
|
912
|
-
|
|
1001
|
+
loadMarkets();
|
|
913
1002
|
loadLearnings();
|
|
914
1003
|
loadWallet();
|
|
1004
|
+
document.querySelector('[data-tab="markets"]')?.addEventListener('click', loadMarkets);
|
|
915
1005
|
setInterval(() => api('wallet').then(w => {
|
|
916
1006
|
if (w) {
|
|
917
1007
|
document.getElementById('balance').textContent = usdBig(w.balance) + ' USDC';
|
package/dist/panel/server.js
CHANGED
|
@@ -13,7 +13,8 @@ import { listSessions, loadSessionHistory } from '../session/storage.js';
|
|
|
13
13
|
import { searchSessions } from '../session/search.js';
|
|
14
14
|
import { loadLearnings } from '../learnings/store.js';
|
|
15
15
|
import { readAudit } from '../stats/audit.js';
|
|
16
|
-
import {
|
|
16
|
+
import { snapshot as marketsSnapshot } from '../trading/providers/telemetry.js';
|
|
17
|
+
import { describeWiring } from '../trading/providers/registry.js';
|
|
17
18
|
import { getHTML } from './html.js';
|
|
18
19
|
const sseClients = new Set();
|
|
19
20
|
function json(res, data, status = 200) {
|
|
@@ -312,9 +313,19 @@ export function createPanelServer(port) {
|
|
|
312
313
|
}
|
|
313
314
|
return;
|
|
314
315
|
}
|
|
315
|
-
if (p === '/api/
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
if (p === '/api/markets') {
|
|
317
|
+
// Snapshot of every active data provider for the Markets panel:
|
|
318
|
+
// pipeline wiring (which endpoint serves which asset class), live
|
|
319
|
+
// health + latency per provider, and today's paid-call ledger.
|
|
320
|
+
const snap = marketsSnapshot();
|
|
321
|
+
const wiring = describeWiring();
|
|
322
|
+
json(res, {
|
|
323
|
+
chain: loadChain(),
|
|
324
|
+
wiring,
|
|
325
|
+
providers: snap.providers,
|
|
326
|
+
totals: snap.totals,
|
|
327
|
+
recentPaidCalls: snap.recentPaidCalls,
|
|
328
|
+
});
|
|
318
329
|
return;
|
|
319
330
|
}
|
|
320
331
|
if (p === '/api/learnings') {
|