@tiny-fish/cli 0.48.3-next.389 → 0.48.3-next.392
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/README.md +41 -0
- package/dist/commands/browser.js +31 -2
- package/dist/commands/fetch.js +9 -14
- package/dist/commands/monitor.d.ts +2 -0
- package/dist/commands/monitor.js +150 -0
- package/dist/commands/profile.js +166 -70
- package/dist/commands/search.js +9 -2
- package/dist/lib/auth.d.ts +1 -0
- package/dist/lib/auth.js +10 -1
- package/dist/lib/client.d.ts +110 -2
- package/dist/lib/client.js +132 -33
- package/dist/lib/connect-runtime.js +21 -5
- package/dist/lib/harness/harness-messages.js +9 -18
- package/dist/lib/harness/harness-spec.d.ts +5 -0
- package/dist/lib/harness/harness-spec.js +4 -0
- package/dist/lib/list-options.d.ts +21 -0
- package/dist/lib/list-options.js +46 -0
- package/dist/program.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -331,6 +331,47 @@ TINYFISH_PROXY_PASSWORD=<password> \
|
|
|
331
331
|
tinyfish browser session create --no-proxy
|
|
332
332
|
```
|
|
333
333
|
|
|
334
|
+
A session bills until it closes or times out, so close it when you are done:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
tinyfish browser session list --status running
|
|
338
|
+
tinyfish browser session close <session_id>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Monitors
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
# Watch search results for a topic, every 6 hours (runs a billed baseline now)
|
|
345
|
+
tinyfish monitor create --query "tinyfish funding news" --schedule "0 */6 * * *"
|
|
346
|
+
|
|
347
|
+
# Watch a page for changes, daily at 09:00 UTC
|
|
348
|
+
tinyfish monitor create --url https://example.com/pricing --schedule "0 9 * * *"
|
|
349
|
+
|
|
350
|
+
tinyfish monitor list --pretty
|
|
351
|
+
tinyfish monitor pause <monitor_id>
|
|
352
|
+
tinyfish monitor resume <monitor_id>
|
|
353
|
+
tinyfish monitor run <monitor_id> # billed
|
|
354
|
+
tinyfish monitor cancel <monitor_id> # deletes it
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Profiles
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
tinyfish profile list --pretty
|
|
361
|
+
|
|
362
|
+
# Create a profile and get a link to sign in and save it (opens at a terminal)
|
|
363
|
+
tinyfish profile create --name work --site github.com --proxy-country GB
|
|
364
|
+
|
|
365
|
+
# Change the proxy later; every run on the profile uses it
|
|
366
|
+
tinyfish profile update <profile_id> --proxy-country DE
|
|
367
|
+
|
|
368
|
+
# Or import cookies from your local Chrome instead
|
|
369
|
+
tinyfish profile create --name work --import-cookies --domain github.com
|
|
370
|
+
|
|
371
|
+
# Sign in to another site on an existing profile
|
|
372
|
+
tinyfish profile sign-in <profile_id> --site accounts.google.com
|
|
373
|
+
```
|
|
374
|
+
|
|
334
375
|
### Vault
|
|
335
376
|
|
|
336
377
|
Connect a credential provider (1Password / Bitwarden), then have runs consume those credentials.
|
package/dist/commands/browser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getApiKey } from '../lib/auth.js';
|
|
2
|
-
import { browserSessionCreate } from '../lib/client.js';
|
|
1
|
+
import { getApiKey, withResolvedKey } from '../lib/auth.js';
|
|
2
|
+
import { browserSessionClose, browserSessionCreate, browserSessionList } from '../lib/client.js';
|
|
3
|
+
import { addPageOptions, pageParams, printPage } from '../lib/list-options.js';
|
|
3
4
|
import { err, handleApiError, out, outLine } from '../lib/output.js';
|
|
4
5
|
const VALID_BROWSER_PROFILES = ['lite', 'stealth'];
|
|
5
6
|
function parseBrowserProfile(value) {
|
|
@@ -48,6 +49,26 @@ function printPrettyBrowserSession(session) {
|
|
|
48
49
|
outLine(`CDP URL: ${session.cdp_url}`);
|
|
49
50
|
outLine(`Base URL: ${session.base_url}`);
|
|
50
51
|
}
|
|
52
|
+
function browserSessionListAction(opts) {
|
|
53
|
+
const params = {
|
|
54
|
+
...pageParams(opts),
|
|
55
|
+
...(opts.sessionId !== undefined ? { session_id: opts.sessionId } : {}),
|
|
56
|
+
};
|
|
57
|
+
return withResolvedKey(async (apiKey) => {
|
|
58
|
+
const result = await browserSessionList(params, apiKey);
|
|
59
|
+
printPage(result, opts.pretty, (s) => `${s.session_id} ${s.status ?? '-'} ${s.start_time ?? '-'}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// REST answers 204; echo the MCP close_browser_session shape instead.
|
|
63
|
+
function browserSessionCloseAction(sessionId, opts) {
|
|
64
|
+
return withResolvedKey(async (apiKey) => {
|
|
65
|
+
await browserSessionClose(sessionId, apiKey);
|
|
66
|
+
if (opts.pretty)
|
|
67
|
+
outLine(`Browser session ${sessionId} closed`);
|
|
68
|
+
else
|
|
69
|
+
out({ session_id: sessionId, status: 'closed' });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
51
72
|
export function registerBrowser(program) {
|
|
52
73
|
const browserCmd = program
|
|
53
74
|
.command('browser')
|
|
@@ -88,4 +109,12 @@ export function registerBrowser(program) {
|
|
|
88
109
|
handleApiError(error);
|
|
89
110
|
}
|
|
90
111
|
});
|
|
112
|
+
addPageOptions(sessionCmd.command('list').description('List browser sessions'), 'running|ended')
|
|
113
|
+
.option('--session-id <id>', 'Only this session')
|
|
114
|
+
.action(browserSessionListAction);
|
|
115
|
+
sessionCmd
|
|
116
|
+
.command('close <session_id>')
|
|
117
|
+
.description('Close a browser session and stop its billing (idempotent)')
|
|
118
|
+
.option('--pretty', 'Human-readable output')
|
|
119
|
+
.action(browserSessionCloseAction);
|
|
91
120
|
}
|
package/dist/commands/fetch.js
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { getApiKey, withResolvedKey } from '../lib/auth.js';
|
|
2
|
+
import { fetchContentGet, fetchUsage } from '../lib/client.js';
|
|
3
|
+
import { addPageOptions, integerInRange, pageParams, printPage, } from '../lib/list-options.js';
|
|
4
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
|
const MIN_HIGHLIGHT_SNIPPETS = 1;
|
|
8
8
|
const MAX_HIGHLIGHT_SNIPPETS = 20;
|
|
9
9
|
const MAX_URLS = 10;
|
|
10
|
-
function integerInRange(min, max) {
|
|
11
|
-
return (value) => {
|
|
12
|
-
const parsed = Number(value);
|
|
13
|
-
if (!Number.isSafeInteger(parsed) || parsed < min || (max !== undefined && parsed > max)) {
|
|
14
|
-
throw new InvalidArgumentError(max === undefined
|
|
15
|
-
? 'must be a positive integer'
|
|
16
|
-
: `must be an integer between ${min} and ${max}`);
|
|
17
|
-
}
|
|
18
|
-
return parsed;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
10
|
function validateHighlightsOptions(opts) {
|
|
22
11
|
const hasSubFlag = opts.maxSnippets !== undefined ||
|
|
23
12
|
opts.maxCharacters !== undefined ||
|
|
@@ -102,6 +91,11 @@ function printPrettyFetch(response) {
|
|
|
102
91
|
}
|
|
103
92
|
}
|
|
104
93
|
}
|
|
94
|
+
function fetchUsageAction(opts) {
|
|
95
|
+
return withResolvedKey(async (apiKey) => {
|
|
96
|
+
printPage(await fetchUsage(pageParams(opts), apiKey), opts.pretty, (item) => `${item.created_at} ${item.status} ${item.url}`);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
105
99
|
export function registerFetch(program) {
|
|
106
100
|
const fetchCmd = program.command('fetch').description('Fetch commands').enablePositionalOptions();
|
|
107
101
|
const contentCmd = fetchCmd
|
|
@@ -156,4 +150,5 @@ export function registerFetch(program) {
|
|
|
156
150
|
handleApiError(error);
|
|
157
151
|
}
|
|
158
152
|
});
|
|
153
|
+
addPageOptions(fetchCmd.command('usage').description('List your past fetch requests'), 'completed|failed').action(fetchUsageAction);
|
|
159
154
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { withResolvedKey } from '../lib/auth.js';
|
|
2
|
+
import { monitorCreate, monitorDelete, monitorGet, monitorList, monitorRun, monitorSetStatus, } from '../lib/client.js';
|
|
3
|
+
import { integerInRange } from '../lib/list-options.js';
|
|
4
|
+
import { err, out, outLine } from '../lib/output.js';
|
|
5
|
+
const MAX_RESULT_LIMIT = 10;
|
|
6
|
+
function searchConfig(query, opts) {
|
|
7
|
+
return {
|
|
8
|
+
query,
|
|
9
|
+
...(opts.recencyMinutes !== undefined ? { recency_minutes: opts.recencyMinutes } : {}),
|
|
10
|
+
...(opts.resultLimit !== undefined ? { result_limit: opts.resultLimit } : {}),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function fetchConfig(url, opts) {
|
|
14
|
+
return {
|
|
15
|
+
url,
|
|
16
|
+
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
17
|
+
...(opts.links ? { links: true } : {}),
|
|
18
|
+
...(opts.imageLinks ? { image_links: true } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const SEARCH_ONLY_FLAGS = { recencyMinutes: '--recency-minutes', resultLimit: '--result-limit' };
|
|
22
|
+
const FETCH_ONLY_FLAGS = { format: '--format', links: '--links', imageLinks: '--image-links' };
|
|
23
|
+
function exitWithError(message) {
|
|
24
|
+
err({ error: message });
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
// Unsent flags would otherwise vanish silently on a billed create.
|
|
28
|
+
function rejectFlags(opts, flags, type) {
|
|
29
|
+
const used = Object.entries(flags).filter(([key]) => opts[key] !== undefined);
|
|
30
|
+
if (used.length > 0) {
|
|
31
|
+
exitWithError(`${used.map(([, flag]) => flag).join(', ')} cannot be used with a ${type} monitor`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function buildCreateBody(opts) {
|
|
35
|
+
if ((opts.query === undefined) === (opts.url === undefined)) {
|
|
36
|
+
exitWithError('Provide exactly one of --query (search monitor) or --url (fetch monitor)');
|
|
37
|
+
}
|
|
38
|
+
if (opts.query !== undefined)
|
|
39
|
+
rejectFlags(opts, FETCH_ONLY_FLAGS, 'search');
|
|
40
|
+
else
|
|
41
|
+
rejectFlags(opts, SEARCH_ONLY_FLAGS, 'fetch');
|
|
42
|
+
const target = opts.query !== undefined
|
|
43
|
+
? { type: 'search', config: searchConfig(opts.query, opts) }
|
|
44
|
+
: { type: 'fetch', config: fetchConfig(opts.url, opts) };
|
|
45
|
+
return {
|
|
46
|
+
...target,
|
|
47
|
+
schedule_cron: opts.schedule,
|
|
48
|
+
...(opts.name !== undefined ? { name: opts.name } : {}),
|
|
49
|
+
...(opts.webhookUrl !== undefined ? { webhook_url: opts.webhookUrl } : {}),
|
|
50
|
+
...(opts.purpose !== undefined ? { purpose: opts.purpose } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function monitorLine(monitor) {
|
|
54
|
+
const target = monitor.config.query ?? monitor.config.url ?? '';
|
|
55
|
+
return `${monitor.id} ${monitor.type} ${monitor.status} ${monitor.schedule_cron} ${target}`;
|
|
56
|
+
}
|
|
57
|
+
function printPrettyRun(run) {
|
|
58
|
+
outLine(`Run: ${run.id}`);
|
|
59
|
+
if (run.result_count !== undefined)
|
|
60
|
+
outLine(`Results: ${run.result_count}`);
|
|
61
|
+
else
|
|
62
|
+
outLine(`Fetched: ${run.results?.length ?? 0}, errors: ${run.errors?.length ?? 0}`);
|
|
63
|
+
}
|
|
64
|
+
function monitorCreateAction(opts) {
|
|
65
|
+
const body = buildCreateBody(opts);
|
|
66
|
+
return withResolvedKey(async (apiKey) => {
|
|
67
|
+
const result = await monitorCreate(body, apiKey);
|
|
68
|
+
if (!opts.pretty)
|
|
69
|
+
return out(result);
|
|
70
|
+
outLine(monitorLine(result.monitor));
|
|
71
|
+
printPrettyRun(result.run);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function monitorListAction(opts) {
|
|
75
|
+
return withResolvedKey(async (apiKey) => {
|
|
76
|
+
const result = await monitorList(apiKey);
|
|
77
|
+
if (!opts.pretty)
|
|
78
|
+
return out(result);
|
|
79
|
+
if (result.monitors.length === 0)
|
|
80
|
+
outLine('No monitors.');
|
|
81
|
+
for (const monitor of result.monitors)
|
|
82
|
+
outLine(monitorLine(monitor));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function monitorShow(fetchMonitor) {
|
|
86
|
+
return (id, opts) => withResolvedKey(async (apiKey) => {
|
|
87
|
+
const monitor = await fetchMonitor(id, apiKey);
|
|
88
|
+
if (opts.pretty)
|
|
89
|
+
outLine(monitorLine(monitor));
|
|
90
|
+
else
|
|
91
|
+
out(monitor);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function monitorRunAction(id, opts) {
|
|
95
|
+
return withResolvedKey(async (apiKey) => {
|
|
96
|
+
const result = await monitorRun(id, apiKey);
|
|
97
|
+
if (opts.pretty)
|
|
98
|
+
printPrettyRun(result.run);
|
|
99
|
+
else
|
|
100
|
+
out(result);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function monitorCancelAction(id, opts) {
|
|
104
|
+
return withResolvedKey(async (apiKey) => {
|
|
105
|
+
const result = await monitorDelete(id, apiKey);
|
|
106
|
+
if (opts.pretty)
|
|
107
|
+
outLine(`Monitor ${result.id} deleted`);
|
|
108
|
+
else
|
|
109
|
+
out(result);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function addIdCommand(parent, name, description, action) {
|
|
113
|
+
parent
|
|
114
|
+
.command(`${name} <monitor_id>`)
|
|
115
|
+
.description(description)
|
|
116
|
+
.option('--pretty', 'Human-readable output')
|
|
117
|
+
.action(action);
|
|
118
|
+
}
|
|
119
|
+
export function registerMonitor(program) {
|
|
120
|
+
const monitorCmd = program
|
|
121
|
+
.command('monitor')
|
|
122
|
+
.description('Recurring search and fetch monitors')
|
|
123
|
+
.enablePositionalOptions();
|
|
124
|
+
monitorCmd
|
|
125
|
+
.command('create')
|
|
126
|
+
.description('Create a monitor and run its baseline now. Each completed run is billed to your wallet.')
|
|
127
|
+
.option('--query <query>', 'Search monitor: query to watch')
|
|
128
|
+
.option('--url <url>', 'Fetch monitor: page to watch')
|
|
129
|
+
.requiredOption('--schedule <cron>', '5-field cron, UTC unless prefixed "CRON_TZ=<zone> " (search: every 30 min at most)')
|
|
130
|
+
.option('--name <name>', 'Monitor name')
|
|
131
|
+
.option('--webhook-url <url>', 'POST each run result to this URL')
|
|
132
|
+
.option('--purpose <text>', 'Why you are monitoring this')
|
|
133
|
+
.option('--recency-minutes <n>', 'Search: only results this recent', integerInRange(1))
|
|
134
|
+
.option('--result-limit <n>', `Search: results per run, 1-${MAX_RESULT_LIMIT} (default ${MAX_RESULT_LIMIT})`, integerInRange(1, MAX_RESULT_LIMIT))
|
|
135
|
+
.option('--format <format>', 'Fetch: markdown, html, or json (default json)')
|
|
136
|
+
.option('--links', 'Fetch: include extracted links')
|
|
137
|
+
.option('--image-links', 'Fetch: include extracted image links')
|
|
138
|
+
.option('--pretty', 'Human-readable output')
|
|
139
|
+
.action(monitorCreateAction);
|
|
140
|
+
monitorCmd
|
|
141
|
+
.command('list')
|
|
142
|
+
.description('List your monitors')
|
|
143
|
+
.option('--pretty', 'Human-readable output')
|
|
144
|
+
.action(monitorListAction);
|
|
145
|
+
addIdCommand(monitorCmd, 'get', 'Get a monitor by ID', monitorShow(monitorGet));
|
|
146
|
+
addIdCommand(monitorCmd, 'pause', 'Pause a monitor', monitorShow((id, apiKey) => monitorSetStatus(id, 'paused', apiKey)));
|
|
147
|
+
addIdCommand(monitorCmd, 'resume', 'Resume a paused monitor', monitorShow((id, apiKey) => monitorSetStatus(id, 'active', apiKey)));
|
|
148
|
+
addIdCommand(monitorCmd, 'run', 'Run a monitor now (billed)', monitorRunAction);
|
|
149
|
+
addIdCommand(monitorCmd, 'cancel', 'Delete a monitor and its schedule', monitorCancelAction);
|
|
150
|
+
}
|
package/dist/commands/profile.js
CHANGED
|
@@ -2,10 +2,13 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
|
+
import { InvalidArgumentError } from 'commander';
|
|
5
6
|
import { z } from 'zod';
|
|
6
|
-
import { getResolvedApiKey, rejectedKeyHint } from '../lib/auth.js';
|
|
7
|
+
import { getResolvedApiKey, rejectedKeyHint, withResolvedKey } from '../lib/auth.js';
|
|
7
8
|
import { deriveChromeKey, listAuthRelevantHostKeys, openCookiesDb, readCookiesForHost, } from '../lib/chrome-cookies.js';
|
|
8
|
-
import { profileCreate, profileGet, profileList, profileUpload } from '../lib/client.js';
|
|
9
|
+
import { profileCreate, profileGet, profileList, profileUpdate, profileUpload, } from '../lib/client.js';
|
|
10
|
+
import { BASE_URL } from '../lib/constants.js';
|
|
11
|
+
import { detectHumanInitiated } from '../lib/harness.js';
|
|
9
12
|
import { err, errLine, handleApiError, out, outLine } from '../lib/output.js';
|
|
10
13
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
11
14
|
const chromeLocalStateSchema = z.object({
|
|
@@ -249,12 +252,6 @@ export function buildChunks(groups) {
|
|
|
249
252
|
return chunks;
|
|
250
253
|
}
|
|
251
254
|
// ── Pretty printers ───────────────────────────────────────────────────────────
|
|
252
|
-
function printPrettyProfileCreate(profile) {
|
|
253
|
-
outLine('Profile created');
|
|
254
|
-
outLine(`ID: ${profile.id}`);
|
|
255
|
-
outLine(`Name: ${profile.name}`);
|
|
256
|
-
outLine(`Created at: ${profile.created_at}`);
|
|
257
|
-
}
|
|
258
255
|
function printPrettyProfileUpload(result) {
|
|
259
256
|
if (result.profile_id)
|
|
260
257
|
outLine(`Profile ID: ${result.profile_id}`);
|
|
@@ -313,79 +310,178 @@ export async function uploadAllDomains(profileId, cookies, apiKey) {
|
|
|
313
310
|
...(domainsFailed.length > 0 ? { domains_failed: domainsFailed } : {}),
|
|
314
311
|
};
|
|
315
312
|
}
|
|
313
|
+
// Commander defaults `proxy` to true once --no-proxy is declared.
|
|
314
|
+
function proxyParams(opts) {
|
|
315
|
+
if (opts.proxy === false) {
|
|
316
|
+
if (opts.proxyCountry) {
|
|
317
|
+
err({ error: '--proxy-country cannot be combined with --no-proxy' });
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
return { proxy_country_code: null };
|
|
321
|
+
}
|
|
322
|
+
return opts.proxyCountry ? { proxy_country_code: opts.proxyCountry.toUpperCase() } : {};
|
|
323
|
+
}
|
|
324
|
+
function printPrettyProfileList({ profiles }) {
|
|
325
|
+
if (profiles.length === 0)
|
|
326
|
+
outLine('No profiles.');
|
|
327
|
+
for (const profile of profiles) {
|
|
328
|
+
const sites = profile.signed_in_sites?.map((entry) => entry.site).join(', ');
|
|
329
|
+
const proxy = profile.proxy_country_code ? ` proxy: ${profile.proxy_country_code}` : '';
|
|
330
|
+
outLine(`${profile.id} ${profile.name}${proxy}${sites ? ` signed in: ${sites}` : ''}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// The setup page ignores anything but a bare hostname.
|
|
334
|
+
function parseSite(value) {
|
|
335
|
+
try {
|
|
336
|
+
return new URL(value.includes('://') ? value : `https://${value}`).hostname;
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
throw new InvalidArgumentError('must be a site such as github.com or a URL');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Dashboard page with its own browser and Save button; nothing bills until opened.
|
|
343
|
+
function profileSetupUrl(profileId, site) {
|
|
344
|
+
const url = new URL(`/profiles/${encodeURIComponent(profileId)}/setup`, BASE_URL);
|
|
345
|
+
if (site)
|
|
346
|
+
url.searchParams.set('domain', site);
|
|
347
|
+
// The page opens domain= only for agent sources (mcp, cli).
|
|
348
|
+
url.searchParams.set('source', 'cli');
|
|
349
|
+
return url.toString();
|
|
350
|
+
}
|
|
351
|
+
function signInNextStep(profileId) {
|
|
352
|
+
return `Give the user setup_url. They sign in there and click Save profile; never ask for their password. Confirm with \`tinyfish profile list\`. Runs use \`tinyfish agent run --use-profile --profile-id ${profileId}\`.`;
|
|
353
|
+
}
|
|
354
|
+
async function handOffSignIn(profile, opts, created) {
|
|
355
|
+
const setupUrl = profileSetupUrl(profile.id, opts.site);
|
|
356
|
+
if (detectHumanInitiated()) {
|
|
357
|
+
const { openExternalUrl } = await import('../lib/connect-clients.js');
|
|
358
|
+
openExternalUrl(setupUrl);
|
|
359
|
+
}
|
|
360
|
+
if (!opts.pretty) {
|
|
361
|
+
return out({ ...profile, setup_url: setupUrl, next_step: signInNextStep(profile.id) });
|
|
362
|
+
}
|
|
363
|
+
if (created)
|
|
364
|
+
outLine(`Profile ${profile.id} created`);
|
|
365
|
+
outLine(`Sign in at: ${setupUrl}`);
|
|
366
|
+
outLine('Click Save profile there when you are done.');
|
|
367
|
+
}
|
|
368
|
+
function profileListAction(opts) {
|
|
369
|
+
return withResolvedKey(async (apiKey) => {
|
|
370
|
+
const result = await profileList(apiKey);
|
|
371
|
+
if (opts.pretty)
|
|
372
|
+
printPrettyProfileList(result);
|
|
373
|
+
else
|
|
374
|
+
out(result);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
function profileCreateAction(opts) {
|
|
378
|
+
const newProfile = { name: opts.name, ...proxyParams(opts) };
|
|
379
|
+
if (opts.importCookies)
|
|
380
|
+
return profileImportCookiesAction({ ...opts, newProfile });
|
|
381
|
+
return withResolvedKey(async (apiKey) => {
|
|
382
|
+
await handOffSignIn(await profileCreate(newProfile, apiKey), opts, true);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function profileUpdateAction(profileId, opts) {
|
|
386
|
+
const params = { ...(opts.name !== undefined ? { name: opts.name } : {}), ...proxyParams(opts) };
|
|
387
|
+
if (Object.keys(params).length === 0) {
|
|
388
|
+
err({ error: 'Provide --name, --proxy-country, or --no-proxy' });
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
return withResolvedKey(async (apiKey) => {
|
|
392
|
+
const profile = await profileUpdate(profileId, params, apiKey);
|
|
393
|
+
if (opts.pretty)
|
|
394
|
+
outLine(`Profile ${profile.id} updated`);
|
|
395
|
+
else
|
|
396
|
+
out(profile);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function profileSignInAction(profileId, opts) {
|
|
400
|
+
return withResolvedKey(async (apiKey) => {
|
|
401
|
+
await handOffSignIn(await profileGet(profileId, apiKey), opts, false);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
async function profileImportCookiesAction(opts) {
|
|
405
|
+
if (Boolean(opts.domain) === Boolean(opts.allDomains)) {
|
|
406
|
+
err({ error: 'Provide exactly one of --domain or --all-domains' });
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
const { key: apiKey, source } = getResolvedApiKey();
|
|
410
|
+
if (opts.domain) {
|
|
411
|
+
validateDomain(opts.domain);
|
|
412
|
+
}
|
|
413
|
+
let profileId = opts.profileId;
|
|
414
|
+
try {
|
|
415
|
+
const cookiesPath = resolveCookiesPath(opts.chromeProfile);
|
|
416
|
+
await verifyImportTarget(profileId, apiKey);
|
|
417
|
+
const cookies = extractCookiesOrThrow(opts, cookiesPath);
|
|
418
|
+
if (profileId === undefined) {
|
|
419
|
+
const fallbackName = opts.allDomains ? 'chrome-import' : opts.domain;
|
|
420
|
+
const created = await profileCreate(opts.newProfile ?? { name: fallbackName }, apiKey);
|
|
421
|
+
profileId = created.id;
|
|
422
|
+
}
|
|
423
|
+
const uploadResult = opts.allDomains
|
|
424
|
+
? await uploadAllDomains(profileId, cookies, apiKey)
|
|
425
|
+
: await profileUpload(profileId, cookies, apiKey);
|
|
426
|
+
const result = { ...uploadResult, profile_id: profileId };
|
|
427
|
+
if (result.domains_failed && result.domains_failed.length > 0) {
|
|
428
|
+
errLine(`Warning: failed to update cookies for domains: ${result.domains_failed.join(', ')}`);
|
|
429
|
+
}
|
|
430
|
+
if (opts.pretty) {
|
|
431
|
+
printPrettyProfileUpload(result);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
out(result);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
handleApiError(error, rejectedKeyHint(source));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const SITE_OPTION_HELP = 'Site to sign in to, e.g. github.com (a URL works too)';
|
|
442
|
+
const PROXY_COUNTRY_HELP = 'Exit country for sign-in and every run on this profile (ISO 3166-1 alpha-2, e.g. GB)';
|
|
443
|
+
function addImportOptions(cmd) {
|
|
444
|
+
return cmd
|
|
445
|
+
.option('--domain <domain>', 'Domain to import cookies for (e.g. github.com)')
|
|
446
|
+
.option('--all-domains', 'Import cookies for all registrable domains (e.g. github.com) found in your Chrome profile. IPs and non-resolvable hostnames (e.g. localhost) are skipped.')
|
|
447
|
+
.option('--chrome-profile <name>', 'Chrome profile directory, e.g. "Profile 1" (default: last-used profile, else Default)');
|
|
448
|
+
}
|
|
316
449
|
export function registerProfile(program) {
|
|
317
450
|
const profileCmd = program
|
|
318
451
|
.command('profile')
|
|
319
452
|
.description('Profile commands')
|
|
320
453
|
.enablePositionalOptions();
|
|
321
454
|
profileCmd
|
|
455
|
+
.command('list')
|
|
456
|
+
.description('List your browser profiles and the sites each is signed in to')
|
|
457
|
+
.option('--pretty', 'Human-readable output')
|
|
458
|
+
.action(profileListAction);
|
|
459
|
+
addImportOptions(profileCmd
|
|
322
460
|
.command('create')
|
|
323
|
-
.description('Create a browser profile')
|
|
461
|
+
.description('Create a browser profile and get a link where the user signs in and saves it. Opens the link at a terminal.')
|
|
324
462
|
.requiredOption('--name <name>', 'Profile name')
|
|
463
|
+
.option('--site <site>', SITE_OPTION_HELP, parseSite)
|
|
464
|
+
.option('--import-cookies', 'Import Chrome cookies instead of signing in through a link')
|
|
465
|
+
.option('--proxy-country <code>', PROXY_COUNTRY_HELP)
|
|
466
|
+
.option('--pretty', 'Human-readable output')).action(profileCreateAction);
|
|
467
|
+
profileCmd
|
|
468
|
+
.command('update <profile_id>')
|
|
469
|
+
.description('Rename a profile or change its proxy')
|
|
470
|
+
.option('--name <name>', 'New profile name')
|
|
471
|
+
.option('--proxy-country <code>', PROXY_COUNTRY_HELP)
|
|
472
|
+
.option('--no-proxy', 'Connect directly, without a proxy')
|
|
325
473
|
.option('--pretty', 'Human-readable output')
|
|
326
|
-
.action(
|
|
327
|
-
const { key: apiKey, source } = getResolvedApiKey();
|
|
328
|
-
try {
|
|
329
|
-
const response = await profileCreate(opts.name, apiKey);
|
|
330
|
-
if (opts.pretty) {
|
|
331
|
-
printPrettyProfileCreate(response);
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
out(response);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
handleApiError(error, rejectedKeyHint(source));
|
|
339
|
-
}
|
|
340
|
-
});
|
|
474
|
+
.action(profileUpdateAction);
|
|
341
475
|
profileCmd
|
|
476
|
+
.command('sign-in <profile_id>')
|
|
477
|
+
.description('Get a sign-in link for an existing profile, to add or refresh a site')
|
|
478
|
+
.option('--site <site>', SITE_OPTION_HELP, parseSite)
|
|
479
|
+
.option('--pretty', 'Human-readable output')
|
|
480
|
+
.action(profileSignInAction);
|
|
481
|
+
addImportOptions(profileCmd
|
|
342
482
|
.command('import-cookies')
|
|
343
|
-
.description('Import Chrome cookies into a profile. Use --domain for a single domain or --all-domains for every domain in your Chrome profile. Exactly one must be provided.')
|
|
344
|
-
.option('--domain <domain>', 'Domain to import cookies for (e.g. github.com)')
|
|
345
|
-
.option('--all-domains', 'Import cookies for all registrable domains (e.g. github.com) found in your Chrome profile. IPs and non-resolvable hostnames (e.g. localhost) are skipped.')
|
|
346
|
-
.option('--chrome-profile <name>', 'Chrome profile directory, e.g. "Profile 1" (default: last-used profile, else Default)')
|
|
483
|
+
.description('Import Chrome cookies into a profile. Use --domain for a single domain or --all-domains for every domain in your Chrome profile. Exactly one must be provided.'))
|
|
347
484
|
.option('--profile-id <id>', 'Profile ID to import cookies into (creates new profile if omitted)')
|
|
348
485
|
.option('--pretty', 'Human-readable output')
|
|
349
|
-
.action(
|
|
350
|
-
// Mutual-exclusivity guard — must provide exactly one of --domain / --all-domains
|
|
351
|
-
if (!opts.domain && !opts.allDomains) {
|
|
352
|
-
err({ error: 'Provide exactly one of --domain or --all-domains' });
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
if (opts.domain && opts.allDomains) {
|
|
356
|
-
err({ error: 'Provide exactly one of --domain or --all-domains' });
|
|
357
|
-
process.exit(1);
|
|
358
|
-
}
|
|
359
|
-
const { key: apiKey, source } = getResolvedApiKey();
|
|
360
|
-
if (opts.domain) {
|
|
361
|
-
validateDomain(opts.domain);
|
|
362
|
-
}
|
|
363
|
-
let profileId = opts.profileId;
|
|
364
|
-
try {
|
|
365
|
-
const cookiesPath = resolveCookiesPath(opts.chromeProfile);
|
|
366
|
-
await verifyImportTarget(profileId, apiKey);
|
|
367
|
-
const cookies = extractCookiesOrThrow(opts, cookiesPath);
|
|
368
|
-
if (profileId === undefined) {
|
|
369
|
-
const name = opts.allDomains ? 'chrome-import' : opts.domain;
|
|
370
|
-
const created = await profileCreate(name, apiKey);
|
|
371
|
-
profileId = created.id;
|
|
372
|
-
}
|
|
373
|
-
const uploadResult = opts.allDomains
|
|
374
|
-
? await uploadAllDomains(profileId, cookies, apiKey)
|
|
375
|
-
: await profileUpload(profileId, cookies, apiKey);
|
|
376
|
-
const result = { ...uploadResult, profile_id: profileId };
|
|
377
|
-
if (result.domains_failed && result.domains_failed.length > 0) {
|
|
378
|
-
errLine(`Warning: failed to update cookies for domains: ${result.domains_failed.join(', ')}`);
|
|
379
|
-
}
|
|
380
|
-
if (opts.pretty) {
|
|
381
|
-
printPrettyProfileUpload(result);
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
out(result);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
catch (error) {
|
|
388
|
-
handleApiError(error, rejectedKeyHint(source));
|
|
389
|
-
}
|
|
390
|
-
});
|
|
486
|
+
.action(profileImportCookiesAction);
|
|
391
487
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getApiKey } from '../lib/auth.js';
|
|
2
|
-
import { searchQuery } from '../lib/client.js';
|
|
1
|
+
import { getApiKey, withResolvedKey } from '../lib/auth.js';
|
|
2
|
+
import { searchQuery, searchUsage } from '../lib/client.js';
|
|
3
|
+
import { addPageOptions, pageParams, printPage } from '../lib/list-options.js';
|
|
3
4
|
import { err, handleApiError, out, outLine } from '../lib/output.js';
|
|
4
5
|
function printPrettySearch(response) {
|
|
5
6
|
outLine(`Query: ${response.query}`);
|
|
@@ -27,6 +28,11 @@ function parsePageOption(value) {
|
|
|
27
28
|
}
|
|
28
29
|
return Number(value);
|
|
29
30
|
}
|
|
31
|
+
function searchUsageAction(opts) {
|
|
32
|
+
return withResolvedKey(async (apiKey) => {
|
|
33
|
+
printPage(await searchUsage(pageParams(opts), apiKey), opts.pretty, (item) => `${item.created_at} ${item.status_code} ${item.query} (${item.result_count ?? 0} results)`);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
30
36
|
export function registerSearch(program) {
|
|
31
37
|
const searchCmd = program
|
|
32
38
|
.command('search')
|
|
@@ -65,4 +71,5 @@ export function registerSearch(program) {
|
|
|
65
71
|
handleApiError(error);
|
|
66
72
|
}
|
|
67
73
|
});
|
|
74
|
+
addPageOptions(searchCmd.command('usage').description('List your past search requests, newest first'), 'completed|failed').action(searchUsageAction);
|
|
68
75
|
}
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -72,5 +72,6 @@ export declare function validatedApiKey(explicitApiKey?: string): string | undef
|
|
|
72
72
|
export declare function apiKeyStatus(explicitApiKey?: string): ResolvedKey | UnresolvedKey;
|
|
73
73
|
export declare function getResolvedApiKey(): ResolvedKey;
|
|
74
74
|
export declare function getApiKey(): string;
|
|
75
|
+
export declare function withResolvedKey(call: (apiKey: string) => Promise<void>): Promise<void>;
|
|
75
76
|
/** 401 repair hint that names where the rejected key came from. */
|
|
76
77
|
export declare function rejectedKeyHint(source: ResolvedKey['source']): string | undefined;
|
package/dist/lib/auth.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import spawn from 'cross-spawn';
|
|
5
|
-
import { err, errLine } from './output.js';
|
|
5
|
+
import { err, errLine, handleApiError } from './output.js';
|
|
6
6
|
import { TINYFISH_API_KEY_VAR } from './constants.js';
|
|
7
7
|
const KEY_PREFIXES = ['sk-tinyfish-', 'sk-mino-'];
|
|
8
8
|
const MIN_KEY_LENGTH = 20;
|
|
@@ -329,6 +329,15 @@ export function getResolvedApiKey() {
|
|
|
329
329
|
export function getApiKey() {
|
|
330
330
|
return getResolvedApiKey().key;
|
|
331
331
|
}
|
|
332
|
+
export async function withResolvedKey(call) {
|
|
333
|
+
const { key, source } = getResolvedApiKey();
|
|
334
|
+
try {
|
|
335
|
+
await call(key);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
handleApiError(error, rejectedKeyHint(source));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
332
341
|
/** 401 repair hint that names where the rejected key came from. */
|
|
333
342
|
export function rejectedKeyHint(source) {
|
|
334
343
|
if (source === 'env') {
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type AgentRunAsyncResponse, type AgentRunResponse, type AgentRunWithStreamingResponse, type BrowserSession, type FetchGetContentsParams, type FetchResponse, type Run, type RunListParams, type RunListResponse, type SearchQueryParams, type SearchQueryResponse, type WalletResponse } from '@tiny-fish/sdk';
|
|
2
|
+
import type { PageParams } from './list-options.js';
|
|
2
3
|
import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, CookieRecord, ProfileCreateResponse, ProfileSummary, ProfileUploadResponse, RunStepsResponse, VaultConnectRequest, VaultConnectResponse, VaultConnectionsListResponse, VaultDisconnectResponse, VaultItemsListResponse, VaultItemsSyncResponse } from './types.js';
|
|
4
|
+
import { z } from 'zod';
|
|
3
5
|
type ConnectMap = Record<string, {
|
|
4
6
|
attempt_id: string;
|
|
5
7
|
}> | undefined;
|
|
@@ -15,14 +17,120 @@ export declare function getRunSteps(runId: string, apiKey: string): Promise<RunS
|
|
|
15
17
|
export declare function searchQuery(params: SearchQueryParams, apiKey: string): Promise<SearchQueryResponse>;
|
|
16
18
|
export declare function fetchContentGet(params: FetchGetContentsParams, apiKey: string): Promise<FetchResponse>;
|
|
17
19
|
export declare function browserSessionCreate(params: CliBrowserSessionCreateParams, apiKey: string): Promise<BrowserSession>;
|
|
20
|
+
declare const browserSessionPageSchema: z.ZodObject<{
|
|
21
|
+
items: z.ZodArray<z.ZodObject<{
|
|
22
|
+
session_id: z.ZodString;
|
|
23
|
+
status: z.ZodNullable<z.ZodString>;
|
|
24
|
+
start_time: z.ZodNullable<z.ZodString>;
|
|
25
|
+
}, z.core.$loose>>;
|
|
26
|
+
total: z.ZodNumber;
|
|
27
|
+
page: z.ZodNumber;
|
|
28
|
+
total_pages: z.ZodNumber;
|
|
29
|
+
}, z.core.$loose>;
|
|
30
|
+
declare const searchUsagePageSchema: z.ZodObject<{
|
|
31
|
+
items: z.ZodArray<z.ZodObject<{
|
|
32
|
+
query: z.ZodString;
|
|
33
|
+
status_code: z.ZodNumber;
|
|
34
|
+
result_count: z.ZodNullable<z.ZodNumber>;
|
|
35
|
+
created_at: z.ZodString;
|
|
36
|
+
}, z.core.$loose>>;
|
|
37
|
+
total: z.ZodNumber;
|
|
38
|
+
page: z.ZodNumber;
|
|
39
|
+
total_pages: z.ZodNumber;
|
|
40
|
+
}, z.core.$loose>;
|
|
41
|
+
declare const fetchUsagePageSchema: z.ZodObject<{
|
|
42
|
+
items: z.ZodArray<z.ZodObject<{
|
|
43
|
+
url: z.ZodString;
|
|
44
|
+
status: z.ZodString;
|
|
45
|
+
created_at: z.ZodString;
|
|
46
|
+
}, z.core.$loose>>;
|
|
47
|
+
total: z.ZodNumber;
|
|
48
|
+
page: z.ZodNumber;
|
|
49
|
+
total_pages: z.ZodNumber;
|
|
50
|
+
}, z.core.$loose>;
|
|
51
|
+
export type BrowserSessionPage = z.infer<typeof browserSessionPageSchema>;
|
|
52
|
+
export type SearchUsagePage = z.infer<typeof searchUsagePageSchema>;
|
|
53
|
+
export type FetchUsagePage = z.infer<typeof fetchUsagePageSchema>;
|
|
54
|
+
export declare function browserSessionList(params: PageParams, apiKey: string): Promise<BrowserSessionPage>;
|
|
55
|
+
export declare function browserSessionClose(sessionId: string, apiKey: string): Promise<void>;
|
|
56
|
+
export declare function searchUsage(params: PageParams, apiKey: string): Promise<SearchUsagePage>;
|
|
57
|
+
export declare function fetchUsage(params: PageParams, apiKey: string): Promise<FetchUsagePage>;
|
|
58
|
+
declare const monitorSchema: z.ZodObject<{
|
|
59
|
+
id: z.ZodString;
|
|
60
|
+
type: z.ZodString;
|
|
61
|
+
config: z.ZodObject<{
|
|
62
|
+
query: z.ZodOptional<z.ZodString>;
|
|
63
|
+
url: z.ZodOptional<z.ZodString>;
|
|
64
|
+
}, z.core.$loose>;
|
|
65
|
+
schedule_cron: z.ZodString;
|
|
66
|
+
status: z.ZodString;
|
|
67
|
+
}, z.core.$loose>;
|
|
68
|
+
declare const monitorRunSchema: z.ZodObject<{
|
|
69
|
+
id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
70
|
+
result_count: z.ZodOptional<z.ZodNumber>;
|
|
71
|
+
results: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
72
|
+
errors: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
73
|
+
}, z.core.$loose>;
|
|
74
|
+
declare const monitorCreateResponseSchema: z.ZodObject<{
|
|
75
|
+
monitor: z.ZodObject<{
|
|
76
|
+
id: z.ZodString;
|
|
77
|
+
type: z.ZodString;
|
|
78
|
+
config: z.ZodObject<{
|
|
79
|
+
query: z.ZodOptional<z.ZodString>;
|
|
80
|
+
url: z.ZodOptional<z.ZodString>;
|
|
81
|
+
}, z.core.$loose>;
|
|
82
|
+
schedule_cron: z.ZodString;
|
|
83
|
+
status: z.ZodString;
|
|
84
|
+
}, z.core.$loose>;
|
|
85
|
+
run: z.ZodObject<{
|
|
86
|
+
id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
87
|
+
result_count: z.ZodOptional<z.ZodNumber>;
|
|
88
|
+
results: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
89
|
+
errors: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
90
|
+
}, z.core.$loose>;
|
|
91
|
+
}, z.core.$loose>;
|
|
92
|
+
export type Monitor = z.infer<typeof monitorSchema>;
|
|
93
|
+
export type MonitorRun = z.infer<typeof monitorRunSchema>;
|
|
94
|
+
export type MonitorCreateResponse = z.infer<typeof monitorCreateResponseSchema>;
|
|
95
|
+
export declare function monitorCreate(body: object, apiKey: string): Promise<MonitorCreateResponse>;
|
|
96
|
+
export declare function monitorList(apiKey: string): Promise<{
|
|
97
|
+
monitors: Monitor[];
|
|
98
|
+
}>;
|
|
99
|
+
export declare function monitorGet(id: string, apiKey: string): Promise<Monitor>;
|
|
100
|
+
export declare function monitorSetStatus(id: string, status: 'active' | 'paused', apiKey: string): Promise<Monitor>;
|
|
101
|
+
export declare function monitorRun(id: string, apiKey: string): Promise<{
|
|
102
|
+
run: MonitorRun;
|
|
103
|
+
}>;
|
|
104
|
+
export declare function monitorDelete(id: string, apiKey: string): Promise<{
|
|
105
|
+
deleted: true;
|
|
106
|
+
id: string;
|
|
107
|
+
}>;
|
|
18
108
|
export declare function cancelRun(runId: string, apiKey: string): Promise<CancelRunResponse>;
|
|
19
109
|
export declare function getWallet(apiKey: string): Promise<WalletResponse>;
|
|
20
110
|
export declare function submitBatch(req: BatchRunRequest, apiKey: string): Promise<BatchRunResponse>;
|
|
21
111
|
export declare function getBatchRuns(runIds: string[], apiKey: string): Promise<BatchGetResponse>;
|
|
22
112
|
export declare function cancelBatchRuns(runIds: string[], apiKey: string): Promise<BatchCancelResponse>;
|
|
23
|
-
|
|
113
|
+
declare const profileListResponseSchema: z.ZodObject<{
|
|
114
|
+
profiles: z.ZodArray<z.ZodObject<{
|
|
115
|
+
id: z.ZodString;
|
|
116
|
+
name: z.ZodString;
|
|
117
|
+
proxy_country_code: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
118
|
+
signed_in_sites: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
119
|
+
site: z.ZodString;
|
|
120
|
+
}, z.core.$loose>>>;
|
|
121
|
+
}, z.core.$loose>>;
|
|
122
|
+
}, z.core.$loose>;
|
|
123
|
+
export type ProfileListResponse = z.infer<typeof profileListResponseSchema>;
|
|
124
|
+
export interface ProfileWriteParams {
|
|
125
|
+
name?: string;
|
|
126
|
+
proxy_country_code?: string | null;
|
|
127
|
+
}
|
|
128
|
+
export declare function profileCreate(params: ProfileWriteParams & {
|
|
129
|
+
name: string;
|
|
130
|
+
}, apiKey: string): Promise<ProfileCreateResponse>;
|
|
131
|
+
export declare function profileUpdate(profileId: string, params: ProfileWriteParams, apiKey: string): Promise<ProfileSummary>;
|
|
24
132
|
export declare function profileGet(profileId: string, apiKey: string): Promise<ProfileSummary>;
|
|
25
|
-
export declare function profileList(apiKey: string): Promise<
|
|
133
|
+
export declare function profileList(apiKey: string): Promise<ProfileListResponse>;
|
|
26
134
|
export declare function profileUpload(profileId: string, cookies: CookieRecord[], apiKey: string): Promise<ProfileUploadResponse>;
|
|
27
135
|
export declare function vaultConnections(apiKey: string): Promise<VaultConnectionsListResponse>;
|
|
28
136
|
export declare function vaultConnect(req: VaultConnectRequest, apiKey: string): Promise<VaultConnectResponse>;
|
package/dist/lib/client.js
CHANGED
|
@@ -3,7 +3,7 @@ import { CONNECT_SOURCE, RUNTIME_ATTEMPT_ENV, loadConfig } from './auth.js';
|
|
|
3
3
|
import { API_URL_OVERRIDE, CLI_AGENT_IDENTITY, CLI_VERSION } from './constants.js';
|
|
4
4
|
import { detectHarness, detectHumanInitiated } from './harness.js';
|
|
5
5
|
import { captureNotice } from './notice.js';
|
|
6
|
-
import { ApiError } from './output.js';
|
|
6
|
+
import { ApiError, parseJson } from './output.js';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
// Install-level connect context (PF-3169). A known caller is credited only to its own
|
|
9
9
|
// install; under an unknown caller we credit a lone install, else send source only.
|
|
@@ -84,38 +84,30 @@ class TinyFishCliClient extends TinyFish {
|
|
|
84
84
|
}
|
|
85
85
|
return headers;
|
|
86
86
|
}
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
// matches the CLI's maxRetries:0; reuse the SDK request helper if more DELETE
|
|
90
|
-
// routes appear.
|
|
91
|
-
async del(path) {
|
|
87
|
+
// SDK has no PATCH and its delete() throws on 204.
|
|
88
|
+
async send(method, path, json) {
|
|
92
89
|
const response = await fetch(`${this.baseURL}${path}`, {
|
|
93
|
-
method
|
|
90
|
+
method,
|
|
94
91
|
headers: this._buildHeaders(),
|
|
92
|
+
...(json !== undefined ? { body: JSON.stringify(json) } : {}),
|
|
95
93
|
});
|
|
96
94
|
captureNotice(response.headers);
|
|
97
|
-
if (!response.ok)
|
|
98
|
-
|
|
99
|
-
let code;
|
|
100
|
-
try {
|
|
101
|
-
const body = (await response.json());
|
|
102
|
-
if (typeof body?.error?.message === 'string')
|
|
103
|
-
message = body.error.message;
|
|
104
|
-
else if (typeof body?.message === 'string')
|
|
105
|
-
message = body.message;
|
|
106
|
-
if (typeof body?.error?.code === 'string')
|
|
107
|
-
code = body.error.code;
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
// Non-JSON body — keep statusText
|
|
111
|
-
}
|
|
112
|
-
throw new ApiError(response.status, message, code);
|
|
113
|
-
}
|
|
95
|
+
if (!response.ok)
|
|
96
|
+
throw await apiErrorFrom(response);
|
|
114
97
|
if (response.status === 204)
|
|
115
98
|
return undefined;
|
|
116
99
|
return (await response.json());
|
|
117
100
|
}
|
|
118
101
|
}
|
|
102
|
+
// SDK order: JSON message, raw text, then status.
|
|
103
|
+
async function apiErrorFrom(response) {
|
|
104
|
+
const text = await response.text().catch(() => '');
|
|
105
|
+
const body = parseJson(text);
|
|
106
|
+
const message = [body?.error?.message, body?.message].find((m) => typeof m === 'string') ??
|
|
107
|
+
(text || `HTTP ${response.status}`);
|
|
108
|
+
const code = typeof body?.error?.code === 'string' ? body.error.code : undefined;
|
|
109
|
+
return new ApiError(response.status, message, code);
|
|
110
|
+
}
|
|
119
111
|
function createSdkClient(apiKey, timeout, baseUrl) {
|
|
120
112
|
// An explicit base outranks the env default.
|
|
121
113
|
const resolvedBase = baseUrl ?? API_URL_OVERRIDE;
|
|
@@ -331,6 +323,103 @@ export function browserSessionCreate(params, apiKey) {
|
|
|
331
323
|
return browserSessionSchema.parse(response);
|
|
332
324
|
});
|
|
333
325
|
}
|
|
326
|
+
// New commands echo the full server body; looseObject keeps unknown keys.
|
|
327
|
+
function pageSchema(item) {
|
|
328
|
+
return z.looseObject({
|
|
329
|
+
items: z.array(item),
|
|
330
|
+
total: z.number(),
|
|
331
|
+
page: z.number(),
|
|
332
|
+
total_pages: z.number(),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const browserSessionUsageSchema = z.looseObject({
|
|
336
|
+
session_id: z.string(),
|
|
337
|
+
status: z.string().nullable(),
|
|
338
|
+
start_time: z.string().nullable(),
|
|
339
|
+
});
|
|
340
|
+
const searchUsageItemSchema = z.looseObject({
|
|
341
|
+
query: z.string(),
|
|
342
|
+
status_code: z.number(),
|
|
343
|
+
result_count: z.number().nullable(),
|
|
344
|
+
created_at: z.string(),
|
|
345
|
+
});
|
|
346
|
+
const fetchUsageItemSchema = z.looseObject({
|
|
347
|
+
url: z.string(),
|
|
348
|
+
status: z.string(),
|
|
349
|
+
created_at: z.string(),
|
|
350
|
+
});
|
|
351
|
+
const browserSessionPageSchema = pageSchema(browserSessionUsageSchema);
|
|
352
|
+
const searchUsagePageSchema = pageSchema(searchUsageItemSchema);
|
|
353
|
+
const fetchUsagePageSchema = pageSchema(fetchUsageItemSchema);
|
|
354
|
+
function getPage(path, schema, params, apiKey) {
|
|
355
|
+
return sdk(apiKey, async (client) => parseWithSchema(schema, await client.get(path, { params }), `Invalid ${path} response`));
|
|
356
|
+
}
|
|
357
|
+
export function browserSessionList(params, apiKey) {
|
|
358
|
+
return getPage('/v1/browser/usage', browserSessionPageSchema, params, apiKey);
|
|
359
|
+
}
|
|
360
|
+
export function browserSessionClose(sessionId, apiKey) {
|
|
361
|
+
return sdk(apiKey, (client) => client.send('DELETE', `/v1/browser/${encodeURIComponent(sessionId)}`));
|
|
362
|
+
}
|
|
363
|
+
export function searchUsage(params, apiKey) {
|
|
364
|
+
return getPage('/v1/search/usage', searchUsagePageSchema, params, apiKey);
|
|
365
|
+
}
|
|
366
|
+
export function fetchUsage(params, apiKey) {
|
|
367
|
+
return getPage('/v1/fetch/usage', fetchUsagePageSchema, params, apiKey);
|
|
368
|
+
}
|
|
369
|
+
const monitorSchema = z.looseObject({
|
|
370
|
+
id: z.string(),
|
|
371
|
+
type: z.string(),
|
|
372
|
+
config: z.looseObject({ query: z.string().optional(), url: z.string().optional() }),
|
|
373
|
+
schedule_cron: z.string(),
|
|
374
|
+
status: z.string(),
|
|
375
|
+
});
|
|
376
|
+
const monitorRunSchema = z.looseObject({
|
|
377
|
+
id: z.union([z.string(), z.number()]),
|
|
378
|
+
result_count: z.number().optional(),
|
|
379
|
+
results: z.array(z.unknown()).optional(),
|
|
380
|
+
errors: z.array(z.unknown()).optional(),
|
|
381
|
+
});
|
|
382
|
+
const monitorCreateResponseSchema = z.looseObject({
|
|
383
|
+
monitor: monitorSchema,
|
|
384
|
+
run: monitorRunSchema,
|
|
385
|
+
});
|
|
386
|
+
const monitorListResponseSchema = z.looseObject({ monitors: z.array(monitorSchema) });
|
|
387
|
+
const monitorRunResponseSchema = z.looseObject({ run: monitorRunSchema });
|
|
388
|
+
const monitorDeleteResponseSchema = z.looseObject({ deleted: z.literal(true), id: z.string() });
|
|
389
|
+
const monitorPath = (id) => `/v1/monitors/${encodeURIComponent(id)}`;
|
|
390
|
+
export function monitorCreate(body, apiKey) {
|
|
391
|
+
return sdk(apiKey, async (client) => {
|
|
392
|
+
const response = await client.post('/v1/monitors', { json: body });
|
|
393
|
+
return parseWithSchema(monitorCreateResponseSchema, response, 'Invalid monitor create response');
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
export function monitorList(apiKey) {
|
|
397
|
+
return sdk(apiKey, async (client) => {
|
|
398
|
+
const response = await client.get('/v1/monitors');
|
|
399
|
+
return parseWithSchema(monitorListResponseSchema, response, 'Invalid monitor list response');
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
export function monitorGet(id, apiKey) {
|
|
403
|
+
return sdk(apiKey, async (client) => parseWithSchema(monitorSchema, await client.get(monitorPath(id)), 'Invalid monitor response'));
|
|
404
|
+
}
|
|
405
|
+
export function monitorSetStatus(id, status, apiKey) {
|
|
406
|
+
return sdk(apiKey, async (client) => {
|
|
407
|
+
const response = await client.send('PATCH', monitorPath(id), { status });
|
|
408
|
+
return parseWithSchema(monitorSchema, response, 'Invalid monitor response');
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
export function monitorRun(id, apiKey) {
|
|
412
|
+
return sdk(apiKey, async (client) => {
|
|
413
|
+
const response = await client.post(`${monitorPath(id)}/runs`, { json: {} });
|
|
414
|
+
return parseWithSchema(monitorRunResponseSchema, response, 'Invalid monitor run response');
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
export function monitorDelete(id, apiKey) {
|
|
418
|
+
return sdk(apiKey, async (client) => {
|
|
419
|
+
const response = await client.send('DELETE', monitorPath(id));
|
|
420
|
+
return parseWithSchema(monitorDeleteResponseSchema, response, 'Invalid monitor delete response');
|
|
421
|
+
});
|
|
422
|
+
}
|
|
334
423
|
export function cancelRun(runId, apiKey) {
|
|
335
424
|
return sdk(apiKey, async (client) => {
|
|
336
425
|
const response = await client.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
@@ -367,27 +456,38 @@ export function cancelBatchRuns(runIds, apiKey) {
|
|
|
367
456
|
return parseWithSchema(batchCancelResponseSchema, response, 'Invalid batch cancel response');
|
|
368
457
|
});
|
|
369
458
|
}
|
|
370
|
-
const profileCreateResponseSchema = z.
|
|
459
|
+
const profileCreateResponseSchema = z.looseObject({
|
|
371
460
|
id: z.string(),
|
|
372
461
|
name: z.string(),
|
|
373
462
|
created_at: z.string(),
|
|
374
463
|
});
|
|
375
464
|
const profileSummarySchema = z.object({ id: z.string(), name: z.string() });
|
|
376
|
-
const profileListResponseSchema = z.
|
|
465
|
+
const profileListResponseSchema = z.looseObject({
|
|
466
|
+
profiles: z.array(profileSummarySchema
|
|
467
|
+
.extend({
|
|
468
|
+
proxy_country_code: z.string().nullish(),
|
|
469
|
+
signed_in_sites: z.array(z.looseObject({ site: z.string() })).optional(),
|
|
470
|
+
})
|
|
471
|
+
.loose()),
|
|
472
|
+
});
|
|
377
473
|
const profileUploadResponseSchema = z.object({
|
|
378
474
|
profile_id: z.string().optional(),
|
|
379
475
|
cookie_count: z.number(),
|
|
380
476
|
domains_updated: z.array(z.string()),
|
|
381
477
|
domains_failed: z.array(z.string()).optional(),
|
|
382
478
|
});
|
|
383
|
-
export function profileCreate(
|
|
479
|
+
export function profileCreate(params, apiKey) {
|
|
384
480
|
return sdk(apiKey, async (client) => {
|
|
385
|
-
const response = await client.post('/v1/profiles', {
|
|
386
|
-
json: { name },
|
|
387
|
-
});
|
|
481
|
+
const response = await client.post('/v1/profiles', { json: params });
|
|
388
482
|
return parseWithSchema(profileCreateResponseSchema, response, 'Invalid profile create response');
|
|
389
483
|
});
|
|
390
484
|
}
|
|
485
|
+
export function profileUpdate(profileId, params, apiKey) {
|
|
486
|
+
return sdk(apiKey, async (client) => {
|
|
487
|
+
const response = await client.send('PATCH', `/v1/profiles/${encodeURIComponent(profileId)}`, params);
|
|
488
|
+
return parseWithSchema(profileSummarySchema.loose(), response, 'Invalid profile response');
|
|
489
|
+
});
|
|
490
|
+
}
|
|
391
491
|
export function profileGet(profileId, apiKey) {
|
|
392
492
|
return sdk(apiKey, async (client) => {
|
|
393
493
|
const response = await client.get(`/v1/profiles/${encodeURIComponent(profileId)}`);
|
|
@@ -397,8 +497,7 @@ export function profileGet(profileId, apiKey) {
|
|
|
397
497
|
export function profileList(apiKey) {
|
|
398
498
|
return sdk(apiKey, async (client) => {
|
|
399
499
|
const response = await client.get('/v1/profiles');
|
|
400
|
-
return parseWithSchema(profileListResponseSchema, response, 'Invalid profile list response')
|
|
401
|
-
.profiles;
|
|
500
|
+
return parseWithSchema(profileListResponseSchema, response, 'Invalid profile list response');
|
|
402
501
|
});
|
|
403
502
|
}
|
|
404
503
|
export function profileUpload(profileId, cookies, apiKey) {
|
|
@@ -470,7 +569,7 @@ export function vaultConnect(req, apiKey) {
|
|
|
470
569
|
}
|
|
471
570
|
export function vaultDisconnect(connectionId, apiKey) {
|
|
472
571
|
return sdk(apiKey, async (client) => {
|
|
473
|
-
const response = await client.
|
|
572
|
+
const response = await client.send('DELETE', `/v1/vault/connections/${encodeURIComponent(connectionId)}`);
|
|
474
573
|
return parseWithSchema(vaultDisconnectResponseSchema, response, 'Invalid vault disconnect response');
|
|
475
574
|
});
|
|
476
575
|
}
|
|
@@ -179,6 +179,18 @@ export async function runGuarded(state, telemetry, body) {
|
|
|
179
179
|
function probeTimeoutMs(client) {
|
|
180
180
|
return client.supportCheck.timeoutMs ?? NON_INTERACTIVE_TIMEOUT_MS;
|
|
181
181
|
}
|
|
182
|
+
// Flags go unseen for reasons besides age; state facts, not causes.
|
|
183
|
+
function unavailableMessage(client, version) {
|
|
184
|
+
const { minVersion } = client.supportCheck;
|
|
185
|
+
const found = version ? ` (found ${version})` : '';
|
|
186
|
+
// Numeric collation orders dotted versions: 7.7.3 sorts after 7.4.13.
|
|
187
|
+
const belowMin = minVersion && (!version || version.localeCompare(minVersion, 'en', { numeric: true }) < 0);
|
|
188
|
+
const required = belowMin
|
|
189
|
+
? ` ${client.displayName} ${minVersion} or newer is required${found}.`
|
|
190
|
+
: '';
|
|
191
|
+
return (`${client.supportCheck.unavailableMessage}${required}` +
|
|
192
|
+
' Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.');
|
|
193
|
+
}
|
|
182
194
|
function throwProbeFailure(client, result) {
|
|
183
195
|
throwIfInterrupted(result);
|
|
184
196
|
// A hung probe is a timeout, not a missing capability.
|
|
@@ -188,10 +200,13 @@ function throwProbeFailure(client, result) {
|
|
|
188
200
|
`${probeTimeoutMs(client) / 1000}s. Retry.`, 'timeout', { cause: result.error, failureDetail: spawnFailureDetail(result) });
|
|
189
201
|
}
|
|
190
202
|
// It ran and exited non-zero, so the version is still answerable.
|
|
191
|
-
|
|
203
|
+
debugProbeOutput(client, sanitizeLine(`${result.stdout ?? ''}\n${result.stderr ?? ''}`));
|
|
204
|
+
const harnessVersion = probeHarnessVersion(client);
|
|
205
|
+
throw new ConnectStepError(unavailableMessage(client, harnessVersion), 'harness_command_unsupported', {
|
|
192
206
|
cause: result.error,
|
|
193
|
-
harnessVersion
|
|
194
|
-
|
|
207
|
+
harnessVersion,
|
|
208
|
+
// A spawn error leaves stderr empty; its message is the diagnosis.
|
|
209
|
+
failureDetail: result.error?.message ?? spawnFailureDetail(result),
|
|
195
210
|
});
|
|
196
211
|
}
|
|
197
212
|
function supportProbeOutput(client) {
|
|
@@ -220,8 +235,9 @@ function requireEssential(client, output) {
|
|
|
220
235
|
if (client.supportCheck.patterns.every((pattern) => pattern.test(output)))
|
|
221
236
|
return;
|
|
222
237
|
debugProbeOutput(client, output);
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
const harnessVersion = probeHarnessVersion(client);
|
|
239
|
+
throw new ConnectStepError(unavailableMessage(client, harnessVersion), 'harness_too_old', {
|
|
240
|
+
harnessVersion,
|
|
225
241
|
});
|
|
226
242
|
}
|
|
227
243
|
/** Absence is a fact; an unknown flag means an upstream rename. */
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// User-facing copy for the harness specs; kept apart so the spec file stays structure.
|
|
2
|
-
// An old install is only one reason a flag can go unseen, so no message asserts the cause.
|
|
3
|
-
const SUPPORT_CHECK_DEBUG_HINT = ' Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.';
|
|
4
2
|
export const MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this Claude Code installation exposes `claude mcp add`: `claude mcp ' +
|
|
5
|
-
'--help` did not list it. Update Claude Code with `claude update` and retry.'
|
|
6
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
3
|
+
'--help` did not list it. Update Claude Code with `claude update` and retry.';
|
|
7
4
|
// `claude mcp login` only exists upstream from Claude Code 2.1.186; older installs still register.
|
|
8
5
|
export const CLAUDE_CODE_KEYED_FALLBACK_NOTE = 'Using your stored TinyFish API key: this Claude Code predates one-command sign-in ' +
|
|
9
6
|
'(`claude mcp login`), so no browser sign-in is needed.';
|
|
@@ -13,8 +10,7 @@ export const CLAUDE_CODE_DEFERRED_AUTH_NOTE = 'TinyFish is registered in Claude
|
|
|
13
10
|
'Run `tinyfish connect claude-code --api-key <key>` instead.';
|
|
14
11
|
export const CODEX_MCP_URL_UNAVAILABLE_MESSAGE = 'Could not confirm this Codex installation supports remote MCP servers: `codex mcp add ' +
|
|
15
12
|
'--help` did not list `--url`. Update Codex and retry, or add TinyFish manually with ' +
|
|
16
|
-
'`codex mcp add`.'
|
|
17
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
13
|
+
'`codex mcp add`.';
|
|
18
14
|
export const CODEX_KEYED_FALLBACK_NOTE = 'Using your stored TinyFish API key: this Codex cannot report whether an OAuth sign-in ' +
|
|
19
15
|
'finished, so no browser sign-in is attempted.';
|
|
20
16
|
export const CODEX_DEFERRED_AUTH_NOTE = 'TinyFish is registered in Codex but not signed in: this Codex predates reliable sign-in ' +
|
|
@@ -22,24 +18,20 @@ export const CODEX_DEFERRED_AUTH_NOTE = 'TinyFish is registered in Codex but not
|
|
|
22
18
|
'`npm install -g @openai/codex@latest` for one-command sign-in.';
|
|
23
19
|
export const HERMES_OAUTH_UNAVAILABLE_MESSAGE = 'Could not confirm this Hermes installation supports one-command MCP authentication: `hermes ' +
|
|
24
20
|
'mcp add --help` did not list `--auth oauth`. Update Hermes and retry, or add TinyFish ' +
|
|
25
|
-
'manually with `hermes mcp add`.'
|
|
26
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
21
|
+
'manually with `hermes mcp add`.';
|
|
27
22
|
export const GROK_HTTP_UNAVAILABLE_MESSAGE = 'Could not confirm this Grok installation supports remote MCP servers: `grok mcp add --help` ' +
|
|
28
23
|
'did not list `--transport`. Update Grok and retry, or add TinyFish manually with ' +
|
|
29
|
-
'`grok mcp add`.'
|
|
30
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
24
|
+
'`grok mcp add`.';
|
|
31
25
|
// Grok has no `mcp login`; sign-in only happens inside the TUI. No key nudge here: a revoked
|
|
32
26
|
// key leaves Grok with no OAuth fallback, and doctor cannot spot that yet (PF-3618).
|
|
33
27
|
export const GROK_SIGN_IN_HINT = 'TinyFish is registered in Grok. Open Grok, run `/mcps`, select tinyfish, and press `i` to ' +
|
|
34
28
|
'sign in.';
|
|
35
29
|
export const OPENCLAW_SKILL_UNAVAILABLE_MESSAGE = 'Could not confirm this OpenClaw installation supports global skill installation: `openclaw ' +
|
|
36
30
|
'skills install --help` did not list `--global`. Update OpenClaw and retry, or install ' +
|
|
37
|
-
'manually with `openclaw skills install @tinyfish/tinyfish --global`.'
|
|
38
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
31
|
+
'manually with `openclaw skills install @tinyfish/tinyfish --global`.';
|
|
39
32
|
export const OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this OpenCode installation supports one-command MCP setup: `opencode mcp ' +
|
|
40
33
|
'add --help` did not list `--url`. Update OpenCode and retry, or add TinyFish manually with ' +
|
|
41
|
-
'`opencode mcp add`.'
|
|
42
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
34
|
+
'`opencode mcp add`.';
|
|
43
35
|
// OpenCode is model-agnostic (unlike Claude Code / Codex), so a fresh install can default to a
|
|
44
36
|
// model with no tool support — TinyFish runs entirely on tool calls, so it would fail there.
|
|
45
37
|
export const OPENCODE_MODEL_NOTE = 'Note: TinyFish runs on tool calls, so OpenCode needs a model that supports tool use. Image-only ' +
|
|
@@ -47,9 +39,8 @@ export const OPENCODE_MODEL_NOTE = 'Note: TinyFish runs on tool calls, so OpenCo
|
|
|
47
39
|
"OpenCode's model if the walkthrough can't start.";
|
|
48
40
|
export const KILO_MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this Kilo Code installation supports remote MCP servers: `kilo mcp add ' +
|
|
49
41
|
'--help` did not list `--url`. Update Kilo Code with `kilo upgrade` and retry, or add ' +
|
|
50
|
-
'TinyFish manually with `kilo mcp add`.'
|
|
51
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
42
|
+
'TinyFish manually with `kilo mcp add`.';
|
|
52
43
|
export const HERMES_RESTART_NOTE = 'Restart your Hermes session to pick up TinyFish — Hermes discovers MCP servers at startup.';
|
|
53
44
|
export const CLINE_MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this Cline installation supports non-interactive MCP setup: `cline mcp add ' +
|
|
54
|
-
'--help` did not list `--transport` and `--yes`. Update Cline with `cline
|
|
55
|
-
|
|
45
|
+
'--help` did not list `--transport` and `--yes`. Update Cline with `npm i -g cline@latest` ' +
|
|
46
|
+
'or `cline --update` and retry.';
|
|
@@ -14,6 +14,8 @@ export interface HarnessSupportCheck {
|
|
|
14
14
|
unknownVariant?: RegExp;
|
|
15
15
|
/** Defaults to NON_INTERACTIVE_TIMEOUT_MS; raise for a slow cold start. */
|
|
16
16
|
timeoutMs?: number;
|
|
17
|
+
/** First release with the probed flags; bisected upstream, not derivable. */
|
|
18
|
+
minVersion?: string;
|
|
17
19
|
unavailableMessage: string;
|
|
18
20
|
}
|
|
19
21
|
/** One entry per harness: every per-harness fact, and no behaviour. */
|
|
@@ -124,6 +126,8 @@ export declare const HARNESS_SPECS: {
|
|
|
124
126
|
args: string[];
|
|
125
127
|
patterns: RegExp[];
|
|
126
128
|
keyAuthPattern: RegExp;
|
|
129
|
+
timeoutMs: number;
|
|
130
|
+
minVersion: string;
|
|
127
131
|
unavailableMessage: string;
|
|
128
132
|
};
|
|
129
133
|
urlStyle: "positional";
|
|
@@ -232,6 +236,7 @@ export declare const HARNESS_SPECS: {
|
|
|
232
236
|
args: string[];
|
|
233
237
|
patterns: RegExp[];
|
|
234
238
|
keyAuthPattern: RegExp;
|
|
239
|
+
minVersion: string;
|
|
235
240
|
unavailableMessage: string;
|
|
236
241
|
};
|
|
237
242
|
urlStyle: "flag";
|
|
@@ -51,6 +51,9 @@ export const HARNESS_SPECS = {
|
|
|
51
51
|
// Without `--yes` the add opens a wizard and exits non-zero off a TTY.
|
|
52
52
|
patterns: [/--transport(?:[\s<=]|$)/m, /--yes(?:[\s<=]|$)/m],
|
|
53
53
|
keyAuthPattern: HEADER_FLAG,
|
|
54
|
+
// Cold start on first run outlasts the 10s default.
|
|
55
|
+
timeoutMs: 30_000,
|
|
56
|
+
minVersion: '3.0.30',
|
|
54
57
|
unavailableMessage: CLINE_MCP_ADD_UNAVAILABLE_MESSAGE,
|
|
55
58
|
},
|
|
56
59
|
urlStyle: 'positional',
|
|
@@ -162,6 +165,7 @@ export const HARNESS_SPECS = {
|
|
|
162
165
|
args: ['mcp', 'add', '--help'],
|
|
163
166
|
patterns: [/--url(?:\s|$)/m],
|
|
164
167
|
keyAuthPattern: HEADER_FLAG,
|
|
168
|
+
minVersion: '7.4.13',
|
|
165
169
|
unavailableMessage: KILO_MCP_ADD_UNAVAILABLE_MESSAGE,
|
|
166
170
|
},
|
|
167
171
|
urlStyle: 'flag',
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
export declare function integerInRange(min: number, max?: number): (value: string) => number;
|
|
3
|
+
export interface PageOptions {
|
|
4
|
+
startAfter?: string;
|
|
5
|
+
endBefore?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
page?: number;
|
|
9
|
+
pretty?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export type PageParams = Record<string, string | number>;
|
|
12
|
+
export declare function addPageOptions(cmd: Command, statuses: string): Command;
|
|
13
|
+
export declare function pageParams(opts: PageOptions): PageParams;
|
|
14
|
+
interface Page<T> {
|
|
15
|
+
items: T[];
|
|
16
|
+
total: number;
|
|
17
|
+
page: number;
|
|
18
|
+
total_pages: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function printPage<T>(result: Page<T>, pretty: boolean | undefined, line: (item: T) => string): void;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { InvalidArgumentError } from 'commander';
|
|
2
|
+
import { out, outLine } from './output.js';
|
|
3
|
+
const MAX_PAGE_SIZE = 1000;
|
|
4
|
+
export function integerInRange(min, max) {
|
|
5
|
+
return (value) => {
|
|
6
|
+
const parsed = Number(value);
|
|
7
|
+
if (!Number.isSafeInteger(parsed) || parsed < min || (max !== undefined && parsed > max)) {
|
|
8
|
+
throw new InvalidArgumentError(max === undefined
|
|
9
|
+
? 'must be a positive integer'
|
|
10
|
+
: `must be an integer between ${min} and ${max}`);
|
|
11
|
+
}
|
|
12
|
+
return parsed;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function addPageOptions(cmd, statuses) {
|
|
16
|
+
return cmd
|
|
17
|
+
.option('--start-after <iso>', 'Only items created after this ISO 8601 timestamp')
|
|
18
|
+
.option('--end-before <iso>', 'Only items created before this ISO 8601 timestamp')
|
|
19
|
+
.option('--status <status>', `Filter by status (${statuses})`)
|
|
20
|
+
.option('--limit <n>', `Items per page, 1-${MAX_PAGE_SIZE} (default 100)`, integerInRange(1, MAX_PAGE_SIZE))
|
|
21
|
+
.option('--page <n>', 'Page number, starting from 1', integerInRange(1))
|
|
22
|
+
.option('--pretty', 'Human-readable output');
|
|
23
|
+
}
|
|
24
|
+
export function pageParams(opts) {
|
|
25
|
+
const params = {};
|
|
26
|
+
if (opts.startAfter !== undefined)
|
|
27
|
+
params.start_after = opts.startAfter;
|
|
28
|
+
if (opts.endBefore !== undefined)
|
|
29
|
+
params.end_before = opts.endBefore;
|
|
30
|
+
if (opts.status !== undefined)
|
|
31
|
+
params.status = opts.status;
|
|
32
|
+
if (opts.limit !== undefined)
|
|
33
|
+
params.limit = opts.limit;
|
|
34
|
+
if (opts.page !== undefined)
|
|
35
|
+
params.page = opts.page;
|
|
36
|
+
return params;
|
|
37
|
+
}
|
|
38
|
+
export function printPage(result, pretty, line) {
|
|
39
|
+
if (!pretty)
|
|
40
|
+
return out(result);
|
|
41
|
+
if (result.items.length === 0)
|
|
42
|
+
outLine('No items.');
|
|
43
|
+
for (const item of result.items)
|
|
44
|
+
outLine(line(item));
|
|
45
|
+
outLine(`Page ${result.page} of ${result.total_pages} (${result.total} total)`);
|
|
46
|
+
}
|
package/dist/program.js
CHANGED
|
@@ -26,6 +26,7 @@ const GROUPS = {
|
|
|
26
26
|
},
|
|
27
27
|
search: async (p) => (await import('./commands/search.js')).registerSearch(p),
|
|
28
28
|
fetch: async (p) => (await import('./commands/fetch.js')).registerFetch(p),
|
|
29
|
+
monitor: async (p) => (await import('./commands/monitor.js')).registerMonitor(p),
|
|
29
30
|
};
|
|
30
31
|
/** Commander answers these before routing, so no command module is needed. */
|
|
31
32
|
const VERSION_FLAGS = ['-V', '--version'];
|