@tiny-fish/cli 0.39.1-next.309 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/connect.d.ts +2 -14
- package/dist/commands/connect.js +109 -75
- package/dist/commands/doctor.js +3 -1
- 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-summary.js +4 -1
- package/dist/lib/connect-all-uninstall.js +12 -1
- package/dist/lib/connect-all.d.ts +4 -2
- package/dist/lib/connect-all.js +21 -32
- package/dist/lib/connect-clients.d.ts +11 -8
- package/dist/lib/connect-clients.js +53 -13
- 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/doctor-checks.d.ts +1 -0
- package/dist/lib/doctor-checks.js +38 -3
- package/dist/lib/doctor-report.d.ts +6 -0
- package/dist/lib/harness-detect.d.ts +2 -0
- package/dist/lib/harness-detect.js +11 -3
- package/dist/lib/harness-spec.d.ts +20 -39
- package/dist/lib/harness-spec.js +12 -31
- package/dist/lib/harness.js +2 -0
- package/dist/lib/hermes-config.d.ts +3 -0
- package/dist/lib/hermes-config.js +8 -4
- package/dist/lib/hermes-env.d.ts +7 -2
- package/dist/lib/hermes-env.js +7 -6
- package/dist/lib/hermes-plugin.d.ts +4 -0
- package/dist/lib/hermes-plugin.js +17 -9
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +9 -0
- package/dist/lib/pi-config.d.ts +26 -0
- package/dist/lib/pi-config.js +111 -0
- package/dist/lib/registration-detect.d.ts +0 -1
- package/dist/lib/registration-detect.js +60 -81
- package/dist/lib/setup-telemetry.d.ts +13 -7
- package/dist/lib/setup-telemetry.js +5 -5
- package/dist/lib/skill-install.d.ts +1 -0
- package/dist/lib/skill-install.js +43 -44
- package/dist/lib/types.d.ts +4 -3
- package/dist/program.js +2 -3
- package/package.json +1 -2
|
@@ -8,9 +8,19 @@ import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
|
|
|
8
8
|
import { errLine } from './output.js';
|
|
9
9
|
import { readCursorTinyfishEntry } from './cursor-config.js';
|
|
10
10
|
import { readOmpTinyfishEntry } from './omp-config.js';
|
|
11
|
+
import { readPiTinyfishEntry } from './pi-config.js';
|
|
11
12
|
import { readHermesKey, resolveHermesHome } from './hermes-env.js';
|
|
12
13
|
import { readHermesEntry } from './hermes-config.js';
|
|
13
14
|
import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMode, Registered, } from './harness-detect.js';
|
|
15
|
+
const NOT_REGISTERED = Object.freeze({
|
|
16
|
+
registered: Registered.No,
|
|
17
|
+
authMode: AuthMode.Unknown,
|
|
18
|
+
});
|
|
19
|
+
const unverified = (reason) => ({
|
|
20
|
+
registered: Registered.Unknown,
|
|
21
|
+
authMode: AuthMode.Unknown,
|
|
22
|
+
reason,
|
|
23
|
+
});
|
|
14
24
|
// A list, verified against codex 0.146. A name-keyed map was accepted here too, but every
|
|
15
25
|
// field is optional, so that arm parsed *any* object-of-objects: a wrapper like
|
|
16
26
|
// `{"servers": {...}}` decoded as one entry named `servers` and reported TinyFish absent.
|
|
@@ -151,15 +161,11 @@ function parseMcpGet(output) {
|
|
|
151
161
|
function fromMcpGet(command) {
|
|
152
162
|
const probe = runProbe(command, ['mcp', 'get', 'tinyfish']);
|
|
153
163
|
if (probe.outcome === 'unavailable') {
|
|
154
|
-
return
|
|
164
|
+
return unverified(probe.reason);
|
|
155
165
|
}
|
|
156
166
|
if (probe.exitCode !== 0) {
|
|
157
167
|
if (UNSUPPORTED_SUBCOMMAND.test(probe.output)) {
|
|
158
|
-
return {
|
|
159
|
-
registered: Registered.Unknown,
|
|
160
|
-
authMode: AuthMode.Unknown,
|
|
161
|
-
reason: `\`${command} mcp get\` is unsupported by this version`,
|
|
162
|
-
};
|
|
168
|
+
return unverified(`\`${command} mcp get\` is unsupported by this version`);
|
|
163
169
|
}
|
|
164
170
|
// The roster is free; `mcp list` health-checks every server.
|
|
165
171
|
const scoped = tinyfishServerName(probe.output);
|
|
@@ -178,13 +184,9 @@ function fromMcpGet(command) {
|
|
|
178
184
|
return plugin;
|
|
179
185
|
// A broken CLI also exits nonzero, and reading that as absence earns a spurious repair.
|
|
180
186
|
if (!NO_SUCH_SERVER.test(probe.output)) {
|
|
181
|
-
return {
|
|
182
|
-
registered: Registered.Unknown,
|
|
183
|
-
authMode: AuthMode.Unknown,
|
|
184
|
-
reason: `\`${command} mcp get\` failed without reporting the server as absent`,
|
|
185
|
-
};
|
|
187
|
+
return unverified(`\`${command} mcp get\` failed without reporting the server as absent`);
|
|
186
188
|
}
|
|
187
|
-
return
|
|
189
|
+
return NOT_REGISTERED;
|
|
188
190
|
}
|
|
189
191
|
return parseMcpGet(probe.output);
|
|
190
192
|
}
|
|
@@ -218,7 +220,7 @@ function readCodexEntry() {
|
|
|
218
220
|
}
|
|
219
221
|
// `mcp add` exits 0 whether or not its inline OAuth finished, so `not_logged_in` is the only
|
|
220
222
|
// evidence of an abandoned sign-in; every other state, older Codex included, is unknown.
|
|
221
|
-
|
|
223
|
+
function codexOauthCompleted() {
|
|
222
224
|
const read = readCodexEntry();
|
|
223
225
|
if ('reason' in read)
|
|
224
226
|
return undefined;
|
|
@@ -255,10 +257,10 @@ function envKeyVerdict(envVar) {
|
|
|
255
257
|
function probeCodex() {
|
|
256
258
|
const read = readCodexEntry();
|
|
257
259
|
if ('reason' in read)
|
|
258
|
-
return
|
|
260
|
+
return unverified(read.reason);
|
|
259
261
|
const entry = read.entry;
|
|
260
262
|
if (!entry)
|
|
261
|
-
return
|
|
263
|
+
return NOT_REGISTERED;
|
|
262
264
|
const codexUrl = entry.transport?.url;
|
|
263
265
|
// Read from this entry: codex emits `bearer_token_env_var` on every HTTP server, so testing
|
|
264
266
|
// the whole payload reports api-key for TinyFish because some other server carries a key.
|
|
@@ -279,14 +281,10 @@ function probeCodex() {
|
|
|
279
281
|
function probeCursor() {
|
|
280
282
|
const entry = readCursorTinyfishEntry();
|
|
281
283
|
if (entry.error) {
|
|
282
|
-
return
|
|
283
|
-
registered: Registered.Unknown,
|
|
284
|
-
authMode: AuthMode.Unknown,
|
|
285
|
-
reason: 'mcp.json exists but could not be read or parsed',
|
|
286
|
-
};
|
|
284
|
+
return unverified('mcp.json exists but could not be read or parsed');
|
|
287
285
|
}
|
|
288
286
|
if (!entry.present)
|
|
289
|
-
return
|
|
287
|
+
return NOT_REGISTERED;
|
|
290
288
|
return {
|
|
291
289
|
registered: Registered.Yes,
|
|
292
290
|
authMode: entry.hasApiKeyHeader ? AuthMode.ApiKey : AuthMode.Unknown,
|
|
@@ -298,12 +296,29 @@ function probeCursor() {
|
|
|
298
296
|
function probeOmp() {
|
|
299
297
|
const entry = readOmpTinyfishEntry();
|
|
300
298
|
if (!entry) {
|
|
301
|
-
return
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
};
|
|
299
|
+
return unverified('`omp config path` did not report a config directory');
|
|
300
|
+
}
|
|
301
|
+
if (entry.error) {
|
|
302
|
+
return unverified('mcp.json exists but could not be read or parsed');
|
|
306
303
|
}
|
|
304
|
+
if (!entry.present)
|
|
305
|
+
return NOT_REGISTERED;
|
|
306
|
+
// A hand-written `${VAR}` header resolves like Codex's env-var key.
|
|
307
|
+
const keyVerdict = entry.keyTemplateVar
|
|
308
|
+
? envKeyVerdict(entry.keyTemplateVar)
|
|
309
|
+
: entry.keyMatchesCliKey
|
|
310
|
+
? { keyMatchesCliKey: true }
|
|
311
|
+
: {};
|
|
312
|
+
return {
|
|
313
|
+
registered: Registered.Yes,
|
|
314
|
+
authMode: entry.hasApiKeyHeader ? AuthMode.ApiKey : AuthMode.Unknown,
|
|
315
|
+
...(entry.url ? { registeredUrl: entry.url } : {}),
|
|
316
|
+
...keyVerdict,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// Whether anything reads the entry is the pi-adapter check's question, not this one.
|
|
320
|
+
function probePi() {
|
|
321
|
+
const entry = readPiTinyfishEntry();
|
|
307
322
|
if (entry.error) {
|
|
308
323
|
return {
|
|
309
324
|
registered: Registered.Unknown,
|
|
@@ -313,7 +328,6 @@ function probeOmp() {
|
|
|
313
328
|
}
|
|
314
329
|
if (!entry.present)
|
|
315
330
|
return { registered: Registered.No, authMode: AuthMode.Unknown };
|
|
316
|
-
// A hand-written `${VAR}` header resolves like Codex's env-var key.
|
|
317
331
|
const keyVerdict = entry.keyTemplateVar
|
|
318
332
|
? envKeyVerdict(entry.keyTemplateVar)
|
|
319
333
|
: entry.keyMatchesCliKey
|
|
@@ -342,34 +356,23 @@ function fromMcpList(command, output, registeredMode, urlPattern) {
|
|
|
342
356
|
};
|
|
343
357
|
}
|
|
344
358
|
if (!MCP_LIST_HEADER.test(output)) {
|
|
345
|
-
return {
|
|
346
|
-
registered: Registered.Unknown,
|
|
347
|
-
authMode: AuthMode.Unknown,
|
|
348
|
-
reason: `could not interpret \`${command} mcp list\` output`,
|
|
349
|
-
};
|
|
359
|
+
return unverified(`could not interpret \`${command} mcp list\` output`);
|
|
350
360
|
}
|
|
351
|
-
return
|
|
361
|
+
return NOT_REGISTERED;
|
|
352
362
|
}
|
|
353
363
|
// `hermes mcp list` only pretty-prints this file, so connect's gate reads it too.
|
|
354
364
|
function probeHermes() {
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
return
|
|
358
|
-
registered: Registered.Unknown,
|
|
359
|
-
authMode: AuthMode.Unknown,
|
|
360
|
-
reason: '`hermes dump` did not report a home directory',
|
|
361
|
-
};
|
|
365
|
+
const resolved = resolveHermesHome();
|
|
366
|
+
if (typeof resolved !== 'string') {
|
|
367
|
+
return unverified('`hermes dump` did not report a home directory');
|
|
362
368
|
}
|
|
369
|
+
const home = resolved;
|
|
363
370
|
const entry = readHermesEntry(home);
|
|
364
371
|
if (entry.state === 'unreadable' || entry.state === 'unparseable') {
|
|
365
|
-
return {
|
|
366
|
-
registered: Registered.Unknown,
|
|
367
|
-
authMode: AuthMode.Unknown,
|
|
368
|
-
reason: `Hermes' config.yaml could not be ${entry.state === 'unreadable' ? 'read' : 'parsed'}`,
|
|
369
|
-
};
|
|
372
|
+
return unverified(`Hermes' config.yaml could not be ${entry.state === 'unreadable' ? 'read' : 'parsed'}`);
|
|
370
373
|
}
|
|
371
374
|
if (entry.state === 'absent')
|
|
372
|
-
return
|
|
375
|
+
return NOT_REGISTERED;
|
|
373
376
|
// `mcp add` saves a disabled entry on a failed connect, which connect refuses too.
|
|
374
377
|
const enabled = entry.state === 'enabled' ? {} : { connected: false };
|
|
375
378
|
// The .env key is Hermes-wide; the header template ties it here.
|
|
@@ -393,16 +396,12 @@ function probeOpenClaw() {
|
|
|
393
396
|
entries = fs.readdirSync(skillsDir);
|
|
394
397
|
}
|
|
395
398
|
catch {
|
|
396
|
-
return {
|
|
397
|
-
registered: Registered.Unknown,
|
|
398
|
-
authMode: AuthMode.Unknown,
|
|
399
|
-
reason: `no global skills directory at ${harnessDisplayPath('openclaw')}/skills; OpenClaw layout is unverified`,
|
|
400
|
-
};
|
|
399
|
+
return unverified(`no global skills directory at ${harnessDisplayPath('openclaw')}/skills; OpenClaw layout is unverified`);
|
|
401
400
|
}
|
|
402
401
|
// A bare substring also matched a skill merely named `not-tinyfish-thing`.
|
|
403
402
|
return entries.some((name) => SKILL_DIR_IS_TINYFISH.test(name))
|
|
404
403
|
? { registered: Registered.Yes, authMode: AuthMode.ApiKey, usesCliKey: true }
|
|
405
|
-
:
|
|
404
|
+
: NOT_REGISTERED;
|
|
406
405
|
}
|
|
407
406
|
// `opencode mcp list` prints no headers, so a key-authed registration is indistinguishable.
|
|
408
407
|
function probeOpencode() {
|
|
@@ -410,7 +409,7 @@ function probeOpencode() {
|
|
|
410
409
|
const probe = runProbe('opencode', args);
|
|
411
410
|
if (probe.outcome !== 'ran' || probe.exitCode !== 0) {
|
|
412
411
|
const reason = probeReason('opencode', args, probe);
|
|
413
|
-
return
|
|
412
|
+
return unverified(reason);
|
|
414
413
|
}
|
|
415
414
|
return fromMcpList('opencode', probe.output, AuthMode.Unknown, OPENCODE_ROW_URL);
|
|
416
415
|
}
|
|
@@ -440,11 +439,7 @@ function fromGrokPlugin() {
|
|
|
440
439
|
// repair that double-registers a working one. Anything but a list is unknown, not absent.
|
|
441
440
|
const shape = grokPluginSchema.safeParse(listed);
|
|
442
441
|
if (!shape.success) {
|
|
443
|
-
return
|
|
444
|
-
registered: Registered.Unknown,
|
|
445
|
-
authMode: AuthMode.Unknown,
|
|
446
|
-
reason: '`grok plugin list --json` returned an unexpected shape',
|
|
447
|
-
};
|
|
442
|
+
return unverified('`grok plugin list --json` returned an unexpected shape');
|
|
448
443
|
}
|
|
449
444
|
if (!shape.data.some((plugin) => plugin.name === 'tinyfish'))
|
|
450
445
|
return undefined;
|
|
@@ -477,36 +472,24 @@ function probeGrok() {
|
|
|
477
472
|
const args = ['mcp', 'list', '--json'];
|
|
478
473
|
const probe = runProbe('grok', args);
|
|
479
474
|
if (probe.outcome === 'unavailable') {
|
|
480
|
-
return
|
|
475
|
+
return unverified(probe.reason);
|
|
481
476
|
}
|
|
482
477
|
if (probe.exitCode !== 0) {
|
|
483
478
|
if (UNSUPPORTED_SUBCOMMAND.test(probe.output)) {
|
|
484
|
-
return
|
|
485
|
-
registered: Registered.Unknown,
|
|
486
|
-
authMode: AuthMode.Unknown,
|
|
487
|
-
reason: 'this Grok build has no `mcp list --json`',
|
|
488
|
-
};
|
|
479
|
+
return unverified('this Grok build has no `mcp list --json`');
|
|
489
480
|
}
|
|
490
|
-
return
|
|
491
|
-
registered: Registered.Unknown,
|
|
492
|
-
authMode: AuthMode.Unknown,
|
|
493
|
-
reason: probeReason('grok', args, probe),
|
|
494
|
-
};
|
|
481
|
+
return unverified(probeReason('grok', args, probe));
|
|
495
482
|
}
|
|
496
483
|
let entries;
|
|
497
484
|
try {
|
|
498
485
|
entries = grokListSchema.parse(JSON.parse(probe.stdout));
|
|
499
486
|
}
|
|
500
487
|
catch {
|
|
501
|
-
return
|
|
502
|
-
registered: Registered.Unknown,
|
|
503
|
-
authMode: AuthMode.Unknown,
|
|
504
|
-
reason: '`grok mcp list --json` did not return the expected shape',
|
|
505
|
-
};
|
|
488
|
+
return unverified('`grok mcp list --json` did not return the expected shape');
|
|
506
489
|
}
|
|
507
490
|
const entry = entries.find((server) => server.name === 'tinyfish');
|
|
508
491
|
if (!entry)
|
|
509
|
-
return fromGrokPlugin() ??
|
|
492
|
+
return fromGrokPlugin() ?? NOT_REGISTERED;
|
|
510
493
|
return {
|
|
511
494
|
registered: Registered.Yes,
|
|
512
495
|
...grokKeyAuth(entry.headers ?? {}),
|
|
@@ -522,6 +505,7 @@ const PROBES = {
|
|
|
522
505
|
omp: probeOmp,
|
|
523
506
|
openclaw: probeOpenClaw,
|
|
524
507
|
opencode: probeOpencode,
|
|
508
|
+
pi: probePi,
|
|
525
509
|
};
|
|
526
510
|
/** Never throws; a failed probe is `unknown` with a reason, so no caller can read it as healthy. */
|
|
527
511
|
export function detectRegistrations(harnesses) {
|
|
@@ -549,12 +533,7 @@ export function detectRegistrations(harnesses) {
|
|
|
549
533
|
if (process.env['TINYFISH_DEBUG']) {
|
|
550
534
|
errLine(`probe ${detection.harness} threw: ${e instanceof Error ? e.message : String(e)}`);
|
|
551
535
|
}
|
|
552
|
-
return {
|
|
553
|
-
...base,
|
|
554
|
-
registered: Registered.Unknown,
|
|
555
|
-
authMode: AuthMode.Unknown,
|
|
556
|
-
reason: 'the probe failed unexpectedly',
|
|
557
|
-
};
|
|
536
|
+
return { ...base, ...unverified('the probe failed unexpectedly') };
|
|
558
537
|
}
|
|
559
538
|
});
|
|
560
539
|
}
|
|
@@ -6,9 +6,6 @@ import { AuthMode, Registered } from './harness-detect.js';
|
|
|
6
6
|
* opts out — a privacy switch must not fail closed on `true`.
|
|
7
7
|
*/
|
|
8
8
|
export declare function telemetryDisabled(): boolean;
|
|
9
|
-
export declare function isTrustedTelemetryOrigin(endpoint: string): boolean;
|
|
10
|
-
/** SHA-256 lookup key only, and only to trusted origins — the raw key never rides telemetry. */
|
|
11
|
-
export declare function telemetryHeaders(endpoint: string, apiKey?: string): Record<string, string>;
|
|
12
9
|
/**
|
|
13
10
|
* The one POST to `/api/cli/connect-event`, and the only place its security-relevant
|
|
14
11
|
* properties live: trusted-origin key gating, `redirect: "error"`, the 2s cap, notice
|
|
@@ -32,6 +29,7 @@ declare const harnessResultSchema: z.ZodObject<{
|
|
|
32
29
|
codex: "codex";
|
|
33
30
|
hermes: "hermes";
|
|
34
31
|
opencode: "opencode";
|
|
32
|
+
pi: "pi";
|
|
35
33
|
"claude-code": "claude-code";
|
|
36
34
|
}>;
|
|
37
35
|
detected: z.ZodBoolean;
|
|
@@ -66,6 +64,7 @@ export declare const setupCompletedPayloadSchema: z.ZodObject<{
|
|
|
66
64
|
codex: "codex";
|
|
67
65
|
hermes: "hermes";
|
|
68
66
|
opencode: "opencode";
|
|
67
|
+
pi: "pi";
|
|
69
68
|
"claude-code": "claude-code";
|
|
70
69
|
}>;
|
|
71
70
|
detected: z.ZodBoolean;
|
|
@@ -84,7 +83,7 @@ export declare const setupCompletedPayloadSchema: z.ZodObject<{
|
|
|
84
83
|
}>>;
|
|
85
84
|
connect_attempt_id: z.ZodOptional<z.ZodUUID>;
|
|
86
85
|
}, z.core.$strip>;
|
|
87
|
-
|
|
86
|
+
type SetupCompletedPayload = z.infer<typeof setupCompletedPayloadSchema>;
|
|
88
87
|
export type HarnessResult = z.infer<typeof harnessResultSchema>;
|
|
89
88
|
export type SetupMode = NonNullable<SetupCompletedPayload['mode']>;
|
|
90
89
|
export type UpgradeOutcome = 'updated' | 'failed' | 'interrupted';
|
|
@@ -93,12 +92,11 @@ export type UpgradeScope = 'both' | 'cli' | 'skill';
|
|
|
93
92
|
/** `toVersion` is null when the run was interrupted or the installed version could not be read. */
|
|
94
93
|
export declare function sendUpgradeCompleted(fromVersion: string, toVersion: string | null, outcome: UpgradeOutcome, scope: UpgradeScope, apiKey?: string): Promise<void>;
|
|
95
94
|
export declare function sendSetupCompleted(mcpUrl: string, harnesses: HarnessResult[], apiKey?: string, mode?: SetupMode, attemptId?: string): Promise<void>;
|
|
96
|
-
export declare const DOCTOR_HARNESS_SCOPES: readonly ["all", ...("openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code")[]];
|
|
97
95
|
export declare const DOCTOR_ERROR_CLASSES: readonly ["command_not_found", "timeout", "spawn_error", "nonzero_exit", "unexpected_error"];
|
|
98
96
|
export type DoctorErrorClass = (typeof DOCTOR_ERROR_CLASSES)[number];
|
|
99
97
|
/** Derived, not read: the catch is generic, so only the code is trustworthy. */
|
|
100
98
|
export declare function doctorErrorClass(error: unknown): DoctorErrorClass;
|
|
101
|
-
|
|
99
|
+
declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
102
100
|
outcome: z.ZodLiteral<"completed">;
|
|
103
101
|
schema_version: z.ZodInt;
|
|
104
102
|
cli_version: z.ZodString;
|
|
@@ -128,6 +126,7 @@ export declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
|
128
126
|
codex: "codex";
|
|
129
127
|
hermes: "hermes";
|
|
130
128
|
opencode: "opencode";
|
|
129
|
+
pi: "pi";
|
|
131
130
|
"claude-code": "claude-code";
|
|
132
131
|
all: "all";
|
|
133
132
|
}>;
|
|
@@ -141,6 +140,7 @@ export declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
|
141
140
|
codex: "codex";
|
|
142
141
|
hermes: "hermes";
|
|
143
142
|
opencode: "opencode";
|
|
143
|
+
pi: "pi";
|
|
144
144
|
"claude-code": "claude-code";
|
|
145
145
|
}>>;
|
|
146
146
|
harnesses: z.ZodArray<z.ZodObject<{
|
|
@@ -152,6 +152,7 @@ export declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
|
152
152
|
codex: "codex";
|
|
153
153
|
hermes: "hermes";
|
|
154
154
|
opencode: "opencode";
|
|
155
|
+
pi: "pi";
|
|
155
156
|
"claude-code": "claude-code";
|
|
156
157
|
}>;
|
|
157
158
|
detected: z.ZodBoolean;
|
|
@@ -163,7 +164,7 @@ export declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
|
163
164
|
hermes_plugin_version: z.ZodOptional<z.ZodString>;
|
|
164
165
|
hermes_plugin_expected_version: z.ZodOptional<z.ZodString>;
|
|
165
166
|
}, z.core.$strip>;
|
|
166
|
-
|
|
167
|
+
declare const doctorCouldNotRunPayloadSchema: z.ZodObject<{
|
|
167
168
|
outcome: z.ZodLiteral<"could_not_run">;
|
|
168
169
|
error_class: z.ZodEnum<{
|
|
169
170
|
timeout: "timeout";
|
|
@@ -187,6 +188,7 @@ export declare const doctorCouldNotRunPayloadSchema: z.ZodObject<{
|
|
|
187
188
|
codex: "codex";
|
|
188
189
|
hermes: "hermes";
|
|
189
190
|
opencode: "opencode";
|
|
191
|
+
pi: "pi";
|
|
190
192
|
"claude-code": "claude-code";
|
|
191
193
|
all: "all";
|
|
192
194
|
}>;
|
|
@@ -223,6 +225,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
223
225
|
codex: "codex";
|
|
224
226
|
hermes: "hermes";
|
|
225
227
|
opencode: "opencode";
|
|
228
|
+
pi: "pi";
|
|
226
229
|
"claude-code": "claude-code";
|
|
227
230
|
all: "all";
|
|
228
231
|
}>;
|
|
@@ -236,6 +239,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
236
239
|
codex: "codex";
|
|
237
240
|
hermes: "hermes";
|
|
238
241
|
opencode: "opencode";
|
|
242
|
+
pi: "pi";
|
|
239
243
|
"claude-code": "claude-code";
|
|
240
244
|
}>>;
|
|
241
245
|
harnesses: z.ZodArray<z.ZodObject<{
|
|
@@ -247,6 +251,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
247
251
|
codex: "codex";
|
|
248
252
|
hermes: "hermes";
|
|
249
253
|
opencode: "opencode";
|
|
254
|
+
pi: "pi";
|
|
250
255
|
"claude-code": "claude-code";
|
|
251
256
|
}>;
|
|
252
257
|
detected: z.ZodBoolean;
|
|
@@ -281,6 +286,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
281
286
|
codex: "codex";
|
|
282
287
|
hermes: "hermes";
|
|
283
288
|
opencode: "opencode";
|
|
289
|
+
pi: "pi";
|
|
284
290
|
"claude-code": "claude-code";
|
|
285
291
|
all: "all";
|
|
286
292
|
}>;
|
|
@@ -20,7 +20,7 @@ export function telemetryDisabled() {
|
|
|
20
20
|
// `--url` accepts any http(s) URL, so a user-supplied origin must never receive the key.
|
|
21
21
|
const TRUSTED_TELEMETRY_HOST_SUFFIXES = ['.tinyfish.ai', '.tinyfish.io'];
|
|
22
22
|
const TRUSTED_TELEMETRY_LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '[::1]'];
|
|
23
|
-
|
|
23
|
+
function isTrustedTelemetryOrigin(endpoint) {
|
|
24
24
|
let url;
|
|
25
25
|
try {
|
|
26
26
|
url = new URL(endpoint);
|
|
@@ -38,7 +38,7 @@ export function isTrustedTelemetryOrigin(endpoint) {
|
|
|
38
38
|
return TRUSTED_TELEMETRY_HOST_SUFFIXES.some((suffix) => host === suffix.slice(1) || host.endsWith(suffix));
|
|
39
39
|
}
|
|
40
40
|
/** SHA-256 lookup key only, and only to trusted origins — the raw key never rides telemetry. */
|
|
41
|
-
|
|
41
|
+
function telemetryHeaders(endpoint, apiKey) {
|
|
42
42
|
const headers = { 'content-type': 'application/json' };
|
|
43
43
|
if (apiKey && validateKeyFormat(apiKey) && isTrustedTelemetryOrigin(endpoint)) {
|
|
44
44
|
headers['x-api-key-lookup'] = createHash('sha256').update(apiKey).digest('hex');
|
|
@@ -210,7 +210,7 @@ export async function sendSetupCompleted(mcpUrl, harnesses, apiKey, mode, attemp
|
|
|
210
210
|
}
|
|
211
211
|
await postConnectEvent({ endpoint, body, attempts: 2, label: 'setup', apiKey });
|
|
212
212
|
}
|
|
213
|
-
|
|
213
|
+
const DOCTOR_HARNESS_SCOPES = ['all', ...ALL_HARNESSES];
|
|
214
214
|
// Mirrors the route's cap; drops what the route would 400 un-retried.
|
|
215
215
|
const versionSchema = z
|
|
216
216
|
.string()
|
|
@@ -237,7 +237,7 @@ export function doctorErrorClass(error) {
|
|
|
237
237
|
return 'timeout';
|
|
238
238
|
return code ? 'spawn_error' : 'unexpected_error';
|
|
239
239
|
}
|
|
240
|
-
|
|
240
|
+
const doctorCompletedPayloadSchema = z.object({
|
|
241
241
|
outcome: z.literal('completed'),
|
|
242
242
|
schema_version: z.int().positive(),
|
|
243
243
|
cli_version: z.string().max(32),
|
|
@@ -274,7 +274,7 @@ export const doctorCompletedPayloadSchema = z.object({
|
|
|
274
274
|
hermes_plugin_expected_version: versionSchema.optional(),
|
|
275
275
|
});
|
|
276
276
|
// Detection threw, so there is no report to aggregate: enumerated outcome and timing only.
|
|
277
|
-
|
|
277
|
+
const doctorCouldNotRunPayloadSchema = z.object({
|
|
278
278
|
outcome: z.literal('could_not_run'),
|
|
279
279
|
error_class: z.enum(DOCTOR_ERROR_CLASSES),
|
|
280
280
|
schema_version: z.int().positive(),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type InstallOptions } from './cli-install.js';
|
|
2
2
|
import { type SkillAgent } from './connect-clients.js';
|
|
3
|
+
export declare const SKILLS_CLI_PACKAGE = "skills@1.5.15";
|
|
3
4
|
export declare function installWebSkill(client: {
|
|
4
5
|
skillAgent?: SkillAgent;
|
|
5
6
|
displayName: string;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
1
3
|
import * as path from 'node:path';
|
|
2
4
|
import spawn from 'cross-spawn';
|
|
3
|
-
import { z } from 'zod';
|
|
4
5
|
import { loadConfig } from './auth.js';
|
|
5
6
|
import { captureStdio, capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
|
|
6
7
|
import { CURSOR_SKILL_TARGET, NATIVE_MCP_CLIENTS, OPENCLAW, openclawSkillInstallArgs, } from './connect-clients.js';
|
|
7
|
-
import { ConnectInterruptedError, ConnectStepError, probeSupportVariant,
|
|
8
|
+
import { ConnectInterruptedError, ConnectStepError, probeSupportVariant, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
|
|
8
9
|
import { CLI_AGENT_IDENTITY } from './constants.js';
|
|
9
|
-
import { errLine } from './output.js';
|
|
10
|
+
import { errLine, sanitizeLine } from './output.js';
|
|
10
11
|
// Supports Hermes without node:util.styleText, so the installer still runs on Node 20.11.
|
|
11
|
-
const SKILLS_CLI_PACKAGE = 'skills@1.5.15';
|
|
12
|
+
export const SKILLS_CLI_PACKAGE = 'skills@1.5.15';
|
|
12
13
|
const TINYFISH_WEB_SKILL_SOURCE = 'tinyfish-io/tinyfish-cookbook';
|
|
13
14
|
const TINYFISH_WEB_SKILL = 'use-tinyfish';
|
|
14
15
|
/** `skills add` is an unconditional overwrite, so it doubles as the refresh path. */
|
|
@@ -44,35 +45,34 @@ const SKILL_ALREADY_CURRENT_PATTERN = /All global skills are up to date/;
|
|
|
44
45
|
// A lock entry with no recorded hash is untrackable, so `skills` reports it as skipped rather
|
|
45
46
|
// than failed. Left undetected that reads as "up to date" while nothing was refreshed.
|
|
46
47
|
const SKILL_UNCHECKABLE_PATTERN = /cannot be checked automatically/;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const list = spawn.sync('npx', ['-y', SKILLS_CLI_PACKAGE, 'list', '--global', '--json'], {
|
|
51
|
-
encoding: 'utf8',
|
|
52
|
-
env: skillSpawnEnv(),
|
|
53
|
-
maxBuffer: STEP_MAX_BUFFER,
|
|
54
|
-
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
55
|
-
});
|
|
56
|
-
const listRan = !list.error && list.status === 0;
|
|
57
|
-
if (listRan && skillListed(list.stdout ?? ''))
|
|
58
|
-
return null;
|
|
59
|
-
// A clean list that omits the skill is not a real exit (PF-3680).
|
|
60
|
-
const tag = listRan ? 'skill_absent_after_add' : `skill_list_failed ${spawnFailureDetail(list)}`;
|
|
61
|
-
return { result: list, tag };
|
|
48
|
+
// `skills` writes here for the universal agents, whose own config dirs it never touches.
|
|
49
|
+
function canonicalSkillsDir() {
|
|
50
|
+
return path.join(os.homedir(), '.agents', 'skills'); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
|
|
62
51
|
}
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
52
|
+
// Where the harness reads. Both of the modes `add` picks land the skill here.
|
|
53
|
+
const SKILL_DIR_BY_AGENT = {
|
|
54
|
+
'claude-code': () => path.join(agentHome('CLAUDE_CONFIG_DIR', '.claude'), 'skills'),
|
|
55
|
+
// The env value, not resolveHermesHome(): the child we spawn reads the env.
|
|
56
|
+
'hermes-agent': () => path.join(agentHome('HERMES_HOME', '.hermes'), 'skills'),
|
|
57
|
+
codex: canonicalSkillsDir,
|
|
58
|
+
cursor: canonicalSkillsDir,
|
|
59
|
+
opencode: canonicalSkillsDir,
|
|
60
|
+
// `skills` writes pi's here whatever PI_CODING_AGENT_DIR says.
|
|
61
|
+
pi: () => path.join(os.homedir(), '.pi', 'agent', 'skills'), // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
|
|
62
|
+
};
|
|
63
|
+
function agentHome(override, fallback) {
|
|
64
|
+
return process.env[override]?.trim() || path.join(os.homedir(), fallback); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
|
|
68
65
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
/** `add` exits 0 on per-agent failure, so the file it should have written is the verdict. */
|
|
67
|
+
function skillOnDisk(agent) {
|
|
68
|
+
// SKILL.md, not the dir: `skills` mkdirs before it copies, so a failed copy leaves one.
|
|
69
|
+
return fs.existsSync(path.join(SKILL_DIR_BY_AGENT[agent](), TINYFISH_WEB_SKILL, 'SKILL.md'));
|
|
70
|
+
}
|
|
71
|
+
// The route tail-slices to 500, so an over-long tail would cut the tag off the front.
|
|
72
|
+
const SKILL_DETAIL_TAIL_MAX_CHARS = 425;
|
|
73
|
+
function absentDetail(addOutput) {
|
|
74
|
+
const tail = sanitizeLine(addOutput).trim().slice(-SKILL_DETAIL_TAIL_MAX_CHARS);
|
|
75
|
+
return tail ? `skill_absent_after_add | ${tail}` : 'skill_absent_after_add';
|
|
76
76
|
}
|
|
77
77
|
export function installWebSkill(client, { verbose }) {
|
|
78
78
|
if (!client.skillAgent)
|
|
@@ -82,14 +82,13 @@ export function installWebSkill(client, { verbose }) {
|
|
|
82
82
|
...captureStdio(verbose),
|
|
83
83
|
env: skillSpawnEnv(),
|
|
84
84
|
});
|
|
85
|
+
const output = capturedOutput(add);
|
|
85
86
|
const addFailed = Boolean(add.error) || add.status !== 0;
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
if (failed) {
|
|
87
|
+
const absent = !addFailed && !skillOnDisk(client.skillAgent);
|
|
88
|
+
if (addFailed || absent) {
|
|
89
89
|
// Built first: its interrupt check must run before any replay.
|
|
90
|
-
const error = spawnStepError(`Could not install the TinyFish web skill in ${client.displayName}`,
|
|
91
|
-
|
|
92
|
-
replay(capturedOutput(add));
|
|
90
|
+
const error = spawnStepError(`Could not install the TinyFish web skill in ${client.displayName}`, add, absent ? absentDetail(output) : undefined);
|
|
91
|
+
replay(output);
|
|
93
92
|
throw error;
|
|
94
93
|
}
|
|
95
94
|
}
|
|
@@ -176,18 +175,18 @@ function reinstallWebSkill(skillAgents, verbose) {
|
|
|
176
175
|
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
177
176
|
});
|
|
178
177
|
const output = capturedOutput(result);
|
|
179
|
-
// Quiet capture stays (#4434); the
|
|
178
|
+
// Quiet capture stays (#4434); the on-disk check replaced prose matching.
|
|
180
179
|
const addFailed = Boolean(result.error) || result.status !== 0;
|
|
181
|
-
const
|
|
182
|
-
const failed = addFailed ||
|
|
180
|
+
const missing = addFailed ? undefined : skillAgents.find((agent) => !skillOnDisk(agent));
|
|
181
|
+
const failed = addFailed || missing !== undefined;
|
|
183
182
|
if (failed)
|
|
184
|
-
throwIfInterrupted(
|
|
183
|
+
throwIfInterrupted(result);
|
|
185
184
|
if (verbose || failed)
|
|
186
185
|
replay(output);
|
|
187
186
|
if (failed) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
});
|
|
187
|
+
// Upgrade telemetry carries no detail, so the agent has to ride the message.
|
|
188
|
+
const scope = missing ? ` for ${missing}` : '';
|
|
189
|
+
throw new Error(`Could not refresh the TinyFish web skill${scope}`, { cause: result.error });
|
|
191
190
|
}
|
|
192
191
|
}
|
|
193
192
|
/** Fallback when no skill-bearing harness is recorded: refresh whatever `skills` tracks. */
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -21,14 +21,14 @@ export interface CliAgentRunParams extends AgentRunParams {
|
|
|
21
21
|
use_vault?: boolean;
|
|
22
22
|
credential_item_ids?: string[];
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
type VaultProvider = '1password' | 'bitwarden';
|
|
25
25
|
export interface VaultConnection {
|
|
26
26
|
id: string;
|
|
27
27
|
provider: VaultProvider;
|
|
28
28
|
connectionStatus: string;
|
|
29
29
|
lastValidatedAt: string | null;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
interface VaultFieldMetadata {
|
|
32
32
|
fieldId: string;
|
|
33
33
|
label: string;
|
|
34
34
|
type: 'STRING' | 'CONCEALED' | 'OTP';
|
|
@@ -119,7 +119,7 @@ export interface BatchGetResponse {
|
|
|
119
119
|
data: Run[];
|
|
120
120
|
not_found: string[] | null;
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
interface BatchCancelResult {
|
|
123
123
|
run_id: string;
|
|
124
124
|
status: string;
|
|
125
125
|
cancelled_at: string | null;
|
|
@@ -157,3 +157,4 @@ export interface ProfileUploadResponse {
|
|
|
157
157
|
domains_updated: string[];
|
|
158
158
|
domains_failed?: string[];
|
|
159
159
|
}
|
|
160
|
+
export {};
|
package/dist/program.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createRequire } from 'module';
|
|
2
1
|
import { Command } from 'commander';
|
|
2
|
+
import { CLI_VERSION } from './lib/constants.js';
|
|
3
3
|
import { registerAuth } from './commands/auth.js';
|
|
4
4
|
import { registerBatch } from './commands/batch.js';
|
|
5
5
|
import { registerFetch } from './commands/fetch.js';
|
|
@@ -18,12 +18,11 @@ import { registerOnboard } from './commands/onboard.js';
|
|
|
18
18
|
import { installNoticeEmit } from './lib/notice.js';
|
|
19
19
|
/** Split from index.ts so tests can drive the real program instead of hand-assembling one. */
|
|
20
20
|
export function buildProgram() {
|
|
21
|
-
const { version } = createRequire(import.meta.url)('../package.json');
|
|
22
21
|
const program = new Command();
|
|
23
22
|
program
|
|
24
23
|
.name('tinyfish')
|
|
25
24
|
.description('TinyFish CLI — run web automations from your terminal or agent.')
|
|
26
|
-
.version(
|
|
25
|
+
.version(CLI_VERSION, '-V, --version', 'Show version')
|
|
27
26
|
.helpOption('-h, --help', 'Show help')
|
|
28
27
|
.addHelpCommand(false)
|
|
29
28
|
.enablePositionalOptions()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiny-fish/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "TinyFish CLI — run web automations from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"@tiny-fish/sdk": "^0.5.0",
|
|
38
38
|
"commander": "^12.0.0",
|
|
39
39
|
"cross-spawn": "^7.0.6",
|
|
40
|
-
"globals": "^17.4.0",
|
|
41
40
|
"tldts": "^6.1.86",
|
|
42
41
|
"which": "^2.0.2",
|
|
43
42
|
"yaml": "^2.9.0",
|