@tiny-fish/cli 0.27.1-next.228 → 0.28.1-next.233
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/auth.js +2 -1
- package/dist/commands/connect.js +9 -6
- package/dist/commands/onboard.js +3 -3
- package/dist/lib/auth.js +10 -9
- package/dist/lib/connect-all.js +2 -1
- package/dist/lib/connect-auth.js +2 -1
- package/dist/lib/connect-clients.d.ts +2 -4
- package/dist/lib/connect-clients.js +5 -6
- package/dist/lib/connect-runtime.js +3 -1
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/harness.js +2 -0
- package/dist/lib/registration-detect.d.ts +3 -0
- package/dist/lib/registration-detect.js +56 -31
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "child_process";
|
|
|
2
2
|
import * as readline from "readline";
|
|
3
3
|
import { Option } from "commander";
|
|
4
4
|
import { CONNECT_ATTEMPT_ENV, clearConfig, getDashboardUrl, loadConfig, maskKey, savePendingConnectAttempt, saveConfig, validateKeyFormat, } from "../lib/auth.js";
|
|
5
|
+
import { TINYFISH_API_KEY_VAR } from "../lib/constants.js";
|
|
5
6
|
import { err, errLine, out, outLine } from "../lib/output.js";
|
|
6
7
|
/**
|
|
7
8
|
* Read a key from stdin without echoing it to the terminal.
|
|
@@ -143,7 +144,7 @@ export function registerAuth(program) {
|
|
|
143
144
|
.description("Show which API key is active and its source")
|
|
144
145
|
.option("--pretty", "Human-readable output")
|
|
145
146
|
.action((opts) => {
|
|
146
|
-
const envKey = process.env
|
|
147
|
+
const envKey = process.env[TINYFISH_API_KEY_VAR];
|
|
147
148
|
if (envKey) {
|
|
148
149
|
const authenticated = validateKeyFormat(envKey);
|
|
149
150
|
const result = { source: "env", key_preview: maskKey(envKey), authenticated };
|
package/dist/commands/connect.js
CHANGED
|
@@ -9,7 +9,9 @@ import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCur
|
|
|
9
9
|
import { runConnectAll } from "../lib/connect-all.js";
|
|
10
10
|
import { AuthMode } from "../lib/harness-detect.js";
|
|
11
11
|
import { detectHumanInitiated } from "../lib/harness.js";
|
|
12
|
+
import { TINYFISH_API_KEY_VAR } from "../lib/constants.js";
|
|
12
13
|
import { emitNotice } from "../lib/notice.js";
|
|
14
|
+
import { SIGN_IN_PROBES } from "../lib/registration-detect.js";
|
|
13
15
|
import { errLine } from "../lib/output.js";
|
|
14
16
|
import { z } from "zod";
|
|
15
17
|
import { verifyMcpAuth } from "../lib/verify.js";
|
|
@@ -38,7 +40,7 @@ function exportApiKey(apiKey, displayName, deferOutro) {
|
|
|
38
40
|
"signing in instead.");
|
|
39
41
|
return false;
|
|
40
42
|
}
|
|
41
|
-
process.env[
|
|
43
|
+
process.env[TINYFISH_API_KEY_VAR] = apiKey;
|
|
42
44
|
if (deferOutro) {
|
|
43
45
|
deferOutro("Exported TINYFISH_API_KEY to your shell profile — restart your agents (or open a new " +
|
|
44
46
|
"terminal) before use.");
|
|
@@ -52,6 +54,7 @@ function exportApiKey(apiKey, displayName, deferOutro) {
|
|
|
52
54
|
async function connectNativeMcpClient(client, options) {
|
|
53
55
|
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
|
|
54
56
|
const state = { stage: "prerequisite_check", settled: false };
|
|
57
|
+
const probeAuthenticated = SIGN_IN_PROBES[client.connectClient];
|
|
55
58
|
await runGuarded(state, telemetry, () => {
|
|
56
59
|
telemetry.track("started");
|
|
57
60
|
const { optionalSupported, keyAuthSupported, harnessVersion } = requireCommandSupport(client);
|
|
@@ -63,7 +66,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
63
66
|
? { key: storedKey, spec: client.keyAuth }
|
|
64
67
|
: undefined;
|
|
65
68
|
// Before the removals: a failed export must not strand the old registration.
|
|
66
|
-
if (keyedAdd?.spec.
|
|
69
|
+
if (keyedAdd?.spec.envVar &&
|
|
67
70
|
!exportApiKey(keyedAdd.key, client.displayName, options.deferOutro)) {
|
|
68
71
|
keyedAdd = undefined;
|
|
69
72
|
}
|
|
@@ -90,7 +93,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
90
93
|
const loginArgs = loginDegraded ? undefined : client.loginArgs;
|
|
91
94
|
// A probed client authenticates inside `mcp add` too, so a failure there is either.
|
|
92
95
|
state.stage =
|
|
93
|
-
loginArgs && !
|
|
96
|
+
loginArgs && !probeAuthenticated ? "registration" : "registration_or_authentication";
|
|
94
97
|
errLine(`Adding TinyFish to ${client.displayName}...`);
|
|
95
98
|
// Header keys ride argv (/proc/<pid>/cmdline); these CLIs take headers no other way.
|
|
96
99
|
const addArgs = keyedAdd
|
|
@@ -109,7 +112,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
109
112
|
telemetry.track("checkpoint", { phase: "registered" });
|
|
110
113
|
// Codex runs OAuth inside `mcp add` and exits 0 either way, so the recorded state decides
|
|
111
114
|
// whether the login step below still runs. Unknown keeps the pre-probe behaviour.
|
|
112
|
-
const addSignedIn =
|
|
115
|
+
const addSignedIn = probeAuthenticated?.();
|
|
113
116
|
if (addSignedIn)
|
|
114
117
|
telemetry.track("checkpoint", { phase: "oauth_done" });
|
|
115
118
|
const degradedNotes = loginDegraded ? client.degradedAuthNotes : undefined;
|
|
@@ -121,7 +124,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
121
124
|
errLine(degradedNotes.deferred);
|
|
122
125
|
}
|
|
123
126
|
// A probed client signs in again only when its inline OAuth is known to have been abandoned.
|
|
124
|
-
const retryLogin =
|
|
127
|
+
const retryLogin = probeAuthenticated ? addSignedIn === false : true;
|
|
125
128
|
const pendingLogin = !useKeyAuth && retryLogin ? loginArgs : undefined;
|
|
126
129
|
let signInDeferred = !useKeyAuth && !addSignedIn && !pendingLogin;
|
|
127
130
|
if (pendingLogin) {
|
|
@@ -131,7 +134,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
131
134
|
if (loginResult.error || loginResult.status !== 0) {
|
|
132
135
|
throw spawnStepError(`Could not authenticate TinyFish in ${client.displayName}`, loginResult);
|
|
133
136
|
}
|
|
134
|
-
if (
|
|
137
|
+
if (probeAuthenticated?.() === false)
|
|
135
138
|
signInDeferred = true;
|
|
136
139
|
else
|
|
137
140
|
telemetry.track("checkpoint", { phase: "oauth_done" });
|
package/dist/commands/onboard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as readline from "readline/promises";
|
|
2
2
|
import { getApiKey, saveConfig } from "../lib/auth.js";
|
|
3
3
|
import { fetchContentGet, searchQuery } from "../lib/client.js";
|
|
4
|
-
import { BASE_URL } from "../lib/constants.js";
|
|
4
|
+
import { BASE_URL, TINYFISH_API_KEY_VAR } from "../lib/constants.js";
|
|
5
5
|
import { errLine, handleApiError, outLine } from "../lib/output.js";
|
|
6
6
|
import { connectClaudeCode, connectCodex, connectHermes, connectOpenClaw, connectOpencode, launchAgent, } from "./connect.js";
|
|
7
7
|
const EXAMPLE_QUERIES = [
|
|
@@ -213,8 +213,8 @@ export function registerOnboard(program) {
|
|
|
213
213
|
"Run `tinyfish connect <client> --launch` later to add one directly.")
|
|
214
214
|
.action(async (query) => {
|
|
215
215
|
const apiKey = getApiKey();
|
|
216
|
-
const persistEnvKey = Boolean(process.env[
|
|
217
|
-
delete process.env[
|
|
216
|
+
const persistEnvKey = Boolean(process.env[TINYFISH_API_KEY_VAR]);
|
|
217
|
+
delete process.env[TINYFISH_API_KEY_VAR];
|
|
218
218
|
try {
|
|
219
219
|
const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
220
220
|
const selectedQuery = await chooseQuery(query, interactive);
|
package/dist/lib/auth.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as os from "os";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import spawn from "cross-spawn";
|
|
5
5
|
import { err, errLine } from "./output.js";
|
|
6
|
+
import { TINYFISH_API_KEY_VAR } from "./constants.js";
|
|
6
7
|
const KEY_PREFIXES = ["sk-tinyfish-", "sk-mino-"];
|
|
7
8
|
const MIN_KEY_LENGTH = 20;
|
|
8
9
|
export { DASHBOARD_URL, getDashboardUrl } from "./constants.js";
|
|
@@ -133,7 +134,7 @@ export function saveConnectContext(client, attemptId) {
|
|
|
133
134
|
export function persistApiKeyToEnvironment(apiKey, options = {}) {
|
|
134
135
|
const platform = options.platform ?? process.platform;
|
|
135
136
|
if (platform === "win32") {
|
|
136
|
-
const result = spawn.sync("setx", [
|
|
137
|
+
const result = spawn.sync("setx", [TINYFISH_API_KEY_VAR, apiKey], { stdio: "ignore" });
|
|
137
138
|
if (result.error || result.status !== 0) {
|
|
138
139
|
throw new Error("Could not set TINYFISH_API_KEY in the Windows user environment", {
|
|
139
140
|
cause: result.error,
|
|
@@ -157,14 +158,14 @@ export function persistApiKeyToEnvironment(apiKey, options = {}) {
|
|
|
157
158
|
? path.join(home, ".config", "fish", "config.fish")
|
|
158
159
|
: path.join(home, ".profile");
|
|
159
160
|
const quotedKey = `'${apiKey.replaceAll("'", "'\\''")}'`;
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
: `export TINYFISH_API_KEY=${quotedKey}`;
|
|
163
|
-
const matcher = fish
|
|
164
|
-
? /^set -gx TINYFISH_API_KEY .*(?:\r?\n|$)/gm
|
|
165
|
-
: /^export TINYFISH_API_KEY=.*(?:\r?\n|$)/gm;
|
|
161
|
+
const prefix = fish ? `set -gx ${TINYFISH_API_KEY_VAR} ` : `export ${TINYFISH_API_KEY_VAR}=`;
|
|
162
|
+
const assignment = `${prefix}${quotedKey}`;
|
|
166
163
|
const existing = fs.existsSync(profile) ? fs.readFileSync(profile, "utf8") : "";
|
|
167
|
-
|
|
164
|
+
// Drops prior assignments, so repeated logins do not stack stale keys.
|
|
165
|
+
const cleaned = existing
|
|
166
|
+
.split("\n")
|
|
167
|
+
.filter((line) => !line.startsWith(prefix))
|
|
168
|
+
.join("\n");
|
|
168
169
|
const updated = `${cleaned}${cleaned && !cleaned.endsWith("\n") ? "\n" : ""}${assignment}\n`;
|
|
169
170
|
fs.mkdirSync(path.dirname(profile), { recursive: true });
|
|
170
171
|
fs.writeFileSync(profile, updated, { mode: 0o600 });
|
|
@@ -208,7 +209,7 @@ export function maskKey(key) {
|
|
|
208
209
|
function lookupApiKey(explicitApiKey) {
|
|
209
210
|
if (explicitApiKey)
|
|
210
211
|
return { value: explicitApiKey, source: "explicit" };
|
|
211
|
-
const envKey = process.env[
|
|
212
|
+
const envKey = process.env[TINYFISH_API_KEY_VAR];
|
|
212
213
|
if (envKey)
|
|
213
214
|
return { value: envKey, source: "env" };
|
|
214
215
|
// No try/catch: loadConfig swallows its own errors and returns {}.
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -2,6 +2,7 @@ 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
4
|
import { loadConfig, validatedApiKey } from "./auth.js";
|
|
5
|
+
import { TINYFISH_API_KEY_VAR } from "./constants.js";
|
|
5
6
|
import { installTinyFishCli, } from "./cli-install.js";
|
|
6
7
|
import { NATIVE_MCP_CLIENTS } from "./connect-clients.js";
|
|
7
8
|
import { ensureCliAuthenticated, TINYFISH_CLI_NOT_FOUND_MESSAGE } from "./connect-auth.js";
|
|
@@ -330,7 +331,7 @@ async function runCliFallback(opts, prompt) {
|
|
|
330
331
|
outLine("Retry: npx @tiny-fish/cli connect --all");
|
|
331
332
|
return 1;
|
|
332
333
|
}
|
|
333
|
-
const apiKey = opts.apiKey ?? process.env[
|
|
334
|
+
const apiKey = opts.apiKey ?? process.env[TINYFISH_API_KEY_VAR];
|
|
334
335
|
if (!prompt && !apiKey) {
|
|
335
336
|
outLine("Retry in a terminal: npx @tiny-fish/cli connect --all");
|
|
336
337
|
return 1;
|
package/dist/lib/connect-auth.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
|
+
import { TINYFISH_API_KEY_VAR } from "./constants.js";
|
|
2
3
|
import { persistApiKeyToEnvironment, validateKeyFormat, writeConfig } from "./auth.js";
|
|
3
4
|
import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "./connect-runtime.js";
|
|
4
5
|
import { errLine } from "./output.js";
|
|
5
6
|
export const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
|
|
6
7
|
export function ensureCliAuthenticated(source, apiKey) {
|
|
7
|
-
const envKey = apiKey ?? process.env[
|
|
8
|
+
const envKey = apiKey ?? process.env[TINYFISH_API_KEY_VAR];
|
|
8
9
|
if (envKey) {
|
|
9
10
|
if (!validateKeyFormat(envKey)) {
|
|
10
11
|
throw new ConnectStepError("TINYFISH_API_KEY has invalid format", "invalid_config");
|
|
@@ -18,8 +18,8 @@ export interface NativeMcpClient extends SupportedCommand {
|
|
|
18
18
|
nonInteractiveAdd?: true;
|
|
19
19
|
/** Replaces `addArgs` when a stored key is available: key auth removes the OAuth hop. */
|
|
20
20
|
keyAuth?: {
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/** Which variable the client reads the key from. */
|
|
22
|
+
envVar?: string;
|
|
23
23
|
addArgs: (mcpUrl: string, apiKey: string) => string[];
|
|
24
24
|
};
|
|
25
25
|
/** Shown when a keyless install leaves sign-in to the harness. */
|
|
@@ -30,8 +30,6 @@ export interface NativeMcpClient extends SupportedCommand {
|
|
|
30
30
|
keyed: string;
|
|
31
31
|
deferred: string;
|
|
32
32
|
};
|
|
33
|
-
/** Did the OAuth inside `mcp add` finish? undefined when the client cannot say. */
|
|
34
|
-
probeAuthenticated?: () => boolean | undefined;
|
|
35
33
|
removals: {
|
|
36
34
|
args: string[];
|
|
37
35
|
label: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
import { errLine, sanitizeLine } from "./output.js";
|
|
3
3
|
import { spawnStepError, } from "./connect-runtime.js";
|
|
4
|
-
import {
|
|
4
|
+
import { TINYFISH_API_KEY_VAR } from "./constants.js";
|
|
5
5
|
const HERMES_SEED_TIMEOUT_MS = 120_000;
|
|
6
6
|
// An old install is only one reason a flag can go unseen, so no message asserts the cause.
|
|
7
7
|
const SUPPORT_CHECK_DEBUG_HINT = " Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.";
|
|
@@ -151,11 +151,10 @@ export const CODEX = {
|
|
|
151
151
|
// Codex has no `--header`; the MCP server takes a key as `Bearer sk-…`.
|
|
152
152
|
// Omitting `--oauth-resource` is what keeps `mcp add` from running OAuth.
|
|
153
153
|
keyAuth: {
|
|
154
|
-
|
|
155
|
-
addArgs: (mcpUrl) => [...urlAddArgs(mcpUrl), "--bearer-token-env-var",
|
|
154
|
+
envVar: TINYFISH_API_KEY_VAR,
|
|
155
|
+
addArgs: (mcpUrl) => [...urlAddArgs(mcpUrl), "--bearer-token-env-var", TINYFISH_API_KEY_VAR],
|
|
156
156
|
},
|
|
157
157
|
launchWalkthrough: launchCodexWalkthrough,
|
|
158
|
-
probeAuthenticated: codexOauthCompleted,
|
|
159
158
|
// Retry only: running it after a finished inline OAuth is a second browser hop.
|
|
160
159
|
loginArgs: ["mcp", "login", "tinyfish"],
|
|
161
160
|
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Codex" }],
|
|
@@ -183,11 +182,11 @@ export const GROK = {
|
|
|
183
182
|
// X-API-Key is dropped whenever the server advertises OAuth metadata; Bearer is not.
|
|
184
183
|
// Grok expands `${VAR}` in headers at load, so the key stays out of argv and config.toml.
|
|
185
184
|
keyAuth: {
|
|
186
|
-
|
|
185
|
+
envVar: TINYFISH_API_KEY_VAR,
|
|
187
186
|
addArgs: (mcpUrl) => [
|
|
188
187
|
...grokAddArgs(mcpUrl),
|
|
189
188
|
"--header",
|
|
190
|
-
|
|
189
|
+
`Authorization: Bearer \${${TINYFISH_API_KEY_VAR}}`,
|
|
191
190
|
],
|
|
192
191
|
},
|
|
193
192
|
signInHint: GROK_SIGN_IN_HINT,
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import spawn from "cross-spawn";
|
|
3
3
|
import { CONNECT_ATTEMPT_ENV, isAttemptId, resolvedApiKey, takePendingConnectAttempt, } from "./auth.js";
|
|
4
4
|
import { CLI_VERSION } from "./constants.js";
|
|
5
|
-
import { detectHumanInitiated } from "./harness.js";
|
|
5
|
+
import { detectHarness, detectHumanInitiated } from "./harness.js";
|
|
6
6
|
import { errLine, sanitizeLine } from "./output.js";
|
|
7
7
|
import { postConnectEvent, telemetryDisabled } from "./setup-telemetry.js";
|
|
8
8
|
import { installSignalGuard } from "./signals.js";
|
|
@@ -241,6 +241,8 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
|
|
|
241
241
|
cli_version: CLI_VERSION,
|
|
242
242
|
// Same signal the usage events carry, so TTY and headless connects can be split.
|
|
243
243
|
is_human_initiated: detectHumanInitiated(),
|
|
244
|
+
// "none", not absent — absent has to keep meaning "CLI too old".
|
|
245
|
+
calling_harness: detectHarness() ?? "none",
|
|
244
246
|
})).catch((error) => {
|
|
245
247
|
// postConnectEvent is total, so this only fires if something inside deliver starts
|
|
246
248
|
// throwing. Same channel postConnectEvent uses for its own failures.
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export declare const CLI_VERSION: string;
|
|
|
2
2
|
/** Unversioned: the install site appends `@latest`, the version read must not. */
|
|
3
3
|
export declare const TINYFISH_CLI_PACKAGE = "@tiny-fish/cli";
|
|
4
4
|
export declare const CLI_AGENT_IDENTITY = "tinyfish-cli";
|
|
5
|
+
/** The descriptor hands this name to harnesses; the probes resolve it. */
|
|
6
|
+
export declare const TINYFISH_API_KEY_VAR = "TINYFISH_API_KEY";
|
|
5
7
|
export declare const BASE_URL: string;
|
|
6
8
|
export declare const API_URL_OVERRIDE: string | undefined;
|
|
7
9
|
/** URL where users can create and manage API keys */
|
package/dist/lib/constants.js
CHANGED
|
@@ -6,6 +6,8 @@ export const CLI_VERSION = _pkg.version;
|
|
|
6
6
|
export const TINYFISH_CLI_PACKAGE = "@tiny-fish/cli";
|
|
7
7
|
// How we identify ourselves to other tools; `skills` echoes it back.
|
|
8
8
|
export const CLI_AGENT_IDENTITY = "tinyfish-cli";
|
|
9
|
+
/** The descriptor hands this name to harnesses; the probes resolve it. */
|
|
10
|
+
export const TINYFISH_API_KEY_VAR = "TINYFISH_API_KEY";
|
|
9
11
|
/** Base URL for the TinyFish API. Override with TINYFISH_API_URL for staging/self-hosted. */
|
|
10
12
|
const apiUrlOverride = process.env["TINYFISH_API_URL"]?.trim() || undefined;
|
|
11
13
|
if (apiUrlOverride) {
|
package/dist/lib/harness.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// one. OpenClaw drives Claude Code via `sdk-cli`, so its env carries both
|
|
9
9
|
// CLAUDECODE=1 and OPENCLAW_MCP_*; it must be checked before claude-code.
|
|
10
10
|
const hasVarWithPrefix = (env, prefix) => Object.keys(env).some((key) => key.startsWith(prefix));
|
|
11
|
+
// A new name here must also join KNOWN_CALLING_HARNESSES in
|
|
12
|
+
// frontend/app/api/cli/connect-event/route.ts, or connect telemetry buckets it as `other`.
|
|
11
13
|
// First match wins. Base agents (claude-code) go last so embedding wrappers win.
|
|
12
14
|
const HARNESS_FINGERPRINTS = [
|
|
13
15
|
{ name: "openclaw", matches: (env) => hasVarWithPrefix(env, "OPENCLAW_MCP_") },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentClient } from "./connect-runtime.js";
|
|
1
2
|
import { AuthMode, type Harness, Registered } from "./harness-detect.js";
|
|
2
3
|
export interface RegistrationStatus {
|
|
3
4
|
harness: Harness;
|
|
@@ -17,5 +18,7 @@ export interface RegistrationStatus {
|
|
|
17
18
|
reason?: string;
|
|
18
19
|
}
|
|
19
20
|
export declare function codexOauthCompleted(): boolean | undefined;
|
|
21
|
+
/** Per-harness sign-in probes; connect composes these rather than the descriptor carrying them. */
|
|
22
|
+
export declare const SIGN_IN_PROBES: Partial<Record<AgentClient, () => boolean | undefined>>;
|
|
20
23
|
/** Never throws; a failed probe is `unknown` with a reason, so no caller can read it as healthy. */
|
|
21
24
|
export declare function detectRegistrations(harnesses?: Harness[]): RegistrationStatus[];
|
|
@@ -3,6 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import spawn from "cross-spawn";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { loadConfig, matchesCliKey } from "./auth.js";
|
|
6
|
+
import { NATIVE_MCP_CLIENTS } from "./connect-clients.js";
|
|
6
7
|
import { errLine } from "./output.js";
|
|
7
8
|
import { readCursorTinyfishEntry } from "./cursor-config.js";
|
|
8
9
|
import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMode, Registered, } from "./harness-detect.js";
|
|
@@ -70,6 +71,14 @@ function runProbe(command, args) {
|
|
|
70
71
|
output: `${stdout}${(result.stderr ?? "").replace(ANSI_SGR, "")}`,
|
|
71
72
|
};
|
|
72
73
|
}
|
|
74
|
+
/** Undefined when the probe ran and exited zero. */
|
|
75
|
+
function probeReason(command, args, probe) {
|
|
76
|
+
if (probe.outcome === "unavailable")
|
|
77
|
+
return probe.reason;
|
|
78
|
+
if (probe.exitCode === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
return `\`${command} ${args.join(" ")}\` exited ${probe.exitCode}`;
|
|
81
|
+
}
|
|
73
82
|
// A plugin registers its server as `plugin:<plugin>:<server>`, which `mcp get tinyfish` misses.
|
|
74
83
|
// The optional prefix must end in `:`, so `not-tinyfish` is not a match.
|
|
75
84
|
const MCP_LIST_TINYFISH_LINE = /^((?:\S*:)?tinyfish):\s+(https?:\S+)(.*)$/m;
|
|
@@ -174,14 +183,15 @@ function fromMcpGet(command) {
|
|
|
174
183
|
// `codex mcp get <missing>` prints an error and exits 0, so presence must never be read from
|
|
175
184
|
// its exit code; `mcp list --json` is the only trustworthy read-back.
|
|
176
185
|
function readCodexEntry() {
|
|
177
|
-
const
|
|
186
|
+
const args = ["mcp", "list", "--json"];
|
|
187
|
+
const probe = runProbe("codex", args);
|
|
178
188
|
if (probe.outcome === "unavailable")
|
|
179
189
|
return { reason: probe.reason };
|
|
180
190
|
// Presence is never inferred from exit code 0, but a nonzero exit means the output cannot
|
|
181
191
|
// be trusted either — that is `unknown`, not "no server".
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
192
|
+
const failure = probeReason("codex", args, probe);
|
|
193
|
+
if (failure)
|
|
194
|
+
return { reason: failure };
|
|
185
195
|
let servers;
|
|
186
196
|
try {
|
|
187
197
|
// stdout only: an update banner on stderr would otherwise break the parse and report a
|
|
@@ -211,10 +221,19 @@ export function codexOauthCompleted() {
|
|
|
211
221
|
return false;
|
|
212
222
|
return undefined;
|
|
213
223
|
}
|
|
224
|
+
/** Per-harness sign-in probes; connect composes these rather than the descriptor carrying them. */
|
|
225
|
+
export const SIGN_IN_PROBES = {
|
|
226
|
+
codex: codexOauthCompleted,
|
|
227
|
+
};
|
|
214
228
|
// This reaches the pasted report, so its shape is checked first.
|
|
215
229
|
const ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
230
|
+
const NATIVE_BY_HARNESS = new Map(NATIVE_MCP_CLIENTS.map((client) => [client.connectClient, client]));
|
|
231
|
+
/** Undefined unless the descriptor routes this harness's key through a variable. */
|
|
232
|
+
function declaredKeyEnvVar(harness) {
|
|
233
|
+
return NATIVE_BY_HARNESS.get(harness)?.keyAuth?.envVar;
|
|
234
|
+
}
|
|
216
235
|
/** Codex stores a variable name; resolving it only answers whether it is the CLI's key. */
|
|
217
|
-
function
|
|
236
|
+
function envKeyVerdict(envVar) {
|
|
218
237
|
if (!ENV_VAR_NAME.test(envVar)) {
|
|
219
238
|
return { keyReason: "its key comes from an unusable variable name" };
|
|
220
239
|
}
|
|
@@ -245,7 +264,7 @@ function probeCodex() {
|
|
|
245
264
|
? AuthMode.ApiKey
|
|
246
265
|
: AuthMode.Unknown,
|
|
247
266
|
...(codexUrl ? { registeredUrl: codexUrl } : {}),
|
|
248
|
-
...(envVar ?
|
|
267
|
+
...(envVar ? envKeyVerdict(envVar) : {}),
|
|
249
268
|
};
|
|
250
269
|
}
|
|
251
270
|
function probeCursor() {
|
|
@@ -288,16 +307,11 @@ function fromMcpList(command, output, registeredMode, urlPattern) {
|
|
|
288
307
|
}
|
|
289
308
|
// connect only ever writes an OAuth Hermes entry, so `oauth` is a fact, not a detection gap.
|
|
290
309
|
function probeHermes() {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
registered: Registered.Unknown,
|
|
298
|
-
authMode: AuthMode.Unknown,
|
|
299
|
-
reason: `\`hermes mcp list\` exited ${probe.exitCode}`,
|
|
300
|
-
};
|
|
310
|
+
const args = ["mcp", "list"];
|
|
311
|
+
const probe = runProbe("hermes", args);
|
|
312
|
+
if (probe.outcome !== "ran" || probe.exitCode !== 0) {
|
|
313
|
+
const reason = probeReason("hermes", args, probe);
|
|
314
|
+
return { registered: Registered.Unknown, authMode: AuthMode.Unknown, reason };
|
|
301
315
|
}
|
|
302
316
|
return fromMcpList("hermes", probe.output, AuthMode.OAuth);
|
|
303
317
|
}
|
|
@@ -323,16 +337,11 @@ function probeOpenClaw() {
|
|
|
323
337
|
}
|
|
324
338
|
// `opencode mcp list` prints no headers, so a key-authed registration is indistinguishable.
|
|
325
339
|
function probeOpencode() {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
registered: Registered.Unknown,
|
|
333
|
-
authMode: AuthMode.Unknown,
|
|
334
|
-
reason: `\`opencode mcp list\` exited ${probe.exitCode}`,
|
|
335
|
-
};
|
|
340
|
+
const args = ["mcp", "list"];
|
|
341
|
+
const probe = runProbe("opencode", args);
|
|
342
|
+
if (probe.outcome !== "ran" || probe.exitCode !== 0) {
|
|
343
|
+
const reason = probeReason("opencode", args, probe);
|
|
344
|
+
return { registered: Registered.Unknown, authMode: AuthMode.Unknown, reason };
|
|
336
345
|
}
|
|
337
346
|
return fromMcpList("opencode", probe.output, AuthMode.Unknown, OPENCODE_ROW_URL);
|
|
338
347
|
}
|
|
@@ -377,9 +386,27 @@ function fromGrokPlugin() {
|
|
|
377
386
|
reason: "registered by the TinyFish Grok plugin",
|
|
378
387
|
};
|
|
379
388
|
}
|
|
389
|
+
// Grok inherits our env, so a header we wrote round-trips to our own value.
|
|
390
|
+
function grokWroteIt(value, declared) {
|
|
391
|
+
const ours = process.env[declared];
|
|
392
|
+
return ours ? value === `Bearer ${ours}` : value === `Bearer \${${declared}}`;
|
|
393
|
+
}
|
|
394
|
+
/** The descriptor names the variable; envKeyVerdict owns every wording for it. */
|
|
395
|
+
function grokKeyAuth(headers) {
|
|
396
|
+
const authorization = Object.entries(headers).find(([name]) => name.toLowerCase() === "authorization");
|
|
397
|
+
if (!authorization)
|
|
398
|
+
return { authMode: AuthMode.OAuth };
|
|
399
|
+
const declared = declaredKeyEnvVar("grok");
|
|
400
|
+
const value = typeof authorization[1] === "string" ? authorization[1] : "";
|
|
401
|
+
if (!declared || !grokWroteIt(value, declared)) {
|
|
402
|
+
return { authMode: AuthMode.ApiKey, keyReason: "it holds a key this CLI cannot verify" };
|
|
403
|
+
}
|
|
404
|
+
return { authMode: AuthMode.ApiKey, ...envKeyVerdict(declared) };
|
|
405
|
+
}
|
|
380
406
|
// Grok has no `mcp get`; `mcp list --json` is the only machine-readable view.
|
|
381
407
|
function probeGrok() {
|
|
382
|
-
const
|
|
408
|
+
const args = ["mcp", "list", "--json"];
|
|
409
|
+
const probe = runProbe("grok", args);
|
|
383
410
|
if (probe.outcome === "unavailable") {
|
|
384
411
|
return { registered: Registered.Unknown, authMode: AuthMode.Unknown, reason: probe.reason };
|
|
385
412
|
}
|
|
@@ -394,7 +421,7 @@ function probeGrok() {
|
|
|
394
421
|
return {
|
|
395
422
|
registered: Registered.Unknown,
|
|
396
423
|
authMode: AuthMode.Unknown,
|
|
397
|
-
reason:
|
|
424
|
+
reason: probeReason("grok", args, probe),
|
|
398
425
|
};
|
|
399
426
|
}
|
|
400
427
|
let entries;
|
|
@@ -411,11 +438,9 @@ function probeGrok() {
|
|
|
411
438
|
const entry = entries.find((server) => server.name === "tinyfish");
|
|
412
439
|
if (!entry)
|
|
413
440
|
return fromGrokPlugin() ?? { registered: Registered.No, authMode: AuthMode.Unknown };
|
|
414
|
-
// Grok drops X-API-Key when the server advertises OAuth metadata, so keys ride Authorization.
|
|
415
|
-
const hasAuthorization = Object.keys(entry.headers ?? {}).some((name) => name.toLowerCase() === "authorization");
|
|
416
441
|
return {
|
|
417
442
|
registered: Registered.Yes,
|
|
418
|
-
|
|
443
|
+
...grokKeyAuth(entry.headers ?? {}),
|
|
419
444
|
...(entry.url ? { registeredUrl: entry.url } : {}),
|
|
420
445
|
};
|
|
421
446
|
}
|