@tiny-fish/cli 0.38.1-next.303 → 0.38.1-next.305

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.
@@ -354,7 +354,10 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
354
354
  apiKey: validatedApiKey(options.apiKey),
355
355
  verbose: options.verbose ?? false,
356
356
  });
357
- telemetry.track('checkpoint', { phase: 'plugin_installed' });
357
+ telemetry.track('checkpoint', {
358
+ phase: 'plugin_installed',
359
+ pluginVersion: client.pluginVersion,
360
+ });
358
361
  });
359
362
  }
360
363
  // Before auth: a failed sign-in must not lose the harness record.
@@ -4,12 +4,12 @@ import { DOCTOR_COULD_NOT_RUN, DOCTOR_INVALID_INPUT, DOCTOR_SCHEMA_VERSION, doct
4
4
  import { applyRepairs, repairsFor } from '../lib/doctor-repairs.js';
5
5
  import { couldNotRunPayload, telemetryKey, telemetryPayload } from '../lib/doctor-telemetry.js';
6
6
  import { ALL_HARNESSES, AuthMode, Registered } from '../lib/harness-detect.js';
7
- import { err, errLine, out, outLine } from '../lib/output.js';
7
+ import { boundedVersion, err, errLine, out, outLine } from '../lib/output.js';
8
8
  import { detectRegistrations } from '../lib/registration-detect.js';
9
9
  import { 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
- import { hermesWebBackendsOurs, readHermesPluginStatus, } from '../lib/hermes-plugin.js';
12
+ import { HERMES_PLUGIN_VERSION, hermesWebBackendsOurs, readHermesPluginStatus, } from '../lib/hermes-plugin.js';
13
13
  import { DEFAULT_MCP_URL, endpointOf, isDefaultEndpoint } from '../lib/mcp-endpoint.js';
14
14
  // A green auth mode with a red auth call proves nothing, so the call's verdict gates the claim.
15
15
  function provesHarnessReach(status, keyAuth, healthOk, mcpUrl) {
@@ -249,25 +249,42 @@ function hermesPluginVerdict(home) {
249
249
  detail: '`hermes tinyfish status --json` returned unparseable output',
250
250
  };
251
251
  }
252
- return hermesPluginStatusVerdict(probe.status);
252
+ return {
253
+ ...hermesPluginStatusVerdict(probe.status),
254
+ // Machine-supplied, so it is bounded here rather than trusted onward.
255
+ observedVersion: boundedVersion(probe.status.plugin_version ?? ''),
256
+ };
253
257
  }
254
258
  function checkHermesPlugin(status) {
255
- const check = {
259
+ const base = {
256
260
  id: 'hermes-plugin',
257
261
  title: 'Hermes web plugin',
258
262
  harness: 'hermes',
259
263
  scope: 'harness',
260
264
  };
261
- if (!status.detected)
262
- return { ...check, status: 'skip', detail: 'harness not installed' };
265
+ if (!status.detected) {
266
+ return { check: { ...base, status: 'skip', detail: 'harness not installed' } };
267
+ }
263
268
  if (status.registered !== Registered.Yes) {
264
- return { ...check, status: 'skip', detail: 'TinyFish is not registered in Hermes' };
269
+ return { check: { ...base, status: 'skip', detail: 'TinyFish is not registered in Hermes' } };
265
270
  }
266
271
  const home = resolveHermesHome();
267
272
  if (!home) {
268
- return { ...check, status: 'warn', detail: "could not determine Hermes' home directory" };
273
+ return {
274
+ check: { ...base, status: 'warn', detail: "could not determine Hermes' home directory" },
275
+ };
269
276
  }
270
- return { ...check, ...hermesPluginVerdict(home) };
277
+ const { observedVersion, ...verdict } = hermesPluginVerdict(home);
278
+ return { check: { ...base, ...verdict }, observedVersion };
279
+ }
280
+ /** Skips mean no Hermes; stamping those buries the drift share. */
281
+ function hermesPluginVersions(result) {
282
+ if (!result || result.check.status === 'skip')
283
+ return {};
284
+ return {
285
+ hermes_plugin_expected_version: HERMES_PLUGIN_VERSION,
286
+ ...(result.observedVersion ? { hermes_plugin_version: result.observedVersion } : {}),
287
+ };
271
288
  }
272
289
  export async function runDoctor(options) {
273
290
  const statuses = detectRegistrations(options.harness ? [options.harness] : undefined);
@@ -279,11 +296,12 @@ export async function runDoctor(options) {
279
296
  Promise.all(statuses.map((status) => verifyHarnessKey(status, cliAuth, options.mcpUrl))),
280
297
  ]);
281
298
  const hermesStatus = statuses.find((status) => status.harness === 'hermes');
299
+ const hermesPlugin = hermesStatus ? checkHermesPlugin(hermesStatus) : undefined;
282
300
  const checks = [
283
301
  checkCliVersion(),
284
302
  connectivity,
285
303
  ...statuses.map((status, i) => checkRegistration(status, options.mcpUrl, keyAuths[i], !credential.key)),
286
- ...(hermesStatus ? [checkHermesPlugin(hermesStatus)] : []),
304
+ ...(hermesPlugin ? [hermesPlugin.check] : []),
287
305
  credential.check,
288
306
  checkAuthCall(cliAuthResult),
289
307
  ];
@@ -303,8 +321,9 @@ export async function runDoctor(options) {
303
321
  checks,
304
322
  harnesses,
305
323
  repairs: repairsFor(checks, statuses),
324
+ ...hermesPluginVersions(hermesPlugin),
306
325
  };
307
- // Nothing to scrub: every string above is authored here or enumerated at its source.
326
+ // The one unauthored string is bounded before it lands here.
308
327
  return doctorReportSchema.parse(body);
309
328
  }
310
329
  export function registerDoctor(program) {
@@ -60,6 +60,8 @@ interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent'
60
60
  apiKey?: string;
61
61
  verbose: boolean;
62
62
  }) => void;
