@blockrun/franklin 3.8.2 → 3.8.4

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 (46) hide show
  1. package/README.md +22 -36
  2. package/dist/agent/commands.js +1 -1
  3. package/dist/agent/llm.d.ts +6 -0
  4. package/dist/agent/llm.js +103 -14
  5. package/dist/agent/loop.d.ts +9 -0
  6. package/dist/agent/loop.js +85 -0
  7. package/dist/agent/think-tag-stripper.d.ts +27 -0
  8. package/dist/agent/think-tag-stripper.js +75 -0
  9. package/dist/agent/tokens.js +2 -1
  10. package/dist/agent/types.d.ts +7 -0
  11. package/dist/brain/index.d.ts +1 -1
  12. package/dist/brain/index.js +1 -1
  13. package/dist/brain/store.d.ts +13 -1
  14. package/dist/brain/store.js +74 -5
  15. package/dist/channel/telegram.d.ts +46 -0
  16. package/dist/channel/telegram.js +367 -0
  17. package/dist/commands/migrate.d.ts +5 -3
  18. package/dist/commands/migrate.js +17 -15
  19. package/dist/commands/stats.js +1 -1
  20. package/dist/commands/telegram.d.ts +15 -0
  21. package/dist/commands/telegram.js +95 -0
  22. package/dist/content/library.js +2 -2
  23. package/dist/index.js +9 -0
  24. package/dist/panel/html.js +1 -1
  25. package/dist/router/index.js +5 -5
  26. package/dist/session/storage.d.ts +12 -0
  27. package/dist/session/storage.js +11 -0
  28. package/dist/social/ai.d.ts +3 -2
  29. package/dist/social/ai.js +3 -2
  30. package/dist/stats/insights.d.ts +1 -1
  31. package/dist/stats/tracker.js +1 -1
  32. package/dist/tools/content-execute.d.ts +1 -1
  33. package/dist/tools/content-execute.js +1 -1
  34. package/dist/tools/index.js +11 -3
  35. package/dist/tools/memory.d.ts +16 -0
  36. package/dist/tools/memory.js +86 -0
  37. package/dist/tools/trading-execute.d.ts +2 -2
  38. package/dist/tools/trading-execute.js +2 -2
  39. package/dist/tools/videogen.d.ts +17 -0
  40. package/dist/tools/videogen.js +237 -0
  41. package/dist/trading/trade-log.d.ts +2 -2
  42. package/dist/trading/trade-log.js +2 -2
  43. package/dist/ui/app.js +38 -3
  44. package/dist/ui/markdown.d.ts +16 -0
  45. package/dist/ui/markdown.js +26 -2
  46. package/package.json +5 -2
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `franklin telegram` — start the Telegram ingress bot.
3
+ *
4
+ * Designed to run on a server / always-on laptop. Reads the bot token and
5
+ * owner id from env (or falls back to ~/.blockrun/config). Uses trust-mode
6
+ * permissions because the operator is remote — there's no terminal prompt
7
+ * they can answer per tool call. The owner lock in `runTelegramBot` is the
8
+ * real security boundary.
9
+ */
10
+ import chalk from 'chalk';
11
+ import { loadChain, API_URLS } from '../config.js';
12
+ import { assembleInstructions } from '../agent/context.js';
13
+ import { allCapabilities } from '../tools/index.js';
14
+ import { loadConfig } from './config.js';
15
+ import { runTelegramBot } from '../channel/telegram.js';
16
+ import { findLatestSessionByChannel } from '../session/storage.js';
17
+ export async function telegramCommand(opts) {
18
+ const token = process.env.TELEGRAM_BOT_TOKEN;
19
+ const ownerRaw = process.env.TELEGRAM_OWNER_ID;
20
+ if (!token || !ownerRaw) {
21
+ console.error(chalk.red('Missing Telegram config.'));
22
+ console.error(chalk.dim('\nSet two env vars before running `franklin telegram`:\n' +
23
+ ' TELEGRAM_BOT_TOKEN=<from @BotFather>\n' +
24
+ ' TELEGRAM_OWNER_ID=<your numeric Telegram user id>\n\n' +
25
+ 'Tip: message @userinfobot on Telegram to get your user id.'));
26
+ process.exit(1);
27
+ }
28
+ const ownerId = parseInt(ownerRaw, 10);
29
+ if (!Number.isFinite(ownerId) || ownerId <= 0) {
30
+ console.error(chalk.red(`TELEGRAM_OWNER_ID must be a positive integer, got: ${ownerRaw}`));
31
+ process.exit(1);
32
+ }
33
+ const chain = loadChain();
34
+ const apiUrl = API_URLS[chain];
35
+ const config = loadConfig();
36
+ // Model: --model flag > config default > free default.
37
+ const model = opts.model ||
38
+ config['default-model'] ||
39
+ 'nvidia/nemotron-ultra-253b';
40
+ const workingDir = process.cwd();
41
+ const systemInstructions = assembleInstructions(workingDir, model);
42
+ // Resume the most recent session tagged for THIS owner so a process
43
+ // restart doesn't drop the conversation. First run has no prior session
44
+ // and starts fresh. `/new` will lay down a new session ID within the
45
+ // running process; the next restart picks up the newest one again.
46
+ const channelTag = `telegram:${ownerId}`;
47
+ const prior = findLatestSessionByChannel(channelTag);
48
+ if (prior) {
49
+ console.log(chalk.dim(` resuming session ${prior.id} (${prior.messageCount} msgs, ` +
50
+ `last update ${new Date(prior.updatedAt).toLocaleString()})`));
51
+ }
52
+ const agentConfig = {
53
+ model,
54
+ apiUrl,
55
+ chain,
56
+ systemInstructions,
57
+ capabilities: allCapabilities,
58
+ workingDir,
59
+ // No interactive terminal for permission prompts — remote operator can't
60
+ // answer y/n per tool. The Telegram owner lock is the security boundary.
61
+ permissionMode: 'trust',
62
+ debug: opts.debug,
63
+ sessionChannel: channelTag,
64
+ resumeSessionId: prior?.id,
65
+ };
66
+ console.log(chalk.bold.cyan('Franklin Telegram bot'));
67
+ console.log(chalk.dim(` chain: ${chain}`));
68
+ console.log(chalk.dim(` model: ${model}`));
69
+ console.log(chalk.dim(` owner: ${ownerId}`));
70
+ console.log(chalk.yellow(' permission mode: trust — every tool the model picks will execute ' +
71
+ 'without confirmation. The owner lock is your only gate.\n'));
72
+ // SIGINT → stop the bot cleanly. Wrap runTelegramBot so a second Ctrl-C
73
+ // force-exits if the cleanup path hangs.
74
+ let exitAttempts = 0;
75
+ process.on('SIGINT', () => {
76
+ exitAttempts++;
77
+ if (exitAttempts === 1) {
78
+ console.log(chalk.dim('\nStopping… (press Ctrl-C again to force)'));
79
+ }
80
+ else {
81
+ process.exit(130);
82
+ }
83
+ });
84
+ try {
85
+ await runTelegramBot(agentConfig, {
86
+ token,
87
+ ownerId,
88
+ log: (line) => console.log(chalk.dim(line)),
89
+ });
90
+ }
91
+ catch (err) {
92
+ console.error(chalk.red(`Telegram bot failed: ${err.message}`));
93
+ process.exit(1);
94
+ }
95
+ }
@@ -5,8 +5,8 @@
5
5
  * risk engine, store.ts is the persistence adapter, and tools/content-execute.ts
