@tiny-fish/cli 0.39.1-next.309 → 0.40.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/connect.d.ts +2 -14
- package/dist/commands/connect.js +109 -75
- package/dist/commands/doctor.js +3 -1
- package/dist/commands/profile.d.ts +0 -3
- package/dist/commands/profile.js +7 -22
- package/dist/commands/run.js +20 -40
- package/dist/commands/runs.js +24 -58
- package/dist/lib/auth.d.ts +1 -3
- package/dist/lib/auth.js +1 -1
- package/dist/lib/claude-config.d.ts +0 -4
- package/dist/lib/claude-config.js +4 -4
- package/dist/lib/cli-install.d.ts +0 -1
- package/dist/lib/cli-install.js +0 -1
- package/dist/lib/client.js +82 -147
- package/dist/lib/connect-all-auth.js +1 -1
- 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 +21 -32
- package/dist/lib/connect-clients.d.ts +11 -8
- package/dist/lib/connect-clients.js +53 -13
- package/dist/lib/connect-fallback.d.ts +2 -1
- package/dist/lib/connect-picker.d.ts +3 -2
- package/dist/lib/connect-runtime.js +4 -6
- 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 +20 -39
- package/dist/lib/harness-spec.js +12 -31
- 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 -2
- package/dist/lib/hermes-env.js +7 -6
- package/dist/lib/hermes-plugin.d.ts +4 -0
- package/dist/lib/hermes-plugin.js +17 -9
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +9 -0
- package/dist/lib/pi-config.d.ts +26 -0
- package/dist/lib/pi-config.js +111 -0
- package/dist/lib/registration-detect.d.ts +0 -1
- package/dist/lib/registration-detect.js +60 -81
- package/dist/lib/setup-telemetry.d.ts +13 -7
- package/dist/lib/setup-telemetry.js +5 -5
- package/dist/lib/skill-install.d.ts +1 -0
- package/dist/lib/skill-install.js +43 -44
- package/dist/lib/types.d.ts +4 -3
- package/dist/program.js +2 -3
- package/package.json +1 -2
package/dist/lib/connect-all.js
CHANGED
|
@@ -7,7 +7,6 @@ import { installTinyFishCli, } from './cli-install.js';
|
|
|
7
7
|
import { authGate, mapConnectError, OAUTH_SIGN_IN_TIMEOUT_MS } from './connect-all-auth.js';
|
|
8
8
|
import { actionable, computeExitCode, reloadHint, renderSummary } from './connect-all-summary.js';
|
|
9
9
|
import { planText, uninstallHarness, uninstallPlanText } from './connect-all-uninstall.js';
|
|
10
|
-
import { NATIVE_BY_HARNESS } from './connect-clients.js';
|
|
11
10
|
import { createStdinPrompt, runCliFallback } from './connect-fallback.js';
|
|
12
11
|
import { gateApiKey } from './connect-preflight.js';
|
|
13
12
|
import { ConnectInterruptedError } from './connect-runtime.js';
|
|
@@ -19,21 +18,6 @@ import { errLine, outLine, setErrIndent } from './output.js';
|
|
|
19
18
|
import { sendSetupCompleted, sendSetupStarted, } from './setup-telemetry.js';
|
|
20
19
|
import { verifyMcpAuth, verifyMcpHealth } from './verify.js';
|
|
21
20
|
const STEP_INDENT = ' ';
|
|
22
|
-
// Derived: a native add carries the key, or the CLI holds it itself.
|
|
23
|
-
const KEY_AUTH_CAPABLE = new Set(ALL_HARNESSES.filter((harness) => !!NATIVE_BY_HARNESS.get(harness)?.keyAuth || harnessSpec(harness).keyHeldByCli));
|
|
24
|
-
async function runHarnessConnect(harness, opts, extras) {
|
|
25
|
-
return connectHarness(harness, {
|
|
26
|
-
apiKey: opts.apiKey,
|
|
27
|
-
mcpUrl: opts.mcpUrl,
|
|
28
|
-
launch: false,
|
|
29
|
-
keyAuthOnly: extras.keyAuthOnly,
|
|
30
|
-
authTimeoutMs: extras.authTimeoutMs,
|
|
31
|
-
hoistedCliInstall: extras.hoistedCliInstall,
|
|
32
|
-
onPostInstallFailed: extras.onPostInstallFailed,
|
|
33
|
-
verbose: opts.verbose,
|
|
34
|
-
deferOutro: extras.deferOutro,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
21
|
/** Undefined means Ctrl+C: the caller stops the whole run. */
|
|
38
22
|
function runHoistedCliInstall(verbose) {
|
|
39
23
|
const startedAt = Date.now();
|
|
@@ -90,8 +74,8 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
|
|
|
90
74
|
return settled;
|
|
91
75
|
const isTTY = detectHumanInitiated();
|
|
92
76
|
const apiKey = validatedApiKey(opts.apiKey);
|
|
93
|
-
//
|
|
94
|
-
const keyed = !!apiKey
|
|
77
|
+
// All harnesses carry a key; one that cannot fails harness_too_old.
|
|
78
|
+
const keyed = !!apiKey;
|
|
95
79
|
const fixCommand = `tinyfish connect ${harness}`;
|
|
96
80
|
const gate = await authGate(base, oauthBudget, fixCommand, { prompt, isTTY, keyed });
|
|
97
81
|
if (gate.result)
|
|
@@ -103,7 +87,11 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
|
|
|
103
87
|
let postInstallFailed = false;
|
|
104
88
|
try {
|
|
105
89
|
// A keyed install too old for key auth fails, no browser.
|
|
106
|
-
authMode = await
|
|
90
|
+
authMode = await connectHarness(harness, {
|
|
91
|
+
apiKey: opts.apiKey,
|
|
92
|
+
mcpUrl: opts.mcpUrl,
|
|
93
|
+
launch: false,
|
|
94
|
+
verbose: opts.verbose,
|
|
107
95
|
keyAuthOnly: keyed && !isTTY,
|
|
108
96
|
authTimeoutMs: headlessAuth ? OAUTH_SIGN_IN_TIMEOUT_MS : undefined,
|
|
109
97
|
hoistedCliInstall,
|
|
@@ -139,13 +127,19 @@ function noWorkOutcome(d, uninstall) {
|
|
|
139
127
|
return 'not_detected';
|
|
140
128
|
if (!uninstall && isStaleConfig(d))
|
|
141
129
|
return 'stale_config';
|
|
130
|
+
if (!uninstall && needsFirstRun(d))
|
|
131
|
+
return 'needs_first_run';
|
|
142
132
|
return undefined;
|
|
143
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
|
+
}
|
|
144
138
|
/** A config-writing connect needs no binary; every other harness spawns one. */
|
|
145
139
|
function isStaleConfig(detection) {
|
|
146
140
|
const spec = harnessSpec(detection.harness);
|
|
147
|
-
// Cursor's IDE reads mcp.json binary-less; omp
|
|
148
|
-
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;
|
|
149
143
|
return detection.detected && !detection.binaryOnPath && !binaryless;
|
|
150
144
|
}
|
|
151
145
|
async function resolvePick(detections, opts, prompt) {
|
|
@@ -153,7 +147,7 @@ async function resolvePick(detections, opts, prompt) {
|
|
|
153
147
|
let mode = 'all';
|
|
154
148
|
// Before willAttempt/cliNeeded: a cursor-only pick must not hoist the CLI.
|
|
155
149
|
// A stale config dir has no binary to connect, so never offer it as a choice.
|
|
156
|
-
const offerable = detections.filter((d) => d.detected && !isStaleConfig(d));
|
|
150
|
+
const offerable = detections.filter((d) => d.detected && !isStaleConfig(d) && !needsFirstRun(d));
|
|
157
151
|
if (opts.pick && offerable.length > 0) {
|
|
158
152
|
const connected = new Set(Object.keys(loadConfig().connect ?? {}));
|
|
159
153
|
const pick = await pickHarnesses(offerable, connected, { prompt });
|
|
@@ -248,13 +242,14 @@ async function connectLoop(detections, opts, deselected, io) {
|
|
|
248
242
|
if (results.some((r) => r.harness === detection.harness))
|
|
249
243
|
continue;
|
|
250
244
|
const { harness, detected, configPath } = detection;
|
|
251
|
-
|
|
245
|
+
// One source of truth with `stepped`, or a skipped harness lands on the wrong row.
|
|
246
|
+
const noWork = noWorkOutcome(detection, opts.uninstall);
|
|
252
247
|
results.push({
|
|
253
248
|
harness,
|
|
254
249
|
detected,
|
|
255
250
|
configPath,
|
|
256
251
|
installed: false,
|
|
257
|
-
outcome:
|
|
252
|
+
outcome: noWork ?? 'not_reached',
|
|
258
253
|
fixCommand: `tinyfish connect ${detection.harness}`,
|
|
259
254
|
});
|
|
260
255
|
}
|
|
@@ -337,10 +332,6 @@ export async function runConnectAll(opts) {
|
|
|
337
332
|
const prompt = isTTY ? createStdinPrompt() : undefined;
|
|
338
333
|
const detections = detectInstalledHarnesses();
|
|
339
334
|
const gateKey = validatedApiKey(opts.apiKey);
|
|
340
|
-
// Keyed installs first: working tools land before sign-ins can block.
|
|
341
|
-
if (gateKey) {
|
|
342
|
-
detections.sort((a, b) => Number(KEY_AUTH_CAPABLE.has(b.harness)) - Number(KEY_AUTH_CAPABLE.has(a.harness)));
|
|
343
|
-
}
|
|
344
335
|
const pick = await resolvePick(detections, opts, prompt);
|
|
345
336
|
if (pick.cancelled) {
|
|
346
337
|
// Before summary and telemetry: an explicit nothing is not a setup.
|
|
@@ -349,10 +340,7 @@ export async function runConnectAll(opts) {
|
|
|
349
340
|
}
|
|
350
341
|
const { deselected, mode } = pick;
|
|
351
342
|
// Mirrors processHarness's gate: headless, only keyless CLI-login harnesses skip.
|
|
352
|
-
const willAttempt = (harness) => !deselected.has(harness) &&
|
|
353
|
-
(isTTY ||
|
|
354
|
-
!harnessSpec(harness).signInViaCliLogin ||
|
|
355
|
-
(!!gateKey && KEY_AUTH_CAPABLE.has(harness)));
|
|
343
|
+
const willAttempt = (harness) => !deselected.has(harness) && (isTTY || !harnessSpec(harness).signInViaCliLogin || !!gateKey);
|
|
356
344
|
const hoisted = hoistCliIfNeeded(detections, opts, willAttempt);
|
|
357
345
|
if (hoisted === 'interrupted') {
|
|
358
346
|
errLine('Setup interrupted — run the command again to finish.');
|
|
@@ -408,6 +396,7 @@ const LAUNCHERS = [
|
|
|
408
396
|
{ harness: 'cursor', args: [FIRST_TASK_PROMPT] },
|
|
409
397
|
{ harness: 'claude-code', args: [FIRST_TASK_PROMPT] },
|
|
410
398
|
{ harness: 'omp', args: [FIRST_TASK_PROMPT] },
|
|
399
|
+
{ harness: 'pi', args: [FIRST_TASK_PROMPT] },
|
|
411
400
|
];
|
|
412
401
|
async function launchFirstTask(results) {
|
|
413
402
|
const candidates = LAUNCHERS.filter((l) => results.some((r) => r.harness === l.harness && r.installed) &&
|
|
@@ -18,7 +18,9 @@ export declare const CURSOR_SKILL_TARGET: {
|
|
|
18
18
|
readonly displayName: string;
|
|
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 @@ export 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;
|
|
@@ -45,8 +49,6 @@ interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent'
|
|
|
45
49
|
seedKey?: (apiKey: string) => SeededInstall;
|
|
46
50
|
};
|
|
47
51
|
loginArgs?: string[];
|
|
48
|
-
/** Keyless `mcp add` itself blocks on the browser OAuth. */
|
|
49
|
-
oauthInAdd?: boolean;
|
|
50
52
|
removals: {
|
|
51
53
|
args: string[];
|
|
52
54
|
label: string;
|
|
@@ -59,6 +61,7 @@ interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent'
|
|
|
59
61
|
extraPostInstall?: (options: {
|
|
60
62
|
apiKey?: string;
|
|
61
63
|
verbose: boolean;
|
|
64
|
+
seededHome?: string;
|
|
62
65
|
}) => void;
|
|
63
66
|
/** Pin `extraPostInstall` installs, reported on the plugin_installed checkpoint. */
|
|
64
67
|
pluginVersion?: string;
|
|
@@ -69,17 +72,16 @@ interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent'
|
|
|
69
72
|
};
|
|
70
73
|
}
|
|
71
74
|
/** Keyless installs use this argv; the harness signs itself in. */
|
|
72
|
-
|
|
75
|
+
interface OauthCapableMcpClient extends BaseMcpClient {
|
|
73
76
|
keyRequired?: false;
|
|
74
77
|
addArgs: (mcpUrl: string) => string[];
|
|
75
78
|
}
|
|
76
79
|
/** No sign-in exists here, so connect refuses a keyless install (Hermes). */
|
|
77
|
-
|
|
80
|
+
interface KeyRequiredMcpClient extends BaseMcpClient {
|
|
78
81
|
keyRequired: true;
|
|
79
82
|
keyAuth: NonNullable<BaseMcpClient['keyAuth']>;
|
|
80
83
|
/** Restoring any of these three would restore the OAuth fallback. */
|
|
81
84
|
addArgs?: never;
|
|
82
|
-
oauthInAdd?: never;
|
|
83
85
|
loginArgs?: never;
|
|
84
86
|
}
|
|
85
87
|
export type NativeMcpClient = OauthCapableMcpClient | KeyRequiredMcpClient;
|
|
@@ -89,7 +91,7 @@ export type NativeMcpClient = OauthCapableMcpClient | KeyRequiredMcpClient;
|
|
|
89
91
|
* quotes back out. Untested on Windows.
|
|
90
92
|
*/
|
|
91
93
|
export declare function openExternalUrl(url: string): ReturnType<typeof spawn.sync>;
|
|
92
|
-
/**
|
|
94
|
+
/** Writes exit 0 having saved nothing, a disabled entry, or a keyless one. */
|
|
93
95
|
export declare function hermesRegistrationEnabled(home: string): {
|
|
94
96
|
ok: boolean;
|
|
95
97
|
detail?: string;
|
|
@@ -97,7 +99,7 @@ export declare function hermesRegistrationEnabled(home: string): {
|
|
|
97
99
|
};
|
|
98
100
|
export declare const NATIVE_MCP_CLIENTS: readonly NativeMcpClient[];
|
|
99
101
|
/** Native descriptor by harness id; Cursor and OpenClaw have none. */
|
|
100
|
-
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>;
|
|
101
103
|
export declare const OPENCLAW: SupportedCommand;
|
|
102
104
|
/** "printed" = handed to the user to paste; the walkthrough was never started for them. */
|
|
103
105
|
export type WalkthroughOutcome = 'launched' | 'printed';
|
|
@@ -105,4 +107,5 @@ export type WalkthroughOutcome = 'launched' | 'printed';
|
|
|
105
107
|
type OnWalkthroughDecided = (outcome: WalkthroughOutcome) => void | Promise<void>;
|
|
106
108
|
export declare function launchNativeMcpClient(client: NativeMcpClient, onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
107
109
|
export declare function launchOmpWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
110
|
+
export declare function launchPiWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
108
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
|
}
|
|
@@ -4,7 +4,7 @@ export declare const SEARCH_RETRY = "tinyfish search query --pretty -- \"<your q
|
|
|
4
4
|
export declare const DEFAULT_VERIFICATION_QUERY = "today's top Hacker News story";
|
|
5
5
|
export declare const MALFORMED_ENV_KEY_MESSAGE = "TINYFISH_API_KEY is set but malformed; it will override any stored key. Fix or unset it, then re-run.";
|
|
6
6
|
export declare const MALFORMED_STORED_KEY_MESSAGE = "The stored TinyFish API key is malformed. Run: tinyfish auth login";
|
|
7
|
-
|
|
7
|
+
interface CliFallbackHooks {
|
|
8
8
|
onStepStart?: (stage: ConnectFailureStage) => void;
|
|
9
9
|
onStepDone?: (phase: ConnectCheckpoint) => void;
|
|
10
10
|
}
|
|
@@ -22,3 +22,4 @@ export interface CliFallbackOptions {
|
|
|
22
22
|
export declare function createStdinPrompt(): Prompt;
|
|
23
23
|
/** Steps print retry lines and return outcomes; only interrupts throw. */
|
|
24
24
|
export declare function runCliFallback(opts: CliFallbackOptions): Promise<CliFallbackOutcome>;
|
|
25
|
+
export {};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Readable } from 'node:stream';
|
|
2
2
|
import { type Harness, type HarnessDetection } from './harness-detect.js';
|
|
3
|
-
|
|
3
|
+
type PickSelection = Harness[] | 'cancel';
|
|
4
4
|
export interface PickResult {
|
|
5
5
|
selection: PickSelection;
|
|
6
6
|
mode: 'pick' | 'pick_numeric';
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
type PickerStdin = Readable & {
|
|
9
9
|
setRawMode?: (raw: boolean) => unknown;
|
|
10
10
|
};
|
|
11
11
|
export interface PickerIo {
|
|
@@ -14,3 +14,4 @@ export interface PickerIo {
|
|
|
14
14
|
prompt?: (question: string) => Promise<string>;
|
|
15
15
|
}
|
|
16
16
|
export declare function pickHarnesses(detections: HarnessDetection[], connectedBefore: ReadonlySet<string>, io?: PickerIo): Promise<PickResult>;
|
|
17
|
+
export {};
|
|
@@ -49,8 +49,6 @@ export function stageDurationOf(error) {
|
|
|
49
49
|
const value = error?.stageDurationMs;
|
|
50
50
|
return typeof value === 'number' ? value : undefined;
|
|
51
51
|
}
|
|
52
|
-
class PrerequisiteError extends ConnectStepError {
|
|
53
|
-
}
|
|
54
52
|
/** A bounded sign-in wait elapsed — unfinished, not broken. */
|
|
55
53
|
export class SignInTimeoutError extends ConnectStepError {
|
|
56
54
|
constructor(message, cause) {
|
|
@@ -190,7 +188,7 @@ function throwProbeFailure(client, result) {
|
|
|
190
188
|
`${probeTimeoutMs(client) / 1000}s. Retry.`, 'timeout', { cause: result.error });
|
|
191
189
|
}
|
|
192
190
|
// It ran and exited non-zero, so the version is still answerable.
|
|
193
|
-
throw new
|
|
191
|
+
throw new ConnectStepError(client.supportCheck.unavailableMessage, 'harness_command_unsupported', { cause: result.error, harnessVersion: probeHarnessVersion(client) });
|
|
194
192
|
}
|
|
195
193
|
function supportProbeOutput(client) {
|
|
196
194
|
const result = spawn.sync(client.command, client.supportCheck.args, {
|
|
@@ -201,7 +199,7 @@ function supportProbeOutput(client) {
|
|
|
201
199
|
});
|
|
202
200
|
// 127 is the shell's not-found code; a wrapper shim with no real binary behind it exits that.
|
|
203
201
|
if (commandNotFound(result.error) || result.status === 127) {
|
|
204
|
-
throw new
|
|
202
|
+
throw new ConnectStepError(`${client.displayName} is not installed or not available on PATH.`, 'harness_not_installed', { cause: result.error });
|
|
205
203
|
}
|
|
206
204
|
if (result.error || result.status !== 0) {
|
|
207
205
|
throwProbeFailure(client, result);
|
|
@@ -218,7 +216,7 @@ function requireEssential(client, output) {
|
|
|
218
216
|
if (client.supportCheck.patterns.every((pattern) => pattern.test(output)))
|
|
219
217
|
return;
|
|
220
218
|
debugProbeOutput(client, output);
|
|
221
|
-
throw new
|
|
219
|
+
throw new ConnectStepError(client.supportCheck.unavailableMessage, 'harness_too_old', {
|
|
222
220
|
harnessVersion: probeHarnessVersion(client),
|
|
223
221
|
});
|
|
224
222
|
}
|
|
@@ -235,7 +233,7 @@ function resolveVariant(client, output) {
|
|
|
235
233
|
return undefined;
|
|
236
234
|
// Unacknowledged policy warnings can block the install; fail before it does.
|
|
237
235
|
debugProbeOutput(client, output);
|
|
238
|
-
throw new
|
|
236
|
+
throw new ConnectStepError(`${client.displayName} lists \`${unknown}\`, which this TinyFish CLI does not recognise. ` +
|
|
239
237
|
'Upgrade the TinyFish CLI with `tinyfish upgrade` and retry.', 'harness_too_old', { harnessVersion: probeHarnessVersion(client) });
|
|
240
238
|
}
|
|
241
239
|
/** Refresh paths need the advertised alternative without connect's version probe. */
|
|
@@ -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
|
}
|