@tiny-fish/cli 0.30.2-next.256 → 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 +18 -9
@@ -1,33 +1,37 @@
1
- import * as readline from "node:readline/promises";
2
- import spawn from "cross-spawn";
3
- import { connectClaudeCode, connectCodex, connectCursor, connectGrok, connectHermes, connectOpenClaw, connectOpencode, } from "../commands/connect.js";
4
- import { clearConnectContext, connectContextState, loadConfig, validatedApiKey, } from "./auth.js";
5
- import { installTinyFishCli, } from "./cli-install.js";
6
- import { HARNESS_CAPABILITIES, NATIVE_BY_HARNESS, openclawSkillUninstall, } from "./connect-clients.js";
7
- import { createStdinPrompt, runCliFallback } from "./connect-fallback.js";
8
- import { ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, spawnRemoval, } from "./connect-runtime.js";
9
- import { pickHarnesses } from "./connect-picker.js";
10
- import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, RELOAD_ACTION, } from "./harness-detect.js";
11
- import { detectHumanInitiated } from "./harness.js";
12
- import { emitNotice } from "./notice.js";
13
- import { removeCursorMcpServer, planCursorWrite } from "./cursor-config.js";
14
- import { errLine, outLine, setErrIndent } from "./output.js";
15
- import { sendSetupCompleted } from "./setup-telemetry.js";
16
- import { verifyMcpAuth, verifyMcpHealth } from "./verify.js";
1
+ import * as readline from 'node:readline/promises';
2
+ import spawn from 'cross-spawn';
3
+ import { connectClaudeCode, connectCodex, connectCursor, connectGrok, connectHermes, connectOpenClaw, connectOpencode, } from '../commands/connect.js';
4
+ import { clearConnectContext, connectContextState, loadConfig, validatedApiKey } from './auth.js';
5
+ import { installTinyFishCli, } from './cli-install.js';
6
+ import { HARNESS_CAPABILITIES, NATIVE_BY_HARNESS, openclawSkillUninstall, } from './connect-clients.js';
7
+ import { createStdinPrompt, runCliFallback } from './connect-fallback.js';
8
+ import { ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, spawnRemoval, } from './connect-runtime.js';
9
+ import { pickHarnesses } from './connect-picker.js';
10
+ import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, RELOAD_ACTION, } from './harness-detect.js';
11
+ import { detectHumanInitiated } from './harness.js';
12
+ import { emitNotice } from './notice.js';
13
+ import { removeCursorMcpServer, planCursorWrite } from './cursor-config.js';
14
+ import { errLine, outLine, setErrIndent } from './output.js';
15
+ import { sendSetupCompleted } from './setup-telemetry.js';
16
+ import { verifyMcpAuth, verifyMcpHealth } from './verify.js';
17
17
  // A probe that exits non-zero is an install so old the subcommand is missing entirely.
18
18
  const TOO_OLD_REASONS = new Set([
19
- "harness_too_old",
20
- "harness_command_unsupported",
19
+ 'harness_too_old',
20
+ 'harness_command_unsupported',
21
21
  ]);
22
22
  // These failures land before any browser could have opened.
23
23
  const NO_BROWSER_REASONS = new Set([
24
24
  ...TOO_OLD_REASONS,
25
- "harness_not_installed",
26
- "command_not_found",
25
+ 'harness_not_installed',
26
+ 'command_not_found',
27
27
  ]);
28
28
  // Naming a harness the user does not have is the noise PF-3708 cut.
29
- const HIDDEN_ONCE_SOMETHING_WORKED = new Set(["deselected", "not_detected", "not_installed"]);
30
- const STEP_INDENT = " ";
29
+ const HIDDEN_ONCE_SOMETHING_WORKED = new Set([
30
+ 'deselected',
31
+ 'not_detected',
32
+ 'not_installed',
33
+ ]);
34
+ const STEP_INDENT = ' ';
31
35
  // Must undercut agent tool-call budgets (Claude Code Bash defaults to 120s).
32
36
  const OAUTH_SIGN_IN_TIMEOUT_MS = 90_000;
33
37
  // Derived: these block `connect` on a browser sign-in when keyless.
