@tiny-fish/cli 0.39.0 → 0.39.1-next.311

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.
@@ -23,20 +23,8 @@ export interface NativeConnectOptions {
23
23
  /** Targeted connects only: --all keeps its per-harness failed row. */
24
24
  fallbackWhenMissing?: boolean;
25
25
  }
26
- export declare function connectOpenClaw(options: {
27
- apiKey?: string;
28
- mcpUrl: string;
29
- launch: boolean;
30
- cliInstall?: boolean;
31
- attemptId?: string;
32
- /** Set by `--all`: the CLI install already ran once, upfront. */
33
- hoistedCliInstall?: TakeHoistedCliInstall;
34
- verbose?: boolean;
35
- /** Set by `--all`: repeated outro lines print once, after the summary. */
36
- deferOutro?: (line: string) => void;
37
- /** Targeted connects only: --all keeps its per-harness failed row. */
38
- fallbackWhenMissing?: boolean;
39
- }): Promise<void>;
26
+ /** Skill install only: no key handoff, sign-in wait, or post-install callback to honour. */
27
+ export declare function connectOpenClaw(options: Omit<NativeConnectOptions, 'keyAuthOnly' | 'authTimeoutMs' | 'onPostInstallFailed'>): Promise<void>;
40
28
  /** Writes mcp.json directly — no `cursor mcp add` exists. */
