@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.
- package/dist/commands/connect.js +4 -1
- package/dist/commands/doctor.js +30 -11
- package/dist/lib/connect-clients.d.ts +2 -0
- package/dist/lib/connect-clients.js +3 -1
- package/dist/lib/connect-runtime.d.ts +2 -0
- package/dist/lib/connect-runtime.js +3 -7
- package/dist/lib/doctor-report.d.ts +2 -0
- package/dist/lib/doctor-report.js +3 -0
- package/dist/lib/doctor-telemetry.js +6 -0
- package/dist/lib/hermes-plugin.d.ts +1 -0
- package/dist/lib/hermes-plugin.js +2 -0
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +9 -0
- package/dist/lib/setup-telemetry.d.ts +4 -0
- package/dist/lib/setup-telemetry.js +10 -2
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -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', {
|
|
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.
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
|
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
|
|
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 { ...
|
|
265
|
+
if (!status.detected) {
|
|
266
|
+
return { check: { ...base, status: 'skip', detail: 'harness not installed' } };
|
|
267
|
+
}
|
|
263
268
|
if (status.registered !== Registered.Yes) {
|
|
264
|
-
return { ...
|
|
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 {
|
|
273
|
+
return {
|
|
274
|
+
check: { ...base, status: 'warn', detail: "could not determine Hermes' home directory" },
|
|
275
|
+
};
|
|
269
276
|
}
|
|
270
|
-
|
|
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
|
-
...(
|
|
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
|
-
//
|
|
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
|
-
|
|
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';
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/output.js
CHANGED
|
@@ -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
|
-
//
|
|
215
|
-
|
|
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({
|