@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
|
@@ -23,20 +23,8 @@ export interface NativeConnectOptions {
|
|
|
23
23
|
/** Targeted connects only: --all keeps its per-harness failed row. */
|
|
24
24
|
fallbackWhenMissing?: boolean;
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
mcpUrl: string;
|
|
29
|
-
launch: boolean;
|
|
30
|
-
cliInstall?: boolean;
|
|
31
|
-
attemptId?: string;
|
|
32
|
-
/** Set by `--all`: the CLI install already ran once, upfront. */
|
|
33
|
-
hoistedCliInstall?: TakeHoistedCliInstall;
|
|
34
|
-
verbose?: boolean;
|
|
35
|
-
/** Set by `--all`: repeated outro lines print once, after the summary. */
|
|
36
|
-
deferOutro?: (line: string) => void;
|
|
37
|
-
/** Targeted connects only: --all keeps its per-harness failed row. */
|
|
38
|
-
fallbackWhenMissing?: boolean;
|
|
39
|
-
}): Promise<void>;
|
|
26
|
+
/** Skill install only: no key handoff, sign-in wait, or post-install callback to honour. */
|
|
27
|
+
export declare function connectOpenClaw(options: Omit<NativeConnectOptions, 'keyAuthOnly' | 'authTimeoutMs' | 'onPostInstallFailed'>): Promise<void>;
|
|
40
28
|
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
41
29
|
export declare function connectCursor(options: {
|
|
42
30
|
apiKey?: string;
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
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';
|
|
4
|
-
import { captureStdio, capturedOutput,
|
|
3
|
+
import { CURSOR_SKILL_TARGET, OPENCLAW, openclawSkillInstallArgs, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchPiWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
|
|
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';
|
|
7
7
|
import { installWebSkill } from '../lib/skill-install.js';
|
|
8
|
-
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, stageDurationOf, withStageDuration, spawnRemoval, } from '../lib/connect-runtime.js';
|
|
8
|
+
import { ConnectInterruptedError, ConnectStepError, HoistedFailureReportedError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, stageDurationOf, withStageDuration, spawnRemoval, } from '../lib/connect-runtime.js';
|
|
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';
|
|
@@ -62,14 +63,16 @@ function resolveKeyedAdd(client, storedKey, keyAuthSupported, deferOutro) {
|
|
|
62
63
|
return keyed;
|
|
63
64
|
}
|
|
64
65
|
/** No sign-in to degrade to, so the install stops. */
|
|
65
|
-
function refuseKeylessInstall(client, keyAuthSupported) {
|
|
66
|
-
const noSignIn =
|
|
66
|
+
function refuseKeylessInstall(client, keyAuthSupported, headless = false) {
|
|
67
|
+
const noSignIn = headless
|
|
68
|
+
? 'signing in needs a terminal'
|
|
69
|
+
: `TinyFish does not sign in to ${client.displayName} any other way`;
|
|
67
70
|
const rerun = `re-run: tinyfish connect ${client.connectClient}`;
|
|
68
71
|
if (!client.keyAuth) {
|
|
69
72
|
throw new ConnectStepError(`${client.displayName} cannot take a TinyFish API key, and ${noSignIn}. ` +
|
|
70
73
|
`Run: tinyfish connect ${client.connectClient}`, 'key_auth_unsupported');
|
|
71
74
|
}
|
|
72
|
-
if (!keyAuthSupported) {
|
|
75
|
+
if (!keyAuthSupported || headless) {
|
|
73
76
|
throw new ConnectStepError(`This ${client.displayName} install cannot take a TinyFish API key, and ${noSignIn}. ` +
|
|
74
77
|
`Update it, then ${rerun}`, 'harness_too_old');
|
|
75
78
|
}
|
|
@@ -159,21 +162,30 @@ async function prepareInstallAuth(client, options, keyAuthSupported) {
|
|
|
159
162
|
const buildAddArgs = keyedAdd
|
|
160
163
|
? (mcpUrl) => keyedAdd.spec.addArgs(mcpUrl, keyedAdd.key)
|
|
161
164
|
: keylessAddArgs(client, keyAuthSupported);
|
|
162
|
-
if (options.keyAuthOnly && !keyedAdd)
|
|
163
|
-
|
|
164
|
-
if (client.keyAuth) {
|
|
165
|
-
throw new ConnectStepError(`This ${client.displayName} install cannot take a TinyFish API key, and signing in ` +
|
|
166
|
-
`needs a terminal. Update it, then re-run: tinyfish connect ${client.connectClient}`, 'harness_too_old');
|
|
167
|
-
}
|
|
168
|
-
throw new ConnectStepError(`${client.displayName} cannot take a TinyFish API key, and signing in needs a ` +
|
|
169
|
-
`terminal. Run: tinyfish connect ${client.connectClient}`, 'key_auth_unsupported');
|
|
170
|
-
}
|
|
165
|
+
if (options.keyAuthOnly && !keyedAdd)
|
|
166
|
+
refuseKeylessInstall(client, keyAuthSupported, true);
|
|
171
167
|
// Verify first: a revoked key in a foreign .env is unrecoverable.
|
|
172
168
|
if (keyedAdd?.spec.seedKey) {
|
|
173
169
|
await verifyKeyBeforeSeeding(keyedAdd.key, client.displayName, options.mcpUrl);
|
|
174
170
|
}
|
|
175
171
|
return { keyedAdd, buildAddArgs };
|
|
176
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
|
+
}
|
|
177
189
|
/** Returns the seeded-credential note when a key was written into a user-owned file. */
|
|
178
190
|
function performMcpAdd(client, options, plan, attemptId) {
|
|
179
191
|
const { keyedAdd, buildAddArgs } = plan;
|
|
@@ -188,20 +200,11 @@ function performMcpAdd(client, options, plan, attemptId) {
|
|
|
188
200
|
const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
|
|
189
201
|
// After the removals: an interrupt there leaves no key behind.
|
|
190
202
|
const seeded = keyedAdd?.spec.seedKey?.(keyedAdd.key);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
: seeded
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// Without it, the 1 MiB default overflows into a misread Ctrl+C.
|
|
197
|
-
maxBuffer: STEP_MAX_BUFFER,
|
|
198
|
-
// The unattended path had no bound at all before; captureStdio's is the floor.
|
|
199
|
-
timeout: options.authTimeoutMs ?? SKILL_INSTALL_TIMEOUT_MS,
|
|
200
|
-
input: seeded.stdinInput,
|
|
201
|
-
env: seeded.env,
|
|
202
|
-
}
|
|
203
|
-
: captureStdio(options.verbose ?? false);
|
|
204
|
-
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));
|
|
205
208
|
// Rolls back first: signInStepError throws on a Ctrl+C.
|
|
206
209
|
const failAdd = (build) => {
|
|
207
210
|
seeded?.rollback();
|
|
@@ -219,7 +222,7 @@ function performMcpAdd(client, options, plan, attemptId) {
|
|
|
219
222
|
// The tag keeps the path-bearing prose out of telemetry.
|
|
220
223
|
{ failureDetail: registration.tag }));
|
|
221
224
|
}
|
|
222
|
-
return seeded?.note;
|
|
225
|
+
return { note: seeded?.note, seededHome: seeded?.home };
|
|
223
226
|
}
|
|
224
227
|
function performLoginStep(client, pendingLogin, options) {
|
|
225
228
|
errLine('Signing in to TinyFish...');
|
|
@@ -294,7 +297,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
294
297
|
// A probed client authenticates inside `mcp add` too, so a failure there is either.
|
|
295
298
|
state.stage =
|
|
296
299
|
loginArgs && !probeAuthenticated ? 'registration' : 'registration_or_authentication';
|
|
297
|
-
const seededNote = performMcpAdd(client, options, plan, telemetry.attemptId);
|
|
300
|
+
const { note: seededNote, seededHome } = performMcpAdd(client, options, plan, telemetry.attemptId);
|
|
298
301
|
telemetry.track('checkpoint', { phase: 'registered' });
|
|
299
302
|
const { authMode, signInDeferred } = completeAuthFlow(client, options, state, telemetry, {
|
|
300
303
|
useKeyAuth,
|
|
@@ -306,7 +309,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
306
309
|
state.authMode = authMode;
|
|
307
310
|
// Registration/OAuth make MCP work; later steps are cosmetic and must not fail the attempt.
|
|
308
311
|
settle(state, telemetry, 'completed', { authMode });
|
|
309
|
-
await runPostInstallSteps(client, options, state, telemetry, signInDeferred);
|
|
312
|
+
await runPostInstallSteps(client, options, state, telemetry, signInDeferred, seededHome);
|
|
310
313
|
});
|
|
311
314
|
return state.authMode;
|
|
312
315
|
}
|
|
@@ -322,7 +325,7 @@ function connectedLine(client, signInDeferred) {
|
|
|
322
325
|
`${RELOAD_ACTION[client.connectClient]} to pick up the tools.`);
|
|
323
326
|
}
|
|
324
327
|
// Runs after `completed` settled: steps fail alone, the rest still run.
|
|
325
|
-
async function runPostInstallSteps(client, options, state, telemetry, signInDeferred) {
|
|
328
|
+
async function runPostInstallSteps(client, options, state, telemetry, signInDeferred, seededHome) {
|
|
326
329
|
let failed = false;
|
|
327
330
|
// Modelled on upgrade's attempt(); interrupts rethrow and abandon remaining steps.
|
|
328
331
|
const attempt = async (stage, run) => {
|
|
@@ -353,6 +356,7 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
353
356
|
extraPostInstall({
|
|
354
357
|
apiKey: validatedApiKey(options.apiKey),
|
|
355
358
|
verbose: options.verbose ?? false,
|
|
359
|
+
seededHome,
|
|
356
360
|
});
|
|
357
361
|
telemetry.track('checkpoint', {
|
|
358
362
|
phase: 'plugin_installed',
|
|
@@ -447,6 +451,7 @@ function trackPostInstallFailure(displayName, state, telemetry, error) {
|
|
|
447
451
|
errLine(`The ${displayName} MCP connection succeeded, but a finishing step ` +
|
|
448
452
|
`(${state.stage}) failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
449
453
|
}
|
|
454
|
+
/** Skill install only: no key handoff, sign-in wait, or post-install callback to honour. */
|
|
450
455
|
export async function connectOpenClaw(options) {
|
|
451
456
|
const telemetry = createConnectTelemetry(options.mcpUrl, 'openclaw', options);
|
|
452
457
|
// Skill install, no MCP server — never degraded, and false keeps it inside a `= false` filter.
|
|
@@ -462,15 +467,9 @@ export async function connectOpenClaw(options) {
|
|
|
462
467
|
return;
|
|
463
468
|
telemetry.setHarnessVersion(support.harnessVersion);
|
|
464
469
|
telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
|
|
465
|
-
if (options.
|
|
466
|
-
state.stage = 'cli_install';
|
|
467
|
-
// A carried first-take failure fails openclaw like the inline install.
|
|
468
|
-
takeHoistedCliInstall(options.hoistedCliInstall, telemetry);
|
|
469
|
-
}
|
|
470
|
-
else if (options.cliInstall !== false) {
|
|
470
|
+
if (options.cliInstall !== false) {
|
|
471
471
|
state.stage = 'cli_install';
|
|
472
|
-
|
|
473
|
-
telemetry.track('checkpoint', { phase: 'cli_installed' });
|
|
472
|
+
runCliInstallStep(options, telemetry);
|
|
474
473
|
}
|
|
475
474
|
state.stage = 'skill_install';
|
|
476
475
|
errLine('Installing the TinyFish skill in OpenClaw...');
|
|
@@ -511,6 +510,31 @@ export async function connectOpenClaw(options) {
|
|
|
511
510
|
}
|
|
512
511
|
});
|
|
513
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
|
+
}
|
|
514
538
|
// Only interactive omp wires the OAuth handler; headless cannot sign in.
|
|
515
539
|
const OMP_KEYLESS_COPY = 'TinyFish is configured in omp. Open omp and sign in to TinyFish when prompted, or run ' +
|
|
516
540
|
'`/mcp reauth tinyfish` there.';
|
|
@@ -545,6 +569,22 @@ const CONFIG_FILE_HARNESSES = {
|
|
|
545
569
|
reload: OMP_KEYLESS_COPY,
|
|
546
570
|
},
|
|
547
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
|
+
},
|
|
548
588
|
};
|
|
549
589
|
/** Writes the harness's MCP config file itself; no `mcp add` exists. */
|
|
550
590
|
async function connectConfigFileHarness(harness, options) {
|
|
@@ -638,6 +678,9 @@ async function connectConfigFileHarness(harness, options) {
|
|
|
638
678
|
else {
|
|
639
679
|
errLine(spec.copy.reload);
|
|
640
680
|
}
|
|
681
|
+
const note = spec.postConnectNote?.();
|
|
682
|
+
if (note)
|
|
683
|
+
errLine(note);
|
|
641
684
|
settle(state, telemetry, 'completed', { authMode: resolvedKey ? AuthMode.ApiKey : 'deferred' });
|
|
642
685
|
});
|
|
643
686
|
}
|
|
@@ -653,6 +696,7 @@ function launchCursorDeeplink(mcpUrl) {
|
|
|
653
696
|
/** Total over the non-native harnesses: a new one fails to compile until wired here. */
|
|
654
697
|
const LAUNCH_OVERRIDES = {
|
|
655
698
|
omp: launchOmpWalkthrough,
|
|
699
|
+
pi: launchPiWalkthrough,
|
|
656
700
|
openclaw: launchOpenClawWalkthrough,
|
|
657
701
|
// Cursor has no chat CLI to launch.
|
|
658
702
|
cursor: () => {
|
|
@@ -693,6 +737,19 @@ const CONNECTOR_OVERRIDES = {
|
|
|
693
737
|
}
|
|
694
738
|
return undefined;
|
|
695
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
|
+
},
|
|
696
753
|
};
|
|
697
754
|
function requireNative(harness) {
|
|
698
755
|
const client = NATIVE_BY_HARNESS.get(harness);
|
|
@@ -707,17 +764,8 @@ export async function connectHarness(harness, options) {
|
|
|
707
764
|
}
|
|
708
765
|
function parseMcpUrlOption(options) {
|
|
709
766
|
let mcpUrl = options.url ?? DEFAULT_MCP_URL;
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
let parsedUrl;
|
|
714
|
-
try {
|
|
715
|
-
parsedUrl = new URL(mcpUrl);
|
|
716
|
-
}
|
|
717
|
-
catch {
|
|
718
|
-
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
719
|
-
}
|
|
720
|
-
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
767
|
+
const parsedUrl = URL.parse(mcpUrl);
|
|
768
|
+
if (mcpUrl.startsWith('-') || !parsedUrl || !['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
721
769
|
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
722
770
|
}
|
|
723
771
|
// Page-minted attempt id rides in on --url. Strip before the URL becomes the MCP
|
|
@@ -732,10 +780,16 @@ function parseMcpUrlOption(options) {
|
|
|
732
780
|
}
|
|
733
781
|
return { mcpUrl, attemptId };
|
|
734
782
|
}
|
|
735
|
-
|
|
783
|
+
/** Bare `connect` offers the picker; `--all` takes the dry-run/uninstall/skip-launch flags. */
|
|
784
|
+
async function connectManyPath(options, mcpUrl, attemptId) {
|
|
736
785
|
if (options.launch) {
|
|
737
786
|
throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
|
|
738
787
|
}
|
|
788
|
+
const pick = !options.all;
|
|
789
|
+
const humanInitiated = pick && detectHumanInitiated();
|
|
790
|
+
if (pick && !humanInitiated) {
|
|
791
|
+
errLine('No terminal detected: connecting every detected harness (same as tinyfish connect --all).');
|
|
792
|
+
}
|
|
739
793
|
const exitCode = await runConnectAll({
|
|
740
794
|
apiKey: options.apiKey,
|
|
741
795
|
mcpUrl,
|
|
@@ -743,26 +797,6 @@ async function connectAllPath(options, mcpUrl, attemptId) {
|
|
|
743
797
|
dryRun: options.dryRun ?? false,
|
|
744
798
|
uninstall: options.uninstall ?? false,
|
|
745
799
|
noLaunch: options.skipLaunch === true,
|
|
746
|
-
verbose: options.verbose ?? false,
|
|
747
|
-
});
|
|
748
|
-
// Preserve the 130 runGuarded set on Ctrl+C.
|
|
749
|
-
if (!process.exitCode)
|
|
750
|
-
process.exitCode = exitCode;
|
|
751
|
-
}
|
|
752
|
-
async function connectPickPath(options, mcpUrl, attemptId) {
|
|
753
|
-
if (options.launch) {
|
|
754
|
-
throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
|
|
755
|
-
}
|
|
756
|
-
const humanInitiated = detectHumanInitiated();
|
|
757
|
-
if (!humanInitiated) {
|
|
758
|
-
errLine('No terminal detected: connecting every detected harness (same as tinyfish connect --all).');
|
|
759
|
-
}
|
|
760
|
-
const exitCode = await runConnectAll({
|
|
761
|
-
apiKey: options.apiKey,
|
|
762
|
-
mcpUrl,
|
|
763
|
-
attemptId,
|
|
764
|
-
dryRun: false,
|
|
765
|
-
uninstall: false,
|
|
766
800
|
pick: humanInitiated,
|
|
767
801
|
verbose: options.verbose ?? false,
|
|
768
802
|
});
|
|
@@ -820,14 +854,14 @@ export function registerConnect(program) {
|
|
|
820
854
|
if (client) {
|
|
821
855
|
throw new Error('Pass either a client or --all, not both.');
|
|
822
856
|
}
|
|
823
|
-
await
|
|
857
|
+
await connectManyPath(options, mcpUrl, attemptId);
|
|
824
858
|
return;
|
|
825
859
|
}
|
|
826
860
|
if (options.dryRun || options.uninstall || options.skipLaunch) {
|
|
827
861
|
throw new Error('--dry-run, --uninstall and --skip-launch are only supported with --all.');
|
|
828
862
|
}
|
|
829
863
|
if (!client) {
|
|
830
|
-
await
|
|
864
|
+
await connectManyPath(options, mcpUrl, attemptId);
|
|
831
865
|
return;
|
|
832
866
|
}
|
|
833
867
|
await connectSingleClient(client, options, mcpUrl, attemptId);
|
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
|
];
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { type DecryptStats } from '../lib/chrome-cookies.js';
|
|
3
2
|
import type { CookieRecord, ProfileUploadResponse } from '../lib/types.js';
|
|
4
|
-
export declare function extractChromeCookies(domain: string, stats?: DecryptStats): CookieRecord[];
|
|
5
|
-
export declare function extractAllChromeCookies(stats?: DecryptStats): CookieRecord[];
|
|
6
3
|
type TldtsParse = (host: string, opts: {
|
|
7
4
|
allowPrivateDomains: boolean;
|
|
8
5
|
}) => {
|
package/dist/commands/profile.js
CHANGED
|
@@ -6,15 +6,6 @@ import { getApiKey } from '../lib/auth.js';
|
|
|
6
6
|
import { deriveChromeKey, listAuthRelevantHostKeys, openCookiesDb, readCookiesForHost, } from '../lib/chrome-cookies.js';
|
|
7
7
|
import { profileCreate, profileUpload } from '../lib/client.js';
|
|
8
8
|
import { err, errLine, handleApiError, out, outLine } from '../lib/output.js';
|
|
9
|
-
// ── UserFacingError ───────────────────────────────────────────────────────────
|
|
10
|
-
// Thrown by helpers to signal a terminal error that should be printed and exit 1
|
|
11
|
-
// without going through handleApiError (which would print a second error line).
|
|
12
|
-
class UserFacingError extends Error {
|
|
13
|
-
constructor(message) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.name = 'UserFacingError';
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
9
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
19
10
|
function getChromeCookiesPath() {
|
|
20
11
|
const platform = process.platform;
|
|
@@ -36,11 +27,11 @@ function validateDomain(domain) {
|
|
|
36
27
|
// Opens a private copy of Chrome's cookie DB, derives the decryption key, and calls fn.
|
|
37
28
|
function withCookiesDb(fn) {
|
|
38
29
|
if (process.platform === 'win32') {
|
|
39
|
-
throw new
|
|
30
|
+
throw new Error('Cookie extraction is not supported on Windows yet');
|
|
40
31
|
}
|
|
41
32
|
const cookiesPath = getChromeCookiesPath();
|
|
42
33
|
if (!fs.existsSync(cookiesPath)) {
|
|
43
|
-
throw new
|
|
34
|
+
throw new Error(`Chrome cookies file not found at ${cookiesPath}. Is Google Chrome installed?`);
|
|
44
35
|
}
|
|
45
36
|
// Copy the DB out of the profile directory so we avoid the Chrome file lock.
|
|
46
37
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tinyfish-chrome-cookies-'));
|
|
@@ -57,7 +48,7 @@ function withCookiesDb(fn) {
|
|
|
57
48
|
db = openCookiesDb(dbPath);
|
|
58
49
|
}
|
|
59
50
|
catch (e) {
|
|
60
|
-
throw new
|
|
51
|
+
throw new Error(`Failed to read Chrome cookies database: ${e instanceof Error ? e.message : String(e)}`);
|
|
61
52
|
}
|
|
62
53
|
return fn(db, deriveChromeKey());
|
|
63
54
|
}
|
|
@@ -76,19 +67,17 @@ function withCookiesDb(fn) {
|
|
|
76
67
|
}
|
|
77
68
|
}
|
|
78
69
|
}
|
|
79
|
-
|
|
80
|
-
export function extractChromeCookies(domain, stats) {
|
|
70
|
+
function extractChromeCookies(domain, stats) {
|
|
81
71
|
return withCookiesDb((db, key) => readCookiesForHost(db, key, domain, stats));
|
|
82
72
|
}
|
|
83
|
-
|
|
84
|
-
export function extractAllChromeCookies(stats) {
|
|
73
|
+
function extractAllChromeCookies(stats) {
|
|
85
74
|
return withCookiesDb((db, key) => {
|
|
86
75
|
let hostKeys;
|
|
87
76
|
try {
|
|
88
77
|
hostKeys = listAuthRelevantHostKeys(db);
|
|
89
78
|
}
|
|
90
79
|
catch (e) {
|
|
91
|
-
throw new
|
|
80
|
+
throw new Error(`Failed to read Chrome cookies database: ${e instanceof Error ? e.message : String(e)}`);
|
|
92
81
|
}
|
|
93
82
|
// Canonicalize host_keys → registrable domains (mirrors frontend/app/lib/services/domain-utils.ts)
|
|
94
83
|
const require = createRequire(import.meta.url);
|
|
@@ -181,7 +170,7 @@ function extractCookiesOrThrow(opts) {
|
|
|
181
170
|
errLine(`Skipped ${stats.keyringSkipped} keyring-encrypted cookies (not supported on Linux yet).`);
|
|
182
171
|
}
|
|
183
172
|
if (cookies.length === 0) {
|
|
184
|
-
throw new
|
|
173
|
+
throw new Error(stats.keyringSkipped > 0
|
|
185
174
|
? 'Chrome on Linux encrypts cookies with the system keyring, which is not supported yet.'
|
|
186
175
|
: opts.allDomains
|
|
187
176
|
? 'No Chrome cookies found. Is Google Chrome installed and have you logged in to any sites?'
|
|
@@ -281,10 +270,6 @@ export function registerProfile(program) {
|
|
|
281
270
|
}
|
|
282
271
|
}
|
|
283
272
|
catch (error) {
|
|
284
|
-
if (error instanceof UserFacingError) {
|
|
285
|
-
err({ error: error.message });
|
|
286
|
-
process.exit(1);
|
|
287
|
-
}
|
|
288
273
|
handleApiError(error);
|
|
289
274
|
}
|
|
290
275
|
});
|
package/dist/commands/run.js
CHANGED
|
@@ -172,34 +172,26 @@ function buildAgentConfig(opts) {
|
|
|
172
172
|
const cursorStyle = parseChoice(opts.cursorStyle, VALID_CURSOR_STYLES, '--cursor-style');
|
|
173
173
|
const maxSteps = parsePositiveInteger(opts.maxSteps, '--max-steps', MAX_STEPS_LIMIT);
|
|
174
174
|
const maxDurationSeconds = parsePositiveInteger(opts.maxDurationSeconds, '--max-duration-seconds');
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
};
|
|
175
|
+
return compact({
|
|
176
|
+
mode,
|
|
177
|
+
cursor_style: cursorStyle,
|
|
178
|
+
max_steps: maxSteps,
|
|
179
|
+
max_duration_seconds: maxDurationSeconds,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** Drops undefined fields; undefined when nothing is left. */
|
|
183
|
+
function compact(obj) {
|
|
184
|
+
const kept = Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
185
|
+
return Object.keys(kept).length ? kept : undefined;
|
|
187
186
|
}
|
|
188
187
|
function buildCaptureConfig(opts) {
|
|
189
|
-
|
|
190
|
-
opts.
|
|
191
|
-
opts.
|
|
192
|
-
opts.
|
|
193
|
-
opts.
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
...(opts.captureElements !== undefined ? { elements: opts.captureElements } : {}),
|
|
198
|
-
...(opts.captureSnapshots !== undefined ? { snapshots: opts.captureSnapshots } : {}),
|
|
199
|
-
...(opts.captureScreenshots !== undefined ? { screenshots: opts.captureScreenshots } : {}),
|
|
200
|
-
...(opts.captureRecording !== undefined ? { recording: opts.captureRecording } : {}),
|
|
201
|
-
...(opts.captureHtml !== undefined ? { html: opts.captureHtml } : {}),
|
|
202
|
-
};
|
|
188
|
+
return compact({
|
|
189
|
+
elements: opts.captureElements,
|
|
190
|
+
snapshots: opts.captureSnapshots,
|
|
191
|
+
screenshots: opts.captureScreenshots,
|
|
192
|
+
recording: opts.captureRecording,
|
|
193
|
+
html: opts.captureHtml,
|
|
194
|
+
});
|
|
203
195
|
}
|
|
204
196
|
async function buildRunRequest(goal, normalizedUrl, opts) {
|
|
205
197
|
const outputSchema = await loadOutputSchema(opts);
|
|
@@ -224,13 +216,7 @@ async function buildRunRequest(goal, normalizedUrl, opts) {
|
|
|
224
216
|
};
|
|
225
217
|
}
|
|
226
218
|
async function runAsyncPath(req, apiKey, pretty) {
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
result = await runAsync(req, apiKey);
|
|
230
|
-
}
|
|
231
|
-
catch (e) {
|
|
232
|
-
handleApiError(e);
|
|
233
|
-
}
|
|
219
|
+
const result = await runAsync(req, apiKey).catch(handleApiError);
|
|
234
220
|
checkRunResult(result, false); // async = PENDING is expected
|
|
235
221
|
const asyncResultWithUrl = {
|
|
236
222
|
...result,
|
|
@@ -246,13 +232,7 @@ async function runAsyncPath(req, apiKey, pretty) {
|
|
|
246
232
|
}
|
|
247
233
|
async function runSyncPath(req, apiKey, pretty) {
|
|
248
234
|
errLine('Waiting for result...');
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
result = await runSync(req, apiKey);
|
|
252
|
-
}
|
|
253
|
-
catch (e) {
|
|
254
|
-
handleApiError(e);
|
|
255
|
-
}
|
|
235
|
+
const result = await runSync(req, apiKey).catch(handleApiError);
|
|
256
236
|
checkRunResult(result);
|
|
257
237
|
const syncResultWithUrl = {
|
|
258
238
|
...result,
|