phewsh 0.14.2 → 0.14.3
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 +61 -13
- package/lib/harnesses.js +11 -11
- package/lib/outcomes.js +9 -2
- package/lib/projects-index.js +6 -1
- package/package.json +1 -1
package/commands/session.js
CHANGED
|
@@ -291,13 +291,6 @@ async function main() {
|
|
|
291
291
|
// PROJECT what am I in · ROUTE where typing goes · BACKUP what's ready if
|
|
292
292
|
// the route hits a wall · WEB am I mirrored · RECORD what's accumulated
|
|
293
293
|
let syncState = null;
|
|
294
|
-
if (config?.supabaseUserId && intentFiles.length > 0) {
|
|
295
|
-
syncState = await Promise.race([
|
|
296
|
-
checkSyncStatus(config),
|
|
297
|
-
new Promise(resolve => setTimeout(() => resolve(null), 3000)),
|
|
298
|
-
]);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
294
|
const row = (label, value) => console.log(` ${slate(label.padEnd(9))}${value}`);
|
|
302
295
|
|
|
303
296
|
// realpath both sides — macOS /tmp and /var are symlinks into /private
|
|
@@ -307,8 +300,21 @@ async function main() {
|
|
|
307
300
|
const rp = realPath(p);
|
|
308
301
|
return rp.startsWith(home) ? '~' + rp.slice(home.length) : p;
|
|
309
302
|
};
|
|
310
|
-
|
|
311
|
-
|
|
303
|
+
let atHome = false;
|
|
304
|
+
let recents = [];
|
|
305
|
+
|
|
306
|
+
// Fail-soft render: corrupt data or a network hiccup may cost a row —
|
|
307
|
+
// it must never kill the session.
|
|
308
|
+
try {
|
|
309
|
+
if (config?.supabaseUserId && intentFiles.length > 0) {
|
|
310
|
+
syncState = await Promise.race([
|
|
311
|
+
checkSyncStatus(config),
|
|
312
|
+
new Promise(resolve => setTimeout(() => resolve(null), 3000)),
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
atHome = realPath(process.cwd()) === realPath(os.homedir());
|
|
317
|
+
recents = intentFiles.length === 0
|
|
312
318
|
? listProjects().filter(p => realPath(p.path) !== realPath(process.cwd())).slice(0, 3)
|
|
313
319
|
: [];
|
|
314
320
|
|
|
@@ -371,6 +377,18 @@ async function main() {
|
|
|
371
377
|
} else {
|
|
372
378
|
row('RECORD', slate('empty — decisions and outcomes accumulate as you work'));
|
|
373
379
|
}
|
|
380
|
+
} catch (cockpitErr) {
|
|
381
|
+
console.log(` ${slate('(cockpit row unavailable — ' + cockpitErr.message + ' · PHEWSH_DEBUG=1 phewsh for details)')}`);
|
|
382
|
+
if (process.env.PHEWSH_DEBUG) console.error(cockpitErr.stack);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Chat-routable options as they exist on THIS machine — every usage hint
|
|
386
|
+
// derives from this so /use, /provider, and reality never disagree.
|
|
387
|
+
function useOptions() {
|
|
388
|
+
const opts = harnesses.filter(h => h.installed && h.headless).map(h => h.id);
|
|
389
|
+
if (config?.apiKey) opts.push('api');
|
|
390
|
+
return opts;
|
|
391
|
+
}
|
|
374
392
|
|
|
375
393
|
function showModeMenu() {
|
|
376
394
|
console.log(` ${b(cream('What are you trying to do?'))}`);
|
|
@@ -704,6 +722,7 @@ async function main() {
|
|
|
704
722
|
console.log(` ${teal('/use')} ${slate('<route>')} ${sage('Switch: claude-code, codex, gemini, cursor, opencode, api')}`);
|
|
705
723
|
console.log(` ${teal('/harnesses')} ${sage('Agent CLIs detected on this machine')}`);
|
|
706
724
|
console.log(` ${teal('/provider')} ${sage('Current route + what\'s available')}`);
|
|
725
|
+
console.log(` ${teal('/fallback')} ${sage('What happens at a usage wall: ask or auto-switch')}`);
|
|
707
726
|
console.log(` ${teal('/outcomes')} ${sage('Decision record — kept/reverted/superseded/failed')}`);
|
|
708
727
|
console.log('');
|
|
709
728
|
console.log(` ${cream('session')}`);
|
|
@@ -1091,22 +1110,49 @@ async function main() {
|
|
|
1091
1110
|
['Route', routeLabel(route, config), 'green'],
|
|
1092
1111
|
];
|
|
1093
1112
|
for (const h of harnesses) {
|
|
1094
|
-
|
|
1113
|
+
if (!h.installed && !['aider', 'goose', 'amp', 'droid'].includes(h.id)) {
|
|
1114
|
+
rows.push([h.label, 'not installed']);
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (!h.installed) continue; // hide the long tail of uninstalled extras
|
|
1118
|
+
const via = h.headless ? '' : ' · /work only';
|
|
1119
|
+
rows.push([h.label, `ready — ${h.role}${via}`, 'green']);
|
|
1095
1120
|
}
|
|
1096
1121
|
rows.push(['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '... (' + (config.provider || 'anthropic') + ')' : 'not set — optional', config?.apiKey ? 'green' : 'yellow']);
|
|
1097
|
-
rows.push(['Fallback', config?.fallback === 'auto' ? 'auto-switch on failure' : 'ask before switching', 'peach']);
|
|
1122
|
+
rows.push(['Fallback', (config?.fallback === 'auto' ? 'auto-switch on failure' : 'ask before switching') + ' — /fallback to change', 'peach']);
|
|
1098
1123
|
if (route?.type === 'api') rows.push(['Model', MODELS[currentModel].name, 'cyan']);
|
|
1099
1124
|
ui.statusPanel('Provider', rows);
|
|
1100
|
-
console.log(` ${
|
|
1125
|
+
console.log(` ${sage('One terminal. Every AI worker. Shared project memory.')}`);
|
|
1126
|
+
console.log(` ${slate('switch:')} ${cream('/use <' + useOptions().join('|') + '>')} ${slate('· interactive tools: /work <hermes|pi>')}`);
|
|
1101
1127
|
console.log('');
|
|
1102
1128
|
rl.prompt();
|
|
1103
1129
|
return;
|
|
1104
1130
|
}
|
|
1105
1131
|
|
|
1132
|
+
if (cmd === 'fallback') {
|
|
1133
|
+
const arg = cmdArg?.trim().toLowerCase();
|
|
1134
|
+
if (arg === 'ask' || arg === 'auto') {
|
|
1135
|
+
config = loadConfig() || {};
|
|
1136
|
+
config.fallback = arg;
|
|
1137
|
+
saveConfig(config);
|
|
1138
|
+
console.log(` ${teal('●')} ${sage('Fallback:')} ${cream(arg === 'auto' ? 'auto-switch to the next route on failure' : 'ask before switching')}`);
|
|
1139
|
+
console.log(` ${slate('either way your project context and record stay intact')}`);
|
|
1140
|
+
} else {
|
|
1141
|
+
console.log(` ${sage('Fallback is')} ${cream(config?.fallback === 'auto' ? 'auto-switch' : 'ask first')} ${slate('— when your route hits a usage wall, context travels to the next one.')}`);
|
|
1142
|
+
console.log(` ${sage('Usage:')} ${cream('/fallback ask')} ${slate('·')} ${cream('/fallback auto')}`);
|
|
1143
|
+
}
|
|
1144
|
+
rl.prompt();
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1106
1148
|
if (cmd === 'use') {
|
|
1107
1149
|
if (!cmdArg) {
|
|
1108
1150
|
console.log(` ${sage('Current route:')} ${cream(routeLabel(route, config))}`);
|
|
1109
|
-
console.log(` ${sage('Usage:')} ${cream('/use <
|
|
1151
|
+
console.log(` ${sage('Usage:')} ${cream('/use <' + useOptions().join('|') + '>')}`);
|
|
1152
|
+
const workOnlyInstalled = harnesses.filter(h => h.installed && !h.headless);
|
|
1153
|
+
if (workOnlyInstalled.length > 0) {
|
|
1154
|
+
console.log(` ${slate('interactive tools: /work <' + workOnlyInstalled.map(h => h.id).join('|') + '>')}`);
|
|
1155
|
+
}
|
|
1110
1156
|
rl.prompt();
|
|
1111
1157
|
return;
|
|
1112
1158
|
}
|
|
@@ -1364,5 +1410,7 @@ async function main() {
|
|
|
1364
1410
|
|
|
1365
1411
|
main().catch(err => {
|
|
1366
1412
|
console.error('\n Error:', err.message);
|
|
1413
|
+
if (process.env.PHEWSH_DEBUG) console.error('\n' + err.stack);
|
|
1414
|
+
else console.error(' (run PHEWSH_DEBUG=1 phewsh for the full trace)');
|
|
1367
1415
|
process.exit(1);
|
|
1368
1416
|
});
|
package/lib/harnesses.js
CHANGED
|
@@ -15,17 +15,17 @@ const { execSync, spawn } = require('child_process');
|
|
|
15
15
|
// how to launch it interactively (detection + /work still fully supported —
|
|
16
16
|
// never guess flags; a wrong invocation looks like phewsh being broken).
|
|
17
17
|
const HARNESSES = {
|
|
18
|
-
'claude-code': { bin: 'claude', label: 'Claude Code', auth: 'Claude subscription / Console', args: (p) => ['-p', p, '--output-format', 'text'] },
|
|
19
|
-
'codex': { bin: 'codex', label: 'Codex CLI', auth: 'ChatGPT plan', args: (p) => ['exec', p] },
|
|
20
|
-
'gemini': { bin: 'gemini', label: 'Gemini CLI', auth: 'Google login', args: (p) => ['-p', p] },
|
|
21
|
-
'cursor': { bin: 'cursor-agent', label: 'Cursor Agent', auth: 'Cursor account', args: (p) => ['-p', p, '--output-format', 'text'] },
|
|
22
|
-
'opencode': { bin: 'opencode', label: 'OpenCode', auth: 'OpenCode Zen / configured', args: (p) => ['run', p] },
|
|
23
|
-
'hermes': { bin: 'hermes', label: 'Hermes', auth: 'Nous account', args: null },
|
|
24
|
-
'pi': { bin: 'pi', label: 'Pi', auth: 'Pi login', args: null },
|
|
25
|
-
'aider': { bin: 'aider', label: 'Aider', auth: 'configured keys', args: (p) => ['--message', p] },
|
|
26
|
-
'goose': { bin: 'goose', label: 'Goose', auth: 'Block / configured', args: (p) => ['run', '-t', p] },
|
|
27
|
-
'amp': { bin: 'amp', label: 'Amp', auth: 'Sourcegraph account', args: (p) => ['-x', p] },
|
|
28
|
-
'droid': { bin: 'droid', label: 'Droid', auth: 'Factory account', args: (p) => ['exec', p] },
|
|
18
|
+
'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', auth: 'Claude subscription / Console', args: (p) => ['-p', p, '--output-format', 'text'] },
|
|
19
|
+
'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', auth: 'ChatGPT plan', args: (p) => ['exec', p] },
|
|
20
|
+
'gemini': { bin: 'gemini', label: 'Gemini CLI', role: "another model's take", auth: 'Google login', args: (p) => ['-p', p] },
|
|
21
|
+
'cursor': { bin: 'cursor-agent', label: 'Cursor Agent', role: 'edits files', auth: 'Cursor account', args: (p) => ['-p', p, '--output-format', 'text'] },
|
|
22
|
+
'opencode': { bin: 'opencode', label: 'OpenCode', role: 'general agent', auth: 'OpenCode Zen / configured', args: (p) => ['run', p] },
|
|
23
|
+
'hermes': { bin: 'hermes', label: 'Hermes', role: 'runs loops', auth: 'Nous account', args: null },
|
|
24
|
+
'pi': { bin: 'pi', label: 'Pi', role: 'conversation', auth: 'Pi login', args: null },
|
|
25
|
+
'aider': { bin: 'aider', label: 'Aider', role: 'pair-codes', auth: 'configured keys', args: (p) => ['--message', p] },
|
|
26
|
+
'goose': { bin: 'goose', label: 'Goose', role: 'automates tasks', auth: 'Block / configured', args: (p) => ['run', '-t', p] },
|
|
27
|
+
'amp': { bin: 'amp', label: 'Amp', role: 'agentic coding', auth: 'Sourcegraph account', args: (p) => ['-x', p] },
|
|
28
|
+
'droid': { bin: 'droid', label: 'Droid', role: 'agentic coding', auth: 'Factory account', args: (p) => ['exec', p] },
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
function isInstalled(id) {
|
package/lib/outcomes.js
CHANGED
|
@@ -19,7 +19,11 @@ const DECISIONS_FILE = path.join(OUTCOMES_DIR, 'decisions.json');
|
|
|
19
19
|
const OUTCOMES = ['kept', 'reverted', 'superseded', 'failed'];
|
|
20
20
|
|
|
21
21
|
function load() {
|
|
22
|
-
|
|
22
|
+
// Array-or-nothing: a corrupt/odd-shaped file must degrade, never throw
|
|
23
|
+
try {
|
|
24
|
+
const d = JSON.parse(fs.readFileSync(DECISIONS_FILE, 'utf-8'));
|
|
25
|
+
return Array.isArray(d) ? d : [];
|
|
26
|
+
} catch { return []; }
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
function save(decisions) {
|
|
@@ -122,7 +126,10 @@ const BYPASS_REASONS = [
|
|
|
122
126
|
];
|
|
123
127
|
|
|
124
128
|
function loadBypasses() {
|
|
125
|
-
try {
|
|
129
|
+
try {
|
|
130
|
+
const d = JSON.parse(fs.readFileSync(BYPASSES_FILE, 'utf-8'));
|
|
131
|
+
return Array.isArray(d) ? d : [];
|
|
132
|
+
} catch { return []; }
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
function recordBypass(reason, note = '') {
|
package/lib/projects-index.js
CHANGED
|
@@ -24,7 +24,12 @@ const SCAN_ROOTS = [
|
|
|
24
24
|
];
|
|
25
25
|
|
|
26
26
|
function load() {
|
|
27
|
-
|
|
27
|
+
// Shape-or-nothing: corrupt index degrades to empty, never throws
|
|
28
|
+
try {
|
|
29
|
+
const i = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'));
|
|
30
|
+
if (i && typeof i === 'object' && i.projects && typeof i.projects === 'object') return i;
|
|
31
|
+
return { projects: {} };
|
|
32
|
+
} catch { return { projects: {} }; }
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
function save(index) {
|