claude-code-workflow 6.0.4 → 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
+ }
@@ -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();
@@ -160,6 +163,24 @@ export async function startServer(options = {}) {
160
163
  return;
161
164
  }
162
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
+
163
184
  // API: Remove a recent path
164
185
  if (pathname === '/api/remove-recent-path' && req.method === 'POST') {
165
186
  handlePostRequest(req, res, async (body) => {
@@ -1006,22 +1027,82 @@ async function loadRecentPaths() {
1006
1027
  // ========================================
1007
1028
 
1008
1029
  /**
1009
- * 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
1010
1060
  * @returns {Object}
1011
1061
  */
1012
1062
  function getMcpConfig() {
1013
1063
  try {
1014
- if (!existsSync(CLAUDE_CONFIG_PATH)) {
1015
- 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 || {};
1016
1071
  }
1017
- const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
1018
- const config = JSON.parse(content);
1019
- return {
1020
- projects: config.projects || {}
1021
- };
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;
1022
1103
  } catch (error) {
1023
1104
  console.error('Error reading MCP config:', error);
1024
- return { projects: {}, error: error.message };
1105
+ return { projects: {}, globalServers: {}, error: error.message };
1025
1106
  }
1026
1107
  }
1027
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);
@@ -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 => {
@@ -8112,3 +8112,76 @@ code.ctx-meta-chip-value {
8112
8112
  opacity: 0.5;
8113
8113
  cursor: not-allowed;
8114
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.4",
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",