claude-code-pilot 2.0.0
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/LICENSE +21 -0
- package/README.md +151 -0
- package/bin/install.js +431 -0
- package/docs/agent-guides/architecture.md +107 -0
- package/ecc/agents/architect.md +211 -0
- package/ecc/agents/code-reviewer.md +237 -0
- package/ecc/agents/doc-updater.md +107 -0
- package/ecc/agents/e2e-runner.md +107 -0
- package/ecc/agents/security-reviewer.md +108 -0
- package/ecc/agents/tdd-guide.md +91 -0
- package/ecc/commands/checkpoint.md +74 -0
- package/ecc/commands/evolve.md +178 -0
- package/ecc/commands/learn.md +70 -0
- package/ecc/commands/model-route.md +26 -0
- package/ecc/commands/quality-gate.md +29 -0
- package/ecc/commands/resume-session.md +155 -0
- package/ecc/commands/save-session.md +275 -0
- package/ecc/commands/sessions.md +305 -0
- package/ecc/commands/verify.md +59 -0
- package/ecc/contexts/dev.md +20 -0
- package/ecc/contexts/research.md +26 -0
- package/ecc/contexts/review.md +22 -0
- package/ecc/examples/CLAUDE.md +100 -0
- package/ecc/examples/django-api-CLAUDE.md +308 -0
- package/ecc/examples/go-microservice-CLAUDE.md +267 -0
- package/ecc/examples/rust-api-CLAUDE.md +285 -0
- package/ecc/examples/saas-nextjs-CLAUDE.md +166 -0
- package/ecc/examples/user-CLAUDE.md +109 -0
- package/ecc/rules/common/agents.md +49 -0
- package/ecc/rules/common/coding-style.md +48 -0
- package/ecc/rules/common/development-workflow.md +37 -0
- package/ecc/rules/common/git-workflow.md +24 -0
- package/ecc/rules/common/hooks.md +30 -0
- package/ecc/rules/common/patterns.md +31 -0
- package/ecc/rules/common/performance.md +55 -0
- package/ecc/rules/common/security.md +29 -0
- package/ecc/rules/common/testing.md +29 -0
- package/ecc/rules/golang/coding-style.md +32 -0
- package/ecc/rules/golang/hooks.md +17 -0
- package/ecc/rules/golang/patterns.md +45 -0
- package/ecc/rules/golang/security.md +34 -0
- package/ecc/rules/golang/testing.md +31 -0
- package/ecc/rules/kotlin/coding-style.md +86 -0
- package/ecc/rules/kotlin/patterns.md +146 -0
- package/ecc/rules/kotlin/security.md +82 -0
- package/ecc/rules/kotlin/testing.md +128 -0
- package/ecc/rules/perl/coding-style.md +46 -0
- package/ecc/rules/perl/hooks.md +22 -0
- package/ecc/rules/perl/patterns.md +76 -0
- package/ecc/rules/perl/security.md +69 -0
- package/ecc/rules/perl/testing.md +54 -0
- package/ecc/rules/php/coding-style.md +35 -0
- package/ecc/rules/php/hooks.md +24 -0
- package/ecc/rules/php/patterns.md +32 -0
- package/ecc/rules/php/security.md +33 -0
- package/ecc/rules/php/testing.md +34 -0
- package/ecc/rules/python/coding-style.md +42 -0
- package/ecc/rules/python/hooks.md +19 -0
- package/ecc/rules/python/patterns.md +39 -0
- package/ecc/rules/python/security.md +30 -0
- package/ecc/rules/python/testing.md +38 -0
- package/ecc/rules/swift/coding-style.md +47 -0
- package/ecc/rules/swift/hooks.md +20 -0
- package/ecc/rules/swift/patterns.md +66 -0
- package/ecc/rules/swift/security.md +33 -0
- package/ecc/rules/swift/testing.md +45 -0
- package/ecc/rules/typescript/coding-style.md +199 -0
- package/ecc/rules/typescript/hooks.md +22 -0
- package/ecc/rules/typescript/patterns.md +52 -0
- package/ecc/rules/typescript/security.md +28 -0
- package/ecc/rules/typescript/testing.md +18 -0
- package/ecc/scripts/hooks/check-hook-enabled.js +12 -0
- package/ecc/scripts/hooks/evaluate-session.js +100 -0
- package/ecc/scripts/hooks/pre-compact.js +48 -0
- package/ecc/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/ecc/scripts/hooks/run-with-flags.js +120 -0
- package/ecc/scripts/hooks/session-end-marker.js +15 -0
- package/ecc/scripts/hooks/session-end.js +258 -0
- package/ecc/scripts/hooks/session-start.js +97 -0
- package/ecc/scripts/hooks/suggest-compact.js +80 -0
- package/ecc/scripts/lib/hook-flags.js +74 -0
- package/ecc/scripts/lib/package-manager.d.ts +119 -0
- package/ecc/scripts/lib/package-manager.js +431 -0
- package/ecc/scripts/lib/project-detect.js +428 -0
- package/ecc/scripts/lib/resolve-formatter.js +185 -0
- package/ecc/scripts/lib/session-aliases.d.ts +136 -0
- package/ecc/scripts/lib/session-aliases.js +481 -0
- package/ecc/scripts/lib/session-manager.d.ts +131 -0
- package/ecc/scripts/lib/session-manager.js +444 -0
- package/ecc/scripts/lib/shell-split.js +86 -0
- package/ecc/scripts/lib/utils.d.ts +183 -0
- package/ecc/scripts/lib/utils.js +543 -0
- package/ecc/skills/continuous-learning-v2/SKILL.md +365 -0
- package/ecc/skills/continuous-learning-v2/agents/observer-loop.sh +144 -0
- package/ecc/skills/continuous-learning-v2/agents/observer.md +198 -0
- package/ecc/skills/continuous-learning-v2/agents/start-observer.sh +194 -0
- package/ecc/skills/continuous-learning-v2/config.json +8 -0
- package/ecc/skills/continuous-learning-v2/hooks/observe.sh +246 -0
- package/ecc/skills/continuous-learning-v2/scripts/detect-project.sh +218 -0
- package/ecc/skills/continuous-learning-v2/scripts/instinct-cli.py +1148 -0
- package/ecc/skills/continuous-learning-v2/scripts/test_parse_instinct.py +984 -0
- package/ecc/skills/strategic-compact/SKILL.md +103 -0
- package/ecc/skills/strategic-compact/suggest-compact.sh +54 -0
- package/ecc/skills/verification-loop-SKILL.md +126 -0
- package/gsd/LICENSE +21 -0
- package/gsd/agents/gsd-codebase-mapper.md +772 -0
- package/gsd/agents/gsd-debugger.md +1257 -0
- package/gsd/agents/gsd-executor.md +489 -0
- package/gsd/agents/gsd-integration-checker.md +445 -0
- package/gsd/agents/gsd-nyquist-auditor.md +178 -0
- package/gsd/agents/gsd-phase-researcher.md +555 -0
- package/gsd/agents/gsd-plan-checker.md +708 -0
- package/gsd/agents/gsd-planner.md +1309 -0
- package/gsd/agents/gsd-project-researcher.md +631 -0
- package/gsd/agents/gsd-research-synthesizer.md +249 -0
- package/gsd/agents/gsd-roadmapper.md +652 -0
- package/gsd/agents/gsd-verifier.md +581 -0
- package/gsd/commands-gsd/add-phase.md +43 -0
- package/gsd/commands-gsd/add-tests.md +41 -0
- package/gsd/commands-gsd/add-todo.md +47 -0
- package/gsd/commands-gsd/audit-milestone.md +36 -0
- package/gsd/commands-gsd/check-todos.md +45 -0
- package/gsd/commands-gsd/cleanup.md +18 -0
- package/gsd/commands-gsd/complete-milestone.md +136 -0
- package/gsd/commands-gsd/debug.md +168 -0
- package/gsd/commands-gsd/discuss-phase.md +90 -0
- package/gsd/commands-gsd/execute-phase.md +41 -0
- package/gsd/commands-gsd/health.md +22 -0
- package/gsd/commands-gsd/help.md +22 -0
- package/gsd/commands-gsd/insert-phase.md +32 -0
- package/gsd/commands-gsd/join-discord.md +18 -0
- package/gsd/commands-gsd/list-phase-assumptions.md +46 -0
- package/gsd/commands-gsd/map-codebase.md +71 -0
- package/gsd/commands-gsd/new-milestone.md +44 -0
- package/gsd/commands-gsd/new-project.md +42 -0
- package/gsd/commands-gsd/pause-work.md +38 -0
- package/gsd/commands-gsd/plan-milestone-gaps.md +34 -0
- package/gsd/commands-gsd/plan-phase.md +45 -0
- package/gsd/commands-gsd/progress.md +24 -0
- package/gsd/commands-gsd/quick.md +45 -0
- package/gsd/commands-gsd/reapply-patches.md +123 -0
- package/gsd/commands-gsd/remove-phase.md +31 -0
- package/gsd/commands-gsd/research-phase.md +190 -0
- package/gsd/commands-gsd/resume-work.md +40 -0
- package/gsd/commands-gsd/set-profile.md +34 -0
- package/gsd/commands-gsd/settings.md +36 -0
- package/gsd/commands-gsd/update.md +37 -0
- package/gsd/commands-gsd/validate-phase.md +35 -0
- package/gsd/commands-gsd/verify-work.md +38 -0
- package/gsd/get-shit-done/bin/gsd-tools.cjs +592 -0
- package/gsd/get-shit-done/bin/lib/commands.cjs +548 -0
- package/gsd/get-shit-done/bin/lib/config.cjs +169 -0
- package/gsd/get-shit-done/bin/lib/core.cjs +492 -0
- package/gsd/get-shit-done/bin/lib/frontmatter.cjs +299 -0
- package/gsd/get-shit-done/bin/lib/init.cjs +710 -0
- package/gsd/get-shit-done/bin/lib/milestone.cjs +241 -0
- package/gsd/get-shit-done/bin/lib/phase.cjs +901 -0
- package/gsd/get-shit-done/bin/lib/roadmap.cjs +298 -0
- package/gsd/get-shit-done/bin/lib/state.cjs +721 -0
- package/gsd/get-shit-done/bin/lib/template.cjs +222 -0
- package/gsd/get-shit-done/bin/lib/verify.cjs +820 -0
- package/gsd/get-shit-done/references/checkpoints.md +776 -0
- package/gsd/get-shit-done/references/continuation-format.md +249 -0
- package/gsd/get-shit-done/references/decimal-phase-calculation.md +65 -0
- package/gsd/get-shit-done/references/git-integration.md +248 -0
- package/gsd/get-shit-done/references/git-planning-commit.md +38 -0
- package/gsd/get-shit-done/references/model-profile-resolution.md +34 -0
- package/gsd/get-shit-done/references/model-profiles.md +93 -0
- package/gsd/get-shit-done/references/phase-argument-parsing.md +61 -0
- package/gsd/get-shit-done/references/planning-config.md +200 -0
- package/gsd/get-shit-done/references/questioning.md +162 -0
- package/gsd/get-shit-done/references/tdd.md +263 -0
- package/gsd/get-shit-done/references/ui-brand.md +160 -0
- package/gsd/get-shit-done/references/verification-patterns.md +612 -0
- package/gsd/get-shit-done/templates/DEBUG.md +164 -0
- package/gsd/get-shit-done/templates/UAT.md +247 -0
- package/gsd/get-shit-done/templates/VALIDATION.md +76 -0
- package/gsd/get-shit-done/templates/codebase/architecture.md +255 -0
- package/gsd/get-shit-done/templates/codebase/concerns.md +310 -0
- package/gsd/get-shit-done/templates/codebase/conventions.md +307 -0
- package/gsd/get-shit-done/templates/codebase/integrations.md +280 -0
- package/gsd/get-shit-done/templates/codebase/stack.md +186 -0
- package/gsd/get-shit-done/templates/codebase/structure.md +285 -0
- package/gsd/get-shit-done/templates/codebase/testing.md +480 -0
- package/gsd/get-shit-done/templates/config.json +37 -0
- package/gsd/get-shit-done/templates/context.md +297 -0
- package/gsd/get-shit-done/templates/continue-here.md +78 -0
- package/gsd/get-shit-done/templates/debug-subagent-prompt.md +91 -0
- package/gsd/get-shit-done/templates/discovery.md +146 -0
- package/gsd/get-shit-done/templates/milestone-archive.md +123 -0
- package/gsd/get-shit-done/templates/milestone.md +115 -0
- package/gsd/get-shit-done/templates/phase-prompt.md +569 -0
- package/gsd/get-shit-done/templates/planner-subagent-prompt.md +117 -0
- package/gsd/get-shit-done/templates/project.md +184 -0
- package/gsd/get-shit-done/templates/requirements.md +231 -0
- package/gsd/get-shit-done/templates/research-project/ARCHITECTURE.md +204 -0
- package/gsd/get-shit-done/templates/research-project/FEATURES.md +147 -0
- package/gsd/get-shit-done/templates/research-project/PITFALLS.md +200 -0
- package/gsd/get-shit-done/templates/research-project/STACK.md +120 -0
- package/gsd/get-shit-done/templates/research-project/SUMMARY.md +170 -0
- package/gsd/get-shit-done/templates/research.md +552 -0
- package/gsd/get-shit-done/templates/retrospective.md +54 -0
- package/gsd/get-shit-done/templates/roadmap.md +202 -0
- package/gsd/get-shit-done/templates/state.md +176 -0
- package/gsd/get-shit-done/templates/summary-complex.md +59 -0
- package/gsd/get-shit-done/templates/summary-minimal.md +41 -0
- package/gsd/get-shit-done/templates/summary-standard.md +48 -0
- package/gsd/get-shit-done/templates/summary.md +248 -0
- package/gsd/get-shit-done/templates/user-setup.md +311 -0
- package/gsd/get-shit-done/templates/verification-report.md +322 -0
- package/gsd/get-shit-done/workflows/add-phase.md +112 -0
- package/gsd/get-shit-done/workflows/add-tests.md +351 -0
- package/gsd/get-shit-done/workflows/add-todo.md +158 -0
- package/gsd/get-shit-done/workflows/audit-milestone.md +332 -0
- package/gsd/get-shit-done/workflows/check-todos.md +177 -0
- package/gsd/get-shit-done/workflows/cleanup.md +152 -0
- package/gsd/get-shit-done/workflows/complete-milestone.md +764 -0
- package/gsd/get-shit-done/workflows/diagnose-issues.md +219 -0
- package/gsd/get-shit-done/workflows/discovery-phase.md +289 -0
- package/gsd/get-shit-done/workflows/discuss-phase.md +676 -0
- package/gsd/get-shit-done/workflows/execute-phase.md +459 -0
- package/gsd/get-shit-done/workflows/execute-plan.md +449 -0
- package/gsd/get-shit-done/workflows/health.md +159 -0
- package/gsd/get-shit-done/workflows/help.md +489 -0
- package/gsd/get-shit-done/workflows/insert-phase.md +130 -0
- package/gsd/get-shit-done/workflows/list-phase-assumptions.md +178 -0
- package/gsd/get-shit-done/workflows/map-codebase.md +316 -0
- package/gsd/get-shit-done/workflows/new-milestone.md +384 -0
- package/gsd/get-shit-done/workflows/new-project.md +1111 -0
- package/gsd/get-shit-done/workflows/pause-work.md +122 -0
- package/gsd/get-shit-done/workflows/plan-milestone-gaps.md +274 -0
- package/gsd/get-shit-done/workflows/plan-phase.md +560 -0
- package/gsd/get-shit-done/workflows/progress.md +382 -0
- package/gsd/get-shit-done/workflows/quick.md +601 -0
- package/gsd/get-shit-done/workflows/remove-phase.md +155 -0
- package/gsd/get-shit-done/workflows/research-phase.md +74 -0
- package/gsd/get-shit-done/workflows/resume-project.md +307 -0
- package/gsd/get-shit-done/workflows/set-profile.md +81 -0
- package/gsd/get-shit-done/workflows/settings.md +214 -0
- package/gsd/get-shit-done/workflows/transition.md +544 -0
- package/gsd/get-shit-done/workflows/update.md +240 -0
- package/gsd/get-shit-done/workflows/validate-phase.md +167 -0
- package/gsd/get-shit-done/workflows/verify-phase.md +243 -0
- package/gsd/get-shit-done/workflows/verify-work.md +583 -0
- package/gsd/hooks/gsd-check-update.js +81 -0
- package/gsd/hooks/gsd-context-monitor.js +141 -0
- package/gsd/hooks/gsd-statusline.js +115 -0
- package/kit/CLAUDE.md +43 -0
- package/kit/commands/kit/update.md +46 -0
- package/kit/commands/setup-refresh.md +50 -0
- package/kit/commands/setup.md +579 -0
- package/kit/commands/tool-guide.md +44 -0
- package/kit/hooks/kit-check-update.js +54 -0
- package/kit/mcp.json +10 -0
- package/kit/rules/code-style.md +24 -0
- package/manifest.json +30 -0
- package/package.json +36 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreCompact Hook - Save state before context compaction
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform (Windows, macOS, Linux)
|
|
6
|
+
*
|
|
7
|
+
* Runs before Claude compacts context, giving you a chance to
|
|
8
|
+
* preserve important state that might get lost in summarization.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const {
|
|
13
|
+
getSessionsDir,
|
|
14
|
+
getDateTimeString,
|
|
15
|
+
getTimeString,
|
|
16
|
+
findFiles,
|
|
17
|
+
ensureDir,
|
|
18
|
+
appendFile,
|
|
19
|
+
log
|
|
20
|
+
} = require('../lib/utils');
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const sessionsDir = getSessionsDir();
|
|
24
|
+
const compactionLog = path.join(sessionsDir, 'compaction-log.txt');
|
|
25
|
+
|
|
26
|
+
ensureDir(sessionsDir);
|
|
27
|
+
|
|
28
|
+
// Log compaction event with timestamp
|
|
29
|
+
const timestamp = getDateTimeString();
|
|
30
|
+
appendFile(compactionLog, `[${timestamp}] Context compaction triggered\n`);
|
|
31
|
+
|
|
32
|
+
// If there's an active session file, note the compaction
|
|
33
|
+
const sessions = findFiles(sessionsDir, '*-session.tmp');
|
|
34
|
+
|
|
35
|
+
if (sessions.length > 0) {
|
|
36
|
+
const activeSession = sessions[0].path;
|
|
37
|
+
const timeStr = getTimeString();
|
|
38
|
+
appendFile(activeSession, `\n---\n**[Compaction occurred at ${timeStr}]** - Context was summarized\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
log('[PreCompact] State saved before compaction');
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
main().catch(err => {
|
|
46
|
+
console.error('[PreCompact] Error:', err.message);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
HOOK_ID="${1:-}"
|
|
5
|
+
REL_SCRIPT_PATH="${2:-}"
|
|
6
|
+
PROFILES_CSV="${3:-standard,strict}"
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
|
|
9
|
+
|
|
10
|
+
# Preserve stdin for passthrough or script execution
|
|
11
|
+
INPUT="$(cat)"
|
|
12
|
+
|
|
13
|
+
if [[ -z "$HOOK_ID" || -z "$REL_SCRIPT_PATH" ]]; then
|
|
14
|
+
printf '%s' "$INPUT"
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Ask Node helper if this hook is enabled
|
|
19
|
+
ENABLED="$(node "${PLUGIN_ROOT}/scripts/hooks/check-hook-enabled.js" "$HOOK_ID" "$PROFILES_CSV" 2>/dev/null || echo yes)"
|
|
20
|
+
if [[ "$ENABLED" != "yes" ]]; then
|
|
21
|
+
printf '%s' "$INPUT"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
SCRIPT_PATH="${PLUGIN_ROOT}/${REL_SCRIPT_PATH}"
|
|
26
|
+
if [[ ! -f "$SCRIPT_PATH" ]]; then
|
|
27
|
+
echo "[Hook] Script not found for ${HOOK_ID}: ${SCRIPT_PATH}" >&2
|
|
28
|
+
printf '%s' "$INPUT"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
printf '%s' "$INPUT" | "$SCRIPT_PATH"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Executes a hook script only when enabled by ECC hook profile flags.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node run-with-flags.js <hookId> <scriptRelativePath> [profilesCsv]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { spawnSync } = require('child_process');
|
|
14
|
+
const { isHookEnabled } = require('../lib/hook-flags');
|
|
15
|
+
|
|
16
|
+
const MAX_STDIN = 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
function readStdinRaw() {
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
let raw = '';
|
|
21
|
+
process.stdin.setEncoding('utf8');
|
|
22
|
+
process.stdin.on('data', chunk => {
|
|
23
|
+
if (raw.length < MAX_STDIN) {
|
|
24
|
+
const remaining = MAX_STDIN - raw.length;
|
|
25
|
+
raw += chunk.substring(0, remaining);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
process.stdin.on('end', () => resolve(raw));
|
|
29
|
+
process.stdin.on('error', () => resolve(raw));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getPluginRoot() {
|
|
34
|
+
if (process.env.CLAUDE_PLUGIN_ROOT && process.env.CLAUDE_PLUGIN_ROOT.trim()) {
|
|
35
|
+
return process.env.CLAUDE_PLUGIN_ROOT;
|
|
36
|
+
}
|
|
37
|
+
return path.resolve(__dirname, '..', '..');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
const [, , hookId, relScriptPath, profilesCsv] = process.argv;
|
|
42
|
+
const raw = await readStdinRaw();
|
|
43
|
+
|
|
44
|
+
if (!hookId || !relScriptPath) {
|
|
45
|
+
process.stdout.write(raw);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!isHookEnabled(hookId, { profiles: profilesCsv })) {
|
|
50
|
+
process.stdout.write(raw);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const pluginRoot = getPluginRoot();
|
|
55
|
+
const resolvedRoot = path.resolve(pluginRoot);
|
|
56
|
+
const scriptPath = path.resolve(pluginRoot, relScriptPath);
|
|
57
|
+
|
|
58
|
+
// Prevent path traversal outside the plugin root
|
|
59
|
+
if (!scriptPath.startsWith(resolvedRoot + path.sep)) {
|
|
60
|
+
process.stderr.write(`[Hook] Path traversal rejected for ${hookId}: ${scriptPath}\n`);
|
|
61
|
+
process.stdout.write(raw);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(scriptPath)) {
|
|
66
|
+
process.stderr.write(`[Hook] Script not found for ${hookId}: ${scriptPath}\n`);
|
|
67
|
+
process.stdout.write(raw);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Prefer direct require() when the hook exports a run(rawInput) function.
|
|
72
|
+
// This eliminates one Node.js process spawn (~50-100ms savings per hook).
|
|
73
|
+
//
|
|
74
|
+
// SAFETY: Only require() hooks that export run(). Legacy hooks execute
|
|
75
|
+
// side effects at module scope (stdin listeners, process.exit, main() calls)
|
|
76
|
+
// which would interfere with the parent process or cause double execution.
|
|
77
|
+
let hookModule;
|
|
78
|
+
const src = fs.readFileSync(scriptPath, 'utf8');
|
|
79
|
+
const hasRunExport = /\bmodule\.exports\b/.test(src) && /\brun\b/.test(src);
|
|
80
|
+
|
|
81
|
+
if (hasRunExport) {
|
|
82
|
+
try {
|
|
83
|
+
hookModule = require(scriptPath);
|
|
84
|
+
} catch (requireErr) {
|
|
85
|
+
process.stderr.write(`[Hook] require() failed for ${hookId}: ${requireErr.message}\n`);
|
|
86
|
+
// Fall through to legacy spawnSync path
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (hookModule && typeof hookModule.run === 'function') {
|
|
91
|
+
try {
|
|
92
|
+
const output = hookModule.run(raw);
|
|
93
|
+
if (output !== null && output !== undefined) process.stdout.write(output);
|
|
94
|
+
} catch (runErr) {
|
|
95
|
+
process.stderr.write(`[Hook] run() error for ${hookId}: ${runErr.message}\n`);
|
|
96
|
+
process.stdout.write(raw);
|
|
97
|
+
}
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Legacy path: spawn a child Node process for hooks without run() export
|
|
102
|
+
const result = spawnSync('node', [scriptPath], {
|
|
103
|
+
input: raw,
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
env: process.env,
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
timeout: 30000
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
111
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
112
|
+
|
|
113
|
+
const code = Number.isInteger(result.status) ? result.status : 0;
|
|
114
|
+
process.exit(code);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main().catch(err => {
|
|
118
|
+
process.stderr.write(`[Hook] run-with-flags error: ${err.message}\n`);
|
|
119
|
+
process.exit(0);
|
|
120
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const MAX_STDIN = 1024 * 1024;
|
|
5
|
+
let raw = '';
|
|
6
|
+
process.stdin.setEncoding('utf8');
|
|
7
|
+
process.stdin.on('data', chunk => {
|
|
8
|
+
if (raw.length < MAX_STDIN) {
|
|
9
|
+
const remaining = MAX_STDIN - raw.length;
|
|
10
|
+
raw += chunk.substring(0, remaining);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
process.stdin.on('end', () => {
|
|
14
|
+
process.stdout.write(raw);
|
|
15
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stop Hook (Session End) - Persist learnings during active sessions
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform (Windows, macOS, Linux)
|
|
6
|
+
*
|
|
7
|
+
* Runs on Stop events (after each response). Extracts a meaningful summary
|
|
8
|
+
* from the session transcript (via stdin JSON transcript_path) and updates a
|
|
9
|
+
* session file for cross-session continuity.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const {
|
|
15
|
+
getSessionsDir,
|
|
16
|
+
getDateString,
|
|
17
|
+
getTimeString,
|
|
18
|
+
getSessionIdShort,
|
|
19
|
+
ensureDir,
|
|
20
|
+
readFile,
|
|
21
|
+
writeFile,
|
|
22
|
+
replaceInFile,
|
|
23
|
+
log
|
|
24
|
+
} = require('../lib/utils');
|
|
25
|
+
|
|
26
|
+
const SUMMARY_START_MARKER = '<!-- ECC:SUMMARY:START -->';
|
|
27
|
+
const SUMMARY_END_MARKER = '<!-- ECC:SUMMARY:END -->';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract a meaningful summary from the session transcript.
|
|
31
|
+
* Reads the JSONL transcript and pulls out key information:
|
|
32
|
+
* - User messages (tasks requested)
|
|
33
|
+
* - Tools used
|
|
34
|
+
* - Files modified
|
|
35
|
+
*/
|
|
36
|
+
function extractSessionSummary(transcriptPath) {
|
|
37
|
+
const content = readFile(transcriptPath);
|
|
38
|
+
if (!content) return null;
|
|
39
|
+
|
|
40
|
+
const lines = content.split('\n').filter(Boolean);
|
|
41
|
+
const userMessages = [];
|
|
42
|
+
const toolsUsed = new Set();
|
|
43
|
+
const filesModified = new Set();
|
|
44
|
+
let parseErrors = 0;
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
try {
|
|
48
|
+
const entry = JSON.parse(line);
|
|
49
|
+
|
|
50
|
+
// Collect user messages (first 200 chars each)
|
|
51
|
+
if (entry.type === 'user' || entry.role === 'user' || entry.message?.role === 'user') {
|
|
52
|
+
// Support both direct content and nested message.content (Claude Code JSONL format)
|
|
53
|
+
const rawContent = entry.message?.content ?? entry.content;
|
|
54
|
+
const text = typeof rawContent === 'string'
|
|
55
|
+
? rawContent
|
|
56
|
+
: Array.isArray(rawContent)
|
|
57
|
+
? rawContent.map(c => (c && c.text) || '').join(' ')
|
|
58
|
+
: '';
|
|
59
|
+
if (text.trim()) {
|
|
60
|
+
userMessages.push(text.trim().slice(0, 200));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Collect tool names and modified files (direct tool_use entries)
|
|
65
|
+
if (entry.type === 'tool_use' || entry.tool_name) {
|
|
66
|
+
const toolName = entry.tool_name || entry.name || '';
|
|
67
|
+
if (toolName) toolsUsed.add(toolName);
|
|
68
|
+
|
|
69
|
+
const filePath = entry.tool_input?.file_path || entry.input?.file_path || '';
|
|
70
|
+
if (filePath && (toolName === 'Edit' || toolName === 'Write')) {
|
|
71
|
+
filesModified.add(filePath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Extract tool uses from assistant message content blocks (Claude Code JSONL format)
|
|
76
|
+
if (entry.type === 'assistant' && Array.isArray(entry.message?.content)) {
|
|
77
|
+
for (const block of entry.message.content) {
|
|
78
|
+
if (block.type === 'tool_use') {
|
|
79
|
+
const toolName = block.name || '';
|
|
80
|
+
if (toolName) toolsUsed.add(toolName);
|
|
81
|
+
|
|
82
|
+
const filePath = block.input?.file_path || '';
|
|
83
|
+
if (filePath && (toolName === 'Edit' || toolName === 'Write')) {
|
|
84
|
+
filesModified.add(filePath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
parseErrors++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (parseErrors > 0) {
|
|
95
|
+
log(`[SessionEnd] Skipped ${parseErrors}/${lines.length} unparseable transcript lines`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (userMessages.length === 0) return null;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
userMessages: userMessages.slice(-10), // Last 10 user messages
|
|
102
|
+
toolsUsed: Array.from(toolsUsed).slice(0, 20),
|
|
103
|
+
filesModified: Array.from(filesModified).slice(0, 30),
|
|
104
|
+
totalMessages: userMessages.length
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Read hook input from stdin (Claude Code provides transcript_path via stdin JSON)
|
|
109
|
+
const MAX_STDIN = 1024 * 1024;
|
|
110
|
+
let stdinData = '';
|
|
111
|
+
process.stdin.setEncoding('utf8');
|
|
112
|
+
|
|
113
|
+
process.stdin.on('data', chunk => {
|
|
114
|
+
if (stdinData.length < MAX_STDIN) {
|
|
115
|
+
const remaining = MAX_STDIN - stdinData.length;
|
|
116
|
+
stdinData += chunk.substring(0, remaining);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
process.stdin.on('end', () => {
|
|
121
|
+
runMain();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
function runMain() {
|
|
125
|
+
main().catch(err => {
|
|
126
|
+
console.error('[SessionEnd] Error:', err.message);
|
|
127
|
+
process.exit(0);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function main() {
|
|
132
|
+
// Parse stdin JSON to get transcript_path
|
|
133
|
+
let transcriptPath = null;
|
|
134
|
+
try {
|
|
135
|
+
const input = JSON.parse(stdinData);
|
|
136
|
+
transcriptPath = input.transcript_path;
|
|
137
|
+
} catch {
|
|
138
|
+
// Fallback: try env var for backwards compatibility
|
|
139
|
+
transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const sessionsDir = getSessionsDir();
|
|
143
|
+
const today = getDateString();
|
|
144
|
+
const shortId = getSessionIdShort();
|
|
145
|
+
const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`);
|
|
146
|
+
|
|
147
|
+
ensureDir(sessionsDir);
|
|
148
|
+
|
|
149
|
+
const currentTime = getTimeString();
|
|
150
|
+
|
|
151
|
+
// Try to extract summary from transcript
|
|
152
|
+
let summary = null;
|
|
153
|
+
|
|
154
|
+
if (transcriptPath) {
|
|
155
|
+
if (fs.existsSync(transcriptPath)) {
|
|
156
|
+
summary = extractSessionSummary(transcriptPath);
|
|
157
|
+
} else {
|
|
158
|
+
log(`[SessionEnd] Transcript not found: ${transcriptPath}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (fs.existsSync(sessionFile)) {
|
|
163
|
+
// Update existing session file
|
|
164
|
+
const updated = replaceInFile(
|
|
165
|
+
sessionFile,
|
|
166
|
+
/\*\*Last Updated:\*\*.*/,
|
|
167
|
+
`**Last Updated:** ${currentTime}`
|
|
168
|
+
);
|
|
169
|
+
if (!updated) {
|
|
170
|
+
log(`[SessionEnd] Failed to update timestamp in ${sessionFile}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// If we have a new summary, update only the generated summary block.
|
|
174
|
+
// This keeps repeated Stop invocations idempotent and preserves
|
|
175
|
+
// user-authored sections in the same session file.
|
|
176
|
+
if (summary) {
|
|
177
|
+
const existing = readFile(sessionFile);
|
|
178
|
+
if (existing) {
|
|
179
|
+
const summaryBlock = buildSummaryBlock(summary);
|
|
180
|
+
let updatedContent = existing;
|
|
181
|
+
|
|
182
|
+
if (existing.includes(SUMMARY_START_MARKER) && existing.includes(SUMMARY_END_MARKER)) {
|
|
183
|
+
updatedContent = existing.replace(
|
|
184
|
+
new RegExp(`${escapeRegExp(SUMMARY_START_MARKER)}[\\s\\S]*?${escapeRegExp(SUMMARY_END_MARKER)}`),
|
|
185
|
+
summaryBlock
|
|
186
|
+
);
|
|
187
|
+
} else {
|
|
188
|
+
// Migration path for files created before summary markers existed.
|
|
189
|
+
updatedContent = existing.replace(
|
|
190
|
+
/## (?:Session Summary|Current State)[\s\S]*?$/,
|
|
191
|
+
`${summaryBlock}\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\`\n`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
writeFile(sessionFile, updatedContent);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
log(`[SessionEnd] Updated session file: ${sessionFile}`);
|
|
200
|
+
} else {
|
|
201
|
+
// Create new session file
|
|
202
|
+
const summarySection = summary
|
|
203
|
+
? `${buildSummaryBlock(summary)}\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\``
|
|
204
|
+
: `## Current State\n\n[Session context goes here]\n\n### Completed\n- [ ]\n\n### In Progress\n- [ ]\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\``;
|
|
205
|
+
|
|
206
|
+
const template = `# Session: ${today}
|
|
207
|
+
**Date:** ${today}
|
|
208
|
+
**Started:** ${currentTime}
|
|
209
|
+
**Last Updated:** ${currentTime}
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
${summarySection}
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
writeFile(sessionFile, template);
|
|
217
|
+
log(`[SessionEnd] Created session file: ${sessionFile}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildSummarySection(summary) {
|
|
224
|
+
let section = '## Session Summary\n\n';
|
|
225
|
+
|
|
226
|
+
// Tasks (from user messages — collapse newlines and escape backticks to prevent markdown breaks)
|
|
227
|
+
section += '### Tasks\n';
|
|
228
|
+
for (const msg of summary.userMessages) {
|
|
229
|
+
section += `- ${msg.replace(/\n/g, ' ').replace(/`/g, '\\`')}\n`;
|
|
230
|
+
}
|
|
231
|
+
section += '\n';
|
|
232
|
+
|
|
233
|
+
// Files modified
|
|
234
|
+
if (summary.filesModified.length > 0) {
|
|
235
|
+
section += '### Files Modified\n';
|
|
236
|
+
for (const f of summary.filesModified) {
|
|
237
|
+
section += `- ${f}\n`;
|
|
238
|
+
}
|
|
239
|
+
section += '\n';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Tools used
|
|
243
|
+
if (summary.toolsUsed.length > 0) {
|
|
244
|
+
section += `### Tools Used\n${summary.toolsUsed.join(', ')}\n\n`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
section += `### Stats\n- Total user messages: ${summary.totalMessages}\n`;
|
|
248
|
+
|
|
249
|
+
return section;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function buildSummaryBlock(summary) {
|
|
253
|
+
return `${SUMMARY_START_MARKER}\n${buildSummarySection(summary).trim()}\n${SUMMARY_END_MARKER}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function escapeRegExp(value) {
|
|
257
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
258
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SessionStart Hook - Load previous context on new session
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform (Windows, macOS, Linux)
|
|
6
|
+
*
|
|
7
|
+
* Runs when a new Claude session starts. Loads the most recent session
|
|
8
|
+
* summary into Claude's context via stdout, and reports available
|
|
9
|
+
* sessions and learned skills.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
getSessionsDir,
|
|
14
|
+
getLearnedSkillsDir,
|
|
15
|
+
findFiles,
|
|
16
|
+
ensureDir,
|
|
17
|
+
readFile,
|
|
18
|
+
log,
|
|
19
|
+
output
|
|
20
|
+
} = require('../lib/utils');
|
|
21
|
+
const { getPackageManager, getSelectionPrompt } = require('../lib/package-manager');
|
|
22
|
+
const { listAliases } = require('../lib/session-aliases');
|
|
23
|
+
const { detectProjectType } = require('../lib/project-detect');
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
const sessionsDir = getSessionsDir();
|
|
27
|
+
const learnedDir = getLearnedSkillsDir();
|
|
28
|
+
|
|
29
|
+
// Ensure directories exist
|
|
30
|
+
ensureDir(sessionsDir);
|
|
31
|
+
ensureDir(learnedDir);
|
|
32
|
+
|
|
33
|
+
// Check for recent session files (last 7 days)
|
|
34
|
+
const recentSessions = findFiles(sessionsDir, '*-session.tmp', { maxAge: 7 });
|
|
35
|
+
|
|
36
|
+
if (recentSessions.length > 0) {
|
|
37
|
+
const latest = recentSessions[0];
|
|
38
|
+
log(`[SessionStart] Found ${recentSessions.length} recent session(s)`);
|
|
39
|
+
log(`[SessionStart] Latest: ${latest.path}`);
|
|
40
|
+
|
|
41
|
+
// Read and inject the latest session content into Claude's context
|
|
42
|
+
const content = readFile(latest.path);
|
|
43
|
+
if (content && !content.includes('[Session context goes here]')) {
|
|
44
|
+
// Only inject if the session has actual content (not the blank template)
|
|
45
|
+
output(`Previous session summary:\n${content}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for learned skills
|
|
50
|
+
const learnedSkills = findFiles(learnedDir, '*.md');
|
|
51
|
+
|
|
52
|
+
if (learnedSkills.length > 0) {
|
|
53
|
+
log(`[SessionStart] ${learnedSkills.length} learned skill(s) available in ${learnedDir}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check for available session aliases
|
|
57
|
+
const aliases = listAliases({ limit: 5 });
|
|
58
|
+
|
|
59
|
+
if (aliases.length > 0) {
|
|
60
|
+
const aliasNames = aliases.map(a => a.name).join(', ');
|
|
61
|
+
log(`[SessionStart] ${aliases.length} session alias(es) available: ${aliasNames}`);
|
|
62
|
+
log(`[SessionStart] Use /sessions load <alias> to continue a previous session`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Detect and report package manager
|
|
66
|
+
const pm = getPackageManager();
|
|
67
|
+
log(`[SessionStart] Package manager: ${pm.name} (${pm.source})`);
|
|
68
|
+
|
|
69
|
+
// If no explicit package manager config was found, show selection prompt
|
|
70
|
+
if (pm.source === 'default') {
|
|
71
|
+
log('[SessionStart] No package manager preference found.');
|
|
72
|
+
log(getSelectionPrompt());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Detect project type and frameworks (#293)
|
|
76
|
+
const projectInfo = detectProjectType();
|
|
77
|
+
if (projectInfo.languages.length > 0 || projectInfo.frameworks.length > 0) {
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (projectInfo.languages.length > 0) {
|
|
80
|
+
parts.push(`languages: ${projectInfo.languages.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
if (projectInfo.frameworks.length > 0) {
|
|
83
|
+
parts.push(`frameworks: ${projectInfo.frameworks.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
log(`[SessionStart] Project detected — ${parts.join('; ')}`);
|
|
86
|
+
output(`Project type: ${JSON.stringify(projectInfo)}`);
|
|
87
|
+
} else {
|
|
88
|
+
log('[SessionStart] No specific project type detected');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main().catch(err => {
|
|
95
|
+
console.error('[SessionStart] Error:', err.message);
|
|
96
|
+
process.exit(0); // Don't block on errors
|
|
97
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Strategic Compact Suggester
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform (Windows, macOS, Linux)
|
|
6
|
+
*
|
|
7
|
+
* Runs on PreToolUse or periodically to suggest manual compaction at logical intervals
|
|
8
|
+
*
|
|
9
|
+
* Why manual over auto-compact:
|
|
10
|
+
* - Auto-compact happens at arbitrary points, often mid-task
|
|
11
|
+
* - Strategic compacting preserves context through logical phases
|
|
12
|
+
* - Compact after exploration, before execution
|
|
13
|
+
* - Compact after completing a milestone, before starting next
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const {
|
|
19
|
+
getTempDir,
|
|
20
|
+
writeFile,
|
|
21
|
+
log
|
|
22
|
+
} = require('../lib/utils');
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
// Track tool call count (increment in a temp file)
|
|
26
|
+
// Use a session-specific counter file based on session ID from environment
|
|
27
|
+
// or parent PID as fallback
|
|
28
|
+
const sessionId = (process.env.CLAUDE_SESSION_ID || 'default').replace(/[^a-zA-Z0-9_-]/g, '') || 'default';
|
|
29
|
+
const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`);
|
|
30
|
+
const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
|
|
31
|
+
const threshold = Number.isFinite(rawThreshold) && rawThreshold > 0 && rawThreshold <= 10000
|
|
32
|
+
? rawThreshold
|
|
33
|
+
: 50;
|
|
34
|
+
|
|
35
|
+
let count = 1;
|
|
36
|
+
|
|
37
|
+
// Read existing count or start at 1
|
|
38
|
+
// Use fd-based read+write to reduce (but not eliminate) race window
|
|
39
|
+
// between concurrent hook invocations
|
|
40
|
+
try {
|
|
41
|
+
const fd = fs.openSync(counterFile, 'a+');
|
|
42
|
+
try {
|
|
43
|
+
const buf = Buffer.alloc(64);
|
|
44
|
+
const bytesRead = fs.readSync(fd, buf, 0, 64, 0);
|
|
45
|
+
if (bytesRead > 0) {
|
|
46
|
+
const parsed = parseInt(buf.toString('utf8', 0, bytesRead).trim(), 10);
|
|
47
|
+
// Clamp to reasonable range — corrupted files could contain huge values
|
|
48
|
+
// that pass Number.isFinite() (e.g., parseInt('9'.repeat(30)) => 1e+29)
|
|
49
|
+
count = (Number.isFinite(parsed) && parsed > 0 && parsed <= 1000000)
|
|
50
|
+
? parsed + 1
|
|
51
|
+
: 1;
|
|
52
|
+
}
|
|
53
|
+
// Truncate and write new value
|
|
54
|
+
fs.ftruncateSync(fd, 0);
|
|
55
|
+
fs.writeSync(fd, String(count), 0);
|
|
56
|
+
} finally {
|
|
57
|
+
fs.closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Fallback: just use writeFile if fd operations fail
|
|
61
|
+
writeFile(counterFile, String(count));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Suggest compact after threshold tool calls
|
|
65
|
+
if (count === threshold) {
|
|
66
|
+
log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Suggest at regular intervals after threshold (every 25 calls from threshold)
|
|
70
|
+
if (count > threshold && (count - threshold) % 25 === 0) {
|
|
71
|
+
log(`[StrategicCompact] ${count} tool calls - good checkpoint for /compact if context is stale`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
main().catch(err => {
|
|
78
|
+
console.error('[StrategicCompact] Error:', err.message);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
});
|