Skip to content

Commit 87aac2c

Browse files
author
catlog22
committed
feat: update CLI entrypoints and improve session management with optional dependencies
1 parent dc390cc commit 87aac2c

File tree

12 files changed

+359
-92
lines changed

12 files changed

+359
-92
lines changed

assets/wechat-group-qr.png

53.2 KB
Loading

bin/ccw-mcp.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Root package MCP server entrypoint for npm global installs.
4+
* Avoids coupling the npm shim to nested internal bin paths.
5+
*/
6+
7+
const toStderr = (...args) => console.error(...args);
8+
console.log = toStderr;
9+
console.info = toStderr;
10+
console.debug = toStderr;
11+
console.dir = toStderr;
12+
13+
try {
14+
await import('../ccw/dist/mcp-server/index.js');
15+
} catch (err) {
16+
console.error('[ccw-mcp] Failed to start MCP server:', err);
17+
process.exit(1);
18+
}

bin/ccw.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Root package CLI entrypoint for npm global installs.
5+
* Keeps npm-generated shims stable even if internal package layout changes.
6+
*/
7+
8+
import { run } from '../ccw/dist/cli.js';
9+
10+
await run(process.argv);

ccw/bin/ccw.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77

88
import { run } from '../dist/cli.js';
99

10-
run(process.argv);
10+
await run(process.argv);

