@tiny-fish/cli 0.30.2-next.264 → 0.30.2-next.266
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 +46 -17
- package/dist/commands/run.js +193 -174
- package/dist/lib/client.d.ts +1 -1
- package/dist/lib/client.js +6 -4
- package/dist/lib/connect-all.js +4 -1
- package/dist/lib/connect-clients.d.ts +17 -3
- package/dist/lib/connect-clients.js +1 -3
- package/dist/lib/mcp-endpoint.d.ts +3 -1
- package/dist/lib/mcp-endpoint.js +10 -1
- package/dist/lib/verify.d.ts +1 -1
- package/dist/lib/verify.js +2 -2
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -16,7 +16,7 @@ import { SIGN_IN_PROBES } from '../lib/registration-detect.js';
|
|
|
16
16
|
import { errLine, warnLine } from '../lib/output.js';
|
|
17
17
|
import { z } from 'zod';
|
|
18
18
|
import { verifyMcpAuth } from '../lib/verify.js';
|
|
19
|
-
import {
|
|
19
|
+
import { apiBaseFromMcpUrl, DEFAULT_MCP_URL } from '../lib/mcp-endpoint.js';
|
|
20
20
|
function removeExistingRegistration(client, removal, verbose) {
|
|
21
21
|
const result = spawnRemoval(client.command, removal.args);
|
|
22
22
|
if (result.status === 0)
|
|
@@ -48,9 +48,43 @@ function exportApiKey(apiKey, displayName, deferOutro) {
|
|
|
48
48
|
}
|
|
49
49
|
return true;
|
|
50
50
|
}
|
|
51
|
+
/** Undefined leaves the client on its keyless path, if any. */
|
|
52
|
+
function resolveKeyedAdd(client, storedKey, keyAuthSupported, deferOutro) {
|
|
53
|
+
if (!storedKey || !keyAuthSupported || !client.keyAuth)
|
|
54
|
+
return undefined;
|
|
55
|
+
const keyed = { key: storedKey, spec: client.keyAuth };
|
|
56
|
+
// Before the removals: a failed export must not strand the old registration.
|
|
57
|
+
if (keyed.spec.envVar && !exportApiKey(keyed.key, client.displayName, deferOutro)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return keyed;
|
|
61
|
+
}
|
|
62
|
+
/** No sign-in to degrade to, so the install stops. */
|
|
63
|
+
function refuseKeylessInstall(client, keyAuthSupported) {
|
|
64
|
+
const noSignIn = `TinyFish does not sign in to ${client.displayName} any other way`;
|
|
65
|
+
const rerun = `re-run: tinyfish connect ${client.connectClient}`;
|
|
66
|
+
if (!client.keyAuth) {
|
|
67
|
+
throw new ConnectStepError(`${client.displayName} cannot take a TinyFish API key, and ${noSignIn}. ` +
|
|
68
|
+
`Run: tinyfish connect ${client.connectClient}`, 'key_auth_unsupported');
|
|
69
|
+
}
|
|
70
|
+
if (!keyAuthSupported) {
|
|
71
|
+
throw new ConnectStepError(`This ${client.displayName} install cannot take a TinyFish API key, and ${noSignIn}. ` +
|
|
72
|
+
`Update it, then ${rerun}`, 'harness_too_old');
|
|
73
|
+
}
|
|
74
|
+
throw new ConnectStepError(`Connecting ${client.displayName} needs a TinyFish API key, and ${noSignIn}. ` +
|
|
75
|
+
`Run: tinyfish auth login, then ${rerun}`, 'invalid_config');
|
|
76
|
+
}
|
|
77
|
+
/** Resolved before the removals, so a refusal strands no registration. */
|
|
78
|
+
function keylessAddArgs(client, keyAuthSupported, oauthResource) {
|
|
79
|
+
if (!client.addArgs)
|
|
80
|
+
refuseKeylessInstall(client, keyAuthSupported);
|
|
81
|
+
const addArgs = client.addArgs;
|
|
82
|
+
return (mcpUrl) => addArgs(mcpUrl, oauthResource);
|
|
83
|
+
}
|
|
51
84
|
/** Runs before any write: a refused key strands nothing. */
|
|
52
|
-
async function verifyKeyBeforeSeeding(apiKey, displayName) {
|
|
53
|
-
|
|
85
|
+
async function verifyKeyBeforeSeeding(apiKey, displayName, mcpUrl) {
|
|
86
|
+
// One host serves `/mcp` and `/v1`, so `--url` decides where the key is checked.
|
|
87
|
+
const verified = await verifyMcpAuth(apiKey, apiBaseFromMcpUrl(mcpUrl));
|
|
54
88
|
if (verified.ok)
|
|
55
89
|
return;
|
|
56
90
|
// No status means the network failed, not the key.
|
|
@@ -128,15 +162,12 @@ async function connectNativeMcpClient(client, options) {
|
|
|
128
162
|
state.harnessDegraded = !!client.loginArgs && !optionalSupported;
|
|
129
163
|
// Prod MCP accepts X-API-Key, so a stored key replaces the browser OAuth hop.
|
|
130
164
|
const storedKey = validatedApiKey(options.apiKey);
|
|
131
|
-
|
|
132
|
-
? { key: storedKey, spec: client.keyAuth }
|
|
133
|
-
: undefined;
|
|
134
|
-
// Before the removals: a failed export must not strand the old registration.
|
|
135
|
-
if (keyedAdd?.spec.envVar &&
|
|
136
|
-
!exportApiKey(keyedAdd.key, client.displayName, options.deferOutro)) {
|
|
137
|
-
keyedAdd = undefined;
|
|
138
|
-
}
|
|
165
|
+
const keyedAdd = resolveKeyedAdd(client, storedKey, keyAuthSupported, options.deferOutro);
|
|
139
166
|
const useKeyAuth = !!keyedAdd;
|
|
167
|
+
// Refuses here when key-required; others keep their OAuth argv.
|
|
168
|
+
const buildAddArgs = keyedAdd
|
|
169
|
+
? (mcpUrl) => keyedAdd.spec.addArgs(mcpUrl, keyedAdd.key)
|
|
170
|
+
: keylessAddArgs(client, keyAuthSupported, options.mcpUrl);
|
|
140
171
|
if (options.keyAuthOnly && !useKeyAuth) {
|
|
141
172
|
// All native clients have keyAuth; missing flags mean an old install.
|
|
142
173
|
if (client.keyAuth) {
|
|
@@ -146,9 +177,9 @@ async function connectNativeMcpClient(client, options) {
|
|
|
146
177
|
throw new ConnectStepError(`${client.displayName} cannot take a TinyFish API key, and signing in needs a ` +
|
|
147
178
|
`terminal. Run: tinyfish connect ${client.connectClient}`, 'key_auth_unsupported');
|
|
148
179
|
}
|
|
149
|
-
//
|
|
150
|
-
if (keyedAdd?.spec.seedKey
|
|
151
|
-
await verifyKeyBeforeSeeding(keyedAdd.key, client.displayName);
|
|
180
|
+
// Verify first: a revoked key in a foreign .env is unrecoverable.
|
|
181
|
+
if (keyedAdd?.spec.seedKey) {
|
|
182
|
+
await verifyKeyBeforeSeeding(keyedAdd.key, client.displayName, options.mcpUrl);
|
|
152
183
|
}
|
|
153
184
|
telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
|
|
154
185
|
state.stage = 'registration_cleanup';
|
|
@@ -166,9 +197,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
166
197
|
loginArgs && !probeAuthenticated ? 'registration' : 'registration_or_authentication';
|
|
167
198
|
errLine(`Adding TinyFish to ${client.displayName}...`);
|
|
168
199
|
// Header keys ride argv (/proc/<pid>/cmdline); these CLIs take headers no other way.
|
|
169
|
-
const addArgs =
|
|
170
|
-
? keyedAdd.spec.addArgs(mcpUrl.toString(), keyedAdd.key)
|
|
171
|
-
: client.addArgs(mcpUrl.toString(), options.mcpUrl);
|
|
200
|
+
const addArgs = buildAddArgs(mcpUrl.toString());
|
|
172
201
|
// Capturing an inline-OAuth add hides the sign-in URL and hangs.
|
|
173
202
|
const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
|
|
174
203
|
// After the removals: an interrupt there leaves no key behind.
|
package/dist/commands/run.js
CHANGED
|
@@ -124,6 +124,194 @@ async function loadOutputSchema(opts) {
|
|
|
124
124
|
}
|
|
125
125
|
return parseOutputSchema(fileContents, '--output-schema-file');
|
|
126
126
|
}
|
|
127
|
+
function validateRunInvocation(goal, opts) {
|
|
128
|
+
if (!opts.url) {
|
|
129
|
+
err({ error: "required option '--url <url>' not specified" });
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (opts.sync && opts.async) {
|
|
133
|
+
err({ error: '--sync and --async are mutually exclusive' });
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
// Normalise URL: accept bare hostnames by prepending https://.
|
|
137
|
+
// The server requires a valid URL — it does not auto-prefix.
|
|
138
|
+
let normalizedUrl = opts.url;
|
|
139
|
+
try {
|
|
140
|
+
new URL(opts.url);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
try {
|
|
144
|
+
new URL(`https://${opts.url}`);
|
|
145
|
+
normalizedUrl = `https://${opts.url}`;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
err({
|
|
149
|
+
error: `Invalid --url "${opts.url}". Provide a valid URL, e.g. https://example.com or example.com`,
|
|
150
|
+
});
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!goal.trim()) {
|
|
155
|
+
err({ error: 'Goal cannot be empty' });
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
// The server rejects credential_item_ids without use_vault (400); fail fast here.
|
|
159
|
+
if (opts.credentialItemId.length > 0 && !opts.useVault) {
|
|
160
|
+
err({ error: '--credential-item-id requires --use-vault' });
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
// The server rejects profile_id without use_profile (400); fail fast here.
|
|
164
|
+
if (opts.profileId !== undefined && !opts.useProfile) {
|
|
165
|
+
err({ error: '--profile-id requires --use-profile' });
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
return normalizedUrl;
|
|
169
|
+
}
|
|
170
|
+
function buildAgentConfig(opts) {
|
|
171
|
+
const mode = parseChoice(opts.mode, VALID_MODES, '--mode');
|
|
172
|
+
const cursorStyle = parseChoice(opts.cursorStyle, VALID_CURSOR_STYLES, '--cursor-style');
|
|
173
|
+
const maxSteps = parsePositiveInteger(opts.maxSteps, '--max-steps', MAX_STEPS_LIMIT);
|
|
174
|
+
const maxDurationSeconds = parsePositiveInteger(opts.maxDurationSeconds, '--max-duration-seconds');
|
|
175
|
+
if (mode === undefined &&
|
|
176
|
+
cursorStyle === undefined &&
|
|
177
|
+
maxSteps === undefined &&
|
|
178
|
+
maxDurationSeconds === undefined) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
...(mode !== undefined ? { mode } : {}),
|
|
183
|
+
...(cursorStyle !== undefined ? { cursor_style: cursorStyle } : {}),
|
|
184
|
+
...(maxSteps !== undefined ? { max_steps: maxSteps } : {}),
|
|
185
|
+
...(maxDurationSeconds !== undefined ? { max_duration_seconds: maxDurationSeconds } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function buildCaptureConfig(opts) {
|
|
189
|
+
if (opts.captureElements === undefined &&
|
|
190
|
+
opts.captureSnapshots === undefined &&
|
|
191
|
+
opts.captureScreenshots === undefined &&
|
|
192
|
+
opts.captureRecording === undefined &&
|
|
193
|
+
opts.captureHtml === undefined) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
...(opts.captureElements !== undefined ? { elements: opts.captureElements } : {}),
|
|
198
|
+
...(opts.captureSnapshots !== undefined ? { snapshots: opts.captureSnapshots } : {}),
|
|
199
|
+
...(opts.captureScreenshots !== undefined ? { screenshots: opts.captureScreenshots } : {}),
|
|
200
|
+
...(opts.captureRecording !== undefined ? { recording: opts.captureRecording } : {}),
|
|
201
|
+
...(opts.captureHtml !== undefined ? { html: opts.captureHtml } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function buildRunRequest(goal, normalizedUrl, opts) {
|
|
205
|
+
const outputSchema = await loadOutputSchema(opts);
|
|
206
|
+
const browserProfile = parseChoice(opts.browserProfile, VALID_BROWSER_PROFILES, '--browser-profile');
|
|
207
|
+
const agentConfig = buildAgentConfig(opts);
|
|
208
|
+
const captureConfig = buildCaptureConfig(opts);
|
|
209
|
+
const sessionId = parseSessionId(opts.sessionId);
|
|
210
|
+
const webhookUrl = parseHttpsUrl(opts.webhookUrl, '--webhook-url');
|
|
211
|
+
return {
|
|
212
|
+
goal,
|
|
213
|
+
url: normalizedUrl,
|
|
214
|
+
...(outputSchema !== undefined ? { output_schema: outputSchema } : {}),
|
|
215
|
+
...(browserProfile !== undefined ? { browser_profile: browserProfile } : {}),
|
|
216
|
+
...(agentConfig !== undefined ? { agent_config: agentConfig } : {}),
|
|
217
|
+
...(captureConfig !== undefined ? { capture_config: captureConfig } : {}),
|
|
218
|
+
...(webhookUrl !== undefined ? { webhook_url: webhookUrl } : {}),
|
|
219
|
+
...(opts.useProfile !== undefined ? { use_profile: opts.useProfile } : {}),
|
|
220
|
+
...(opts.profileId !== undefined ? { profile_id: opts.profileId } : {}),
|
|
221
|
+
...(sessionId !== undefined ? { session_id: sessionId } : {}),
|
|
222
|
+
...(opts.useVault ? { use_vault: true } : {}),
|
|
223
|
+
...(opts.credentialItemId.length > 0 ? { credential_item_ids: opts.credentialItemId } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function runAsyncPath(req, apiKey, pretty) {
|
|
227
|
+
let result;
|
|
228
|
+
try {
|
|
229
|
+
result = await runAsync(req, apiKey);
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
handleApiError(e);
|
|
233
|
+
}
|
|
234
|
+
checkRunResult(result, false); // async = PENDING is expected
|
|
235
|
+
const asyncResultWithUrl = {
|
|
236
|
+
...result,
|
|
237
|
+
status: RunStatus.PENDING,
|
|
238
|
+
run_url: `${BASE_URL}/runs/${result.run_id}`,
|
|
239
|
+
};
|
|
240
|
+
if (pretty) {
|
|
241
|
+
outLine(`Run submitted\nID: ${asyncResultWithUrl.run_id}\nURL: ${asyncResultWithUrl.run_url}`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
out(asyncResultWithUrl);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function runSyncPath(req, apiKey, pretty) {
|
|
248
|
+
errLine('Waiting for result...');
|
|
249
|
+
let result;
|
|
250
|
+
try {
|
|
251
|
+
result = await runSync(req, apiKey);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
handleApiError(e);
|
|
255
|
+
}
|
|
256
|
+
checkRunResult(result);
|
|
257
|
+
const syncResultWithUrl = {
|
|
258
|
+
...result,
|
|
259
|
+
run_url: `${BASE_URL}/runs/${result.run_id}`,
|
|
260
|
+
};
|
|
261
|
+
if (pretty) {
|
|
262
|
+
outLine(`Status: ${syncResultWithUrl.status}\n\n${JSON.stringify(syncResultWithUrl.result ?? {}, null, 2)}`);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
out(syncResultWithUrl);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Default: SSE stream — Ctrl+C cancels the active run.
|
|
269
|
+
async function runStreamPath(req, apiKey, pretty) {
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
let siginted = false;
|
|
272
|
+
let capturedRunId = null;
|
|
273
|
+
const onSigint = () => {
|
|
274
|
+
siginted = true;
|
|
275
|
+
controller.abort();
|
|
276
|
+
};
|
|
277
|
+
process.once('SIGINT', onSigint);
|
|
278
|
+
let streamFailed = false;
|
|
279
|
+
try {
|
|
280
|
+
for await (const event of runStream(req, apiKey, controller.signal)) {
|
|
281
|
+
if (!handleStreamEvent(event, pretty, (id) => {
|
|
282
|
+
capturedRunId = id;
|
|
283
|
+
})) {
|
|
284
|
+
streamFailed = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
if (event.type === EventType.COMPLETE) {
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
if (controller.signal.aborted && siginted) {
|
|
294
|
+
let cancelled = false;
|
|
295
|
+
if (capturedRunId) {
|
|
296
|
+
try {
|
|
297
|
+
await cancelRun(capturedRunId, apiKey);
|
|
298
|
+
cancelled = true;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
/* ignore */
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
process.stderr.write(cancelled ? '\nRun cancelled.\n' : '\n');
|
|
305
|
+
process.exit(130);
|
|
306
|
+
}
|
|
307
|
+
handleApiError(e);
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
process.removeListener('SIGINT', onSigint);
|
|
311
|
+
}
|
|
312
|
+
if (streamFailed)
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
127
315
|
export function registerRun(agentCmd) {
|
|
128
316
|
const runCmd = agentCmd
|
|
129
317
|
.command('run')
|
|
@@ -159,187 +347,18 @@ export function registerRun(agentCmd) {
|
|
|
159
347
|
runCmd.help();
|
|
160
348
|
return;
|
|
161
349
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
166
|
-
if (opts.sync && opts.async) {
|
|
167
|
-
err({ error: '--sync and --async are mutually exclusive' });
|
|
168
|
-
process.exit(1);
|
|
169
|
-
}
|
|
170
|
-
// Normalise URL: accept bare hostnames by prepending https://.
|
|
171
|
-
// The server requires a valid URL — it does not auto-prefix.
|
|
172
|
-
let normalizedUrl = opts.url;
|
|
173
|
-
try {
|
|
174
|
-
new URL(opts.url);
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
try {
|
|
178
|
-
new URL(`https://${opts.url}`);
|
|
179
|
-
normalizedUrl = `https://${opts.url}`;
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
err({
|
|
183
|
-
error: `Invalid --url "${opts.url}". Provide a valid URL, e.g. https://example.com or example.com`,
|
|
184
|
-
});
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (!goal.trim()) {
|
|
189
|
-
err({ error: 'Goal cannot be empty' });
|
|
190
|
-
process.exit(1);
|
|
191
|
-
}
|
|
192
|
-
// The server rejects credential_item_ids without use_vault (400); fail fast here.
|
|
193
|
-
const hasCredentialItemIds = opts.credentialItemId.length > 0;
|
|
194
|
-
if (hasCredentialItemIds && !opts.useVault) {
|
|
195
|
-
err({ error: '--credential-item-id requires --use-vault' });
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
// The server rejects profile_id without use_profile (400); fail fast here.
|
|
199
|
-
if (opts.profileId !== undefined && !opts.useProfile) {
|
|
200
|
-
err({ error: '--profile-id requires --use-profile' });
|
|
201
|
-
process.exit(1);
|
|
202
|
-
}
|
|
203
|
-
const outputSchema = await loadOutputSchema(opts);
|
|
204
|
-
const browserProfile = parseChoice(opts.browserProfile, VALID_BROWSER_PROFILES, '--browser-profile');
|
|
205
|
-
const mode = parseChoice(opts.mode, VALID_MODES, '--mode');
|
|
206
|
-
const cursorStyle = parseChoice(opts.cursorStyle, VALID_CURSOR_STYLES, '--cursor-style');
|
|
207
|
-
const maxSteps = parsePositiveInteger(opts.maxSteps, '--max-steps', MAX_STEPS_LIMIT);
|
|
208
|
-
const maxDurationSeconds = parsePositiveInteger(opts.maxDurationSeconds, '--max-duration-seconds');
|
|
209
|
-
const sessionId = parseSessionId(opts.sessionId);
|
|
210
|
-
const webhookUrl = parseHttpsUrl(opts.webhookUrl, '--webhook-url');
|
|
211
|
-
const agentConfig = mode !== undefined ||
|
|
212
|
-
cursorStyle !== undefined ||
|
|
213
|
-
maxSteps !== undefined ||
|
|
214
|
-
maxDurationSeconds !== undefined
|
|
215
|
-
? {
|
|
216
|
-
...(mode !== undefined ? { mode } : {}),
|
|
217
|
-
...(cursorStyle !== undefined ? { cursor_style: cursorStyle } : {}),
|
|
218
|
-
...(maxSteps !== undefined ? { max_steps: maxSteps } : {}),
|
|
219
|
-
...(maxDurationSeconds !== undefined
|
|
220
|
-
? { max_duration_seconds: maxDurationSeconds }
|
|
221
|
-
: {}),
|
|
222
|
-
}
|
|
223
|
-
: undefined;
|
|
224
|
-
const captureConfig = opts.captureElements !== undefined ||
|
|
225
|
-
opts.captureSnapshots !== undefined ||
|
|
226
|
-
opts.captureScreenshots !== undefined ||
|
|
227
|
-
opts.captureRecording !== undefined ||
|
|
228
|
-
opts.captureHtml !== undefined
|
|
229
|
-
? {
|
|
230
|
-
...(opts.captureElements !== undefined ? { elements: opts.captureElements } : {}),
|
|
231
|
-
...(opts.captureSnapshots !== undefined ? { snapshots: opts.captureSnapshots } : {}),
|
|
232
|
-
...(opts.captureScreenshots !== undefined
|
|
233
|
-
? { screenshots: opts.captureScreenshots }
|
|
234
|
-
: {}),
|
|
235
|
-
...(opts.captureRecording !== undefined ? { recording: opts.captureRecording } : {}),
|
|
236
|
-
...(opts.captureHtml !== undefined ? { html: opts.captureHtml } : {}),
|
|
237
|
-
}
|
|
238
|
-
: undefined;
|
|
350
|
+
const normalizedUrl = validateRunInvocation(goal, opts);
|
|
351
|
+
const req = await buildRunRequest(goal, normalizedUrl, opts);
|
|
239
352
|
const apiKey = getApiKey();
|
|
240
|
-
const req = {
|
|
241
|
-
goal,
|
|
242
|
-
url: normalizedUrl,
|
|
243
|
-
...(outputSchema !== undefined ? { output_schema: outputSchema } : {}),
|
|
244
|
-
...(browserProfile !== undefined ? { browser_profile: browserProfile } : {}),
|
|
245
|
-
...(agentConfig !== undefined ? { agent_config: agentConfig } : {}),
|
|
246
|
-
...(captureConfig !== undefined ? { capture_config: captureConfig } : {}),
|
|
247
|
-
...(webhookUrl !== undefined ? { webhook_url: webhookUrl } : {}),
|
|
248
|
-
...(opts.useProfile !== undefined ? { use_profile: opts.useProfile } : {}),
|
|
249
|
-
...(opts.profileId !== undefined ? { profile_id: opts.profileId } : {}),
|
|
250
|
-
...(sessionId !== undefined ? { session_id: sessionId } : {}),
|
|
251
|
-
...(opts.useVault ? { use_vault: true } : {}),
|
|
252
|
-
...(hasCredentialItemIds ? { credential_item_ids: opts.credentialItemId } : {}),
|
|
253
|
-
};
|
|
254
353
|
if (opts.async) {
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
result = await runAsync(req, apiKey);
|
|
258
|
-
}
|
|
259
|
-
catch (e) {
|
|
260
|
-
handleApiError(e);
|
|
261
|
-
}
|
|
262
|
-
checkRunResult(result, false); // async = PENDING is expected
|
|
263
|
-
const asyncResultWithUrl = {
|
|
264
|
-
...result,
|
|
265
|
-
status: RunStatus.PENDING,
|
|
266
|
-
run_url: `${BASE_URL}/runs/${result.run_id}`,
|
|
267
|
-
};
|
|
268
|
-
if (opts.pretty) {
|
|
269
|
-
outLine(`Run submitted\nID: ${asyncResultWithUrl.run_id}\nURL: ${asyncResultWithUrl.run_url}`);
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
out(asyncResultWithUrl);
|
|
273
|
-
}
|
|
354
|
+
await runAsyncPath(req, apiKey, opts.pretty);
|
|
274
355
|
return;
|
|
275
356
|
}
|
|
276
357
|
if (opts.sync) {
|
|
277
|
-
|
|
278
|
-
let result;
|
|
279
|
-
try {
|
|
280
|
-
result = await runSync(req, apiKey);
|
|
281
|
-
}
|
|
282
|
-
catch (e) {
|
|
283
|
-
handleApiError(e);
|
|
284
|
-
}
|
|
285
|
-
checkRunResult(result);
|
|
286
|
-
const syncResultWithUrl = {
|
|
287
|
-
...result,
|
|
288
|
-
run_url: `${BASE_URL}/runs/${result.run_id}`,
|
|
289
|
-
};
|
|
290
|
-
if (opts.pretty) {
|
|
291
|
-
outLine(`Status: ${syncResultWithUrl.status}\n\n${JSON.stringify(syncResultWithUrl.result ?? {}, null, 2)}`);
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
out(syncResultWithUrl);
|
|
295
|
-
}
|
|
358
|
+
await runSyncPath(req, apiKey, opts.pretty);
|
|
296
359
|
return;
|
|
297
360
|
}
|
|
298
|
-
|
|
299
|
-
const controller = new AbortController();
|
|
300
|
-
let siginted = false;
|
|
301
|
-
let capturedRunId = null;
|
|
302
|
-
const onSigint = () => {
|
|
303
|
-
siginted = true;
|
|
304
|
-
controller.abort();
|
|
305
|
-
};
|
|
306
|
-
process.once('SIGINT', onSigint);
|
|
307
|
-
let streamFailed = false;
|
|
308
|
-
try {
|
|
309
|
-
for await (const event of runStream(req, apiKey, controller.signal)) {
|
|
310
|
-
if (!handleStreamEvent(event, opts.pretty, (id) => {
|
|
311
|
-
capturedRunId = id;
|
|
312
|
-
})) {
|
|
313
|
-
streamFailed = true;
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
if (event.type === EventType.COMPLETE) {
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
catch (e) {
|
|
322
|
-
if (controller.signal.aborted && siginted) {
|
|
323
|
-
let cancelled = false;
|
|
324
|
-
if (capturedRunId) {
|
|
325
|
-
try {
|
|
326
|
-
await cancelRun(capturedRunId, apiKey);
|
|
327
|
-
cancelled = true;
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
/* ignore */
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
process.stderr.write(cancelled ? '\nRun cancelled.\n' : '\n');
|
|
334
|
-
process.exit(130);
|
|
335
|
-
}
|
|
336
|
-
handleApiError(e);
|
|
337
|
-
}
|
|
338
|
-
finally {
|
|
339
|
-
process.removeListener('SIGINT', onSigint);
|
|
340
|
-
}
|
|
341
|
-
if (streamFailed)
|
|
342
|
-
process.exit(1);
|
|
361
|
+
await runStreamPath(req, apiKey, opts.pretty);
|
|
343
362
|
});
|
|
344
363
|
return runCmd;
|
|
345
364
|
}
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare function buildConnectHeaders(connect: ConnectMap, caller: string
|
|
|
7
7
|
export declare function runSync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunResponse>;
|
|
8
8
|
export declare function runAsync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunAsyncResponse>;
|
|
9
9
|
export declare function runStream(req: CliAgentRunParams, apiKey: string, signal?: AbortSignal): AsyncGenerator<AgentRunWithStreamingResponse>;
|
|
10
|
-
export declare function listRuns(opts: RunListParams, apiKey: string, timeout?: number): Promise<RunListResponse>;
|
|
10
|
+
export declare function listRuns(opts: RunListParams, apiKey: string, timeout?: number, baseUrl?: string): Promise<RunListResponse>;
|
|
11
11
|
export declare function getRun(runId: string, apiKey: string): Promise<Run>;
|
|
12
12
|
export declare function getRunSteps(runId: string, apiKey: string): Promise<RunStepsResponse>;
|
|
13
13
|
export declare function searchQuery(params: SearchQueryParams, apiKey: string): Promise<SearchQueryResponse>;
|
package/dist/lib/client.js
CHANGED
|
@@ -93,10 +93,12 @@ class TinyFishCliClient extends TinyFish {
|
|
|
93
93
|
return (await response.json());
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
function createSdkClient(apiKey, timeout) {
|
|
96
|
+
function createSdkClient(apiKey, timeout, baseUrl) {
|
|
97
|
+
// An explicit base outranks the env default.
|
|
98
|
+
const resolvedBase = baseUrl ?? API_URL_OVERRIDE;
|
|
97
99
|
return new TinyFishCliClient({
|
|
98
100
|
apiKey,
|
|
99
|
-
...(
|
|
101
|
+
...(resolvedBase !== undefined ? { baseURL: resolvedBase } : {}),
|
|
100
102
|
maxRetries: 0,
|
|
101
103
|
// Fires per HTTP attempt, but maxRetries is 0 and the notice latches once per invocation.
|
|
102
104
|
onResponse: ({ headers }) => captureNotice(headers),
|
|
@@ -263,9 +265,9 @@ export async function* runStream(req, apiKey, signal) {
|
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
267
|
}
|
|
266
|
-
export async function listRuns(opts, apiKey, timeout) {
|
|
268
|
+
export async function listRuns(opts, apiKey, timeout, baseUrl) {
|
|
267
269
|
try {
|
|
268
|
-
return await createSdkClient(apiKey, timeout).runs.list(opts);
|
|
270
|
+
return await createSdkClient(apiKey, timeout, baseUrl).runs.list(opts);
|
|
269
271
|
}
|
|
270
272
|
catch (error) {
|
|
271
273
|
rethrowSdkError(error);
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -40,6 +40,8 @@ const BROWSER_SIGN_IN = new Set(ALL_HARNESSES.filter((harness) => {
|
|
|
40
40
|
const native = NATIVE_BY_HARNESS.get(harness);
|
|
41
41
|
return !!native?.loginArgs || !!native?.oauthInAdd;
|
|
42
42
|
}));
|
|
43
|
+
// Derived: these refuse a keyless install, so no sign-in helps.
|
|
44
|
+
const KEY_REQUIRED = new Set(ALL_HARNESSES.filter((harness) => !!NATIVE_BY_HARNESS.get(harness)?.keyRequired));
|
|
43
45
|
// Derived: a native add carries the key, or the CLI holds it itself.
|
|
44
46
|
const KEY_AUTH_CAPABLE = new Set(ALL_HARNESSES.filter((harness) => !!NATIVE_BY_HARNESS.get(harness)?.keyAuth || HARNESS_CAPABILITIES[harness].keyHeldByCli));
|
|
45
47
|
// OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
|
|
@@ -311,7 +313,8 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
|
|
|
311
313
|
}
|
|
312
314
|
headlessAuth = true;
|
|
313
315
|
}
|
|
314
|
-
|
|
316
|
+
// Key-required harnesses refuse below; offering a browser sign-in would mislead.
|
|
317
|
+
if (isTTY && !keyed && prompt && !KEY_REQUIRED.has(harness)) {
|
|
315
318
|
errLine(`step: waiting for browser sign-in to TinyFish for ${DISPLAY_NAMES[harness]}`);
|
|
316
319
|
try {
|
|
317
320
|
await prompt(`Press enter to open the browser and sign in to TinyFish for ${DISPLAY_NAMES[harness]}... `);
|
|
@@ -34,7 +34,7 @@ export interface SeededInstall {
|
|
|
34
34
|
detail?: string;
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
interface BaseMcpClient extends SupportedCommand {
|
|
38
38
|
connectClient: Exclude<AgentClient, 'openclaw'>;
|
|
39
39
|
/** Omitted where the `skills` CLI has no agent for this harness (Grok ships its own plugin). */
|
|
40
40
|
skillAgent?: SkillAgent;
|
|
@@ -62,7 +62,6 @@ export interface NativeMcpClient extends SupportedCommand {
|
|
|
62
62
|
args: string[];
|
|
63
63
|
label: string;
|
|
64
64
|
}[];
|
|
65
|
-
addArgs: (mcpUrl: string, oauthResource: string) => string[];
|
|
66
65
|
/** Blocking terminal walkthrough; runs only behind the TTY gate. */
|
|
67
66
|
launchWalkthrough?: () => void;
|
|
68
67
|
/** Own window, never inherits stdio; reports its own outcome. */
|
|
@@ -70,6 +69,21 @@ export interface NativeMcpClient extends SupportedCommand {
|
|
|
70
69
|
/** Printed after a successful connect, on both the launch and non-launch paths. */
|
|
71
70
|
postConnectNote?: string;
|
|
72
71
|
}
|
|
72
|
+
/** Keyless installs use this argv; the harness signs itself in. */
|
|
73
|
+
export interface OauthCapableMcpClient extends BaseMcpClient {
|
|
74
|
+
keyRequired?: false;
|
|
75
|
+
addArgs: (mcpUrl: string, oauthResource: string) => string[];
|
|
76
|
+
}
|
|
77
|
+
/** No sign-in exists here, so connect refuses a keyless install (Hermes). */
|
|
78
|
+
export interface KeyRequiredMcpClient extends BaseMcpClient {
|
|
79
|
+
keyRequired: true;
|
|
80
|
+
keyAuth: NonNullable<BaseMcpClient['keyAuth']>;
|
|
81
|
+
/** Restoring any of these three would restore the OAuth fallback. */
|
|
82
|
+
addArgs?: never;
|
|
83
|
+
oauthInAdd?: never;
|
|
84
|
+
loginArgs?: never;
|
|
85
|
+
}
|
|
86
|
+
export type NativeMcpClient = OauthCapableMcpClient | KeyRequiredMcpClient;
|
|
73
87
|
export declare const CLAUDE_CODE: NativeMcpClient;
|
|
74
88
|
/**
|
|
75
89
|
* `cmd` re-parses its command line after `/c`, so an unquoted `&` in the query string ends the
|
|
@@ -84,7 +98,7 @@ export declare function hermesRegistrationEnabled(home: string): {
|
|
|
84
98
|
ok: boolean;
|
|
85
99
|
detail?: string;
|
|
86
100
|
};
|
|
87
|
-
export declare const HERMES:
|
|
101
|
+
export declare const HERMES: KeyRequiredMcpClient;
|
|
88
102
|
export declare const OPENCODE: NativeMcpClient;
|
|
89
103
|
/** Every native MCP client. Derive from this, so a new harness cannot be missed. */
|
|
90
104
|
export declare const NATIVE_MCP_CLIENTS: readonly NativeMcpClient[];
|
|
@@ -291,6 +291,7 @@ export const HERMES = {
|
|
|
291
291
|
connectClient: 'hermes',
|
|
292
292
|
skillAgent: 'hermes-agent',
|
|
293
293
|
displayName: 'Hermes',
|
|
294
|
+
keyRequired: true,
|
|
294
295
|
supportCheck: {
|
|
295
296
|
args: ['mcp', 'add', '--help'],
|
|
296
297
|
patterns: [/--auth\s+\{[^}]*oauth[^}]*\}/],
|
|
@@ -303,9 +304,6 @@ export const HERMES = {
|
|
|
303
304
|
seedKey: seedHermesKey,
|
|
304
305
|
},
|
|
305
306
|
removals: [{ args: ['mcp', 'remove', 'tinyfish'], label: 'TinyFish registration from Hermes' }],
|
|
306
|
-
// Hermes completes OAuth while adding the server; a separate `mcp login` would authenticate twice.
|
|
307
|
-
oauthInAdd: true,
|
|
308
|
-
addArgs: (mcpUrl) => [...urlAddArgs(mcpUrl), '--auth', 'oauth'],
|
|
309
307
|
launchWalkthrough: launchHermesWalkthrough,
|
|
310
308
|
postConnectNote: 'Restart your Hermes session to pick up TinyFish — Hermes discovers MCP servers at startup.',
|
|
311
309
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
|
|
2
2
|
export declare function endpointOf(url: string): string;
|
|
3
|
-
/** `
|
|
3
|
+
/** One host serves `/mcp` and `/v1`, so origin is the API base. */
|
|
4
|
+
export declare function apiBaseFromMcpUrl(mcpUrl: string): string | undefined;
|
|
5
|
+
/** doctor reuses the CLI's own BASE_URL check; off-default endpoints cannot. */
|
|
4
6
|
export declare function isDefaultEndpoint(mcpUrl: string): boolean;
|
package/dist/lib/mcp-endpoint.js
CHANGED
|
@@ -4,7 +4,16 @@ export function endpointOf(url) {
|
|
|
4
4
|
const parsed = new URL(url);
|
|
5
5
|
return `${parsed.origin}${parsed.pathname.replace(/\/$/, '')}`;
|
|
6
6
|
}
|
|
7
|
-
/** `
|
|
7
|
+
/** One host serves `/mcp` and `/v1`, so origin is the API base. */
|
|
8
|
+
export function apiBaseFromMcpUrl(mcpUrl) {
|
|
9
|
+
try {
|
|
10
|
+
return new URL(mcpUrl).origin;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** doctor reuses the CLI's own BASE_URL check; off-default endpoints cannot. */
|
|
8
17
|
export function isDefaultEndpoint(mcpUrl) {
|
|
9
18
|
try {
|
|
10
19
|
return endpointOf(mcpUrl) === endpointOf(DEFAULT_MCP_URL);
|
package/dist/lib/verify.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export interface VerifyResult {
|
|
|
11
11
|
/** Reachability check. Verify failure is a warning, never install failure. */
|
|
12
12
|
export declare function verifyMcpHealth(mcpUrl: string): Promise<VerifyResult>;
|
|
13
13
|
/** Authenticated check — where the CLI holds the key (Cursor, OpenClaw, Hermes). */
|
|
14
|
-
export declare function verifyMcpAuth(apiKey: string): Promise<VerifyResult>;
|
|
14
|
+
export declare function verifyMcpAuth(apiKey: string, apiBaseUrl?: string): Promise<VerifyResult>;
|
package/dist/lib/verify.js
CHANGED
|
@@ -62,10 +62,10 @@ export async function verifyMcpHealth(mcpUrl) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
/** Authenticated check — where the CLI holds the key (Cursor, OpenClaw, Hermes). */
|
|
65
|
-
export async function verifyMcpAuth(apiKey) {
|
|
65
|
+
export async function verifyMcpAuth(apiKey, apiBaseUrl) {
|
|
66
66
|
try {
|
|
67
67
|
// Bound it: the SDK default is 600s, and a warning-only check must never hang an install.
|
|
68
|
-
await listRuns({ limit: 1 }, apiKey, VERIFY_TIMEOUT_MS);
|
|
68
|
+
await listRuns({ limit: 1 }, apiKey, VERIFY_TIMEOUT_MS, apiBaseUrl);
|
|
69
69
|
return { depth: 'health+auth', ok: true };
|
|
70
70
|
}
|
|
71
71
|
catch (e) {
|