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 +26 -11
- package/bin/install.test.js +93 -167
- package/hooks/df-command-usage.js +17 -26
- package/hooks/df-execution-history.js +43 -53
- package/hooks/df-explore-protocol.js +30 -39
- package/hooks/df-invariant-check.js +85 -58
- package/hooks/df-invariant-check.test.js +175 -1
- package/hooks/df-snapshot-guard.js +32 -40
- package/hooks/df-statusline.js +3 -12
- package/hooks/df-stdin-migration.test.js +106 -0
- package/hooks/df-subagent-registry.js +42 -48
- package/hooks/df-tool-usage-spike.js +15 -26
- package/hooks/df-tool-usage.js +37 -47
- package/hooks/df-worktree-guard.js +28 -36
- package/hooks/lib/hook-stdin.js +47 -0
- package/hooks/lib/hook-stdin.test.js +200 -0
- package/hooks/lib/lint-no-bare-stdin.js +68 -0
- package/hooks/lib/lint-no-bare-stdin.test.js +82 -0
- package/package.json +1 -1
- package/src/commands/df/execute.md +25 -174
- package/src/eval/git-memory.js +8 -8
- package/src/eval/git-memory.test.js +128 -1
- package/src/eval/loop.js +3 -3
- package/src/eval/loop.test.js +158 -0
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
path.join(
|
|
163
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
+
}
|
package/bin/install.test.js
CHANGED
|
@@ -590,217 +590,143 @@ describe('Uninstaller — file removal and settings cleanup', () => {
|
|
|
590
590
|
});
|
|
591
591
|
|
|
592
592
|
// ---------------------------------------------------------------------------
|
|
593
|
-
// T4. command-usage hook registration
|
|
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
|
-
// --
|
|
598
|
+
// -- @hook-event tag: verify df-command-usage.js declares correct events --
|
|
599
599
|
|
|
600
|
-
test('
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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('
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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
|
|
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(
|
|
646
|
-
'
|
|
638
|
+
src.includes('if (!settings.hooks[event]) settings.hooks[event] = [];'),
|
|
639
|
+
'Should initialize event array dynamically'
|
|
647
640
|
);
|
|
648
641
|
});
|
|
649
642
|
|
|
650
|
-
// --
|
|
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
|
-
|
|
664
|
-
|
|
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('
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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('
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
730
|
-
'
|
|
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
|
|
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
|
-
|
|
741
|
-
'Uninstall
|
|
717
|
+
uninstallSection[0].includes('removeDeepflowHooks'),
|
|
718
|
+
'Uninstall should use removeDeepflowHooks for generic cleanup'
|
|
742
719
|
);
|
|
743
720
|
});
|
|
744
721
|
|
|
745
|
-
test('uninstall
|
|
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(
|
|
750
|
-
'Uninstall
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
if (
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
74
|
+
// Only record if we have a task_id
|
|
75
|
+
if (!taskId) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
114
78
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
108
|
+
|
|
109
|
+
fs.appendFileSync(historyFile, JSON.stringify(startRecord) + '\n');
|
|
110
|
+
fs.appendFileSync(historyFile, JSON.stringify(endRecord) + '\n');
|
|
121
111
|
});
|