@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.
- package/README.md +216 -233
- package/dist/agent/commands.js +24 -12
- package/dist/agent/context.js +18 -1
- package/dist/agent/loop.js +48 -19
- package/dist/banner.js +40 -27
- package/dist/commands/migrate.d.ts +13 -0
- package/dist/commands/migrate.js +389 -0
- package/dist/commands/panel.d.ts +6 -0
- package/dist/commands/panel.js +29 -0
- package/dist/commands/start.js +12 -4
- package/dist/index.js +15 -0
- package/dist/mcp/client.js +9 -2
- package/dist/panel/html.d.ts +5 -0
- package/dist/panel/html.js +341 -0
- package/dist/panel/server.d.ts +7 -0
- package/dist/panel/server.js +152 -0
- package/dist/session/storage.js +4 -2
- package/dist/stats/tracker.d.ts +1 -0
- package/dist/stats/tracker.js +59 -13
- package/dist/tools/bash.js +6 -1
- package/dist/tools/index.js +0 -4
- package/dist/tools/webfetch.js +19 -9
- package/dist/tools/write.js +2 -0
- package/dist/ui/app.js +73 -44
- package/dist/ui/markdown.d.ts +9 -0
- package/dist/ui/markdown.js +86 -0
- package/package.json +1 -1
package/dist/agent/commands.js
CHANGED
|
@@ -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
|
-
|
|
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>
|
|
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' ||
|
|
491
|
-
|
|
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
|
|
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 => {
|
package/dist/agent/context.js
CHANGED
|
@@ -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();
|
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
11
|
-
// (sips --cropToHeightWidth
|
|
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=
|
|
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: ~
|
|
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;
|
|
23
|
-
'\x1b[
|
|
24
|
-
'\x1b[38;
|
|
25
|
-
'\x1b[38;
|
|
26
|
-
'\x1b[38;
|
|
27
|
-
'\x1b[38;
|
|
28
|
-
'\x1b[38;
|
|
29
|
-
'\x1b[38;
|
|
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 ~
|
|
67
|
-
// gap =
|
|
68
|
-
const MIN_WIDTH_FOR_PORTRAIT =
|
|
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
|
|
97
|
-
* centred inside the portrait with
|
|
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
|
|
100
|
-
* [portrait row
|
|
101
|
-
* [portrait row
|
|
102
|
-
* [portrait row
|
|
103
|
-
* [portrait row
|
|
104
|
-
* [portrait row
|
|
105
|
-
* [portrait row
|
|
106
|
-
* [portrait row
|
|
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 =
|
|
110
|
-
const PORTRAIT_WIDTH =
|
|
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>;
|