@tiny-fish/cli 0.39.1-next.311 → 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.js +86 -19
- package/dist/commands/doctor.js +3 -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 +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/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import spawn from 'cross-spawn';
|
|
2
2
|
import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from '../lib/auth.js';
|
|
3
|
-
import { CURSOR_SKILL_TARGET, OPENCLAW, openclawSkillInstallArgs, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
|
|
3
|
+
import { CURSOR_SKILL_TARGET, OPENCLAW, openclawSkillInstallArgs, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchPiWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
|
|
4
4
|
import { captureStdio, capturedOutput, installTinyFishCli, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from '../lib/cli-install.js';
|
|
5
5
|
import { ensureCliAuthenticated } from '../lib/connect-auth.js';
|
|
6
6
|
import { createStdinPrompt, runCliFallback } from '../lib/connect-fallback.js';
|
|
@@ -9,6 +9,7 @@ import { ConnectInterruptedError, ConnectStepError, HoistedFailureReportedError,
|
|
|
9
9
|
import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from '../lib/cursor-config.js';
|
|
10
10
|
import { runConnectAll } from '../lib/connect-all.js';
|
|
11
11
|
import { ompMcpPath, readOmpTinyfishEntry, writeOmpMcpConfig } from '../lib/omp-config.js';
|
|
12
|
+
import { PI_ADAPTER_INSTALL_COMMAND, piMcpAdapterState, piSkillDirMismatch, piMcpPath, readPiTinyfishEntry, writePiMcpConfig, } from '../lib/pi-config.js';
|
|
12
13
|
import { ALL_HARNESSES, AuthMode, HARNESS_DISPLAY_NAMES, RELOAD_ACTION, } from '../lib/harness-detect.js';
|
|
13
14
|
import { detectHumanInitiated } from '../lib/harness.js';
|
|
14
15
|
import { TINYFISH_API_KEY_VAR } from '../lib/constants.js';
|
|
@@ -169,6 +170,22 @@ async function prepareInstallAuth(client, options, keyAuthSupported) {
|
|
|
169
170
|
}
|
|
170
171
|
return { keyedAdd, buildAddArgs };
|
|
171
172
|
}
|
|
173
|
+
/** Inherit for an inline-OAuth add; capture otherwise, piping prompts when seeded. */
|
|
174
|
+
function addSpawnOptions(options, interactiveAdd, seeded) {
|
|
175
|
+
if (interactiveAdd)
|
|
176
|
+
return { stdio: 'inherit', timeout: options.authTimeoutMs };
|
|
177
|
+
if (!seeded)
|
|
178
|
+
return captureStdio(options.verbose ?? false);
|
|
179
|
+
return {
|
|
180
|
+
encoding: 'utf8',
|
|
181
|
+
// Without it, the 1 MiB default overflows into a misread Ctrl+C.
|
|
182
|
+
maxBuffer: STEP_MAX_BUFFER,
|
|
183
|
+
// The unattended path had no bound at all before; captureStdio's is the floor.
|
|
184
|
+
timeout: options.authTimeoutMs ?? SKILL_INSTALL_TIMEOUT_MS,
|
|
185
|
+
input: seeded.stdinInput,
|
|
186
|
+
env: seeded.env,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
172
189
|
/** Returns the seeded-credential note when a key was written into a user-owned file. */
|
|
173
190
|
function performMcpAdd(client, options, plan, attemptId) {
|
|
174
191
|
const { keyedAdd, buildAddArgs } = plan;
|
|
@@ -183,20 +200,11 @@ function performMcpAdd(client, options, plan, attemptId) {
|
|
|
183
200
|
const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
|
|
184
201
|
// After the removals: an interrupt there leaves no key behind.
|
|
185
202
|
const seeded = keyedAdd?.spec.seedKey?.(keyedAdd.key);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
: seeded
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Without it, the 1 MiB default overflows into a misread Ctrl+C.
|
|
192
|
-
maxBuffer: STEP_MAX_BUFFER,
|
|
193
|
-
// The unattended path had no bound at all before; captureStdio's is the floor.
|
|
194
|
-
timeout: options.authTimeoutMs ?? SKILL_INSTALL_TIMEOUT_MS,
|
|
195
|
-
input: seeded.stdinInput,
|
|
196
|
-
env: seeded.env,
|
|
197
|
-
}
|
|
198
|
-
: captureStdio(options.verbose ?? false);
|
|
199
|
-
const addResult = spawn.sync(client.command, addArgs, addOptions);
|
|
203
|
+
// Writing the entry ourselves skips the harness probe that saves it disabled.
|
|
204
|
+
if (seeded?.register(mcpUrl.toString())) {
|
|
205
|
+
return { note: seeded.note, seededHome: seeded.home };
|
|
206
|
+
}
|
|
207
|
+
const addResult = spawn.sync(client.command, addArgs, addSpawnOptions(options, interactiveAdd, seeded));
|
|
200
208
|
// Rolls back first: signInStepError throws on a Ctrl+C.
|
|
201
209
|
const failAdd = (build) => {
|
|
202
210
|
seeded?.rollback();
|
|
@@ -214,7 +222,7 @@ function performMcpAdd(client, options, plan, attemptId) {
|
|
|
214
222
|
// The tag keeps the path-bearing prose out of telemetry.
|
|
215
223
|
{ failureDetail: registration.tag }));
|
|
216
224
|
}
|
|
217
|
-
return seeded?.note;
|
|
225
|
+
return { note: seeded?.note, seededHome: seeded?.home };
|
|
218
226
|
}
|
|
219
227
|
function performLoginStep(client, pendingLogin, options) {
|
|
220
228
|
errLine('Signing in to TinyFish...');
|
|
@@ -289,7 +297,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
289
297
|
// A probed client authenticates inside `mcp add` too, so a failure there is either.
|
|
290
298
|
state.stage =
|
|
291
299
|
loginArgs && !probeAuthenticated ? 'registration' : 'registration_or_authentication';
|
|
292
|
-
const seededNote = performMcpAdd(client, options, plan, telemetry.attemptId);
|
|
300
|
+
const { note: seededNote, seededHome } = performMcpAdd(client, options, plan, telemetry.attemptId);
|
|
293
301
|
telemetry.track('checkpoint', { phase: 'registered' });
|
|
294
302
|
const { authMode, signInDeferred } = completeAuthFlow(client, options, state, telemetry, {
|
|
295
303
|
useKeyAuth,
|
|
@@ -301,7 +309,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
301
309
|
state.authMode = authMode;
|
|
302
310
|
// Registration/OAuth make MCP work; later steps are cosmetic and must not fail the attempt.
|
|
303
311
|
settle(state, telemetry, 'completed', { authMode });
|
|
304
|
-
await runPostInstallSteps(client, options, state, telemetry, signInDeferred);
|
|
312
|
+
await runPostInstallSteps(client, options, state, telemetry, signInDeferred, seededHome);
|
|
305
313
|
});
|
|
306
314
|
return state.authMode;
|
|
307
315
|
}
|
|
@@ -317,7 +325,7 @@ function connectedLine(client, signInDeferred) {
|
|
|
317
325
|
`${RELOAD_ACTION[client.connectClient]} to pick up the tools.`);
|
|
318
326
|
}
|
|
319
327
|
// Runs after `completed` settled: steps fail alone, the rest still run.
|
|
320
|
-
async function runPostInstallSteps(client, options, state, telemetry, signInDeferred) {
|
|
328
|
+
async function runPostInstallSteps(client, options, state, telemetry, signInDeferred, seededHome) {
|
|
321
329
|
let failed = false;
|
|
322
330
|
// Modelled on upgrade's attempt(); interrupts rethrow and abandon remaining steps.
|
|
323
331
|
const attempt = async (stage, run) => {
|
|
@@ -348,6 +356,7 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
348
356
|
extraPostInstall({
|
|
349
357
|
apiKey: validatedApiKey(options.apiKey),
|
|
350
358
|
verbose: options.verbose ?? false,
|
|
359
|
+
seededHome,
|
|
351
360
|
});
|
|
352
361
|
telemetry.track('checkpoint', {
|
|
353
362
|
phase: 'plugin_installed',
|
|
@@ -501,6 +510,31 @@ export async function connectOpenClaw(options) {
|
|
|
501
510
|
}
|
|
502
511
|
});
|
|
503
512
|
}
|
|
513
|
+
const PI_RELOAD_COPY = 'TinyFish is configured in pi. Restart pi, then run `/mcp-auth tinyfish` there if it asks you ' +
|
|
514
|
+
'to sign in.';
|
|
515
|
+
// The skill is the working path either way, so the note must not read as a failure.
|
|
516
|
+
const PI_ADAPTER_DORMANT_NOTE = 'Note: pi has no built-in MCP support, so the entry in its mcp.json stays dormant until you ' +
|
|
517
|
+
`run \`${PI_ADAPTER_INSTALL_COMMAND}\`. The TinyFish skill works without it.`;
|
|
518
|
+
// Only the dormant case was spoken for, leaving a working MCP install silent.
|
|
519
|
+
const PI_ADAPTER_LIVE_NOTE = 'pi-mcp-adapter is installed, so pi loads the TinyFish MCP tools as well as the skill once ' +
|
|
520
|
+
'you restart it.';
|
|
521
|
+
function piAdapterNote() {
|
|
522
|
+
const state = piMcpAdapterState();
|
|
523
|
+
if (state === 'installed')
|
|
524
|
+
return PI_ADAPTER_LIVE_NOTE;
|
|
525
|
+
return state === 'absent' ? PI_ADAPTER_DORMANT_NOTE : undefined;
|
|
526
|
+
}
|
|
527
|
+
// The `skills` CLI writes to the default dir even when pi reads another one.
|
|
528
|
+
function piPostConnectNotes() {
|
|
529
|
+
const notes = [piAdapterNote()];
|
|
530
|
+
const skills = piSkillDirMismatch();
|
|
531
|
+
if (skills) {
|
|
532
|
+
notes.push(`Note: PI_CODING_AGENT_DIR points pi at ${skills.readFrom}, but the TinyFish skill ` +
|
|
533
|
+
`installed to ${skills.installedTo}. Copy or symlink it across for pi to load it.`);
|
|
534
|
+
}
|
|
535
|
+
const printable = notes.filter(Boolean);
|
|
536
|
+
return printable.length > 0 ? printable.join('\n') : undefined;
|
|
537
|
+
}
|
|
504
538
|
// Only interactive omp wires the OAuth handler; headless cannot sign in.
|
|
505
539
|
const OMP_KEYLESS_COPY = 'TinyFish is configured in omp. Open omp and sign in to TinyFish when prompted, or run ' +
|
|
506
540
|
'`/mcp reauth tinyfish` there.';
|
|
@@ -535,6 +569,22 @@ const CONFIG_FILE_HARNESSES = {
|
|
|
535
569
|
reload: OMP_KEYLESS_COPY,
|
|
536
570
|
},
|
|
537
571
|
},
|
|
572
|
+
pi: {
|
|
573
|
+
skillTarget: { skillAgent: 'pi', displayName: HARNESS_DISPLAY_NAMES.pi },
|
|
574
|
+
registeredUrl: () => readPiTinyfishEntry().url,
|
|
575
|
+
write: writePiMcpConfig,
|
|
576
|
+
configPath: piMcpPath,
|
|
577
|
+
// No install deeplink exists; the copy carries the restart instead.
|
|
578
|
+
tryLaunch: () => false,
|
|
579
|
+
postConnectNote: piPostConnectNotes,
|
|
580
|
+
copy: {
|
|
581
|
+
verified: "TinyFish is connected and verified (health+auth). Restart pi and it's live.",
|
|
582
|
+
authFailed: (reason) => `TinyFish is configured, but the authenticated check failed (${reason}). ` +
|
|
583
|
+
'Fix: rotate/re-enter your key with `tinyfish auth login`, then re-run: tinyfish connect pi',
|
|
584
|
+
launched: PI_RELOAD_COPY,
|
|
585
|
+
reload: PI_RELOAD_COPY,
|
|
586
|
+
},
|
|
587
|
+
},
|
|
538
588
|
};
|
|
539
589
|
/** Writes the harness's MCP config file itself; no `mcp add` exists. */
|
|
540
590
|
async function connectConfigFileHarness(harness, options) {
|
|
@@ -628,6 +678,9 @@ async function connectConfigFileHarness(harness, options) {
|
|
|
628
678
|
else {
|
|
629
679
|
errLine(spec.copy.reload);
|
|
630
680
|
}
|
|
681
|
+
const note = spec.postConnectNote?.();
|
|
682
|
+
if (note)
|
|
683
|
+
errLine(note);
|
|
631
684
|
settle(state, telemetry, 'completed', { authMode: resolvedKey ? AuthMode.ApiKey : 'deferred' });
|
|
632
685
|
});
|
|
633
686
|
}
|
|
@@ -643,6 +696,7 @@ function launchCursorDeeplink(mcpUrl) {
|
|
|
643
696
|
/** Total over the non-native harnesses: a new one fails to compile until wired here. */
|
|
644
697
|
const LAUNCH_OVERRIDES = {
|
|
645
698
|
omp: launchOmpWalkthrough,
|
|
699
|
+
pi: launchPiWalkthrough,
|
|
646
700
|
openclaw: launchOpenClawWalkthrough,
|
|
647
701
|
// Cursor has no chat CLI to launch.
|
|
648
702
|
cursor: () => {
|
|
@@ -683,6 +737,19 @@ const CONNECTOR_OVERRIDES = {
|
|
|
683
737
|
}
|
|
684
738
|
return undefined;
|
|
685
739
|
},
|
|
740
|
+
pi: async (options) => {
|
|
741
|
+
await connectConfigFileHarness('pi', options);
|
|
742
|
+
if (options.launch) {
|
|
743
|
+
try {
|
|
744
|
+
await launchPiWalkthrough();
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
errLine(error instanceof Error ? error.message : String(error));
|
|
748
|
+
errLine(finishSetupHint('pi'));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return undefined;
|
|
752
|
+
},
|
|
686
753
|
};
|
|
687
754
|
function requireNative(harness) {
|
|
688
755
|
const client = NATIVE_BY_HARNESS.get(harness);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CLI_VERSION } from '../lib/constants.js';
|
|
2
|
-
import { checkAuthCall, checkCliVersion, checkConnectivity, checkCredential, checkHermesPlugin, checkRegistration, provesHarnessReach, verifyHarnessKey, } from '../lib/doctor-checks.js';
|
|
2
|
+
import { checkAuthCall, checkCliVersion, checkConnectivity, checkCredential, checkHermesPlugin, checkPiAdapter, checkRegistration, provesHarnessReach, verifyHarnessKey, } from '../lib/doctor-checks.js';
|
|
3
3
|
import { DOCTOR_COULD_NOT_RUN, DOCTOR_INVALID_INPUT, DOCTOR_SCHEMA_VERSION, doctorReportSchema, exitCodeFor, renderPretty, verdictFor, } from '../lib/doctor-report.js';
|
|
4
4
|
import { applyRepairs, repairsFor } from '../lib/doctor-repairs.js';
|
|
5
5
|
import { couldNotRunPayload, telemetryKey, telemetryPayload } from '../lib/doctor-telemetry.js';
|
|
@@ -30,11 +30,13 @@ export async function runDoctor(options) {
|
|
|
30
30
|
]);
|
|
31
31
|
const hermesStatus = statuses.find((status) => status.harness === 'hermes');
|
|
32
32
|
const hermesPlugin = hermesStatus ? checkHermesPlugin(hermesStatus) : undefined;
|
|
33
|
+
const piStatus = statuses.find((status) => status.harness === 'pi');
|
|
33
34
|
const checks = [
|
|
34
35
|
checkCliVersion(),
|
|
35
36
|
connectivity,
|
|
36
37
|
...statuses.map((status, i) => checkRegistration(status, options.mcpUrl, keyAuths[i], !credential.key)),
|
|
37
38
|
...(hermesPlugin ? [hermesPlugin.check] : []),
|
|
39
|
+
...(piStatus ? [checkPiAdapter(piStatus)] : []),
|
|
38
40
|
credential.check,
|
|
39
41
|
checkAuthCall(cliAuthResult),
|
|
40
42
|
];
|
|
@@ -9,7 +9,7 @@ const HIDDEN_ONCE_SOMETHING_WORKED = new Set([
|
|
|
9
9
|
]);
|
|
10
10
|
/** A stale config dir is a detection signal with nothing behind it. */
|
|
11
11
|
export function actionable(result) {
|
|
12
|
-
return result.detected && result.outcome !== 'stale_config';
|
|
12
|
+
return (result.detected && result.outcome !== 'stale_config' && result.outcome !== 'needs_first_run');
|
|
13
13
|
}
|
|
14
14
|
function absentSummaryLine(result, uninstall) {
|
|
15
15
|
const name = DISPLAY_NAMES[result.harness];
|
|
@@ -29,6 +29,9 @@ function skippedSummaryLine(result) {
|
|
|
29
29
|
if (result.outcome === 'stale_config') {
|
|
30
30
|
return `… ${name} — config at ${result.configPath} but no \`${HARNESS_COMMANDS[result.harness]}\` on PATH, skipped. Install ${name}, then run: ${result.fixCommand}`;
|
|
31
31
|
}
|
|
32
|
+
if (result.outcome === 'needs_first_run') {
|
|
33
|
+
return `… ${name} — installed but never run, so ${result.configPath} does not exist yet, skipped. Run \`${HARNESS_COMMANDS[result.harness]}\` once, then: ${result.fixCommand}`;
|
|
34
|
+
}
|
|
32
35
|
if (result.outcome === 'not_reached') {
|
|
33
36
|
return `… ${name} — not reached, the run stopped early. Fix: ${result.fixCommand}`;
|
|
34
37
|
}
|
|
@@ -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 { planOmpWrite, removeOmpMcpServer } from './omp-config.js';
|
|
6
|
+
import { piMcpPath, planPiWrite, removePiMcpServer } from './pi-config.js';
|
|
6
7
|
import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
|
|
7
8
|
import { clearHermesWebBackends, HERMES_PLUGIN_SHA } from './hermes-plugin.js';
|
|
8
9
|
import { HARNESS_DISPLAY_NAMES as DISPLAY_NAMES } from './harness-detect.js';
|
|
@@ -14,6 +15,8 @@ export function planText(harness, mcpUrl, apiKey) {
|
|
|
14
15
|
return planCursorWrite(mcpUrl, apiKey);
|
|
15
16
|
if (harness === 'omp')
|
|
16
17
|
return planOmpWrite(mcpUrl, apiKey);
|
|
18
|
+
if (harness === 'pi')
|
|
19
|
+
return planPiWrite(mcpUrl, apiKey);
|
|
17
20
|
// Only the keyed path seeds the .env; a keyless Hermes install writes nothing.
|
|
18
21
|
if (harness === 'hermes' && apiKey) {
|
|
19
22
|
return (`would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ` +
|
|
@@ -29,6 +32,9 @@ export function uninstallPlanText(harness) {
|
|
|
29
32
|
if (harness === 'omp') {
|
|
30
33
|
return "would remove the tinyfish entry from omp's mcp.json (located via `omp config path`)";
|
|
31
34
|
}
|
|
35
|
+
if (harness === 'pi') {
|
|
36
|
+
return `would remove the tinyfish entry from ${piMcpPath()}`;
|
|
37
|
+
}
|
|
32
38
|
if (!uninstallRuns(harness)) {
|
|
33
39
|
return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
|
|
34
40
|
}
|
|
@@ -62,7 +68,8 @@ function uninstallPointer(harness) {
|
|
|
62
68
|
}
|
|
63
69
|
/** `mcp remove` clears neither the key nor the backends we wrote. */
|
|
64
70
|
function clearHermesLocalState() {
|
|
65
|
-
const
|
|
71
|
+
const resolved = resolveHermesHome();
|
|
72
|
+
const home = typeof resolved === 'string' ? resolved : undefined;
|
|
66
73
|
const advisories = [clearHermesEnvKey(home), clearHermesBackends(home)].filter((advisory) => advisory !== undefined);
|
|
67
74
|
return advisories.length > 0 ? advisories.join(' ') : undefined;
|
|
68
75
|
}
|
|
@@ -186,6 +193,10 @@ const MCP_JSON_UNINSTALLS = {
|
|
|
186
193
|
remove: removeOmpMcpServer,
|
|
187
194
|
fix: "Fix omp's mcp.json (at `omp config path`) by hand, then re-run: tinyfish connect --all --uninstall",
|
|
188
195
|
},
|
|
196
|
+
pi: {
|
|
197
|
+
remove: removePiMcpServer,
|
|
198
|
+
fix: `Fix ${piMcpPath()} by hand, then re-run: tinyfish connect --all --uninstall`,
|
|
199
|
+
},
|
|
189
200
|
};
|
|
190
201
|
function uninstallMcpJson(detection, spec) {
|
|
191
202
|
let result;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type Harness } from './harness-detect.js';
|
|
1
|
+
import { type Harness, type HarnessDetection } from './harness-detect.js';
|
|
2
2
|
import { type VerifyDepth } from './verify.js';
|
|
3
|
-
export type Outcome = 'not_detected' | 'not_installed' | 'stale_config' | 'installed' | 'failed' | 'harness_too_old' | 'interrupted' | 'auth_pending' | 'no_tty_auth_skip' | 'uninstalled' | 'uninstall_noop' | 'uninstall_pointer' | 'dry_run' | 'not_reached' | 'deselected';
|
|
3
|
+
export type Outcome = 'not_detected' | 'not_installed' | 'stale_config' | 'needs_first_run' | 'installed' | 'failed' | 'harness_too_old' | 'interrupted' | 'auth_pending' | 'no_tty_auth_skip' | 'uninstalled' | 'uninstall_noop' | 'uninstall_pointer' | 'dry_run' | 'not_reached' | 'deselected';
|
|
4
4
|
export type HarnessResultBase = {
|
|
5
5
|
harness: Harness;
|
|
6
6
|
detected: boolean;
|
|
@@ -34,5 +34,7 @@ export interface ConnectAllOptions {
|
|
|
34
34
|
/** Setup-page id riding in on --url; joins "copied the command" to this run. */
|
|
35
35
|
attemptId?: string;
|
|
36
36
|
}
|
|
37
|
+
/** The harness has never created its dir, so connecting would build one it does not own. */
|
|
38
|
+
export declare function needsFirstRun(detection: HarnessDetection): boolean;
|
|
37
39
|
export declare function runConnectAll(opts: ConnectAllOptions): Promise<number>;
|
|
38
40
|
export declare const FIRST_TASK_PROMPT: string;
|
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
|
}
|