@tiny-fish/cli 0.25.1-next.214 → 0.25.1
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/lib/connect-all.js
CHANGED
|
@@ -6,7 +6,7 @@ import { installTinyFishCli, } from "./cli-install.js";
|
|
|
6
6
|
import { NATIVE_MCP_CLIENTS } from "./connect-clients.js";
|
|
7
7
|
import { ensureCliAuthenticated, TINYFISH_CLI_NOT_FOUND_MESSAGE } from "./connect-auth.js";
|
|
8
8
|
import { commandNotFound, ConnectInterruptedError, ConnectStepError, finishSetupCommand, throwIfInterrupted, } from "./connect-runtime.js";
|
|
9
|
-
import { detectInstalledHarnesses, ALL_HARNESSES, } from "./harness-detect.js";
|
|
9
|
+
import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, ALL_HARNESSES, } from "./harness-detect.js";
|
|
10
10
|
import { detectHumanInitiated } from "./harness.js";
|
|
11
11
|
import { emitNotice } from "./notice.js";
|
|
12
12
|
import { removeCursorMcpServer, planCursorWrite } from "./cursor-config.js";
|
|
@@ -145,11 +145,13 @@ function uninstallHarness(harness, configPath) {
|
|
|
145
145
|
return { ...detection, outcome: "uninstalled" };
|
|
146
146
|
}
|
|
147
147
|
async function processHarness(detection, opts, prompt, hoistedCliInstall) {
|
|
148
|
-
const { harness, configPath } = detection;
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
const { harness, configPath, detected } = detection;
|
|
149
|
+
const base = { harness, detected, configPath, installed: false };
|
|
150
|
+
if (!detected)
|
|
151
|
+
return { ...base, outcome: "not_detected" };
|
|
152
|
+
if (!opts.uninstall && isStaleConfig(detection)) {
|
|
153
|
+
return { ...base, outcome: "stale_config", fixCommand: `tinyfish connect ${harness}` };
|
|
151
154
|
}
|
|
152
|
-
const base = { harness, detected: true, configPath, installed: false };
|
|
153
155
|
// dryRun first: --dry-run --uninstall must plan, never reach removeCursorMcpServer().
|
|
154
156
|
if (opts.dryRun) {
|
|
155
157
|
const plan = opts.uninstall
|
|
@@ -222,7 +224,10 @@ function summaryLine(result, uninstall) {
|
|
|
222
224
|
const next = uninstall
|
|
223
225
|
? "nothing to remove"
|
|
224
226
|
: `Install ${name}, then run: tinyfish connect ${result.harness}`;
|
|
225
|
-
return `- ${name} — not detected (
|
|
227
|
+
return `- ${name} — not detected (no \`${HARNESS_COMMANDS[result.harness]}\` on PATH, no ${result.configPath}). ${next}`;
|
|
228
|
+
}
|
|
229
|
+
if (result.outcome === "stale_config") {
|
|
230
|
+
return `… ${name} — config at ${result.configPath} but no \`${HARNESS_COMMANDS[result.harness]}\` on PATH, skipped. Install ${name}, then run: ${result.fixCommand}`;
|
|
226
231
|
}
|
|
227
232
|
if (result.outcome === "not_reached") {
|
|
228
233
|
return `… ${name} — not reached, the run stopped early. Fix: ${result.fixCommand}`;
|
|
@@ -269,8 +274,17 @@ function summaryLine(result, uninstall) {
|
|
|
269
274
|
}
|
|
270
275
|
return `✓ ${name} — installed, ${verifyLabel}`;
|
|
271
276
|
}
|
|
277
|
+
/** Cursor's connect writes its config file; every other harness spawns its binary. */
|
|
278
|
+
// TODO(PF-3668): 4th cursor carve-out here; fold them into one harness capability table.
|
|
279
|
+
function isStaleConfig(detection) {
|
|
280
|
+
return detection.detected && !detection.binaryOnPath && detection.harness !== "cursor";
|
|
281
|
+
}
|
|
282
|
+
/** A stale config dir is a detection signal with nothing behind it. */
|
|
283
|
+
function actionable(result) {
|
|
284
|
+
return result.detected && result.outcome !== "stale_config";
|
|
285
|
+
}
|
|
272
286
|
function computeExitCode(results) {
|
|
273
|
-
const detected = results.filter(
|
|
287
|
+
const detected = results.filter(actionable);
|
|
274
288
|
if (detected.length === 0)
|
|
275
289
|
return 1;
|
|
276
290
|
// Only real install attempts count toward the success/failure denominator.
|
|
@@ -376,7 +390,7 @@ export async function runConnectAll(opts) {
|
|
|
376
390
|
// Machine-global, so once per invocation; cursor's connect never installs it.
|
|
377
391
|
const cliNeeded = !opts.dryRun &&
|
|
378
392
|
!opts.uninstall &&
|
|
379
|
-
detections.some((d) => d.detected && d.harness !== "cursor" && willAttempt(d.harness));
|
|
393
|
+
detections.some((d) => d.detected && d.binaryOnPath && d.harness !== "cursor" && willAttempt(d.harness));
|
|
380
394
|
let takeHoistedCli;
|
|
381
395
|
if (cliNeeded) {
|
|
382
396
|
errLine("First: the TinyFish CLI, shared by every harness.");
|
|
@@ -422,10 +436,14 @@ export async function runConnectAll(opts) {
|
|
|
422
436
|
for (const detection of detections) {
|
|
423
437
|
if (results.some((r) => r.harness === detection.harness))
|
|
424
438
|
continue;
|
|
439
|
+
const { harness, detected, configPath } = detection;
|
|
440
|
+
const staleConfig = isStaleConfig(detection);
|
|
425
441
|
results.push({
|
|
426
|
-
|
|
442
|
+
harness,
|
|
443
|
+
detected,
|
|
444
|
+
configPath,
|
|
427
445
|
installed: false,
|
|
428
|
-
outcome:
|
|
446
|
+
outcome: staleConfig ? "stale_config" : detected ? "not_reached" : "not_detected",
|
|
429
447
|
fixCommand: `tinyfish connect ${detection.harness}`,
|
|
430
448
|
});
|
|
431
449
|
}
|
|
@@ -433,7 +451,7 @@ export async function runConnectAll(opts) {
|
|
|
433
451
|
outLine("TinyFish setup summary:");
|
|
434
452
|
for (const result of results)
|
|
435
453
|
outLine(summaryLine(result, opts.uninstall));
|
|
436
|
-
if (results.
|
|
454
|
+
if (!results.some(actionable)) {
|
|
437
455
|
if (!opts.dryRun && !opts.uninstall) {
|
|
438
456
|
try {
|
|
439
457
|
return await runCliFallback(opts, prompt);
|
|
@@ -446,8 +464,11 @@ export async function runConnectAll(opts) {
|
|
|
446
464
|
return 130;
|
|
447
465
|
}
|
|
448
466
|
}
|
|
467
|
+
const stale = results.filter((r) => r.outcome === "stale_config");
|
|
449
468
|
outLine("");
|
|
450
|
-
outLine(
|
|
469
|
+
outLine(stale.length > 0
|
|
470
|
+
? `No runnable harness found. Reinstall ${stale.map((r) => DISPLAY_NAMES[r.harness]).join(", ")}, or install one of: ${ALL_HARNESSES.join(", ")}`
|
|
471
|
+
: "No supported harness found. Install one of: " + ALL_HARNESSES.join(", "));
|
|
451
472
|
if (!opts.uninstall)
|
|
452
473
|
outLine("Then run: npx @tiny-fish/cli connect --all");
|
|
453
474
|
return 1;
|
|
@@ -455,7 +476,9 @@ export async function runConnectAll(opts) {
|
|
|
455
476
|
if (!opts.dryRun && !opts.uninstall) {
|
|
456
477
|
const harnessTelemetry = results.map((r) => ({
|
|
457
478
|
harness: r.harness,
|
|
458
|
-
|
|
479
|
+
// TODO(PF-3668): send outcome per harness instead; the frontend strips
|
|
480
|
+
// unknown harness keys, so the CLI can ship it before the reader does.
|
|
481
|
+
detected: actionable(r),
|
|
459
482
|
installed: r.installed,
|
|
460
483
|
verify_depth: r.verifyDepth ?? null,
|
|
461
484
|
verify_ok: r.verifyOk ?? null,
|
|
@@ -484,11 +507,12 @@ const FIRST_TASK_PROMPT = "My TinyFish setup just finished — verify it with a
|
|
|
484
507
|
"me a two-line summary. Show which tools you call.";
|
|
485
508
|
// Launchable seeded agents, best-first — cursor-agent shares ~/.cursor/mcp.json with the IDE.
|
|
486
509
|
const LAUNCHERS = [
|
|
487
|
-
{ harness: "cursor",
|
|
488
|
-
{ harness: "claude-code",
|
|
510
|
+
{ harness: "cursor", args: [FIRST_TASK_PROMPT] },
|
|
511
|
+
{ harness: "claude-code", args: [FIRST_TASK_PROMPT] },
|
|
489
512
|
];
|
|
490
513
|
async function launchFirstTask(results) {
|
|
491
|
-
const candidates = LAUNCHERS.filter((l) => results.some((r) => r.harness === l.harness && r.installed)
|
|
514
|
+
const candidates = LAUNCHERS.filter((l) => results.some((r) => r.harness === l.harness && r.installed) &&
|
|
515
|
+
commandOnPath(HARNESS_COMMANDS[l.harness]));
|
|
492
516
|
if (candidates.length === 0)
|
|
493
517
|
return false;
|
|
494
518
|
// Several installs: ask rather than pick a favorite; plain enter skips.
|
|
@@ -514,6 +538,6 @@ async function launchFirstTask(results) {
|
|
|
514
538
|
if (!chosen)
|
|
515
539
|
return false;
|
|
516
540
|
outLine(`Starting ${DISPLAY_NAMES[chosen.harness]} with your first TinyFish task...`);
|
|
517
|
-
const result = spawn.sync(chosen.
|
|
541
|
+
const result = spawn.sync(HARNESS_COMMANDS[chosen.harness], chosen.args, { stdio: "inherit" });
|
|
518
542
|
return !result.error;
|
|
519
543
|
}
|
|
@@ -53,5 +53,5 @@ export declare const OPENCODE: NativeMcpClient;
|
|
|
53
53
|
/** Every native MCP client. Derive from this, so a new harness cannot be missed. */
|
|
54
54
|
export declare const NATIVE_MCP_CLIENTS: readonly NativeMcpClient[];
|
|
55
55
|
export declare const OPENCLAW: SupportedCommand;
|
|
56
|
-
export declare function launchNativeMcpClient(client: NativeMcpClient):
|
|
57
|
-
export declare function launchOpenClawWalkthrough():
|
|
56
|
+
export declare function launchNativeMcpClient(client: NativeMcpClient): void;
|
|
57
|
+
export declare function launchOpenClawWalkthrough(): void;
|
|
@@ -192,6 +192,13 @@ export const GROK = {
|
|
|
192
192
|
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Grok" }],
|
|
193
193
|
addArgs: grokAddArgs,
|
|
194
194
|
};
|
|
195
|
+
// Handover: spawn errors are ours; the exit is the user's.
|
|
196
|
+
function handOverTerminal(command, args, displayName) {
|
|
197
|
+
const result = spawn.sync(command, args, { stdio: "inherit" });
|
|
198
|
+
if (result.error) {
|
|
199
|
+
throw spawnStepError(`Could not launch ${displayName}`, result);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
195
202
|
function launchHermesWalkthrough() {
|
|
196
203
|
const seedResult = spawn.sync("hermes", ["chat", "-Q", "-q", DEFAULT_ONBOARDING_PROMPT], {
|
|
197
204
|
encoding: "utf8",
|
|
@@ -205,15 +212,7 @@ function launchHermesWalkthrough() {
|
|
|
205
212
|
if (!sessionId) {
|
|
206
213
|
throw new Error("Hermes did not return a walkthrough session ID");
|
|
207
214
|
}
|
|
208
|
-
|
|
209
|
-
if (result.error) {
|
|
210
|
-
throw spawnStepError("Could not launch Hermes", result);
|
|
211
|
-
}
|
|
212
|
-
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
213
|
-
return;
|
|
214
|
-
if (result.status !== 0) {
|
|
215
|
-
throw spawnStepError(`Hermes walkthrough exited with status ${result.status ?? "unknown"}`, result);
|
|
216
|
-
}
|
|
215
|
+
handOverTerminal("hermes", ["--resume", sessionId], "Hermes");
|
|
217
216
|
}
|
|
218
217
|
export const HERMES = {
|
|
219
218
|
command: "hermes",
|
|
@@ -235,17 +234,7 @@ export const HERMES = {
|
|
|
235
234
|
// would be read as a folder. The `--prompt` flag seeds the interactive TUI with a first message
|
|
236
235
|
// (opencode.ai/docs/cli), so the onboarding guide fires just like the other agents.
|
|
237
236
|
function launchOpencode() {
|
|
238
|
-
|
|
239
|
-
stdio: "inherit",
|
|
240
|
-
});
|
|
241
|
-
if (result.error) {
|
|
242
|
-
throw spawnStepError("Could not launch OpenCode", result);
|
|
243
|
-
}
|
|
244
|
-
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
245
|
-
return;
|
|
246
|
-
if (result.status !== 0) {
|
|
247
|
-
throw spawnStepError(`OpenCode exited with status ${result.status ?? "unknown"}`, result);
|
|
248
|
-
}
|
|
237
|
+
handOverTerminal("opencode", ["--prompt", DEFAULT_ONBOARDING_PROMPT], "OpenCode");
|
|
249
238
|
}
|
|
250
239
|
export const OPENCODE = {
|
|
251
240
|
command: "opencode",
|
|
@@ -288,31 +277,11 @@ export function launchNativeMcpClient(client) {
|
|
|
288
277
|
errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
|
|
289
278
|
if (client.launchWalkthrough) {
|
|
290
279
|
client.launchWalkthrough();
|
|
291
|
-
return
|
|
292
|
-
}
|
|
293
|
-
const result = spawn.sync(client.command, [DEFAULT_ONBOARDING_PROMPT], { stdio: "inherit" });
|
|
294
|
-
if (result.error) {
|
|
295
|
-
throw spawnStepError(`Could not launch ${client.displayName}`, result);
|
|
296
|
-
}
|
|
297
|
-
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
298
|
-
return false;
|
|
299
|
-
if (result.status !== 0) {
|
|
300
|
-
throw spawnStepError(`${client.displayName} walkthrough exited with status ${result.status ?? "unknown"}`, result);
|
|
280
|
+
return;
|
|
301
281
|
}
|
|
302
|
-
|
|
282
|
+
handOverTerminal(client.command, [DEFAULT_ONBOARDING_PROMPT], client.displayName);
|
|
303
283
|
}
|
|
304
284
|
export function launchOpenClawWalkthrough() {
|
|
305
285
|
errLine("Starting the TinyFish walkthrough in OpenClaw...");
|
|
306
|
-
|
|
307
|
-
stdio: "inherit",
|
|
308
|
-
});
|
|
309
|
-
if (result.error) {
|
|
310
|
-
throw spawnStepError("Could not launch OpenClaw", result);
|
|
311
|
-
}
|
|
312
|
-
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
313
|
-
return false;
|
|
314
|
-
if (result.status !== 0) {
|
|
315
|
-
throw spawnStepError(`OpenClaw walkthrough exited with status ${result.status ?? "unknown"}`, result);
|
|
316
|
-
}
|
|
317
|
-
return true;
|
|
286
|
+
handOverTerminal("openclaw", ["chat", "--message", OPENCLAW_ONBOARDING_PROMPT], "OpenClaw");
|
|
318
287
|
}
|
|
@@ -10,6 +10,8 @@ export declare enum AuthMode {
|
|
|
10
10
|
OAuth = "oauth",
|
|
11
11
|
Unknown = "unknown"
|
|
12
12
|
}
|
|
13
|
+
export declare const HARNESS_COMMANDS: Record<Harness, string>;
|
|
14
|
+
export declare function commandOnPath(command: string): boolean;
|
|
13
15
|
export declare function harnessConfigPath(harness: Harness): string;
|
|
14
16
|
/** For reason strings that name a location; keeps them in sync with the table above. */
|
|
15
17
|
export declare function harnessDisplayPath(harness: Harness): string;
|
|
@@ -17,5 +19,7 @@ export interface HarnessDetection {
|
|
|
17
19
|
harness: Harness;
|
|
18
20
|
detected: boolean;
|
|
19
21
|
configPath: string;
|
|
22
|
+
/** False with `detected` true means a stale config dir: nothing to run. */
|
|
23
|
+
binaryOnPath: boolean;
|
|
20
24
|
}
|
|
21
25
|
export declare function detectInstalledHarnesses(): HarnessDetection[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "path";
|
|
4
|
+
import which from "which";
|
|
4
5
|
export const ALL_HARNESSES = [
|
|
5
6
|
"claude-code",
|
|
6
7
|
"codex",
|
|
@@ -23,7 +24,6 @@ export var AuthMode;
|
|
|
23
24
|
AuthMode["OAuth"] = "oauth";
|
|
24
25
|
AuthMode["Unknown"] = "unknown";
|
|
25
26
|
})(AuthMode || (AuthMode = {}));
|
|
26
|
-
// Presence detection only — dir existence means "installed", nothing more.
|
|
27
27
|
const CONFIG_DIRS = {
|
|
28
28
|
"claude-code": ".claude",
|
|
29
29
|
codex: ".codex",
|
|
@@ -33,6 +33,26 @@ const CONFIG_DIRS = {
|
|
|
33
33
|
openclaw: ".openclaw",
|
|
34
34
|
opencode: ".config/opencode",
|
|
35
35
|
};
|
|
36
|
+
// Harness id is not the binary name (cursor-agent).
|
|
37
|
+
// TODO(PF-3668): a same-named non-harness binary still reads as installed.
|
|
38
|
+
export const HARNESS_COMMANDS = {
|
|
39
|
+
"claude-code": "claude",
|
|
40
|
+
codex: "codex",
|
|
41
|
+
cursor: "cursor-agent",
|
|
42
|
+
grok: "grok",
|
|
43
|
+
hermes: "hermes",
|
|
44
|
+
openclaw: "openclaw",
|
|
45
|
+
opencode: "opencode",
|
|
46
|
+
};
|
|
47
|
+
// cross-spawn resolves the same way, so detection cannot disagree with the spawn.
|
|
48
|
+
export function commandOnPath(command) {
|
|
49
|
+
try {
|
|
50
|
+
return which.sync(command, { nothrow: true }) !== null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
36
56
|
export function harnessConfigPath(harness) {
|
|
37
57
|
return path.join(os.homedir(), CONFIG_DIRS[harness]); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
|
|
38
58
|
}
|
|
@@ -43,13 +63,20 @@ export function harnessDisplayPath(harness) {
|
|
|
43
63
|
export function detectInstalledHarnesses() {
|
|
44
64
|
return ALL_HARNESSES.map((harness) => {
|
|
45
65
|
const configPath = harnessConfigPath(harness);
|
|
46
|
-
let
|
|
66
|
+
let configDirExists;
|
|
47
67
|
try {
|
|
48
|
-
|
|
68
|
+
configDirExists = fs.statSync(configPath).isDirectory();
|
|
49
69
|
}
|
|
50
70
|
catch {
|
|
51
|
-
|
|
71
|
+
configDirExists = false;
|
|
52
72
|
}
|
|
53
|
-
|
|
73
|
+
// Config dir means ran once; PATH means runnable now.
|
|
74
|
+
const binaryOnPath = commandOnPath(HARNESS_COMMANDS[harness]);
|
|
75
|
+
return {
|
|
76
|
+
harness,
|
|
77
|
+
detected: configDirExists || binaryOnPath,
|
|
78
|
+
configPath,
|
|
79
|
+
binaryOnPath,
|
|
80
|
+
};
|
|
54
81
|
});
|
|
55
82
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiny-fish/cli",
|
|
3
|
-
"version": "0.25.1
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "TinyFish CLI — run web automations from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,12 +37,14 @@
|
|
|
37
37
|
"commander": "^12.0.0",
|
|
38
38
|
"cross-spawn": "^7.0.6",
|
|
39
39
|
"globals": "^17.4.0",
|
|
40
|
+
"which": "^2.0.2",
|
|
40
41
|
"zod": "^4.3.6"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@eslint/js": "^10.0.1",
|
|
44
45
|
"@types/cross-spawn": "^6.0.6",
|
|
45
46
|
"@types/node": "^20.0.0",
|
|
47
|
+
"@types/which": "^2.0.2",
|
|
46
48
|
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
47
49
|
"@typescript-eslint/parser": "^8.57.2",
|
|
48
50
|
"eslint": "^10.1.0",
|