@tiny-fish/cli 0.30.2-next.257 → 0.30.2-next.259
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/commands/connect.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import spawn from
|
|
2
|
-
import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from
|
|
3
|
-
import { CLAUDE_CODE, CODEX, CURSOR_SKILL_TARGET, GROK, HERMES, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, OPENCODE, launchNativeMcpClient, launchOpenClawWalkthrough, openExternalUrl, } from
|
|
4
|
-
import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, } from
|
|
5
|
-
import { ensureCliAuthenticated } from
|
|
6
|
-
import { createStdinPrompt, runCliFallback } from
|
|
7
|
-
import { installWebSkill } from
|
|
8
|
-
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, spawnRemoval, } from
|
|
9
|
-
import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from
|
|
10
|
-
import { runConnectAll } from
|
|
11
|
-
import { AuthMode, RELOAD_ACTION } from
|
|
12
|
-
import { detectHumanInitiated } from
|
|
13
|
-
import { TINYFISH_API_KEY_VAR } from
|
|
14
|
-
import { emitNotice } from
|
|
15
|
-
import { SIGN_IN_PROBES } from
|
|
16
|
-
import { errLine, warnLine } from
|
|
17
|
-
import { z } from
|
|
18
|
-
import { verifyMcpAuth } from
|
|
19
|
-
export const DEFAULT_MCP_URL =
|
|
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 ||
|
|
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
|
-
|
|
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(
|
|
43
|
-
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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(
|
|
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,
|
|
94
|
-
failedStage:
|
|
95
|
-
failureReason:
|
|
93
|
+
settle(state, telemetry, 'failed', {
|
|
94
|
+
failedStage: 'prerequisite_check',
|
|
95
|
+
failureReason: 'harness_not_installed',
|
|
96
96
|
});
|
|
97
|
-
if (outcome !==
|
|
97
|
+
if (outcome !== 'cli_verified') {
|
|
98
98
|
process.exitCode = 1;
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
-
errLine(
|
|
102
|
-
errLine(
|
|
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:
|
|
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(
|
|
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}`,
|
|
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}`,
|
|
134
|
+
`terminal. Run: tinyfish connect ${client.connectClient}`, 'key_auth_unsupported');
|
|
135
135
|
}
|
|
136
|
-
telemetry.track(
|
|
137
|
-
state.stage =
|
|
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(
|
|
142
|
-
mcpUrl.searchParams.set(
|
|
143
|
-
mcpUrl.searchParams.set(
|
|
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 ?
|
|
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:
|
|
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(
|
|
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(
|
|
171
|
+
telemetry.track('checkpoint', { phase: 'oauth_done' });
|
|
172
172
|
const degradedNotes = loginDegraded ? client.degradedAuthNotes : undefined;
|
|
173
173
|
if (useKeyAuth) {
|
|
174
|
-
errLine(degradedNotes?.keyed ??
|
|
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 =
|
|
186
|
-
errLine(
|
|
185
|
+
state.stage = 'client_oauth';
|
|
186
|
+
errLine('Signing in to TinyFish...');
|
|
187
187
|
const loginResult = spawn.sync(client.command, pendingLogin, {
|
|
188
|
-
stdio:
|
|
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(
|
|
197
|
+
telemetry.track('checkpoint', { phase: 'oauth_done' });
|
|
198
198
|
}
|
|
199
199
|
const authMode = useKeyAuth
|
|
200
200
|
? AuthMode.ApiKey
|
|
201
201
|
: signInDeferred
|
|
202
|
-
?
|
|
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,
|
|
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
|
-
|
|
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(
|
|
240
|
+
await attempt('cli_install', () => runCliInstallStep(options, telemetry));
|
|
241
241
|
}
|
|
242
242
|
if (client.skillAgent) {
|
|
243
|
-
await attempt(
|
|
243
|
+
await attempt('skill_install', () => {
|
|
244
244
|
installWebSkill(client, { verbose: options.verbose ?? false });
|
|
245
|
-
telemetry.track(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
298
|
-
phase:
|
|
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(
|
|
305
|
+
telemetry.track('checkpoint', { phase: 'cli_installed' });
|
|
306
306
|
}
|
|
307
307
|
function walkthroughPhase(outcome) {
|
|
308
|
-
return outcome ===
|
|
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(
|
|
319
|
+
telemetry.track('post_install_failed', {
|
|
320
320
|
failedStage: state.stage,
|
|
321
|
-
failureReason: error instanceof ConnectStepError ? error.failureReason :
|
|
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,
|
|
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:
|
|
346
|
+
stage: 'prerequisite_check',
|
|
347
347
|
settled: false,
|
|
348
348
|
harnessDegraded: false,
|
|
349
349
|
};
|
|
350
350
|
await runGuarded(state, telemetry, async () => {
|
|
351
|
-
telemetry.track(
|
|
352
|
-
const support = await requireSupportOrRescue(OPENCLAW,
|
|
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(
|
|
356
|
+
telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
|
|
357
357
|
if (options.hoistedCliInstall) {
|
|
358
|
-
state.stage =
|
|
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(
|
|
365
|
-
phase:
|
|
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 =
|
|
371
|
+
state.stage = 'cli_install';
|
|
372
372
|
installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
|
|
373
|
-
telemetry.track(
|
|
373
|
+
telemetry.track('checkpoint', { phase: 'cli_installed' });
|
|
374
374
|
}
|
|
375
|
-
state.stage =
|
|
376
|
-
errLine(
|
|
377
|
-
const skillInstallResult = spawn.sync(
|
|
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(
|
|
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(
|
|
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 =
|
|
387
|
-
ensureCliAuthenticated(
|
|
388
|
-
telemetry.track(
|
|
389
|
-
saveConnectContext(
|
|
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,
|
|
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(
|
|
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 =
|
|
400
|
+
state.stage = 'walkthrough_launch';
|
|
401
401
|
try {
|
|
402
402
|
await launchOpenClawWalkthrough(async (outcome) => {
|
|
403
|
-
telemetry.track(
|
|
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(
|
|
410
|
-
errLine(finishSetupHint(
|
|
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(
|
|
423
|
-
const registeredSource = registered.searchParams.get(
|
|
424
|
-
const registeredClient = registered.searchParams.get(
|
|
425
|
-
for (const key of [
|
|
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 ===
|
|
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,
|
|
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:
|
|
449
|
+
stage: 'prerequisite_check',
|
|
450
450
|
settled: false,
|
|
451
451
|
harnessDegraded: false,
|
|
452
452
|
};
|
|
453
453
|
await runGuarded(state, telemetry, async () => {
|
|
454
|
-
telemetry.track(
|
|
455
|
-
telemetry.track(
|
|
456
|
-
state.stage =
|
|
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(
|
|
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 =
|
|
461
|
-
ensureCliAuthenticated(
|
|
462
|
-
telemetry.track(
|
|
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(
|
|
466
|
+
errLine('Ignoring the stored API key: invalid format. Run: tinyfish auth login');
|
|
467
467
|
}
|
|
468
|
-
state.stage =
|
|
469
|
-
errLine(
|
|
468
|
+
state.stage = 'registration';
|
|
469
|
+
errLine('Adding TinyFish to Cursor...');
|
|
470
470
|
const mcpUrl = new URL(options.mcpUrl);
|
|
471
|
-
mcpUrl.searchParams.set(
|
|
472
|
-
mcpUrl.searchParams.set(
|
|
473
|
-
mcpUrl.searchParams.set(
|
|
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 ===
|
|
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`,
|
|
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(
|
|
483
|
+
errLine('Repaired the existing TinyFish entry (updated auth/config to current).');
|
|
484
484
|
}
|
|
485
|
-
telemetry.track(
|
|
486
|
-
saveConnectContext(
|
|
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 ??
|
|
495
|
-
|
|
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(
|
|
499
|
+
errLine('Cursor should pop a TinyFish install confirmation — approve it, then sign in when prompted.');
|
|
500
500
|
}
|
|
501
501
|
else {
|
|
502
|
-
errLine(
|
|
502
|
+
errLine('TinyFish is connected. Reload the Cursor window, then approve/sign in under Settings → MCP.');
|
|
503
503
|
}
|
|
504
|
-
settle(state, telemetry,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
547
|
-
.description(
|
|
548
|
-
.argument(
|
|
549
|
-
.option(
|
|
550
|
-
.option(
|
|
551
|
-
.option(
|
|
552
|
-
.option(
|
|
553
|
-
.option(
|
|
554
|
-
.option(
|
|
555
|
-
.option(
|
|
556
|
-
.option(
|
|
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(
|
|
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 (![
|
|
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(
|
|
577
|
+
const pageAttemptId = parsedUrl.searchParams.get('connect_attempt_id');
|
|
578
578
|
let attemptId;
|
|
579
579
|
if (pageAttemptId !== null) {
|
|
580
|
-
parsedUrl.searchParams.delete(
|
|
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(
|
|
587
|
+
throw new Error('Pass either a client or --all, not both.');
|
|
588
588
|
}
|
|
589
589
|
if (options.launch) {
|
|
590
|
-
throw new Error(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
645
|
+
throw new Error('Unsupported client: ' +
|
|
646
646
|
client +
|
|
647
|
-
|
|
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();
|