@tiny-fish/cli 0.30.2-next.257 → 0.30.2-next.258

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 (77) hide show
  1. package/dist/commands/auth.d.ts +1 -1
  2. package/dist/commands/auth.js +52 -52
  3. package/dist/commands/batch.d.ts +1 -1
  4. package/dist/commands/batch.js +54 -46
  5. package/dist/commands/browser.d.ts +1 -1
  6. package/dist/commands/browser.js +14 -14
  7. package/dist/commands/config-claude.d.ts +1 -1
  8. package/dist/commands/config-claude.js +22 -22
  9. package/dist/commands/connect.d.ts +3 -3
  10. package/dist/commands/connect.js +150 -150
  11. package/dist/commands/doctor.d.ts +4 -4
  12. package/dist/commands/doctor.js +104 -104
  13. package/dist/commands/fetch.d.ts +1 -1
  14. package/dist/commands/fetch.js +24 -27
  15. package/dist/commands/onboard.d.ts +1 -1
  16. package/dist/commands/onboard.js +50 -50
  17. package/dist/commands/profile.d.ts +3 -3
  18. package/dist/commands/profile.js +45 -45
  19. package/dist/commands/run.d.ts +1 -1
  20. package/dist/commands/run.js +78 -65
  21. package/dist/commands/runs.d.ts +1 -1
  22. package/dist/commands/runs.js +63 -50
  23. package/dist/commands/search.d.ts +1 -1
  24. package/dist/commands/search.js +16 -16
  25. package/dist/commands/upgrade.d.ts +2 -2
  26. package/dist/commands/upgrade.js +36 -36
  27. package/dist/commands/vault.d.ts +1 -1
  28. package/dist/commands/vault.js +48 -45
  29. package/dist/commands/wallet.d.ts +1 -1
  30. package/dist/commands/wallet.js +14 -14
  31. package/dist/index.js +2 -2
  32. package/dist/lib/auth.d.ts +5 -5
  33. package/dist/lib/auth.js +56 -56
  34. package/dist/lib/batch-store.d.ts +1 -1
  35. package/dist/lib/batch-store.js +4 -4
  36. package/dist/lib/chrome-cookies.d.ts +1 -1
  37. package/dist/lib/chrome-cookies.js +24 -24
  38. package/dist/lib/claude-config.js +37 -37
  39. package/dist/lib/cli-install.js +12 -12
  40. package/dist/lib/client.d.ts +2 -2
  41. package/dist/lib/client.js +61 -61
  42. package/dist/lib/connect-all.js +140 -134
  43. package/dist/lib/connect-auth.d.ts +2 -2
  44. package/dist/lib/connect-auth.js +15 -15
  45. package/dist/lib/connect-clients.d.ts +6 -6
  46. package/dist/lib/connect-clients.js +143 -143
  47. package/dist/lib/connect-fallback.d.ts +1 -1
  48. package/dist/lib/connect-fallback.js +38 -38
  49. package/dist/lib/connect-picker.d.ts +4 -4
  50. package/dist/lib/connect-picker.js +28 -28
  51. package/dist/lib/connect-runtime.d.ts +7 -7
  52. package/dist/lib/connect-runtime.js +61 -61
  53. package/dist/lib/constants.js +13 -13
  54. package/dist/lib/cursor-config.d.ts +1 -1
  55. package/dist/lib/cursor-config.js +30 -30
  56. package/dist/lib/doctor-report.d.ts +2 -2
  57. package/dist/lib/doctor-report.js +22 -22
  58. package/dist/lib/harness-detect.js +39 -39
  59. package/dist/lib/harness.js +9 -9
  60. package/dist/lib/hermes-env.js +26 -23
  61. package/dist/lib/install-root.js +12 -12
  62. package/dist/lib/notice.d.ts +1 -1
  63. package/dist/lib/notice.js +12 -12
  64. package/dist/lib/output.js +14 -14
  65. package/dist/lib/registration-detect.d.ts +3 -3
  66. package/dist/lib/registration-detect.js +82 -82
  67. package/dist/lib/setup-telemetry.d.ts +4 -4
  68. package/dist/lib/setup-telemetry.js +34 -34
  69. package/dist/lib/signals.js +1 -1
  70. package/dist/lib/skill-install.d.ts +3 -3
  71. package/dist/lib/skill-install.js +40 -40
  72. package/dist/lib/types.d.ts +8 -8
  73. package/dist/lib/verify.d.ts +1 -1
  74. package/dist/lib/verify.js +28 -28
  75. package/dist/program.d.ts +1 -1
  76. package/dist/program.js +28 -28
  77. package/package.json +1 -1
