ctx-cc 3.5.0 → 4.1.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.
Files changed (74) hide show
  1. package/README.md +375 -676
  2. package/agents/ctx-arch-mapper.md +5 -3
  3. package/agents/ctx-auditor.md +5 -3
  4. package/agents/ctx-codex-reviewer.md +214 -0
  5. package/agents/ctx-concerns-mapper.md +5 -3
  6. package/agents/ctx-criteria-suggester.md +6 -4
  7. package/agents/ctx-debugger.md +5 -3
  8. package/agents/ctx-designer.md +488 -114
  9. package/agents/ctx-discusser.md +5 -3
  10. package/agents/ctx-executor.md +5 -3
  11. package/agents/ctx-handoff.md +6 -4
  12. package/agents/ctx-learner.md +5 -3
  13. package/agents/ctx-mapper.md +4 -3
  14. package/agents/ctx-ml-analyst.md +600 -0
  15. package/agents/ctx-ml-engineer.md +933 -0
  16. package/agents/ctx-ml-reviewer.md +485 -0
  17. package/agents/ctx-ml-scientist.md +626 -0
  18. package/agents/ctx-parallelizer.md +4 -3
  19. package/agents/ctx-planner.md +5 -3
  20. package/agents/ctx-predictor.md +4 -3
  21. package/agents/ctx-qa.md +5 -3
  22. package/agents/ctx-quality-mapper.md +5 -3
  23. package/agents/ctx-researcher.md +5 -3
  24. package/agents/ctx-reviewer.md +6 -4
  25. package/agents/ctx-team-coordinator.md +5 -3
  26. package/agents/ctx-tech-mapper.md +5 -3
  27. package/agents/ctx-verifier.md +5 -3
  28. package/bin/ctx.js +199 -27
  29. package/commands/brand.md +309 -0
  30. package/commands/ctx.md +10 -10
  31. package/commands/design.md +304 -0
  32. package/commands/experiment.md +251 -0
  33. package/commands/help.md +57 -7
  34. package/commands/init.md +25 -0
  35. package/commands/metrics.md +1 -1
  36. package/commands/milestone.md +1 -1
  37. package/commands/ml-status.md +197 -0
  38. package/commands/monitor.md +1 -1
  39. package/commands/train.md +266 -0
  40. package/commands/visual-qa.md +559 -0
  41. package/commands/voice.md +1 -1
  42. package/hooks/post-tool-use.js +39 -0
  43. package/hooks/pre-tool-use.js +94 -0
  44. package/hooks/subagent-stop.js +32 -0
  45. package/package.json +9 -3
  46. package/plugin.json +46 -0
  47. package/skills/ctx-design-system/SKILL.md +572 -0
  48. package/skills/ctx-ml-experiment/SKILL.md +334 -0
  49. package/skills/ctx-ml-pipeline/SKILL.md +437 -0
  50. package/skills/ctx-orchestrator/SKILL.md +91 -0
  51. package/skills/ctx-review-gate/SKILL.md +147 -0
  52. package/skills/ctx-state/SKILL.md +100 -0
  53. package/skills/ctx-visual-qa/SKILL.md +587 -0
  54. package/src/agents.js +109 -0
  55. package/src/auto.js +287 -0
  56. package/src/capabilities.js +226 -0
  57. package/src/commits.js +94 -0
  58. package/src/config.js +112 -0
  59. package/src/context.js +241 -0
  60. package/src/handoff.js +156 -0
  61. package/src/hooks.js +218 -0
  62. package/src/install.js +125 -50
  63. package/src/lifecycle.js +194 -0
  64. package/src/metrics.js +198 -0
  65. package/src/pipeline.js +269 -0
  66. package/src/review-gate.js +338 -0
  67. package/src/runner.js +120 -0
  68. package/src/skills.js +143 -0
  69. package/src/state.js +267 -0
  70. package/src/worktree.js +244 -0
  71. package/templates/PRD.json +1 -1
  72. package/templates/config.json +4 -237
  73. package/workflows/ctx-router.md +0 -485
  74. package/workflows/map-codebase.md +0 -329
