@tiny-fish/cli 0.36.1-next.293 → 0.37.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/dist/commands/doctor.js +28 -4
- package/dist/lib/setup-telemetry.d.ts +125 -1
- package/dist/lib/setup-telemetry.js +34 -1
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { ALL_HARNESSES, AuthMode, Registered } from '../lib/harness-detect.js';
|
|
|
6
6
|
import { detectHumanInitiated } from '../lib/harness.js';
|
|
7
7
|
import { err, errLine, out, outLine } from '../lib/output.js';
|
|
8
8
|
import { detectRegistrations } from '../lib/registration-detect.js';
|
|
9
|
-
import { sendDoctorCompleted } from '../lib/setup-telemetry.js';
|
|
9
|
+
import { doctorErrorClass, sendDoctorCompleted, } from '../lib/setup-telemetry.js';
|
|
10
10
|
import { verifyMcpAuth, verifyMcpHealth } from '../lib/verify.js';
|
|
11
11
|
import { resolveHermesHome } from '../lib/hermes-env.js';
|
|
12
12
|
import { hermesWebBackendsOurs, readHermesPluginStatus, } from '../lib/hermes-plugin.js';
|
|
@@ -423,11 +423,15 @@ export async function applyRepairs(repairs, options) {
|
|
|
423
423
|
}
|
|
424
424
|
/** Mirrors the route's cap; over it the payload would 400 and never be retried. */
|
|
425
425
|
const MAX_DURATION_MS = 3_600_000;
|
|
426
|
+
function fixModeOf(options) {
|
|
427
|
+
return !options.fix ? 'none' : options.yes ? 'fix_yes' : 'fix';
|
|
428
|
+
}
|
|
426
429
|
/** Enumerated fields only; check `detail` text never leaves the machine. */
|
|
427
430
|
function telemetryPayload(report, offered, outcomes, options, durationMs) {
|
|
428
431
|
const count = (status) => report.checks.filter((check) => check.status === status).length;
|
|
429
432
|
const failed = report.checks.filter((check) => check.status === 'fail');
|
|
430
433
|
return {
|
|
434
|
+
outcome: 'completed',
|
|
431
435
|
schema_version: report.schema_version,
|
|
432
436
|
cli_version: report.cli_version,
|
|
433
437
|
ok_harnesses: report.ok_harnesses,
|
|
@@ -441,7 +445,7 @@ function telemetryPayload(report, offered, outcomes, options, durationMs) {
|
|
|
441
445
|
repair_actions: [...new Set(offered.map((repair) => repair.action))],
|
|
442
446
|
repairs_attempted: outcomes.filter((outcome) => outcome.status !== 'skipped').length,
|
|
443
447
|
repairs_succeeded: outcomes.filter((outcome) => outcome.status === 'repaired').length,
|
|
444
|
-
fix_mode:
|
|
448
|
+
fix_mode: fixModeOf(options),
|
|
445
449
|
// --harness scopes detection, so the counts above cover that harness alone.
|
|
446
450
|
harness_scope: options.harness ?? 'all',
|
|
447
451
|
// Same signal cli_connect_stage carries, so agent and human doctor runs split.
|
|
@@ -454,6 +458,25 @@ function telemetryPayload(report, offered, outcomes, options, durationMs) {
|
|
|
454
458
|
duration_ms: Math.min(durationMs, MAX_DURATION_MS),
|
|
455
459
|
};
|
|
456
460
|
}
|
|
461
|
+
/** Both paths send keyless when there is no key; the route records those anonymously. */
|
|
462
|
+
function telemetryKey() {
|
|
463
|
+
const resolved = apiKeyStatus();
|
|
464
|
+
return 'key' in resolved ? resolved.key : undefined;
|
|
465
|
+
}
|
|
466
|
+
/** Enumerated outcome and timing only; the raw error stays behind --debug. */
|
|
467
|
+
function couldNotRunPayload(error, options, durationMs) {
|
|
468
|
+
return {
|
|
469
|
+
outcome: 'could_not_run',
|
|
470
|
+
error_class: doctorErrorClass(error),
|
|
471
|
+
schema_version: DOCTOR_SCHEMA_VERSION,
|
|
472
|
+
cli_version: CLI_VERSION,
|
|
473
|
+
fix_mode: fixModeOf(options),
|
|
474
|
+
harness_scope: options.harness ?? 'all',
|
|
475
|
+
is_human_initiated: detectHumanInitiated(),
|
|
476
|
+
// Clamped like the completed row: over the cap the payload would 400 un-retried.
|
|
477
|
+
duration_ms: Math.min(durationMs, MAX_DURATION_MS),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
457
480
|
export function registerDoctor(program) {
|
|
458
481
|
program
|
|
459
482
|
.command('doctor')
|
|
@@ -500,6 +523,8 @@ export function registerDoctor(program) {
|
|
|
500
523
|
}
|
|
501
524
|
err({ error: 'doctor could not complete; re-run with --debug for the raw error' });
|
|
502
525
|
process.exitCode = DOCTOR_COULD_NOT_RUN;
|
|
526
|
+
// After the exit code is set: telemetry never decides how doctor exits.
|
|
527
|
+
await sendDoctorCompleted(doctorOptions.mcpUrl, couldNotRunPayload(e, doctorOptions, Math.round(performance.now() - startedAt)), telemetryKey());
|
|
503
528
|
return;
|
|
504
529
|
}
|
|
505
530
|
if (options.pretty)
|
|
@@ -508,7 +533,6 @@ export function registerDoctor(program) {
|
|
|
508
533
|
out(report);
|
|
509
534
|
process.exitCode = exitCodeFor(report.checks);
|
|
510
535
|
// After the exit code is set: telemetry never decides how doctor exits.
|
|
511
|
-
|
|
512
|
-
await sendDoctorCompleted(doctorOptions.mcpUrl, telemetryPayload(report, offered, outcomes, doctorOptions, Math.round(performance.now() - startedAt)), 'key' in resolved ? resolved.key : undefined);
|
|
536
|
+
await sendDoctorCompleted(doctorOptions.mcpUrl, telemetryPayload(report, offered, outcomes, doctorOptions, Math.round(performance.now() - startedAt)), telemetryKey());
|
|
513
537
|
});
|
|
514
538
|
}
|
|
@@ -80,7 +80,12 @@ export type UpgradeScope = 'both' | 'cli' | 'skill';
|
|
|
80
80
|
export declare function sendUpgradeCompleted(fromVersion: string, toVersion: string | null, outcome: UpgradeOutcome, scope: UpgradeScope, apiKey?: string): Promise<void>;
|
|
81
81
|
export declare function sendSetupCompleted(mcpUrl: string, harnesses: HarnessResult[], apiKey?: string, mode?: SetupMode, attemptId?: string): Promise<void>;
|
|
82
82
|
export declare const DOCTOR_HARNESS_SCOPES: readonly ["all", ...("openclaw" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code")[]];
|
|
83
|
+
export declare const DOCTOR_ERROR_CLASSES: readonly ["command_not_found", "timeout", "spawn_error", "nonzero_exit", "unexpected_error"];
|
|
84
|
+
export type DoctorErrorClass = (typeof DOCTOR_ERROR_CLASSES)[number];
|
|
85
|
+
/** Derived, not read: the catch is generic, so only the code is trustworthy. */
|
|
86
|
+
export declare function doctorErrorClass(error: unknown): DoctorErrorClass;
|
|
83
87
|
export declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
88
|
+
outcome: z.ZodLiteral<"completed">;
|
|
84
89
|
schema_version: z.ZodInt;
|
|
85
90
|
cli_version: z.ZodString;
|
|
86
91
|
ok_harnesses: z.ZodBoolean;
|
|
@@ -139,6 +144,125 @@ export declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
|
139
144
|
}, z.core.$strip>>;
|
|
140
145
|
duration_ms: z.ZodInt;
|
|
141
146
|
}, z.core.$strip>;
|
|
147
|
+
export declare const doctorCouldNotRunPayloadSchema: z.ZodObject<{
|
|
148
|
+
outcome: z.ZodLiteral<"could_not_run">;
|
|
149
|
+
error_class: z.ZodEnum<{
|
|
150
|
+
timeout: "timeout";
|
|
151
|
+
command_not_found: "command_not_found";
|
|
152
|
+
spawn_error: "spawn_error";
|
|
153
|
+
nonzero_exit: "nonzero_exit";
|
|
154
|
+
unexpected_error: "unexpected_error";
|
|
155
|
+
}>;
|
|
156
|
+
schema_version: z.ZodInt;
|
|
157
|
+
cli_version: z.ZodString;
|
|
158
|
+
fix_mode: z.ZodEnum<{
|
|
159
|
+
none: "none";
|
|
160
|
+
fix: "fix";
|
|
161
|
+
fix_yes: "fix_yes";
|
|
162
|
+
}>;
|
|
163
|
+
harness_scope: z.ZodEnum<{
|
|
164
|
+
openclaw: "openclaw";
|
|
165
|
+
grok: "grok";
|
|
166
|
+
cursor: "cursor";
|
|
167
|
+
codex: "codex";
|
|
168
|
+
hermes: "hermes";
|
|
169
|
+
opencode: "opencode";
|
|
170
|
+
"claude-code": "claude-code";
|
|
171
|
+
all: "all";
|
|
172
|
+
}>;
|
|
173
|
+
is_human_initiated: z.ZodBoolean;
|
|
174
|
+
duration_ms: z.ZodInt;
|
|
175
|
+
}, z.core.$strip>;
|
|
176
|
+
declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
177
|
+
outcome: z.ZodLiteral<"completed">;
|
|
178
|
+
schema_version: z.ZodInt;
|
|
179
|
+
cli_version: z.ZodString;
|
|
180
|
+
ok_harnesses: z.ZodBoolean;
|
|
181
|
+
ok_cli: z.ZodBoolean;
|
|
182
|
+
checks_pass: z.ZodInt;
|
|
183
|
+
checks_fail: z.ZodInt;
|
|
184
|
+
checks_warn: z.ZodInt;
|
|
185
|
+
checks_skip: z.ZodInt;
|
|
186
|
+
repairs_offered: z.ZodInt;
|
|
187
|
+
repair_actions: z.ZodArray<z.ZodEnum<{
|
|
188
|
+
connect: "connect";
|
|
189
|
+
"auth-login": "auth-login";
|
|
190
|
+
}>>;
|
|
191
|
+
repairs_attempted: z.ZodInt;
|
|
192
|
+
repairs_succeeded: z.ZodInt;
|
|
193
|
+
fix_mode: z.ZodEnum<{
|
|
194
|
+
none: "none";
|
|
195
|
+
fix: "fix";
|
|
196
|
+
fix_yes: "fix_yes";
|
|
197
|
+
}>;
|
|
198
|
+
harness_scope: z.ZodEnum<{
|
|
199
|
+
openclaw: "openclaw";
|
|
200
|
+
grok: "grok";
|
|
201
|
+
cursor: "cursor";
|
|
202
|
+
codex: "codex";
|
|
203
|
+
hermes: "hermes";
|
|
204
|
+
opencode: "opencode";
|
|
205
|
+
"claude-code": "claude-code";
|
|
206
|
+
all: "all";
|
|
207
|
+
}>;
|
|
208
|
+
is_human_initiated: z.ZodBoolean;
|
|
209
|
+
failed_check_ids: z.ZodArray<z.ZodString>;
|
|
210
|
+
failed_harnesses: z.ZodArray<z.ZodEnum<{
|
|
211
|
+
openclaw: "openclaw";
|
|
212
|
+
grok: "grok";
|
|
213
|
+
cursor: "cursor";
|
|
214
|
+
codex: "codex";
|
|
215
|
+
hermes: "hermes";
|
|
216
|
+
opencode: "opencode";
|
|
217
|
+
"claude-code": "claude-code";
|
|
218
|
+
}>>;
|
|
219
|
+
harnesses: z.ZodArray<z.ZodObject<{
|
|
220
|
+
harness: z.ZodEnum<{
|
|
221
|
+
openclaw: "openclaw";
|
|
222
|
+
grok: "grok";
|
|
223
|
+
cursor: "cursor";
|
|
224
|
+
codex: "codex";
|
|
225
|
+
hermes: "hermes";
|
|
226
|
+
opencode: "opencode";
|
|
227
|
+
"claude-code": "claude-code";
|
|
228
|
+
}>;
|
|
229
|
+
detected: z.ZodBoolean;
|
|
230
|
+
registered: z.ZodEnum<typeof Registered>;
|
|
231
|
+
auth_mode: z.ZodEnum<typeof AuthMode>;
|
|
232
|
+
proves_harness_reach: z.ZodBoolean;
|
|
233
|
+
}, z.core.$strip>>;
|
|
234
|
+
duration_ms: z.ZodInt;
|
|
235
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
236
|
+
outcome: z.ZodLiteral<"could_not_run">;
|
|
237
|
+
error_class: z.ZodEnum<{
|
|
238
|
+
timeout: "timeout";
|
|
239
|
+
command_not_found: "command_not_found";
|
|
240
|
+
spawn_error: "spawn_error";
|
|
241
|
+
nonzero_exit: "nonzero_exit";
|
|
242
|
+
unexpected_error: "unexpected_error";
|
|
243
|
+
}>;
|
|
244
|
+
schema_version: z.ZodInt;
|
|
245
|
+
cli_version: z.ZodString;
|
|
246
|
+
fix_mode: z.ZodEnum<{
|
|
247
|
+
none: "none";
|
|
248
|
+
fix: "fix";
|
|
249
|
+
fix_yes: "fix_yes";
|
|
250
|
+
}>;
|
|
251
|
+
harness_scope: z.ZodEnum<{
|
|
252
|
+
openclaw: "openclaw";
|
|
253
|
+
grok: "grok";
|
|
254
|
+
cursor: "cursor";
|
|
255
|
+
codex: "codex";
|
|
256
|
+
hermes: "hermes";
|
|
257
|
+
opencode: "opencode";
|
|
258
|
+
"claude-code": "claude-code";
|
|
259
|
+
all: "all";
|
|
260
|
+
}>;
|
|
261
|
+
is_human_initiated: z.ZodBoolean;
|
|
262
|
+
duration_ms: z.ZodInt;
|
|
263
|
+
}, z.core.$strip>], "outcome">;
|
|
142
264
|
export type DoctorCompletedPayload = z.infer<typeof doctorCompletedPayloadSchema>;
|
|
143
|
-
export
|
|
265
|
+
export type DoctorCouldNotRunPayload = z.infer<typeof doctorCouldNotRunPayloadSchema>;
|
|
266
|
+
export type DoctorPayload = z.infer<typeof doctorPayloadSchema>;
|
|
267
|
+
export declare function sendDoctorCompleted(mcpUrl: string, payload: DoctorPayload, apiKey?: string): Promise<void>;
|
|
144
268
|
export {};
|
|
@@ -191,7 +191,25 @@ export const DOCTOR_HARNESS_SCOPES = ['all', ...ALL_HARNESSES];
|
|
|
191
191
|
// user-authored or credential-shaped can ride a doctor run to PostHog. `detail` never leaves.
|
|
192
192
|
// Caps mirror the route's schema: a payload the server would 400 is dropped here instead,
|
|
193
193
|
// because postConnectEvent never retries a 4xx and the event would vanish silently.
|
|
194
|
+
// Same spellings as cli_connect_stage.failure_reason; the type pins them.
|
|
195
|
+
export const DOCTOR_ERROR_CLASSES = [
|
|
196
|
+
'command_not_found',
|
|
197
|
+
'timeout',
|
|
198
|
+
'spawn_error',
|
|
199
|
+
'nonzero_exit',
|
|
200
|
+
'unexpected_error',
|
|
201
|
+
];
|
|
202
|
+
/** Derived, not read: the catch is generic, so only the code is trustworthy. */
|
|
203
|
+
export function doctorErrorClass(error) {
|
|
204
|
+
const code = error?.code;
|
|
205
|
+
if (code === 'ENOENT')
|
|
206
|
+
return 'command_not_found';
|
|
207
|
+
if (code === 'ETIMEDOUT')
|
|
208
|
+
return 'timeout';
|
|
209
|
+
return code ? 'spawn_error' : 'unexpected_error';
|
|
210
|
+
}
|
|
194
211
|
export const doctorCompletedPayloadSchema = z.object({
|
|
212
|
+
outcome: z.literal('completed'),
|
|
195
213
|
schema_version: z.int().positive(),
|
|
196
214
|
cli_version: z.string().max(32),
|
|
197
215
|
ok_harnesses: z.boolean(),
|
|
@@ -223,6 +241,21 @@ export const doctorCompletedPayloadSchema = z.object({
|
|
|
223
241
|
.max(16),
|
|
224
242
|
duration_ms: z.int().nonnegative().max(3_600_000),
|
|
225
243
|
});
|
|
244
|
+
// Detection threw, so there is no report to aggregate: enumerated outcome and timing only.
|
|
245
|
+
export const doctorCouldNotRunPayloadSchema = z.object({
|
|
246
|
+
outcome: z.literal('could_not_run'),
|
|
247
|
+
error_class: z.enum(DOCTOR_ERROR_CLASSES),
|
|
248
|
+
schema_version: z.int().positive(),
|
|
249
|
+
cli_version: z.string().max(32),
|
|
250
|
+
fix_mode: z.enum(['none', 'fix', 'fix_yes']),
|
|
251
|
+
harness_scope: z.enum(DOCTOR_HARNESS_SCOPES),
|
|
252
|
+
is_human_initiated: z.boolean(),
|
|
253
|
+
duration_ms: z.int().nonnegative().max(3_600_000),
|
|
254
|
+
});
|
|
255
|
+
const doctorPayloadSchema = z.discriminatedUnion('outcome', [
|
|
256
|
+
doctorCompletedPayloadSchema,
|
|
257
|
+
doctorCouldNotRunPayloadSchema,
|
|
258
|
+
]);
|
|
226
259
|
export async function sendDoctorCompleted(mcpUrl, payload, apiKey) {
|
|
227
260
|
if (telemetryDisabled())
|
|
228
261
|
return;
|
|
@@ -230,7 +263,7 @@ export async function sendDoctorCompleted(mcpUrl, payload, apiKey) {
|
|
|
230
263
|
let body;
|
|
231
264
|
try {
|
|
232
265
|
// Parse inside try — a schema violation must not fail a diagnosis that already ran.
|
|
233
|
-
const parsed =
|
|
266
|
+
const parsed = doctorPayloadSchema.parse(payload);
|
|
234
267
|
endpoint = new URL('/api/cli/connect-event', mcpUrl).toString();
|
|
235
268
|
// Stamped after the parse: process-derived and bounded, cannot trip route caps.
|
|
236
269
|
body = JSON.stringify({
|