6
6
  * wires everything into agent-facing capabilities.
7
7
  *
8
- * Why this exists: Claude Code / Cursor cannot carry a content project across
9
- * sessions. If you start drafting a podcast episode Monday and come back
8
+ * Why this exists: stateless coding agents can't carry a content project
9
+ * across sessions. If you start drafting a podcast episode Monday and come back
10
10
  * Wednesday, there's no concept of *the same* piece of work. Franklin tracks
11
11
  * outline → drafts → assets → distribution as one durable object and lets
12
12
  * the agent spend USDC (image generation, audio, stock footage) against a
package/dist/index.js CHANGED
@@ -166,6 +166,15 @@ program
166
166
  });
167
167
  }
168
168
  }
169
+ program
170
+ .command('telegram')
171
+ .description('Drive Franklin from Telegram (requires TELEGRAM_BOT_TOKEN + TELEGRAM_OWNER_ID env vars)')
172
+ .option('-m, --model <model>', 'Model to use (default from config)')
173
+ .option('--debug', 'Enable debug logging')
174
+ .action(async (opts) => {
175
+ const { telegramCommand } = await import('./commands/telegram.js');
176
+ await telegramCommand(opts);
177
+ });
169
178
  program
170
179
  .command('migrate')
171
180
  .description('Import preferences and MCP servers from existing AI agent configs')