ccw/src/cli.ts

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,4 @@
11
import { Command } from 'commander';
2-
import { viewCommand } from './commands/view.js';
3-
import { serveCommand } from './commands/serve.js';
4-
import { stopCommand } from './commands/stop.js';
5-
import { installCommand, installSkillHubCommand } from './commands/install.js';
6-
import { uninstallCommand } from './commands/uninstall.js';
7-
import { upgradeCommand } from './commands/upgrade.js';
8-
import { listCommand } from './commands/list.js';
9-
import { toolCommand } from './commands/tool.js';
10-
import { sessionCommand } from './commands/session.js';
11-
import { cliCommand } from './commands/cli.js';
12-
import { memoryCommand } from './commands/memory.js';
13-
import { coreMemoryCommand } from './commands/core-memory.js';
14-
import { hookCommand } from './commands/hook.js';
15-
import { specCommand } from './commands/spec.js';
16-
import { issueCommand } from './commands/issue.js';
17-
import { workflowCommand } from './commands/workflow.js';
18-
import { loopCommand } from './commands/loop.js';
19-
import { teamCommand } from './commands/team.js';
20-
import { launcherCommand } from './commands/launcher.js';
21-
import { chainLoaderCommand } from './commands/chain-loader.js';
222
import { readFileSync, existsSync } from 'fs';
233
import { fileURLToPath } from 'url';
244
import { dirname, join } from 'path';
@@ -75,7 +55,22 @@ function loadPackageInfo(): PackageInfo {
7555

7656
const pkg = loadPackageInfo();
7757

78-
export function run(argv: string[]): void {
58+
type CommandHandler = (...args: any[]) => void | Promise<void>;
59+
60+
async function invokeNamedExport<TModule extends Record<string, unknown>>(
61+
loader: () => Promise<TModule>,
62+
exportName: string,
63+
...args: unknown[]
64+
): Promise<void> {
65+
const mod = await loader();
66+
const handler = mod[exportName];
67+
if (typeof handler !== 'function') {
68+
throw new Error(`CCW CLI bootstrap error: ${exportName} is not exported by the command module.`);
69+
}
70+
await (handler as CommandHandler)(...args);
71+
}
72+
73+
export async function run(argv: string[]): Promise<void> {
7974
const program = new Command();
8075

8176
program
@@ -91,7 +86,7 @@ export function run(argv: string[]): void {
9186
.option('--port <port>', 'Server port', '3456')
9287
.option('--host <host>', 'Server host to bind', '127.0.0.1')
9388
.option('--no-browser', 'Start server without opening browser')
94-
.action(viewCommand);
89+
.action(async (options) => invokeNamedExport(() => import('./commands/view.js'), 'viewCommand', options));
9590

9691
// Serve command (alias for view)
9792
program
@@ -101,15 +96,15 @@ export function run(argv: string[]): void {
10196
.option('--port <port>', 'Server port', '3456')
10297
.option('--host <host>', 'Server host to bind', '127.0.0.1')
10398
.option('--no-browser', 'Start server without opening browser')
104-
.action(serveCommand);
99+
.action(async (options) => invokeNamedExport(() => import('./commands/serve.js'), 'serveCommand', options));
105100

106101
// Stop command
107102
program
108103
.command('stop')
109104
.description('Stop the running CCW dashboard server')
110105
.option('--port <port>', 'Server port', '3456')
111106
.option('-f, --force', 'Force kill process on the port')
112-
.action(stopCommand);
107+
.action(async (options) => invokeNamedExport(() => import('./commands/stop.js'), 'stopCommand', options));
113108

114109
// Install command
115110
program
@@ -121,37 +116,38 @@ export function run(argv: string[]): void {
121116
.option('--skill-hub [skillId]', 'Install skill from skill-hub (use --list to see available)')
122117
.option('--cli <type>', 'Target CLI for skill installation (claude or codex)', 'claude')
123118
.option('--list', 'List available skills in skill-hub')
124-
.action((options) => {
119+
.action(async (options) => {
120+
const installModule = await import('./commands/install.js');
125121
// If skill-hub option is used, route to skill hub command
126122
if (options.skillHub !== undefined || options.list) {
127-
return installSkillHubCommand({
123+
return installModule.installSkillHubCommand({
128124
skillId: typeof options.skillHub === 'string' ? options.skillHub : undefined,
129125
cliType: options.cli,
130126
list: options.list,
131127
});
132128
}
133129
// Otherwise use normal install
134-
return installCommand(options);
130+
return installModule.installCommand(options);
135131
});
136132

137133
// Uninstall command
138134
program
139135
.command('uninstall')
140136
.description('Uninstall Claude Code Workflow')
141-
.action(uninstallCommand);
137+
.action(async () => invokeNamedExport(() => import('./commands/uninstall.js'), 'uninstallCommand'));
142138

143139
// Upgrade command
144140
program
145141
.command('upgrade')
146142
.description('Upgrade Claude Code Workflow installations')
147143
.option('-a, --all', 'Upgrade all installations without prompting')
148-
.action(upgradeCommand);
144+
.action(async (options) => invokeNamedExport(() => import('./commands/upgrade.js'), 'upgradeCommand', options));
149145

150146
// List command
151147
program
152148
.command('list')
153149
.description('List all installed Claude Code Workflow instances')
154-
.action(listCommand);
150+
.action(async () => invokeNamedExport(() => import('./commands/list.js'), 'listCommand'));
155151

156152
// Tool command
157153
program
@@ -166,7 +162,7 @@ export function run(argv: string[]): void {
166162
.option('--file <file>', 'File path for symbol extraction (for codex_lens)')
167163
.option('--files <files>', 'Comma-separated file paths (for codex_lens update)')
168164
.option('--languages <langs>', 'Comma-separated languages (for codex_lens init)')
169-
.action((subcommand, args, options) => toolCommand(subcommand, args, options));
165+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/tool.js'), 'toolCommand', subcommand, args, options));
170166

171167
// Session command
172168
program
@@ -183,7 +179,7 @@ export function run(argv: string[]): void {
183179
.option('--raw', 'Output raw content only')
184180
.option('--no-metadata', 'Exclude metadata from list')
185181
.option('--no-update-status', 'Skip status update on archive')
186-
.action((subcommand, args, options) => sessionCommand(subcommand, args, options));
182+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/session.js'), 'sessionCommand', subcommand, args, options));
187183

188184
// CLI command
189185
program
@@ -233,7 +229,7 @@ export function run(argv: string[]): void {
233229
.option('--timeout <seconds>', 'Timeout for watch command')
234230
.option('--all', 'Show all executions in show command')
235231
.option('--to-file <path>', 'Save output to file')
236-
.action((subcommand, args, options) => cliCommand(subcommand, args, options));
232+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/cli.js'), 'cliCommand', subcommand, args, options));
237233

238234
// Memory command
239235
program
@@ -262,7 +258,7 @@ export function run(argv: string[]): void {
262258
.option('--path <path>', 'Project path (pipeline commands)')
263259
.option('--max-sessions <n>', 'Max sessions to extract (extract)')
264260
.option('--session-ids <ids>', 'Comma-separated session IDs (extract)')
265-
.action((subcommand, args, options) => memoryCommand(subcommand, args, options));
261+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/memory.js'), 'memoryCommand', subcommand, args, options));
266262

267263
// Core Memory command
268264
program
@@ -294,7 +290,7 @@ export function run(argv: string[]): void {
294290
.option('--topK <n>', 'Max results for unified search', '20')
295291
.option('--minScore <n>', 'Min relevance score for unified search', '0')
296292
.option('--category <cat>', 'Filter by category for unified search')
297-
.action((subcommand, args, options) => coreMemoryCommand(subcommand, args, options));
293+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/core-memory.js'), 'coreMemoryCommand', subcommand, args, options));
298294

299295
// Hook command - CLI endpoint for Claude Code hooks
300296
program
@@ -306,7 +302,7 @@ export function run(argv: string[]): void {
306302
.option('--type <type>', 'Context type: session-start, context')
307303
.option('--path <path>', 'File or project path')
308304
.option('--limit <n>', 'Max entries to return (for project-state)')
309-
.action((subcommand, args, options) => hookCommand(subcommand, args, options));
305+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/hook.js'), 'hookCommand', subcommand, args, options));
310306

311307
// Spec command - Project spec management (load/list/rebuild/status/init)
312308
program
@@ -317,7 +313,7 @@ export function run(argv: string[]): void {
317313
.option('--keywords <text>', 'Keywords for spec matching (CLI mode)')
318314
.option('--stdin', 'Read input from stdin (Hook mode)')
319315
.option('--json', 'Output as JSON')
320-
.action((subcommand, args, options) => specCommand(subcommand, args, options));
316+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/spec.js'), 'specCommand', subcommand, args, options));
321317

322318
// Issue command - Issue lifecycle management with JSONL task tracking
323319
program
@@ -350,14 +346,14 @@ export function run(argv: string[]): void {
350346
.option('--state <state>', 'GitHub issue state: open, closed, or all')
351347
.option('--limit <n>', 'Maximum number of issues to pull from GitHub')
352348
.option('--labels <labels>', 'Filter by GitHub labels (comma-separated)')
353-
.action((subcommand, args, options) => issueCommand(subcommand, args, options));
349+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/issue.js'), 'issueCommand', subcommand, args, options));
354350

355351
// Loop command - Loop management for multi-CLI orchestration
356352
program
357353
.command('loop [subcommand] [args...]')
358354
.description('Loop management for automated multi-CLI execution')
359355
.option('--session <name>', 'Specify workflow session')
360-
.action((subcommand, args, options) => loopCommand(subcommand, args, options));
356+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/loop.js'), 'loopCommand', subcommand, args, options));
361357

362358
// Team command - Team Message Bus CLI interface
363359
program
@@ -373,15 +369,15 @@ export function run(argv: string[]): void {
373369
.option('--last <n>', 'Last N messages (for list)')
374370
.option('--role <role>', 'Role name (for get_state)')
375371
.option('--json', 'Output as JSON')
376-
.action((subcommand, args, options) => teamCommand(subcommand, args, options));
372+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/team.js'), 'teamCommand', subcommand, args, options));
377373

378374
// Workflow command - Workflow installation and management
379375
program
380376
.command('workflow [subcommand] [args...]')
381377
.description('Workflow installation and management (install, list, sync)')
382378
.option('-f, --force', 'Force installation without prompts')
383379
.option('--source <source>', 'Install specific source only')
384-
.action((subcommand, args, options) => workflowCommand(subcommand, args, options));
380+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/workflow.js'), 'workflowCommand', subcommand, args, options));
385381

386382
// Launcher command - unified Claude Code launcher with workflow switching
387383
program
@@ -391,15 +387,15 @@ export function run(argv: string[]): void {
391387
.option('-s, --settings <name>', 'Settings profile to use')
392388
.option('--claude-md <path>', 'Path to CLAUDE.md (for add-workflow)')
393389
.option('--cli-tools <path>', 'Path to cli-tools.json (for add-workflow)')
394-
.action((subcommand, args, options) => launcherCommand(subcommand, args, options));
390+
.action(async (subcommand, args, options) => invokeNamedExport(() => import('./commands/launcher.js'), 'launcherCommand', subcommand, args, options));
395391

396392
// Chain-loader command - progressive skill chain loader
397393
program
398394
.command('chain-loader [json]')
399395
.description('Progressive skill chain loader (JSON params: cmd, skill, chain, session_id, choice, node, entry_name)')
400-
.action((json) => chainLoaderCommand(json));
396+
.action(async (json) => invokeNamedExport(() => import('./commands/chain-loader.js'), 'chainLoaderCommand', json));
401397

402-
program.parse(argv);
398+
await program.parseAsync(argv);
403399
}
404400

405401
// Note: run() is called by bin/ccw.js entry point

ccw/src/core/routes/cli-routes.ts

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,18 @@ export function updateActiveExecution(event: {
276276
}
277277

278278
if (type === 'started') {
279+
// If output arrived before started (out-of-order events), we may already have a placeholder.
280+
// Update fields in-place and preserve any buffered output.
281+
const existing = activeExecutions.get(executionId);
282+
if (existing) {
283+
existing.tool = tool || existing.tool || 'unknown';
284+
existing.mode = mode || existing.mode || 'analysis';
285+
if (prompt) {
286+
existing.prompt = (prompt || '').substring(0, 500);
287+
}
288+
return;
289+
}
290+
279291
// Check map size limit before creating new execution
280292
if (activeExecutions.size >= MAX_ACTIVE_EXECUTIONS) {
281293
console.warn(`[ActiveExec] Max executions limit reached (${MAX_ACTIVE_EXECUTIONS}), cleanup may be needed`);
@@ -292,23 +304,62 @@ export function updateActiveExecution(event: {
292304
status: 'running'
293305
});
294306
} else if (type === 'output') {
295-
// Append output to existing execution using array with size limit
296-
const activeExec = activeExecutions.get(executionId);
297-
if (activeExec && output) {
298-
activeExec.output.push(output);
299-
// Keep buffer size under limit by shifting old entries
300-
if (activeExec.output.length > MAX_OUTPUT_BUFFER_LINES) {
301-
activeExec.output.shift(); // Remove oldest entry
307+
// Append output to existing execution using array with size limit.
308+
//
309+
// IMPORTANT: In practice, hook events can arrive out-of-order (or the STARTED event can be dropped).
310+
// If we ignore output before "started", the dashboard will show "session not registered / not started"
311+
// even though streaming output exists. Create a placeholder execution on first output to be robust.
312+
if (!output) {
313+
return;
314+
}
315+
316+
let activeExec = activeExecutions.get(executionId);
317+
if (!activeExec) {
318+
if (activeExecutions.size >= MAX_ACTIVE_EXECUTIONS) {
319+
console.warn(`[ActiveExec] Max executions limit reached (${MAX_ACTIVE_EXECUTIONS}), cleanup may be needed`);
302320
}
321+
322+
activeExec = {
323+
id: executionId,
324+
tool: tool || 'unknown',
325+
mode: mode || 'analysis',
326+
prompt: (prompt || '').substring(0, 500),
327+
startTime: Date.now(),
328+
output: [],
329+
status: 'running'
330+
};
331+
activeExecutions.set(executionId, activeExec);
332+
console.warn(`[ActiveExec] Missing started event; created placeholder for output: ${executionId}`);
333+
}
334+
335+
activeExec.output.push(output);
336+
// Keep buffer size under limit by shifting old entries
337+
if (activeExec.output.length > MAX_OUTPUT_BUFFER_LINES) {
338+
activeExec.output.shift(); // Remove oldest entry
303339
}
304340
} else if (type === 'completed') {
305341
// Mark as completed with timestamp for retention-based cleanup
306-
const activeExec = activeExecutions.get(executionId);
307-
if (activeExec) {
308-
activeExec.status = success ? 'completed' : 'error';
309-
activeExec.completedTimestamp = Date.now();
310-
console.log(`[ActiveExec] Marked as ${activeExec.status}, retained for ${EXECUTION_RETENTION_MS / 1000}s`);
342+
let activeExec = activeExecutions.get(executionId);
343+
if (!activeExec) {
344+
// Completion can also arrive without a STARTED event (dropped/out-of-order).
345+
activeExec = {
346+
id: executionId,
347+
tool: tool || 'unknown',
348+
mode: mode || 'analysis',
349+
prompt: (prompt || '').substring(0, 500),
350+
startTime: Date.now(),
351+
output: [],
352+
status: success ? 'completed' : 'error',
353+
completedTimestamp: Date.now(),
354+
};
355+
activeExecutions.set(executionId, activeExec);
356+
console.warn(`[ActiveExec] Missing started event; created placeholder for completed: ${executionId}`);
357+
return;
311358
}
359+
360+
activeExec.status = success ? 'completed' : 'error';
361+
activeExec.completedTimestamp = Date.now();
362+
console.log(`[ActiveExec] Marked as ${activeExec.status}, retained for ${EXECUTION_RETENTION_MS / 1000}s`);
312363
}
313364
}
314365

0 commit comments

Comments
 (0)