phewsh 0.14.6 → 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 +14 -0
- package/bin/phewsh.js +3 -0
- package/commands/ambient.js +238 -0
- package/commands/hook.js +120 -0
- package/commands/session.js +180 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,20 @@ state with the bridges (`~/.phewsh/`). No second install.
|
|
|
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/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/session.js
CHANGED
|
@@ -274,6 +274,31 @@ async function main() {
|
|
|
274
274
|
}
|
|
275
275
|
let currentModel = DEFAULT_MODEL;
|
|
276
276
|
let harnessModel = null; // pass-through preference; the harness validates it
|
|
277
|
+
let liveModelsCache = null; // fetched once per session from the provider's own list
|
|
278
|
+
|
|
279
|
+
// Discovery, not hardcoding: the provider's models endpoint is the truth.
|
|
280
|
+
async function fetchLiveModels() {
|
|
281
|
+
if (liveModelsCache) return liveModelsCache;
|
|
282
|
+
try {
|
|
283
|
+
if (config?.provider === 'openrouter') {
|
|
284
|
+
const res = await fetch('https://openrouter.ai/api/v1/models', { signal: AbortSignal.timeout(4000) });
|
|
285
|
+
if (res.ok) {
|
|
286
|
+
const data = await res.json();
|
|
287
|
+
liveModelsCache = (data.data || []).map(m => m.id);
|
|
288
|
+
}
|
|
289
|
+
} else if (config?.apiKey) {
|
|
290
|
+
const res = await fetch('https://api.anthropic.com/v1/models?limit=100', {
|
|
291
|
+
headers: { 'x-api-key': config.apiKey, 'anthropic-version': '2023-06-01' },
|
|
292
|
+
signal: AbortSignal.timeout(4000),
|
|
293
|
+
});
|
|
294
|
+
if (res.ok) {
|
|
295
|
+
const data = await res.json();
|
|
296
|
+
liveModelsCache = (data.data || []).map(m => m.id);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch { /* offline or no key — aliases still work */ }
|
|
300
|
+
return liveModelsCache;
|
|
301
|
+
}
|
|
277
302
|
let totalPromptTokens = 0;
|
|
278
303
|
let totalCompletionTokens = 0;
|
|
279
304
|
|
|
@@ -561,6 +586,29 @@ async function main() {
|
|
|
561
586
|
historySize: 100,
|
|
562
587
|
});
|
|
563
588
|
|
|
589
|
+
// Live input coloring: slash commands render teal, @mentions peach, as
|
|
590
|
+
// you type — same signal Claude Code gives. TTY-only, fail-soft: if the
|
|
591
|
+
// private readline API ever changes, input falls back to plain.
|
|
592
|
+
if (process.stdout.isTTY && typeof rl._writeToOutput === 'function') {
|
|
593
|
+
const origWrite = rl._writeToOutput.bind(rl);
|
|
594
|
+
rl._writeToOutput = function (s) {
|
|
595
|
+
try {
|
|
596
|
+
const cur = rl.line || '';
|
|
597
|
+
if ((cur[0] === '/' || cur[0] === '@') && typeof s === 'string' && s.includes(cur)) {
|
|
598
|
+
const c = cur[0] === '/' ? '\x1b[38;5;79m' : '\x1b[38;5;216m';
|
|
599
|
+
s = s.split(cur).join(c + cur + '\x1b[0m');
|
|
600
|
+
}
|
|
601
|
+
} catch { /* never break input */ }
|
|
602
|
+
origWrite(s);
|
|
603
|
+
};
|
|
604
|
+
process.stdin.on('keypress', () => {
|
|
605
|
+
try {
|
|
606
|
+
const cur = rl.line || '';
|
|
607
|
+
if (cur[0] === '/' || cur[0] === '@') rl._refreshLine();
|
|
608
|
+
} catch { /* never break input */ }
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
564
612
|
rl.prompt();
|
|
565
613
|
|
|
566
614
|
rl.on('line', async (line) => {
|
|
@@ -713,6 +761,7 @@ async function main() {
|
|
|
713
761
|
console.log('');
|
|
714
762
|
console.log(` ${cream('author .intent/')}`);
|
|
715
763
|
console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
|
|
764
|
+
console.log(` ${teal('/intent')} ${sage('Pause and reflect — view or update .intent/ before moving on')}`);
|
|
716
765
|
console.log(` ${teal('/clarify')} ${sage('Turn ideas into .intent/ artifacts')}`);
|
|
717
766
|
console.log(` ${teal('/gate')} ${sage('Set constraints (budget, time, skill)')}`);
|
|
718
767
|
console.log(` ${teal('/context')} ${sage('Show loaded .intent/ files')}`);
|
|
@@ -1085,16 +1134,41 @@ async function main() {
|
|
|
1085
1134
|
if (cmd === 'models') {
|
|
1086
1135
|
console.log('');
|
|
1087
1136
|
ui.divider('line');
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
console.log(`
|
|
1137
|
+
if (route?.type === 'harness') {
|
|
1138
|
+
const h = HARNESSES[route.id];
|
|
1139
|
+
console.log(` ${b(cream('Models'))} ${sage('— ' + h.label + ' owns its own model list')}`);
|
|
1140
|
+
ui.divider('line');
|
|
1141
|
+
console.log(` ${sage('Current preference:')} ${cream(harnessModel || h.label + ' default')}`);
|
|
1142
|
+
console.log(` ${sage('Set one with')} ${cream('/model <anything ' + h.label + ' accepts>')} ${slate('— it validates, not phewsh')}`);
|
|
1143
|
+
console.log(` ${slate('shortcuts that work for Claude Code: sonnet · opus · haiku · fable · any full model id')}`);
|
|
1144
|
+
console.log('');
|
|
1145
|
+
rl.prompt();
|
|
1146
|
+
return;
|
|
1093
1147
|
}
|
|
1094
|
-
|
|
1148
|
+
// API route: ask the provider for its real list.
|
|
1149
|
+
const providerName = config?.provider === 'openrouter' ? 'OpenRouter' : 'Anthropic';
|
|
1150
|
+
const live = await fetchLiveModels();
|
|
1151
|
+
if (live && live.length > 0) {
|
|
1152
|
+
console.log(` ${b(cream('Available models'))} ${sage('— live from ' + providerName)}`);
|
|
1153
|
+
ui.divider('line');
|
|
1154
|
+
const shown = live.slice(0, 24);
|
|
1155
|
+
shown.forEach(id => {
|
|
1156
|
+
const active = modelId(currentModel) === id ? ` ${teal('●')}` : '';
|
|
1157
|
+
console.log(` ${cream(id)}${active}`);
|
|
1158
|
+
});
|
|
1159
|
+
if (live.length > shown.length) console.log(` ${slate('… +' + (live.length - shown.length) + ' more — /model <id> takes any of them')}`);
|
|
1160
|
+
} else {
|
|
1161
|
+
console.log(` ${b(cream('Available models'))} ${slate('(offline — shortcuts only; any id still passes through)')}`);
|
|
1162
|
+
ui.divider('line');
|
|
1163
|
+
for (const [key, model] of Object.entries(MODELS)) {
|
|
1164
|
+
const active = key === currentModel ? ` ${teal('●')}` : '';
|
|
1165
|
+
console.log(` ${cream(key.padEnd(16))} ${sage(model.name)}${active}`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (!MODELS[currentModel] && !live?.includes(modelId(currentModel))) {
|
|
1095
1169
|
console.log(` ${cream(String(currentModel).padEnd(16))} ${sage('(pass-through)')} ${teal('●')}`);
|
|
1096
1170
|
}
|
|
1097
|
-
console.log(`\n ${sage('Switch with:')} ${cream('/model <name>')} ${slate('—
|
|
1171
|
+
console.log(`\n ${sage('Switch with:')} ${cream('/model <name>')} ${slate('— shortcuts: ' + Object.keys(MODELS).map(k => k.replace('claude-', '')).join(' · '))}\n`);
|
|
1098
1172
|
rl.prompt();
|
|
1099
1173
|
return;
|
|
1100
1174
|
}
|
|
@@ -1150,6 +1224,105 @@ async function main() {
|
|
|
1150
1224
|
return;
|
|
1151
1225
|
}
|
|
1152
1226
|
|
|
1227
|
+
if (cmd === 'intent') {
|
|
1228
|
+
// Pause and reflect before moving forward — the samurai check.
|
|
1229
|
+
// View what the project says it is; update it when reality moved.
|
|
1230
|
+
const artifacts = ['vision', 'plan', 'next', 'status'];
|
|
1231
|
+
const intentDir = path.join(process.cwd(), '.intent');
|
|
1232
|
+
|
|
1233
|
+
const renderMd = (raw) => {
|
|
1234
|
+
let body = raw;
|
|
1235
|
+
if (body.startsWith('---')) {
|
|
1236
|
+
const end = body.indexOf('\n---', 3);
|
|
1237
|
+
if (end !== -1) body = body.slice(end + 4);
|
|
1238
|
+
}
|
|
1239
|
+
return body.trim().split('\n').map(l => {
|
|
1240
|
+
if (/^#{1,2}\s/.test(l)) return ` ${b(teal(l.replace(/^#+\s*/, '')))}`;
|
|
1241
|
+
if (/^#{3,}\s/.test(l)) return ` ${cream(l.replace(/^#+\s*/, ''))}`;
|
|
1242
|
+
if (/^\s*[-*]\s/.test(l)) return ` ${teal('·')} ${sage(l.replace(/^\s*[-*]\s*/, ''))}`;
|
|
1243
|
+
if (/^\s*\d+\.\s/.test(l)) return ` ${sage(l.trim())}`;
|
|
1244
|
+
return ` ${slate(l)}`;
|
|
1245
|
+
}).join('\n');
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
if (!fs.existsSync(intentDir)) {
|
|
1249
|
+
console.log(` ${sage('No .intent/ here yet.')} ${cream('/init')} ${sage('creates it — that is the whole point.')}`);
|
|
1250
|
+
rl.prompt();
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const sub = (cmdArg || '').trim().toLowerCase();
|
|
1255
|
+
|
|
1256
|
+
if (sub.startsWith('view')) {
|
|
1257
|
+
const which = sub.split(/\s+/)[1];
|
|
1258
|
+
const targets = which && artifacts.includes(which) ? [which] : artifacts;
|
|
1259
|
+
for (const a of targets) {
|
|
1260
|
+
const p = path.join(intentDir, `${a}.md`);
|
|
1261
|
+
if (!fs.existsSync(p)) continue;
|
|
1262
|
+
console.log('');
|
|
1263
|
+
ui.divider('line');
|
|
1264
|
+
console.log(` ${b(cream(a.toUpperCase()))} ${slate('.intent/' + a + '.md')}`);
|
|
1265
|
+
ui.divider('line');
|
|
1266
|
+
console.log(renderMd(fs.readFileSync(p, 'utf-8')));
|
|
1267
|
+
}
|
|
1268
|
+
console.log('');
|
|
1269
|
+
console.log(` ${slate('moved on since this was written?')} ${cream('/intent update')}`);
|
|
1270
|
+
console.log('');
|
|
1271
|
+
rl.prompt();
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (sub === 'update') {
|
|
1276
|
+
// Reflect first: what does the record say happened since last update?
|
|
1277
|
+
console.log('');
|
|
1278
|
+
console.log(` ${b(cream('Before updating — what actually happened:'))}`);
|
|
1279
|
+
try {
|
|
1280
|
+
const { recentDecisions } = require('../lib/outcomes');
|
|
1281
|
+
const recent = recentDecisions(5, { project: projectName });
|
|
1282
|
+
if (recent.length > 0) {
|
|
1283
|
+
recent.forEach(d => {
|
|
1284
|
+
const mark = d.outcome === 'kept' ? green('✓') : d.outcome ? yellow('~') : slate('·');
|
|
1285
|
+
console.log(` ${mark} ${sage((d.summary || '').slice(0, 70))} ${slate('[' + (d.outcome || 'unlabeled') + ']')}`);
|
|
1286
|
+
});
|
|
1287
|
+
} else {
|
|
1288
|
+
console.log(` ${slate('no decisions recorded yet')}`);
|
|
1289
|
+
}
|
|
1290
|
+
} catch { console.log(` ${slate('record unavailable')}`); }
|
|
1291
|
+
console.log('');
|
|
1292
|
+
console.log(` ${teal('●')} ${sage('Handing you to the guided update')} ${slate('— exit to come back to phewsh')}`);
|
|
1293
|
+
console.log('');
|
|
1294
|
+
rl.pause();
|
|
1295
|
+
const { spawnSync } = require('child_process');
|
|
1296
|
+
spawnSync(process.execPath, [path.join(__dirname, '..', 'bin', 'phewsh.js'), 'clarify'], { stdio: 'inherit' });
|
|
1297
|
+
rl.resume();
|
|
1298
|
+
console.log('');
|
|
1299
|
+
console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate('/intent view to see the result — agents pick it up automatically')}`);
|
|
1300
|
+
console.log('');
|
|
1301
|
+
rl.prompt();
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// No arg: the pause. Where the project stands, in one screen.
|
|
1306
|
+
const present = artifacts.filter(a => fs.existsSync(path.join(intentDir, `${a}.md`)));
|
|
1307
|
+
const stale = present.map(a => {
|
|
1308
|
+
const raw = fs.readFileSync(path.join(intentDir, `${a}.md`), 'utf-8');
|
|
1309
|
+
const m = raw.match(/^updated:\s*(\S+)/m);
|
|
1310
|
+
return { a, updated: m ? m[1] : null };
|
|
1311
|
+
});
|
|
1312
|
+
console.log('');
|
|
1313
|
+
console.log(` ${b(cream('INTENT'))} ${slate('— .intent/ in ' + projectName)}`);
|
|
1314
|
+
stale.forEach(({ a, updated }) => {
|
|
1315
|
+
const age = updated ? slate('updated ' + updated) : slate('no date');
|
|
1316
|
+
console.log(` ${teal('·')} ${cream(a.padEnd(8))} ${age}`);
|
|
1317
|
+
});
|
|
1318
|
+
console.log('');
|
|
1319
|
+
console.log(` ${cream('/intent view')} ${slate('[vision|plan|next|status]')} ${sage('read it, rendered')}`);
|
|
1320
|
+
console.log(` ${cream('/intent update')} ${sage('reflect on the record, then guided rewrite')}`);
|
|
1321
|
+
console.log('');
|
|
1322
|
+
rl.prompt();
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1153
1326
|
if (cmd === 'council' || cmd === 'all') {
|
|
1154
1327
|
// One prompt, every installed harness, in parallel. Different
|
|
1155
1328
|
// models disagreeing is the signal — and which answer you KEEP
|