@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.
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.js +52 -52
- package/dist/commands/batch.d.ts +1 -1
- package/dist/commands/batch.js +54 -46
- package/dist/commands/browser.d.ts +1 -1
- package/dist/commands/browser.js +14 -14
- package/dist/commands/config-claude.d.ts +1 -1
- package/dist/commands/config-claude.js +22 -22
- package/dist/commands/connect.d.ts +3 -3
- package/dist/commands/connect.js +150 -150
- package/dist/commands/doctor.d.ts +4 -4
- package/dist/commands/doctor.js +104 -104
- package/dist/commands/fetch.d.ts +1 -1
- package/dist/commands/fetch.js +24 -27
- package/dist/commands/onboard.d.ts +1 -1
- package/dist/commands/onboard.js +50 -50
- package/dist/commands/profile.d.ts +3 -3
- package/dist/commands/profile.js +45 -45
- package/dist/commands/run.d.ts +1 -1
- package/dist/commands/run.js +78 -65
- package/dist/commands/runs.d.ts +1 -1
- package/dist/commands/runs.js +63 -50
- package/dist/commands/search.d.ts +1 -1
- package/dist/commands/search.js +16 -16
- package/dist/commands/upgrade.d.ts +2 -2
- package/dist/commands/upgrade.js +36 -36
- package/dist/commands/vault.d.ts +1 -1
- package/dist/commands/vault.js +48 -45
- package/dist/commands/wallet.d.ts +1 -1
- package/dist/commands/wallet.js +14 -14
- package/dist/index.js +2 -2
- package/dist/lib/auth.d.ts +5 -5
- package/dist/lib/auth.js +56 -56
- package/dist/lib/batch-store.d.ts +1 -1
- package/dist/lib/batch-store.js +4 -4
- package/dist/lib/chrome-cookies.d.ts +1 -1
- package/dist/lib/chrome-cookies.js +24 -24
- package/dist/lib/claude-config.js +37 -37
- package/dist/lib/cli-install.js +12 -12
- package/dist/lib/client.d.ts +2 -2
- package/dist/lib/client.js +61 -61
- package/dist/lib/connect-all.js +140 -134
- package/dist/lib/connect-auth.d.ts +2 -2
- package/dist/lib/connect-auth.js +15 -15
- package/dist/lib/connect-clients.d.ts +6 -6
- package/dist/lib/connect-clients.js +143 -143
- package/dist/lib/connect-fallback.d.ts +1 -1
- package/dist/lib/connect-fallback.js +38 -38
- package/dist/lib/connect-picker.d.ts +4 -4
- package/dist/lib/connect-picker.js +28 -28
- package/dist/lib/connect-runtime.d.ts +7 -7
- package/dist/lib/connect-runtime.js +61 -61
- package/dist/lib/constants.js +13 -13
- package/dist/lib/cursor-config.d.ts +1 -1
- package/dist/lib/cursor-config.js +30 -30
- package/dist/lib/doctor-report.d.ts +2 -2
- package/dist/lib/doctor-report.js +22 -22
- package/dist/lib/harness-detect.js +39 -39
- package/dist/lib/harness.js +9 -9
- package/dist/lib/hermes-env.js +26 -23
- package/dist/lib/install-root.js +12 -12
- package/dist/lib/notice.d.ts +1 -1
- package/dist/lib/notice.js +12 -12
- package/dist/lib/output.js +14 -14
- package/dist/lib/registration-detect.d.ts +3 -3
- package/dist/lib/registration-detect.js +82 -82
- package/dist/lib/setup-telemetry.d.ts +4 -4
- package/dist/lib/setup-telemetry.js +34 -34
- package/dist/lib/signals.js +1 -1
- package/dist/lib/skill-install.d.ts +3 -3
- package/dist/lib/skill-install.js +40 -40
- package/dist/lib/types.d.ts +8 -8
- package/dist/lib/verify.d.ts +1 -1
- package/dist/lib/verify.js +28 -28
- package/dist/program.d.ts +1 -1
- package/dist/program.js +28 -28
- package/package.json +1 -1
package/dist/lib/connect-all.js
CHANGED
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
import * as readline from
|
|
2
|
-
import spawn from
|
|
3
|
-
import { connectClaudeCode, connectCodex, connectCursor, connectGrok, connectHermes, connectOpenClaw, connectOpencode, } from
|
|
4
|
-
import { clearConnectContext, connectContextState, loadConfig, validatedApiKey
|
|
5
|
-
import { installTinyFishCli, } from
|
|
6
|
-
import { HARNESS_CAPABILITIES, NATIVE_BY_HARNESS, openclawSkillUninstall, } from
|
|
7
|
-
import { createStdinPrompt, runCliFallback } from
|
|
8
|
-
import { ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, spawnRemoval, } from
|
|
9
|
-
import { pickHarnesses } from
|
|
10
|
-
import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, RELOAD_ACTION, } from
|
|
11
|
-
import { detectHumanInitiated } from
|
|
12
|
-
import { emitNotice } from
|
|
13
|
-
import { removeCursorMcpServer, planCursorWrite } from
|
|
14
|
-
import { errLine, outLine, setErrIndent } from
|
|
15
|
-
import { sendSetupCompleted } from
|
|
16
|
-
import { verifyMcpAuth, verifyMcpHealth } from
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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([
|
|
30
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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 ===
|
|
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 ===
|
|
103
|
-
return
|
|
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 ===
|
|
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 ===
|
|
129
|
-
return removal.runs.map((args) => `${removal.command} ${args.join(
|
|
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 !==
|
|
137
|
+
if (harness !== 'cursor') {
|
|
134
138
|
const removal = uninstallRuns(harness);
|
|
135
139
|
if (!removal) {
|
|
136
|
-
return { ...detection, outcome:
|
|
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 !==
|
|
144
|
+
if (harness !== 'openclaw' && !binaryOnPath) {
|
|
141
145
|
return {
|
|
142
146
|
...detection,
|
|
143
|
-
outcome:
|
|
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) !==
|
|
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(
|
|
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:
|
|
164
|
-
errorMessage: `could not run \`${removal.command} ${args.join(
|
|
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:
|
|
178
|
-
fixCommand:
|
|
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:
|
|
190
|
+
return { ...detection, outcome: 'uninstalled' };
|
|
187
191
|
}
|
|
188
192
|
if (!connectedBefore)
|
|
189
|
-
return { ...detection, outcome:
|
|
193
|
+
return { ...detection, outcome: 'uninstall_noop' };
|
|
190
194
|
return {
|
|
191
195
|
...detection,
|
|
192
|
-
outcome:
|
|
193
|
-
errorMessage:
|
|
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:
|
|
206
|
+
result = { status: 'corrupt_skip', error: e instanceof Error ? e.message : String(e) };
|
|
203
207
|
}
|
|
204
|
-
if (result.status ===
|
|
208
|
+
if (result.status === 'corrupt_skip') {
|
|
205
209
|
return {
|
|
206
210
|
...detection,
|
|
207
|
-
outcome:
|
|
211
|
+
outcome: 'uninstalled',
|
|
208
212
|
errorMessage: `could not update Cursor's mcp.json (${result.error}) — nothing removed.`,
|
|
209
|
-
fixCommand:
|
|
213
|
+
fixCommand: 'Fix ~/.cursor/mcp.json by hand, then re-run: tinyfish connect --all --uninstall',
|
|
210
214
|
};
|
|
211
215
|
}
|
|
212
|
-
if (result.status ===
|
|
213
|
-
return { ...detection, outcome:
|
|
216
|
+
if (result.status === 'unchanged') {
|
|
217
|
+
return { ...detection, outcome: 'uninstall_noop' };
|
|
214
218
|
}
|
|
215
|
-
clearConnectContext(
|
|
216
|
-
return { ...detection, outcome:
|
|
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 ===
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
270
|
+
errLine('Setup interrupted — run the command again to finish.');
|
|
267
271
|
process.exitCode = 130;
|
|
268
|
-
return { ...base, outcome:
|
|
272
|
+
return { ...base, outcome: 'interrupted', fixCommand };
|
|
269
273
|
}
|
|
270
|
-
return { ...base, outcome:
|
|
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:
|
|
288
|
-
errorMessage:
|
|
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 &&
|
|
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 ===
|
|
298
|
-
return { ...base, detected: false, outcome:
|
|
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:
|
|
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:
|
|
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:
|
|
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 ===
|
|
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:
|
|
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 ===
|
|
341
|
+
if (result.outcome === 'not_installed') {
|
|
336
342
|
const next = uninstall
|
|
337
|
-
?
|
|
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
|
-
?
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
359
|
+
if (result.outcome === 'dry_run') {
|
|
354
360
|
return `… ${name} — ${result.fixCommand}`;
|
|
355
361
|
}
|
|
356
|
-
if (result.outcome ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
376
|
+
if (result.outcome === 'uninstall_noop') {
|
|
371
377
|
return `… ${name} — no tinyfish entry found, nothing removed`;
|
|
372
378
|
}
|
|
373
|
-
if (result.outcome ===
|
|
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 ===
|
|
384
|
+
if (result.outcome === 'interrupted') {
|
|
379
385
|
return `✗ ${name} — interrupted before finishing. Fix: ${result.fixCommand}`;
|
|
380
386
|
}
|
|
381
|
-
if (result.outcome ===
|
|
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 ===
|
|
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}` :
|
|
400
|
+
const verifyLabel = result.verifyDepth ? `verified: ${result.verifyDepth}` : 'not verified';
|
|
395
401
|
if (authDeferred || result.verifyOk === false) {
|
|
396
402
|
const reason = authDeferred
|
|
397
|
-
?
|
|
398
|
-
: `verify warning (${verifyLabel}): ${result.verifyReason ??
|
|
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(
|
|
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 ===
|
|
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(
|
|
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
|
|
440
|
+
return 'not_detected';
|
|
435
441
|
if (!uninstall && isStaleConfig(d))
|
|
436
|
-
return
|
|
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 !==
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
461
|
-
r.outcome ===
|
|
462
|
-
r.outcome ===
|
|
463
|
-
r.outcome ===
|
|
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 ===
|
|
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 =
|
|
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 ===
|
|
493
|
+
if (pick.selection === 'cancel') {
|
|
488
494
|
// Before summary and telemetry: an explicit nothing is not a setup.
|
|
489
|
-
outLine(
|
|
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(
|
|
515
|
+
errLine('First: the TinyFish CLI, shared by every harness.');
|
|
510
516
|
const outcome = runHoistedCliInstall(opts.verbose ?? false);
|
|
511
517
|
if (!outcome) {
|
|
512
|
-
errLine(
|
|
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:
|
|
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:
|
|
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 ===
|
|
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 ?
|
|
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(
|
|
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:
|
|
613
|
+
retryCommand: 'npx @tiny-fish/cli connect --all',
|
|
608
614
|
});
|
|
609
|
-
return outcome ===
|
|
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(
|
|
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 ===
|
|
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(
|
|
623
|
-
:
|
|
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(
|
|
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 ===
|
|
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 =
|
|
658
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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:
|
|
667
|
-
{ harness:
|
|
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:
|
|
703
|
+
const result = spawn.sync(HARNESS_COMMANDS[chosen.harness], chosen.args, { stdio: 'inherit' });
|
|
698
704
|
return !result.error;
|
|
699
705
|
}
|