@albinocrabs/feynman 0.2.2 → 0.2.5
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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +67 -1
- package/CONTRIBUTING.md +1 -0
- package/README.md +260 -22
- package/SECURITY.md +11 -0
- package/bin/feynman.js +419 -36
- package/docs/architecture.md +27 -17
- package/docs/launch.md +10 -2
- package/docs/object-passport.md +91 -0
- package/docs/release.md +162 -0
- package/examples/activity-sequence.md +105 -0
- package/examples/api-flow.md +32 -3
- package/examples/bug-isolation.md +89 -0
- package/examples/c4-platform-diagramming.md +112 -0
- package/examples/context-splitting.md +77 -0
- package/examples/feature-planning.md +107 -0
- package/examples/incident-response.md +77 -0
- package/examples/release-readiness.md +73 -0
- package/examples/service-migration.md +72 -0
- package/hooks/feynman-activate.js +11 -4
- package/hooks/feynman-session-start.js +79 -0
- package/hooks/hooks.json +12 -2
- package/hooks.json +13 -2
- package/package.json +5 -3
- package/rules/feynman-activate.md +5 -7
- package/skills/feynman/SKILL.md +11 -9
package/bin/feynman.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// bin/feynman.js — feynman unified CLI
|
|
3
|
-
// Subcommands: install, uninstall, doctor, lint, version, help
|
|
3
|
+
// Subcommands: install, uninstall, doctor, lint, examples, bootstrap, version, help
|
|
4
4
|
// Zero runtime deps. CJS only. Node >= 18.
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
@@ -22,16 +22,22 @@ const c = {
|
|
|
22
22
|
|
|
23
23
|
const PKG = require('../package.json');
|
|
24
24
|
const VERSION = PKG.version;
|
|
25
|
+
const ROOT_DIR = path.resolve(__dirname, '..');
|
|
25
26
|
|
|
26
27
|
// Resolve paths using os.homedir() — never tilde literal (bug #8810)
|
|
27
28
|
const HOME = os.homedir();
|
|
28
29
|
|
|
29
30
|
// Hook script lives relative to this file
|
|
30
31
|
const HOOK_PATH = path.resolve(__dirname, '..', 'hooks', 'feynman-activate.js');
|
|
32
|
+
const SESSION_HOOK_PATH = path.resolve(__dirname, '..', 'hooks', 'feynman-session-start.js');
|
|
31
33
|
const RULES_PATH = path.resolve(__dirname, '..', 'rules', 'feynman-activate.md');
|
|
32
34
|
|
|
33
35
|
const DEFAULT_STATE = { enabled: true, intensity: 'full', injections: 0 };
|
|
34
|
-
const
|
|
36
|
+
const TARGET_ALIASES = {
|
|
37
|
+
all: 'both',
|
|
38
|
+
'*': 'both',
|
|
39
|
+
};
|
|
40
|
+
const VALID_TARGETS = ['claude', 'codex', 'both', 'all', '*'];
|
|
35
41
|
|
|
36
42
|
function targetConfig(name) {
|
|
37
43
|
const dirName = name === 'codex' ? '.codex' : '.claude';
|
|
@@ -52,7 +58,7 @@ function targetNames(target) {
|
|
|
52
58
|
return target === 'both' ? ['claude', 'codex'] : [target];
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
function parseTarget(args, fallback = '
|
|
61
|
+
function parseTarget(args, fallback = 'codex') {
|
|
56
62
|
let target = fallback;
|
|
57
63
|
const keep = [];
|
|
58
64
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -66,9 +72,10 @@ function parseTarget(args, fallback = 'claude') {
|
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
if (!VALID_TARGETS.includes(target)) {
|
|
69
|
-
console.error(`feynman: invalid --target '${target}' (expected claude, codex, or
|
|
75
|
+
console.error(`feynman: invalid --target '${target}' (expected claude, codex, both, all, or *)`);
|
|
70
76
|
process.exit(2);
|
|
71
77
|
}
|
|
78
|
+
target = TARGET_ALIASES[target] || target;
|
|
72
79
|
return { target, args: keep };
|
|
73
80
|
}
|
|
74
81
|
|
|
@@ -89,30 +96,329 @@ ${c.bold('Commands:')}
|
|
|
89
96
|
uninstall Remove feynman hook (state preserved)
|
|
90
97
|
doctor Check installation health
|
|
91
98
|
lint <file> Lint a markdown file for diagram rule violations
|
|
99
|
+
examples List and display example prompts from the repository
|
|
100
|
+
bootstrap Export shared Feynman assets to a local folder
|
|
92
101
|
version Print version number
|
|
93
102
|
help Show this help
|
|
94
103
|
|
|
95
104
|
${c.bold('Options:')}
|
|
96
105
|
--help, -h Show help for a command
|
|
97
|
-
--target claude | codex | both
|
|
106
|
+
--target claude | codex | both | all | *
|
|
98
107
|
--force (install) Re-register even if already installed
|
|
99
108
|
|
|
100
109
|
${c.bold('Examples:')}
|
|
101
110
|
npx @albinocrabs/feynman install
|
|
102
111
|
npx @albinocrabs/feynman install --target codex
|
|
103
112
|
npx @albinocrabs/feynman install --target both
|
|
113
|
+
npx @albinocrabs/feynman install --target all
|
|
104
114
|
npx @albinocrabs/feynman doctor
|
|
105
115
|
feynman lint response.md
|
|
116
|
+
feynman bootstrap --out ./feynman-package
|
|
117
|
+
feynman examples
|
|
106
118
|
feynman uninstall
|
|
107
119
|
`;
|
|
108
120
|
|
|
121
|
+
const EXAMPLES_HELP = `${c.bold('feynman examples')} — print built-in demonstration prompts
|
|
122
|
+
|
|
123
|
+
${c.bold('Usage:')}
|
|
124
|
+
feynman examples # list available examples
|
|
125
|
+
feynman examples --name <fileBase> # print a specific example
|
|
126
|
+
feynman examples --random # print a random example
|
|
127
|
+
|
|
128
|
+
${c.bold('Options:')}
|
|
129
|
+
--name Example filename without .md extension (examples/feature-planning)
|
|
130
|
+
--random Show one random example in full
|
|
131
|
+
--help Show this help
|
|
132
|
+
|
|
133
|
+
Example filenames:
|
|
134
|
+
- architecture-review
|
|
135
|
+
- api-flow
|
|
136
|
+
- c4-platform-diagramming
|
|
137
|
+
- db-schema
|
|
138
|
+
- algorithm-explain
|
|
139
|
+
- deploy-pipeline
|
|
140
|
+
- code-review
|
|
141
|
+
- incident-response
|
|
142
|
+
- feature-planning
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
const EXAMPLES_DIR = path.resolve(__dirname, '..', 'examples');
|
|
146
|
+
const SKILL_SRC = path.resolve(ROOT_DIR, 'skills', 'feynman', 'SKILL.md');
|
|
147
|
+
const CLAUDE_PLUGIN = path.resolve(ROOT_DIR, '.claude-plugin', 'plugin.json');
|
|
148
|
+
const CODEX_PLUGIN = path.resolve(ROOT_DIR, '.codex-plugin', 'plugin.json');
|
|
149
|
+
const PACKAGE_HOOKS = path.resolve(ROOT_DIR, 'hooks', 'hooks.json');
|
|
150
|
+
const DEFAULT_BOOTSTRAP_DIR = 'feynman-package';
|
|
151
|
+
const ACTIVATOR_JS = path.resolve(ROOT_DIR, 'hooks', 'feynman-activate.js');
|
|
152
|
+
const CLI_JS = path.resolve(ROOT_DIR, 'bin', 'feynman.js');
|
|
153
|
+
const PACKAGE_JSON = path.resolve(ROOT_DIR, 'package.json');
|
|
154
|
+
|
|
155
|
+
const BOOTSTRAP_HELP = `${c.bold('feynman bootstrap')} — export Feynman assets into local folder
|
|
156
|
+
|
|
157
|
+
${c.bold('Usage:')}
|
|
158
|
+
feynman bootstrap
|
|
159
|
+
feynman bootstrap --out <directory>
|
|
160
|
+
|
|
161
|
+
${c.bold('Options:')}
|
|
162
|
+
--out Output folder (default: ./feynman-package)
|
|
163
|
+
--force Recreate output folder if it exists
|
|
164
|
+
--help Show this help
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
function ensureDir(dir) {
|
|
168
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function copyFileIfExists(src, dest) {
|
|
172
|
+
if (!fs.existsSync(src)) return false;
|
|
173
|
+
ensureDir(path.dirname(dest));
|
|
174
|
+
fs.copyFileSync(src, dest);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function copyMarkdownDir(src, dest) {
|
|
179
|
+
if (!fs.existsSync(src)) return 0;
|
|
180
|
+
let copied = 0;
|
|
181
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
182
|
+
const sourcePath = path.join(src, entry.name);
|
|
183
|
+
const destPath = path.join(dest, entry.name);
|
|
184
|
+
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
copied += copyMarkdownDir(sourcePath, destPath);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
ensureDir(path.dirname(destPath));
|
|
195
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
196
|
+
copied += 1;
|
|
197
|
+
}
|
|
198
|
+
return copied;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function examplesIndex() {
|
|
202
|
+
if (!fs.existsSync(EXAMPLES_DIR)) return [];
|
|
203
|
+
|
|
204
|
+
return fs.readdirSync(EXAMPLES_DIR)
|
|
205
|
+
.filter((name) => name.endsWith('.md'))
|
|
206
|
+
.sort()
|
|
207
|
+
.map((name) => {
|
|
208
|
+
const file = path.join(EXAMPLES_DIR, name);
|
|
209
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
210
|
+
const title = (content.match(/^#\s*(.+)$/m) || [null, name])[1].trim();
|
|
211
|
+
const question = (content.match(/^> (.*)$/m) || [null, ''])[1].trim();
|
|
212
|
+
return {
|
|
213
|
+
name: name.replace(/\.md$/, ''),
|
|
214
|
+
title,
|
|
215
|
+
question,
|
|
216
|
+
path: file,
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function cmdExamples(args) {
|
|
222
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
223
|
+
console.log(EXAMPLES_HELP);
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const entries = examplesIndex();
|
|
228
|
+
if (!entries.length) {
|
|
229
|
+
console.log('No examples found under examples/.');
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let random = false;
|
|
234
|
+
let wantsName = null;
|
|
235
|
+
const unknown = [];
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
238
|
+
const arg = args[i];
|
|
239
|
+
if (arg === '--name' || arg === '-n') {
|
|
240
|
+
const value = args[i + 1];
|
|
241
|
+
if (!value || value.startsWith('-')) {
|
|
242
|
+
console.error('feynman examples: --name requires a value');
|
|
243
|
+
process.exit(2);
|
|
244
|
+
}
|
|
245
|
+
if (wantsName !== null) {
|
|
246
|
+
console.error('feynman examples: duplicate --name');
|
|
247
|
+
process.exit(2);
|
|
248
|
+
}
|
|
249
|
+
wantsName = value;
|
|
250
|
+
i += 1;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (arg === '--random' || arg === '-r') {
|
|
255
|
+
random = true;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (arg.startsWith('-')) {
|
|
260
|
+
unknown.push(arg);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
unknown.push(arg);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (random && wantsName) {
|
|
268
|
+
console.error('feynman examples: use either --random or --name');
|
|
269
|
+
process.exit(2);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (unknown.length > 0) {
|
|
273
|
+
console.error(`feynman examples: unexpected arguments "${unknown.join(' ')}"`);
|
|
274
|
+
console.error('Run `feynman examples --help` for usage.');
|
|
275
|
+
process.exit(2);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (random) {
|
|
279
|
+
const entry = entries[Math.floor(Math.random() * entries.length)];
|
|
280
|
+
const content = fs.readFileSync(entry.path, 'utf8');
|
|
281
|
+
console.log(`\n[${entry.name}] ${entry.title}\n`);
|
|
282
|
+
console.log('Question:');
|
|
283
|
+
console.log(entry.question ? `> ${entry.question}` : '(no question marker found)');
|
|
284
|
+
console.log('\nPreview:\n');
|
|
285
|
+
const lines = content.split('\n').slice(0, 26);
|
|
286
|
+
console.log(lines.join('\n'));
|
|
287
|
+
process.exit(0);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (wantsName) {
|
|
291
|
+
const entry = entries.find((item) => item.name === wantsName);
|
|
292
|
+
if (!entry) {
|
|
293
|
+
console.error(`feynman examples: unknown example '${wantsName}'`);
|
|
294
|
+
process.exit(2);
|
|
295
|
+
}
|
|
296
|
+
const content = fs.readFileSync(entry.path, 'utf8');
|
|
297
|
+
console.log(`\n[${entry.name}] ${entry.title}\n`);
|
|
298
|
+
console.log(content);
|
|
299
|
+
process.exit(0);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (args.length === 0) {
|
|
303
|
+
console.log('Available examples:\n');
|
|
304
|
+
for (const entry of entries) {
|
|
305
|
+
const q = entry.question ? ` — ${entry.question}` : '';
|
|
306
|
+
console.log(`- ${entry.name}`);
|
|
307
|
+
console.log(` ${entry.title}${q ? ` — ${q}` : ''}`);
|
|
308
|
+
}
|
|
309
|
+
process.exit(0);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function cmdBootstrap(args) {
|
|
314
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
315
|
+
console.log(BOOTSTRAP_HELP);
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let out = path.resolve(process.cwd(), DEFAULT_BOOTSTRAP_DIR);
|
|
320
|
+
let force = false;
|
|
321
|
+
const unknown = [];
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
324
|
+
const arg = args[i];
|
|
325
|
+
|
|
326
|
+
if (arg === '--force') {
|
|
327
|
+
force = true;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (arg === '--out') {
|
|
332
|
+
const value = args[i + 1];
|
|
333
|
+
if (!value || value.startsWith('-')) {
|
|
334
|
+
console.error('feynman bootstrap: --out requires a value');
|
|
335
|
+
process.exit(2);
|
|
336
|
+
}
|
|
337
|
+
out = path.resolve(process.cwd(), value);
|
|
338
|
+
i += 1;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (arg.startsWith('--out=')) {
|
|
343
|
+
const value = arg.slice('--out='.length);
|
|
344
|
+
if (!value) {
|
|
345
|
+
console.error('feynman bootstrap: invalid --out argument');
|
|
346
|
+
process.exit(2);
|
|
347
|
+
}
|
|
348
|
+
out = path.resolve(process.cwd(), value);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (arg.startsWith('-')) {
|
|
353
|
+
unknown.push(arg);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
unknown.push(arg);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (unknown.length > 0) {
|
|
361
|
+
console.error(`feynman bootstrap: unexpected arguments "${unknown.join(' ')}"`);
|
|
362
|
+
console.error('Run `feynman bootstrap --help` for usage.');
|
|
363
|
+
process.exit(2);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (fs.existsSync(out) && !force) {
|
|
367
|
+
console.log(`feynman bootstrap: output already exists at ${out}`);
|
|
368
|
+
console.log('Use `--force` to recreate it.');
|
|
369
|
+
process.exit(0);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (fs.existsSync(out)) {
|
|
373
|
+
fs.rmSync(out, { recursive: true, force: true });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const counts = {
|
|
377
|
+
examples: copyMarkdownDir(EXAMPLES_DIR, path.join(out, 'examples')),
|
|
378
|
+
rules: copyFileIfExists(RULES_PATH, path.join(out, 'rules', 'feynman-activate.md')) ? 1 : 0,
|
|
379
|
+
hooks: copyFileIfExists(PACKAGE_HOOKS, path.join(out, 'hooks', 'hooks.json')) ? 1 : 0,
|
|
380
|
+
hookRuntime: copyFileIfExists(ACTIVATOR_JS, path.join(out, 'hooks', 'feynman-activate.js')) ? 1 : 0,
|
|
381
|
+
cliRuntime: copyFileIfExists(CLI_JS, path.join(out, 'bin', 'feynman.js')) ? 1 : 0,
|
|
382
|
+
packageManifest: copyFileIfExists(PACKAGE_JSON, path.join(out, 'package.json')) ? 1 : 0,
|
|
383
|
+
plugins:
|
|
384
|
+
(copyFileIfExists(CLAUDE_PLUGIN, path.join(out, '.claude-plugin', 'plugin.json')) ? 1 : 0) +
|
|
385
|
+
(copyFileIfExists(CODEX_PLUGIN, path.join(out, '.codex-plugin', 'plugin.json')) ? 1 : 0),
|
|
386
|
+
skill: copyFileIfExists(SKILL_SRC, path.join(out, 'skills', 'feynman', 'SKILL.md')) ? 1 : 0,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
ensureDir(out);
|
|
390
|
+
fs.writeFileSync(
|
|
391
|
+
path.join(out, 'feynman-bootstrap.json'),
|
|
392
|
+
JSON.stringify({
|
|
393
|
+
version: VERSION,
|
|
394
|
+
createdAt: new Date().toISOString(),
|
|
395
|
+
outputDir: out,
|
|
396
|
+
counts,
|
|
397
|
+
}, null, 2) + '\n'
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const total = Object.values(counts).reduce((sum, count) => sum + (count || 0), 1);
|
|
401
|
+
console.log('');
|
|
402
|
+
console.log('┌─ feynman bootstrap ────────────────────────────────────────┐');
|
|
403
|
+
console.log(`│ output: ${out}`);
|
|
404
|
+
console.log(`│ examples: ${counts.examples}`);
|
|
405
|
+
console.log(`│ rules: ${counts.rules}`);
|
|
406
|
+
console.log(`│ hooks: ${counts.hooks}`);
|
|
407
|
+
console.log(`│ runtime: ${counts.hookRuntime + counts.cliRuntime + counts.packageManifest}`);
|
|
408
|
+
console.log(`│ plugins: ${counts.plugins}`);
|
|
409
|
+
console.log(`│ skill: ${counts.skill}`);
|
|
410
|
+
console.log(`│ files: ${total}`);
|
|
411
|
+
console.log('└───────────────────────────────────────────────────────────┘');
|
|
412
|
+
process.exit(0);
|
|
413
|
+
}
|
|
414
|
+
|
|
109
415
|
const INSTALL_HELP = `${c.bold('feynman install')} — register feynman hook
|
|
110
416
|
|
|
111
417
|
${c.bold('Usage:')}
|
|
112
|
-
feynman install [--target claude|codex|both] [--force]
|
|
418
|
+
feynman install [--target claude|codex|both|all|*] [--force]
|
|
113
419
|
|
|
114
420
|
${c.bold('Options:')}
|
|
115
|
-
--target Install into Claude Code, Codex, or
|
|
421
|
+
--target Install into Claude Code, Codex, both, all, or * (default: codex)
|
|
116
422
|
--force Re-register hook even if already installed
|
|
117
423
|
|
|
118
424
|
Claude creates:
|
|
@@ -120,17 +426,17 @@ Claude creates:
|
|
|
120
426
|
~/.claude/.feynman-active — presence flag
|
|
121
427
|
|
|
122
428
|
Codex creates:
|
|
123
|
-
~/.codex/hooks.json — UserPromptSubmit hook registration
|
|
429
|
+
~/.codex/hooks.json — SessionStart + UserPromptSubmit hook registration
|
|
124
430
|
~/.codex/.feynman/state.json — feynman state (enabled, intensity, injections)
|
|
125
431
|
~/.codex/.feynman-active — presence flag
|
|
126
432
|
|
|
127
|
-
Idempotent by default: skips if feynman
|
|
433
|
+
Idempotent by default: skips if feynman hook entries already exist.
|
|
128
434
|
`;
|
|
129
435
|
|
|
130
436
|
const UNINSTALL_HELP = `${c.bold('feynman uninstall')} — remove feynman hook
|
|
131
437
|
|
|
132
438
|
${c.bold('Usage:')}
|
|
133
|
-
feynman uninstall [--target claude|codex|both]
|
|
439
|
+
feynman uninstall [--target claude|codex|both|all|*]
|
|
134
440
|
|
|
135
441
|
Removes feynman hook entries from target config.
|
|
136
442
|
Preserves .feynman/state.json (user data).
|
|
@@ -142,15 +448,15 @@ Idempotent: safe to run multiple times.
|
|
|
142
448
|
const DOCTOR_HELP = `${c.bold('feynman doctor')} — check feynman installation health
|
|
143
449
|
|
|
144
450
|
${c.bold('Usage:')}
|
|
145
|
-
feynman doctor [--target claude|codex]
|
|
451
|
+
feynman doctor [--target claude|codex|both|all|*]
|
|
146
452
|
|
|
147
453
|
Checks:
|
|
148
454
|
1. target hook config present
|
|
149
|
-
2. UserPromptSubmit
|
|
150
|
-
3. Hook script
|
|
455
|
+
2. SessionStart and UserPromptSubmit hooks reference feynman scripts
|
|
456
|
+
3. Hook script files exist and are readable
|
|
151
457
|
4. Rules file exists and is non-empty
|
|
152
458
|
5. state.json valid JSON with enabled field
|
|
153
|
-
6. .feynman-active flag
|
|
459
|
+
6. .feynman-active flag matches enabled state
|
|
154
460
|
7. (INFO) lint hook registered (optional)
|
|
155
461
|
|
|
156
462
|
Exit code: always 0 (advisory only).
|
|
@@ -202,23 +508,31 @@ function writeSettings(target, settings) {
|
|
|
202
508
|
}
|
|
203
509
|
|
|
204
510
|
function hasFeynmanHook(settings) {
|
|
205
|
-
|
|
511
|
+
const promptHook = ((settings.hooks && settings.hooks.UserPromptSubmit) || []).some(g =>
|
|
206
512
|
g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-activate.js'))
|
|
207
513
|
);
|
|
514
|
+
const sessionHook = ((settings.hooks && settings.hooks.SessionStart) || []).some(g =>
|
|
515
|
+
g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-session-start.js'))
|
|
516
|
+
);
|
|
517
|
+
return promptHook && sessionHook;
|
|
208
518
|
}
|
|
209
519
|
|
|
210
520
|
function removeFeynmanHooks(settings) {
|
|
211
|
-
if (!settings.hooks
|
|
212
|
-
|
|
213
|
-
!(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
h.command
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
521
|
+
if (!settings.hooks) return settings;
|
|
522
|
+
for (const eventName of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
|
|
523
|
+
if (!Array.isArray(settings.hooks[eventName])) continue;
|
|
524
|
+
settings.hooks[eventName] = settings.hooks[eventName].filter(g =>
|
|
525
|
+
!(g.hooks && g.hooks.some(h =>
|
|
526
|
+
h.command && (
|
|
527
|
+
h.command.includes('feynman-session-start.js') ||
|
|
528
|
+
h.command.includes('feynman-activate.js') ||
|
|
529
|
+
h.command.includes('feynman-lint.js')
|
|
530
|
+
)
|
|
531
|
+
))
|
|
532
|
+
);
|
|
533
|
+
if (settings.hooks[eventName].length === 0) {
|
|
534
|
+
delete settings.hooks[eventName];
|
|
535
|
+
}
|
|
222
536
|
}
|
|
223
537
|
if (Object.keys(settings.hooks).length === 0) {
|
|
224
538
|
delete settings.hooks;
|
|
@@ -229,10 +543,21 @@ function removeFeynmanHooks(settings) {
|
|
|
229
543
|
function bootstrapState(target) {
|
|
230
544
|
const cfg = targetConfig(target);
|
|
231
545
|
fs.mkdirSync(cfg.feynmanDir, { recursive: true });
|
|
546
|
+
let state = DEFAULT_STATE;
|
|
232
547
|
if (!fs.existsSync(cfg.statePath)) {
|
|
233
548
|
fs.writeFileSync(cfg.statePath, JSON.stringify(DEFAULT_STATE, null, 2) + '\n');
|
|
549
|
+
} else {
|
|
550
|
+
try {
|
|
551
|
+
state = { ...DEFAULT_STATE, ...JSON.parse(fs.readFileSync(cfg.statePath, 'utf8')) };
|
|
552
|
+
} catch (_) {
|
|
553
|
+
fs.writeFileSync(cfg.statePath, JSON.stringify(DEFAULT_STATE, null, 2) + '\n');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (state.enabled) {
|
|
557
|
+
fs.writeFileSync(cfg.flagPath, state.intensity || DEFAULT_STATE.intensity);
|
|
558
|
+
} else if (fs.existsSync(cfg.flagPath)) {
|
|
559
|
+
fs.unlinkSync(cfg.flagPath);
|
|
234
560
|
}
|
|
235
|
-
fs.writeFileSync(cfg.flagPath, DEFAULT_STATE.intensity);
|
|
236
561
|
}
|
|
237
562
|
|
|
238
563
|
function installClaudeCommand() {
|
|
@@ -256,6 +581,7 @@ function installOne(target, opts) {
|
|
|
256
581
|
// Read or create settings
|
|
257
582
|
const cfg = readSettings(target);
|
|
258
583
|
cfg.hooks = cfg.hooks || {};
|
|
584
|
+
cfg.hooks.SessionStart = cfg.hooks.SessionStart || [];
|
|
259
585
|
cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
|
|
260
586
|
|
|
261
587
|
// Idempotency check
|
|
@@ -267,20 +593,36 @@ function installOne(target, opts) {
|
|
|
267
593
|
return { target, already: true };
|
|
268
594
|
}
|
|
269
595
|
|
|
270
|
-
// If force
|
|
596
|
+
// If force or partial legacy install, remove old feynman entries first.
|
|
271
597
|
if (already && force) {
|
|
272
598
|
removeFeynmanHooks(cfg);
|
|
273
599
|
cfg.hooks = cfg.hooks || {};
|
|
600
|
+
cfg.hooks.SessionStart = cfg.hooks.SessionStart || [];
|
|
601
|
+
cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
|
|
602
|
+
} else if (!already) {
|
|
603
|
+
removeFeynmanHooks(cfg);
|
|
604
|
+
cfg.hooks = cfg.hooks || {};
|
|
605
|
+
cfg.hooks.SessionStart = cfg.hooks.SessionStart || [];
|
|
274
606
|
cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
|
|
275
607
|
}
|
|
276
608
|
|
|
277
|
-
// Append hook
|
|
609
|
+
// Append hook entries
|
|
610
|
+
const sessionEntry = {
|
|
611
|
+
hooks: [{
|
|
612
|
+
type: 'command',
|
|
613
|
+
command: hookCommandFor(target).replace(HOOK_PATH, SESSION_HOOK_PATH),
|
|
614
|
+
timeout: 5,
|
|
615
|
+
}]
|
|
616
|
+
};
|
|
617
|
+
if (target === 'codex') {
|
|
618
|
+
sessionEntry.matcher = 'startup|resume';
|
|
619
|
+
}
|
|
620
|
+
cfg.hooks.SessionStart.push(sessionEntry);
|
|
278
621
|
cfg.hooks.UserPromptSubmit.push({
|
|
279
622
|
hooks: [{
|
|
280
623
|
type: 'command',
|
|
281
624
|
command: hookCommandFor(target),
|
|
282
625
|
timeout: 5,
|
|
283
|
-
statusMessage: 'Injecting diagram rules...',
|
|
284
626
|
}]
|
|
285
627
|
});
|
|
286
628
|
|
|
@@ -318,7 +660,7 @@ function cmdInstall(opts) {
|
|
|
318
660
|
}
|
|
319
661
|
console.log('└──────────────────────────────────────────────────────────────┘');
|
|
320
662
|
console.log('');
|
|
321
|
-
console.log('Restart Claude Code or Codex to activate feynman.');
|
|
663
|
+
console.log('Restart Claude Code or Codex to activate feynman full mode.');
|
|
322
664
|
|
|
323
665
|
process.exit(0);
|
|
324
666
|
}
|
|
@@ -379,11 +721,24 @@ function cmdDoctor(opts = {}) {
|
|
|
379
721
|
const settingsExists = fs.existsSync(tc.settingsPath);
|
|
380
722
|
check(`${tc.settingsPath.replace(HOME, '~')} present`, settingsExists);
|
|
381
723
|
|
|
382
|
-
// 2. UserPromptSubmit
|
|
724
|
+
// 2. SessionStart and UserPromptSubmit hooks reference feynman scripts
|
|
725
|
+
let sessionHookRegistered = false;
|
|
383
726
|
let hookRegistered = false;
|
|
727
|
+
let sessionHookAbsPath = null;
|
|
384
728
|
let hookAbsPath = null;
|
|
385
729
|
if (settingsExists) {
|
|
386
730
|
const cfg = readSettings(target);
|
|
731
|
+
const sessionEntries = (cfg.hooks && cfg.hooks.SessionStart) || [];
|
|
732
|
+
const feynmanSessionEntry = sessionEntries.find(g =>
|
|
733
|
+
g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-session-start.js'))
|
|
734
|
+
);
|
|
735
|
+
sessionHookRegistered = !!feynmanSessionEntry;
|
|
736
|
+
if (feynmanSessionEntry) {
|
|
737
|
+
const hookCmd = feynmanSessionEntry.hooks.find(h => h.command && h.command.includes('feynman-session-start.js')).command;
|
|
738
|
+
const match = hookCmd.match(/"([^"]+feynman-session-start\.js)"/);
|
|
739
|
+
if (match) sessionHookAbsPath = match[1];
|
|
740
|
+
}
|
|
741
|
+
|
|
387
742
|
const entries = (cfg.hooks && cfg.hooks.UserPromptSubmit) || [];
|
|
388
743
|
const feynmanEntry = entries.find(g =>
|
|
389
744
|
g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-activate.js'))
|
|
@@ -396,9 +751,24 @@ function cmdDoctor(opts = {}) {
|
|
|
396
751
|
if (match) hookAbsPath = match[1];
|
|
397
752
|
}
|
|
398
753
|
}
|
|
754
|
+
check('hook registered (feynman-session-start.js in SessionStart)', sessionHookRegistered);
|
|
399
755
|
check('hook registered (feynman-activate.js in UserPromptSubmit)', hookRegistered);
|
|
400
756
|
|
|
401
|
-
// 3. Hook script
|
|
757
|
+
// 3. Hook script files exist + readable
|
|
758
|
+
let sessionHookFileOk = false;
|
|
759
|
+
if (sessionHookAbsPath) {
|
|
760
|
+
try {
|
|
761
|
+
fs.accessSync(sessionHookAbsPath, fs.constants.R_OK);
|
|
762
|
+
sessionHookFileOk = true;
|
|
763
|
+
} catch (_) {}
|
|
764
|
+
} else if (sessionHookRegistered) {
|
|
765
|
+
try {
|
|
766
|
+
fs.accessSync(SESSION_HOOK_PATH, fs.constants.R_OK);
|
|
767
|
+
sessionHookFileOk = true;
|
|
768
|
+
} catch (_) {}
|
|
769
|
+
}
|
|
770
|
+
check('session hook script file exists and is readable', sessionHookFileOk);
|
|
771
|
+
|
|
402
772
|
let hookFileOk = false;
|
|
403
773
|
if (hookAbsPath) {
|
|
404
774
|
try {
|
|
@@ -412,7 +782,7 @@ function cmdDoctor(opts = {}) {
|
|
|
412
782
|
hookFileOk = true;
|
|
413
783
|
} catch (_) {}
|
|
414
784
|
}
|
|
415
|
-
check('hook script file exists and is readable', hookFileOk);
|
|
785
|
+
check('prompt hook script file exists and is readable', hookFileOk);
|
|
416
786
|
|
|
417
787
|
// 4. Rules file exists + non-empty
|
|
418
788
|
let rulesOk = false;
|
|
@@ -424,15 +794,20 @@ function cmdDoctor(opts = {}) {
|
|
|
424
794
|
|
|
425
795
|
// 5. state.json valid JSON + has enabled field
|
|
426
796
|
let stateOk = false;
|
|
797
|
+
let stateEnabled = false;
|
|
427
798
|
try {
|
|
428
799
|
const state = JSON.parse(fs.readFileSync(tc.statePath, 'utf8'));
|
|
429
800
|
stateOk = 'enabled' in state;
|
|
801
|
+
stateEnabled = state.enabled === true;
|
|
430
802
|
} catch (_) {}
|
|
431
803
|
check('state.json valid (has enabled field)', stateOk);
|
|
432
804
|
|
|
433
|
-
// 6. .feynman-active flag
|
|
805
|
+
// 6. .feynman-active flag matches state
|
|
434
806
|
const flagPresent = fs.existsSync(tc.flagPath);
|
|
435
|
-
check(
|
|
807
|
+
check(
|
|
808
|
+
stateEnabled ? '.feynman-active flag present when enabled' : '.feynman-active flag absent when disabled',
|
|
809
|
+
stateEnabled ? flagPresent : !flagPresent
|
|
810
|
+
);
|
|
436
811
|
|
|
437
812
|
// 7. (INFO) lint hook registered
|
|
438
813
|
let lintHookRegistered = false;
|
|
@@ -547,6 +922,14 @@ switch (sub) {
|
|
|
547
922
|
cmdLint(rest);
|
|
548
923
|
break;
|
|
549
924
|
}
|
|
925
|
+
case 'examples': {
|
|
926
|
+
cmdExamples(rest);
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
case 'bootstrap': {
|
|
930
|
+
cmdBootstrap(rest);
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
550
933
|
case 'version': {
|
|
551
934
|
cmdVersion(rest);
|
|
552
935
|
break;
|