@blockrun/franklin 3.2.4 → 3.3.1

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 (61) hide show
  1. package/README.md +216 -233
  2. package/dist/agent/commands.js +54 -13
  3. package/dist/agent/context.js +31 -1
  4. package/dist/agent/loop.js +48 -19
  5. package/dist/agent/permissions.js +3 -3
  6. package/dist/commands/migrate.d.ts +13 -0
  7. package/dist/commands/migrate.js +389 -0
  8. package/dist/commands/panel.d.ts +6 -0
  9. package/dist/commands/panel.js +29 -0
  10. package/dist/commands/start.js +41 -2
  11. package/dist/events/bridge.d.ts +1 -0
  12. package/dist/events/bridge.js +24 -0
  13. package/dist/events/bus.d.ts +17 -0
  14. package/dist/events/bus.js +55 -0
  15. package/dist/events/types.d.ts +49 -0
  16. package/dist/events/types.js +8 -0
  17. package/dist/index.js +15 -0
  18. package/dist/learnings/extractor.d.ts +16 -0
  19. package/dist/learnings/extractor.js +234 -0
  20. package/dist/learnings/index.d.ts +3 -0
  21. package/dist/learnings/index.js +2 -0
  22. package/dist/learnings/store.d.ts +15 -0
  23. package/dist/learnings/store.js +130 -0
  24. package/dist/learnings/types.d.ts +24 -0
  25. package/dist/learnings/types.js +7 -0
  26. package/dist/mcp/client.js +9 -2
  27. package/dist/narrative/state.d.ts +30 -0
  28. package/dist/narrative/state.js +69 -0
  29. package/dist/panel/html.d.ts +5 -0
  30. package/dist/panel/html.js +341 -0
  31. package/dist/panel/server.d.ts +7 -0
  32. package/dist/panel/server.js +152 -0
  33. package/dist/session/storage.js +4 -2
  34. package/dist/social/browser-pool.d.ts +29 -0
  35. package/dist/social/browser-pool.js +57 -0
  36. package/dist/social/preflight.d.ts +14 -0
  37. package/dist/social/preflight.js +26 -0
  38. package/dist/social/x.d.ts +8 -0
  39. package/dist/social/x.js +9 -1
  40. package/dist/stats/tracker.d.ts +1 -0
  41. package/dist/stats/tracker.js +59 -13
  42. package/dist/tools/bash.js +6 -1
  43. package/dist/tools/index.js +3 -0
  44. package/dist/tools/posttox.d.ts +7 -0
  45. package/dist/tools/posttox.js +137 -0
  46. package/dist/tools/searchx.d.ts +7 -0
  47. package/dist/tools/searchx.js +111 -0
  48. package/dist/tools/trading.d.ts +3 -0
  49. package/dist/tools/trading.js +168 -0
  50. package/dist/tools/webfetch.js +19 -9
  51. package/dist/tools/write.js +2 -0
  52. package/dist/trading/config.d.ts +23 -0
  53. package/dist/trading/config.js +45 -0
  54. package/dist/trading/data.d.ts +30 -0
  55. package/dist/trading/data.js +112 -0
  56. package/dist/trading/metrics.d.ts +29 -0
  57. package/dist/trading/metrics.js +105 -0
  58. package/dist/ui/app.js +73 -44
  59. package/dist/ui/markdown.d.ts +9 -0
  60. package/dist/ui/markdown.js +86 -0
  61. package/package.json +1 -1
@@ -196,9 +196,9 @@ const DIRECT_COMMANDS = {
196
196
  ` **Coding:** /commit /review /test /fix /debug /explain /search /find /refactor /scaffold\n` +
197
197
  ` **Git:** /push /pr /undo /status /diff /log /branch /stash /unstash\n` +
198
198
  ` **Analysis:** /security /lint /optimize /todo /deps /clean /migrate /doc\n` +
199
- ` **Session:** /plan /ultraplan /execute /compact /retry /sessions /resume /context /tasks\n` +
199
+ ` **Session:** /plan /ultraplan /execute /compact /retry /sessions /resume /session-search /context /tasks\n` +
200
200
  ` **Power:** /ultrathink [query] /ultraplan /dump\n` +
201
- ` **Info:** /model /wallet /cost /tokens /mcp /doctor /version /bug /help\n` +
201
+ ` **Info:** /model /wallet /cost /tokens /learnings /mcp /doctor /version /bug /help\n` +
202
202
  ` **UI:** /clear /exit\n` +
203
203
  (ultrathinkOn ? `\n Ultrathink: ON\n` : '')
204
204
  });
