@claude-flow/cli 3.0.0-alpha.7 → 3.0.0-alpha.9

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.
Files changed (38) hide show
  1. package/.agentic-flow/intelligence.json +4 -4
  2. package/.claude-flow/daemon-state.json +42 -35
  3. package/.claude-flow/daemon-test.log +0 -0
  4. package/.claude-flow/daemon.log +0 -0
  5. package/.claude-flow/daemon2.log +0 -0
  6. package/.claude-flow/daemon3.log +0 -0
  7. package/.claude-flow/metrics/codebase-map.json +2 -2
  8. package/.claude-flow/metrics/consolidation.json +1 -1
  9. package/.claude-flow/metrics/performance.json +12 -84
  10. package/.claude-flow/metrics/security-audit.json +1 -1
  11. package/.claude-flow/metrics/task-metrics.json +3 -3
  12. package/.claude-flow/metrics/test-gaps.json +1 -1
  13. package/agents/architect.yaml +1 -1
  14. package/agents/coder.yaml +1 -1
  15. package/agents/reviewer.yaml +1 -1
  16. package/agents/security-architect.yaml +1 -1
  17. package/agents/tester.yaml +1 -1
  18. package/dist/src/commands/daemon.d.ts.map +1 -1
  19. package/dist/src/commands/daemon.js +182 -8
  20. package/dist/src/commands/daemon.js.map +1 -1
  21. package/dist/src/index.d.ts +1 -1
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/dist/src/index.js +21 -0
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/output.d.ts +16 -0
  26. package/dist/src/output.d.ts.map +1 -1
  27. package/dist/src/output.js +42 -0
  28. package/dist/src/output.js.map +1 -1
  29. package/dist/src/services/worker-daemon.d.ts +29 -2
  30. package/dist/src/services/worker-daemon.d.ts.map +1 -1
  31. package/dist/src/services/worker-daemon.js +123 -20
  32. package/dist/src/services/worker-daemon.js.map +1 -1
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +1 -1
  35. package/src/commands/daemon.ts +211 -8
  36. package/src/index.ts +25 -1
  37. package/src/output.ts +47 -1
  38. package/src/services/worker-daemon.ts +153 -21
@@ -6,6 +6,10 @@
6
6
  import type { Command, CommandContext, CommandResult } from '../types.js';
7
7
  import { output } from '../output.js';
8
8
  import { WorkerDaemon, getDaemon, startDaemon, stopDaemon, type WorkerType } from '../services/worker-daemon.js';
9
+ import { spawn } from 'child_process';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname, join } from 'path';
12
+ import * as fs from 'fs';
9
13
 
10
14
  // Start daemon subcommand
