phewsh 0.14.5 → 0.15.0

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 CHANGED
@@ -31,18 +31,32 @@ 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
- ## Two packages, one system
34
+ ## One package, one system
35
35
 
36
- - **`phewsh`** (this package) — the CLI: intent authoring, sync, bridges,
37
- receipts, dispatch.
38
- - **`phewsh-mcp-server`** the MCP server that `phewsh mcp setup` wires into
39
- Claude Code / Cursor / any MCP client. It gives an *interactive* agent
40
- session your project's briefing, task queue, and enforcement gate over
41
- stdio, and shares state with the bridges (`~/.phewsh/`).
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.
45
45
 
46
+ ## Ambient — continuity without launching phewsh
47
+
48
+ ```bash
49
+ phewsh ambient on
50
+ ```
51
+
52
+ A consent screen shows exactly what changes (two hook entries in Claude
53
+ Code's settings), then: every Claude Code session in a project with
54
+ `.intent/` starts pre-briefed, and every session leaves a one-line
55
+ metadata breadcrumb (never transcript contents) in
56
+ `~/.phewsh/ambient-sessions.jsonl`. `phewsh ambient status` shows the
57
+ full ledger; `phewsh ambient off` removes everything. Mission control
58
+ is optional. Continuity is not.
59
+
46
60
  ## Live Execution
47
61
 
48
62
  Connect the web app to your local machine for real-time task execution:
package/bin/phewsh.js CHANGED
@@ -71,6 +71,8 @@ const COMMANDS = {
71
71
  serve: () => require('../commands/serve')(),
72
72
  sequence: () => require('../commands/sequence')(),
73
73
  seq: () => require('../commands/sequence')(),
74
+ ambient: () => require('../commands/ambient')(),
75
+ hook: () => require('../commands/hook')(),
74
76
  help: showHelp,
75
77
  version: showVersion,
76
78
  };
@@ -103,6 +105,7 @@ function showHelp() {
103
105
  console.log(` ${cyan('push/pull')} ${g('Manual sync to/from phewsh.com/intent')}`);
104
106
  console.log(` ${cyan('serve')} ${g('Execution bridge — run from phewsh.com/intent')}`);
105
107
  console.log(` ${cyan('mcp')} ${g('Connect AI agents via MCP protocol')}`);
108
+ console.log(` ${cyan('ambient')} ${g('Continuity without launching phewsh — enhance your other tools')}`);
106
109
  console.log(` ${cyan('receipts')} ${g('Proof trail — what agents actually did, with evidence')}`);
107
110
  console.log(` ${cyan('outcomes')} ${g('Decision record — what was kept, reverted, or failed')}`);
108
111
  console.log(` ${cyan('bypass')} ${g('Went around phewsh? Record why — 10 seconds, no guilt')}`);
@@ -0,0 +1,238 @@
1
+ // phewsh ambient — continuity without launching phewsh.
2
+ //
3
+ // Principle (Jun 12): ambient before primary. Installing phewsh should make
4
+ // the tools you already use better — context arrives, sessions leave
5
+ // breadcrumbs — without you ever typing `phewsh`.
6
+ //
7
+ // Constraint (non-negotiable): radical transparency. Nothing changes
8
+ // without a consent screen that names every file and every line. The
9
+ // ledger (~/.phewsh/ambient.json) records exactly what was applied and
10
+ // how to undo it. Trust matters more than automation here.
11
+ //
12
+ // phewsh ambient — status: detected tools, what's enhanced
13
+ // phewsh ambient on [--yes] — consent screen, then apply
14
+ // phewsh ambient off — remove everything, update ledger
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+ const readline = require('readline');
20
+ const { listHarnesses } = require('../lib/harnesses');
21
+
22
+ const PHEWSH_DIR = path.join(os.homedir(), '.phewsh');
23
+ const LEDGER_FILE = path.join(PHEWSH_DIR, 'ambient.json');
24
+ const CLAUDE_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json');
25
+
26
+ const HOOK_START = { type: 'command', command: 'phewsh hook session-start' };
27
+ const HOOK_END = { type: 'command', command: 'phewsh hook session-end' };
28
+
29
+ // ANSI helpers (256-color per cli/lib/ui.js palette rules)
30
+ const b = (s) => `\x1b[1m${s}\x1b[0m`;
31
+ const teal = (s) => `\x1b[38;5;79m${s}\x1b[0m`;
32
+ const sage = (s) => `\x1b[38;5;151m${s}\x1b[0m`;
33
+ const slate = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
34
+ const cream = (s) => `\x1b[38;5;230m${s}\x1b[0m`;
35
+ const peach = (s) => `\x1b[38;5;216m${s}\x1b[0m`;
36
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
37
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
38
+
39
+ function loadLedger() {
40
+ try { return JSON.parse(fs.readFileSync(LEDGER_FILE, 'utf-8')); } catch { return { version: 1, applied: {}, seenHarnesses: [] }; }
41
+ }
42
+
43
+ function saveLedger(ledger) {
44
+ if (!fs.existsSync(PHEWSH_DIR)) fs.mkdirSync(PHEWSH_DIR, { recursive: true });
45
+ fs.writeFileSync(LEDGER_FILE, JSON.stringify(ledger, null, 2));
46
+ }
47
+
48
+ function loadClaudeSettings() {
49
+ try { return JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, 'utf-8')); } catch { return null; }
50
+ }
51
+
52
+ function hasHook(settings, eventName, command) {
53
+ const entries = settings?.hooks?.[eventName];
54
+ if (!Array.isArray(entries)) return false;
55
+ return entries.some(e => (e.hooks || []).some(h => h.command === command));
56
+ }
57
+
58
+ function claudeApplied() {
59
+ const s = loadClaudeSettings();
60
+ return !!s && hasHook(s, 'SessionStart', HOOK_START.command) && hasHook(s, 'SessionEnd', HOOK_END.command);
61
+ }
62
+
63
+ function applyClaudeHooks() {
64
+ const settings = loadClaudeSettings() || {};
65
+ settings.hooks = settings.hooks || {};
66
+ const changes = [];
67
+ for (const [event, hook] of [['SessionStart', HOOK_START], ['SessionEnd', HOOK_END]]) {
68
+ settings.hooks[event] = settings.hooks[event] || [];
69
+ if (!hasHook(settings, event, hook.command)) {
70
+ settings.hooks[event].push({ hooks: [hook] });
71
+ changes.push(`hooks.${event} += "${hook.command}"`);
72
+ }
73
+ }
74
+ if (changes.length > 0) {
75
+ fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
76
+ }
77
+ return changes;
78
+ }
79
+
80
+ function removeClaudeHooks() {
81
+ const settings = loadClaudeSettings();
82
+ if (!settings?.hooks) return [];
83
+ const removed = [];
84
+ for (const [event, hook] of [['SessionStart', HOOK_START], ['SessionEnd', HOOK_END]]) {
85
+ const entries = settings.hooks[event];
86
+ if (!Array.isArray(entries)) continue;
87
+ const before = entries.length;
88
+ settings.hooks[event] = entries
89
+ .map(e => ({ ...e, hooks: (e.hooks || []).filter(h => h.command !== hook.command) }))
90
+ .filter(e => e.hooks.length > 0);
91
+ if (settings.hooks[event].length !== before) removed.push(`hooks.${event} -= "${hook.command}"`);
92
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
93
+ }
94
+ if (removed.length > 0) fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
95
+ return removed;
96
+ }
97
+
98
+ function showConsentScreen(harnesses) {
99
+ console.log('');
100
+ console.log(` ${b(cream('PHEWSH ambient'))} ${sage('— continuity without launching phewsh')}`);
101
+ console.log('');
102
+ console.log(` ${sage('Detected on this machine:')}`);
103
+ for (const h of harnesses.filter(h => h.installed)) {
104
+ const note = h.id === 'claude-code' ? teal('enhanceable now') : slate('detection only in v1 — `phewsh sequence` + `phewsh mcp setup` work today');
105
+ console.log(` ${green('✓')} ${cream(h.label.padEnd(14))} ${note}`);
106
+ }
107
+ console.log('');
108
+ console.log(` ${b('Claude Code enhancements:')}`);
109
+ console.log(` ${teal('Context sync')} ${sage('SessionStart hook — when a project has')} ${cream('.intent/')}${sage(', a short brief')}`);
110
+ console.log(` ${sage('(vision, next steps, constraints) is injected at session start.')}`);
111
+ console.log(` ${teal('Session capture')} ${sage('SessionEnd hook — appends one metadata line (time, project, cwd)')}`);
112
+ console.log(` ${sage('to')} ${cream('~/.phewsh/ambient-sessions.jsonl')}${sage('.')} ${b(sage('Never transcript contents.'))}`);
113
+ console.log('');
114
+ console.log(` ${peach('Exactly what changes:')} ${sage('two hook entries in')} ${cream(CLAUDE_SETTINGS)}${sage('. Nothing else is touched.')}`);
115
+ console.log(` ${sage('Undo anytime:')} ${cream('phewsh ambient off')} ${sage('· full record:')} ${cream('phewsh ambient status')}`);
116
+ console.log('');
117
+ }
118
+
119
+ async function confirm(question) {
120
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
121
+ return new Promise(resolve => {
122
+ rl.question(question, answer => {
123
+ rl.close();
124
+ resolve(/^y(es)?$/i.test(answer.trim()));
125
+ });
126
+ });
127
+ }
128
+
129
+ async function turnOn(skipConfirm) {
130
+ const harnesses = listHarnesses();
131
+ showConsentScreen(harnesses);
132
+
133
+ if (claudeApplied()) {
134
+ console.log(` ${green('Already applied.')} ${sage('Status:')} ${cream('phewsh ambient status')}`);
135
+ console.log('');
136
+ return;
137
+ }
138
+
139
+ if (!skipConfirm) {
140
+ const ok = await confirm(` ${b('Apply?')} ${slate('[y/N] ')}`);
141
+ if (!ok) {
142
+ console.log(` ${sage('Nothing changed.')}`);
143
+ console.log('');
144
+ return;
145
+ }
146
+ }
147
+
148
+ const changes = applyClaudeHooks();
149
+ const ledger = loadLedger();
150
+ ledger.applied['claude-code'] = {
151
+ at: new Date().toISOString(),
152
+ file: CLAUDE_SETTINGS,
153
+ changes,
154
+ captures: '~/.phewsh/ambient-sessions.jsonl — timestamp, project, cwd only',
155
+ undo: 'phewsh ambient off',
156
+ };
157
+ ledger.seenHarnesses = harnesses.filter(h => h.installed).map(h => h.id);
158
+ saveLedger(ledger);
159
+
160
+ console.log('');
161
+ console.log(` ${green('●')} ${b('Ambient is on.')}`);
162
+ changes.forEach(c => console.log(` ${teal('+')} ${slate(c)}`));
163
+ console.log('');
164
+ console.log(` ${sage('Next Claude Code session in a project with')} ${cream('.intent/')} ${sage('starts pre-briefed.')}`);
165
+ console.log(` ${sage('You never have to launch phewsh for this to work.')}`);
166
+ console.log('');
167
+ }
168
+
169
+ function turnOff() {
170
+ const removed = removeClaudeHooks();
171
+ const ledger = loadLedger();
172
+ delete ledger.applied['claude-code'];
173
+ saveLedger(ledger);
174
+ console.log('');
175
+ if (removed.length > 0) {
176
+ console.log(` ${green('●')} ${b('Ambient is off.')}`);
177
+ removed.forEach(c => console.log(` ${peach('-')} ${slate(c)}`));
178
+ console.log(` ${sage('Breadcrumb log kept at')} ${cream('~/.phewsh/ambient-sessions.jsonl')} ${sage('— delete it if you want; phewsh never will.')}`);
179
+ } else {
180
+ console.log(` ${sage('Nothing was applied — nothing to remove.')}`);
181
+ }
182
+ console.log('');
183
+ }
184
+
185
+ function status() {
186
+ const harnesses = listHarnesses();
187
+ const ledger = loadLedger();
188
+ console.log('');
189
+ console.log(` ${b(cream('PHEWSH ambient'))} ${sage('— status')}`);
190
+ console.log('');
191
+ for (const h of harnesses.filter(h => h.installed)) {
192
+ if (h.id === 'claude-code') {
193
+ const on = claudeApplied();
194
+ console.log(` ${on ? green('●') : yellow('○')} ${cream(h.label.padEnd(14))} ${on ? teal('ambient on') : sage('not enhanced — phewsh ambient on')}`);
195
+ const entry = ledger.applied['claude-code'];
196
+ if (entry) {
197
+ console.log(` ${slate('applied ' + entry.at)}`);
198
+ (entry.changes || []).forEach(c => console.log(` ${slate('· ' + c + ' (' + entry.file + ')')}`));
199
+ console.log(` ${slate('· captures: ' + entry.captures)}`);
200
+ }
201
+ } else {
202
+ console.log(` ${slate('○')} ${cream(h.label.padEnd(14))} ${slate('detected — v1 enhances Claude Code only')}`);
203
+ }
204
+ }
205
+
206
+ // New harnesses since last apply — the re-offer.
207
+ const seen = ledger.seenHarnesses || [];
208
+ const fresh = harnesses.filter(h => h.installed && !seen.includes(h.id));
209
+ if (seen.length > 0 && fresh.length > 0) {
210
+ console.log('');
211
+ console.log(` ${peach('New since last time:')} ${cream(fresh.map(h => h.label).join(', '))}`);
212
+ }
213
+
214
+ // Recent breadcrumbs — show the user exactly what ambient has recorded.
215
+ const logFile = path.join(PHEWSH_DIR, 'ambient-sessions.jsonl');
216
+ try {
217
+ const lines = fs.readFileSync(logFile, 'utf-8').trim().split('\n');
218
+ console.log('');
219
+ console.log(` ${sage('Last ambient breadcrumbs (' + lines.length + ' total):')}`);
220
+ lines.slice(-3).forEach(l => {
221
+ try {
222
+ const e = JSON.parse(l);
223
+ console.log(` ${slate(e.ts + ' · ' + e.event + ' · ' + (e.project || path.basename(e.cwd || '?')))}`);
224
+ } catch { /* skip bad line */ }
225
+ });
226
+ } catch { /* no breadcrumbs yet */ }
227
+ console.log('');
228
+ }
229
+
230
+ async function main() {
231
+ const sub = process.argv[3] || 'status';
232
+ const skipConfirm = process.argv.includes('--yes');
233
+ if (sub === 'on') return turnOn(skipConfirm);
234
+ if (sub === 'off') return turnOff();
235
+ return status();
236
+ }
237
+
238
+ module.exports = main;
@@ -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: 'claude-sonnet-4-6',
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: 'claude-sonnet-4-6',
240
+ model: require('../lib/providers').DEFAULT_ANTHROPIC_MODEL,
241
241
  max_tokens: 1024,
