feat: cross-platform support with Node.js scripts

- Rewrite all bash hooks to Node.js for Windows/macOS/Linux compatibility
- Add package manager auto-detection (npm, pnpm, yarn, bun)
- Add scripts/lib/ with cross-platform utilities
- Add /setup-pm command for package manager configuration
- Add comprehensive test suite (62 tests)

Co-authored-by: zerx-lab
This commit is contained in:
zerx-lab
2026-01-23 15:08:07 +08:00
committed by GitHub
parent 4ec7a6b15a
commit 970f8bf884
16 changed files with 2443 additions and 17 deletions

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env node
/**
* Continuous Learning - Session Evaluator
*
* Cross-platform (Windows, macOS, Linux)
*
* Runs on Stop hook to extract reusable patterns from Claude Code sessions
*
* Why Stop hook instead of UserPromptSubmit:
* - Stop runs once at session end (lightweight)
* - UserPromptSubmit runs every message (heavy, adds latency)
*/
const path = require('path');
const fs = require('fs');
const {
getLearnedSkillsDir,
ensureDir,
readFile,
countInFile,
log
} = require('../lib/utils');
async function main() {
// Get script directory to find config
const scriptDir = __dirname;
const configFile = path.join(scriptDir, '..', '..', 'skills', 'continuous-learning', 'config.json');
// Default configuration
let minSessionLength = 10;
let learnedSkillsPath = getLearnedSkillsDir();
// Load config if exists
const configContent = readFile(configFile);
if (configContent) {
try {
const config = JSON.parse(configContent);
minSessionLength = config.min_session_length || 10;
if (config.learned_skills_path) {
// Handle ~ in path
learnedSkillsPath = config.learned_skills_path.replace(/^~/, require('os').homedir());
}
} catch {
// Invalid config, use defaults
}
}
// Ensure learned skills directory exists
ensureDir(learnedSkillsPath);
// Get transcript path from environment (set by Claude Code)
const transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH;
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
process.exit(0);
}
// Count user messages in session
const messageCount = countInFile(transcriptPath, /"type":"user"/g);
// Skip short sessions
if (messageCount < minSessionLength) {
log(`[ContinuousLearning] Session too short (${messageCount} messages), skipping`);
process.exit(0);
}
// Signal to Claude that session should be evaluated for extractable patterns
log(`[ContinuousLearning] Session has ${messageCount} messages - evaluate for extractable patterns`);
log(`[ContinuousLearning] Save learned skills to: ${learnedSkillsPath}`);
process.exit(0);
}
main().catch(err => {
console.error('[ContinuousLearning] Error:', err.message);
process.exit(0);
});

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env node
/**
* PreCompact Hook - Save state before context compaction
*
* Cross-platform (Windows, macOS, Linux)
*
* Runs before Claude compacts context, giving you a chance to
* preserve important state that might get lost in summarization.
*/
const path = require('path');
const {
getSessionsDir,
getDateTimeString,
getTimeString,
findFiles,
ensureDir,
appendFile,
log
} = require('../lib/utils');
async function main() {
const sessionsDir = getSessionsDir();
const compactionLog = path.join(sessionsDir, 'compaction-log.txt');
ensureDir(sessionsDir);
// Log compaction event with timestamp
const timestamp = getDateTimeString();
appendFile(compactionLog, `[${timestamp}] Context compaction triggered\n`);
// If there's an active session file, note the compaction
const sessions = findFiles(sessionsDir, '*.tmp');
if (sessions.length > 0) {
const activeSession = sessions[0].path;
const timeStr = getTimeString();
appendFile(activeSession, `\n---\n**[Compaction occurred at ${timeStr}]** - Context was summarized\n`);
}
log('[PreCompact] State saved before compaction');
process.exit(0);
}
main().catch(err => {
console.error('[PreCompact] Error:', err.message);
process.exit(0);
});

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env node
/**
* Stop Hook (Session End) - Persist learnings when session ends
*
* Cross-platform (Windows, macOS, Linux)
*
* Runs when Claude session ends. Creates/updates session log file
* with timestamp for continuity tracking.
*/
const path = require('path');
const fs = require('fs');
const {
getSessionsDir,
getDateString,
getTimeString,
ensureDir,
readFile,
writeFile,
replaceInFile,
log
} = require('../lib/utils');
async function main() {
const sessionsDir = getSessionsDir();
const today = getDateString();
const sessionFile = path.join(sessionsDir, `${today}-session.tmp`);
ensureDir(sessionsDir);
const currentTime = getTimeString();
// If session file exists for today, update the end time
if (fs.existsSync(sessionFile)) {
const success = replaceInFile(
sessionFile,
/\*\*Last Updated:\*\*.*/,
`**Last Updated:** ${currentTime}`
);
if (success) {
log(`[SessionEnd] Updated session file: ${sessionFile}`);
}
} else {
// Create new session file with template
const template = `# Session: ${today}
**Date:** ${today}
**Started:** ${currentTime}
**Last Updated:** ${currentTime}
---
## Current State
[Session context goes here]
### Completed
- [ ]
### In Progress
- [ ]
### Notes for Next Session
-
### Context to Load
\`\`\`
[relevant files]
\`\`\`
`;
writeFile(sessionFile, template);
log(`[SessionEnd] Created session file: ${sessionFile}`);
}
process.exit(0);
}
main().catch(err => {
console.error('[SessionEnd] Error:', err.message);
process.exit(0);
});

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env node
/**
* SessionStart Hook - Load previous context on new session
*
* Cross-platform (Windows, macOS, Linux)
*
* Runs when a new Claude session starts. Checks for recent session
* files and notifies Claude of available context to load.
*/
const path = require('path');
const {
getSessionsDir,
getLearnedSkillsDir,
findFiles,
ensureDir,
log
} = require('../lib/utils');
const { getPackageManager, getSelectionPrompt } = require('../lib/package-manager');
async function main() {
const sessionsDir = getSessionsDir();
const learnedDir = getLearnedSkillsDir();
// Ensure directories exist
ensureDir(sessionsDir);
ensureDir(learnedDir);
// Check for recent session files (last 7 days)
const recentSessions = findFiles(sessionsDir, '*.tmp', { maxAge: 7 });
if (recentSessions.length > 0) {
const latest = recentSessions[0];
log(`[SessionStart] Found ${recentSessions.length} recent session(s)`);
log(`[SessionStart] Latest: ${latest.path}`);
}
// Check for learned skills
const learnedSkills = findFiles(learnedDir, '*.md');
if (learnedSkills.length > 0) {
log(`[SessionStart] ${learnedSkills.length} learned skill(s) available in ${learnedDir}`);
}
// Detect and report package manager
const pm = getPackageManager();
log(`[SessionStart] Package manager: ${pm.name} (${pm.source})`);
// If package manager was detected via fallback, show selection prompt
if (pm.source === 'fallback' || pm.source === 'default') {
log('[SessionStart] No package manager preference found.');
log(getSelectionPrompt());
}
process.exit(0);
}
main().catch(err => {
console.error('[SessionStart] Error:', err.message);
process.exit(0); // Don't block on errors
});

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env node
/**
* Strategic Compact Suggester
*
* Cross-platform (Windows, macOS, Linux)
*
* Runs on PreToolUse or periodically to suggest manual compaction at logical intervals
*
* Why manual over auto-compact:
* - Auto-compact happens at arbitrary points, often mid-task
* - Strategic compacting preserves context through logical phases
* - Compact after exploration, before execution
* - Compact after completing a milestone, before starting next
*/
const path = require('path');
const fs = require('fs');
const {
getTempDir,
readFile,
writeFile,
log
} = require('../lib/utils');
async function main() {
// Track tool call count (increment in a temp file)
// Use a session-specific counter file based on PID from parent process
// or session ID from environment
const sessionId = process.env.CLAUDE_SESSION_ID || process.ppid || 'default';
const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`);
const threshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
let count = 1;
// Read existing count or start at 1
const existing = readFile(counterFile);
if (existing) {
count = parseInt(existing.trim(), 10) + 1;
}
// Save updated count
writeFile(counterFile, String(count));
// Suggest compact after threshold tool calls
if (count === threshold) {
log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`);
}
// Suggest at regular intervals after threshold
if (count > threshold && count % 25 === 0) {
log(`[StrategicCompact] ${count} tool calls - good checkpoint for /compact if context is stale`);
}
process.exit(0);
}
main().catch(err => {
console.error('[StrategicCompact] Error:', err.message);
process.exit(0);
});