@@ -1,22 +1,22 @@
1
- import spawn from "cross-spawn";
2
- import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from "../lib/auth.js";
3
- import { CLAUDE_CODE, CODEX, CURSOR_SKILL_TARGET, GROK, HERMES, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, OPENCODE, launchNativeMcpClient, launchOpenClawWalkthrough, openExternalUrl, } from "../lib/connect-clients.js";
4
- import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, } from "../lib/cli-install.js";
5
- import { ensureCliAuthenticated } from "../lib/connect-auth.js";
6
- import { createStdinPrompt, runCliFallback } from "../lib/connect-fallback.js";
7
- import { installWebSkill } from "../lib/skill-install.js";
8
- import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, spawnRemoval, } from "../lib/connect-runtime.js";
9
- import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from "../lib/cursor-config.js";
10
- import { runConnectAll } from "../lib/connect-all.js";
11
- import { AuthMode, RELOAD_ACTION } from "../lib/harness-detect.js";
12
- import { detectHumanInitiated } from "../lib/harness.js";
13
- import { TINYFISH_API_KEY_VAR } from "../lib/constants.js";
14
- import { emitNotice } from "../lib/notice.js";
15
- import { SIGN_IN_PROBES } from "../lib/registration-detect.js";
16
- import { errLine, warnLine } from "../lib/output.js";
17
- import { z } from "zod";
18
- import { verifyMcpAuth } from "../lib/verify.js";
19
- export const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
1
+ import spawn from 'cross-spawn';
2
+ import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from '../lib/auth.js';
3
+ import { CLAUDE_CODE, CODEX, CURSOR_SKILL_TARGET, GROK, HERMES, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, OPENCODE, launchNativeMcpClient, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
4
+ import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, } from '../lib/cli-install.js';
5
+ import { ensureCliAuthenticated } from '../lib/connect-auth.js';
6
+ import { createStdinPrompt, runCliFallback } from '../lib/connect-fallback.js';
7
+ import { installWebSkill } from '../lib/skill-install.js';
8
+ import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, spawnRemoval, } from '../lib/connect-runtime.js';
9
+ import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from '../lib/cursor-config.js';
10
+ import { runConnectAll } from '../lib/connect-all.js';
11
+ import { AuthMode, RELOAD_ACTION } from '../lib/harness-detect.js';
12
+ import { detectHumanInitiated } from '../lib/harness.js';
13
+ import { TINYFISH_API_KEY_VAR } from '../lib/constants.js';
14
+ import { emitNotice } from '../lib/notice.js';
15
+ import { SIGN_IN_PROBES } from '../lib/registration-detect.js';
16
+ import { errLine, warnLine } from '../lib/output.js';
17
+ import { z } from 'zod';
18
+ import { verifyMcpAuth } from '../lib/verify.js';
19
+ export const DEFAULT_MCP_URL = 'https://agent.tinyfish.ai/mcp';
20
20
  function removeExistingRegistration(client, removal, verbose) {
21
21
  const result = spawnRemoval(client.command, removal.args);
22
22
  if (result.status === 0)
@@ -24,7 +24,7 @@ function removeExistingRegistration(client, removal, verbose) {
24
24
  // Best-effort: a stale entry makes `mcp add` error or upsert, with a truer message.
25
25
  if (!verbose)
26
26
  return;
27
- const details = result.stderr?.trim() || result.error?.message || "unknown error";
27
+ const details = result.stderr?.trim() || result.error?.message || 'unknown error';
28
28
  warnLine(`Could not remove existing ${removal.label}: ${details}`);
29
29
  }
30
30
  /** false: fall back to OAuth — a config naming an unset variable authenticates nothing. */
@@ -34,29 +34,29 @@ function exportApiKey(apiKey, displayName, deferOutro) {
34
34
  }
35
35
  catch (error) {
36
36
  errLine(`Could not export TINYFISH_API_KEY (${error instanceof Error ? error.message : String(error)}); ` +
37
- "signing in instead.");
37
+ 'signing in instead.');
38
38
  return false;
39
39
  }
40
40
  process.env[TINYFISH_API_KEY_VAR] = apiKey;
41
41
  if (deferOutro) {
42
- deferOutro("Exported TINYFISH_API_KEY to your shell profile — restart your agents (or open a new " +
43
- "terminal) before use.");
42
+ deferOutro('Exported TINYFISH_API_KEY to your shell profile — restart your agents (or open a new ' +
43
+ 'terminal) before use.');
44
44
  }
45
45
  else {
46
46
  errLine(`Exported TINYFISH_API_KEY to your shell profile — ${displayName} reads the key from there, ` +
47
- "so restart it (or open a new terminal) before use.");
47
+ 'so restart it (or open a new terminal) before use.');
48
48
  }
49
49
  return true;
50
50
  }
51
51
  /** A bounded spawn's timeout means the sign-in never finished. */
52
52
  function signInStepError(message, result, bounded) {
53
53
  const error = spawnStepError(message, result);
54
- return bounded && error.failureReason === "timeout"
54
+ return bounded && error.failureReason === 'timeout'
55
55
  ? new SignInTimeoutError(error.message, error.cause)
56
56
  : error;
57
57
  }
58
58
  function isMissingHarness(error) {
59
- return error instanceof ConnectStepError && error.failureReason === "harness_not_installed";
59
+ return error instanceof ConnectStepError && error.failureReason === 'harness_not_installed';
60
60
  }
61
61
  /** Undefined means the rescue ran; the caller returns immediately. */
