phewsh 0.13.1 → 0.13.2

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.
@@ -107,14 +107,16 @@ const DEFAULT_MODEL = 'claude-sonnet';
107
107
  // explicit config.defaultRoute → API key if set → first installed harness.
108
108
 
109
109
  function resolveRoute(config, harnesses) {
110
- const installed = harnesses.filter(h => h.installed);
110
+ // Chat routing needs a headless-capable harness; interactive-only ones
111
+ // (Hermes, Pi) are still detected and reachable via /work.
112
+ const chatCapable = harnesses.filter(h => h.installed && h.headless);
111
113
  const preferred = config?.defaultRoute;
112
114
  if (preferred === 'api' && config?.apiKey) return { type: 'api' };
113
- if (preferred && installed.some(h => h.id === preferred)) {
115
+ if (preferred && chatCapable.some(h => h.id === preferred)) {
114
116
  return { type: 'harness', id: preferred };
115
117
  }
116
118
  if (config?.apiKey) return { type: 'api' };
117
- if (installed.length > 0) return { type: 'harness', id: installed[0].id };
119
+ if (chatCapable.length > 0) return { type: 'harness', id: chatCapable[0].id };
118
120
  return null;
119
121
  }
120
122
 
@@ -288,8 +290,8 @@ async function main() {
288
290
 
289
291
  // Capabilities: what's installed on this machine, no setup required
290
292
  if (installedHarnesses.length > 0) {
291
- const caps = harnesses.map(h =>
292
- h.installed ? `${teal('✓')} ${sage(h.label)}` : slate(`✗ ${h.label}`)
293
+ const caps = installedHarnesses.map(h =>
294
+ `${teal('✓')} ${sage(h.label)}${h.headless ? '' : slate(' (/work)')}`
293
295
  ).join(slate(' · '));
294
296
  console.log(` ${caps}`);
295
297
  }
@@ -858,6 +860,8 @@ async function main() {
858
860
  } else if (HARNESSES[target]) {
859
861
  if (!harnesses.find(h => h.id === target)?.installed) {
860
862
  console.log(` ${ember('!')} ${sage(HARNESSES[target].label + ' is not installed on this machine.')}`);
863
+ } else if (!HARNESSES[target].args) {
864
+ console.log(` ${sage(HARNESSES[target].label + ' is interactive-only — drop into it with')} ${cream('/work ' + target)} ${sage('(phewsh records the outcome when you return)')}`);
861
865
  } else {
862
866
  route = { type: 'harness', id: target };
863
867
  console.log(` ${teal('●')} ${sage('Routing via')} ${cream(routeLabel(route, config))} ${slate('— no API key, your subscription')}`);
@@ -876,8 +880,9 @@ async function main() {
876
880
  ui.divider('line');
877
881
  for (const h of harnesses) {
878
882
  const active = route?.type === 'harness' && route.id === h.id ? ` ${teal('● active')}` : '';
883
+ const mode = h.headless ? '' : slate(' · interactive (/work)');
879
884
  const status = h.installed ? green('installed') : slate('not installed');
880
- console.log(` ${cream(h.id.padEnd(12))} ${sage(h.label.padEnd(14))} ${status}${active}`);
885
+ console.log(` ${cream(h.id.padEnd(12))} ${sage(h.label.padEnd(14))} ${status}${mode}${active}`);
881
886
  }
882
887
  console.log('');
883
888
  rl.prompt();
package/commands/setup.js CHANGED
@@ -46,7 +46,8 @@ module.exports = async function setup() {
46
46
  console.log(` ${b(cream('Detected on this machine'))}`);
47
47
  for (const h of harnesses) {
48
48
  const status = h.installed ? green('✓ installed') : slate('✗ not installed');
49
- console.log(` ${cream(h.label.padEnd(14))} ${status} ${slate('(' + h.auth + ')')}`);
49
+ const mode = h.headless ? '' : slate(' · interactive — /work ' + h.id);
50
+ console.log(` ${cream(h.label.padEnd(14))} ${status} ${slate('(' + h.auth + ')')}${mode}`);
50
51
  }
51
52
  if (installed.length === 0) {
52
53
  console.log('');
@@ -58,11 +59,12 @@ module.exports = async function setup() {
58
59
 
59
60
  // Agent-run (no TTY): auto-configure instead of asking questions nobody
60
61
  // can answer. Pick the first installed harness; humans can change it later.
62
+ const chatCapable = installed.filter(h => h.headless);
61
63
  if (!process.stdin.isTTY) {
62
- if (installed.length > 0) {
63
- config.defaultRoute = installed[0].id;
64
+ if (chatCapable.length > 0) {
65
+ config.defaultRoute = chatCapable[0].id;
64
66
  saveConfig(config);
65
- console.log(` ${teal('●')} ${sage('Auto-configured (non-interactive): default route =')} ${cream(installed[0].label)} ${slate('— no API key needed')}`);
67
+ console.log(` ${teal('●')} ${sage('Auto-configured (non-interactive): default route =')} ${cream(chatCapable[0].label)} ${slate('— no API key needed')}`);
66
68
  console.log(` ${slate('Change anytime: run `phewsh setup` in your own terminal, or /use inside a session.')}`);
67
69
  } else if (config.apiKey) {
68
70
  config.defaultRoute = 'api';
@@ -81,7 +83,9 @@ module.exports = async function setup() {
81
83
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
82
84
 
83
85
  // ── 2. Pick the default route ─────────────────────────
84
- const options = installed.map(h => ({ kind: 'harness', id: h.id, label: `${h.label} your ${h.auth.split(' / ')[0].toLowerCase()}, no API key` }));
86
+ // Interactive-only harnesses (Hermes, Pi) can't take chat routingthey
87
+ // stay reachable via /work in a session, so they're not default options.
88
+ const options = chatCapable.map(h => ({ kind: 'harness', id: h.id, label: `${h.label} — your ${h.auth.split(' / ')[0].toLowerCase()}, no API key` }));
85
89
  options.push({ kind: 'api', id: 'api', label: 'Direct API — bring your own Anthropic/OpenRouter key' });
86
90
 
87
91
  console.log(` ${b(cream('Where should phewsh route your work by default?'))}`);
package/lib/harnesses.js CHANGED
@@ -11,12 +11,21 @@
11
11
 
12
12
  const { execSync, spawn } = require('child_process');
13
13
 
14
+ // args: how to run a one-shot prompt headlessly. args: null = we only know
15
+ // how to launch it interactively (detection + /work still fully supported —
16
+ // never guess flags; a wrong invocation looks like phewsh being broken).
14
17
  const HARNESSES = {
15
18
  'claude-code': { bin: 'claude', label: 'Claude Code', auth: 'Claude subscription / Console', args: (p) => ['-p', p, '--output-format', 'text'] },
16
19
  'codex': { bin: 'codex', label: 'Codex CLI', auth: 'ChatGPT plan', args: (p) => ['exec', p] },
17
20
  'gemini': { bin: 'gemini', label: 'Gemini CLI', auth: 'Google login', args: (p) => ['-p', p] },
18
21
  'cursor': { bin: 'cursor-agent', label: 'Cursor Agent', auth: 'Cursor account', args: (p) => ['-p', p, '--output-format', 'text'] },
19
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] },
20
29
  };
21
30
 
22
31
  function isInstalled(id) {
@@ -25,16 +34,16 @@ function isInstalled(id) {
25
34
  try { execSync(`which ${h.bin}`, { stdio: 'pipe' }); return true; } catch { return false; }
26
35
  }
27
36
 
28
- /** First installed harness in preference order, or null. */
37
+ /** First installed chat-capable harness in preference order, or null. */
29
38
  function detectInstalled() {
30
39
  for (const id of Object.keys(HARNESSES)) {
31
- if (isInstalled(id)) return id;
40
+ if (HARNESSES[id].args && isInstalled(id)) return id;
32
41
  }
33
42
  return null;
34
43
  }
35
44
 
36
45
  function listHarnesses() {
37
- return Object.entries(HARNESSES).map(([id, h]) => ({ id, ...h, installed: isInstalled(id) }));
46
+ return Object.entries(HARNESSES).map(([id, h]) => ({ id, ...h, headless: !!h.args, installed: isInstalled(id) }));
38
47
  }
39
48
 
40
49
  /**
@@ -45,6 +54,7 @@ function listHarnesses() {
45
54
  function runViaHarness(id, systemPrompt, userPrompt) {
46
55
  const h = HARNESSES[id];
47
56
  if (!h) return Promise.reject(new Error(`Unknown harness: ${id}`));
57
+ if (!h.args) return Promise.reject(new Error(`${h.label} is interactive-only here — launch it with /work ${id}`));
48
58
  const prompt = systemPrompt ? `${systemPrompt}\n\n---\n\n${userPrompt}` : userPrompt;
49
59
 
50
60
  return new Promise((resolve, reject) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"