package/src/hooks.js ADDED
@@ -0,0 +1,218 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { readState, writeState } from './state.js';
4
+
5
+ const CTX_HOOK_MARKER = '// CTX_HOOKS';
6
+
7
+ /**
8
+ * Hook definitions that CTX generates for Claude Code settings.json.
9
+ * Each hook has: event, matcher (optional), command, description.
10
+ */
11
+ const HOOK_DEFINITIONS = {
12
+ // Record agent completion in STATE.json
13
+ subagentCompletion: {
14
+ event: 'SubagentStop',
15
+ command: `node -e "
16
+ const fs=require('fs'),p=require('path');
17
+ const d=p.join(process.cwd(),'.ctx','STATE.json');
18
+ if(!fs.existsSync(d))process.exit(0);
19
+ const s=JSON.parse(fs.readFileSync(d,'utf-8'));
20
+ const h=s.agentHistory||[];
21
+ for(let i=h.length-1;i>=0;i--){
22
+ if(!h[i].completedAt){h[i].completedAt=new Date().toISOString();break;}
23
+ }
24
+ s.session={...s.session,lastActivity:new Date().toISOString()};
25
+ fs.writeFileSync(d,JSON.stringify(s,null,2)+'\\n');
26
+ "`,
27
+ description: 'Record agent completion in STATE.json',
28
+ configKey: 'hooks.subagentCompletion',
29
+ default: true,
30
+ },
31
+
32
+ // Block commits without test file changes
33
+ blockCommitWithoutTests: {
34
+ event: 'PreToolUse',
35
+ matcher: 'Bash',
36
+ command: `node -e "
37
+ const cmd=process.env.TOOL_INPUT||'';
38
+ if(!/git commit/.test(cmd))process.exit(0);
39
+ const {execSync}=require('child_process');
40
+ try{
41
+ const diff=execSync('git diff --cached --name-only',{encoding:'utf-8'});
42
+ const files=diff.trim().split('\\n');
43
+ const hasCode=files.some(f=>/\\.(js|ts|jsx|tsx|py|go|rs)$/.test(f));
44
+ const hasTest=files.some(f=>/\\.(test|spec)\\.|__tests__/.test(f));
45
+ if(hasCode&&!hasTest){
46
+ console.error('CTX: Commit blocked — code changes without tests.');
47
+ console.error('Files: '+files.filter(f=>/\\.(js|ts|jsx|tsx|py|go|rs)$/.test(f)).join(', '));
48
+ process.exit(2);
49
+ }
50
+ }catch{}
51
+ "`,
52
+ description: 'Block commits without corresponding test changes',
53
+ configKey: 'hooks.blockCommitWithoutTests',
54
+ default: false,
55
+ },
56
+
57
+ // Enforce TDD mode (strict/warn/off)
58
+ tddEnforcement: {
59
+ event: 'PreToolUse',
60
+ matcher: 'Bash',
61
+ command: `node -e "
62
+ const cmd=process.env.TOOL_INPUT||'';
63
+ if(!/git commit/.test(cmd))process.exit(0);
64
+ const fs=require('fs'),p=require('path');
65
+ const cfgPath=p.join(process.cwd(),'.ctx','config.json');
66
+ let mode='off';
67
+ try{const c=JSON.parse(fs.readFileSync(cfgPath,'utf-8'));mode=c.hooks?.tddMode||'off';}catch{}
68
+ if(mode==='off')process.exit(0);
69
+ const {execSync}=require('child_process');
70
+ try{
71
+ const diff=execSync('git diff --cached --name-only',{encoding:'utf-8'});
72
+ const files=diff.trim().split('\\n');
73
+ const hasCode=files.some(f=>/\\.(js|ts|jsx|tsx|py|go|rs)$/.test(f)&&!/\\.(test|spec)\\.|__tests__/.test(f));
74
+ const hasTest=files.some(f=>/\\.(test|spec)\\.|__tests__/.test(f));
75
+ if(hasCode&&!hasTest){
76
+ if(mode==='strict'){
77
+ console.error('CTX TDD: Commit blocked — write tests first.');
78
+ process.exit(2);
79
+ }else if(mode==='warn'){
80
+ console.error('CTX TDD Warning: Code changes without tests.');
81
+ }
82
+ }
83
+ }catch{}
84
+ "`,
85
+ description: 'TDD enforcement (strict/warn/off)',
86
+ configKey: 'hooks.tddMode',
87
+ default: 'off',
88
+ },
89
+ };
90
+
91
+ /**
92
+ * Generate Claude Code hooks section for settings.json.
93
+ * Reads current config to determine which hooks are enabled.
94
+ *
95
+ * Returns an array of hook objects ready for settings.json.
96
+ */
97
+ export function generateHooks(config = {}) {
98
+ const hooks = [];
99
+ const hooksConfig = config.hooks || {};
100
+
101
+ for (const [key, def] of Object.entries(HOOK_DEFINITIONS)) {
102
+ const configValue = getNestedValue(hooksConfig, key);
103
+ const enabled = configValue !== undefined ? configValue : def.default;
104
+
105
+ // Skip disabled hooks
106
+ if (enabled === false || enabled === 'off') continue;
107
+
108
+ const hook = {
109
+ type: def.event,
110
+ command: `${CTX_HOOK_MARKER} ${def.command.replace(/\n\s*/g, ' ').trim()}`,
111
+ };
112
+
113
+ if (def.matcher) {
114
+ hook.matcher = def.matcher;
115
+ }
116
+
117
+ hooks.push(hook);
118
+ }
119
+
120
+ return hooks;
121
+ }
122
+
123
+ /**
124
+ * Write hooks to .claude/settings.json, preserving user's existing hooks.
125
+ */
126
+ export function syncHooks(settingsDir, config = {}) {
127
+ const settingsPath = path.join(settingsDir, 'settings.json');
128
+ let settings = {};
129
+
130
+ // Read existing settings
131
+ try {
132
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
133
+ } catch {}
134
+
135
+ // Preserve user hooks (non-CTX)
136
+ const existingHooks = settings.hooks || {};
137
+ const userHooks = {};
138
+
139
+ for (const [event, hookList] of Object.entries(existingHooks)) {
140
+ if (Array.isArray(hookList)) {
141
+ userHooks[event] = hookList.filter(h => !h.command?.includes(CTX_HOOK_MARKER));
142
+ }
143
+ }
144
+
145
+ // Generate CTX hooks
146
+ const ctxHooks = generateHooks(config);
147
+
148
+ // Merge: group by event type
149
+ const merged = { ...userHooks };
150
+ for (const hook of ctxHooks) {
151
+ const event = hook.type;
152
+ if (!merged[event]) merged[event] = [];
153
+ merged[event].push({
154
+ matcher: hook.matcher || undefined,
155
+ command: hook.command,
156
+ });
157
+ }
158
+
159
+ // Clean up empty arrays
160
+ for (const [key, val] of Object.entries(merged)) {
161
+ if (Array.isArray(val) && val.length === 0) delete merged[key];
162
+ }
163
+
164
+ settings.hooks = merged;
165
+
166
+ // Write back
167
+ if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
168
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
169
+
170
+ return { hooksGenerated: ctxHooks.length, settingsPath };
171
+ }
172
+
173
+ /**
174
+ * List all available hooks with their current status.
175
+ */
176
+ export function listHooks(config = {}) {
177
+ const hooksConfig = config.hooks || {};
178
+ const result = [];
179
+
180
+ for (const [key, def] of Object.entries(HOOK_DEFINITIONS)) {
181
+ const configValue = getNestedValue(hooksConfig, key);
182
+ const status = configValue !== undefined ? configValue : def.default;
183
+
184
+ result.push({
185
+ key,
186
+ event: def.event,
187
+ description: def.description,
188
+ configKey: def.configKey,
189
+ status: status === false ? 'disabled' : status === true ? 'enabled' : String(status),
190
+ default: def.default,
191
+ });
192
+ }
193
+
194
+ return result;
195
+ }
196
+
197
+ /**
198
+ * Format hooks list for display.
199
+ */
200
+ export function formatHooksList(hooks) {
201
+ const lines = [];
202
+ const maxKey = Math.max(20, ...hooks.map(h => h.configKey.length));
203
+
204
+ for (const h of hooks) {
205
+ const icon = h.status === 'disabled' || h.status === 'off' ? '○' : '●';
206
+ lines.push(` ${icon} ${h.configKey.padEnd(maxKey)} ${h.status.padEnd(10)} ${h.description}`);
207
+ }
208
+
209
+ return lines.join('\n');
210
+ }
211
+
212
+ // --- internal ---
213
+
214
+ function getNestedValue(obj, key) {
215
+ return key.split('.').reduce((o, k) => o?.[k], obj);
216
+ }
217
+
218
+ export { HOOK_DEFINITIONS, CTX_HOOK_MARKER };
package/src/install.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import readline from 'readline';
4
+ import { saveCapabilityManifest } from './capabilities.js';
5
5
 
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
@@ -15,7 +15,6 @@ const VERSION = JSON.parse(
15
15
  const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
16
16
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
17
17
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
18
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
19
18
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
20
19
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
21
20
 
@@ -28,23 +27,18 @@ function printBanner() {
28
27
  ╚██████╗ ██║ ██╔╝ ██╗
29
28
  ╚═════╝ ╚═╝ ╚═╝ ╚═╝
30
29
  `));
31
- console.log(` ${bold('CTX 3.3')} ${dim(`v${VERSION}`)}`);
30
+ console.log(` ${bold('CTX 4.0')} ${dim(`v${VERSION}`)}`);
32
31
  console.log(' Intelligent workflow orchestration for Claude Code.');
33
- console.log(' 20 agents. Learning system. Predictive planning. Self-healing.\n');
32
+ console.log(' 26 agents. 7 skills. Hooks. Phase-based lifecycle.\n');
34
33
  }
35
34
 
36
35
  function copyDir(src, dest) {
37
- if (!fs.existsSync(dest)) {
38
- fs.mkdirSync(dest, { recursive: true });
39
- }
40
-
36
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
41
37
  const entries = fs.readdirSync(src, { withFileTypes: true });
42
38
  let count = 0;
43
-
44
39
  for (const entry of entries) {
45
40
  const srcPath = path.join(src, entry.name);
46
41
  const destPath = path.join(dest, entry.name);
47
-
48
42
  if (entry.isDirectory()) {
49
43
  count += copyDir(srcPath, destPath);
50
44
  } else {
@@ -52,20 +46,16 @@ function copyDir(src, dest) {
52
46
  count++;
53
47
  }
54
48
  }
55
-
56
49
  return count;
57
50
  }
58
51
 
59
52
  function removeDir(dir) {
60
- if (fs.existsSync(dir)) {
61
- fs.rmSync(dir, { recursive: true, force: true });
62
- }
53
+ if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
63
54
  }
64
55
 
65
56
  export async function install(options) {
66
57
  printBanner();
67
58
 
68
- // Determine target directory
69
59
  const homeDir = process.env.HOME || process.env.USERPROFILE;
70
60
  let targetBase;
71
61
 
@@ -77,19 +67,18 @@ export async function install(options) {
77
67
  console.log(` Installing ${cyan('globally')} to ${cyan('~/.claude')}\n`);
78
68
  }
79
69
 
80
- // Check for existing installation
81
70
  const commandsDir = path.join(targetBase, 'commands', 'ctx');
82
- const agentsDir = targetBase === path.join(homeDir, '.claude')
83
- ? path.join(targetBase, 'agents')
84
- : path.join(targetBase, 'agents');
71
+ const agentsDir = path.join(targetBase, 'agents');
72
+ const skillsDir = path.join(targetBase, 'skills');
73
+ const hooksDir = path.join(targetBase, 'hooks');
85
74
  const ctxDir = path.join(targetBase, 'ctx');
86
75
 
76
+ // Check existing installation
87
77
  if (fs.existsSync(commandsDir) && !options.force) {
88
78
  const existingVersion = getExistingVersion(ctxDir);
89
79
  if (existingVersion) {
90
80
  console.log(yellow(` ⚠ CTX ${existingVersion} is already installed.`));
91
81
  console.log(` Use ${cyan('--force')} to reinstall.\n`);
92
-
93
82
  if (existingVersion === VERSION) {
94
83
  console.log(green(' ✓ Already up to date.\n'));
95
84
  process.exit(0);
@@ -97,56 +86,77 @@ export async function install(options) {
97
86
  }
98
87
  }
99
88
 
100
- // Clean install - remove existing
89
+ // Clean previous installation
101
90
  if (options.force || !fs.existsSync(commandsDir)) {
102
91
  console.log(' Cleaning previous installation...');
103
92
  removeDir(commandsDir);
104
93
  removeDir(ctxDir);
105
- // Only remove ctx-* agents, not all agents
106
- if (fs.existsSync(agentsDir)) {
107
- const agents = fs.readdirSync(agentsDir);
108
- for (const agent of agents) {
109
- if (agent.startsWith('ctx-')) {
110
- fs.unlinkSync(path.join(agentsDir, agent));
111
- }
112
- }
113
- }
94
+ // Only remove ctx-* agents and ctx-* skills
95
+ cleanupPrefixed(agentsDir, 'ctx-');
96
+ cleanupPrefixed(skillsDir, 'ctx-');
97
+ cleanupPrefixed(hooksDir, 'ctx-');
114
98
  }
115
99
 
116
100
  // Create directories
117
101
  fs.mkdirSync(commandsDir, { recursive: true });
118
102
  fs.mkdirSync(ctxDir, { recursive: true });
119
103
  fs.mkdirSync(agentsDir, { recursive: true });
104
+ fs.mkdirSync(skillsDir, { recursive: true });
105
+ fs.mkdirSync(hooksDir, { recursive: true });
120
106
 
121
- // Copy commands
107
+ // 1. Install commands (slash commands)
122
108
  const srcCommands = path.join(packageRoot, 'commands');
123
109
  if (fs.existsSync(srcCommands)) {
124
110
  const count = copyDir(srcCommands, commandsDir);
125
111
  console.log(green(` ✓`) + ` Installed commands/ctx (${count} files)`);
126
112
 
127
- // Also copy ctx.md to commands root so /ctx works directly
113
+ // Copy ctx.md to commands root so /ctx works directly
128
114
  const mainRouter = path.join(commandsDir, 'ctx.md');
129
115
  const rootRouter = path.join(targetBase, 'commands', 'ctx.md');
130
116
  if (fs.existsSync(mainRouter)) {
131
117
  fs.copyFileSync(mainRouter, rootRouter);
132
- console.log(green(` ✓`) + ` Installed /ctx command`);
118
+ console.log(green(` ✓`) + ` Installed /ctx router command`);
133
119
  }
134
120
  }
135
121
 
136
- // Copy agents
122
+ // 2. Install agents (subagents with native frontmatter)
137
123
  const srcAgents = path.join(packageRoot, 'agents');
138
124
  if (fs.existsSync(srcAgents)) {
139
- const agents = fs.readdirSync(srcAgents);
125
+ const agents = fs.readdirSync(srcAgents).filter(f => f.endsWith('.md'));
140
126
  for (const agent of agents) {
141
- fs.copyFileSync(
142
- path.join(srcAgents, agent),
143
- path.join(agentsDir, agent)
144
- );
127
+ fs.copyFileSync(path.join(srcAgents, agent), path.join(agentsDir, agent));
145
128
  }
146
- console.log(green(` ✓`) + ` Installed agents (${agents.length} files)`);
129
+ console.log(green(` ✓`) + ` Installed agents (${agents.length} subagents)`);
147
130
  }
148
131
 
149
- // Copy references
132
+ // 3. Install skills (progressive disclosure)
133
+ const srcSkills = path.join(packageRoot, 'skills');
134
+ if (fs.existsSync(srcSkills)) {
135
+ const skillDirs = fs.readdirSync(srcSkills, { withFileTypes: true })
136
+ .filter(d => d.isDirectory() && d.name.startsWith('ctx-'));
137
+ for (const dir of skillDirs) {
138
+ const destSkillDir = path.join(skillsDir, dir.name);
139
+ copyDir(path.join(srcSkills, dir.name), destSkillDir);
140
+ }
141
+ console.log(green(` ✓`) + ` Installed skills (${skillDirs.length} skills)`);
142
+ }
143
+
144
+ // 4. Install hooks (deterministic enforcement scripts)
145
+ const srcHooks = path.join(packageRoot, 'hooks');
146
+ if (fs.existsSync(srcHooks)) {
147
+ const hookFiles = fs.readdirSync(srcHooks).filter(f => f.endsWith('.js'));
148
+ for (const hookFile of hookFiles) {
149
+ const destFile = path.join(hooksDir, `ctx-${hookFile}`);
150
+ fs.copyFileSync(path.join(srcHooks, hookFile), destFile);
151
+ }
152
+ console.log(green(` ✓`) + ` Installed hooks (${hookFiles.length} scripts)`);
153
+ }
154
+
155
+ // 5. Generate/merge settings.json with hooks
156
+ mergeHooksIntoSettings(targetBase, hooksDir);
157
+ console.log(green(` ✓`) + ` Configured hooks in settings.json`);
158
+
159
+ // 6. Install references and templates
150
160
  const srcRefs = path.join(packageRoot, 'references');
151
161
  const destRefs = path.join(ctxDir, 'references');
152
162
  if (fs.existsSync(srcRefs)) {
@@ -154,7 +164,6 @@ export async function install(options) {
154
164
  console.log(green(` ✓`) + ` Installed references (${count} files)`);
155
165
  }
156
166
 
157
- // Copy templates
158
167
  const srcTemplates = path.join(packageRoot, 'templates');
159
168
  const destTemplates = path.join(ctxDir, 'templates');
160
169
  if (fs.existsSync(srcTemplates)) {
@@ -162,13 +171,11 @@ export async function install(options) {
162
171
  console.log(green(` ✓`) + ` Installed templates (${count} files)`);
163
172
  }
164
173
 
165
- // Copy workflows
166
- const srcWorkflows = path.join(packageRoot, 'workflows');
167
- const destWorkflows = path.join(ctxDir, 'workflows');
168
- if (fs.existsSync(srcWorkflows)) {
169
- const count = copyDir(srcWorkflows, destWorkflows);
170
- console.log(green(` ✓`) + ` Installed workflows (${count} files)`);
171
- }
174
+ // Generate capability-manifest.json template from DEFAULT_CAPABILITIES.
175
+ // /ctx:init copies this into each project's .ctx/ so the PreToolUse hook
176
+ // (hooks/pre-tool-use.js) has a manifest to enforce against.
177
+ saveCapabilityManifest(destTemplates);
178
+ console.log(green(` ✓`) + ` Generated capability-manifest.json template`);
172
179
 
173
180
  // Write VERSION file
174
181
  fs.writeFileSync(path.join(ctxDir, 'VERSION'), VERSION);
@@ -176,10 +183,78 @@ export async function install(options) {
176
183
 
177
184
  // Success message
178
185
  console.log(`\n ${green('Done!')} Launch Claude Code and run ${cyan('/ctx:help')}.`);
179
- console.log(`\n ${dim('GitHub:')} https://github.com/jufjuf/CTX`);
186
+ console.log(`
187
+ ${bold('What was installed:')}
188
+ ${dim('Agents:')} ~/.claude/agents/ctx-*.md (26 subagents)
189
+ ${dim('Skills:')} ~/.claude/skills/ctx-*/ (7 skills)
190
+ ${dim('Commands:')} ~/.claude/commands/ctx/ (slash commands)
191
+ ${dim('Hooks:')} ~/.claude/hooks/ctx-*.js (3 hook scripts)
192
+ ${dim('Config:')} ~/.claude/settings.json (hooks registered)
193
+ `);
194
+ console.log(` ${dim('GitHub:')} https://github.com/jufjuf/CTX`);
180
195
  console.log(` ${dim('Report issues:')} https://github.com/jufjuf/CTX/issues\n`);
181
196
  }
182
197
 
198
+ // --- Helpers ---
199
+
200
+ function cleanupPrefixed(dir, prefix) {
201
+ if (!fs.existsSync(dir)) return;
202
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
203
+ for (const entry of entries) {
204
+ if (entry.name.startsWith(prefix)) {
205
+ const fullPath = path.join(dir, entry.name);
206
+ if (entry.isDirectory()) {
207
+ removeDir(fullPath);
208
+ } else {
209
+ fs.unlinkSync(fullPath);
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ function mergeHooksIntoSettings(targetBase, hooksDir) {
216
+ const settingsPath = path.join(targetBase, 'settings.json');
217
+ let settings = {};
218
+
219
+ // Read existing settings
220
+ try {
221
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
222
+ } catch {}
223
+
224
+ // Preserve non-CTX hooks
225
+ const existingHooks = settings.hooks || {};
226
+ const userHooks = {};
227
+ for (const [event, hookList] of Object.entries(existingHooks)) {
228
+ if (Array.isArray(hookList)) {
229
+ userHooks[event] = hookList.filter(h => !h.command?.includes('ctx-'));
230
+ }
231
+ }
232
+
233
+ // Add CTX hooks (referencing installed script files)
234
+ const ctxHooks = {
235
+ SubagentStop: [
236
+ { command: `node ${path.join(hooksDir, 'ctx-subagent-stop.js')}` }
237
+ ],
238
+ PreToolUse: [
239
+ { command: `node ${path.join(hooksDir, 'ctx-pre-tool-use.js')}` }
240
+ ],
241
+ PostToolUse: [
242
+ { command: `node ${path.join(hooksDir, 'ctx-post-tool-use.js')}` }
243
+ ],
244
+ };
245
+
246
+ // Merge
247
+ const merged = {};
248
+ for (const [event, hooks] of Object.entries({ ...userHooks, ...ctxHooks })) {
249
+ const userList = userHooks[event] || [];
250
+ const ctxList = ctxHooks[event] || [];
251
+ merged[event] = [...userList, ...ctxList];
252
+ }
253
+
254
+ settings.hooks = merged;
255
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
256
+ }
257
+
183
258
  function getExistingVersion(ctxDir) {
184
259
  const versionFile = path.join(ctxDir, 'VERSION');
185
260
  if (fs.existsSync(versionFile)) {