242
242
  messages: [{ role: 'user', content: prompt }],
243
243
  }),
@@ -0,0 +1,120 @@
1
+ // phewsh hook — runtime endpoints for ambient harness hooks.
2
+ //
3
+ // These are invoked BY other tools (Claude Code hooks), not by people.
4
+ // Contract: fast, silent when there's nothing to say, and they never
5
+ // read or store transcript contents — metadata only. What gets written
6
+ // is documented in `phewsh ambient status`.
7
+ //
8
+ // phewsh hook session-start stdout → injected into the agent's context
9
+ // phewsh hook session-end stdin (hook JSON) → metadata breadcrumb
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ const PHEWSH_DIR = path.join(os.homedir(), '.phewsh');
16
+ const AMBIENT_LOG = path.join(PHEWSH_DIR, 'ambient-sessions.jsonl');
17
+ const INTENT_DIR = path.join(process.cwd(), '.intent');
18
+
19
+ function readIfExists(p, maxBytes = 16384) {
20
+ try { return fs.readFileSync(p, 'utf-8').slice(0, maxBytes); } catch { return null; }
21
+ }
22
+
23
+ function projectName() {
24
+ const meta = readIfExists(path.join(INTENT_DIR, 'project.json'));
25
+ if (meta) {
26
+ try { const m = JSON.parse(meta); if (m.name) return m.name; } catch { /* fall through */ }
27
+ }
28
+ return path.basename(process.cwd());
29
+ }
30
+
31
+ function appendBreadcrumb(event, extra = {}) {
32
+ try {
33
+ if (!fs.existsSync(PHEWSH_DIR)) fs.mkdirSync(PHEWSH_DIR, { recursive: true });
34
+ const line = JSON.stringify({
35
+ ts: new Date().toISOString(),
36
+ event,
37
+ cwd: process.cwd(),
38
+ project: fs.existsSync(INTENT_DIR) ? projectName() : null,
39
+ source: 'claude-code-ambient',
40
+ ...extra,
41
+ });
42
+ fs.appendFileSync(AMBIENT_LOG, line + '\n');
43
+ } catch { /* breadcrumbs must never break the host tool */ }
44
+ }
45
+
46
+ function firstLines(text, n) {
47
+ let body = text;
48
+ // Strip YAML frontmatter — metadata, not context.
49
+ if (body.startsWith('---')) {
50
+ const end = body.indexOf('\n---', 3);
51
+ if (end !== -1) body = body.slice(end + 4);
52
+ }
53
+ return body.split('\n')
54
+ .filter(l => l.trim())
55
+ .filter(l => !/^#+\s*$/.test(l.trim()))
56
+ .slice(0, n)
57
+ .join('\n');
58
+ }
59
+
60
+ function sessionStart() {
61
+ if (!fs.existsSync(path.join(INTENT_DIR, 'vision.md')) &&
62
+ !fs.existsSync(path.join(INTENT_DIR, 'plan.md'))) {
63
+ // No .intent/ here — stay silent, cost the host nothing.
64
+ process.exit(0);
65
+ }
66
+
67
+ const parts = [];
68
+ parts.push(`# Project brief (from .intent/ — PHEWSH continuity layer)`);
69
+ parts.push(`Project: ${projectName()}`);
70
+
71
+ const meta = readIfExists(path.join(INTENT_DIR, 'project.json'));
72
+ if (meta) {
73
+ try {
74
+ const m = JSON.parse(meta);
75
+ if (m.tldr) parts.push(`TLDR: ${m.tldr}`);
76
+ if (m.constraints) {
77
+ const c = Object.entries(m.constraints).map(([k, v]) => `${k}: ${v}`).join(' · ');
78
+ if (c) parts.push(`Constraints: ${c}`);
79
+ }
80
+ } catch { /* skip meta */ }
81
+ }
82
+
83
+ const vision = readIfExists(path.join(INTENT_DIR, 'vision.md'));
84
+ if (vision) parts.push(`\n## Vision (excerpt)\n${firstLines(vision, 8)}`);
85
+
86
+ const next = readIfExists(path.join(INTENT_DIR, 'next.md'));
87
+ if (next) parts.push(`\n## Next (excerpt)\n${firstLines(next, 8)}`);
88
+
89
+ const status = readIfExists(path.join(INTENT_DIR, 'status.md'));
90
+ if (status) parts.push(`\n## Status (excerpt)\n${firstLines(status, 5)}`);
91
+
92
+ parts.push(`\n(Brief injected by PHEWSH ambient from .intent/. Honor the constraints above. The human can run \`phewsh\` for mission control — council, outcomes, the decision record.)`);
93
+
94
+ process.stdout.write(parts.join('\n') + '\n');
95
+ appendBreadcrumb('session-start');
96
+ process.exit(0);
97
+ }
98
+
99
+ function sessionEnd() {
100
+ let stdin = '';
101
+ process.stdin.on('data', d => { stdin += d.toString(); });
102
+ process.stdin.on('end', () => {
103
+ let reason = null;
104
+ try { reason = JSON.parse(stdin).reason || null; } catch { /* metadata only; fine */ }
105
+ appendBreadcrumb('session-end', reason ? { reason } : {});
106
+ process.exit(0);
107
+ });
108
+ // If the host never closes stdin, don't hang it.
109
+ setTimeout(() => { appendBreadcrumb('session-end'); process.exit(0); }, 1500);
110
+ }
111
+
112
+ function main() {
113
+ const event = process.argv[3];
114
+ if (event === 'session-start') return sessionStart();
115
+ if (event === 'session-end') return sessionEnd();
116
+ // Unknown event: exit silently — hooks must never error the host tool.
117
+ process.exit(0);
118
+ }
119
+
120
+ module.exports = main;
@@ -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: 'claude-sonnet-4-6',
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 Install MCP server deps + configure Claude Code
5
- // phewsh mcp sync — Sync local .intent/ + cloud projects → ~/.phewsh/projects.json
6
- // phewsh mcp status — Check what agents can see right now
7
- // phewsh mcp serve — Start the HTTP transport for the web bridge (:7483)
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 { execSync, spawn } = require('child_process');
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
- function findMcpServerPath() {
134
- // Check common locations
135
- const candidates = [
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
- for (const p of candidates) {
147
- if (fs.existsSync(p)) return path.resolve(p);
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
- const stdioPath = findMcpServerPath();
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 mcp/src/http-server.js`);
183
- console.log(` ${g('Make sure you have the latest phewsh monorepo or reinstall the MCP package.')}`);
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. Find or install MCP server
226
- let serverPath = findMcpServerPath();
233
+ // 1. Confirm the bundled server is present
234
+ const serverPath = findMcpServerPath();
227
235
 
228
236
  if (!serverPath) {
229
- console.log(` ${yellow('MCP server not found locally.')}`);
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('Found MCP server:')} ${g(serverPath)}`);
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
- // 3. Generate settings.json snippet
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: 'node',
260
- args: [serverPath],
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
- if (existing.mcpServers?.phewsh) {
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')} Install and configure the MCP server`);
446
- console.log(` ${w('sync')} Sync projects → ~/.phewsh/projects.json`);
447
- console.log(` ${w('status')} Check what agents can see right now`);
448
- console.log(` ${w('serve')} Run the HTTP bridge for the web app (:7483)`);
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
  }