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.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/bin/cli-copilot-worker.mjs +3 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +682 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/core/copilot.d.ts +13 -0
- package/dist/src/core/copilot.js +56 -0
- package/dist/src/core/copilot.js.map +1 -0
- package/dist/src/core/failure-classifier.d.ts +8 -0
- package/dist/src/core/failure-classifier.js +148 -0
- package/dist/src/core/failure-classifier.js.map +1 -0
- package/dist/src/core/ids.d.ts +1 -0
- package/dist/src/core/ids.js +11 -0
- package/dist/src/core/ids.js.map +1 -0
- package/dist/src/core/markdown.d.ts +5 -0
- package/dist/src/core/markdown.js +14 -0
- package/dist/src/core/markdown.js.map +1 -0
- package/dist/src/core/paths.d.ts +18 -0
- package/dist/src/core/paths.js +42 -0
- package/dist/src/core/paths.js.map +1 -0
- package/dist/src/core/profile-faults.d.ts +15 -0
- package/dist/src/core/profile-faults.js +110 -0
- package/dist/src/core/profile-faults.js.map +1 -0
- package/dist/src/core/profile-manager.d.ts +25 -0
- package/dist/src/core/profile-manager.js +162 -0
- package/dist/src/core/profile-manager.js.map +1 -0
- package/dist/src/core/question-registry.d.ts +25 -0
- package/dist/src/core/question-registry.js +154 -0
- package/dist/src/core/question-registry.js.map +1 -0
- package/dist/src/core/store.d.ts +39 -0
- package/dist/src/core/store.js +206 -0
- package/dist/src/core/store.js.map +1 -0
- package/dist/src/core/types.d.ts +152 -0
- package/dist/src/core/types.js +2 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/daemon/client.d.ts +6 -0
- package/dist/src/daemon/client.js +117 -0
- package/dist/src/daemon/client.js.map +1 -0
- package/dist/src/daemon/server.d.ts +1 -0
- package/dist/src/daemon/server.js +149 -0
- package/dist/src/daemon/server.js.map +1 -0
- package/dist/src/daemon/service.d.ts +69 -0
- package/dist/src/daemon/service.js +800 -0
- package/dist/src/daemon/service.js.map +1 -0
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +74 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/output.d.ts +28 -0
- package/dist/src/output.js +307 -0
- package/dist/src/output.js.map +1 -0
- package/package.json +59 -0
- package/src/cli.ts +881 -0
- package/src/core/copilot.ts +75 -0
- package/src/core/failure-classifier.ts +202 -0
- package/src/core/ids.ts +11 -0
- package/src/core/markdown.ts +19 -0
- package/src/core/paths.ts +56 -0
- package/src/core/profile-faults.ts +140 -0
- package/src/core/profile-manager.ts +220 -0
- package/src/core/question-registry.ts +191 -0
- package/src/core/store.ts +273 -0
- package/src/core/types.ts +211 -0
- package/src/daemon/client.ts +137 -0
- package/src/daemon/server.ts +167 -0
- package/src/daemon/service.ts +968 -0
- package/src/doctor.ts +82 -0
- package/src/index.ts +3 -0
- 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
|
+
});
|