@tiny-fish/cli 0.48.3-next.391 → 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 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.
@@ -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
  }
@@ -1,23 +1,12 @@
1
- import { InvalidArgumentError } from 'commander';
2
- import { getApiKey } from '../lib/auth.js';
3
- import { fetchContentGet } from '../lib/client.js';
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,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerMonitor(program: Command): void;
@@ -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
+ }
@@ -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(async (opts) => {
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(async (opts) => {
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
  }
@@ -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
  }
@@ -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') {
@@ -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
- export declare function profileCreate(name: string, apiKey: string): Promise<ProfileCreateResponse>;
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<ProfileSummary[]>;
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>;
@@ -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
- // The SDK base client only exposes get/post/postStream; DELETE is needed for
88
- // disconnecting a vault connection. ponytail: raw fetch, no retry/timeout —
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: 'DELETE',
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
- let message = response.statusText;
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.object({
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.object({ profiles: z.array(profileSummarySchema) });
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(name, apiKey) {
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.del(`/v1/vault/connections/${encodeURIComponent(connectionId)}`);
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'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.48.3-next.391",
3
+ "version": "0.48.3-next.392",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {