@tiny-fish/cli 0.19.1-next.180 → 0.20.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/commands/connect.js
CHANGED
|
@@ -35,6 +35,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
35
35
|
await runGuarded(state, telemetry, () => {
|
|
36
36
|
telemetry.track("started");
|
|
37
37
|
const { optionalSupported } = requireCommandSupport(client);
|
|
38
|
+
state.harnessDegraded = !!client.loginArgs && !optionalSupported;
|
|
38
39
|
telemetry.track("checkpoint", { phase: "prerequisite_ok" });
|
|
39
40
|
state.stage = "registration_cleanup";
|
|
40
41
|
for (const removal of client.removals)
|
|
@@ -47,7 +48,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
47
48
|
const storedKey = validatedApiKey(options.apiKey);
|
|
48
49
|
const useKeyAuth = client.headerAuthSupported === true && !!storedKey;
|
|
49
50
|
// Undefining `loginArgs` reuses the deferred-auth path codex/hermes already take.
|
|
50
|
-
const loginDegraded =
|
|
51
|
+
const loginDegraded = state.harnessDegraded === true;
|
|
51
52
|
const loginArgs = loginDegraded ? undefined : client.loginArgs;
|
|
52
53
|
// A probed client authenticates inside `mcp add` too, so a failure there is either.
|
|
53
54
|
state.stage =
|
|
@@ -98,9 +99,15 @@ async function connectNativeMcpClient(client, options) {
|
|
|
98
99
|
}
|
|
99
100
|
telemetry.track("checkpoint", { phase: "oauth_done" });
|
|
100
101
|
}
|
|
102
|
+
const signInDeferred = !useKeyAuth && !addSignedIn && !pendingLogin;
|
|
103
|
+
const authMode = useKeyAuth
|
|
104
|
+
? "api-key"
|
|
105
|
+
: signInDeferred
|
|
106
|
+
? "deferred"
|
|
107
|
+
: "oauth";
|
|
101
108
|
// Registration/OAuth make MCP work; later steps are cosmetic and must not fail the attempt.
|
|
102
|
-
settle(state, telemetry, "completed");
|
|
103
|
-
runPostInstallSteps(client, options, state, telemetry,
|
|
109
|
+
settle(state, telemetry, "completed", { authMode });
|
|
110
|
+
runPostInstallSteps(client, options, state, telemetry, signInDeferred);
|
|
104
111
|
});
|
|
105
112
|
}
|
|
106
113
|
function connectedLine(client, signInDeferred) {
|
|
@@ -163,7 +170,12 @@ export async function connectOpencode(options) {
|
|
|
163
170
|
}
|
|
164
171
|
export async function connectOpenClaw(options) {
|
|
165
172
|
const telemetry = createConnectTelemetry(options.mcpUrl, "openclaw", options);
|
|
166
|
-
|
|
173
|
+
// Skill install, no MCP server — never degraded, and false keeps it inside a `= false` filter.
|
|
174
|
+
const state = {
|
|
175
|
+
stage: "prerequisite_check",
|
|
176
|
+
settled: false,
|
|
177
|
+
harnessDegraded: false,
|
|
178
|
+
};
|
|
167
179
|
await runGuarded(state, telemetry, () => {
|
|
168
180
|
telemetry.track("started");
|
|
169
181
|
requireCommandSupport(OPENCLAW);
|
|
@@ -230,7 +242,13 @@ export async function connectOpenClaw(options) {
|
|
|
230
242
|
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
231
243
|
export async function connectCursor(options) {
|
|
232
244
|
const telemetry = createConnectTelemetry(options.mcpUrl, "cursor", options);
|
|
233
|
-
|
|
245
|
+
// Never degraded: no sign-in command to be missing. False, not absent, so a `= false`
|
|
246
|
+
// filter still catches Cursor.
|
|
247
|
+
const state = {
|
|
248
|
+
stage: "prerequisite_check",
|
|
249
|
+
settled: false,
|
|
250
|
+
harnessDegraded: false,
|
|
251
|
+
};
|
|
234
252
|
await runGuarded(state, telemetry, async () => {
|
|
235
253
|
telemetry.track("started");
|
|
236
254
|
telemetry.track("checkpoint", { phase: "prerequisite_ok" });
|
|
@@ -279,7 +297,7 @@ export async function connectCursor(options) {
|
|
|
279
297
|
else {
|
|
280
298
|
errLine("TinyFish is connected. Reload the Cursor window, then approve/sign in under Settings → MCP.");
|
|
281
299
|
}
|
|
282
|
-
settle(state, telemetry, "completed");
|
|
300
|
+
settle(state, telemetry, "completed", { authMode: resolvedKey ? "api-key" : "deferred" });
|
|
283
301
|
});
|
|
284
302
|
}
|
|
285
303
|
/** Best-effort: false when no handler/open fails — caller falls back to reload copy. */
|
package/dist/commands/doctor.js
CHANGED
|
@@ -93,7 +93,7 @@ function checkCredential() {
|
|
|
93
93
|
id: "cli-credential",
|
|
94
94
|
title: "CLI credential",
|
|
95
95
|
status: "key" in resolved ? "pass" : "fail",
|
|
96
|
-
detail: "key" in resolved ? `
|
|
96
|
+
detail: "key" in resolved ? `key present from ${resolved.source}` : resolved.error,
|
|
97
97
|
harness: null,
|
|
98
98
|
};
|
|
99
99
|
return "key" in resolved ? { check, key: resolved.key } : { check };
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -210,46 +210,53 @@ function computeExitCode(results) {
|
|
|
210
210
|
}
|
|
211
211
|
export async function runConnectAll(opts) {
|
|
212
212
|
const isTTY = detectHumanInitiated();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const detections = detectInstalledHarnesses();
|
|
224
|
-
const results = [];
|
|
225
|
-
try {
|
|
226
|
-
for (const [index, detection] of detections.entries()) {
|
|
227
|
-
const { harness } = detection;
|
|
228
|
-
errLine(`step ${index + 1} of ${detections.length} — ${DISPLAY_NAMES[harness]}`);
|
|
229
|
-
let result;
|
|
213
|
+
let stdinClosed = false;
|
|
214
|
+
// Open interface holds stdin raw; hermes' input() then wedges.
|
|
215
|
+
const prompt = isTTY
|
|
216
|
+
? async (question) => {
|
|
217
|
+
if (stdinClosed)
|
|
218
|
+
throw new Error("stdin is closed");
|
|
219
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
220
|
+
// Without a close-linked signal, question() never settles on Ctrl+D.
|
|
221
|
+
const closed = new AbortController();
|
|
222
|
+
rl.on("close", () => closed.abort());
|
|
230
223
|
try {
|
|
231
|
-
|
|
224
|
+
return await rl.question(question, { signal: closed.signal });
|
|
232
225
|
}
|
|
233
226
|
catch (e) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
installed: false,
|
|
240
|
-
outcome: "failed",
|
|
241
|
-
errorMessage: e instanceof Error ? e.message : String(e),
|
|
242
|
-
fixCommand: `tinyfish connect ${harness}`,
|
|
243
|
-
};
|
|
227
|
+
stdinClosed = true;
|
|
228
|
+
throw e;
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
rl.close();
|
|
244
232
|
}
|
|
245
|
-
results.push(result);
|
|
246
|
-
// Ctrl+C means stop, not continue to the next harness.
|
|
247
|
-
if (result.outcome === "interrupted")
|
|
248
|
-
break;
|
|
249
233
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
234
|
+
: undefined;
|
|
235
|
+
const detections = detectInstalledHarnesses();
|
|
236
|
+
const results = [];
|
|
237
|
+
for (const [index, detection] of detections.entries()) {
|
|
238
|
+
const { harness } = detection;
|
|
239
|
+
errLine(`step ${index + 1} of ${detections.length} — ${DISPLAY_NAMES[harness]}`);
|
|
240
|
+
let result;
|
|
241
|
+
try {
|
|
242
|
+
result = await processHarness(detection, opts, prompt);
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
// Isolation: one harness throwing never stops the others.
|
|
246
|
+
result = {
|
|
247
|
+
harness,
|
|
248
|
+
detected: detection.detected,
|
|
249
|
+
configPath: detection.configPath,
|
|
250
|
+
installed: false,
|
|
251
|
+
outcome: "failed",
|
|
252
|
+
errorMessage: e instanceof Error ? e.message : String(e),
|
|
253
|
+
fixCommand: `tinyfish connect ${harness}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
results.push(result);
|
|
257
|
+
// Ctrl+C means stop, not continue to the next harness.
|
|
258
|
+
if (result.outcome === "interrupted")
|
|
259
|
+
break;
|
|
253
260
|
}
|
|
254
261
|
// The interrupt break truncates `results`; pad so the 5-entry telemetry schema still parses.
|
|
255
262
|
for (const detection of detections) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { type AuthMode } from "./registration-detect.js";
|
|
1
2
|
export declare const NON_INTERACTIVE_TIMEOUT_MS = 10000;
|
|
2
3
|
export type AgentClient = "claude-code" | "codex" | "cursor" | "hermes" | "openclaw" | "opencode";
|
|
3
4
|
type ConnectStage = "started" | "checkpoint" | "completed" | "failed" | "aborted" | "post_install_failed";
|
|
4
5
|
type ConnectFailureStage = "prerequisite_check" | "registration_cleanup" | "registration" | "registration_or_authentication" | "client_oauth" | "cli_install" | "skill_install" | "authentication" | "walkthrough_launch";
|
|
5
6
|
type ConnectFailureReason = "harness_not_installed" | "harness_command_unsupported" | "harness_too_old";
|
|
7
|
+
export type ConnectAuthMode = Extract<AuthMode, "oauth" | "api-key"> | "deferred";
|
|
6
8
|
type ConnectCheckpoint = "prerequisite_ok" | "registered" | "oauth_done" | "cli_installed" | "skill_installed" | "authenticated";
|
|
7
9
|
/** Ctrl+C/SIGTERM killed a setup child — abandonment, not error. */
|
|
8
10
|
export declare class ConnectInterruptedError extends Error {
|
|
@@ -15,12 +17,17 @@ export declare function commandNotFound(error: unknown): boolean;
|
|
|
15
17
|
export interface ConnectRunState {
|
|
16
18
|
stage: ConnectFailureStage;
|
|
17
19
|
settled: boolean;
|
|
20
|
+
/** On state, not per-call: otherwise degraded installs never appear to fail. */
|
|
21
|
+
harnessDegraded?: boolean;
|
|
18
22
|
}
|
|
19
23
|
interface ConnectStageDetail {
|
|
20
24
|
failedStage?: ConnectFailureStage;
|
|
21
25
|
phase?: ConnectCheckpoint;
|
|
22
26
|
failureReason?: ConnectFailureReason;
|
|
23
27
|
harnessVersion?: string;
|
|
28
|
+
authMode?: ConnectAuthMode;
|
|
29
|
+
/** Orthogonal to `authMode`: a stored key wins over `mcp login` on a healthy client too. */
|
|
30
|
+
harnessDegraded?: boolean;
|
|
24
31
|
}
|
|
25
32
|
export interface ConnectTelemetry {
|
|
26
33
|
attemptId: string;
|
|
@@ -36,7 +36,7 @@ export function settle(state, telemetry, stage, detail) {
|
|
|
36
36
|
if (state.settled)
|
|
37
37
|
return;
|
|
38
38
|
state.settled = true;
|
|
39
|
-
telemetry.track(stage, detail);
|
|
39
|
+
telemetry.track(stage, { harnessDegraded: state.harnessDegraded, ...detail });
|
|
40
40
|
}
|
|
41
41
|
// Raw-mode children still surface via throwIfInterrupted; cooked-mode stages
|
|
42
42
|
// (npm/npx/OAuth waits) land here.
|
|
@@ -165,6 +165,8 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
|
|
|
165
165
|
phase: detail?.phase,
|
|
166
166
|
failure_reason: detail?.failureReason,
|
|
167
167
|
harness_version: detail?.harnessVersion,
|
|
168
|
+
auth_mode: detail?.authMode,
|
|
169
|
+
harness_degraded: detail?.harnessDegraded,
|
|
168
170
|
runtime_platform: process.platform,
|
|
169
171
|
node_version: process.version,
|
|
170
172
|
cli_version: CLI_VERSION,
|
|
@@ -6,8 +6,8 @@ import { loadConfig } from "./auth.js";
|
|
|
6
6
|
import { errLine } from "./output.js";
|
|
7
7
|
import { readCursorTinyfishEntry } from "./cursor-config.js";
|
|
8
8
|
import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, } from "./harness-detect.js";
|
|
9
|
-
//
|
|
10
|
-
const PROBE_TIMEOUT_MS =
|
|
9
|
+
// 6s tripped on a cold `hermes mcp list`, measured at 6.34s.
|
|
10
|
+
const PROBE_TIMEOUT_MS = 15_000;
|
|
11
11
|
// A list, verified against codex 0.146. A name-keyed map was accepted here too, but every
|
|
12
12
|
// field is optional, so that arm parsed *any* object-of-objects: a wrapper like
|
|
13
13
|
// `{"servers": {...}}` decoded as one entry named `servers` and reported TinyFish absent.
|