claude-cli-advanced-starter-pack 1.0.16 → 1.8.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/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 +309 -80
- 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 +209 -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/context-injector.template.js +261 -0
- package/templates/hooks/git-commit-tracker.template.js +267 -0
- package/templates/hooks/happy-mode-detector.template.js +214 -0
- package/templates/hooks/happy-title-generator.template.js +260 -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-budget-loader.template.js +234 -0
- package/templates/hooks/token-usage-monitor.template.js +193 -0
- package/templates/hooks/tool-output-cacher.template.js +219 -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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CCASP Panel Queue Reader Hook
|
|
3
|
+
*
|
|
4
|
+
* Reads commands from the CCASP Panel queue and injects them
|
|
5
|
+
* when user submits an empty prompt (just presses Enter).
|
|
6
|
+
*
|
|
7
|
+
* Install: ccasp install-panel-hook
|
|
8
|
+
* Or copy to: .claude/hooks/panel-queue-reader.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
|
|
15
|
+
// Queue file location
|
|
16
|
+
const QUEUE_FILE = join(homedir(), '.claude', 'ccasp-panel', 'command-queue.json');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the next pending command from queue
|
|
20
|
+
*/
|
|
21
|
+
function getNextPendingCommand() {
|
|
22
|
+
if (!existsSync(QUEUE_FILE)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const queue = JSON.parse(readFileSync(QUEUE_FILE, 'utf8'));
|
|
28
|
+
return queue.find(entry => entry.status === 'pending') || null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mark a command as processed
|
|
36
|
+
*/
|
|
37
|
+
function markCommandProcessed(id) {
|
|
38
|
+
if (!existsSync(QUEUE_FILE)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const queue = JSON.parse(readFileSync(QUEUE_FILE, 'utf8'));
|
|
44
|
+
const entry = queue.find(e => e.id === id);
|
|
45
|
+
if (entry) {
|
|
46
|
+
entry.status = 'completed';
|
|
47
|
+
entry.processedAt = Date.now();
|
|
48
|
+
writeFileSync(QUEUE_FILE, JSON.stringify(queue, null, 2), 'utf8');
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Ignore errors
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hook handler for UserPromptSubmit
|
|
57
|
+
*/
|
|
58
|
+
export default async function panelQueueReader(params) {
|
|
59
|
+
const { prompt } = params;
|
|
60
|
+
|
|
61
|
+
// Only trigger on empty prompts (user just pressed Enter)
|
|
62
|
+
if (prompt && prompt.trim().length > 0) {
|
|
63
|
+
return { proceed: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for pending commands
|
|
67
|
+
const pendingCommand = getNextPendingCommand();
|
|
68
|
+
|
|
69
|
+
if (pendingCommand) {
|
|
70
|
+
// Mark as processed immediately
|
|
71
|
+
markCommandProcessed(pendingCommand.id);
|
|
72
|
+
|
|
73
|
+
// Return the command to inject
|
|
74
|
+
return {
|
|
75
|
+
proceed: true,
|
|
76
|
+
updatedPrompt: pendingCommand.command + (pendingCommand.args ? ' ' + pendingCommand.args : ''),
|
|
77
|
+
message: `[CCASP Panel] Executing: ${pendingCommand.label || pendingCommand.command}`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// No pending commands - let empty prompt through (will show help or similar)
|
|
82
|
+
return { proceed: true };
|
|
83
|
+
}
|
|
@@ -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
|
+
}
|