@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.
@@ -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: !options.fix ? 'none' : options.yes ? 'fix_yes' : 'fix',
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
- const resolved = apiKeyStatus();
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 declare function sendDoctorCompleted(mcpUrl: string, payload: DoctorCompletedPayload, apiKey?: string): Promise<void>;
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 = doctorCompletedPayloadSchema.parse(payload);
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.36.1-next.293",
3
+ "version": "0.37.0",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {