cli-copilot-worker 0.1.0

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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/bin/cli-copilot-worker.mjs +3 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +682 -0
  6. package/dist/src/cli.js.map +1 -0
  7. package/dist/src/core/copilot.d.ts +13 -0
  8. package/dist/src/core/copilot.js +56 -0
  9. package/dist/src/core/copilot.js.map +1 -0
  10. package/dist/src/core/failure-classifier.d.ts +8 -0
  11. package/dist/src/core/failure-classifier.js +148 -0
  12. package/dist/src/core/failure-classifier.js.map +1 -0
  13. package/dist/src/core/ids.d.ts +1 -0
  14. package/dist/src/core/ids.js +11 -0
  15. package/dist/src/core/ids.js.map +1 -0
  16. package/dist/src/core/markdown.d.ts +5 -0
  17. package/dist/src/core/markdown.js +14 -0
  18. package/dist/src/core/markdown.js.map +1 -0
  19. package/dist/src/core/paths.d.ts +18 -0
  20. package/dist/src/core/paths.js +42 -0
  21. package/dist/src/core/paths.js.map +1 -0
  22. package/dist/src/core/profile-faults.d.ts +15 -0
  23. package/dist/src/core/profile-faults.js +110 -0
  24. package/dist/src/core/profile-faults.js.map +1 -0
  25. package/dist/src/core/profile-manager.d.ts +25 -0
  26. package/dist/src/core/profile-manager.js +162 -0
  27. package/dist/src/core/profile-manager.js.map +1 -0
  28. package/dist/src/core/question-registry.d.ts +25 -0
  29. package/dist/src/core/question-registry.js +154 -0
  30. package/dist/src/core/question-registry.js.map +1 -0
  31. package/dist/src/core/store.d.ts +39 -0
  32. package/dist/src/core/store.js +206 -0
  33. package/dist/src/core/store.js.map +1 -0
  34. package/dist/src/core/types.d.ts +152 -0
  35. package/dist/src/core/types.js +2 -0
  36. package/dist/src/core/types.js.map +1 -0
  37. package/dist/src/daemon/client.d.ts +6 -0
  38. package/dist/src/daemon/client.js +117 -0
  39. package/dist/src/daemon/client.js.map +1 -0
  40. package/dist/src/daemon/server.d.ts +1 -0
  41. package/dist/src/daemon/server.js +149 -0
  42. package/dist/src/daemon/server.js.map +1 -0
  43. package/dist/src/daemon/service.d.ts +69 -0
  44. package/dist/src/daemon/service.js +800 -0
  45. package/dist/src/daemon/service.js.map +1 -0
  46. package/dist/src/doctor.d.ts +1 -0
  47. package/dist/src/doctor.js +74 -0
  48. package/dist/src/doctor.js.map +1 -0
  49. package/dist/src/index.d.ts +3 -0
  50. package/dist/src/index.js +4 -0
  51. package/dist/src/index.js.map +1 -0
  52. package/dist/src/output.d.ts +28 -0
  53. package/dist/src/output.js +307 -0
  54. package/dist/src/output.js.map +1 -0
  55. package/package.json +59 -0
  56. package/src/cli.ts +881 -0
  57. package/src/core/copilot.ts +75 -0
  58. package/src/core/failure-classifier.ts +202 -0
  59. package/src/core/ids.ts +11 -0
  60. package/src/core/markdown.ts +19 -0
  61. package/src/core/paths.ts +56 -0
  62. package/src/core/profile-faults.ts +140 -0
  63. package/src/core/profile-manager.ts +220 -0
  64. package/src/core/question-registry.ts +191 -0
  65. package/src/core/store.ts +273 -0
  66. package/src/core/types.ts +211 -0
  67. package/src/daemon/client.ts +137 -0
  68. package/src/daemon/server.ts +167 -0
  69. package/src/daemon/service.ts +968 -0
  70. package/src/doctor.ts +82 -0
  71. package/src/index.ts +3 -0
  72. package/src/output.ts +391 -0
package/src/cli.ts ADDED
@@ -0,0 +1,881 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve as resolvePath } from 'node:path';
4
+ import process from 'node:process';
5
+
6
+ import { Command } from 'commander';
7
+
8
+ import pkg from '../package.json' with { type: 'json' };
9
+ import { readMarkdownFile } from './core/markdown.js';
10
+ import { daemonIsRunning, ensureDaemonMeta, sendDaemonRequest } from './daemon/client.js';
11
+ import { runDaemonServer } from './daemon/server.js';
12
+ import { inspectDoctor } from './doctor.js';
13
+ import {
14
+ createEventPrinter,
15
+ formatActions,
16
+ formatAttemptHistory,
17
+ formatEntries,
18
+ formatPendingQuestion,
19
+ formatProfileSummary,
20
+ formatProfilesSection,
21
+ printJson,
22
+ resolveOutputFormat,
23
+ shortenPath,
24
+ type OutputFormat,
25
+ } from './output.js';
26
+
27
+ interface GlobalOptions {
28
+ output?: string | undefined;
29
+ }
30
+
31
+ interface ReadResponse {
32
+ conversation: {
33
+ id: string;
34
+ status: string;
35
+ cwd: string;
36
+ model: string;
37
+ pendingQuestion?: {
38
+ question: string;
39
+ choices?: string[] | undefined;
40
+ allowFreeform: boolean;
41
+ askedAt: string;
42
+ sessionId: string;
43
+ } | undefined;
44
+ lastJobId?: string | undefined;
45
+ };
46
+ meta: {
47
+ totalEntries: number;
48
+ returnedEntries: number;
49
+ tokenBudget: number;
50
+ approxTokens: number;
51
+ nextActions: Record<string, unknown>;
52
+ };
53
+ entries: Array<{
54
+ index: number;
55
+ role: 'user' | 'assistant' | 'tool_use' | 'tool_result' | 'question' | 'answer' | 'status' | 'error';
56
+ content: string;
57
+ timestamp: string;
58
+ data?: Record<string, unknown> | undefined;
59
+ }>;
60
+ }
61
+
62
+ function getOutputFormat(program: Command): OutputFormat {
63
+ return resolveOutputFormat((program.opts() as GlobalOptions).output);
64
+ }
65
+
66
+ function parseInteger(value: string | undefined, label: string): number | undefined {
67
+ if (value === undefined) {
68
+ return undefined;
69
+ }
70
+
71
+ const parsed = Number.parseInt(value, 10);
72
+ if (Number.isNaN(parsed)) {
73
+ throw new Error(`Invalid ${label}: ${value}`);
74
+ }
75
+
76
+ return parsed;
77
+ }
78
+
79
+ function isRecord(value: unknown): value is Record<string, unknown> {
80
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
81
+ }
82
+
83
+ function stringValue(value: unknown): string | undefined {
84
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
85
+ }
86
+
87
+ function extractPendingQuestion(result: Record<string, unknown>): ReadResponse['conversation']['pendingQuestion'] | undefined {
88
+ const candidates: unknown[] = [
89
+ result.pendingQuestion,
90
+ isRecord(result.conversation) ? result.conversation.pendingQuestion : undefined,
91
+ ];
92
+
93
+ for (const candidate of candidates) {
94
+ if (!isRecord(candidate)) {
95
+ continue;
96
+ }
97
+
98
+ if (typeof candidate.question === 'string' && typeof candidate.allowFreeform === 'boolean' && typeof candidate.askedAt === 'string' && typeof candidate.sessionId === 'string') {
99
+ return candidate as ReadResponse['conversation']['pendingQuestion'];
100
+ }
101
+ }
102
+
103
+ return undefined;
104
+ }
105
+
106
+ function extractAttempts(result: unknown): unknown {
107
+ if (!isRecord(result)) {
108
+ return undefined;
109
+ }
110
+
111
+ if (Array.isArray(result.attempts)) {
112
+ return result.attempts;
113
+ }
114
+
115
+ if (isRecord(result.job) && Array.isArray(result.job.attempts)) {
116
+ return result.job.attempts;
117
+ }
118
+
119
+ if (isRecord(result.conversation) && Array.isArray(result.conversation.attempts)) {
120
+ return result.conversation.attempts;
121
+ }
122
+
123
+ return undefined;
124
+ }
125
+
126
+ function formatProfileIdentity(result: Record<string, unknown>): string | undefined {
127
+ const sources = [
128
+ result,
129
+ isRecord(result.conversation) ? result.conversation : undefined,
130
+ isRecord(result.job) ? result.job : undefined,
131
+ ].filter((source): source is Record<string, unknown> => source !== undefined);
132
+
133
+ for (const source of sources) {
134
+ const hasProfileSignals =
135
+ source.activeProfileId !== undefined ||
136
+ source.currentProfileId !== undefined ||
137
+ source.profileId !== undefined ||
138
+ source.activeProfileConfigDir !== undefined ||
139
+ source.currentProfileConfigDir !== undefined ||
140
+ source.profileConfigDir !== undefined ||
141
+ source.cooldownUntil !== undefined ||
142
+ source.failureCount !== undefined ||
143
+ source.lastFailureCategory !== undefined ||
144
+ source.lastFailureReason !== undefined ||
145
+ source.authenticated !== undefined ||
146
+ source.login !== undefined ||
147
+ source.models !== undefined;
148
+
149
+ if (!hasProfileSignals) {
150
+ continue;
151
+ }
152
+
153
+ const id = stringValue(source.activeProfileId)
154
+ ?? stringValue(source.currentProfileId)
155
+ ?? stringValue(source.profileId)
156
+ ?? stringValue(source.id);
157
+ const configDir = stringValue(source.activeProfileConfigDir)
158
+ ?? stringValue(source.currentProfileConfigDir)
159
+ ?? stringValue(source.profileConfigDir)
160
+ ?? stringValue(source.configDir);
161
+
162
+ if (id || configDir) {
163
+ return [
164
+ id ?? 'unknown',
165
+ configDir ? shortenPath(configDir) : undefined,
166
+ ].filter((part): part is string => part !== undefined).join(' ');
167
+ }
168
+ }
169
+
170
+ return undefined;
171
+ }
172
+
173
+ function formatPrimitiveFields(result: Record<string, unknown>, excludeKeys: string[]): string[] {
174
+ return Object.entries(result)
175
+ .filter(([key, value]) => !excludeKeys.includes(key) && (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'))
176
+ .map(([key, value]) => `${key}: ${String(value)}`);
177
+ }
178
+
179
+ function renderAttemptsSection(result: unknown): string {
180
+ return formatAttemptHistory(extractAttempts(result));
181
+ }
182
+
183
+ function renderProfilesSection(result: Record<string, unknown>, options?: Parameters<typeof formatProfilesSection>[1]): string {
184
+ return formatProfilesSection(result.profiles, options);
185
+ }
186
+
187
+ function indentBlock(text: string, prefix = ' '): string {
188
+ return text.split('\n').map((line) => `${prefix}${line}`).join('\n');
189
+ }
190
+
191
+ function renderRunLikeResult(result: Record<string, unknown>): string {
192
+ const lines = [
193
+ `Conversation: ${String(result.conversationId ?? 'unknown')}`,
194
+ `Job: ${String(result.jobId ?? 'unknown')}`,
195
+ `Status: ${String(result.status ?? 'unknown')}`,
196
+ ];
197
+
198
+ const profile = formatProfileIdentity(result);
199
+ if (profile) {
200
+ lines.push('');
201
+ lines.push(`Profile: ${profile}`);
202
+ }
203
+
204
+ const attempts = renderAttemptsSection(result);
205
+ if (attempts) {
206
+ lines.push('');
207
+ lines.push(attempts);
208
+ }
209
+
210
+ const pendingQuestion = extractPendingQuestion(result);
211
+ if (pendingQuestion) {
212
+ lines.push('');
213
+ lines.push('Pending Question:');
214
+ lines.push(formatPendingQuestion(pendingQuestion));
215
+ }
216
+
217
+ lines.push('');
218
+ lines.push('Actions:');
219
+ lines.push(formatActions(result.actions as Record<string, unknown> | undefined));
220
+ return lines.join('\n');
221
+ }
222
+
223
+ function renderListResult(result: Record<string, unknown>): string {
224
+ const conversations = Array.isArray(result.conversations)
225
+ ? result.conversations as Array<Record<string, unknown>>
226
+ : [];
227
+
228
+ if (conversations.length === 0) {
229
+ return 'No conversations.';
230
+ }
231
+
232
+ return conversations
233
+ .map((conversation) => {
234
+ const pending = typeof conversation.pendingQuestion === 'string' ? ' pending-question' : '';
235
+ return [
236
+ `${String(conversation.id)} ${String(conversation.status)}${pending}`,
237
+ ` cwd: ${shortenPath(String(conversation.cwd ?? ''))}`,
238
+ ` model: ${String(conversation.model ?? '')}`,
239
+ ` updated: ${String(conversation.updatedAt ?? '')}`,
240
+ ].join('\n');
241
+ })
242
+ .join('\n\n');
243
+ }
244
+
245
+ function renderInfoResult(result: Record<string, unknown>): string {
246
+ const conversation = result.conversation as Record<string, unknown>;
247
+ const jobs = Array.isArray(result.jobs) ? result.jobs as Array<Record<string, unknown>> : [];
248
+
249
+ const lines = [
250
+ `Conversation: ${String(conversation.id ?? 'unknown')}`,
251
+ `Status: ${String(conversation.status ?? 'unknown')}`,
252
+ `Model: ${String(conversation.model ?? 'unknown')}`,
253
+ `cwd: ${shortenPath(String(conversation.cwd ?? ''))}`,
254
+ `Created: ${String(conversation.createdAt ?? '')}`,
255
+ `Updated: ${String(conversation.updatedAt ?? '')}`,
256
+ ];
257
+
258
+ const profile = formatProfileIdentity(conversation);
259
+ if (profile) {
260
+ lines.push('');
261
+ lines.push(`Profile: ${profile}`);
262
+ }
263
+
264
+ const pendingQuestion = extractPendingQuestion(conversation);
265
+ if (pendingQuestion) {
266
+ lines.push('');
267
+ lines.push('Pending Question:');
268
+ lines.push(formatPendingQuestion(pendingQuestion));
269
+ }
270
+
271
+ lines.push('');
272
+ lines.push('Jobs:');
273
+ if (jobs.length === 0) {
274
+ lines.push('- none');
275
+ } else {
276
+ jobs.forEach((job) => {
277
+ const jobProfile = formatProfileIdentity(job);
278
+ const attempts = renderAttemptsSection(job);
279
+ lines.push(
280
+ [
281
+ `- ${String(job.id)} ${String(job.kind)} ${String(job.status)} started=${String(job.startedAt ?? '')}`,
282
+ jobProfile ? `profile=${jobProfile}` : undefined,
283
+ Array.isArray(job.attempts) ? `attempts=${job.attempts.length}` : undefined,
284
+ job.error ? `error=${String(job.error)}` : undefined,
285
+ ].filter((part): part is string => part !== undefined).join(' '),
286
+ );
287
+ if (attempts) {
288
+ lines.push(indentBlock(attempts));
289
+ }
290
+ });
291
+ }
292
+
293
+ return lines.join('\n');
294
+ }
295
+
296
+ function renderJobResult(result: Record<string, unknown>): string {
297
+ const job = result.job as Record<string, unknown>;
298
+ const lines = [
299
+ `Job: ${String(job.id ?? 'unknown')}`,
300
+ `Conversation: ${String(job.conversationId ?? 'unknown')}`,
301
+ `Kind: ${String(job.kind ?? 'unknown')}`,
302
+ `Status: ${String(job.status ?? 'unknown')}`,
303
+ `Input: ${shortenPath(String(job.inputFilePath ?? ''))}`,
304
+ `Started: ${String(job.startedAt ?? '')}`,
305
+ ];
306
+
307
+ if (job.endedAt) {
308
+ lines.push(`Ended: ${String(job.endedAt)}`);
309
+ }
310
+ if (job.error) {
311
+ lines.push(`Error: ${String(job.error)}`);
312
+ }
313
+
314
+ const attempts = renderAttemptsSection(result);
315
+ if (attempts) {
316
+ lines.push('');
317
+ lines.push(attempts);
318
+ }
319
+
320
+ const pendingQuestion = extractPendingQuestion(result);
321
+ if (pendingQuestion) {
322
+ lines.push('');
323
+ lines.push('Pending Question:');
324
+ lines.push(formatPendingQuestion(pendingQuestion));
325
+ }
326
+
327
+ lines.push('');
328
+ lines.push('Actions:');
329
+ lines.push(formatActions({
330
+ ...(result.actions as Record<string, unknown> | undefined),
331
+ answer: job.status === 'waiting_answer'
332
+ ? `cli-copilot-worker answer ${String(job.conversationId)} answer.md`
333
+ : null,
334
+ }));
335
+ return lines.join('\n');
336
+ }
337
+
338
+ function renderReadResult(result: ReadResponse): string {
339
+ const lines = [
340
+ `Conversation: ${result.conversation.id}`,
341
+ `Status: ${result.conversation.status}`,
342
+ `cwd: ${shortenPath(result.conversation.cwd)}`,
343
+ `Model: ${result.conversation.model}`,
344
+ `Entries: ${result.meta.returnedEntries}/${result.meta.totalEntries}`,
345
+ `Approx Tokens: ${result.meta.approxTokens}/${result.meta.tokenBudget}`,
346
+ ];
347
+
348
+ const profile = formatProfileIdentity(result.conversation);
349
+ if (profile) {
350
+ lines.push('');
351
+ lines.push(`Profile: ${profile}`);
352
+ }
353
+
354
+ const pendingQuestion = extractPendingQuestion(result.conversation);
355
+ if (pendingQuestion) {
356
+ lines.push('');
357
+ lines.push('Pending Question:');
358
+ lines.push(formatPendingQuestion(pendingQuestion));
359
+ }
360
+
361
+ const attempts = renderAttemptsSection(result);
362
+ if (attempts) {
363
+ lines.push('');
364
+ lines.push(attempts);
365
+ }
366
+
367
+ lines.push('');
368
+ lines.push(formatEntries(result.entries));
369
+
370
+ lines.push('');
371
+ lines.push('Actions:');
372
+ lines.push(formatActions(result.meta.nextActions));
373
+ return lines.join('\n');
374
+ }
375
+
376
+ function renderDaemonResult(result: Record<string, unknown>): string {
377
+ const lines = formatPrimitiveFields(result, ['profiles', 'attempts', 'jobs']);
378
+
379
+ const profiles = renderProfilesSection(result, { includeHealth: true });
380
+ if (profiles) {
381
+ if (lines.length > 0) {
382
+ lines.push('');
383
+ }
384
+ lines.push(profiles);
385
+ }
386
+
387
+ const attempts = renderAttemptsSection(result);
388
+ if (attempts) {
389
+ if (lines.length > 0) {
390
+ lines.push('');
391
+ }
392
+ lines.push(attempts);
393
+ }
394
+
395
+ return lines.join('\n');
396
+ }
397
+
398
+ async function readRequestPayload(filePath: string): Promise<{ path: string; content: string }> {
399
+ return await readMarkdownFile(filePath, process.cwd());
400
+ }
401
+
402
+ async function handleRun(taskPath: string, options: { cwd?: string; model?: string; timeout?: string; async?: boolean }, program: Command): Promise<void> {
403
+ const payload = await readRequestPayload(taskPath);
404
+ const output = getOutputFormat(program);
405
+ const eventPrinter = createEventPrinter(output === 'text' && !options.async && (process.stdout.isTTY ?? false));
406
+ const result = await sendDaemonRequest(
407
+ 'run',
408
+ {
409
+ inputFilePath: payload.path,
410
+ content: payload.content,
411
+ cwd: resolvePath(options.cwd ?? process.cwd()),
412
+ model: options.model,
413
+ timeoutMs: parseInteger(options.timeout, 'timeout'),
414
+ async: options.async ?? false,
415
+ },
416
+ { onEvent: eventPrinter.onEvent },
417
+ );
418
+ eventPrinter.finish();
419
+
420
+ if (output === 'json') {
421
+ printJson(result);
422
+ return;
423
+ }
424
+
425
+ process.stdout.write(`${renderRunLikeResult(result)}\n`);
426
+ }
427
+
428
+ async function handleSend(conversationId: string, messagePath: string, options: { timeout?: string; async?: boolean }, program: Command): Promise<void> {
429
+ const payload = await readRequestPayload(messagePath);
430
+ const output = getOutputFormat(program);
431
+ const eventPrinter = createEventPrinter(output === 'text' && !options.async && (process.stdout.isTTY ?? false));
432
+ const result = await sendDaemonRequest(
433
+ 'send',
434
+ {
435
+ conversationId,
436
+ inputFilePath: payload.path,
437
+ content: payload.content,
438
+ timeoutMs: parseInteger(options.timeout, 'timeout'),
439
+ async: options.async ?? false,
440
+ },
441
+ { onEvent: eventPrinter.onEvent },
442
+ );
443
+ eventPrinter.finish();
444
+
445
+ if (output === 'json') {
446
+ printJson(result);
447
+ return;
448
+ }
449
+
450
+ process.stdout.write(`${renderRunLikeResult(result)}\n`);
451
+ }
452
+
453
+ async function handleAnswer(conversationId: string, answerPath: string, program: Command): Promise<void> {
454
+ const payload = await readRequestPayload(answerPath);
455
+ const result = await sendDaemonRequest('answer', {
456
+ conversationId,
457
+ inputFilePath: payload.path,
458
+ content: payload.content,
459
+ });
460
+
461
+ if (getOutputFormat(program) === 'json') {
462
+ printJson(result);
463
+ return;
464
+ }
465
+
466
+ process.stdout.write(
467
+ [
468
+ `Conversation: ${String(result.conversationId ?? conversationId)}`,
469
+ `Status: ${String(result.status ?? 'submitted')}`,
470
+ `Answer: ${String(result.answer ?? '')}`,
471
+ '',
472
+ 'Actions:',
473
+ `- read: cli-copilot-worker read ${String(result.conversationId ?? conversationId)} --follow`,
474
+ `- info: cli-copilot-worker info ${String(result.conversationId ?? conversationId)}`,
475
+ ].join('\n') + '\n',
476
+ );
477
+ }
478
+
479
+ async function handleRead(conversationId: string, options: {
480
+ from?: string;
481
+ to?: string;
482
+ after?: string;
483
+ before?: string;
484
+ tokens?: string;
485
+ detail?: 'standard' | 'verbose' | 'full' | 'meta';
486
+ follow?: boolean;
487
+ }, program: Command): Promise<void> {
488
+ const output = getOutputFormat(program);
489
+ if (options.follow && output === 'json') {
490
+ throw new Error('--follow only supports text output');
491
+ }
492
+
493
+ let after = parseInteger(options.after, 'after');
494
+ let firstPass = true;
495
+
496
+ while (true) {
497
+ const result = await sendDaemonRequest('read', {
498
+ conversationId,
499
+ from: firstPass ? parseInteger(options.from, 'from') : undefined,
500
+ to: firstPass ? parseInteger(options.to, 'to') : undefined,
501
+ after,
502
+ before: firstPass ? parseInteger(options.before, 'before') : undefined,
503
+ tokens: parseInteger(options.tokens, 'tokens'),
504
+ detail: options.detail,
505
+ }) as unknown as ReadResponse;
506
+
507
+ if (output === 'json') {
508
+ printJson(result);
509
+ return;
510
+ }
511
+
512
+ if (firstPass || result.entries.length > 0) {
513
+ process.stdout.write(`${renderReadResult(result)}\n`);
514
+ }
515
+
516
+ if (!options.follow) {
517
+ return;
518
+ }
519
+
520
+ const lastEntry = result.entries[result.entries.length - 1];
521
+ after = lastEntry?.index ?? after;
522
+ firstPass = false;
523
+
524
+ if (result.conversation.status !== 'running') {
525
+ return;
526
+ }
527
+
528
+ await new Promise((resolve) => setTimeout(resolve, 2000));
529
+ }
530
+ }
531
+
532
+ async function handleList(program: Command): Promise<void> {
533
+ const result = await sendDaemonRequest('list');
534
+ if (getOutputFormat(program) === 'json') {
535
+ printJson(result);
536
+ return;
537
+ }
538
+
539
+ process.stdout.write(`${renderListResult(result)}\n`);
540
+ }
541
+
542
+ async function handleInfo(conversationId: string, program: Command): Promise<void> {
543
+ const result = await sendDaemonRequest('info', { conversationId });
544
+ if (getOutputFormat(program) === 'json') {
545
+ printJson(result);
546
+ return;
547
+ }
548
+
549
+ process.stdout.write(`${renderInfoResult(result)}\n`);
550
+ }
551
+
552
+ async function handleJobStatus(jobId: string, program: Command): Promise<void> {
553
+ const result = await sendDaemonRequest('job.status', { jobId });
554
+ if (getOutputFormat(program) === 'json') {
555
+ printJson(result);
556
+ return;
557
+ }
558
+
559
+ process.stdout.write(`${renderJobResult(result)}\n`);
560
+ }
561
+
562
+ async function handleJobList(program: Command): Promise<void> {
563
+ const result = await sendDaemonRequest('job.list');
564
+ if (getOutputFormat(program) === 'json') {
565
+ printJson(result);
566
+ return;
567
+ }
568
+
569
+ const jobs = Array.isArray(result.jobs) ? result.jobs as Array<Record<string, unknown>> : [];
570
+ if (jobs.length === 0) {
571
+ process.stdout.write('No jobs.\n');
572
+ return;
573
+ }
574
+
575
+ const lines = jobs.map((job) =>
576
+ `${String(job.id)} ${String(job.kind)} ${String(job.status)} conversation=${String(job.conversationId)}`,
577
+ );
578
+ process.stdout.write(`${lines.join('\n')}\n`);
579
+ }
580
+
581
+ async function handleJobWait(jobId: string, options: { timeout?: string; interval?: string }, program: Command): Promise<void> {
582
+ const result = await sendDaemonRequest('job.wait', {
583
+ jobId,
584
+ timeoutSeconds: parseInteger(options.timeout, 'timeout'),
585
+ intervalSeconds: parseInteger(options.interval, 'interval'),
586
+ });
587
+
588
+ if (getOutputFormat(program) === 'json') {
589
+ printJson(result);
590
+ return;
591
+ }
592
+
593
+ process.stdout.write(`${renderJobResult(result)}\n`);
594
+ }
595
+
596
+ async function handleJobRead(jobId: string, program: Command): Promise<void> {
597
+ const result = await sendDaemonRequest('job.read', { jobId });
598
+ if (getOutputFormat(program) === 'json') {
599
+ printJson(result);
600
+ return;
601
+ }
602
+
603
+ const job = result.job as Record<string, unknown>;
604
+ const entries = Array.isArray(result.entries) ? result.entries as ReadResponse['entries'] : [];
605
+ const attempts = renderAttemptsSection(result);
606
+ const pendingQuestion = extractPendingQuestion(result);
607
+ const profile = formatProfileIdentity(job);
608
+ process.stdout.write(
609
+ [
610
+ `Job: ${String(job.id ?? jobId)}`,
611
+ `Conversation: ${String(job.conversationId ?? 'unknown')}`,
612
+ `Status: ${String(job.status ?? 'unknown')}`,
613
+ `Kind: ${String(job.kind ?? 'unknown')}`,
614
+ `Started: ${String(job.startedAt ?? '')}`,
615
+ ...(job.endedAt ? [`Ended: ${String(job.endedAt)}`] : []),
616
+ ...(profile ? [`Profile: ${profile}`] : []),
617
+ '',
618
+ ...(attempts ? [attempts, ''] : []),
619
+ ...(pendingQuestion
620
+ ? ['Pending Question:', formatPendingQuestion(pendingQuestion), '']
621
+ : []),
622
+ formatEntries(entries),
623
+ ].join('\n') + '\n',
624
+ );
625
+ }
626
+
627
+ async function handleJobCancel(jobId: string, program: Command): Promise<void> {
628
+ const result = await sendDaemonRequest('job.cancel', { jobId });
629
+ if (getOutputFormat(program) === 'json') {
630
+ printJson(result);
631
+ return;
632
+ }
633
+
634
+ process.stdout.write(`${renderJobResult(result)}\n`);
635
+ }
636
+
637
+ async function handleDaemonStart(program: Command): Promise<void> {
638
+ const meta = await ensureDaemonMeta();
639
+ const result = await sendDaemonRequest('daemon.status');
640
+ const payload = { ...result, pid: meta.pid, startedAt: meta.startedAt };
641
+ if (getOutputFormat(program) === 'json') {
642
+ printJson(payload);
643
+ return;
644
+ }
645
+
646
+ process.stdout.write(`${renderDaemonResult(payload)}\n`);
647
+ }
648
+
649
+ async function handleDaemonStatus(program: Command): Promise<void> {
650
+ if (!await daemonIsRunning()) {
651
+ const result = { status: 'stopped' };
652
+ if (getOutputFormat(program) === 'json') {
653
+ printJson(result);
654
+ return;
655
+ }
656
+
657
+ process.stdout.write('status: stopped\n');
658
+ return;
659
+ }
660
+
661
+ const result = await sendDaemonRequest('daemon.status');
662
+ if (getOutputFormat(program) === 'json') {
663
+ printJson(result);
664
+ return;
665
+ }
666
+
667
+ process.stdout.write(`${renderDaemonResult(result)}\n`);
668
+ }
669
+
670
+ async function handleDaemonStop(program: Command): Promise<void> {
671
+ if (!await daemonIsRunning()) {
672
+ const result = { status: 'stopped' };
673
+ if (getOutputFormat(program) === 'json') {
674
+ printJson(result);
675
+ return;
676
+ }
677
+
678
+ process.stdout.write('status: stopped\n');
679
+ return;
680
+ }
681
+
682
+ const result = await sendDaemonRequest('daemon.stop');
683
+ if (getOutputFormat(program) === 'json') {
684
+ printJson(result);
685
+ return;
686
+ }
687
+
688
+ process.stdout.write(`${renderDaemonResult(result)}\n`);
689
+ }
690
+
691
+ async function handleDoctor(program: Command): Promise<void> {
692
+ const result = await inspectDoctor();
693
+ if (getOutputFormat(program) === 'json') {
694
+ printJson(result);
695
+ return;
696
+ }
697
+
698
+ const profiles = Array.isArray(result.profiles) ? result.profiles as Array<Record<string, unknown>> : [];
699
+ const now = Date.now();
700
+ process.stdout.write(
701
+ [
702
+ `Node: ${String(result.node ?? '')}`,
703
+ `Copilot CLI: ${String(result.copilot ?? 'not found')}`,
704
+ `mcpc: ${String(result.mcpc ?? 'not found')}`,
705
+ `Daemon running: ${String(result.daemonRunning ?? false)}`,
706
+ `State root: ${shortenPath(String(result.stateRoot ?? ''))}`,
707
+ '',
708
+ 'Profiles:',
709
+ ...(profiles.length > 0
710
+ ? profiles.map((profile) =>
711
+ formatProfileSummary(profile, {
712
+ includeAuth: true,
713
+ includeModels: true,
714
+ includeHealth: true,
715
+ now,
716
+ }),
717
+ )
718
+ : ['- none']),
719
+ ].join('\n') + '\n',
720
+ );
721
+ }
722
+
723
+ const program = new Command();
724
+
725
+ program
726
+ .name('cli-copilot-worker')
727
+ .description('Copilot-only worker CLI')
728
+ .version(pkg.version)
729
+ .option('--output <format>', 'Output format: text or json');
730
+
731
+ program
732
+ .command('run')
733
+ .argument('<task.md>')
734
+ .description('Run a Markdown task file')
735
+ .option('--cwd <dir>', 'Working directory for the Copilot session')
736
+ .option('--model <id>', 'Copilot model id')
737
+ .option('--timeout <ms>', 'Timeout in milliseconds')
738
+ .option('--async', 'Return immediately and keep the job in the daemon')
739
+ .action(async (taskPath, options) => {
740
+ await handleRun(taskPath, options, program);
741
+ });
742
+
743
+ program
744
+ .command('send')
745
+ .argument('<conversation-id>')
746
+ .argument('<message.md>')
747
+ .description('Send a Markdown follow-up message to an existing conversation')
748
+ .option('--timeout <ms>', 'Timeout in milliseconds')
749
+ .option('--async', 'Return immediately and keep the job in the daemon')
750
+ .action(async (conversationId, messagePath, options) => {
751
+ await handleSend(conversationId, messagePath, options, program);
752
+ });
753
+
754
+ program
755
+ .command('answer')
756
+ .argument('<conversation-id>')
757
+ .argument('<answer.md>')
758
+ .description('Answer a pending Copilot question from a Markdown file')
759
+ .action(async (conversationId, answerPath) => {
760
+ await handleAnswer(conversationId, answerPath, program);
761
+ });
762
+
763
+ program
764
+ .command('read')
765
+ .argument('<conversation-id>')
766
+ .description('Read a conversation transcript')
767
+ .option('--from <n>', 'First transcript entry index')
768
+ .option('--to <n>', 'Last transcript entry index')
769
+ .option('--after <n>', 'Only show entries after index N')
770
+ .option('--before <n>', 'Only show entries before index N')
771
+ .option('--tokens <n>', 'Approx token budget for the returned slice')
772
+ .option('--detail <level>', 'Detail level: standard, verbose, full, meta')
773
+ .option('--follow', 'Poll for new transcript entries while the conversation is running')
774
+ .action(async (conversationId, options) => {
775
+ await handleRead(conversationId, options, program);
776
+ });
777
+
778
+ program
779
+ .command('list')
780
+ .description('List conversations')
781
+ .action(async () => {
782
+ await handleList(program);
783
+ });
784
+
785
+ program
786
+ .command('info')
787
+ .argument('<conversation-id>')
788
+ .description('Show conversation metadata')
789
+ .action(async (conversationId) => {
790
+ await handleInfo(conversationId, program);
791
+ });
792
+
793
+ const job = program
794
+ .command('job')
795
+ .description('Inspect background jobs');
796
+
797
+ job
798
+ .command('list')
799
+ .description('List jobs')
800
+ .action(async () => {
801
+ await handleJobList(program);
802
+ });
803
+
804
+ job
805
+ .command('status')
806
+ .argument('<job-id>')
807
+ .description('Show job status')
808
+ .action(async (jobId) => {
809
+ await handleJobStatus(jobId, program);
810
+ });
811
+
812
+ job
813
+ .command('wait')
814
+ .argument('<job-id>')
815
+ .description('Wait for an async job to finish')
816
+ .option('--timeout <seconds>', 'Timeout in seconds')
817
+ .option('--interval <seconds>', 'Polling interval in seconds')
818
+ .action(async (jobId, options) => {
819
+ await handleJobWait(jobId, options, program);
820
+ });
821
+
822
+ job
823
+ .command('read')
824
+ .argument('<job-id>')
825
+ .description('Read transcript entries for a single job')
826
+ .action(async (jobId) => {
827
+ await handleJobRead(jobId, program);
828
+ });
829
+
830
+ job
831
+ .command('cancel')
832
+ .argument('<job-id>')
833
+ .description('Cancel a running job')
834
+ .action(async (jobId) => {
835
+ await handleJobCancel(jobId, program);
836
+ });
837
+
838
+ const daemon = program
839
+ .command('daemon')
840
+ .description('Manage the local cli-copilot-worker daemon');
841
+
842
+ daemon
843
+ .command('start')
844
+ .description('Start the daemon if it is not already running')
845
+ .action(async () => {
846
+ await handleDaemonStart(program);
847
+ });
848
+
849
+ daemon
850
+ .command('status')
851
+ .description('Show daemon status')
852
+ .action(async () => {
853
+ await handleDaemonStatus(program);
854
+ });
855
+
856
+ daemon
857
+ .command('stop')
858
+ .description('Stop the daemon')
859
+ .action(async () => {
860
+ await handleDaemonStop(program);
861
+ });
862
+
863
+ program
864
+ .command('doctor')
865
+ .description('Check local environment and Copilot auth')
866
+ .action(async () => {
867
+ await handleDoctor(program);
868
+ });
869
+
870
+ program
871
+ .command('daemon-run', { hidden: true })
872
+ .description('Internal daemon entrypoint')
873
+ .action(async () => {
874
+ await runDaemonServer();
875
+ });
876
+
877
+ await program.parseAsync(process.argv).catch((error) => {
878
+ const message = error instanceof Error ? error.message : String(error);
879
+ process.stderr.write(`${message}\n`);
880
+ process.exit(1);
881
+ });