@tiny-fish/cli 0.30.2-next.257 → 0.30.2-next.259
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.d.ts +1 -1
- package/dist/commands/auth.js +52 -52
- package/dist/commands/batch.d.ts +1 -1
- package/dist/commands/batch.js +54 -46
- package/dist/commands/browser.d.ts +1 -1
- package/dist/commands/browser.js +14 -14
- package/dist/commands/config-claude.d.ts +1 -1
- package/dist/commands/config-claude.js +22 -22
- package/dist/commands/connect.d.ts +3 -3
- package/dist/commands/connect.js +150 -150
- package/dist/commands/doctor.d.ts +4 -4
- package/dist/commands/doctor.js +104 -104
- package/dist/commands/fetch.d.ts +1 -1
- package/dist/commands/fetch.js +24 -27
- package/dist/commands/onboard.d.ts +1 -1
- package/dist/commands/onboard.js +50 -50
- package/dist/commands/profile.d.ts +3 -3
- package/dist/commands/profile.js +45 -45
- package/dist/commands/run.d.ts +1 -1
- package/dist/commands/run.js +78 -65
- package/dist/commands/runs.d.ts +1 -1
- package/dist/commands/runs.js +63 -50
- package/dist/commands/search.d.ts +1 -1
- package/dist/commands/search.js +16 -16
- package/dist/commands/upgrade.d.ts +2 -2
- package/dist/commands/upgrade.js +36 -36
- package/dist/commands/vault.d.ts +1 -1
- package/dist/commands/vault.js +48 -45
- package/dist/commands/wallet.d.ts +1 -1
- package/dist/commands/wallet.js +14 -14
- package/dist/index.js +2 -2
- package/dist/lib/auth.d.ts +5 -5
- package/dist/lib/auth.js +56 -56
- package/dist/lib/batch-store.d.ts +1 -1
- package/dist/lib/batch-store.js +4 -4
- package/dist/lib/chrome-cookies.d.ts +1 -1
- package/dist/lib/chrome-cookies.js +24 -24
- package/dist/lib/claude-config.js +37 -37
- package/dist/lib/cli-install.js +12 -12
- package/dist/lib/client.d.ts +2 -2
- package/dist/lib/client.js +61 -61
- package/dist/lib/connect-all.js +140 -134
- package/dist/lib/connect-auth.d.ts +2 -2
- package/dist/lib/connect-auth.js +15 -15
- package/dist/lib/connect-clients.d.ts +6 -6
- package/dist/lib/connect-clients.js +143 -143
- package/dist/lib/connect-fallback.d.ts +1 -1
- package/dist/lib/connect-fallback.js +38 -38
- package/dist/lib/connect-picker.d.ts +4 -4
- package/dist/lib/connect-picker.js +28 -28
- package/dist/lib/connect-runtime.d.ts +7 -7
- package/dist/lib/connect-runtime.js +61 -61
- package/dist/lib/constants.js +13 -13
- package/dist/lib/cursor-config.d.ts +1 -1
- package/dist/lib/cursor-config.js +30 -30
- package/dist/lib/doctor-report.d.ts +2 -2
- package/dist/lib/doctor-report.js +22 -22
- package/dist/lib/harness-detect.js +39 -39
- package/dist/lib/harness.js +9 -9
- package/dist/lib/hermes-env.js +26 -23
- package/dist/lib/install-root.js +12 -12
- package/dist/lib/notice.d.ts +1 -1
- package/dist/lib/notice.js +12 -12
- package/dist/lib/output.js +14 -14
- package/dist/lib/registration-detect.d.ts +3 -3
- package/dist/lib/registration-detect.js +82 -82
- package/dist/lib/setup-telemetry.d.ts +4 -4
- package/dist/lib/setup-telemetry.js +34 -34
- package/dist/lib/signals.js +1 -1
- package/dist/lib/skill-install.d.ts +3 -3
- package/dist/lib/skill-install.js +40 -40
- package/dist/lib/types.d.ts +8 -8
- package/dist/lib/verify.d.ts +1 -1
- package/dist/lib/verify.js +28 -28
- package/dist/program.d.ts +1 -1
- package/dist/program.js +28 -28
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Command } from
|
|
2
|
-
import { type DoctorRepair, type DoctorReport } from
|
|
3
|
-
import { type Harness } from
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { type DoctorRepair, type DoctorReport } from '../lib/doctor-report.js';
|
|
3
|
+
import { type Harness } from '../lib/harness-detect.js';
|
|
4
4
|
export interface DoctorOptions {
|
|
5
5
|
mcpUrl: string;
|
|
6
6
|
harness?: Harness;
|
|
@@ -10,7 +10,7 @@ export interface DoctorOptions {
|
|
|
10
10
|
export declare function runDoctor(options: DoctorOptions): Promise<DoctorReport>;
|
|
11
11
|
export interface RepairOutcome {
|
|
12
12
|
repair: DoctorRepair;
|
|
13
|
-
status:
|
|
13
|
+
status: 'repaired' | 'skipped' | 'failed';
|
|
14
14
|
reason?: string;
|
|
15
15
|
}
|
|
16
16
|
export declare function applyRepairs(repairs: DoctorRepair[], options: DoctorOptions): Promise<RepairOutcome[]>;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import spawn from
|
|
2
|
-
import { apiKeyStatus } from
|
|
3
|
-
import { CLI_VERSION } from
|
|
4
|
-
import { DOCTOR_COULD_NOT_RUN, DOCTOR_SCHEMA_VERSION, doctorReportSchema, exitCodeFor, renderPretty, verdictFor, } from
|
|
5
|
-
import { ALL_HARNESSES, AuthMode, Registered } from
|
|
6
|
-
import { detectHumanInitiated } from
|
|
7
|
-
import { err, errLine, out, outLine } from
|
|
8
|
-
import { detectRegistrations } from
|
|
9
|
-
import { sendDoctorCompleted } from
|
|
10
|
-
import { verifyMcpAuth, verifyMcpHealth } from
|
|
11
|
-
import { connectClaudeCode, connectCodex, connectCursor, connectGrok, connectHermes, connectOpenClaw, connectOpencode, DEFAULT_MCP_URL, } from
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
import { apiKeyStatus } from '../lib/auth.js';
|
|
3
|
+
import { CLI_VERSION } from '../lib/constants.js';
|
|
4
|
+
import { DOCTOR_COULD_NOT_RUN, DOCTOR_SCHEMA_VERSION, doctorReportSchema, exitCodeFor, renderPretty, verdictFor, } from '../lib/doctor-report.js';
|
|
5
|
+
import { ALL_HARNESSES, AuthMode, Registered } from '../lib/harness-detect.js';
|
|
6
|
+
import { detectHumanInitiated } from '../lib/harness.js';
|
|
7
|
+
import { err, errLine, out, outLine } from '../lib/output.js';
|
|
8
|
+
import { detectRegistrations } from '../lib/registration-detect.js';
|
|
9
|
+
import { sendDoctorCompleted } from '../lib/setup-telemetry.js';
|
|
10
|
+
import { verifyMcpAuth, verifyMcpHealth } from '../lib/verify.js';
|
|
11
|
+
import { connectClaudeCode, connectCodex, connectCursor, connectGrok, connectHermes, connectOpenClaw, connectOpencode, DEFAULT_MCP_URL, } from './connect.js';
|
|
12
12
|
// A green auth mode with a red auth call proves nothing, so the call's verdict gates the claim.
|
|
13
13
|
function provesHarnessReach(status, keyAuth, healthOk, mcpUrl) {
|
|
14
14
|
// An endpoint doctor could not reach proves nothing about a harness pointed at it.
|
|
@@ -26,30 +26,30 @@ function provesHarnessReach(status, keyAuth, healthOk, mcpUrl) {
|
|
|
26
26
|
// Reports the version, never judges it: staleness is the server's call via X-TF-Notice.
|
|
27
27
|
function checkCliVersion() {
|
|
28
28
|
return {
|
|
29
|
-
id:
|
|
29
|
+
id: 'cli-version',
|
|
30
30
|
title: `CLI ${CLI_VERSION}`,
|
|
31
|
-
status:
|
|
31
|
+
status: 'pass',
|
|
32
32
|
detail: `${CLI_VERSION}; the server reports staleness via X-TF-Notice`,
|
|
33
33
|
harness: null,
|
|
34
|
-
scope:
|
|
34
|
+
scope: 'info',
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
async function checkConnectivity(mcpUrl) {
|
|
38
38
|
const health = await verifyMcpHealth(mcpUrl);
|
|
39
39
|
return {
|
|
40
|
-
id:
|
|
41
|
-
title:
|
|
42
|
-
status: health.ok ?
|
|
43
|
-
detail: health.ok ? mcpUrl : (health.code ??
|
|
40
|
+
id: 'mcp-connectivity',
|
|
41
|
+
title: 'MCP endpoint reachable',
|
|
42
|
+
status: health.ok ? 'pass' : 'fail',
|
|
43
|
+
detail: health.ok ? mcpUrl : (health.code ?? 'the endpoint is unreachable'),
|
|
44
44
|
// Harness-scoped: the CLI's own reachability is `cli-auth-call`'s claim.
|
|
45
45
|
harness: null,
|
|
46
|
-
scope:
|
|
46
|
+
scope: 'harness',
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
// Query strings carry the connect attempt id, so they differ on every healthy install.
|
|
50
50
|
function endpointOf(url) {
|
|
51
51
|
const parsed = new URL(url);
|
|
52
|
-
return `${parsed.origin}${parsed.pathname.replace(/\/$/,
|
|
52
|
+
return `${parsed.origin}${parsed.pathname.replace(/\/$/, '')}`;
|
|
53
53
|
}
|
|
54
54
|
/** Host alone let `/wrong-path` and plain `http://` through and claim the harness reaches us. */
|
|
55
55
|
function pointsElsewhere(registeredUrl, mcpUrl) {
|
|
@@ -61,7 +61,7 @@ function pointsElsewhere(registeredUrl, mcpUrl) {
|
|
|
61
61
|
}
|
|
62
62
|
catch {
|
|
63
63
|
// An endpoint that will not parse cannot be shown to match, so it does not.
|
|
64
|
-
return
|
|
64
|
+
return 'an unparseable endpoint';
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
/** Either it reads the CLI's key at runtime or it holds a copy: the CLI's own verdict covers both. */
|
|
@@ -74,59 +74,59 @@ function keyedVerdict(status, keyAuth, cliKeyMissing) {
|
|
|
74
74
|
if (runsOnCliCredential(status)) {
|
|
75
75
|
return cliKeyMissing
|
|
76
76
|
? {
|
|
77
|
-
status:
|
|
78
|
-
detail:
|
|
77
|
+
status: 'fail',
|
|
78
|
+
detail: 'registered, but it runs on the CLI credential, which is missing or unusable',
|
|
79
79
|
}
|
|
80
80
|
: // Reachable with a key only when `--url` skipped verification.
|
|
81
81
|
{
|
|
82
|
-
status:
|
|
83
|
-
detail:
|
|
82
|
+
status: 'warn',
|
|
83
|
+
detail: 'registered, but its CLI credential was not verified against this endpoint',
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
// A bare warn beside exit 0 reads as a pass.
|
|
87
|
-
const because = status.keyReason ??
|
|
88
|
-
return { status:
|
|
87
|
+
const because = status.keyReason ?? 'the CLI cannot verify a key it does not hold';
|
|
88
|
+
return { status: 'warn', detail: `registered, but ${because}, so its reach is unproven` };
|
|
89
89
|
}
|
|
90
90
|
if (keyAuth.ok) {
|
|
91
91
|
return {
|
|
92
|
-
status:
|
|
93
|
-
detail:
|
|
92
|
+
status: 'pass',
|
|
93
|
+
detail: 'registered, auth mode api-key (key verified against the TinyFish API)',
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
if (keyAuth.status === 401) {
|
|
97
|
-
return { status:
|
|
97
|
+
return { status: 'fail', detail: 'registered, but TinyFish rejects its API key' };
|
|
98
98
|
}
|
|
99
99
|
// Only 401 blames the key; connect would loop on the rest.
|
|
100
100
|
if (keyAuth.status === 403) {
|
|
101
|
-
return { status:
|
|
101
|
+
return { status: 'warn', detail: 'registered, but its API key is not allowed to list runs' };
|
|
102
102
|
}
|
|
103
|
-
return { status:
|
|
103
|
+
return { status: 'warn', detail: 'registered, key could not be verified' };
|
|
104
104
|
}
|
|
105
105
|
function registrationVerdict(status, mcpUrl, keyAuth, cliKeyMissing) {
|
|
106
106
|
if (!status.detected)
|
|
107
|
-
return { status:
|
|
107
|
+
return { status: 'skip', detail: 'harness not installed' };
|
|
108
108
|
if (status.registered === Registered.Unknown) {
|
|
109
|
-
return { status:
|
|
109
|
+
return { status: 'warn', detail: status.reason ?? 'could not determine registration' };
|
|
110
110
|
}
|
|
111
111
|
if (status.registered === Registered.Yes) {
|
|
112
112
|
// A `tinyfish` entry aimed at a dev server is registered and still cannot reach TinyFish.
|
|
113
113
|
const elsewhere = pointsElsewhere(status.registeredUrl, mcpUrl);
|
|
114
114
|
if (elsewhere) {
|
|
115
115
|
return {
|
|
116
|
-
status:
|
|
116
|
+
status: 'fail',
|
|
117
117
|
detail: `registered, but points at ${elsewhere} rather than ${endpointOf(mcpUrl)}`,
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
// Ordered before the key check: the harness's own verdict wins.
|
|
121
121
|
if (status.connected === false) {
|
|
122
122
|
return {
|
|
123
|
-
status:
|
|
124
|
-
detail:
|
|
123
|
+
status: 'warn',
|
|
124
|
+
detail: 'registered, but the harness reports it cannot reach TinyFish; sign in again there',
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
// Symmetric with provesHarnessReach, which already reads a handshake as decisive.
|
|
128
128
|
if (status.connected === true) {
|
|
129
|
-
return { status:
|
|
129
|
+
return { status: 'pass', detail: 'registered, and the harness reports it reaches TinyFish' };
|
|
130
130
|
}
|
|
131
131
|
// Widening past api-key would warn every healthy oauth install.
|
|
132
132
|
if (status.authMode === AuthMode.ApiKey)
|
|
@@ -134,59 +134,59 @@ function registrationVerdict(status, mcpUrl, keyAuth, cliKeyMissing) {
|
|
|
134
134
|
// Recorded, not verified: it never feeds provesHarnessReach.
|
|
135
135
|
if (status.recordedAuthMode) {
|
|
136
136
|
return {
|
|
137
|
-
status:
|
|
137
|
+
status: 'pass',
|
|
138
138
|
detail: `registered, auth mode ${status.recordedAuthMode} ` +
|
|
139
|
-
|
|
139
|
+
'(recorded at connect; this harness does not report its headers)',
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
// A probe that had to explain itself to reach `yes` is the only thing that explains `unknown`.
|
|
143
|
-
const because = status.reason ? ` (${status.reason})` :
|
|
144
|
-
return { status:
|
|
143
|
+
const because = status.reason ? ` (${status.reason})` : '';
|
|
144
|
+
return { status: 'pass', detail: `registered, auth mode ${status.authMode}${because}` };
|
|
145
145
|
}
|
|
146
146
|
// Connected once and now absent is a broken install; never connected is just not set up.
|
|
147
147
|
return status.connectedBefore
|
|
148
|
-
? { status:
|
|
149
|
-
: { status:
|
|
148
|
+
? { status: 'fail', detail: 'connected previously but TinyFish is no longer registered' }
|
|
149
|
+
: { status: 'warn', detail: 'installed but TinyFish was never connected' };
|
|
150
150
|
}
|
|
151
151
|
function checkRegistration(status, mcpUrl, keyAuth, cliKeyMissing) {
|
|
152
152
|
return {
|
|
153
|
-
id:
|
|
153
|
+
id: 'harness-registration',
|
|
154
154
|
title: `${status.harness} registration`,
|
|
155
155
|
harness: status.harness,
|
|
156
|
-
scope:
|
|
156
|
+
scope: 'harness',
|
|
157
157
|
...registrationVerdict(status, mcpUrl, keyAuth, cliKeyMissing),
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
160
|
function checkCredential() {
|
|
161
161
|
const resolved = apiKeyStatus();
|
|
162
162
|
const check = {
|
|
163
|
-
id:
|
|
164
|
-
title:
|
|
165
|
-
status:
|
|
166
|
-
detail:
|
|
163
|
+
id: 'cli-credential',
|
|
164
|
+
title: 'CLI credential',
|
|
165
|
+
status: 'key' in resolved ? 'pass' : 'fail',
|
|
166
|
+
detail: 'key' in resolved ? `key present from ${resolved.source}` : resolved.error,
|
|
167
167
|
harness: null,
|
|
168
|
-
scope:
|
|
168
|
+
scope: 'cli',
|
|
169
169
|
};
|
|
170
|
-
return
|
|
170
|
+
return 'key' in resolved ? { check, key: resolved.key } : { check };
|
|
171
171
|
}
|
|
172
172
|
function checkAuthCall(auth) {
|
|
173
173
|
if (!auth) {
|
|
174
174
|
return {
|
|
175
|
-
id:
|
|
176
|
-
title:
|
|
177
|
-
status:
|
|
178
|
-
detail:
|
|
175
|
+
id: 'cli-auth-call',
|
|
176
|
+
title: 'Authenticated call',
|
|
177
|
+
status: 'skip',
|
|
178
|
+
detail: 'no credential to test',
|
|
179
179
|
harness: null,
|
|
180
|
-
scope:
|
|
180
|
+
scope: 'cli',
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
183
|
return {
|
|
184
|
-
id:
|
|
185
|
-
title:
|
|
186
|
-
status: auth.ok ?
|
|
187
|
-
detail: auth.ok ?
|
|
184
|
+
id: 'cli-auth-call',
|
|
185
|
+
title: 'Authenticated call',
|
|
186
|
+
status: auth.ok ? 'pass' : 'fail',
|
|
187
|
+
detail: auth.ok ? 'listRuns succeeded' : (auth.code ?? 'the authenticated call failed'),
|
|
188
188
|
harness: null,
|
|
189
|
-
scope:
|
|
189
|
+
scope: 'cli',
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
192
|
// `verifyMcpAuth` hits BASE_URL; a sandbox key 401s against prod.
|
|
@@ -208,26 +208,26 @@ function verifyHarnessKey(status, cliAuth, mcpUrl) {
|
|
|
208
208
|
return runsOnCliCredential(status) ? cliAuth : undefined;
|
|
209
209
|
}
|
|
210
210
|
// Only Cursor's repair is a pure local file write; every other one needs a browser sign-in.
|
|
211
|
-
const UNATTENDED_SAFE = new Set([
|
|
211
|
+
const UNATTENDED_SAFE = new Set(['cursor']);
|
|
212
212
|
function repairsFor(checks, statuses) {
|
|
213
213
|
const repairs = [];
|
|
214
214
|
// A revoked-but-well-formed key passes the credential check and fails the call; both need login.
|
|
215
|
-
const credentialBroken = checks.some((c) => (c.id ===
|
|
215
|
+
const credentialBroken = checks.some((c) => (c.id === 'cli-credential' || c.id === 'cli-auth-call') && c.status === 'fail');
|
|
216
216
|
// connect writes the stored key, so replace a dead one first.
|
|
217
217
|
if (credentialBroken) {
|
|
218
218
|
repairs.push({
|
|
219
|
-
for:
|
|
220
|
-
action:
|
|
219
|
+
for: 'cli-credential-invalid',
|
|
220
|
+
action: 'auth-login',
|
|
221
221
|
harness: null,
|
|
222
|
-
command:
|
|
222
|
+
command: 'tinyfish auth login',
|
|
223
223
|
unattended_safe: false,
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
|
-
const authCallPassed = checks.some((c) => c.id ===
|
|
226
|
+
const authCallPassed = checks.some((c) => c.id === 'cli-auth-call' && c.status === 'pass');
|
|
227
227
|
for (const status of statuses) {
|
|
228
228
|
// `unknown` earns nothing: connect would contradict the check's detail.
|
|
229
229
|
// A registered entry that still fails earns one too.
|
|
230
|
-
const brokenRegistration = checks.some((c) => c.harness === status.harness && c.status ===
|
|
230
|
+
const brokenRegistration = checks.some((c) => c.harness === status.harness && c.status === 'fail');
|
|
231
231
|
if (!status.detected || (status.registered !== Registered.No && !brokenRegistration))
|
|
232
232
|
continue;
|
|
233
233
|
repairs.push({
|
|
@@ -236,7 +236,7 @@ function repairsFor(checks, statuses) {
|
|
|
236
236
|
: status.connectedBefore
|
|
237
237
|
? `${status.harness}-registration-lost`
|
|
238
238
|
: `${status.harness}-not-connected`,
|
|
239
|
-
action:
|
|
239
|
+
action: 'connect',
|
|
240
240
|
harness: status.harness,
|
|
241
241
|
command: `tinyfish connect ${status.harness}`,
|
|
242
242
|
// A dead well-formed key passes the format check.
|
|
@@ -261,7 +261,7 @@ export async function runDoctor(options) {
|
|
|
261
261
|
credential.check,
|
|
262
262
|
checkAuthCall(cliAuthResult),
|
|
263
263
|
];
|
|
264
|
-
const healthOk = checks.some((c) => c.id ===
|
|
264
|
+
const healthOk = checks.some((c) => c.id === 'mcp-connectivity' && c.status === 'pass');
|
|
265
265
|
const harnesses = statuses.map((status, i) => ({
|
|
266
266
|
harness: status.harness,
|
|
267
267
|
detected: status.detected,
|
|
@@ -287,18 +287,18 @@ export async function runDoctor(options) {
|
|
|
287
287
|
function runAuthLogin() {
|
|
288
288
|
const entry = process.argv[1];
|
|
289
289
|
if (!entry)
|
|
290
|
-
throw new Error(
|
|
291
|
-
const result = spawn.sync(process.execPath, [entry,
|
|
292
|
-
stdio:
|
|
290
|
+
throw new Error('Could not locate the TinyFish CLI entry point to re-invoke');
|
|
291
|
+
const result = spawn.sync(process.execPath, [entry, 'auth', 'login', '--source', 'doctor'], {
|
|
292
|
+
stdio: 'inherit',
|
|
293
293
|
});
|
|
294
294
|
if (result.error)
|
|
295
|
-
throw new Error(
|
|
295
|
+
throw new Error('Could not run `tinyfish auth login`', { cause: result.error });
|
|
296
296
|
if (result.status !== 0) {
|
|
297
|
-
throw new Error(`\`tinyfish auth login\` exited ${result.status ??
|
|
297
|
+
throw new Error(`\`tinyfish auth login\` exited ${result.status ?? 'unknown'}`);
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
const CONNECTORS = {
|
|
301
|
-
|
|
301
|
+
'claude-code': (opts) => connectClaudeCode({ ...opts, launch: false }),
|
|
302
302
|
codex: (opts) => connectCodex({ ...opts, launch: false }),
|
|
303
303
|
cursor: (opts) => connectCursor(opts),
|
|
304
304
|
grok: (opts) => connectGrok({ ...opts, launch: false }),
|
|
@@ -308,7 +308,7 @@ const CONNECTORS = {
|
|
|
308
308
|
};
|
|
309
309
|
/** Dispatches on `action`, never on a null harness: that conflation made the credential fix a no-op. */
|
|
310
310
|
async function runRepair(repair, mcpUrl) {
|
|
311
|
-
if (repair.action ===
|
|
311
|
+
if (repair.action === 'auth-login')
|
|
312
312
|
return runAuthLogin();
|
|
313
313
|
if (repair.harness)
|
|
314
314
|
await CONNECTORS[repair.harness]({ mcpUrl });
|
|
@@ -320,32 +320,32 @@ export async function applyRepairs(repairs, options) {
|
|
|
320
320
|
if (!interactive && !options.yes) {
|
|
321
321
|
outcomes.push({
|
|
322
322
|
repair,
|
|
323
|
-
status:
|
|
324
|
-
reason:
|
|
323
|
+
status: 'skipped',
|
|
324
|
+
reason: 'no terminal; re-run with --yes to repair unattended',
|
|
325
325
|
});
|
|
326
326
|
continue;
|
|
327
327
|
}
|
|
328
328
|
if (!interactive && !repair.unattended_safe) {
|
|
329
329
|
outcomes.push({
|
|
330
330
|
repair,
|
|
331
|
-
status:
|
|
332
|
-
reason:
|
|
331
|
+
status: 'skipped',
|
|
332
|
+
reason: 'needs a browser sign-in, which has no unattended path',
|
|
333
333
|
});
|
|
334
334
|
continue;
|
|
335
335
|
}
|
|
336
336
|
try {
|
|
337
337
|
await runRepair(repair, options.mcpUrl);
|
|
338
|
-
outcomes.push({ repair, status:
|
|
338
|
+
outcomes.push({ repair, status: 'repaired' });
|
|
339
339
|
}
|
|
340
340
|
catch (e) {
|
|
341
341
|
// Connect quotes absolute config paths in its errors, so the raw text stays behind
|
|
342
342
|
// --debug and the user is told where to find it rather than handed a generic dead end.
|
|
343
|
-
if (process.env[
|
|
343
|
+
if (process.env['TINYFISH_DEBUG']) {
|
|
344
344
|
errLine(e instanceof Error ? (e.stack ?? e.message) : String(e));
|
|
345
345
|
}
|
|
346
346
|
outcomes.push({
|
|
347
347
|
repair,
|
|
348
|
-
status:
|
|
348
|
+
status: 'failed',
|
|
349
349
|
reason: `\`${repair.command}\` failed; re-run it directly, or --debug for the raw error`,
|
|
350
350
|
});
|
|
351
351
|
}
|
|
@@ -360,31 +360,31 @@ function telemetryPayload(report, offered, outcomes, options) {
|
|
|
360
360
|
cli_version: report.cli_version,
|
|
361
361
|
ok_harnesses: report.ok_harnesses,
|
|
362
362
|
ok_cli: report.ok_cli,
|
|
363
|
-
checks_pass: count(
|
|
364
|
-
checks_fail: count(
|
|
365
|
-
checks_warn: count(
|
|
366
|
-
checks_skip: count(
|
|
363
|
+
checks_pass: count('pass'),
|
|
364
|
+
checks_fail: count('fail'),
|
|
365
|
+
checks_warn: count('warn'),
|
|
366
|
+
checks_skip: count('skip'),
|
|
367
367
|
// From the pre-repair report; the verdicts above describe the machine after it.
|
|
368
368
|
repairs_offered: offered.length,
|
|
369
369
|
repair_actions: [...new Set(offered.map((repair) => repair.action))],
|
|
370
|
-
repairs_attempted: outcomes.filter((outcome) => outcome.status !==
|
|
371
|
-
repairs_succeeded: outcomes.filter((outcome) => outcome.status ===
|
|
372
|
-
fix_mode: !options.fix ?
|
|
370
|
+
repairs_attempted: outcomes.filter((outcome) => outcome.status !== 'skipped').length,
|
|
371
|
+
repairs_succeeded: outcomes.filter((outcome) => outcome.status === 'repaired').length,
|
|
372
|
+
fix_mode: !options.fix ? 'none' : options.yes ? 'fix_yes' : 'fix',
|
|
373
373
|
// --harness scopes detection, so the counts above cover that harness alone.
|
|
374
|
-
harness_scope: options.harness ??
|
|
374
|
+
harness_scope: options.harness ?? 'all',
|
|
375
375
|
// Same signal cli_connect_stage carries, so agent and human doctor runs split.
|
|
376
376
|
is_human_initiated: detectHumanInitiated(),
|
|
377
377
|
};
|
|
378
378
|
}
|
|
379
379
|
export function registerDoctor(program) {
|
|
380
380
|
program
|
|
381
|
-
.command(
|
|
382
|
-
.description(
|
|
383
|
-
.option(
|
|
384
|
-
.option(
|
|
385
|
-
.option(
|
|
386
|
-
.option(
|
|
387
|
-
.option(
|
|
381
|
+
.command('doctor')
|
|
382
|
+
.description('Diagnose your TinyFish setup')
|
|
383
|
+
.option('--pretty', 'Human-readable check list instead of JSON')
|
|
384
|
+
.option('--harness <name>', `Restrict to one harness (${ALL_HARNESSES.join(', ')})`)
|
|
385
|
+
.option('--url <mcpUrl>', 'MCP endpoint override')
|
|
386
|
+
.option('--fix', 'Repair what can be repaired by re-running tinyfish connect')
|
|
387
|
+
.option('--yes', 'Allow --fix to run without a terminal (unattended-safe repairs only)')
|
|
388
388
|
.action(async (options) => {
|
|
389
389
|
if (options.harness && !ALL_HARNESSES.includes(options.harness)) {
|
|
390
390
|
err({ error: `Unknown --harness value: ${options.harness}` });
|
|
@@ -406,7 +406,7 @@ export function registerDoctor(program) {
|
|
|
406
406
|
if (doctorOptions.fix && report.repairs.length > 0) {
|
|
407
407
|
outcomes = await applyRepairs(report.repairs, doctorOptions);
|
|
408
408
|
for (const outcome of outcomes) {
|
|
409
|
-
const reason = outcome.reason ? ` (${outcome.reason})` :
|
|
409
|
+
const reason = outcome.reason ? ` (${outcome.reason})` : '';
|
|
410
410
|
errLine(`${outcome.status}: ${outcome.repair.command}${reason}`);
|
|
411
411
|
}
|
|
412
412
|
// Re-diagnose so the emitted report describes the machine after repair, not before.
|
|
@@ -416,10 +416,10 @@ export function registerDoctor(program) {
|
|
|
416
416
|
catch (e) {
|
|
417
417
|
// Exit 2 means doctor itself broke, a different claim from "your setup is broken".
|
|
418
418
|
// Authored, like every other string a user might paste; raw text is behind --debug.
|
|
419
|
-
if (process.env[
|
|
419
|
+
if (process.env['TINYFISH_DEBUG']) {
|
|
420
420
|
errLine(e instanceof Error ? (e.stack ?? e.message) : String(e));
|
|
421
421
|
}
|
|
422
|
-
err({ error:
|
|
422
|
+
err({ error: 'doctor could not complete; re-run with --debug for the raw error' });
|
|
423
423
|
process.exitCode = DOCTOR_COULD_NOT_RUN;
|
|
424
424
|
return;
|
|
425
425
|
}
|
|
@@ -430,6 +430,6 @@ export function registerDoctor(program) {
|
|
|
430
430
|
process.exitCode = exitCodeFor(report.checks);
|
|
431
431
|
// After the exit code is set: telemetry never decides how doctor exits.
|
|
432
432
|
const resolved = apiKeyStatus();
|
|
433
|
-
await sendDoctorCompleted(doctorOptions.mcpUrl, telemetryPayload(report, offered, outcomes, doctorOptions),
|
|
433
|
+
await sendDoctorCompleted(doctorOptions.mcpUrl, telemetryPayload(report, offered, outcomes, doctorOptions), 'key' in resolved ? resolved.key : undefined);
|
|
434
434
|
});
|
|
435
435
|
}
|
package/dist/commands/fetch.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Command } from
|
|
1
|
+
import { Command } from 'commander';
|
|
2
2
|
export declare function registerFetch(program: Command): void;
|
package/dist/commands/fetch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { InvalidArgumentError } from
|
|
2
|
-
import { getApiKey } from
|
|
3
|
-
import { fetchContentGet } from
|
|
4
|
-
import { err, handleApiError, out, outLine } from
|
|
1
|
+
import { InvalidArgumentError } from 'commander';
|
|
2
|
+
import { getApiKey } from '../lib/auth.js';
|
|
3
|
+
import { fetchContentGet } from '../lib/client.js';
|
|
4
|
+
import { err, handleApiError, out, outLine } from '../lib/output.js';
|
|
5
5
|
const MIN_PER_URL_TIMEOUT_MS = 1;
|
|
6
6
|
const MAX_PER_URL_TIMEOUT_MS = 110000;
|
|
7
7
|
function parsePerUrlTimeoutMs(value) {
|
|
@@ -16,9 +16,9 @@ function parsePerUrlTimeoutMs(value) {
|
|
|
16
16
|
function printPrettyFetch(response) {
|
|
17
17
|
outLine(`Results: ${response.results.length}`);
|
|
18
18
|
outLine(`Errors: ${response.errors.length}`);
|
|
19
|
-
outLine(
|
|
19
|
+
outLine('');
|
|
20
20
|
if (response.results.length > 0) {
|
|
21
|
-
outLine(
|
|
21
|
+
outLine('Successful fetches:');
|
|
22
22
|
for (const result of response.results) {
|
|
23
23
|
outLine(`- ${result.url}`);
|
|
24
24
|
outLine(` Format: ${result.format}`);
|
|
@@ -32,44 +32,41 @@ function printPrettyFetch(response) {
|
|
|
32
32
|
outLine(` ETag: ${result.etag}`);
|
|
33
33
|
if (result.last_modified)
|
|
34
34
|
outLine(` Last-Modified: ${result.last_modified}`);
|
|
35
|
-
outLine(
|
|
35
|
+
outLine('');
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
if (response.errors.length > 0) {
|
|
39
|
-
outLine(
|
|
39
|
+
outLine('Errors:');
|
|
40
40
|
for (const error of response.errors) {
|
|
41
41
|
outLine(`- ${error.url}`);
|
|
42
42
|
outLine(` ${error.error}`);
|
|
43
|
-
outLine(
|
|
43
|
+
outLine('');
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
export function registerFetch(program) {
|
|
48
|
-
const fetchCmd = program
|
|
49
|
-
.command("fetch")
|
|
50
|
-
.description("Fetch commands")
|
|
51
|
-
.enablePositionalOptions();
|
|
48
|
+
const fetchCmd = program.command('fetch').description('Fetch commands').enablePositionalOptions();
|
|
52
49
|
const contentCmd = fetchCmd
|
|
53
|
-
.command(
|
|
54
|
-
.description(
|
|
50
|
+
.command('content')
|
|
51
|
+
.description('Content extraction commands')
|
|
55
52
|
.enablePositionalOptions();
|
|
56
53
|
contentCmd
|
|
57
|
-
.command(
|
|
58
|
-
.description(
|
|
59
|
-
.argument(
|
|
60
|
-
.option(
|
|
61
|
-
.option(
|
|
62
|
-
.option(
|
|
63
|
-
.option(
|
|
64
|
-
.option(
|
|
65
|
-
.option(
|
|
66
|
-
.option(
|
|
67
|
-
.option(
|
|
54
|
+
.command('get')
|
|
55
|
+
.description('Fetch clean content from one or more URLs')
|
|
56
|
+
.argument('<urls...>', 'One or more URLs to fetch')
|
|
57
|
+
.option('--format <format>', 'Output format: markdown, html, or json')
|
|
58
|
+
.option('--links', 'Include extracted links')
|
|
59
|
+
.option('--image-links', 'Include extracted image links')
|
|
60
|
+
.option('--per-url-timeout-ms <milliseconds>', 'Per-URL timeout budget in milliseconds', parsePerUrlTimeoutMs)
|
|
61
|
+
.option('--if-none-match <etag>', 'ETag validator for a conditional GET (single URL only)')
|
|
62
|
+
.option('--if-modified-since <http-date>', 'Last-Modified validator for a conditional GET (single URL only)')
|
|
63
|
+
.option('--include-etag-and-last-modified', 'Include etag / last_modified validators on each result')
|
|
64
|
+
.option('--pretty', 'Human-readable output')
|
|
68
65
|
.action(async (urls, opts) => {
|
|
69
66
|
if ((opts.ifNoneMatch !== undefined || opts.ifModifiedSince !== undefined) &&
|
|
70
67
|
urls.length > 1) {
|
|
71
68
|
err({
|
|
72
|
-
error:
|
|
69
|
+
error: '--if-none-match and --if-modified-since can only be used with a single URL, not a batch',
|
|
73
70
|
});
|
|
74
71
|
process.exit(1);
|
|
75
72
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Command } from
|
|
1
|
+
import { Command } from 'commander';
|
|
2
2
|
export declare function registerOnboard(program: Command): void;
|