deepflow 0.1.105 → 0.1.107

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/bin/install.js CHANGED
@@ -156,14 +156,21 @@ async function main() {
156
156
  if (level === 'global') {
157
157
  const hooksDir = path.join(PACKAGE_DIR, 'hooks');
158
158
  if (fs.existsSync(hooksDir)) {
159
- for (const file of fs.readdirSync(hooksDir)) {
160
- if (file.endsWith('.js')) {
161
- fs.copyFileSync(
162
- path.join(hooksDir, file),
163
- path.join(CLAUDE_DIR, 'hooks', file)
164
- );
159
+ const copyDirRecursive = (srcDir, destDir) => {
160
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
161
+ if (entry.isDirectory()) {
162
+ const subDest = path.join(destDir, entry.name);
163
+ fs.mkdirSync(subDest, { recursive: true });
164
+ copyDirRecursive(path.join(srcDir, entry.name), subDest);
165
+ } else if (entry.name.endsWith('.js')) {
166
+ fs.copyFileSync(
167
+ path.join(srcDir, entry.name),
168
+ path.join(destDir, entry.name)
169
+ );
170
+ }
165
171
  }
166
- }
172
+ };
173
+ copyDirRecursive(hooksDir, path.join(CLAUDE_DIR, 'hooks'));
167
174
  log('Hooks installed');
168
175
  }
169
176
  }
@@ -620,6 +627,8 @@ async function uninstall() {
620
627
  }
621
628
  }
622
629
  }
630
+ // Remove hooks/lib (shared hook utilities)
631
+ toRemove.push('hooks/lib');
623
632
  }
624
633
 
