coder-config 0.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +553 -0
- package/cli.js +431 -0
- package/config-loader.js +294 -0
- package/hooks/activity-track.sh +56 -0
- package/hooks/codex-workstream.sh +44 -0
- package/hooks/gemini-workstream.sh +44 -0
- package/hooks/workstream-inject.sh +20 -0
- package/lib/activity.js +283 -0
- package/lib/apply.js +344 -0
- package/lib/cli.js +267 -0
- package/lib/config.js +171 -0
- package/lib/constants.js +55 -0
- package/lib/env.js +114 -0
- package/lib/index.js +47 -0
- package/lib/init.js +122 -0
- package/lib/mcps.js +139 -0
- package/lib/memory.js +201 -0
- package/lib/projects.js +138 -0
- package/lib/registry.js +83 -0
- package/lib/utils.js +129 -0
- package/lib/workstreams.js +652 -0
- package/package.json +80 -0
- package/scripts/capture-screenshots.js +142 -0
- package/scripts/postinstall.js +122 -0
- package/scripts/release.sh +71 -0
- package/scripts/sync-version.js +77 -0
- package/scripts/tauri-prepare.js +328 -0
- package/shared/mcp-registry.json +76 -0
- package/ui/dist/assets/index-DbZ3_HBD.js +3204 -0
- package/ui/dist/assets/index-DjLdm3Mr.css +32 -0
- package/ui/dist/icons/icon-192.svg +16 -0
- package/ui/dist/icons/icon-512.svg +16 -0
- package/ui/dist/index.html +39 -0
- package/ui/dist/manifest.json +25 -0
- package/ui/dist/sw.js +24 -0
- package/ui/dist/tutorial/claude-settings.png +0 -0
- package/ui/dist/tutorial/header.png +0 -0
- package/ui/dist/tutorial/mcp-registry.png +0 -0
- package/ui/dist/tutorial/memory-view.png +0 -0
- package/ui/dist/tutorial/permissions.png +0 -0
- package/ui/dist/tutorial/plugins-view.png +0 -0
- package/ui/dist/tutorial/project-explorer.png +0 -0
- package/ui/dist/tutorial/projects-view.png +0 -0
- package/ui/dist/tutorial/sidebar.png +0 -0
- package/ui/dist/tutorial/tutorial-view.png +0 -0
- package/ui/dist/tutorial/workstreams-view.png +0 -0
- package/ui/routes/activity.js +58 -0
- package/ui/routes/commands.js +74 -0
- package/ui/routes/configs.js +329 -0
- package/ui/routes/env.js +40 -0
- package/ui/routes/file-explorer.js +668 -0
- package/ui/routes/index.js +41 -0
- package/ui/routes/mcp-discovery.js +235 -0
- package/ui/routes/memory.js +385 -0
- package/ui/routes/package.json +3 -0
- package/ui/routes/plugins.js +466 -0
- package/ui/routes/projects.js +198 -0
- package/ui/routes/registry.js +30 -0
- package/ui/routes/rules.js +74 -0
- package/ui/routes/search.js +125 -0
- package/ui/routes/settings.js +381 -0
- package/ui/routes/subprojects.js +208 -0
- package/ui/routes/tool-sync.js +127 -0
- package/ui/routes/updates.js +339 -0
- package/ui/routes/workstreams.js +224 -0
- package/ui/server.cjs +773 -0
- package/ui/terminal-server.cjs +160 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Explorer Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scan a directory for .claude, .agent, .gemini folders
|
|
11
|
+
* Returns folder object for FileExplorer
|
|
12
|
+
*/
|
|
13
|
+
function scanFolderForExplorer(dir, manager, label = null) {
|
|
14
|
+
const home = os.homedir();
|
|
15
|
+
const claudeDir = path.join(dir, '.claude');
|
|
16
|
+
const agentDir = path.join(dir, '.agent');
|
|
17
|
+
const geminiDir = path.join(dir, '.gemini');
|
|
18
|
+
|
|
19
|
+
// Use label or generate from path
|
|
20
|
+
if (!label) {
|
|
21
|
+
if (dir === home) {
|
|
22
|
+
label = '~';
|
|
23
|
+
} else if (dir.startsWith(home + '/')) {
|
|
24
|
+
label = '~' + dir.slice(home.length);
|
|
25
|
+
} else {
|
|
26
|
+
label = dir;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const folder = {
|
|
31
|
+
dir: dir,
|
|
32
|
+
label,
|
|
33
|
+
claudePath: claudeDir,
|
|
34
|
+
agentPath: agentDir,
|
|
35
|
+
geminiPath: geminiDir,
|
|
36
|
+
exists: fs.existsSync(claudeDir),
|
|
37
|
+
agentExists: fs.existsSync(agentDir),
|
|
38
|
+
geminiExists: fs.existsSync(geminiDir),
|
|
39
|
+
files: [],
|
|
40
|
+
agentFiles: [],
|
|
41
|
+
geminiFiles: [],
|
|
42
|
+
appliedTemplate: null
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// If none of the config folders exist, don't include
|
|
46
|
+
if (!folder.exists && !folder.agentExists && !folder.geminiExists) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Scan .claude folder
|
|
51
|
+
if (folder.exists) {
|
|
52
|
+
scanClaudeFolder(folder, claudeDir, manager);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Scan .agent folder
|
|
56
|
+
if (folder.agentExists) {
|
|
57
|
+
scanAgentFolder(folder, agentDir);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Scan .gemini folder
|
|
61
|
+
if (folder.geminiExists) {
|
|
62
|
+
scanGeminiFolder(folder, geminiDir, manager);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Root CLAUDE.md
|
|
66
|
+
const rootClaudeMd = path.join(dir, 'CLAUDE.md');
|
|
67
|
+
if (fs.existsSync(rootClaudeMd)) {
|
|
68
|
+
folder.files.push({
|
|
69
|
+
name: 'CLAUDE.md (root)',
|
|
70
|
+
path: rootClaudeMd,
|
|
71
|
+
type: 'claudemd',
|
|
72
|
+
size: fs.statSync(rootClaudeMd).size,
|
|
73
|
+
isRoot: true
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Root GEMINI.md
|
|
78
|
+
const rootGeminiMd = path.join(dir, 'GEMINI.md');
|
|
79
|
+
if (fs.existsSync(rootGeminiMd)) {
|
|
80
|
+
folder.agentFiles.push({
|
|
81
|
+
name: 'GEMINI.md (root)',
|
|
82
|
+
path: rootGeminiMd,
|
|
83
|
+
type: 'geminimd',
|
|
84
|
+
size: fs.statSync(rootGeminiMd).size,
|
|
85
|
+
isRoot: true
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return folder;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Scan .claude folder contents
|
|
94
|
+
*/
|
|
95
|
+
function scanClaudeFolder(folder, claudeDir, manager) {
|
|
96
|
+
// mcps.json
|
|
97
|
+
const mcpsPath = path.join(claudeDir, 'mcps.json');
|
|
98
|
+
if (fs.existsSync(mcpsPath)) {
|
|
99
|
+
const content = manager.loadJson(mcpsPath) || {};
|
|
100
|
+
folder.files.push({
|
|
101
|
+
name: 'mcps.json',
|
|
102
|
+
path: mcpsPath,
|
|
103
|
+
type: 'mcps',
|
|
104
|
+
size: fs.statSync(mcpsPath).size,
|
|
105
|
+
mcpCount: (content.include?.length || 0) + Object.keys(content.mcpServers || {}).length
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// settings.json
|
|
110
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
111
|
+
if (fs.existsSync(settingsPath)) {
|
|
112
|
+
folder.files.push({
|
|
113
|
+
name: 'settings.json',
|
|
114
|
+
path: settingsPath,
|
|
115
|
+
type: 'settings',
|
|
116
|
+
size: fs.statSync(settingsPath).size
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// commands folder
|
|
121
|
+
addSubfolder(folder.files, claudeDir, 'commands', '.md', 'command');
|
|
122
|
+
|
|
123
|
+
// rules folder
|
|
124
|
+
addSubfolder(folder.files, claudeDir, 'rules', '.md', 'rule');
|
|
125
|
+
|
|
126
|
+
// workflows folder
|
|
127
|
+
addSubfolder(folder.files, claudeDir, 'workflows', '.md', 'workflow');
|
|
128
|
+
|
|
129
|
+
// memory folder
|
|
130
|
+
addSubfolder(folder.files, claudeDir, 'memory', '.md', 'memory');
|
|
131
|
+
|
|
132
|
+
// CLAUDE.md inside .claude
|
|
133
|
+
const claudeMdPath = path.join(claudeDir, 'CLAUDE.md');
|
|
134
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
135
|
+
folder.files.push({
|
|
136
|
+
name: 'CLAUDE.md',
|
|
137
|
+
path: claudeMdPath,
|
|
138
|
+
type: 'claudemd',
|
|
139
|
+
size: fs.statSync(claudeMdPath).size
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Scan .agent folder contents
|
|
146
|
+
*/
|
|
147
|
+
function scanAgentFolder(folder, agentDir) {
|
|
148
|
+
// rules folder
|
|
149
|
+
addSubfolder(folder.agentFiles, agentDir, 'rules', '.md', 'rule');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Scan .gemini folder contents
|
|
154
|
+
*/
|
|
155
|
+
function scanGeminiFolder(folder, geminiDir, manager) {
|
|
156
|
+
// settings.json
|
|
157
|
+
const geminiSettingsPath = path.join(geminiDir, 'settings.json');
|
|
158
|
+
if (fs.existsSync(geminiSettingsPath)) {
|
|
159
|
+
const content = manager.loadJson(geminiSettingsPath) || {};
|
|
160
|
+
folder.geminiFiles.push({
|
|
161
|
+
name: 'settings.json',
|
|
162
|
+
path: geminiSettingsPath,
|
|
163
|
+
type: 'settings',
|
|
164
|
+
size: fs.statSync(geminiSettingsPath).size,
|
|
165
|
+
mcpCount: Object.keys(content.mcpServers || {}).length
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// GEMINI.md
|
|
170
|
+
const geminiMdPath = path.join(geminiDir, 'GEMINI.md');
|
|
171
|
+
if (fs.existsSync(geminiMdPath)) {
|
|
172
|
+
folder.geminiFiles.push({
|
|
173
|
+
name: 'GEMINI.md',
|
|
174
|
+
path: geminiMdPath,
|
|
175
|
+
type: 'geminimd',
|
|
176
|
+
size: fs.statSync(geminiMdPath).size
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// commands folder (Gemini uses TOML)
|
|
181
|
+
const commandsDir = path.join(geminiDir, 'commands');
|
|
182
|
+
if (fs.existsSync(commandsDir)) {
|
|
183
|
+
const commands = fs.readdirSync(commandsDir)
|
|
184
|
+
.filter(f => f.endsWith('.toml') || f.endsWith('.md'))
|
|
185
|
+
.map(f => ({
|
|
186
|
+
name: f,
|
|
187
|
+
path: path.join(commandsDir, f),
|
|
188
|
+
type: 'command',
|
|
189
|
+
size: fs.statSync(path.join(commandsDir, f)).size
|
|
190
|
+
}));
|
|
191
|
+
if (commands.length > 0) {
|
|
192
|
+
folder.geminiFiles.push({
|
|
193
|
+
name: 'commands',
|
|
194
|
+
path: commandsDir,
|
|
195
|
+
type: 'folder',
|
|
196
|
+
children: commands
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Helper to add subfolder contents
|
|
204
|
+
*/
|
|
205
|
+
function addSubfolder(filesArray, parentDir, folderName, extension, fileType) {
|
|
206
|
+
const dir = path.join(parentDir, folderName);
|
|
207
|
+
if (fs.existsSync(dir)) {
|
|
208
|
+
const files = fs.readdirSync(dir)
|
|
209
|
+
.filter(f => f.endsWith(extension))
|
|
210
|
+
.map(f => ({
|
|
211
|
+
name: f,
|
|
212
|
+
path: path.join(dir, f),
|
|
213
|
+
type: fileType,
|
|
214
|
+
size: fs.statSync(path.join(dir, f)).size
|
|
215
|
+
}));
|
|
216
|
+
if (files.length > 0) {
|
|
217
|
+
filesArray.push({
|
|
218
|
+
name: folderName,
|
|
219
|
+
path: dir,
|
|
220
|
+
type: 'folder',
|
|
221
|
+
children: files
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get all intermediate paths between home and project
|
|
229
|
+
*/
|
|
230
|
+
function getIntermediatePaths(projectDir) {
|
|
231
|
+
const home = os.homedir();
|
|
232
|
+
const paths = [];
|
|
233
|
+
let current = projectDir;
|
|
234
|
+
|
|
235
|
+
while (current && current !== path.dirname(current)) {
|
|
236
|
+
const claudeDir = path.join(current, '.claude');
|
|
237
|
+
let label = current;
|
|
238
|
+
if (current === home) {
|
|
239
|
+
label = '~';
|
|
240
|
+
} else if (current.startsWith(home + '/')) {
|
|
241
|
+
label = '~' + current.slice(home.length);
|
|
242
|
+
}
|
|
243
|
+
paths.unshift({
|
|
244
|
+
dir: current,
|
|
245
|
+
label,
|
|
246
|
+
hasClaudeFolder: fs.existsSync(claudeDir),
|
|
247
|
+
isHome: current === home,
|
|
248
|
+
isProject: current === projectDir
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (current === home) break;
|
|
252
|
+
current = path.dirname(current);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return paths;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get contents of a specific .claude file
|
|
260
|
+
*/
|
|
261
|
+
function getClaudeFile(filePath) {
|
|
262
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
263
|
+
return { error: 'File not found', path: filePath };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const stat = fs.statSync(filePath);
|
|
267
|
+
if (stat.isDirectory()) {
|
|
268
|
+
return { error: 'Path is a directory', path: filePath };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
272
|
+
const ext = path.extname(filePath);
|
|
273
|
+
|
|
274
|
+
if (ext === '.json') {
|
|
275
|
+
try {
|
|
276
|
+
return { path: filePath, content, parsed: JSON.parse(content) };
|
|
277
|
+
} catch (e) {
|
|
278
|
+
return { path: filePath, content, parseError: e.message };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { path: filePath, content };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Save content to a .claude file
|
|
287
|
+
*/
|
|
288
|
+
function saveClaudeFile(body) {
|
|
289
|
+
const { path: filePath, content } = body;
|
|
290
|
+
if (!filePath) {
|
|
291
|
+
return { error: 'Path is required' };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const dir = path.dirname(filePath);
|
|
295
|
+
if (!fs.existsSync(dir)) {
|
|
296
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
300
|
+
return { success: true, path: filePath };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Delete a .claude file or folder
|
|
305
|
+
*/
|
|
306
|
+
function deleteClaudeFile(filePath) {
|
|
307
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
308
|
+
return { error: 'File not found', path: filePath };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const stat = fs.statSync(filePath);
|
|
312
|
+
if (stat.isDirectory()) {
|
|
313
|
+
fs.rmSync(filePath, { recursive: true });
|
|
314
|
+
} else {
|
|
315
|
+
fs.unlinkSync(filePath);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return { success: true, path: filePath };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Create a new .claude file
|
|
323
|
+
*/
|
|
324
|
+
function createClaudeFile(body) {
|
|
325
|
+
const { dir, name, type, content = '' } = body;
|
|
326
|
+
if (!dir || !name) {
|
|
327
|
+
return { error: 'Dir and name are required' };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let filePath;
|
|
331
|
+
let initialContent = content;
|
|
332
|
+
|
|
333
|
+
switch (type) {
|
|
334
|
+
case 'mcps':
|
|
335
|
+
filePath = path.join(dir, '.claude', 'mcps.json');
|
|
336
|
+
initialContent = content || JSON.stringify({ include: [], mcpServers: {} }, null, 2);
|
|
337
|
+
break;
|
|
338
|
+
case 'settings':
|
|
339
|
+
filePath = path.join(dir, '.claude', 'settings.json');
|
|
340
|
+
initialContent = content || JSON.stringify({}, null, 2);
|
|
341
|
+
break;
|
|
342
|
+
case 'command':
|
|
343
|
+
filePath = path.join(dir, '.claude', 'commands', name);
|
|
344
|
+
break;
|
|
345
|
+
case 'rule':
|
|
346
|
+
filePath = path.join(dir, '.claude', 'rules', name);
|
|
347
|
+
break;
|
|
348
|
+
case 'workflow':
|
|
349
|
+
filePath = path.join(dir, '.claude', 'workflows', name);
|
|
350
|
+
break;
|
|
351
|
+
case 'memory':
|
|
352
|
+
filePath = path.join(dir, '.claude', 'memory', name);
|
|
353
|
+
break;
|
|
354
|
+
case 'claudemd':
|
|
355
|
+
filePath = path.join(dir, '.claude', 'CLAUDE.md');
|
|
356
|
+
break;
|
|
357
|
+
default:
|
|
358
|
+
filePath = path.join(dir, '.claude', name);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const parentDir = path.dirname(filePath);
|
|
362
|
+
if (!fs.existsSync(parentDir)) {
|
|
363
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (fs.existsSync(filePath)) {
|
|
367
|
+
return { error: 'File already exists', path: filePath };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
fs.writeFileSync(filePath, initialContent, 'utf8');
|
|
371
|
+
return { success: true, path: filePath, content: initialContent };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Rename a .claude file
|
|
376
|
+
*/
|
|
377
|
+
function renameClaudeFile(body) {
|
|
378
|
+
const { oldPath, newName } = body;
|
|
379
|
+
if (!oldPath || !newName) {
|
|
380
|
+
return { error: 'oldPath and newName are required' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!fs.existsSync(oldPath)) {
|
|
384
|
+
return { error: 'File not found', path: oldPath };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const dir = path.dirname(oldPath);
|
|
388
|
+
const newPath = path.join(dir, newName.endsWith('.md') ? newName : `${newName}.md`);
|
|
389
|
+
|
|
390
|
+
if (fs.existsSync(newPath)) {
|
|
391
|
+
return { error: 'A file with that name already exists', path: newPath };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
fs.renameSync(oldPath, newPath);
|
|
395
|
+
return { success: true, oldPath, newPath };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Initialize a .claude folder in a directory
|
|
400
|
+
*/
|
|
401
|
+
function initClaudeFolder(dir) {
|
|
402
|
+
if (!dir) {
|
|
403
|
+
return { error: 'dir is required' };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const absDir = path.resolve(dir.replace(/^~/, os.homedir()));
|
|
407
|
+
if (!fs.existsSync(absDir)) {
|
|
408
|
+
return { error: 'Directory not found', dir: absDir };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const claudeDir = path.join(absDir, '.claude');
|
|
412
|
+
if (fs.existsSync(claudeDir)) {
|
|
413
|
+
return { error: '.claude folder already exists', dir: claudeDir };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
417
|
+
fs.writeFileSync(
|
|
418
|
+
path.join(claudeDir, 'mcps.json'),
|
|
419
|
+
JSON.stringify({ mcpServers: {} }, null, 2)
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
return { success: true, dir: claudeDir };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Delete a .claude folder
|
|
427
|
+
*/
|
|
428
|
+
function deleteClaudeFolder(dir) {
|
|
429
|
+
if (!dir) {
|
|
430
|
+
return { error: 'dir is required' };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const absDir = path.resolve(dir.replace(/^~/, os.homedir()));
|
|
434
|
+
const claudeDir = path.join(absDir, '.claude');
|
|
435
|
+
|
|
436
|
+
if (!fs.existsSync(claudeDir)) {
|
|
437
|
+
return { error: '.claude folder not found', dir: claudeDir };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
fs.rmSync(claudeDir, { recursive: true, force: true });
|
|
441
|
+
return { success: true, dir: claudeDir };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Initialize .claude folders in batch
|
|
446
|
+
*/
|
|
447
|
+
function initClaudeFolderBatch(dirs) {
|
|
448
|
+
if (!dirs || !Array.isArray(dirs) || dirs.length === 0) {
|
|
449
|
+
return { error: 'dirs array is required' };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const results = [];
|
|
453
|
+
let successCount = 0;
|
|
454
|
+
|
|
455
|
+
for (const dir of dirs) {
|
|
456
|
+
const result = initClaudeFolder(dir);
|
|
457
|
+
results.push({ dir, ...result });
|
|
458
|
+
if (result.success) {
|
|
459
|
+
successCount++;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
success: true,
|
|
465
|
+
count: successCount,
|
|
466
|
+
results
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Move or copy a .claude file/folder
|
|
472
|
+
*/
|
|
473
|
+
function moveClaudeItem(body, manager) {
|
|
474
|
+
const { sourcePath, targetDir, mode = 'copy', merge = false } = body;
|
|
475
|
+
if (!sourcePath || !targetDir) {
|
|
476
|
+
return { error: 'sourcePath and targetDir are required' };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!fs.existsSync(sourcePath)) {
|
|
480
|
+
return { error: 'Source not found', path: sourcePath };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const sourceName = path.basename(sourcePath);
|
|
484
|
+
const targetClaudeDir = path.join(targetDir, '.claude');
|
|
485
|
+
let targetPath;
|
|
486
|
+
|
|
487
|
+
// Determine target path based on source type
|
|
488
|
+
if (sourceName === 'mcps.json' || sourceName === 'settings.json' || sourceName === 'CLAUDE.md') {
|
|
489
|
+
targetPath = path.join(targetClaudeDir, sourceName);
|
|
490
|
+
} else if (sourcePath.includes('/commands/')) {
|
|
491
|
+
targetPath = path.join(targetClaudeDir, 'commands', sourceName);
|
|
492
|
+
} else if (sourcePath.includes('/rules/')) {
|
|
493
|
+
targetPath = path.join(targetClaudeDir, 'rules', sourceName);
|
|
494
|
+
} else if (sourcePath.includes('/workflows/')) {
|
|
495
|
+
targetPath = path.join(targetClaudeDir, 'workflows', sourceName);
|
|
496
|
+
} else {
|
|
497
|
+
targetPath = path.join(targetClaudeDir, sourceName);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Ensure target directory exists
|
|
501
|
+
const targetParent = path.dirname(targetPath);
|
|
502
|
+
if (!fs.existsSync(targetParent)) {
|
|
503
|
+
fs.mkdirSync(targetParent, { recursive: true });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Handle existing target
|
|
507
|
+
if (fs.existsSync(targetPath)) {
|
|
508
|
+
if (!merge) {
|
|
509
|
+
return { error: 'Target already exists', targetPath, needsMerge: true };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (targetPath.endsWith('.json')) {
|
|
513
|
+
const sourceContent = manager.loadJson(sourcePath) || {};
|
|
514
|
+
const targetContent = manager.loadJson(targetPath) || {};
|
|
515
|
+
|
|
516
|
+
if (sourceName === 'mcps.json') {
|
|
517
|
+
const merged = {
|
|
518
|
+
...targetContent,
|
|
519
|
+
...sourceContent,
|
|
520
|
+
include: [...new Set([...(targetContent.include || []), ...(sourceContent.include || [])])],
|
|
521
|
+
mcpServers: { ...(targetContent.mcpServers || {}), ...(sourceContent.mcpServers || {}) }
|
|
522
|
+
};
|
|
523
|
+
fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2));
|
|
524
|
+
} else {
|
|
525
|
+
const merged = { ...targetContent, ...sourceContent };
|
|
526
|
+
fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2));
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (mode === 'move') {
|
|
536
|
+
fs.unlinkSync(sourcePath);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return { success: true, sourcePath, targetPath, mode };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Scan directory for MCP tool projects
|
|
544
|
+
*/
|
|
545
|
+
async function scanMcpTools(toolsDir) {
|
|
546
|
+
const tools = [];
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
if (!fs.existsSync(toolsDir)) {
|
|
550
|
+
return tools;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const entries = fs.readdirSync(toolsDir, { withFileTypes: true });
|
|
554
|
+
|
|
555
|
+
for (const entry of entries) {
|
|
556
|
+
if (!entry.isDirectory()) continue;
|
|
557
|
+
|
|
558
|
+
const projectPath = path.join(toolsDir, entry.name);
|
|
559
|
+
const tool = { name: entry.name, path: projectPath, type: null };
|
|
560
|
+
|
|
561
|
+
// Check for Python MCP
|
|
562
|
+
const pyprojectPath = path.join(projectPath, 'pyproject.toml');
|
|
563
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
564
|
+
tool.type = 'python';
|
|
565
|
+
try {
|
|
566
|
+
const content = fs.readFileSync(pyprojectPath, 'utf8');
|
|
567
|
+
const descMatch = content.match(/description\s*=\s*"([^"]+)"/);
|
|
568
|
+
if (descMatch) tool.description = descMatch[1];
|
|
569
|
+
if (content.includes('mcp') || content.includes('fastmcp')) {
|
|
570
|
+
tool.framework = 'fastmcp';
|
|
571
|
+
}
|
|
572
|
+
} catch (e) {}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Check for Node MCP
|
|
576
|
+
const packagePath = path.join(projectPath, 'package.json');
|
|
577
|
+
if (fs.existsSync(packagePath)) {
|
|
578
|
+
tool.type = tool.type || 'node';
|
|
579
|
+
try {
|
|
580
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
581
|
+
tool.description = tool.description || pkg.description;
|
|
582
|
+
if (pkg.dependencies?.['@modelcontextprotocol/sdk'] || pkg.name?.includes('mcp')) {
|
|
583
|
+
tool.framework = 'mcp-sdk';
|
|
584
|
+
}
|
|
585
|
+
} catch (e) {}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Check for mcp_server.py
|
|
589
|
+
const mcpServerPath = path.join(projectPath, 'mcp_server.py');
|
|
590
|
+
if (fs.existsSync(mcpServerPath)) {
|
|
591
|
+
tool.type = 'python';
|
|
592
|
+
tool.framework = tool.framework || 'fastmcp';
|
|
593
|
+
tool.entryPoint = 'mcp_server.py';
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (tool.type) {
|
|
597
|
+
tools.push(tool);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch (e) {
|
|
601
|
+
console.error('Error scanning MCP tools:', e.message);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return tools;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Get file hashes for change detection
|
|
609
|
+
*/
|
|
610
|
+
function getFileHashes(manager, projectDir) {
|
|
611
|
+
const hashes = {};
|
|
612
|
+
const crypto = require('crypto');
|
|
613
|
+
|
|
614
|
+
const hashFile = (filePath) => {
|
|
615
|
+
try {
|
|
616
|
+
if (!fs.existsSync(filePath)) return null;
|
|
617
|
+
const content = fs.readFileSync(filePath);
|
|
618
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
619
|
+
} catch (e) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// Hash all config files in hierarchy
|
|
625
|
+
const configs = manager.findAllConfigs(projectDir);
|
|
626
|
+
for (const { configPath } of configs) {
|
|
627
|
+
const hash = hashFile(configPath);
|
|
628
|
+
if (hash) hashes[configPath] = hash;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Hash registry
|
|
632
|
+
const registryHash = hashFile(manager.registryPath);
|
|
633
|
+
if (registryHash) hashes[manager.registryPath] = registryHash;
|
|
634
|
+
|
|
635
|
+
// Hash all rules and commands
|
|
636
|
+
const rules = manager.collectFilesFromHierarchy(configs, 'rules');
|
|
637
|
+
const commands = manager.collectFilesFromHierarchy(configs, 'commands');
|
|
638
|
+
|
|
639
|
+
for (const { fullPath } of [...rules, ...commands]) {
|
|
640
|
+
const hash = hashFile(fullPath);
|
|
641
|
+
if (hash) hashes[fullPath] = hash;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Hash env files
|
|
645
|
+
for (const { dir } of configs) {
|
|
646
|
+
const envPath = path.join(dir, '.claude', '.env');
|
|
647
|
+
const hash = hashFile(envPath);
|
|
648
|
+
if (hash) hashes[envPath] = hash;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return { hashes };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
module.exports = {
|
|
655
|
+
scanFolderForExplorer,
|
|
656
|
+
getIntermediatePaths,
|
|
657
|
+
getClaudeFile,
|
|
658
|
+
saveClaudeFile,
|
|
659
|
+
deleteClaudeFile,
|
|
660
|
+
createClaudeFile,
|
|
661
|
+
renameClaudeFile,
|
|
662
|
+
initClaudeFolder,
|
|
663
|
+
deleteClaudeFolder,
|
|
664
|
+
initClaudeFolderBatch,
|
|
665
|
+
moveClaudeItem,
|
|
666
|
+
scanMcpTools,
|
|
667
|
+
getFileHashes,
|
|
668
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes Index - Aggregates all route modules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const projects = require('./projects');
|
|
6
|
+
const workstreams = require('./workstreams');
|
|
7
|
+
const activity = require('./activity');
|
|
8
|
+
const subprojects = require('./subprojects');
|
|
9
|
+
const registry = require('./registry');
|
|
10
|
+
const rules = require('./rules');
|
|
11
|
+
const commands = require('./commands');
|
|
12
|
+
const plugins = require('./plugins');
|
|
13
|
+
const updates = require('./updates');
|
|
14
|
+
const search = require('./search');
|
|
15
|
+
const toolSync = require('./tool-sync');
|
|
16
|
+
const fileExplorer = require('./file-explorer');
|
|
17
|
+
const memory = require('./memory');
|
|
18
|
+
const settings = require('./settings');
|
|
19
|
+
const env = require('./env');
|
|
20
|
+
const configs = require('./configs');
|
|
21
|
+
const mcpDiscovery = require('./mcp-discovery');
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
projects,
|
|
25
|
+
workstreams,
|
|
26
|
+
activity,
|
|
27
|
+
subprojects,
|
|
28
|
+
registry,
|
|
29
|
+
rules,
|
|
30
|
+
commands,
|
|
31
|
+
plugins,
|
|
32
|
+
updates,
|
|
33
|
+
search,
|
|
34
|
+
toolSync,
|
|
35
|
+
fileExplorer,
|
|
36
|
+
memory,
|
|
37
|
+
settings,
|
|
38
|
+
env,
|
|
39
|
+
configs,
|
|
40
|
+
mcpDiscovery,
|
|
41
|
+
};
|