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 +21 -7
- package/bin/phewsh.js +3 -0
- package/commands/ambient.js +238 -0
- package/commands/clarify.js +1 -1
- package/commands/gate.js +1 -1
- package/commands/hook.js +120 -0
- package/commands/intent.js +1 -1
- package/commands/mcp.js +53 -58
- package/commands/session.js +324 -19
- 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,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
|
-
##
|
|
34
|
+
## One package, one system
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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;
|
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/hook.js
ADDED
|
@@ -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;
|
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
|
}
|