phewsh 0.14.2 → 0.14.4

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.
@@ -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
- const atHome = realPath(process.cwd()) === realPath(os.homedir());
311
- const recents = intentFiles.length === 0
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
- rows.push([h.label, h.installed ? `installed (${h.auth})` : 'not installed', h.installed ? 'green' : undefined]);
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(` ${slate('switch:')} ${cream('/use <' + harnesses.filter(h => h.installed).map(h => h.id).concat(config?.apiKey ? ['api'] : []).join('|') + '>')}`);
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 <claude-code|codex|gemini|cursor|opencode|api>')}`);
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
- try { return JSON.parse(fs.readFileSync(DECISIONS_FILE, 'utf-8')); } catch { return []; }
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 { return JSON.parse(fs.readFileSync(BYPASSES_FILE, 'utf-8')); } catch { return []; }
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 = '') {
@@ -4,13 +4,16 @@
4
4
  // so running `phewsh` at machine root becomes mission-control bootstrap
5
5
  // ("where do you want to work?") instead of "no project found, goodbye."
6
6
  //
7
- // Storage: ~/.phewsh/projects.json. Local-first; web sync layers on top.
7
+ // Storage: ~/.phewsh/project-index.json. Local-first; web sync layers on top.
8
+ // NOT projects.json — that file belongs to `phewsh mcp` (web project cache,
9
+ // array-shaped). Claiming it caused a startup crash AND would have clobbered
10
+ // MCP data on first write. One file, one owner.
8
11
 
9
12
  const fs = require('fs');
10
13
  const path = require('path');
11
14
  const os = require('os');
12
15
 
13
- const INDEX_FILE = path.join(os.homedir(), '.phewsh', 'projects.json');
16
+ const INDEX_FILE = path.join(os.homedir(), '.phewsh', 'project-index.json');
14
17
 
15
18
  // Shallow-scanned roots when the user asks to find projects. One level deep,
16
19
  // opt-in only — deep-scanning someone's machine uninvited is invasive.
@@ -24,7 +27,12 @@ const SCAN_ROOTS = [
24
27
  ];
25
28
 
26
29
  function load() {
27
- try { return JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8')); } catch { return { projects: {} }; }
30
+ // Shape-or-nothing: corrupt index degrades to empty, never throws
31
+ try {
32
+ const i = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'));
33
+ if (i && typeof i === 'object' && i.projects && typeof i.projects === 'object') return i;
34
+ return { projects: {} };
35
+ } catch { return { projects: {} }; }
28
36
  }
29
37
 
30
38
  function save(index) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"