41
29
  export declare function connectCursor(options: {
42
30
  apiKey?: string;
@@ -1,11 +1,11 @@
1
1
  import spawn from 'cross-spawn';
2
2
  import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from '../lib/auth.js';
3
3
  import { CURSOR_SKILL_TARGET, OPENCLAW, openclawSkillInstallArgs, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
4
- import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from '../lib/cli-install.js';
4
+ import { captureStdio, capturedOutput, installTinyFishCli, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from '../lib/cli-install.js';
5
5
  import { ensureCliAuthenticated } from '../lib/connect-auth.js';
6
6
  import { createStdinPrompt, runCliFallback } from '../lib/connect-fallback.js';
7
7
  import { installWebSkill } from '../lib/skill-install.js';
8
- import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, stageDurationOf, withStageDuration, spawnRemoval, } from '../lib/connect-runtime.js';
8
+ import { ConnectInterruptedError, ConnectStepError, HoistedFailureReportedError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, stageDurationOf, withStageDuration, spawnRemoval, } from '../lib/connect-runtime.js';
9
9
  import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from '../lib/cursor-config.js';
10
10
  import { runConnectAll } from '../lib/connect-all.js';
11
11
  import { ompMcpPath, readOmpTinyfishEntry, writeOmpMcpConfig } from '../lib/omp-config.js';
@@ -62,14 +62,16 @@ function resolveKeyedAdd(client, storedKey, keyAuthSupported, deferOutro) {
62
62
  return keyed;
63
63
  }
64
64
  /** No sign-in to degrade to, so the install stops. */
65
- function refuseKeylessInstall(client, keyAuthSupported) {
66
- const noSignIn = `TinyFish does not sign in to ${client.displayName} any other way`;
65
+ function refuseKeylessInstall(client, keyAuthSupported, headless = false) {
66
+ const noSignIn = headless
67
+ ? 'signing in needs a terminal'
68
+ : `TinyFish does not sign in to ${client.displayName} any other way`;
67
69
  const rerun = `re-run: tinyfish connect ${client.connectClient}`;
68
70
  if (!client.keyAuth) {
69
71
  throw new ConnectStepError(`${client.displayName} cannot take a TinyFish API key, and ${noSignIn}. ` +
70
72
  `Run: tinyfish connect ${client.connectClient}`, 'key_auth_unsupported');
71
73
  }
72
- if (!keyAuthSupported) {
74
+ if (!keyAuthSupported || headless) {
73
75
  throw new ConnectStepError(`This ${client.displayName} install cannot take a TinyFish API key, and ${noSignIn}. ` +
74
76
  `Update it, then ${rerun}`, 'harness_too_old');
75
77
  }
@@ -159,15 +161,8 @@ async function prepareInstallAuth(client, options, keyAuthSupported) {
159
161
  const buildAddArgs = keyedAdd
160
162
  ? (mcpUrl) => keyedAdd.spec.addArgs(mcpUrl, keyedAdd.key)
161
163
  : keylessAddArgs(client, keyAuthSupported);
162
- if (options.keyAuthOnly && !keyedAdd) {
163
- // All native clients have keyAuth; missing flags mean an old install.
164
- if (client.keyAuth) {
165
- throw new ConnectStepError(`This ${client.displayName} install cannot take a TinyFish API key, and signing in ` +
166
- `needs a terminal. Update it, then re-run: tinyfish connect ${client.connectClient}`, 'harness_too_old');
167
- }
168
- throw new ConnectStepError(`${client.displayName} cannot take a TinyFish API key, and signing in needs a ` +
169
- `terminal. Run: tinyfish connect ${client.connectClient}`, 'key_auth_unsupported');
170
- }
164
+ if (options.keyAuthOnly && !keyedAdd)
165
+ refuseKeylessInstall(client, keyAuthSupported, true);
171
166
  // Verify first: a revoked key in a foreign .env is unrecoverable.
172
167
  if (keyedAdd?.spec.seedKey) {
173
168
  await verifyKeyBeforeSeeding(keyedAdd.key, client.displayName, options.mcpUrl);
@@ -447,6 +442,7 @@ function trackPostInstallFailure(displayName, state, telemetry, error) {
447
442
  errLine(`The ${displayName} MCP connection succeeded, but a finishing step ` +
448
443
  `(${state.stage}) failed: ${error instanceof Error ? error.message : String(error)}`);
449
444
  }
445
+ /** Skill install only: no key handoff, sign-in wait, or post-install callback to honour. */
450
446
  export async function connectOpenClaw(options) {
451
447
  const telemetry = createConnectTelemetry(options.mcpUrl, 'openclaw', options);
452
448
  // Skill install, no MCP server — never degraded, and false keeps it inside a `= false` filter.
@@ -462,15 +458,9 @@ export async function connectOpenClaw(options) {
462
458
  return;
463
459
  telemetry.setHarnessVersion(support.harnessVersion);
464
460
  telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
465
- if (options.hoistedCliInstall) {
466
- state.stage = 'cli_install';
467
- // A carried first-take failure fails openclaw like the inline install.
468
- takeHoistedCliInstall(options.hoistedCliInstall, telemetry);
469
- }
470
- else if (options.cliInstall !== false) {
461
+ if (options.cliInstall !== false) {
471
462
  state.stage = 'cli_install';
472
- installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
473
- telemetry.track('checkpoint', { phase: 'cli_installed' });
463
+ runCliInstallStep(options, telemetry);
474
464
  }
475
465
  state.stage = 'skill_install';
476
466
  errLine('Installing the TinyFish skill in OpenClaw...');
@@ -707,17 +697,8 @@ export async function connectHarness(harness, options) {
707
697
  }
708
698
  function parseMcpUrlOption(options) {
709
699
  let mcpUrl = options.url ?? DEFAULT_MCP_URL;
710
- if (mcpUrl.startsWith('-')) {
711
- throw new Error(`Invalid --url value: ${mcpUrl}`);
712
- }
713
- let parsedUrl;
714
- try {
715
- parsedUrl = new URL(mcpUrl);
716
- }
717
- catch {
718
- throw new Error(`Invalid --url value: ${mcpUrl}`);
719
- }
720
- if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
700
+ const parsedUrl = URL.parse(mcpUrl);
701
+ if (mcpUrl.startsWith('-') || !parsedUrl || !['http:', 'https:'].includes(parsedUrl.protocol)) {
721
702
  throw new Error(`Invalid --url value: ${mcpUrl}`);
722
703
  }
723
704
  // Page-minted attempt id rides in on --url. Strip before the URL becomes the MCP
@@ -732,10 +713,16 @@ function parseMcpUrlOption(options) {
732
713
  }
733
714
  return { mcpUrl, attemptId };
734
715
  }
735
- async function connectAllPath(options, mcpUrl, attemptId) {
716
+ /** Bare `connect` offers the picker; `--all` takes the dry-run/uninstall/skip-launch flags. */
717
+ async function connectManyPath(options, mcpUrl, attemptId) {
736
718
  if (options.launch) {
737
719
  throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
738
720
  }
721
+ const pick = !options.all;
722
+ const humanInitiated = pick && detectHumanInitiated();
723
+ if (pick && !humanInitiated) {
724
+ errLine('No terminal detected: connecting every detected harness (same as tinyfish connect --all).');
725
+ }
739
726
  const exitCode = await runConnectAll({
740
727
  apiKey: options.apiKey,
741
728
  mcpUrl,
@@ -743,26 +730,6 @@ async function connectAllPath(options, mcpUrl, attemptId) {
743
730
  dryRun: options.dryRun ?? false,
744
731
  uninstall: options.uninstall ?? false,
745
732
  noLaunch: options.skipLaunch === true,
746
- verbose: options.verbose ?? false,
747
- });
748
- // Preserve the 130 runGuarded set on Ctrl+C.
749
- if (!process.exitCode)
750
- process.exitCode = exitCode;
751
- }
752
- async function connectPickPath(options, mcpUrl, attemptId) {
753
- if (options.launch) {
754
- throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
755
- }
756
- const humanInitiated = detectHumanInitiated();
757
- if (!humanInitiated) {
758
- errLine('No terminal detected: connecting every detected harness (same as tinyfish connect --all).');
759
- }
760
- const exitCode = await runConnectAll({
761
- apiKey: options.apiKey,
762
- mcpUrl,
763
- attemptId,
764
- dryRun: false,
765
- uninstall: false,
766
733
  pick: humanInitiated,
767
734
  verbose: options.verbose ?? false,
768
735
  });
@@ -820,14 +787,14 @@ export function registerConnect(program) {
820
787
  if (client) {
821
788
  throw new Error('Pass either a client or --all, not both.');
822
789
  }
823
- await connectAllPath(options, mcpUrl, attemptId);
790
+ await connectManyPath(options, mcpUrl, attemptId);
824
791
  return;
825
792
  }
826
793
  if (options.dryRun || options.uninstall || options.skipLaunch) {
827
794
  throw new Error('--dry-run, --uninstall and --skip-launch are only supported with --all.');
828
795
  }
829
796
  if (!client) {
830
- await connectPickPath(options, mcpUrl, attemptId);
797
+ await connectManyPath(options, mcpUrl, attemptId);
831
798
  return;
832
799
  }
833
800
  await connectSingleClient(client, options, mcpUrl, attemptId);
@@ -1,8 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { type DecryptStats } from '../lib/chrome-cookies.js';
3
2
  import type { CookieRecord, ProfileUploadResponse } from '../lib/types.js';
4
- export declare function extractChromeCookies(domain: string, stats?: DecryptStats): CookieRecord[];
5
- export declare function extractAllChromeCookies(stats?: DecryptStats): CookieRecord[];
6
3
  type TldtsParse = (host: string, opts: {
7
4
  allowPrivateDomains: boolean;
8
5
  }) => {
@@ -6,15 +6,6 @@ import { getApiKey } from '../lib/auth.js';
6
6
  import { deriveChromeKey, listAuthRelevantHostKeys, openCookiesDb, readCookiesForHost, } from '../lib/chrome-cookies.js';
7
7
  import { profileCreate, profileUpload } from '../lib/client.js';
8
8
  import { err, errLine, handleApiError, out, outLine } from '../lib/output.js';
9
- // ── UserFacingError ───────────────────────────────────────────────────────────
10
- // Thrown by helpers to signal a terminal error that should be printed and exit 1
11
- // without going through handleApiError (which would print a second error line).
12
- class UserFacingError extends Error {
13
- constructor(message) {
14
- super(message);
15
- this.name = 'UserFacingError';
16
- }
17
- }
18
9
  // ── Helpers ───────────────────────────────────────────────────────────────────
19
10
  function getChromeCookiesPath() {
20
11
  const platform = process.platform;
@@ -36,11 +27,11 @@ function validateDomain(domain) {
36
27
  // Opens a private copy of Chrome's cookie DB, derives the decryption key, and calls fn.
37
28
  function withCookiesDb(fn) {
38
29
  if (process.platform === 'win32') {
39
- throw new UserFacingError('Cookie extraction is not supported on Windows yet');
30
+ throw new Error('Cookie extraction is not supported on Windows yet');
40
31
  }
41
32
  const cookiesPath = getChromeCookiesPath();
42
33
  if (!fs.existsSync(cookiesPath)) {
43
- throw new UserFacingError(`Chrome cookies file not found at ${cookiesPath}. Is Google Chrome installed?`);
34
+ throw new Error(`Chrome cookies file not found at ${cookiesPath}. Is Google Chrome installed?`);
44
35
  }
45
36
  // Copy the DB out of the profile directory so we avoid the Chrome file lock.
46
37
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tinyfish-chrome-cookies-'));
@@ -57,7 +48,7 @@ function withCookiesDb(fn) {
57
48
  db = openCookiesDb(dbPath);
58
49
  }
59
50
  catch (e) {
60
- throw new UserFacingError(`Failed to read Chrome cookies database: ${e instanceof Error ? e.message : String(e)}`);
51
+ throw new Error(`Failed to read Chrome cookies database: ${e instanceof Error ? e.message : String(e)}`);
61
52
  }
62
53
  return fn(db, deriveChromeKey());
63
54
  }
@@ -76,19 +67,17 @@ function withCookiesDb(fn) {
76
67
  }
77
68
  }
78
69
  }
79
- // Exported for testing
80
- export function extractChromeCookies(domain, stats) {
70
+ function extractChromeCookies(domain, stats) {
81
71
  return withCookiesDb((db, key) => readCookiesForHost(db, key, domain, stats));
82
72
  }
83
- // Exported for testing
84
- export function extractAllChromeCookies(stats) {
73
+ function extractAllChromeCookies(stats) {
85
74
  return withCookiesDb((db, key) => {
86
75
  let hostKeys;
87
76
  try {
88
77
  hostKeys = listAuthRelevantHostKeys(db);
89
78
  }
90
79
  catch (e) {
91
- throw new UserFacingError(`Failed to read Chrome cookies database: ${e instanceof Error ? e.message : String(e)}`);
80
+ throw new Error(`Failed to read Chrome cookies database: ${e instanceof Error ? e.message : String(e)}`);
92
81
  }
93
82
  // Canonicalize host_keys → registrable domains (mirrors frontend/app/lib/services/domain-utils.ts)
94
83
  const require = createRequire(import.meta.url);
@@ -181,7 +170,7 @@ function extractCookiesOrThrow(opts) {
181
170
  errLine(`Skipped ${stats.keyringSkipped} keyring-encrypted cookies (not supported on Linux yet).`);
182
171
  }
183
172
  if (cookies.length === 0) {
184
- throw new UserFacingError(stats.keyringSkipped > 0
173
+ throw new Error(stats.keyringSkipped > 0
185
174
  ? 'Chrome on Linux encrypts cookies with the system keyring, which is not supported yet.'
186
175
  : opts.allDomains
187
176
  ? 'No Chrome cookies found. Is Google Chrome installed and have you logged in to any sites?'
@@ -281,10 +270,6 @@ export function registerProfile(program) {
281
270
  }
282
271
  }
283
272
  catch (error) {
284
- if (error instanceof UserFacingError) {
285
- err({ error: error.message });
286
- process.exit(1);
287
- }
288
273
  handleApiError(error);
289
274
  }
290
275
  });
@@ -172,34 +172,26 @@ function buildAgentConfig(opts) {
172
172
  const cursorStyle = parseChoice(opts.cursorStyle, VALID_CURSOR_STYLES, '--cursor-style');
173
173
  const maxSteps = parsePositiveInteger(opts.maxSteps, '--max-steps', MAX_STEPS_LIMIT);
174
174
  const maxDurationSeconds = parsePositiveInteger(opts.maxDurationSeconds, '--max-duration-seconds');
175
- if (mode === undefined &&
176
- cursorStyle === undefined &&
177
- maxSteps === undefined &&
178
- maxDurationSeconds === undefined) {
179
- return undefined;
180
- }
181
- return {
182
- ...(mode !== undefined ? { mode } : {}),
183
- ...(cursorStyle !== undefined ? { cursor_style: cursorStyle } : {}),
184
- ...(maxSteps !== undefined ? { max_steps: maxSteps } : {}),
185
- ...(maxDurationSeconds !== undefined ? { max_duration_seconds: maxDurationSeconds } : {}),
186
- };
175
+ return compact({
176
+ mode,
177
+ cursor_style: cursorStyle,
178
+ max_steps: maxSteps,
179
+ max_duration_seconds: maxDurationSeconds,
180
+ });
181
+ }
182
+ /** Drops undefined fields; undefined when nothing is left. */
183
+ function compact(obj) {
184
+ const kept = Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
185
+ return Object.keys(kept).length ? kept : undefined;
187
186
  }
188
187
  function buildCaptureConfig(opts) {
189
- if (opts.captureElements === undefined &&
190
- opts.captureSnapshots === undefined &&
191
- opts.captureScreenshots === undefined &&
192
- opts.captureRecording === undefined &&
193
- opts.captureHtml === undefined) {
194
- return undefined;
195
- }
196
- return {
197
- ...(opts.captureElements !== undefined ? { elements: opts.captureElements } : {}),
198
- ...(opts.captureSnapshots !== undefined ? { snapshots: opts.captureSnapshots } : {}),
199
- ...(opts.captureScreenshots !== undefined ? { screenshots: opts.captureScreenshots } : {}),
200
- ...(opts.captureRecording !== undefined ? { recording: opts.captureRecording } : {}),
201
- ...(opts.captureHtml !== undefined ? { html: opts.captureHtml } : {}),
202
- };
188
+ return compact({
189
+ elements: opts.captureElements,
190
+ snapshots: opts.captureSnapshots,
191
+ screenshots: opts.captureScreenshots,
192
+ recording: opts.captureRecording,
193
+ html: opts.captureHtml,
194
+ });
203
195
  }
204
196
  async function buildRunRequest(goal, normalizedUrl, opts) {
205
197
  const outputSchema = await loadOutputSchema(opts);
@@ -224,13 +216,7 @@ async function buildRunRequest(goal, normalizedUrl, opts) {
224
216
  };
225
217
  }
226
218
  async function runAsyncPath(req, apiKey, pretty) {
227
- let result;
228
- try {
229
- result = await runAsync(req, apiKey);
230
- }
231
- catch (e) {
232
- handleApiError(e);
233
- }
219
+ const result = await runAsync(req, apiKey).catch(handleApiError);
234
220
  checkRunResult(result, false); // async = PENDING is expected
235
221
  const asyncResultWithUrl = {
236
222
  ...result,
@@ -246,13 +232,7 @@ async function runAsyncPath(req, apiKey, pretty) {
246
232
  }
247
233
  async function runSyncPath(req, apiKey, pretty) {
248
234
  errLine('Waiting for result...');
249
- let result;
250
- try {
251
- result = await runSync(req, apiKey);
252
- }
253
- catch (e) {
254
- handleApiError(e);
255
- }
235
+ const result = await runSync(req, apiKey).catch(handleApiError);
256
236
  checkRunResult(result);
257
237
  const syncResultWithUrl = {
258
238
  ...result,
@@ -1,3 +1,4 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
1
2
  import { RunStatus } from '@tiny-fish/sdk';
2
3
  import { getApiKey } from '../lib/auth.js';
3
4
  import { cancelRun, getRun, getRunSteps, listRuns } from '../lib/client.js';
@@ -16,6 +17,7 @@ function formatStep(step, index) {
16
17
  const meta = [step.status, step.duration].filter(Boolean).join(', ');
17
18
  return `${String(index + 1).padStart(2, ' ')}. ${action}${meta ? ` (${meta})` : ''}`;
18
19
  }
20
+ const MAX_TIMER_MS = 2 ** 31 - 1;
19
21
  function parsePositiveInt(value, flag, defaultMs, minMs = 1) {
20
22
  if (value === undefined)
21
23
  return defaultMs;
@@ -27,56 +29,24 @@ function parsePositiveInt(value, flag, defaultMs, minMs = 1) {
27
29
  process.exit(1);
28
30
  }
29
31
  const parsed = Number(value);
30
- if (parsed < minMs) {
31
- err({ error: `Invalid ${flag} "${value}". Must be an integer >= ${minMs}ms.` });
32
+ // Node timers take a signed 32-bit delay; larger overflows to 1ms or throws.
33
+ if (parsed < minMs || parsed > MAX_TIMER_MS) {
34
+ err({
35
+ error: `Invalid ${flag} "${value}". Must be an integer between ${minMs} and ${MAX_TIMER_MS}ms.`,
36
+ });
32
37
  process.exit(1);
33
38
  }
34
39
  return parsed;
35
40
  }
36
- function withDeadline(promise, deadline, signal) {
37
- return new Promise((resolve, reject) => {
38
- const remaining = deadline - Date.now();
39
- if (remaining <= 0) {
40
- reject(new Error('watch deadline exceeded'));
41
- return;
42
- }
43
- if (signal.aborted) {
44
- reject(new Error('watch aborted'));
45
- return;
46
- }
47
- const timer = setTimeout(() => reject(new Error('watch deadline exceeded')), remaining);
48
- const onAbort = () => {
49
- clearTimeout(timer);
50
- reject(new Error('watch aborted'));
51
- };
52
- signal.addEventListener('abort', onAbort, { once: true });
53
- promise.then((v) => {
54
- clearTimeout(timer);
55
- signal.removeEventListener('abort', onAbort);
56
- resolve(v);
57
- }, (e) => {
58
- clearTimeout(timer);
59
- signal.removeEventListener('abort', onAbort);
60
- reject(e);
61
- });
62
- });
63
- }
64
- function sleep(ms, signal) {
65
- return new Promise((resolve, reject) => {
66
- if (signal.aborted) {
67
- reject(new Error('aborted'));
68
- return;
69
- }
70
- const timer = setTimeout(() => {
71
- signal.removeEventListener('abort', onAbort);
72
- resolve();
73
- }, ms);
74
- const onAbort = () => {
75
- clearTimeout(timer);
76
- reject(new Error('aborted'));
77
- };
78
- signal.addEventListener('abort', onAbort, { once: true });
41
+ /** The SDK calls take no signal, so the race is the only cancel. */
42
+ function withSignal(promise, signal) {
43
+ if (signal.aborted)
44
+ return Promise.reject(signal.reason);
45
+ const done = new AbortController();
46
+ const aborted = new Promise((_, reject) => {
47
+ signal.addEventListener('abort', () => reject(signal.reason), { signal: done.signal });
79
48
  });
49
+ return Promise.race([promise, aborted]).finally(() => done.abort());
80
50
  }
81
51
  async function runsListAction(opts) {
82
52
  // Validate --status
@@ -193,18 +163,20 @@ async function runsWatchAction(runId, opts) {
193
163
  };
194
164
  process.once('SIGINT', onSigint);
195
165
  const deadline = Date.now() + timeoutMs;
166
+ const timedOut = AbortSignal.timeout(timeoutMs);
167
+ const signal = AbortSignal.any([controller.signal, timedOut]);
196
168
  const seen = new Set();
197
169
  try {
198
- const initial = await withDeadline(getRun(runId, apiKey), deadline, controller.signal);
170
+ const initial = await withSignal(getRun(runId, apiKey), signal);
199
171
  if (TERMINAL_STATUSES.includes(initial.status)) {
200
172
  err({
201
173
  error: `Run ${runId} is already ${initial.status}; nothing to watch. Use 'tinyfish agent run steps ${runId}' for the trace.`,
202
174
  });
203
175
  process.exit(1);
204
176
  }
205
- while (!controller.signal.aborted) {
177
+ while (!signal.aborted) {
206
178
  // One API call per tick — the steps endpoint also returns the run's current status.
207
- const tick = await withDeadline(getRunSteps(runId, apiKey), deadline, controller.signal);
179
+ const tick = await withSignal(getRunSteps(runId, apiKey), signal);
208
180
  let nextIndex = seen.size;
209
181
  for (const step of tick.steps) {
210
182
  if (seen.has(step.id))
@@ -228,22 +200,16 @@ async function runsWatchAction(runId, opts) {
228
200
  err({ error: `Watch timed out after ${timeoutMs}ms (run still ${tick.status}).` });
229
201
  process.exit(1);
230
202
  }
231
- try {
232
- await sleep(intervalMs, controller.signal);
233
- }
234
- catch {
235
- return; // aborted
236
- }
203
+ await sleep(intervalMs, undefined, { signal });
237
204
  }
238
205
  }
239
206
  catch (e) {
240
- if (e instanceof Error && e.message === 'watch deadline exceeded') {
207
+ if (timedOut.aborted) {
241
208
  err({ error: `Watch timed out after ${timeoutMs}ms.` });
242
209
  process.exit(1);
243
210
  }
244
- if (e instanceof Error && e.message === 'watch aborted') {
245
- return; // SIGINT handler already triggered process.exit(130)
246
- }
211
+ if (controller.signal.aborted)
212
+ return; // SIGINT handler already exited 130
247
213
  handleApiError(e);
248
214
  }
249
215
  finally {
@@ -6,7 +6,7 @@ export declare const CONNECT_SOURCE = "tinyfish_cli";
6
6
  export declare const CONNECT_ATTEMPT_ENV = "TINYFISH_CONNECT_ATTEMPT_ID";
7
7
  /** Mirrors ConnectAuthMode; duplicated to keep auth.ts free of connect-runtime imports. */
8
8
  export type RecordedAuthMode = 'api-key' | 'oauth' | 'deferred';
9
- export interface ConnectEntry {
9
+ interface ConnectEntry {
10
10
  attempt_id: string;
11
11
  auth_mode?: RecordedAuthMode;
12
12
  }
@@ -43,8 +43,6 @@ interface EnvironmentOptions {
43
43
  }
44
44
  export declare function persistApiKeyToEnvironment(apiKey: string, options?: EnvironmentOptions): void;
45
45
  export declare function clearConfig(): boolean;
46
- /** The one rule that is ours to enforce: the key becomes an HTTP header, so CR/LF cannot pass. */
47
- export declare function isHeaderSafe(key: string): boolean;
48
46
  export declare function validateKeyFormat(key: string): boolean;
49
47
  export declare function maskKey(key: string): string;
50
48
  export type KeySource = 'explicit' | 'env' | 'config' | 'none';
package/dist/lib/auth.js CHANGED
@@ -236,7 +236,7 @@ export function clearConfig() {
236
236
  }
237
237
  }
238
238
  /** The one rule that is ours to enforce: the key becomes an HTTP header, so CR/LF cannot pass. */
239
- export function isHeaderSafe(key) {
239
+ function isHeaderSafe(key) {
240
240
  for (let i = 0; i < key.length; i += 1) {
241
241
  if (key.charCodeAt(i) < 0x20)
242
242
  return false;
@@ -1,17 +1,13 @@
1
- export declare function claudeDir(): string;
2
1
  export declare function claudeSettingsPath(): string;
3
2
  export declare function claudeMdPath(): string;
4
3
  export declare const TINYFISH_PERMISSION = "Bash(tinyfish:*)";
5
4
  export declare const CLAUDE_MD_MARKER = "<!-- tinyfish-configure-claude -->";
6
- export declare const CLAUDE_MD_BLOCK = "<!-- tinyfish-configure-claude -->\n## WebSearch or Search\nWhen looking to use Search or WebSearch, always use the TinyFish Web Search tool, run `tinyfish search query \"<query>\"`\nOnly fall back to native Search when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)\n\n## WebFetch or Fetch\nWhen looking to use Fetch or WebFetch, always use the TinyFish Fetch tool, run `tinyfish fetch content get \"<url>\"`\nOnly fall back to native Fetch when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)\n<!-- tinyfish-configure-claude -->";
7
5
  export declare function readSettingsJson(): Record<string, unknown>;
8
6
  export declare function writeSettingsJson(settings: Record<string, unknown>): void;
9
7
  export declare function mergeSettings(existing: Record<string, unknown>): Record<string, unknown>;
10
8
  export declare function removeFromSettings(existing: Record<string, unknown>): Record<string, unknown>;
11
9
  export declare function readClaudeMd(): string;
12
10
  export declare function writeClaudeMd(content: string): void;
13
- export declare function hasMarker(content: string): boolean;
14
- export declare function hasLegacyBlock(content: string): boolean;
15
11
  export declare function mergeClaudeMd(existing: string): {
16
12
  content: string;
17
13
  replaced: boolean;
@@ -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
  // ─── Path helpers ─────────────────────────────────────────────────────────────
5
- export function claudeDir() {
5
+ function claudeDir() {
6
6
  return path.join(os.homedir(), '.claude');
7
7
  }
8
8
  export function claudeSettingsPath() {
@@ -40,7 +40,7 @@ const TINYFISH_SUBAGENT_HOOK = {
40
40
  ],
41
41
  };
42
42
  export const CLAUDE_MD_MARKER = '<!-- tinyfish-configure-claude -->';
43
- export const CLAUDE_MD_BLOCK = `${CLAUDE_MD_MARKER}
43
+ const CLAUDE_MD_BLOCK = `${CLAUDE_MD_MARKER}
44
44
  ## WebSearch or Search
45
45
  When looking to use Search or WebSearch, always use the TinyFish Web Search tool, run \`tinyfish search query "<query>"\`
46
46
  Only fall back to native Search when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)
@@ -159,10 +159,10 @@ function escapeRegExp(s) {
159
159
  }
160
160
  const MARKER_REGEX = new RegExp(`${escapeRegExp(CLAUDE_MD_MARKER)}[\\s\\S]*?${escapeRegExp(CLAUDE_MD_MARKER)}`, 'm');
161
161
  const LEGACY_REGEX = /## Web(?:Search|Seach) or Search[\s\S]*?(?:tinyfish fetch[^\n]*\n(?:[^\n]*tinyfish[^\n]*\n)*[^\n]*)\n*/m;
162
- export function hasMarker(content) {
162
+ function hasMarker(content) {
163
163
  return content.includes(CLAUDE_MD_MARKER);
164
164
  }
165
- export function hasLegacyBlock(content) {
165
+ function hasLegacyBlock(content) {
166
166
  return !content.includes(CLAUDE_MD_MARKER) && LEGACY_REGEX.test(content);
167
167
  }
168
168
  export function mergeClaudeMd(existing) {
@@ -20,7 +20,6 @@ export type TakeHoistedCliInstall = () => {
20
20
  outcome: HoistedCliInstall;
21
21
  firstTake: boolean;
22
22
  };
23
- export { HoistedFailureReportedError } from './connect-runtime.js';
24
23
  export declare function captureStdio(verbose: boolean): {
25
24
  stdio: "inherit";
26
25
  timeout: number;
@@ -7,7 +7,6 @@ export const SKILL_INSTALL_TIMEOUT_MS = 120_000;
7
7
  const TINYFISH_CLI_INSTALL_SPEC = `${TINYFISH_CLI_PACKAGE}@latest`;
8
8
  // spawnSync caps piped output at 1 MiB by default.
9
9
  export const STEP_MAX_BUFFER = 10 * 1024 * 1024;
10
- export { HoistedFailureReportedError } from './connect-runtime.js';
11
10
  export function captureStdio(verbose) {
12
11
  return verbose
13
12
  ? { stdio: 'inherit', timeout: SKILL_INSTALL_TIMEOUT_MS }