phewsh 0.15.72 → 0.15.73
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/commands/ambient.js +18 -0
- package/commands/hook.js +30 -1
- package/commands/pack.js +34 -1
- package/commands/session.js +49 -32
- package/lib/cors.js +6 -0
- package/package.json +1 -1
package/commands/ambient.js
CHANGED
|
@@ -262,6 +262,10 @@ async function turnOn(skipConfirm) {
|
|
|
262
262
|
|
|
263
263
|
function turnOff() {
|
|
264
264
|
const removed = removeClaudeHooks();
|
|
265
|
+
// Full reversibility: ambient off takes the lifecycle hooks with it too.
|
|
266
|
+
let gateRemoved = false;
|
|
267
|
+
try { gateRemoved = disablePreToolGate(); } catch { /* best-effort */ }
|
|
268
|
+
if (gateRemoved) removed.push('lifecycle hooks (PreToolUse gate + PostToolUse receipt)');
|
|
265
269
|
const { removed: removedGlobal } = selfheal.removeGlobalBaseFiles();
|
|
266
270
|
const { removed: removedProject } = selfheal.removeProjectContextFiles();
|
|
267
271
|
const { removed: removedSlash } = slash.removeSlashCommands();
|
|
@@ -269,6 +273,7 @@ function turnOff() {
|
|
|
269
273
|
delete ledger.applied['claude-code'];
|
|
270
274
|
delete ledger.applied.globalBase;
|
|
271
275
|
delete ledger.applied.slashCommands;
|
|
276
|
+
delete ledger.applied.lifecycle;
|
|
272
277
|
delete ledger.autoEnabledAt;
|
|
273
278
|
ledger.disabled = true; // sticky opt-out — first-run auto-enable must respect this
|
|
274
279
|
saveLedger(ledger);
|
|
@@ -365,6 +370,11 @@ async function ensureAuto() {
|
|
|
365
370
|
|
|
366
371
|
const hasClaude = installed.some(h => h.id === 'claude-code');
|
|
367
372
|
const changes = hasClaude ? applyClaudeHooks() : [];
|
|
373
|
+
// Neal's ruling (Jul 5): the lifecycle hooks (PreToolUse gate + PostToolUse
|
|
374
|
+
// receipt) install with first-run auto-enable too — they're fail-open,
|
|
375
|
+
// redacted, and removed by the same `phewsh ambient off` / `gate enforce off`.
|
|
376
|
+
let gateApplied = false;
|
|
377
|
+
if (hasClaude) { try { gateApplied = enablePreToolGate(); } catch { /* best-effort */ } }
|
|
368
378
|
const { written } = selfheal.syncGlobalBaseFiles();
|
|
369
379
|
const slashWritten = slash.installSlashCommands().written;
|
|
370
380
|
// Refresh existing project files only — first-run auto-enable must not dump
|
|
@@ -378,6 +388,13 @@ async function ensureAuto() {
|
|
|
378
388
|
captures: '~/.phewsh/ambient-sessions.jsonl — timestamp, project, cwd only',
|
|
379
389
|
undo: 'phewsh ambient off',
|
|
380
390
|
};
|
|
391
|
+
if (gateApplied) {
|
|
392
|
+
ledger.applied.lifecycle = {
|
|
393
|
+
at: now, file: CLAUDE_SETTINGS,
|
|
394
|
+
what: 'PreToolUse gate (deny protected paths, ask on high blast radius) + PostToolUse redacted receipt',
|
|
395
|
+
undo: 'phewsh gate enforce off (or phewsh ambient off)',
|
|
396
|
+
};
|
|
397
|
+
}
|
|
381
398
|
}
|
|
382
399
|
if (written.length > 0) {
|
|
383
400
|
ledger.applied.globalBase = { at: now, files: written, undo: 'phewsh ambient off' };
|
|
@@ -394,6 +411,7 @@ async function ensureAuto() {
|
|
|
394
411
|
console.log('');
|
|
395
412
|
console.log(` ${b(cream('phewsh set itself up across your AI tools'))} ${sage('— so they stay in sync with your project intent.')}`);
|
|
396
413
|
if (hasClaude) console.log(` ${teal('+')} ${slate('Claude Code gets a brief from a project\'s .intent/ at session start')}`);
|
|
414
|
+
if (gateApplied) console.log(` ${teal('+')} ${slate('guardrails: protected-path writes gated + a redacted receipt of what ran')}`);
|
|
397
415
|
written.forEach(f => console.log(` ${teal('+')} ${slate('environment note added to ' + f)}`));
|
|
398
416
|
if (slashWritten.length) console.log(` ${teal('+')} ${cream('/intent')} ${slate('command added to ' + slashWritten.join(', '))}`);
|
|
399
417
|
console.log(` ${sage('In any project with')} ${cream('.intent/')}${sage(', your tools now read its real intent. Reversible:')} ${cream('phewsh ambient off')}`);
|
package/commands/hook.js
CHANGED
|
@@ -74,10 +74,39 @@ function firstLines(text, n) {
|
|
|
74
74
|
.join('\n');
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// The accountability invitation. A real project (it has .git) with no
|
|
78
|
+
// .intent/ gets ONE gentle note injected — telling the agent to offer
|
|
79
|
+
// grounding once, and to drop it if declined. Once per project, ever
|
|
80
|
+
// (tracked in ~/.phewsh/intent-nudges.json); every later session is
|
|
81
|
+
// silent again. Invite, never nag — the recorded stance (decisions.md).
|
|
82
|
+
function maybeIntentInvitation() {
|
|
83
|
+
try {
|
|
84
|
+
if (!fs.existsSync(path.join(process.cwd(), '.git'))) return; // not a real project — cost nothing
|
|
85
|
+
const nudgeFile = path.join(PHEWSH_DIR, 'intent-nudges.json');
|
|
86
|
+
let seen = {};
|
|
87
|
+
try { seen = JSON.parse(fs.readFileSync(nudgeFile, 'utf-8')); } catch { /* first nudge ever */ }
|
|
88
|
+
if (seen && seen[process.cwd()]) return; // already invited here — never again
|
|
89
|
+
seen = (seen && typeof seen === 'object') ? seen : {};
|
|
90
|
+
seen[process.cwd()] = new Date().toISOString();
|
|
91
|
+
fs.mkdirSync(PHEWSH_DIR, { recursive: true });
|
|
92
|
+
fs.writeFileSync(nudgeFile, JSON.stringify(seen, null, 2));
|
|
93
|
+
process.stdout.write([
|
|
94
|
+
'# phewsh — one-time note for this project',
|
|
95
|
+
'This repo has no `.intent/` yet — no durable project truth that travels between AI tools.',
|
|
96
|
+
'If the user starts substantial (non-trivial) work here, offer ONCE, in plain language, to ground the project:',
|
|
97
|
+
'`phewsh clarify` (guided walk) or `phewsh init` (two questions) creates `.intent/` — vision, plan, and next steps that every AI tool reads, so the next session picks up where this one left off.',
|
|
98
|
+
'If they decline, or the work is trivial/exploratory, drop it and never raise it again. Do not create `.intent/` yourself uninvited.',
|
|
99
|
+
].join('\n') + '\n');
|
|
100
|
+
appendBreadcrumb('intent-invite');
|
|
101
|
+
} catch { /* the invitation is a nicety — never break the host */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
77
104
|
function sessionStart() {
|
|
78
105
|
if (!fs.existsSync(path.join(INTENT_DIR, 'vision.md')) &&
|
|
79
106
|
!fs.existsSync(path.join(INTENT_DIR, 'plan.md'))) {
|
|
80
|
-
// No .intent/ here —
|
|
107
|
+
// No .intent/ here — one gentle invitation if this is a real project,
|
|
108
|
+
// then silence forever.
|
|
109
|
+
maybeIntentInvitation();
|
|
81
110
|
process.exit(0);
|
|
82
111
|
}
|
|
83
112
|
|
package/commands/pack.js
CHANGED
|
@@ -36,7 +36,39 @@ function list() {
|
|
|
36
36
|
console.log(` ${slate('source: ' + p.source)}`);
|
|
37
37
|
}
|
|
38
38
|
console.log('');
|
|
39
|
-
console.log(` ${sage('Install:')} ${cream('phewsh pack install <name>')} ${sage('
|
|
39
|
+
console.log(` ${sage('Install:')} ${cream('phewsh pack install <name>')} ${sage('All official packs at once:')} ${cream('phewsh pack install all')}`);
|
|
40
|
+
console.log(` ${sage('Remove:')} ${cream('phewsh pack remove <name>')} ${sage('Read about every pack:')} ${cream('phewsh.com/cli#packs')}`);
|
|
41
|
+
console.log('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// `phewsh pack install all` — every official (vendored) pack in one confirmed
|
|
45
|
+
// pass. Linked packs stay pointers to their upstream source; we list them so
|
|
46
|
+
// nothing external ever installs silently.
|
|
47
|
+
async function installAll() {
|
|
48
|
+
const vendored = Object.entries(packs.PACKS).filter(([, p]) => p.kind !== 'linked');
|
|
49
|
+
const linked = Object.entries(packs.PACKS).filter(([, p]) => p.kind === 'linked');
|
|
50
|
+
const pending = vendored.filter(([name]) => !packs.isInstalled(name));
|
|
51
|
+
|
|
52
|
+
console.log('');
|
|
53
|
+
if (pending.length === 0) {
|
|
54
|
+
console.log(` ${sage('All official packs are already installed here.')}`);
|
|
55
|
+
} else {
|
|
56
|
+
console.log(` ${b(cream('Official phewsh packs'))} ${sage('— ' + pending.length + ' to install:')}`);
|
|
57
|
+
pending.forEach(([name, p]) => console.log(` ${cream(name.padEnd(16))} ${slate(p.desc)}`));
|
|
58
|
+
console.log('');
|
|
59
|
+
const ok = await confirm(` ${b('Install ' + (pending.length === 1 ? 'it' : 'all ' + pending.length) + ' here?')} ${slate('[y/N] ')}`);
|
|
60
|
+
if (!ok) { console.log(` ${sage('Nothing changed.')}\n`); return; }
|
|
61
|
+
for (const [name] of pending) {
|
|
62
|
+
const { written } = packs.install(name);
|
|
63
|
+
console.log(` ${green('●')} ${cream(name)} ${slate('→ ' + written.join(', '))}`);
|
|
64
|
+
}
|
|
65
|
+
console.log(` ${sage('Remove any:')} ${cream('phewsh pack remove <name>')}`);
|
|
66
|
+
}
|
|
67
|
+
if (linked.length > 0) {
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log(` ${sage(linked.length + ' more are linked packs — separate tools phewsh points at but never auto-installs:')}`);
|
|
70
|
+
console.log(` ${cream('phewsh pack')} ${sage('lists them ·')} ${cream('phewsh.com/cli#packs')} ${sage('tells their stories')}`);
|
|
71
|
+
}
|
|
40
72
|
console.log('');
|
|
41
73
|
}
|
|
42
74
|
|
|
@@ -89,6 +121,7 @@ function remove(name) {
|
|
|
89
121
|
async function main() {
|
|
90
122
|
const sub = process.argv[3];
|
|
91
123
|
const name = process.argv[4];
|
|
124
|
+
if (sub === 'install' && name === 'all') return installAll();
|
|
92
125
|
if (sub === 'install' && name) return install(name);
|
|
93
126
|
if (sub === 'remove' && name) return remove(name);
|
|
94
127
|
return list();
|
package/commands/session.js
CHANGED
|
@@ -733,6 +733,39 @@ async function main() {
|
|
|
733
733
|
console.log('');
|
|
734
734
|
}
|
|
735
735
|
|
|
736
|
+
// The project scanner — bootstrap option AND /scan slash command. Lists
|
|
737
|
+
// .intent/ projects plus likely candidates (git, no .intent yet, reason
|
|
738
|
+
// shown) from the usual folders, numbered so a bare digit opens one.
|
|
739
|
+
function runScanMenu() {
|
|
740
|
+
const spin = ui.spinner('scanning your usual folders');
|
|
741
|
+
const found = scanForProjects();
|
|
742
|
+
let candidates = [];
|
|
743
|
+
try { candidates = scanForCandidates(); } catch { /* advisory — scan still useful without it */ }
|
|
744
|
+
spin.stop();
|
|
745
|
+
if (found.length === 0 && candidates.length === 0) {
|
|
746
|
+
bootstrapChoices = null;
|
|
747
|
+
console.log(` ${sage('No .intent/ projects found in the usual folders.')}`);
|
|
748
|
+
console.log(` ${slate('cd into a project and run phewsh, or /init to start one here.')}`);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
bootstrapChoices = [];
|
|
752
|
+
if (found.length > 0) {
|
|
753
|
+
console.log(` ${teal('●')} ${sage(`Found ${found.length} project${found.length !== 1 ? 's' : ''} with .intent/:`)}`);
|
|
754
|
+
for (const p of found) {
|
|
755
|
+
bootstrapChoices.push({ kind: 'open', path: p.path });
|
|
756
|
+
console.log(` ${teal(String(bootstrapChoices.length))} ${cream(p.name)} ${slate('· ' + tildify(p.path))}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (candidates.length > 0) {
|
|
760
|
+
console.log(` ${teal('●')} ${sage(`${candidates.length} likely candidate${candidates.length !== 1 ? 's' : ''} — no shared memory yet:`)}`);
|
|
761
|
+
for (const p of candidates) {
|
|
762
|
+
bootstrapChoices.push({ kind: 'open', path: p.path });
|
|
763
|
+
console.log(` ${teal(String(bootstrapChoices.length))} ${cream(p.name)} ${slate('· ' + tildify(p.path) + ' · ' + p.reason)}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
console.log(` ${slate('pick a number to open it')}`);
|
|
767
|
+
}
|
|
768
|
+
|
|
736
769
|
function showBootstrapMenu(projects) {
|
|
737
770
|
console.log(` ${b(cream('Where do you want to work?'))}`);
|
|
738
771
|
bootstrapChoices = [];
|
|
@@ -976,7 +1009,7 @@ async function main() {
|
|
|
976
1009
|
// RECOGNIZED leading /command (or @harness) token turns teal (peach for @)
|
|
977
1010
|
// so you know it registered. Arguments stay plain. TTY-only, fail-soft.
|
|
978
1011
|
const KNOWN_COMMANDS = new Set([
|
|
979
|
-
'quit', 'exit', 'q', 'help', 'h', 'init', 'intent', 'clarify', 'model',
|
|
1012
|
+
'quit', 'exit', 'q', 'help', 'h', 'init', 'intent', 'clarify', 'scan', 'model',
|
|
980
1013
|
'models', 'council', 'all', 'provider', 'route', 'use', 'work', 'switch', 'run',
|
|
981
1014
|
'clear', 'status', 'key', 'login', 'export', 'push', 'pull', 'serve',
|
|
982
1015
|
'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
|
|
@@ -1277,8 +1310,11 @@ async function main() {
|
|
|
1277
1310
|
return;
|
|
1278
1311
|
}
|
|
1279
1312
|
|
|
1280
|
-
//
|
|
1281
|
-
|
|
1313
|
+
// Scan/bootstrap menu: a bare number opens a project, inits, or scans.
|
|
1314
|
+
// Any other input means the user moved on — drop the menu so it never
|
|
1315
|
+
// shadows a later digit (mirrors the nextChoices rule above).
|
|
1316
|
+
if (bootstrapChoices && !/^[0-9]{1,2}$/.test(input)) bootstrapChoices = null;
|
|
1317
|
+
if (bootstrapChoices && /^[0-9]{1,2}$/.test(input)) {
|
|
1282
1318
|
const choice = bootstrapChoices[parseInt(input, 10) - 1];
|
|
1283
1319
|
if (!choice) {
|
|
1284
1320
|
console.log(` ${sage('Pick 1-' + bootstrapChoices.length)}`);
|
|
@@ -1309,35 +1345,7 @@ async function main() {
|
|
|
1309
1345
|
return;
|
|
1310
1346
|
}
|
|
1311
1347
|
if (choice.kind === 'scan') {
|
|
1312
|
-
|
|
1313
|
-
const found = scanForProjects();
|
|
1314
|
-
// Likely candidates: real repos with no .intent/ yet — the projects
|
|
1315
|
-
// most worth grounding. Same shallow scan; shown with their reason.
|
|
1316
|
-
let candidates = [];
|
|
1317
|
-
try { candidates = scanForCandidates(); } catch { /* advisory — scan still useful without it */ }
|
|
1318
|
-
spin.stop();
|
|
1319
|
-
if (found.length === 0 && candidates.length === 0) {
|
|
1320
|
-
bootstrapChoices = null;
|
|
1321
|
-
console.log(` ${sage('No .intent/ projects found in the usual folders.')}`);
|
|
1322
|
-
console.log(` ${slate('cd into a project and run phewsh, or /init to start one here.')}`);
|
|
1323
|
-
} else {
|
|
1324
|
-
bootstrapChoices = [];
|
|
1325
|
-
if (found.length > 0) {
|
|
1326
|
-
console.log(` ${teal('●')} ${sage(`Found ${found.length} project${found.length !== 1 ? 's' : ''} with .intent/:`)}`);
|
|
1327
|
-
for (const p of found) {
|
|
1328
|
-
bootstrapChoices.push({ kind: 'open', path: p.path });
|
|
1329
|
-
console.log(` ${teal(String(bootstrapChoices.length))} ${cream(p.name)} ${slate('· ' + tildify(p.path))}`);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
if (candidates.length > 0) {
|
|
1333
|
-
console.log(` ${teal('●')} ${sage(`${candidates.length} likely candidate${candidates.length !== 1 ? 's' : ''} — no shared memory yet:`)}`);
|
|
1334
|
-
for (const p of candidates) {
|
|
1335
|
-
bootstrapChoices.push({ kind: 'open', path: p.path });
|
|
1336
|
-
console.log(` ${teal(String(bootstrapChoices.length))} ${cream(p.name)} ${slate('· ' + tildify(p.path) + ' · ' + p.reason)}`);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
console.log(` ${slate('pick a number to open it')}`);
|
|
1340
|
-
}
|
|
1348
|
+
runScanMenu();
|
|
1341
1349
|
console.log('');
|
|
1342
1350
|
rl.prompt();
|
|
1343
1351
|
return;
|
|
@@ -1671,6 +1679,7 @@ async function main() {
|
|
|
1671
1679
|
console.log(` ${teal('@name')} ${slate('<msg>')} ${sage('one message to one tool — @codex review this')}`);
|
|
1672
1680
|
console.log(` ${teal('/work')} ${slate('[tool]')} ${sage('hand off to the full interactive tool, outcome on return')}`);
|
|
1673
1681
|
console.log(` ${teal('/clarify')} ${sage('turn messy thoughts into .intent/ artifacts')}`);
|
|
1682
|
+
console.log(` ${teal('/scan')} ${sage('find your projects — and repos that need shared memory')}`);
|
|
1674
1683
|
console.log(` ${teal('/outcomes')} ${sage('label what you kept — the record that gets smarter')}`);
|
|
1675
1684
|
console.log('');
|
|
1676
1685
|
console.log(` ${slate('/help all')} ${sage('everything')} ${slate('·')} ${slate('/tour')} ${sage('walkthrough')} ${slate('·')} ${slate('/quit')} ${sage('exit')}`);
|
|
@@ -1688,6 +1697,7 @@ async function main() {
|
|
|
1688
1697
|
console.log(` ${teal('/learn')} ${sage('what your record taught — which tool keeps best, by kind of work')}`);
|
|
1689
1698
|
console.log('');
|
|
1690
1699
|
console.log(` ${cream('author .intent/')}`);
|
|
1700
|
+
console.log(` ${teal('/scan')} ${sage('Find your projects — and likely candidates with no .intent/ yet')}`);
|
|
1691
1701
|
console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
|
|
1692
1702
|
console.log(` ${teal('/intent')} ${sage('Pause and reflect — view or update .intent/ before moving on')}`);
|
|
1693
1703
|
console.log(` ${teal('/remember')} ${sage('Jot a decision to .intent/decisions.md — every tool inherits it')}`);
|
|
@@ -1922,6 +1932,13 @@ async function main() {
|
|
|
1922
1932
|
return;
|
|
1923
1933
|
}
|
|
1924
1934
|
|
|
1935
|
+
if (cmd === 'scan') {
|
|
1936
|
+
runScanMenu();
|
|
1937
|
+
console.log('');
|
|
1938
|
+
rl.prompt();
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1925
1942
|
if (cmd === 'clarify') {
|
|
1926
1943
|
try {
|
|
1927
1944
|
const { spawnSync } = require('child_process');
|
package/lib/cors.js
CHANGED
|
@@ -37,6 +37,12 @@ function corsHeaders(req) {
|
|
|
37
37
|
'Access-Control-Allow-Origin': origin,
|
|
38
38
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
39
39
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Phewsh-Runtime',
|
|
40
|
+
// Chrome Local/Private Network Access: an HTTPS page (phewsh.com) fetching
|
|
41
|
+
// plain-http loopback gets a preflight carrying Access-Control-Request-
|
|
42
|
+
// Private-Network; without this answer Chrome kills the request before it
|
|
43
|
+
// is ever sent — the cockpit shows "bridge offline" while serve is
|
|
44
|
+
// demonstrably up. Loopback-only server, allowlisted origins only.
|
|
45
|
+
'Access-Control-Allow-Private-Network': 'true',
|
|
40
46
|
'Access-Control-Max-Age': '600',
|
|
41
47
|
Vary: 'Origin',
|
|
42
48
|
};
|