@@ -350,11 +350,12 @@ const DIRECT_COMMANDS = {
350
350
  for (const s of sessions.slice(0, 10)) {
351
351
  const date = new Date(s.updatedAt).toLocaleString();
352
352
  const dir = s.workDir ? ` — ${s.workDir.split('/').pop()}` : '';
353
- text += ` ${s.id} ${s.model} ${s.turnCount} turns ${date}${dir}\n`;
353
+ const current = s.id === ctx.sessionId ? ' (current)' : '';
354
+ text += ` ${s.id} ${s.model} ${s.turnCount} turns ${date}${dir}${current}\n`;
354
355
  }
355
356
  if (sessions.length > 10)
356
357
  text += ` ... and ${sessions.length - 10} more\n`;
357
- text += '\nUse /resume <session-id> to continue a session.\n';
358
+ text += '\nUse /resume to restore the latest session, or /resume <session-id> for a specific one.\n';
358
359
  ctx.onEvent({ kind: 'text_delta', text });
359
360
  }
360
361
  emitDone(ctx);
@@ -486,13 +487,17 @@ export async function handleSlashCommand(input, ctx) {
486
487
  await DIRECT_COMMANDS[input](ctx);
487
488
  return { handled: true };
488
489
  }
489
- // /search <query> — full-text search past sessions
490
- if (input === '/search' || input.startsWith('/search ')) {
491
- const query = input === '/search' ? '' : input.slice('/search '.length).trim();
490
+ // /session-search <query> — full-text search past sessions
491
+ if (input === '/session-search' ||
492
+ input.startsWith('/session-search ') ||
493
+ input === '/ssearch' ||
494
+ input.startsWith('/ssearch ')) {
495
+ const prefix = input.startsWith('/ssearch') ? '/ssearch' : '/session-search';
496
+ const query = input === prefix ? '' : input.slice(prefix.length + 1).trim();
492
497
  if (!query) {
493
- ctx.onEvent({ kind: 'text_delta', text: 'Usage: /search <query>\n' +
498
+ ctx.onEvent({ kind: 'text_delta', text: 'Usage: /session-search <query>\n' +
494
499
  'Finds past sessions whose messages match the query.\n' +
495
- 'Use quotes for phrase search: /search "payment loop"\n'
500
+ 'Use quotes for phrase search: /session-search "payment loop"\n'
496
501
  });
497
502
  emitDone(ctx);
498
503
  return { handled: true };
@@ -513,6 +518,34 @@ export async function handleSlashCommand(input, ctx) {
513
518
  emitDone(ctx);
514
519
  return { handled: true };
515
520
  }
521
+ // /learnings — view or clear per-user learnings
522
+ if (input === '/learnings' || input.startsWith('/learnings ')) {
523
+ const { loadLearnings, decayLearnings, saveLearnings } = await import('../learnings/store.js');
524
+ const arg = input.slice('/learnings'.length).trim();
525
+ if (arg === 'clear') {
526
+ saveLearnings([]);
527
+ ctx.onEvent({ kind: 'text_delta', text: 'All learnings cleared.\n' });
528
+ }
529
+ else {
530
+ let learnings = loadLearnings();
531
+ if (learnings.length === 0) {
532
+ ctx.onEvent({ kind: 'text_delta', text: 'No learnings yet. Franklin learns your preferences over time.\n' });
533
+ }
534
+ else {
535
+ learnings = decayLearnings(learnings);
536
+ const sorted = [...learnings].sort((a, b) => (b.confidence * b.times_confirmed) - (a.confidence * a.times_confirmed));
537
+ let text = `**Personal Learnings** (${sorted.length})\n\n`;
538
+ for (const l of sorted) {
539
+ const conf = l.confidence >= 0.8 ? 'high' : l.confidence >= 0.5 ? 'mid' : 'low';
540
+ text += ` [${conf}] ${l.learning} (×${l.times_confirmed})\n`;
541
+ }
542
+ text += '\nUse `/learnings clear` to reset.\n';
543
+ ctx.onEvent({ kind: 'text_delta', text });
544
+ }
545
+ }
546
+ emitDone(ctx);
547
+ return { handled: true };
548
+ }
516
549
  // /model — show current model or switch with /model <name>
517
550
  if (input === '/model' || input.startsWith('/model ')) {
518
551
  if (input === '/model') {
@@ -523,6 +556,7 @@ export async function handleSlashCommand(input, ctx) {
523
556
  else {
524
557
  const newModel = resolveModel(input.slice(7).trim());
525
558
  ctx.config.model = newModel;
559
+ ctx.config.onModelChange?.(newModel);
526
560
  ctx.onEvent({ kind: 'text_delta', text: `Model → **${newModel}**\n` });
527
561
  }
528
562
  emitDone(ctx);
@@ -604,9 +638,16 @@ export async function handleSlashCommand(input, ctx) {
604
638
  emitDone(ctx);
605
639
  return { handled: true };
606
640
  }
607
- // /resume <id>
608
- if (input.startsWith('/resume ')) {
609
- const targetId = input.slice(8).trim();
641
+ // /resume or /resume <id>
642
+ if (input === '/resume' || input.startsWith('/resume ')) {
643
+ const targetId = input === '/resume'
644
+ ? listSessions().find((session) => session.id !== ctx.sessionId)?.id ?? ''
645
+ : input.slice(8).trim();
646
+ if (!targetId) {
647
+ ctx.onEvent({ kind: 'text_delta', text: 'No previous session available to resume.\n' });
648
+ emitDone(ctx);
649
+ return { handled: true };
650
+ }
610
651
  const restored = loadSessionHistory(targetId);
611
652
  if (restored.length === 0) {
612
653
  ctx.onEvent({ kind: 'text_delta', text: `Session "${targetId}" not found or empty.\n` });
@@ -636,7 +677,7 @@ export async function handleSlashCommand(input, ctx) {
636
677
  ...Object.keys(DIRECT_COMMANDS),
637
678
  ...Object.keys(REWRITE_COMMANDS),
638
679
  ...ARG_COMMANDS.map(c => c.prefix.trim()),
639
- '/branch', '/resume', '/model', '/wallet', '/cost', '/help', '/clear', '/retry', '/exit',
680
+ '/branch', '/resume', '/model', '/wallet', '/cost', '/help', '/clear', '/retry', '/exit', '/session-search', '/ssearch',
640
681
  ];
641
682
  const cmd = input.split(/\s/)[0];
642
683
  const close = allCommands.filter(c => {
@@ -5,6 +5,7 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { execSync } from 'node:child_process';
8
+ import { loadLearnings, decayLearnings, saveLearnings, formatForPrompt } from '../learnings/store.js';
8
9
  // ─── System Instructions Assembly ──────────────────────────────────────────
9
10
  const BASE_INSTRUCTIONS = `You are runcode, an AI coding agent that helps users with software engineering tasks.
10
11
  You have access to tools for reading, writing, editing files, running shell commands, searching codebases, web browsing, and more.
@@ -32,7 +33,24 @@ You have access to tools for reading, writing, editing files, running shell comm
32
33
  - **Batch bash**: combine sequential shell commands into one Bash call with && or a script. Only split when you need to inspect intermediate output.
33
34
  - **AskUser**: Only use AskUser when you are about to perform a destructive action (deleting files, dropping databases) and need explicit confirmation. NEVER use AskUser to ask what the user wants — just answer their message directly. If their request is vague, make a reasonable assumption and proceed.
34
35
  - Never write to /etc, /usr, ~/.ssh, ~/.aws. Don't commit secrets.
35
- - Type /help to see all slash commands.`;
36
+ - Type /help to see all slash commands.
37
+
38
+ # Missing Access
39
+ Always deliver results first using whatever tools work (WebSearch, WebFetch, etc.). Never let missing access block you.
40
+ After delivering results, if a better data source exists, add one line at the end:
41
+ "Tip: run franklin social setup && franklin social login x for live X data."
42
+ Do NOT check access before acting. Do NOT explain what you tried. Just deliver, then tip.
43
+
44
+ # X / Social Marketing
45
+ When the user asks about X posts, trending topics, or social growth:
46
+ 1. Search and find relevant posts immediately (WebSearch or SearchX).
47
+ 2. Present results as a numbered list of SUGGESTIONS, not actions. Each item must include:
48
+ - The post author and a short snippet
49
+ - A clickable link (https://x.com/...)
50
+ - A suggested reply draft (2-3 sentences, natural tone, not salesy)
51
+ 3. End with: "Reply to any of these? Give me the number."
52
+ 4. Do NOT auto-post. Do NOT explain how the social system works. Do NOT dump config JSON.
53
+ 5. If the user asks to set up X access, ask them simple questions one at a time (handle? product? keywords?) and write the config yourself. Never show raw JSON to the user.`;
36
54
  // Cache assembled instructions per workingDir — avoids re-running git commands
37
55
  // when sub-agents are spawned (common in parallel tool use patterns).
38
56
  const _instructionCache = new Map();
@@ -57,6 +75,18 @@ export function assembleInstructions(workingDir) {
57
75
  if (gitInfo) {
58
76
  parts.push(`# Git Context\n\n${gitInfo}`);
59
77
  }
78
+ // Inject per-user learnings from self-evolution system
79
+ try {
80
+ let learnings = loadLearnings();
81
+ if (learnings.length > 0) {
82
+ learnings = decayLearnings(learnings);
83
+ saveLearnings(learnings);
84
+ const personalContext = formatForPrompt(learnings);
85
+ if (personalContext)
86
+ parts.push(personalContext);
87
+ }
88
+ }
89
+ catch { /* learnings are optional — never block startup */ }
60
90
  _instructionCache.set(workingDir, parts);
61
91
  return parts;
62
92
  }
@@ -42,7 +42,21 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
42
42
  const sessionId = createSessionId();
43
43
  let turnCount = 0;
44
44
  let tokenBudgetWarned = false; // Emit token budget warning at most once per session
45
+ let lastSessionActivity = Date.now();
46
+ const persistSessionMeta = () => {
47
+ updateSessionMeta(sessionId, {
48
+ model: config.model,
49
+ workDir,
50
+ turnCount,
51
+ messageCount: history.length,
52
+ });
53
+ };
54
+ const persistSessionMessage = (message) => {
55
+ appendToSession(sessionId, message);
56
+ persistSessionMeta();
57
+ };
45
58
  pruneOldSessions(sessionId); // Cleanup old sessions on start, protect current
59
+ persistSessionMeta();
46
60
  while (true) {
47
61
  let input = await getUserInput();
48
62
  if (input === null)
@@ -72,23 +86,28 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
72
86
  }
73
87
  lastUserInput = input;
74
88
  history.push({ role: 'user', content: input });
75
- appendToSession(sessionId, { role: 'user', content: input });
76
89
  turnCount++;
90
+ persistSessionMessage({ role: 'user', content: input });
77
91
  const abort = new AbortController();
78
92
  onAbortReady?.(() => abort.abort());
79
93
  let loopCount = 0;
80
94
  let recoveryAttempts = 0;
81
95
  let compactFailures = 0;
82
96
  let maxTokensOverride;
83
- let lastActivity = Date.now();
97
+ const turnIdleReference = lastSessionActivity;
98
+ lastSessionActivity = Date.now();
84
99
  // Agent loop for this user message
85
100
  while (loopCount < maxTurns) {
86
101
  loopCount++;
102
+ // Signal UI that a new LLM round is starting (shows spinner between tool results and next response)
103
+ if (loopCount > 1) {
104
+ onEvent({ kind: 'thinking_delta', text: '' });
105
+ }
87
106
  // ── Token optimization pipeline ──
88
107
  // 1. Strip thinking, budget tool results, time-based cleanup (always — cheap)
89
108
  const optimized = optimizeHistory(history, {
90
109
  debug: config.debug,
91
- lastActivityTimestamp: lastActivity,
110
+ lastActivityTimestamp: loopCount === 1 ? turnIdleReference : lastSessionActivity,
92
111
  });
93
112
  if (optimized !== history) {
94
113
  history.length = 0;
@@ -185,9 +204,12 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
185
204
  if (err.name === 'AbortError' || abort.signal.aborted) {
186
205
  // Save any partial response that was streamed before abort
187
206
  if (responseParts && responseParts.length > 0) {
188
- history.push({ role: 'assistant', content: responseParts });
189
- appendToSession(sessionId, { role: 'assistant', content: responseParts });
207
+ const partialAssistant = { role: 'assistant', content: responseParts };
208
+ history.push(partialAssistant);
209
+ persistSessionMessage(partialAssistant);
190
210
  }
211
+ lastSessionActivity = Date.now();
212
+ persistSessionMeta();
191
213
  onEvent({ kind: 'turn_done', reason: 'aborted' });
192
214
  break;
193
215
  }
@@ -249,6 +271,8 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
249
271
  reason: 'error',
250
272
  error: `[${classified.label}] ${errMsg}${suggestion}`,
251
273
  });
274
+ lastSessionActivity = Date.now();
275
+ persistSessionMeta();
252
276
  break;
253
277
  }
254
278
  // When API doesn't return input tokens (some models return 0), estimate from history
@@ -278,11 +302,16 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
278
302
  }
279
303
  }
280
304
  // Append what we got + a continuation prompt (text already streamed)
281
- history.push({ role: 'assistant', content: responseParts });
282
- history.push({
305
+ const partialAssistant = { role: 'assistant', content: responseParts };
306
+ const continuationPrompt = {
283
307
  role: 'user',
284
308
  content: 'Continue where you left off. Do not repeat what you already said.',
285
- });
309
+ };
310
+ history.push(partialAssistant);
311
+ persistSessionMessage(partialAssistant);
312
+ history.push(continuationPrompt);
313
+ persistSessionMessage(continuationPrompt);
314
+ lastSessionActivity = Date.now();
286
315
  continue; // Retry with higher limit
287
316
  }
288
317
  // Reset recovery counter on successful completion
@@ -294,17 +323,13 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
294
323
  invocations.push(part);
295
324
  }
296
325
  }
297
- history.push({ role: 'assistant', content: responseParts });
326
+ const assistantMessage = { role: 'assistant', content: responseParts };
327
+ history.push(assistantMessage);
328
+ persistSessionMessage(assistantMessage);
298
329
  // No more capabilities → done with this user message
299
330
  if (invocations.length === 0) {
300
- // Save session on completed turn
301
- appendToSession(sessionId, { role: 'assistant', content: responseParts });
302
- updateSessionMeta(sessionId, {
303
- model: config.model,
304
- workDir: config.workingDir || process.cwd(),
305
- turnCount,
306
- messageCount: history.length,
307
- });
331
+ lastSessionActivity = Date.now();
332
+ persistSessionMeta();
308
333
  // Token budget warning — emit once per session when crossing 70%
309
334
  if (!tokenBudgetWarned) {
310
335
  const { estimated } = getAnchoredTokenCount(history);
@@ -327,7 +352,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
327
352
  onEvent({ kind: 'capability_done', id: inv.id, result });
328
353
  }
329
354
  // Refresh activity timestamp after tool execution
330
- lastActivity = Date.now();
355
+ lastSessionActivity = Date.now();
331
356
  // Append outcomes
332
357
  const outcomeContent = results.map(([inv, result]) => ({
333
358
  type: 'tool_result',
@@ -335,9 +360,13 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
335
360
  content: result.output,
336
361
  is_error: result.isError,
337
362
  }));
338
- history.push({ role: 'user', content: outcomeContent });
363
+ const toolResultMessage = { role: 'user', content: outcomeContent };
364
+ history.push(toolResultMessage);
365
+ persistSessionMessage(toolResultMessage);
339
366
  }
340
367
  if (loopCount >= maxTurns) {
368
+ lastSessionActivity = Date.now();
369
+ persistSessionMeta();
341
370
  onEvent({ kind: 'turn_done', reason: 'max_turns' });
342
371
  }
343
372
  }
@@ -8,12 +8,12 @@ import readline from 'node:readline';
8
8
  import chalk from 'chalk';
9
9
  import { BLOCKRUN_DIR } from '../config.js';
10
10
  // ─── Default Rules ─────────────────────────────────────────────────────────
11
- const READ_ONLY_TOOLS = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'Task', 'AskUser', 'ImageGen']);
11
+ const READ_ONLY_TOOLS = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'Task', 'AskUser', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX']);
12
12
  const DESTRUCTIVE_TOOLS = new Set(['Write', 'Edit', 'Bash']);
13
13
  const DEFAULT_RULES = {
14
- allow: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'AskUser', 'ImageGen'],
14
+ allow: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'AskUser', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX'],
15
15
  deny: [],
16
- ask: ['Write', 'Edit', 'Bash', 'Agent'],
16
+ ask: ['Write', 'Edit', 'Bash', 'Agent', 'PostToX'],
17
17
  };
18
18
  // ─── Permission Manager ────────────────────────────────────────────────────
19
19
  export class PermissionManager {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * franklin migrate — one-click import from other AI coding agents.
3
+ *
4
+ * Detects installed tools (Claude Code, Cline, Cursor, etc.),
5
+ * shows what can be migrated, and imports with user confirmation.
6
+ */
7
+ export declare function migrateCommand(): Promise<void>;
8
+ /**
9
+ * Check if other AI tools are installed and suggest migration.
10
+ * Only runs once — writes a marker file after first check.
11
+ * Returns true if the user chose to migrate (caller should re-run start after).
12
+ */
13
+ export declare function checkAndSuggestMigration(): Promise<boolean>;