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/doctor.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
|
|
5
|
+
import { CopilotClient } from '@github/copilot-sdk';
|
|
6
|
+
|
|
7
|
+
import { buildCopilotClientOptions } from './core/copilot.js';
|
|
8
|
+
import { stateRootDir } from './core/paths.js';
|
|
9
|
+
import { ProfileManager } from './core/profile-manager.js';
|
|
10
|
+
import { PersistentStore } from './core/store.js';
|
|
11
|
+
import { daemonIsRunning } from './daemon/client.js';
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
async function commandVersion(command: string, args: string[] = ['--version']): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
const { stdout, stderr } = await execFileAsync(command, args, { encoding: 'utf8' });
|
|
18
|
+
return (stdout || stderr).trim() || null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function inspectDoctor(): Promise<Record<string, unknown>> {
|
|
25
|
+
const store = new PersistentStore();
|
|
26
|
+
await store.load();
|
|
27
|
+
const profileManager = ProfileManager.fromEnvironment(store.getProfiles());
|
|
28
|
+
const profiles = profileManager.getProfiles();
|
|
29
|
+
const inspectedProfiles = await Promise.all(
|
|
30
|
+
profiles.map(async (profile) => {
|
|
31
|
+
const health = {
|
|
32
|
+
id: profile.id,
|
|
33
|
+
configDir: profile.configDir,
|
|
34
|
+
cooldownUntil: profile.cooldownUntil,
|
|
35
|
+
failureCount: profile.failureCount,
|
|
36
|
+
lastFailureReason: profile.lastFailureReason,
|
|
37
|
+
lastFailureCategory: profile.lastFailureCategory,
|
|
38
|
+
lastFailureAt: profile.lastFailureAt,
|
|
39
|
+
lastSuccessAt: profile.lastSuccessAt,
|
|
40
|
+
};
|
|
41
|
+
const client = new CopilotClient(
|
|
42
|
+
buildCopilotClientOptions({ cwd: process.cwd(), profile }),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await client.start();
|
|
47
|
+
const auth = await client.getAuthStatus();
|
|
48
|
+
const models = auth.isAuthenticated
|
|
49
|
+
? await client.listModels().catch(() => [])
|
|
50
|
+
: [];
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...health,
|
|
54
|
+
authenticated: auth.isAuthenticated,
|
|
55
|
+
login: auth.login,
|
|
56
|
+
models: models.slice(0, 10).map((model) => model.id),
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
...health,
|
|
61
|
+
authenticated: false,
|
|
62
|
+
error: error instanceof Error ? error.message : String(error),
|
|
63
|
+
models: [],
|
|
64
|
+
};
|
|
65
|
+
} finally {
|
|
66
|
+
await client.stop().catch(async () => {
|
|
67
|
+
await client.forceStop().catch(() => {});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
node: process.version,
|
|
75
|
+
copilot: await commandVersion('copilot'),
|
|
76
|
+
mcpc: await commandVersion('mcpc'),
|
|
77
|
+
cwd: process.cwd(),
|
|
78
|
+
daemonRunning: await daemonIsRunning(),
|
|
79
|
+
stateRoot: stateRootDir(),
|
|
80
|
+
profiles: inspectedProfiles,
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/index.ts
ADDED
package/src/output.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
|
|
4
|
+
import type { PendingQuestion, TranscriptEntry } from './core/types.js';
|
|
5
|
+
|
|
6
|
+
export type OutputFormat = 'text' | 'json';
|
|
7
|
+
|
|
8
|
+
export interface EventPrinter {
|
|
9
|
+
onEvent: (event: string, data: Record<string, unknown>) => void;
|
|
10
|
+
finish: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveOutputFormat(explicit?: string | undefined): OutputFormat {
|
|
14
|
+
if (!explicit) {
|
|
15
|
+
return process.stdout.isTTY ? 'text' : 'json';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (explicit === 'text' || explicit === 'json') {
|
|
19
|
+
return explicit;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
throw new Error(`Invalid output format: ${explicit}. Use "text" or "json".`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function printJson(data: unknown): void {
|
|
26
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function shortenPath(path: string): string {
|
|
30
|
+
const home = homedir();
|
|
31
|
+
return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatActions(actions?: Record<string, unknown> | undefined): string {
|
|
35
|
+
const lines = Object.entries(actions ?? {})
|
|
36
|
+
.filter(([, value]) => typeof value === 'string' && value.length > 0)
|
|
37
|
+
.map(([name, value]) => `- ${name}: ${String(value)}`);
|
|
38
|
+
|
|
39
|
+
return lines.length > 0 ? lines.join('\n') : '- none';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatPendingQuestion(question?: PendingQuestion | undefined): string {
|
|
43
|
+
if (!question) {
|
|
44
|
+
return 'No pending question.';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lines = [
|
|
48
|
+
question.question,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
if (question.choices && question.choices.length > 0) {
|
|
52
|
+
lines.push('');
|
|
53
|
+
question.choices.forEach((choice, index) => {
|
|
54
|
+
lines.push(`${index + 1}. ${choice}`);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
lines.push('');
|
|
59
|
+
lines.push(`Freeform allowed: ${question.allowFreeform ? 'yes' : 'no'}`);
|
|
60
|
+
return lines.join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
64
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function stringValue(value: unknown): string | undefined {
|
|
68
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function numberValue(value: unknown): number | undefined {
|
|
72
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function trimText(value: string, maxLength = 140): string {
|
|
76
|
+
if (value.length <= maxLength) {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `${value.slice(0, maxLength - 1)}…`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatOutcomeDetails(attempt: Record<string, unknown>): string | undefined {
|
|
84
|
+
const category = stringValue(attempt.failureCategory);
|
|
85
|
+
const message = stringValue(attempt.failureMessage);
|
|
86
|
+
|
|
87
|
+
if (!category && !message) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (category && message) {
|
|
92
|
+
return `[${category}] ${trimText(message)}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return category ? `[${category}]` : trimText(message!);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatAttemptMeta(attempt: Record<string, unknown>): string[] {
|
|
99
|
+
const lines: string[] = [];
|
|
100
|
+
const startedAt = stringValue(attempt.startedAt);
|
|
101
|
+
const endedAt = stringValue(attempt.endedAt);
|
|
102
|
+
const sessionErrorType = stringValue(attempt.sessionErrorType);
|
|
103
|
+
const sessionStatusCode = numberValue(attempt.sessionStatusCode);
|
|
104
|
+
|
|
105
|
+
if (startedAt) {
|
|
106
|
+
lines.push(`started: ${startedAt}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (endedAt) {
|
|
110
|
+
lines.push(`ended: ${endedAt}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (sessionErrorType || sessionStatusCode !== undefined) {
|
|
114
|
+
const sessionBits: string[] = [];
|
|
115
|
+
if (sessionErrorType) {
|
|
116
|
+
sessionBits.push(`type=${sessionErrorType}`);
|
|
117
|
+
}
|
|
118
|
+
if (sessionStatusCode !== undefined) {
|
|
119
|
+
sessionBits.push(`status=${sessionStatusCode}`);
|
|
120
|
+
}
|
|
121
|
+
lines.push(`session: ${sessionBits.join(' ')}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return lines;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function formatAttemptHistory(attempts: unknown): string {
|
|
128
|
+
const records = Array.isArray(attempts)
|
|
129
|
+
? attempts.filter(isRecord)
|
|
130
|
+
: [];
|
|
131
|
+
|
|
132
|
+
if (records.length === 0) {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const lines: string[] = ['Attempts:'];
|
|
137
|
+
const firstProfile = stringValue(records[0]?.profileId);
|
|
138
|
+
const lastProfile = stringValue(records[records.length - 1]?.profileId);
|
|
139
|
+
const firstConfigDir = stringValue(records[0]?.profileConfigDir);
|
|
140
|
+
const lastConfigDir = stringValue(records[records.length - 1]?.profileConfigDir);
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
records.length > 1 &&
|
|
144
|
+
firstProfile &&
|
|
145
|
+
lastProfile &&
|
|
146
|
+
firstProfile !== lastProfile
|
|
147
|
+
) {
|
|
148
|
+
const from = firstConfigDir ? `${firstProfile} ${shortenPath(firstConfigDir)}` : firstProfile;
|
|
149
|
+
const to = lastConfigDir ? `${lastProfile} ${shortenPath(lastConfigDir)}` : lastProfile;
|
|
150
|
+
lines.push(`Failover: ${from} -> ${to}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
records.forEach((attempt, index) => {
|
|
154
|
+
const profileId = stringValue(attempt.profileId) ?? 'unknown';
|
|
155
|
+
const configDir = stringValue(attempt.profileConfigDir);
|
|
156
|
+
const outcome = stringValue(attempt.outcome) ?? 'unknown';
|
|
157
|
+
const outcomeDetails = formatOutcomeDetails(attempt);
|
|
158
|
+
|
|
159
|
+
const headlineParts = [`- ${index + 1}. ${profileId}`];
|
|
160
|
+
if (configDir) {
|
|
161
|
+
headlineParts.push(shortenPath(configDir));
|
|
162
|
+
}
|
|
163
|
+
headlineParts.push(outcome);
|
|
164
|
+
if (outcomeDetails) {
|
|
165
|
+
headlineParts.push(outcomeDetails);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
lines.push(headlineParts.join(' '));
|
|
169
|
+
for (const detail of formatAttemptMeta(attempt)) {
|
|
170
|
+
lines.push(` ${detail}`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function formatModelSummary(models: unknown): string | undefined {
|
|
178
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const ids = models.filter((model): model is string => typeof model === 'string' && model.length > 0);
|
|
183
|
+
if (ids.length === 0) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const preview = ids.slice(0, 3);
|
|
188
|
+
const suffix = ids.length > preview.length ? ', …' : '';
|
|
189
|
+
return `models=${ids.length} [${preview.join(', ')}${suffix}]`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatHealthSummary(profile: Record<string, unknown>, now = Date.now()): string {
|
|
193
|
+
const cooldownUntil = numberValue(profile.cooldownUntil);
|
|
194
|
+
const failureCount = numberValue(profile.failureCount);
|
|
195
|
+
const lastFailureCategory = stringValue(profile.lastFailureCategory);
|
|
196
|
+
const lastFailureReason = stringValue(profile.lastFailureReason);
|
|
197
|
+
const lastSuccessAt = stringValue(profile.lastSuccessAt);
|
|
198
|
+
|
|
199
|
+
if (cooldownUntil !== undefined && cooldownUntil > now) {
|
|
200
|
+
const parts = [
|
|
201
|
+
'cooling-down',
|
|
202
|
+
`until=${new Date(cooldownUntil).toISOString()}`,
|
|
203
|
+
];
|
|
204
|
+
if (lastFailureCategory || lastFailureReason) {
|
|
205
|
+
parts.push(`lastFailure=${formatOutcomeDetails({
|
|
206
|
+
failureCategory: lastFailureCategory,
|
|
207
|
+
failureMessage: lastFailureReason,
|
|
208
|
+
}) ?? 'unknown'}`);
|
|
209
|
+
}
|
|
210
|
+
if (failureCount !== undefined) {
|
|
211
|
+
parts.push(`failures=${failureCount}`);
|
|
212
|
+
}
|
|
213
|
+
return parts.join(' ');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const parts = [failureCount && failureCount > 0 ? 'degraded' : 'healthy'];
|
|
217
|
+
if (failureCount !== undefined) {
|
|
218
|
+
parts.push(`failures=${failureCount}`);
|
|
219
|
+
}
|
|
220
|
+
if (lastFailureCategory || lastFailureReason) {
|
|
221
|
+
parts.push(`lastFailure=${formatOutcomeDetails({
|
|
222
|
+
failureCategory: lastFailureCategory,
|
|
223
|
+
failureMessage: lastFailureReason,
|
|
224
|
+
}) ?? 'unknown'}`);
|
|
225
|
+
}
|
|
226
|
+
if (lastSuccessAt) {
|
|
227
|
+
parts.push(`lastSuccess=${lastSuccessAt}`);
|
|
228
|
+
}
|
|
229
|
+
return parts.join(' ');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function formatProfileSummary(
|
|
233
|
+
profile: Record<string, unknown>,
|
|
234
|
+
options?: {
|
|
235
|
+
includeAuth?: boolean | undefined;
|
|
236
|
+
includeHealth?: boolean | undefined;
|
|
237
|
+
includeModels?: boolean | undefined;
|
|
238
|
+
now?: number | undefined;
|
|
239
|
+
},
|
|
240
|
+
): string {
|
|
241
|
+
const parts = [
|
|
242
|
+
`- ${stringValue(profile.id) ?? 'unknown'}`,
|
|
243
|
+
stringValue(profile.configDir) ? shortenPath(stringValue(profile.configDir)!) : undefined,
|
|
244
|
+
].filter((part): part is string => part !== undefined);
|
|
245
|
+
|
|
246
|
+
if (options?.includeAuth) {
|
|
247
|
+
parts.push(`authenticated=${String(profile.authenticated ?? false)}`);
|
|
248
|
+
const login = stringValue(profile.login);
|
|
249
|
+
if (login) {
|
|
250
|
+
parts.push(`login=${login}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (options?.includeModels) {
|
|
255
|
+
const models = formatModelSummary(profile.models);
|
|
256
|
+
if (models) {
|
|
257
|
+
parts.push(models);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (options?.includeHealth ?? true) {
|
|
262
|
+
parts.push(`health=${formatHealthSummary(profile, options?.now)}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const error = stringValue(profile.error);
|
|
266
|
+
if (error) {
|
|
267
|
+
parts.push(`error=${trimText(error)}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return parts.join(' ');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function formatProfilesSection(
|
|
274
|
+
profiles: unknown,
|
|
275
|
+
options?: {
|
|
276
|
+
includeAuth?: boolean | undefined;
|
|
277
|
+
includeHealth?: boolean | undefined;
|
|
278
|
+
includeModels?: boolean | undefined;
|
|
279
|
+
now?: number | undefined;
|
|
280
|
+
},
|
|
281
|
+
): string {
|
|
282
|
+
const items = Array.isArray(profiles)
|
|
283
|
+
? profiles.filter(isRecord)
|
|
284
|
+
: [];
|
|
285
|
+
|
|
286
|
+
if (items.length === 0) {
|
|
287
|
+
return '';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return [
|
|
291
|
+
'Profiles:',
|
|
292
|
+
...items.map((profile) => formatProfileSummary(profile, options)),
|
|
293
|
+
].join('\n');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
type TranscriptRenderableEntry = Pick<TranscriptEntry, 'index' | 'role' | 'content' | 'timestamp' | 'data'>;
|
|
297
|
+
|
|
298
|
+
export function formatEntries(entries: TranscriptRenderableEntry[]): string {
|
|
299
|
+
if (entries.length === 0) {
|
|
300
|
+
return 'No transcript entries.';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return entries
|
|
304
|
+
.map((entry) => {
|
|
305
|
+
const header = `${entry.index}. ${entry.role} @ ${entry.timestamp}`;
|
|
306
|
+
const content = entry.content.trim().length > 0 ? entry.content : JSON.stringify(entry.data ?? {});
|
|
307
|
+
const body = content
|
|
308
|
+
.split('\n')
|
|
309
|
+
.map((line) => ` ${line}`)
|
|
310
|
+
.join('\n');
|
|
311
|
+
return `${header}\n${body}`;
|
|
312
|
+
})
|
|
313
|
+
.join('\n\n');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function createEventPrinter(enabled: boolean): EventPrinter {
|
|
317
|
+
let assistantOpen = false;
|
|
318
|
+
|
|
319
|
+
const ensureNewline = () => {
|
|
320
|
+
if (assistantOpen) {
|
|
321
|
+
process.stdout.write('\n');
|
|
322
|
+
assistantOpen = false;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
onEvent(event, data) {
|
|
328
|
+
if (!enabled) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
switch (event) {
|
|
333
|
+
case 'assistant.delta': {
|
|
334
|
+
const text = typeof data.text === 'string' ? data.text : '';
|
|
335
|
+
if (!text) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
process.stdout.write(text);
|
|
339
|
+
assistantOpen = !text.endsWith('\n');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
case 'status': {
|
|
343
|
+
ensureNewline();
|
|
344
|
+
const message = typeof data.message === 'string' ? data.message : JSON.stringify(data);
|
|
345
|
+
process.stdout.write(`[status] ${message}\n`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
case 'tool.start': {
|
|
349
|
+
ensureNewline();
|
|
350
|
+
process.stdout.write(`[tool] start ${String(data.toolName ?? 'unknown')}\n`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
case 'tool.complete': {
|
|
354
|
+
ensureNewline();
|
|
355
|
+
process.stdout.write(
|
|
356
|
+
`[tool] complete ${String(data.toolName ?? 'unknown')} success=${String(data.success ?? false)}\n`,
|
|
357
|
+
);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
case 'question': {
|
|
361
|
+
ensureNewline();
|
|
362
|
+
const question = typeof data.question === 'string' ? data.question : 'Question requested';
|
|
363
|
+
process.stdout.write(`[question] ${question}\n`);
|
|
364
|
+
const choices = Array.isArray(data.choices) ? data.choices.filter((item): item is string => typeof item === 'string') : [];
|
|
365
|
+
choices.forEach((choice, index) => {
|
|
366
|
+
process.stdout.write(` ${index + 1}. ${choice}\n`);
|
|
367
|
+
});
|
|
368
|
+
process.stdout.write(` freeform=${String(data.allowFreeform ?? true)}\n`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
case 'answer': {
|
|
372
|
+
ensureNewline();
|
|
373
|
+
process.stdout.write(`[answer] ${String(data.answer ?? 'submitted')}\n`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
case 'error': {
|
|
377
|
+
ensureNewline();
|
|
378
|
+
process.stdout.write(`[error] ${String(data.message ?? JSON.stringify(data))}\n`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
default: {
|
|
382
|
+
ensureNewline();
|
|
383
|
+
process.stdout.write(`[${event}] ${JSON.stringify(data)}\n`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
finish() {
|
|
388
|
+
ensureNewline();
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|