@tiny-fish/cli 0.48.3-next.391 → 0.49.0
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/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
|
}
|
|
@@ -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'];
|