@tiny-fish/cli 0.18.1-next.172 → 0.18.1-next.174
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 +10 -1
- package/dist/commands/connect.js +36 -7
- package/dist/lib/auth.d.ts +14 -0
- package/dist/lib/auth.js +69 -17
- package/dist/lib/connect-all.js +1 -0
- package/dist/lib/doctor-report.d.ts +2 -2
- package/dist/lib/registration-detect.d.ts +1 -0
- package/dist/lib/registration-detect.js +27 -23
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import * as readline from "readline";
|
|
3
3
|
import { Option } from "commander";
|
|
4
|
-
import { clearConfig, getDashboardUrl, loadConfig, maskKey, saveConfig, validateKeyFormat, } from "../lib/auth.js";
|
|
4
|
+
import { CONNECT_ATTEMPT_ENV, clearConfig, getDashboardUrl, loadConfig, maskKey, savePendingConnectAttempt, saveConfig, validateKeyFormat, } from "../lib/auth.js";
|
|
5
5
|
import { err, errLine, out, outLine } from "../lib/output.js";
|
|
6
6
|
/**
|
|
7
7
|
* Read a key from stdin without echoing it to the terminal.
|
|
@@ -66,6 +66,13 @@ async function readKeyFromStdinOrPrompt() {
|
|
|
66
66
|
}
|
|
67
67
|
return readKeyHidden("Paste your API key: ");
|
|
68
68
|
}
|
|
69
|
+
// Sign-in and the connect that follows are separate copied commands, so a page-minted id
|
|
70
|
+
// handed to this process has to outlive it.
|
|
71
|
+
function parkSeedAttempt() {
|
|
72
|
+
const seed = process.env[CONNECT_ATTEMPT_ENV];
|
|
73
|
+
if (seed)
|
|
74
|
+
savePendingConnectAttempt(seed);
|
|
75
|
+
}
|
|
69
76
|
export function registerAuth(program) {
|
|
70
77
|
const auth = program.command("auth").description("Manage your TinyFish API key");
|
|
71
78
|
auth
|
|
@@ -110,6 +117,7 @@ export function registerAuth(program) {
|
|
|
110
117
|
process.exit(1);
|
|
111
118
|
}
|
|
112
119
|
saveConfig(key);
|
|
120
|
+
parkSeedAttempt();
|
|
113
121
|
out({ status: "ok", message: "API key saved", key_preview: maskKey(key) });
|
|
114
122
|
});
|
|
115
123
|
auth
|
|
@@ -127,6 +135,7 @@ export function registerAuth(program) {
|
|
|
127
135
|
process.exit(1);
|
|
128
136
|
}
|
|
129
137
|
saveConfig(key);
|
|
138
|
+
parkSeedAttempt();
|
|
130
139
|
out({ status: "ok", message: "API key saved", key_preview: maskKey(key) });
|
|
131
140
|
});
|
|
132
141
|
auth
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import spawn from "cross-spawn";
|
|
4
|
-
import { CONNECT_SOURCE, loadConfig, persistApiKeyToEnvironment, resolvedApiKey, saveConnectContext, validateKeyFormat, validatedApiKey, writeConfig, } from "../lib/auth.js";
|
|
4
|
+
import { CONNECT_ATTEMPT_ENV, CONNECT_SOURCE, isAttemptId, loadConfig, persistApiKeyToEnvironment, resolvedApiKey, saveConnectContext, takePendingConnectAttempt, validateKeyFormat, validatedApiKey, writeConfig, } from "../lib/auth.js";
|
|
5
5
|
import { CLI_VERSION, TINYFISH_CLI_PACKAGE } from "../lib/constants.js";
|
|
6
6
|
import { cursorInstallDeeplink, cursorMcpPath, writeCursorMcpConfig, } from "../lib/cursor-config.js";
|
|
7
7
|
import { runConnectAll } from "../lib/connect-all.js";
|
|
8
8
|
import { detectHumanInitiated } from "../lib/harness.js";
|
|
9
9
|
import { emitNotice } from "../lib/notice.js";
|
|
10
10
|
import { errLine, sanitizeLine } from "../lib/output.js";
|
|
11
|
+
import { codexOauthCompleted } from "../lib/registration-detect.js";
|
|
11
12
|
import { z } from "zod";
|
|
12
13
|
import { installSignalGuard } from "../lib/signals.js";
|
|
13
14
|
import { postConnectEvent, telemetryDisabled } from "../lib/setup-telemetry.js";
|
|
@@ -192,6 +193,9 @@ const CODEX = {
|
|
|
192
193
|
unavailableMessage: CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE,
|
|
193
194
|
},
|
|
194
195
|
launchWalkthrough: launchCodexWalkthrough,
|
|
196
|
+
probeAuthenticated: codexOauthCompleted,
|
|
197
|
+
// Retry only: running it after a finished inline OAuth is a second browser hop.
|
|
198
|
+
loginArgs: ["mcp", "login", "tinyfish"],
|
|
195
199
|
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Codex" }],
|
|
196
200
|
addArgs: (mcpUrl, oauthResource) => [
|
|
197
201
|
"mcp",
|
|
@@ -287,9 +291,24 @@ const OPENCLAW = {
|
|
|
287
291
|
unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
|
|
288
292
|
},
|
|
289
293
|
};
|
|
294
|
+
/**
|
|
295
|
+
* One page-minted id seeds one install attempt, so `tinyfish onboard` connecting three agents
|
|
296
|
+
* does not report three installs under it. Later connects in the run fall back to a random id.
|
|
297
|
+
*/
|
|
298
|
+
function takeSeedAttemptId() {
|
|
299
|
+
const fromEnv = process.env[CONNECT_ATTEMPT_ENV];
|
|
300
|
+
// Both sources are read-and-clear, and the delete lands before any subprocess inherits it.
|
|
301
|
+
delete process.env[CONNECT_ATTEMPT_ENV];
|
|
302
|
+
const parked = takePendingConnectAttempt();
|
|
303
|
+
return fromEnv && isAttemptId(fromEnv) ? fromEnv : parked;
|
|
304
|
+
}
|
|
290
305
|
function createConnectTelemetry(mcpUrl, client, opts) {
|
|
291
|
-
//
|
|
292
|
-
|
|
306
|
+
// Consumed even when --url already supplied an id: leaving it behind would let a stale
|
|
307
|
+
// seed reach the agent this connect launches, or the next connect on this machine.
|
|
308
|
+
const seeded = takeSeedAttemptId();
|
|
309
|
+
// A setup-page id (via --url, the env var, or one parked by login) joins "copied the
|
|
310
|
+
// command" to this install attempt.
|
|
311
|
+
const attemptId = opts?.attemptId ?? seeded ?? randomUUID();
|
|
293
312
|
const endpoint = new URL("/api/cli/connect-event", mcpUrl).toString();
|
|
294
313
|
const pending = [];
|
|
295
314
|
async function deliver(body) {
|
|
@@ -612,7 +631,9 @@ async function connectNativeMcpClient(client, options) {
|
|
|
612
631
|
// Undefining `loginArgs` reuses the deferred-auth path codex/hermes already take.
|
|
613
632
|
const loginDegraded = !!client.loginArgs && !optionalSupported;
|
|
614
633
|
const loginArgs = loginDegraded ? undefined : client.loginArgs;
|
|
615
|
-
|
|
634
|
+
// A probed client authenticates inside `mcp add` too, so a failure there is either.
|
|
635
|
+
state.stage =
|
|
636
|
+
loginArgs && !client.probeAuthenticated ? "registration" : "registration_or_authentication";
|
|
616
637
|
errLine(`Adding TinyFish to ${client.displayName}...`);
|
|
617
638
|
const addResult = spawn.sync(client.command, [
|
|
618
639
|
...client.addArgs(mcpUrl.toString(), options.mcpUrl),
|
|
@@ -629,6 +650,11 @@ async function connectNativeMcpClient(client, options) {
|
|
|
629
650
|
});
|
|
630
651
|
}
|
|
631
652
|
telemetry.track("checkpoint", { phase: "registered" });
|
|
653
|
+
// Codex runs OAuth inside `mcp add` and exits 0 either way, so the recorded state decides
|
|
654
|
+
// whether the login step below still runs. Unknown keeps the pre-probe behaviour.
|
|
655
|
+
const addSignedIn = client.probeAuthenticated?.();
|
|
656
|
+
if (addSignedIn)
|
|
657
|
+
telemetry.track("checkpoint", { phase: "oauth_done" });
|
|
632
658
|
const degradedNotes = loginDegraded ? client.degradedAuthNotes : undefined;
|
|
633
659
|
if (useKeyAuth) {
|
|
634
660
|
errLine(degradedNotes?.keyed ?? "Using your stored TinyFish API key — no browser sign-in needed.");
|
|
@@ -637,10 +663,13 @@ async function connectNativeMcpClient(client, options) {
|
|
|
637
663
|
// Printed here, not with the closing line, so a `--launch` run sees it before the handover.
|
|
638
664
|
errLine(degradedNotes.deferred);
|
|
639
665
|
}
|
|
640
|
-
|
|
666
|
+
// A probed client signs in again only when its inline OAuth is known to have been abandoned.
|
|
667
|
+
const retryLogin = client.probeAuthenticated ? addSignedIn === false : true;
|
|
668
|
+
const pendingLogin = !useKeyAuth && retryLogin ? loginArgs : undefined;
|
|
669
|
+
if (pendingLogin) {
|
|
641
670
|
state.stage = "client_oauth";
|
|
642
671
|
errLine("Signing in to TinyFish...");
|
|
643
|
-
const loginResult = spawn.sync(client.command,
|
|
672
|
+
const loginResult = spawn.sync(client.command, pendingLogin, {
|
|
644
673
|
stdio: "inherit",
|
|
645
674
|
});
|
|
646
675
|
if (loginResult.error || loginResult.status !== 0) {
|
|
@@ -653,7 +682,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
653
682
|
}
|
|
654
683
|
// Registration/OAuth make MCP work; later steps are cosmetic and must not fail the attempt.
|
|
655
684
|
settle(state, telemetry, "completed");
|
|
656
|
-
runPostInstallSteps(client, options, state, telemetry, !
|
|
685
|
+
runPostInstallSteps(client, options, state, telemetry, !useKeyAuth && !addSignedIn && !pendingLogin);
|
|
657
686
|
});
|
|
658
687
|
}
|
|
659
688
|
function connectedLine(client, signInDeferred) {
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -2,14 +2,28 @@ export { DASHBOARD_URL, getDashboardUrl } from "./constants.js";
|
|
|
2
2
|
export declare function configDir(): string;
|
|
3
3
|
/** Install channel baked onto connect context; the connect flow is always CLI-driven. */
|
|
4
4
|
export declare const CONNECT_SOURCE = "tinyfish_cli";
|
|
5
|
+
export declare const CONNECT_ATTEMPT_ENV = "TINYFISH_CONNECT_ATTEMPT_ID";
|
|
5
6
|
interface ConnectEntry {
|
|
6
7
|
attempt_id: string;
|
|
7
8
|
}
|
|
9
|
+
interface PendingConnectAttempt {
|
|
10
|
+
id: string;
|
|
11
|
+
ts: number;
|
|
12
|
+
}
|
|
8
13
|
interface TinyfishConfig {
|
|
9
14
|
api_key?: string;
|
|
10
15
|
connect?: Record<string, ConnectEntry>;
|
|
16
|
+
pending_connect_attempt?: PendingConnectAttempt;
|
|
11
17
|
}
|
|
12
18
|
export declare function loadConfig(): TinyfishConfig;
|
|
19
|
+
export declare function isAttemptId(value: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Park a page-minted attempt id for the connect that follows in a later process:
|
|
22
|
+
* `auth login` and the connect it leads to are separate copied commands.
|
|
23
|
+
*/
|
|
24
|
+
export declare function savePendingConnectAttempt(attemptId: string): void;
|
|
25
|
+
/** Read-and-clear: one parked id seeds one install attempt, expired or not. */
|
|
26
|
+
export declare function takePendingConnectAttempt(): string | undefined;
|
|
13
27
|
/** Throwing variant for callers with their own cleanup (connect's telemetry flush). */
|
|
14
28
|
export declare function writeConfig(apiKey: string): void;
|
|
15
29
|
export declare function saveConfig(apiKey: string): void;
|
package/dist/lib/auth.js
CHANGED
|
@@ -15,6 +15,9 @@ function configFile() {
|
|
|
15
15
|
}
|
|
16
16
|
/** Install channel baked onto connect context; the connect flow is always CLI-driven. */
|
|
17
17
|
export const CONNECT_SOURCE = "tinyfish_cli";
|
|
18
|
+
// An env var, not a flag: released CLIs ignore an unknown one, and the installer's PATH
|
|
19
|
+
// prepend means a copied command can still run a CLI far older than the page that wrote it.
|
|
20
|
+
export const CONNECT_ATTEMPT_ENV = "TINYFISH_CONNECT_ATTEMPT_ID";
|
|
18
21
|
export function loadConfig() {
|
|
19
22
|
try {
|
|
20
23
|
const raw = JSON.parse(fs.readFileSync(configFile(), "utf8"));
|
|
@@ -27,6 +30,9 @@ export function loadConfig() {
|
|
|
27
30
|
const connect = parseConnectMap(obj["connect"]);
|
|
28
31
|
if (connect)
|
|
29
32
|
config.connect = connect;
|
|
33
|
+
const pending = parsePendingAttempt(obj["pending_connect_attempt"]);
|
|
34
|
+
if (pending)
|
|
35
|
+
config.pending_connect_attempt = pending;
|
|
30
36
|
return config;
|
|
31
37
|
}
|
|
32
38
|
catch {
|
|
@@ -44,16 +50,63 @@ function parseConnectMap(value) {
|
|
|
44
50
|
}
|
|
45
51
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
46
52
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
function parsePendingAttempt(value) {
|
|
54
|
+
const entry = value;
|
|
55
|
+
if (typeof entry?.id !== "string" || typeof entry.ts !== "number")
|
|
56
|
+
return undefined;
|
|
57
|
+
return isAttemptId(entry.id) ? { id: entry.id, ts: entry.ts } : undefined;
|
|
58
|
+
}
|
|
59
|
+
export function isAttemptId(value) {
|
|
60
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The one read-modify-write of the config file. Every caller goes through it so the
|
|
64
|
+
* loadConfig whitelist stays the single definition of what survives a write. Throws;
|
|
65
|
+
* callers own their failure policy, which differs per command.
|
|
66
|
+
*/
|
|
67
|
+
function updateConfig(change) {
|
|
50
68
|
// 0o700: only owner can read/write/execute the directory
|
|
51
|
-
fs.mkdirSync(
|
|
52
|
-
const existing = loadConfig();
|
|
69
|
+
fs.mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
53
70
|
// 0o600: only owner can read/write the config file (key is sensitive)
|
|
54
|
-
fs.writeFileSync(configFile(), JSON.stringify({
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
fs.writeFileSync(configFile(), JSON.stringify(change(loadConfig())), { mode: 0o600 });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Park a page-minted attempt id for the connect that follows in a later process:
|
|
75
|
+
* `auth login` and the connect it leads to are separate copied commands.
|
|
76
|
+
*/
|
|
77
|
+
export function savePendingConnectAttempt(attemptId) {
|
|
78
|
+
if (!isAttemptId(attemptId))
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
updateConfig((config) => ({
|
|
82
|
+
...config,
|
|
83
|
+
pending_connect_attempt: { id: attemptId, ts: Date.now() },
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Attribution is never worth failing a sign-in for.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Older than this and the id belongs to some abandoned setup, not the connect running now.
|
|
91
|
+
const PENDING_ATTEMPT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
92
|
+
/** Read-and-clear: one parked id seeds one install attempt, expired or not. */
|
|
93
|
+
export function takePendingConnectAttempt() {
|
|
94
|
+
const pending = loadConfig().pending_connect_attempt;
|
|
95
|
+
if (!pending)
|
|
96
|
+
return undefined;
|
|
97
|
+
try {
|
|
98
|
+
updateConfig(({ pending_connect_attempt: _cleared, ...rest }) => rest);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// An id still on disk would be claimed twice, and a double-counted install is worse
|
|
102
|
+
// than a missed join.
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return Date.now() - pending.ts <= PENDING_ATTEMPT_MAX_AGE_MS ? pending.id : undefined;
|
|
106
|
+
}
|
|
107
|
+
/** Throwing variant for callers with their own cleanup (connect's telemetry flush). */
|
|
108
|
+
export function writeConfig(apiKey) {
|
|
109
|
+
updateConfig((config) => ({ ...config, api_key: apiKey }));
|
|
57
110
|
}
|
|
58
111
|
export function saveConfig(apiKey) {
|
|
59
112
|
try {
|
|
@@ -65,12 +118,11 @@ export function saveConfig(apiKey) {
|
|
|
65
118
|
}
|
|
66
119
|
}
|
|
67
120
|
export function saveConnectContext(client, attemptId) {
|
|
68
|
-
const dir = configDir();
|
|
69
121
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
122
|
+
updateConfig((config) => ({
|
|
123
|
+
...config,
|
|
124
|
+
connect: { ...config.connect, [client]: { attempt_id: attemptId } },
|
|
125
|
+
}));
|
|
74
126
|
}
|
|
75
127
|
catch (e) {
|
|
76
128
|
// Best-effort attribution marker: a write failure must not fail a connect that has
|
|
@@ -119,12 +171,12 @@ export function persistApiKeyToEnvironment(apiKey, options = {}) {
|
|
|
119
171
|
fs.chmodSync(profile, 0o600);
|
|
120
172
|
}
|
|
121
173
|
export function clearConfig() {
|
|
122
|
-
|
|
123
|
-
if (!config.api_key)
|
|
174
|
+
if (!loadConfig().api_key)
|
|
124
175
|
return false;
|
|
125
|
-
delete config.api_key;
|
|
126
176
|
try {
|
|
127
|
-
|
|
177
|
+
// The parked id goes too: the next person to sign in on this machine is not the one
|
|
178
|
+
// whose copy minted it.
|
|
179
|
+
updateConfig(({ api_key: _key, pending_connect_attempt: _attempt, ...rest }) => rest);
|
|
128
180
|
return true;
|
|
129
181
|
}
|
|
130
182
|
catch {
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -18,6 +18,7 @@ const DISPLAY_NAMES = {
|
|
|
18
18
|
opencode: "OpenCode",
|
|
19
19
|
};
|
|
20
20
|
// `codex mcp add` succeeds unauthenticated; OAuth lands at first tool use.
|
|
21
|
+
// TODO(PF-3580): connect now probes Codex sign-in, so this over-warns a signed-in install.
|
|
21
22
|
const AUTH_DEFERRED_AT_INSTALL = new Set(["codex"]);
|
|
22
23
|
// Harnesses that drive their own OAuth at install — a stored TinyFish key says nothing
|
|
23
24
|
// about their session, so it must never unlock a headless run for them.
|
|
@@ -43,8 +43,8 @@ declare const doctorHarnessSchema: z.ZodObject<{
|
|
|
43
43
|
}>;
|
|
44
44
|
auth_mode: z.ZodEnum<{
|
|
45
45
|
unknown: "unknown";
|
|
46
|
-
oauth: "oauth";
|
|
47
46
|
"api-key": "api-key";
|
|
47
|
+
oauth: "oauth";
|
|
48
48
|
}>;
|
|
49
49
|
proves_harness_reach: z.ZodBoolean;
|
|
50
50
|
}, z.core.$strip>;
|
|
@@ -105,8 +105,8 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
105
105
|
}>;
|
|
106
106
|
auth_mode: z.ZodEnum<{
|
|
107
107
|
unknown: "unknown";
|
|
108
|
-
oauth: "oauth";
|
|
109
108
|
"api-key": "api-key";
|
|
109
|
+
oauth: "oauth";
|
|
110
110
|
}>;
|
|
111
111
|
proves_harness_reach: z.ZodBoolean;
|
|
112
112
|
}, z.core.$strip>>;
|
|
@@ -11,5 +11,6 @@ export interface RegistrationStatus {
|
|
|
11
11
|
registeredUrl?: string;
|
|
12
12
|
reason?: string;
|
|
13
13
|
}
|
|
14
|
+
export declare function codexOauthCompleted(): boolean | undefined;
|
|
14
15
|
/** Never throws; a failed probe is `unknown` with a reason, so no caller can read it as healthy. */
|
|
15
16
|
export declare function detectRegistrations(harnesses?: Harness[]): RegistrationStatus[];
|
|
@@ -129,19 +129,14 @@ function fromMcpGet(command, keyAuthPattern) {
|
|
|
129
129
|
}
|
|
130
130
|
// `codex mcp get <missing>` prints an error and exits 0, so presence must never be read from
|
|
131
131
|
// its exit code; `mcp list --json` is the only trustworthy read-back.
|
|
132
|
-
function
|
|
132
|
+
function readCodexEntry() {
|
|
133
133
|
const probe = runProbe("codex", ["mcp", "list", "--json"]);
|
|
134
|
-
if (probe.outcome === "unavailable")
|
|
135
|
-
return {
|
|
136
|
-
}
|
|
134
|
+
if (probe.outcome === "unavailable")
|
|
135
|
+
return { reason: probe.reason };
|
|
137
136
|
// Presence is never inferred from exit code 0, but a nonzero exit means the output cannot
|
|
138
137
|
// be trusted either — that is `unknown`, not "no server".
|
|
139
138
|
if (probe.exitCode !== 0) {
|
|
140
|
-
return {
|
|
141
|
-
registered: "unknown",
|
|
142
|
-
authMode: "unknown",
|
|
143
|
-
reason: `\`codex mcp list --json\` exited ${probe.exitCode}`,
|
|
144
|
-
};
|
|
139
|
+
return { reason: `\`codex mcp list --json\` exited ${probe.exitCode}` };
|
|
145
140
|
}
|
|
146
141
|
let servers;
|
|
147
142
|
try {
|
|
@@ -150,26 +145,35 @@ function probeCodex() {
|
|
|
150
145
|
servers = JSON.parse(probe.stdout);
|
|
151
146
|
}
|
|
152
147
|
catch {
|
|
153
|
-
return {
|
|
154
|
-
registered: "unknown",
|
|
155
|
-
authMode: "unknown",
|
|
156
|
-
reason: "`codex mcp list --json` returned unparseable output",
|
|
157
|
-
};
|
|
148
|
+
return { reason: "`codex mcp list --json` returned unparseable output" };
|
|
158
149
|
}
|
|
159
150
|
// A scalar or a wrapper object decodes fine and then silently reports "no server", which
|
|
160
151
|
// would suggest a spurious repair. Anything but a list is unknown.
|
|
161
152
|
const shape = codexListSchema.safeParse(servers);
|
|
162
|
-
if (!shape.success)
|
|
163
|
-
return {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
153
|
+
if (!shape.success)
|
|
154
|
+
return { reason: "`codex mcp list --json` returned an unexpected shape" };
|
|
155
|
+
return { entry: shape.data.find((s) => s.name === "tinyfish") };
|
|
156
|
+
}
|
|
157
|
+
// `mcp add` exits 0 whether or not its inline OAuth finished, so `not_logged_in` is the only
|
|
158
|
+
// evidence of an abandoned sign-in; every other state, older Codex included, is unknown.
|
|
159
|
+
export function codexOauthCompleted() {
|
|
160
|
+
const read = readCodexEntry();
|
|
161
|
+
if ("reason" in read)
|
|
162
|
+
return undefined;
|
|
163
|
+
const status = read.entry?.auth_status;
|
|
164
|
+
if (status === "o_auth")
|
|
165
|
+
return true;
|
|
166
|
+
if (status === "not_logged_in")
|
|
167
|
+
return false;
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
function probeCodex() {
|
|
171
|
+
const read = readCodexEntry();
|
|
172
|
+
if ("reason" in read)
|
|
173
|
+
return { registered: "unknown", authMode: "unknown", reason: read.reason };
|
|
174
|
+
const entry = read.entry;
|
|
170
175
|
if (!entry)
|
|
171
176
|
return { registered: "no", authMode: "unknown" };
|
|
172
|
-
// auth_status is the configured auth type, not whether a login succeeded — exactly auth_mode.
|
|
173
177
|
const codexUrl = entry.transport?.url;
|
|
174
178
|
return {
|
|
175
179
|
registered: "yes",
|