@tiny-fish/cli 0.34.1-next.285 → 0.34.1-next.287
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
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
|
];
|
|
@@ -3,6 +3,7 @@ import { NATIVE_BY_HARNESS, openclawSkillUninstall } from './connect-clients.js'
|
|
|
3
3
|
import { ConnectInterruptedError, spawnRemoval } from './connect-runtime.js';
|
|
4
4
|
import { removeCursorMcpServer, planCursorWrite } from './cursor-config.js';
|
|
5
5
|
import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
|
|
6
|
+
import { clearHermesWebBackends, HERMES_PLUGIN_SHA } from './hermes-plugin.js';
|
|
6
7
|
import { HARNESS_DISPLAY_NAMES as DISPLAY_NAMES } from './harness-detect.js';
|
|
7
8
|
import { errLine } from './output.js';
|
|
8
9
|
// OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
|
|
@@ -12,7 +13,9 @@ export function planText(harness, mcpUrl, apiKey) {
|
|
|
12
13
|
return planCursorWrite(mcpUrl, apiKey);
|
|
13
14
|
// Only the keyed path seeds the .env; a keyless Hermes install writes nothing.
|
|
14
15
|
if (harness === 'hermes' && apiKey) {
|
|
15
|
-
return `would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes
|
|
16
|
+
return (`would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ` +
|
|
17
|
+
`${HERMES_KEY_VAR} to Hermes' .env, installs the tinyfish plugin at ` +
|
|
18
|
+
`${HERMES_PLUGIN_SHA.slice(0, 12)}, and points its web backends at tinyfish)`);
|
|
16
19
|
}
|
|
17
20
|
return `would run \`tinyfish connect ${harness}\` (harness-owned MCP write; no local file touched by the CLI)`;
|
|
18
21
|
}
|
|
@@ -24,7 +27,9 @@ export function uninstallPlanText(harness) {
|
|
|
24
27
|
return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
|
|
25
28
|
}
|
|
26
29
|
// Hermes alone leaves a seeded key behind; `mcp remove` never clears it.
|
|
27
|
-
const envNote = harness === 'hermes'
|
|
30
|
+
const envNote = harness === 'hermes'
|
|
31
|
+
? ` and clear ${HERMES_KEY_VAR} from Hermes' .env plus its tinyfish web backends`
|
|
32
|
+
: '';
|
|
28
33
|
return `would run \`${uninstallPointer(harness)}\`${envNote}`;
|
|
29
34
|
}
|
|
30
35
|
/** Undefined: no harness-CLI removal exists (opencode). */
|
|
@@ -36,7 +41,8 @@ function uninstallRuns(harness) {
|
|
|
36
41
|
const native = NATIVE_BY_HARNESS.get(harness);
|
|
37
42
|
if (!native || native.removals.length === 0)
|
|
38
43
|
return undefined;
|
|
39
|
-
|
|
44
|
+
const extra = native.uninstallExtra ? [native.uninstallExtra.args] : [];
|
|
45
|
+
return { command: native.command, runs: [...native.removals.map((r) => r.args), ...extra] };
|
|
40
46
|
}
|
|
41
47
|
/** Derived from the same args the spawn runs, so the advice cannot drift. */
|
|
42
48
|
function uninstallPointer(harness) {
|
|
@@ -48,9 +54,29 @@ function uninstallPointer(harness) {
|
|
|
48
54
|
const joiner = process.platform === 'win32' ? ' && ' : ' ; ';
|
|
49
55
|
return removal.runs.map((args) => `${removal.command} ${args.join(' ')}`).join(joiner);
|
|
50
56
|
}
|
|
51
|
-
/** `mcp remove`
|
|
52
|
-
function
|
|
57
|
+
/** `mcp remove` clears neither the key nor the backends we wrote. */
|
|
58
|
+
function clearHermesLocalState() {
|
|
53
59
|
const home = resolveHermesHome();
|
|
60
|
+
const advisories = [clearHermesEnvKey(home), clearHermesBackends(home)].filter((advisory) => advisory !== undefined);
|
|
61
|
+
return advisories.length > 0 ? advisories.join(' ') : undefined;
|
|
62
|
+
}
|
|
63
|
+
/** Backends left naming an absent provider would break Hermes' web tools. */
|
|
64
|
+
function clearHermesBackends(home) {
|
|
65
|
+
// The key clearer already reported the unresolved home.
|
|
66
|
+
if (!home)
|
|
67
|
+
return undefined;
|
|
68
|
+
try {
|
|
69
|
+
clearHermesWebBackends(home);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
74
|
+
return (`could not unset Hermes' tinyfish web backends (${detail}). Run \`hermes config unset ` +
|
|
75
|
+
'web.search_backend`, `web.extract_backend` and `web.backend` by hand.');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** `mcp remove` leaves this behind; a stale value gets silently reused. */
|
|
79
|
+
function clearHermesEnvKey(home) {
|
|
54
80
|
const retry = 're-run `tinyfish connect --all --uninstall`';
|
|
55
81
|
if (!home) {
|
|
56
82
|
return (`could not locate Hermes' home, so ${HERMES_KEY_VAR} may still be set in its .env. ` +
|
|
@@ -177,7 +203,7 @@ export function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
|
|
|
177
203
|
// Clearing a key the entry still references would break a working install.
|
|
178
204
|
const removed = !result.removalFailed &&
|
|
179
205
|
(result.outcome === 'uninstalled' || result.outcome === 'uninstall_noop');
|
|
180
|
-
const advisory = removed ?
|
|
206
|
+
const advisory = removed ? clearHermesLocalState() : hermesKeyKeptAdvisory();
|
|
181
207
|
if (!advisory)
|
|
182
208
|
return result;
|
|
183
209
|
// Appended, never replacing: an advisory must not hide the removal's message.
|
|
@@ -74,6 +74,11 @@ interface BaseMcpClient extends SupportedCommand {
|
|
|
74
74
|
apiKey?: string;
|
|
75
75
|
verbose: boolean;
|
|
76
76
|
}) => void;
|
|
77
|
+
/** Extra --uninstall row beyond `removals`; never runs pre-add. */
|
|
78
|
+
uninstallExtra?: {
|
|
79
|
+
args: string[];
|
|
80
|
+
label: string;
|
|
81
|
+
};
|
|
77
82
|
}
|
|
78
83
|
/** Keyless installs use this argv; the harness signs itself in. */
|
|
79
84
|
export interface OauthCapableMcpClient extends BaseMcpClient {
|
|
@@ -7,7 +7,7 @@ import { ConnectStepError, spawnStepError, } from './connect-runtime.js';
|
|
|
7
7
|
import { TINYFISH_API_KEY_VAR } from './constants.js';
|
|
8
8
|
import { HERMES_KEY_VAR, captureHermesKeyRestore, hermesEnvPath, resolveHermesHome, writeHermesKey, } from './hermes-env.js';
|
|
9
9
|
import { hermesConfigPath, readHermesEntry } from './hermes-config.js';
|
|
10
|
-
import { installHermesPlugin, setHermesWebBackends } from './hermes-plugin.js';
|
|
10
|
+
import { HERMES_PLUGIN_NAME, installHermesPlugin, setHermesWebBackends } from './hermes-plugin.js';
|
|
11
11
|
const HERMES_SEED_TIMEOUT_MS = 120_000;
|
|
12
12
|
// An old install is only one reason a flag can go unseen, so no message asserts the cause.
|
|
13
13
|
const SUPPORT_CHECK_DEBUG_HINT = ' Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.';
|
|
@@ -332,6 +332,10 @@ export const HERMES = {
|
|
|
332
332
|
},
|
|
333
333
|
removals: [{ args: ['mcp', 'remove', 'tinyfish'], label: 'TinyFish registration from Hermes' }],
|
|
334
334
|
extraPostInstall: installHermesWebPlugin,
|
|
335
|
+
uninstallExtra: {
|
|
336
|
+
args: ['plugins', 'uninstall', HERMES_PLUGIN_NAME],
|
|
337
|
+
label: 'TinyFish web plugin from Hermes',
|
|
338
|
+
},
|
|
335
339
|
launchWalkthrough: launchHermesWalkthrough,
|
|
336
340
|
postConnectNote: 'Restart your Hermes session to pick up TinyFish — Hermes discovers MCP servers at startup.',
|
|
337
341
|
};
|
|
@@ -1,6 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { type InstallOptions } from './cli-install.js';
|
|
2
3
|
export declare const HERMES_PLUGIN_SHA = "496cd63fefd982bbaa8a85ce78ef2270d700984f";
|
|
4
|
+
export declare const HERMES_PLUGIN_NAME = "tinyfish";
|
|
3
5
|
export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish-io/tinyfish-web-agent-integrations/hermes --ref 496cd63fefd982bbaa8a85ce78ef2270d700984f --enable";
|
|
4
6
|
export declare function installHermesPlugin(home: string, apiKey: string, { verbose }: Pick<InstallOptions, 'verbose'>): void;
|
|
5
7
|
/** After install only: backends must never name an absent provider. */
|
|
6
8
|
export declare function setHermesWebBackends(home: string): void;
|
|
9
|
+
/** Ours-only unsets: a user-chosen backend value survives the uninstall. */
|
|
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,12 +3,15 @@ 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
|
-
import { commandNotFound, ConnectStepError, spawnStepError } from './connect-runtime.js';
|
|
8
|
+
import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
|
|
8
9
|
import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
|
|
9
10
|
import { errLine } from './output.js';
|
|
10
11
|
// Bump per CLI release.
|
|
11
12
|
export const HERMES_PLUGIN_SHA = '496cd63fefd982bbaa8a85ce78ef2270d700984f';
|
|
13
|
+
// Manifest `name:`, not the repo subdir; `plugins uninstall` keys on it.
|
|
14
|
+
export const HERMES_PLUGIN_NAME = 'tinyfish';
|
|
12
15
|
const PLUGIN_SOURCE = 'tinyfish-io/tinyfish-web-agent-integrations/hermes';
|
|
13
16
|
const PLUGIN_REPO_URL = 'https://github.com/tinyfish-io/tinyfish-web-agent-integrations.git';
|
|
14
17
|
export const HERMES_PLUGIN_MANUAL_INSTALL = `hermes plugins install ${PLUGIN_SOURCE} --ref ${HERMES_PLUGIN_SHA} --enable`;
|
|
@@ -92,8 +95,63 @@ function configWrite(home, args) {
|
|
|
92
95
|
throw error;
|
|
93
96
|
}
|
|
94
97
|
const HERMES_BACKEND_KEYS = ['web.search_backend', 'web.extract_backend'];
|
|
98
|
+
// Dispatch falls back to web.backend, and the plugin's own setup can set it.
|
|
99
|
+
const HERMES_OWNED_BACKEND_KEYS = [...HERMES_BACKEND_KEYS, 'web.backend'];
|
|
95
100
|
/** After install only: backends must never name an absent provider. */
|
|
96
101
|
export function setHermesWebBackends(home) {
|
|
97
102
|
for (const key of HERMES_BACKEND_KEYS)
|
|
98
103
|
configWrite(home, ['set', key, 'tinyfish']);
|
|
99
104
|
}
|
|
105
|
+
/** Unset prints empty and exits 0, so a failed read must not read as unset. */
|
|
106
|
+
function configGet(home, key) {
|
|
107
|
+
const result = hermesConfig(['get', key], home);
|
|
108
|
+
throwIfInterrupted(result);
|
|
109
|
+
if (result.error || result.status !== 0) {
|
|
110
|
+
throw spawnStepError(`Could not read \`hermes config get ${key}\``, result);
|
|
111
|
+
}
|
|
112
|
+
return String(result.stdout ?? '').trim() || undefined;
|
|
113
|
+
}
|
|
114
|
+
/** Ours-only unsets: a user-chosen backend value survives the uninstall. */
|
|
115
|
+
export function clearHermesWebBackends(home) {
|
|
116
|
+
for (const key of HERMES_OWNED_BACKEND_KEYS) {
|
|
117
|
+
if (configGet(home, key) === 'tinyfish')
|
|
118
|
+
configWrite(home, ['unset', key]);
|
|
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
|
+
}
|