@exero1/claudecontext 0.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.
- package/README.md +286 -0
- package/dist/installer/install.d.ts +12 -0
- package/dist/installer/install.d.ts.map +1 -0
- package/dist/installer/install.js +714 -0
- package/dist/installer/install.js.map +1 -0
- package/dist/src/cache/budget.d.ts +48 -0
- package/dist/src/cache/budget.d.ts.map +1 -0
- package/dist/src/cache/budget.js +55 -0
- package/dist/src/cache/budget.js.map +1 -0
- package/dist/src/cache/compressor.d.ts +21 -0
- package/dist/src/cache/compressor.d.ts.map +1 -0
- package/dist/src/cache/compressor.js +89 -0
- package/dist/src/cache/compressor.js.map +1 -0
- package/dist/src/cache/levels.d.ts +16 -0
- package/dist/src/cache/levels.d.ts.map +1 -0
- package/dist/src/cache/levels.js +41 -0
- package/dist/src/cache/levels.js.map +1 -0
- package/dist/src/cache/manager.d.ts +38 -0
- package/dist/src/cache/manager.d.ts.map +1 -0
- package/dist/src/cache/manager.js +196 -0
- package/dist/src/cache/manager.js.map +1 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +279 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/detection/areas.d.ts +13 -0
- package/dist/src/detection/areas.d.ts.map +1 -0
- package/dist/src/detection/areas.js +96 -0
- package/dist/src/detection/areas.js.map +1 -0
- package/dist/src/detection/task.d.ts +28 -0
- package/dist/src/detection/task.d.ts.map +1 -0
- package/dist/src/detection/task.js +77 -0
- package/dist/src/detection/task.js.map +1 -0
- package/dist/src/gating/gate.d.ts +38 -0
- package/dist/src/gating/gate.d.ts.map +1 -0
- package/dist/src/gating/gate.js +74 -0
- package/dist/src/gating/gate.js.map +1 -0
- package/dist/src/graph/edges.d.ts +41 -0
- package/dist/src/graph/edges.d.ts.map +1 -0
- package/dist/src/graph/edges.js +115 -0
- package/dist/src/graph/edges.js.map +1 -0
- package/dist/src/graph/indexer.d.ts +38 -0
- package/dist/src/graph/indexer.d.ts.map +1 -0
- package/dist/src/graph/indexer.js +228 -0
- package/dist/src/graph/indexer.js.map +1 -0
- package/dist/src/graph/traversal.d.ts +25 -0
- package/dist/src/graph/traversal.d.ts.map +1 -0
- package/dist/src/graph/traversal.js +173 -0
- package/dist/src/graph/traversal.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +82 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/indexing/codebase.d.ts +30 -0
- package/dist/src/indexing/codebase.d.ts.map +1 -0
- package/dist/src/indexing/codebase.js +127 -0
- package/dist/src/indexing/codebase.js.map +1 -0
- package/dist/src/markdown/writer.d.ts +34 -0
- package/dist/src/markdown/writer.d.ts.map +1 -0
- package/dist/src/markdown/writer.js +96 -0
- package/dist/src/markdown/writer.js.map +1 -0
- package/dist/src/server.d.ts +15 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +520 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/storage/db.d.ts +123 -0
- package/dist/src/storage/db.d.ts.map +1 -0
- package/dist/src/storage/db.js +318 -0
- package/dist/src/storage/db.js.map +1 -0
- package/dist/src/utils/glob.d.ts +11 -0
- package/dist/src/utils/glob.d.ts.map +1 -0
- package/dist/src/utils/glob.js +20 -0
- package/dist/src/utils/glob.js.map +1 -0
- package/hooks/post-write.mjs +57 -0
- package/hooks/pre-compact.mjs +44 -0
- package/hooks/pre-tool-use.mjs +87 -0
- package/hooks/session-start.mjs +54 -0
- package/package.json +51 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ClaudeContext installer.
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* claudecontext init [--project-root <path>] Install in a project
|
|
7
|
+
* claudecontext remove Remove hooks/config (preserves state.db)
|
|
8
|
+
* claudecontext migrate Migrate schema for upgrades
|
|
9
|
+
* claudecontext doctor Check installation health
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, copyFileSync, rmSync } from 'node:fs';
|
|
12
|
+
import { join, resolve, dirname, basename } from 'node:path';
|
|
13
|
+
import { createInterface } from 'node:readline';
|
|
14
|
+
import { execSync } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { detectAreas } from '../src/detection/areas.js';
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const isWindows = process.platform === 'win32';
|
|
19
|
+
// ── Utilities ──────────────────────────────────────────────────────────────
|
|
20
|
+
function log(msg) {
|
|
21
|
+
process.stdout.write(` ${msg}\n`);
|
|
22
|
+
}
|
|
23
|
+
function ok(msg) {
|
|
24
|
+
process.stdout.write(` ✓ ${msg}\n`);
|
|
25
|
+
}
|
|
26
|
+
function warn(msg) {
|
|
27
|
+
process.stdout.write(` ⚠ ${msg}\n`);
|
|
28
|
+
}
|
|
29
|
+
function err(msg) {
|
|
30
|
+
process.stdout.write(` ✗ ${msg}\n`);
|
|
31
|
+
}
|
|
32
|
+
function ask(rl, question) {
|
|
33
|
+
return new Promise(resolve => {
|
|
34
|
+
rl.question(` ${question} `, answer => {
|
|
35
|
+
resolve(answer.trim());
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function mergeJson(existing, patch) {
|
|
40
|
+
return { ...existing, ...patch };
|
|
41
|
+
}
|
|
42
|
+
async function cmdInit(projectRoot, opts = {}) {
|
|
43
|
+
process.stdout.write('\n╔══════════════════════════════════════╗\n');
|
|
44
|
+
process.stdout.write('║ ClaudeContext Setup Wizard ║\n');
|
|
45
|
+
process.stdout.write('╚══════════════════════════════════════╝\n\n');
|
|
46
|
+
log(`Setting up ClaudeContext in: ${projectRoot}`);
|
|
47
|
+
let projectName;
|
|
48
|
+
let projectDesc;
|
|
49
|
+
let techStack;
|
|
50
|
+
let testStrategy;
|
|
51
|
+
let areas;
|
|
52
|
+
let conventions;
|
|
53
|
+
let invariants;
|
|
54
|
+
if (opts.config) {
|
|
55
|
+
// ── Non-interactive: load from config file ──
|
|
56
|
+
let cfg = {};
|
|
57
|
+
try {
|
|
58
|
+
cfg = JSON.parse(readFileSync(resolve(opts.config), 'utf8'));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
err(`Cannot read config file: ${opts.config}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
projectName = cfg.projectName ?? basename(projectRoot);
|
|
65
|
+
projectDesc = cfg.projectDesc ?? '';
|
|
66
|
+
techStack = cfg.techStack ?? '';
|
|
67
|
+
testStrategy = cfg.testStrategy ?? '';
|
|
68
|
+
conventions = cfg.conventions ?? '';
|
|
69
|
+
invariants = cfg.invariants ?? '';
|
|
70
|
+
areas = cfg.areas ?? detectAreas(projectRoot);
|
|
71
|
+
log(`Loaded config from: ${opts.config}`);
|
|
72
|
+
log(`Project: ${projectName} | Areas: ${areas.length}`);
|
|
73
|
+
}
|
|
74
|
+
else if (opts.yes) {
|
|
75
|
+
// ── Non-interactive: use defaults + auto-detect ──
|
|
76
|
+
projectName = basename(projectRoot);
|
|
77
|
+
projectDesc = '';
|
|
78
|
+
techStack = '';
|
|
79
|
+
testStrategy = '';
|
|
80
|
+
conventions = '';
|
|
81
|
+
invariants = '';
|
|
82
|
+
areas = detectAreas(projectRoot);
|
|
83
|
+
log(`Using --yes: project="${projectName}", auto-detected ${areas.length} area(s)`);
|
|
84
|
+
if (areas.length > 0) {
|
|
85
|
+
log(` Areas: ${areas.map(a => a.name).join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// ── Interactive wizard ──
|
|
90
|
+
if (!process.stdin.isTTY) {
|
|
91
|
+
err('No TTY detected. Run with --yes to use defaults, or --config <file> for non-interactive setup.');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
95
|
+
// Step 1/4: Project info
|
|
96
|
+
process.stdout.write('\n── Step 1/4: Project information ────────\n');
|
|
97
|
+
projectName = await ask(rl, 'Project name:');
|
|
98
|
+
projectDesc = await ask(rl, 'Brief project description (1-2 sentences):');
|
|
99
|
+
techStack = await ask(rl, 'Tech stack (e.g. TypeScript, React, PostgreSQL):');
|
|
100
|
+
testStrategy = await ask(rl, 'Test strategy (e.g. Vitest unit + Playwright E2E):');
|
|
101
|
+
// Step 2/4: Code areas
|
|
102
|
+
process.stdout.write('\n── Step 2/4: Code areas (subsystems) ───\n');
|
|
103
|
+
const detected = detectAreas(projectRoot);
|
|
104
|
+
if (detected.length > 0) {
|
|
105
|
+
log(`Auto-detected ${detected.length} area(s): ${detected.map(a => a.name).join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
log('Enter your main code areas. Press Enter with empty input to finish.');
|
|
108
|
+
log('Example: auth, api, database, frontend');
|
|
109
|
+
areas = [];
|
|
110
|
+
while (true) {
|
|
111
|
+
const areaName = await ask(rl, `Area name (or Enter to finish):`);
|
|
112
|
+
if (!areaName)
|
|
113
|
+
break;
|
|
114
|
+
const globs = await ask(rl, ` File patterns for "${areaName}" (comma-separated, e.g. src/auth/**,src/middleware/auth*):`);
|
|
115
|
+
areas.push({
|
|
116
|
+
name: areaName,
|
|
117
|
+
globs: globs.split(',').map(g => g.trim()).filter(Boolean),
|
|
118
|
+
docPath: `docs/context/${areaName}.md`,
|
|
119
|
+
});
|
|
120
|
+
ok(`Area "${areaName}" added`);
|
|
121
|
+
}
|
|
122
|
+
if (areas.length === 0 && detected.length > 0) {
|
|
123
|
+
areas = detected;
|
|
124
|
+
log(`No areas entered — using auto-detected areas: ${areas.map(a => a.name).join(', ')}`);
|
|
125
|
+
}
|
|
126
|
+
// Step 3/4: Conventions
|
|
127
|
+
process.stdout.write('\n── Step 3/4: Conventions ───────────────\n');
|
|
128
|
+
conventions = await ask(rl, 'Key conventions (e.g. "Use async/await, no callbacks; snake_case for DB"):');
|
|
129
|
+
invariants = await ask(rl, 'Critical invariants (e.g. "Never expose user IDs in URLs"):');
|
|
130
|
+
rl.close();
|
|
131
|
+
}
|
|
132
|
+
process.stdout.write('\n── Step 4/4: Creating files ────────────\n');
|
|
133
|
+
// Create directories
|
|
134
|
+
const stateDir = join(projectRoot, '.claudecontext');
|
|
135
|
+
const hooksDir = join(projectRoot, '.claude', 'hooks');
|
|
136
|
+
const tasksDir = join(projectRoot, 'tasks');
|
|
137
|
+
const docsDir = join(projectRoot, 'docs', 'context');
|
|
138
|
+
for (const dir of [stateDir, hooksDir, tasksDir, docsDir]) {
|
|
139
|
+
if (!existsSync(dir)) {
|
|
140
|
+
mkdirSync(dir, { recursive: true });
|
|
141
|
+
ok(`Created ${dir}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Write CLAUDE.md (preserve if user-modified)
|
|
145
|
+
const claudeMdPath = join(projectRoot, 'CLAUDE.md');
|
|
146
|
+
if (existsSync(claudeMdPath)) {
|
|
147
|
+
copyFileSync(claudeMdPath, `${claudeMdPath}.backup`);
|
|
148
|
+
warn(`Backed up existing CLAUDE.md to CLAUDE.md.backup`);
|
|
149
|
+
}
|
|
150
|
+
const areasSection = areas.length > 0
|
|
151
|
+
? areas.map(a => `- **${a.name}**: \`${a.globs.join(', ')}\` → docs at \`${a.docPath}\``).join('\n')
|
|
152
|
+
: '- (no areas configured — run `claudecontext init` to add areas)';
|
|
153
|
+
const claudeMdContent = `# ${projectName || 'Project'}
|
|
154
|
+
|
|
155
|
+
## ClaudeContext — System Instructions
|
|
156
|
+
|
|
157
|
+
**MANDATORY — do these every session:**
|
|
158
|
+
|
|
159
|
+
1. **Start of session / after compaction:** call \`context_get_bundle\` immediately. This restores your task context.
|
|
160
|
+
2. **After EVERY technical decision** (architecture choice, tradeoff accepted, approach selected): call \`context_update_task\` and record it in the "decisions" field. Do not defer this — decisions not recorded here are permanently lost on compaction.
|
|
161
|
+
3. **First time you understand the goal:** call \`context_update_task\` to set the "goal" field in plain English.
|
|
162
|
+
4. **Files touched** are auto-tracked by the post-write hook — you do NOT need to add them manually.
|
|
163
|
+
5. To show the user what context is loaded: call \`context_status\`.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Project Description
|
|
168
|
+
|
|
169
|
+
${projectDesc || '(add description)'}
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Tech Stack
|
|
174
|
+
|
|
175
|
+
${techStack || '(add tech stack)'}
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Key Areas
|
|
180
|
+
|
|
181
|
+
${areasSection}
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Conventions
|
|
186
|
+
|
|
187
|
+
${conventions || '(add conventions)'}
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Critical Invariants
|
|
192
|
+
|
|
193
|
+
${invariants || '(add invariants)'}
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Test Strategy
|
|
198
|
+
|
|
199
|
+
${testStrategy || '(add test strategy)'}
|
|
200
|
+
`;
|
|
201
|
+
writeFileSync(claudeMdPath, claudeMdContent);
|
|
202
|
+
ok('Generated CLAUDE.md');
|
|
203
|
+
// Write areas.json
|
|
204
|
+
const areasPath = join(stateDir, 'areas.json');
|
|
205
|
+
writeFileSync(areasPath, JSON.stringify(areas, null, 2));
|
|
206
|
+
ok('Written .claudecontext/areas.json');
|
|
207
|
+
// Write empty gate-rules.json (opt-in custom deny/warn rules)
|
|
208
|
+
const gateRulesPath = join(stateDir, 'gate-rules.json');
|
|
209
|
+
if (!existsSync(gateRulesPath)) {
|
|
210
|
+
writeFileSync(gateRulesPath, JSON.stringify([], null, 2) + '\n');
|
|
211
|
+
ok('Created .claudecontext/gate-rules.json (add custom deny/warn rules here)');
|
|
212
|
+
}
|
|
213
|
+
// Write config.json with default budget structure (all keys omitted = use defaults)
|
|
214
|
+
const configPath = join(stateDir, 'config.json');
|
|
215
|
+
if (!existsSync(configPath)) {
|
|
216
|
+
const defaultConfig = {
|
|
217
|
+
// Override token budgets by adding a "budgets" key, e.g.:
|
|
218
|
+
// "budgets": { "L1": 15000, "total": 40000 }
|
|
219
|
+
// Defaults: L0=3000 L1=8000 L2=10000 L3=2000 L4=2000 graph=4000 total=29000
|
|
220
|
+
budgets: {},
|
|
221
|
+
};
|
|
222
|
+
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
|
|
223
|
+
ok('Created .claudecontext/config.json (add budgets overrides to customise token limits)');
|
|
224
|
+
}
|
|
225
|
+
// Write .mcp.json (always use npx — never absolute paths)
|
|
226
|
+
const mcpJsonPath = join(projectRoot, '.mcp.json');
|
|
227
|
+
const mcpConfig = {
|
|
228
|
+
mcpServers: {
|
|
229
|
+
claudecontext: {
|
|
230
|
+
command: 'npx',
|
|
231
|
+
args: ['claudecontext-mcp'],
|
|
232
|
+
env: {
|
|
233
|
+
CLAUDECONTEXT_PROJECT_ROOT: projectRoot,
|
|
234
|
+
NODE_OPTIONS: '--no-warnings',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
|
|
240
|
+
ok('Written .mcp.json (uses npx — works across npm updates)');
|
|
241
|
+
// Install hook scripts
|
|
242
|
+
// install.js is at dist/installer/install.js → go up 2 levels to reach package root → hooks/
|
|
243
|
+
const hooksSource = resolve(__dirname, '..', '..', 'hooks');
|
|
244
|
+
const hookFiles = ['session-start.mjs', 'pre-tool-use.mjs', 'post-write.mjs', 'pre-compact.mjs'];
|
|
245
|
+
for (const hookFile of hookFiles) {
|
|
246
|
+
const src = join(hooksSource, hookFile);
|
|
247
|
+
const dest = join(hooksDir, hookFile);
|
|
248
|
+
if (existsSync(src)) {
|
|
249
|
+
writeFileSync(dest, readFileSync(src, 'utf8'));
|
|
250
|
+
if (!isWindows)
|
|
251
|
+
chmodSync(dest, 0o755);
|
|
252
|
+
ok(`Installed hook: ${hookFile}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Merge hooks config into .claude/settings.json
|
|
256
|
+
const settingsPath = join(projectRoot, '.claude', 'settings.json');
|
|
257
|
+
let settings = {};
|
|
258
|
+
if (existsSync(settingsPath)) {
|
|
259
|
+
try {
|
|
260
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
261
|
+
copyFileSync(settingsPath, `${settingsPath}.backup`);
|
|
262
|
+
warn('Backed up existing .claude/settings.json');
|
|
263
|
+
}
|
|
264
|
+
catch { /* ignore */ }
|
|
265
|
+
}
|
|
266
|
+
// Use project-relative paths so hooks work after the project is moved or cloned.
|
|
267
|
+
// On Windows: prefix with "node" since shebangs are ignored.
|
|
268
|
+
const hookCmd = (name) => isWindows ? `node .claude/hooks/${name}` : `.claude/hooks/${name}`;
|
|
269
|
+
const hooksConfig = {
|
|
270
|
+
hooks: {
|
|
271
|
+
SessionStart: [{ matcher: 'startup|resume', hooks: [{ type: 'command', command: hookCmd('session-start.mjs') }] }],
|
|
272
|
+
PreToolUse: [{ matcher: 'Bash|Write|Edit|MultiEdit', hooks: [{ type: 'command', command: hookCmd('pre-tool-use.mjs') }] }],
|
|
273
|
+
PostToolUse: [{ matcher: 'Write|Edit|MultiEdit', hooks: [{ type: 'command', command: hookCmd('post-write.mjs') }] }],
|
|
274
|
+
PreCompact: [{ hooks: [{ type: 'command', command: hookCmd('pre-compact.mjs') }] }],
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
const mergedSettings = mergeJson(settings, hooksConfig);
|
|
278
|
+
writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2));
|
|
279
|
+
ok('Merged hooks into .claude/settings.json');
|
|
280
|
+
// Create area docs stubs
|
|
281
|
+
for (const area of areas) {
|
|
282
|
+
const docPath = join(projectRoot, area.docPath);
|
|
283
|
+
if (!existsSync(docPath)) {
|
|
284
|
+
const docContent = `# ${area.name} — Architecture\n\n## Overview\n\n(Describe the ${area.name} subsystem)\n\n## Key Files\n\n${area.globs.map(g => `- \`${g}\``).join('\n')}\n\n## API Contracts\n\n(Document key interfaces and contracts)\n\n## Gotchas\n\n(Known footguns and non-obvious behavior)\n`;
|
|
285
|
+
mkdirSync(dirname(docPath), { recursive: true });
|
|
286
|
+
writeFileSync(docPath, docContent);
|
|
287
|
+
ok(`Created area doc: ${area.docPath}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
process.stdout.write('\n╔══════════════════════════════════════╗\n');
|
|
291
|
+
process.stdout.write('║ Setup complete! 🎉 ║\n');
|
|
292
|
+
process.stdout.write('╚══════════════════════════════════════╝\n\n');
|
|
293
|
+
log('Next steps:');
|
|
294
|
+
log(' 1. Open this project in Claude Code');
|
|
295
|
+
log(' 2. Run: context_status() to verify the MCP server is connected');
|
|
296
|
+
log(' 3. Run: context_get_bundle() to load your context');
|
|
297
|
+
log('');
|
|
298
|
+
log('Docs: Fill in docs/context/<area>.md for each subsystem you defined.');
|
|
299
|
+
log('');
|
|
300
|
+
}
|
|
301
|
+
function cmdRemove(projectRoot, opts = {}) {
|
|
302
|
+
log(`Removing ClaudeContext from: ${projectRoot}`);
|
|
303
|
+
const stateDir = join(projectRoot, '.claudecontext');
|
|
304
|
+
// Files always removed (hooks, config, mcp.json)
|
|
305
|
+
const toRemove = [
|
|
306
|
+
join(projectRoot, '.mcp.json'),
|
|
307
|
+
...['session-start', 'pre-tool-use', 'post-write', 'pre-compact'].flatMap(h => [
|
|
308
|
+
join(projectRoot, '.claude', 'hooks', `${h}.mjs`),
|
|
309
|
+
join(projectRoot, '.claude', 'hooks', `${h}.js`),
|
|
310
|
+
]),
|
|
311
|
+
join(stateDir, 'areas.json'),
|
|
312
|
+
join(stateDir, 'config.json'),
|
|
313
|
+
join(stateDir, 'gate-rules.json'),
|
|
314
|
+
join(stateDir, 'l0.log'),
|
|
315
|
+
join(stateDir, '.session.lock'),
|
|
316
|
+
];
|
|
317
|
+
// Files only removed with --purge
|
|
318
|
+
const toRemoveIfPurge = [
|
|
319
|
+
join(stateDir, 'state.db'),
|
|
320
|
+
stateDir,
|
|
321
|
+
];
|
|
322
|
+
if (opts.dryRun) {
|
|
323
|
+
log('Dry run — nothing will be deleted:');
|
|
324
|
+
for (const f of toRemove) {
|
|
325
|
+
if (existsSync(f))
|
|
326
|
+
log(` would remove: ${f}`);
|
|
327
|
+
}
|
|
328
|
+
if (opts.purge) {
|
|
329
|
+
for (const f of toRemoveIfPurge) {
|
|
330
|
+
if (existsSync(f))
|
|
331
|
+
log(` would remove: ${f}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
warn(`Would keep: ${join(stateDir, 'state.db')} (pass --purge to delete all state)`);
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const removeOne = (path) => {
|
|
340
|
+
if (existsSync(path)) {
|
|
341
|
+
rmSync(path, { recursive: true, force: true });
|
|
342
|
+
ok(`Removed: ${path}`);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
for (const f of toRemove)
|
|
346
|
+
removeOne(f);
|
|
347
|
+
if (opts.purge) {
|
|
348
|
+
for (const f of toRemoveIfPurge)
|
|
349
|
+
removeOne(f);
|
|
350
|
+
ok('All state deleted (--purge)');
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
warn(`Kept ${join(stateDir, 'state.db')} — pass --purge to delete all state`);
|
|
354
|
+
}
|
|
355
|
+
ok('Removal complete');
|
|
356
|
+
}
|
|
357
|
+
async function cmdMigrate(projectRoot) {
|
|
358
|
+
const dbPath = join(projectRoot, '.claudecontext', 'state.db');
|
|
359
|
+
if (!existsSync(dbPath)) {
|
|
360
|
+
warn('No state.db found — nothing to migrate');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Import Db dynamically to avoid errors if node:sqlite unavailable
|
|
364
|
+
try {
|
|
365
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
366
|
+
const db = new DatabaseSync(dbPath);
|
|
367
|
+
// Add columns if missing — SQLite does not support IF NOT EXISTS in ALTER TABLE,
|
|
368
|
+
// so we catch the "already has column" error specifically.
|
|
369
|
+
const migrations = [
|
|
370
|
+
{ col: 'co_occurrence_count', sql: `ALTER TABLE edges ADD COLUMN co_occurrence_count INTEGER DEFAULT 1` },
|
|
371
|
+
{ col: 'confidence', sql: `ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0` },
|
|
372
|
+
];
|
|
373
|
+
for (const { col, sql } of migrations) {
|
|
374
|
+
try {
|
|
375
|
+
db.exec(sql);
|
|
376
|
+
ok(`Migration applied: ${col}`);
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
380
|
+
if (msg.includes('already has column')) {
|
|
381
|
+
ok(`Column ${col} already present — skipping`);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
throw e; // re-throw unexpected errors
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
db.close();
|
|
389
|
+
ok('Migration complete');
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
err(`Migration failed: ${e}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function cmdDoctor(projectRoot) {
|
|
396
|
+
let allOk = true;
|
|
397
|
+
const check = (label, ok_, hint) => {
|
|
398
|
+
if (ok_) {
|
|
399
|
+
ok(label);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
err(`${label}${hint ? ` — ${hint}` : ''}`);
|
|
403
|
+
allOk = false;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
process.stdout.write('\n── ClaudeContext Doctor ─────────────────\n\n');
|
|
407
|
+
// Node version
|
|
408
|
+
const nodeVersion = process.version;
|
|
409
|
+
const major = parseInt(nodeVersion.slice(1), 10);
|
|
410
|
+
check(`Node.js version: ${nodeVersion}`, major >= 22, 'Node 22+ required (node:sqlite)');
|
|
411
|
+
// State directory
|
|
412
|
+
const stateDir = join(projectRoot, '.claudecontext');
|
|
413
|
+
check('.claudecontext/ exists', existsSync(stateDir), 'Run: claudecontext init');
|
|
414
|
+
// Database
|
|
415
|
+
const dbPath = join(stateDir, 'state.db');
|
|
416
|
+
check('.claudecontext/state.db exists', existsSync(dbPath), 'Run: claudecontext init');
|
|
417
|
+
// .mcp.json
|
|
418
|
+
const mcpPath = join(projectRoot, '.mcp.json');
|
|
419
|
+
check('.mcp.json exists', existsSync(mcpPath), 'Run: claudecontext init');
|
|
420
|
+
if (existsSync(mcpPath)) {
|
|
421
|
+
try {
|
|
422
|
+
const mcp = JSON.parse(readFileSync(mcpPath, 'utf8'));
|
|
423
|
+
const server = mcp?.mcpServers?.claudecontext;
|
|
424
|
+
check('.mcp.json uses npx (not absolute path)', server?.command === 'npx', 'Absolute paths break on npm update');
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
check('.mcp.json is valid JSON', false);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// On Windows: verify node is in PATH (required for hook invocation)
|
|
431
|
+
if (isWindows) {
|
|
432
|
+
try {
|
|
433
|
+
execSync('node --version', { stdio: 'ignore' });
|
|
434
|
+
ok('node is in PATH (required for hooks on Windows)');
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
err('node not found in PATH — hooks use "node .claude/hooks/..." and require node in PATH');
|
|
438
|
+
allOk = false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Hook scripts — existence + execute permissions
|
|
442
|
+
const hooksDir = join(projectRoot, '.claude', 'hooks');
|
|
443
|
+
const hookFiles = ['session-start.mjs', 'pre-tool-use.mjs', 'post-write.mjs', 'pre-compact.mjs'];
|
|
444
|
+
for (const hookFile of hookFiles) {
|
|
445
|
+
const hookPath = join(hooksDir, hookFile);
|
|
446
|
+
const exists = existsSync(hookPath);
|
|
447
|
+
check(`Hook ${hookFile} exists`, exists, `Run: claudecontext init`);
|
|
448
|
+
if (exists && !isWindows) {
|
|
449
|
+
try {
|
|
450
|
+
const { statSync } = await import('node:fs');
|
|
451
|
+
const stats = statSync(hookPath);
|
|
452
|
+
const isExecutable = (stats.mode & 0o111) !== 0;
|
|
453
|
+
check(`Hook ${hookFile} is executable`, isExecutable, `Run: chmod +x .claude/hooks/${hookFile}`);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
check(`Hook ${hookFile} is executable`, false, `Cannot stat file`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// areas.json
|
|
461
|
+
const areasPath = join(stateDir, 'areas.json');
|
|
462
|
+
check('.claudecontext/areas.json exists', existsSync(areasPath), 'Run: claudecontext init to configure areas');
|
|
463
|
+
if (existsSync(areasPath)) {
|
|
464
|
+
try {
|
|
465
|
+
const existingAreas = JSON.parse(readFileSync(areasPath, 'utf8'));
|
|
466
|
+
if (Array.isArray(existingAreas) && existingAreas.length === 0) {
|
|
467
|
+
const detectable = detectAreas(projectRoot);
|
|
468
|
+
if (detectable.length > 0) {
|
|
469
|
+
warn(`areas.json is empty — ${detectable.length} area(s) detected (${detectable.map(a => a.name).join(', ')}). Run: claudecontext auto-areas`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
catch { /* ignore parse errors — already reported */ }
|
|
474
|
+
}
|
|
475
|
+
// config.json (optional — validate if present)
|
|
476
|
+
const configPath = join(stateDir, 'config.json');
|
|
477
|
+
if (existsSync(configPath)) {
|
|
478
|
+
try {
|
|
479
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
480
|
+
const budgets = cfg.budgets;
|
|
481
|
+
if (budgets !== undefined) {
|
|
482
|
+
const budgetKeys = ['L0', 'L1', 'L2', 'L3', 'L4', 'graph', 'total'];
|
|
483
|
+
const invalidKeys = Object.keys(budgets).filter(k => !budgetKeys.includes(k));
|
|
484
|
+
const invalidValues = Object.entries(budgets)
|
|
485
|
+
.filter(([, v]) => typeof v !== 'number' || v <= 0 || !Number.isInteger(v));
|
|
486
|
+
const totalOver = typeof budgets.total === 'number' && budgets.total > 200_000;
|
|
487
|
+
if (invalidKeys.length > 0) {
|
|
488
|
+
err(`.claudecontext/config.json budgets has unknown keys: ${invalidKeys.join(', ')}`);
|
|
489
|
+
allOk = false;
|
|
490
|
+
}
|
|
491
|
+
else if (invalidValues.length > 0) {
|
|
492
|
+
err(`.claudecontext/config.json budgets values must be positive integers`);
|
|
493
|
+
allOk = false;
|
|
494
|
+
}
|
|
495
|
+
else if (totalOver) {
|
|
496
|
+
err(`.claudecontext/config.json total budget > 200000 — this may cause performance issues`);
|
|
497
|
+
allOk = false;
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
ok('.claudecontext/config.json is valid');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
ok('.claudecontext/config.json is valid (no budget overrides)');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
err('.claudecontext/config.json is not valid JSON — run: claudecontext init');
|
|
509
|
+
allOk = false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// gate-rules.json (optional — skip if missing)
|
|
513
|
+
const gateRulesPath = join(stateDir, 'gate-rules.json');
|
|
514
|
+
if (existsSync(gateRulesPath)) {
|
|
515
|
+
try {
|
|
516
|
+
const rules = JSON.parse(readFileSync(gateRulesPath, 'utf8'));
|
|
517
|
+
const valid = Array.isArray(rules) && rules.every((r) => r && typeof r === 'object' &&
|
|
518
|
+
'action' in r && 'pattern' in r && 'reason' in r);
|
|
519
|
+
if (valid) {
|
|
520
|
+
ok('.claudecontext/gate-rules.json is valid');
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
err('.claudecontext/gate-rules.json has invalid entries — each entry needs { action, pattern, reason }');
|
|
524
|
+
allOk = false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
err('.claudecontext/gate-rules.json is not valid JSON — run: claudecontext init');
|
|
529
|
+
allOk = false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
process.stdout.write('\n');
|
|
533
|
+
if (allOk) {
|
|
534
|
+
ok('All checks passed — ClaudeContext is healthy');
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
warn('Some checks failed — run: claudecontext init to fix');
|
|
538
|
+
}
|
|
539
|
+
process.stdout.write('\n');
|
|
540
|
+
}
|
|
541
|
+
async function cmdAutoAreas(projectRoot, dryRun) {
|
|
542
|
+
process.stdout.write('\n── Auto-detecting areas ─────────────────\n\n');
|
|
543
|
+
log(`Scanning project structure in: ${projectRoot}`);
|
|
544
|
+
const proposed = detectAreas(projectRoot);
|
|
545
|
+
if (proposed.length === 0) {
|
|
546
|
+
warn('No detectable areas found — make sure src/ (or lib/, app/) has subdirectories with ≥3 source files.');
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
process.stdout.write('\nProposed areas.json entries:\n');
|
|
550
|
+
for (const area of proposed) {
|
|
551
|
+
process.stdout.write(` • ${area.name}: globs=${area.globs.join(', ')} → ${area.docPath}\n`);
|
|
552
|
+
}
|
|
553
|
+
if (dryRun) {
|
|
554
|
+
process.stdout.write('\n(dry-run — no changes written)\n\n');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
// Merge with existing areas (don't overwrite user-defined globs for same-name areas)
|
|
558
|
+
const stateDir = join(projectRoot, '.claudecontext');
|
|
559
|
+
const areasPath = join(stateDir, 'areas.json');
|
|
560
|
+
let existing = [];
|
|
561
|
+
if (existsSync(areasPath)) {
|
|
562
|
+
try {
|
|
563
|
+
existing = JSON.parse(readFileSync(areasPath, 'utf8'));
|
|
564
|
+
}
|
|
565
|
+
catch { /* ignore */ }
|
|
566
|
+
}
|
|
567
|
+
const existingNames = new Set(existing.map(a => a.name));
|
|
568
|
+
const toAdd = proposed.filter(a => !existingNames.has(a.name));
|
|
569
|
+
if (toAdd.length === 0) {
|
|
570
|
+
ok('All detected areas are already configured.');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const merged = [...existing, ...toAdd];
|
|
574
|
+
if (!existsSync(stateDir))
|
|
575
|
+
mkdirSync(stateDir, { recursive: true });
|
|
576
|
+
writeFileSync(areasPath, JSON.stringify(merged, null, 2));
|
|
577
|
+
ok(`Added ${toAdd.length} area(s) to .claudecontext/areas.json: ${toAdd.map(a => a.name).join(', ')}`);
|
|
578
|
+
log('Run `claudecontext doctor` to verify.');
|
|
579
|
+
process.stdout.write('\n');
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Register claudecontext as a user-scoped (global) MCP server.
|
|
583
|
+
*
|
|
584
|
+
* This means it's available in every project without per-project .mcp.json.
|
|
585
|
+
* Uses `claude mcp add --scope user` which writes to ~/.claude.json.
|
|
586
|
+
*
|
|
587
|
+
* Hooks are still per-project — run `claudecontext init` in each project for hooks.
|
|
588
|
+
*/
|
|
589
|
+
async function cmdGlobalInstall() {
|
|
590
|
+
process.stdout.write('\n╔══════════════════════════════════════╗\n');
|
|
591
|
+
process.stdout.write('║ ClaudeContext Global Install ║\n');
|
|
592
|
+
process.stdout.write('╚══════════════════════════════════════╝\n\n');
|
|
593
|
+
log('This registers claudecontext as a user-scoped MCP server.');
|
|
594
|
+
log('After this, the MCP server is available in ALL your projects automatically.');
|
|
595
|
+
log('');
|
|
596
|
+
// Check Node version
|
|
597
|
+
const nodeVersion = process.version;
|
|
598
|
+
const major = parseInt(nodeVersion.slice(1), 10);
|
|
599
|
+
if (major < 22) {
|
|
600
|
+
err(`Node.js ${nodeVersion} detected — Node 22+ required. Please upgrade.`);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
ok(`Node.js ${nodeVersion}`);
|
|
604
|
+
// Check claude CLI is available (Windows uses claude.cmd)
|
|
605
|
+
const claudeBin = isWindows ? 'claude.cmd' : 'claude';
|
|
606
|
+
try {
|
|
607
|
+
execSync(`${claudeBin} --version`, { stdio: 'pipe' });
|
|
608
|
+
ok('claude CLI found');
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
err('claude CLI not found — install it with: npm install -g @anthropic-ai/claude-code');
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
// Register as user-scoped MCP server
|
|
615
|
+
// The MCP server uses process.cwd() as project root, which Claude Code sets to the project directory.
|
|
616
|
+
log('Registering with claude mcp add --scope user ...');
|
|
617
|
+
try {
|
|
618
|
+
execSync(`${claudeBin} mcp add --transport stdio --scope user claudecontext -- npx --no-warnings claudecontext-mcp`, { stdio: 'inherit' });
|
|
619
|
+
ok('claudecontext registered as global MCP server');
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
warn('claude mcp add failed — attempting manual registration in ~/.claude.json ...');
|
|
623
|
+
// Fallback: write directly to ~/.claude.json
|
|
624
|
+
const homeDir = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
|
|
625
|
+
const claudeJsonPath = join(homeDir, '.claude.json');
|
|
626
|
+
let claudeJson = {};
|
|
627
|
+
if (existsSync(claudeJsonPath)) {
|
|
628
|
+
try {
|
|
629
|
+
claudeJson = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
|
|
630
|
+
}
|
|
631
|
+
catch { /* ignore */ }
|
|
632
|
+
}
|
|
633
|
+
const mcpServers = claudeJson['mcpServers'] ?? {};
|
|
634
|
+
mcpServers['claudecontext'] = {
|
|
635
|
+
type: 'stdio',
|
|
636
|
+
command: 'npx',
|
|
637
|
+
args: ['claudecontext-mcp'],
|
|
638
|
+
env: { NODE_OPTIONS: '--no-warnings' },
|
|
639
|
+
};
|
|
640
|
+
claudeJson['mcpServers'] = mcpServers;
|
|
641
|
+
writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
|
|
642
|
+
ok(`Written to ${claudeJsonPath}`);
|
|
643
|
+
}
|
|
644
|
+
process.stdout.write('\n╔══════════════════════════════════════╗\n');
|
|
645
|
+
process.stdout.write('║ Global install complete! ║\n');
|
|
646
|
+
process.stdout.write('╚══════════════════════════════════════╝\n\n');
|
|
647
|
+
log('The claudecontext MCP server is now available in all your projects.');
|
|
648
|
+
log('');
|
|
649
|
+
log('To verify: open any project in Claude Code and run context_status()');
|
|
650
|
+
log('');
|
|
651
|
+
log('Note: Hooks (SessionStart, PreToolUse, etc.) are still per-project.');
|
|
652
|
+
log('Run `claudecontext init` in each project to install hooks.');
|
|
653
|
+
log('');
|
|
654
|
+
}
|
|
655
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
656
|
+
const args = process.argv.slice(2);
|
|
657
|
+
const command = args[0];
|
|
658
|
+
const projectRootArg = args.indexOf('--project-root');
|
|
659
|
+
const projectRoot = projectRootArg !== -1
|
|
660
|
+
? resolve(args[projectRootArg + 1] ?? process.cwd())
|
|
661
|
+
: resolve(process.cwd());
|
|
662
|
+
switch (command) {
|
|
663
|
+
case 'init': {
|
|
664
|
+
const yes = args.includes('--yes');
|
|
665
|
+
const configIdx = args.indexOf('--config');
|
|
666
|
+
const config = configIdx !== -1 ? args[configIdx + 1] : undefined;
|
|
667
|
+
const initOpts = {};
|
|
668
|
+
if (yes)
|
|
669
|
+
initOpts.yes = true;
|
|
670
|
+
if (config)
|
|
671
|
+
initOpts.config = config;
|
|
672
|
+
await cmdInit(projectRoot, initOpts);
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
case 'global-install':
|
|
676
|
+
await cmdGlobalInstall();
|
|
677
|
+
break;
|
|
678
|
+
case 'remove': {
|
|
679
|
+
const dryRun = args.includes('--dry-run');
|
|
680
|
+
const purge = args.includes('--purge');
|
|
681
|
+
cmdRemove(projectRoot, { dryRun, purge });
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
case 'migrate':
|
|
685
|
+
await cmdMigrate(projectRoot);
|
|
686
|
+
break;
|
|
687
|
+
case 'doctor':
|
|
688
|
+
await cmdDoctor(projectRoot);
|
|
689
|
+
break;
|
|
690
|
+
case 'auto-areas': {
|
|
691
|
+
const dryRun = args.includes('--dry-run');
|
|
692
|
+
await cmdAutoAreas(projectRoot, dryRun);
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
default:
|
|
696
|
+
process.stdout.write(`
|
|
697
|
+
ClaudeContext — Persistent memory for Claude Code
|
|
698
|
+
|
|
699
|
+
Commands:
|
|
700
|
+
claudecontext init [--project-root <path>] Set up ClaudeContext in a project (interactive)
|
|
701
|
+
--yes Skip prompts, use auto-detected defaults
|
|
702
|
+
--config <file> Load setup from a JSON config file (non-interactive)
|
|
703
|
+
claudecontext global-install Register as global MCP (available in all projects)
|
|
704
|
+
claudecontext remove Remove hooks and config (preserves state.db)
|
|
705
|
+
--dry-run Show what would be removed without deleting
|
|
706
|
+
--purge Also delete state.db and .claudecontext/
|
|
707
|
+
claudecontext migrate Migrate database schema
|
|
708
|
+
claudecontext doctor Check installation health
|
|
709
|
+
claudecontext auto-areas [--dry-run] Auto-detect subsystem areas from directory structure
|
|
710
|
+
|
|
711
|
+
`);
|
|
712
|
+
process.exit(command ? 1 : 0);
|
|
713
|
+
}
|
|
714
|
+
//# sourceMappingURL=install.js.map
|