@@ -423,7 +423,7 @@ a:hover { text-decoration:underline; }
423
423
  <div class="savings-hero" id="savings-hero" style="display:none">
424
424
  <div>
425
425
  <div class="savings-detail">
426
- <div class="label">Saved vs Claude Opus</div>
426
+ <div class="label">Saved vs Opus tier</div>
427
427
  </div>
428
428
  <div class="savings-amount" id="savings-amount">&mdash;</div>
429
429
  <div class="savings-detail">
@@ -33,10 +33,10 @@ function loadLearnedWeights() {
33
33
  return null;
34
34
  }
35
35
  // ─── Tier Model Configs ───
36
- // Agent-first defaults. Claude Sonnet 4.6 is the industry standard for multi-step
37
- // tool-use agent work; cheap models keep derailing on simple agent loops.
38
- // Each tier's fallback ends with a cheaper option so payment/quota failures
39
- // don't strand users on equally expensive alternatives.
36
+ // Agent-first defaults. Sonnet-tier models are the current sweet spot for
37
+ // multi-step tool-use agent work; cheap models keep derailing on simple agent
38
+ // loops. Each tier's fallback ends with a cheaper option so payment/quota
39
+ // failures don't strand users on equally expensive alternatives.
40
40
  const AUTO_TIERS = {
41
41
  SIMPLE: {
42
42
  primary: 'google/gemini-2.5-flash',
@@ -282,7 +282,7 @@ export function routeRequest(prompt, profile = 'auto') {
282
282
  // Auto profile bypasses learned routing. The learned Elo scores grow with
283
283
  // usage volume rather than pure quality, which biased the router toward
284
284
  // cheap/weak models on agentic work. Classic AUTO_TIERS defaults are
285
- // agent-tuned (Claude Sonnet as backbone) and more predictable for users.
285
+ // agent-tuned (Sonnet-tier backbone) and more predictable for users.
286
286
  if (profile === 'auto') {
287
287
  return classicRouteRequest(prompt, profile);
288
288
  }
@@ -15,6 +15,12 @@ export interface SessionMeta {
15
15
  outputTokens?: number;
16
16
  costUsd?: number;
17
17
  savedVsOpusUsd?: number;
18
+ /**
19
+ * Origin channel tag. Unset for regular CLI sessions; set to a string like
20
+ * `telegram:<ownerId>` when the session was started by a non-CLI driver.
21
+ * Lets findLatestSessionByChannel pick up the right session on bot restart.
22
+ */
23
+ channel?: string;
18
24
  }
19
25
  /** Get the absolute path to a session's JSONL file (for external readers like search). */
20
26
  export declare function getSessionFilePath(id: string): string;
@@ -42,6 +48,12 @@ export declare function loadSessionHistory(sessionId: string): Dialogue[];
42
48
  * List all saved sessions, newest first.
43
49
  */
44
50
  export declare function listSessions(): SessionMeta[];
51
+ /**
52
+ * Find the latest saved session tagged with a given channel (e.g.
53
+ * `telegram:12345`). Used by non-CLI drivers to resume across process
54
+ * restarts. Returns undefined when no matching session exists.
55
+ */
56
+ export declare function findLatestSessionByChannel(channel: string): SessionMeta | undefined;
45
57
  /**
46
58
  * Prune old sessions beyond MAX_SESSIONS.
47
59
  */
@@ -92,6 +92,9 @@ export function updateSessionMeta(sessionId, meta) {
92
92
  outputTokens: meta.outputTokens ?? existing?.outputTokens ?? 0,
93
93
  costUsd: meta.costUsd ?? existing?.costUsd ?? 0,
94
94
  savedVsOpusUsd: meta.savedVsOpusUsd ?? existing?.savedVsOpusUsd ?? 0,
95
+ ...(meta.channel !== undefined || existing?.channel !== undefined
96
+ ? { channel: meta.channel ?? existing?.channel }
97
+ : {}),
95
98
  };
96
99
  // Atomic write: tmp file + rename. Prevents corruption when parent
97
100
  // and sub-agent update the same session meta concurrently.
@@ -176,6 +179,14 @@ export function listSessions() {
176
179
  return [];
177
180
  }
178
181
  }
182
+ /**
183
+ * Find the latest saved session tagged with a given channel (e.g.
184
+ * `telegram:12345`). Used by non-CLI drivers to resume across process
185
+ * restarts. Returns undefined when no matching session exists.
186
+ */
187
+ export function findLatestSessionByChannel(channel) {
188
+ return listSessions().find(m => m.channel === channel);
189
+ }
179
190
  /**
180
191
  * Prune old sessions beyond MAX_SESSIONS.
181
192
  */
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * Key improvements over social-bot:
9
9
  * - Uses Franklin's multi-model router (tier-based: free / cheap / premium)
10
- * instead of hardcoded Claude Sonnet for every call — throwaway replies
11
- * can run on free NVIDIA models, high-value leads can escalate to Opus.
10
+ * instead of hardcoding one model for every call — throwaway replies
11
+ * can run on free NVIDIA models, high-value leads can escalate to the
12
+ * top tier.
12
13
  * - x402 payment flow handled by ModelClient — no Anthropic billing relationship.
13
14
  * - SKIP detection lives in the caller so we can commit a 'skipped' record
14
15
  * for visibility in stats.
package/dist/social/ai.js CHANGED
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * Key improvements over social-bot:
9
9
  * - Uses Franklin's multi-model router (tier-based: free / cheap / premium)
10
- * instead of hardcoded Claude Sonnet for every call — throwaway replies
11
- * can run on free NVIDIA models, high-value leads can escalate to Opus.
10
+ * instead of hardcoding one model for every call — throwaway replies
11
+ * can run on free NVIDIA models, high-value leads can escalate to the
12
+ * top tier.
12
13
  * - x402 payment flow handled by ModelClient — no Anthropic billing relationship.
13
14
  * - SKIP detection lives in the caller so we can commit a 'skipped' record
14
15
  * for visibility in stats.
@@ -21,7 +21,7 @@ export interface InsightsReport {
21
21
  totalInputTokens: number;
22
22
  /** Total output tokens in window */
23
23
  totalOutputTokens: number;
24
- /** Savings vs always using Claude Opus */
24
+ /** Savings vs always using the Opus-tier baseline */
25
25
  savedVsOpusUsd: number;
26
26
  /** Per-model breakdown, sorted by cost desc */
27
27
  byModel: Array<{
@@ -207,7 +207,7 @@ export function recordUsage(model, inputTokens, outputTokens, costUsd, latencyMs
207
207
  */
208
208
  export function getStatsSummary() {
209
209
  const stats = loadStats();
210
- // Calculate what it would cost with Claude Opus
210
+ // Calculate what it would cost with the Opus-tier baseline
211
211
  const opusCost = (stats.totalInputTokens / 1_000_000) * OPUS_PRICING.input +
212
212
  (stats.totalOutputTokens / 1_000_000) * OPUS_PRICING.output;
213
213
  const saved = opusCost - stats.totalCostUsd;
@@ -9,7 +9,7 @@
9
9
  * ContentShow — dump a single piece's state as actionable markdown
10
10
  * ContentList — summary of every piece, newest first
11
11
  *
12
- * This is the surface Claude Code and Cursor cannot cover: durable content
12
+ * This is the surface generic coding agents can't cover: durable content
13
13
  * state with autonomous spending gating. An agent can spin up "Franklin
14
14
  * launch thread" with a $3 budget, hit DALL-E twice for hero images, blow
15
15
  * the budget, read the refusal, pick a cheaper model, and continue — all
@@ -9,7 +9,7 @@
9
9
  * ContentShow — dump a single piece's state as actionable markdown
10
10
  * ContentList — summary of every piece, newest first
11
11
  *
12
- * This is the surface Claude Code and Cursor cannot cover: durable content
12
+ * This is the surface generic coding agents can't cover: durable content
13
13
  * state with autonomous spending gating. An agent can spin up "Franklin
14
14
  * launch thread" with a $3 budget, hit DALL-E twice for hero images, blow
15
15
  * the budget, read the refusal, pick a cheaper model, and continue — all
@@ -13,6 +13,8 @@ import { webFetchCapability, clearSessionState as clearWebFetchSessionState } fr
13
13
  import { webSearchCapability } from './websearch.js';
14
14
  import { taskCapability } from './task.js';
15
15
  import { createImageGenCapability } from './imagegen.js';
16
+ import { createVideoGenCapability } from './videogen.js';
17
+ import { memoryRecallCapability } from './memory.js';
16
18
  import { askUserCapability } from './askuser.js';
17
19
  import { tradingSignalCapability, tradingMarketCapability } from './trading.js';
18
20
  import { searchXCapability } from './searchx.js';
@@ -34,7 +36,7 @@ import { loadLibrary as loadContentLibrary, saveLibrary as saveContentLibrary }
34
36
  // (2.5 positions fully loaded), $900 total exposure cap (keep 10% cash buffer).
35
37
  // Live prices from CoinGecko; simulated fills at 10 bps. Portfolio persists
36
38
  // to ~/.blockrun/portfolio.json across sessions — that persistence is the
37
- // whole point of this vertical (Claude Code / Cursor cannot carry trading
39
+ // whole point of this vertical (stateless coding agents can't carry trading
38
40
  // state between runs).
39
41
  const DEFAULT_PORTFOLIO_PATH = path.join(os.homedir(), '.blockrun', 'portfolio.json');
40
42
  const DEFAULT_TRADE_LOG_PATH = path.join(os.homedir(), '.blockrun', 'trades.jsonl');
@@ -68,8 +70,8 @@ function buildDefaultTradingCapabilities() {
68
70
  const defaultTradingCapabilities = buildDefaultTradingCapabilities();
69
71
  // ─── Default Content Library ──────────────────────────────────────────────
70
72
  // Durable content projects at ~/.blockrun/content.json. Like the portfolio,
71
- // this is persistent cross-session state — something Claude Code structurally
72
- // cannot offer.
73
+ // this is persistent cross-session state — something stateless coding agents
74
+ // structurally can't offer.
73
75
  const DEFAULT_CONTENT_PATH = path.join(os.homedir(), '.blockrun', 'content.json');
74
76
  // Build a single ContentLibrary instance so both the Content capabilities and
75
77
  // the content-aware ImageGen capability share state and persistence.
@@ -90,6 +92,10 @@ const defaultImageGenCapability = createImageGenCapability({
90
92
  library: defaultContentLibrary,
91
93
  onContentChange: persistContentLibrary,
92
94
  });
95
+ const defaultVideoGenCapability = createVideoGenCapability({
96
+ library: defaultContentLibrary,
97
+ onContentChange: persistContentLibrary,
98
+ });
93
99
  /**
94
100
  * Reset module-level tool state that would otherwise leak between sessions
95
101
  * when the same process runs `interactiveSession()` more than once (library
@@ -112,6 +118,8 @@ export const allCapabilities = [
112
118
  webSearchCapability,
113
119
  taskCapability,
114
120
  defaultImageGenCapability,
121
+ defaultVideoGenCapability,
122
+ memoryRecallCapability,
115
123
  askUserCapability,
116
124
  tradingSignalCapability,
117
125
  tradingMarketCapability,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * MemoryRecall — let the agent query Franklin's Brain explicitly.
3
+ *
4
+ * Auto-recall (in loop.ts) covers the "user just mentioned X" case, but the
5
+ * agent often needs to check memory on its own: "what do I already know
6
+ * about the user's portfolio?", "have I seen this company before?". This
7
+ * tool exposes the Brain's search + observation read path as a first-class
8
+ * capability so the agent can decide when to pull from long-term memory.
9
+ *
10
+ * Read-only. Writing to the Brain happens after-the-fact via
11
+ * extractBrainEntities on session end; we don't expose a write tool here
12
+ * because giving the model direct write access to the knowledge graph
13
+ * invites fabricated "facts" that never saw a real conversation.
14
+ */
15
+ import type { CapabilityHandler } from '../agent/types.js';
16
+ export declare const memoryRecallCapability: CapabilityHandler;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * MemoryRecall — let the agent query Franklin's Brain explicitly.
3
+ *
4
+ * Auto-recall (in loop.ts) covers the "user just mentioned X" case, but the
5
+ * agent often needs to check memory on its own: "what do I already know
6
+ * about the user's portfolio?", "have I seen this company before?". This
7
+ * tool exposes the Brain's search + observation read path as a first-class
8
+ * capability so the agent can decide when to pull from long-term memory.
9
+ *
10
+ * Read-only. Writing to the Brain happens after-the-fact via
11
+ * extractBrainEntities on session end; we don't expose a write tool here
12
+ * because giving the model direct write access to the knowledge graph
13
+ * invites fabricated "facts" that never saw a real conversation.
14
+ */
15
+ import { searchEntities, getEntityObservations, getEntityRelations, loadEntities, getBrainStats, } from '../brain/store.js';
16
+ const MAX_OBS_PER_ENTITY = 5;
17
+ const MAX_REL_PER_ENTITY = 3;
18
+ export const memoryRecallCapability = {
19
+ spec: {
20
+ name: 'MemoryRecall',
21
+ description: "Query Franklin's long-term memory (the Brain) for what it already " +
22
+ "knows about a person, project, company, product, or concept. Use " +
23
+ "this BEFORE asking the user a question the memory might already " +
24
+ "answer, or when deciding how to act toward a named entity. Returns " +
25
+ "matching entities with their observations and relations. " +
26
+ "Read-only — observations are harvested automatically at session end.",
27
+ input_schema: {
28
+ type: 'object',
29
+ properties: {
30
+ query: {
31
+ type: 'string',
32
+ description: 'Name, alias, or substring to search for. Case-insensitive.',
33
+ },
34
+ limit: {
35
+ type: 'number',
36
+ description: 'Max entities to return (default 5, max 15).',
37
+ },
38
+ },
39
+ required: ['query'],
40
+ },
41
+ },
42
+ execute: async (input) => {
43
+ const { query, limit } = input;
44
+ if (!query || !query.trim()) {
45
+ return { output: 'Error: query is required', isError: true };
46
+ }
47
+ const cap = Math.min(Math.max(1, limit ?? 5), 15);
48
+ const hits = searchEntities(query, cap);
49
+ const stats = getBrainStats();
50
+ if (hits.length === 0) {
51
+ return {
52
+ output: `No memory match for "${query}".\n\n` +
53
+ `Brain holds ${stats.entities} entities, ${stats.observations} observations.`,
54
+ };
55
+ }
56
+ const entities = loadEntities();
57
+ const lines = [`# Memory — ${hits.length} match${hits.length === 1 ? '' : 'es'} for "${query}"`];
58
+ for (const hit of hits) {
59
+ lines.push(`\n## ${hit.name} (${hit.type})`);
60
+ if (hit.aliases.length > 0) {
61
+ lines.push(`aka: ${hit.aliases.join(', ')}`);
62
+ }
63
+ lines.push(`_referenced ${hit.reference_count}×, last seen ${new Date(hit.updated_at).toISOString().slice(0, 10)}_`);
64
+ const obs = getEntityObservations(hit.id)
65
+ .sort((a, b) => b.confidence - a.confidence)
66
+ .slice(0, MAX_OBS_PER_ENTITY);
67
+ if (obs.length > 0) {
68
+ lines.push('\n**Facts**');
69
+ for (const o of obs)
70
+ lines.push(`- ${o.content}`);
71
+ }
72
+ const rels = getEntityRelations(hit.id).slice(0, MAX_REL_PER_ENTITY);
73
+ if (rels.length > 0) {
74
+ lines.push('\n**Relations**');
75
+ for (const r of rels) {
76
+ const otherId = r.from_id === hit.id ? r.to_id : r.from_id;
77
+ const other = entities.find(e => e.id === otherId);
78
+ if (other)
79
+ lines.push(`- ${r.type} → ${other.name}`);
80
+ }
81
+ }
82
+ }
83
+ return { output: lines.join('\n') };
84
+ },
85
+ concurrent: true,
86
+ };
@@ -4,8 +4,8 @@
4
4
  * TradingOpenPosition (buy side), TradingClosePosition (sell side).
5
5
  *
6
6
  * This is the surface that differentiates Franklin from generic coding
7
- * agents — Claude Code and Cursor cannot hold a wallet, track positions
8
- * across sessions, or reason about P&L. Every output here is deliberately
7
+ * agents — stateless tools can't hold a wallet, track positions across
8
+ * sessions, or reason about P&L. Every output here is deliberately
9
9
  * information-rich so the agent has the numbers it needs to make the next
10
10
  * economic decision (cash left, risk utilization, unrealized vs realized
11
11
  * P&L, fill detail) without a follow-up tool call.
@@ -4,8 +4,8 @@
4
4
  * TradingOpenPosition (buy side), TradingClosePosition (sell side).
5
5
  *
6
6
  * This is the surface that differentiates Franklin from generic coding
7
- * agents — Claude Code and Cursor cannot hold a wallet, track positions
8
- * across sessions, or reason about P&L. Every output here is deliberately
7
+ * agents — stateless tools can't hold a wallet, track positions across
8
+ * sessions, or reason about P&L. Every output here is deliberately
9
9
  * information-rich so the agent has the numbers it needs to make the next
10
10
  * economic decision (cash left, risk utilization, unrealized vs realized
11
11
  * P&L, fill detail) without a follow-up tool call.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Video Generation capability — generate short MP4 videos via the BlockRun
3
+ * /v1/videos/generations endpoint. Uses x402 payment (Base or Solana).
4
+ *
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.
9
+ */
10
+ import type { CapabilityHandler } from '../agent/types.js';
11
+ import type { ContentLibrary } from '../content/library.js';
12
+ export interface VideoGenDeps {
13
+ library?: ContentLibrary;
14
+ onContentChange?: () => void | Promise<void>;
15
+ }
16
+ export declare function createVideoGenCapability(deps?: VideoGenDeps): CapabilityHandler;
17
+ export declare const videoGenCapability: CapabilityHandler;