@albinocrabs/feynman 0.2.4 → 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 +17 -0
- package/README.md +11 -9
- package/bin/feynman.js +97 -27
- package/docs/architecture.md +12 -9
- 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 +1 -1
- package/skills/feynman/SKILL.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.5 - 2026-05-08
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- add SessionStart hook priming alongside UserPromptSubmit reinforcement
|
|
10
|
+
- keep full diagram rules enabled by default for Claude Code and Codex
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
- preserve explicit /feynman off behavior with silent hooks
|
|
15
|
+
- keep feynman hooks quiet by omitting status messages
|
|
16
|
+
|
|
17
|
+
### Maintenance
|
|
18
|
+
|
|
19
|
+
- bump package and plugin manifest versions to 0.2.5
|
|
20
|
+
- expand tests for SessionStart registration and runtime behavior
|
|
21
|
+
|
|
5
22
|
## 0.2.4 - 2026-05-07
|
|
6
23
|
|
|
7
24
|
Changes since v0.2.2.
|
package/README.md
CHANGED
|
@@ -199,8 +199,7 @@ Add to `~/.claude/settings.json` — use the absolute path, not `~/`
|
|
|
199
199
|
{
|
|
200
200
|
"type": "command",
|
|
201
201
|
"command": "node \"/absolute/path/to/feynman/hooks/feynman-activate.js\"",
|
|
202
|
-
"timeout": 5
|
|
203
|
-
"statusMessage": "Injecting diagram rules..."
|
|
202
|
+
"timeout": 5
|
|
204
203
|
}
|
|
205
204
|
]
|
|
206
205
|
}
|
|
@@ -221,8 +220,7 @@ For Codex, add the same shape to `~/.codex/hooks.json` and set
|
|
|
221
220
|
{
|
|
222
221
|
"type": "command",
|
|
223
222
|
"command": "FEYNMAN_HOME=\"$HOME/.codex\" node \"/absolute/path/to/feynman/hooks/feynman-activate.js\"",
|
|
224
|
-
"timeout": 5
|
|
225
|
-
"statusMessage": "Injecting diagram rules..."
|
|
223
|
+
"timeout": 5
|
|
226
224
|
}
|
|
227
225
|
]
|
|
228
226
|
}
|
|
@@ -232,6 +230,10 @@ For Codex, add the same shape to `~/.codex/hooks.json` and set
|
|
|
232
230
|
```
|
|
233
231
|
</details>
|
|
234
232
|
|
|
233
|
+
After install, feynman starts in `full` mode by default. Disable or change it
|
|
234
|
+
explicitly with `/feynman off`, `/feynman lite`, `/feynman full`, or
|
|
235
|
+
`/feynman ultra`.
|
|
236
|
+
|
|
235
237
|
## Intensity Levels
|
|
236
238
|
|
|
237
239
|
| Level | What draws | Use when |
|
|
@@ -481,11 +483,11 @@ from day one.
|
|
|
481
483
|
|
|
482
484
|
## How it works
|
|
483
485
|
|
|
484
|
-
The `
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
the
|
|
486
|
+
The `SessionStart` hook primes fresh Claude Code or Codex sessions with the
|
|
487
|
+
active rules, and the `UserPromptSubmit` hook reinforces them on every prompt.
|
|
488
|
+
Both hooks read the target-local state file
|
|
489
|
+
(`~/.claude/.feynman/state.json` or `~/.codex/.feynman/state.json`), extract
|
|
490
|
+
the rules for the active intensity level, and inject them into model context.
|
|
489
491
|
|
|
490
492
|
```
|
|
491
493
|
[your prompt]
|
package/bin/feynman.js
CHANGED
|
@@ -29,6 +29,7 @@ const HOME = os.homedir();
|
|
|
29
29
|
|
|
30
30
|
// Hook script lives relative to this file
|
|
31
31
|
const HOOK_PATH = path.resolve(__dirname, '..', 'hooks', 'feynman-activate.js');
|
|
32
|
+
const SESSION_HOOK_PATH = path.resolve(__dirname, '..', 'hooks', 'feynman-session-start.js');
|
|
32
33
|
const RULES_PATH = path.resolve(__dirname, '..', 'rules', 'feynman-activate.md');
|
|
33
34
|
|
|
34
35
|
const DEFAULT_STATE = { enabled: true, intensity: 'full', injections: 0 };
|
|
@@ -425,11 +426,11 @@ Claude creates:
|
|
|
425
426
|
~/.claude/.feynman-active — presence flag
|
|
426
427
|
|
|
427
428
|
Codex creates:
|
|
428
|
-
~/.codex/hooks.json — UserPromptSubmit hook registration
|
|
429
|
+
~/.codex/hooks.json — SessionStart + UserPromptSubmit hook registration
|
|
429
430
|
~/.codex/.feynman/state.json — feynman state (enabled, intensity, injections)
|
|
430
431
|
~/.codex/.feynman-active — presence flag
|
|
431
432
|
|
|
432
|
-
Idempotent by default: skips if feynman
|
|
433
|
+
Idempotent by default: skips if feynman hook entries already exist.
|
|
433
434
|
`;
|
|
434
435
|
|
|
435
436
|
const UNINSTALL_HELP = `${c.bold('feynman uninstall')} — remove feynman hook
|
|
@@ -451,11 +452,11 @@ ${c.bold('Usage:')}
|
|
|
451
452
|
|
|
452
453
|
Checks:
|
|
453
454
|
1. target hook config present
|
|
454
|
-
2. UserPromptSubmit
|
|
455
|
-
3. Hook script
|
|
455
|
+
2. SessionStart and UserPromptSubmit hooks reference feynman scripts
|
|
456
|
+
3. Hook script files exist and are readable
|
|
456
457
|
4. Rules file exists and is non-empty
|
|
457
458
|
5. state.json valid JSON with enabled field
|
|
458
|
-
6. .feynman-active flag
|
|
459
|
+
6. .feynman-active flag matches enabled state
|
|
459
460
|
7. (INFO) lint hook registered (optional)
|
|
460
461
|
|
|
461
462
|
Exit code: always 0 (advisory only).
|
|
@@ -507,23 +508,31 @@ function writeSettings(target, settings) {
|
|
|
507
508
|
}
|
|
508
509
|
|
|
509
510
|
function hasFeynmanHook(settings) {
|
|
510
|
-
|
|
511
|
+
const promptHook = ((settings.hooks && settings.hooks.UserPromptSubmit) || []).some(g =>
|
|
511
512
|
g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-activate.js'))
|
|
512
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;
|
|
513
518
|
}
|
|
514
519
|
|
|
515
520
|
function removeFeynmanHooks(settings) {
|
|
516
|
-
if (!settings.hooks
|
|
517
|
-
|
|
518
|
-
!(
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
h.command
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
+
}
|
|
527
536
|
}
|
|
528
537
|
if (Object.keys(settings.hooks).length === 0) {
|
|
529
538
|
delete settings.hooks;
|
|
@@ -534,10 +543,21 @@ function removeFeynmanHooks(settings) {
|
|
|
534
543
|
function bootstrapState(target) {
|
|
535
544
|
const cfg = targetConfig(target);
|
|
536
545
|
fs.mkdirSync(cfg.feynmanDir, { recursive: true });
|
|
546
|
+
let state = DEFAULT_STATE;
|
|
537
547
|
if (!fs.existsSync(cfg.statePath)) {
|
|
538
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);
|
|
539
560
|
}
|
|
540
|
-
fs.writeFileSync(cfg.flagPath, DEFAULT_STATE.intensity);
|
|
541
561
|
}
|
|
542
562
|
|
|
543
563
|
function installClaudeCommand() {
|
|
@@ -561,6 +581,7 @@ function installOne(target, opts) {
|
|
|
561
581
|
// Read or create settings
|
|
562
582
|
const cfg = readSettings(target);
|
|
563
583
|
cfg.hooks = cfg.hooks || {};
|
|
584
|
+
cfg.hooks.SessionStart = cfg.hooks.SessionStart || [];
|
|
564
585
|
cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
|
|
565
586
|
|
|
566
587
|
// Idempotency check
|
|
@@ -572,20 +593,36 @@ function installOne(target, opts) {
|
|
|
572
593
|
return { target, already: true };
|
|
573
594
|
}
|
|
574
595
|
|
|
575
|
-
// If force
|
|
596
|
+
// If force or partial legacy install, remove old feynman entries first.
|
|
576
597
|
if (already && force) {
|
|
577
598
|
removeFeynmanHooks(cfg);
|
|
578
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 || [];
|
|
579
606
|
cfg.hooks.UserPromptSubmit = cfg.hooks.UserPromptSubmit || [];
|
|
580
607
|
}
|
|
581
608
|
|
|
582
|
-
// 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);
|
|
583
621
|
cfg.hooks.UserPromptSubmit.push({
|
|
584
622
|
hooks: [{
|
|
585
623
|
type: 'command',
|
|
586
624
|
command: hookCommandFor(target),
|
|
587
625
|
timeout: 5,
|
|
588
|
-
statusMessage: 'Injecting diagram rules...',
|
|
589
626
|
}]
|
|
590
627
|
});
|
|
591
628
|
|
|
@@ -623,7 +660,7 @@ function cmdInstall(opts) {
|
|
|
623
660
|
}
|
|
624
661
|
console.log('└──────────────────────────────────────────────────────────────┘');
|
|
625
662
|
console.log('');
|
|
626
|
-
console.log('Restart Claude Code or Codex to activate feynman.');
|
|
663
|
+
console.log('Restart Claude Code or Codex to activate feynman full mode.');
|
|
627
664
|
|
|
628
665
|
process.exit(0);
|
|
629
666
|
}
|
|
@@ -684,11 +721,24 @@ function cmdDoctor(opts = {}) {
|
|
|
684
721
|
const settingsExists = fs.existsSync(tc.settingsPath);
|
|
685
722
|
check(`${tc.settingsPath.replace(HOME, '~')} present`, settingsExists);
|
|
686
723
|
|
|
687
|
-
// 2. UserPromptSubmit
|
|
724
|
+
// 2. SessionStart and UserPromptSubmit hooks reference feynman scripts
|
|
725
|
+
let sessionHookRegistered = false;
|
|
688
726
|
let hookRegistered = false;
|
|
727
|
+
let sessionHookAbsPath = null;
|
|
689
728
|
let hookAbsPath = null;
|
|
690
729
|
if (settingsExists) {
|
|
691
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
|
+
|
|
692
742
|
const entries = (cfg.hooks && cfg.hooks.UserPromptSubmit) || [];
|
|
693
743
|
const feynmanEntry = entries.find(g =>
|
|
694
744
|
g.hooks && g.hooks.some(h => h.command && h.command.includes('feynman-activate.js'))
|
|
@@ -701,9 +751,24 @@ function cmdDoctor(opts = {}) {
|
|
|
701
751
|
if (match) hookAbsPath = match[1];
|
|
702
752
|
}
|
|
703
753
|
}
|
|
754
|
+
check('hook registered (feynman-session-start.js in SessionStart)', sessionHookRegistered);
|
|
704
755
|
check('hook registered (feynman-activate.js in UserPromptSubmit)', hookRegistered);
|
|
705
756
|
|
|
706
|
-
// 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
|
+
|
|
707
772
|
let hookFileOk = false;
|
|
708
773
|
if (hookAbsPath) {
|
|
709
774
|
try {
|
|
@@ -717,7 +782,7 @@ function cmdDoctor(opts = {}) {
|
|
|
717
782
|
hookFileOk = true;
|
|
718
783
|
} catch (_) {}
|
|
719
784
|
}
|
|
720
|
-
check('hook script file exists and is readable', hookFileOk);
|
|
785
|
+
check('prompt hook script file exists and is readable', hookFileOk);
|
|
721
786
|
|
|
722
787
|
// 4. Rules file exists + non-empty
|
|
723
788
|
let rulesOk = false;
|
|
@@ -729,15 +794,20 @@ function cmdDoctor(opts = {}) {
|
|
|
729
794
|
|
|
730
795
|
// 5. state.json valid JSON + has enabled field
|
|
731
796
|
let stateOk = false;
|
|
797
|
+
let stateEnabled = false;
|
|
732
798
|
try {
|
|
733
799
|
const state = JSON.parse(fs.readFileSync(tc.statePath, 'utf8'));
|
|
734
800
|
stateOk = 'enabled' in state;
|
|
801
|
+
stateEnabled = state.enabled === true;
|
|
735
802
|
} catch (_) {}
|
|
736
803
|
check('state.json valid (has enabled field)', stateOk);
|
|
737
804
|
|
|
738
|
-
// 6. .feynman-active flag
|
|
805
|
+
// 6. .feynman-active flag matches state
|
|
739
806
|
const flagPresent = fs.existsSync(tc.flagPath);
|
|
740
|
-
check(
|
|
807
|
+
check(
|
|
808
|
+
stateEnabled ? '.feynman-active flag present when enabled' : '.feynman-active flag absent when disabled',
|
|
809
|
+
stateEnabled ? flagPresent : !flagPresent
|
|
810
|
+
);
|
|
741
811
|
|
|
742
812
|
// 7. (INFO) lint hook registered
|
|
743
813
|
let lintHookRegistered = false;
|
package/docs/architecture.md
CHANGED
|
@@ -6,16 +6,18 @@ Three independent layers: hook lifecycle, lint pipeline, and state schema.
|
|
|
6
6
|
|
|
7
7
|
## Layer 1: Hook Lifecycle
|
|
8
8
|
|
|
9
|
-
The `
|
|
10
|
-
|
|
11
|
-
`additionalContext`.
|
|
9
|
+
The `SessionStart` hook primes fresh Claude Code or Codex sessions with the
|
|
10
|
+
active rules. The `UserPromptSubmit` hook fires before every prompt and
|
|
11
|
+
reinforces the same rules as `additionalContext`.
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
~/.claude/settings.json ~/.codex/hooks.json
|
|
15
15
|
│
|
|
16
|
-
|
|
16
|
+
├─ hooks.SessionStart primes new sessions
|
|
17
|
+
│
|
|
18
|
+
└─ hooks.UserPromptSubmit reinforces every prompt
|
|
17
19
|
▼
|
|
18
|
-
hooks/feynman-activate.js
|
|
20
|
+
hooks/feynman-session-start.js + hooks/feynman-activate.js
|
|
19
21
|
│
|
|
20
22
|
├─ [0] FEYNMAN_HOME selects client state root
|
|
21
23
|
│ unset → ~/.claude (backward compatible)
|
|
@@ -25,9 +27,10 @@ hooks/feynman-activate.js
|
|
|
25
27
|
├─ [1] validate session_id (path-traversal guard)
|
|
26
28
|
│
|
|
27
29
|
├─ [2] $FEYNMAN_HOME/.feynman-active ← flag file
|
|
28
|
-
│ absent + no state.json
|
|
29
|
-
│ absent + state.
|
|
30
|
-
│
|
|
30
|
+
│ absent + no state.json → bootstrap first run
|
|
31
|
+
│ absent + state.enabled=true → recreate flag
|
|
32
|
+
│ absent + state.enabled=false → exit 0 (user disabled)
|
|
33
|
+
│ present → continue
|
|
31
34
|
│
|
|
32
35
|
├─ [3] $FEYNMAN_HOME/.feynman/state.json
|
|
33
36
|
│ enabled: false → exit 0
|
|
@@ -121,7 +124,7 @@ All runtime state lives in two files under the selected client root:
|
|
|
121
124
|
~/.claude/ or ~/.codex/
|
|
122
125
|
├── .feynman-active ← presence flag
|
|
123
126
|
│ present = feynman active
|
|
124
|
-
│ absent = user disabled
|
|
127
|
+
│ absent = user disabled only when state.enabled=false
|
|
125
128
|
│ content = current intensity string (informational)
|
|
126
129
|
│
|
|
127
130
|
└── .feynman/
|
|
@@ -31,18 +31,25 @@ process.stdin.on('end', () => {
|
|
|
31
31
|
if (sessionId && /[/\\]|\.\./.test(sessionId)) process.exit(0);
|
|
32
32
|
|
|
33
33
|
// Step 2: flag file + first-run bootstrap (D-05, D-07, bug #35713)
|
|
34
|
-
// True first run: neither flag nor state exists
|
|
35
|
-
// Intentionally disabled: flag absent
|
|
34
|
+
// True first run: neither flag nor state exists -> bootstrap default full mode.
|
|
35
|
+
// Intentionally disabled: flag absent + state.enabled=false -> exit 0.
|
|
36
36
|
const flagExists = fs.existsSync(FLAG_PATH);
|
|
37
37
|
const stateExists = fs.existsSync(STATE_PATH);
|
|
38
38
|
if (!flagExists) {
|
|
39
39
|
if (!stateExists) {
|
|
40
|
-
// First install
|
|
40
|
+
// First install: bootstrap and activate full mode.
|
|
41
41
|
fs.mkdirSync(FEYNMAN_DIR, { recursive: true });
|
|
42
42
|
fs.writeFileSync(STATE_PATH, JSON.stringify(DEFAULT_STATE, null, 2));
|
|
43
43
|
fs.writeFileSync(FLAG_PATH, DEFAULT_STATE.intensity);
|
|
44
44
|
} else {
|
|
45
|
-
|
|
45
|
+
let existingState;
|
|
46
|
+
try {
|
|
47
|
+
existingState = { ...DEFAULT_STATE, ...JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')) };
|
|
48
|
+
} catch (_) {
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
if (!existingState.enabled) process.exit(0);
|
|
52
|
+
fs.writeFileSync(FLAG_PATH, existingState.intensity || DEFAULT_STATE.intensity);
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// feynman — SessionStart hook — injects active diagram rules at session start.
|
|
3
|
+
// UserPromptSubmit still reinforces rules every turn; this primes fresh sessions.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const HOME = os.homedir();
|
|
11
|
+
const CLIENT_HOME = process.env.FEYNMAN_HOME || path.join(HOME, '.claude');
|
|
12
|
+
const FEYNMAN_DIR = path.join(CLIENT_HOME, '.feynman');
|
|
13
|
+
const STATE_PATH = path.join(FEYNMAN_DIR, 'state.json');
|
|
14
|
+
const FLAG_PATH = path.join(CLIENT_HOME, '.feynman-active');
|
|
15
|
+
const RULES_PATH = path.join(__dirname, '..', 'rules', 'feynman-activate.md');
|
|
16
|
+
|
|
17
|
+
const DEFAULT_STATE = { enabled: true, intensity: 'full', injections: 0 };
|
|
18
|
+
const VALID_INTENSITIES = ['lite', 'full', 'ultra'];
|
|
19
|
+
|
|
20
|
+
function readState() {
|
|
21
|
+
try {
|
|
22
|
+
return { ...DEFAULT_STATE, ...JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')) };
|
|
23
|
+
} catch (_) {
|
|
24
|
+
return { ...DEFAULT_STATE };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeState(state) {
|
|
29
|
+
fs.mkdirSync(FEYNMAN_DIR, { recursive: true });
|
|
30
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readRules(intensity) {
|
|
34
|
+
const rulesContent = fs.readFileSync(RULES_PATH, 'utf8');
|
|
35
|
+
const selected = VALID_INTENSITIES.includes(intensity) ? intensity : 'full';
|
|
36
|
+
const openMarker = '<!-- ' + selected + ' -->';
|
|
37
|
+
const closeMarker = '<!-- /' + selected + ' -->';
|
|
38
|
+
const i1 = rulesContent.indexOf(openMarker);
|
|
39
|
+
const i2 = rulesContent.indexOf(closeMarker, i1);
|
|
40
|
+
if (i1 === -1 || i2 === -1) return '';
|
|
41
|
+
return rulesContent.slice(i1 + openMarker.length, i2).trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let input = '';
|
|
45
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
46
|
+
process.stdin.on('end', () => {
|
|
47
|
+
try {
|
|
48
|
+
if (input.trim()) {
|
|
49
|
+
const data = JSON.parse(input);
|
|
50
|
+
const sessionId = data.session_id || '';
|
|
51
|
+
if (sessionId && /[/\\]|\.\./.test(sessionId)) process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const stateExists = fs.existsSync(STATE_PATH);
|
|
55
|
+
const flagExists = fs.existsSync(FLAG_PATH);
|
|
56
|
+
const state = readState();
|
|
57
|
+
|
|
58
|
+
if (!stateExists) {
|
|
59
|
+
writeState(state);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!state.enabled) {
|
|
63
|
+
try { fs.unlinkSync(FLAG_PATH); } catch (_) {}
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!flagExists) {
|
|
68
|
+
fs.writeFileSync(FLAG_PATH, state.intensity || DEFAULT_STATE.intensity);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rulesText = readRules(state.intensity);
|
|
72
|
+
if (!rulesText) process.exit(0);
|
|
73
|
+
|
|
74
|
+
// SessionStart accepts plain stdout as context, matching caveman's hook shape.
|
|
75
|
+
process.stdout.write(rulesText);
|
|
76
|
+
} catch (_) {
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
});
|
package/hooks/hooks.json
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"description": "Inject feynman ASCII diagram rules before each Claude Code prompt.",
|
|
3
3
|
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "FEYNMAN_HOME=\"$HOME/.claude\" node \"${CLAUDE_PLUGIN_ROOT}/hooks/feynman-session-start.js\"",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
4
15
|
"UserPromptSubmit": [
|
|
5
16
|
{
|
|
6
17
|
"hooks": [
|
|
7
18
|
{
|
|
8
19
|
"type": "command",
|
|
9
20
|
"command": "FEYNMAN_HOME=\"$HOME/.claude\" node \"${CLAUDE_PLUGIN_ROOT}/hooks/feynman-activate.js\"",
|
|
10
|
-
"timeout": 5
|
|
11
|
-
"statusMessage": "Injecting diagram rules..."
|
|
21
|
+
"timeout": 5
|
|
12
22
|
}
|
|
13
23
|
]
|
|
14
24
|
}
|
package/hooks.json
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "PLUGIN_ROOT=\"${PLUGIN_ROOT:-$CLAUDE_PLUGIN_ROOT}\"; FEYNMAN_HOME=\"$HOME/.codex\" node \"$PLUGIN_ROOT/hooks/feynman-session-start.js\"",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
3
15
|
"UserPromptSubmit": [
|
|
4
16
|
{
|
|
5
17
|
"hooks": [
|
|
6
18
|
{
|
|
7
19
|
"type": "command",
|
|
8
20
|
"command": "PLUGIN_ROOT=\"${PLUGIN_ROOT:-$CLAUDE_PLUGIN_ROOT}\"; FEYNMAN_HOME=\"$HOME/.codex\" node \"$PLUGIN_ROOT/hooks/feynman-activate.js\"",
|
|
9
|
-
"timeout": 5
|
|
10
|
-
"statusMessage": "Injecting diagram rules..."
|
|
21
|
+
"timeout": 5
|
|
11
22
|
}
|
|
12
23
|
]
|
|
13
24
|
}
|
package/package.json
CHANGED
package/skills/feynman/SKILL.md
CHANGED
|
@@ -28,7 +28,7 @@ node -e "
|
|
|
28
28
|
const fs = require('fs'), os = require('os'), path = require('path');
|
|
29
29
|
const stateFile = path.join(os.homedir(), '.claude', '.feynman', 'state.json');
|
|
30
30
|
const flagFile = path.join(os.homedir(), '.claude', '.feynman-active');
|
|
31
|
-
let st = {enabled:
|
|
31
|
+
let st = {enabled: true, intensity: 'full', injections: 0};
|
|
32
32
|
try { st = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch(e) {}
|
|
33
33
|
console.log('enabled:', st.enabled, '| intensity:', st.intensity, '| injections:', (st.injections ?? st.count ?? 0), '| flag:', fs.existsSync(flagFile));
|
|
34
34
|
"
|
|
@@ -43,7 +43,7 @@ const stateFile = path.join(os.homedir(), '.claude', '.feynman', 'state.json');
|
|
|
43
43
|
const flagFile = path.join(os.homedir(), '.claude', '.feynman-active');
|
|
44
44
|
const arg = (process.argv[1] || '').trim().toLowerCase();
|
|
45
45
|
const normalized = arg === 'start' ? 'on' : arg === 'stop' ? 'off' : arg;
|
|
46
|
-
let st = {enabled:
|
|
46
|
+
let st = {enabled: false, intensity: 'full', injections: 0};
|
|
47
47
|
try { st = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch(e) {}
|
|
48
48
|
if (normalized === 'on') { st.enabled = true; fs.writeFileSync(flagFile, st.intensity); }
|
|
49
49
|
if (normalized === 'off') { st.enabled = false; try { fs.unlinkSync(flagFile); } catch(e) {} }
|