@blockrun/franklin 3.3.0 → 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.
@@ -196,7 +196,7 @@ 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
201
  ` **Info:** /model /wallet /cost /tokens /learnings /mcp /doctor /version /bug /help\n` +
202
202
  ` **UI:** /clear /exit\n` +
@@ -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 };
@@ -633,9 +638,16 @@ export async function handleSlashCommand(input, ctx) {
633
638
  emitDone(ctx);
634
639
  return { handled: true };
635
640
  }
636
- // /resume <id>
637
- if (input.startsWith('/resume ')) {
638
- 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
+ }
639
651
  const restored = loadSessionHistory(targetId);
640
652
  if (restored.length === 0) {
641
653
  ctx.onEvent({ kind: 'text_delta', text: `Session "${targetId}" not found or empty.\n` });
@@ -665,7 +677,7 @@ export async function handleSlashCommand(input, ctx) {
665
677
  ...Object.keys(DIRECT_COMMANDS),
666
678
  ...Object.keys(REWRITE_COMMANDS),
667
679
  ...ARG_COMMANDS.map(c => c.prefix.trim()),
668
- '/branch', '/resume', '/model', '/wallet', '/cost', '/help', '/clear', '/retry', '/exit',
680
+ '/branch', '/resume', '/model', '/wallet', '/cost', '/help', '/clear', '/retry', '/exit', '/session-search', '/ssearch',
669
681
  ];
670
682
  const cmd = input.split(/\s/)[0];
671
683
  const close = allCommands.filter(c => {
@@ -33,7 +33,24 @@ You have access to tools for reading, writing, editing files, running shell comm
33
33
  - **Batch bash**: combine sequential shell commands into one Bash call with && or a script. Only split when you need to inspect intermediate output.
34
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.
35
35
  - Never write to /etc, /usr, ~/.ssh, ~/.aws. Don't commit secrets.
36
- - 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.`;
37
54
  // Cache assembled instructions per workingDir — avoids re-running git commands
38
55
  // when sub-agents are spawned (common in parallel tool use patterns).
39
56
  const _instructionCache = new Map();
@@ -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
  }
package/dist/banner.js CHANGED
@@ -7,26 +7,32 @@ import chalk from 'chalk';
7
7
  // https://commons.wikimedia.org/wiki/File:BenFranklinDuplessis.jpg
8
8
  //
9
9
  // Pipeline:
10
- // 1. Crop the 800×989 thumb to a 500×500 square centred on the face
11
- // (sips --cropToHeightWidth 500 500 --cropOffset 140 150)
10
+ // 1. Crop the 2403×2971 original to a 1400×1400 square centred on the face
11
+ // (sips --cropToHeightWidth 1400 1400 --cropOffset 400 500)
12
12
  // 2. Convert via chafa:
13
- // chafa --size=16x8 --symbols=block --colors=full ben-face.jpg
13
+ // chafa --size=30x14 --symbols=block --colors=256 ben-face.jpg
14
14
  // 3. Strip cursor visibility control codes (\x1b[?25l / \x1b[?25h)
15
15
  // 4. Paste here as hex-escaped string array (readable + diff-friendly)
16
16
  //
17
- // Visible dimensions: ~16 characters wide × 8 rows tall.
17
+ // Visible dimensions: ~28 characters wide × 14 rows tall.
18
18
  //
19
19
  // Rendered best in a 256-color or truecolor terminal. Degrades gracefully
20
20
  // on ancient terminals — but those are long gone and we don't support them.
21
21
  const BEN_PORTRAIT_ROWS = [
22
- '\x1b[0m\x1b[38;2;7;0;0;48;2;8;0;0m▔ \x1b[38;2;9;1;0m▂\x1b[38;2;56;36;15;48;2;11;2;0m▗\x1b[38;2;100;73;36;48;2;31;16;6m▅\x1b[38;2;189;141;75;48;2;117;87;43m▅\x1b[38;2;217;162;85;48;2;152;111;51m▆\x1b[38;2;164;122;64;48;2;215;158;85m▔\x1b[38;2;124;90;46;48;2;217;160;93m▔\x1b[38;2;185;136;75;48;2;77;48;20m▅\x1b[38;2;100;61;24;48;2;39;18;4m▖\x1b[38;2;48;26;9;48;2;32;13;3m▃\x1b[38;2;39;18;4;48;2;30;11;2m▄\x1b[38;2;38;17;4;48;2;32;13;3m▄\x1b[38;2;40;20;5;48;2;35;15;2m▃\x1b[38;2;41;21;5;48;2;36;16;3m▂\x1b[0m',
23
- '\x1b[7m\x1b[38;2;8;0;0m \x1b[0m\x1b[38;2;0;0;0;48;2;8;0;0m \x1b[38;2;13;2;1;48;2;45;26;10m▊\x1b[38;2;61;40;17;48;2;87;63;31m▎\x1b[38;2;88;61;29;48;2;134;94;42m▋\x1b[38;2;182;132;66;48;2;223;172;93m▏\x1b[38;2;140;91;38;48;2;233;193;106m▂\x1b[38;2;135;82;35;48;2;229;178;106m▂\x1b[38;2;201;145;78;48;2;223;166;95m▂\x1b[38;2;133;88;46;48;2;198;148;86m▁\x1b[38;2;144;96;47;48;2;96;57;21m▍\x1b[38;2;66;42;15;48;2;58;33;11m▗\x1b[38;2;59;36;13;48;2;47;25;9m▆\x1b[38;2;57;35;11;48;2;46;24;7m▅\x1b[38;2;58;36;11;48;2;50;29;8m▖\x1b[38;2;53;32;8;48;2;48;26;7m▃\x1b[0m',
24
- '\x1b[38;2;12;3;3;48;2;9;0;0m▁\x1b[38;2;102;76;40;48;2;19;8;4m▗\x1b[38;2;110;83;45;48;2;56;35;15m▄\x1b[38;2;91;67;37;48;2;105;79;45m▌\x1b[38;2;96;64;31;48;2;186;135;70m▊\x1b[38;2;226;169;101;48;2;217;162;91m▗\x1b[38;2;216;159;89;48;2;144;93;44m▅\x1b[38;2;195;145;83;48;2;112;62;24m▅\x1b[38;2;233;178;110;48;2;206;151;81m▆\x1b[38;2;207;155;92;48;2;105;61;30m▎\x1b[38;2;145;94;46;48;2;94;50;19m▖\x1b[38;2;90;48;17;48;2;52;26;8m▎\x1b[38;2;59;33;9;48;2;64;40;14m▖\x1b[38;2;63;39;13;48;2;65;41;13m▊\x1b[38;2;58;36;11;48;2;64;40;14m▝\x1b[38;2;60;38;13;48;2;57;35;10m▍\x1b[0m',
25
- '\x1b[38;2;37;22;12;48;2;11;2;2m▕\x1b[38;2;52;32;16;48;2;94;67;32m▘\x1b[38;2;77;53;21;48;2;125;96;52m▗\x1b[38;2;44;15;6;48;2;83;48;21m▞\x1b[38;2;122;73;33;48;2;195;138;72m▍\x1b[38;2;209;149;77;48;2;223;160;89m▋\x1b[38;2;228;157;84;48;2;234;173;98m▆\x1b[38;2;207;140;80;48;2;225;167;96m▝\x1b[38;2;213;151;88;48;2;193;135;79m▏\x1b[38;2;164;111;60;48;2;104;54;21m▍\x1b[38;2;175;110;52;48;2;136;78;32m▘\x1b[38;2;93;47;15;48;2;26;5;2m▎\x1b[38;2;39;13;4;48;2;54;28;8m▍\x1b[38;2;63;40;13;48;2;67;44;16m▔\x1b[38;2;68;44;15;48;2;65;41;16m▊\x1b[38;2;60;36;11;48;2;63;39;14m▝\x1b[0m',
26
- '\x1b[38;2;12;1;0;48;2;55;33;13m▌\x1b[38;2;92;63;32;48;2;68;43;17m▝\x1b[38;2;75;51;24;48;2;93;65;34m▗\x1b[38;2;88;61;30;48;2;42;18;8m▘\x1b[38;2;62;35;18;48;2;191;150;83m▍\x1b[38;2;186;140;75;48;2;194;138;63m▁\x1b[38;2;189;130;61;48;2;219;157;79m▄\x1b[38;2;191;132;70;48;2;217;159;87m▂\x1b[38;2;179;105;60;48;2;207;146;83m▔\x1b[38;2;171;106;51;48;2;135;79;32m▋\x1b[38;2;64;30;8;48;2;120;69;27m▗\x1b[38;2;56;26;8;48;2;39;13;5m▂\x1b[38;2;44;18;7;48;2;72;44;16m▘\x1b[38;2;72;47;18;48;2;69;44;14m▖\x1b[38;2;70;46;14;48;2;68;44;14m▁\x1b[38;2;65;41;12;48;2;65;41;14m▘\x1b[0m',
27
- '\x1b[38;2;77;56;35;48;2;22;8;3m▂\x1b[38;2;126;100;69;48;2;59;36;15m▃\x1b[38;2;131;105;70;48;2;80;54;27m▄\x1b[38;2;128;103;68;48;2;57;33;14m▄\x1b[38;2;191;174;117;48;2;125;103;69m▝\x1b[38;2;191;164;108;48;2;236;227;160m▞\x1b[38;2;220;202;137;48;2;173;123;63m▃\x1b[38;2;130;85;43;48;2;164;111;58m▄\x1b[38;2;117;68;26;48;2;185;116;58m▆\x1b[38;2;135;80;33;48;2;94;52;15m▘\x1b[38;2;51;28;9;48;2;80;50;16m▂\x1b[38;2;62;33;9;48;2;76;46;14m▘\x1b[38;2;75;50;16;48;2;74;47;15m▗\x1b[38;2;71;46;14;48;2;72;47;15m▝\x1b[38;2;73;48;16;48;2;69;44;14m▏\x1b[38;2;65;41;11;48;2;66;41;15m▆\x1b[0m',
28
- '\x1b[38;2;125;101;70;48;2;159;129;87m▔\x1b[38;2;145;114;71;48;2;124;100;70m▆\x1b[38;2;152;123;81;48;2;121;100;69m▃\x1b[38;2;117;95;60;48;2;129;106;70m▖\x1b[38;2;115;91;61;48;2;131;105;69m▗\x1b[38;2;166;145;103;48;2;140;113;71m▔\x1b[38;2;162;135;87;48;2;231;217;147m▅\x1b[38;2;133;107;71;48;2;199;171;110m▂\x1b[38;2;131;100;59;48;2;107;75;37m▍\x1b[38;2;166;139;88;48;2;67;40;14m▃\x1b[38;2;204;179;121;48;2;39;19;8m▄\x1b[38;2;137;112;73;48;2;52;28;10m▖\x1b[38;2;54;32;10;48;2;76;49;16m▅\x1b[38;2;56;33;9;48;2;74;48;15m▃\x1b[38;2;60;37;10;48;2;70;47;14m▁\x1b[38;2;66;43;12;48;2;64;40;11m▅\x1b[0m',
29
- '\x1b[38;2;157;128;85;48;2;167;138;98m▝\x1b[38;2;141;111;71;48;2;166;136;98m▝\x1b[38;2;149;119;83;48;2;126;96;60m▞\x1b[38;2;157;129;93;48;2;139;113;81m▅\x1b[38;2;144;117;79;48;2;117;92;58m▋\x1b[38;2;130;102;62;48;2;169;138;87m▋\x1b[38;2;171;141;87;48;2;143;117;77m▖\x1b[38;2;144;117;79;48;2;122;96;63m▊\x1b[38;2;132;105;68;48;2;144;117;82m▖\x1b[38;2;153;127;92;48;2;140;115;83m▞\x1b[38;2;134;108;71;48;2;217;193;135m▅\x1b[38;2;176;150;98;48;2;129;105;66m▋\x1b[38;2;118;94;61;48;2;54;32;14m▂\x1b[38;2;44;23;8;48;2;59;37;13m▃\x1b[38;2;62;41;16;48;2;48;26;9m▖\x1b[38;2;46;24;6;48;2;66;42;15m▖\x1b[0m',
22
+ '\x1b[0m\x1b[38;5;16;48;5;16m \x1b[38;5;232m▁\x1b[38;5;235;48;5;232m▂\x1b[38;5;58;48;5;233m▄\x1b[38;5;95;48;5;234m▆\x1b[38;5;137;48;5;58m▄\x1b[38;5;173m▅\x1b[48;5;94m▅\x1b[48;5;58m▆▅\x1b[48;5;237m▄\x1b[38;5;137;48;5;234m▃\x1b[38;5;235;48;5;233m▂ \x1b[38;5;233;48;5;232m▂▅\x1b[48;5;233m \x1b[0m',
23
+ '\x1b[38;5;16;48;5;16m \x1b[38;5;235;48;5;232m▗\x1b[38;5;233;48;5;236m▘\x1b[38;5;8;48;5;239m▌\x1b[38;5;95;48;5;137m▋\x1b[38;5;137;48;5;179m▘ \x1b[38;5;179;48;5;173m▃\x1b[48;5;179m \x1b[48;5;173m▊\x1b[38;5;58;48;5;137m▝\x1b[38;5;94;48;5;235m▖\x1b[38;5;234;48;5;233m▅▄▂ ▂▗▄▃\x1b[0m',
24
+ '\x1b[38;5;16;48;5;16m \x1b[38;5;235;48;5;232m▗\x1b[38;5;236;48;5;237m▍ \x1b[38;5;58;48;5;94m▋\x1b[38;5;95;48;5;173m▌\x1b[48;5;179m \x1b[38;5;179;48;5;215m▍\x1b[48;5;221m▔\x1b[38;5;222;48;5;180m▍\x1b[48;5;179m \x1b[38;5;173m▕\x1b[38;5;179;48;5;173m▅\x1b[38;5;137m▕\x1b[38;5;95;48;5;58m▍\x1b[38;5;58;48;5;235m▖\x1b[38;5;235;48;5;234m▖▃▄ \x1b[0m',
25
+ '\x1b[38;5;16;48;5;16m \x1b[38;5;233m▗\x1b[48;5;235m▏\x1b[38;5;237;48;5;238m▊\x1b[38;5;238;48;5;236m▌\x1b[38;5;236;48;5;58m▖\x1b[38;5;95;48;5;179m▌ \x1b[38;5;137m▗\x1b[38;5;94m▄\x1b[38;5;58m▄\x1b[38;5;94m▄\x1b[38;5;137m▖\x1b[38;5;173m▗\x1b[38;5;131m▗\x1b[38;5;58;48;5;137m▃\x1b[38;5;131;48;5;58m▘\x1b[38;5;234m▕\x1b[48;5;236m▖\x1b[38;5;236;48;5;235m▃ \x1b[38;5;234m▝\x1b[38;5;235;48;5;234m▃\x1b[0m',
26
+ '\x1b[38;5;16;48;5;16m \x1b[38;5;235;48;5;232m▂\x1b[38;5;236;48;5;234m▄\x1b[38;5;237;48;5;236m▗\x1b[38;5;8;48;5;239m▖\x1b[38;5;240;48;5;8m▎\x1b[38;5;94;48;5;236m▕\x1b[38;5;137;48;5;179m▍ \x1b[38;5;94;48;5;137m▝\x1b[38;5;173;48;5;94m▂\x1b[38;5;137;48;5;58m▂\x1b[48;5;94m▃\x1b[48;5;179m▘\x1b[38;5;173m▝\x1b[38;5;137;48;5;235m▍\x1b[38;5;94;48;5;236m▝\x1b[38;5;235;48;5;94m▖\x1b[38;5;52;48;5;58m▖\x1b[38;5;235;48;5;233m▝\x1b[48;5;236m▁\x1b[48;5;235m \x1b[0m',
27
+ '\x1b[38;5;232;48;5;16m▗\x1b[38;5;233;48;5;236m▌\x1b[38;5;95;48;5;239m▅\x1b[48;5;240m▃\x1b[38;5;94;48;5;238m▖\x1b[38;5;240;48;5;8m▝\x1b[38;5;95;48;5;236m▘\x1b[38;5;236;48;5;95m▘\x1b[38;5;173;48;5;179m▏ \x1b[38;5;215m▄ \x1b[38;5;179;48;5;137m▅\x1b[38;5;137;48;5;179m▘\x1b[38;5;216m▘\x1b[38;5;179;48;5;216m▃\x1b[48;5;94m▌\x1b[38;5;94;48;5;131m▘\x1b[38;5;95;48;5;94m▋\x1b[38;5;94;48;5;52m▃\x1b[38;5;52;48;5;233m▎\x1b[38;5;233;48;5;235m▅\x1b[38;5;234m▂ \x1b[0m',
28
+ '\x1b[38;5;233;48;5;232m▕\x1b[38;5;234;48;5;236m▘\x1b[38;5;8;48;5;95m▌ \x1b[38;5;236m▃\x1b[38;5;58;48;5;234m▘\x1b[38;5;94m▝\x1b[48;5;137m▎\x1b[38;5;179;48;5;173m▍\x1b[38;5;173;48;5;179m▌▆▖▃▞\x1b[38;5;94;48;5;173m▗\x1b[48;5;179m▄\x1b[38;5;179;48;5;58m▘\x1b[38;5;94;48;5;52m▝\x1b[38;5;130;48;5;131m▃\x1b[38;5;94;48;5;58m▍\x1b[38;5;52;48;5;232m▎\x1b[38;5;232;48;5;233m▌\x1b[38;5;233;48;5;234m▏\x1b[38;5;234;48;5;235m▎\x1b[38;5;236m▌▅▄ \x1b[0m',
29
+ '\x1b[38;5;232;48;5;235m▋\x1b[38;5;58;48;5;236m▝\x1b[48;5;58m \x1b[38;5;239;48;5;94m▅\x1b[38;5;237;48;5;235m▂\x1b[38;5;235;48;5;233m▂\x1b[38;5;234;48;5;94m▄\x1b[38;5;94;48;5;137m▖\x1b[48;5;173m \x1b[38;5;173;48;5;179m▃ \x1b[38;5;137m▂▃▂\x1b[38;5;131;48;5;137m▃\x1b[38;5;58;48;5;131m▝\x1b[38;5;94;48;5;52m▅\x1b[48;5;94m \x1b[48;5;58m▍\x1b[38;5;235;48;5;232m▎\x1b[38;5;232;48;5;233m▋\x1b[38;5;233;48;5;234m▍\x1b[38;5;235;48;5;236m▏ \x1b[38;5;236;48;5;235m▎ \x1b[0m',
30
+ '\x1b[38;5;234;48;5;235m▏\x1b[38;5;236;48;5;237m▋\x1b[38;5;237;48;5;8m▃\x1b[38;5;235;48;5;238m▗\x1b[38;5;237m▖\x1b[38;5;58;48;5;234m▌\x1b[38;5;234;48;5;233m▎\x1b[38;5;236;48;5;137m▎\x1b[38;5;137;48;5;173m▄ \x1b[38;5;173;48;5;179m▄▃ \x1b[38;5;179;48;5;215m▅\x1b[38;5;173;48;5;179m▄\x1b[38;5;179;48;5;137m▘\x1b[38;5;137;48;5;131m▌\x1b[48;5;94m \x1b[38;5;58m▗\x1b[38;5;233;48;5;58m▗\x1b[48;5;233m \x1b[38;5;234;48;5;236m▘ \x1b[38;5;236;48;5;235m▃▞\x1b[38;5;235;48;5;236m▄\x1b[48;5;235m \x1b[0m',
31
+ '\x1b[38;5;234;48;5;235m▏▆\x1b[38;5;235;48;5;237m▌\x1b[38;5;236m▝\x1b[38;5;237;48;5;234m▍\x1b[38;5;234;48;5;233m▖\x1b[38;5;240;48;5;234m▗\x1b[38;5;101;48;5;186m▌\x1b[38;5;137m▝\x1b[48;5;137m \x1b[48;5;173m▆▄▃\x1b[38;5;131m▂\x1b[38;5;130;48;5;137m▂\x1b[38;5;58;48;5;94m▃\x1b[48;5;58m \x1b[38;5;234;48;5;233m▏\x1b[38;5;235;48;5;234m▅\x1b[48;5;236m▌ ▝ \x1b[48;5;235m \x1b[0m',
32
+ '\x1b[38;5;234;48;5;233m▕\x1b[38;5;239;48;5;235m▂\x1b[38;5;95m▃\x1b[48;5;237m▄\x1b[48;5;236m▄\x1b[48;5;235m▄\x1b[38;5;236;48;5;240m▘\x1b[38;5;101;48;5;95m▕\x1b[48;5;186m▖\x1b[38;5;179;48;5;229m▝\x1b[38;5;223;48;5;137m▃\x1b[38;5;137;48;5;131m▁\x1b[38;5;95m▅\x1b[38;5;94m▂\x1b[48;5;94m \x1b[38;5;58m▗\x1b[38;5;94;48;5;58m▔\x1b[38;5;236m▁ \x1b[48;5;235m▆\x1b[38;5;235;48;5;236m▍\x1b[38;5;236;48;5;235m▆\x1b[48;5;236m \x1b[38;5;235m▅\x1b[48;5;235m \x1b[0m',
33
+ '\x1b[38;5;237;48;5;95m▔ \x1b[38;5;137;48;5;101m▝\x1b[48;5;187m▅\x1b[38;5;180;48;5;229m▂\x1b[38;5;143;48;5;222m▔\x1b[38;5;186;48;5;58m▅\x1b[38;5;179m▂\x1b[38;5;95m▁\x1b[38;5;235m▂\x1b[38;5;236m▄\x1b[48;5;233m▌\x1b[38;5;235m▔\x1b[38;5;233;48;5;236m▅\x1b[38;5;234m▃\x1b[38;5;235m▁ ▔\x1b[48;5;235m \x1b[0m',
34
+ '\x1b[38;5;101;48;5;137m▔\x1b[38;5;95;48;5;101m▄▔\x1b[38;5;101;48;5;95m▄ ▗ \x1b[38;5;240m▖\x1b[38;5;95;48;5;101m▘\x1b[38;5;137m▔\x1b[48;5;222m▅\x1b[48;5;186m▃\x1b[48;5;179m▂\x1b[38;5;101;48;5;95m▌\x1b[48;5;58m \x1b[38;5;238;48;5;236m▁\x1b[38;5;180;48;5;234m▃\x1b[48;5;235m▄\x1b[38;5;179;48;5;234m▃\x1b[38;5;95m▁\x1b[38;5;234;48;5;235m▊\x1b[48;5;236m▆\x1b[38;5;235m▃\x1b[38;5;234m▂\x1b[38;5;235m▁ \x1b[38;5;236;48;5;235m▎\x1b[0m',
35
+ '\x1b[38;5;137;48;5;137m \x1b[48;5;95m▄ \x1b[38;5;95;48;5;101m▖\x1b[48;5;137m▝\x1b[48;5;95m \x1b[38;5;101m▅\x1b[48;5;239m▋\x1b[48;5;95m \x1b[38;5;95;48;5;137m▋\x1b[38;5;101;48;5;95m▍\x1b[38;5;95;48;5;101m▖\x1b[38;5;101;48;5;95m▆\x1b[38;5;239m▗\x1b[38;5;101m▄ \x1b[38;5;95;48;5;137m▅\x1b[38;5;137;48;5;180m▅\x1b[38;5;180;48;5;186m▃\x1b[48;5;143m▆\x1b[38;5;95m▔\x1b[38;5;143;48;5;235m▖\x1b[48;5;234m \x1b[38;5;235m▆\x1b[38;5;234;48;5;235m▝\x1b[38;5;235;48;5;234m▞\x1b[38;5;234;48;5;235m▄ \x1b[0m',
30
36
  ];
31
37
  // ─── FRANKLIN text banner (gold → emerald gradient) ────────────────────────
32
38
  //
@@ -63,9 +69,9 @@ function interpolateHex(start, end, t) {
63
69
  }
64
70
  // ─── Banner layout ─────────────────────────────────────────────────────────
65
71
  // Minimum terminal width to show the side-by-side portrait + text layout.
66
- // The portrait is ~16 chars, the FRANKLIN text is ~65 chars, plus a 3-char
67
- // gap = 84 chars. We round up to 85 cols as the threshold.
68
- const MIN_WIDTH_FOR_PORTRAIT = 85;
72
+ // The portrait is ~28 chars, the FRANKLIN text is ~65 chars, plus a 3-char
73
+ // gap = 96 chars. We add a small margin so 100 cols is the threshold.
74
+ const MIN_WIDTH_FOR_PORTRAIT = 100;
69
75
  /**
70
76
  * Pad a line to an exact visual width, ignoring ANSI escape codes when
71
77
  * measuring. Used to align the portrait's right edge before the text block.
@@ -93,21 +99,28 @@ export function printBanner(version) {
93
99
  }
94
100
  /**
95
101
  * Full layout: Ben Franklin portrait on the left, FRANKLIN text block on the
96
- * right. Portrait is 8 rows × ~16 chars, text is 6 rows — text is vertically
97
- * centred inside the portrait with 1 row of padding above.
102
+ * right. Portrait is 14 rows × ~28 chars, text is 6 rows — text is vertically
103
+ * centred inside the portrait with 4 rows of padding above and 4 below,
104
+ * tagline sitting right under the FRANKLIN block.
98
105
  *
99
- * [portrait row 1] (empty)
100
- * [portrait row 2] ███████╗██████╗ █████╗ ...
101
- * [portrait row 3] ██╔════╝██╔══██╗██╔══██╗...
102
- * [portrait row 4] █████╗ ██████╔╝███████║...
103
- * [portrait row 5] ██╔══╝ ██╔══██╗██╔══██║...
104
- * [portrait row 6] ██║ ██║ ██║██║ ██║...
105
- * [portrait row 7] ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝...
106
- * [portrait row 8] blockrun.ai · The AI agent with a wallet · vX
106
+ * [portrait row 1] (empty)
107
+ * [portrait row 2] (empty)
108
+ * [portrait row 3] (empty)
109
+ * [portrait row 4] (empty)
110
+ * [portrait row 5] ███████╗██████╗ █████╗ ...
111
+ * [portrait row 6] ██╔════╝██╔══██╗██╔══██╗...
112
+ * [portrait row 7] █████╗ ██████╔╝███████║...
113
+ * [portrait row 8] ██╔══╝ ██╔══██╗██╔══██║...
114
+ * [portrait row 9] ██║ ██║ ██║██║ ██║...
115
+ * [portrait row 10] ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝...
116
+ * [portrait row 11] blockrun.ai · The AI agent with a wallet · vX
117
+ * [portrait row 12] (empty)
118
+ * [portrait row 13] (empty)
119
+ * [portrait row 14] (empty)
107
120
  */
108
121
  function printSideBySide(version) {
109
- const TEXT_TOP_OFFSET = 1; // rows of portrait above the text
110
- const PORTRAIT_WIDTH = 17; // columns (char width) of the portrait + 1 pad
122
+ const TEXT_TOP_OFFSET = 4; // rows of portrait above the text
123
+ const PORTRAIT_WIDTH = 29; // columns (char width) of the portrait + 1 pad
111
124
  const GAP = ' '; // gap between portrait and text
112
125
  const portraitRows = BEN_PORTRAIT_ROWS;
113
126
  const textRows = FRANKLIN_ART.length;
@@ -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>;