@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.
Files changed (51) hide show
  1. package/dist/commands/connect.d.ts +2 -14
  2. package/dist/commands/connect.js +109 -75
  3. package/dist/commands/doctor.js +3 -1
  4. package/dist/commands/profile.d.ts +0 -3
  5. package/dist/commands/profile.js +7 -22
  6. package/dist/commands/run.js +20 -40
  7. package/dist/commands/runs.js +24 -58
  8. package/dist/lib/auth.d.ts +1 -3
  9. package/dist/lib/auth.js +1 -1
  10. package/dist/lib/claude-config.d.ts +0 -4
  11. package/dist/lib/claude-config.js +4 -4
  12. package/dist/lib/cli-install.d.ts +0 -1
  13. package/dist/lib/cli-install.js +0 -1
  14. package/dist/lib/client.js +82 -147
  15. package/dist/lib/connect-all-auth.js +1 -1
  16. package/dist/lib/connect-all-summary.js +4 -1
  17. package/dist/lib/connect-all-uninstall.js +12 -1
  18. package/dist/lib/connect-all.d.ts +4 -2
  19. package/dist/lib/connect-all.js +21 -32
  20. package/dist/lib/connect-clients.d.ts +11 -8
  21. package/dist/lib/connect-clients.js +53 -13
  22. package/dist/lib/connect-fallback.d.ts +2 -1
  23. package/dist/lib/connect-picker.d.ts +3 -2
  24. package/dist/lib/connect-runtime.js +4 -6
  25. package/dist/lib/doctor-checks.d.ts +1 -0
  26. package/dist/lib/doctor-checks.js +38 -3
  27. package/dist/lib/doctor-report.d.ts +6 -0
  28. package/dist/lib/harness-detect.d.ts +2 -0
  29. package/dist/lib/harness-detect.js +11 -3
  30. package/dist/lib/harness-spec.d.ts +20 -39
  31. package/dist/lib/harness-spec.js +12 -31
  32. package/dist/lib/harness.js +2 -0
  33. package/dist/lib/hermes-config.d.ts +3 -0
  34. package/dist/lib/hermes-config.js +8 -4
  35. package/dist/lib/hermes-env.d.ts +7 -2
  36. package/dist/lib/hermes-env.js +7 -6
  37. package/dist/lib/hermes-plugin.d.ts +4 -0
  38. package/dist/lib/hermes-plugin.js +17 -9
  39. package/dist/lib/output.d.ts +2 -0
  40. package/dist/lib/output.js +9 -0
  41. package/dist/lib/pi-config.d.ts +26 -0
  42. package/dist/lib/pi-config.js +111 -0
  43. package/dist/lib/registration-detect.d.ts +0 -1
  44. package/dist/lib/registration-detect.js +60 -81
  45. package/dist/lib/setup-telemetry.d.ts +13 -7
  46. package/dist/lib/setup-telemetry.js +5 -5
  47. package/dist/lib/skill-install.d.ts +1 -0
  48. package/dist/lib/skill-install.js +43 -44
  49. package/dist/lib/types.d.ts +4 -3
  50. package/dist/program.js +2 -3
  51. 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
- export declare function connectOpenClaw(options: {
27
- apiKey?: string;
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;
@@ -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, HoistedFailureReportedError, installTinyFishCli, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from '../lib/cli-install.js';
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 = `TinyFish does not sign in to ${client.displayName} any other way`;
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
- // All native clients have keyAuth; missing flags mean an old install.
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
- const addOptions = interactiveAdd
192
- ? { stdio: 'inherit', timeout: options.authTimeoutMs }
193
- : seeded
194
- ? {
195
- encoding: 'utf8',
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.hoistedCliInstall) {
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
- installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
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
- if (mcpUrl.startsWith('-')) {
711
- throw new Error(`Invalid --url value: ${mcpUrl}`);
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
- async function connectAllPath(options, mcpUrl, attemptId) {
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 connectAllPath(options, mcpUrl, attemptId);
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 connectPickPath(options, mcpUrl, attemptId);
864
+ await connectManyPath(options, mcpUrl, attemptId);
831
865
  return;
832
866
  }
833
867
  await connectSingleClient(client, options, mcpUrl, attemptId);
@@ -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
  }) => {
@@ -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 UserFacingError('Cookie extraction is not supported on Windows yet');
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 UserFacingError(`Chrome cookies file not found at ${cookiesPath}. Is Google Chrome installed?`);
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 UserFacingError(`Failed to read Chrome cookies database: ${e instanceof Error ? e.message : String(e)}`);
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
- // Exported for testing
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
- // Exported for testing
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 UserFacingError(`Failed to read Chrome cookies database: ${e instanceof Error ? e.message : String(e)}`);
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 UserFacingError(stats.keyringSkipped > 0
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
  });
@@ -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
- if (mode === undefined &&
176
- cursorStyle === undefined &&
177
- maxSteps === undefined &&
178
- maxDurationSeconds === undefined) {
179
- return undefined;
180
- }
181
- return {
182
- ...(mode !== undefined ? { mode } : {}),
183
- ...(cursorStyle !== undefined ? { cursor_style: cursorStyle } : {}),
184
- ...(maxSteps !== undefined ? { max_steps: maxSteps } : {}),
185
- ...(maxDurationSeconds !== undefined ? { max_duration_seconds: maxDurationSeconds } : {}),
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
- if (opts.captureElements === undefined &&
190
- opts.captureSnapshots === undefined &&
191
- opts.captureScreenshots === undefined &&
192
- opts.captureRecording === undefined &&
193
- opts.captureHtml === undefined) {
194
- return undefined;
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
- let result;
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
- let result;
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,