phewsh 0.15.59 → 0.15.61
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/commands/session.js +74 -18
- package/lib/md.js +10 -1
- package/package.json +1 -1
package/commands/session.js
CHANGED
|
@@ -73,6 +73,35 @@ function copyToClipboard(text) {
|
|
|
73
73
|
return false;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Show the cross-harness handoff — the heart of phewsh's continuity promise:
|
|
77
|
+
// the verified brief that travels to whatever AI tool you pick up next, now
|
|
78
|
+
// rendered as readable markdown instead of raw text and saved so the next tool
|
|
79
|
+
// (or the next phewsh session) inherits it. Best-effort and fail-soft; a
|
|
80
|
+
// handoff that can't generate must never block exit or a switch.
|
|
81
|
+
async function showHandoff({ projectName, route, reason = '', nextHint = true } = {}) {
|
|
82
|
+
let result = { shown: false, file: null };
|
|
83
|
+
try {
|
|
84
|
+
const { content } = await generateBrief();
|
|
85
|
+
const { renderMarkdown } = require('../lib/md');
|
|
86
|
+
const saved = persistBrief(content, { project: projectName, route: route || 'unknown' });
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(` ${teal('●')} ${b(cream('Handoff ready'))}${reason ? ' ' + slate('— ' + reason) : ''}`);
|
|
89
|
+
console.log(` ${slate('This is exactly what travels to the next AI tool — nothing re-explained.')}`);
|
|
90
|
+
ui.divider('line');
|
|
91
|
+
console.log(renderMarkdown(content));
|
|
92
|
+
ui.divider('line');
|
|
93
|
+
if (saved.written) console.log(` ${slate('saved · ' + saved.file.replace(require('os').homedir(), '~'))}`);
|
|
94
|
+
copyToClipboard(content);
|
|
95
|
+
console.log(` ${slate('copied to clipboard · paste into any tool, or')} ${cream('/use codex')} ${slate('to continue here')}`);
|
|
96
|
+
if (nextHint) console.log(` ${slate('full verified brief anytime:')} ${cream('/brief')} ${slate('·')} ${cream('/handoff')}`);
|
|
97
|
+
console.log('');
|
|
98
|
+
result = { shown: true, file: saved.file };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.log(` ${ember('!')} ${slate('Could not build the handoff: ' + err.message)}`);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
76
105
|
// Sync awareness: compare local .intent/ timestamps with cloud updated_at
|
|
77
106
|
async function checkSyncStatus(config) {
|
|
78
107
|
if (!config?.supabaseUserId || !config?.supabaseAccessToken) return null;
|
|
@@ -397,27 +426,31 @@ async function main() {
|
|
|
397
426
|
// ── The Exhale: animated brand reveal ──────────────────
|
|
398
427
|
await ui.brandReveal();
|
|
399
428
|
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
|
|
429
|
+
// The welcome beat — warm, plain-language, and continuity-first. This is the
|
|
430
|
+
// "you made it" moment that frames what phewsh is FOR: one thread across every
|
|
431
|
+
// AI tool, so you switch harnesses mid-work and nothing gets re-explained.
|
|
432
|
+
// Non-technical readers get one inviting sentence; the cockpit rows below give
|
|
433
|
+
// engineers the exact facts. Fail-soft: a missing piece shortens the line, it
|
|
434
|
+
// never blocks the door.
|
|
435
|
+
(function welcome() {
|
|
405
436
|
try {
|
|
406
437
|
const toolCount = harnesses.filter(h => h.installed).length;
|
|
407
438
|
const hasProject = intentFiles.length > 0;
|
|
408
439
|
const dot = teal('●');
|
|
409
440
|
const tools = `${toolCount} AI tool${toolCount !== 1 ? 's' : ''}`;
|
|
410
|
-
if (hasProject && toolCount >
|
|
411
|
-
console.log(` ${dot} ${sage(
|
|
441
|
+
if (hasProject && toolCount > 1) {
|
|
442
|
+
console.log(` ${dot} ${sage('Welcome back. One shared memory of')} ${cream(projectName)}${sage(`, and ${tools} that all read it`)} ${slate('— switch tools mid-thought, nothing re-explained.')}`);
|
|
412
443
|
} else if (hasProject) {
|
|
413
|
-
console.log(` ${dot} ${sage('
|
|
414
|
-
} else if (toolCount >
|
|
415
|
-
console.log(` ${dot} ${sage(
|
|
444
|
+
console.log(` ${dot} ${sage('Welcome back. One verified memory of')} ${cream(projectName)} ${sage('— ready for whatever AI tool you reach for next.')}`);
|
|
445
|
+
} else if (toolCount > 1) {
|
|
446
|
+
console.log(` ${dot} ${sage(`Welcome — you made it. ${tools} here; this is where they share one thread.`)} ${slate('/init')} ${sage('to begin.')}`);
|
|
447
|
+
} else if (toolCount === 1) {
|
|
448
|
+
console.log(` ${dot} ${sage('Welcome — you made it. Add Codex or Gemini and phewsh keeps every tool on the same page.')}`);
|
|
416
449
|
} else {
|
|
417
|
-
console.log(` ${dot} ${sage('Install Claude Code, Codex, or Gemini
|
|
450
|
+
console.log(` ${dot} ${sage('Welcome — you made it. Install Claude Code, Codex, or Gemini and phewsh keeps them all on the same page.')}`);
|
|
418
451
|
}
|
|
419
452
|
console.log('');
|
|
420
|
-
} catch { /* the
|
|
453
|
+
} catch { /* the welcome is a flourish, never a blocker */ }
|
|
421
454
|
})();
|
|
422
455
|
|
|
423
456
|
if (config?.apiKey && !config.apiKey.startsWith('sk-')) {
|
|
@@ -554,6 +587,10 @@ async function main() {
|
|
|
554
587
|
console.log(` ${b(cream('What are you trying to do?'))}`);
|
|
555
588
|
console.log(` ${teal('1')} ${sage('Build')} ${slate('·')} ${teal('2')} ${sage('Research')} ${slate('·')} ${teal('3')} ${sage('Decide')} ${slate('·')} ${teal('4')} ${sage('Review')} ${slate('·')} ${teal('5')} ${sage('Ask another model')}`);
|
|
556
589
|
console.log(` ${slate('pick a number, or just type — your context travels with every route')}`);
|
|
590
|
+
// One quiet line to the deeper power, without dumping the whole command list:
|
|
591
|
+
// the moves that make phewsh more than a chat box — true continuity across
|
|
592
|
+
// harnesses. /help opens everything when they're ready.
|
|
593
|
+
console.log(` ${slate('⌄ deeper:')} ${cream('/work')} ${slate('hand to a tool')} ${slate('·')} ${cream('/council')} ${slate('ask them all')} ${slate('·')} ${cream('/handoff')} ${slate('what carries over')} ${slate('·')} ${cream('/help')} ${slate('all')}`);
|
|
557
594
|
}
|
|
558
595
|
|
|
559
596
|
// Self-aware guidance: snapshot the session's state so phewsh can recommend
|
|
@@ -637,6 +674,7 @@ async function main() {
|
|
|
637
674
|
const tools = continuity.toolsInThread(decisions, { project: projectName });
|
|
638
675
|
const span = tools >= 2 ? slate(` · ${tools} tools, one thread`) : '';
|
|
639
676
|
console.log(` ${teal('↻')} ${sage('Picking up — ' + line)}${span} ${slate('· /thread')}`);
|
|
677
|
+
console.log(''); // breathe before "What are you trying to do?"
|
|
640
678
|
} catch { /* continuity is a nicety, never a blocker */ }
|
|
641
679
|
}
|
|
642
680
|
|
|
@@ -704,10 +742,8 @@ async function main() {
|
|
|
704
742
|
console.log('');
|
|
705
743
|
if (!route) {
|
|
706
744
|
// Nothing to route through: no key, no agent CLIs found on this machine.
|
|
707
|
-
console.log(` ${b(cream('
|
|
708
|
-
console.log('');
|
|
709
|
-
console.log(` ${sage('No agent CLI found (Claude Code, Codex, Gemini, Cursor, OpenCode)')}`);
|
|
710
|
-
console.log(` ${sage('and no API key set. Either one gets you running:')}`);
|
|
745
|
+
console.log(` ${b(cream('Two ways to get running:'))}`);
|
|
746
|
+
console.log(` ${slate('No agent CLI found (Claude Code, Codex, Gemini, Cursor, OpenCode) and no API key yet.')}`);
|
|
711
747
|
console.log('');
|
|
712
748
|
console.log(` ${teal('/key')} ${sage('Set an API key (10 seconds)')}`);
|
|
713
749
|
console.log(` ${teal('/tour')} ${sage('See what this does (nothing needed)')}`);
|
|
@@ -938,7 +974,7 @@ async function main() {
|
|
|
938
974
|
'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
|
|
939
975
|
'agents', 'context', 'truth', 'brief', 'wrap', 'reconcile', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
|
|
940
976
|
'next', 'recommend', 'guide', 'thread', 'continuity', 'learn', 'stats',
|
|
941
|
-
'pack', 'packs', 'remember', 'commands', 'width',
|
|
977
|
+
'pack', 'packs', 'remember', 'commands', 'width', 'handoff',
|
|
942
978
|
]);
|
|
943
979
|
const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
|
|
944
980
|
let turnAbort = null; // AbortController while an API turn streams
|
|
@@ -1346,6 +1382,12 @@ async function main() {
|
|
|
1346
1382
|
const h = selfheal.heal();
|
|
1347
1383
|
if (h.healed) console.log(` ${teal('↻')} ${sage("Harness context refreshed from .intent/ — you didn't have to")}`);
|
|
1348
1384
|
} catch { /* self-heal must never block exit */ }
|
|
1385
|
+
// The handoff is given, not asked for: leaving a project hands you the
|
|
1386
|
+
// verified brief that the next AI tool inherits — the continuity promise
|
|
1387
|
+
// made visible on the way out. Only in a real project; never blocks exit.
|
|
1388
|
+
if (intentFiles.length > 0) {
|
|
1389
|
+
await showHandoff({ projectName, route: route?.id, reason: 'carry this into your next tool', nextHint: false });
|
|
1390
|
+
}
|
|
1349
1391
|
try { require('../lib/intro').farewell(); } catch { /* sign-off is a nicety */ }
|
|
1350
1392
|
console.log(` ${sage('session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}`);
|
|
1351
1393
|
if (decisionsThisSession > 0) {
|
|
@@ -1658,6 +1700,7 @@ async function main() {
|
|
|
1658
1700
|
console.log('');
|
|
1659
1701
|
console.log(` ${cream('session')}`);
|
|
1660
1702
|
console.log(` ${teal('/work')} ${slate('[harness]')} ${sage('Preflight, brief, native handoff, automatic postflight')}`);
|
|
1703
|
+
console.log(` ${teal('/handoff')} ${sage('See the verified brief the next AI tool inherits — rendered + saved')}`);
|
|
1661
1704
|
console.log(` ${teal('/switch')} ${slate('<harness>')} ${sage('Launch another native tool with a freshly verified briefing')}`);
|
|
1662
1705
|
console.log(` ${teal('/run')} ${slate('<prompt>')} ${sage('One-shot prompt (no history)')}`);
|
|
1663
1706
|
console.log(` ${teal('esc')} ${sage('Cancel a running turn · clear the input line')}`);
|
|
@@ -1785,8 +1828,20 @@ async function main() {
|
|
|
1785
1828
|
if (cmd === 'brief') {
|
|
1786
1829
|
console.log('');
|
|
1787
1830
|
const { content } = await generateBrief();
|
|
1788
|
-
|
|
1831
|
+
const { renderMarkdown } = require('../lib/md');
|
|
1832
|
+
console.log(renderMarkdown(content));
|
|
1789
1833
|
console.log('');
|
|
1834
|
+
console.log(` ${slate('this is your handoff — it travels to the next AI tool ·')} ${cream('/handoff')} ${slate('to save + copy it')}`);
|
|
1835
|
+
console.log('');
|
|
1836
|
+
rl.prompt();
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// /handoff — explicitly produce, render, save, and copy the cross-harness
|
|
1841
|
+
// handoff so you can SEE what would carry into the next tool.
|
|
1842
|
+
if (cmd === 'handoff') {
|
|
1843
|
+
console.log(` ${sage('Building your handoff…')}`);
|
|
1844
|
+
await showHandoff({ projectName, route: route?.id, reason: 'what the next AI tool will pick up' });
|
|
1790
1845
|
rl.prompt();
|
|
1791
1846
|
return;
|
|
1792
1847
|
}
|
|
@@ -2661,6 +2716,7 @@ async function main() {
|
|
|
2661
2716
|
} else {
|
|
2662
2717
|
console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate('No changes detected. 1 kept · 2 undid · 3 redid · 4 flopped · /reconcile · /switch <tool>')}`);
|
|
2663
2718
|
}
|
|
2719
|
+
console.log(` ${slate('continuing elsewhere?')} ${cream('/handoff')} ${slate('shows the updated brief the next tool inherits')}`);
|
|
2664
2720
|
console.log('');
|
|
2665
2721
|
rl.prompt();
|
|
2666
2722
|
return;
|
package/lib/md.js
CHANGED
|
@@ -120,4 +120,13 @@ function streamRenderer(write) {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
// Render a complete markdown string to styled, word-wrapped terminal text —
|
|
124
|
+
// the non-streaming counterpart to streamRenderer, for content we already have
|
|
125
|
+
// in full (a handoff brief, a saved record). Carries fence state across lines
|
|
126
|
+
// so code blocks render literally.
|
|
127
|
+
function renderMarkdown(md) {
|
|
128
|
+
const state = { inFence: false };
|
|
129
|
+
return String(md).split('\n').map((line) => renderLine(line, state)).join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { inlineMd, renderLine, streamRenderer, renderMarkdown };
|