@tiny-fish/cli 0.39.1-next.311 → 0.40.1-next.317
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/README.md +7 -0
- package/dist/commands/connect.js +86 -19
- package/dist/commands/doctor.js +3 -1
- package/dist/commands/fetch.js +79 -24
- package/dist/lib/client.d.ts +113 -3
- package/dist/lib/client.js +14 -3
- package/dist/lib/connect-all-summary.js +4 -1
- package/dist/lib/connect-all-uninstall.js +12 -1
- package/dist/lib/connect-all.d.ts +4 -2
- package/dist/lib/connect-all.js +13 -5
- package/dist/lib/connect-clients.d.ts +8 -2
- package/dist/lib/connect-clients.js +53 -13
- package/dist/lib/doctor-checks.d.ts +1 -0
- package/dist/lib/doctor-checks.js +38 -3
- package/dist/lib/doctor-report.d.ts +6 -0
- package/dist/lib/harness-detect.d.ts +2 -0
- package/dist/lib/harness-detect.js +11 -3
- package/dist/lib/harness-spec.d.ts +15 -5
- package/dist/lib/harness-spec.js +11 -1
- package/dist/lib/harness.js +2 -0
- package/dist/lib/hermes-config.d.ts +3 -0
- package/dist/lib/hermes-config.js +8 -4
- package/dist/lib/hermes-env.d.ts +7 -1
- package/dist/lib/hermes-env.js +6 -3
- package/dist/lib/hermes-plugin.d.ts +4 -0
- package/dist/lib/hermes-plugin.js +16 -0
- package/dist/lib/pi-config.d.ts +26 -0
- package/dist/lib/pi-config.js +111 -0
- package/dist/lib/registration-detect.js +29 -2
- package/dist/lib/setup-telemetry.d.ts +10 -0
- package/dist/lib/skill-install.d.ts +1 -0
- package/dist/lib/skill-install.js +44 -37
- package/dist/lib/types.d.ts +10 -1
- package/package.json +1 -1
package/dist/lib/connect-all.js
CHANGED
|
@@ -127,13 +127,19 @@ function noWorkOutcome(d, uninstall) {
|
|
|
127
127
|
return 'not_detected';
|
|
128
128
|
if (!uninstall && isStaleConfig(d))
|
|
129
129
|
return 'stale_config';
|
|
130
|
+
if (!uninstall && needsFirstRun(d))
|
|
131
|
+
return 'needs_first_run';
|
|
130
132
|
return undefined;
|
|
131
133
|
}
|
|
134
|
+
/** The harness has never created its dir, so connecting would build one it does not own. */
|
|
135
|
+
export function needsFirstRun(detection) {
|
|
136
|
+
return detection.detected && !detection.configDirExists;
|
|
137
|
+
}
|
|
132
138
|
/** A config-writing connect needs no binary; every other harness spawns one. */
|
|
133
139
|
function isStaleConfig(detection) {
|
|
134
140
|
const spec = harnessSpec(detection.harness);
|
|
135
|
-
// Cursor's IDE reads mcp.json binary-less; omp
|
|
136
|
-
const binaryless = spec.cliWritesConfig &&
|
|
141
|
+
// Cursor's IDE reads mcp.json binary-less; omp and pi have no second reader.
|
|
142
|
+
const binaryless = spec.cliWritesConfig && spec.readableWithoutBinary;
|
|
137
143
|
return detection.detected && !detection.binaryOnPath && !binaryless;
|
|
138
144
|
}
|
|
139
145
|
async function resolvePick(detections, opts, prompt) {
|
|
@@ -141,7 +147,7 @@ async function resolvePick(detections, opts, prompt) {
|
|
|
141
147
|
let mode = 'all';
|
|
142
148
|
// Before willAttempt/cliNeeded: a cursor-only pick must not hoist the CLI.
|
|
143
149
|
// A stale config dir has no binary to connect, so never offer it as a choice.
|
|
144
|
-
const offerable = detections.filter((d) => d.detected && !isStaleConfig(d));
|
|
150
|
+
const offerable = detections.filter((d) => d.detected && !isStaleConfig(d) && !needsFirstRun(d));
|
|
145
151
|
if (opts.pick && offerable.length > 0) {
|
|
146
152
|
const connected = new Set(Object.keys(loadConfig().connect ?? {}));
|
|
147
153
|
const pick = await pickHarnesses(offerable, connected, { prompt });
|
|
@@ -236,13 +242,14 @@ async function connectLoop(detections, opts, deselected, io) {
|
|
|
236
242
|
if (results.some((r) => r.harness === detection.harness))
|
|
237
243
|
continue;
|
|
238
244
|
const { harness, detected, configPath } = detection;
|
|
239
|
-
|
|
245
|
+
// One source of truth with `stepped`, or a skipped harness lands on the wrong row.
|
|
246
|
+
const noWork = noWorkOutcome(detection, opts.uninstall);
|
|
240
247
|
results.push({
|
|
241
248
|
harness,
|
|
242
249
|
detected,
|
|
243
250
|
configPath,
|
|
244
251
|
installed: false,
|
|
245
|
-
outcome:
|
|
252
|
+
outcome: noWork ?? 'not_reached',
|
|
246
253
|
fixCommand: `tinyfish connect ${detection.harness}`,
|
|
247
254
|
});
|
|
248
255
|
}
|
|
@@ -389,6 +396,7 @@ const LAUNCHERS = [
|
|
|
389
396
|
{ harness: 'cursor', args: [FIRST_TASK_PROMPT] },
|
|
390
397
|
{ harness: 'claude-code', args: [FIRST_TASK_PROMPT] },
|
|
391
398
|
{ harness: 'omp', args: [FIRST_TASK_PROMPT] },
|
|
399
|
+
{ harness: 'pi', args: [FIRST_TASK_PROMPT] },
|
|
392
400
|
];
|
|
393
401
|
async function launchFirstTask(results) {
|
|
394
402
|
const candidates = LAUNCHERS.filter((l) => results.some((r) => r.harness === l.harness && r.installed) &&
|
|
@@ -19,6 +19,8 @@ export declare const CURSOR_SKILL_TARGET: {
|
|
|
19
19
|
};
|
|
20
20
|
/** The harness reads the key from its own store. */
|
|
21
21
|
interface SeededInstall {
|
|
22
|
+
/** Reused by the plugin step, so a second probe cannot disagree. */
|
|
23
|
+
home: string;
|
|
22
24
|
/** Pins the add child at the home the key landed in. */
|
|
23
25
|
env: typeof process.env;
|
|
24
26
|
/** Answers prompts `mcp add` still asks once the key is seeded. */
|
|
@@ -27,6 +29,8 @@ interface SeededInstall {
|
|
|
27
29
|
note: string;
|
|
28
30
|
/** Undoes the seed, carrying the value it replaced. */
|
|
29
31
|
rollback: () => void;
|
|
32
|
+
/** Writes the entry ourselves; false falls back to the harness command. */
|
|
33
|
+
register: (mcpUrl: string) => boolean;
|
|
30
34
|
/** `mcp add` exits 0 even having saved nothing; the code lies. */
|
|
31
35
|
confirmRegistered: () => {
|
|
32
36
|
ok: boolean;
|
|
@@ -57,6 +61,7 @@ interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent'
|
|
|
57
61
|
extraPostInstall?: (options: {
|
|
58
62
|
apiKey?: string;
|
|
59
63
|
verbose: boolean;
|
|
64
|
+
seededHome?: string;
|
|
60
65
|
}) => void;
|
|
61
66
|
/** Pin `extraPostInstall` installs, reported on the plugin_installed checkpoint. */
|
|
62
67
|
pluginVersion?: string;
|
|
@@ -86,7 +91,7 @@ export type NativeMcpClient = OauthCapableMcpClient | KeyRequiredMcpClient;
|
|
|
86
91
|
* quotes back out. Untested on Windows.
|
|
87
92
|
*/
|
|
88
93
|
export declare function openExternalUrl(url: string): ReturnType<typeof spawn.sync>;
|
|
89
|
-
/**
|
|
94
|
+
/** Writes exit 0 having saved nothing, a disabled entry, or a keyless one. */
|
|
90
95
|
export declare function hermesRegistrationEnabled(home: string): {
|
|
91
96
|
ok: boolean;
|
|
92
97
|
detail?: string;
|
|
@@ -94,7 +99,7 @@ export declare function hermesRegistrationEnabled(home: string): {
|
|
|
94
99
|
};
|
|
95
100
|
export declare const NATIVE_MCP_CLIENTS: readonly NativeMcpClient[];
|
|
96
101
|
/** Native descriptor by harness id; Cursor and OpenClaw have none. */
|
|
97
|
-
export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code", NativeMcpClient>;
|
|
102
|
+
export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "pi" | "claude-code", NativeMcpClient>;
|
|
98
103
|
export declare const OPENCLAW: SupportedCommand;
|
|
99
104
|
/** "printed" = handed to the user to paste; the walkthrough was never started for them. */
|
|
100
105
|
export type WalkthroughOutcome = 'launched' | 'printed';
|
|
@@ -102,4 +107,5 @@ export type WalkthroughOutcome = 'launched' | 'printed';
|
|
|
102
107
|
type OnWalkthroughDecided = (outcome: WalkthroughOutcome) => void | Promise<void>;
|
|
103
108
|
export declare function launchNativeMcpClient(client: NativeMcpClient, onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
104
109
|
export declare function launchOmpWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
110
|
+
export declare function launchPiWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
105
111
|
export declare function launchOpenClawWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
@@ -3,12 +3,12 @@ import { SKILL_INSTALL_TIMEOUT_MS } from './cli-install.js';
|
|
|
3
3
|
import { detectHumanInitiated } from './harness.js';
|
|
4
4
|
import { harnessConfigPath } from './harness-detect.js';
|
|
5
5
|
import { errLine, sanitizeLine, warnLine } from './output.js';
|
|
6
|
-
import { ConnectStepError, spawnStepError, } from './connect-runtime.js';
|
|
6
|
+
import { ConnectInterruptedError, ConnectStepError, spawnStepError, } from './connect-runtime.js';
|
|
7
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 { HERMES_PLUGIN_VERSION, installHermesPlugin, setHermesWebBackends, } from './hermes-plugin.js';
|
|
11
|
+
import { HERMES_PLUGIN_VERSION, installHermesPlugin, removeHermesMcpEntry, setHermesWebBackends, writeHermesMcpEntry, } 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.
|
|
@@ -100,11 +100,19 @@ function launchHermesWalkthrough() {
|
|
|
100
100
|
}
|
|
101
101
|
handOverTerminal('hermes', ['--resume', sessionId], 'Hermes');
|
|
102
102
|
}
|
|
103
|
-
/**
|
|
103
|
+
/** Writes exit 0 having saved nothing, a disabled entry, or a keyless one. */
|
|
104
104
|
export function hermesRegistrationEnabled(home) {
|
|
105
105
|
const entry = readHermesEntry(home);
|
|
106
|
-
if (entry.state === 'enabled')
|
|
107
|
-
|
|
106
|
+
if (entry.state === 'enabled') {
|
|
107
|
+
if (entry.usesKeyHeader)
|
|
108
|
+
return { ok: true };
|
|
109
|
+
// Exit codes carry no signal, so a corrupt entry can only be caught here.
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
detail: 'the entry does not read the seeded key',
|
|
113
|
+
tag: 'hermes_entry_no_key_header',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
108
116
|
// Each of these is a different repair, so none of them share wording.
|
|
109
117
|
const detail = {
|
|
110
118
|
disabled: 'the entry was saved disabled',
|
|
@@ -117,28 +125,47 @@ export function hermesRegistrationEnabled(home) {
|
|
|
117
125
|
}
|
|
118
126
|
// A plain Error settles unexpected_error; cli_connect_stage is load-bearing.
|
|
119
127
|
function requireHermesHome() {
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
|
|
128
|
+
const resolved = resolveHermesHome();
|
|
129
|
+
if (typeof resolved === 'string')
|
|
130
|
+
return resolved;
|
|
131
|
+
throw new ConnectStepError("Could not determine Hermes' home directory from `hermes dump`", 'invalid_config', { failureDetail: `hermes_home_undetermined_${resolved.reason}` });
|
|
132
|
+
}
|
|
133
|
+
/** Our own write, so `mcp add`'s install-time probe never disables the entry. */
|
|
134
|
+
function registerHermesEntry(home, mcpUrl) {
|
|
135
|
+
try {
|
|
136
|
+
writeHermesMcpEntry(home, mcpUrl);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
// A Ctrl+C must abandon the attempt, not fall through to `mcp add`.
|
|
140
|
+
if (error instanceof ConnectInterruptedError)
|
|
141
|
+
throw error;
|
|
142
|
+
return false;
|
|
123
143
|
}
|
|
124
|
-
return home;
|
|
144
|
+
return hermesRegistrationEnabled(home).ok;
|
|
125
145
|
}
|
|
126
146
|
function seedHermesKey(apiKey) {
|
|
127
147
|
const home = requireHermesHome();
|
|
128
148
|
const restore = captureHermesKeyRestore(home);
|
|
129
149
|
writeHermesKey(home, apiKey);
|
|
150
|
+
// Set before the write, which can land even when it later throws.
|
|
151
|
+
let wroteEntry = false;
|
|
130
152
|
return {
|
|
153
|
+
home,
|
|
131
154
|
// Pins HERMES_HOME so a sticky profile can't shadow this key.
|
|
132
155
|
env: { ...process.env, HERMES_HOME: home },
|
|
133
156
|
// Three: a failed `mcp remove` adds an extra Overwrite prompt.
|
|
134
157
|
stdinInput: 'y\ny\ny\n',
|
|
135
158
|
note: `Wrote ${HERMES_KEY_VAR} to ${hermesEnvPath(home)} — Hermes reads the key from there.`,
|
|
136
|
-
rollback: () => rollbackHermesKey(home, restore),
|
|
159
|
+
rollback: () => rollbackHermesKey(home, restore, wroteEntry),
|
|
160
|
+
register: (mcpUrl) => {
|
|
161
|
+
wroteEntry = true;
|
|
162
|
+
return registerHermesEntry(home, mcpUrl);
|
|
163
|
+
},
|
|
137
164
|
confirmRegistered: () => hermesRegistrationEnabled(home),
|
|
138
165
|
};
|
|
139
166
|
}
|
|
140
167
|
// A failed re-run must not cost a working key.
|
|
141
|
-
function rollbackHermesKey(home, restore) {
|
|
168
|
+
function rollbackHermesKey(home, restore, wroteEntry) {
|
|
142
169
|
try {
|
|
143
170
|
// The next `hermes mcp add` silently reuses whatever value survives here.
|
|
144
171
|
restore();
|
|
@@ -146,13 +173,23 @@ function rollbackHermesKey(home, restore) {
|
|
|
146
173
|
catch {
|
|
147
174
|
warnLine(`Could not restore ${HERMES_KEY_VAR} in ${hermesEnvPath(home)}; check it by hand.`);
|
|
148
175
|
}
|
|
176
|
+
if (!wroteEntry)
|
|
177
|
+
return;
|
|
178
|
+
try {
|
|
179
|
+
// Left behind, the row stays enabled with no key behind it.
|
|
180
|
+
removeHermesMcpEntry(home);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
warnLine(`Could not remove the tinyfish entry in ${hermesConfigPath(home)}; check it by hand.`);
|
|
184
|
+
}
|
|
149
185
|
}
|
|
150
186
|
// Hermes is keyRequired; a missing key here is a resolution bug.
|
|
151
|
-
function installHermesWebPlugin({ apiKey, verbose }) {
|
|
187
|
+
function installHermesWebPlugin({ apiKey, verbose, seededHome, }) {
|
|
152
188
|
if (!apiKey) {
|
|
153
189
|
throw new ConnectStepError('No TinyFish API key was available to install the Hermes web plugin', 'invalid_config');
|
|
154
190
|
}
|
|
155
|
-
|
|
191
|
+
// Reuses the seed's home: a second `hermes dump` can disagree with the first.
|
|
192
|
+
const home = seededHome ?? requireHermesHome();
|
|
156
193
|
installHermesPlugin(home, apiKey, { verbose });
|
|
157
194
|
setHermesWebBackends(home);
|
|
158
195
|
}
|
|
@@ -295,6 +332,9 @@ export async function launchNativeMcpClient(client, onDecided) {
|
|
|
295
332
|
export function launchOmpWalkthrough(onDecided) {
|
|
296
333
|
return deliverWalkthrough('omp', 'omp', DEFAULT_ONBOARDING_PROMPT, () => handOverTerminal('omp', [DEFAULT_ONBOARDING_PROMPT], 'omp'), onDecided);
|
|
297
334
|
}
|
|
335
|
+
export function launchPiWalkthrough(onDecided) {
|
|
336
|
+
return deliverWalkthrough('Pi', 'pi', DEFAULT_ONBOARDING_PROMPT, () => handOverTerminal('pi', [DEFAULT_ONBOARDING_PROMPT], 'Pi'), onDecided);
|
|
337
|
+
}
|
|
298
338
|
export function launchOpenClawWalkthrough(onDecided) {
|
|
299
339
|
return deliverWalkthrough('OpenClaw', 'openclaw', OPENCLAW_ONBOARDING_PROMPT, () => handOverTerminal('openclaw', ['chat', '--message', OPENCLAW_ONBOARDING_PROMPT], 'OpenClaw'), onDecided);
|
|
300
340
|
}
|
|
@@ -11,6 +11,7 @@ export declare function checkCredential(): {
|
|
|
11
11
|
};
|
|
12
12
|
export declare function checkAuthCall(auth: VerifyResult | undefined): DoctorCheck;
|
|
13
13
|
export declare function verifyHarnessKey(status: RegistrationStatus, cliAuth: Promise<VerifyResult> | undefined, mcpUrl: string): Promise<VerifyResult> | undefined;
|
|
14
|
+
export declare function checkPiAdapter(status: RegistrationStatus): DoctorCheck;
|
|
14
15
|
export interface HermesPluginResult {
|
|
15
16
|
check: DoctorCheck;
|
|
16
17
|
observedVersion?: string;
|
|
@@ -4,6 +4,7 @@ import { AuthMode, Registered } from './harness-detect.js';
|
|
|
4
4
|
import { resolveHermesHome } from './hermes-env.js';
|
|
5
5
|
import { hermesWebBackendsOurs, readHermesPluginStatus, } from './hermes-plugin.js';
|
|
6
6
|
import { endpointOf, isDefaultEndpoint } from './mcp-endpoint.js';
|
|
7
|
+
import { PI_ADAPTER_INSTALL_COMMAND, piMcpAdapterState } from './pi-config.js';
|
|
7
8
|
import { boundedVersion } from './output.js';
|
|
8
9
|
import { verifyMcpHealth } from './verify.js';
|
|
9
10
|
// A green auth mode with a red auth call proves nothing, so the call's verdict gates the claim.
|
|
@@ -250,6 +251,40 @@ function hermesPluginVerdict(home) {
|
|
|
250
251
|
observedVersion: boundedVersion(probe.status.plugin_version ?? ''),
|
|
251
252
|
};
|
|
252
253
|
}
|
|
254
|
+
// Warn, not fail: the TinyFish skill works in pi with no adapter, so nothing is broken.
|
|
255
|
+
export function checkPiAdapter(status) {
|
|
256
|
+
const base = {
|
|
257
|
+
id: 'pi-adapter',
|
|
258
|
+
title: 'Pi MCP adapter',
|
|
259
|
+
harness: 'pi',
|
|
260
|
+
scope: 'harness',
|
|
261
|
+
};
|
|
262
|
+
if (!status.detected)
|
|
263
|
+
return { ...base, status: 'skip', detail: 'harness not installed' };
|
|
264
|
+
if (status.registered !== Registered.Yes) {
|
|
265
|
+
const why = status.registered === Registered.Unknown
|
|
266
|
+
? 'pi registration could not be determined'
|
|
267
|
+
: 'TinyFish is not registered in pi';
|
|
268
|
+
return { ...base, status: 'skip', detail: why };
|
|
269
|
+
}
|
|
270
|
+
const state = piMcpAdapterState();
|
|
271
|
+
if (state === 'installed') {
|
|
272
|
+
return { ...base, status: 'pass', detail: 'installed, so pi reads the tinyfish MCP entry' };
|
|
273
|
+
}
|
|
274
|
+
if (state === 'unknown') {
|
|
275
|
+
return {
|
|
276
|
+
...base,
|
|
277
|
+
status: 'warn',
|
|
278
|
+
detail: "could not read pi's settings.json, so whether the adapter is installed is unknown",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
...base,
|
|
283
|
+
status: 'warn',
|
|
284
|
+
detail: 'not installed, so the tinyfish MCP entry is dormant (the TinyFish skill works without it). ' +
|
|
285
|
+
`Fix: ${PI_ADAPTER_INSTALL_COMMAND}`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
253
288
|
export function checkHermesPlugin(status) {
|
|
254
289
|
const base = {
|
|
255
290
|
id: 'hermes-plugin',
|
|
@@ -263,12 +298,12 @@ export function checkHermesPlugin(status) {
|
|
|
263
298
|
if (status.registered !== Registered.Yes) {
|
|
264
299
|
return { check: { ...base, status: 'skip', detail: 'TinyFish is not registered in Hermes' } };
|
|
265
300
|
}
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
301
|
+
const resolved = resolveHermesHome();
|
|
302
|
+
if (typeof resolved !== 'string') {
|
|
268
303
|
return {
|
|
269
304
|
check: { ...base, status: 'warn', detail: "could not determine Hermes' home directory" },
|
|
270
305
|
};
|
|
271
306
|
}
|
|
272
|
-
const { observedVersion, ...verdict } = hermesPluginVerdict(
|
|
307
|
+
const { observedVersion, ...verdict } = hermesPluginVerdict(resolved);
|
|
273
308
|
return { check: { ...base, ...verdict }, observedVersion };
|
|
274
309
|
}
|
|
@@ -31,6 +31,7 @@ declare const doctorCheckSchema: z.ZodObject<{
|
|
|
31
31
|
codex: "codex";
|
|
32
32
|
hermes: "hermes";
|
|
33
33
|
opencode: "opencode";
|
|
34
|
+
pi: "pi";
|
|
34
35
|
"claude-code": "claude-code";
|
|
35
36
|
}>>;
|
|
36
37
|
scope: z.ZodEnum<{
|
|
@@ -48,6 +49,7 @@ declare const doctorHarnessSchema: z.ZodObject<{
|
|
|
48
49
|
codex: "codex";
|
|
49
50
|
hermes: "hermes";
|
|
50
51
|
opencode: "opencode";
|
|
52
|
+
pi: "pi";
|
|
51
53
|
"claude-code": "claude-code";
|
|
52
54
|
}>;
|
|
53
55
|
detected: z.ZodBoolean;
|
|
@@ -74,6 +76,7 @@ declare const doctorRepairSchema: z.ZodObject<{
|
|
|
74
76
|
codex: "codex";
|
|
75
77
|
hermes: "hermes";
|
|
76
78
|
opencode: "opencode";
|
|
79
|
+
pi: "pi";
|
|
77
80
|
"claude-code": "claude-code";
|
|
78
81
|
}>>;
|
|
79
82
|
command: z.ZodString;
|
|
@@ -102,6 +105,7 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
102
105
|
codex: "codex";
|
|
103
106
|
hermes: "hermes";
|
|
104
107
|
opencode: "opencode";
|
|
108
|
+
pi: "pi";
|
|
105
109
|
"claude-code": "claude-code";
|
|
106
110
|
}>>;
|
|
107
111
|
scope: z.ZodEnum<{
|
|
@@ -119,6 +123,7 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
119
123
|
codex: "codex";
|
|
120
124
|
hermes: "hermes";
|
|
121
125
|
opencode: "opencode";
|
|
126
|
+
pi: "pi";
|
|
122
127
|
"claude-code": "claude-code";
|
|
123
128
|
}>;
|
|
124
129
|
detected: z.ZodBoolean;
|
|
@@ -145,6 +150,7 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
145
150
|
codex: "codex";
|
|
146
151
|
hermes: "hermes";
|
|
147
152
|
opencode: "opencode";
|
|
153
|
+
pi: "pi";
|
|
148
154
|
"claude-code": "claude-code";
|
|
149
155
|
}>>;
|
|
150
156
|
command: z.ZodString;
|
|
@@ -23,5 +23,7 @@ export interface HarnessDetection {
|
|
|
23
23
|
configPath: string;
|
|
24
24
|
/** False with `detected` true means a stale config dir: nothing to run. */
|
|
25
25
|
binaryOnPath: boolean;
|
|
26
|
+
/** True with no config dir means installed but never launched. */
|
|
27
|
+
configDirExists: boolean;
|
|
26
28
|
}
|
|
27
29
|
export declare function detectInstalledHarnesses(): HarnessDetection[];
|
|
@@ -3,6 +3,7 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import which from 'which';
|
|
5
5
|
import { ALL_HARNESSES, harnessSpec } from './harness-spec.js';
|
|
6
|
+
import { piAgentDir, piBinaryIsPi } from './pi-config.js';
|
|
6
7
|
export { ALL_HARNESSES };
|
|
7
8
|
export const HARNESS_DISPLAY_NAMES = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).displayName]));
|
|
8
9
|
export const RELOAD_ACTION = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).reloadAction]));
|
|
@@ -31,13 +32,18 @@ export function commandOnPath(command) {
|
|
|
31
32
|
return false;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
35
|
+
// PI_CODING_AGENT_DIR moves pi's dir; a guessed path would be written unread.
|
|
36
|
+
const CONFIG_PATH_RESOLVERS = { pi: piAgentDir };
|
|
34
37
|
export function harnessConfigPath(harness) {
|
|
35
|
-
|
|
38
|
+
const resolved = CONFIG_PATH_RESOLVERS[harness]?.();
|
|
39
|
+
return resolved ?? path.join(os.homedir(), harnessSpec(harness).configDir); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
|
|
36
40
|
}
|
|
37
41
|
/** For reason strings that name a location; keeps them in sync with the spec. */
|
|
38
42
|
export function harnessDisplayPath(harness) {
|
|
39
43
|
return `~/${harnessSpec(harness).configDir}`;
|
|
40
44
|
}
|
|
45
|
+
// `pi` is a two-letter name an unrelated npm package also claims; confirm before trusting it.
|
|
46
|
+
const BINARY_VERIFIERS = { pi: piBinaryIsPi };
|
|
41
47
|
export function detectInstalledHarnesses() {
|
|
42
48
|
return ALL_HARNESSES.map((harness) => {
|
|
43
49
|
const configPath = harnessConfigPath(harness);
|
|
@@ -48,13 +54,15 @@ export function detectInstalledHarnesses() {
|
|
|
48
54
|
catch {
|
|
49
55
|
configDirExists = false;
|
|
50
56
|
}
|
|
51
|
-
// Config dir means ran once;
|
|
52
|
-
const
|
|
57
|
+
// Config dir means ran once; a verified binary means runnable now.
|
|
58
|
+
const onPath = commandOnPath(HARNESS_COMMANDS[harness]);
|
|
59
|
+
const binaryOnPath = onPath && (BINARY_VERIFIERS[harness]?.() ?? true);
|
|
53
60
|
return {
|
|
54
61
|
harness,
|
|
55
62
|
detected: configDirExists || binaryOnPath,
|
|
56
63
|
configPath,
|
|
57
64
|
binaryOnPath,
|
|
65
|
+
configDirExists,
|
|
58
66
|
};
|
|
59
67
|
});
|
|
60
68
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The `skills` CLI's own agent names, not ours. */
|
|
2
|
-
export type SkillAgent = 'claude-code' | 'codex' | 'cursor' | 'hermes-agent' | 'opencode';
|
|
2
|
+
export type SkillAgent = 'claude-code' | 'codex' | 'cursor' | 'hermes-agent' | 'opencode' | 'pi';
|
|
3
3
|
export interface HarnessSupportCheck {
|
|
4
4
|
args: string[];
|
|
5
5
|
/** Missing → nothing can work; setup fails. */
|
|
@@ -74,8 +74,8 @@ export interface HarnessSpec {
|
|
|
74
74
|
keyHeldByCli?: true;
|
|
75
75
|
/** Connect writes the MCP config file; no harness binary is spawned. */
|
|
76
76
|
cliWritesConfig?: true;
|
|
77
|
-
/**
|
|
78
|
-
|
|
77
|
+
/** A second reader (Cursor's IDE) loads the config with no harness binary on PATH. */
|
|
78
|
+
readableWithoutBinary?: true;
|
|
79
79
|
/** `mcp add` succeeds unauthenticated; OAuth lands at first tool use. */
|
|
80
80
|
authDeferredAtInstall?: true;
|
|
81
81
|
}
|
|
@@ -139,6 +139,7 @@ export declare const HARNESS_SPECS: {
|
|
|
139
139
|
configDir: string;
|
|
140
140
|
reloadAction: string;
|
|
141
141
|
skillAgent: "cursor";
|
|
142
|
+
readableWithoutBinary: true;
|
|
142
143
|
canVerifyAuth: true;
|
|
143
144
|
keyHeldByCli: true;
|
|
144
145
|
cliWritesConfig: true;
|
|
@@ -192,7 +193,6 @@ export declare const HARNESS_SPECS: {
|
|
|
192
193
|
displayName: string;
|
|
193
194
|
configDir: string;
|
|
194
195
|
reloadAction: string;
|
|
195
|
-
configPathFromBinary: true;
|
|
196
196
|
canVerifyAuth: true;
|
|
197
197
|
keyHeldByCli: true;
|
|
198
198
|
cliWritesConfig: true;
|
|
@@ -236,6 +236,16 @@ export declare const HARNESS_SPECS: {
|
|
|
236
236
|
removals: never[];
|
|
237
237
|
postConnectNote: string;
|
|
238
238
|
};
|
|
239
|
+
pi: {
|
|
240
|
+
command: string;
|
|
241
|
+
displayName: string;
|
|
242
|
+
configDir: string;
|
|
243
|
+
reloadAction: string;
|
|
244
|
+
skillAgent: "pi";
|
|
245
|
+
canVerifyAuth: true;
|
|
246
|
+
keyHeldByCli: true;
|
|
247
|
+
cliWritesConfig: true;
|
|
248
|
+
};
|
|
239
249
|
};
|
|
240
250
|
/** Derived from spec keys; one entry extends every union. */
|
|
241
251
|
export type Harness = keyof typeof HARNESS_SPECS;
|
|
@@ -246,4 +256,4 @@ export type NonNativeHarness = {
|
|
|
246
256
|
[K in Harness]: 'urlStyle' extends keyof (typeof HARNESS_SPECS)[K] ? never : K;
|
|
247
257
|
}[Harness];
|
|
248
258
|
/** Native MCP harnesses carry add-generation fields; the rest override connect. */
|
|
249
|
-
export declare const NATIVE_HARNESSES: ("openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code")[];
|
|
259
|
+
export declare const NATIVE_HARNESSES: ("openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "pi" | "claude-code")[];
|
package/dist/lib/harness-spec.js
CHANGED
|
@@ -114,6 +114,7 @@ export const HARNESS_SPECS = {
|
|
|
114
114
|
configDir: '.cursor',
|
|
115
115
|
reloadAction: 'reload the window',
|
|
116
116
|
skillAgent: 'cursor',
|
|
117
|
+
readableWithoutBinary: true,
|
|
117
118
|
canVerifyAuth: true,
|
|
118
119
|
keyHeldByCli: true,
|
|
119
120
|
cliWritesConfig: true,
|
|
@@ -167,7 +168,6 @@ export const HARNESS_SPECS = {
|
|
|
167
168
|
displayName: 'omp',
|
|
168
169
|
configDir: '.omp',
|
|
169
170
|
reloadAction: 'restart it',
|
|
170
|
-
configPathFromBinary: true,
|
|
171
171
|
canVerifyAuth: true,
|
|
172
172
|
keyHeldByCli: true,
|
|
173
173
|
cliWritesConfig: true,
|
|
@@ -213,6 +213,16 @@ export const HARNESS_SPECS = {
|
|
|
213
213
|
removals: [],
|
|
214
214
|
postConnectNote: OPENCODE_MODEL_NOTE,
|
|
215
215
|
},
|
|
216
|
+
pi: {
|
|
217
|
+
command: 'pi',
|
|
218
|
+
displayName: 'Pi',
|
|
219
|
+
configDir: '.pi/agent',
|
|
220
|
+
reloadAction: 'restart it',
|
|
221
|
+
skillAgent: 'pi',
|
|
222
|
+
canVerifyAuth: true,
|
|
223
|
+
keyHeldByCli: true,
|
|
224
|
+
cliWritesConfig: true,
|
|
225
|
+
},
|
|
216
226
|
};
|
|
217
227
|
export const ALL_HARNESSES = Object.keys(HARNESS_SPECS);
|
|
218
228
|
export function harnessSpec(harness) {
|
package/dist/lib/harness.js
CHANGED
|
@@ -28,6 +28,8 @@ const HARNESS_FINGERPRINTS = [
|
|
|
28
28
|
{ name: 'hermes', matches: (env) => hasVarWithPrefix(env, 'HERMES_') },
|
|
29
29
|
// Exact key, not a prefix: opencode writes OPENCODE=1 at startup, reads every OPENCODE_* as config.
|
|
30
30
|
{ name: 'opencode', matches: (env) => Boolean(env['OPENCODE']) },
|
|
31
|
+
// Observed: pi launched from Claude Code inherits CLAUDECODE=1.
|
|
32
|
+
{ name: 'pi', matches: (env) => env['PI_CODING_AGENT'] === 'true' },
|
|
31
33
|
{
|
|
32
34
|
name: 'claude-code',
|
|
33
35
|
matches: (env) => env['CLAUDECODE'] === '1' || Boolean(env['CLAUDE_CODE_ENTRYPOINT']),
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** Every message about the entry names this path. */
|
|
2
2
|
export declare function hermesConfigPath(home: string): string;
|
|
3
|
+
export declare const HERMES_HEADER_TEMPLATE = "Bearer ${MCP_TINYFISH_API_KEY}";
|
|
4
|
+
/** Both the writer and the gate key on this, so they cannot disagree. */
|
|
5
|
+
export declare const HERMES_MCP_SERVER_KEY = "tinyfish";
|
|
3
6
|
/** Callers branch on these; each is a different repair. */
|
|
4
7
|
export type HermesEntry = {
|
|
5
8
|
state: 'unreadable';
|
|
@@ -8,7 +8,9 @@ export function hermesConfigPath(home) {
|
|
|
8
8
|
return path.join(home, 'config.yaml');
|
|
9
9
|
}
|
|
10
10
|
// Hermes persists the key as an interpolation template (mcp_config.py:174-182).
|
|
11
|
-
const
|
|
11
|
+
export const HERMES_HEADER_TEMPLATE = `Bearer \${${HERMES_KEY_VAR}}`;
|
|
12
|
+
/** Both the writer and the gate key on this, so they cannot disagree. */
|
|
13
|
+
export const HERMES_MCP_SERVER_KEY = 'tinyfish';
|
|
12
14
|
// Foreign file: model only what we read, tolerate the rest.
|
|
13
15
|
const entrySchema = z.looseObject({
|
|
14
16
|
// Anything goes: the runtime defaults unrecognised values to enabled.
|
|
@@ -16,7 +18,9 @@ const entrySchema = z.looseObject({
|
|
|
16
18
|
headers: z.record(z.string(), z.unknown()).optional(),
|
|
17
19
|
});
|
|
18
20
|
const configSchema = z
|
|
19
|
-
.looseObject({
|
|
21
|
+
.looseObject({
|
|
22
|
+
mcp_servers: z.looseObject({ [HERMES_MCP_SERVER_KEY]: entrySchema.nullish() }).nullish(),
|
|
23
|
+
})
|
|
20
24
|
.nullish();
|
|
21
25
|
const TRUTHY_STRINGS = new Set(['true', '1', 'yes', 'on']);
|
|
22
26
|
const FALSY_STRINGS = new Set(['false', '0', 'no', 'off']);
|
|
@@ -42,7 +46,7 @@ function isEnabled(value) {
|
|
|
42
46
|
/** Only the header template proves this entry uses the seeded key. */
|
|
43
47
|
function hasKeyHeader(headers) {
|
|
44
48
|
const authorization = Object.entries(headers ?? {}).find(([name]) => name.toLowerCase() === 'authorization');
|
|
45
|
-
return authorization?.[1] ===
|
|
49
|
+
return authorization?.[1] === HERMES_HEADER_TEMPLATE;
|
|
46
50
|
}
|
|
47
51
|
/** Connect's gate and doctor's probe share this, so they cannot disagree. */
|
|
48
52
|
export function readHermesEntry(home) {
|
|
@@ -60,7 +64,7 @@ export function readHermesEntry(home) {
|
|
|
60
64
|
catch {
|
|
61
65
|
return { state: 'unparseable' };
|
|
62
66
|
}
|
|
63
|
-
const entry = config?.mcp_servers?.
|
|
67
|
+
const entry = config?.mcp_servers?.[HERMES_MCP_SERVER_KEY];
|
|
64
68
|
if (!entry)
|
|
65
69
|
return { state: 'absent' };
|
|
66
70
|
const usesKeyHeader = hasKeyHeader(entry.headers);
|
package/dist/lib/hermes-env.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
/** Each reason is a different repair, so telemetry keeps them apart. */
|
|
2
|
+
export type HermesHomeReason = 'timeout' | 'exit' | 'no_line' | 'relative';
|
|
3
|
+
/** Failure carries why, so connect tags it without a second probe. */
|
|
4
|
+
export interface HermesHomeFailure {
|
|
5
|
+
reason: HermesHomeReason;
|
|
6
|
+
}
|
|
1
7
|
/** Asking Hermes beats reimplementing its profile override, which we'd drift from. */
|
|
2
|
-
export declare function resolveHermesHome(): string |
|
|
8
|
+
export declare function resolveHermesHome(): string | HermesHomeFailure;
|
|
3
9
|
export declare const HERMES_KEY_VAR = "MCP_TINYFISH_API_KEY";
|
|
4
10
|
/** Named in every message about the seeded key, so no caller rebuilds the path. */
|
|
5
11
|
export declare function hermesEnvPath(home: string): string;
|
package/dist/lib/hermes-env.js
CHANGED
|
@@ -14,13 +14,16 @@ export function resolveHermesHome() {
|
|
|
14
14
|
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
15
15
|
timeout: HARNESS_PROBE_TIMEOUT_MS,
|
|
16
16
|
});
|
|
17
|
+
if (result.error?.code === 'ETIMEDOUT') {
|
|
18
|
+
return { reason: 'timeout' };
|
|
19
|
+
}
|
|
17
20
|
if (result.error || result.status !== 0)
|
|
18
|
-
return
|
|
21
|
+
return { reason: 'exit' };
|
|
19
22
|
const raw = HERMES_HOME_LINE.exec(sanitizeLine(`${result.stdout ?? ''}\n${result.stderr ?? ''}`))?.[1];
|
|
20
23
|
if (!raw)
|
|
21
|
-
return
|
|
24
|
+
return { reason: 'no_line' };
|
|
22
25
|
const expanded = raw === '~' || raw.startsWith('~/') ? path.join(os.homedir(), raw.slice(2)) : raw;
|
|
23
|
-
return path.isAbsolute(expanded) ? expanded :
|
|
26
|
+
return path.isAbsolute(expanded) ? expanded : { reason: 'relative' };
|
|
24
27
|
}
|
|
25
28
|
export const HERMES_KEY_VAR = 'MCP_TINYFISH_API_KEY';
|
|
26
29
|
/** Named in every message about the seeded key, so no caller rebuilds the path. */
|
|
@@ -4,6 +4,10 @@ export declare const HERMES_PLUGIN_SHA = "496cd63fefd982bbaa8a85ce78ef2270d70098
|
|
|
4
4
|
export declare const HERMES_PLUGIN_VERSION = "0.1.0";
|
|
5
5
|
export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish-io/tinyfish-web-agent-integrations/hermes --ref 496cd63fefd982bbaa8a85ce78ef2270d700984f --enable";
|
|
6
6
|
export declare function installHermesPlugin(home: string, apiKey: string, { verbose }: Pick<InstallOptions, 'verbose'>): void;
|
|
7
|
+
/** Absent `enabled` reads as enabled, so never write this field-by-field. */
|
|
8
|
+
export declare function writeHermesMcpEntry(home: string, mcpUrl: string): void;
|
|
9
|
+
/** Retracts our row; an absent `enabled` reads as enabled, so a partial one is live. */
|
|
10
|
+
export declare function removeHermesMcpEntry(home: string): void;
|
|
7
11
|
/** After install only: backends must never name an absent provider. */
|
|
8
12
|
export declare function setHermesWebBackends(home: string): void;
|
|
9
13
|
/** Ours-only unsets: a user-chosen backend value survives the uninstall. */
|
|
@@ -7,6 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
import { capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
|
|
8
8
|
import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
|
|
9
9
|
import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
|
|
10
|
+
import { HERMES_HEADER_TEMPLATE, HERMES_MCP_SERVER_KEY } from './hermes-config.js';
|
|
10
11
|
import { errLine, parseJson } from './output.js';
|
|
11
12
|
// Bump per CLI release.
|
|
12
13
|
export const HERMES_PLUGIN_SHA = '496cd63fefd982bbaa8a85ce78ef2270d700984f';
|
|
@@ -95,6 +96,21 @@ function configWrite(home, args) {
|
|
|
95
96
|
replay(capturedOutput(result));
|
|
96
97
|
throw error;
|
|
97
98
|
}
|
|
99
|
+
/** Absent `enabled` reads as enabled, so never write this field-by-field. */
|
|
100
|
+
export function writeHermesMcpEntry(home, mcpUrl) {
|
|
101
|
+
const entry = {
|
|
102
|
+
url: mcpUrl,
|
|
103
|
+
headers: { Authorization: HERMES_HEADER_TEMPLATE },
|
|
104
|
+
enabled: true,
|
|
105
|
+
};
|
|
106
|
+
const result = hermesConfig(['set', `mcp_servers.${HERMES_MCP_SERVER_KEY}`, JSON.stringify(entry)], home);
|
|
107
|
+
// `config set` exits 0 on a bad key, so only an interrupt is worth raising here.
|
|
108
|
+
throwIfInterrupted(result);
|
|
109
|
+
}
|
|
110
|
+
/** Retracts our row; an absent `enabled` reads as enabled, so a partial one is live. */
|
|
111
|
+
export function removeHermesMcpEntry(home) {
|
|
112
|
+
configWrite(home, ['unset', `mcp_servers.${HERMES_MCP_SERVER_KEY}`]);
|
|
113
|
+
}
|
|
98
114
|
const HERMES_BACKEND_KEYS = ['web.search_backend', 'web.extract_backend'];
|
|
99
115
|
// Dispatch falls back to web.backend, and the plugin's own setup can set it.
|
|
100
116
|
const HERMES_OWNED_BACKEND_KEYS = [...HERMES_BACKEND_KEYS, 'web.backend'];
|