625
634
  for (const item of toRemove) {
@@ -707,7 +716,13 @@ async function uninstall() {
707
716
  console.log('');
708
717
  }
709
718
 
710
- main().catch(err => {
711
- console.error('Installation failed:', err.message);
712
- process.exit(1);
713
- });
719
+ // Export for testing
720
+ module.exports = { scanHookEvents, removeDeepflowHooks };
721
+
722
+ // Only run main when executed directly (not when required by tests)
723
+ if (require.main === module) {
724
+ main().catch(err => {
725
+ console.error('Installation failed:', err.message);
726
+ process.exit(1);
727
+ });
728
+ }
@@ -590,217 +590,143 @@ describe('Uninstaller — file removal and settings cleanup', () => {
590
590
  });
591
591
 
592
592
  // ---------------------------------------------------------------------------
593
- // T4. command-usage hook registration (PreToolUse, PostToolUse, SessionEnd)
593
+ // T4. command-usage hook registration via dynamic @hook-event tags
594
594
  // ---------------------------------------------------------------------------
595
595
 
596
596
  describe('T4 — command-usage hook registration in install.js', () => {
597
597
 
598
- // -- Source-level checks: verify install.js registers df-command-usage.js --
598
+ // -- @hook-event tag: verify df-command-usage.js declares correct events --
599
599
 
600
- test('source defines commandUsageCmd variable', () => {
601
- const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
602
- const pattern = /commandUsageCmd\s*=\s*`node.*df-command-usage\.js/;
603
- assert.ok(
604
- pattern.test(src),
605
- 'install.js should define commandUsageCmd pointing to df-command-usage.js'
606
- );
600
+ test('df-command-usage.js has @hook-event tag for PreToolUse, PostToolUse, SessionEnd', () => {
601
+ const hookPath = path.resolve(__dirname, '..', 'hooks', 'df-command-usage.js');
602
+ const content = fs.readFileSync(hookPath, 'utf8');
603
+ const firstLines = content.split('\n').slice(0, 10).join('\n');
604
+ const match = firstLines.match(/\/\/\s*@hook-event:\s*(.+)/);
605
+ assert.ok(match, 'df-command-usage.js should have @hook-event tag in first 10 lines');
606
+ const events = match[1].split(',').map(e => e.trim());
607
+ assert.ok(events.includes('PreToolUse'), 'Should declare PreToolUse event');
608
+ assert.ok(events.includes('PostToolUse'), 'Should declare PostToolUse event');
609
+ assert.ok(events.includes('SessionEnd'), 'Should declare SessionEnd event');
607
610
  });
608
611
 
609
- test('source pushes command-usage hook to PreToolUse', () => {
610
- const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
611
- // Find PreToolUse section — should contain a push with commandUsageCmd
612
- const preToolUseSection = src.match(/PreToolUse[\s\S]*?log\('PreToolUse hook configured'\)/);
613
- assert.ok(preToolUseSection, 'Should have a PreToolUse configuration section');
614
- assert.ok(
615
- preToolUseSection[0].includes('commandUsageCmd'),
616
- 'PreToolUse section should push commandUsageCmd'
617
- );
618
- });
612
+ // -- scanHookEvents: verify dynamic hook scanning maps events correctly --
619
613
 
620
- test('source pushes command-usage hook to PostToolUse', () => {
621
- const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
622
- // Find PostToolUse section
623
- const postToolUseSection = src.match(/PostToolUse[\s\S]*?log\('PostToolUse hook configured'\)/);
624
- assert.ok(postToolUseSection, 'Should have a PostToolUse configuration section');
625
- assert.ok(
626
- postToolUseSection[0].includes('commandUsageCmd'),
627
- 'PostToolUse section should push commandUsageCmd'
628
- );
614
+ test('scanHookEvents maps df-command-usage.js to all three events', () => {
615
+ const { scanHookEvents } = require('./install.js');
616
+ const hooksDir = path.resolve(__dirname, '..', 'hooks');
617
+ const { eventMap } = scanHookEvents(hooksDir);
618
+ for (const event of ['PreToolUse', 'PostToolUse', 'SessionEnd']) {
619
+ assert.ok(eventMap.has(event), `eventMap should have ${event}`);
620
+ assert.ok(
621
+ eventMap.get(event).includes('df-command-usage.js'),
622
+ `${event} should include df-command-usage.js`
623
+ );
624
+ }
629
625
  });
630
626
 
631
- test('source pushes command-usage hook to SessionEnd', () => {
627
+ // -- configureHooks uses dynamic wiring (no hardcoded per-hook variables) --
628
+
629
+ test('source uses scanHookEvents for dynamic hook wiring', () => {
632
630
  const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
633
- // Find SessionEnd section — should include command-usage alongside quota-logger + dashboard-push
634
- const sessionEndSection = src.match(/SessionEnd[\s\S]*?log\('Quota logger.*configured.*SessionEnd/);
635
- assert.ok(sessionEndSection, 'Should have a SessionEnd configuration section');
636
- assert.ok(
637
- sessionEndSection[0].includes('commandUsageCmd'),
638
- 'SessionEnd section should push commandUsageCmd'
639
- );
631
+ assert.ok(src.includes('scanHookEvents('), 'Should call scanHookEvents');
632
+ assert.ok(src.includes('for (const [event, files] of eventMap)'), 'Should iterate eventMap to wire hooks');
640
633
  });
641
634
 
642
- test('source creates PreToolUse array if missing', () => {
635
+ test('source initializes event array if missing', () => {
643
636
  const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
644
637
  assert.ok(
645
- src.includes("if (!settings.hooks.PreToolUse)"),
646
- 'install.js should initialize PreToolUse array if not present'
638
+ src.includes('if (!settings.hooks[event]) settings.hooks[event] = [];'),
639
+ 'Should initialize event array dynamically'
647
640
  );
648
641
  });
649
642
 
650
- // -- Dedup logic: filter removes existing command-usage before re-adding --
651
-
652
- test('PreToolUse dedup filter removes existing df-command-usage entries', () => {
653
- const preToolUse = [
654
- { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
655
- { hooks: [{ type: 'command', command: 'node /usr/local/my-custom.js' }] },
656
- ];
657
-
658
- const filtered = preToolUse.filter(hook => {
659
- const cmd = hook.hooks?.[0]?.command || '';
660
- return !cmd.includes('df-command-usage');
661
- });
643
+ // -- removeDeepflowHooks: generic removal of all /hooks/df- entries --
662
644
 
663
- assert.equal(filtered.length, 1, 'Should remove existing df-command-usage hook');
664
- assert.ok(filtered[0].hooks[0].command.includes('my-custom.js'), 'Should keep non-deepflow hooks');
645
+ test('removeDeepflowHooks removes df-command-usage from all events', () => {
646
+ const { removeDeepflowHooks } = require('./install.js');
647
+ const settings = {
648
+ hooks: {
649
+ PreToolUse: [
650
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
651
+ { hooks: [{ type: 'command', command: 'node /usr/local/my-custom.js' }] },
652
+ ],
653
+ PostToolUse: [
654
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
655
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-tool-usage.js' }] },
656
+ ],
657
+ SessionEnd: [
658
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
659
+ { hooks: [{ type: 'command', command: 'node /usr/local/keep.js' }] },
660
+ ],
661
+ }
662
+ };
663
+ removeDeepflowHooks(settings);
664
+ assert.equal(settings.hooks.PreToolUse.length, 1);
665
+ assert.ok(settings.hooks.PreToolUse[0].hooks[0].command.includes('my-custom.js'));
666
+ assert.ok(!('PostToolUse' in settings.hooks), 'PostToolUse should be deleted when only deepflow hooks');
667
+ assert.equal(settings.hooks.SessionEnd.length, 1);
668
+ assert.ok(settings.hooks.SessionEnd[0].hooks[0].command.includes('keep.js'));
665
669
  });
666
670
 
667
- test('PostToolUse dedup filter removes df-command-usage alongside other deepflow hooks', () => {
668
- const postToolUse = [
669
- { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-tool-usage.js' }] },
670
- { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
671
- { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-worktree-guard.js' }] },
672
- { hooks: [{ type: 'command', command: 'node /usr/local/keep-me.js' }] },
673
- ];
674
-
675
- const filtered = postToolUse.filter(hook => {
676
- const cmd = hook.hooks?.[0]?.command || '';
677
- return !cmd.includes('df-tool-usage') &&
678
- !cmd.includes('df-execution-history') &&
679
- !cmd.includes('df-worktree-guard') &&
680
- !cmd.includes('df-snapshot-guard') &&
681
- !cmd.includes('df-invariant-check') &&
682
- !cmd.includes('df-command-usage');
683
- });
684
-
685
- assert.equal(filtered.length, 1);
686
- assert.ok(filtered[0].hooks[0].command.includes('keep-me.js'));
671
+ test('removeDeepflowHooks deletes event key when array becomes empty', () => {
672
+ const { removeDeepflowHooks } = require('./install.js');
673
+ const settings = {
674
+ hooks: {
675
+ PreToolUse: [
676
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
677
+ ],
678
+ }
679
+ };
680
+ removeDeepflowHooks(settings);
681
+ assert.ok(!('PreToolUse' in (settings.hooks || {})), 'PreToolUse should be deleted when empty');
687
682
  });
688
683
 
689
- test('SessionEnd dedup filter removes df-command-usage alongside quota-logger and dashboard-push', () => {
690
- const sessionEnd = [
691
- { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-quota-logger.js' }] },
692
- { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-dashboard-push.js' }] },
693
- { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
694
- { hooks: [{ type: 'command', command: 'node /usr/local/keep.js' }] },
695
- ];
696
-
697
- const filtered = sessionEnd.filter(hook => {
698
- const cmd = hook.hooks?.[0]?.command || '';
699
- return !cmd.includes('df-quota-logger') &&
700
- !cmd.includes('df-dashboard-push') &&
701
- !cmd.includes('df-command-usage');
702
- });
703
-
704
- assert.equal(filtered.length, 1);
705
- assert.ok(filtered[0].hooks[0].command.includes('keep.js'));
684
+ test('removeDeepflowHooks keeps non-deepflow hooks intact', () => {
685
+ const { removeDeepflowHooks } = require('./install.js');
686
+ const settings = {
687
+ hooks: {
688
+ PreToolUse: [
689
+ { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
690
+ { hooks: [{ type: 'command', command: 'node /usr/local/custom-pre-hook.js' }] },
691
+ ],
692
+ }
693
+ };
694
+ removeDeepflowHooks(settings);
695
+ assert.ok('PreToolUse' in settings.hooks, 'PreToolUse should be kept when custom hooks remain');
696
+ assert.equal(settings.hooks.PreToolUse.length, 1);
697
+ assert.ok(settings.hooks.PreToolUse[0].hooks[0].command.includes('custom-pre-hook.js'));
706
698
  });
707
699
 
708
- // -- Uninstall cleanup --
709
-
710
- test('uninstall toRemove includes df-command-usage.js', () => {
711
- const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
712
- // Find the toRemove.push(...) line for hooks in uninstall
713
- assert.ok(
714
- src.includes("'hooks/df-command-usage.js'"),
715
- 'toRemove should include hooks/df-command-usage.js for uninstall'
716
- );
717
- });
700
+ // -- Uninstall: dynamic df-*.js discovery --
718
701
 
719
- test('uninstall SessionEnd filter removes df-command-usage', () => {
702
+ test('uninstall dynamically discovers df-*.js hooks to remove', () => {
720
703
  const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
721
- // In the uninstall function, the SessionEnd filter should include df-command-usage
722
- // Find the uninstall section's SessionEnd filter
723
704
  const uninstallSection = src.match(/async function uninstall[\s\S]+$/);
724
705
  assert.ok(uninstallSection, 'Should have uninstall function');
725
- // Check SessionEnd filter in uninstall includes command-usage
726
- const sessionEndFilter = uninstallSection[0].match(/SessionEnd[\s\S]*?\.filter[\s\S]*?\);/);
727
- assert.ok(sessionEndFilter, 'Should have SessionEnd filter in uninstall');
728
706
  assert.ok(
729
- sessionEndFilter[0].includes('df-command-usage'),
730
- 'Uninstall SessionEnd filter should remove df-command-usage hooks'
707
+ uninstallSection[0].includes("file.startsWith('df-')") &&
708
+ uninstallSection[0].includes("file.endsWith('.js')"),
709
+ 'Uninstall should dynamically find df-*.js hook files'
731
710
  );
732
711
  });
733
712
 
734
- test('uninstall PostToolUse filter removes df-command-usage', () => {
713
+ test('uninstall uses removeDeepflowHooks for settings cleanup', () => {
735
714
  const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
736
715
  const uninstallSection = src.match(/async function uninstall[\s\S]+$/);
737
- const postToolUseFilter = uninstallSection[0].match(/PostToolUse[\s\S]*?\.filter[\s\S]*?\);/);
738
- assert.ok(postToolUseFilter, 'Should have PostToolUse filter in uninstall');
739
716
  assert.ok(
740
- postToolUseFilter[0].includes('df-command-usage'),
741
- 'Uninstall PostToolUse filter should remove df-command-usage hooks'
717
+ uninstallSection[0].includes('removeDeepflowHooks'),
718
+ 'Uninstall should use removeDeepflowHooks for generic cleanup'
742
719
  );
743
720
  });
744
721
 
745
- test('uninstall cleans up PreToolUse hooks', () => {
722
+ test('uninstall removes hooks/lib directory', () => {
746
723
  const src = fs.readFileSync(path.resolve(__dirname, 'install.js'), 'utf8');
747
724
  const uninstallSection = src.match(/async function uninstall[\s\S]+$/);
748
725
  assert.ok(
749
- uninstallSection[0].includes('PreToolUse'),
750
- 'Uninstall function should handle PreToolUse cleanup'
751
- );
752
- // Verify it filters out df-command-usage from PreToolUse
753
- const preToolUseFilter = uninstallSection[0].match(/PreToolUse[\s\S]*?\.filter[\s\S]*?\);/);
754
- assert.ok(preToolUseFilter, 'Should have PreToolUse filter in uninstall');
755
- assert.ok(
756
- preToolUseFilter[0].includes('df-command-usage'),
757
- 'Uninstall PreToolUse filter should remove df-command-usage hooks'
726
+ uninstallSection[0].includes("hooks/lib"),
727
+ 'Uninstall should remove hooks/lib directory'
758
728
  );
759
729
  });
760
-
761
- test('uninstall deletes PreToolUse key when array becomes empty', () => {
762
- // Reproduce the uninstall logic for PreToolUse
763
- const settings = {
764
- hooks: {
765
- PreToolUse: [
766
- { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
767
- ],
768
- }
769
- };
770
-
771
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(hook => {
772
- const cmd = hook.hooks?.[0]?.command || '';
773
- return !cmd.includes('df-command-usage');
774
- });
775
- if (settings.hooks.PreToolUse.length === 0) {
776
- delete settings.hooks.PreToolUse;
777
- }
778
-
779
- assert.ok(!('PreToolUse' in settings.hooks), 'PreToolUse should be deleted when empty after filtering');
780
- });
781
-
782
- test('uninstall keeps PreToolUse when non-deepflow hooks remain', () => {
783
- const settings = {
784
- hooks: {
785
- PreToolUse: [
786
- { hooks: [{ type: 'command', command: 'node /home/.claude/hooks/df-command-usage.js' }] },
787
- { hooks: [{ type: 'command', command: 'node /usr/local/custom-pre-hook.js' }] },
788
- ],
789
- }
790
- };
791
-
792
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(hook => {
793
- const cmd = hook.hooks?.[0]?.command || '';
794
- return !cmd.includes('df-command-usage');
795
- });
796
- if (settings.hooks.PreToolUse.length === 0) {
797
- delete settings.hooks.PreToolUse;
798
- }
799
-
800
- assert.ok('PreToolUse' in settings.hooks, 'PreToolUse should be kept when custom hooks remain');
801
- assert.equal(settings.hooks.PreToolUse.length, 1);
802
- assert.ok(settings.hooks.PreToolUse[0].hooks[0].command.includes('custom-pre-hook.js'));
803
- });
804
730
  });
805
731
 
806
732
  // ---------------------------------------------------------------------------
@@ -18,24 +18,20 @@
18
18
 
19
19
  const fs = require('fs');
20
20
  const path = require('path');
21
+ const { readStdinIfMain } = require('./lib/hook-stdin');
21
22
 
22
23
  const event = process.env.CLAUDE_HOOK_EVENT || '';
23
24
 
24
- // Read stdin for hook payload
25
- let raw = '';
26
- process.stdin.setEncoding('utf8');
27
- process.stdin.on('data', d => raw += d);
28
- process.stdin.on('end', () => {
25
+ readStdinIfMain(module, (data) => {
29
26
  try {
30
- main();
27
+ main(data);
31
28
  } catch (_e) {
32
29
  // REQ-8: never break Claude Code
33
30
  }
34
- process.exit(0);
35
31
  });
36
32
 
37
- function main() {
38
- const baseDir = findProjectDir();
33
+ function main(data) {
34
+ const baseDir = findProjectDir(data);
39
35
  if (!baseDir) return;
40
36
 
41
37
  const deepflowDir = path.join(baseDir, '.deepflow');
@@ -44,20 +40,18 @@ function main() {
44
40
  const tokenHistoryPath = path.join(deepflowDir, 'token-history.jsonl');
45
41
 
46
42
  if (event === 'PreToolUse') {
47
- handlePreToolUse(deepflowDir, markerPath, usagePath, tokenHistoryPath);
43
+ handlePreToolUse(data, deepflowDir, markerPath, usagePath, tokenHistoryPath);
48
44
  } else if (event === 'PostToolUse') {
49
- handlePostToolUse(markerPath);
45
+ handlePostToolUse(data, markerPath);
50
46
  } else if (event === 'SessionStart') {
51
- handleSessionStart(markerPath, usagePath, tokenHistoryPath);
47
+ handleSessionStart(data, markerPath, usagePath, tokenHistoryPath);
52
48
  } else if (event === 'SessionEnd') {
53
49
  handleSessionEnd(deepflowDir, markerPath, usagePath, tokenHistoryPath);
54
50
  }
55
51
  }
56
52
 
57
- function handlePreToolUse(deepflowDir, markerPath, usagePath, tokenHistoryPath) {
58
- let payload;
59
- try { payload = JSON.parse(raw); } catch { return; }
60
-
53
+ function handlePreToolUse(data, deepflowDir, markerPath, usagePath, tokenHistoryPath) {
54
+ const payload = data;
61
55
  const toolName = payload.tool_name || '';
62
56
  const toolInput = payload.tool_input || {};
63
57
 
@@ -96,12 +90,11 @@ function handlePreToolUse(deepflowDir, markerPath, usagePath, tokenHistoryPath)
96
90
  safeWriteFile(markerPath, JSON.stringify(marker, null, 2));
97
91
  }
98
92
 
99
- function handlePostToolUse(markerPath) {
93
+ function handlePostToolUse(data, markerPath) {
100
94
  if (!safeExists(markerPath)) return;
101
95
 
102
96
  // Don't count the Skill call itself (the one that opened the marker)
103
- let payload;
104
- try { payload = JSON.parse(raw); } catch { return; }
97
+ const payload = data;
105
98
  const toolName = payload.tool_name || '';
106
99
  const toolInput = payload.tool_input || {};
107
100
  if (toolName === 'Skill' && (toolInput.skill || '').startsWith('df:')) return;
@@ -119,10 +112,9 @@ function handlePostToolUse(markerPath) {
119
112
  * On /clear or /compact, context resets — close any orphaned marker.
120
113
  * Only fires for source=clear|compact (not startup/resume).
121
114
  */
122
- function handleSessionStart(markerPath, usagePath, tokenHistoryPath) {
115
+ function handleSessionStart(data, markerPath, usagePath, tokenHistoryPath) {
123
116
  if (!safeExists(markerPath)) return;
124
- let payload;
125
- try { payload = JSON.parse(raw); } catch { return; }
117
+ const payload = data;
126
118
  const source = payload.source || '';
127
119
  if (source === 'clear' || source === 'compact') {
128
120
  closeCommand(markerPath, usagePath, tokenHistoryPath);
@@ -250,11 +242,10 @@ function parseTranscriptOutputTokens(transcriptPath, offset) {
250
242
  /**
251
243
  * Find the project directory from hook payload or environment.
252
244
  */
253
- function findProjectDir() {
245
+ function findProjectDir(data) {
254
246
  try {
255
- const payload = JSON.parse(raw);
256
- if (payload.cwd) return payload.cwd;
257
- if (payload.workspace && payload.workspace.current_dir) return payload.workspace.current_dir;
247
+ if (data && data.cwd) return data.cwd;
248
+ if (data && data.workspace && data.workspace.current_dir) return data.workspace.current_dir;
258
249
  } catch (_e) {
259
250
  // fall through
260
251
  }
@@ -14,6 +14,7 @@
14
14
 
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
+ const { readStdinIfMain } = require('./lib/hook-stdin');
17
18
 
18
19
  /**
19
20
  * Extract task_id from Agent prompt.
@@ -61,61 +62,50 @@ function resolveProjectRoot(cwd) {
61
62
  return cwd;
62
63
  }
63
64
 
64
- // Read all stdin, then process
65
- let raw = '';
66
- process.stdin.setEncoding('utf8');
67
- process.stdin.on('data', chunk => { raw += chunk; });
68
- process.stdin.on('end', () => {
69
- try {
70
- const data = JSON.parse(raw);
71
-
72
- // Only fire for Agent tool calls
73
- if (data.tool_name !== 'Agent') {
74
- process.exit(0);
75
- }
76
-
77
- const prompt = (data.tool_input && data.tool_input.prompt) || '';
78
- const taskId = extractTaskId(prompt);
79
-
80
- // Only record if we have a task_id
81
- if (!taskId) {
82
- process.exit(0);
83
- }
84
-
85
- const cwd = data.cwd || process.cwd();
86
- const projectRoot = resolveProjectRoot(cwd);
87
- const historyFile = path.join(projectRoot, '.deepflow', 'execution-history.jsonl');
88
-
89
- const timestamp = new Date().toISOString();
90
- const sessionId = data.session_id || null;
91
- const spec = extractSpec(prompt);
92
- const status = extractStatus(data.tool_response);
93
-
94
- const startRecord = {
95
- type: 'task_start',
96
- task_id: taskId,
97
- spec,
98
- session_id: sessionId,
99
- timestamp,
100
- };
65
+ readStdinIfMain(module, (data) => {
66
+ // Only fire for Agent tool calls
67
+ if (data.tool_name !== 'Agent') {
68
+ return;
69
+ }
101
70
 
102
- const endRecord = {
103
- type: 'task_end',
104
- task_id: taskId,
105
- session_id: sessionId,
106
- status,
107
- timestamp,
108
- };
71
+ const prompt = (data.tool_input && data.tool_input.prompt) || '';
72
+ const taskId = extractTaskId(prompt);
109
73
 
110
- const logDir = path.dirname(historyFile);
111
- if (!fs.existsSync(logDir)) {
112
- fs.mkdirSync(logDir, { recursive: true });
113
- }
74
+ // Only record if we have a task_id
75
+ if (!taskId) {
76
+ return;
77
+ }
114
78
 
115
- fs.appendFileSync(historyFile, JSON.stringify(startRecord) + '\n');
116
- fs.appendFileSync(historyFile, JSON.stringify(endRecord) + '\n');
117
- } catch (_e) {
118
- // Fail silently — never break tool execution (REQ-8)
79
+ const cwd = data.cwd || process.cwd();
80
+ const projectRoot = resolveProjectRoot(cwd);
81
+ const historyFile = path.join(projectRoot, '.deepflow', 'execution-history.jsonl');
82
+
83
+ const timestamp = new Date().toISOString();
84
+ const sessionId = data.session_id || null;
85
+ const spec = extractSpec(prompt);
86
+ const status = extractStatus(data.tool_response);
87
+
88
+ const startRecord = {
89
+ type: 'task_start',
90
+ task_id: taskId,
91
+ spec,
92
+ session_id: sessionId,
93
+ timestamp,
94
+ };
95
+
96
+ const endRecord = {
97
+ type: 'task_end',
98
+ task_id: taskId,
99
+ session_id: sessionId,
100
+ status,
101
+ timestamp,
102
+ };
103
+
104
+ const logDir = path.dirname(historyFile);
105
+ if (!fs.existsSync(logDir)) {
106
+ fs.mkdirSync(logDir, { recursive: true });
119
107
  }
120
- process.exit(0);
108
+
109
+ fs.appendFileSync(historyFile, JSON.stringify(startRecord) + '\n');
110
+ fs.appendFileSync(historyFile, JSON.stringify(endRecord) + '\n');
121
111
  });