claude-cli-advanced-starter-pack 1.1.0 → 1.8.1
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/OVERVIEW.md +5 -1
- package/README.md +241 -132
- package/bin/gtask.js +53 -0
- package/package.json +1 -1
- package/src/cli/menu.js +27 -0
- package/src/commands/explore-mcp/mcp-registry.js +99 -0
- package/src/commands/init.js +339 -351
- package/src/commands/install-panel-hook.js +108 -0
- package/src/commands/install-scripts.js +232 -0
- package/src/commands/install-skill.js +220 -0
- package/src/commands/panel.js +297 -0
- package/src/commands/setup-wizard.js +4 -3
- package/src/commands/test-setup.js +4 -5
- package/src/data/releases.json +164 -0
- package/src/panel/queue.js +188 -0
- package/templates/commands/ask-claude.template.md +118 -0
- package/templates/commands/ccasp-panel.template.md +72 -0
- package/templates/commands/ccasp-setup.template.md +470 -79
- package/templates/commands/create-smoke-test.template.md +186 -0
- package/templates/commands/project-impl.template.md +9 -113
- package/templates/commands/refactor-check.template.md +112 -0
- package/templates/commands/refactor-cleanup.template.md +144 -0
- package/templates/commands/refactor-prep.template.md +192 -0
- package/templates/docs/AI_ARCHITECTURE_CONSTITUTION.template.md +198 -0
- package/templates/docs/DETAILED_GOTCHAS.template.md +347 -0
- package/templates/docs/PHASE-DEV-CHECKLIST.template.md +241 -0
- package/templates/docs/PROGRESS_JSON_TEMPLATE.json +117 -0
- package/templates/docs/background-agent.template.md +264 -0
- package/templates/hooks/autonomous-decision-logger.template.js +207 -0
- package/templates/hooks/branch-merge-checker.template.js +272 -0
- package/templates/hooks/git-commit-tracker.template.js +267 -0
- package/templates/hooks/issue-completion-detector.template.js +205 -0
- package/templates/hooks/panel-queue-reader.template.js +83 -0
- package/templates/hooks/phase-validation-gates.template.js +307 -0
- package/templates/hooks/session-id-generator.template.js +236 -0
- package/templates/hooks/token-usage-monitor.template.js +193 -0
- package/templates/patterns/README.md +129 -0
- package/templates/patterns/l1-l2-orchestration.md +189 -0
- package/templates/patterns/multi-phase-orchestration.md +258 -0
- package/templates/patterns/two-tier-query-pipeline.md +192 -0
- package/templates/scripts/README.md +109 -0
- package/templates/scripts/analyze-delegation-log.js +299 -0
- package/templates/scripts/autonomous-decision-logger.js +277 -0
- package/templates/scripts/git-history-analyzer.py +269 -0
- package/templates/scripts/phase-validation-gates.js +307 -0
- package/templates/scripts/poll-deployment-status.js +260 -0
- package/templates/scripts/roadmap-scanner.js +263 -0
- package/templates/scripts/validate-deployment.js +293 -0
- package/templates/skills/agent-creator/skill.json +18 -0
- package/templates/skills/agent-creator/skill.md +335 -0
- package/templates/skills/hook-creator/skill.json +18 -0
- package/templates/skills/hook-creator/skill.md +318 -0
- package/templates/skills/panel/skill.json +18 -0
- package/templates/skills/panel/skill.md +90 -0
- package/templates/skills/rag-agent-creator/skill.json +18 -0
- package/templates/skills/rag-agent-creator/skill.md +307 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Phase Validation Gates Hook
|
|
4
|
+
*
|
|
5
|
+
* 5-gate validation system for phased development.
|
|
6
|
+
* Ensures quality standards are met before auto-chaining to next phase.
|
|
7
|
+
*
|
|
8
|
+
* Event: PostToolUse (custom trigger)
|
|
9
|
+
* Trigger: Phase completion markers
|
|
10
|
+
*
|
|
11
|
+
* Configuration: Reads from .claude/config/hooks-config.json
|
|
12
|
+
*
|
|
13
|
+
* Gates:
|
|
14
|
+
* 1. Tasks Complete - All phase tasks marked done
|
|
15
|
+
* 2. Files Created - Expected output files exist
|
|
16
|
+
* 3. Tests Passing - Required tests pass (if configured)
|
|
17
|
+
* 4. No Errors - No blocking errors detected
|
|
18
|
+
* 5. Token Budget - Within configured token limits
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// Default configuration
|
|
25
|
+
const DEFAULT_CONFIG = {
|
|
26
|
+
enabled: true,
|
|
27
|
+
gates: {
|
|
28
|
+
tasks_complete: { enabled: true, required: true },
|
|
29
|
+
files_created: { enabled: true, required: false },
|
|
30
|
+
tests_passing: { enabled: true, required: false },
|
|
31
|
+
no_errors: { enabled: true, required: true },
|
|
32
|
+
token_budget: { enabled: true, required: false },
|
|
33
|
+
},
|
|
34
|
+
auto_chain_on_pass: false, // Automatically start next phase
|
|
35
|
+
report_failures: true, // Log detailed failure reports
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Paths
|
|
39
|
+
const CONFIG_PATH = path.join(process.cwd(), '.claude', 'config', 'hooks-config.json');
|
|
40
|
+
const STATE_FILE = path.join(process.cwd(), '.claude', 'hooks', 'config', 'phase-validation-state.json');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load configuration
|
|
44
|
+
*/
|
|
45
|
+
function loadConfig() {
|
|
46
|
+
try {
|
|
47
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
48
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
49
|
+
return { ...DEFAULT_CONFIG, ...(config.phase_validation || {}) };
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Use defaults
|
|
53
|
+
}
|
|
54
|
+
return DEFAULT_CONFIG;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load validation state
|
|
59
|
+
*/
|
|
60
|
+
function loadState() {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
63
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// Use defaults
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
last_validation: null,
|
|
70
|
+
gate_results: {},
|
|
71
|
+
phases_validated: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Save validation state
|
|
77
|
+
*/
|
|
78
|
+
function saveState(state) {
|
|
79
|
+
try {
|
|
80
|
+
const dir = path.dirname(STATE_FILE);
|
|
81
|
+
if (!fs.existsSync(dir)) {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error(`[phase-validation] Error saving state: ${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Gate 1: Check if all tasks are complete
|
|
92
|
+
*/
|
|
93
|
+
async function checkTasksComplete(phase, result) {
|
|
94
|
+
const tasks = phase.tasks || [];
|
|
95
|
+
const completed = tasks.filter(t => t.status === 'completed');
|
|
96
|
+
|
|
97
|
+
const passed = completed.length >= tasks.length;
|
|
98
|
+
const message = passed
|
|
99
|
+
? `All tasks complete (${tasks.length}/${tasks.length})`
|
|
100
|
+
: `${tasks.length - completed.length} tasks incomplete`;
|
|
101
|
+
|
|
102
|
+
return { passed, message, completed: completed.length, total: tasks.length };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Gate 2: Check if expected files were created
|
|
107
|
+
*/
|
|
108
|
+
async function checkFilesCreated(phase, result) {
|
|
109
|
+
const expectedFiles = phase.expectedOutputFiles || [];
|
|
110
|
+
const actualFiles = result?.filesCreated || [];
|
|
111
|
+
|
|
112
|
+
if (expectedFiles.length === 0) {
|
|
113
|
+
return { passed: true, message: 'No expected files specified', expected: 0, found: 0 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const missingFiles = expectedFiles.filter(file => {
|
|
117
|
+
const fullPath = path.resolve(file);
|
|
118
|
+
return !fs.existsSync(fullPath) && !actualFiles.includes(file);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const passed = missingFiles.length === 0;
|
|
122
|
+
const message = passed
|
|
123
|
+
? `All expected files created (${expectedFiles.length})`
|
|
124
|
+
: `Missing files: ${missingFiles.slice(0, 3).join(', ')}${missingFiles.length > 3 ? '...' : ''}`;
|
|
125
|
+
|
|
126
|
+
return { passed, message, expected: expectedFiles.length, missing: missingFiles };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Gate 3: Check if tests are passing
|
|
131
|
+
*/
|
|
132
|
+
async function checkTestsPassing(phase, result) {
|
|
133
|
+
if (!phase.requireTests) {
|
|
134
|
+
return { passed: true, message: 'Tests not required for this phase' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const testResults = result?.testResults || { passed: 0, failed: 0, total: 0 };
|
|
138
|
+
|
|
139
|
+
if (testResults.total === 0) {
|
|
140
|
+
return { passed: false, message: 'No tests executed (requireTests=true)', testResults };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const passed = testResults.failed === 0;
|
|
144
|
+
const message = passed
|
|
145
|
+
? `All tests passing (${testResults.passed}/${testResults.total})`
|
|
146
|
+
: `${testResults.failed} tests failed`;
|
|
147
|
+
|
|
148
|
+
return { passed, message, testResults };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Gate 4: Check for errors
|
|
153
|
+
*/
|
|
154
|
+
async function checkNoErrors(phase, result) {
|
|
155
|
+
const errors = result?.errors || [];
|
|
156
|
+
|
|
157
|
+
const passed = errors.length === 0;
|
|
158
|
+
const message = passed
|
|
159
|
+
? 'No errors detected'
|
|
160
|
+
: `${errors.length} error(s) detected`;
|
|
161
|
+
|
|
162
|
+
return { passed, message, errors: errors.slice(0, 5) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Gate 5: Check token budget
|
|
167
|
+
*/
|
|
168
|
+
async function checkTokenBudget(phase, result) {
|
|
169
|
+
const budget = phase.tokenBudget || Infinity;
|
|
170
|
+
const used = result?.tokens || 0;
|
|
171
|
+
|
|
172
|
+
if (budget === Infinity) {
|
|
173
|
+
return { passed: true, message: `No token budget specified (used: ${used.toLocaleString()})`, used };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const passed = used <= budget;
|
|
177
|
+
const percentage = Math.round((used / budget) * 100);
|
|
178
|
+
const message = passed
|
|
179
|
+
? `Within budget: ${used.toLocaleString()}/${budget.toLocaleString()} (${percentage}%)`
|
|
180
|
+
: `Budget exceeded: ${used.toLocaleString()}/${budget.toLocaleString()} (${percentage}%)`;
|
|
181
|
+
|
|
182
|
+
return { passed, message, used, budget, percentage };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Run all validation gates
|
|
187
|
+
*/
|
|
188
|
+
async function validatePhase(phase, result, config) {
|
|
189
|
+
const gateConfig = config.gates;
|
|
190
|
+
const gateResults = {};
|
|
191
|
+
let allPassed = true;
|
|
192
|
+
let requiredPassed = true;
|
|
193
|
+
|
|
194
|
+
const gates = [
|
|
195
|
+
{ id: 'tasks_complete', name: 'Tasks Complete', fn: checkTasksComplete },
|
|
196
|
+
{ id: 'files_created', name: 'Files Created', fn: checkFilesCreated },
|
|
197
|
+
{ id: 'tests_passing', name: 'Tests Passing', fn: checkTestsPassing },
|
|
198
|
+
{ id: 'no_errors', name: 'No Errors', fn: checkNoErrors },
|
|
199
|
+
{ id: 'token_budget', name: 'Token Budget', fn: checkTokenBudget },
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
console.log(`\n[phase-validation] Running validation gates for Phase ${phase.phase_number || '?'}`);
|
|
203
|
+
console.log('─'.repeat(60));
|
|
204
|
+
|
|
205
|
+
for (const gate of gates) {
|
|
206
|
+
const gateSettings = gateConfig[gate.id] || { enabled: true, required: false };
|
|
207
|
+
|
|
208
|
+
if (!gateSettings.enabled) {
|
|
209
|
+
gateResults[gate.id] = { skipped: true, message: 'Gate disabled' };
|
|
210
|
+
console.log(`⏭️ ${gate.name}: Skipped (disabled)`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const gateResult = await gate.fn(phase, result);
|
|
215
|
+
gateResults[gate.id] = gateResult;
|
|
216
|
+
|
|
217
|
+
if (gateResult.passed) {
|
|
218
|
+
console.log(`✅ ${gate.name}: ${gateResult.message}`);
|
|
219
|
+
} else {
|
|
220
|
+
console.log(`❌ ${gate.name}: ${gateResult.message}`);
|
|
221
|
+
allPassed = false;
|
|
222
|
+
if (gateSettings.required) {
|
|
223
|
+
requiredPassed = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log('─'.repeat(60));
|
|
229
|
+
|
|
230
|
+
if (allPassed) {
|
|
231
|
+
console.log('✅ All gates passed\n');
|
|
232
|
+
} else if (requiredPassed) {
|
|
233
|
+
console.log('⚠️ Some optional gates failed (required gates passed)\n');
|
|
234
|
+
} else {
|
|
235
|
+
console.log('❌ Required gates failed\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
allPassed,
|
|
240
|
+
requiredPassed,
|
|
241
|
+
gateResults,
|
|
242
|
+
phase_number: phase.phase_number,
|
|
243
|
+
timestamp: new Date().toISOString(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Main hook handler
|
|
249
|
+
*/
|
|
250
|
+
module.exports = async function phaseValidationGates(context) {
|
|
251
|
+
const approve = () => ({ continue: true });
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const config = loadConfig();
|
|
255
|
+
|
|
256
|
+
if (!config.enabled) {
|
|
257
|
+
return approve();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// This hook is typically called programmatically with phase data
|
|
261
|
+
// For now, return approve and let the caller use validatePhase directly
|
|
262
|
+
return approve();
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(`[phase-validation] Error: ${error.message}`);
|
|
265
|
+
return approve();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Export validation functions for programmatic use
|
|
270
|
+
module.exports.validatePhase = validatePhase;
|
|
271
|
+
module.exports.gates = {
|
|
272
|
+
checkTasksComplete,
|
|
273
|
+
checkFilesCreated,
|
|
274
|
+
checkTestsPassing,
|
|
275
|
+
checkNoErrors,
|
|
276
|
+
checkTokenBudget,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Direct execution support
|
|
280
|
+
if (require.main === module) {
|
|
281
|
+
const config = loadConfig();
|
|
282
|
+
|
|
283
|
+
// Example phase for testing
|
|
284
|
+
const testPhase = {
|
|
285
|
+
phase_number: 1,
|
|
286
|
+
tasks: [
|
|
287
|
+
{ id: 'T1', status: 'completed' },
|
|
288
|
+
{ id: 'T2', status: 'completed' },
|
|
289
|
+
{ id: 'T3', status: 'pending' },
|
|
290
|
+
],
|
|
291
|
+
expectedOutputFiles: [],
|
|
292
|
+
requireTests: false,
|
|
293
|
+
tokenBudget: 50000,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const testResult = {
|
|
297
|
+
tokens: 25000,
|
|
298
|
+
errors: [],
|
|
299
|
+
filesCreated: [],
|
|
300
|
+
testResults: { passed: 0, failed: 0, total: 0 },
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
validatePhase(testPhase, testResult, config).then(result => {
|
|
304
|
+
console.log('\nValidation Result:');
|
|
305
|
+
console.log(JSON.stringify(result, null, 2));
|
|
306
|
+
});
|
|
307
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Session ID Generator Hook
|
|
4
|
+
*
|
|
5
|
+
* Generates unique session IDs with PID-keyed registry for parallel session isolation.
|
|
6
|
+
* Enables multiple Claude Code sessions without interference.
|
|
7
|
+
*
|
|
8
|
+
* Event: SessionStart
|
|
9
|
+
*
|
|
10
|
+
* Configuration: Reads from .claude/config/hooks-config.json
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - UUID session generation
|
|
14
|
+
* - PID-keyed registry (handles PID reuse)
|
|
15
|
+
* - Stale session cleanup
|
|
16
|
+
* - Cross-platform process detection
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
|
|
23
|
+
// Default configuration
|
|
24
|
+
const DEFAULT_CONFIG = {
|
|
25
|
+
enabled: true,
|
|
26
|
+
retention_hours: 24, // Keep session records for 24 hours
|
|
27
|
+
cleanup_on_start: true, // Clean stale sessions on startup
|
|
28
|
+
registry_enabled: true, // Enable PID-keyed registry
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Paths
|
|
32
|
+
const CONFIG_PATH = path.join(process.cwd(), '.claude', 'config', 'hooks-config.json');
|
|
33
|
+
const CONFIG_DIR = path.join(process.cwd(), '.claude', 'hooks', 'config');
|
|
34
|
+
const SESSION_FILE = path.join(CONFIG_DIR, 'current-session.json');
|
|
35
|
+
const REGISTRY_FILE = path.join(CONFIG_DIR, 'session-registry.json');
|
|
36
|
+
const SESSIONS_DIR = path.join(process.cwd(), '.claude', 'sessions');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load configuration
|
|
40
|
+
*/
|
|
41
|
+
function loadConfig() {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
44
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
45
|
+
return { ...DEFAULT_CONFIG, ...(config.session_generator || {}) };
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// Use defaults
|
|
49
|
+
}
|
|
50
|
+
return DEFAULT_CONFIG;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a process is still running (cross-platform)
|
|
55
|
+
*/
|
|
56
|
+
function isProcessRunning(pid) {
|
|
57
|
+
try {
|
|
58
|
+
process.kill(pid, 0);
|
|
59
|
+
return true;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// EPERM means process exists but we don't have permission
|
|
62
|
+
// ESRCH means process doesn't exist
|
|
63
|
+
return error.code === 'EPERM';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Clean stale entries from the session registry
|
|
69
|
+
*/
|
|
70
|
+
function cleanStaleRegistryEntries(registry) {
|
|
71
|
+
const cleaned = {};
|
|
72
|
+
for (const [key, data] of Object.entries(registry)) {
|
|
73
|
+
if (isProcessRunning(data.pid)) {
|
|
74
|
+
cleaned[key] = data;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return cleaned;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read the session registry
|
|
82
|
+
*/
|
|
83
|
+
function readRegistry() {
|
|
84
|
+
try {
|
|
85
|
+
if (fs.existsSync(REGISTRY_FILE)) {
|
|
86
|
+
return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf8'));
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// Registry corrupt or unreadable - start fresh
|
|
90
|
+
}
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Write the session registry atomically
|
|
96
|
+
*/
|
|
97
|
+
function writeRegistry(registry) {
|
|
98
|
+
const tempFile = REGISTRY_FILE + '.tmp';
|
|
99
|
+
try {
|
|
100
|
+
fs.writeFileSync(tempFile, JSON.stringify(registry, null, 2));
|
|
101
|
+
fs.renameSync(tempFile, REGISTRY_FILE);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// Fallback: direct write if rename fails (Windows)
|
|
104
|
+
try {
|
|
105
|
+
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2));
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// Silent fail - registry is not critical
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Cleanup old session files
|
|
114
|
+
*/
|
|
115
|
+
function cleanupOldSessions(config) {
|
|
116
|
+
const retentionMs = config.retention_hours * 60 * 60 * 1000;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
if (!fs.existsSync(SESSIONS_DIR)) return;
|
|
120
|
+
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const files = fs.readdirSync(SESSIONS_DIR);
|
|
123
|
+
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
if (!file.endsWith('-task.json') && !file.endsWith('-session.json')) continue;
|
|
126
|
+
|
|
127
|
+
const filePath = path.join(SESSIONS_DIR, file);
|
|
128
|
+
const stat = fs.statSync(filePath);
|
|
129
|
+
|
|
130
|
+
if (now - stat.mtimeMs > retentionMs) {
|
|
131
|
+
fs.unlinkSync(filePath);
|
|
132
|
+
console.log(`[session-generator] Cleaned old session: ${file}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// Silent fail - cleanup is non-critical
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Main hook handler
|
|
142
|
+
*/
|
|
143
|
+
module.exports = async function sessionIdGenerator(context) {
|
|
144
|
+
const approve = () => ({ continue: true });
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const config = loadConfig();
|
|
148
|
+
|
|
149
|
+
if (!config.enabled) {
|
|
150
|
+
return approve();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Cleanup on start if enabled
|
|
154
|
+
if (config.cleanup_on_start) {
|
|
155
|
+
cleanupOldSessions(config);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Generate UUID for this session
|
|
159
|
+
const sessionId = crypto.randomUUID();
|
|
160
|
+
const startTime = Date.now();
|
|
161
|
+
const pid = process.pid;
|
|
162
|
+
|
|
163
|
+
// Create session data
|
|
164
|
+
const sessionData = {
|
|
165
|
+
session_id: sessionId,
|
|
166
|
+
pid: pid,
|
|
167
|
+
start_time: startTime,
|
|
168
|
+
started_at: new Date().toISOString(),
|
|
169
|
+
cwd: process.cwd(),
|
|
170
|
+
node_version: process.version,
|
|
171
|
+
platform: process.platform,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Ensure config directory exists
|
|
175
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
176
|
+
|
|
177
|
+
// Update session registry if enabled
|
|
178
|
+
if (config.registry_enabled) {
|
|
179
|
+
let registry = readRegistry();
|
|
180
|
+
registry = cleanStaleRegistryEntries(registry);
|
|
181
|
+
|
|
182
|
+
// Create composite key: pid-startTime (handles PID reuse)
|
|
183
|
+
const registryKey = `${pid}-${startTime}`;
|
|
184
|
+
registry[registryKey] = sessionData;
|
|
185
|
+
|
|
186
|
+
writeRegistry(registry);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Write current-session.json (backwards compatibility)
|
|
190
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(sessionData, null, 2));
|
|
191
|
+
|
|
192
|
+
// Set environment variable for downstream hooks
|
|
193
|
+
process.env.CLAUDE_SESSION_ID = sessionId;
|
|
194
|
+
|
|
195
|
+
console.log(`[session-generator] Session started: ${sessionId.substring(0, 8)}...`);
|
|
196
|
+
|
|
197
|
+
return approve();
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error(`[session-generator] Error: ${error.message}`);
|
|
200
|
+
return approve();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Direct execution support
|
|
205
|
+
if (require.main === module) {
|
|
206
|
+
const config = loadConfig();
|
|
207
|
+
|
|
208
|
+
if (config.cleanup_on_start) {
|
|
209
|
+
cleanupOldSessions(config);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const sessionId = crypto.randomUUID();
|
|
213
|
+
const startTime = Date.now();
|
|
214
|
+
const pid = process.pid;
|
|
215
|
+
|
|
216
|
+
const sessionData = {
|
|
217
|
+
session_id: sessionId,
|
|
218
|
+
pid: pid,
|
|
219
|
+
start_time: startTime,
|
|
220
|
+
started_at: new Date().toISOString(),
|
|
221
|
+
cwd: process.cwd(),
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
225
|
+
|
|
226
|
+
if (config.registry_enabled) {
|
|
227
|
+
let registry = readRegistry();
|
|
228
|
+
registry = cleanStaleRegistryEntries(registry);
|
|
229
|
+
registry[`${pid}-${startTime}`] = sessionData;
|
|
230
|
+
writeRegistry(registry);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(sessionData, null, 2));
|
|
234
|
+
|
|
235
|
+
console.log(JSON.stringify({ result: 'continue' }));
|
|
236
|
+
}
|