@tiny-fish/cli 0.30.1-next.251 → 0.30.1-next.252
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.js +2 -6
- 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 +8 -1
- package/dist/lib/connect-clients.js +20 -1
- package/dist/lib/connect-runtime.d.ts +2 -0
- package/dist/lib/connect-runtime.js +9 -0
- package/package.json +1 -1
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;
|
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. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
|
+
import { SKILL_INSTALL_TIMEOUT_MS } from "./cli-install.js";
|
|
3
|
+
import { harnessConfigPath } from "./harness-detect.js";
|
|
2
4
|
import { errLine, sanitizeLine } from "./output.js";
|
|
3
5
|
import { spawnStepError, } from "./connect-runtime.js";
|
|
4
6
|
import { TINYFISH_API_KEY_VAR } from "./constants.js";
|
|
@@ -55,6 +57,23 @@ export const OPENCLAW_SKILL_INSTALL_ARGS = [
|
|
|
55
57
|
"--force",
|
|
56
58
|
"--acknowledge-clawhub-risk",
|
|
57
59
|
];
|
|
60
|
+
/** `openclaw skills` has no uninstall; ClawHub removes from the global dir. */
|
|
61
|
+
export function openclawSkillUninstall() {
|
|
62
|
+
return {
|
|
63
|
+
command: "npx",
|
|
64
|
+
args: [
|
|
65
|
+
"-y",
|
|
66
|
+
// Pinned: npx would otherwise execute whatever is newest at run time.
|
|
67
|
+
"clawhub@0.23.3",
|
|
68
|
+
"--workdir",
|
|
69
|
+
harnessConfigPath("openclaw"),
|
|
70
|
+
"uninstall",
|
|
71
|
+
OPENCLAW_SKILL,
|
|
72
|
+
"--yes",
|
|
73
|
+
],
|
|
74
|
+
timeoutMs: SKILL_INSTALL_TIMEOUT_MS,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
58
77
|
export const DEFAULT_ONBOARDING_PROMPT = "I just connected TinyFish. Call the guide_next_step tool now to begin. Then follow its " +
|
|
59
78
|
"instructions one step at a time. Ask for my input and wait for my reply before each subsequent " +
|
|
60
79
|
"TinyFish tool call.";
|
|
@@ -253,7 +272,7 @@ export const OPENCODE = {
|
|
|
253
272
|
keyAuth: headerKeyAuth,
|
|
254
273
|
// `opencode mcp add` upserts by name and there is no `opencode mcp remove`, so no cleanup step.
|
|
255
274
|
removals: [],
|
|
256
|
-
// OpenCode writes ~/.config/opencode/opencode.
|
|
275
|
+
// OpenCode writes ~/.config/opencode/opencode.json itself; `mcp auth` is the separate OAuth step.
|
|
257
276
|
addArgs: (mcpUrl) => urlAddArgs(mcpUrl),
|
|
258
277
|
loginArgs: ["mcp", "auth", "tinyfish"],
|
|
259
278
|
launchWalkthrough: launchOpencode,
|
|
@@ -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);
|