@regression-io/claude-config 0.32.1 → 0.32.2
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/config-loader.js +172 -2882
- package/package.json +1 -1
- package/ui/dist/assets/{index-CZbHWyrm.js → index-BM67y5Li.js} +892 -892
- package/ui/dist/index.html +1 -1
- package/ui/server.cjs +219 -3909
package/config-loader.js
CHANGED
|
@@ -17,57 +17,23 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
-
const { execSync } = require('child_process');
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
supportsEnvInterpolation: true,
|
|
39
|
-
},
|
|
40
|
-
gemini: {
|
|
41
|
-
name: 'Gemini CLI',
|
|
42
|
-
icon: 'terminal',
|
|
43
|
-
color: 'blue',
|
|
44
|
-
globalConfig: '~/.gemini/settings.json', // MCP config is merged into settings.json under mcpServers key
|
|
45
|
-
globalSettings: '~/.gemini/settings.json',
|
|
46
|
-
globalMcpConfig: '~/.gemini/mcps.json', // Source config for MCPs (like Claude's)
|
|
47
|
-
projectFolder: '.gemini',
|
|
48
|
-
projectConfig: '.gemini/mcps.json', // Project-level MCP config
|
|
49
|
-
projectRules: '.gemini',
|
|
50
|
-
projectCommands: '.gemini/commands', // Uses TOML format
|
|
51
|
-
projectInstructions: 'GEMINI.md',
|
|
52
|
-
outputFile: '~/.gemini/settings.json', // Output merged into global settings
|
|
53
|
-
supportsEnvInterpolation: true, // Gemini CLI likely supports ${VAR}
|
|
54
|
-
mergeIntoSettings: true, // MCP config is merged into settings.json, not standalone
|
|
55
|
-
},
|
|
56
|
-
antigravity: {
|
|
57
|
-
name: 'Antigravity',
|
|
58
|
-
icon: 'rocket',
|
|
59
|
-
color: 'purple',
|
|
60
|
-
globalConfig: '~/.gemini/antigravity/mcp_config.json',
|
|
61
|
-
globalMcpConfig: '~/.gemini/antigravity/mcps.json', // Source config for MCPs
|
|
62
|
-
globalRules: '~/.gemini/GEMINI.md',
|
|
63
|
-
projectFolder: '.agent',
|
|
64
|
-
projectConfig: '.agent/mcps.json', // Project-level MCP config
|
|
65
|
-
projectRules: '.agent/rules',
|
|
66
|
-
projectInstructions: 'GEMINI.md',
|
|
67
|
-
outputFile: '~/.gemini/antigravity/mcp_config.json', // Output to global config
|
|
68
|
-
supportsEnvInterpolation: false, // Must resolve to actual values
|
|
69
|
-
},
|
|
70
|
-
};
|
|
21
|
+
// Import from modular lib
|
|
22
|
+
const { VERSION, TOOL_PATHS } = require('./lib/constants');
|
|
23
|
+
const { loadJson, saveJson, loadEnvFile, interpolate, resolveEnvVars, copyDirRecursive } = require('./lib/utils');
|
|
24
|
+
const { findProjectRoot, findAllConfigs, mergeConfigs, getConfigPath, collectFilesFromHierarchy, findAllConfigsForTool } = require('./lib/config');
|
|
25
|
+
const { listTemplates, findTemplate, resolveTemplateChain, copyTemplateFiles, trackAppliedTemplate, getAppliedTemplate } = require('./lib/templates');
|
|
26
|
+
const { apply, applyForAntigravity, applyForGemini, detectInstalledTools, applyForTools } = require('./lib/apply');
|
|
27
|
+
const { list, add, remove } = require('./lib/mcps');
|
|
28
|
+
const { registryAdd, registryRemove } = require('./lib/registry');
|
|
29
|
+
const { init, applyTemplate, show } = require('./lib/init');
|
|
30
|
+
const { memoryList, memoryInit, memoryAdd, memorySearch } = require('./lib/memory');
|
|
31
|
+
const { envList, envSet, envUnset } = require('./lib/env');
|
|
32
|
+
const { getProjectsRegistryPath, loadProjectsRegistry, saveProjectsRegistry, projectList, projectAdd, projectRemove } = require('./lib/projects');
|
|
33
|
+
const { getWorkstreamsPath, loadWorkstreams, saveWorkstreams, workstreamList, workstreamCreate, workstreamUpdate, workstreamDelete, workstreamUse, workstreamActive, workstreamAddProject, workstreamRemoveProject, workstreamInject, workstreamDetect, workstreamGet } = require('./lib/workstreams');
|
|
34
|
+
const { getActivityPath, getDefaultActivity, loadActivity, saveActivity, detectProjectRoot, activityLog, activitySummary, generateWorkstreamName, activitySuggestWorkstreams, activityClear } = require('./lib/activity');
|
|
35
|
+
const { getSmartSyncPath, loadSmartSyncPrefs, saveSmartSyncPrefs, smartSyncRememberChoice, smartSyncDismissNudge, smartSyncUpdateSettings, smartSyncDetect, smartSyncCheckNudge, smartSyncHandleAction, smartSyncStatus } = require('./lib/smart-sync');
|
|
36
|
+
const { runCli } = require('./lib/cli');
|
|
71
37
|
|
|
72
38
|
class ClaudeConfigManager {
|
|
73
39
|
constructor() {
|
|
@@ -89,2867 +55,191 @@ class ClaudeConfigManager {
|
|
|
89
55
|
this.templatesDir = templatePaths.find(p => fs.existsSync(p)) || templatePaths[0];
|
|
90
56
|
}
|
|
91
57
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} catch (error) {
|
|
100
|
-
console.error(`Error loading ${filePath}:`, error.message);
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Save JSON file
|
|
107
|
-
*/
|
|
108
|
-
saveJson(filePath, data) {
|
|
109
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Load environment variables from .env file
|
|
114
|
-
*/
|
|
115
|
-
loadEnvFile(envPath) {
|
|
116
|
-
if (!fs.existsSync(envPath)) return {};
|
|
117
|
-
const envVars = {};
|
|
118
|
-
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
119
|
-
for (const line of lines) {
|
|
120
|
-
const trimmed = line.trim();
|
|
121
|
-
if (trimmed && !trimmed.startsWith('#')) {
|
|
122
|
-
const eqIndex = trimmed.indexOf('=');
|
|
123
|
-
if (eqIndex > 0) {
|
|
124
|
-
const key = trimmed.substring(0, eqIndex).trim();
|
|
125
|
-
let value = trimmed.substring(eqIndex + 1).trim();
|
|
126
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
127
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
128
|
-
value = value.slice(1, -1);
|
|
129
|
-
}
|
|
130
|
-
envVars[key] = value;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return envVars;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Interpolate ${VAR} in object values
|
|
139
|
-
*/
|
|
140
|
-
interpolate(obj, env) {
|
|
141
|
-
if (typeof obj === 'string') {
|
|
142
|
-
return obj.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
143
|
-
return env[varName] || process.env[varName] || match;
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
if (Array.isArray(obj)) {
|
|
147
|
-
return obj.map(v => this.interpolate(v, env));
|
|
148
|
-
}
|
|
149
|
-
if (obj !== null && typeof obj === 'object') {
|
|
150
|
-
const result = {};
|
|
151
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
152
|
-
result[k] = this.interpolate(v, env);
|
|
153
|
-
}
|
|
154
|
-
return result;
|
|
155
|
-
}
|
|
156
|
-
return obj;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Find project root (has .claude/ directory)
|
|
161
|
-
*/
|
|
162
|
-
findProjectRoot(startDir = process.cwd()) {
|
|
163
|
-
let dir = path.resolve(startDir);
|
|
164
|
-
const root = path.parse(dir).root;
|
|
165
|
-
while (dir !== root) {
|
|
166
|
-
if (fs.existsSync(path.join(dir, '.claude'))) {
|
|
167
|
-
return dir;
|
|
168
|
-
}
|
|
169
|
-
dir = path.dirname(dir);
|
|
170
|
-
}
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Find ALL .claude/mcps.json configs from cwd up to root (and ~/.claude)
|
|
176
|
-
* Returns array from root to leaf (so child overrides parent when merged)
|
|
177
|
-
*/
|
|
178
|
-
findAllConfigs(startDir = process.cwd()) {
|
|
179
|
-
const configs = [];
|
|
180
|
-
let dir = path.resolve(startDir);
|
|
181
|
-
const root = path.parse(dir).root;
|
|
182
|
-
const homeDir = process.env.HOME || '';
|
|
183
|
-
|
|
184
|
-
// Walk up directory tree
|
|
185
|
-
while (dir !== root) {
|
|
186
|
-
const configPath = path.join(dir, '.claude', 'mcps.json');
|
|
187
|
-
if (fs.existsSync(configPath)) {
|
|
188
|
-
configs.unshift({ dir, configPath }); // Add at beginning (root first)
|
|
189
|
-
}
|
|
190
|
-
dir = path.dirname(dir);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Also check ~/.claude/mcps.json (global user config)
|
|
194
|
-
const homeConfig = path.join(homeDir, '.claude', 'mcps.json');
|
|
195
|
-
if (fs.existsSync(homeConfig)) {
|
|
196
|
-
// Only add if not already included
|
|
197
|
-
if (!configs.some(c => c.configPath === homeConfig)) {
|
|
198
|
-
configs.unshift({ dir: homeDir, configPath: homeConfig });
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return configs;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Merge multiple configs (later ones override earlier)
|
|
207
|
-
*/
|
|
208
|
-
mergeConfigs(configs) {
|
|
209
|
-
const merged = {
|
|
210
|
-
include: [],
|
|
211
|
-
mcpServers: {},
|
|
212
|
-
template: null
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
for (const { config } of configs) {
|
|
216
|
-
if (!config) continue;
|
|
217
|
-
|
|
218
|
-
// Merge include arrays (dedupe)
|
|
219
|
-
if (config.include && Array.isArray(config.include)) {
|
|
220
|
-
for (const mcp of config.include) {
|
|
221
|
-
if (!merged.include.includes(mcp)) {
|
|
222
|
-
merged.include.push(mcp);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Merge mcpServers (override)
|
|
228
|
-
if (config.mcpServers) {
|
|
229
|
-
Object.assign(merged.mcpServers, config.mcpServers);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Take the most specific template
|
|
233
|
-
if (config.template) {
|
|
234
|
-
merged.template = config.template;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return merged;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Get project config path
|
|
243
|
-
*/
|
|
244
|
-
getConfigPath(projectDir = null) {
|
|
245
|
-
const dir = projectDir || this.findProjectRoot() || process.cwd();
|
|
246
|
-
return path.join(dir, '.claude', 'mcps.json');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Collect files (rules or commands) from all directories in hierarchy
|
|
251
|
-
* Returns array of { file, source, fullPath } with child files overriding parent
|
|
252
|
-
*/
|
|
253
|
-
collectFilesFromHierarchy(configLocations, subdir) {
|
|
254
|
-
const fileMap = new Map(); // filename -> { file, source, fullPath }
|
|
255
|
-
|
|
256
|
-
// Process from root to leaf (so child overrides parent)
|
|
257
|
-
for (const { dir } of configLocations) {
|
|
258
|
-
const dirPath = path.join(dir, '.claude', subdir);
|
|
259
|
-
if (fs.existsSync(dirPath)) {
|
|
260
|
-
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md'));
|
|
261
|
-
for (const file of files) {
|
|
262
|
-
fileMap.set(file, {
|
|
263
|
-
file,
|
|
264
|
-
source: dir,
|
|
265
|
-
fullPath: path.join(dirPath, file)
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
58
|
+
// Utils
|
|
59
|
+
loadJson(filePath) { return loadJson(filePath); }
|
|
60
|
+
saveJson(filePath, data) { return saveJson(filePath, data); }
|
|
61
|
+
loadEnvFile(envPath) { return loadEnvFile(envPath); }
|
|
62
|
+
interpolate(obj, env) { return interpolate(obj, env); }
|
|
63
|
+
resolveEnvVars(obj, env) { return resolveEnvVars(obj, env); }
|
|
64
|
+
copyDirRecursive(src, dest) { return copyDirRecursive(src, dest); }
|
|
270
65
|
|
|
271
|
-
|
|
272
|
-
}
|
|
66
|
+
// Config
|
|
67
|
+
findProjectRoot(startDir) { return findProjectRoot(startDir); }
|
|
68
|
+
findAllConfigs(startDir) { return findAllConfigs(startDir); }
|
|
69
|
+
mergeConfigs(configs) { return mergeConfigs(configs); }
|
|
70
|
+
getConfigPath(projectDir) { return getConfigPath(this.installDir, projectDir); }
|
|
71
|
+
collectFilesFromHierarchy(configLocations, subdir) { return collectFilesFromHierarchy(configLocations, subdir); }
|
|
72
|
+
findAllConfigsForTool(toolId, startDir) { return findAllConfigsForTool(toolId, startDir); }
|
|
273
73
|
|
|
274
|
-
/**
|
|
275
|
-
* Get all rules from hierarchy (for external use)
|
|
276
|
-
*/
|
|
277
74
|
getAllRules(startDir = process.cwd()) {
|
|
278
|
-
const configLocations =
|
|
279
|
-
return
|
|
75
|
+
const configLocations = findAllConfigs(startDir);
|
|
76
|
+
return collectFilesFromHierarchy(configLocations, 'rules');
|
|
280
77
|
}
|
|
281
78
|
|
|
282
|
-
/**
|
|
283
|
-
* Get all commands from hierarchy (for external use)
|
|
284
|
-
*/
|
|
285
79
|
getAllCommands(startDir = process.cwd()) {
|
|
286
|
-
const configLocations =
|
|
287
|
-
return
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Copy template files to project (won't overwrite existing)
|
|
388
|
-
*/
|
|
389
|
-
copyTemplateFiles(templatePath, projectDir, options = {}) {
|
|
390
|
-
const { force = false, verbose = true } = options;
|
|
391
|
-
const rulesDir = path.join(templatePath, 'rules');
|
|
392
|
-
const commandsDir = path.join(templatePath, 'commands');
|
|
393
|
-
const projectRulesDir = path.join(projectDir, '.claude', 'rules');
|
|
394
|
-
const projectCommandsDir = path.join(projectDir, '.claude', 'commands');
|
|
395
|
-
|
|
396
|
-
let copied = 0;
|
|
397
|
-
let skipped = 0;
|
|
398
|
-
|
|
399
|
-
// Copy rules
|
|
400
|
-
if (fs.existsSync(rulesDir)) {
|
|
401
|
-
if (!fs.existsSync(projectRulesDir)) {
|
|
402
|
-
fs.mkdirSync(projectRulesDir, { recursive: true });
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
for (const file of fs.readdirSync(rulesDir)) {
|
|
406
|
-
if (!file.endsWith('.md')) continue;
|
|
407
|
-
const src = path.join(rulesDir, file);
|
|
408
|
-
const dest = path.join(projectRulesDir, file);
|
|
409
|
-
|
|
410
|
-
if (fs.existsSync(dest) && !force) {
|
|
411
|
-
skipped++;
|
|
412
|
-
if (verbose) console.log(` ⏭ rules/${file} (exists)`);
|
|
413
|
-
} else {
|
|
414
|
-
fs.copyFileSync(src, dest);
|
|
415
|
-
copied++;
|
|
416
|
-
if (verbose) console.log(` ✓ rules/${file}`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Copy commands
|
|
422
|
-
if (fs.existsSync(commandsDir)) {
|
|
423
|
-
if (!fs.existsSync(projectCommandsDir)) {
|
|
424
|
-
fs.mkdirSync(projectCommandsDir, { recursive: true });
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
for (const file of fs.readdirSync(commandsDir)) {
|
|
428
|
-
if (!file.endsWith('.md')) continue;
|
|
429
|
-
const src = path.join(commandsDir, file);
|
|
430
|
-
const dest = path.join(projectCommandsDir, file);
|
|
431
|
-
|
|
432
|
-
if (fs.existsSync(dest) && !force) {
|
|
433
|
-
skipped++;
|
|
434
|
-
if (verbose) console.log(` ⏭ commands/${file} (exists)`);
|
|
435
|
-
} else {
|
|
436
|
-
fs.copyFileSync(src, dest);
|
|
437
|
-
copied++;
|
|
438
|
-
if (verbose) console.log(` ✓ commands/${file}`);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
return { copied, skipped };
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// ===========================================================================
|
|
447
|
-
// CORE COMMANDS
|
|
448
|
-
// ===========================================================================
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Generate .mcp.json for a project (with hierarchical config merging)
|
|
452
|
-
*/
|
|
453
|
-
apply(projectDir = null) {
|
|
454
|
-
const dir = projectDir || this.findProjectRoot() || process.cwd();
|
|
455
|
-
|
|
456
|
-
const registry = this.loadJson(this.registryPath);
|
|
457
|
-
if (!registry) {
|
|
458
|
-
console.error('Error: Could not load MCP registry from', this.registryPath);
|
|
459
|
-
return false;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Find and load all configs in hierarchy
|
|
463
|
-
const configLocations = this.findAllConfigs(dir);
|
|
464
|
-
|
|
465
|
-
if (configLocations.length === 0) {
|
|
466
|
-
console.error(`No .claude/mcps.json found in ${dir} or parent directories`);
|
|
467
|
-
console.error('Run: claude-config init');
|
|
468
|
-
return false;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Load all configs
|
|
472
|
-
const loadedConfigs = configLocations.map(loc => ({
|
|
473
|
-
...loc,
|
|
474
|
-
config: this.loadJson(loc.configPath)
|
|
475
|
-
}));
|
|
476
|
-
|
|
477
|
-
// Show config hierarchy if multiple configs found
|
|
478
|
-
if (loadedConfigs.length > 1) {
|
|
479
|
-
console.log('📚 Config hierarchy (merged):');
|
|
480
|
-
for (const { dir: d, configPath } of loadedConfigs) {
|
|
481
|
-
const relPath = d === process.env.HOME ? '~' : path.relative(process.cwd(), d) || '.';
|
|
482
|
-
console.log(` • ${relPath}/.claude/mcps.json`);
|
|
483
|
-
}
|
|
484
|
-
console.log('');
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Merge all configs
|
|
488
|
-
const mergedConfig = this.mergeConfigs(loadedConfigs);
|
|
489
|
-
|
|
490
|
-
// Collect env vars from all levels (child overrides parent)
|
|
491
|
-
const globalEnvPath = path.join(path.dirname(this.registryPath), '.env');
|
|
492
|
-
let env = this.loadEnvFile(globalEnvPath);
|
|
493
|
-
|
|
494
|
-
for (const { dir: d } of loadedConfigs) {
|
|
495
|
-
const envPath = path.join(d, '.claude', '.env');
|
|
496
|
-
env = { ...env, ...this.loadEnvFile(envPath) };
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const output = { mcpServers: {} };
|
|
500
|
-
|
|
501
|
-
// Add MCPs from include list
|
|
502
|
-
if (mergedConfig.include && Array.isArray(mergedConfig.include)) {
|
|
503
|
-
for (const name of mergedConfig.include) {
|
|
504
|
-
if (registry.mcpServers && registry.mcpServers[name]) {
|
|
505
|
-
output.mcpServers[name] = this.interpolate(registry.mcpServers[name], env);
|
|
506
|
-
} else {
|
|
507
|
-
console.warn(`Warning: MCP "${name}" not found in registry`);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Add custom mcpServers (override registry)
|
|
513
|
-
if (mergedConfig.mcpServers) {
|
|
514
|
-
for (const [name, config] of Object.entries(mergedConfig.mcpServers)) {
|
|
515
|
-
if (name.startsWith('_')) continue;
|
|
516
|
-
output.mcpServers[name] = this.interpolate(config, env);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const outputPath = path.join(dir, '.mcp.json');
|
|
521
|
-
this.saveJson(outputPath, output);
|
|
522
|
-
|
|
523
|
-
const count = Object.keys(output.mcpServers).length;
|
|
524
|
-
console.log(`✓ Generated ${outputPath}`);
|
|
525
|
-
console.log(` └─ ${count} MCP(s): ${Object.keys(output.mcpServers).join(', ')}`);
|
|
526
|
-
|
|
527
|
-
return true;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Resolve ${VAR} to actual values (for tools that don't support interpolation)
|
|
532
|
-
*/
|
|
533
|
-
resolveEnvVars(obj, env) {
|
|
534
|
-
if (typeof obj === 'string') {
|
|
535
|
-
return obj.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
536
|
-
const value = env[varName] || process.env[varName];
|
|
537
|
-
if (!value) {
|
|
538
|
-
console.warn(`Warning: Environment variable ${varName} not set`);
|
|
539
|
-
return ''; // Return empty instead of keeping ${VAR}
|
|
540
|
-
}
|
|
541
|
-
return value;
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
if (Array.isArray(obj)) {
|
|
545
|
-
return obj.map(v => this.resolveEnvVars(v, env));
|
|
546
|
-
}
|
|
547
|
-
if (obj && typeof obj === 'object') {
|
|
548
|
-
const result = {};
|
|
549
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
550
|
-
result[key] = this.resolveEnvVars(value, env);
|
|
551
|
-
}
|
|
552
|
-
return result;
|
|
553
|
-
}
|
|
554
|
-
return obj;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Generate MCP config for Antigravity
|
|
559
|
-
*/
|
|
560
|
-
/**
|
|
561
|
-
* Generate MCP config for Antigravity
|
|
562
|
-
* Reads from .agent/mcps.json (NOT .claude/mcps.json)
|
|
563
|
-
*/
|
|
564
|
-
applyForAntigravity(projectDir = null) {
|
|
565
|
-
const dir = projectDir || this.findProjectRoot() || process.cwd();
|
|
566
|
-
const paths = TOOL_PATHS.antigravity;
|
|
567
|
-
const homeDir = process.env.HOME || '';
|
|
568
|
-
|
|
569
|
-
const registry = this.loadJson(this.registryPath);
|
|
570
|
-
if (!registry) {
|
|
571
|
-
console.error('Error: Could not load MCP registry');
|
|
80
|
+
const configLocations = findAllConfigs(startDir);
|
|
81
|
+
return collectFilesFromHierarchy(configLocations, 'commands');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Templates
|
|
85
|
+
listTemplates() { return listTemplates(this.templatesDir); }
|
|
86
|
+
findTemplate(name) { return findTemplate(this.templatesDir, name); }
|
|
87
|
+
resolveTemplateChain(templatePath, visited) { return resolveTemplateChain(this.templatesDir, templatePath, visited); }
|
|
88
|
+
copyTemplateFiles(templatePath, projectDir, options) { return copyTemplateFiles(templatePath, projectDir, options); }
|
|
89
|
+
trackAppliedTemplate(dir, templateName) { return trackAppliedTemplate(dir, templateName); }
|
|
90
|
+
getAppliedTemplate(dir) { return getAppliedTemplate(dir); }
|
|
91
|
+
|
|
92
|
+
// Apply
|
|
93
|
+
apply(projectDir) { return apply(this.registryPath, projectDir); }
|
|
94
|
+
applyForAntigravity(projectDir) { return applyForAntigravity(this.registryPath, projectDir); }
|
|
95
|
+
applyForGemini(projectDir) { return applyForGemini(this.registryPath, projectDir); }
|
|
96
|
+
detectInstalledTools() { return detectInstalledTools(); }
|
|
97
|
+
getToolPaths() { return TOOL_PATHS; }
|
|
98
|
+
applyForTools(projectDir, tools) { return applyForTools(this.registryPath, projectDir, tools); }
|
|
99
|
+
|
|
100
|
+
// MCPs
|
|
101
|
+
list() { return list(this.registryPath); }
|
|
102
|
+
add(mcpNames) { return add(this.registryPath, this.installDir, mcpNames); }
|
|
103
|
+
remove(mcpNames) { return remove(this.installDir, mcpNames); }
|
|
104
|
+
|
|
105
|
+
// Registry
|
|
106
|
+
registryAdd(name, configJson) { return registryAdd(this.registryPath, name, configJson); }
|
|
107
|
+
registryRemove(name) { return registryRemove(this.registryPath, name); }
|
|
108
|
+
|
|
109
|
+
// Init
|
|
110
|
+
init(projectDir, templateName) { return init(this.templatesDir, this.registryPath, projectDir, templateName); }
|
|
111
|
+
applyTemplate(templateName, projectDir) { return applyTemplate(this.templatesDir, templateName, projectDir); }
|
|
112
|
+
show(projectDir) { return show(projectDir); }
|
|
113
|
+
|
|
114
|
+
// Memory
|
|
115
|
+
memoryList(projectDir) { return memoryList(projectDir); }
|
|
116
|
+
memoryInit(projectDir) { return memoryInit(projectDir); }
|
|
117
|
+
memoryAdd(type, content, projectDir) { return memoryAdd(type, content, projectDir); }
|
|
118
|
+
memorySearch(query, projectDir) { return memorySearch(query, projectDir); }
|
|
119
|
+
|
|
120
|
+
// Env
|
|
121
|
+
envList(projectDir) { return envList(projectDir); }
|
|
122
|
+
envSet(key, value, projectDir) { return envSet(key, value, projectDir); }
|
|
123
|
+
envUnset(key, projectDir) { return envUnset(key, projectDir); }
|
|
124
|
+
|
|
125
|
+
// Projects
|
|
126
|
+
getProjectsRegistryPath() { return getProjectsRegistryPath(this.installDir); }
|
|
127
|
+
loadProjectsRegistry() { return loadProjectsRegistry(this.installDir); }
|
|
128
|
+
saveProjectsRegistry(registry) { return saveProjectsRegistry(this.installDir, registry); }
|
|
129
|
+
projectList() { return projectList(this.installDir); }
|
|
130
|
+
projectAdd(projectPath, name) { return projectAdd(this.installDir, projectPath, name); }
|
|
131
|
+
projectRemove(nameOrPath) { return projectRemove(this.installDir, nameOrPath); }
|
|
132
|
+
|
|
133
|
+
// Workstreams
|
|
134
|
+
getWorkstreamsPath() { return getWorkstreamsPath(this.installDir); }
|
|
135
|
+
loadWorkstreams() { return loadWorkstreams(this.installDir); }
|
|
136
|
+
saveWorkstreams(data) { return saveWorkstreams(this.installDir, data); }
|
|
137
|
+
workstreamList() { return workstreamList(this.installDir); }
|
|
138
|
+
workstreamCreate(name, projects, rules) { return workstreamCreate(this.installDir, name, projects, rules); }
|
|
139
|
+
workstreamUpdate(idOrName, updates) { return workstreamUpdate(this.installDir, idOrName, updates); }
|
|
140
|
+
workstreamDelete(idOrName) { return workstreamDelete(this.installDir, idOrName); }
|
|
141
|
+
workstreamUse(idOrName) { return workstreamUse(this.installDir, idOrName); }
|
|
142
|
+
workstreamActive() { return workstreamActive(this.installDir); }
|
|
143
|
+
workstreamAddProject(idOrName, projectPath) { return workstreamAddProject(this.installDir, idOrName, projectPath); }
|
|
144
|
+
workstreamRemoveProject(idOrName, projectPath) { return workstreamRemoveProject(this.installDir, idOrName, projectPath); }
|
|
145
|
+
workstreamInject(silent) { return workstreamInject(this.installDir, silent); }
|
|
146
|
+
workstreamDetect(dir) { return workstreamDetect(this.installDir, dir); }
|
|
147
|
+
workstreamGet(id) { return workstreamGet(this.installDir, id); }
|
|
148
|
+
|
|
149
|
+
// Activity
|
|
150
|
+
getActivityPath() { return getActivityPath(this.installDir); }
|
|
151
|
+
loadActivity() { return loadActivity(this.installDir); }
|
|
152
|
+
getDefaultActivity() { return getDefaultActivity(); }
|
|
153
|
+
saveActivity(data) { return saveActivity(this.installDir, data); }
|
|
154
|
+
detectProjectRoot(filePath) { return detectProjectRoot(filePath); }
|
|
155
|
+
activityLog(files, sessionId) { return activityLog(this.installDir, files, sessionId); }
|
|
156
|
+
activitySummary() { return activitySummary(this.installDir); }
|
|
157
|
+
generateWorkstreamName(projects) { return generateWorkstreamName(projects); }
|
|
158
|
+
activitySuggestWorkstreams() { return activitySuggestWorkstreams(this.installDir); }
|
|
159
|
+
activityClear(olderThanDays) { return activityClear(this.installDir, olderThanDays); }
|
|
160
|
+
|
|
161
|
+
// Smart Sync
|
|
162
|
+
getSmartSyncPath() { return getSmartSyncPath(this.installDir); }
|
|
163
|
+
loadSmartSyncPrefs() { return loadSmartSyncPrefs(this.installDir); }
|
|
164
|
+
saveSmartSyncPrefs(prefs) { return saveSmartSyncPrefs(this.installDir, prefs); }
|
|
165
|
+
smartSyncRememberChoice(projectPath, workstreamId, choice) { return smartSyncRememberChoice(this.installDir, projectPath, workstreamId, choice); }
|
|
166
|
+
smartSyncDismissNudge(nudgeKey) { return smartSyncDismissNudge(this.installDir, nudgeKey); }
|
|
167
|
+
smartSyncUpdateSettings(settings) { return smartSyncUpdateSettings(this.installDir, settings); }
|
|
168
|
+
smartSyncDetect(currentProjects) { return smartSyncDetect(this.installDir, currentProjects); }
|
|
169
|
+
smartSyncCheckNudge(currentProjects) { return smartSyncCheckNudge(this.installDir, currentProjects); }
|
|
170
|
+
smartSyncHandleAction(nudgeKey, action, context) { return smartSyncHandleAction(this.installDir, nudgeKey, action, context); }
|
|
171
|
+
smartSyncStatus() { return smartSyncStatus(this.installDir); }
|
|
172
|
+
|
|
173
|
+
// Update
|
|
174
|
+
update(sourcePath) {
|
|
175
|
+
if (!sourcePath) {
|
|
176
|
+
console.error('Usage: claude-config update /path/to/claude-config');
|
|
177
|
+
console.log('\nThis copies updated files from the source to your installation.');
|
|
572
178
|
return false;
|
|
573
179
|
}
|
|
574
180
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (configLocations.length === 0) {
|
|
579
|
-
// No Antigravity-specific config found - skip silently
|
|
580
|
-
console.log(` ℹ No .agent/mcps.json found - skipping Antigravity`);
|
|
581
|
-
console.log(` Create one with: mkdir -p .agent && echo '{"include":["filesystem"]}' > .agent/mcps.json`);
|
|
582
|
-
return true; // Not an error, just no config
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Load all configs and merge
|
|
586
|
-
const loadedConfigs = configLocations.map(loc => ({
|
|
587
|
-
...loc,
|
|
588
|
-
config: this.loadJson(loc.configPath)
|
|
589
|
-
}));
|
|
590
|
-
const mergedConfig = this.mergeConfigs(loadedConfigs);
|
|
591
|
-
|
|
592
|
-
// Collect env vars from Antigravity-specific .env files
|
|
593
|
-
let env = {};
|
|
594
|
-
|
|
595
|
-
// Global env from ~/.gemini/antigravity/.env
|
|
596
|
-
const globalEnvPath = path.join(homeDir, '.gemini', 'antigravity', '.env');
|
|
597
|
-
env = { ...env, ...this.loadEnvFile(globalEnvPath) };
|
|
598
|
-
|
|
599
|
-
// Project-level env files
|
|
600
|
-
for (const { dir: d } of configLocations) {
|
|
601
|
-
if (d !== homeDir) {
|
|
602
|
-
const envPath = path.join(d, '.agent', '.env');
|
|
603
|
-
env = { ...env, ...this.loadEnvFile(envPath) };
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const output = { mcpServers: {} };
|
|
608
|
-
|
|
609
|
-
// Add MCPs from include list
|
|
610
|
-
if (mergedConfig.include && Array.isArray(mergedConfig.include)) {
|
|
611
|
-
for (const name of mergedConfig.include) {
|
|
612
|
-
if (registry.mcpServers && registry.mcpServers[name]) {
|
|
613
|
-
// Resolve env vars to actual values (Antigravity doesn't support ${VAR})
|
|
614
|
-
output.mcpServers[name] = this.resolveEnvVars(registry.mcpServers[name], env);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Add custom mcpServers
|
|
620
|
-
if (mergedConfig.mcpServers) {
|
|
621
|
-
for (const [name, config] of Object.entries(mergedConfig.mcpServers)) {
|
|
622
|
-
if (name.startsWith('_')) continue;
|
|
623
|
-
output.mcpServers[name] = this.resolveEnvVars(config, env);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Expand ~ in output path
|
|
628
|
-
const outputPath = paths.outputFile.replace(/^~/, homeDir);
|
|
629
|
-
|
|
630
|
-
// Ensure directory exists
|
|
631
|
-
const outputDir = path.dirname(outputPath);
|
|
632
|
-
if (!fs.existsSync(outputDir)) {
|
|
633
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
this.saveJson(outputPath, output);
|
|
637
|
-
|
|
638
|
-
const count = Object.keys(output.mcpServers).length;
|
|
639
|
-
console.log(`✓ Generated ${outputPath} (Antigravity)`);
|
|
640
|
-
console.log(` └─ ${count} MCP(s): ${Object.keys(output.mcpServers).join(', ')}`);
|
|
641
|
-
|
|
642
|
-
return true;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Find all MCP configs for a specific tool in hierarchy
|
|
647
|
-
* Similar to findAllConfigs but uses tool-specific folder paths
|
|
648
|
-
*/
|
|
649
|
-
findAllConfigsForTool(toolId, startDir = null) {
|
|
650
|
-
const tool = TOOL_PATHS[toolId];
|
|
651
|
-
if (!tool) return [];
|
|
652
|
-
|
|
653
|
-
const dir = startDir || this.findProjectRoot() || process.cwd();
|
|
654
|
-
const homeDir = process.env.HOME || '';
|
|
655
|
-
const configs = [];
|
|
656
|
-
|
|
657
|
-
// Walk up from project to find project-level configs
|
|
658
|
-
let currentDir = dir;
|
|
659
|
-
const root = path.parse(currentDir).root;
|
|
660
|
-
|
|
661
|
-
while (currentDir && currentDir !== root && currentDir !== homeDir) {
|
|
662
|
-
const configPath = path.join(currentDir, tool.projectConfig || `${tool.projectFolder}/mcps.json`);
|
|
663
|
-
if (fs.existsSync(configPath)) {
|
|
664
|
-
configs.push({
|
|
665
|
-
dir: currentDir,
|
|
666
|
-
configPath,
|
|
667
|
-
type: 'project'
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
currentDir = path.dirname(currentDir);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Check for global config
|
|
674
|
-
if (tool.globalMcpConfig) {
|
|
675
|
-
const globalPath = tool.globalMcpConfig.replace(/^~/, homeDir);
|
|
676
|
-
if (fs.existsSync(globalPath)) {
|
|
677
|
-
configs.push({
|
|
678
|
-
dir: homeDir,
|
|
679
|
-
configPath: globalPath,
|
|
680
|
-
type: 'global'
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Reverse so global is first, then parent dirs, then project dir
|
|
686
|
-
return configs.reverse();
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* Generate MCP config for Gemini CLI
|
|
691
|
-
* Gemini CLI stores MCP config inside ~/.gemini/settings.json under mcpServers key
|
|
692
|
-
* Reads from .gemini/mcps.json (NOT .claude/mcps.json)
|
|
693
|
-
*/
|
|
694
|
-
applyForGemini(projectDir = null) {
|
|
695
|
-
const dir = projectDir || this.findProjectRoot() || process.cwd();
|
|
696
|
-
const paths = TOOL_PATHS.gemini;
|
|
697
|
-
const homeDir = process.env.HOME || '';
|
|
698
|
-
|
|
699
|
-
const registry = this.loadJson(this.registryPath);
|
|
700
|
-
if (!registry) {
|
|
701
|
-
console.error('Error: Could not load MCP registry');
|
|
181
|
+
if (!fs.existsSync(sourcePath)) {
|
|
182
|
+
console.error(`Source not found: ${sourcePath}`);
|
|
702
183
|
return false;
|
|
703
184
|
}
|
|
704
185
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
console.log(` ℹ No .gemini/mcps.json found - skipping Gemini CLI`);
|
|
711
|
-
console.log(` Create one with: mkdir -p .gemini && echo '{"include":["filesystem"]}' > .gemini/mcps.json`);
|
|
712
|
-
return true; // Not an error, just no config
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Load all configs and merge
|
|
716
|
-
const loadedConfigs = configLocations.map(loc => ({
|
|
717
|
-
...loc,
|
|
718
|
-
config: this.loadJson(loc.configPath)
|
|
719
|
-
}));
|
|
720
|
-
const mergedConfig = this.mergeConfigs(loadedConfigs);
|
|
721
|
-
|
|
722
|
-
// Collect env vars from Gemini-specific .env files
|
|
723
|
-
let env = {};
|
|
724
|
-
|
|
725
|
-
// Global env from ~/.gemini/.env
|
|
726
|
-
const globalEnvPath = path.join(homeDir, '.gemini', '.env');
|
|
727
|
-
env = { ...env, ...this.loadEnvFile(globalEnvPath) };
|
|
728
|
-
|
|
729
|
-
// Project-level env files
|
|
730
|
-
for (const { dir: d } of configLocations) {
|
|
731
|
-
if (d !== homeDir) {
|
|
732
|
-
const envPath = path.join(d, '.gemini', '.env');
|
|
733
|
-
env = { ...env, ...this.loadEnvFile(envPath) };
|
|
734
|
-
}
|
|
735
|
-
}
|
|
186
|
+
const files = [
|
|
187
|
+
'config-loader.js',
|
|
188
|
+
'shared/mcp-registry.json',
|
|
189
|
+
'shell/claude-config.zsh'
|
|
190
|
+
];
|
|
736
191
|
|
|
737
|
-
|
|
192
|
+
let updated = 0;
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
const src = path.join(sourcePath, file);
|
|
195
|
+
const dest = path.join(this.installDir, file);
|
|
738
196
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
// Keep ${VAR} interpolation for Gemini CLI (it supports it)
|
|
744
|
-
mcpServers[name] = this.interpolate(registry.mcpServers[name], env);
|
|
197
|
+
if (fs.existsSync(src)) {
|
|
198
|
+
const destDir = path.dirname(dest);
|
|
199
|
+
if (!fs.existsSync(destDir)) {
|
|
200
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
745
201
|
}
|
|
202
|
+
fs.copyFileSync(src, dest);
|
|
203
|
+
console.log(`✓ Updated ${file}`);
|
|
204
|
+
updated++;
|
|
746
205
|
}
|
|
747
206
|
}
|
|
748
207
|
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
// Expand ~ in output path
|
|
758
|
-
const outputPath = paths.outputFile.replace(/^~/, homeDir);
|
|
759
|
-
|
|
760
|
-
// Ensure directory exists
|
|
761
|
-
const outputDir = path.dirname(outputPath);
|
|
762
|
-
if (!fs.existsSync(outputDir)) {
|
|
763
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Load existing settings.json and merge (preserve other keys)
|
|
767
|
-
let existingSettings = {};
|
|
768
|
-
if (fs.existsSync(outputPath)) {
|
|
769
|
-
try {
|
|
770
|
-
existingSettings = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
|
771
|
-
} catch (e) {
|
|
772
|
-
// If corrupt, start fresh
|
|
773
|
-
existingSettings = {};
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Merge mcpServers into existing settings
|
|
778
|
-
const output = {
|
|
779
|
-
...existingSettings,
|
|
780
|
-
mcpServers
|
|
781
|
-
};
|
|
782
|
-
|
|
783
|
-
this.saveJson(outputPath, output);
|
|
784
|
-
|
|
785
|
-
const count = Object.keys(mcpServers).length;
|
|
786
|
-
console.log(`✓ Generated ${outputPath} (Gemini CLI)`);
|
|
787
|
-
console.log(` └─ ${count} MCP(s): ${Object.keys(mcpServers).join(', ')}`);
|
|
788
|
-
|
|
789
|
-
return true;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* Detect which AI coding tools are installed
|
|
794
|
-
*/
|
|
795
|
-
detectInstalledTools() {
|
|
796
|
-
const homeDir = process.env.HOME || '';
|
|
797
|
-
const results = {};
|
|
798
|
-
|
|
799
|
-
// Check Claude Code - look for claude command or ~/.claude directory
|
|
800
|
-
try {
|
|
801
|
-
execSync('which claude', { stdio: 'ignore' });
|
|
802
|
-
results.claude = { installed: true, method: 'command' };
|
|
803
|
-
} catch {
|
|
804
|
-
results.claude = {
|
|
805
|
-
installed: fs.existsSync(path.join(homeDir, '.claude')),
|
|
806
|
-
method: 'directory'
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Check Gemini CLI - look for gemini command or ~/.gemini directory
|
|
811
|
-
try {
|
|
812
|
-
execSync('which gemini', { stdio: 'ignore' });
|
|
813
|
-
results.gemini = { installed: true, method: 'command' };
|
|
814
|
-
} catch {
|
|
815
|
-
results.gemini = {
|
|
816
|
-
installed: fs.existsSync(path.join(homeDir, '.gemini')),
|
|
817
|
-
method: 'directory'
|
|
818
|
-
};
|
|
208
|
+
// Copy templates directory
|
|
209
|
+
const srcTemplates = path.join(sourcePath, 'templates');
|
|
210
|
+
const destTemplates = path.join(this.installDir, 'templates');
|
|
211
|
+
if (fs.existsSync(srcTemplates)) {
|
|
212
|
+
copyDirRecursive(srcTemplates, destTemplates);
|
|
213
|
+
console.log(`✓ Updated templates/`);
|
|
214
|
+
updated++;
|
|
819
215
|
}
|
|
820
216
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
return results;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
* Get tool paths configuration
|
|
832
|
-
*/
|
|
833
|
-
getToolPaths() {
|
|
834
|
-
return TOOL_PATHS;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
/**
|
|
838
|
-
* Apply config for multiple tools based on preferences
|
|
839
|
-
*/
|
|
840
|
-
applyForTools(projectDir = null, tools = ['claude']) {
|
|
841
|
-
const results = {};
|
|
842
|
-
|
|
843
|
-
for (const tool of tools) {
|
|
844
|
-
if (tool === 'claude') {
|
|
845
|
-
results.claude = this.apply(projectDir);
|
|
846
|
-
} else if (tool === 'gemini') {
|
|
847
|
-
results.gemini = this.applyForGemini(projectDir);
|
|
848
|
-
} else if (tool === 'antigravity') {
|
|
849
|
-
results.antigravity = this.applyForAntigravity(projectDir);
|
|
850
|
-
}
|
|
217
|
+
if (updated > 0) {
|
|
218
|
+
console.log(`\n✅ Updated ${updated} item(s)`);
|
|
219
|
+
console.log('Restart your shell or run: source ~/.zshrc');
|
|
220
|
+
} else {
|
|
221
|
+
console.log('No files found to update');
|
|
851
222
|
}
|
|
852
223
|
|
|
853
|
-
return
|
|
224
|
+
return updated > 0;
|
|
854
225
|
}
|
|
855
226
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
console.error('Error: Could not load MCP registry');
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
const dir = this.findProjectRoot();
|
|
867
|
-
const projectConfig = dir ? this.loadJson(path.join(dir, '.claude', 'mcps.json')) : null;
|
|
868
|
-
const included = projectConfig?.include || [];
|
|
869
|
-
|
|
870
|
-
console.log('\n📚 Available MCPs:\n');
|
|
871
|
-
for (const name of Object.keys(registry.mcpServers)) {
|
|
872
|
-
const active = included.includes(name) ? ' ✓' : '';
|
|
873
|
-
console.log(` • ${name}${active}`);
|
|
874
|
-
}
|
|
875
|
-
console.log(`\n Total: ${Object.keys(registry.mcpServers).length} in registry`);
|
|
876
|
-
if (included.length) {
|
|
877
|
-
console.log(` Active: ${included.join(', ')}`);
|
|
878
|
-
}
|
|
879
|
-
console.log('');
|
|
227
|
+
// Version
|
|
228
|
+
version() {
|
|
229
|
+
console.log(`claude-config v${VERSION}`);
|
|
230
|
+
console.log(`Install: ${this.installDir}`);
|
|
231
|
+
console.log(`Registry: ${this.registryPath}`);
|
|
232
|
+
console.log(`Templates: ${this.templatesDir}`);
|
|
880
233
|
}
|
|
234
|
+
}
|
|
881
235
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
init(projectDir = null, templateName = null) {
|
|
886
|
-
const dir = projectDir || process.cwd();
|
|
887
|
-
const claudeDir = path.join(dir, '.claude');
|
|
888
|
-
const configPath = path.join(claudeDir, 'mcps.json');
|
|
889
|
-
|
|
890
|
-
// Create .claude directory
|
|
891
|
-
if (!fs.existsSync(claudeDir)) {
|
|
892
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// Determine MCPs to include
|
|
896
|
-
let mcpDefaults = ['github', 'filesystem'];
|
|
897
|
-
let templateChain = [];
|
|
898
|
-
|
|
899
|
-
if (templateName) {
|
|
900
|
-
const templatePath = this.findTemplate(templateName);
|
|
901
|
-
if (!templatePath) {
|
|
902
|
-
console.error(`Template not found: ${templateName}`);
|
|
903
|
-
console.log('Run "claude-config templates" to see available templates.');
|
|
904
|
-
return false;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// Resolve full template chain
|
|
908
|
-
templateChain = this.resolveTemplateChain(templatePath);
|
|
909
|
-
|
|
910
|
-
// Get MCP defaults from the main template
|
|
911
|
-
const templateJson = this.loadJson(path.join(templatePath, 'template.json'));
|
|
912
|
-
if (templateJson?.mcpDefaults) {
|
|
913
|
-
mcpDefaults = templateJson.mcpDefaults;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
console.log(`\n🎯 Using template: ${templateName}`);
|
|
917
|
-
console.log(` Includes: ${templateChain.map(p => path.basename(p)).join(' → ')}\n`);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// Create or update mcps.json
|
|
921
|
-
if (!fs.existsSync(configPath)) {
|
|
922
|
-
const template = {
|
|
923
|
-
"include": mcpDefaults,
|
|
924
|
-
"template": templateName || null,
|
|
925
|
-
"mcpServers": {}
|
|
926
|
-
};
|
|
927
|
-
this.saveJson(configPath, template);
|
|
928
|
-
console.log(`✓ Created ${configPath}`);
|
|
929
|
-
} else {
|
|
930
|
-
console.log(`⏭ ${configPath} already exists`);
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// Copy template files
|
|
934
|
-
if (templateChain.length > 0) {
|
|
935
|
-
console.log('\nCopying template files:');
|
|
936
|
-
let totalCopied = 0;
|
|
937
|
-
let totalSkipped = 0;
|
|
938
|
-
|
|
939
|
-
for (const tplPath of templateChain) {
|
|
940
|
-
const { copied, skipped } = this.copyTemplateFiles(tplPath, dir);
|
|
941
|
-
totalCopied += copied;
|
|
942
|
-
totalSkipped += skipped;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
console.log(`\n Total: ${totalCopied} copied, ${totalSkipped} skipped (already exist)`);
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
// Create .env file
|
|
949
|
-
const envPath = path.join(claudeDir, '.env');
|
|
950
|
-
if (!fs.existsSync(envPath)) {
|
|
951
|
-
fs.writeFileSync(envPath, `# Project secrets (gitignored)
|
|
952
|
-
# GITHUB_TOKEN=ghp_xxx
|
|
953
|
-
# DATABASE_URL=postgres://...
|
|
954
|
-
`);
|
|
955
|
-
console.log(`✓ Created ${envPath}`);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// Update .gitignore
|
|
959
|
-
const gitignorePath = path.join(dir, '.gitignore');
|
|
960
|
-
if (fs.existsSync(gitignorePath)) {
|
|
961
|
-
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
962
|
-
if (!content.includes('.claude/.env')) {
|
|
963
|
-
fs.appendFileSync(gitignorePath, '\n.claude/.env\n');
|
|
964
|
-
console.log('✓ Updated .gitignore');
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
console.log('\n✅ Project initialized!');
|
|
969
|
-
console.log('Next steps:');
|
|
970
|
-
console.log(' 1. Edit .claude/mcps.json to customize MCPs');
|
|
971
|
-
console.log(' 2. Review .claude/rules/ and .claude/commands/');
|
|
972
|
-
console.log(' 3. Run: claude-config apply\n');
|
|
973
|
-
|
|
974
|
-
return true;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
/**
|
|
978
|
-
* Apply templates to existing project (add rules/commands without overwriting)
|
|
979
|
-
*/
|
|
980
|
-
applyTemplate(templateName, projectDir = null) {
|
|
981
|
-
const dir = projectDir || this.findProjectRoot() || process.cwd();
|
|
982
|
-
|
|
983
|
-
if (!templateName) {
|
|
984
|
-
console.error('Usage: claude-config apply-template <template-name>');
|
|
985
|
-
console.log('Run "claude-config templates" to see available templates.');
|
|
986
|
-
return false;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
const templatePath = this.findTemplate(templateName);
|
|
990
|
-
if (!templatePath) {
|
|
991
|
-
console.error(`Template not found: ${templateName}`);
|
|
992
|
-
console.log('Run "claude-config templates" to see available templates.');
|
|
993
|
-
return false;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// Resolve full template chain
|
|
997
|
-
const templateChain = this.resolveTemplateChain(templatePath);
|
|
998
|
-
|
|
999
|
-
console.log(`\n🎯 Applying template: ${templateName}`);
|
|
1000
|
-
console.log(` Includes: ${templateChain.map(p => path.basename(p)).join(' → ')}\n`);
|
|
1001
|
-
|
|
1002
|
-
console.log('Copying template files (won\'t overwrite existing):');
|
|
1003
|
-
let totalCopied = 0;
|
|
1004
|
-
let totalSkipped = 0;
|
|
1005
|
-
|
|
1006
|
-
for (const tplPath of templateChain) {
|
|
1007
|
-
const { copied, skipped } = this.copyTemplateFiles(tplPath, dir);
|
|
1008
|
-
totalCopied += copied;
|
|
1009
|
-
totalSkipped += skipped;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
console.log(`\n✅ Applied template: ${totalCopied} files copied, ${totalSkipped} skipped\n`);
|
|
1013
|
-
|
|
1014
|
-
// Track applied template in templates.json
|
|
1015
|
-
this.trackAppliedTemplate(dir, templateName);
|
|
1016
|
-
|
|
1017
|
-
return true;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
/**
|
|
1021
|
-
* Track an applied template in .claude/templates.json
|
|
1022
|
-
* Only one template per project (templates chain internally)
|
|
1023
|
-
*/
|
|
1024
|
-
trackAppliedTemplate(dir, templateName) {
|
|
1025
|
-
const claudeDir = path.join(dir, '.claude');
|
|
1026
|
-
const templatesPath = path.join(claudeDir, 'templates.json');
|
|
1027
|
-
|
|
1028
|
-
// Ensure .claude directory exists
|
|
1029
|
-
if (!fs.existsSync(claudeDir)) {
|
|
1030
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// Save single template (replaces any previous)
|
|
1034
|
-
const data = {
|
|
1035
|
-
template: templateName,
|
|
1036
|
-
appliedAt: new Date().toISOString()
|
|
1037
|
-
};
|
|
1038
|
-
|
|
1039
|
-
fs.writeFileSync(templatesPath, JSON.stringify(data, null, 2) + '\n');
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* Get applied template for a directory
|
|
1044
|
-
* Returns { template, appliedAt } or null
|
|
1045
|
-
*/
|
|
1046
|
-
getAppliedTemplate(dir) {
|
|
1047
|
-
const templatesPath = path.join(dir, '.claude', 'templates.json');
|
|
1048
|
-
if (!fs.existsSync(templatesPath)) {
|
|
1049
|
-
return null;
|
|
1050
|
-
}
|
|
1051
|
-
try {
|
|
1052
|
-
const data = JSON.parse(fs.readFileSync(templatesPath, 'utf8'));
|
|
1053
|
-
if (!data.template) return null;
|
|
1054
|
-
return {
|
|
1055
|
-
template: data.template,
|
|
1056
|
-
appliedAt: data.appliedAt
|
|
1057
|
-
};
|
|
1058
|
-
} catch (e) {
|
|
1059
|
-
return null;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
/**
|
|
1064
|
-
* Show current project config (including hierarchy)
|
|
1065
|
-
*/
|
|
1066
|
-
show(projectDir = null) {
|
|
1067
|
-
const dir = projectDir || this.findProjectRoot() || process.cwd();
|
|
1068
|
-
|
|
1069
|
-
// Find all configs in hierarchy
|
|
1070
|
-
const configLocations = this.findAllConfigs(dir);
|
|
1071
|
-
|
|
1072
|
-
if (configLocations.length === 0) {
|
|
1073
|
-
console.log('No .claude/mcps.json found in current directory or parents');
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
console.log(`\n📁 Project: ${dir}`);
|
|
1078
|
-
|
|
1079
|
-
// Show each config in hierarchy
|
|
1080
|
-
if (configLocations.length > 1) {
|
|
1081
|
-
console.log('\n📚 Config Hierarchy (root → leaf):');
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
for (const { dir: d, configPath } of configLocations) {
|
|
1085
|
-
const config = this.loadJson(configPath);
|
|
1086
|
-
const relPath = d === process.env.HOME ? '~' : path.relative(process.cwd(), d) || '.';
|
|
1087
|
-
|
|
1088
|
-
console.log(`\n📄 ${relPath}/.claude/mcps.json:`);
|
|
1089
|
-
console.log(JSON.stringify(config, null, 2));
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// Show merged result
|
|
1093
|
-
if (configLocations.length > 1) {
|
|
1094
|
-
const loadedConfigs = configLocations.map(loc => ({
|
|
1095
|
-
...loc,
|
|
1096
|
-
config: this.loadJson(loc.configPath)
|
|
1097
|
-
}));
|
|
1098
|
-
const merged = this.mergeConfigs(loadedConfigs);
|
|
1099
|
-
console.log('\n🔀 Merged Config (effective):');
|
|
1100
|
-
console.log(JSON.stringify(merged, null, 2));
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Collect rules and commands from all levels in hierarchy
|
|
1104
|
-
const allRules = this.collectFilesFromHierarchy(configLocations, 'rules');
|
|
1105
|
-
const allCommands = this.collectFilesFromHierarchy(configLocations, 'commands');
|
|
1106
|
-
|
|
1107
|
-
if (allRules.length) {
|
|
1108
|
-
console.log(`\n📜 Rules (${allRules.length} total):`);
|
|
1109
|
-
for (const { file, source } of allRules) {
|
|
1110
|
-
const sourceLabel = source === process.env.HOME ? '~' : path.relative(process.cwd(), source) || '.';
|
|
1111
|
-
console.log(` • ${file} (${sourceLabel})`);
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
if (allCommands.length) {
|
|
1116
|
-
console.log(`\n⚡ Commands (${allCommands.length} total):`);
|
|
1117
|
-
for (const { file, source } of allCommands) {
|
|
1118
|
-
const sourceLabel = source === process.env.HOME ? '~' : path.relative(process.cwd(), source) || '.';
|
|
1119
|
-
console.log(` • ${file} (${sourceLabel})`);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
console.log('');
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// ===========================================================================
|
|
1126
|
-
// MCP EDIT COMMANDS
|
|
1127
|
-
// ===========================================================================
|
|
1128
|
-
|
|
1129
|
-
/**
|
|
1130
|
-
* Add MCP(s) to current project
|
|
1131
|
-
*/
|
|
1132
|
-
add(mcpNames) {
|
|
1133
|
-
if (!mcpNames || mcpNames.length === 0) {
|
|
1134
|
-
console.error('Usage: claude-config add <mcp-name> [mcp-name...]');
|
|
1135
|
-
return false;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
const configPath = this.getConfigPath();
|
|
1139
|
-
let config = this.loadJson(configPath);
|
|
1140
|
-
|
|
1141
|
-
if (!config) {
|
|
1142
|
-
console.error('No .claude/mcps.json found. Run: claude-config init');
|
|
1143
|
-
return false;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
const registry = this.loadJson(this.registryPath);
|
|
1147
|
-
if (!config.include) config.include = [];
|
|
1148
|
-
|
|
1149
|
-
const added = [];
|
|
1150
|
-
const notFound = [];
|
|
1151
|
-
const alreadyExists = [];
|
|
1152
|
-
|
|
1153
|
-
for (const name of mcpNames) {
|
|
1154
|
-
if (config.include.includes(name)) {
|
|
1155
|
-
alreadyExists.push(name);
|
|
1156
|
-
} else if (registry?.mcpServers?.[name]) {
|
|
1157
|
-
config.include.push(name);
|
|
1158
|
-
added.push(name);
|
|
1159
|
-
} else {
|
|
1160
|
-
notFound.push(name);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
if (added.length) {
|
|
1165
|
-
this.saveJson(configPath, config);
|
|
1166
|
-
console.log(`✓ Added: ${added.join(', ')}`);
|
|
1167
|
-
}
|
|
1168
|
-
if (alreadyExists.length) {
|
|
1169
|
-
console.log(`Already included: ${alreadyExists.join(', ')}`);
|
|
1170
|
-
}
|
|
1171
|
-
if (notFound.length) {
|
|
1172
|
-
console.log(`Not in registry: ${notFound.join(', ')}`);
|
|
1173
|
-
console.log(' (Use "claude-config list" to see available MCPs)');
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
if (added.length) {
|
|
1177
|
-
console.log('\nRun "claude-config apply" to regenerate .mcp.json');
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
return added.length > 0;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
/**
|
|
1184
|
-
* Remove MCP(s) from current project
|
|
1185
|
-
*/
|
|
1186
|
-
remove(mcpNames) {
|
|
1187
|
-
if (!mcpNames || mcpNames.length === 0) {
|
|
1188
|
-
console.error('Usage: claude-config remove <mcp-name> [mcp-name...]');
|
|
1189
|
-
return false;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
const configPath = this.getConfigPath();
|
|
1193
|
-
let config = this.loadJson(configPath);
|
|
1194
|
-
|
|
1195
|
-
if (!config) {
|
|
1196
|
-
console.error('No .claude/mcps.json found');
|
|
1197
|
-
return false;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
if (!config.include) config.include = [];
|
|
1201
|
-
|
|
1202
|
-
const removed = [];
|
|
1203
|
-
const notFound = [];
|
|
1204
|
-
|
|
1205
|
-
for (const name of mcpNames) {
|
|
1206
|
-
const idx = config.include.indexOf(name);
|
|
1207
|
-
if (idx !== -1) {
|
|
1208
|
-
config.include.splice(idx, 1);
|
|
1209
|
-
removed.push(name);
|
|
1210
|
-
} else {
|
|
1211
|
-
notFound.push(name);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
if (removed.length) {
|
|
1216
|
-
this.saveJson(configPath, config);
|
|
1217
|
-
console.log(`✓ Removed: ${removed.join(', ')}`);
|
|
1218
|
-
console.log('\nRun "claude-config apply" to regenerate .mcp.json');
|
|
1219
|
-
}
|
|
1220
|
-
if (notFound.length) {
|
|
1221
|
-
console.log(`Not in project: ${notFound.join(', ')}`);
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
return removed.length > 0;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// ===========================================================================
|
|
1228
|
-
// REGISTRY COMMANDS
|
|
1229
|
-
// ===========================================================================
|
|
1230
|
-
|
|
1231
|
-
/**
|
|
1232
|
-
* Add MCP to global registry
|
|
1233
|
-
*/
|
|
1234
|
-
registryAdd(name, configJson) {
|
|
1235
|
-
if (!name || !configJson) {
|
|
1236
|
-
console.error('Usage: claude-config registry-add <name> \'{"command":"...","args":[...]}\'');
|
|
1237
|
-
return false;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
let mcpConfig;
|
|
1241
|
-
try {
|
|
1242
|
-
mcpConfig = JSON.parse(configJson);
|
|
1243
|
-
} catch (e) {
|
|
1244
|
-
console.error('Invalid JSON:', e.message);
|
|
1245
|
-
return false;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
const registry = this.loadJson(this.registryPath) || { mcpServers: {} };
|
|
1249
|
-
registry.mcpServers[name] = mcpConfig;
|
|
1250
|
-
this.saveJson(this.registryPath, registry);
|
|
1251
|
-
|
|
1252
|
-
console.log(`✓ Added "${name}" to registry`);
|
|
1253
|
-
return true;
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
/**
|
|
1257
|
-
* Remove MCP from global registry
|
|
1258
|
-
*/
|
|
1259
|
-
registryRemove(name) {
|
|
1260
|
-
if (!name) {
|
|
1261
|
-
console.error('Usage: claude-config registry-remove <name>');
|
|
1262
|
-
return false;
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
const registry = this.loadJson(this.registryPath);
|
|
1266
|
-
if (!registry?.mcpServers?.[name]) {
|
|
1267
|
-
console.error(`"${name}" not found in registry`);
|
|
1268
|
-
return false;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
delete registry.mcpServers[name];
|
|
1272
|
-
this.saveJson(this.registryPath, registry);
|
|
1273
|
-
|
|
1274
|
-
console.log(`✓ Removed "${name}" from registry`);
|
|
1275
|
-
return true;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// ===========================================================================
|
|
1279
|
-
// UPDATE COMMAND
|
|
1280
|
-
// ===========================================================================
|
|
1281
|
-
|
|
1282
|
-
/**
|
|
1283
|
-
* Update claude-config from source
|
|
1284
|
-
*/
|
|
1285
|
-
update(sourcePath) {
|
|
1286
|
-
if (!sourcePath) {
|
|
1287
|
-
console.error('Usage: claude-config update /path/to/claude-config');
|
|
1288
|
-
console.log('\nThis copies updated files from the source to your installation.');
|
|
1289
|
-
return false;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
if (!fs.existsSync(sourcePath)) {
|
|
1293
|
-
console.error(`Source not found: ${sourcePath}`);
|
|
1294
|
-
return false;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
const files = [
|
|
1298
|
-
'config-loader.js',
|
|
1299
|
-
'shared/mcp-registry.json',
|
|
1300
|
-
'shell/claude-config.zsh'
|
|
1301
|
-
];
|
|
1302
|
-
|
|
1303
|
-
let updated = 0;
|
|
1304
|
-
for (const file of files) {
|
|
1305
|
-
const src = path.join(sourcePath, file);
|
|
1306
|
-
const dest = path.join(this.installDir, file);
|
|
1307
|
-
|
|
1308
|
-
if (fs.existsSync(src)) {
|
|
1309
|
-
const destDir = path.dirname(dest);
|
|
1310
|
-
if (!fs.existsSync(destDir)) {
|
|
1311
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
1312
|
-
}
|
|
1313
|
-
fs.copyFileSync(src, dest);
|
|
1314
|
-
console.log(`✓ Updated ${file}`);
|
|
1315
|
-
updated++;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
// Copy templates directory
|
|
1320
|
-
const srcTemplates = path.join(sourcePath, 'templates');
|
|
1321
|
-
const destTemplates = path.join(this.installDir, 'templates');
|
|
1322
|
-
if (fs.existsSync(srcTemplates)) {
|
|
1323
|
-
this.copyDirRecursive(srcTemplates, destTemplates);
|
|
1324
|
-
console.log(`✓ Updated templates/`);
|
|
1325
|
-
updated++;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
if (updated > 0) {
|
|
1329
|
-
console.log(`\n✅ Updated ${updated} item(s)`);
|
|
1330
|
-
console.log('Restart your shell or run: source ~/.zshrc');
|
|
1331
|
-
} else {
|
|
1332
|
-
console.log('No files found to update');
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
return updated > 0;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
/**
|
|
1339
|
-
* Recursively copy directory
|
|
1340
|
-
*/
|
|
1341
|
-
copyDirRecursive(src, dest) {
|
|
1342
|
-
if (!fs.existsSync(dest)) {
|
|
1343
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
for (const item of fs.readdirSync(src)) {
|
|
1347
|
-
const srcPath = path.join(src, item);
|
|
1348
|
-
const destPath = path.join(dest, item);
|
|
1349
|
-
|
|
1350
|
-
if (fs.statSync(srcPath).isDirectory()) {
|
|
1351
|
-
this.copyDirRecursive(srcPath, destPath);
|
|
1352
|
-
} else {
|
|
1353
|
-
fs.copyFileSync(srcPath, destPath);
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
/**
|
|
1359
|
-
* Show version
|
|
1360
|
-
*/
|
|
1361
|
-
version() {
|
|
1362
|
-
console.log(`claude-config v${VERSION}`);
|
|
1363
|
-
console.log(`Install: ${this.installDir}`);
|
|
1364
|
-
console.log(`Registry: ${this.registryPath}`);
|
|
1365
|
-
console.log(`Templates: ${this.templatesDir}`);
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
// ===========================================================================
|
|
1369
|
-
// MEMORY COMMANDS
|
|
1370
|
-
// ===========================================================================
|
|
1371
|
-
|
|
1372
|
-
/**
|
|
1373
|
-
* Show memory status and contents
|
|
1374
|
-
*/
|
|
1375
|
-
memoryList(projectDir = process.cwd()) {
|
|
1376
|
-
const homeDir = process.env.HOME || '';
|
|
1377
|
-
const globalMemoryDir = path.join(homeDir, '.claude', 'memory');
|
|
1378
|
-
const projectMemoryDir = path.join(projectDir, '.claude', 'memory');
|
|
1379
|
-
|
|
1380
|
-
console.log('\n📝 Memory System\n');
|
|
1381
|
-
|
|
1382
|
-
// Global memory
|
|
1383
|
-
console.log('Global (~/.claude/memory/):');
|
|
1384
|
-
if (fs.existsSync(globalMemoryDir)) {
|
|
1385
|
-
const files = ['preferences.md', 'corrections.md', 'facts.md'];
|
|
1386
|
-
for (const file of files) {
|
|
1387
|
-
const filePath = path.join(globalMemoryDir, file);
|
|
1388
|
-
if (fs.existsSync(filePath)) {
|
|
1389
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
1390
|
-
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#')).length;
|
|
1391
|
-
console.log(` ✓ ${file} (${lines} entries)`);
|
|
1392
|
-
} else {
|
|
1393
|
-
console.log(` ○ ${file} (not created)`);
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
} else {
|
|
1397
|
-
console.log(' Not initialized');
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// Project memory
|
|
1401
|
-
console.log(`\nProject (${projectDir}/.claude/memory/):`);
|
|
1402
|
-
if (fs.existsSync(projectMemoryDir)) {
|
|
1403
|
-
const files = ['context.md', 'patterns.md', 'decisions.md', 'issues.md', 'history.md'];
|
|
1404
|
-
for (const file of files) {
|
|
1405
|
-
const filePath = path.join(projectMemoryDir, file);
|
|
1406
|
-
if (fs.existsSync(filePath)) {
|
|
1407
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
1408
|
-
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#')).length;
|
|
1409
|
-
console.log(` ✓ ${file} (${lines} entries)`);
|
|
1410
|
-
} else {
|
|
1411
|
-
console.log(` ○ ${file} (not created)`);
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
} else {
|
|
1415
|
-
console.log(' Not initialized. Run: claude-config memory init');
|
|
1416
|
-
}
|
|
1417
|
-
console.log();
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
/**
|
|
1421
|
-
* Initialize project memory
|
|
1422
|
-
*/
|
|
1423
|
-
memoryInit(projectDir = process.cwd()) {
|
|
1424
|
-
const memoryDir = path.join(projectDir, '.claude', 'memory');
|
|
1425
|
-
|
|
1426
|
-
if (fs.existsSync(memoryDir)) {
|
|
1427
|
-
console.log('Project memory already initialized at', memoryDir);
|
|
1428
|
-
return;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
fs.mkdirSync(memoryDir, { recursive: true });
|
|
1432
|
-
|
|
1433
|
-
const files = {
|
|
1434
|
-
'context.md': '# Project Context\n\n<!-- Project overview and key information -->\n',
|
|
1435
|
-
'patterns.md': '# Code Patterns\n\n<!-- Established patterns in this codebase -->\n',
|
|
1436
|
-
'decisions.md': '# Architecture Decisions\n\n<!-- Key decisions and their rationale -->\n',
|
|
1437
|
-
'issues.md': '# Known Issues\n\n<!-- Current issues and workarounds -->\n',
|
|
1438
|
-
'history.md': '# Session History\n\n<!-- Notable changes and milestones -->\n'
|
|
1439
|
-
};
|
|
1440
|
-
|
|
1441
|
-
for (const [file, content] of Object.entries(files)) {
|
|
1442
|
-
fs.writeFileSync(path.join(memoryDir, file), content);
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
console.log(`✓ Initialized project memory at ${memoryDir}`);
|
|
1446
|
-
console.log('\nCreated:');
|
|
1447
|
-
for (const file of Object.keys(files)) {
|
|
1448
|
-
console.log(` ${file}`);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
/**
|
|
1453
|
-
* Add entry to memory
|
|
1454
|
-
*/
|
|
1455
|
-
memoryAdd(type, content, projectDir = process.cwd()) {
|
|
1456
|
-
if (!type || !content) {
|
|
1457
|
-
console.error('Usage: claude-config memory add <type> "<content>"');
|
|
1458
|
-
console.log('\nTypes:');
|
|
1459
|
-
console.log(' Global: preference, correction, fact');
|
|
1460
|
-
console.log(' Project: context, pattern, decision, issue, history');
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
const homeDir = process.env.HOME || '';
|
|
1465
|
-
const timestamp = new Date().toISOString().split('T')[0];
|
|
1466
|
-
|
|
1467
|
-
// Map type to file
|
|
1468
|
-
const typeMap = {
|
|
1469
|
-
// Global
|
|
1470
|
-
preference: { dir: path.join(homeDir, '.claude', 'memory'), file: 'preferences.md' },
|
|
1471
|
-
correction: { dir: path.join(homeDir, '.claude', 'memory'), file: 'corrections.md' },
|
|
1472
|
-
fact: { dir: path.join(homeDir, '.claude', 'memory'), file: 'facts.md' },
|
|
1473
|
-
// Project
|
|
1474
|
-
context: { dir: path.join(projectDir, '.claude', 'memory'), file: 'context.md' },
|
|
1475
|
-
pattern: { dir: path.join(projectDir, '.claude', 'memory'), file: 'patterns.md' },
|
|
1476
|
-
decision: { dir: path.join(projectDir, '.claude', 'memory'), file: 'decisions.md' },
|
|
1477
|
-
issue: { dir: path.join(projectDir, '.claude', 'memory'), file: 'issues.md' },
|
|
1478
|
-
history: { dir: path.join(projectDir, '.claude', 'memory'), file: 'history.md' }
|
|
1479
|
-
};
|
|
1480
|
-
|
|
1481
|
-
const target = typeMap[type];
|
|
1482
|
-
if (!target) {
|
|
1483
|
-
console.error(`Unknown type: ${type}`);
|
|
1484
|
-
console.log('Valid types: preference, correction, fact, context, pattern, decision, issue, history');
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
// Ensure directory exists
|
|
1489
|
-
if (!fs.existsSync(target.dir)) {
|
|
1490
|
-
fs.mkdirSync(target.dir, { recursive: true });
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
const filePath = path.join(target.dir, target.file);
|
|
1494
|
-
|
|
1495
|
-
// Create file with header if it doesn't exist
|
|
1496
|
-
if (!fs.existsSync(filePath)) {
|
|
1497
|
-
const headers = {
|
|
1498
|
-
'preferences.md': '# Preferences\n',
|
|
1499
|
-
'corrections.md': '# Corrections\n',
|
|
1500
|
-
'facts.md': '# Facts\n',
|
|
1501
|
-
'context.md': '# Project Context\n',
|
|
1502
|
-
'patterns.md': '# Code Patterns\n',
|
|
1503
|
-
'decisions.md': '# Architecture Decisions\n',
|
|
1504
|
-
'issues.md': '# Known Issues\n',
|
|
1505
|
-
'history.md': '# Session History\n'
|
|
1506
|
-
};
|
|
1507
|
-
fs.writeFileSync(filePath, headers[target.file] || '');
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// Append entry
|
|
1511
|
-
const entry = `\n- [${timestamp}] ${content}\n`;
|
|
1512
|
-
fs.appendFileSync(filePath, entry);
|
|
1513
|
-
|
|
1514
|
-
console.log(`✓ Added ${type} to ${target.file}`);
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
/**
|
|
1518
|
-
* Search memory files
|
|
1519
|
-
*/
|
|
1520
|
-
memorySearch(query, projectDir = process.cwd()) {
|
|
1521
|
-
if (!query) {
|
|
1522
|
-
console.error('Usage: claude-config memory search <query>');
|
|
1523
|
-
return;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
const homeDir = process.env.HOME || '';
|
|
1527
|
-
const searchDirs = [
|
|
1528
|
-
{ label: 'Global', dir: path.join(homeDir, '.claude', 'memory') },
|
|
1529
|
-
{ label: 'Project', dir: path.join(projectDir, '.claude', 'memory') }
|
|
1530
|
-
];
|
|
1531
|
-
|
|
1532
|
-
const results = [];
|
|
1533
|
-
const queryLower = query.toLowerCase();
|
|
1534
|
-
|
|
1535
|
-
for (const { label, dir } of searchDirs) {
|
|
1536
|
-
if (!fs.existsSync(dir)) continue;
|
|
1537
|
-
|
|
1538
|
-
for (const file of fs.readdirSync(dir)) {
|
|
1539
|
-
if (!file.endsWith('.md')) continue;
|
|
1540
|
-
const filePath = path.join(dir, file);
|
|
1541
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
1542
|
-
const lines = content.split('\n');
|
|
1543
|
-
|
|
1544
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1545
|
-
if (lines[i].toLowerCase().includes(queryLower)) {
|
|
1546
|
-
results.push({
|
|
1547
|
-
location: `${label}/${file}`,
|
|
1548
|
-
line: i + 1,
|
|
1549
|
-
content: lines[i].trim()
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
if (results.length === 0) {
|
|
1557
|
-
console.log(`No matches found for "${query}"`);
|
|
1558
|
-
return;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
console.log(`\n🔍 Found ${results.length} match(es) for "${query}":\n`);
|
|
1562
|
-
for (const r of results) {
|
|
1563
|
-
console.log(` ${r.location}:${r.line}`);
|
|
1564
|
-
console.log(` ${r.content}\n`);
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
// ===========================================================================
|
|
1569
|
-
// ENV COMMANDS
|
|
1570
|
-
// ===========================================================================
|
|
1571
|
-
|
|
1572
|
-
/**
|
|
1573
|
-
* List environment variables
|
|
1574
|
-
*/
|
|
1575
|
-
envList(projectDir = process.cwd()) {
|
|
1576
|
-
const envPath = path.join(projectDir, '.claude', '.env');
|
|
1577
|
-
|
|
1578
|
-
console.log(`\n🔐 Environment Variables (${projectDir}/.claude/.env)\n`);
|
|
1579
|
-
|
|
1580
|
-
if (!fs.existsSync(envPath)) {
|
|
1581
|
-
console.log(' No .env file found.');
|
|
1582
|
-
console.log(' Create with: claude-config env set <KEY> <value>\n');
|
|
1583
|
-
return;
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
const content = fs.readFileSync(envPath, 'utf8');
|
|
1587
|
-
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
|
|
1588
|
-
|
|
1589
|
-
if (lines.length === 0) {
|
|
1590
|
-
console.log(' No variables set.\n');
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
for (const line of lines) {
|
|
1595
|
-
const [key] = line.split('=');
|
|
1596
|
-
if (key) {
|
|
1597
|
-
console.log(` ${key}=****`);
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
console.log(`\n Total: ${lines.length} variable(s)\n`);
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
/**
|
|
1604
|
-
* Set environment variable
|
|
1605
|
-
*/
|
|
1606
|
-
envSet(key, value, projectDir = process.cwd()) {
|
|
1607
|
-
if (!key || value === undefined) {
|
|
1608
|
-
console.error('Usage: claude-config env set <KEY> <value>');
|
|
1609
|
-
return;
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
const claudeDir = path.join(projectDir, '.claude');
|
|
1613
|
-
const envPath = path.join(claudeDir, '.env');
|
|
1614
|
-
|
|
1615
|
-
// Ensure .claude directory exists
|
|
1616
|
-
if (!fs.existsSync(claudeDir)) {
|
|
1617
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
// Read existing content
|
|
1621
|
-
let lines = [];
|
|
1622
|
-
if (fs.existsSync(envPath)) {
|
|
1623
|
-
lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
// Update or add the variable
|
|
1627
|
-
const keyUpper = key.toUpperCase();
|
|
1628
|
-
let found = false;
|
|
1629
|
-
lines = lines.map(line => {
|
|
1630
|
-
if (line.startsWith(`${keyUpper}=`)) {
|
|
1631
|
-
found = true;
|
|
1632
|
-
return `${keyUpper}=${value}`;
|
|
1633
|
-
}
|
|
1634
|
-
return line;
|
|
1635
|
-
});
|
|
1636
|
-
|
|
1637
|
-
if (!found) {
|
|
1638
|
-
lines.push(`${keyUpper}=${value}`);
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
// Write back
|
|
1642
|
-
fs.writeFileSync(envPath, lines.filter(l => l.trim()).join('\n') + '\n');
|
|
1643
|
-
|
|
1644
|
-
console.log(`✓ Set ${keyUpper} in .claude/.env`);
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
/**
|
|
1648
|
-
* Unset environment variable
|
|
1649
|
-
*/
|
|
1650
|
-
envUnset(key, projectDir = process.cwd()) {
|
|
1651
|
-
if (!key) {
|
|
1652
|
-
console.error('Usage: claude-config env unset <KEY>');
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
const envPath = path.join(projectDir, '.claude', '.env');
|
|
1657
|
-
|
|
1658
|
-
if (!fs.existsSync(envPath)) {
|
|
1659
|
-
console.log('No .env file found.');
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
const keyUpper = key.toUpperCase();
|
|
1664
|
-
let lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
1665
|
-
const originalLength = lines.length;
|
|
1666
|
-
|
|
1667
|
-
lines = lines.filter(line => !line.startsWith(`${keyUpper}=`));
|
|
1668
|
-
|
|
1669
|
-
if (lines.length === originalLength) {
|
|
1670
|
-
console.log(`Variable ${keyUpper} not found.`);
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
fs.writeFileSync(envPath, lines.filter(l => l.trim()).join('\n') + '\n');
|
|
1675
|
-
console.log(`✓ Removed ${keyUpper} from .claude/.env`);
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
// ===========================================================================
|
|
1679
|
-
// PROJECT REGISTRY (for UI project switching)
|
|
1680
|
-
// ===========================================================================
|
|
1681
|
-
|
|
1682
|
-
/**
|
|
1683
|
-
* Get projects registry path
|
|
1684
|
-
*/
|
|
1685
|
-
getProjectsRegistryPath() {
|
|
1686
|
-
return path.join(this.installDir, 'projects.json');
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
/**
|
|
1690
|
-
* Load projects registry
|
|
1691
|
-
*/
|
|
1692
|
-
loadProjectsRegistry() {
|
|
1693
|
-
const registryPath = this.getProjectsRegistryPath();
|
|
1694
|
-
if (fs.existsSync(registryPath)) {
|
|
1695
|
-
try {
|
|
1696
|
-
return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
1697
|
-
} catch (e) {
|
|
1698
|
-
return { projects: [], activeProjectId: null };
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
return { projects: [], activeProjectId: null };
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
/**
|
|
1705
|
-
* Save projects registry
|
|
1706
|
-
*/
|
|
1707
|
-
saveProjectsRegistry(registry) {
|
|
1708
|
-
const registryPath = this.getProjectsRegistryPath();
|
|
1709
|
-
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
/**
|
|
1713
|
-
* List registered projects
|
|
1714
|
-
*/
|
|
1715
|
-
projectList() {
|
|
1716
|
-
const registry = this.loadProjectsRegistry();
|
|
1717
|
-
|
|
1718
|
-
if (registry.projects.length === 0) {
|
|
1719
|
-
console.log('\nNo projects registered.');
|
|
1720
|
-
console.log('Add one with: claude-config project add [path]\n');
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
console.log('\n📁 Registered Projects:\n');
|
|
1725
|
-
for (const p of registry.projects) {
|
|
1726
|
-
const active = p.id === registry.activeProjectId ? '→ ' : ' ';
|
|
1727
|
-
const exists = fs.existsSync(p.path) ? '' : ' (not found)';
|
|
1728
|
-
console.log(`${active}${p.name}${exists}`);
|
|
1729
|
-
console.log(` ${p.path}`);
|
|
1730
|
-
}
|
|
1731
|
-
console.log('');
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
/**
|
|
1735
|
-
* Add project to registry
|
|
1736
|
-
*/
|
|
1737
|
-
projectAdd(projectPath = process.cwd(), name = null) {
|
|
1738
|
-
const absPath = path.resolve(projectPath.replace(/^~/, process.env.HOME || ''));
|
|
1739
|
-
|
|
1740
|
-
if (!fs.existsSync(absPath)) {
|
|
1741
|
-
console.error(`Path not found: ${absPath}`);
|
|
1742
|
-
return false;
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
const registry = this.loadProjectsRegistry();
|
|
1746
|
-
|
|
1747
|
-
// Check for duplicate
|
|
1748
|
-
if (registry.projects.some(p => p.path === absPath)) {
|
|
1749
|
-
console.log(`Already registered: ${absPath}`);
|
|
1750
|
-
return false;
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
const project = {
|
|
1754
|
-
id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
|
|
1755
|
-
name: name || path.basename(absPath),
|
|
1756
|
-
path: absPath,
|
|
1757
|
-
addedAt: new Date().toISOString(),
|
|
1758
|
-
lastOpened: null
|
|
1759
|
-
};
|
|
1760
|
-
|
|
1761
|
-
registry.projects.push(project);
|
|
1762
|
-
|
|
1763
|
-
// If first project, make it active
|
|
1764
|
-
if (!registry.activeProjectId) {
|
|
1765
|
-
registry.activeProjectId = project.id;
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
this.saveProjectsRegistry(registry);
|
|
1769
|
-
console.log(`✓ Added project: ${project.name}`);
|
|
1770
|
-
console.log(` ${absPath}`);
|
|
1771
|
-
return true;
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
/**
|
|
1775
|
-
* Remove project from registry
|
|
1776
|
-
*/
|
|
1777
|
-
projectRemove(nameOrPath) {
|
|
1778
|
-
if (!nameOrPath) {
|
|
1779
|
-
console.error('Usage: claude-config project remove <name|path>');
|
|
1780
|
-
return false;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
const registry = this.loadProjectsRegistry();
|
|
1784
|
-
const absPath = path.resolve(nameOrPath.replace(/^~/, process.env.HOME || ''));
|
|
1785
|
-
|
|
1786
|
-
const idx = registry.projects.findIndex(
|
|
1787
|
-
p => p.name === nameOrPath || p.path === absPath
|
|
1788
|
-
);
|
|
1789
|
-
|
|
1790
|
-
if (idx === -1) {
|
|
1791
|
-
console.error(`Project not found: ${nameOrPath}`);
|
|
1792
|
-
return false;
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
const removed = registry.projects.splice(idx, 1)[0];
|
|
1796
|
-
|
|
1797
|
-
// If removed active project, select first remaining
|
|
1798
|
-
if (registry.activeProjectId === removed.id) {
|
|
1799
|
-
registry.activeProjectId = registry.projects[0]?.id || null;
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
this.saveProjectsRegistry(registry);
|
|
1803
|
-
console.log(`✓ Removed project: ${removed.name}`);
|
|
1804
|
-
return true;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// ===========================================================================
|
|
1808
|
-
// WORKSTREAMS
|
|
1809
|
-
// ===========================================================================
|
|
1810
|
-
|
|
1811
|
-
/**
|
|
1812
|
-
* Get workstreams file path
|
|
1813
|
-
*/
|
|
1814
|
-
getWorkstreamsPath() {
|
|
1815
|
-
return path.join(this.installDir, 'workstreams.json');
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
/**
|
|
1819
|
-
* Load workstreams
|
|
1820
|
-
*/
|
|
1821
|
-
loadWorkstreams() {
|
|
1822
|
-
const wsPath = this.getWorkstreamsPath();
|
|
1823
|
-
if (fs.existsSync(wsPath)) {
|
|
1824
|
-
try {
|
|
1825
|
-
return JSON.parse(fs.readFileSync(wsPath, 'utf8'));
|
|
1826
|
-
} catch (e) {
|
|
1827
|
-
return { workstreams: [], activeId: null, lastUsedByProject: {} };
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
return { workstreams: [], activeId: null, lastUsedByProject: {} };
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
/**
|
|
1834
|
-
* Save workstreams
|
|
1835
|
-
*/
|
|
1836
|
-
saveWorkstreams(data) {
|
|
1837
|
-
const wsPath = this.getWorkstreamsPath();
|
|
1838
|
-
const dir = path.dirname(wsPath);
|
|
1839
|
-
if (!fs.existsSync(dir)) {
|
|
1840
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1841
|
-
}
|
|
1842
|
-
fs.writeFileSync(wsPath, JSON.stringify(data, null, 2) + '\n');
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
/**
|
|
1846
|
-
* List all workstreams
|
|
1847
|
-
*/
|
|
1848
|
-
workstreamList() {
|
|
1849
|
-
const data = this.loadWorkstreams();
|
|
1850
|
-
|
|
1851
|
-
if (data.workstreams.length === 0) {
|
|
1852
|
-
console.log('\nNo workstreams defined.');
|
|
1853
|
-
console.log('Create one with: claude-config workstream create "Name"\n');
|
|
1854
|
-
return data.workstreams;
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
console.log('\n📋 Workstreams:\n');
|
|
1858
|
-
for (const ws of data.workstreams) {
|
|
1859
|
-
const active = ws.id === data.activeId ? '● ' : '○ ';
|
|
1860
|
-
console.log(`${active}${ws.name}`);
|
|
1861
|
-
if (ws.projects && ws.projects.length > 0) {
|
|
1862
|
-
console.log(` Projects: ${ws.projects.map(p => path.basename(p)).join(', ')}`);
|
|
1863
|
-
}
|
|
1864
|
-
if (ws.rules) {
|
|
1865
|
-
const preview = ws.rules.substring(0, 60).replace(/\n/g, ' ');
|
|
1866
|
-
console.log(` Rules: ${preview}${ws.rules.length > 60 ? '...' : ''}`);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
console.log('');
|
|
1870
|
-
return data.workstreams;
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
/**
|
|
1874
|
-
* Create a new workstream
|
|
1875
|
-
*/
|
|
1876
|
-
workstreamCreate(name, projects = [], rules = '') {
|
|
1877
|
-
if (!name) {
|
|
1878
|
-
console.error('Usage: claude-config workstream create "Name"');
|
|
1879
|
-
return null;
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
const data = this.loadWorkstreams();
|
|
1883
|
-
|
|
1884
|
-
// Check for duplicate name
|
|
1885
|
-
if (data.workstreams.some(ws => ws.name.toLowerCase() === name.toLowerCase())) {
|
|
1886
|
-
console.error(`Workstream "${name}" already exists`);
|
|
1887
|
-
return null;
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
const workstream = {
|
|
1891
|
-
id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
|
|
1892
|
-
name,
|
|
1893
|
-
projects: projects.map(p => path.resolve(p.replace(/^~/, process.env.HOME || ''))),
|
|
1894
|
-
rules: rules || '',
|
|
1895
|
-
createdAt: new Date().toISOString(),
|
|
1896
|
-
updatedAt: new Date().toISOString()
|
|
1897
|
-
};
|
|
1898
|
-
|
|
1899
|
-
data.workstreams.push(workstream);
|
|
1900
|
-
|
|
1901
|
-
// If first workstream, make it active
|
|
1902
|
-
if (!data.activeId) {
|
|
1903
|
-
data.activeId = workstream.id;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
this.saveWorkstreams(data);
|
|
1907
|
-
console.log(`✓ Created workstream: ${name}`);
|
|
1908
|
-
return workstream;
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
/**
|
|
1912
|
-
* Update a workstream
|
|
1913
|
-
*/
|
|
1914
|
-
workstreamUpdate(idOrName, updates) {
|
|
1915
|
-
const data = this.loadWorkstreams();
|
|
1916
|
-
const ws = data.workstreams.find(
|
|
1917
|
-
w => w.id === idOrName || w.name.toLowerCase() === idOrName.toLowerCase()
|
|
1918
|
-
);
|
|
1919
|
-
|
|
1920
|
-
if (!ws) {
|
|
1921
|
-
console.error(`Workstream not found: ${idOrName}`);
|
|
1922
|
-
return null;
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
if (updates.name !== undefined) ws.name = updates.name;
|
|
1926
|
-
if (updates.projects !== undefined) {
|
|
1927
|
-
ws.projects = updates.projects.map(p =>
|
|
1928
|
-
path.resolve(p.replace(/^~/, process.env.HOME || ''))
|
|
1929
|
-
);
|
|
1930
|
-
}
|
|
1931
|
-
if (updates.rules !== undefined) ws.rules = updates.rules;
|
|
1932
|
-
ws.updatedAt = new Date().toISOString();
|
|
1933
|
-
|
|
1934
|
-
this.saveWorkstreams(data);
|
|
1935
|
-
console.log(`✓ Updated workstream: ${ws.name}`);
|
|
1936
|
-
return ws;
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
/**
|
|
1940
|
-
* Delete a workstream
|
|
1941
|
-
*/
|
|
1942
|
-
workstreamDelete(idOrName) {
|
|
1943
|
-
const data = this.loadWorkstreams();
|
|
1944
|
-
const idx = data.workstreams.findIndex(
|
|
1945
|
-
w => w.id === idOrName || w.name.toLowerCase() === idOrName.toLowerCase()
|
|
1946
|
-
);
|
|
1947
|
-
|
|
1948
|
-
if (idx === -1) {
|
|
1949
|
-
console.error(`Workstream not found: ${idOrName}`);
|
|
1950
|
-
return false;
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
const removed = data.workstreams.splice(idx, 1)[0];
|
|
1954
|
-
|
|
1955
|
-
// If removed active workstream, select first remaining
|
|
1956
|
-
if (data.activeId === removed.id) {
|
|
1957
|
-
data.activeId = data.workstreams[0]?.id || null;
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
this.saveWorkstreams(data);
|
|
1961
|
-
console.log(`✓ Deleted workstream: ${removed.name}`);
|
|
1962
|
-
return true;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
/**
|
|
1966
|
-
* Set active workstream
|
|
1967
|
-
*/
|
|
1968
|
-
workstreamUse(idOrName) {
|
|
1969
|
-
const data = this.loadWorkstreams();
|
|
1970
|
-
|
|
1971
|
-
if (!idOrName) {
|
|
1972
|
-
// Show current active
|
|
1973
|
-
const active = data.workstreams.find(w => w.id === data.activeId);
|
|
1974
|
-
if (active) {
|
|
1975
|
-
console.log(`Active workstream: ${active.name}`);
|
|
1976
|
-
} else {
|
|
1977
|
-
console.log('No active workstream');
|
|
1978
|
-
}
|
|
1979
|
-
return active || null;
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
const ws = data.workstreams.find(
|
|
1983
|
-
w => w.id === idOrName || w.name.toLowerCase() === idOrName.toLowerCase()
|
|
1984
|
-
);
|
|
1985
|
-
|
|
1986
|
-
if (!ws) {
|
|
1987
|
-
console.error(`Workstream not found: ${idOrName}`);
|
|
1988
|
-
return null;
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
data.activeId = ws.id;
|
|
1992
|
-
this.saveWorkstreams(data);
|
|
1993
|
-
console.log(`✓ Switched to workstream: ${ws.name}`);
|
|
1994
|
-
return ws;
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
/**
|
|
1998
|
-
* Get active workstream
|
|
1999
|
-
*/
|
|
2000
|
-
workstreamActive() {
|
|
2001
|
-
const data = this.loadWorkstreams();
|
|
2002
|
-
return data.workstreams.find(w => w.id === data.activeId) || null;
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
/**
|
|
2006
|
-
* Add project to workstream
|
|
2007
|
-
*/
|
|
2008
|
-
workstreamAddProject(idOrName, projectPath) {
|
|
2009
|
-
const data = this.loadWorkstreams();
|
|
2010
|
-
const ws = data.workstreams.find(
|
|
2011
|
-
w => w.id === idOrName || w.name.toLowerCase() === idOrName.toLowerCase()
|
|
2012
|
-
);
|
|
2013
|
-
|
|
2014
|
-
if (!ws) {
|
|
2015
|
-
console.error(`Workstream not found: ${idOrName}`);
|
|
2016
|
-
return null;
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
const absPath = path.resolve(projectPath.replace(/^~/, process.env.HOME || ''));
|
|
2020
|
-
|
|
2021
|
-
if (!ws.projects.includes(absPath)) {
|
|
2022
|
-
ws.projects.push(absPath);
|
|
2023
|
-
ws.updatedAt = new Date().toISOString();
|
|
2024
|
-
this.saveWorkstreams(data);
|
|
2025
|
-
console.log(`✓ Added ${path.basename(absPath)} to ${ws.name}`);
|
|
2026
|
-
} else {
|
|
2027
|
-
console.log(`Project already in workstream: ${path.basename(absPath)}`);
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
return ws;
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
/**
|
|
2034
|
-
* Remove project from workstream
|
|
2035
|
-
*/
|
|
2036
|
-
workstreamRemoveProject(idOrName, projectPath) {
|
|
2037
|
-
const data = this.loadWorkstreams();
|
|
2038
|
-
const ws = data.workstreams.find(
|
|
2039
|
-
w => w.id === idOrName || w.name.toLowerCase() === idOrName.toLowerCase()
|
|
2040
|
-
);
|
|
2041
|
-
|
|
2042
|
-
if (!ws) {
|
|
2043
|
-
console.error(`Workstream not found: ${idOrName}`);
|
|
2044
|
-
return null;
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
const absPath = path.resolve(projectPath.replace(/^~/, process.env.HOME || ''));
|
|
2048
|
-
const idx = ws.projects.indexOf(absPath);
|
|
2049
|
-
|
|
2050
|
-
if (idx !== -1) {
|
|
2051
|
-
ws.projects.splice(idx, 1);
|
|
2052
|
-
ws.updatedAt = new Date().toISOString();
|
|
2053
|
-
this.saveWorkstreams(data);
|
|
2054
|
-
console.log(`✓ Removed ${path.basename(absPath)} from ${ws.name}`);
|
|
2055
|
-
} else {
|
|
2056
|
-
console.log(`Project not in workstream: ${path.basename(absPath)}`);
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
return ws;
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
/**
|
|
2063
|
-
* Inject active workstream rules into Claude context
|
|
2064
|
-
* Called by pre-prompt hook
|
|
2065
|
-
*/
|
|
2066
|
-
workstreamInject(silent = false) {
|
|
2067
|
-
const active = this.workstreamActive();
|
|
2068
|
-
|
|
2069
|
-
if (!active) {
|
|
2070
|
-
if (!silent) console.log('No active workstream');
|
|
2071
|
-
return null;
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
if (!active.rules || active.rules.trim() === '') {
|
|
2075
|
-
if (!silent) console.log(`Workstream "${active.name}" has no rules defined`);
|
|
2076
|
-
return null;
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
// Output rules to stdout for hook to capture
|
|
2080
|
-
const header = `## Active Workstream: ${active.name}\n\n`;
|
|
2081
|
-
const output = header + active.rules;
|
|
2082
|
-
|
|
2083
|
-
if (!silent) {
|
|
2084
|
-
console.log(output);
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
return output;
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
/**
|
|
2091
|
-
* Detect workstream from current directory
|
|
2092
|
-
*/
|
|
2093
|
-
workstreamDetect(dir = process.cwd()) {
|
|
2094
|
-
const data = this.loadWorkstreams();
|
|
2095
|
-
const absDir = path.resolve(dir.replace(/^~/, process.env.HOME || ''));
|
|
2096
|
-
|
|
2097
|
-
// Find workstreams that contain this directory or a parent
|
|
2098
|
-
const matches = data.workstreams.filter(ws =>
|
|
2099
|
-
ws.projects.some(p => absDir.startsWith(p) || p.startsWith(absDir))
|
|
2100
|
-
);
|
|
2101
|
-
|
|
2102
|
-
if (matches.length === 0) {
|
|
2103
|
-
return null;
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
if (matches.length === 1) {
|
|
2107
|
-
return matches[0];
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
// Multiple matches - check lastUsedByProject
|
|
2111
|
-
if (data.lastUsedByProject && data.lastUsedByProject[absDir]) {
|
|
2112
|
-
const lastUsed = matches.find(ws => ws.id === data.lastUsedByProject[absDir]);
|
|
2113
|
-
if (lastUsed) return lastUsed;
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
// Return most recently updated
|
|
2117
|
-
return matches.sort((a, b) =>
|
|
2118
|
-
new Date(b.updatedAt) - new Date(a.updatedAt)
|
|
2119
|
-
)[0];
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
/**
|
|
2123
|
-
* Get workstream by ID
|
|
2124
|
-
*/
|
|
2125
|
-
workstreamGet(id) {
|
|
2126
|
-
const data = this.loadWorkstreams();
|
|
2127
|
-
return data.workstreams.find(w => w.id === id) || null;
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
// ===========================================================================
|
|
2131
|
-
// ACTIVITY TRACKING
|
|
2132
|
-
// ===========================================================================
|
|
2133
|
-
|
|
2134
|
-
/**
|
|
2135
|
-
* Get activity file path
|
|
2136
|
-
*/
|
|
2137
|
-
getActivityPath() {
|
|
2138
|
-
return path.join(this.installDir, 'activity.json');
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
/**
|
|
2142
|
-
* Load activity data
|
|
2143
|
-
*/
|
|
2144
|
-
loadActivity() {
|
|
2145
|
-
const activityPath = this.getActivityPath();
|
|
2146
|
-
if (fs.existsSync(activityPath)) {
|
|
2147
|
-
try {
|
|
2148
|
-
return JSON.parse(fs.readFileSync(activityPath, 'utf8'));
|
|
2149
|
-
} catch (e) {
|
|
2150
|
-
return this.getDefaultActivity();
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
return this.getDefaultActivity();
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
/**
|
|
2157
|
-
* Get default activity structure
|
|
2158
|
-
*/
|
|
2159
|
-
getDefaultActivity() {
|
|
2160
|
-
return {
|
|
2161
|
-
sessions: [],
|
|
2162
|
-
projectStats: {},
|
|
2163
|
-
coActivity: {},
|
|
2164
|
-
lastUpdated: null
|
|
2165
|
-
};
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
/**
|
|
2169
|
-
* Save activity data
|
|
2170
|
-
*/
|
|
2171
|
-
saveActivity(data) {
|
|
2172
|
-
const activityPath = this.getActivityPath();
|
|
2173
|
-
const dir = path.dirname(activityPath);
|
|
2174
|
-
if (!fs.existsSync(dir)) {
|
|
2175
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
2176
|
-
}
|
|
2177
|
-
data.lastUpdated = new Date().toISOString();
|
|
2178
|
-
fs.writeFileSync(activityPath, JSON.stringify(data, null, 2) + '\n');
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
/**
|
|
2182
|
-
* Log activity from a Claude session
|
|
2183
|
-
*/
|
|
2184
|
-
activityLog(files, sessionId = null) {
|
|
2185
|
-
const data = this.loadActivity();
|
|
2186
|
-
const now = new Date().toISOString();
|
|
2187
|
-
|
|
2188
|
-
if (!sessionId) {
|
|
2189
|
-
sessionId = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
let session = data.sessions.find(s => s.id === sessionId);
|
|
2193
|
-
if (!session) {
|
|
2194
|
-
session = { id: sessionId, startedAt: now, files: [], projects: [] };
|
|
2195
|
-
data.sessions.push(session);
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
const projectsInSession = new Set(session.projects);
|
|
2199
|
-
|
|
2200
|
-
for (const file of files) {
|
|
2201
|
-
// Handle both string paths and {path, action} objects
|
|
2202
|
-
const rawPath = typeof file === 'string' ? file : file.path;
|
|
2203
|
-
if (!rawPath) continue;
|
|
2204
|
-
const filePath = path.resolve(rawPath.replace(/^~/, process.env.HOME || ''));
|
|
2205
|
-
const action = typeof file === 'object' ? (file.action || 'access') : 'access';
|
|
2206
|
-
|
|
2207
|
-
session.files.push({ path: filePath, action, timestamp: now });
|
|
2208
|
-
|
|
2209
|
-
const projectPath = this.detectProjectRoot(filePath);
|
|
2210
|
-
if (projectPath) {
|
|
2211
|
-
projectsInSession.add(projectPath);
|
|
2212
|
-
|
|
2213
|
-
if (!data.projectStats[projectPath]) {
|
|
2214
|
-
data.projectStats[projectPath] = { fileCount: 0, lastActive: now, sessionCount: 0 };
|
|
2215
|
-
}
|
|
2216
|
-
data.projectStats[projectPath].fileCount++;
|
|
2217
|
-
data.projectStats[projectPath].lastActive = now;
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
session.projects = Array.from(projectsInSession);
|
|
2222
|
-
|
|
2223
|
-
// Update co-activity
|
|
2224
|
-
const projects = session.projects;
|
|
2225
|
-
for (let i = 0; i < projects.length; i++) {
|
|
2226
|
-
for (let j = i + 1; j < projects.length; j++) {
|
|
2227
|
-
const p1 = projects[i], p2 = projects[j];
|
|
2228
|
-
if (!data.coActivity[p1]) data.coActivity[p1] = {};
|
|
2229
|
-
if (!data.coActivity[p2]) data.coActivity[p2] = {};
|
|
2230
|
-
data.coActivity[p1][p2] = (data.coActivity[p1][p2] || 0) + 1;
|
|
2231
|
-
data.coActivity[p2][p1] = (data.coActivity[p2][p1] || 0) + 1;
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
if (data.sessions.length > 100) {
|
|
2236
|
-
data.sessions = data.sessions.slice(-100);
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
this.saveActivity(data);
|
|
2240
|
-
return { sessionId, filesLogged: files.length, projects: session.projects };
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
/**
|
|
2244
|
-
* Detect project root by finding .git or .claude folder
|
|
2245
|
-
*/
|
|
2246
|
-
detectProjectRoot(filePath) {
|
|
2247
|
-
let dir = path.dirname(filePath);
|
|
2248
|
-
const home = process.env.HOME || '';
|
|
2249
|
-
|
|
2250
|
-
while (dir && dir !== '/' && dir !== home) {
|
|
2251
|
-
if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, '.claude'))) {
|
|
2252
|
-
return dir;
|
|
2253
|
-
}
|
|
2254
|
-
dir = path.dirname(dir);
|
|
2255
|
-
}
|
|
2256
|
-
return null;
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
/**
|
|
2260
|
-
* Get activity summary for UI
|
|
2261
|
-
*/
|
|
2262
|
-
activitySummary() {
|
|
2263
|
-
const data = this.loadActivity();
|
|
2264
|
-
const now = new Date();
|
|
2265
|
-
const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
|
|
2266
|
-
|
|
2267
|
-
const recentSessions = data.sessions.filter(s => new Date(s.startedAt) > oneDayAgo);
|
|
2268
|
-
|
|
2269
|
-
const projectActivity = Object.entries(data.projectStats)
|
|
2270
|
-
.map(([projectPath, stats]) => ({
|
|
2271
|
-
path: projectPath,
|
|
2272
|
-
name: path.basename(projectPath),
|
|
2273
|
-
...stats,
|
|
2274
|
-
isRecent: new Date(stats.lastActive) > oneDayAgo
|
|
2275
|
-
}))
|
|
2276
|
-
.sort((a, b) => new Date(b.lastActive) - new Date(a.lastActive));
|
|
2277
|
-
|
|
2278
|
-
const coActiveProjects = [];
|
|
2279
|
-
for (const [project, coProjects] of Object.entries(data.coActivity)) {
|
|
2280
|
-
for (const [otherProject, count] of Object.entries(coProjects)) {
|
|
2281
|
-
if (count >= 2 && project < otherProject) {
|
|
2282
|
-
coActiveProjects.push({
|
|
2283
|
-
projects: [project, otherProject],
|
|
2284
|
-
names: [path.basename(project), path.basename(otherProject)],
|
|
2285
|
-
count
|
|
2286
|
-
});
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
coActiveProjects.sort((a, b) => b.count - a.count);
|
|
2291
|
-
|
|
2292
|
-
// Calculate total files across all sessions
|
|
2293
|
-
const totalFiles = data.sessions.reduce((sum, s) => sum + (s.files?.length || 0), 0);
|
|
2294
|
-
|
|
2295
|
-
return {
|
|
2296
|
-
totalSessions: data.sessions.length,
|
|
2297
|
-
recentSessions: recentSessions.length,
|
|
2298
|
-
totalFiles,
|
|
2299
|
-
projectCount: Object.keys(data.projectStats).length,
|
|
2300
|
-
topProjects: projectActivity.slice(0, 10),
|
|
2301
|
-
projectActivity: projectActivity.slice(0, 20),
|
|
2302
|
-
coActiveProjects: coActiveProjects.slice(0, 10),
|
|
2303
|
-
lastUpdated: data.lastUpdated
|
|
2304
|
-
};
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
/**
|
|
2308
|
-
* Suggest workstreams based on activity patterns
|
|
2309
|
-
*/
|
|
2310
|
-
activitySuggestWorkstreams() {
|
|
2311
|
-
const data = this.loadActivity();
|
|
2312
|
-
const workstreams = this.loadWorkstreams();
|
|
2313
|
-
const suggestions = [];
|
|
2314
|
-
|
|
2315
|
-
const coGroups = new Map();
|
|
2316
|
-
|
|
2317
|
-
for (const session of data.sessions) {
|
|
2318
|
-
if (session.projects.length >= 2) {
|
|
2319
|
-
const key = session.projects.sort().join('|');
|
|
2320
|
-
coGroups.set(key, (coGroups.get(key) || 0) + 1);
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
for (const [key, count] of coGroups) {
|
|
2325
|
-
if (count >= 3) {
|
|
2326
|
-
const projects = key.split('|');
|
|
2327
|
-
const existingWs = workstreams.workstreams.find(ws =>
|
|
2328
|
-
projects.every(p => ws.projects.includes(p))
|
|
2329
|
-
);
|
|
2330
|
-
|
|
2331
|
-
if (!existingWs) {
|
|
2332
|
-
// Calculate co-activity score as percentage of total sessions
|
|
2333
|
-
const totalSessions = data.sessions.length;
|
|
2334
|
-
const coActivityScore = totalSessions > 0 ? Math.round((count / totalSessions) * 100) : 0;
|
|
2335
|
-
|
|
2336
|
-
suggestions.push({
|
|
2337
|
-
projects,
|
|
2338
|
-
name: this.generateWorkstreamName(projects),
|
|
2339
|
-
names: projects.map(p => path.basename(p)),
|
|
2340
|
-
sessionCount: count,
|
|
2341
|
-
coActivityScore: Math.min(coActivityScore, 100),
|
|
2342
|
-
});
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
suggestions.sort((a, b) => b.sessionCount - a.sessionCount);
|
|
2348
|
-
return suggestions.slice(0, 5);
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
/**
|
|
2352
|
-
* Generate a workstream name from project names
|
|
2353
|
-
*/
|
|
2354
|
-
generateWorkstreamName(projects) {
|
|
2355
|
-
const names = projects.map(p => path.basename(p));
|
|
2356
|
-
if (names.length <= 2) return names.join(' + ');
|
|
2357
|
-
return `${names[0]} + ${names.length - 1} more`;
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
/**
|
|
2361
|
-
* Clear old activity data
|
|
2362
|
-
*/
|
|
2363
|
-
activityClear(olderThanDays = 30) {
|
|
2364
|
-
const data = this.loadActivity();
|
|
2365
|
-
const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
|
|
2366
|
-
|
|
2367
|
-
data.sessions = data.sessions.filter(s => new Date(s.startedAt) > cutoff);
|
|
2368
|
-
data.projectStats = {};
|
|
2369
|
-
data.coActivity = {};
|
|
2370
|
-
|
|
2371
|
-
for (const session of data.sessions) {
|
|
2372
|
-
for (const file of session.files) {
|
|
2373
|
-
const projectPath = this.detectProjectRoot(file.path);
|
|
2374
|
-
if (projectPath) {
|
|
2375
|
-
if (!data.projectStats[projectPath]) {
|
|
2376
|
-
data.projectStats[projectPath] = { fileCount: 0, lastActive: session.startedAt, sessionCount: 0 };
|
|
2377
|
-
}
|
|
2378
|
-
data.projectStats[projectPath].fileCount++;
|
|
2379
|
-
if (session.startedAt > data.projectStats[projectPath].lastActive) {
|
|
2380
|
-
data.projectStats[projectPath].lastActive = session.startedAt;
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
const projects = session.projects;
|
|
2386
|
-
for (let i = 0; i < projects.length; i++) {
|
|
2387
|
-
for (let j = i + 1; j < projects.length; j++) {
|
|
2388
|
-
const p1 = projects[i], p2 = projects[j];
|
|
2389
|
-
if (!data.coActivity[p1]) data.coActivity[p1] = {};
|
|
2390
|
-
if (!data.coActivity[p2]) data.coActivity[p2] = {};
|
|
2391
|
-
data.coActivity[p1][p2] = (data.coActivity[p1][p2] || 0) + 1;
|
|
2392
|
-
data.coActivity[p2][p1] = (data.coActivity[p2][p1] || 0) + 1;
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
|
-
this.saveActivity(data);
|
|
2398
|
-
return { sessionsRemaining: data.sessions.length };
|
|
2399
|
-
}
|
|
2400
|
-
|
|
2401
|
-
// ===========================================================================
|
|
2402
|
-
// SMART SYNC (Phase 3) - Auto-detect and nudge workstream switching
|
|
2403
|
-
// ===========================================================================
|
|
2404
|
-
|
|
2405
|
-
/**
|
|
2406
|
-
* Get path to smart sync preferences file
|
|
2407
|
-
*/
|
|
2408
|
-
getSmartSyncPath() {
|
|
2409
|
-
return path.join(this.installDir, 'smart-sync.json');
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
/**
|
|
2413
|
-
* Load smart sync preferences
|
|
2414
|
-
*/
|
|
2415
|
-
loadSmartSyncPrefs() {
|
|
2416
|
-
const prefsPath = this.getSmartSyncPath();
|
|
2417
|
-
try {
|
|
2418
|
-
if (fs.existsSync(prefsPath)) {
|
|
2419
|
-
return JSON.parse(fs.readFileSync(prefsPath, 'utf8'));
|
|
2420
|
-
}
|
|
2421
|
-
} catch (e) {
|
|
2422
|
-
// Ignore errors, return defaults
|
|
2423
|
-
}
|
|
2424
|
-
return {
|
|
2425
|
-
enabled: true,
|
|
2426
|
-
autoSwitchThreshold: 80, // % of activity that must match
|
|
2427
|
-
projectChoices: {}, // { projectPath: { workstreamId, choice: 'always'|'never'|'ask' } }
|
|
2428
|
-
dismissedNudges: [], // Array of dismissed nudge keys
|
|
2429
|
-
lastActiveWorkstream: null,
|
|
2430
|
-
lastNudgeTime: null
|
|
2431
|
-
};
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
/**
|
|
2435
|
-
* Save smart sync preferences
|
|
2436
|
-
*/
|
|
2437
|
-
saveSmartSyncPrefs(prefs) {
|
|
2438
|
-
const prefsPath = this.getSmartSyncPath();
|
|
2439
|
-
fs.writeFileSync(prefsPath, JSON.stringify(prefs, null, 2));
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
/**
|
|
2443
|
-
* Remember user's choice for a project-workstream association
|
|
2444
|
-
* @param {string} projectPath - The project path
|
|
2445
|
-
* @param {string} workstreamId - The workstream ID
|
|
2446
|
-
* @param {string} choice - 'always', 'never', or 'ask'
|
|
2447
|
-
*/
|
|
2448
|
-
smartSyncRememberChoice(projectPath, workstreamId, choice) {
|
|
2449
|
-
const prefs = this.loadSmartSyncPrefs();
|
|
2450
|
-
prefs.projectChoices[projectPath] = { workstreamId, choice, savedAt: new Date().toISOString() };
|
|
2451
|
-
this.saveSmartSyncPrefs(prefs);
|
|
2452
|
-
return { success: true, projectPath, workstreamId, choice };
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
/**
|
|
2456
|
-
* Dismiss a nudge so it won't show again
|
|
2457
|
-
* @param {string} nudgeKey - Unique key for the nudge (e.g., "switch:ws123" or "add:proj:/path")
|
|
2458
|
-
*/
|
|
2459
|
-
smartSyncDismissNudge(nudgeKey) {
|
|
2460
|
-
const prefs = this.loadSmartSyncPrefs();
|
|
2461
|
-
if (!prefs.dismissedNudges.includes(nudgeKey)) {
|
|
2462
|
-
prefs.dismissedNudges.push(nudgeKey);
|
|
2463
|
-
}
|
|
2464
|
-
this.saveSmartSyncPrefs(prefs);
|
|
2465
|
-
return { success: true, nudgeKey };
|
|
2466
|
-
}
|
|
2467
|
-
|
|
2468
|
-
/**
|
|
2469
|
-
* Update smart sync settings
|
|
2470
|
-
*/
|
|
2471
|
-
smartSyncUpdateSettings(settings) {
|
|
2472
|
-
const prefs = this.loadSmartSyncPrefs();
|
|
2473
|
-
if (settings.enabled !== undefined) prefs.enabled = settings.enabled;
|
|
2474
|
-
if (settings.autoSwitchThreshold !== undefined) prefs.autoSwitchThreshold = settings.autoSwitchThreshold;
|
|
2475
|
-
this.saveSmartSyncPrefs(prefs);
|
|
2476
|
-
return { success: true, settings: prefs };
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
/**
|
|
2480
|
-
* Detect which workstream best matches current activity
|
|
2481
|
-
* @param {string[]} currentProjects - Array of project paths currently being worked on
|
|
2482
|
-
* @returns {Object} Detection result with suggested workstream and confidence
|
|
2483
|
-
*/
|
|
2484
|
-
smartSyncDetect(currentProjects = []) {
|
|
2485
|
-
const prefs = this.loadSmartSyncPrefs();
|
|
2486
|
-
const workstreams = this.loadWorkstreams();
|
|
2487
|
-
|
|
2488
|
-
if (!prefs.enabled || !currentProjects.length || !workstreams.workstreams.length) {
|
|
2489
|
-
return { suggestion: null, reason: 'disabled_or_no_data' };
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
// Check for "always" choices first
|
|
2493
|
-
for (const projectPath of currentProjects) {
|
|
2494
|
-
const choice = prefs.projectChoices[projectPath];
|
|
2495
|
-
if (choice && choice.choice === 'always') {
|
|
2496
|
-
const ws = workstreams.workstreams.find(w => w.id === choice.workstreamId);
|
|
2497
|
-
if (ws) {
|
|
2498
|
-
return {
|
|
2499
|
-
suggestion: ws,
|
|
2500
|
-
confidence: 100,
|
|
2501
|
-
reason: 'user_preference',
|
|
2502
|
-
autoSwitch: true
|
|
2503
|
-
};
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
// Check for "never" choices - exclude those workstreams
|
|
2509
|
-
const excludedWorkstreams = new Set();
|
|
2510
|
-
for (const projectPath of currentProjects) {
|
|
2511
|
-
const choice = prefs.projectChoices[projectPath];
|
|
2512
|
-
if (choice && choice.choice === 'never') {
|
|
2513
|
-
excludedWorkstreams.add(choice.workstreamId);
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
// Score each workstream based on project overlap
|
|
2518
|
-
const scores = [];
|
|
2519
|
-
for (const ws of workstreams.workstreams) {
|
|
2520
|
-
if (excludedWorkstreams.has(ws.id)) continue;
|
|
2521
|
-
if (ws.id === workstreams.activeId) continue; // Don't suggest current
|
|
2522
|
-
|
|
2523
|
-
const wsProjects = ws.projects || [];
|
|
2524
|
-
if (wsProjects.length === 0) continue;
|
|
2525
|
-
|
|
2526
|
-
// Calculate overlap
|
|
2527
|
-
const matchingProjects = currentProjects.filter(p => wsProjects.includes(p));
|
|
2528
|
-
const overlapPercent = (matchingProjects.length / currentProjects.length) * 100;
|
|
2529
|
-
const coveragePercent = (matchingProjects.length / wsProjects.length) * 100;
|
|
2530
|
-
|
|
2531
|
-
// Combined score: weighted average of overlap and coverage
|
|
2532
|
-
const confidence = Math.round((overlapPercent * 0.7) + (coveragePercent * 0.3));
|
|
2533
|
-
|
|
2534
|
-
if (confidence > 0) {
|
|
2535
|
-
scores.push({
|
|
2536
|
-
workstream: ws,
|
|
2537
|
-
confidence,
|
|
2538
|
-
matchingProjects,
|
|
2539
|
-
overlapPercent,
|
|
2540
|
-
coveragePercent
|
|
2541
|
-
});
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
// Sort by confidence
|
|
2546
|
-
scores.sort((a, b) => b.confidence - a.confidence);
|
|
2547
|
-
|
|
2548
|
-
if (scores.length === 0) {
|
|
2549
|
-
return { suggestion: null, reason: 'no_matching_workstream' };
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
const best = scores[0];
|
|
2553
|
-
const shouldAutoSwitch = best.confidence >= prefs.autoSwitchThreshold;
|
|
2554
|
-
|
|
2555
|
-
return {
|
|
2556
|
-
suggestion: best.workstream,
|
|
2557
|
-
confidence: best.confidence,
|
|
2558
|
-
matchingProjects: best.matchingProjects,
|
|
2559
|
-
reason: shouldAutoSwitch ? 'high_confidence_match' : 'partial_match',
|
|
2560
|
-
autoSwitch: shouldAutoSwitch,
|
|
2561
|
-
alternatives: scores.slice(1, 3).map(s => ({
|
|
2562
|
-
workstream: s.workstream,
|
|
2563
|
-
confidence: s.confidence
|
|
2564
|
-
}))
|
|
2565
|
-
};
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
/**
|
|
2569
|
-
* Check if we should show a nudge and what type
|
|
2570
|
-
* @param {string[]} currentProjects - Currently active projects
|
|
2571
|
-
* @returns {Object|null} Nudge to show, or null if none needed
|
|
2572
|
-
*/
|
|
2573
|
-
smartSyncCheckNudge(currentProjects = []) {
|
|
2574
|
-
const prefs = this.loadSmartSyncPrefs();
|
|
2575
|
-
const workstreams = this.loadWorkstreams();
|
|
2576
|
-
const activeWs = workstreams.workstreams.find(w => w.id === workstreams.activeId);
|
|
2577
|
-
|
|
2578
|
-
if (!prefs.enabled || !currentProjects.length) {
|
|
2579
|
-
return null;
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
// Rate limit nudges (max once per 5 minutes)
|
|
2583
|
-
if (prefs.lastNudgeTime) {
|
|
2584
|
-
const timeSince = Date.now() - new Date(prefs.lastNudgeTime).getTime();
|
|
2585
|
-
if (timeSince < 5 * 60 * 1000) {
|
|
2586
|
-
return null;
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
|
-
const nudges = [];
|
|
2591
|
-
|
|
2592
|
-
// Check 1: Should we suggest switching workstreams?
|
|
2593
|
-
const detection = this.smartSyncDetect(currentProjects);
|
|
2594
|
-
if (detection.suggestion && detection.confidence >= 50) {
|
|
2595
|
-
const nudgeKey = `switch:${detection.suggestion.id}`;
|
|
2596
|
-
if (!prefs.dismissedNudges.includes(nudgeKey)) {
|
|
2597
|
-
nudges.push({
|
|
2598
|
-
type: 'switch',
|
|
2599
|
-
key: nudgeKey,
|
|
2600
|
-
message: `Working on ${currentProjects.map(p => path.basename(p)).join(', ')}. Switch to "${detection.suggestion.name}"?`,
|
|
2601
|
-
workstream: detection.suggestion,
|
|
2602
|
-
confidence: detection.confidence,
|
|
2603
|
-
autoSwitch: detection.autoSwitch,
|
|
2604
|
-
actions: [
|
|
2605
|
-
{ label: 'Yes', action: 'switch' },
|
|
2606
|
-
{ label: 'No', action: 'dismiss' },
|
|
2607
|
-
{ label: 'Always', action: 'always' }
|
|
2608
|
-
]
|
|
2609
|
-
});
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
// Check 2: New project not in active workstream?
|
|
2614
|
-
if (activeWs) {
|
|
2615
|
-
for (const projectPath of currentProjects) {
|
|
2616
|
-
if (!activeWs.projects?.includes(projectPath)) {
|
|
2617
|
-
const nudgeKey = `add:${activeWs.id}:${projectPath}`;
|
|
2618
|
-
if (!prefs.dismissedNudges.includes(nudgeKey)) {
|
|
2619
|
-
// Check if this project isn't in any workstream
|
|
2620
|
-
const inOtherWs = workstreams.workstreams.some(
|
|
2621
|
-
ws => ws.id !== activeWs.id && ws.projects?.includes(projectPath)
|
|
2622
|
-
);
|
|
2623
|
-
if (!inOtherWs) {
|
|
2624
|
-
nudges.push({
|
|
2625
|
-
type: 'add_project',
|
|
2626
|
-
key: nudgeKey,
|
|
2627
|
-
message: `New project "${path.basename(projectPath)}" detected. Add to "${activeWs.name}"?`,
|
|
2628
|
-
workstream: activeWs,
|
|
2629
|
-
projectPath,
|
|
2630
|
-
actions: [
|
|
2631
|
-
{ label: 'Yes', action: 'add' },
|
|
2632
|
-
{ label: 'No', action: 'dismiss' },
|
|
2633
|
-
{ label: 'Never', action: 'never' }
|
|
2634
|
-
]
|
|
2635
|
-
});
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
if (nudges.length === 0) {
|
|
2643
|
-
return null;
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
// Return the highest priority nudge (switch > add_project)
|
|
2647
|
-
const nudge = nudges.find(n => n.type === 'switch') || nudges[0];
|
|
2648
|
-
|
|
2649
|
-
// Update last nudge time
|
|
2650
|
-
prefs.lastNudgeTime = new Date().toISOString();
|
|
2651
|
-
this.saveSmartSyncPrefs(prefs);
|
|
2652
|
-
|
|
2653
|
-
return nudge;
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
/**
|
|
2657
|
-
* Handle a nudge action
|
|
2658
|
-
* @param {string} nudgeKey - The nudge key
|
|
2659
|
-
* @param {string} action - The action taken ('switch', 'add', 'dismiss', 'always', 'never')
|
|
2660
|
-
* @param {Object} context - Additional context (workstreamId, projectPath, etc.)
|
|
2661
|
-
*/
|
|
2662
|
-
smartSyncHandleAction(nudgeKey, action, context = {}) {
|
|
2663
|
-
const prefs = this.loadSmartSyncPrefs();
|
|
2664
|
-
|
|
2665
|
-
switch (action) {
|
|
2666
|
-
case 'switch':
|
|
2667
|
-
// Switch to the suggested workstream
|
|
2668
|
-
if (context.workstreamId) {
|
|
2669
|
-
this.workstreamUse(context.workstreamId);
|
|
2670
|
-
}
|
|
2671
|
-
break;
|
|
2672
|
-
|
|
2673
|
-
case 'add':
|
|
2674
|
-
// Add project to workstream
|
|
2675
|
-
if (context.workstreamId && context.projectPath) {
|
|
2676
|
-
this.workstreamAddProject(context.workstreamId, context.projectPath);
|
|
2677
|
-
}
|
|
2678
|
-
break;
|
|
2679
|
-
|
|
2680
|
-
case 'always':
|
|
2681
|
-
// Remember to always use this workstream for these projects
|
|
2682
|
-
if (context.workstreamId && context.projects) {
|
|
2683
|
-
for (const projectPath of context.projects) {
|
|
2684
|
-
this.smartSyncRememberChoice(projectPath, context.workstreamId, 'always');
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
// Also switch
|
|
2688
|
-
if (context.workstreamId) {
|
|
2689
|
-
this.workstreamUse(context.workstreamId);
|
|
2690
|
-
}
|
|
2691
|
-
break;
|
|
2692
|
-
|
|
2693
|
-
case 'never':
|
|
2694
|
-
// Remember to never suggest this
|
|
2695
|
-
if (context.workstreamId && context.projectPath) {
|
|
2696
|
-
this.smartSyncRememberChoice(context.projectPath, context.workstreamId, 'never');
|
|
2697
|
-
}
|
|
2698
|
-
this.smartSyncDismissNudge(nudgeKey);
|
|
2699
|
-
break;
|
|
2700
|
-
|
|
2701
|
-
case 'dismiss':
|
|
2702
|
-
// Just dismiss this nudge
|
|
2703
|
-
this.smartSyncDismissNudge(nudgeKey);
|
|
2704
|
-
break;
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
return { success: true, action, nudgeKey };
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
/**
|
|
2711
|
-
* Get smart sync status and settings
|
|
2712
|
-
*/
|
|
2713
|
-
smartSyncStatus() {
|
|
2714
|
-
const prefs = this.loadSmartSyncPrefs();
|
|
2715
|
-
const activity = this.loadActivity();
|
|
2716
|
-
|
|
2717
|
-
// Get recent projects from activity
|
|
2718
|
-
const recentProjects = [];
|
|
2719
|
-
const recentSessions = activity.sessions.slice(-5);
|
|
2720
|
-
for (const session of recentSessions) {
|
|
2721
|
-
for (const proj of session.projects || []) {
|
|
2722
|
-
if (!recentProjects.includes(proj)) {
|
|
2723
|
-
recentProjects.push(proj);
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
return {
|
|
2729
|
-
enabled: prefs.enabled,
|
|
2730
|
-
autoSwitchThreshold: prefs.autoSwitchThreshold,
|
|
2731
|
-
savedChoicesCount: Object.keys(prefs.projectChoices).length,
|
|
2732
|
-
dismissedNudgesCount: prefs.dismissedNudges.length,
|
|
2733
|
-
recentProjects: recentProjects.slice(0, 10),
|
|
2734
|
-
lastNudgeTime: prefs.lastNudgeTime
|
|
2735
|
-
};
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
// =============================================================================
|
|
2740
|
-
// CLI
|
|
2741
|
-
// =============================================================================
|
|
236
|
+
// =============================================================================
|
|
237
|
+
// CLI
|
|
238
|
+
// =============================================================================
|
|
2742
239
|
|
|
2743
240
|
if (require.main === module) {
|
|
2744
|
-
const args = process.argv.slice(2);
|
|
2745
|
-
const command = args[0];
|
|
2746
241
|
const manager = new ClaudeConfigManager();
|
|
2747
|
-
|
|
2748
|
-
// Parse --template flag for init
|
|
2749
|
-
const templateIndex = args.indexOf('--template');
|
|
2750
|
-
const templateArg = templateIndex !== -1 ? args[templateIndex + 1] : null;
|
|
2751
|
-
|
|
2752
|
-
switch (command) {
|
|
2753
|
-
// Core
|
|
2754
|
-
case 'init':
|
|
2755
|
-
if (templateArg) {
|
|
2756
|
-
// Remove --template and its value from args for path detection
|
|
2757
|
-
const filteredArgs = args.filter((_, i) => i !== templateIndex && i !== templateIndex + 1);
|
|
2758
|
-
manager.init(filteredArgs[1], templateArg);
|
|
2759
|
-
} else {
|
|
2760
|
-
manager.init(args[1]);
|
|
2761
|
-
}
|
|
2762
|
-
break;
|
|
2763
|
-
case 'apply':
|
|
2764
|
-
manager.apply(args[1]);
|
|
2765
|
-
break;
|
|
2766
|
-
case 'apply-template':
|
|
2767
|
-
manager.applyTemplate(args[1], args[2]);
|
|
2768
|
-
break;
|
|
2769
|
-
case 'show':
|
|
2770
|
-
manager.show(args[1]);
|
|
2771
|
-
break;
|
|
2772
|
-
case 'list':
|
|
2773
|
-
case 'mcps':
|
|
2774
|
-
manager.list();
|
|
2775
|
-
break;
|
|
2776
|
-
case 'templates':
|
|
2777
|
-
manager.listTemplates();
|
|
2778
|
-
break;
|
|
2779
|
-
|
|
2780
|
-
// Edit MCPs
|
|
2781
|
-
case 'add':
|
|
2782
|
-
manager.add(args.slice(1));
|
|
2783
|
-
break;
|
|
2784
|
-
case 'remove':
|
|
2785
|
-
case 'rm':
|
|
2786
|
-
manager.remove(args.slice(1));
|
|
2787
|
-
break;
|
|
2788
|
-
|
|
2789
|
-
// Registry management
|
|
2790
|
-
case 'registry-add':
|
|
2791
|
-
manager.registryAdd(args[1], args[2]);
|
|
2792
|
-
break;
|
|
2793
|
-
case 'registry-remove':
|
|
2794
|
-
case 'registry-rm':
|
|
2795
|
-
manager.registryRemove(args[1]);
|
|
2796
|
-
break;
|
|
2797
|
-
|
|
2798
|
-
// Memory
|
|
2799
|
-
case 'memory':
|
|
2800
|
-
if (args[1] === 'init') {
|
|
2801
|
-
manager.memoryInit(args[2]);
|
|
2802
|
-
} else if (args[1] === 'add') {
|
|
2803
|
-
manager.memoryAdd(args[2], args.slice(3).join(' '));
|
|
2804
|
-
} else if (args[1] === 'search') {
|
|
2805
|
-
manager.memorySearch(args.slice(2).join(' '));
|
|
2806
|
-
} else {
|
|
2807
|
-
manager.memoryList();
|
|
2808
|
-
}
|
|
2809
|
-
break;
|
|
2810
|
-
|
|
2811
|
-
// Environment
|
|
2812
|
-
case 'env':
|
|
2813
|
-
if (args[1] === 'set') {
|
|
2814
|
-
manager.envSet(args[2], args[3]);
|
|
2815
|
-
} else if (args[1] === 'unset') {
|
|
2816
|
-
manager.envUnset(args[2]);
|
|
2817
|
-
} else {
|
|
2818
|
-
manager.envList();
|
|
2819
|
-
}
|
|
2820
|
-
break;
|
|
2821
|
-
|
|
2822
|
-
// Project registry (for UI)
|
|
2823
|
-
case 'project':
|
|
2824
|
-
case 'projects':
|
|
2825
|
-
if (args[1] === 'add') {
|
|
2826
|
-
const nameIdx = args.indexOf('--name');
|
|
2827
|
-
const name = nameIdx !== -1 ? args[nameIdx + 1] : null;
|
|
2828
|
-
const projectPath = args[2] && !args[2].startsWith('--') ? args[2] : process.cwd();
|
|
2829
|
-
manager.projectAdd(projectPath, name);
|
|
2830
|
-
} else if (args[1] === 'remove' || args[1] === 'rm') {
|
|
2831
|
-
manager.projectRemove(args[2]);
|
|
2832
|
-
} else {
|
|
2833
|
-
manager.projectList();
|
|
2834
|
-
}
|
|
2835
|
-
break;
|
|
2836
|
-
|
|
2837
|
-
// Workstreams
|
|
2838
|
-
case 'workstream':
|
|
2839
|
-
case 'ws':
|
|
2840
|
-
if (args[1] === 'create' || args[1] === 'new') {
|
|
2841
|
-
manager.workstreamCreate(args[2]);
|
|
2842
|
-
} else if (args[1] === 'delete' || args[1] === 'rm') {
|
|
2843
|
-
manager.workstreamDelete(args[2]);
|
|
2844
|
-
} else if (args[1] === 'use' || args[1] === 'switch') {
|
|
2845
|
-
manager.workstreamUse(args[2]);
|
|
2846
|
-
} else if (args[1] === 'add-project') {
|
|
2847
|
-
manager.workstreamAddProject(args[2], args[3]);
|
|
2848
|
-
} else if (args[1] === 'remove-project') {
|
|
2849
|
-
manager.workstreamRemoveProject(args[2], args[3]);
|
|
2850
|
-
} else if (args[1] === 'inject') {
|
|
2851
|
-
const silent = args.includes('--silent') || args.includes('-s');
|
|
2852
|
-
manager.workstreamInject(silent);
|
|
2853
|
-
} else if (args[1] === 'detect') {
|
|
2854
|
-
const ws = manager.workstreamDetect(args[2] || process.cwd());
|
|
2855
|
-
if (ws) {
|
|
2856
|
-
console.log(ws.name);
|
|
2857
|
-
}
|
|
2858
|
-
} else if (args[1] === 'active') {
|
|
2859
|
-
const ws = manager.workstreamActive();
|
|
2860
|
-
if (ws) {
|
|
2861
|
-
console.log(`Active: ${ws.name}`);
|
|
2862
|
-
if (ws.projects.length > 0) {
|
|
2863
|
-
console.log(`Projects: ${ws.projects.map(p => path.basename(p)).join(', ')}`);
|
|
2864
|
-
}
|
|
2865
|
-
} else {
|
|
2866
|
-
console.log('No active workstream');
|
|
2867
|
-
}
|
|
2868
|
-
} else {
|
|
2869
|
-
manager.workstreamList();
|
|
2870
|
-
}
|
|
2871
|
-
break;
|
|
2872
|
-
|
|
2873
|
-
// Maintenance
|
|
2874
|
-
case 'update':
|
|
2875
|
-
manager.update(args[1]);
|
|
2876
|
-
break;
|
|
2877
|
-
case 'ui': {
|
|
2878
|
-
const UIServer = require('./ui/server.cjs');
|
|
2879
|
-
const port = parseInt(args.find(a => a.startsWith('--port='))?.split('=')[1] || '3333');
|
|
2880
|
-
const uiDir = args.find(a => !a.startsWith('--') && a !== 'ui') || process.cwd();
|
|
2881
|
-
const uiServer = new UIServer(port, uiDir, manager);
|
|
2882
|
-
uiServer.start();
|
|
2883
|
-
break;
|
|
2884
|
-
}
|
|
2885
|
-
case 'version':
|
|
2886
|
-
case '-v':
|
|
2887
|
-
case '--version':
|
|
2888
|
-
manager.version();
|
|
2889
|
-
break;
|
|
2890
|
-
|
|
2891
|
-
default:
|
|
2892
|
-
console.log(`
|
|
2893
|
-
claude-config v${VERSION}
|
|
2894
|
-
|
|
2895
|
-
Usage:
|
|
2896
|
-
claude-config <command> [args]
|
|
2897
|
-
|
|
2898
|
-
Project Commands:
|
|
2899
|
-
init [--template <name>] Initialize project (optionally with template)
|
|
2900
|
-
apply Generate .mcp.json from config
|
|
2901
|
-
apply-template <name> Add template rules/commands to existing project
|
|
2902
|
-
show Show current project config
|
|
2903
|
-
list List available MCPs (✓ = active)
|
|
2904
|
-
templates List available templates
|
|
2905
|
-
add <mcp> [mcp...] Add MCP(s) to project
|
|
2906
|
-
remove <mcp> [mcp...] Remove MCP(s) from project
|
|
2907
|
-
|
|
2908
|
-
Memory Commands:
|
|
2909
|
-
memory Show memory status
|
|
2910
|
-
memory init Initialize project memory
|
|
2911
|
-
memory add <type> <content> Add entry (types: preference, correction, fact,
|
|
2912
|
-
context, pattern, decision, issue, history)
|
|
2913
|
-
memory search <query> Search all memory files
|
|
2914
|
-
|
|
2915
|
-
Environment Commands:
|
|
2916
|
-
env List environment variables
|
|
2917
|
-
env set <KEY> <value> Set variable in .claude/.env
|
|
2918
|
-
env unset <KEY> Remove variable
|
|
2919
|
-
|
|
2920
|
-
Project Commands (for UI):
|
|
2921
|
-
project List registered projects
|
|
2922
|
-
project add [path] Add project (defaults to cwd)
|
|
2923
|
-
project add [path] --name X Add with custom display name
|
|
2924
|
-
project remove <name|path> Remove project from registry
|
|
2925
|
-
|
|
2926
|
-
Workstream Commands:
|
|
2927
|
-
workstream List all workstreams
|
|
2928
|
-
workstream create "Name" Create new workstream
|
|
2929
|
-
workstream delete <name> Delete workstream
|
|
2930
|
-
workstream use <name> Set active workstream
|
|
2931
|
-
workstream active Show current active workstream
|
|
2932
|
-
workstream add-project <ws> <path> Add project to workstream
|
|
2933
|
-
workstream remove-project <ws> <path> Remove project from workstream
|
|
2934
|
-
workstream inject [--silent] Output active workstream rules (for hooks)
|
|
2935
|
-
workstream detect [path] Detect workstream for directory
|
|
2936
|
-
|
|
2937
|
-
Registry Commands:
|
|
2938
|
-
registry-add <name> '<json>' Add MCP to global registry
|
|
2939
|
-
registry-remove <name> Remove MCP from registry
|
|
2940
|
-
|
|
2941
|
-
Maintenance:
|
|
2942
|
-
ui [--port=3333] Open web UI
|
|
2943
|
-
version Show version info
|
|
2944
|
-
|
|
2945
|
-
Examples:
|
|
2946
|
-
claude-config init --template fastapi
|
|
2947
|
-
claude-config add postgres github
|
|
2948
|
-
claude-config memory add preference "Use TypeScript for new files"
|
|
2949
|
-
claude-config env set GITHUB_TOKEN ghp_xxx
|
|
2950
|
-
claude-config apply
|
|
2951
|
-
`);
|
|
2952
|
-
}
|
|
242
|
+
runCli(manager);
|
|
2953
243
|
}
|
|
2954
244
|
|
|
2955
245
|
module.exports = ClaudeConfigManager;
|