62
62
  async function requireSupportOrRescue(client, connectClient, state, telemetry, options) {
@@ -73,7 +73,7 @@ async function requireSupportOrRescue(client, connectClient, state, telemetry, o
73
73
  // The harness attempt stays failed; the rescue rides fallback_outcome (PF-3707).
74
74
  async function runMissingHarnessFallback(displayName, connectClient, state, telemetry, options) {
75
75
  // Stamped before any step: Ctrl+C settles before an outcome exists.
76
- telemetry.setFallbackOutcome("abandoned");
76
+ telemetry.setFallbackOutcome('abandoned');
77
77
  errLine(`${displayName} was not found on PATH. Setting up the TinyFish CLI instead.`);
78
78
  const outcome = await runCliFallback({
79
79
  apiKey: options.apiKey,
@@ -85,28 +85,28 @@ async function runMissingHarnessFallback(displayName, connectClient, state, tele
85
85
  onStepStart: (stage) => {
86
86
  state.stage = stage;
87
87
  },
88
- onStepDone: (phase) => telemetry.track("checkpoint", { phase }),
88
+ onStepDone: (phase) => telemetry.track('checkpoint', { phase }),
89
89
  },
90
90
  });
91
91
  telemetry.setFallbackOutcome(outcome);
92
92
  // Never `completed`: dashboards read that as "the harness install worked".
93
- settle(state, telemetry, "failed", {
94
- failedStage: "prerequisite_check",
95
- failureReason: "harness_not_installed",
93
+ settle(state, telemetry, 'failed', {
94
+ failedStage: 'prerequisite_check',
95
+ failureReason: 'harness_not_installed',
96
96
  });
97
- if (outcome !== "cli_verified") {
97
+ if (outcome !== 'cli_verified') {
98
98
  process.exitCode = 1;
99
99
  return;
100
100
  }
101
- errLine("The TinyFish CLI is installed and working.");
102
- errLine("Using a different agent? Run: tinyfish connect --all");
101
+ errLine('The TinyFish CLI is installed and working.');
102
+ errLine('Using a different agent? Run: tinyfish connect --all');
103
103
  }
104
104
  async function connectNativeMcpClient(client, options) {
105
105
  const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
106
- const state = { stage: "prerequisite_check", settled: false };
106
+ const state = { stage: 'prerequisite_check', settled: false };
107
107
  const probeAuthenticated = SIGN_IN_PROBES[client.connectClient];
108
108
  await runGuarded(state, telemetry, async () => {
109
- telemetry.track("started");
109
+ telemetry.track('started');
110
110
  const support = await requireSupportOrRescue(client, client.connectClient, state, telemetry, options);
111
111
  if (!support)
112
112
  return;
@@ -128,25 +128,25 @@ async function connectNativeMcpClient(client, options) {
128
128
  // No keyAuth at all is structural (Hermes); a missing flag is just an old install.
129
129
  if (client.keyAuth) {
130
130
  throw new ConnectStepError(`This ${client.displayName} install cannot take a TinyFish API key, and signing in ` +
131
- `needs a terminal. Update it, then re-run: tinyfish connect ${client.connectClient}`, "harness_too_old");
131
+ `needs a terminal. Update it, then re-run: tinyfish connect ${client.connectClient}`, 'harness_too_old');
132
132
  }
133
133
  throw new ConnectStepError(`${client.displayName} cannot take a TinyFish API key, and signing in needs a ` +
134
- `terminal. Run: tinyfish connect ${client.connectClient}`, "key_auth_unsupported");
134
+ `terminal. Run: tinyfish connect ${client.connectClient}`, 'key_auth_unsupported');
135
135
  }
136
- telemetry.track("checkpoint", { phase: "prerequisite_ok" });
137
- state.stage = "registration_cleanup";
136
+ telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
137
+ state.stage = 'registration_cleanup';
138
138
  for (const removal of client.removals)
139
139
  removeExistingRegistration(client, removal, options.verbose ?? false);
140
140
  const mcpUrl = new URL(options.mcpUrl);
141
- mcpUrl.searchParams.set("source", CONNECT_SOURCE);
142
- mcpUrl.searchParams.set("client", client.connectClient);
143
- mcpUrl.searchParams.set("connect_attempt_id", telemetry.attemptId);
141
+ mcpUrl.searchParams.set('source', CONNECT_SOURCE);
142
+ mcpUrl.searchParams.set('client', client.connectClient);
143
+ mcpUrl.searchParams.set('connect_attempt_id', telemetry.attemptId);
144
144
  // Undefining `loginArgs` reuses the deferred-auth path codex/hermes already take.
145
145
  const loginDegraded = state.harnessDegraded === true;
146
146
  const loginArgs = loginDegraded ? undefined : client.loginArgs;
147
147
  // A probed client authenticates inside `mcp add` too, so a failure there is either.
148
148
  state.stage =
149
- loginArgs && !probeAuthenticated ? "registration" : "registration_or_authentication";
149
+ loginArgs && !probeAuthenticated ? 'registration' : 'registration_or_authentication';
150
150
  errLine(`Adding TinyFish to ${client.displayName}...`);
151
151
  // Header keys ride argv (/proc/<pid>/cmdline); these CLIs take headers no other way.
152
152
  const addArgs = keyedAdd
@@ -155,7 +155,7 @@ async function connectNativeMcpClient(client, options) {
155
155
  // Capturing an inline-OAuth add hides the sign-in URL and hangs.
156
156
  const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
157
157
  const addResult = spawn.sync(client.command, addArgs, interactiveAdd
158
- ? { stdio: "inherit", timeout: options.authTimeoutMs }
158
+ ? { stdio: 'inherit', timeout: options.authTimeoutMs }
159
159
  : captureStdio(options.verbose ?? false));
160
160
  if (addResult.error || addResult.status !== 0) {
161
161
  // Built first: its interrupt check must run before any replay.
@@ -163,15 +163,15 @@ async function connectNativeMcpClient(client, options) {
163
163
  replay(capturedOutput(addResult));
164
164
  throw error;
165
165
  }
166
- telemetry.track("checkpoint", { phase: "registered" });
166
+ telemetry.track('checkpoint', { phase: 'registered' });
167
167
  // Codex runs OAuth inside `mcp add` and exits 0 either way, so the recorded state decides
168
168
  // whether the login step below still runs. Unknown keeps the pre-probe behaviour.
169
169
  const addSignedIn = probeAuthenticated?.();
170
170
  if (addSignedIn)
171
- telemetry.track("checkpoint", { phase: "oauth_done" });
171
+ telemetry.track('checkpoint', { phase: 'oauth_done' });
172
172
  const degradedNotes = loginDegraded ? client.degradedAuthNotes : undefined;
173
173
  if (useKeyAuth) {
174
- errLine(degradedNotes?.keyed ?? "Using your stored TinyFish API key — no browser sign-in needed.");
174
+ errLine(degradedNotes?.keyed ?? 'Using your stored TinyFish API key — no browser sign-in needed.');
175
175
  }
176
176
  else if (degradedNotes) {
177
177
  // Printed here, not with the closing line, so a `--launch` run sees it before the handover.
@@ -182,10 +182,10 @@ async function connectNativeMcpClient(client, options) {
182
182
  const pendingLogin = !useKeyAuth && retryLogin ? loginArgs : undefined;
183
183
  let signInDeferred = !useKeyAuth && !addSignedIn && !pendingLogin;
184
184
  if (pendingLogin) {
185
- state.stage = "client_oauth";
186
- errLine("Signing in to TinyFish...");
185
+ state.stage = 'client_oauth';
186
+ errLine('Signing in to TinyFish...');
187
187
  const loginResult = spawn.sync(client.command, pendingLogin, {
188
- stdio: "inherit",
188
+ stdio: 'inherit',
189
189
  timeout: options.authTimeoutMs,
190
190
  });
191
191
  if (loginResult.error || loginResult.status !== 0) {
@@ -194,16 +194,16 @@ async function connectNativeMcpClient(client, options) {
194
194
  if (probeAuthenticated?.() === false)
195
195
  signInDeferred = true;
196
196
  else
197
- telemetry.track("checkpoint", { phase: "oauth_done" });
197
+ telemetry.track('checkpoint', { phase: 'oauth_done' });
198
198
  }
199
199
  const authMode = useKeyAuth
200
200
  ? AuthMode.ApiKey
201
201
  : signInDeferred
202
- ? "deferred"
202
+ ? 'deferred'
203
203
  : AuthMode.OAuth;
204
204
  state.authMode = authMode;
205
205
  // Registration/OAuth make MCP work; later steps are cosmetic and must not fail the attempt.
206
- settle(state, telemetry, "completed", { authMode });
206
+ settle(state, telemetry, 'completed', { authMode });
207
207
  await runPostInstallSteps(client, options, state, telemetry, signInDeferred);
208
208
  });
209
209
  return state.authMode;
@@ -214,7 +214,7 @@ function connectedLine(client, signInDeferred) {
214
214
  if (client.signInHint)
215
215
  return client.signInHint;
216
216
  return (`TinyFish is configured in ${client.displayName}. ${client.displayName} will ask you ` +
217
- "to sign in to TinyFish the first time you use it.");
217
+ 'to sign in to TinyFish the first time you use it.');
218
218
  }
219
219
  return (`TinyFish is connected. Open ${client.displayName}; if it is already running, ` +
220
220
  `${RELOAD_ACTION[client.connectClient]} to pick up the tools.`);
@@ -237,22 +237,22 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
237
237
  };
238
238
  try {
239
239
  if (options.cliInstall !== false) {
240
- await attempt("cli_install", () => runCliInstallStep(options, telemetry));
240
+ await attempt('cli_install', () => runCliInstallStep(options, telemetry));
241
241
  }
242
242
  if (client.skillAgent) {
243
- await attempt("skill_install", () => {
243
+ await attempt('skill_install', () => {
244
244
  installWebSkill(client, { verbose: options.verbose ?? false });
245
- telemetry.track("checkpoint", { phase: "skill_installed" });
245
+ telemetry.track('checkpoint', { phase: 'skill_installed' });
246
246
  });
247
247
  }
248
248
  // Before auth: a failed sign-in must not lose the harness record.
249
249
  saveConnectContext(client.connectClient, telemetry.attemptId, state.authMode);
250
- await attempt("authentication", () => {
250
+ await attempt('authentication', () => {
251
251
  // A set timeout means headless; never shell an interactive login.
252
252
  ensureCliAuthenticated(client.connectClient, options.apiKey, {
253
253
  interactiveLogin: options.authTimeoutMs === undefined,
254
254
  });
255
- telemetry.track("checkpoint", { phase: "authenticated" });
255
+ telemetry.track('checkpoint', { phase: 'authenticated' });
256
256
  });
257
257
  if (client.postConnectNote)
258
258
  errLine(client.postConnectNote);
@@ -262,12 +262,12 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
262
262
  errLine(connectedLine(client, signInDeferred));
263
263
  }
264
264
  else {
265
- await attempt("walkthrough_launch", async () => {
265
+ await attempt('walkthrough_launch', async () => {
266
266
  // Before the handover, or the only sign-in instructions scroll past under the agent.
267
267
  if (signInDeferred && client.signInHint)
268
268
  errLine(client.signInHint);
269
269
  await launchNativeMcpClient(client, async (outcome) => {
270
- telemetry.track("checkpoint", { phase: walkthroughPhase(outcome) });
270
+ telemetry.track('checkpoint', { phase: walkthroughPhase(outcome) });
271
271
  // Flushed pre-block; the session-long handover would strand the delivery.
272
272
  await telemetry.flush();
273
273
  });
@@ -294,18 +294,18 @@ function runCliInstallStep(options, telemetry) {
294
294
  throw firstTake ? outcome.error : new HoistedFailureReportedError(outcome.error.message);
295
295
  }
296
296
  if (firstTake) {
297
- telemetry.track("checkpoint", {
298
- phase: "cli_installed",
297
+ telemetry.track('checkpoint', {
298
+ phase: 'cli_installed',
299
299
  stageDurationMs: outcome.durationMs,
300
300
  });
301
301
  }
302
302
  return;
303
303
  }
304
304
  installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
305
- telemetry.track("checkpoint", { phase: "cli_installed" });
305
+ telemetry.track('checkpoint', { phase: 'cli_installed' });
306
306
  }
307
307
  function walkthroughPhase(outcome) {
308
- return outcome === "printed" ? "walkthrough_prompt_printed" : "walkthrough_launched";
308
+ return outcome === 'printed' ? 'walkthrough_prompt_printed' : 'walkthrough_launched';
309
309
  }
310
310
  function finishSetupHint(connectClient) {
311
311
  return `Finish setup with: ${finishSetupCommand(connectClient)}`;
@@ -316,9 +316,9 @@ function trackPostInstallFailure(displayName, state, telemetry, error) {
316
316
  return;
317
317
  // An earlier harness already owns the shared install's failure record.
318
318
  if (!(error instanceof HoistedFailureReportedError)) {
319
- telemetry.track("post_install_failed", {
319
+ telemetry.track('post_install_failed', {
320
320
  failedStage: state.stage,
321
- failureReason: error instanceof ConnectStepError ? error.failureReason : "unexpected_error",
321
+ failureReason: error instanceof ConnectStepError ? error.failureReason : 'unexpected_error',
322
322
  });
323
323
  }
324
324
  errLine(`The ${displayName} MCP connection succeeded, but a finishing step ` +
@@ -340,74 +340,74 @@ export async function connectOpencode(options) {
340
340
  return connectNativeMcpClient(OPENCODE, options);
341
341
  }
342
342
  export async function connectOpenClaw(options) {
343
- const telemetry = createConnectTelemetry(options.mcpUrl, "openclaw", options);
343
+ const telemetry = createConnectTelemetry(options.mcpUrl, 'openclaw', options);
344
344
  // Skill install, no MCP server — never degraded, and false keeps it inside a `= false` filter.
345
345
  const state = {
346
- stage: "prerequisite_check",
346
+ stage: 'prerequisite_check',
347
347
  settled: false,
348
348
  harnessDegraded: false,
349
349
  };
350
350
  await runGuarded(state, telemetry, async () => {
351
- telemetry.track("started");
352
- const support = await requireSupportOrRescue(OPENCLAW, "openclaw", state, telemetry, options);
351
+ telemetry.track('started');
352
+ const support = await requireSupportOrRescue(OPENCLAW, 'openclaw', state, telemetry, options);
353
353
  if (!support)
354
354
  return;
355
355
  telemetry.setHarnessVersion(support.harnessVersion);
356
- telemetry.track("checkpoint", { phase: "prerequisite_ok" });
356
+ telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
357
357
  if (options.hoistedCliInstall) {
358
- state.stage = "cli_install";
358
+ state.stage = 'cli_install';
359
359
  const { outcome, firstTake } = options.hoistedCliInstall();
360
360
  // A carried failure fails openclaw exactly like the inline install.
361
361
  if (!outcome.ok)
362
362
  throw outcome.error;
363
363
  if (firstTake) {
364
- telemetry.track("checkpoint", {
365
- phase: "cli_installed",
364
+ telemetry.track('checkpoint', {
365
+ phase: 'cli_installed',
366
366
  stageDurationMs: outcome.durationMs,
367
367
  });
368
368
  }
369
369
  }
370
370
  else if (options.cliInstall !== false) {
371
- state.stage = "cli_install";
371
+ state.stage = 'cli_install';
372
372
  installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
373
- telemetry.track("checkpoint", { phase: "cli_installed" });
373
+ telemetry.track('checkpoint', { phase: 'cli_installed' });
374
374
  }
375
- state.stage = "skill_install";
376
- errLine("Installing the TinyFish skill in OpenClaw...");
377
- const skillInstallResult = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, captureStdio(options.verbose ?? false));
375
+ state.stage = 'skill_install';
376
+ errLine('Installing the TinyFish skill in OpenClaw...');
377
+ const skillInstallResult = spawn.sync('openclaw', OPENCLAW_SKILL_INSTALL_ARGS, captureStdio(options.verbose ?? false));
378
378
  if (skillInstallResult.error || skillInstallResult.status !== 0) {
379
379
  // Built first: its interrupt check must run before any replay.
380
- const error = spawnStepError("Could not install the TinyFish skill in OpenClaw", skillInstallResult);
380
+ const error = spawnStepError('Could not install the TinyFish skill in OpenClaw', skillInstallResult);
381
381
  replay(capturedOutput(skillInstallResult));
382
382
  throw error;
383
383
  }
384
- telemetry.track("checkpoint", { phase: "skill_installed" });
384
+ telemetry.track('checkpoint', { phase: 'skill_installed' });
385
385
  // Stores the key before probing, so a passed --api-key never reaches an interactive login.
386
- state.stage = "authentication";
387
- ensureCliAuthenticated("openclaw", options.apiKey);
388
- telemetry.track("checkpoint", { phase: "authenticated" });
389
- saveConnectContext("openclaw", telemetry.attemptId);
386
+ state.stage = 'authentication';
387
+ ensureCliAuthenticated('openclaw', options.apiKey);
388
+ telemetry.track('checkpoint', { phase: 'authenticated' });
389
+ saveConnectContext('openclaw', telemetry.attemptId);
390
390
  // For openclaw the skill install and auth ARE functional; only the walkthrough is cosmetic.
391
- settle(state, telemetry, "completed");
391
+ settle(state, telemetry, 'completed');
392
392
  if (!options.launch) {
393
393
  // Under --all the summary already lists OpenClaw.
394
394
  if (!options.deferOutro) {
395
- errLine("TinyFish is connected. Open OpenClaw; if it is already running, " +
395
+ errLine('TinyFish is connected. Open OpenClaw; if it is already running, ' +
396
396
  `${RELOAD_ACTION.openclaw} to pick up the skill.`);
397
397
  }
398
398
  return;
399
399
  }
400
- state.stage = "walkthrough_launch";
400
+ state.stage = 'walkthrough_launch';
401
401
  try {
402
402
  await launchOpenClawWalkthrough(async (outcome) => {
403
- telemetry.track("checkpoint", { phase: walkthroughPhase(outcome) });
403
+ telemetry.track('checkpoint', { phase: walkthroughPhase(outcome) });
404
404
  // Flushed pre-block; the session-long handover would strand the delivery.
405
405
  await telemetry.flush();
406
406
  });
407
407
  }
408
408
  catch (error) {
409
- trackPostInstallFailure("OpenClaw", state, telemetry, error);
410
- errLine(finishSetupHint("openclaw"));
409
+ trackPostInstallFailure('OpenClaw', state, telemetry, error);
410
+ errLine(finishSetupHint('openclaw'));
411
411
  }
412
412
  });
413
413
  }
@@ -419,10 +419,10 @@ export async function connectCursor(options) {
419
419
  try {
420
420
  const registered = new URL(registeredUrl);
421
421
  const requested = new URL(options.mcpUrl);
422
- const registeredId = registered.searchParams.get("connect_attempt_id");
423
- const registeredSource = registered.searchParams.get("source");
424
- const registeredClient = registered.searchParams.get("client");
425
- for (const key of ["source", "client", "connect_attempt_id"]) {
422
+ const registeredId = registered.searchParams.get('connect_attempt_id');
423
+ const registeredSource = registered.searchParams.get('source');
424
+ const registeredClient = registered.searchParams.get('client');
425
+ for (const key of ['source', 'client', 'connect_attempt_id']) {
426
426
  registered.searchParams.delete(key);
427
427
  requested.searchParams.delete(key);
428
428
  }
@@ -430,7 +430,7 @@ export async function connectCursor(options) {
430
430
  requested.searchParams.sort();
431
431
  if (registered.toString() === requested.toString() &&
432
432
  registeredSource === CONNECT_SOURCE &&
433
- registeredClient === "cursor" &&
433
+ registeredClient === 'cursor' &&
434
434
  z.uuid().safeParse(registeredId).success) {
435
435
  fallbackAttemptId = registeredId ?? undefined;
436
436
  }
@@ -439,51 +439,51 @@ export async function connectCursor(options) {
439
439
  // A hand-edited invalid URL is repaired below with a fresh attempt id.
440
440
  }
441
441
  }
442
- const telemetry = createConnectTelemetry(options.mcpUrl, "cursor", {
442
+ const telemetry = createConnectTelemetry(options.mcpUrl, 'cursor', {
443
443
  ...options,
444
444
  fallbackAttemptId,
445
445
  });
446
446
  // Never degraded: no sign-in command to be missing. False, not absent, so a `= false`
447
447
  // filter still catches Cursor.
448
448
  const state = {
449
- stage: "prerequisite_check",
449
+ stage: 'prerequisite_check',
450
450
  settled: false,
451
451
  harnessDegraded: false,
452
452
  };
453
453
  await runGuarded(state, telemetry, async () => {
454
- telemetry.track("started");
455
- telemetry.track("checkpoint", { phase: "prerequisite_ok" });
456
- state.stage = "skill_install";
454
+ telemetry.track('started');
455
+ telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
456
+ state.stage = 'skill_install';
457
457
  installWebSkill(CURSOR_SKILL_TARGET, { verbose: options.verbose ?? false });
458
- telemetry.track("checkpoint", { phase: "skill_installed" });
458
+ telemetry.track('checkpoint', { phase: 'skill_installed' });
459
459
  // Auth first: a key lets the config carry an X-API-Key header, removing the sign-in hand-back.
460
- state.stage = "authentication";
461
- ensureCliAuthenticated("cursor", options.apiKey);
462
- telemetry.track("checkpoint", { phase: "authenticated" });
460
+ state.stage = 'authentication';
461
+ ensureCliAuthenticated('cursor', options.apiKey);
462
+ telemetry.track('checkpoint', { phase: 'authenticated' });
463
463
  const resolvedKey = validatedApiKey(options.apiKey);
464
464
  // ensureCliAuthenticated leaves a key stored, and `auth status` exits 0 on a malformed one.
465
465
  if (!resolvedKey) {
466
- errLine("Ignoring the stored API key: invalid format. Run: tinyfish auth login");
466
+ errLine('Ignoring the stored API key: invalid format. Run: tinyfish auth login');
467
467
  }
468
- state.stage = "registration";
469
- errLine("Adding TinyFish to Cursor...");
468
+ state.stage = 'registration';
469
+ errLine('Adding TinyFish to Cursor...');
470
470
  const mcpUrl = new URL(options.mcpUrl);
471
- mcpUrl.searchParams.set("source", CONNECT_SOURCE);
472
- mcpUrl.searchParams.set("client", "cursor");
473
- mcpUrl.searchParams.set("connect_attempt_id", telemetry.attemptId);
471
+ mcpUrl.searchParams.set('source', CONNECT_SOURCE);
472
+ mcpUrl.searchParams.set('client', 'cursor');
473
+ mcpUrl.searchParams.set('connect_attempt_id', telemetry.attemptId);
474
474
  const result = writeCursorMcpConfig(mcpUrl.toString(), resolvedKey);
475
- if (result.status === "corrupt_skip") {
475
+ if (result.status === 'corrupt_skip') {
476
476
  throw new ConnectStepError(`Could not update ${cursorMcpPath()}: existing file could not be read or is not valid JSON (${result.error}). ` +
477
- `Fix the file, then re-run: tinyfish connect cursor`, "invalid_config");
477
+ `Fix the file, then re-run: tinyfish connect cursor`, 'invalid_config');
478
478
  }
479
479
  if (result.backupPath) {
480
480
  errLine(`Backed up existing Cursor MCP config to ${result.backupPath}`);
481
481
  }
482
482
  if (result.repaired) {
483
- errLine("Repaired the existing TinyFish entry (updated auth/config to current).");
483
+ errLine('Repaired the existing TinyFish entry (updated auth/config to current).');
484
484
  }
485
- telemetry.track("checkpoint", { phase: "registered" });
486
- saveConnectContext("cursor", telemetry.attemptId);
485
+ telemetry.track('checkpoint', { phase: 'registered' });
486
+ saveConnectContext('cursor', telemetry.attemptId);
487
487
  if (resolvedKey) {
488
488
  // Deeplink would embed the key in a URL (process args, LaunchServices logs) — reload instead.
489
489
  const verify = await verifyMcpAuth(resolvedKey);
@@ -491,17 +491,17 @@ export async function connectCursor(options) {
491
491
  errLine("TinyFish is connected and verified (health+auth). Reload the Cursor window (or restart cursor-agent) and it's live.");
492
492
  }
493
493
  else {
494
- errLine(`TinyFish is configured, but the authenticated check failed (${verify.reason ?? "unknown"}). ` +
495
- "Fix: rotate/re-enter your key with `tinyfish auth login`, then re-run: tinyfish connect cursor");
494
+ errLine(`TinyFish is configured, but the authenticated check failed (${verify.reason ?? 'unknown'}). ` +
495
+ 'Fix: rotate/re-enter your key with `tinyfish auth login`, then re-run: tinyfish connect cursor');
496
496
  }
497
497
  }
498
498
  else if (detectHumanInitiated() && launchCursorDeeplink(mcpUrl.toString())) {
499
- errLine("Cursor should pop a TinyFish install confirmation — approve it, then sign in when prompted.");
499
+ errLine('Cursor should pop a TinyFish install confirmation — approve it, then sign in when prompted.');
500
500
  }
501
501
  else {
502
- errLine("TinyFish is connected. Reload the Cursor window, then approve/sign in under Settings → MCP.");
502
+ errLine('TinyFish is connected. Reload the Cursor window, then approve/sign in under Settings → MCP.');
503
503
  }
504
- settle(state, telemetry, "completed", { authMode: resolvedKey ? AuthMode.ApiKey : "deferred" });
504
+ settle(state, telemetry, 'completed', { authMode: resolvedKey ? AuthMode.ApiKey : 'deferred' });
505
505
  });
506
506
  }
507
507
  /** Best-effort: false when no handler/open fails — caller falls back to reload copy. */
@@ -511,7 +511,7 @@ function launchCursorDeeplink(mcpUrl) {
511
511
  }
512
512
  // A new client fails to compile until wired here.
513
513
  const AGENT_LAUNCHERS = {
514
- "claude-code": () => launchNativeMcpClient(CLAUDE_CODE),
514
+ 'claude-code': () => launchNativeMcpClient(CLAUDE_CODE),
515
515
  codex: () => launchNativeMcpClient(CODEX),
516
516
  grok: () => launchNativeMcpClient(GROK),
517
517
  hermes: () => launchNativeMcpClient(HERMES),
@@ -519,7 +519,7 @@ const AGENT_LAUNCHERS = {
519
519
  openclaw: launchOpenClawWalkthrough,
520
520
  // Cursor has no chat CLI to launch.
521
521
  cursor: () => {
522
- throw new Error("Cursor has no launchable walkthrough. Open Cursor and start chatting.");
522
+ throw new Error('Cursor has no launchable walkthrough. Open Cursor and start chatting.');
523
523
  },
524
524
  };
525
525
  export async function launchAgent(client) {
@@ -527,7 +527,7 @@ export async function launchAgent(client) {
527
527
  }
528
528
  // A new client fails to compile until wired here.
529
529
  const CLIENT_CONNECTORS = {
530
- "claude-code": connectClaudeCode,
530
+ 'claude-code': connectClaudeCode,
531
531
  codex: connectCodex,
532
532
  grok: connectGrok,
533
533
  hermes: connectHermes,
@@ -537,29 +537,29 @@ const CLIENT_CONNECTORS = {
537
537
  cursor: async (options) => {
538
538
  await connectCursor(options);
539
539
  if (options.launch) {
540
- errLine("Cursor has no launchable walkthrough. Open Cursor and start chatting.");
540
+ errLine('Cursor has no launchable walkthrough. Open Cursor and start chatting.');
541
541
  }
542
542
  },
543
543
  };
544
544
  export function registerConnect(program) {
545
545
  program
546
- .command("connect")
547
- .description("Connect TinyFish to an AI agent")
548
- .argument("[client]", "Agent client to connect (claude-code, codex, cursor, grok, hermes, openclaw, or opencode); omit to pick from the agents detected on this machine")
549
- .option("--all", "Detect and connect every supported harness found on this machine")
550
- .option("--dry-run", "Print planned writes per harness without touching anything (--all only)")
551
- .option("--uninstall", "Remove TinyFish entries written by connect --all")
552
- .option("--skip-launch", "Skip the end-of-install agent launch (--all only)")
553
- .option("--api-key <apiKey>", "TinyFish API key to persist for the installed skill")
554
- .option("--launch", "Launch the agent and start the TinyFish walkthrough")
555
- .option("--url <mcpUrl>", "MCP endpoint override")
556
- .option("--verbose", "Print the full npm and skills output instead of only on failure")
546
+ .command('connect')
547
+ .description('Connect TinyFish to an AI agent')
548
+ .argument('[client]', 'Agent client to connect (claude-code, codex, cursor, grok, hermes, openclaw, or opencode); omit to pick from the agents detected on this machine')
549
+ .option('--all', 'Detect and connect every supported harness found on this machine')
550
+ .option('--dry-run', 'Print planned writes per harness without touching anything (--all only)')
551
+ .option('--uninstall', 'Remove TinyFish entries written by connect --all')
552
+ .option('--skip-launch', 'Skip the end-of-install agent launch (--all only)')
553
+ .option('--api-key <apiKey>', 'TinyFish API key to persist for the installed skill')
554
+ .option('--launch', 'Launch the agent and start the TinyFish walkthrough')
555
+ .option('--url <mcpUrl>', 'MCP endpoint override')
556
+ .option('--verbose', 'Print the full npm and skills output instead of only on failure')
557
557
  .action(async (client, options) => {
558
558
  if (options.apiKey && !validateKeyFormat(options.apiKey)) {
559
- throw new Error("Invalid --api-key value");
559
+ throw new Error('Invalid --api-key value');
560
560
  }
561
561
  let mcpUrl = options.url ?? DEFAULT_MCP_URL;
562
- if (mcpUrl.startsWith("-")) {
562
+ if (mcpUrl.startsWith('-')) {
563
563
  throw new Error(`Invalid --url value: ${mcpUrl}`);
564
564
  }
565
565
  let parsedUrl;
@@ -569,25 +569,25 @@ export function registerConnect(program) {
569
569
  catch {
570
570
  throw new Error(`Invalid --url value: ${mcpUrl}`);
571
571
  }
572
- if (!["http:", "https:"].includes(parsedUrl.protocol)) {
572
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
573
573
  throw new Error(`Invalid --url value: ${mcpUrl}`);
574
574
  }
575
575
  // Page-minted attempt id rides in on --url. Strip before the URL becomes the MCP
576
576
  // base — registration re-stamps it, and nothing else may carry it.
577
- const pageAttemptId = parsedUrl.searchParams.get("connect_attempt_id");
577
+ const pageAttemptId = parsedUrl.searchParams.get('connect_attempt_id');
578
578
  let attemptId;
579
579
  if (pageAttemptId !== null) {
580
- parsedUrl.searchParams.delete("connect_attempt_id");
580
+ parsedUrl.searchParams.delete('connect_attempt_id');
581
581
  mcpUrl = parsedUrl.toString();
582
582
  if (z.uuid().safeParse(pageAttemptId).success)
583
583
  attemptId = pageAttemptId;
584
584
  }
585
585
  if (options.all) {
586
586
  if (client) {
587
- throw new Error("Pass either a client or --all, not both.");
587
+ throw new Error('Pass either a client or --all, not both.');
588
588
  }
589
589
  if (options.launch) {
590
- throw new Error("--launch is not supported with --all. Connect a single client to launch it.");
590
+ throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
591
591
  }
592
592
  const exitCode = await runConnectAll({
593
593
  apiKey: options.apiKey,
@@ -603,15 +603,15 @@ export function registerConnect(program) {
603
603
  return;
604
604
  }
605
605
  if (options.dryRun || options.uninstall || options.skipLaunch) {
606
- throw new Error("--dry-run, --uninstall and --skip-launch are only supported with --all.");
606
+ throw new Error('--dry-run, --uninstall and --skip-launch are only supported with --all.');
607
607
  }
608
608
  if (!client) {
609
609
  if (options.launch) {
610
- throw new Error("--launch is not supported with --all. Connect a single client to launch it.");
610
+ throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
611
611
  }
612
612
  const humanInitiated = detectHumanInitiated();
613
613
  if (!humanInitiated) {
614
- errLine("No terminal detected: connecting every detected harness (same as tinyfish connect --all).");
614
+ errLine('No terminal detected: connecting every detected harness (same as tinyfish connect --all).');
615
615
  }
616
616
  const exitCode = await runConnectAll({
617
617
  apiKey: options.apiKey,
@@ -642,9 +642,9 @@ export function registerConnect(program) {
642
642
  await connector(connectOptions);
643
643
  }
644
644
  else {
645
- throw new Error("Unsupported client: " +
645
+ throw new Error('Unsupported client: ' +
646
646
  client +
647
- ". Supported clients: claude-code, codex, cursor, grok, hermes, openclaw, opencode");
647
+ '. Supported clients: claude-code, codex, cursor, grok, hermes, openclaw, opencode');
648
648
  }
649
649
  // Deferred to here so the notice lands after install output, not interleaved with it.
650
650
  emitNotice();