@@ -38,10 +42,10 @@ const BROWSER_SIGN_IN = new Set(ALL_HARNESSES.filter((harness) => {
38
42
  // Derived: a native add carries the key, or the CLI holds it itself.
39
43
  const KEY_AUTH_CAPABLE = new Set(ALL_HARNESSES.filter((harness) => !!NATIVE_BY_HARNESS.get(harness)?.keyAuth || HARNESS_CAPABILITIES[harness].keyHeldByCli));
40
44
  // OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
41
- const OPENCODE_UNINSTALL_POINTER = "remove the tinyfish entry from ~/.config/opencode/opencode.json";
45
+ const OPENCODE_UNINSTALL_POINTER = 'remove the tinyfish entry from ~/.config/opencode/opencode.json';
42
46
  // A new harness fails to compile until wired here.
43
47
  const HARNESS_CONNECTORS = {
44
- "claude-code": connectClaudeCode,
48
+ 'claude-code': connectClaudeCode,
45
49
  codex: connectCodex,
46
50
  grok: connectGrok,
47
51
  hermes: connectHermes,
@@ -90,17 +94,17 @@ async function verifyHarness(harness, mcpUrl, keyed, apiKey) {
90
94
  if (!capable || !health.ok)
91
95
  return health;
92
96
  if (!apiKey)
93
- return { depth: "health+auth", ok: false, reason: "no stored API key" };
97
+ return { depth: 'health+auth', ok: false, reason: 'no stored API key' };
94
98
  return verifyMcpAuth(apiKey);
95
99
  }
96
100
  function planText(harness, mcpUrl, apiKey) {
97
- if (harness === "cursor")
101
+ if (harness === 'cursor')
98
102
  return planCursorWrite(mcpUrl, apiKey);
99
103
  return `would run \`tinyfish connect ${harness}\` (harness-owned MCP write; no local file touched by the CLI)`;
100
104
  }
101
105
  function uninstallPlanText(harness) {
102
- if (harness === "cursor") {
103
- return "would remove the tinyfish entry from ~/.cursor/mcp.json";
106
+ if (harness === 'cursor') {
107
+ return 'would remove the tinyfish entry from ~/.cursor/mcp.json';
104
108
  }
105
109
  if (!uninstallRuns(harness)) {
106
110
  return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
@@ -109,7 +113,7 @@ function uninstallPlanText(harness) {
109
113
  }
110
114
  /** Undefined: no harness-CLI removal exists (opencode). */
111
115
  function uninstallRuns(harness) {
112
- if (harness === "openclaw") {
116
+ if (harness === 'openclaw') {
113
117
  const removal = openclawSkillUninstall();
114
118
  return { command: removal.command, runs: [removal.args], timeoutMs: removal.timeoutMs };
115
119
  }
@@ -125,43 +129,43 @@ function uninstallPointer(harness) {
125
129
  return OPENCODE_UNINSTALL_POINTER;
126
130
  // `;` runs every removal but breaks cmd.exe; `&&` is the reverse — see
127
131
  // internal-docs/install-command-shell-quirks.md (PS 5.1 stays a known gap).
128
- const joiner = process.platform === "win32" ? " && " : " ; ";
129
- return removal.runs.map((args) => `${removal.command} ${args.join(" ")}`).join(joiner);
132
+ const joiner = process.platform === 'win32' ? ' && ' : ' ; ';
133
+ return removal.runs.map((args) => `${removal.command} ${args.join(' ')}`).join(joiner);
130
134
  }
131
135
  function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
132
136
  const detection = { harness, detected: true, configPath, installed: false };
133
- if (harness !== "cursor") {
137
+ if (harness !== 'cursor') {
134
138
  const removal = uninstallRuns(harness);
135
139
  if (!removal) {
136
- return { ...detection, outcome: "uninstall_pointer", fixCommand: uninstallPointer(harness) };
140
+ return { ...detection, outcome: 'uninstall_pointer', fixCommand: uninstallPointer(harness) };
137
141
  }
138
142
  // Stale config: the harness binary is gone, so its removal command cannot run.
139
143
  // openclaw is exempt — its removal runs through npx, not the harness binary.
140
- if (harness !== "openclaw" && !binaryOnPath) {
144
+ if (harness !== 'openclaw' && !binaryOnPath) {
141
145
  return {
142
146
  ...detection,
143
- outcome: "uninstall_pointer",
147
+ outcome: 'uninstall_pointer',
144
148
  errorMessage: `no \`${removal.command}\` binary on PATH — nothing to run.`,
145
149
  fixCommand: `Reinstall ${DISPLAY_NAMES[harness]} first, then: ${uninstallPointer(harness)}`,
146
150
  };
147
151
  }
148
152
  // Absent marker = never connected here, so a non-zero removal exit is a no-op.
149
153
  // Unreadable config cannot prove that, so it keeps the pointer.
150
- const connectedBefore = connectContextState(harness) !== "absent";
154
+ const connectedBefore = connectContextState(harness) !== 'absent';
151
155
  let removed = false;
152
156
  try {
153
157
  for (const args of removal.runs) {
154
158
  const result = spawnRemoval(removal.command, args, removal.timeoutMs);
155
159
  if (result.status !== 0 && verbose) {
156
160
  const details = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
157
- errLine(`${removal.command} ${args.join(" ")}: ${details}`);
161
+ errLine(`${removal.command} ${args.join(' ')}: ${details}`);
158
162
  }
159
163
  // Same binary: a spawn-level failure (ENOENT, EPERM) repeats identically.
160
164
  if (result.error) {
161
165
  return {
162
166
  ...detection,
163
- outcome: "uninstalled",
164
- errorMessage: `could not run \`${removal.command} ${args.join(" ")}\` (${result.error.message}) — nothing removed.`,
167
+ outcome: 'uninstalled',
168
+ errorMessage: `could not run \`${removal.command} ${args.join(' ')}\` (${result.error.message}) — nothing removed.`,
165
169
  fixCommand: uninstallPointer(harness),
166
170
  };
167
171
  }
@@ -174,8 +178,8 @@ function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
174
178
  if (e instanceof ConnectInterruptedError) {
175
179
  return {
176
180
  ...detection,
177
- outcome: "interrupted",
178
- fixCommand: "tinyfish connect --all --uninstall",
181
+ outcome: 'interrupted',
182
+ fixCommand: 'tinyfish connect --all --uninstall',
179
183
  };
180
184
  }
181
185
  throw e;
@@ -183,14 +187,14 @@ function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
183
187
  if (removed) {
184
188
  // Kept marker makes doctor emit a registration-lost repair that undoes this.
185
189
  clearConnectContext(harness);
186
- return { ...detection, outcome: "uninstalled" };
190
+ return { ...detection, outcome: 'uninstalled' };
187
191
  }
188
192
  if (!connectedBefore)
189
- return { ...detection, outcome: "uninstall_noop" };
193
+ return { ...detection, outcome: 'uninstall_noop' };
190
194
  return {
191
195
  ...detection,
192
- outcome: "uninstall_pointer",
193
- errorMessage: "could not confirm removal (command exited non-zero; there may have been nothing to remove).",
196
+ outcome: 'uninstall_pointer',
197
+ errorMessage: 'could not confirm removal (command exited non-zero; there may have been nothing to remove).',
194
198
  fixCommand: `Retry by hand: ${uninstallPointer(harness)}`,
195
199
  };
196
200
  }
@@ -199,27 +203,27 @@ function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
199
203
  result = removeCursorMcpServer();
200
204
  }
201
205
  catch (e) {
202
- result = { status: "corrupt_skip", error: e instanceof Error ? e.message : String(e) };
206
+ result = { status: 'corrupt_skip', error: e instanceof Error ? e.message : String(e) };
203
207
  }
204
- if (result.status === "corrupt_skip") {
208
+ if (result.status === 'corrupt_skip') {
205
209
  return {
206
210
  ...detection,
207
- outcome: "uninstalled",
211
+ outcome: 'uninstalled',
208
212
  errorMessage: `could not update Cursor's mcp.json (${result.error}) — nothing removed.`,
209
- fixCommand: "Fix ~/.cursor/mcp.json by hand, then re-run: tinyfish connect --all --uninstall",
213
+ fixCommand: 'Fix ~/.cursor/mcp.json by hand, then re-run: tinyfish connect --all --uninstall',
210
214
  };
211
215
  }
212
- if (result.status === "unchanged") {
213
- return { ...detection, outcome: "uninstall_noop" };
216
+ if (result.status === 'unchanged') {
217
+ return { ...detection, outcome: 'uninstall_noop' };
214
218
  }
215
- clearConnectContext("cursor");
216
- return { ...detection, outcome: "uninstalled" };
219
+ clearConnectContext('cursor');
220
+ return { ...detection, outcome: 'uninstalled' };
217
221
  }
218
222
  async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliInstall, deferOutro) {
219
223
  const { harness, configPath, detected } = detection;
220
224
  const base = { harness, detected, configPath, installed: false };
221
225
  const noWork = noWorkOutcome(detection, opts.uninstall);
222
- if (noWork === "not_detected")
226
+ if (noWork === 'not_detected')
223
227
  return { ...base, outcome: noWork };
224
228
  if (noWork)
225
229
  return { ...base, outcome: noWork, fixCommand: `tinyfish connect ${harness}` };
@@ -228,7 +232,7 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
228
232
  const plan = opts.uninstall
229
233
  ? uninstallPlanText(harness)
230
234
  : planText(harness, opts.mcpUrl, validatedApiKey(opts.apiKey));
231
- return { ...base, outcome: "dry_run", fixCommand: plan };
235
+ return { ...base, outcome: 'dry_run', fixCommand: plan };
232
236
  }
233
237
  if (opts.uninstall)
234
238
  return uninstallHarness(harness, configPath, opts.verbose ?? false, detection.binaryOnPath);
@@ -242,12 +246,12 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
242
246
  if (!isTTY && !keyed) {
243
247
  // A CLI-login sign-in is interactive-only — unboundable headless.
244
248
  if (HARNESS_CAPABILITIES[harness].signInViaCliLogin) {
245
- return { ...base, outcome: "auth_pending", fixCommand };
249
+ return { ...base, outcome: 'auth_pending', fixCommand };
246
250
  }
247
251
  if (BROWSER_SIGN_IN.has(harness)) {
248
252
  // One browser sign-in per run; the rest wait for a re-run.
249
253
  if (oauthBudget.spent)
250
- return { ...base, outcome: "auth_pending", fixCommand };
254
+ return { ...base, outcome: 'auth_pending', fixCommand };
251
255
  oauthBudget.spent = true;
252
256
  spentBrowserSlot = true;
253
257
  errLine(`${DISPLAY_NAMES[harness]} needs a browser sign-in — finish it when your browser ` +
@@ -263,11 +267,11 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
263
267
  catch (error) {
264
268
  // Ctrl+C stops the run; Ctrl+D only skips this harness.
265
269
  if (error instanceof ConnectInterruptedError) {
266
- errLine("Setup interrupted — run the command again to finish.");
270
+ errLine('Setup interrupted — run the command again to finish.');
267
271
  process.exitCode = 130;
268
- return { ...base, outcome: "interrupted", fixCommand };
272
+ return { ...base, outcome: 'interrupted', fixCommand };
269
273
  }
270
- return { ...base, outcome: "no_tty_auth_skip", fixCommand };
274
+ return { ...base, outcome: 'no_tty_auth_skip', fixCommand };
271
275
  }
272
276
  }
273
277
  // runGuarded swallows Ctrl+C and sets exitCode 130 instead of throwing.
@@ -284,45 +288,47 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
284
288
  if (e instanceof SignInTimeoutError) {
285
289
  return {
286
290
  ...base,
287
- outcome: "auth_pending",
288
- errorMessage: "sign-in did not finish in time",
291
+ outcome: 'auth_pending',
292
+ errorMessage: 'sign-in did not finish in time',
289
293
  fixCommand,
290
294
  };
291
295
  }
292
296
  // A pre-browser failure must not burn the run's only browser slot.
293
- if (spentBrowserSlot && e instanceof ConnectStepError && NO_BROWSER_REASONS.has(e.failureReason)) {
297
+ if (spentBrowserSlot &&
298
+ e instanceof ConnectStepError &&
299
+ NO_BROWSER_REASONS.has(e.failureReason)) {
294
300
  oauthBudget.spent = false;
295
301
  }
296
302
  // Detection saw a name on PATH; the probe proved nothing is behind it.
297
- if (e instanceof ConnectStepError && e.failureReason === "harness_not_installed") {
298
- return { ...base, detected: false, outcome: "not_installed" };
303
+ if (e instanceof ConnectStepError && e.failureReason === 'harness_not_installed') {
304
+ return { ...base, detected: false, outcome: 'not_installed' };
299
305
  }
300
306
  // Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
301
307
  if (e instanceof ConnectStepError && TOO_OLD_REASONS.has(e.failureReason)) {
302
308
  return {
303
309
  ...base,
304
- outcome: "harness_too_old",
310
+ outcome: 'harness_too_old',
305
311
  errorMessage: e.message,
306
312
  fixCommand: `tinyfish connect ${harness}`,
307
313
  };
308
314
  }
309
315
  return {
310
316
  ...base,
311
- outcome: "failed",
317
+ outcome: 'failed',
312
318
  errorMessage: e instanceof Error ? e.message : String(e),
313
319
  fixCommand: `tinyfish connect ${harness}`,
314
320
  };
315
321
  }
316
322
  if (!interruptedBefore && process.exitCode === 130) {
317
- return { ...base, outcome: "interrupted", fixCommand: `tinyfish connect ${harness}` };
323
+ return { ...base, outcome: 'interrupted', fixCommand: `tinyfish connect ${harness}` };
318
324
  }
319
325
  // The probe, not the flag, decides: a harness that degraded to OAuth is not key-authed.
320
- const keyAuthed = authMode ? authMode === "api-key" : keyed;
326
+ const keyAuthed = authMode ? authMode === 'api-key' : keyed;
321
327
  const verify = await verifyHarness(harness, opts.mcpUrl, keyAuthed, apiKey);
322
328
  return {
323
329
  ...base,
324
330
  installed: true,
325
- outcome: "installed",
331
+ outcome: 'installed',
326
332
  verifyDepth: verify.depth,
327
333
  verifyOk: verify.ok,
328
334
  verifyReason: verify.reason,
@@ -332,56 +338,56 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
332
338
  }
333
339
  function summaryLine(result, uninstall) {
334
340
  const name = DISPLAY_NAMES[result.harness];
335
- if (result.outcome === "not_installed") {
341
+ if (result.outcome === 'not_installed') {
336
342
  const next = uninstall
337
- ? "nothing to remove"
343
+ ? 'nothing to remove'
338
344
  : `Install ${name}, then run: tinyfish connect ${result.harness}`;
339
345
  return `- ${name} — not installed (\`${HARNESS_COMMANDS[result.harness]}\` is on PATH but does not run). ${next}`;
340
346
  }
341
347
  if (!result.detected) {
342
348
  const next = uninstall
343
- ? "nothing to remove"
349
+ ? 'nothing to remove'
344
350
  : `Install ${name}, then run: tinyfish connect ${result.harness}`;
345
351
  return `- ${name} — not detected (no \`${HARNESS_COMMANDS[result.harness]}\` on PATH, no ${result.configPath}). ${next}`;
346
352
  }
347
- if (result.outcome === "stale_config") {
353
+ if (result.outcome === 'stale_config') {
348
354
  return `… ${name} — config at ${result.configPath} but no \`${HARNESS_COMMANDS[result.harness]}\` on PATH, skipped. Install ${name}, then run: ${result.fixCommand}`;
349
355
  }
350
- if (result.outcome === "not_reached") {
356
+ if (result.outcome === 'not_reached') {
351
357
  return `… ${name} — not reached, the run stopped early. Fix: ${result.fixCommand}`;
352
358
  }
353
- if (result.outcome === "dry_run") {
359
+ if (result.outcome === 'dry_run') {
354
360
  return `… ${name} — ${result.fixCommand}`;
355
361
  }
356
- if (result.outcome === "no_tty_auth_skip") {
362
+ if (result.outcome === 'no_tty_auth_skip') {
357
363
  return `… ${name} — skipped, no stored credentials and no terminal to sign in. Fix: ${result.fixCommand}`;
358
364
  }
359
- if (result.outcome === "auth_pending") {
365
+ if (result.outcome === 'auth_pending') {
360
366
  return result.errorMessage
361
367
  ? `… ${name} — ${result.errorMessage}. Finish it in a terminal: ${result.fixCommand}`
362
368
  : `… ${name} — needs a browser sign-in. Run in a terminal: ${result.fixCommand}`;
363
369
  }
364
- if (result.outcome === "uninstall_pointer") {
370
+ if (result.outcome === 'uninstall_pointer') {
365
371
  // errorMessage set = a removal was attempted or impossible; absent = no removal command exists.
366
372
  return result.errorMessage
367
373
  ? `… ${name} — ${result.errorMessage} ${result.fixCommand}`
368
374
  : `… ${name} — not removed automatically (config is owned by ${name}). Run: ${result.fixCommand}`;
369
375
  }
370
- if (result.outcome === "uninstall_noop") {
376
+ if (result.outcome === 'uninstall_noop') {
371
377
  return `… ${name} — no tinyfish entry found, nothing removed`;
372
378
  }
373
- if (result.outcome === "uninstalled") {
379
+ if (result.outcome === 'uninstalled') {
374
380
  return result.errorMessage
375
381
  ? `✗ ${name} — ${result.errorMessage} Fix: ${result.fixCommand}`
376
382
  : `✓ ${name} — removed`;
377
383
  }
378
- if (result.outcome === "interrupted") {
384
+ if (result.outcome === 'interrupted') {
379
385
  return `✗ ${name} — interrupted before finishing. Fix: ${result.fixCommand}`;
380
386
  }
381
- if (result.outcome === "harness_too_old") {
387
+ if (result.outcome === 'harness_too_old') {
382
388
  return `… ${name} — needs a newer ${name}: ${result.errorMessage} Fix, once upgraded: ${result.fixCommand}`;
383
389
  }
384
- if (result.outcome === "failed") {
390
+ if (result.outcome === 'failed') {
385
391
  return `✗ ${name} — install failed: ${result.errorMessage}. Fix: ${result.fixCommand}`;
386
392
  }
387
393
  // The summary is what users read; stderr hints scroll away.
@@ -391,11 +397,11 @@ function summaryLine(result, uninstall) {
391
397
  }
392
398
  // A keyed install has no sign-in left, so the deferred-auth warning would contradict it.
393
399
  const authDeferred = HARNESS_CAPABILITIES[result.harness].authDeferredAtInstall && !result.keyAuthed;
394
- const verifyLabel = result.verifyDepth ? `verified: ${result.verifyDepth}` : "not verified";
400
+ const verifyLabel = result.verifyDepth ? `verified: ${result.verifyDepth}` : 'not verified';
395
401
  if (authDeferred || result.verifyOk === false) {
396
402
  const reason = authDeferred
397
- ? "one step left: sign in when your agent first runs"
398
- : `verify warning (${verifyLabel}): ${result.verifyReason ?? "unknown"}`;
403
+ ? 'one step left: sign in when your agent first runs'
404
+ : `verify warning (${verifyLabel}): ${result.verifyReason ?? 'unknown'}`;
399
405
  return `⚠ ${name} — installed, ${reason}. Fix: run \`/tinyfish:doctor\` in ${name} once it's open.`;
400
406
  }
401
407
  return `✓ ${name} — installed, ${verifyLabel}`;
@@ -411,29 +417,29 @@ function reloadHint(results) {
411
417
  }
412
418
  if (byAction.size === 0)
413
419
  return undefined;
414
- const parts = [...byAction].map(([action, names]) => `${names.join(", ")} — ${action}`);
415
- return `TinyFish tools load when a harness starts. If one is already open: ${parts.join("; ")}.`;
420
+ const parts = [...byAction].map(([action, names]) => `${names.join(', ')} — ${action}`);
421
+ return `TinyFish tools load when a harness starts. If one is already open: ${parts.join('; ')}.`;
416
422
  }
417
423
  /** Absent and unpicked harnesses are noise once something real happened. */
418
424
  function renderSummary(results, uninstall) {
419
425
  if (!results.some(actionable))
420
426
  return results.map((r) => summaryLine(r, uninstall));
421
427
  // Ids, not display names: they double as the `connect` argument.
422
- const skipped = results.filter((r) => r.outcome === "deselected").map((r) => r.harness);
428
+ const skipped = results.filter((r) => r.outcome === 'deselected').map((r) => r.harness);
423
429
  const lines = results
424
430
  .filter((r) => !HIDDEN_ONCE_SOMETHING_WORKED.has(r.outcome))
425
431
  .map((r) => summaryLine(r, uninstall));
426
432
  if (skipped.length === 0)
427
433
  return lines;
428
- lines.push("", `Skipped (not selected): ${skipped.join(", ")}`);
434
+ lines.push('', `Skipped (not selected): ${skipped.join(', ')}`);
429
435
  return lines;
430
436
  }
431
437
  /** The states processHarness settles without work; stepTotal must agree with it. */
432
438
  function noWorkOutcome(d, uninstall) {
433
439
  if (!d.detected)
434
- return "not_detected";
440
+ return 'not_detected';
435
441
  if (!uninstall && isStaleConfig(d))
436
- return "stale_config";
442
+ return 'stale_config';
437
443
  return undefined;
438
444
  }
439
445
  /** A config-writing connect needs no binary; every other harness spawns one. */
@@ -444,26 +450,26 @@ function isStaleConfig(detection) {
444
450
  }
445
451
  /** A stale config dir is a detection signal with nothing behind it. */
446
452
  function actionable(result) {
447
- return result.detected && result.outcome !== "stale_config";
453
+ return result.detected && result.outcome !== 'stale_config';
448
454
  }
449
455
  function computeExitCode(results) {
450
456
  // Flag-independent: uninstall interrupts have no other 130 site.
451
- if (results.some((r) => r.outcome === "interrupted"))
457
+ if (results.some((r) => r.outcome === 'interrupted'))
452
458
  return 130;
453
459
  // A removal that could not run is a failure, not a silent success.
454
- if (results.some((r) => r.outcome === "uninstalled" && r.errorMessage))
460
+ if (results.some((r) => r.outcome === 'uninstalled' && r.errorMessage))
455
461
  return 1;
456
462
  const detected = results.filter(actionable);
457
463
  if (detected.length === 0)
458
464
  return 1;
459
465
  // Only real install attempts count toward the success/failure denominator.
460
- const attempted = detected.filter((r) => r.outcome === "installed" ||
461
- r.outcome === "failed" ||
462
- r.outcome === "harness_too_old" ||
463
- r.outcome === "interrupted");
466
+ const attempted = detected.filter((r) => r.outcome === 'installed' ||
467
+ r.outcome === 'failed' ||
468
+ r.outcome === 'harness_too_old' ||
469
+ r.outcome === 'interrupted');
464
470
  if (attempted.length === 0)
465
471
  return 0;
466
- return attempted.some((r) => r.outcome === "installed") ? 0 : 1;
472
+ return attempted.some((r) => r.outcome === 'installed') ? 0 : 1;
467
473
  }
468
474
  export async function runConnectAll(opts) {
469
475
  const isTTY = detectHumanInitiated();
@@ -476,7 +482,7 @@ export async function runConnectAll(opts) {
476
482
  detections.sort((a, b) => Number(KEY_AUTH_CAPABLE.has(b.harness)) - Number(KEY_AUTH_CAPABLE.has(a.harness)));
477
483
  }
478
484
  let deselected = new Set();
479
- let mode = "all";
485
+ let mode = 'all';
480
486
  // Before willAttempt/cliNeeded: a cursor-only pick must not hoist the CLI.
481
487
  // A stale config dir has no binary to connect, so never offer it as a choice.
482
488
  const offerable = detections.filter((d) => d.detected && !isStaleConfig(d));
@@ -484,9 +490,9 @@ export async function runConnectAll(opts) {
484
490
  const connected = new Set(Object.keys(loadConfig().connect ?? {}));
485
491
  const pick = await pickHarnesses(offerable, connected, { prompt });
486
492
  mode = pick.mode;
487
- if (pick.selection === "cancel") {
493
+ if (pick.selection === 'cancel') {
488
494
  // Before summary and telemetry: an explicit nothing is not a setup.
489
- outLine("Nothing installed. Run: tinyfish connect --all or tinyfish connect <client>");
495
+ outLine('Nothing installed. Run: tinyfish connect --all or tinyfish connect <client>');
490
496
  return 0;
491
497
  }
492
498
  const chosen = new Set(pick.selection);
@@ -506,10 +512,10 @@ export async function runConnectAll(opts) {
506
512
  willAttempt(d.harness));
507
513
  let takeHoistedCli;
508
514
  if (cliNeeded) {
509
- errLine("First: the TinyFish CLI, shared by every harness.");
515
+ errLine('First: the TinyFish CLI, shared by every harness.');
510
516
  const outcome = runHoistedCliInstall(opts.verbose ?? false);
511
517
  if (!outcome) {
512
- errLine("Setup interrupted — run the command again to finish.");
518
+ errLine('Setup interrupted — run the command again to finish.');
513
519
  return 130;
514
520
  }
515
521
  let taken = false;
@@ -534,7 +540,7 @@ export async function runConnectAll(opts) {
534
540
  results.push({
535
541
  ...detection,
536
542
  installed: false,
537
- outcome: "deselected",
543
+ outcome: 'deselected',
538
544
  fixCommand: `tinyfish connect ${harness}`,
539
545
  });
540
546
  continue;
@@ -556,18 +562,18 @@ export async function runConnectAll(opts) {
556
562
  detected: detection.detected,
557
563
  configPath: detection.configPath,
558
564
  installed: false,
559
- outcome: "failed",
565
+ outcome: 'failed',
560
566
  errorMessage: e instanceof Error ? e.message : String(e),
561
567
  fixCommand: `tinyfish connect ${harness}`,
562
568
  };
563
569
  }
564
570
  finally {
565
571
  // The summary and the outro sit outside any step.
566
- setErrIndent("");
572
+ setErrIndent('');
567
573
  }
568
574
  results.push(result);
569
575
  // Ctrl+C means stop, not continue to the next harness.
570
- if (result.outcome === "interrupted")
576
+ if (result.outcome === 'interrupted')
571
577
  break;
572
578
  }
573
579
  // The interrupt break truncates `results`; pad so the one-row-per-harness schema still parses.
@@ -581,17 +587,17 @@ export async function runConnectAll(opts) {
581
587
  detected,
582
588
  configPath,
583
589
  installed: false,
584
- outcome: staleConfig ? "stale_config" : detected ? "not_reached" : "not_detected",
590
+ outcome: staleConfig ? 'stale_config' : detected ? 'not_reached' : 'not_detected',
585
591
  fixCommand: `tinyfish connect ${detection.harness}`,
586
592
  });
587
593
  }
588
- outLine("");
589
- outLine("TinyFish setup summary:");
594
+ outLine('');
595
+ outLine('TinyFish setup summary:');
590
596
  for (const line of renderSummary(results, opts.uninstall))
591
597
  outLine(line);
592
598
  const reload = opts.uninstall ? undefined : reloadHint(results);
593
599
  if (reload) {
594
- outLine("");
600
+ outLine('');
595
601
  outLine(reload);
596
602
  }
597
603
  // Before the launch handover: anything printed after it is never seen.
@@ -604,25 +610,25 @@ export async function runConnectAll(opts) {
604
610
  apiKey: opts.apiKey,
605
611
  verbose: opts.verbose ?? false,
606
612
  prompt,
607
- retryCommand: "npx @tiny-fish/cli connect --all",
613
+ retryCommand: 'npx @tiny-fish/cli connect --all',
608
614
  });
609
- return outcome === "cli_verified" ? 0 : 1;
615
+ return outcome === 'cli_verified' ? 0 : 1;
610
616
  }
611
617
  catch (error) {
612
618
  if (!(error instanceof ConnectInterruptedError))
613
619
  throw error;
614
- errLine("Setup interrupted — run the command again to finish.");
620
+ errLine('Setup interrupted — run the command again to finish.');
615
621
  process.exitCode = 130;
616
622
  return 130;
617
623
  }
618
624
  }
619
- const stale = results.filter((r) => r.outcome === "stale_config");
620
- outLine("");
625
+ const stale = results.filter((r) => r.outcome === 'stale_config');
626
+ outLine('');
621
627
  outLine(stale.length > 0
622
- ? `No runnable harness found. Reinstall ${stale.map((r) => DISPLAY_NAMES[r.harness]).join(", ")}, or install one of: ${ALL_HARNESSES.join(", ")}`
623
- : "No supported harness found. Install one of: " + ALL_HARNESSES.join(", "));
628
+ ? `No runnable harness found. Reinstall ${stale.map((r) => DISPLAY_NAMES[r.harness]).join(', ')}, or install one of: ${ALL_HARNESSES.join(', ')}`
629
+ : 'No supported harness found. Install one of: ' + ALL_HARNESSES.join(', '));
624
630
  if (!opts.uninstall)
625
- outLine("Then run: npx @tiny-fish/cli connect --all");
631
+ outLine('Then run: npx @tiny-fish/cli connect --all');
626
632
  return 1;
627
633
  }
628
634
  if (!opts.dryRun && !opts.uninstall) {
@@ -640,13 +646,13 @@ export async function runConnectAll(opts) {
640
646
  emitNotice();
641
647
  }
642
648
  const exitCode = computeExitCode(results);
643
- const interrupted = results.some((r) => r.outcome === "interrupted");
649
+ const interrupted = results.some((r) => r.outcome === 'interrupted');
644
650
  if (exitCode === 0 &&
645
651
  !interrupted &&
646
652
  !opts.dryRun &&
647
653
  !opts.uninstall &&
648
654
  results.some((r) => r.installed)) {
649
- outLine("");
655
+ outLine('');
650
656
  if (isTTY && !opts.noLaunch && (await launchFirstTask(results))) {
651
657
  return exitCode;
652
658
  }
@@ -654,17 +660,17 @@ export async function runConnectAll(opts) {
654
660
  }
655
661
  return exitCode;
656
662
  }
657
- export const FIRST_TASK_PROMPT = "My TinyFish setup just finished — verify it with a real run. Use the MCP server named " +
658
- "`tinyfish` for every TinyFish tool call; if another TinyFish server is also registered, still " +
663
+ export const FIRST_TASK_PROMPT = 'My TinyFish setup just finished — verify it with a real run. Use the MCP server named ' +
664
+ '`tinyfish` for every TinyFish tool call; if another TinyFish server is also registered, still ' +
659
665
  "use `tinyfish` and do not ask me which. Then use the TinyFish search tool to find today's " +
660
- "top Hacker News story, fetch that page with the TinyFish fetch tool, and give me a two-line " +
661
- "summary. Show which tools you call. If a tool call comes back blocked or not approved, tell " +
662
- "me which tool needs approval and how to grant it, then wait for me instead of abandoning the " +
663
- "task.";
666
+ 'top Hacker News story, fetch that page with the TinyFish fetch tool, and give me a two-line ' +
667
+ 'summary. Show which tools you call. If a tool call comes back blocked or not approved, tell ' +
668
+ 'me which tool needs approval and how to grant it, then wait for me instead of abandoning the ' +
669
+ 'task.';
664
670
  // Launchable seeded agents, best-first — cursor-agent shares ~/.cursor/mcp.json with the IDE.
665
671
  const LAUNCHERS = [
666
- { harness: "cursor", args: [FIRST_TASK_PROMPT] },
667
- { harness: "claude-code", args: [FIRST_TASK_PROMPT] },
672
+ { harness: 'cursor', args: [FIRST_TASK_PROMPT] },
673
+ { harness: 'claude-code', args: [FIRST_TASK_PROMPT] },
668
674
  ];
669
675
  async function launchFirstTask(results) {
670
676
  const candidates = LAUNCHERS.filter((l) => results.some((r) => r.harness === l.harness && r.installed) &&
@@ -674,7 +680,7 @@ async function launchFirstTask(results) {
674
680
  // Several installs: ask rather than pick a favorite; plain enter skips.
675
681
  let chosen = candidates[0];
676
682
  if (candidates.length > 1) {
677
- const menu = candidates.map((l, i) => `[${i + 1}] ${DISPLAY_NAMES[l.harness]}`).join(" ");
683
+ const menu = candidates.map((l, i) => `[${i + 1}] ${DISPLAY_NAMES[l.harness]}`).join(' ');
678
684
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
679
685
  let answer;
680
686
  try {
@@ -694,6 +700,6 @@ async function launchFirstTask(results) {
694
700
  if (!chosen)
695
701
  return false;
696
702
  outLine(`Starting ${DISPLAY_NAMES[chosen.harness]} with your first TinyFish task...`);
697
- const result = spawn.sync(HARNESS_COMMANDS[chosen.harness], chosen.args, { stdio: "inherit" });
703
+ const result = spawn.sync(HARNESS_COMMANDS[chosen.harness], chosen.args, { stdio: 'inherit' });
698
704
  return !result.error;
699
705
  }