@tiny-fish/cli 0.15.1-next.161 → 0.16.1-next.163
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/doctor.d.ts +2 -2
- package/dist/commands/doctor.js +8 -5
- package/dist/lib/connect-all.js +9 -4
- package/dist/lib/doctor-report.d.ts +6 -0
- package/dist/lib/doctor-report.js +2 -2
- package/dist/lib/harness-detect.d.ts +5 -5
- package/dist/lib/harness-detect.js +10 -2
- package/dist/lib/registration-detect.d.ts +3 -3
- package/dist/lib/registration-detect.js +81 -7
- package/dist/lib/setup-telemetry.d.ts +2 -0
- package/dist/lib/setup-telemetry.js +4 -4
- package/package.json +2 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { type DoctorRepair, type DoctorReport } from "../lib/doctor-report.js";
|
|
3
|
-
import { type
|
|
3
|
+
import { type Harness } from "../lib/harness-detect.js";
|
|
4
4
|
export interface DoctorOptions {
|
|
5
5
|
mcpUrl: string;
|
|
6
|
-
harness?:
|
|
6
|
+
harness?: Harness;
|
|
7
7
|
fix?: boolean;
|
|
8
8
|
yes?: boolean;
|
|
9
9
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,12 +2,12 @@ import spawn from "cross-spawn";
|
|
|
2
2
|
import { apiKeyStatus } from "../lib/auth.js";
|
|
3
3
|
import { CLI_VERSION } from "../lib/constants.js";
|
|
4
4
|
import { DOCTOR_COULD_NOT_RUN, DOCTOR_SCHEMA_VERSION, doctorReportSchema, exitCodeFor, renderPretty, } from "../lib/doctor-report.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ALL_HARNESSES } from "../lib/harness-detect.js";
|
|
6
6
|
import { detectHumanInitiated } from "../lib/harness.js";
|
|
7
7
|
import { err, errLine, out, outLine } from "../lib/output.js";
|
|
8
8
|
import { detectRegistrations } from "../lib/registration-detect.js";
|
|
9
9
|
import { verifyMcpAuth, verifyMcpHealth } from "../lib/verify.js";
|
|
10
|
-
import { connectClaudeCode, connectCodex, connectCursor, connectHermes, connectOpenClaw, DEFAULT_MCP_URL, } from "./connect.js";
|
|
10
|
+
import { connectClaudeCode, connectCodex, connectCursor, connectHermes, connectOpenClaw, connectOpencode, DEFAULT_MCP_URL, } from "./connect.js";
|
|
11
11
|
// A green auth mode with a red auth call proves nothing, so the call's verdict gates the claim.
|
|
12
12
|
function provesHarnessReach(status, authCallPassed, healthOk, mcpUrl) {
|
|
13
13
|
// An endpoint doctor could not reach proves nothing about a harness pointed at it.
|
|
@@ -70,7 +70,9 @@ function registrationVerdict(status, mcpUrl) {
|
|
|
70
70
|
detail: `registered, but points at ${elsewhere} rather than ${endpointOf(mcpUrl)}`,
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
// A probe that had to explain itself to reach `yes` is the only thing that explains `unknown`.
|
|
74
|
+
const because = status.reason ? ` (${status.reason})` : "";
|
|
75
|
+
return { status: "pass", detail: `registered, auth mode ${status.authMode}${because}` };
|
|
74
76
|
}
|
|
75
77
|
// Connected once and now absent is a broken install; never connected is just not set up.
|
|
76
78
|
return status.connectedBefore
|
|
@@ -204,6 +206,7 @@ const CONNECTORS = {
|
|
|
204
206
|
cursor: (opts) => connectCursor(opts),
|
|
205
207
|
hermes: (opts) => connectHermes({ ...opts, launch: false }),
|
|
206
208
|
openclaw: (opts) => connectOpenClaw({ ...opts, launch: false }),
|
|
209
|
+
opencode: (opts) => connectOpencode({ ...opts, launch: false }),
|
|
207
210
|
};
|
|
208
211
|
/** Dispatches on `action`, never on a null harness: that conflation made the credential fix a no-op. */
|
|
209
212
|
async function runRepair(repair, mcpUrl) {
|
|
@@ -256,12 +259,12 @@ export function registerDoctor(program) {
|
|
|
256
259
|
.command("doctor")
|
|
257
260
|
.description("Diagnose your TinyFish setup")
|
|
258
261
|
.option("--pretty", "Human-readable check list instead of JSON")
|
|
259
|
-
.option("--harness <name>", `Restrict to one harness (${
|
|
262
|
+
.option("--harness <name>", `Restrict to one harness (${ALL_HARNESSES.join(", ")})`)
|
|
260
263
|
.option("--url <mcpUrl>", "MCP endpoint override")
|
|
261
264
|
.option("--fix", "Repair what can be repaired by re-running tinyfish connect")
|
|
262
265
|
.option("--yes", "Allow --fix to run without a terminal (unattended-safe repairs only)")
|
|
263
266
|
.action(async (options) => {
|
|
264
|
-
if (options.harness && !
|
|
267
|
+
if (options.harness && !ALL_HARNESSES.includes(options.harness)) {
|
|
265
268
|
err({ error: `Unknown --harness value: ${options.harness}` });
|
|
266
269
|
process.exitCode = DOCTOR_COULD_NOT_RUN;
|
|
267
270
|
return;
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as readline from "node:readline/promises";
|
|
2
2
|
import spawn from "cross-spawn";
|
|
3
|
-
import { connectClaudeCode, connectCodex, connectCursor, connectHermes, connectOpenClaw, } from "../commands/connect.js";
|
|
3
|
+
import { connectClaudeCode, connectCodex, connectCursor, connectHermes, connectOpenClaw, connectOpencode, } from "../commands/connect.js";
|
|
4
4
|
import { validatedApiKey } from "./auth.js";
|
|
5
|
-
import { detectInstalledHarnesses,
|
|
5
|
+
import { detectInstalledHarnesses, ALL_HARNESSES, } from "./harness-detect.js";
|
|
6
6
|
import { detectHumanInitiated } from "./harness.js";
|
|
7
7
|
import { emitNotice } from "./notice.js";
|
|
8
8
|
import { removeCursorMcpServer, planCursorWrite } from "./cursor-config.js";
|
|
@@ -15,12 +15,13 @@ const DISPLAY_NAMES = {
|
|
|
15
15
|
cursor: "Cursor",
|
|
16
16
|
hermes: "Hermes",
|
|
17
17
|
openclaw: "OpenClaw",
|
|
18
|
+
opencode: "OpenCode",
|
|
18
19
|
};
|
|
19
20
|
// `codex mcp add` succeeds unauthenticated; OAuth lands at first tool use.
|
|
20
21
|
const AUTH_DEFERRED_AT_INSTALL = new Set(["codex"]);
|
|
21
22
|
// Harnesses that drive their own OAuth at install — a stored TinyFish key says nothing
|
|
22
23
|
// about their session, so it must never unlock a headless run for them.
|
|
23
|
-
const HARNESS_OWN_AUTH = new Set(["claude-code", "codex", "hermes"]);
|
|
24
|
+
const HARNESS_OWN_AUTH = new Set(["claude-code", "codex", "hermes", "opencode"]);
|
|
24
25
|
const AUTH_CHECK_CAPABLE = new Set(["cursor", "openclaw"]);
|
|
25
26
|
// Real removal commands — harness id is not the binary name.
|
|
26
27
|
const UNINSTALL_POINTERS = {
|
|
@@ -28,6 +29,8 @@ const UNINSTALL_POINTERS = {
|
|
|
28
29
|
codex: "codex mcp remove tinyfish",
|
|
29
30
|
hermes: "hermes mcp remove tinyfish",
|
|
30
31
|
openclaw: "openclaw skills uninstall @tinyfish/tinyfish --global",
|
|
32
|
+
// OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
|
|
33
|
+
opencode: "remove the tinyfish entry from ~/.config/opencode/opencode.json",
|
|
31
34
|
};
|
|
32
35
|
async function runHarnessConnect(harness, opts) {
|
|
33
36
|
const base = { apiKey: opts.apiKey, mcpUrl: opts.mcpUrl, launch: false };
|
|
@@ -39,6 +42,8 @@ async function runHarnessConnect(harness, opts) {
|
|
|
39
42
|
return connectHermes(base);
|
|
40
43
|
if (harness === "openclaw")
|
|
41
44
|
return connectOpenClaw({ mcpUrl: opts.mcpUrl, launch: false });
|
|
45
|
+
if (harness === "opencode")
|
|
46
|
+
return connectOpencode(base);
|
|
42
47
|
return connectCursor({ apiKey: opts.apiKey, mcpUrl: opts.mcpUrl });
|
|
43
48
|
}
|
|
44
49
|
async function verifyHarness(harness, mcpUrl, apiKey) {
|
|
@@ -262,7 +267,7 @@ export async function runConnectAll(opts) {
|
|
|
262
267
|
outLine(summaryLine(result, opts.uninstall));
|
|
263
268
|
if (results.every((r) => !r.detected)) {
|
|
264
269
|
outLine("");
|
|
265
|
-
outLine("No supported harness found. Install one of: " +
|
|
270
|
+
outLine("No supported harness found. Install one of: " + ALL_HARNESSES.join(", "));
|
|
266
271
|
if (!opts.uninstall)
|
|
267
272
|
outLine("Then run: npx @tiny-fish/cli connect --all");
|
|
268
273
|
return 1;
|
|
@@ -22,6 +22,7 @@ declare const doctorCheckSchema: z.ZodObject<{
|
|
|
22
22
|
cursor: "cursor";
|
|
23
23
|
codex: "codex";
|
|
24
24
|
hermes: "hermes";
|
|
25
|
+
opencode: "opencode";
|
|
25
26
|
"claude-code": "claude-code";
|
|
26
27
|
}>>;
|
|
27
28
|
}, z.core.$strip>;
|
|
@@ -31,6 +32,7 @@ declare const doctorHarnessSchema: z.ZodObject<{
|
|
|
31
32
|
cursor: "cursor";
|
|
32
33
|
codex: "codex";
|
|
33
34
|
hermes: "hermes";
|
|
35
|
+
opencode: "opencode";
|
|
34
36
|
"claude-code": "claude-code";
|
|
35
37
|
}>;
|
|
36
38
|
detected: z.ZodBoolean;
|
|
@@ -57,6 +59,7 @@ declare const doctorRepairSchema: z.ZodObject<{
|
|
|
57
59
|
cursor: "cursor";
|
|
58
60
|
codex: "codex";
|
|
59
61
|
hermes: "hermes";
|
|
62
|
+
opencode: "opencode";
|
|
60
63
|
"claude-code": "claude-code";
|
|
61
64
|
}>>;
|
|
62
65
|
command: z.ZodString;
|
|
@@ -81,6 +84,7 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
81
84
|
cursor: "cursor";
|
|
82
85
|
codex: "codex";
|
|
83
86
|
hermes: "hermes";
|
|
87
|
+
opencode: "opencode";
|
|
84
88
|
"claude-code": "claude-code";
|
|
85
89
|
}>>;
|
|
86
90
|
}, z.core.$strip>>;
|
|
@@ -90,6 +94,7 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
90
94
|
cursor: "cursor";
|
|
91
95
|
codex: "codex";
|
|
92
96
|
hermes: "hermes";
|
|
97
|
+
opencode: "opencode";
|
|
93
98
|
"claude-code": "claude-code";
|
|
94
99
|
}>;
|
|
95
100
|
detected: z.ZodBoolean;
|
|
@@ -116,6 +121,7 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
116
121
|
cursor: "cursor";
|
|
117
122
|
codex: "codex";
|
|
118
123
|
hermes: "hermes";
|
|
124
|
+
opencode: "opencode";
|
|
119
125
|
"claude-code": "claude-code";
|
|
120
126
|
}>>;
|
|
121
127
|
command: z.ZodString;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { ALL_HARNESSES } from "./harness-detect.js";
|
|
3
3
|
/** Bumped whenever a consumer could misread the payload; the cookbook skill releases separately. */
|
|
4
4
|
export const DOCTOR_SCHEMA_VERSION = 1;
|
|
5
5
|
const checkStatusSchema = z.enum(["pass", "fail", "warn", "skip"]);
|
|
6
|
-
const harnessSchema = z.enum(
|
|
6
|
+
const harnessSchema = z.enum(ALL_HARNESSES);
|
|
7
7
|
const doctorCheckSchema = z.object({
|
|
8
8
|
id: z.string(),
|
|
9
9
|
title: z.string(),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
export type
|
|
3
|
-
export declare function harnessConfigPath(harness:
|
|
1
|
+
export declare const ALL_HARNESSES: readonly ["claude-code", "codex", "cursor", "hermes", "openclaw", "opencode"];
|
|
2
|
+
export type Harness = (typeof ALL_HARNESSES)[number];
|
|
3
|
+
export declare function harnessConfigPath(harness: Harness): string;
|
|
4
4
|
/** For reason strings that name a location; keeps them in sync with the table above. */
|
|
5
|
-
export declare function harnessDisplayPath(harness:
|
|
5
|
+
export declare function harnessDisplayPath(harness: Harness): string;
|
|
6
6
|
export interface HarnessDetection {
|
|
7
|
-
harness:
|
|
7
|
+
harness: Harness;
|
|
8
8
|
detected: boolean;
|
|
9
9
|
configPath: string;
|
|
10
10
|
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
export const
|
|
4
|
+
export const ALL_HARNESSES = [
|
|
5
|
+
"claude-code",
|
|
6
|
+
"codex",
|
|
7
|
+
"cursor",
|
|
8
|
+
"hermes",
|
|
9
|
+
"openclaw",
|
|
10
|
+
"opencode",
|
|
11
|
+
];
|
|
5
12
|
// Presence detection only — dir existence means "installed", nothing more.
|
|
6
13
|
const CONFIG_DIRS = {
|
|
7
14
|
"claude-code": ".claude",
|
|
@@ -9,6 +16,7 @@ const CONFIG_DIRS = {
|
|
|
9
16
|
cursor: ".cursor",
|
|
10
17
|
hermes: ".hermes",
|
|
11
18
|
openclaw: ".openclaw",
|
|
19
|
+
opencode: ".config/opencode",
|
|
12
20
|
};
|
|
13
21
|
export function harnessConfigPath(harness) {
|
|
14
22
|
return path.join(os.homedir(), CONFIG_DIRS[harness]); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
|
|
@@ -18,7 +26,7 @@ export function harnessDisplayPath(harness) {
|
|
|
18
26
|
return `~/${CONFIG_DIRS[harness]}`;
|
|
19
27
|
}
|
|
20
28
|
export function detectInstalledHarnesses() {
|
|
21
|
-
return
|
|
29
|
+
return ALL_HARNESSES.map((harness) => {
|
|
22
30
|
const configPath = harnessConfigPath(harness);
|
|
23
31
|
let detected;
|
|
24
32
|
try {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Harness } from "./harness-detect.js";
|
|
2
2
|
export type Registered = "yes" | "no" | "unknown";
|
|
3
3
|
export type AuthMode = "api-key" | "oauth" | "unknown";
|
|
4
4
|
export interface RegistrationStatus {
|
|
5
|
-
harness:
|
|
5
|
+
harness: Harness;
|
|
6
6
|
detected: boolean;
|
|
7
7
|
registered: Registered;
|
|
8
8
|
authMode: AuthMode;
|
|
@@ -12,4 +12,4 @@ export interface RegistrationStatus {
|
|
|
12
12
|
reason?: string;
|
|
13
13
|
}
|
|
14
14
|
/** Never throws; a failed probe is `unknown` with a reason, so no caller can read it as healthy. */
|
|
15
|
-
export declare function detectRegistrations(harnesses?:
|
|
15
|
+
export declare function detectRegistrations(harnesses?: Harness[]): RegistrationStatus[];
|
|
@@ -24,6 +24,10 @@ const UNSUPPORTED_SUBCOMMAND = /unknown command|unrecognized subcommand|invalid
|
|
|
24
24
|
// `claude mcp get` exits 1 for a usage error too, so only this message is evidence of absence.
|
|
25
25
|
const NO_SUCH_SERVER = /no (?:mcp )?server named/i;
|
|
26
26
|
const API_KEY_HEADER = /x-api-key/i;
|
|
27
|
+
// OpenCode colours its list even through a pipe, putting `\x1b[34m` between the line start and
|
|
28
|
+
// the server name, so an unstripped probe reads a registered harness as absent.
|
|
29
|
+
// eslint-disable-next-line no-control-regex
|
|
30
|
+
const ANSI_SGR = /\x1b\[[0-9;]*m/g;
|
|
27
31
|
// The report is pasted in public, so every reason is authored here. A spawn error's own
|
|
28
32
|
// message quotes absolute paths, which is how a home directory reaches the payload.
|
|
29
33
|
function spawnFailureReason(command, code) {
|
|
@@ -34,7 +38,13 @@ function spawnFailureReason(command, code) {
|
|
|
34
38
|
return `could not run \`${command}\``;
|
|
35
39
|
}
|
|
36
40
|
function runProbe(command, args) {
|
|
37
|
-
const result = spawn.sync(command, args, {
|
|
41
|
+
const result = spawn.sync(command, args, {
|
|
42
|
+
encoding: "utf8",
|
|
43
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
44
|
+
// Belt and braces only: agents export FORCE_COLOR, and `NO_COLOR` does not fully silence
|
|
45
|
+
// OpenCode, so the strip below stays load-bearing rather than a fallback.
|
|
46
|
+
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
47
|
+
});
|
|
38
48
|
if (result.error) {
|
|
39
49
|
const code = result.error.code;
|
|
40
50
|
// A timeout arrives as ETIMEDOUT here, not only as a signal, so it must be named before
|
|
@@ -48,12 +58,36 @@ function runProbe(command, args) {
|
|
|
48
58
|
if (result.signal) {
|
|
49
59
|
return { outcome: "unavailable", reason: `\`${command}\` was killed by ${result.signal}` };
|
|
50
60
|
}
|
|
51
|
-
|
|
61
|
+
// Both streams, since a colour code inside a JSON payload breaks the parse just as it
|
|
62
|
+
// breaks an anchored match. Valid JSON never carries a raw ESC byte, so this cannot corrupt it.
|
|
63
|
+
const stdout = (result.stdout ?? "").replace(ANSI_SGR, "");
|
|
52
64
|
return {
|
|
53
65
|
outcome: "ran",
|
|
54
66
|
exitCode: result.status ?? 1,
|
|
55
67
|
stdout,
|
|
56
|
-
output: `${stdout}${result.stderr ?? ""}`,
|
|
68
|
+
output: `${stdout}${(result.stderr ?? "").replace(ANSI_SGR, "")}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// A plugin registers its server as `plugin:<plugin>:<server>`, which `mcp get tinyfish` misses.
|
|
72
|
+
// The optional prefix must end in `:`, so `not-tinyfish` is not a match.
|
|
73
|
+
const MCP_LIST_TINYFISH_LINE = /^((?:\S*:)?tinyfish):\s+(https?:\S+)/m;
|
|
74
|
+
// A bare substring also matches `not-tinyfish` and any row quoting a tinyfish URL, so the name
|
|
75
|
+
// is required to be the first word on its line, after whatever status glyph the CLI prints.
|
|
76
|
+
const LISTED_AS_TINYFISH = /^[^\w]*tinyfish(?![\w-])/m;
|
|
77
|
+
/** The plugin ships its own MCP server, so a working install can carry no `tinyfish` entry. */
|
|
78
|
+
function fromPluginRegistration(command) {
|
|
79
|
+
const probe = runProbe(command, ["mcp", "list"]);
|
|
80
|
+
if (probe.outcome === "unavailable" || probe.exitCode !== 0)
|
|
81
|
+
return undefined;
|
|
82
|
+
const match = MCP_LIST_TINYFISH_LINE.exec(probe.output);
|
|
83
|
+
if (!match)
|
|
84
|
+
return undefined;
|
|
85
|
+
// `mcp list` prints no headers, so the credential behind it is unproven, never assumed.
|
|
86
|
+
return {
|
|
87
|
+
registered: "yes",
|
|
88
|
+
authMode: "unknown",
|
|
89
|
+
registeredUrl: match[2],
|
|
90
|
+
reason: `registered as ${match[1]}, which \`mcp get tinyfish\` does not resolve`,
|
|
57
91
|
};
|
|
58
92
|
}
|
|
59
93
|
// claude-code only. Codex cannot use this: its `mcp get` exits 0 on a missing server.
|
|
@@ -70,6 +104,10 @@ function fromMcpGet(command, keyAuthPattern) {
|
|
|
70
104
|
reason: `\`${command} mcp get\` is unsupported by this version`,
|
|
71
105
|
};
|
|
72
106
|
}
|
|
107
|
+
// Positive evidence from `mcp list` outranks anything inferred from a failed `mcp get`.
|
|
108
|
+
const plugin = fromPluginRegistration(command);
|
|
109
|
+
if (plugin)
|
|
110
|
+
return plugin;
|
|
73
111
|
// A broken CLI also exits nonzero, and reading that as absence earns a spurious repair.
|
|
74
112
|
if (!NO_SUCH_SERVER.test(probe.output)) {
|
|
75
113
|
return {
|
|
@@ -162,6 +200,26 @@ function probeCursor() {
|
|
|
162
200
|
...(entry.url ? { registeredUrl: entry.url } : {}),
|
|
163
201
|
};
|
|
164
202
|
}
|
|
203
|
+
// Both list commands print this header in every state, including "no servers configured".
|
|
204
|
+
// Exit 0 with output we cannot recognise is not evidence of absence: reporting `no` there would
|
|
205
|
+
// earn a connect repair off a parse failure, which is the bug class this command exists to kill.
|
|
206
|
+
const MCP_LIST_HEADER = /mcp servers/i;
|
|
207
|
+
// OpenCode prints the endpoint on the line under the server row; Hermes truncates its column.
|
|
208
|
+
const OPENCODE_ROW_URL = /^[^\w]*tinyfish(?![\w-]).*\n[^\w]*(https?:\S+)/m;
|
|
209
|
+
function fromMcpList(command, output, registeredMode, urlPattern) {
|
|
210
|
+
if (LISTED_AS_TINYFISH.test(output)) {
|
|
211
|
+
const url = urlPattern?.exec(output)?.[1];
|
|
212
|
+
return { registered: "yes", authMode: registeredMode, ...(url ? { registeredUrl: url } : {}) };
|
|
213
|
+
}
|
|
214
|
+
if (!MCP_LIST_HEADER.test(output)) {
|
|
215
|
+
return {
|
|
216
|
+
registered: "unknown",
|
|
217
|
+
authMode: "unknown",
|
|
218
|
+
reason: `could not interpret \`${command} mcp list\` output`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return { registered: "no", authMode: "unknown" };
|
|
222
|
+
}
|
|
165
223
|
// Hermes registers with `--auth oauth` and has no key path today, so mode is fixed.
|
|
166
224
|
function probeHermes() {
|
|
167
225
|
const probe = runProbe("hermes", ["mcp", "list"]);
|
|
@@ -175,10 +233,9 @@ function probeHermes() {
|
|
|
175
233
|
reason: `\`hermes mcp list\` exited ${probe.exitCode}`,
|
|
176
234
|
};
|
|
177
235
|
}
|
|
178
|
-
return probe.output
|
|
179
|
-
? { registered: "yes", authMode: "oauth" }
|
|
180
|
-
: { registered: "no", authMode: "unknown" };
|
|
236
|
+
return fromMcpList("hermes", probe.output, "oauth");
|
|
181
237
|
}
|
|
238
|
+
const SKILL_DIR_IS_TINYFISH = /^(?:@tinyfish[/_-])?tinyfish(?![\w-])/i;
|
|
182
239
|
// OpenClaw installs a skill, not an MCP server; the skill shells the CLI, so auth is the CLI key.
|
|
183
240
|
function probeOpenClaw() {
|
|
184
241
|
const skillsDir = path.join(harnessConfigPath("openclaw"), "skills");
|
|
@@ -193,16 +250,33 @@ function probeOpenClaw() {
|
|
|
193
250
|
reason: `no global skills directory at ${harnessDisplayPath("openclaw")}/skills; OpenClaw layout is unverified`,
|
|
194
251
|
};
|
|
195
252
|
}
|
|
196
|
-
|
|
253
|
+
// A bare substring also matched a skill merely named `not-tinyfish-thing`.
|
|
254
|
+
return entries.some((name) => SKILL_DIR_IS_TINYFISH.test(name))
|
|
197
255
|
? { registered: "yes", authMode: "api-key" }
|
|
198
256
|
: { registered: "no", authMode: "unknown" };
|
|
199
257
|
}
|
|
258
|
+
// Connect registers with `mcp add --url` then `mcp auth`, so there is no key path today.
|
|
259
|
+
function probeOpencode() {
|
|
260
|
+
const probe = runProbe("opencode", ["mcp", "list"]);
|
|
261
|
+
if (probe.outcome === "unavailable") {
|
|
262
|
+
return { registered: "unknown", authMode: "unknown", reason: probe.reason };
|
|
263
|
+
}
|
|
264
|
+
if (probe.exitCode !== 0) {
|
|
265
|
+
return {
|
|
266
|
+
registered: "unknown",
|
|
267
|
+
authMode: "unknown",
|
|
268
|
+
reason: `\`opencode mcp list\` exited ${probe.exitCode}`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return fromMcpList("opencode", probe.output, "oauth", OPENCODE_ROW_URL);
|
|
272
|
+
}
|
|
200
273
|
const PROBES = {
|
|
201
274
|
"claude-code": () => fromMcpGet("claude", API_KEY_HEADER),
|
|
202
275
|
codex: probeCodex,
|
|
203
276
|
cursor: probeCursor,
|
|
204
277
|
hermes: probeHermes,
|
|
205
278
|
openclaw: probeOpenClaw,
|
|
279
|
+
opencode: probeOpencode,
|
|
206
280
|
};
|
|
207
281
|
/** Never throws; a failed probe is `unknown` with a reason, so no caller can read it as healthy. */
|
|
208
282
|
export function detectRegistrations(harnesses) {
|
|
@@ -28,6 +28,7 @@ declare const harnessResultSchema: z.ZodObject<{
|
|
|
28
28
|
cursor: "cursor";
|
|
29
29
|
codex: "codex";
|
|
30
30
|
hermes: "hermes";
|
|
31
|
+
opencode: "opencode";
|
|
31
32
|
"claude-code": "claude-code";
|
|
32
33
|
}>;
|
|
33
34
|
detected: z.ZodBoolean;
|
|
@@ -45,6 +46,7 @@ export declare const setupCompletedPayloadSchema: z.ZodObject<{
|
|
|
45
46
|
cursor: "cursor";
|
|
46
47
|
codex: "codex";
|
|
47
48
|
hermes: "hermes";
|
|
49
|
+
opencode: "opencode";
|
|
48
50
|
"claude-code": "claude-code";
|
|
49
51
|
}>;
|
|
50
52
|
detected: z.ZodBoolean;
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { validateKeyFormat } from "./auth.js";
|
|
4
4
|
import { BASE_URL, CLI_VERSION } from "./constants.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ALL_HARNESSES } from "./harness-detect.js";
|
|
6
6
|
import { captureNotice } from "./notice.js";
|
|
7
7
|
import { errLine } from "./output.js";
|
|
8
8
|
const CONNECT_EVENT_TIMEOUT_MS = 2_000;
|
|
@@ -95,7 +95,7 @@ export async function postConnectEvent(options) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
const harnessResultSchema = z.object({
|
|
98
|
-
harness: z.enum(
|
|
98
|
+
harness: z.enum(ALL_HARNESSES),
|
|
99
99
|
detected: z.boolean(),
|
|
100
100
|
installed: z.boolean(),
|
|
101
101
|
verify_depth: z.enum(["health", "health+auth"]).nullable(),
|
|
@@ -104,8 +104,8 @@ const harnessResultSchema = z.object({
|
|
|
104
104
|
export const setupCompletedPayloadSchema = z.object({
|
|
105
105
|
harnesses: z
|
|
106
106
|
.array(harnessResultSchema)
|
|
107
|
-
.length(
|
|
108
|
-
.refine((results) => new Set(results.map((r) => r.harness)).size ===
|
|
107
|
+
.length(ALL_HARNESSES.length)
|
|
108
|
+
.refine((results) => new Set(results.map((r) => r.harness)).size === ALL_HARNESSES.length, {
|
|
109
109
|
message: "expected one result per harness",
|
|
110
110
|
}),
|
|
111
111
|
cli_version: z.string(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiny-fish/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1-next.163",
|
|
4
4
|
"description": "TinyFish CLI — run web automations from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"prepublishOnly": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@tiny-fish/sdk": "^0.
|
|
35
|
+
"@tiny-fish/sdk": "^0.4.0",
|
|
36
36
|
"chrome-cookies-secure": "^3.0.2",
|
|
37
37
|
"commander": "^12.0.0",
|
|
38
38
|
"cross-spawn": "^7.0.6",
|