11
15
  const startCommand: Command = {
@@ -14,15 +18,37 @@ const startCommand: Command = {
14
18
  options: [
15
19
  { name: 'workers', short: 'w', type: 'string', description: 'Comma-separated list of workers to enable (default: map,audit,optimize,consolidate,testgaps)' },
16
20
  { name: 'quiet', short: 'q', type: 'boolean', description: 'Suppress output' },
21
+ { name: 'background', short: 'b', type: 'boolean', description: 'Run daemon in background (detached process)', default: true },
22
+ { name: 'foreground', short: 'f', type: 'boolean', description: 'Run daemon in foreground (blocks terminal)' },
17
23
  ],
18
24
  examples: [
19
- { command: 'claude-flow daemon start', description: 'Start daemon with default workers' },
25
+ { command: 'claude-flow daemon start', description: 'Start daemon in background (default)' },
26
+ { command: 'claude-flow daemon start --foreground', description: 'Start in foreground (blocks terminal)' },
20
27
  { command: 'claude-flow daemon start -w map,audit,optimize', description: 'Start with specific workers' },
21
28
  ],
22
29
  action: async (ctx: CommandContext): Promise<CommandResult> => {
23
30
  const quiet = ctx.flags.quiet as boolean;
31
+ const foreground = ctx.flags.foreground as boolean;
24
32
  const projectRoot = process.cwd();
33
+ const isDaemonProcess = process.env.CLAUDE_FLOW_DAEMON === '1';
34
+
35
+ // Check if background daemon already running (skip if we ARE the daemon process)
36
+ if (!isDaemonProcess) {
37
+ const bgPid = getBackgroundDaemonPid(projectRoot);
38
+ if (bgPid && isProcessRunning(bgPid)) {
39
+ if (!quiet) {
40
+ output.printWarning(`Daemon already running in background (PID: ${bgPid})`);
41
+ }
42
+ return { success: true };
43
+ }
44
+ }
25
45
 
46
+ // Background mode (default): fork a detached process
47
+ if (!foreground) {
48
+ return startBackgroundDaemon(projectRoot, quiet);
49
+ }
50
+
51
+ // Foreground mode: run in current process (blocks terminal)
26
52
  try {
27
53
  if (!quiet) {
28
54
  const spinner = output.createSpinner({ text: 'Starting worker daemon...', spinner: 'dots' });
@@ -31,7 +57,7 @@ const startCommand: Command = {
31
57
  const daemon = await startDaemon(projectRoot);
32
58
  const status = daemon.getStatus();
33
59
 
34
- spinner.succeed('Worker daemon started');
60
+ spinner.succeed('Worker daemon started (foreground mode)');
35
61
 
36
62
  output.writeln();
37
63
  output.printBox(
@@ -65,6 +91,9 @@ const startCommand: Command = {
65
91
  })),
66
92
  });
67
93
 
94
+ output.writeln();
95
+ output.writeln(output.dim('Press Ctrl+C to stop daemon'));
96
+
68
97
  // Listen for worker events
69
98
  daemon.on('worker:start', ({ type }: { type: string }) => {
70
99
  output.writeln(output.dim(`[daemon] Worker starting: ${type}`));
@@ -77,8 +106,12 @@ const startCommand: Command = {
77
106
  daemon.on('worker:error', ({ type, error }: { type: string; error: string }) => {
78
107
  output.writeln(output.error(`[daemon] Worker failed: ${type} - ${error}`));
79
108
  });
109
+
110
+ // Keep process alive
111
+ await new Promise(() => {}); // Never resolves - daemon runs until killed
80
112
  } else {
81
113
  await startDaemon(projectRoot);
114
+ await new Promise(() => {}); // Keep alive
82
115
  }
83
116
 
84
117
  return { success: true };
@@ -89,6 +122,75 @@ const startCommand: Command = {
89
122
  },
90
123
  };
91
124
 
125
+ /**
126
+ * Start daemon as a detached background process
127
+ */
128
+ async function startBackgroundDaemon(projectRoot: string, quiet: boolean): Promise<CommandResult> {
129
+ const stateDir = join(projectRoot, '.claude-flow');
130
+ const pidFile = join(stateDir, 'daemon.pid');
131
+ const logFile = join(stateDir, 'daemon.log');
132
+
133
+ // Ensure state directory exists
134
+ if (!fs.existsSync(stateDir)) {
135
+ fs.mkdirSync(stateDir, { recursive: true });
136
+ }
137
+
138
+ // Get path to CLI (from dist/src/commands/daemon.js -> bin/cli.js)
139
+ const __filename = fileURLToPath(import.meta.url);
140
+ const __dirname = dirname(__filename);
141
+ // dist/src/commands -> dist/src -> dist -> package root -> bin/cli.js
142
+ const cliPath = join(__dirname, '..', '..', '..', 'bin', 'cli.js');
143
+
144
+ // Verify CLI path exists
145
+ if (!fs.existsSync(cliPath)) {
146
+ output.printError(`CLI not found at: ${cliPath}`);
147
+ return { success: false, exitCode: 1 };
148
+ }
149
+
150
+ // Use shell to spawn daemon with proper output redirection
151
+ // This ensures the log file stays open even after parent exits
152
+ const shellCmd = `"${process.execPath}" "${cliPath}" daemon start --foreground --quiet >> "${logFile}" 2>&1 & echo $!`;
153
+
154
+ const child = spawn('sh', ['-c', shellCmd], {
155
+ cwd: projectRoot,
156
+ detached: true,
157
+ stdio: ['ignore', 'pipe', 'ignore'],
158
+ env: { ...process.env, CLAUDE_FLOW_DAEMON: '1' },
159
+ });
160
+
161
+ // Wait for the PID to be echoed back
162
+ return new Promise((resolve) => {
163
+ let pidStr = '';
164
+ child.stdout?.on('data', (data: Buffer) => {
165
+ pidStr += data.toString();
166
+ });
167
+
168
+ child.on('close', () => {
169
+ const pid = parseInt(pidStr.trim(), 10);
170
+
171
+ if (isNaN(pid) || pid <= 0) {
172
+ output.printError('Failed to get daemon PID');
173
+ resolve({ success: false, exitCode: 1 });
174
+ return;
175
+ }
176
+
177
+ // Save PID
178
+ fs.writeFileSync(pidFile, String(pid));
179
+
180
+ if (!quiet) {
181
+ output.printSuccess(`Daemon started in background (PID: ${pid})`);
182
+ output.printInfo(`Logs: ${logFile}`);
183
+ output.printInfo(`Stop with: claude-flow daemon stop`);
184
+ }
185
+
186
+ resolve({ success: true });
187
+ });
188
+
189
+ // Unref so parent can exit immediately
190
+ child.unref();
191
+ });
192
+ }
193
+
92
194
  // Stop daemon subcommand
93
195
  const stopCommand: Command = {
94
196
  name: 'stop',
@@ -101,17 +203,23 @@ const stopCommand: Command = {
101
203
  ],
102
204
  action: async (ctx: CommandContext): Promise<CommandResult> => {
103
205
  const quiet = ctx.flags.quiet as boolean;
206
+ const projectRoot = process.cwd();
104
207
 
105
208
  try {
106
209
  if (!quiet) {
107
210
  const spinner = output.createSpinner({ text: 'Stopping worker daemon...', spinner: 'dots' });
108
211
  spinner.start();
109
212
 
213
+ // Try to stop in-process daemon first
110
214
  await stopDaemon();
111
215
 
112
- spinner.succeed('Worker daemon stopped');
216
+ // Also kill any background daemon by PID
217
+ const killed = await killBackgroundDaemon(projectRoot);
218
+
219
+ spinner.succeed(killed ? 'Worker daemon stopped' : 'Worker daemon was not running');
113
220
  } else {
114
221
  await stopDaemon();
222
+ await killBackgroundDaemon(projectRoot);
115
223
  }
116
224
 
117
225
  return { success: true };
@@ -122,6 +230,92 @@ const stopCommand: Command = {
122
230
  },
123
231
  };
124
232
 
233
+ /**
234
+ * Kill background daemon process using PID file
235
+ */
236
+ async function killBackgroundDaemon(projectRoot: string): Promise<boolean> {
237
+ const pidFile = join(projectRoot, '.claude-flow', 'daemon.pid');
238
+
239
+ if (!fs.existsSync(pidFile)) {
240
+ return false;
241
+ }
242
+
243
+ try {
244
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
245
+
246
+ if (isNaN(pid)) {
247
+ fs.unlinkSync(pidFile);
248
+ return false;
249
+ }
250
+
251
+ // Check if process is running
252
+ try {
253
+ process.kill(pid, 0); // Signal 0 = check if alive
254
+ } catch {
255
+ // Process not running, clean up stale PID file
256
+ fs.unlinkSync(pidFile);
257
+ return false;
258
+ }
259
+
260
+ // Kill the process
261
+ process.kill(pid, 'SIGTERM');
262
+
263
+ // Wait a moment then force kill if needed
264
+ await new Promise(resolve => setTimeout(resolve, 1000));
265
+
266
+ try {
267
+ process.kill(pid, 0);
268
+ // Still alive, force kill
269
+ process.kill(pid, 'SIGKILL');
270
+ } catch {
271
+ // Process terminated
272
+ }
273
+
274
+ // Clean up PID file
275
+ if (fs.existsSync(pidFile)) {
276
+ fs.unlinkSync(pidFile);
277
+ }
278
+
279
+ return true;
280
+ } catch (error) {
281
+ // Clean up PID file on any error
282
+ if (fs.existsSync(pidFile)) {
283
+ fs.unlinkSync(pidFile);
284
+ }
285
+ return false;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Get PID of background daemon from PID file
291
+ */
292
+ function getBackgroundDaemonPid(projectRoot: string): number | null {
293
+ const pidFile = join(projectRoot, '.claude-flow', 'daemon.pid');
294
+
295
+ if (!fs.existsSync(pidFile)) {
296
+ return null;
297
+ }
298
+
299
+ try {
300
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
301
+ return isNaN(pid) ? null : pid;
302
+ } catch {
303
+ return null;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Check if a process is running
309
+ */
310
+ function isProcessRunning(pid: number): boolean {
311
+ try {
312
+ process.kill(pid, 0); // Signal 0 = check if alive
313
+ return true;
314
+ } catch {
315
+ return false;
316
+ }
317
+ }
318
+
125
319
  // Status subcommand
126
320
  const statusCommand: Command = {
127
321
  name: 'status',
@@ -135,21 +329,30 @@ const statusCommand: Command = {
135
329
  ],
136
330
  action: async (ctx: CommandContext): Promise<CommandResult> => {
137
331
  const verbose = ctx.flags.verbose as boolean;
332
+ const projectRoot = process.cwd();
138
333
 
139
334
  try {
140
- const daemon = getDaemon(process.cwd());
335
+ const daemon = getDaemon(projectRoot);
141
336
  const status = daemon.getStatus();
142
337
 
338
+ // Also check for background daemon
339
+ const bgPid = getBackgroundDaemonPid(projectRoot);
340
+ const bgRunning = bgPid ? isProcessRunning(bgPid) : false;
341
+
342
+ const isRunning = status.running || bgRunning;
343
+ const displayPid = bgPid || status.pid;
344
+
143
345
  output.writeln();
144
346
 
145
347
  // Daemon status box
146
- const statusIcon = status.running ? output.success('●') : output.error('○');
147
- const statusText = status.running ? output.success('RUNNING') : output.error('STOPPED');
348
+ const statusIcon = isRunning ? output.success('●') : output.error('○');
349
+ const statusText = isRunning ? output.success('RUNNING') : output.error('STOPPED');
350
+ const mode = bgRunning ? output.dim(' (background)') : status.running ? output.dim(' (foreground)') : '';
148
351
 
149
352
  output.printBox(
150
353
  [
151
- `Status: ${statusIcon} ${statusText}`,
152
- `PID: ${status.pid}`,
354
+ `Status: ${statusIcon} ${statusText}${mode}`,
355
+ `PID: ${displayPid}`,
153
356
  status.startedAt ? `Started: ${status.startedAt.toISOString()}` : '',
154
357
  `Workers Enabled: ${status.config.workers.filter(w => w.enabled).length}`,
155
358
  `Max Concurrent: ${status.config.maxConcurrent}`,
package/src/index.ts CHANGED
@@ -78,6 +78,21 @@ export class CLI {
78
78
  this.output.setColorEnabled(false);
79
79
  }
80
80
 
81
+ // Set verbosity level based on flags
82
+ if (flags.quiet) {
83
+ this.output.setVerbosity('quiet');
84
+ } else if (flags.verbose) {
85
+ this.output.setVerbosity(process.env.DEBUG ? 'debug' : 'verbose');
86
+ }
87
+
88
+ // Verbose mode: show parsed arguments
89
+ if (this.output.isVerbose()) {
90
+ this.output.printDebug(`Command: ${commandPath.join(' ') || '(none)'}`);
91
+ this.output.printDebug(`Positional: [${positional.join(', ')}]`);
92
+ this.output.printDebug(`Flags: ${JSON.stringify(Object.fromEntries(Object.entries(flags).filter(([k]) => k !== '_')))}`);
93
+ this.output.printDebug(`CWD: ${process.cwd()}`);
94
+ }
95
+
81
96
  // No command - show help
82
97
  if (commandPath.length === 0 || flags.help || flags.h) {
83
98
  if (commandPath.length > 0) {
@@ -176,8 +191,17 @@ export class CLI {
176
191
 
177
192
  // Execute command
178
193
  if (targetCommand.action) {
194
+ if (this.output.isVerbose()) {
195
+ this.output.printDebug(`Executing: ${targetCommand.name}`);
196
+ }
197
+
198
+ const startTime = Date.now();
179
199
  const result = await targetCommand.action(ctx);
180
200
 
201
+ if (this.output.isVerbose()) {
202
+ this.output.printDebug(`Completed in ${Date.now() - startTime}ms`);
203
+ }
204
+
181
205
  if (result && !result.success) {
182
206
  process.exit(result.exitCode || 1);
183
207
  }
@@ -379,7 +403,7 @@ export * from './types.js';
379
403
  export { CommandParser, commandParser } from './parser.js';
380
404
 
381
405
  // Output
382
- export { OutputFormatter, output, Progress, Spinner } from './output.js';
406
+ export { OutputFormatter, output, Progress, Spinner, type VerbosityLevel } from './output.js';
383
407
 
384
408
  // Prompt
385
409
  export * from './prompt.js';
package/src/output.ts CHANGED
@@ -50,15 +50,46 @@ const COLORS = {
50
50
 
51
51
  type ColorName = keyof typeof COLORS;
52
52
 
53
+ export type VerbosityLevel = 'quiet' | 'normal' | 'verbose' | 'debug';
54
+
53
55
  export class OutputFormatter {
54
56
  private colorEnabled: boolean;
55
57
  private outputStream: NodeJS.WriteStream;
56
58
  private errorStream: NodeJS.WriteStream;
59
+ private verbosity: VerbosityLevel;
57
60
 
58
- constructor(options: { color?: boolean } = {}) {
61
+ constructor(options: { color?: boolean; verbosity?: VerbosityLevel } = {}) {
59
62
  this.colorEnabled = options.color ?? this.supportsColor();
60
63
  this.outputStream = process.stdout;
61
64
  this.errorStream = process.stderr;
65
+ this.verbosity = options.verbosity ?? 'normal';
66
+ }
67
+
68
+ /**
69
+ * Set verbosity level
70
+ * - quiet: Only errors and direct results
71
+ * - normal: Errors, warnings, info, and results
72
+ * - verbose: All of normal + debug messages
73
+ * - debug: All output including trace
74
+ */
75
+ setVerbosity(level: VerbosityLevel): void {
76
+ this.verbosity = level;
77
+ }
78
+
79
+ getVerbosity(): VerbosityLevel {
80
+ return this.verbosity;
81
+ }
82
+
83
+ isQuiet(): boolean {
84
+ return this.verbosity === 'quiet';
85
+ }
86
+
87
+ isVerbose(): boolean {
88
+ return this.verbosity === 'verbose' || this.verbosity === 'debug';
89
+ }
90
+
91
+ isDebug(): boolean {
92
+ return this.verbosity === 'debug';
62
93
  }
63
94
 
64
95
  private supportsColor(): boolean {
@@ -136,11 +167,13 @@ export class OutputFormatter {
136
167
  // ============================================
137
168
 
138
169
  printSuccess(message: string): void {
170
+ // Success always shows (result output)
139
171
  const icon = this.color('[OK]', 'green', 'bold');
140
172
  this.writeln(`${icon} ${message}`);
141
173
  }
142
174
 
143
175
  printError(message: string, details?: string): void {
176
+ // Errors always show
144
177
  const icon = this.color('[ERROR]', 'red', 'bold');
145
178
  this.writeErrorln(`${icon} ${message}`);
146
179
  if (details) {
@@ -149,20 +182,33 @@ export class OutputFormatter {
149
182
  }
150
183
 
151
184
  printWarning(message: string): void {
185
+ // Warnings suppressed in quiet mode
186
+ if (this.verbosity === 'quiet') return;
152
187
  const icon = this.color('[WARN]', 'yellow', 'bold');
153
188
  this.writeln(`${icon} ${message}`);
154
189
  }
155
190
 
156
191
  printInfo(message: string): void {
192
+ // Info suppressed in quiet mode
193
+ if (this.verbosity === 'quiet') return;
157
194
  const icon = this.color('[INFO]', 'blue', 'bold');
158
195
  this.writeln(`${icon} ${message}`);
159
196
  }
160
197
 
161
198
  printDebug(message: string): void {
199
+ // Debug only shows in verbose/debug mode
200
+ if (this.verbosity !== 'verbose' && this.verbosity !== 'debug') return;
162
201
  const icon = this.color('[DEBUG]', 'gray');
163
202
  this.writeln(`${icon} ${this.dim(message)}`);
164
203
  }
165
204
 
205
+ printTrace(message: string): void {
206
+ // Trace only shows in debug mode
207
+ if (this.verbosity !== 'debug') return;
208
+ const icon = this.color('[TRACE]', 'gray', 'dim');
209
+ this.writeln(`${icon} ${this.dim(message)}`);
210
+ }
211
+
166
212
  // ============================================
167
213
  // Table Formatting
168
214
  // ============================================