63
+ /** Pin `extraPostInstall` installs, reported on the plugin_installed checkpoint. */
64
+ pluginVersion?: string;
63
65
  /** Extra --uninstall row beyond `removals`; never runs pre-add. */
64
66
  uninstallExtra?: {
65
67
  args: string[];
@@ -8,7 +8,7 @@ import { TINYFISH_API_KEY_VAR } from './constants.js';
8
8
  import { HARNESS_SPECS, NATIVE_HARNESSES, harnessSpec, } from './harness-spec.js';
9
9
  import { HERMES_KEY_VAR, captureHermesKeyRestore, hermesEnvPath, resolveHermesHome, writeHermesKey, } from './hermes-env.js';
10
10
  import { hermesConfigPath, readHermesEntry } from './hermes-config.js';
11
- import { installHermesPlugin, setHermesWebBackends } from './hermes-plugin.js';
11
+ import { HERMES_PLUGIN_VERSION, installHermesPlugin, setHermesWebBackends, } from './hermes-plugin.js';
12
12
  const HERMES_SEED_TIMEOUT_MS = 120_000;
13
13
  const OPENCLAW_SKILL = '@tinyfish/tinyfish';
14
14
  // --force makes this an unconditional overwrite, so the same call installs and refreshes.
@@ -207,6 +207,7 @@ const HARNESS_OVERRIDES = {
207
207
  hermes: {
208
208
  launchWalkthrough: launchHermesWalkthrough,
209
209
  extraPostInstall: installHermesWebPlugin,
210
+ pluginVersion: HERMES_PLUGIN_VERSION,
210
211
  seedKey: seedHermesKey,
211
212
  },
212
213
  opencode: { launchWalkthrough: launchOpencode },
@@ -238,6 +239,7 @@ function toNativeClient(harness) {
238
239
  detachedLaunch: overrides.detachedLaunch,
239
240
  postConnectNote: spec.postConnectNote,
240
241
  extraPostInstall: overrides.extraPostInstall,
242
+ pluginVersion: overrides.pluginVersion,
241
243
  uninstallExtra: spec.uninstallExtra,
242
244
  };
243
245
  if (spec.keyRequired) {
@@ -66,6 +66,8 @@ interface ConnectStageDetail {
66
66
  phase?: ConnectCheckpoint;
67
67
  failureReason?: ConnectFailureReason;
68
68
  harnessVersion?: string;
69
+ /** The plugin the client pins, not a probe of what landed. */
70
+ pluginVersion?: string;
69
71
  authMode?: ConnectAuthMode;
70
72
  /** Orthogonal to `authMode`: a stored key wins over `mcp login` on a healthy client too. */
71
73
  harnessDegraded?: boolean;
@@ -3,7 +3,7 @@ import spawn from 'cross-spawn';
3
3
  import { CONNECT_ATTEMPT_ENV, isAttemptId, resolvedApiKey, takePendingConnectAttempt, } from './auth.js';
4
4
  import { CLI_INVOCATION_ID, CLI_VERSION } from './constants.js';
5
5
  import { detectHarness, detectHumanInitiated, detectRuntimeEnv } from './harness.js';
6
- import { errLine, sanitizeLine } from './output.js';
6
+ import { boundedVersion, errLine, sanitizeLine } from './output.js';
7
7
  import { postConnectEvent, telemetryDisabled } from './setup-telemetry.js';
8
8
  import { installSignalGuard } from './signals.js';
9
9
  export const NON_INTERACTIVE_TIMEOUT_MS = 10_000;
@@ -232,12 +232,7 @@ function probeHarnessVersion(command) {
232
232
  });
233
233
  if (result.error || result.status !== 0)
234
234
  return undefined;
235
- // First line only: sanitizeLine keeps newlines, so stripping splices lines.
236
- const version = sanitizeLine(result.stdout ?? '')
237
- .split('\n')[0]
238
- .replace(/[^\x20-\x7E]/g, '')
239
- .trim();
240
- return version ? version.slice(0, 32) : undefined;
235
+ return boundedVersion(result.stdout ?? '');
241
236
  }
242
237
  /**
243
238
  * One page-minted id seeds one install attempt, so `tinyfish onboard` connecting three agents
@@ -294,6 +289,7 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
294
289
  // Terminal rows only: rescue checkpoints stay wire-identical to normal ones.
295
290
  fallback_outcome: stage === 'failed' || stage === 'aborted' ? fallbackOutcome : undefined,
296
291
  harness_version: detail?.harnessVersion ?? harnessVersion,
292
+ plugin_version: detail?.pluginVersion,
297
293
  auth_mode: detail?.authMode,
298
294
  harness_degraded: detail?.harnessDegraded,
299
295
  stage_duration_ms: stageDurationMs,
@@ -150,6 +150,8 @@ export declare const doctorReportSchema: z.ZodObject<{
150
150
  command: z.ZodString;
151
151
  unattended_safe: z.ZodBoolean;
152
152
  }, z.core.$strip>>;
153
+ hermes_plugin_version: z.ZodOptional<z.ZodString>;
154
+ hermes_plugin_expected_version: z.ZodOptional<z.ZodString>;
153
155
  }, z.core.$strip>;
154
156
  export type CheckStatus = z.infer<typeof checkStatusSchema>;
155
157
  export type CheckScope = z.infer<typeof checkScopeSchema>;
@@ -46,6 +46,9 @@ export const doctorReportSchema = z
46
46
  checks: z.array(doctorCheckSchema),
47
47
  harnesses: z.array(doctorHarnessSchema),
48
48
  repairs: z.array(doctorRepairSchema),
49
+ // Unbounded like cli_version: a throw here exits 2 unnecessarily.
50
+ hermes_plugin_version: z.string().optional(),
51
+ hermes_plugin_expected_version: z.string().optional(),
49
52
  })
50
53
  // Claiming ok while that scope failed is worse than no report.
51
54
  .refine((report) => !report.ok_harnesses || !hasFailure(report.checks, 'harness'), {
@@ -38,6 +38,12 @@ export function telemetryPayload(report, offered, outcomes, options, durationMs)
38
38
  harnesses: report.harnesses,
39
39
  // Clamped, not validated: a --fix parked at a prompt must not fail the parse and drop the run.
40
40
  duration_ms: Math.min(durationMs, MAX_DURATION_MS),
41
+ ...(report.hermes_plugin_version
42
+ ? { hermes_plugin_version: report.hermes_plugin_version }
43
+ : {}),
44
+ ...(report.hermes_plugin_expected_version
45
+ ? { hermes_plugin_expected_version: report.hermes_plugin_expected_version }
46
+ : {}),
41
47
  };
42
48
  }
43
49
  /** Both paths send keyless when there is no key; the route records those anonymously. */
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { type InstallOptions } from './cli-install.js';
3
3
  export declare const HERMES_PLUGIN_SHA = "496cd63fefd982bbaa8a85ce78ef2270d700984f";
4
+ export declare const HERMES_PLUGIN_VERSION = "0.1.0";
4
5
  export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish-io/tinyfish-web-agent-integrations/hermes --ref 496cd63fefd982bbaa8a85ce78ef2270d700984f --enable";
5
6
  export declare function installHermesPlugin(home: string, apiKey: string, { verbose }: Pick<InstallOptions, 'verbose'>): void;
6
7
  /** After install only: backends must never name an absent provider. */
@@ -10,6 +10,8 @@ import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
10
10
  import { errLine } from './output.js';
11
11
  // Bump per CLI release.
12
12
  export const HERMES_PLUGIN_SHA = '496cd63fefd982bbaa8a85ce78ef2270d700984f';
13
+ // Bump with the SHA; the source is `hermes/plugin.yaml` at that commit.
14
+ export const HERMES_PLUGIN_VERSION = '0.1.0';
13
15
  // Manifest `name:`, not the repo subdir; `plugins uninstall` keys on it.
14
16
  const PLUGIN_SOURCE = 'tinyfish-io/tinyfish-web-agent-integrations/hermes';
15
17
  const PLUGIN_REPO_URL = 'https://github.com/tinyfish-io/tinyfish-web-agent-integrations.git';
@@ -6,6 +6,8 @@ export declare class ApiError extends Error {
6
6
  export declare function out(data: unknown): void;
7
7
  /** The only writer for untrusted text: strips ANSI and C0/C1 controls. */
8
8
  export declare function sanitizeLine(text: string): string;
9
+ /** Every version the route caps at 32 printable ASCII chars. */
10
+ export declare function boundedVersion(text: string): string | undefined;
9
11
  export declare function outLine(line: string): void;
10
12
  /** `connect --all` nests a harness's own lines under its step header. */
11
13
  export declare function setErrIndent(prefix: string): void;
@@ -17,6 +17,15 @@ export function sanitizeLine(text) {
17
17
  // eslint-disable-next-line no-control-regex
18
18
  return stripVTControlCharacters(text).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, '');
19
19
  }
20
+ /** Every version the route caps at 32 printable ASCII chars. */
21
+ export function boundedVersion(text) {
22
+ // First line only: sanitizeLine keeps newlines, so stripping splices lines.
23
+ const version = sanitizeLine(text)
24
+ .split('\n')[0]
25
+ .replace(/[^\x20-\x7E]/g, '')
26
+ .trim();
27
+ return version ? version.slice(0, 32) : undefined;
28
+ }
20
29
  export function outLine(line) {
21
30
  process.stdout.write(sanitizeLine(line) + '\n');
22
31
  }
@@ -160,6 +160,8 @@ export declare const doctorCompletedPayloadSchema: z.ZodObject<{
160
160
  proves_harness_reach: z.ZodBoolean;
161
161
  }, z.core.$strip>>;
162
162
  duration_ms: z.ZodInt;
163
+ hermes_plugin_version: z.ZodOptional<z.ZodString>;
164
+ hermes_plugin_expected_version: z.ZodOptional<z.ZodString>;
163
165
  }, z.core.$strip>;
164
166
  export declare const doctorCouldNotRunPayloadSchema: z.ZodObject<{
165
167
  outcome: z.ZodLiteral<"could_not_run">;
@@ -253,6 +255,8 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
253
255
  proves_harness_reach: z.ZodBoolean;
254
256
  }, z.core.$strip>>;
255
257
  duration_ms: z.ZodInt;
258
+ hermes_plugin_version: z.ZodOptional<z.ZodString>;
259
+ hermes_plugin_expected_version: z.ZodOptional<z.ZodString>;
256
260
  }, z.core.$strip>, z.ZodObject<{
257
261
  outcome: z.ZodLiteral<"could_not_run">;
258
262
  error_class: z.ZodEnum<{
@@ -211,8 +211,13 @@ export async function sendSetupCompleted(mcpUrl, harnesses, apiKey, mode, attemp
211
211
  await postConnectEvent({ endpoint, body, attempts: 2, label: 'setup', apiKey });
212
212
  }
213
213
  export const DOCTOR_HARNESS_SCOPES = ['all', ...ALL_HARNESSES];
214
- // Enumerated only: check ids, harness names and auth modes are authored here, so nothing
215
- // user-authored or credential-shaped can ride a doctor run to PostHog. `detail` never leaves.
214
+ // Mirrors the route's cap; drops what the route would 400 un-retried.
215
+ const versionSchema = z
216
+ .string()
217
+ .max(32)
218
+ .regex(/^[\x20-\x7E]*$/);
219
+ // Ids, harnesses and auth modes are authored here; `detail` never leaves.
220
+ // The plugin version is machine-supplied, sanitized before reaching this schema.
216
221
  // Caps mirror the route's schema: a payload the server would 400 is dropped here instead,
217
222
  // because postConnectEvent never retries a 4xx and the event would vanish silently.
218
223
  // Same spellings as cli_connect_stage.failure_reason; the type pins them.
@@ -264,6 +269,9 @@ export const doctorCompletedPayloadSchema = z.object({
264
269
  }))
265
270
  .max(16),
266
271
  duration_ms: z.int().nonnegative().max(3_600_000),
272
+ /** Bounded here, not on the report: a reject drops only telemetry. */
273
+ hermes_plugin_version: versionSchema.optional(),
274
+ hermes_plugin_expected_version: versionSchema.optional(),
267
275
  });
268
276
  // Detection threw, so there is no report to aggregate: enumerated outcome and timing only.
269
277
  export const doctorCouldNotRunPayloadSchema = z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.38.1-next.303",
3
+ "version": "0.38.1-next.305",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {