claude-code-workflow 6.0.4 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/action-planning-agent.md +1 -1
- package/.claude/agents/cli-execution-agent.md +269 -269
- package/.claude/agents/cli-explore-agent.md +182 -182
- package/.claude/agents/context-search-agent.md +582 -582
- package/.claude/agents/memory-bridge.md +93 -93
- package/.claude/commands/cli/cli-init.md +1 -1
- package/.claude/commands/memory/docs-full-cli.md +471 -471
- package/.claude/commands/memory/docs-related-cli.md +386 -386
- package/.claude/commands/memory/docs.md +615 -615
- package/.claude/commands/memory/load.md +1 -1
- package/.claude/commands/memory/update-full.md +332 -332
- package/.claude/commands/memory/update-related.md +5 -5
- package/.claude/commands/workflow/init.md +1 -1
- package/.claude/commands/workflow/lite-fix.md +621 -621
- package/.claude/commands/workflow/lite-plan.md +592 -592
- package/.claude/commands/workflow/tools/context-gather.md +434 -434
- package/.claude/commands/workflow/ui-design/generate.md +504 -504
- package/.claude/commands/workflow/ui-design/import-from-code.md +537 -537
- package/.claude/scripts/classify-folders.sh +4 -0
- package/.claude/scripts/convert_tokens_to_css.sh +4 -0
- package/.claude/scripts/detect_changed_modules.sh +5 -1
- package/.claude/scripts/discover-design-files.sh +87 -83
- package/.claude/scripts/generate_module_docs.sh +717 -713
- package/.claude/scripts/get_modules_by_depth.sh +5 -1
- package/.claude/scripts/ui-generate-preview.sh +4 -0
- package/.claude/scripts/ui-instantiate-prototypes.sh +4 -0
- package/.claude/scripts/update_module_claude.sh +4 -0
- package/.claude/skills/command-guide/index/all-commands.json +1 -12
- package/.claude/skills/command-guide/index/by-category.json +1 -12
- package/.claude/skills/command-guide/index/by-use-case.json +1 -12
- package/.claude/skills/command-guide/index/essential-commands.json +1 -12
- package/.claude/skills/command-guide/reference/agents/action-planning-agent.md +127 -71
- package/.claude/skills/command-guide/reference/agents/cli-execution-agent.md +269 -269
- package/.claude/skills/command-guide/reference/agents/cli-explore-agent.md +182 -182
- package/.claude/skills/command-guide/reference/agents/conceptual-planning-agent.md +18 -38
- package/.claude/skills/command-guide/reference/agents/context-search-agent.md +582 -577
- package/.claude/skills/command-guide/reference/agents/memory-bridge.md +93 -93
- package/.claude/skills/command-guide/reference/commands/cli/cli-init.md +1 -1
- package/.claude/skills/command-guide/reference/commands/memory/docs-full-cli.md +471 -471
- package/.claude/skills/command-guide/reference/commands/memory/docs-related-cli.md +386 -386
- package/.claude/skills/command-guide/reference/commands/memory/docs.md +615 -610
- package/.claude/skills/command-guide/reference/commands/memory/load.md +1 -1
- package/.claude/skills/command-guide/reference/commands/memory/update-full.md +332 -332
- package/.claude/skills/command-guide/reference/commands/memory/update-related.md +5 -5
- package/.claude/skills/command-guide/reference/commands/workflow/brainstorm/artifacts.md +299 -451
- package/.claude/skills/command-guide/reference/commands/workflow/brainstorm/auto-parallel.md +14 -37
- package/.claude/skills/command-guide/reference/commands/workflow/brainstorm/synthesis.md +252 -350
- package/.claude/skills/command-guide/reference/commands/workflow/init.md +2 -2
- package/.claude/skills/command-guide/reference/commands/workflow/lite-execute.md +52 -0
- package/.claude/skills/command-guide/reference/commands/workflow/lite-fix.md +621 -602
- package/.claude/skills/command-guide/reference/commands/workflow/lite-plan.md +46 -36
- package/.claude/skills/command-guide/reference/commands/workflow/review-fix.md +18 -58
- package/.claude/skills/command-guide/reference/commands/workflow/review-module-cycle.md +22 -52
- package/.claude/skills/command-guide/reference/commands/workflow/review-session-cycle.md +19 -48
- package/.claude/skills/command-guide/reference/commands/workflow/session/start.md +25 -5
- package/.claude/skills/command-guide/reference/commands/workflow/tdd-plan.md +1 -1
- package/.claude/skills/command-guide/reference/commands/workflow/test-fix-gen.md +7 -7
- package/.claude/skills/command-guide/reference/commands/workflow/tools/context-gather.md +434 -434
- package/.claude/skills/command-guide/reference/commands/workflow/tools/task-generate-agent.md +151 -11
- package/.claude/skills/command-guide/reference/commands/workflow/tools/task-generate-tdd.md +4 -4
- package/.claude/skills/command-guide/reference/commands/workflow/tools/test-task-generate.md +1 -1
- package/.claude/skills/command-guide/reference/commands/workflow/ui-design/generate.md +504 -504
- package/.claude/skills/command-guide/reference/commands/workflow/ui-design/import-from-code.md +537 -537
- package/.claude/workflows/context-search-strategy.md +77 -77
- package/.claude/workflows/tool-strategy.md +90 -71
- package/.claude/workflows/workflow-architecture.md +1 -1
- package/ccw/package.json +6 -6
- package/ccw/src/cli.js +16 -0
- package/ccw/src/commands/stop.js +101 -0
- package/ccw/src/commands/tool.js +181 -0
- package/ccw/src/core/dashboard-generator.js +18 -3
- package/ccw/src/core/lite-scanner.js +35 -11
- package/ccw/src/core/server.js +583 -17
- package/ccw/src/templates/dashboard-css/01-base.css +161 -0
- package/ccw/src/templates/dashboard-css/02-session.css +726 -0
- package/ccw/src/templates/dashboard-css/03-tasks.css +512 -0
- package/ccw/src/templates/dashboard-css/04-lite-tasks.css +843 -0
- package/ccw/src/templates/dashboard-css/05-context.css +2206 -0
- package/ccw/src/templates/dashboard-css/06-cards.css +1570 -0
- package/ccw/src/templates/dashboard-css/07-managers.css +936 -0
- package/ccw/src/templates/dashboard-css/08-review.css +1266 -0
- package/ccw/src/templates/dashboard-css/09-explorer.css +1397 -0
- package/ccw/src/templates/dashboard-js/components/global-notifications.js +219 -0
- package/ccw/src/templates/dashboard-js/components/hook-manager.js +10 -0
- package/ccw/src/templates/dashboard-js/components/mcp-manager.js +24 -2
- package/ccw/src/templates/dashboard-js/components/navigation.js +11 -5
- package/ccw/src/templates/dashboard-js/components/tabs-context.js +20 -20
- package/ccw/src/templates/dashboard-js/components/tabs-other.js +11 -11
- package/ccw/src/templates/dashboard-js/components/theme.js +29 -1
- package/ccw/src/templates/dashboard-js/main.js +4 -0
- package/ccw/src/templates/dashboard-js/state.js +5 -0
- package/ccw/src/templates/dashboard-js/views/explorer.js +852 -0
- package/ccw/src/templates/dashboard-js/views/home.js +13 -9
- package/ccw/src/templates/dashboard-js/views/hook-manager.js +8 -5
- package/ccw/src/templates/dashboard-js/views/lite-tasks.js +21 -16
- package/ccw/src/templates/dashboard-js/views/mcp-manager.js +148 -8
- package/ccw/src/templates/dashboard-js/views/project-overview.js +15 -11
- package/ccw/src/templates/dashboard-js/views/review-session.js +3 -3
- package/ccw/src/templates/dashboard-js/views/session-detail.js +38 -28
- package/ccw/src/templates/dashboard.html +129 -28
- package/ccw/src/tools/classify-folders.js +204 -0
- package/ccw/src/tools/convert-tokens-to-css.js +250 -0
- package/ccw/src/tools/detect-changed-modules.js +288 -0
- package/ccw/src/tools/discover-design-files.js +134 -0
- package/ccw/src/tools/edit-file.js +266 -0
- package/ccw/src/tools/generate-module-docs.js +416 -0
- package/ccw/src/tools/get-modules-by-depth.js +308 -0
- package/ccw/src/tools/index.js +176 -0
- package/ccw/src/tools/ui-generate-preview.js +327 -0
- package/ccw/src/tools/ui-instantiate-prototypes.js +301 -0
- package/ccw/src/tools/update-module-claude.js +380 -0
- package/ccw/src/utils/browser-launcher.js +15 -4
- package/package.json +1 -1
- package/.claude/skills/command-guide/reference/commands/workflow/status.md +0 -352
- package/ccw/src/core/server.js.bak +0 -385
- package/ccw/src/core/server_original.bak +0 -385
- package/ccw/src/templates/dashboard.css +0 -8114
- package/ccw/src/templates/dashboard_tailwind.html +0 -42
- package/ccw/src/templates/dashboard_test.html +0 -37
- package/ccw/src/templates/tailwind-base.css +0 -212
package/ccw/src/core/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import { URL } from 'url';
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, promises as fsPromises } from 'fs';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, statSync, promises as fsPromises } from 'fs';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
@@ -8,17 +8,46 @@ import { scanSessions } from './session-scanner.js';
|
|
|
8
8
|
import { aggregateData } from './data-aggregator.js';
|
|
9
9
|
import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js';
|
|
10
10
|
|
|
11
|
-
// Claude config file
|
|
11
|
+
// Claude config file paths
|
|
12
12
|
const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json');
|
|
13
|
+
const CLAUDE_SETTINGS_DIR = join(homedir(), '.claude');
|
|
14
|
+
const CLAUDE_GLOBAL_SETTINGS = join(CLAUDE_SETTINGS_DIR, 'settings.json');
|
|
15
|
+
const CLAUDE_GLOBAL_SETTINGS_LOCAL = join(CLAUDE_SETTINGS_DIR, 'settings.local.json');
|
|
16
|
+
|
|
17
|
+
// Enterprise managed MCP paths (platform-specific)
|
|
18
|
+
function getEnterpriseMcpPath() {
|
|
19
|
+
const platform = process.platform;
|
|
20
|
+
if (platform === 'darwin') {
|
|
21
|
+
return '/Library/Application Support/ClaudeCode/managed-mcp.json';
|
|
22
|
+
} else if (platform === 'win32') {
|
|
23
|
+
return 'C:\\Program Files\\ClaudeCode\\managed-mcp.json';
|
|
24
|
+
} else {
|
|
25
|
+
// Linux and WSL
|
|
26
|
+
return '/etc/claude-code/managed-mcp.json';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
13
29
|
|
|
14
30
|
// WebSocket clients for real-time notifications
|
|
15
31
|
const wsClients = new Set();
|
|
16
32
|
|
|
17
33
|
const TEMPLATE_PATH = join(import.meta.dirname, '../templates/dashboard.html');
|
|
18
|
-
const
|
|
34
|
+
const MODULE_CSS_DIR = join(import.meta.dirname, '../templates/dashboard-css');
|
|
19
35
|
const JS_FILE = join(import.meta.dirname, '../templates/dashboard.js');
|
|
20
36
|
const MODULE_JS_DIR = join(import.meta.dirname, '../templates/dashboard-js');
|
|
21
37
|
|
|
38
|
+
// Modular CSS files in load order
|
|
39
|
+
const MODULE_CSS_FILES = [
|
|
40
|
+
'01-base.css',
|
|
41
|
+
'02-session.css',
|
|
42
|
+
'03-tasks.css',
|
|
43
|
+
'04-lite-tasks.css',
|
|
44
|
+
'05-context.css',
|
|
45
|
+
'06-cards.css',
|
|
46
|
+
'07-managers.css',
|
|
47
|
+
'08-review.css',
|
|
48
|
+
'09-explorer.css'
|
|
49
|
+
];
|
|
50
|
+
|
|
22
51
|
/**
|
|
23
52
|
* Handle POST request with JSON body
|
|
24
53
|
*/
|
|
@@ -56,6 +85,7 @@ const MODULE_FILES = [
|
|
|
56
85
|
'components/sidebar.js',
|
|
57
86
|
'components/carousel.js',
|
|
58
87
|
'components/notifications.js',
|
|
88
|
+
'components/global-notifications.js',
|
|
59
89
|
'components/mcp-manager.js',
|
|
60
90
|
'components/hook-manager.js',
|
|
61
91
|
'components/_exp_helpers.js',
|
|
@@ -74,6 +104,7 @@ const MODULE_FILES = [
|
|
|
74
104
|
'views/fix-session.js',
|
|
75
105
|
'views/mcp-manager.js',
|
|
76
106
|
'views/hook-manager.js',
|
|
107
|
+
'views/explorer.js',
|
|
77
108
|
'main.js'
|
|
78
109
|
];
|
|
79
110
|
/**
|
|
@@ -160,6 +191,24 @@ export async function startServer(options = {}) {
|
|
|
160
191
|
return;
|
|
161
192
|
}
|
|
162
193
|
|
|
194
|
+
// API: Shutdown server (for ccw stop command)
|
|
195
|
+
if (pathname === '/api/shutdown' && req.method === 'POST') {
|
|
196
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
197
|
+
res.end(JSON.stringify({ status: 'shutting_down' }));
|
|
198
|
+
|
|
199
|
+
// Graceful shutdown
|
|
200
|
+
console.log('\n Received shutdown signal...');
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
server.close(() => {
|
|
203
|
+
console.log(' Server stopped.\n');
|
|
204
|
+
process.exit(0);
|
|
205
|
+
});
|
|
206
|
+
// Force exit after 3 seconds if graceful shutdown fails
|
|
207
|
+
setTimeout(() => process.exit(0), 3000);
|
|
208
|
+
}, 100);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
163
212
|
// API: Remove a recent path
|
|
164
213
|
if (pathname === '/api/remove-recent-path' && req.method === 'POST') {
|
|
165
214
|
handlePostRequest(req, res, async (body) => {
|
|
@@ -353,6 +402,41 @@ export async function startServer(options = {}) {
|
|
|
353
402
|
return;
|
|
354
403
|
}
|
|
355
404
|
|
|
405
|
+
// API: List directory files with .gitignore filtering (Explorer view)
|
|
406
|
+
if (pathname === '/api/files') {
|
|
407
|
+
const dirPath = url.searchParams.get('path') || initialPath;
|
|
408
|
+
const filesData = await listDirectoryFiles(dirPath);
|
|
409
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
410
|
+
res.end(JSON.stringify(filesData));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// API: Get file content for preview (Explorer view)
|
|
415
|
+
if (pathname === '/api/file-content') {
|
|
416
|
+
const filePath = url.searchParams.get('path');
|
|
417
|
+
if (!filePath) {
|
|
418
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
419
|
+
res.end(JSON.stringify({ error: 'File path is required' }));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const fileData = await getFileContent(filePath);
|
|
423
|
+
res.writeHead(fileData.error ? 404 : 200, { 'Content-Type': 'application/json' });
|
|
424
|
+
res.end(JSON.stringify(fileData));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// API: Update CLAUDE.md using CLI tools (Explorer view)
|
|
429
|
+
if (pathname === '/api/update-claude-md' && req.method === 'POST') {
|
|
430
|
+
handlePostRequest(req, res, async (body) => {
|
|
431
|
+
const { path: targetPath, tool = 'gemini', strategy = 'single-layer' } = body;
|
|
432
|
+
if (!targetPath) {
|
|
433
|
+
return { error: 'path is required', status: 400 };
|
|
434
|
+
}
|
|
435
|
+
return await triggerUpdateClaudeMd(targetPath, tool, strategy);
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
356
440
|
// Serve dashboard HTML
|
|
357
441
|
if (pathname === '/' || pathname === '/index.html') {
|
|
358
442
|
const html = generateServerDashboard(initialPath);
|
|
@@ -423,9 +507,31 @@ function handleWebSocketUpgrade(req, socket, head) {
|
|
|
423
507
|
// Handle incoming messages
|
|
424
508
|
socket.on('data', (buffer) => {
|
|
425
509
|
try {
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
-
|
|
510
|
+
const frame = parseWebSocketFrame(buffer);
|
|
511
|
+
if (!frame) return;
|
|
512
|
+
|
|
513
|
+
const { opcode, payload } = frame;
|
|
514
|
+
|
|
515
|
+
switch (opcode) {
|
|
516
|
+
case 0x1: // Text frame
|
|
517
|
+
if (payload) {
|
|
518
|
+
console.log('[WS] Received:', payload);
|
|
519
|
+
}
|
|
520
|
+
break;
|
|
521
|
+
case 0x8: // Close frame
|
|
522
|
+
socket.end();
|
|
523
|
+
break;
|
|
524
|
+
case 0x9: // Ping frame - respond with Pong
|
|
525
|
+
const pongFrame = Buffer.alloc(2);
|
|
526
|
+
pongFrame[0] = 0x8A; // Pong opcode with FIN bit
|
|
527
|
+
pongFrame[1] = 0x00; // No payload
|
|
528
|
+
socket.write(pongFrame);
|
|
529
|
+
break;
|
|
530
|
+
case 0xA: // Pong frame - ignore
|
|
531
|
+
break;
|
|
532
|
+
default:
|
|
533
|
+
// Ignore other frame types (binary, continuation)
|
|
534
|
+
break;
|
|
429
535
|
}
|
|
430
536
|
} catch (e) {
|
|
431
537
|
// Ignore parse errors
|
|
@@ -445,10 +551,18 @@ function handleWebSocketUpgrade(req, socket, head) {
|
|
|
445
551
|
|
|
446
552
|
/**
|
|
447
553
|
* Parse WebSocket frame (simplified)
|
|
554
|
+
* Returns { opcode, payload } or null
|
|
448
555
|
*/
|
|
449
556
|
function parseWebSocketFrame(buffer) {
|
|
450
557
|
if (buffer.length < 2) return null;
|
|
451
558
|
|
|
559
|
+
const firstByte = buffer[0];
|
|
560
|
+
const opcode = firstByte & 0x0f; // Extract opcode (bits 0-3)
|
|
561
|
+
|
|
562
|
+
// Opcode types:
|
|
563
|
+
// 0x0 = continuation, 0x1 = text, 0x2 = binary
|
|
564
|
+
// 0x8 = close, 0x9 = ping, 0xA = pong
|
|
565
|
+
|
|
452
566
|
const secondByte = buffer[1];
|
|
453
567
|
const isMasked = (secondByte & 0x80) !== 0;
|
|
454
568
|
let payloadLength = secondByte & 0x7f;
|
|
@@ -476,7 +590,7 @@ function parseWebSocketFrame(buffer) {
|
|
|
476
590
|
}
|
|
477
591
|
}
|
|
478
592
|
|
|
479
|
-
return payload.toString('utf8');
|
|
593
|
+
return { opcode, payload: payload.toString('utf8') };
|
|
480
594
|
}
|
|
481
595
|
|
|
482
596
|
/**
|
|
@@ -931,8 +1045,11 @@ async function updateTaskStatus(sessionPath, taskId, newStatus) {
|
|
|
931
1045
|
function generateServerDashboard(initialPath) {
|
|
932
1046
|
let html = readFileSync(TEMPLATE_PATH, 'utf8');
|
|
933
1047
|
|
|
934
|
-
// Read CSS
|
|
935
|
-
const cssContent =
|
|
1048
|
+
// Read and concatenate modular CSS files in load order
|
|
1049
|
+
const cssContent = MODULE_CSS_FILES.map(file => {
|
|
1050
|
+
const filePath = join(MODULE_CSS_DIR, file);
|
|
1051
|
+
return existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
|
|
1052
|
+
}).join('\n\n');
|
|
936
1053
|
|
|
937
1054
|
// Read and concatenate modular JS files in dependency order
|
|
938
1055
|
let jsContent = MODULE_FILES.map(file => {
|
|
@@ -1006,22 +1123,114 @@ async function loadRecentPaths() {
|
|
|
1006
1123
|
// ========================================
|
|
1007
1124
|
|
|
1008
1125
|
/**
|
|
1009
|
-
*
|
|
1126
|
+
* Safely read and parse JSON file
|
|
1127
|
+
* @param {string} filePath
|
|
1128
|
+
* @returns {Object|null}
|
|
1129
|
+
*/
|
|
1130
|
+
function safeReadJson(filePath) {
|
|
1131
|
+
try {
|
|
1132
|
+
if (!existsSync(filePath)) return null;
|
|
1133
|
+
const content = readFileSync(filePath, 'utf8');
|
|
1134
|
+
return JSON.parse(content);
|
|
1135
|
+
} catch {
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Get MCP servers from a JSON file (expects mcpServers key at top level)
|
|
1142
|
+
* @param {string} filePath
|
|
1143
|
+
* @returns {Object} mcpServers object or empty object
|
|
1144
|
+
*/
|
|
1145
|
+
function getMcpServersFromFile(filePath) {
|
|
1146
|
+
const config = safeReadJson(filePath);
|
|
1147
|
+
if (!config) return {};
|
|
1148
|
+
return config.mcpServers || {};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Get MCP configuration from multiple sources (per official Claude Code docs):
|
|
1153
|
+
*
|
|
1154
|
+
* Priority (highest to lowest):
|
|
1155
|
+
* 1. Enterprise managed-mcp.json (cannot be overridden)
|
|
1156
|
+
* 2. Local scope (project-specific private in ~/.claude.json)
|
|
1157
|
+
* 3. Project scope (.mcp.json in project root)
|
|
1158
|
+
* 4. User scope (mcpServers in ~/.claude.json)
|
|
1159
|
+
*
|
|
1160
|
+
* Note: ~/.claude/settings.json is for MCP PERMISSIONS, NOT definitions!
|
|
1161
|
+
*
|
|
1010
1162
|
* @returns {Object}
|
|
1011
1163
|
*/
|
|
1012
1164
|
function getMcpConfig() {
|
|
1013
1165
|
try {
|
|
1014
|
-
|
|
1015
|
-
|
|
1166
|
+
const result = {
|
|
1167
|
+
projects: {},
|
|
1168
|
+
userServers: {}, // User-level servers from ~/.claude.json mcpServers
|
|
1169
|
+
enterpriseServers: {}, // Enterprise managed servers (highest priority)
|
|
1170
|
+
configSources: [] // Track where configs came from for debugging
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
// 1. Read Enterprise managed MCP servers (highest priority)
|
|
1174
|
+
const enterprisePath = getEnterpriseMcpPath();
|
|
1175
|
+
if (existsSync(enterprisePath)) {
|
|
1176
|
+
const enterpriseConfig = safeReadJson(enterprisePath);
|
|
1177
|
+
if (enterpriseConfig?.mcpServers) {
|
|
1178
|
+
result.enterpriseServers = enterpriseConfig.mcpServers;
|
|
1179
|
+
result.configSources.push({ type: 'enterprise', path: enterprisePath, count: Object.keys(enterpriseConfig.mcpServers).length });
|
|
1180
|
+
}
|
|
1016
1181
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1182
|
+
|
|
1183
|
+
// 2. Read from ~/.claude.json
|
|
1184
|
+
if (existsSync(CLAUDE_CONFIG_PATH)) {
|
|
1185
|
+
const claudeConfig = safeReadJson(CLAUDE_CONFIG_PATH);
|
|
1186
|
+
if (claudeConfig) {
|
|
1187
|
+
// 2a. User-level mcpServers (top-level mcpServers key)
|
|
1188
|
+
if (claudeConfig.mcpServers) {
|
|
1189
|
+
result.userServers = claudeConfig.mcpServers;
|
|
1190
|
+
result.configSources.push({ type: 'user', path: CLAUDE_CONFIG_PATH, count: Object.keys(claudeConfig.mcpServers).length });
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// 2b. Project-specific configurations (projects[path].mcpServers)
|
|
1194
|
+
if (claudeConfig.projects) {
|
|
1195
|
+
result.projects = claudeConfig.projects;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// 3. For each known project, check for .mcp.json (project-level config)
|
|
1201
|
+
const projectPaths = Object.keys(result.projects);
|
|
1202
|
+
for (const projectPath of projectPaths) {
|
|
1203
|
+
const mcpJsonPath = join(projectPath, '.mcp.json');
|
|
1204
|
+
if (existsSync(mcpJsonPath)) {
|
|
1205
|
+
const mcpJsonConfig = safeReadJson(mcpJsonPath);
|
|
1206
|
+
if (mcpJsonConfig?.mcpServers) {
|
|
1207
|
+
// Merge .mcp.json servers into project config
|
|
1208
|
+
// Project's .mcp.json has lower priority than ~/.claude.json projects[path].mcpServers
|
|
1209
|
+
const existingServers = result.projects[projectPath]?.mcpServers || {};
|
|
1210
|
+
result.projects[projectPath] = {
|
|
1211
|
+
...result.projects[projectPath],
|
|
1212
|
+
mcpServers: {
|
|
1213
|
+
...mcpJsonConfig.mcpServers, // .mcp.json (lower priority)
|
|
1214
|
+
...existingServers // ~/.claude.json projects[path] (higher priority)
|
|
1215
|
+
},
|
|
1216
|
+
mcpJsonPath: mcpJsonPath // Track source for debugging
|
|
1217
|
+
};
|
|
1218
|
+
result.configSources.push({ type: 'project-mcp-json', path: mcpJsonPath, count: Object.keys(mcpJsonConfig.mcpServers).length });
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Build globalServers by merging user and enterprise servers
|
|
1224
|
+
// Enterprise servers override user servers
|
|
1225
|
+
result.globalServers = {
|
|
1226
|
+
...result.userServers,
|
|
1227
|
+
...result.enterpriseServers
|
|
1021
1228
|
};
|
|
1229
|
+
|
|
1230
|
+
return result;
|
|
1022
1231
|
} catch (error) {
|
|
1023
1232
|
console.error('Error reading MCP config:', error);
|
|
1024
|
-
return { projects: {}, error: error.message };
|
|
1233
|
+
return { projects: {}, globalServers: {}, userServers: {}, enterpriseServers: {}, configSources: [], error: error.message };
|
|
1025
1234
|
}
|
|
1026
1235
|
}
|
|
1027
1236
|
|
|
@@ -1380,3 +1589,360 @@ function deleteHookFromSettings(projectPath, scope, event, hookIndex) {
|
|
|
1380
1589
|
return { error: error.message };
|
|
1381
1590
|
}
|
|
1382
1591
|
}
|
|
1592
|
+
|
|
1593
|
+
// ========================================
|
|
1594
|
+
// Explorer View Functions
|
|
1595
|
+
// ========================================
|
|
1596
|
+
|
|
1597
|
+
// Directories to always exclude from file tree
|
|
1598
|
+
const EXPLORER_EXCLUDE_DIRS = [
|
|
1599
|
+
'.git', '__pycache__', 'node_modules', '.venv', 'venv', 'env',
|
|
1600
|
+
'dist', 'build', '.cache', '.pytest_cache', '.mypy_cache',
|
|
1601
|
+
'coverage', '.nyc_output', 'logs', 'tmp', 'temp', '.next',
|
|
1602
|
+
'.nuxt', '.output', '.turbo', '.parcel-cache'
|
|
1603
|
+
];
|
|
1604
|
+
|
|
1605
|
+
// File extensions to language mapping for syntax highlighting
|
|
1606
|
+
const EXT_TO_LANGUAGE = {
|
|
1607
|
+
'.js': 'javascript',
|
|
1608
|
+
'.jsx': 'javascript',
|
|
1609
|
+
'.ts': 'typescript',
|
|
1610
|
+
'.tsx': 'typescript',
|
|
1611
|
+
'.py': 'python',
|
|
1612
|
+
'.rb': 'ruby',
|
|
1613
|
+
'.java': 'java',
|
|
1614
|
+
'.go': 'go',
|
|
1615
|
+
'.rs': 'rust',
|
|
1616
|
+
'.c': 'c',
|
|
1617
|
+
'.cpp': 'cpp',
|
|
1618
|
+
'.h': 'c',
|
|
1619
|
+
'.hpp': 'cpp',
|
|
1620
|
+
'.cs': 'csharp',
|
|
1621
|
+
'.php': 'php',
|
|
1622
|
+
'.swift': 'swift',
|
|
1623
|
+
'.kt': 'kotlin',
|
|
1624
|
+
'.scala': 'scala',
|
|
1625
|
+
'.sh': 'bash',
|
|
1626
|
+
'.bash': 'bash',
|
|
1627
|
+
'.zsh': 'bash',
|
|
1628
|
+
'.ps1': 'powershell',
|
|
1629
|
+
'.sql': 'sql',
|
|
1630
|
+
'.html': 'html',
|
|
1631
|
+
'.htm': 'html',
|
|
1632
|
+
'.css': 'css',
|
|
1633
|
+
'.scss': 'scss',
|
|
1634
|
+
'.sass': 'sass',
|
|
1635
|
+
'.less': 'less',
|
|
1636
|
+
'.json': 'json',
|
|
1637
|
+
'.xml': 'xml',
|
|
1638
|
+
'.yaml': 'yaml',
|
|
1639
|
+
'.yml': 'yaml',
|
|
1640
|
+
'.toml': 'toml',
|
|
1641
|
+
'.ini': 'ini',
|
|
1642
|
+
'.cfg': 'ini',
|
|
1643
|
+
'.conf': 'nginx',
|
|
1644
|
+
'.md': 'markdown',
|
|
1645
|
+
'.markdown': 'markdown',
|
|
1646
|
+
'.txt': 'plaintext',
|
|
1647
|
+
'.log': 'plaintext',
|
|
1648
|
+
'.env': 'bash',
|
|
1649
|
+
'.dockerfile': 'dockerfile',
|
|
1650
|
+
'.vue': 'html',
|
|
1651
|
+
'.svelte': 'html'
|
|
1652
|
+
};
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* Parse .gitignore file and return patterns
|
|
1656
|
+
* @param {string} gitignorePath - Path to .gitignore file
|
|
1657
|
+
* @returns {string[]} Array of gitignore patterns
|
|
1658
|
+
*/
|
|
1659
|
+
function parseGitignore(gitignorePath) {
|
|
1660
|
+
try {
|
|
1661
|
+
if (!existsSync(gitignorePath)) return [];
|
|
1662
|
+
const content = readFileSync(gitignorePath, 'utf8');
|
|
1663
|
+
return content
|
|
1664
|
+
.split('\n')
|
|
1665
|
+
.map(line => line.trim())
|
|
1666
|
+
.filter(line => line && !line.startsWith('#'));
|
|
1667
|
+
} catch {
|
|
1668
|
+
return [];
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Check if a file/directory should be ignored based on gitignore patterns
|
|
1674
|
+
* Simple pattern matching (supports basic glob patterns)
|
|
1675
|
+
* @param {string} name - File or directory name
|
|
1676
|
+
* @param {string[]} patterns - Gitignore patterns
|
|
1677
|
+
* @param {boolean} isDirectory - Whether the entry is a directory
|
|
1678
|
+
* @returns {boolean}
|
|
1679
|
+
*/
|
|
1680
|
+
function shouldIgnore(name, patterns, isDirectory) {
|
|
1681
|
+
// Always exclude certain directories
|
|
1682
|
+
if (isDirectory && EXPLORER_EXCLUDE_DIRS.includes(name)) {
|
|
1683
|
+
return true;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// Skip hidden files/directories (starting with .)
|
|
1687
|
+
if (name.startsWith('.') && name !== '.claude' && name !== '.workflow') {
|
|
1688
|
+
return true;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
for (const pattern of patterns) {
|
|
1692
|
+
let p = pattern;
|
|
1693
|
+
|
|
1694
|
+
// Handle negation patterns (we skip them for simplicity)
|
|
1695
|
+
if (p.startsWith('!')) continue;
|
|
1696
|
+
|
|
1697
|
+
// Handle directory-only patterns
|
|
1698
|
+
if (p.endsWith('/')) {
|
|
1699
|
+
if (!isDirectory) continue;
|
|
1700
|
+
p = p.slice(0, -1);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// Simple pattern matching
|
|
1704
|
+
if (p === name) return true;
|
|
1705
|
+
|
|
1706
|
+
// Handle wildcard patterns
|
|
1707
|
+
if (p.includes('*')) {
|
|
1708
|
+
const regex = new RegExp('^' + p.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
1709
|
+
if (regex.test(name)) return true;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Handle extension patterns like *.log
|
|
1713
|
+
if (p.startsWith('*.')) {
|
|
1714
|
+
const ext = p.slice(1);
|
|
1715
|
+
if (name.endsWith(ext)) return true;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* List directory files with .gitignore filtering
|
|
1724
|
+
* @param {string} dirPath - Directory path to list
|
|
1725
|
+
* @returns {Promise<Object>}
|
|
1726
|
+
*/
|
|
1727
|
+
async function listDirectoryFiles(dirPath) {
|
|
1728
|
+
try {
|
|
1729
|
+
// Normalize path
|
|
1730
|
+
let normalizedPath = dirPath.replace(/\\/g, '/');
|
|
1731
|
+
if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
|
|
1732
|
+
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (!existsSync(normalizedPath)) {
|
|
1736
|
+
return { error: 'Directory not found', files: [] };
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
if (!statSync(normalizedPath).isDirectory()) {
|
|
1740
|
+
return { error: 'Not a directory', files: [] };
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Parse .gitignore patterns
|
|
1744
|
+
const gitignorePath = join(normalizedPath, '.gitignore');
|
|
1745
|
+
const gitignorePatterns = parseGitignore(gitignorePath);
|
|
1746
|
+
|
|
1747
|
+
// Read directory entries
|
|
1748
|
+
const entries = readdirSync(normalizedPath, { withFileTypes: true });
|
|
1749
|
+
|
|
1750
|
+
const files = [];
|
|
1751
|
+
for (const entry of entries) {
|
|
1752
|
+
const isDirectory = entry.isDirectory();
|
|
1753
|
+
|
|
1754
|
+
// Check if should be ignored
|
|
1755
|
+
if (shouldIgnore(entry.name, gitignorePatterns, isDirectory)) {
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const entryPath = join(normalizedPath, entry.name);
|
|
1760
|
+
const fileInfo = {
|
|
1761
|
+
name: entry.name,
|
|
1762
|
+
type: isDirectory ? 'directory' : 'file',
|
|
1763
|
+
path: entryPath.replace(/\\/g, '/')
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
// Check if directory has CLAUDE.md
|
|
1767
|
+
if (isDirectory) {
|
|
1768
|
+
const claudeMdPath = join(entryPath, 'CLAUDE.md');
|
|
1769
|
+
fileInfo.hasClaudeMd = existsSync(claudeMdPath);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
files.push(fileInfo);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// Sort: directories first, then alphabetically
|
|
1776
|
+
files.sort((a, b) => {
|
|
1777
|
+
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
|
1778
|
+
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
|
1779
|
+
return a.name.localeCompare(b.name);
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
return {
|
|
1783
|
+
path: normalizedPath.replace(/\\/g, '/'),
|
|
1784
|
+
files,
|
|
1785
|
+
gitignorePatterns
|
|
1786
|
+
};
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
console.error('Error listing directory:', error);
|
|
1789
|
+
return { error: error.message, files: [] };
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Get file content for preview
|
|
1795
|
+
* @param {string} filePath - Path to file
|
|
1796
|
+
* @returns {Promise<Object>}
|
|
1797
|
+
*/
|
|
1798
|
+
async function getFileContent(filePath) {
|
|
1799
|
+
try {
|
|
1800
|
+
// Normalize path
|
|
1801
|
+
let normalizedPath = filePath.replace(/\\/g, '/');
|
|
1802
|
+
if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
|
|
1803
|
+
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
if (!existsSync(normalizedPath)) {
|
|
1807
|
+
return { error: 'File not found' };
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
const stats = statSync(normalizedPath);
|
|
1811
|
+
if (stats.isDirectory()) {
|
|
1812
|
+
return { error: 'Cannot read directory' };
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Check file size (limit to 1MB for preview)
|
|
1816
|
+
if (stats.size > 1024 * 1024) {
|
|
1817
|
+
return { error: 'File too large for preview (max 1MB)', size: stats.size };
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// Read file content
|
|
1821
|
+
const content = readFileSync(normalizedPath, 'utf8');
|
|
1822
|
+
const ext = normalizedPath.substring(normalizedPath.lastIndexOf('.')).toLowerCase();
|
|
1823
|
+
const language = EXT_TO_LANGUAGE[ext] || 'plaintext';
|
|
1824
|
+
const isMarkdown = ext === '.md' || ext === '.markdown';
|
|
1825
|
+
const fileName = normalizedPath.split('/').pop();
|
|
1826
|
+
|
|
1827
|
+
return {
|
|
1828
|
+
content,
|
|
1829
|
+
language,
|
|
1830
|
+
isMarkdown,
|
|
1831
|
+
fileName,
|
|
1832
|
+
path: normalizedPath,
|
|
1833
|
+
size: stats.size,
|
|
1834
|
+
lines: content.split('\n').length
|
|
1835
|
+
};
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
console.error('Error reading file:', error);
|
|
1838
|
+
return { error: error.message };
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Trigger update-module-claude tool (async execution)
|
|
1844
|
+
* @param {string} targetPath - Directory path to update
|
|
1845
|
+
* @param {string} tool - CLI tool to use (gemini, qwen, codex)
|
|
1846
|
+
* @param {string} strategy - Update strategy (single-layer, multi-layer)
|
|
1847
|
+
* @returns {Promise<Object>}
|
|
1848
|
+
*/
|
|
1849
|
+
async function triggerUpdateClaudeMd(targetPath, tool, strategy) {
|
|
1850
|
+
const { spawn } = await import('child_process');
|
|
1851
|
+
|
|
1852
|
+
// Normalize path
|
|
1853
|
+
let normalizedPath = targetPath.replace(/\\/g, '/');
|
|
1854
|
+
if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
|
|
1855
|
+
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
if (!existsSync(normalizedPath)) {
|
|
1859
|
+
return { error: 'Directory not found' };
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
if (!statSync(normalizedPath).isDirectory()) {
|
|
1863
|
+
return { error: 'Not a directory' };
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// Build ccw tool command with JSON parameters
|
|
1867
|
+
const params = JSON.stringify({
|
|
1868
|
+
strategy,
|
|
1869
|
+
path: normalizedPath,
|
|
1870
|
+
tool
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
console.log(`[Explorer] Running async: ccw tool exec update_module_claude with ${tool} (${strategy})`);
|
|
1874
|
+
|
|
1875
|
+
return new Promise((resolve) => {
|
|
1876
|
+
const isWindows = process.platform === 'win32';
|
|
1877
|
+
|
|
1878
|
+
// Spawn the process
|
|
1879
|
+
const child = spawn('ccw', ['tool', 'exec', 'update_module_claude', params], {
|
|
1880
|
+
cwd: normalizedPath,
|
|
1881
|
+
shell: isWindows,
|
|
1882
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
let stdout = '';
|
|
1886
|
+
let stderr = '';
|
|
1887
|
+
|
|
1888
|
+
child.stdout.on('data', (data) => {
|
|
1889
|
+
stdout += data.toString();
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
child.stderr.on('data', (data) => {
|
|
1893
|
+
stderr += data.toString();
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
child.on('close', (code) => {
|
|
1897
|
+
if (code === 0) {
|
|
1898
|
+
// Parse the JSON output from the tool
|
|
1899
|
+
let result;
|
|
1900
|
+
try {
|
|
1901
|
+
result = JSON.parse(stdout);
|
|
1902
|
+
} catch {
|
|
1903
|
+
result = { output: stdout };
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
if (result.success === false || result.error) {
|
|
1907
|
+
resolve({
|
|
1908
|
+
success: false,
|
|
1909
|
+
error: result.error || result.message || 'Update failed',
|
|
1910
|
+
output: stdout
|
|
1911
|
+
});
|
|
1912
|
+
} else {
|
|
1913
|
+
resolve({
|
|
1914
|
+
success: true,
|
|
1915
|
+
message: result.message || `CLAUDE.md updated successfully using ${tool} (${strategy})`,
|
|
1916
|
+
output: stdout,
|
|
1917
|
+
path: normalizedPath
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
} else {
|
|
1921
|
+
resolve({
|
|
1922
|
+
success: false,
|
|
1923
|
+
error: stderr || `Process exited with code ${code}`,
|
|
1924
|
+
output: stdout + stderr
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
child.on('error', (error) => {
|
|
1930
|
+
console.error('Error spawning process:', error);
|
|
1931
|
+
resolve({
|
|
1932
|
+
success: false,
|
|
1933
|
+
error: error.message,
|
|
1934
|
+
output: ''
|
|
1935
|
+
});
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
// Timeout after 5 minutes
|
|
1939
|
+
setTimeout(() => {
|
|
1940
|
+
child.kill();
|
|
1941
|
+
resolve({
|
|
1942
|
+
success: false,
|
|
1943
|
+
error: 'Timeout: Process took longer than 5 minutes',
|
|
1944
|
+
output: stdout
|
|
1945
|
+
});
|
|
1946
|
+
}, 300000);
|
|
1947
|
+
});
|
|
1948
|
+
}
|