@tiny-fish/cli 0.30.1-next.251 → 0.30.1-next.253
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/connect.d.ts +1 -1
- package/dist/commands/connect.js +25 -18
- package/dist/commands/onboard.js +1 -1
- package/dist/lib/auth.d.ts +4 -0
- package/dist/lib/auth.js +37 -0
- package/dist/lib/connect-all.js +105 -19
- package/dist/lib/connect-clients.d.ts +18 -3
- package/dist/lib/connect-clients.js +67 -15
- package/dist/lib/connect-runtime.d.ts +3 -1
- package/dist/lib/connect-runtime.js +9 -0
- package/package.json +1 -1
|
@@ -50,5 +50,5 @@ export declare function connectCursor(options: {
|
|
|
50
50
|
attemptId?: string;
|
|
51
51
|
verbose?: boolean;
|
|
52
52
|
}): Promise<void>;
|
|
53
|
-
export declare function launchAgent(client: AgentClient): void
|
|
53
|
+
export declare function launchAgent(client: AgentClient): Promise<void>;
|
|
54
54
|
export declare function registerConnect(program: Command): void;
|
package/dist/commands/connect.js
CHANGED
|
@@ -5,7 +5,7 @@ import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyF
|
|
|
5
5
|
import { ensureCliAuthenticated } from "../lib/connect-auth.js";
|
|
6
6
|
import { createStdinPrompt, runCliFallback } from "../lib/connect-fallback.js";
|
|
7
7
|
import { installWebSkill } from "../lib/skill-install.js";
|
|
8
|
-
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError,
|
|
8
|
+
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, spawnRemoval, } from "../lib/connect-runtime.js";
|
|
9
9
|
import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from "../lib/cursor-config.js";
|
|
10
10
|
import { runConnectAll } from "../lib/connect-all.js";
|
|
11
11
|
import { AuthMode, RELOAD_ACTION } from "../lib/harness-detect.js";
|
|
@@ -18,13 +18,9 @@ import { z } from "zod";
|
|
|
18
18
|
import { verifyMcpAuth } from "../lib/verify.js";
|
|
19
19
|
export const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
|
|
20
20
|
function removeExistingRegistration(client, removal, verbose) {
|
|
21
|
-
const result =
|
|
22
|
-
encoding: "utf8",
|
|
23
|
-
timeout: NON_INTERACTIVE_TIMEOUT_MS,
|
|
24
|
-
});
|
|
21
|
+
const result = spawnRemoval(client.command, removal.args);
|
|
25
22
|
if (result.status === 0)
|
|
26
23
|
return;
|
|
27
|
-
throwIfInterrupted(result);
|
|
28
24
|
// Best-effort: a stale entry makes `mcp add` error or upsert, with a truer message.
|
|
29
25
|
if (!verbose)
|
|
30
26
|
return;
|
|
@@ -208,7 +204,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
208
204
|
state.authMode = authMode;
|
|
209
205
|
// Registration/OAuth make MCP work; later steps are cosmetic and must not fail the attempt.
|
|
210
206
|
settle(state, telemetry, "completed", { authMode });
|
|
211
|
-
runPostInstallSteps(client, options, state, telemetry, signInDeferred);
|
|
207
|
+
await runPostInstallSteps(client, options, state, telemetry, signInDeferred);
|
|
212
208
|
});
|
|
213
209
|
return state.authMode;
|
|
214
210
|
}
|
|
@@ -224,13 +220,13 @@ function connectedLine(client, signInDeferred) {
|
|
|
224
220
|
`${RELOAD_ACTION[client.connectClient]} to pick up the tools.`);
|
|
225
221
|
}
|
|
226
222
|
// Runs after `completed` settled: steps fail alone, the rest still run.
|
|
227
|
-
function runPostInstallSteps(client, options, state, telemetry, signInDeferred) {
|
|
223
|
+
async function runPostInstallSteps(client, options, state, telemetry, signInDeferred) {
|
|
228
224
|
let failed = false;
|
|
229
225
|
// Modelled on upgrade's attempt(); interrupts rethrow and abandon remaining steps.
|
|
230
|
-
const attempt = (stage, run) => {
|
|
226
|
+
const attempt = async (stage, run) => {
|
|
231
227
|
state.stage = stage;
|
|
232
228
|
try {
|
|
233
|
-
run();
|
|
229
|
+
await run();
|
|
234
230
|
}
|
|
235
231
|
catch (error) {
|
|
236
232
|
if (error instanceof ConnectInterruptedError)
|
|
@@ -241,17 +237,17 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
|
|
|
241
237
|
};
|
|
242
238
|
try {
|
|
243
239
|
if (options.cliInstall !== false) {
|
|
244
|
-
attempt("cli_install", () => runCliInstallStep(options, telemetry));
|
|
240
|
+
await attempt("cli_install", () => runCliInstallStep(options, telemetry));
|
|
245
241
|
}
|
|
246
242
|
if (client.skillAgent) {
|
|
247
|
-
attempt("skill_install", () => {
|
|
243
|
+
await attempt("skill_install", () => {
|
|
248
244
|
installWebSkill(client, { verbose: options.verbose ?? false });
|
|
249
245
|
telemetry.track("checkpoint", { phase: "skill_installed" });
|
|
250
246
|
});
|
|
251
247
|
}
|
|
252
248
|
// Before auth: a failed sign-in must not lose the harness record.
|
|
253
249
|
saveConnectContext(client.connectClient, telemetry.attemptId, state.authMode);
|
|
254
|
-
attempt("authentication", () => {
|
|
250
|
+
await attempt("authentication", () => {
|
|
255
251
|
// A set timeout means headless; never shell an interactive login.
|
|
256
252
|
ensureCliAuthenticated(client.connectClient, options.apiKey, {
|
|
257
253
|
interactiveLogin: options.authTimeoutMs === undefined,
|
|
@@ -266,11 +262,15 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
|
|
|
266
262
|
errLine(connectedLine(client, signInDeferred));
|
|
267
263
|
}
|
|
268
264
|
else {
|
|
269
|
-
attempt("walkthrough_launch", () => {
|
|
265
|
+
await attempt("walkthrough_launch", async () => {
|
|
270
266
|
// Before the handover, or the only sign-in instructions scroll past under the agent.
|
|
271
267
|
if (signInDeferred && client.signInHint)
|
|
272
268
|
errLine(client.signInHint);
|
|
273
|
-
launchNativeMcpClient(client)
|
|
269
|
+
await launchNativeMcpClient(client, async (outcome) => {
|
|
270
|
+
telemetry.track("checkpoint", { phase: walkthroughPhase(outcome) });
|
|
271
|
+
// Flushed pre-block; the session-long handover would strand the delivery.
|
|
272
|
+
await telemetry.flush();
|
|
273
|
+
});
|
|
274
274
|
});
|
|
275
275
|
}
|
|
276
276
|
}
|
|
@@ -304,6 +304,9 @@ function runCliInstallStep(options, telemetry) {
|
|
|
304
304
|
installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
|
|
305
305
|
telemetry.track("checkpoint", { phase: "cli_installed" });
|
|
306
306
|
}
|
|
307
|
+
function walkthroughPhase(outcome) {
|
|
308
|
+
return outcome === "printed" ? "walkthrough_prompt_printed" : "walkthrough_launched";
|
|
309
|
+
}
|
|
307
310
|
function finishSetupHint(connectClient) {
|
|
308
311
|
return `Finish setup with: ${finishSetupCommand(connectClient)}`;
|
|
309
312
|
}
|
|
@@ -396,7 +399,11 @@ export async function connectOpenClaw(options) {
|
|
|
396
399
|
}
|
|
397
400
|
state.stage = "walkthrough_launch";
|
|
398
401
|
try {
|
|
399
|
-
launchOpenClawWalkthrough()
|
|
402
|
+
await launchOpenClawWalkthrough(async (outcome) => {
|
|
403
|
+
telemetry.track("checkpoint", { phase: walkthroughPhase(outcome) });
|
|
404
|
+
// Flushed pre-block; the session-long handover would strand the delivery.
|
|
405
|
+
await telemetry.flush();
|
|
406
|
+
});
|
|
400
407
|
}
|
|
401
408
|
catch (error) {
|
|
402
409
|
trackPostInstallFailure("OpenClaw", state, telemetry, error);
|
|
@@ -515,8 +522,8 @@ const AGENT_LAUNCHERS = {
|
|
|
515
522
|
throw new Error("Cursor has no launchable walkthrough. Open Cursor and start chatting.");
|
|
516
523
|
},
|
|
517
524
|
};
|
|
518
|
-
export function launchAgent(client) {
|
|
519
|
-
AGENT_LAUNCHERS[client]();
|
|
525
|
+
export async function launchAgent(client) {
|
|
526
|
+
await AGENT_LAUNCHERS[client]();
|
|
520
527
|
}
|
|
521
528
|
// A new client fails to compile until wired here.
|
|
522
529
|
const CLIENT_CONNECTORS = {
|
package/dist/commands/onboard.js
CHANGED
package/dist/lib/auth.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export declare function writeConfig(apiKey: string): void;
|
|
|
32
32
|
export declare function saveConfig(apiKey: string): void;
|
|
33
33
|
/** `authMode` omitted rewrites the entry without one: a reconnect that degraded must not keep a stale key claim. */
|
|
34
34
|
export declare function saveConnectContext(client: string, attemptId: string, authMode?: RecordedAuthMode): void;
|
|
35
|
+
/** Distinguishes a missing entry from an unreadable config file. */
|
|
36
|
+
export declare function connectContextState(client: string): "present" | "absent" | "unreadable";
|
|
37
|
+
/** Kept marker after uninstall makes doctor offer to reinstall the harness. */
|
|
38
|
+
export declare function clearConnectContext(client: string): void;
|
|
35
39
|
interface EnvironmentOptions {
|
|
36
40
|
platform?: typeof process.platform;
|
|
37
41
|
shell?: string;
|
package/dist/lib/auth.js
CHANGED
|
@@ -142,6 +142,43 @@ export function saveConnectContext(client, attemptId, authMode) {
|
|
|
142
142
|
errLine(`Warning: could not persist connect context: ${e instanceof Error ? e.message : String(e)}`);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
/** Distinguishes a missing entry from an unreadable config file. */
|
|
146
|
+
export function connectContextState(client) {
|
|
147
|
+
let raw;
|
|
148
|
+
try {
|
|
149
|
+
raw = fs.readFileSync(configFile(), "utf8");
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return e.code === "ENOENT" ? "absent" : "unreadable";
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(raw);
|
|
156
|
+
const entry = parsed && typeof parsed === "object" ? parsed.connect?.[client] : undefined;
|
|
157
|
+
return entry ? "present" : "absent";
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return "unreadable";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Kept marker after uninstall makes doctor offer to reinstall the harness. */
|
|
164
|
+
export function clearConnectContext(client) {
|
|
165
|
+
// Read-first: writing through an unreadable config would rewrite it as {}.
|
|
166
|
+
if (connectContextState(client) !== "present")
|
|
167
|
+
return;
|
|
168
|
+
try {
|
|
169
|
+
updateConfig((config) => {
|
|
170
|
+
if (!config.connect?.[client])
|
|
171
|
+
return config;
|
|
172
|
+
const remaining = Object.fromEntries(Object.entries(config.connect).filter(([key]) => key !== client));
|
|
173
|
+
const { connect, ...rest } = config;
|
|
174
|
+
void connect;
|
|
175
|
+
return Object.keys(remaining).length > 0 ? { ...rest, connect: remaining } : rest;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
errLine(`Warning: could not clear connect context: ${e instanceof Error ? e.message : String(e)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
145
182
|
export function persistApiKeyToEnvironment(apiKey, options = {}) {
|
|
146
183
|
const platform = options.platform ?? process.platform;
|
|
147
184
|
if (platform === "win32") {
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as readline from "node:readline/promises";
|
|
2
2
|
import spawn from "cross-spawn";
|
|
3
3
|
import { connectClaudeCode, connectCodex, connectCursor, connectGrok, connectHermes, connectOpenClaw, connectOpencode, } from "../commands/connect.js";
|
|
4
|
-
import { loadConfig, validatedApiKey } from "./auth.js";
|
|
4
|
+
import { clearConnectContext, connectContextState, loadConfig, validatedApiKey, } from "./auth.js";
|
|
5
5
|
import { installTinyFishCli, } from "./cli-install.js";
|
|
6
|
-
import { HARNESS_CAPABILITIES, NATIVE_BY_HARNESS } from "./connect-clients.js";
|
|
6
|
+
import { HARNESS_CAPABILITIES, NATIVE_BY_HARNESS, openclawSkillUninstall, } from "./connect-clients.js";
|
|
7
7
|
import { createStdinPrompt, runCliFallback } from "./connect-fallback.js";
|
|
8
|
-
import { ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, } from "./connect-runtime.js";
|
|
8
|
+
import { ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, spawnRemoval, } from "./connect-runtime.js";
|
|
9
9
|
import { pickHarnesses } from "./connect-picker.js";
|
|
10
10
|
import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, RELOAD_ACTION, } from "./harness-detect.js";
|
|
11
11
|
import { detectHumanInitiated } from "./harness.js";
|
|
@@ -37,16 +37,8 @@ const BROWSER_SIGN_IN = new Set(ALL_HARNESSES.filter((harness) => {
|
|
|
37
37
|
}));
|
|
38
38
|
// Derived: a native add carries the key, or the CLI holds it itself.
|
|
39
39
|
const KEY_AUTH_CAPABLE = new Set(ALL_HARNESSES.filter((harness) => !!NATIVE_BY_HARNESS.get(harness)?.keyAuth || HARNESS_CAPABILITIES[harness].keyHeldByCli));
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
"claude-code": "claude mcp remove tinyfish --scope user",
|
|
43
|
-
codex: "codex mcp remove tinyfish",
|
|
44
|
-
grok: "grok mcp remove tinyfish",
|
|
45
|
-
hermes: "hermes mcp remove tinyfish",
|
|
46
|
-
openclaw: "openclaw skills uninstall @tinyfish/tinyfish --global",
|
|
47
|
-
// OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
|
|
48
|
-
opencode: "remove the tinyfish entry from ~/.config/opencode/opencode.json",
|
|
49
|
-
};
|
|
40
|
+
// 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";
|
|
50
42
|
// A new harness fails to compile until wired here.
|
|
51
43
|
const HARNESS_CONNECTORS = {
|
|
52
44
|
"claude-code": connectClaudeCode,
|
|
@@ -110,13 +102,97 @@ function uninstallPlanText(harness) {
|
|
|
110
102
|
if (harness === "cursor") {
|
|
111
103
|
return "would remove the tinyfish entry from ~/.cursor/mcp.json";
|
|
112
104
|
}
|
|
113
|
-
|
|
105
|
+
if (!uninstallRuns(harness)) {
|
|
106
|
+
return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
|
|
107
|
+
}
|
|
108
|
+
return `would run \`${uninstallPointer(harness)}\``;
|
|
109
|
+
}
|
|
110
|
+
/** Undefined: no harness-CLI removal exists (opencode). */
|
|
111
|
+
function uninstallRuns(harness) {
|
|
112
|
+
if (harness === "openclaw") {
|
|
113
|
+
const removal = openclawSkillUninstall();
|
|
114
|
+
return { command: removal.command, runs: [removal.args], timeoutMs: removal.timeoutMs };
|
|
115
|
+
}
|
|
116
|
+
const native = NATIVE_BY_HARNESS.get(harness);
|
|
117
|
+
if (!native || native.removals.length === 0)
|
|
118
|
+
return undefined;
|
|
119
|
+
return { command: native.command, runs: native.removals.map((r) => r.args) };
|
|
114
120
|
}
|
|
115
|
-
|
|
121
|
+
/** Derived from the same args the spawn runs, so the advice cannot drift. */
|
|
122
|
+
function uninstallPointer(harness) {
|
|
123
|
+
const removal = uninstallRuns(harness);
|
|
124
|
+
if (!removal)
|
|
125
|
+
return OPENCODE_UNINSTALL_POINTER;
|
|
126
|
+
// `;` runs every removal but breaks cmd.exe; `&&` is the reverse — see
|
|
127
|
+
// 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);
|
|
130
|
+
}
|
|
131
|
+
function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
|
|
116
132
|
const detection = { harness, detected: true, configPath, installed: false };
|
|
117
133
|
if (harness !== "cursor") {
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
const removal = uninstallRuns(harness);
|
|
135
|
+
if (!removal) {
|
|
136
|
+
return { ...detection, outcome: "uninstall_pointer", fixCommand: uninstallPointer(harness) };
|
|
137
|
+
}
|
|
138
|
+
// Stale config: the harness binary is gone, so its removal command cannot run.
|
|
139
|
+
// openclaw is exempt — its removal runs through npx, not the harness binary.
|
|
140
|
+
if (harness !== "openclaw" && !binaryOnPath) {
|
|
141
|
+
return {
|
|
142
|
+
...detection,
|
|
143
|
+
outcome: "uninstall_pointer",
|
|
144
|
+
errorMessage: `no \`${removal.command}\` binary on PATH — nothing to run.`,
|
|
145
|
+
fixCommand: `Reinstall ${DISPLAY_NAMES[harness]} first, then: ${uninstallPointer(harness)}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Absent marker = never connected here, so a non-zero removal exit is a no-op.
|
|
149
|
+
// Unreadable config cannot prove that, so it keeps the pointer.
|
|
150
|
+
const connectedBefore = connectContextState(harness) !== "absent";
|
|
151
|
+
let removed = false;
|
|
152
|
+
try {
|
|
153
|
+
for (const args of removal.runs) {
|
|
154
|
+
const result = spawnRemoval(removal.command, args, removal.timeoutMs);
|
|
155
|
+
if (result.status !== 0 && verbose) {
|
|
156
|
+
const details = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
|
|
157
|
+
errLine(`${removal.command} ${args.join(" ")}: ${details}`);
|
|
158
|
+
}
|
|
159
|
+
// Same binary: a spawn-level failure (ENOENT, EPERM) repeats identically.
|
|
160
|
+
if (result.error) {
|
|
161
|
+
return {
|
|
162
|
+
...detection,
|
|
163
|
+
outcome: "uninstalled",
|
|
164
|
+
errorMessage: `could not run \`${removal.command} ${args.join(" ")}\` (${result.error.message}) — nothing removed.`,
|
|
165
|
+
fixCommand: uninstallPointer(harness),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Non-zero is ambiguous (nothing registered vs real failure) — the pointer covers both.
|
|
169
|
+
if (result.status === 0)
|
|
170
|
+
removed = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
if (e instanceof ConnectInterruptedError) {
|
|
175
|
+
return {
|
|
176
|
+
...detection,
|
|
177
|
+
outcome: "interrupted",
|
|
178
|
+
fixCommand: "tinyfish connect --all --uninstall",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
throw e;
|
|
182
|
+
}
|
|
183
|
+
if (removed) {
|
|
184
|
+
// Kept marker makes doctor emit a registration-lost repair that undoes this.
|
|
185
|
+
clearConnectContext(harness);
|
|
186
|
+
return { ...detection, outcome: "uninstalled" };
|
|
187
|
+
}
|
|
188
|
+
if (!connectedBefore)
|
|
189
|
+
return { ...detection, outcome: "uninstall_noop" };
|
|
190
|
+
return {
|
|
191
|
+
...detection,
|
|
192
|
+
outcome: "uninstall_pointer",
|
|
193
|
+
errorMessage: "could not confirm removal (command exited non-zero; there may have been nothing to remove).",
|
|
194
|
+
fixCommand: `Retry by hand: ${uninstallPointer(harness)}`,
|
|
195
|
+
};
|
|
120
196
|
}
|
|
121
197
|
let result;
|
|
122
198
|
try {
|
|
@@ -136,6 +212,7 @@ function uninstallHarness(harness, configPath) {
|
|
|
136
212
|
if (result.status === "unchanged") {
|
|
137
213
|
return { ...detection, outcome: "uninstall_noop" };
|
|
138
214
|
}
|
|
215
|
+
clearConnectContext("cursor");
|
|
139
216
|
return { ...detection, outcome: "uninstalled" };
|
|
140
217
|
}
|
|
141
218
|
async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliInstall, deferOutro) {
|
|
@@ -154,7 +231,7 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
|
|
|
154
231
|
return { ...base, outcome: "dry_run", fixCommand: plan };
|
|
155
232
|
}
|
|
156
233
|
if (opts.uninstall)
|
|
157
|
-
return uninstallHarness(harness, configPath);
|
|
234
|
+
return uninstallHarness(harness, configPath, opts.verbose ?? false, detection.binaryOnPath);
|
|
158
235
|
const isTTY = detectHumanInitiated();
|
|
159
236
|
const apiKey = validatedApiKey(opts.apiKey);
|
|
160
237
|
// A key only leaves the own-OAuth set where the harness can actually carry one.
|
|
@@ -285,7 +362,10 @@ function summaryLine(result, uninstall) {
|
|
|
285
362
|
: `… ${name} — needs a browser sign-in. Run in a terminal: ${result.fixCommand}`;
|
|
286
363
|
}
|
|
287
364
|
if (result.outcome === "uninstall_pointer") {
|
|
288
|
-
|
|
365
|
+
// errorMessage set = a removal was attempted or impossible; absent = no removal command exists.
|
|
366
|
+
return result.errorMessage
|
|
367
|
+
? `… ${name} — ${result.errorMessage} ${result.fixCommand}`
|
|
368
|
+
: `… ${name} — not removed automatically (config is owned by ${name}). Run: ${result.fixCommand}`;
|
|
289
369
|
}
|
|
290
370
|
if (result.outcome === "uninstall_noop") {
|
|
291
371
|
return `… ${name} — no tinyfish entry found, nothing removed`;
|
|
@@ -367,6 +447,12 @@ function actionable(result) {
|
|
|
367
447
|
return result.detected && result.outcome !== "stale_config";
|
|
368
448
|
}
|
|
369
449
|
function computeExitCode(results) {
|
|
450
|
+
// Flag-independent: uninstall interrupts have no other 130 site.
|
|
451
|
+
if (results.some((r) => r.outcome === "interrupted"))
|
|
452
|
+
return 130;
|
|
453
|
+
// A removal that could not run is a failure, not a silent success.
|
|
454
|
+
if (results.some((r) => r.outcome === "uninstalled" && r.errorMessage))
|
|
455
|
+
return 1;
|
|
370
456
|
const detected = results.filter(actionable);
|
|
371
457
|
if (detected.length === 0)
|
|
372
458
|
return 1;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
|
-
import type
|
|
2
|
+
import { type Harness } from "./harness-detect.js";
|
|
3
3
|
import { type AgentClient, type SupportedCommand } from "./connect-runtime.js";
|
|
4
4
|
export declare const OPENCLAW_SKILL_INSTALL_ARGS: readonly string[];
|
|
5
|
+
/** `openclaw skills` has no uninstall; ClawHub removes from the global dir. */
|
|
6
|
+
export declare function openclawSkillUninstall(): {
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
/** npx fetches the package first; the 10s removal bound starves a cold cache. */
|
|
10
|
+
timeoutMs: number;
|
|
11
|
+
};
|
|
5
12
|
export declare const DEFAULT_ONBOARDING_PROMPT: string;
|
|
6
13
|
export declare const OPENCLAW_ONBOARDING_PROMPT: string;
|
|
7
14
|
/** The `skills` CLI's own agent names — not ours, and not every harness has one. */
|
|
@@ -38,7 +45,10 @@ export interface NativeMcpClient extends SupportedCommand {
|
|
|
38
45
|
label: string;
|
|
39
46
|
}[];
|
|
40
47
|
addArgs: (mcpUrl: string, oauthResource: string) => string[];
|
|
48
|
+
/** Blocking terminal walkthrough; runs only behind the TTY gate. */
|
|
41
49
|
launchWalkthrough?: () => void;
|
|
50
|
+
/** Own window, never inherits stdio; reports its own outcome. */
|
|
51
|
+
detachedLaunch?: () => WalkthroughOutcome;
|
|
42
52
|
/** Printed after a successful connect, on both the launch and non-launch paths. */
|
|
43
53
|
postConnectNote?: string;
|
|
44
54
|
}
|
|
@@ -73,5 +83,10 @@ export interface HarnessCapabilities {
|
|
|
73
83
|
}
|
|
74
84
|
/** Required booleans: a new harness or new flag fails to compile undecided. */
|
|
75
85
|
export declare const HARNESS_CAPABILITIES: Record<Harness, HarnessCapabilities>;
|
|
76
|
-
|
|
77
|
-
export
|
|
86
|
+
/** "printed" = handed to the user to paste; the walkthrough was never started for them. */
|
|
87
|
+
export type WalkthroughOutcome = "launched" | "printed";
|
|
88
|
+
/** Awaited before the handover; track+flush must land pre-block. */
|
|
89
|
+
type OnWalkthroughDecided = (outcome: WalkthroughOutcome) => void | Promise<void>;
|
|
90
|
+
export declare function launchNativeMcpClient(client: NativeMcpClient, onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
91
|
+
export declare function launchOpenClawWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
92
|
+
export {};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
|
+
import { SKILL_INSTALL_TIMEOUT_MS } from "./cli-install.js";
|
|
3
|
+
import { detectHumanInitiated } from "./harness.js";
|
|
4
|
+
import { harnessConfigPath } from "./harness-detect.js";
|
|
2
5
|
import { errLine, sanitizeLine } from "./output.js";
|
|
3
6
|
import { spawnStepError, } from "./connect-runtime.js";
|
|
4
7
|
import { TINYFISH_API_KEY_VAR } from "./constants.js";
|
|
@@ -55,9 +58,30 @@ export const OPENCLAW_SKILL_INSTALL_ARGS = [
|
|
|
55
58
|
"--force",
|
|
56
59
|
"--acknowledge-clawhub-risk",
|
|
57
60
|
];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
/** `openclaw skills` has no uninstall; ClawHub removes from the global dir. */
|
|
62
|
+
export function openclawSkillUninstall() {
|
|
63
|
+
return {
|
|
64
|
+
command: "npx",
|
|
65
|
+
args: [
|
|
66
|
+
"-y",
|
|
67
|
+
// Pinned: npx would otherwise execute whatever is newest at run time.
|
|
68
|
+
"clawhub@0.23.3",
|
|
69
|
+
"--workdir",
|
|
70
|
+
harnessConfigPath("openclaw"),
|
|
71
|
+
"uninstall",
|
|
72
|
+
OPENCLAW_SKILL,
|
|
73
|
+
"--yes",
|
|
74
|
+
],
|
|
75
|
+
timeoutMs: SKILL_INSTALL_TIMEOUT_MS,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export const DEFAULT_ONBOARDING_PROMPT = "I just connected TinyFish. Use the MCP server named `tinyfish` for every TinyFish tool call; " +
|
|
79
|
+
"if another TinyFish server is also registered, still use `tinyfish` and do not ask me which. " +
|
|
80
|
+
"Start by asking me what I want to look up on the web, and wait for my reply. Then call " +
|
|
81
|
+
"`guide_next_step` and follow its instructions one step at a time, asking for my input and " +
|
|
82
|
+
"waiting for my reply before each subsequent TinyFish tool call. If a tool call comes back " +
|
|
83
|
+
"blocked or not approved, tell me which tool needs approval and how to grant it, then wait for " +
|
|
84
|
+
"me instead of ending the walkthrough.";
|
|
61
85
|
export const OPENCLAW_ONBOARDING_PROMPT = "I just installed the TinyFish skill. Guide me through it interactively: first ask what topic I " +
|
|
62
86
|
"want to search and wait for my reply before using TinyFish Search. Show me the results, ask " +
|
|
63
87
|
"which result I want to read, and wait before using TinyFish Fetch. Then ask what browser task " +
|
|
@@ -133,9 +157,12 @@ function launchCodexWalkthrough() {
|
|
|
133
157
|
deepLink.searchParams.set("path", process.cwd());
|
|
134
158
|
const result = openExternalUrl(deepLink.toString());
|
|
135
159
|
if (result.error || result.status !== 0) {
|
|
136
|
-
|
|
160
|
+
// Headless box: nothing can open a window; recover like the gated clients.
|
|
161
|
+
printPromptForHandoff("Codex", "codex", DEFAULT_ONBOARDING_PROMPT, "Codex could not be opened here (no `codex://` handler)");
|
|
162
|
+
return "printed";
|
|
137
163
|
}
|
|
138
164
|
errLine("Codex opened with the TinyFish walkthrough ready. Send the prompt to start.");
|
|
165
|
+
return "launched";
|
|
139
166
|
}
|
|
140
167
|
export const CODEX = {
|
|
141
168
|
command: "codex",
|
|
@@ -154,7 +181,7 @@ export const CODEX = {
|
|
|
154
181
|
envVar: TINYFISH_API_KEY_VAR,
|
|
155
182
|
addArgs: (mcpUrl) => [...urlAddArgs(mcpUrl), "--bearer-token-env-var", TINYFISH_API_KEY_VAR],
|
|
156
183
|
},
|
|
157
|
-
|
|
184
|
+
detachedLaunch: launchCodexWalkthrough,
|
|
158
185
|
// Retry only: running it after a finished inline OAuth is a second browser hop.
|
|
159
186
|
loginArgs: ["mcp", "login", "tinyfish"],
|
|
160
187
|
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Codex" }],
|
|
@@ -253,7 +280,7 @@ export const OPENCODE = {
|
|
|
253
280
|
keyAuth: headerKeyAuth,
|
|
254
281
|
// `opencode mcp add` upserts by name and there is no `opencode mcp remove`, so no cleanup step.
|
|
255
282
|
removals: [],
|
|
256
|
-
// OpenCode writes ~/.config/opencode/opencode.
|
|
283
|
+
// OpenCode writes ~/.config/opencode/opencode.json itself; `mcp auth` is the separate OAuth step.
|
|
257
284
|
addArgs: (mcpUrl) => urlAddArgs(mcpUrl),
|
|
258
285
|
loginArgs: ["mcp", "auth", "tinyfish"],
|
|
259
286
|
launchWalkthrough: launchOpencode,
|
|
@@ -331,15 +358,40 @@ export const HARNESS_CAPABILITIES = {
|
|
|
331
358
|
authDeferredAtInstall: false,
|
|
332
359
|
},
|
|
333
360
|
};
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
361
|
+
function printPromptForHandoff(displayName, command, prompt, reason) {
|
|
362
|
+
// "set up", not "connected": sign-in can still be deferred here.
|
|
363
|
+
errLine(`TinyFish is set up in ${displayName}.`);
|
|
364
|
+
errLine(`${reason}, so TinyFish did not launch it. Start ${displayName} (\`${command}\`) yourself ` +
|
|
365
|
+
`and paste this first message:`);
|
|
366
|
+
errLine("");
|
|
367
|
+
errLine(prompt);
|
|
368
|
+
}
|
|
369
|
+
/** One gate + one decide-before-block path for every terminal handover. */
|
|
370
|
+
async function deliverWalkthrough(displayName, command, prompt, handover, onDecided) {
|
|
371
|
+
// Inherited-stdio handover: a non-TTY parent cannot approve tool calls.
|
|
372
|
+
if (!detectHumanInitiated()) {
|
|
373
|
+
await onDecided?.("printed");
|
|
374
|
+
printPromptForHandoff(displayName, command, prompt, `This terminal cannot answer ${displayName}'s tool-approval prompts`);
|
|
375
|
+
return "printed";
|
|
376
|
+
}
|
|
377
|
+
// Decided and delivered first: the handover blocks for the whole session.
|
|
378
|
+
await onDecided?.("launched");
|
|
379
|
+
errLine(`Starting the TinyFish walkthrough in ${displayName}...`);
|
|
380
|
+
handover();
|
|
381
|
+
return "launched";
|
|
382
|
+
}
|
|
383
|
+
export async function launchNativeMcpClient(client, onDecided) {
|
|
384
|
+
if (client.detachedLaunch) {
|
|
385
|
+
// Detached: the open attempt never blocks, so decide after it.
|
|
386
|
+
errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
|
|
387
|
+
const outcome = client.detachedLaunch();
|
|
388
|
+
await onDecided?.(outcome);
|
|
389
|
+
return outcome;
|
|
339
390
|
}
|
|
340
|
-
|
|
391
|
+
return deliverWalkthrough(client.displayName, client.command, DEFAULT_ONBOARDING_PROMPT, () => client.launchWalkthrough
|
|
392
|
+
? client.launchWalkthrough()
|
|
393
|
+
: handOverTerminal(client.command, [DEFAULT_ONBOARDING_PROMPT], client.displayName), onDecided);
|
|
341
394
|
}
|
|
342
|
-
export function launchOpenClawWalkthrough() {
|
|
343
|
-
|
|
344
|
-
handOverTerminal("openclaw", ["chat", "--message", OPENCLAW_ONBOARDING_PROMPT], "OpenClaw");
|
|
395
|
+
export function launchOpenClawWalkthrough(onDecided) {
|
|
396
|
+
return deliverWalkthrough("OpenClaw", "openclaw", OPENCLAW_ONBOARDING_PROMPT, () => handOverTerminal("openclaw", ["chat", "--message", OPENCLAW_ONBOARDING_PROMPT], "OpenClaw"), onDecided);
|
|
345
397
|
}
|
|
@@ -6,7 +6,7 @@ export type ConnectFailureStage = "prerequisite_check" | "registration_cleanup"
|
|
|
6
6
|
export declare const CONNECT_FAILURE_REASONS: readonly ["harness_not_installed", "harness_command_unsupported", "harness_too_old", "key_auth_unsupported", "command_not_found", "timeout", "spawn_error", "nonzero_exit", "invalid_config", "user_abandoned", "process_terminated", "unexpected_error"];
|
|
7
7
|
export type ConnectFailureReason = (typeof CONNECT_FAILURE_REASONS)[number];
|
|
8
8
|
export type ConnectAuthMode = Exclude<AuthMode, AuthMode.Unknown> | "deferred";
|
|
9
|
-
export type ConnectCheckpoint = "prerequisite_ok" | "registered" | "oauth_done" | "cli_installed" | "skill_installed" | "authenticated" | "cli_verified";
|
|
9
|
+
export type ConnectCheckpoint = "prerequisite_ok" | "registered" | "oauth_done" | "cli_installed" | "skill_installed" | "authenticated" | "cli_verified" | "walkthrough_launched" | "walkthrough_prompt_printed";
|
|
10
10
|
export type CliFallbackOutcome = "cli_verified" | "cli_install_failed" | "no_credentials" | "invalid_credentials" | "authentication_failed" | "verification_failed" | "abandoned";
|
|
11
11
|
/** Ctrl+C killed a setup child — abandonment, not error. */
|
|
12
12
|
export declare class ConnectInterruptedError extends Error {
|
|
@@ -33,6 +33,8 @@ export interface SpawnStepResult {
|
|
|
33
33
|
signal?: string | null;
|
|
34
34
|
error?: Error;
|
|
35
35
|
}
|
|
36
|
+
/** Bounded removal spawn shared by install cleanup and --uninstall; Ctrl+C throws. */
|
|
37
|
+
export declare function spawnRemoval(command: string, args: readonly string[], timeoutMs?: number): import("child_process").SpawnSyncReturns<string>;
|
|
36
38
|
/** Classifies a spawn failure; interrupts throw instead. */
|
|
37
39
|
export declare function spawnStepError(message: string, result: SpawnStepResult): ConnectStepError;
|
|
38
40
|
export declare function commandNotFound(error: unknown): boolean;
|
|
@@ -55,6 +55,15 @@ export function throwIfInterrupted(result) {
|
|
|
55
55
|
throw new ConnectInterruptedError("Setup interrupted");
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/** Bounded removal spawn shared by install cleanup and --uninstall; Ctrl+C throws. */
|
|
59
|
+
export function spawnRemoval(command, args, timeoutMs = NON_INTERACTIVE_TIMEOUT_MS) {
|
|
60
|
+
const result = spawn.sync(command, [...args], {
|
|
61
|
+
encoding: "utf8",
|
|
62
|
+
timeout: timeoutMs,
|
|
63
|
+
});
|
|
64
|
+
throwIfInterrupted(result);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
58
67
|
/** Classifies a spawn failure; interrupts throw instead. */
|
|
59
68
|
export function spawnStepError(message, result) {
|
|
60
69
|
throwIfInterrupted(result);
|