@tiny-fish/cli 0.34.1-next.286 → 0.35.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 +87 -1
- package/dist/lib/hermes-plugin.d.ts +25 -0
- package/dist/lib/hermes-plugin.js +38 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -8,6 +8,8 @@ import { 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
|
+
import { resolveHermesHome } from '../lib/hermes-env.js';
|
|
12
|
+
import { hermesWebBackendsOurs, readHermesPluginStatus, } from '../lib/hermes-plugin.js';
|
|
11
13
|
import { DEFAULT_MCP_URL, endpointOf, isDefaultEndpoint } from '../lib/mcp-endpoint.js';
|
|
12
14
|
import { connectClaudeCode, connectCodex, connectCursor, connectGrok, connectHermes, connectOpenClaw, connectOpencode, } from './connect.js';
|
|
13
15
|
// A green auth mode with a red auth call proves nothing, so the call's verdict gates the claim.
|
|
@@ -197,6 +199,77 @@ function verifyHarnessKey(status, cliAuth, mcpUrl) {
|
|
|
197
199
|
// The CLI never holds a harness's key, so only its own credential can be verified.
|
|
198
200
|
return runsOnCliCredential(status) ? cliAuth : undefined;
|
|
199
201
|
}
|
|
202
|
+
// `ok` is backends && key; provider_available re-reads the key through the provider.
|
|
203
|
+
function hermesPluginStatusVerdict(status) {
|
|
204
|
+
if (status.ok) {
|
|
205
|
+
return status.provider_available === false
|
|
206
|
+
? { status: 'warn', detail: 'plugin configured, but its provider cannot see the key' }
|
|
207
|
+
: {
|
|
208
|
+
status: 'pass',
|
|
209
|
+
detail: `plugin ${status.plugin_version ?? 'installed'}; backends and key configured`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (!status.web_backend_configured) {
|
|
213
|
+
return {
|
|
214
|
+
status: 'warn',
|
|
215
|
+
detail: 'plugin installed but the web backends do not point at tinyfish',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
status: 'fail',
|
|
220
|
+
detail: 'web backends point at tinyfish but no key resolves from ' +
|
|
221
|
+
'TINYFISH_API_KEY or MCP_TINYFISH_API_KEY',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** An unreadable config cannot prove the backends are ours, so doctor never fails on it. */
|
|
225
|
+
function backendsOurs(home) {
|
|
226
|
+
try {
|
|
227
|
+
return hermesWebBackendsOurs(home);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function hermesPluginVerdict(home) {
|
|
234
|
+
const probe = readHermesPluginStatus(home);
|
|
235
|
+
if (probe.state === 'not_installed') {
|
|
236
|
+
// Backends naming an absent provider are broken; never-installed keeps exit 0.
|
|
237
|
+
return backendsOurs(home)
|
|
238
|
+
? {
|
|
239
|
+
status: 'fail',
|
|
240
|
+
detail: 'web backends point at tinyfish but the plugin is not installed or not enabled',
|
|
241
|
+
}
|
|
242
|
+
: { status: 'warn', detail: 'the TinyFish web plugin is not installed or not enabled' };
|
|
243
|
+
}
|
|
244
|
+
if (probe.state === 'unreachable') {
|
|
245
|
+
return { status: 'warn', detail: '`hermes tinyfish status` did not respond in time' };
|
|
246
|
+
}
|
|
247
|
+
if (probe.state === 'unparseable') {
|
|
248
|
+
return {
|
|
249
|
+
status: 'warn',
|
|
250
|
+
detail: '`hermes tinyfish status --json` returned unparseable output',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return hermesPluginStatusVerdict(probe.status);
|
|
254
|
+
}
|
|
255
|
+
function checkHermesPlugin(status) {
|
|
256
|
+
const check = {
|
|
257
|
+
id: 'hermes-plugin',
|
|
258
|
+
title: 'Hermes web plugin',
|
|
259
|
+
harness: 'hermes',
|
|
260
|
+
scope: 'harness',
|
|
261
|
+
};
|
|
262
|
+
if (!status.detected)
|
|
263
|
+
return { ...check, status: 'skip', detail: 'harness not installed' };
|
|
264
|
+
if (status.registered !== Registered.Yes) {
|
|
265
|
+
return { ...check, status: 'skip', detail: 'TinyFish is not registered in Hermes' };
|
|
266
|
+
}
|
|
267
|
+
const home = resolveHermesHome();
|
|
268
|
+
if (!home) {
|
|
269
|
+
return { ...check, status: 'warn', detail: "could not determine Hermes' home directory" };
|
|
270
|
+
}
|
|
271
|
+
return { ...check, ...hermesPluginVerdict(home) };
|
|
272
|
+
}
|
|
200
273
|
// Only Cursor's repair is a pure local file write; every other one needs a browser sign-in.
|
|
201
274
|
const UNATTENDED_SAFE = new Set(['cursor']);
|
|
202
275
|
function repairsFor(checks, statuses) {
|
|
@@ -219,7 +292,7 @@ function repairsFor(checks, statuses) {
|
|
|
219
292
|
// A registered entry that still fails earns one too.
|
|
220
293
|
// `connected: false` is the harness's own no; it only warns, so it earns a repair here.
|
|
221
294
|
const brokenRegistration = status.connected === false ||
|
|
222
|
-
checks.some((c) => c.harness === status.harness && c.status === 'fail');
|
|
295
|
+
checks.some((c) => c.harness === status.harness && c.id !== 'hermes-plugin' && c.status === 'fail');
|
|
223
296
|
if (!status.detected || (status.registered !== Registered.No && !brokenRegistration))
|
|
224
297
|
continue;
|
|
225
298
|
repairs.push({
|
|
@@ -235,6 +308,17 @@ function repairsFor(checks, statuses) {
|
|
|
235
308
|
unattended_safe: UNATTENDED_SAFE.has(status.harness) && authCallPassed,
|
|
236
309
|
});
|
|
237
310
|
}
|
|
311
|
+
// connect hermes reinstalls the plugin; a second row would double it.
|
|
312
|
+
const pluginBroken = checks.some((c) => c.id === 'hermes-plugin' && c.status === 'fail');
|
|
313
|
+
if (pluginBroken && !repairs.some((repair) => repair.harness === 'hermes')) {
|
|
314
|
+
repairs.push({
|
|
315
|
+
for: 'hermes-plugin-broken',
|
|
316
|
+
action: 'connect',
|
|
317
|
+
harness: 'hermes',
|
|
318
|
+
command: 'tinyfish connect hermes',
|
|
319
|
+
unattended_safe: false,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
238
322
|
return repairs;
|
|
239
323
|
}
|
|
240
324
|
export async function runDoctor(options) {
|
|
@@ -246,10 +330,12 @@ export async function runDoctor(options) {
|
|
|
246
330
|
cliAuth,
|
|
247
331
|
Promise.all(statuses.map((status) => verifyHarnessKey(status, cliAuth, options.mcpUrl))),
|
|
248
332
|
]);
|
|
333
|
+
const hermesStatus = statuses.find((status) => status.harness === 'hermes');
|
|
249
334
|
const checks = [
|
|
250
335
|
checkCliVersion(),
|
|
251
336
|
connectivity,
|
|
252
337
|
...statuses.map((status, i) => checkRegistration(status, options.mcpUrl, keyAuths[i], !credential.key)),
|
|
338
|
+
...(hermesStatus ? [checkHermesPlugin(hermesStatus)] : []),
|
|
253
339
|
credential.check,
|
|
254
340
|
checkAuthCall(cliAuthResult),
|
|
255
341
|
];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { type InstallOptions } from './cli-install.js';
|
|
2
3
|
export declare const HERMES_PLUGIN_SHA = "496cd63fefd982bbaa8a85ce78ef2270d700984f";
|
|
3
4
|
export declare const HERMES_PLUGIN_NAME = "tinyfish";
|
|
@@ -7,3 +8,27 @@ export declare function installHermesPlugin(home: string, apiKey: string, { verb
|
|
|
7
8
|
export declare function setHermesWebBackends(home: string): void;
|
|
8
9
|
/** Ours-only unsets: a user-chosen backend value survives the uninstall. */
|
|
9
10
|
export declare function clearHermesWebBackends(home: string): void;
|
|
11
|
+
declare const hermesPluginStatusSchema: z.ZodObject<{
|
|
12
|
+
ok: z.ZodBoolean;
|
|
13
|
+
api_key_configured: z.ZodBoolean;
|
|
14
|
+
api_key_env_var: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
15
|
+
plugin_version: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
16
|
+
provider_available: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
17
|
+
web_backend_configured: z.ZodBoolean;
|
|
18
|
+
}, z.core.$loose>;
|
|
19
|
+
export type HermesPluginStatus = z.infer<typeof hermesPluginStatusSchema>;
|
|
20
|
+
export type HermesPluginProbe = {
|
|
21
|
+
state: 'not_installed';
|
|
22
|
+
} | {
|
|
23
|
+
state: 'unreachable';
|
|
24
|
+
} | {
|
|
25
|
+
state: 'unparseable';
|
|
26
|
+
} | {
|
|
27
|
+
state: 'ok';
|
|
28
|
+
status: HermesPluginStatus;
|
|
29
|
+
};
|
|
30
|
+
/** `status --json` always exits 0; a non-zero exit means the subcommand is absent. */
|
|
31
|
+
export declare function readHermesPluginStatus(home: string): HermesPluginProbe;
|
|
32
|
+
/** Fallback verdict input when the plugin CLI cannot answer. */
|
|
33
|
+
export declare function hermesWebBackendsOurs(home: string): boolean;
|
|
34
|
+
export {};
|
|
@@ -3,6 +3,7 @@ import * as os from 'node:os';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import spawn from 'cross-spawn';
|
|
6
|
+
import { z } from 'zod';
|
|
6
7
|
import { capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
|
|
7
8
|
import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
|
|
8
9
|
import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
|
|
@@ -117,3 +118,40 @@ export function clearHermesWebBackends(home) {
|
|
|
117
118
|
configWrite(home, ['unset', key]);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
121
|
+
const hermesPluginStatusSchema = z.looseObject({
|
|
122
|
+
ok: z.boolean(),
|
|
123
|
+
api_key_configured: z.boolean(),
|
|
124
|
+
api_key_env_var: z.string().nullish(),
|
|
125
|
+
plugin_version: z.string().nullish(),
|
|
126
|
+
provider_available: z.boolean().nullish(),
|
|
127
|
+
web_backend_configured: z.boolean(),
|
|
128
|
+
});
|
|
129
|
+
function parseJson(text) {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(text);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** `status --json` always exits 0; a non-zero exit means the subcommand is absent. */
|
|
138
|
+
export function readHermesPluginStatus(home) {
|
|
139
|
+
const result = spawn.sync('hermes', ['tinyfish', 'status', '--json'], {
|
|
140
|
+
encoding: 'utf8',
|
|
141
|
+
maxBuffer: STEP_MAX_BUFFER,
|
|
142
|
+
timeout: HARNESS_PROBE_TIMEOUT_MS,
|
|
143
|
+
env: { ...process.env, HERMES_HOME: home, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
144
|
+
});
|
|
145
|
+
// A timeout also lands in result.error, where "not installed" would be a lie.
|
|
146
|
+
if (result.error) {
|
|
147
|
+
return commandNotFound(result.error) ? { state: 'not_installed' } : { state: 'unreachable' };
|
|
148
|
+
}
|
|
149
|
+
if (result.status !== 0)
|
|
150
|
+
return { state: 'not_installed' };
|
|
151
|
+
const parsed = hermesPluginStatusSchema.safeParse(parseJson(String(result.stdout ?? '')));
|
|
152
|
+
return parsed.success ? { state: 'ok', status: parsed.data } : { state: 'unparseable' };
|
|
153
|
+
}
|
|
154
|
+
/** Fallback verdict input when the plugin CLI cannot answer. */
|
|
155
|
+
export function hermesWebBackendsOurs(home) {
|
|
156
|
+
return HERMES_OWNED_BACKEND_KEYS.some((key) => configGet(home, key) === 'tinyfish');
|
|
157
|
+
}
|