phewsh 0.14.5 → 0.14.6
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 +8 -8
- package/commands/clarify.js +1 -1
- package/commands/gate.js +1 -1
- package/commands/intent.js +1 -1
- package/commands/mcp.js +53 -58
- package/commands/session.js +146 -14
- package/lib/harnesses.js +21 -10
- package/lib/providers.js +4 -0
- package/mcp/AGENTS.md +132 -0
- package/mcp/http-server.js +360 -0
- package/mcp/index.js +991 -0
- package/mcp/lib/dispatch-queue.js +150 -0
- package/mcp/lib/handlers.js +411 -0
- package/mcp/lib/runtime-registry.js +74 -0
- package/mcp/lib/store.js +51 -0
- package/mcp/package.json +3 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -31,14 +31,14 @@ phewsh ai providers # see what's installed
|
|
|
31
31
|
API keys (OpenRouter, Anthropic, Groq, …) and PHEWSH pooled credits remain
|
|
32
32
|
available as alternatives — `phewsh login --set-key`.
|
|
33
33
|
|
|
34
|
-
##
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
## One package, one system
|
|
35
|
+
|
|
36
|
+
Everything ships in `phewsh` — the CLI (intent authoring, sync, bridges,
|
|
37
|
+
receipts, dispatch) *and* the MCP server. `phewsh mcp setup` wires the
|
|
38
|
+
bundled server into Claude Code / Cursor / any MCP client via
|
|
39
|
+
`phewsh mcp serve --stdio`: an *interactive* agent session gets your
|
|
40
|
+
project's briefing, task queue, and enforcement gate over stdio, and shares
|
|
41
|
+
state with the bridges (`~/.phewsh/`). No second install.
|
|
42
42
|
|
|
43
43
|
Rule of thumb: `phewsh serve` = dispatch tasks *to* your agents.
|
|
44
44
|
`phewsh mcp setup` = your agents pull tasks *from* PHEWSH mid-session.
|
package/commands/clarify.js
CHANGED
|
@@ -82,7 +82,7 @@ ${isRefine ? '\nThis is a refinement of existing intent. The previous goal was:
|
|
|
82
82
|
'content-type': 'application/json',
|
|
83
83
|
},
|
|
84
84
|
body: JSON.stringify({
|
|
85
|
-
model: '
|
|
85
|
+
model: require('../lib/providers').DEFAULT_ANTHROPIC_MODEL,
|
|
86
86
|
max_tokens: 1024,
|
|
87
87
|
system: systemPrompt,
|
|
88
88
|
messages: [{ role: 'user', content: raw }],
|
package/commands/gate.js
CHANGED
|
@@ -237,7 +237,7 @@ ${(plan || 'No plan yet').slice(0, 1500)}`;
|
|
|
237
237
|
'content-type': 'application/json',
|
|
238
238
|
},
|
|
239
239
|
body: JSON.stringify({
|
|
240
|
-
model: '
|
|
240
|
+
model: require('../lib/providers').DEFAULT_ANTHROPIC_MODEL,
|
|
241
241
|
max_tokens: 1024,
|
|
242
242
|
messages: [{ role: 'user', content: prompt }],
|
|
243
243
|
}),
|
package/commands/intent.js
CHANGED
|
@@ -261,7 +261,7 @@ ${(next || 'No next actions yet').slice(0, 2000)}`;
|
|
|
261
261
|
'content-type': 'application/json',
|
|
262
262
|
},
|
|
263
263
|
body: JSON.stringify({
|
|
264
|
-
model: '
|
|
264
|
+
model: require('../lib/providers').DEFAULT_ANTHROPIC_MODEL,
|
|
265
265
|
max_tokens: 4096,
|
|
266
266
|
system: systemPrompt,
|
|
267
267
|
messages: [{ role: 'user', content: userPrompt }],
|
package/commands/mcp.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// phewsh mcp — Set up and sync the PHEWSH MCP coordination layer.
|
|
2
2
|
//
|
|
3
|
+
// The MCP server ships inside this package (mcp/ — ESM subtree).
|
|
4
|
+
// No second install, no phewsh-mcp-server package.
|
|
5
|
+
//
|
|
3
6
|
// Usage:
|
|
4
|
-
// phewsh mcp setup
|
|
5
|
-
// phewsh mcp sync
|
|
6
|
-
// phewsh mcp status
|
|
7
|
-
// phewsh mcp serve
|
|
7
|
+
// phewsh mcp setup — Configure Claude Code to use the bundled server
|
|
8
|
+
// phewsh mcp sync — Sync local .intent/ + cloud projects → ~/.phewsh/projects.json
|
|
9
|
+
// phewsh mcp status — Check what agents can see right now
|
|
10
|
+
// phewsh mcp serve — Start the HTTP transport for the web bridge (:7483)
|
|
11
|
+
// phewsh mcp serve --stdio — Run the stdio MCP server (what agent configs point at)
|
|
8
12
|
|
|
9
13
|
const fs = require('fs');
|
|
10
14
|
const path = require('path');
|
|
11
15
|
const os = require('os');
|
|
12
|
-
const {
|
|
16
|
+
const { spawn } = require('child_process');
|
|
13
17
|
|
|
14
18
|
const PHEWSH_DIR = path.join(os.homedir(), '.phewsh');
|
|
15
19
|
const PROJECTS_FILE = path.join(PHEWSH_DIR, 'projects.json');
|
|
@@ -130,30 +134,16 @@ async function loadCloudProjects() {
|
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
path.join(__dirname, '..', '..', 'mcp', 'src', 'index.js'), // monorepo sibling
|
|
137
|
-
path.join(process.cwd(), 'mcp', 'src', 'index.js'), // cwd
|
|
138
|
-
];
|
|
139
|
-
|
|
140
|
-
// Also check if phewsh-mcp-server is globally installed
|
|
141
|
-
try {
|
|
142
|
-
const globalPath = execSync('npm root -g', { encoding: 'utf-8' }).trim();
|
|
143
|
-
candidates.push(path.join(globalPath, 'phewsh-mcp-server', 'src', 'index.js'));
|
|
144
|
-
} catch { /* not installed globally */ }
|
|
137
|
+
// The server is bundled with this package — no hunting.
|
|
138
|
+
const MCP_SERVER_PATH = path.join(__dirname, '..', 'mcp', 'index.js');
|
|
139
|
+
const MCP_HTTP_PATH = path.join(__dirname, '..', 'mcp', 'http-server.js');
|
|
145
140
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
return null;
|
|
141
|
+
function findMcpServerPath() {
|
|
142
|
+
return fs.existsSync(MCP_SERVER_PATH) ? MCP_SERVER_PATH : null;
|
|
150
143
|
}
|
|
151
144
|
|
|
152
145
|
function findHttpServerPath() {
|
|
153
|
-
|
|
154
|
-
if (!stdioPath) return null;
|
|
155
|
-
const httpPath = path.join(path.dirname(stdioPath), 'http-server.js');
|
|
156
|
-
return fs.existsSync(httpPath) ? httpPath : null;
|
|
146
|
+
return fs.existsSync(MCP_HTTP_PATH) ? MCP_HTTP_PATH : null;
|
|
157
147
|
}
|
|
158
148
|
|
|
159
149
|
const DEFAULT_HTTP_PORT = 7483;
|
|
@@ -170,7 +160,25 @@ async function probeHttp(port = DEFAULT_HTTP_PORT) {
|
|
|
170
160
|
}
|
|
171
161
|
}
|
|
172
162
|
|
|
163
|
+
function serveStdio() {
|
|
164
|
+
// stdio MCP server: stdout IS the protocol — print nothing here.
|
|
165
|
+
ensureDirs();
|
|
166
|
+
const child = spawn(process.execPath, [MCP_SERVER_PATH], {
|
|
167
|
+
stdio: 'inherit',
|
|
168
|
+
env: { ...process.env },
|
|
169
|
+
});
|
|
170
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
171
|
+
const shutdown = () => { if (!child.killed) child.kill('SIGTERM'); };
|
|
172
|
+
process.on('SIGINT', shutdown);
|
|
173
|
+
process.on('SIGTERM', shutdown);
|
|
174
|
+
}
|
|
175
|
+
|
|
173
176
|
async function serve() {
|
|
177
|
+
if (process.argv.includes('--stdio')) {
|
|
178
|
+
serveStdio();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
174
182
|
console.log('');
|
|
175
183
|
console.log(` ${b(w('PHEWSH MCP HTTP transport'))}`);
|
|
176
184
|
console.log('');
|
|
@@ -179,8 +187,8 @@ async function serve() {
|
|
|
179
187
|
|
|
180
188
|
const httpPath = findHttpServerPath();
|
|
181
189
|
if (!httpPath) {
|
|
182
|
-
console.log(` ${yellow('HTTP transport not found.')} Expected at
|
|
183
|
-
console.log(` ${g('
|
|
190
|
+
console.log(` ${yellow('HTTP transport not found.')} Expected bundled at ${g(MCP_HTTP_PATH)}`);
|
|
191
|
+
console.log(` ${g('Reinstall: npm i -g phewsh')}`);
|
|
184
192
|
return;
|
|
185
193
|
}
|
|
186
194
|
|
|
@@ -222,42 +230,24 @@ async function setup() {
|
|
|
222
230
|
|
|
223
231
|
ensureDirs();
|
|
224
232
|
|
|
225
|
-
// 1.
|
|
226
|
-
|
|
233
|
+
// 1. Confirm the bundled server is present
|
|
234
|
+
const serverPath = findMcpServerPath();
|
|
227
235
|
|
|
228
236
|
if (!serverPath) {
|
|
229
|
-
console.log(` ${yellow('MCP server
|
|
230
|
-
console.log(` ${g('Install it:')}`);
|
|
231
|
-
console.log(` npm install -g phewsh-mcp-server`);
|
|
232
|
-
console.log('');
|
|
233
|
-
console.log(` ${g('Or if you have the phewsh repo:')}`);
|
|
234
|
-
console.log(` cd mcp && npm install`);
|
|
237
|
+
console.log(` ${yellow('Bundled MCP server missing.')} Reinstall: ${w('npm i -g phewsh')}`);
|
|
235
238
|
console.log('');
|
|
236
239
|
return;
|
|
237
240
|
}
|
|
238
241
|
|
|
239
|
-
console.log(` ${green('
|
|
240
|
-
|
|
241
|
-
// 2. Check if deps are installed
|
|
242
|
-
const mcpDir = path.dirname(path.dirname(serverPath));
|
|
243
|
-
const nodeModules = path.join(mcpDir, 'node_modules');
|
|
244
|
-
if (!fs.existsSync(nodeModules)) {
|
|
245
|
-
console.log(` ${yellow('Installing dependencies...')}`);
|
|
246
|
-
try {
|
|
247
|
-
execSync('npm install', { cwd: mcpDir, stdio: 'pipe' });
|
|
248
|
-
console.log(` ${green('Dependencies installed.')}`);
|
|
249
|
-
} catch (err) {
|
|
250
|
-
console.log(` ${yellow('Failed to install deps.')} Run manually: cd ${mcpDir} && npm install`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
242
|
+
console.log(` ${green('MCP server:')} ${g('bundled with phewsh — no separate install')}`);
|
|
253
243
|
|
|
254
|
-
//
|
|
244
|
+
// 2. Generate settings.json snippet — points at phewsh itself, survives upgrades
|
|
255
245
|
const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
256
246
|
const snippet = {
|
|
257
247
|
mcpServers: {
|
|
258
248
|
phewsh: {
|
|
259
|
-
command: '
|
|
260
|
-
args: [
|
|
249
|
+
command: 'phewsh',
|
|
250
|
+
args: ['mcp', 'serve', '--stdio'],
|
|
261
251
|
},
|
|
262
252
|
},
|
|
263
253
|
};
|
|
@@ -268,12 +258,16 @@ async function setup() {
|
|
|
268
258
|
console.log(g(' ' + JSON.stringify(snippet, null, 2).split('\n').join('\n ')));
|
|
269
259
|
console.log('');
|
|
270
260
|
|
|
271
|
-
// Check if already configured
|
|
261
|
+
// Check if already configured — and flag stale configs from the two-package era
|
|
272
262
|
if (fs.existsSync(claudeSettingsPath)) {
|
|
273
263
|
try {
|
|
274
264
|
const existing = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));
|
|
275
|
-
|
|
265
|
+
const current = existing.mcpServers?.phewsh;
|
|
266
|
+
if (current?.command === 'phewsh') {
|
|
276
267
|
console.log(` ${green('Already configured in Claude Code settings.')}`);
|
|
268
|
+
} else if (current) {
|
|
269
|
+
console.log(` ${yellow('Configured the old way')} (${g(current.command + ' ' + (current.args || []).join(' '))}).`);
|
|
270
|
+
console.log(` ${g('Update it to the snippet above — the server now ships inside phewsh.')}`);
|
|
277
271
|
} else {
|
|
278
272
|
console.log(` ${yellow('Not yet configured.')} Add the snippet above to your settings.`);
|
|
279
273
|
}
|
|
@@ -442,10 +436,11 @@ async function main() {
|
|
|
442
436
|
break;
|
|
443
437
|
default:
|
|
444
438
|
console.log(`\n ${b('phewsh mcp')} — Connect AI agents to your project intelligence\n`);
|
|
445
|
-
console.log(` ${w('setup')}
|
|
446
|
-
console.log(` ${w('sync')}
|
|
447
|
-
console.log(` ${w('status')}
|
|
448
|
-
console.log(` ${w('serve')}
|
|
439
|
+
console.log(` ${w('setup')} Configure agents to use the bundled MCP server`);
|
|
440
|
+
console.log(` ${w('sync')} Sync projects → ~/.phewsh/projects.json`);
|
|
441
|
+
console.log(` ${w('status')} Check what agents can see right now`);
|
|
442
|
+
console.log(` ${w('serve')} Run the HTTP bridge for the web app (:7483)`);
|
|
443
|
+
console.log(` ${w('serve --stdio')} Run the stdio MCP server (for agent configs)`);
|
|
449
444
|
console.log('');
|
|
450
445
|
}
|
|
451
446
|
}
|
package/commands/session.js
CHANGED
|
@@ -95,14 +95,23 @@ function formatAgo(ms) {
|
|
|
95
95
|
return `${days}d ago`;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// Shortcuts, not a gate. Any string the user types that doesn't match an
|
|
99
|
+
// alias is passed through verbatim — the provider/harness validates it.
|
|
100
|
+
// PHEWSH must never block a model it hasn't heard of (it WILL go stale
|
|
101
|
+
// faster than the providers ship).
|
|
98
102
|
const MODELS = {
|
|
103
|
+
'claude-fable': { id: 'claude-fable-5', name: 'Claude Fable 5', provider: 'anthropic' },
|
|
104
|
+
'claude-opus': { id: 'claude-opus-4-8', name: 'Claude Opus 4.8', provider: 'anthropic' },
|
|
99
105
|
'claude-sonnet': { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'anthropic' },
|
|
100
|
-
'claude-opus': { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', provider: 'anthropic' },
|
|
101
106
|
'claude-haiku': { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', provider: 'anthropic' },
|
|
102
107
|
};
|
|
103
108
|
|
|
104
109
|
const DEFAULT_MODEL = 'claude-sonnet';
|
|
105
110
|
|
|
111
|
+
// currentModel may be an alias key OR a raw model id the user passed through.
|
|
112
|
+
function modelId(m) { return MODELS[m]?.id || m; }
|
|
113
|
+
function modelName(m) { return MODELS[m]?.name || m; }
|
|
114
|
+
|
|
106
115
|
// ── Routing: where plain typed input goes ─────────────────────────────────
|
|
107
116
|
// A route is either an installed harness (your existing subscription — no
|
|
108
117
|
// API key needed in phewsh) or the direct API (your key). Precedence:
|
|
@@ -264,6 +273,7 @@ async function main() {
|
|
|
264
273
|
try { recordProject(process.cwd()); } catch { /* index is best-effort */ }
|
|
265
274
|
}
|
|
266
275
|
let currentModel = DEFAULT_MODEL;
|
|
276
|
+
let harnessModel = null; // pass-through preference; the harness validates it
|
|
267
277
|
let totalPromptTokens = 0;
|
|
268
278
|
let totalCompletionTokens = 0;
|
|
269
279
|
|
|
@@ -456,7 +466,7 @@ async function main() {
|
|
|
456
466
|
});
|
|
457
467
|
decisionsThisSession++;
|
|
458
468
|
try {
|
|
459
|
-
const output = await runViaHarness(harnessId, fullSystem, buildHarnessPrompt(messages, input));
|
|
469
|
+
const output = await runViaHarness(harnessId, fullSystem, buildHarnessPrompt(messages, input), { model: harnessModel });
|
|
460
470
|
messages.push({ role: 'user', content: input });
|
|
461
471
|
messages.push({ role: 'assistant', content: (output || '').trim() });
|
|
462
472
|
recordSessionEvent(harnessId, projectName, 'task_complete', {
|
|
@@ -484,18 +494,18 @@ async function main() {
|
|
|
484
494
|
messages.push({ role: 'user', content: input });
|
|
485
495
|
console.log('');
|
|
486
496
|
try {
|
|
487
|
-
const result = await streamChat(config.apiKey, messages, fullSystem,
|
|
497
|
+
const result = await streamChat(config.apiKey, messages, fullSystem, modelId(currentModel));
|
|
488
498
|
messages.push({ role: 'assistant', content: result.content });
|
|
489
499
|
if (result.promptTokens) totalPromptTokens += result.promptTokens;
|
|
490
500
|
if (result.completionTokens) totalCompletionTokens += result.completionTokens;
|
|
491
501
|
if (result.promptTokens || result.completionTokens) {
|
|
492
|
-
console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${
|
|
502
|
+
console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${modelName(currentModel)} · outcome? 1-4 or keep typing`));
|
|
493
503
|
}
|
|
494
504
|
awaitingOutcome = decisionId;
|
|
495
505
|
trackSap({
|
|
496
506
|
userId: config.supabaseUserId,
|
|
497
507
|
source: 'cli',
|
|
498
|
-
model:
|
|
508
|
+
model: modelId(currentModel),
|
|
499
509
|
promptTokens: result.promptTokens,
|
|
500
510
|
completionTokens: result.completionTokens,
|
|
501
511
|
accessToken: config.supabaseAccessToken,
|
|
@@ -720,6 +730,8 @@ async function main() {
|
|
|
720
730
|
console.log('');
|
|
721
731
|
console.log(` ${cream('route — where your typing goes')}`);
|
|
722
732
|
console.log(` ${teal('/use')} ${slate('<route>')} ${sage('Switch: claude-code, codex, gemini, cursor, opencode, api')}`);
|
|
733
|
+
console.log(` ${teal('@name')} ${slate('<msg>')} ${sage('One message to one harness — @codex review this — context stays shared')}`);
|
|
734
|
+
console.log(` ${teal('/council')} ${slate('<q>')} ${sage('Ask ALL installed harnesses in parallel; keep the best answer')}`);
|
|
723
735
|
console.log(` ${teal('/harnesses')} ${sage('Agent CLIs detected on this machine')}`);
|
|
724
736
|
console.log(` ${teal('/provider')} ${sage('Current route + what\'s available')}`);
|
|
725
737
|
console.log(` ${teal('/fallback')} ${sage('What happens at a usage wall: ask or auto-switch')}`);
|
|
@@ -735,7 +747,7 @@ async function main() {
|
|
|
735
747
|
console.log(` ${cream('configure')}`);
|
|
736
748
|
console.log(` ${teal('/key')} ${sage('Set API key (optional — harnesses need none)')}`);
|
|
737
749
|
console.log(` ${teal('/login')} ${sage('Identity + cloud sync')}`);
|
|
738
|
-
console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch
|
|
750
|
+
console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch model — passed through, the provider validates')}`);
|
|
739
751
|
console.log(` ${teal('/update')} ${sage('Update phewsh')}`);
|
|
740
752
|
console.log(` ${teal('/tour')} ${sage('Quick walkthrough')}`);
|
|
741
753
|
console.log('');
|
|
@@ -1079,19 +1091,51 @@ async function main() {
|
|
|
1079
1091
|
const active = key === currentModel ? ` ${teal('●')}` : '';
|
|
1080
1092
|
console.log(` ${cream(key.padEnd(16))} ${sage(model.name)}${active}`);
|
|
1081
1093
|
}
|
|
1082
|
-
|
|
1094
|
+
if (!MODELS[currentModel] && route?.type !== 'harness') {
|
|
1095
|
+
console.log(` ${cream(String(currentModel).padEnd(16))} ${sage('(pass-through)')} ${teal('●')}`);
|
|
1096
|
+
}
|
|
1097
|
+
console.log(`\n ${sage('Switch with:')} ${cream('/model <name>')} ${slate('— aliases above, or any model id (passed through)')}\n`);
|
|
1083
1098
|
rl.prompt();
|
|
1084
1099
|
return;
|
|
1085
1100
|
}
|
|
1086
1101
|
|
|
1087
1102
|
if (cmd === 'model') {
|
|
1103
|
+
// Harness route: the harness owns its model list — we pass the
|
|
1104
|
+
// preference through and let IT validate. No stale gate here.
|
|
1105
|
+
if (route?.type === 'harness') {
|
|
1106
|
+
const h = HARNESSES[route.id];
|
|
1107
|
+
if (!cmdArg) {
|
|
1108
|
+
console.log(` ${sage('Route:')} ${cream(h.label)} ${sage('— model:')} ${cream(harnessModel || h.label + ' default')}`);
|
|
1109
|
+
console.log(` ${sage('Usage:')} ${cream('/model <anything ' + h.label + ' accepts>')}${h.models ? '' : sage(' — not supported for this harness; set it in ' + h.label + ' itself')}`);
|
|
1110
|
+
console.log(` ${slate('/model default to clear')}`);
|
|
1111
|
+
rl.prompt();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (!h.models) {
|
|
1115
|
+
console.log(` ${sage(h.label + ' doesn\'t take a model flag from phewsh — set the model in ' + h.label + ' itself.')}`);
|
|
1116
|
+
rl.prompt();
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (cmdArg.toLowerCase() === 'default') {
|
|
1120
|
+
harnessModel = null;
|
|
1121
|
+
console.log(` ${teal('●')} ${sage('Cleared — ' + h.label + ' uses its own default.')}`);
|
|
1122
|
+
} else {
|
|
1123
|
+
harnessModel = cmdArg.trim().replace(/\s+/g, '-');
|
|
1124
|
+
console.log(` ${teal('●')} ${sage('Model preference:')} ${cream(harnessModel)} ${slate('— ' + h.label + ' validates it on your next message')}`);
|
|
1125
|
+
}
|
|
1126
|
+
rl.prompt();
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// API route: aliases are shortcuts; anything else passes through
|
|
1131
|
+
// verbatim and the provider validates it.
|
|
1088
1132
|
if (!cmdArg) {
|
|
1089
|
-
console.log(` ${sage('Current:')} ${cream(
|
|
1090
|
-
console.log(` ${sage('Usage:')} ${cream('/model <
|
|
1133
|
+
console.log(` ${sage('Current:')} ${cream(modelName(currentModel))}`);
|
|
1134
|
+
console.log(` ${sage('Usage:')} ${cream('/model <' + Object.keys(MODELS).map(k => k.replace('claude-', '')).join('|') + '|any model id>')}`);
|
|
1091
1135
|
rl.prompt();
|
|
1092
1136
|
return;
|
|
1093
1137
|
}
|
|
1094
|
-
const query = cmdArg.toLowerCase().replace('claude-', '').replace('claude', '');
|
|
1138
|
+
const query = cmdArg.toLowerCase().replace('claude-', '').replace('claude', '').trim();
|
|
1095
1139
|
const match = Object.keys(MODELS).find(k =>
|
|
1096
1140
|
k.includes(query) || MODELS[k].name.toLowerCase().includes(query)
|
|
1097
1141
|
);
|
|
@@ -1099,8 +1143,75 @@ async function main() {
|
|
|
1099
1143
|
currentModel = match;
|
|
1100
1144
|
console.log(` ${teal('●')} ${sage('Switched to')} ${cream(MODELS[match].name)}`);
|
|
1101
1145
|
} else {
|
|
1102
|
-
|
|
1146
|
+
currentModel = cmdArg.trim();
|
|
1147
|
+
console.log(` ${teal('●')} ${sage('Switched to')} ${cream(currentModel)} ${slate('— passed through as-is; the provider validates it')}`);
|
|
1148
|
+
}
|
|
1149
|
+
rl.prompt();
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (cmd === 'council' || cmd === 'all') {
|
|
1154
|
+
// One prompt, every installed harness, in parallel. Different
|
|
1155
|
+
// models disagreeing is the signal — and which answer you KEEP
|
|
1156
|
+
// is the outcome data.
|
|
1157
|
+
const members = harnesses.filter(h => h.installed && h.headless);
|
|
1158
|
+
if (!cmdArg) {
|
|
1159
|
+
console.log(` ${sage('Usage:')} ${cream('/council <question>')} ${sage('— asks all ' + members.length + ' installed harnesses in parallel')}`);
|
|
1160
|
+
console.log(` ${slate(members.map(m => m.label).join(' · '))}`);
|
|
1161
|
+
rl.prompt();
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (members.length < 2) {
|
|
1165
|
+
console.log(` ${sage('Council needs at least 2 installed harnesses — you have ' + members.length + '.')}`);
|
|
1166
|
+
rl.prompt();
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const councilHint = sessionMode
|
|
1170
|
+
? Object.values(INTENT_MODES).find(m => m.id === sessionMode)?.hint
|
|
1171
|
+
: null;
|
|
1172
|
+
const councilSystem = councilHint ? `${systemPrompt}\n\n${councilHint}` : systemPrompt;
|
|
1173
|
+
const decisionId = recordDecision({
|
|
1174
|
+
project: projectName, route: 'council:' + members.map(m => m.id).join('+'),
|
|
1175
|
+
mode: sessionMode, summary: cmdArg,
|
|
1176
|
+
});
|
|
1177
|
+
decisionsThisSession++;
|
|
1178
|
+
console.log('');
|
|
1179
|
+
console.log(` ${b(cream('Council'))} ${sage('— asking')} ${cream(String(members.length))} ${sage('harnesses in parallel. Where they disagree is the insight.')}`);
|
|
1180
|
+
console.log(` ${slate(members.map(m => m.label).join(' · '))}`);
|
|
1181
|
+
|
|
1182
|
+
const prompt = buildHarnessPrompt(messages, cmdArg);
|
|
1183
|
+
const settled = await Promise.allSettled(members.map(m =>
|
|
1184
|
+
runViaHarness(m.id, councilSystem, prompt, { quiet: true })
|
|
1185
|
+
));
|
|
1186
|
+
|
|
1187
|
+
const answers = [];
|
|
1188
|
+
settled.forEach((r, i) => {
|
|
1189
|
+
const m = members[i];
|
|
1190
|
+
console.log('');
|
|
1191
|
+
ui.divider('line');
|
|
1192
|
+
if (r.status === 'fulfilled') {
|
|
1193
|
+
const text = (r.value || '').trim();
|
|
1194
|
+
console.log(` ${b(cream(m.label))} ${slate('(' + m.role + ')')}`);
|
|
1195
|
+
console.log('');
|
|
1196
|
+
console.log(text.split('\n').map(l => ' ' + l).join('\n'));
|
|
1197
|
+
answers.push(`### ${m.label}\n${text}`);
|
|
1198
|
+
} else {
|
|
1199
|
+
console.log(` ${b(cream(m.label))} ${sage('failed')} ${slate('— ' + r.reason.message.split('\n')[0])}`);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
console.log('');
|
|
1203
|
+
ui.divider('line');
|
|
1204
|
+
|
|
1205
|
+
if (answers.length > 0) {
|
|
1206
|
+
messages.push({ role: 'user', content: cmdArg });
|
|
1207
|
+
messages.push({ role: 'assistant', content: `[council of ${answers.length}]\n\n${answers.join('\n\n')}` });
|
|
1208
|
+
awaitingOutcome = decisionId;
|
|
1209
|
+
console.log(slate(` council of ${answers.length} · outcome? 1 kept · 2 reverted · 3 superseded · 4 failed · or keep typing`));
|
|
1210
|
+
} else {
|
|
1211
|
+
try { labelOutcome(decisionId, 'failed'); } catch { /* keep going */ }
|
|
1212
|
+
console.log(` ${sage('Every council member failed — check')} ${cream('/provider')}`);
|
|
1103
1213
|
}
|
|
1214
|
+
console.log('');
|
|
1104
1215
|
rl.prompt();
|
|
1105
1216
|
return;
|
|
1106
1217
|
}
|
|
@@ -1120,7 +1231,8 @@ async function main() {
|
|
|
1120
1231
|
}
|
|
1121
1232
|
rows.push(['API key', config?.apiKey ? config.apiKey.slice(0, 8) + '... (' + (config.provider || 'anthropic') + ')' : 'not set — optional', config?.apiKey ? 'green' : 'yellow']);
|
|
1122
1233
|
rows.push(['Fallback', (config?.fallback === 'auto' ? 'auto-switch on failure' : 'ask before switching') + ' — /fallback to change', 'peach']);
|
|
1123
|
-
if (route?.type === 'api') rows.push(['Model',
|
|
1234
|
+
if (route?.type === 'api') rows.push(['Model', modelName(currentModel), 'cyan']);
|
|
1235
|
+
if (route?.type === 'harness' && harnessModel) rows.push(['Model', `${harnessModel} — passed to ${HARNESSES[route.id].label}`, 'cyan']);
|
|
1124
1236
|
ui.statusPanel('Provider', rows);
|
|
1125
1237
|
console.log(` ${sage('One terminal. Every AI worker. Shared project memory.')}`);
|
|
1126
1238
|
console.log(` ${slate('switch:')} ${cream('/use <' + useOptions().join('|') + '>')} ${slate('· interactive tools: /work <hermes|pi>')}`);
|
|
@@ -1348,7 +1460,7 @@ async function main() {
|
|
|
1348
1460
|
config.apiKey,
|
|
1349
1461
|
[{ role: 'user', content: cmdArg }],
|
|
1350
1462
|
systemPrompt,
|
|
1351
|
-
|
|
1463
|
+
modelId(currentModel)
|
|
1352
1464
|
);
|
|
1353
1465
|
if (result.promptTokens || result.completionTokens) {
|
|
1354
1466
|
console.log(slate(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
|
|
@@ -1356,7 +1468,7 @@ async function main() {
|
|
|
1356
1468
|
trackSap({
|
|
1357
1469
|
userId: config.supabaseUserId,
|
|
1358
1470
|
source: 'cli',
|
|
1359
|
-
model:
|
|
1471
|
+
model: modelId(currentModel),
|
|
1360
1472
|
promptTokens: result.promptTokens,
|
|
1361
1473
|
completionTokens: result.completionTokens,
|
|
1362
1474
|
accessToken: config.supabaseAccessToken,
|
|
@@ -1390,6 +1502,26 @@ async function main() {
|
|
|
1390
1502
|
: null;
|
|
1391
1503
|
const fullSystem = modeHint ? `${systemPrompt}\n\n${modeHint}` : systemPrompt;
|
|
1392
1504
|
|
|
1505
|
+
// @mention: route ONE message to a specific harness without switching.
|
|
1506
|
+
// The answer lands in the shared history, so the next turn — on any
|
|
1507
|
+
// route — knows what was said. "@codex review this" mid-claude-session.
|
|
1508
|
+
const mention = input.match(/^@([\w-]+)\s+([\s\S]+)/);
|
|
1509
|
+
if (mention) {
|
|
1510
|
+
const q = mention[1].toLowerCase();
|
|
1511
|
+
const target = harnesses.find(h => h.installed && h.headless &&
|
|
1512
|
+
(h.id === q || h.id.startsWith(q) || h.label.toLowerCase().replace(/\s+/g, '-').startsWith(q)));
|
|
1513
|
+
if (!target) {
|
|
1514
|
+
console.log(` ${sage('No installed harness matches')} ${cream('@' + q)} ${sage('—')} ${cream('/provider')} ${sage('shows who\'s here.')}`);
|
|
1515
|
+
rl.prompt();
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
const okMention = await runHarnessTurn(mention[2], target.id, fullSystem);
|
|
1519
|
+
if (!okMention) await offerFallbacks(mention[2], fullSystem, target.id);
|
|
1520
|
+
console.log('');
|
|
1521
|
+
rl.prompt();
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1393
1525
|
const ok = route.type === 'harness'
|
|
1394
1526
|
? await runHarnessTurn(input, route.id, fullSystem)
|
|
1395
1527
|
: await runApiTurn(input, fullSystem);
|
package/lib/harnesses.js
CHANGED
|
@@ -14,15 +14,23 @@ const { execSync, spawn } = require('child_process');
|
|
|
14
14
|
// args: how to run a one-shot prompt headlessly. args: null = we only know
|
|
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
|
+
//
|
|
18
|
+
// args takes (prompt, model). model is passed straight through to the
|
|
19
|
+
// harness's own flag and validated BY the harness — phewsh keeps no model
|
|
20
|
+
// list of its own, so it can never go stale. Harnesses without a known
|
|
21
|
+
// model flag ignore the preference and use their own config.
|
|
17
22
|
const HARNESSES = {
|
|
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'] },
|
|
23
|
+
'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', auth: 'Claude subscription / Console', models: true, args: (p, m) => ['-p', p, '--output-format', 'text', ...(m ? ['--model', m] : [])] },
|
|
24
|
+
'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', auth: 'ChatGPT plan', models: true, args: (p, m) => ['exec', ...(m ? ['-m', m] : []), p] },
|
|
25
|
+
'gemini': { bin: 'gemini', label: 'Gemini CLI', role: "another model's take", auth: 'Google login', models: true, args: (p, m) => ['-p', p, ...(m ? ['-m', m] : [])] },
|
|
26
|
+
'cursor': { bin: 'cursor-agent', label: 'Cursor Agent', role: 'edits files', auth: 'Cursor account', models: true, args: (p, m) => ['-p', p, '--output-format', 'text', ...(m ? ['--model', m] : [])] },
|
|
22
27
|
'opencode': { bin: 'opencode', label: 'OpenCode', role: 'general agent', auth: 'OpenCode Zen / configured', args: (p) => ['run', p] },
|
|
28
|
+
'grok': { bin: 'grok', label: 'Grok Build', role: "xAI's take", auth: 'SuperGrok / X Premium+', args: (p) => ['-p', p] },
|
|
29
|
+
'kiro': { bin: 'kiro-cli', label: 'Kiro CLI', role: 'spec-driven dev', auth: 'Kiro / AWS account', args: (p) => ['chat', '--no-interactive', p] },
|
|
30
|
+
'copilot': { bin: 'copilot', label: 'Copilot CLI', role: 'github-native', auth: 'GitHub Copilot plan', args: (p) => ['-p', p] },
|
|
23
31
|
'hermes': { bin: 'hermes', label: 'Hermes', role: 'runs loops', auth: 'Nous account', args: null },
|
|
24
32
|
'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] },
|
|
33
|
+
'aider': { bin: 'aider', label: 'Aider', role: 'pair-codes', auth: 'configured keys', models: true, args: (p, m) => ['--message', p, ...(m ? ['--model', m] : [])] },
|
|
26
34
|
'goose': { bin: 'goose', label: 'Goose', role: 'automates tasks', auth: 'Block / configured', args: (p) => ['run', '-t', p] },
|
|
27
35
|
'amp': { bin: 'amp', label: 'Amp', role: 'agentic coding', auth: 'Sourcegraph account', args: (p) => ['-x', p] },
|
|
28
36
|
'droid': { bin: 'droid', label: 'Droid', role: 'agentic coding', auth: 'Factory account', args: (p) => ['exec', p] },
|
|
@@ -51,24 +59,27 @@ function listHarnesses() {
|
|
|
51
59
|
* stderr is buffered and only surfaced on failure (codex/gemini chat on it).
|
|
52
60
|
* Resolves with the full stdout text so callers can keep conversation history.
|
|
53
61
|
*/
|
|
54
|
-
function runViaHarness(id, systemPrompt, userPrompt) {
|
|
62
|
+
function runViaHarness(id, systemPrompt, userPrompt, opts = {}) {
|
|
55
63
|
const h = HARNESSES[id];
|
|
56
64
|
if (!h) return Promise.reject(new Error(`Unknown harness: ${id}`));
|
|
57
65
|
if (!h.args) return Promise.reject(new Error(`${h.label} is interactive-only here — launch it with /work ${id}`));
|
|
58
66
|
const prompt = systemPrompt ? `${systemPrompt}\n\n---\n\n${userPrompt}` : userPrompt;
|
|
67
|
+
const model = h.models ? opts.model : undefined;
|
|
59
68
|
|
|
60
69
|
return new Promise((resolve, reject) => {
|
|
61
|
-
const child = spawn(h.bin, h.args(prompt), { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
70
|
+
const child = spawn(h.bin, h.args(prompt, model), { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
62
71
|
// Some harnesses (codex exec, gemini) wait for stdin EOF before running.
|
|
63
72
|
child.stdin.end();
|
|
64
73
|
|
|
65
74
|
let stdout = '';
|
|
66
75
|
let stderr = '';
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
// quiet: collect without streaming — parallel runs (council) would
|
|
77
|
+
// interleave live output into soup.
|
|
78
|
+
if (!opts.quiet) process.stdout.write('\n');
|
|
79
|
+
child.stdout.on('data', (d) => { if (!opts.quiet) process.stdout.write(d); stdout += d.toString(); });
|
|
69
80
|
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
70
81
|
child.on('close', (code) => {
|
|
71
|
-
process.stdout.write('\n');
|
|
82
|
+
if (!opts.quiet) process.stdout.write('\n');
|
|
72
83
|
if (code === 0) resolve(stdout);
|
|
73
84
|
else reject(new Error(`${h.label} exited ${code}${stderr ? `\n ${stderr.trim().split('\n').slice(-3).join('\n ')}` : ''}`));
|
|
74
85
|
});
|
package/lib/providers.js
CHANGED
|
@@ -253,8 +253,12 @@ function streamParser(provider) {
|
|
|
253
253
|
return provider.format === 'anthropic' ? streamAnthropicResponse : streamOpenAIResponse;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
// One place for the Anthropic default — commands must not pin their own.
|
|
257
|
+
const DEFAULT_ANTHROPIC_MODEL = PROVIDERS.anthropic.defaultModel;
|
|
258
|
+
|
|
256
259
|
module.exports = {
|
|
257
260
|
PROVIDERS,
|
|
261
|
+
DEFAULT_ANTHROPIC_MODEL,
|
|
258
262
|
getProvider,
|
|
259
263
|
listProviders,
|
|
260
264
|
detectProvider,
|