@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.
- package/dist/commands/connect.d.ts +2 -14
- package/dist/commands/connect.js +23 -56
- package/dist/commands/profile.d.ts +0 -3
- package/dist/commands/profile.js +7 -22
- package/dist/commands/run.js +20 -40
- package/dist/commands/runs.js +24 -58
- package/dist/lib/auth.d.ts +1 -3
- package/dist/lib/auth.js +1 -1
- package/dist/lib/claude-config.d.ts +0 -4
- package/dist/lib/claude-config.js +4 -4
- package/dist/lib/cli-install.d.ts +0 -1
- package/dist/lib/cli-install.js +0 -1
- package/dist/lib/client.js +82 -147
- package/dist/lib/connect-all-auth.js +1 -1
- package/dist/lib/connect-all.js +8 -27
- package/dist/lib/connect-clients.d.ts +3 -6
- package/dist/lib/connect-fallback.d.ts +2 -1
- package/dist/lib/connect-picker.d.ts +3 -2
- package/dist/lib/connect-runtime.js +4 -6
- package/dist/lib/harness-spec.d.ts +5 -34
- package/dist/lib/harness-spec.js +1 -30
- package/dist/lib/hermes-env.d.ts +0 -1
- package/dist/lib/hermes-env.js +1 -3
- package/dist/lib/hermes-plugin.js +1 -9
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +9 -0
- package/dist/lib/registration-detect.d.ts +0 -1
- package/dist/lib/registration-detect.js +36 -84
- package/dist/lib/setup-telemetry.d.ts +3 -7
- package/dist/lib/setup-telemetry.js +5 -5
- package/dist/lib/skill-install.js +1 -9
- package/dist/lib/types.d.ts +4 -3
- package/dist/program.js +2 -3
- package/package.json +1 -2
|
@@ -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
|
-
|
|
27
|
-
|
|
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;
|
package/dist/commands/connect.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}) => {
|
package/dist/commands/profile.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
});
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
opts.
|
|
191
|
-
opts.
|
|
192
|
-
opts.
|
|
193
|
-
opts.
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/dist/commands/runs.js
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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 (
|
|
207
|
+
if (timedOut.aborted) {
|
|
241
208
|
err({ error: `Watch timed out after ${timeoutMs}ms.` });
|
|
242
209
|
process.exit(1);
|
|
243
210
|
}
|
|
244
|
-
if (
|
|
245
|
-
return; // SIGINT handler already
|
|
246
|
-
}
|
|
211
|
+
if (controller.signal.aborted)
|
|
212
|
+
return; // SIGINT handler already exited 130
|
|
247
213
|
handleApiError(e);
|
|
248
214
|
}
|
|
249
215
|
finally {
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
function hasMarker(content) {
|
|
163
163
|
return content.includes(CLAUDE_MD_MARKER);
|
|
164
164
|
}
|
|
165
|
-
|
|
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;
|
package/dist/lib/cli-install.js
CHANGED
|
@@ -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 }
|