claude-code-workflow 6.0.2 → 6.0.5
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/ccw/package.json +6 -6
- package/ccw/src/cli.js +9 -0
- package/ccw/src/commands/stop.js +101 -0
- package/ccw/src/commands/view.js +98 -7
- package/ccw/src/core/server.js +125 -10
- package/ccw/src/templates/dashboard-js/components/mcp-manager.js +13 -1
- package/ccw/src/templates/dashboard-js/main.js +11 -1
- package/ccw/src/templates/dashboard-js/views/mcp-manager.js +73 -4
- package/ccw/src/templates/dashboard.css +256 -0
- package/ccw/src/utils/browser-launcher.js +15 -4
- package/package.json +1 -1
package/ccw/package.json
CHANGED
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
"node": ">=16.0.0"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"
|
|
28
|
-
"open": "^9.1.0",
|
|
27
|
+
"boxen": "^7.1.0",
|
|
29
28
|
"chalk": "^5.3.0",
|
|
29
|
+
"commander": "^11.0.0",
|
|
30
|
+
"figlet": "^1.7.0",
|
|
30
31
|
"glob": "^10.3.0",
|
|
32
|
+
"gradient-string": "^2.0.2",
|
|
31
33
|
"inquirer": "^9.2.0",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"boxen": "^7.1.0",
|
|
35
|
-
"gradient-string": "^2.0.2"
|
|
34
|
+
"open": "^9.1.0",
|
|
35
|
+
"ora": "^7.0.0"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"bin/",
|
package/ccw/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { viewCommand } from './commands/view.js';
|
|
3
3
|
import { serveCommand } from './commands/serve.js';
|
|
4
|
+
import { stopCommand } from './commands/stop.js';
|
|
4
5
|
import { installCommand } from './commands/install.js';
|
|
5
6
|
import { uninstallCommand } from './commands/uninstall.js';
|
|
6
7
|
import { upgradeCommand } from './commands/upgrade.js';
|
|
@@ -68,6 +69,14 @@ export function run(argv) {
|
|
|
68
69
|
.option('--no-browser', 'Start server without opening browser')
|
|
69
70
|
.action(serveCommand);
|
|
70
71
|
|
|
72
|
+
// Stop command
|
|
73
|
+
program
|
|
74
|
+
.command('stop')
|
|
75
|
+
.description('Stop the running CCW dashboard server')
|
|
76
|
+
.option('--port <port>', 'Server port', '3456')
|
|
77
|
+
.option('-f, --force', 'Force kill process on the port')
|
|
78
|
+
.action(stopCommand);
|
|
79
|
+
|
|
71
80
|
// Install command
|
|
72
81
|
program
|
|
73
82
|
.command('install')
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find process using a specific port (Windows)
|
|
9
|
+
* @param {number} port - Port number
|
|
10
|
+
* @returns {Promise<string|null>} PID or null
|
|
11
|
+
*/
|
|
12
|
+
async function findProcessOnPort(port) {
|
|
13
|
+
try {
|
|
14
|
+
const { stdout } = await execAsync(`netstat -ano | findstr :${port} | findstr LISTENING`);
|
|
15
|
+
const lines = stdout.trim().split('\n');
|
|
16
|
+
if (lines.length > 0) {
|
|
17
|
+
const parts = lines[0].trim().split(/\s+/);
|
|
18
|
+
return parts[parts.length - 1]; // PID is the last column
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
// No process found
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Kill process by PID (Windows)
|
|
28
|
+
* @param {string} pid - Process ID
|
|
29
|
+
* @returns {Promise<boolean>} Success status
|
|
30
|
+
*/
|
|
31
|
+
async function killProcess(pid) {
|
|
32
|
+
try {
|
|
33
|
+
await execAsync(`taskkill /PID ${pid} /F`);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Stop command handler - stops the running CCW dashboard server
|
|
42
|
+
* @param {Object} options - Command options
|
|
43
|
+
*/
|
|
44
|
+
export async function stopCommand(options) {
|
|
45
|
+
const port = options.port || 3456;
|
|
46
|
+
const force = options.force || false;
|
|
47
|
+
|
|
48
|
+
console.log(chalk.blue.bold('\n CCW Dashboard\n'));
|
|
49
|
+
console.log(chalk.gray(` Checking server on port ${port}...`));
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Try graceful shutdown via API first
|
|
53
|
+
const healthCheck = await fetch(`http://localhost:${port}/api/health`, {
|
|
54
|
+
signal: AbortSignal.timeout(2000)
|
|
55
|
+
}).catch(() => null);
|
|
56
|
+
|
|
57
|
+
if (healthCheck && healthCheck.ok) {
|
|
58
|
+
// CCW server is running - send shutdown signal
|
|
59
|
+
console.log(chalk.cyan(' CCW server found, sending shutdown signal...'));
|
|
60
|
+
|
|
61
|
+
await fetch(`http://localhost:${port}/api/shutdown`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
signal: AbortSignal.timeout(5000)
|
|
64
|
+
}).catch(() => null);
|
|
65
|
+
|
|
66
|
+
// Wait a moment for shutdown
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
68
|
+
|
|
69
|
+
console.log(chalk.green.bold('\n Server stopped successfully!\n'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// No CCW server responding, check if port is in use
|
|
74
|
+
const pid = await findProcessOnPort(port);
|
|
75
|
+
|
|
76
|
+
if (!pid) {
|
|
77
|
+
console.log(chalk.yellow(` No server running on port ${port}\n`));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Port is in use by another process
|
|
82
|
+
console.log(chalk.yellow(` Port ${port} is in use by process PID: ${pid}`));
|
|
83
|
+
|
|
84
|
+
if (force) {
|
|
85
|
+
console.log(chalk.cyan(' Force killing process...'));
|
|
86
|
+
const killed = await killProcess(pid);
|
|
87
|
+
|
|
88
|
+
if (killed) {
|
|
89
|
+
console.log(chalk.green.bold('\n Process killed successfully!\n'));
|
|
90
|
+
} else {
|
|
91
|
+
console.log(chalk.red('\n Failed to kill process. Try running as administrator.\n'));
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
console.log(chalk.gray(`\n This is not a CCW server. Use --force to kill it:`));
|
|
95
|
+
console.log(chalk.white(` ccw stop --force\n`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(chalk.red(`\n Error: ${err.message}\n`));
|
|
100
|
+
}
|
|
101
|
+
}
|
package/ccw/src/commands/view.js
CHANGED
|
@@ -1,14 +1,105 @@
|
|
|
1
1
|
import { serveCommand } from './serve.js';
|
|
2
|
+
import { launchBrowser } from '../utils/browser-launcher.js';
|
|
3
|
+
import { validatePath } from '../utils/path-resolver.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
*
|
|
7
|
+
* Check if server is already running on the specified port
|
|
8
|
+
* @param {number} port - Port to check
|
|
9
|
+
* @returns {Promise<boolean>} True if server is running
|
|
10
|
+
*/
|
|
11
|
+
async function isServerRunning(port) {
|
|
12
|
+
try {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
|
15
|
+
|
|
16
|
+
const response = await fetch(`http://localhost:${port}/api/health`, {
|
|
17
|
+
signal: controller.signal
|
|
18
|
+
});
|
|
19
|
+
clearTimeout(timeoutId);
|
|
20
|
+
|
|
21
|
+
return response.ok;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Switch workspace on running server
|
|
29
|
+
* @param {number} port - Server port
|
|
30
|
+
* @param {string} path - New workspace path
|
|
31
|
+
* @returns {Promise<Object>} Result with success status
|
|
32
|
+
*/
|
|
33
|
+
async function switchWorkspace(port, path) {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(
|
|
36
|
+
`http://localhost:${port}/api/switch-path?path=${encodeURIComponent(path)}`
|
|
37
|
+
);
|
|
38
|
+
return await response.json();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
return { success: false, error: err.message };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* View command handler - opens dashboard for current workspace
|
|
46
|
+
* If server is already running, switches workspace and opens browser
|
|
47
|
+
* If not running, starts a new server
|
|
5
48
|
* @param {Object} options - Command options
|
|
6
49
|
*/
|
|
7
50
|
export async function viewCommand(options) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
51
|
+
const port = options.port || 3456;
|
|
52
|
+
|
|
53
|
+
// Resolve workspace path
|
|
54
|
+
let workspacePath = process.cwd();
|
|
55
|
+
if (options.path) {
|
|
56
|
+
const pathValidation = validatePath(options.path, { mustExist: true });
|
|
57
|
+
if (!pathValidation.valid) {
|
|
58
|
+
console.error(chalk.red(`\n Error: ${pathValidation.error}\n`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
workspacePath = pathValidation.path;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if server is already running
|
|
65
|
+
const serverRunning = await isServerRunning(port);
|
|
66
|
+
|
|
67
|
+
if (serverRunning) {
|
|
68
|
+
// Server is running - switch workspace and open browser
|
|
69
|
+
console.log(chalk.blue.bold('\n CCW Dashboard\n'));
|
|
70
|
+
console.log(chalk.gray(` Server already running on port ${port}`));
|
|
71
|
+
console.log(chalk.cyan(` Switching workspace to: ${workspacePath}`));
|
|
72
|
+
|
|
73
|
+
const result = await switchWorkspace(port, workspacePath);
|
|
74
|
+
|
|
75
|
+
if (result.success) {
|
|
76
|
+
console.log(chalk.green(` Workspace switched successfully`));
|
|
77
|
+
|
|
78
|
+
// Open browser with the new path
|
|
79
|
+
const url = `http://localhost:${port}/?path=${encodeURIComponent(result.path)}`;
|
|
80
|
+
|
|
81
|
+
if (options.browser !== false) {
|
|
82
|
+
console.log(chalk.cyan(' Opening in browser...'));
|
|
83
|
+
try {
|
|
84
|
+
await launchBrowser(url);
|
|
85
|
+
console.log(chalk.green.bold('\n Dashboard opened!\n'));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.log(chalk.yellow(`\n Could not open browser: ${err.message}`));
|
|
88
|
+
console.log(chalk.gray(` Open manually: ${url}\n`));
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
console.log(chalk.gray(`\n URL: ${url}\n`));
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
console.error(chalk.red(`\n Failed to switch workspace: ${result.error}\n`));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Server not running - start new server
|
|
99
|
+
await serveCommand({
|
|
100
|
+
path: workspacePath,
|
|
101
|
+
port: port,
|
|
102
|
+
browser: options.browser
|
|
103
|
+
});
|
|
104
|
+
}
|
|
14
105
|
}
|
package/ccw/src/core/server.js
CHANGED
|
@@ -8,8 +8,11 @@ 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');
|
|
13
16
|
|
|
14
17
|
// WebSocket clients for real-time notifications
|
|
15
18
|
const wsClients = new Set();
|
|
@@ -126,6 +129,58 @@ export async function startServer(options = {}) {
|
|
|
126
129
|
return;
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
// API: Switch workspace path (for ccw view command)
|
|
133
|
+
if (pathname === '/api/switch-path') {
|
|
134
|
+
const newPath = url.searchParams.get('path');
|
|
135
|
+
if (!newPath) {
|
|
136
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
137
|
+
res.end(JSON.stringify({ error: 'Path is required' }));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const resolved = resolvePath(newPath);
|
|
142
|
+
if (!existsSync(resolved)) {
|
|
143
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
144
|
+
res.end(JSON.stringify({ error: 'Path does not exist' }));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Track the path and return success
|
|
149
|
+
trackRecentPath(resolved);
|
|
150
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
151
|
+
res.end(JSON.stringify({
|
|
152
|
+
success: true,
|
|
153
|
+
path: resolved,
|
|
154
|
+
recentPaths: getRecentPaths()
|
|
155
|
+
}));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// API: Health check (for ccw view to detect running server)
|
|
160
|
+
if (pathname === '/api/health') {
|
|
161
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
162
|
+
res.end(JSON.stringify({ status: 'ok', timestamp: Date.now() }));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// API: Shutdown server (for ccw stop command)
|
|
167
|
+
if (pathname === '/api/shutdown' && req.method === 'POST') {
|
|
168
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
169
|
+
res.end(JSON.stringify({ status: 'shutting_down' }));
|
|
170
|
+
|
|
171
|
+
// Graceful shutdown
|
|
172
|
+
console.log('\n Received shutdown signal...');
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
server.close(() => {
|
|
175
|
+
console.log(' Server stopped.\n');
|
|
176
|
+
process.exit(0);
|
|
177
|
+
});
|
|
178
|
+
// Force exit after 3 seconds if graceful shutdown fails
|
|
179
|
+
setTimeout(() => process.exit(0), 3000);
|
|
180
|
+
}, 100);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
129
184
|
// API: Remove a recent path
|
|
130
185
|
if (pathname === '/api/remove-recent-path' && req.method === 'POST') {
|
|
131
186
|
handlePostRequest(req, res, async (body) => {
|
|
@@ -972,22 +1027,82 @@ async function loadRecentPaths() {
|
|
|
972
1027
|
// ========================================
|
|
973
1028
|
|
|
974
1029
|
/**
|
|
975
|
-
*
|
|
1030
|
+
* Safely read and parse JSON file
|
|
1031
|
+
* @param {string} filePath
|
|
1032
|
+
* @returns {Object|null}
|
|
1033
|
+
*/
|
|
1034
|
+
function safeReadJson(filePath) {
|
|
1035
|
+
try {
|
|
1036
|
+
if (!existsSync(filePath)) return null;
|
|
1037
|
+
const content = readFileSync(filePath, 'utf8');
|
|
1038
|
+
return JSON.parse(content);
|
|
1039
|
+
} catch {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Get MCP servers from a settings file
|
|
1046
|
+
* @param {string} filePath
|
|
1047
|
+
* @returns {Object} mcpServers object or empty object
|
|
1048
|
+
*/
|
|
1049
|
+
function getMcpServersFromSettings(filePath) {
|
|
1050
|
+
const config = safeReadJson(filePath);
|
|
1051
|
+
if (!config) return {};
|
|
1052
|
+
return config.mcpServers || {};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Get MCP configuration from multiple sources:
|
|
1057
|
+
* 1. ~/.claude.json (project-level MCP servers)
|
|
1058
|
+
* 2. ~/.claude/settings.json and settings.local.json (global MCP servers)
|
|
1059
|
+
* 3. Each workspace's .claude/settings.json and settings.local.json
|
|
976
1060
|
* @returns {Object}
|
|
977
1061
|
*/
|
|
978
1062
|
function getMcpConfig() {
|
|
979
1063
|
try {
|
|
980
|
-
|
|
981
|
-
|
|
1064
|
+
const result = { projects: {}, globalServers: {} };
|
|
1065
|
+
|
|
1066
|
+
// 1. Read from ~/.claude.json (primary source for project MCP)
|
|
1067
|
+
if (existsSync(CLAUDE_CONFIG_PATH)) {
|
|
1068
|
+
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
|
1069
|
+
const config = JSON.parse(content);
|
|
1070
|
+
result.projects = config.projects || {};
|
|
982
1071
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
};
|
|
1072
|
+
|
|
1073
|
+
// 2. Read global MCP servers from ~/.claude/settings.json and settings.local.json
|
|
1074
|
+
const globalSettings = getMcpServersFromSettings(CLAUDE_GLOBAL_SETTINGS);
|
|
1075
|
+
const globalSettingsLocal = getMcpServersFromSettings(CLAUDE_GLOBAL_SETTINGS_LOCAL);
|
|
1076
|
+
result.globalServers = { ...globalSettings, ...globalSettingsLocal };
|
|
1077
|
+
|
|
1078
|
+
// 3. For each project, also check .claude/settings.json and settings.local.json
|
|
1079
|
+
for (const projectPath of Object.keys(result.projects)) {
|
|
1080
|
+
const projectClaudeDir = join(projectPath, '.claude');
|
|
1081
|
+
const projectSettings = join(projectClaudeDir, 'settings.json');
|
|
1082
|
+
const projectSettingsLocal = join(projectClaudeDir, 'settings.local.json');
|
|
1083
|
+
|
|
1084
|
+
// Merge MCP servers from workspace settings into project config
|
|
1085
|
+
const workspaceServers = {
|
|
1086
|
+
...getMcpServersFromSettings(projectSettings),
|
|
1087
|
+
...getMcpServersFromSettings(projectSettingsLocal)
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
if (Object.keys(workspaceServers).length > 0) {
|
|
1091
|
+
// Merge workspace servers with existing project servers (workspace takes precedence)
|
|
1092
|
+
result.projects[projectPath] = {
|
|
1093
|
+
...result.projects[projectPath],
|
|
1094
|
+
mcpServers: {
|
|
1095
|
+
...(result.projects[projectPath]?.mcpServers || {}),
|
|
1096
|
+
...workspaceServers
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return result;
|
|
988
1103
|
} catch (error) {
|
|
989
1104
|
console.error('Error reading MCP config:', error);
|
|
990
|
-
return { projects: {}, error: error.message };
|
|
1105
|
+
return { projects: {}, globalServers: {}, error: error.message };
|
|
991
1106
|
}
|
|
992
1107
|
}
|
|
993
1108
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// ========== MCP State ==========
|
|
5
5
|
let mcpConfig = null;
|
|
6
6
|
let mcpAllProjects = {};
|
|
7
|
+
let mcpGlobalServers = {};
|
|
7
8
|
let mcpCurrentProjectServers = {};
|
|
8
9
|
let mcpCreateMode = 'form'; // 'form' or 'json'
|
|
9
10
|
|
|
@@ -31,6 +32,7 @@ async function loadMcpConfig() {
|
|
|
31
32
|
const data = await response.json();
|
|
32
33
|
mcpConfig = data;
|
|
33
34
|
mcpAllProjects = data.projects || {};
|
|
35
|
+
mcpGlobalServers = data.globalServers || {};
|
|
34
36
|
|
|
35
37
|
// Get current project servers
|
|
36
38
|
const currentPath = projectPath.replace(/\//g, '\\');
|
|
@@ -150,6 +152,15 @@ function updateMcpBadge() {
|
|
|
150
152
|
function getAllAvailableMcpServers() {
|
|
151
153
|
const allServers = {};
|
|
152
154
|
|
|
155
|
+
// Collect global servers first
|
|
156
|
+
for (const [name, serverConfig] of Object.entries(mcpGlobalServers)) {
|
|
157
|
+
allServers[name] = {
|
|
158
|
+
config: serverConfig,
|
|
159
|
+
usedIn: [],
|
|
160
|
+
isGlobal: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
153
164
|
// Collect servers from all projects
|
|
154
165
|
for (const [path, config] of Object.entries(mcpAllProjects)) {
|
|
155
166
|
const servers = config.mcpServers || {};
|
|
@@ -157,7 +168,8 @@ function getAllAvailableMcpServers() {
|
|
|
157
168
|
if (!allServers[name]) {
|
|
158
169
|
allServers[name] = {
|
|
159
170
|
config: serverConfig,
|
|
160
|
-
usedIn: []
|
|
171
|
+
usedIn: [],
|
|
172
|
+
isGlobal: false
|
|
161
173
|
};
|
|
162
174
|
}
|
|
163
175
|
allServers[name].usedIn.push(path);
|
|
@@ -20,7 +20,17 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
20
20
|
// Server mode: load data from API
|
|
21
21
|
try {
|
|
22
22
|
if (window.SERVER_MODE) {
|
|
23
|
-
|
|
23
|
+
// Check URL for path parameter (from ccw view command)
|
|
24
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
25
|
+
const urlPath = urlParams.get('path');
|
|
26
|
+
const initialPath = urlPath || window.INITIAL_PATH || projectPath;
|
|
27
|
+
|
|
28
|
+
await switchToPath(initialPath);
|
|
29
|
+
|
|
30
|
+
// Clean up URL after loading (remove query param)
|
|
31
|
+
if (urlPath && window.history.replaceState) {
|
|
32
|
+
window.history.replaceState({}, '', window.location.pathname);
|
|
33
|
+
}
|
|
24
34
|
} else {
|
|
25
35
|
renderDashboard();
|
|
26
36
|
}
|
|
@@ -26,8 +26,12 @@ async function renderMcpManager() {
|
|
|
26
26
|
|
|
27
27
|
// Separate current project servers and available servers
|
|
28
28
|
const currentProjectServerNames = Object.keys(projectServers);
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
// Separate global servers and project servers that are not in current project
|
|
31
|
+
const globalServerEntries = Object.entries(mcpGlobalServers)
|
|
30
32
|
.filter(([name]) => !currentProjectServerNames.includes(name));
|
|
33
|
+
const otherProjectServers = Object.entries(allAvailableServers)
|
|
34
|
+
.filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal);
|
|
31
35
|
|
|
32
36
|
container.innerHTML = `
|
|
33
37
|
<div class="mcp-manager">
|
|
@@ -61,20 +65,39 @@ async function renderMcpManager() {
|
|
|
61
65
|
`}
|
|
62
66
|
</div>
|
|
63
67
|
|
|
68
|
+
<!-- Global MCP Servers -->
|
|
69
|
+
${globalServerEntries.length > 0 ? `
|
|
70
|
+
<div class="mcp-section mb-6">
|
|
71
|
+
<div class="flex items-center justify-between mb-4">
|
|
72
|
+
<div class="flex items-center gap-2">
|
|
73
|
+
<span class="text-lg">🌐</span>
|
|
74
|
+
<h3 class="text-lg font-semibold text-foreground">Global MCP Servers</h3>
|
|
75
|
+
</div>
|
|
76
|
+
<span class="text-sm text-muted-foreground">${globalServerEntries.length} servers from ~/.claude/settings</span>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="mcp-server-grid grid gap-3">
|
|
80
|
+
${globalServerEntries.map(([serverName, serverConfig]) => {
|
|
81
|
+
return renderGlobalServerCard(serverName, serverConfig);
|
|
82
|
+
}).join('')}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
` : ''}
|
|
86
|
+
|
|
64
87
|
<!-- Available MCP Servers from Other Projects -->
|
|
65
88
|
<div class="mcp-section">
|
|
66
89
|
<div class="flex items-center justify-between mb-4">
|
|
67
90
|
<h3 class="text-lg font-semibold text-foreground">Available from Other Projects</h3>
|
|
68
|
-
<span class="text-sm text-muted-foreground">${
|
|
91
|
+
<span class="text-sm text-muted-foreground">${otherProjectServers.length} servers available</span>
|
|
69
92
|
</div>
|
|
70
93
|
|
|
71
|
-
${
|
|
94
|
+
${otherProjectServers.length === 0 ? `
|
|
72
95
|
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
|
73
96
|
<p class="text-muted-foreground">No additional MCP servers found in other projects</p>
|
|
74
97
|
</div>
|
|
75
98
|
` : `
|
|
76
99
|
<div class="mcp-server-grid grid gap-3">
|
|
77
|
-
${
|
|
100
|
+
${otherProjectServers.map(([serverName, serverInfo]) => {
|
|
78
101
|
return renderAvailableServerCard(serverName, serverInfo);
|
|
79
102
|
}).join('')}
|
|
80
103
|
</div>
|
|
@@ -240,6 +263,52 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
|
|
240
263
|
`;
|
|
241
264
|
}
|
|
242
265
|
|
|
266
|
+
function renderGlobalServerCard(serverName, serverConfig) {
|
|
267
|
+
const command = serverConfig.command || 'N/A';
|
|
268
|
+
const args = serverConfig.args || [];
|
|
269
|
+
const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0;
|
|
270
|
+
|
|
271
|
+
return `
|
|
272
|
+
<div class="mcp-server-card mcp-server-global bg-card border border-primary/30 rounded-lg p-4 hover:shadow-md transition-all">
|
|
273
|
+
<div class="flex items-start justify-between mb-3">
|
|
274
|
+
<div class="flex items-center gap-2">
|
|
275
|
+
<span class="text-xl">🌐</span>
|
|
276
|
+
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
|
277
|
+
<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">Global</span>
|
|
278
|
+
</div>
|
|
279
|
+
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
|
280
|
+
data-server-name="${escapeHtml(serverName)}"
|
|
281
|
+
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
|
282
|
+
data-action="add">
|
|
283
|
+
Add to Project
|
|
284
|
+
</button>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div class="mcp-server-details text-sm space-y-1">
|
|
288
|
+
<div class="flex items-center gap-2 text-muted-foreground">
|
|
289
|
+
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">cmd</span>
|
|
290
|
+
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
|
291
|
+
</div>
|
|
292
|
+
${args.length > 0 ? `
|
|
293
|
+
<div class="flex items-start gap-2 text-muted-foreground">
|
|
294
|
+
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
|
|
295
|
+
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}</span>
|
|
296
|
+
</div>
|
|
297
|
+
` : ''}
|
|
298
|
+
${hasEnv ? `
|
|
299
|
+
<div class="flex items-center gap-2 text-muted-foreground">
|
|
300
|
+
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
|
|
301
|
+
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
|
|
302
|
+
</div>
|
|
303
|
+
` : ''}
|
|
304
|
+
<div class="flex items-center gap-2 text-muted-foreground mt-1">
|
|
305
|
+
<span class="text-xs italic">Available to all projects from ~/.claude/settings</span>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
|
|
243
312
|
function attachMcpEventListeners() {
|
|
244
313
|
// Toggle switches
|
|
245
314
|
document.querySelectorAll('.mcp-server-card input[data-action="toggle"]').forEach(input => {
|
|
@@ -7929,3 +7929,259 @@ code.ctx-meta-chip-value {
|
|
|
7929
7929
|
width: 16px;
|
|
7930
7930
|
border-radius: 3px;
|
|
7931
7931
|
}
|
|
7932
|
+
|
|
7933
|
+
/* ===================================
|
|
7934
|
+
Path Selection Modal
|
|
7935
|
+
=================================== */
|
|
7936
|
+
|
|
7937
|
+
.path-modal-overlay {
|
|
7938
|
+
position: fixed;
|
|
7939
|
+
top: 0;
|
|
7940
|
+
left: 0;
|
|
7941
|
+
right: 0;
|
|
7942
|
+
bottom: 0;
|
|
7943
|
+
background: rgba(0, 0, 0, 0.5);
|
|
7944
|
+
display: flex;
|
|
7945
|
+
align-items: center;
|
|
7946
|
+
justify-content: center;
|
|
7947
|
+
z-index: 1000;
|
|
7948
|
+
backdrop-filter: blur(2px);
|
|
7949
|
+
}
|
|
7950
|
+
|
|
7951
|
+
.path-modal {
|
|
7952
|
+
background: hsl(var(--card));
|
|
7953
|
+
border: 1px solid hsl(var(--border));
|
|
7954
|
+
border-radius: 0.75rem;
|
|
7955
|
+
width: 90%;
|
|
7956
|
+
max-width: 480px;
|
|
7957
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
|
7958
|
+
animation: modal-enter 0.2s ease-out;
|
|
7959
|
+
}
|
|
7960
|
+
|
|
7961
|
+
@keyframes modal-enter {
|
|
7962
|
+
from {
|
|
7963
|
+
opacity: 0;
|
|
7964
|
+
transform: scale(0.95) translateY(-10px);
|
|
7965
|
+
}
|
|
7966
|
+
to {
|
|
7967
|
+
opacity: 1;
|
|
7968
|
+
transform: scale(1) translateY(0);
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7971
|
+
|
|
7972
|
+
.path-modal-header {
|
|
7973
|
+
display: flex;
|
|
7974
|
+
align-items: center;
|
|
7975
|
+
gap: 0.75rem;
|
|
7976
|
+
padding: 1.25rem 1.5rem;
|
|
7977
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
7978
|
+
}
|
|
7979
|
+
|
|
7980
|
+
.path-modal-icon {
|
|
7981
|
+
font-size: 1.5rem;
|
|
7982
|
+
color: hsl(var(--primary));
|
|
7983
|
+
}
|
|
7984
|
+
|
|
7985
|
+
.path-modal-header h3 {
|
|
7986
|
+
margin: 0;
|
|
7987
|
+
font-size: 1.1rem;
|
|
7988
|
+
font-weight: 600;
|
|
7989
|
+
color: hsl(var(--foreground));
|
|
7990
|
+
}
|
|
7991
|
+
|
|
7992
|
+
.path-modal-body {
|
|
7993
|
+
padding: 1.5rem;
|
|
7994
|
+
}
|
|
7995
|
+
|
|
7996
|
+
.path-modal-body p {
|
|
7997
|
+
margin: 0 0 1rem;
|
|
7998
|
+
color: hsl(var(--muted-foreground));
|
|
7999
|
+
font-size: 0.9rem;
|
|
8000
|
+
line-height: 1.5;
|
|
8001
|
+
}
|
|
8002
|
+
|
|
8003
|
+
.path-modal-command {
|
|
8004
|
+
display: flex;
|
|
8005
|
+
align-items: center;
|
|
8006
|
+
gap: 0.75rem;
|
|
8007
|
+
background: hsl(var(--muted));
|
|
8008
|
+
padding: 0.75rem 1rem;
|
|
8009
|
+
border-radius: 0.5rem;
|
|
8010
|
+
font-family: var(--font-mono);
|
|
8011
|
+
}
|
|
8012
|
+
|
|
8013
|
+
.path-modal-command code {
|
|
8014
|
+
flex: 1;
|
|
8015
|
+
font-size: 0.85rem;
|
|
8016
|
+
color: hsl(var(--foreground));
|
|
8017
|
+
word-break: break-all;
|
|
8018
|
+
}
|
|
8019
|
+
|
|
8020
|
+
.path-modal-command .copy-btn {
|
|
8021
|
+
display: flex;
|
|
8022
|
+
align-items: center;
|
|
8023
|
+
gap: 0.375rem;
|
|
8024
|
+
padding: 0.375rem 0.75rem;
|
|
8025
|
+
background: hsl(var(--primary));
|
|
8026
|
+
color: white;
|
|
8027
|
+
border: none;
|
|
8028
|
+
border-radius: 0.375rem;
|
|
8029
|
+
font-size: 0.8rem;
|
|
8030
|
+
cursor: pointer;
|
|
8031
|
+
transition: all 0.15s;
|
|
8032
|
+
white-space: nowrap;
|
|
8033
|
+
}
|
|
8034
|
+
|
|
8035
|
+
.path-modal-command .copy-btn:hover {
|
|
8036
|
+
background: hsl(var(--primary) / 0.9);
|
|
8037
|
+
}
|
|
8038
|
+
|
|
8039
|
+
.path-modal-note {
|
|
8040
|
+
font-size: 0.85rem !important;
|
|
8041
|
+
color: hsl(var(--muted-foreground)) !important;
|
|
8042
|
+
}
|
|
8043
|
+
|
|
8044
|
+
.path-modal-note code {
|
|
8045
|
+
background: hsl(var(--muted));
|
|
8046
|
+
padding: 0.125rem 0.375rem;
|
|
8047
|
+
border-radius: 0.25rem;
|
|
8048
|
+
font-size: 0.8rem;
|
|
8049
|
+
}
|
|
8050
|
+
|
|
8051
|
+
.path-modal-input {
|
|
8052
|
+
width: 100%;
|
|
8053
|
+
padding: 0.75rem 1rem;
|
|
8054
|
+
background: hsl(var(--background));
|
|
8055
|
+
border: 1px solid hsl(var(--border));
|
|
8056
|
+
border-radius: 0.5rem;
|
|
8057
|
+
font-size: 0.9rem;
|
|
8058
|
+
color: hsl(var(--foreground));
|
|
8059
|
+
outline: none;
|
|
8060
|
+
transition: border-color 0.15s;
|
|
8061
|
+
}
|
|
8062
|
+
|
|
8063
|
+
.path-modal-input:focus {
|
|
8064
|
+
border-color: hsl(var(--primary));
|
|
8065
|
+
}
|
|
8066
|
+
|
|
8067
|
+
.path-modal-input::placeholder {
|
|
8068
|
+
color: hsl(var(--muted-foreground));
|
|
8069
|
+
}
|
|
8070
|
+
|
|
8071
|
+
.path-modal-footer {
|
|
8072
|
+
display: flex;
|
|
8073
|
+
justify-content: flex-end;
|
|
8074
|
+
gap: 0.75rem;
|
|
8075
|
+
padding: 1rem 1.5rem;
|
|
8076
|
+
border-top: 1px solid hsl(var(--border));
|
|
8077
|
+
background: hsl(var(--muted) / 0.3);
|
|
8078
|
+
border-radius: 0 0 0.75rem 0.75rem;
|
|
8079
|
+
}
|
|
8080
|
+
|
|
8081
|
+
.path-modal-close {
|
|
8082
|
+
padding: 0.5rem 1.25rem;
|
|
8083
|
+
background: hsl(var(--muted));
|
|
8084
|
+
color: hsl(var(--foreground));
|
|
8085
|
+
border: none;
|
|
8086
|
+
border-radius: 0.375rem;
|
|
8087
|
+
font-size: 0.875rem;
|
|
8088
|
+
cursor: pointer;
|
|
8089
|
+
transition: all 0.15s;
|
|
8090
|
+
}
|
|
8091
|
+
|
|
8092
|
+
.path-modal-close:hover {
|
|
8093
|
+
background: hsl(var(--hover));
|
|
8094
|
+
}
|
|
8095
|
+
|
|
8096
|
+
.path-modal-confirm {
|
|
8097
|
+
padding: 0.5rem 1.25rem;
|
|
8098
|
+
background: hsl(var(--primary));
|
|
8099
|
+
color: white;
|
|
8100
|
+
border: none;
|
|
8101
|
+
border-radius: 0.375rem;
|
|
8102
|
+
font-size: 0.875rem;
|
|
8103
|
+
cursor: pointer;
|
|
8104
|
+
transition: all 0.15s;
|
|
8105
|
+
}
|
|
8106
|
+
|
|
8107
|
+
.path-modal-confirm:hover {
|
|
8108
|
+
background: hsl(var(--primary) / 0.9);
|
|
8109
|
+
}
|
|
8110
|
+
|
|
8111
|
+
.path-modal-confirm:disabled {
|
|
8112
|
+
opacity: 0.5;
|
|
8113
|
+
cursor: not-allowed;
|
|
8114
|
+
}
|
|
8115
|
+
|
|
8116
|
+
/* Path Input Group */
|
|
8117
|
+
.path-input-group {
|
|
8118
|
+
display: flex;
|
|
8119
|
+
align-items: center;
|
|
8120
|
+
gap: 0.75rem;
|
|
8121
|
+
flex-wrap: wrap;
|
|
8122
|
+
}
|
|
8123
|
+
|
|
8124
|
+
.path-input-group label {
|
|
8125
|
+
font-size: 0.875rem;
|
|
8126
|
+
color: hsl(var(--muted-foreground));
|
|
8127
|
+
white-space: nowrap;
|
|
8128
|
+
}
|
|
8129
|
+
|
|
8130
|
+
.path-input-group input {
|
|
8131
|
+
flex: 1;
|
|
8132
|
+
min-width: 200px;
|
|
8133
|
+
padding: 0.625rem 0.875rem;
|
|
8134
|
+
background: hsl(var(--background));
|
|
8135
|
+
border: 1px solid hsl(var(--border));
|
|
8136
|
+
border-radius: 0.375rem;
|
|
8137
|
+
font-size: 0.875rem;
|
|
8138
|
+
font-family: var(--font-mono);
|
|
8139
|
+
color: hsl(var(--foreground));
|
|
8140
|
+
outline: none;
|
|
8141
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
8142
|
+
}
|
|
8143
|
+
|
|
8144
|
+
.path-input-group input:focus {
|
|
8145
|
+
border-color: hsl(var(--primary));
|
|
8146
|
+
box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1);
|
|
8147
|
+
}
|
|
8148
|
+
|
|
8149
|
+
.path-input-group input::placeholder {
|
|
8150
|
+
color: hsl(var(--muted-foreground));
|
|
8151
|
+
}
|
|
8152
|
+
|
|
8153
|
+
.path-go-btn {
|
|
8154
|
+
padding: 0.625rem 1.25rem;
|
|
8155
|
+
background: hsl(var(--primary));
|
|
8156
|
+
color: white;
|
|
8157
|
+
border: none;
|
|
8158
|
+
border-radius: 0.375rem;
|
|
8159
|
+
font-size: 0.875rem;
|
|
8160
|
+
font-weight: 500;
|
|
8161
|
+
cursor: pointer;
|
|
8162
|
+
transition: all 0.15s;
|
|
8163
|
+
white-space: nowrap;
|
|
8164
|
+
}
|
|
8165
|
+
|
|
8166
|
+
.path-go-btn:hover {
|
|
8167
|
+
background: hsl(var(--primary) / 0.9);
|
|
8168
|
+
transform: translateY(-1px);
|
|
8169
|
+
}
|
|
8170
|
+
|
|
8171
|
+
.path-go-btn:active {
|
|
8172
|
+
transform: translateY(0);
|
|
8173
|
+
}
|
|
8174
|
+
|
|
8175
|
+
/* Selected Folder Display */
|
|
8176
|
+
.selected-folder {
|
|
8177
|
+
padding: 0.75rem 1rem;
|
|
8178
|
+
background: hsl(var(--muted));
|
|
8179
|
+
border-radius: 0.5rem;
|
|
8180
|
+
margin-bottom: 0.75rem;
|
|
8181
|
+
}
|
|
8182
|
+
|
|
8183
|
+
.selected-folder strong {
|
|
8184
|
+
font-size: 1rem;
|
|
8185
|
+
color: hsl(var(--foreground));
|
|
8186
|
+
font-family: var(--font-mono);
|
|
8187
|
+
}
|
|
@@ -3,13 +3,24 @@ import { platform } from 'os';
|
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Launch a file in the default browser
|
|
6
|
+
* Launch a URL or file in the default browser
|
|
7
7
|
* Cross-platform compatible (Windows/macOS/Linux)
|
|
8
|
-
* @param {string}
|
|
8
|
+
* @param {string} urlOrPath - HTTP URL or path to HTML file
|
|
9
9
|
* @returns {Promise<void>}
|
|
10
10
|
*/
|
|
11
|
-
export async function launchBrowser(
|
|
12
|
-
|
|
11
|
+
export async function launchBrowser(urlOrPath) {
|
|
12
|
+
// Check if it's already a URL (http:// or https://)
|
|
13
|
+
if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) {
|
|
14
|
+
try {
|
|
15
|
+
await open(urlOrPath);
|
|
16
|
+
return;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error(`Failed to open browser: ${error.message}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// It's a file path - convert to file:// URL
|
|
23
|
+
const absolutePath = resolve(urlOrPath);
|
|
13
24
|
|
|
14
25
|
// Construct file:// URL based on platform
|
|
15
26
|
let url;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-workflow",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.5",
|
|
4
4
|
"description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "ccw/src/index.js",
|