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 CHANGED
@@ -24,15 +24,15 @@
24
24
  "node": ">=16.0.0"
25
25
  },
26
26
  "dependencies": {
27
- "commander": "^11.0.0",
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
- "ora": "^7.0.0",
33
- "figlet": "^1.7.0",
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
+ }
@@ -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
- * View command handler - starts dashboard server (unified with serve mode)
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
- // Forward to serve command with same options
9
- await serveCommand({
10
- path: options.path,
11
- port: options.port || 3456,
12
- browser: options.browser
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
  }
@@ -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 path
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
- * Get MCP configuration from .claude.json
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
- if (!existsSync(CLAUDE_CONFIG_PATH)) {
981
- return { projects: {} };
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
- const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
984
- const config = JSON.parse(content);
985
- return {
986
- projects: config.projects || {}
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
- await switchToPath(window.INITIAL_PATH || projectPath);
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
- const otherAvailableServers = Object.entries(allAvailableServers)
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">${otherAvailableServers.length} servers available</span>
91
+ <span class="text-sm text-muted-foreground">${otherProjectServers.length} servers available</span>
69
92
  </div>
70
93
 
71
- ${otherAvailableServers.length === 0 ? `
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
- ${otherAvailableServers.map(([serverName, serverInfo]) => {
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, "&#39;")}'
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} filePath - Path to HTML file
8
+ * @param {string} urlOrPath - HTTP URL or path to HTML file
9
9
  * @returns {Promise<void>}
10
10
  */
11
- export async function launchBrowser(filePath) {
12
- const absolutePath = resolve(filePath);
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.2",
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",