@tiny-fish/cli 0.39.1-next.311 → 0.40.1-next.317
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/commands/connect.js +86 -19
- package/dist/commands/doctor.js +3 -1
- package/dist/commands/fetch.js +79 -24
- package/dist/lib/client.d.ts +113 -3
- package/dist/lib/client.js +14 -3
- 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 +13 -5
- package/dist/lib/connect-clients.d.ts +8 -2
- package/dist/lib/connect-clients.js +53 -13
- 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 +15 -5
- package/dist/lib/harness-spec.js +11 -1
- 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 -1
- package/dist/lib/hermes-env.js +6 -3
- package/dist/lib/hermes-plugin.d.ts +4 -0
- package/dist/lib/hermes-plugin.js +16 -0
- package/dist/lib/pi-config.d.ts +26 -0
- package/dist/lib/pi-config.js +111 -0
- package/dist/lib/registration-detect.js +29 -2
- package/dist/lib/setup-telemetry.d.ts +10 -0
- package/dist/lib/skill-install.d.ts +1 -0
- package/dist/lib/skill-install.js +44 -37
- package/dist/lib/types.d.ts +10 -1
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type McpJsonServerEntry, type McpJsonWriteResult } from './mcp-json-config.js';
|
|
2
|
+
/** Mirrors pi's own getAgentDir: the override wins, else `~/.pi/agent`. */
|
|
3
|
+
export declare function piAgentDir(): string;
|
|
4
|
+
export declare function piMcpPath(): string;
|
|
5
|
+
/** Reports the header's shape, never its value. */
|
|
6
|
+
export declare function readPiTinyfishEntry(): McpJsonServerEntry;
|
|
7
|
+
/** Merges only the `tinyfish` key. */
|
|
8
|
+
export declare function writePiMcpConfig(mcpUrl: string, apiKey?: string): McpJsonWriteResult;
|
|
9
|
+
/** Dry-run description of the pending write; touches nothing. */
|
|
10
|
+
export declare function planPiWrite(mcpUrl: string, apiKey?: string): string;
|
|
11
|
+
/** Removes only the `tinyfish` key. */
|
|
12
|
+
export declare function removePiMcpServer(): McpJsonWriteResult;
|
|
13
|
+
export declare const PI_ADAPTER_INSTALL_COMMAND = "pi install npm:pi-mcp-adapter";
|
|
14
|
+
/** `unknown` keeps a settings file we could not read apart from one that lists no adapter. */
|
|
15
|
+
export type PiAdapterState = 'installed' | 'absent' | 'unknown';
|
|
16
|
+
/** Core pi has no MCP, so without this package the entry we write is never read. */
|
|
17
|
+
export declare function piMcpAdapterState(cwd?: string): PiAdapterState;
|
|
18
|
+
/** Tests share one process; the probe result must not. */
|
|
19
|
+
export declare function resetPiBinaryCache(): void;
|
|
20
|
+
/** npm ships an unrelated `pi` that prints `3`; only a semver answer is the harness. */
|
|
21
|
+
export declare function piBinaryIsPi(): boolean;
|
|
22
|
+
/** `skills` hardcodes the default dir, so an override leaves the skill where pi will not look. */
|
|
23
|
+
export declare function piSkillDirMismatch(): {
|
|
24
|
+
installedTo: string;
|
|
25
|
+
readFrom: string;
|
|
26
|
+
} | undefined;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import spawn from 'cross-spawn';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
|
|
7
|
+
import { planWrite, readTinyfishEntry, removeServer, writeMcpConfig, } from './mcp-json-config.js';
|
|
8
|
+
// pi expands `~` and `~/`; writing one literally would mkdir '~'.
|
|
9
|
+
function expandTilde(dir) {
|
|
10
|
+
if (dir === '~')
|
|
11
|
+
return os.homedir();
|
|
12
|
+
// nosemgrep: path-join-resolve-traversal -- $PI_CODING_AGENT_DIR names the caller's own dir, resolved as pi does
|
|
13
|
+
return dir.startsWith('~/') ? path.join(os.homedir(), dir.slice(2)) : dir;
|
|
14
|
+
}
|
|
15
|
+
/** Mirrors pi's own getAgentDir: the override wins, else `~/.pi/agent`. */
|
|
16
|
+
export function piAgentDir() {
|
|
17
|
+
const override = process.env['PI_CODING_AGENT_DIR'];
|
|
18
|
+
return override ? path.resolve(expandTilde(override)) : path.join(os.homedir(), '.pi', 'agent');
|
|
19
|
+
}
|
|
20
|
+
// Resolved per call, not cached: nothing here spawns, so drift is impossible.
|
|
21
|
+
const PI_MCP_TARGET = {
|
|
22
|
+
serverKey: 'tinyfish',
|
|
23
|
+
dir: piAgentDir,
|
|
24
|
+
file: () => path.join(piAgentDir(), 'mcp.json'),
|
|
25
|
+
};
|
|
26
|
+
export function piMcpPath() {
|
|
27
|
+
return PI_MCP_TARGET.file();
|
|
28
|
+
}
|
|
29
|
+
/** Reports the header's shape, never its value. */
|
|
30
|
+
export function readPiTinyfishEntry() {
|
|
31
|
+
return readTinyfishEntry(PI_MCP_TARGET);
|
|
32
|
+
}
|
|
33
|
+
/** Merges only the `tinyfish` key. */
|
|
34
|
+
export function writePiMcpConfig(mcpUrl, apiKey) {
|
|
35
|
+
return writeMcpConfig(PI_MCP_TARGET, mcpUrl, apiKey);
|
|
36
|
+
}
|
|
37
|
+
/** Dry-run description of the pending write; touches nothing. */
|
|
38
|
+
export function planPiWrite(mcpUrl, apiKey) {
|
|
39
|
+
return planWrite(PI_MCP_TARGET, mcpUrl, apiKey);
|
|
40
|
+
}
|
|
41
|
+
/** Removes only the `tinyfish` key. */
|
|
42
|
+
export function removePiMcpServer() {
|
|
43
|
+
return removeServer(PI_MCP_TARGET);
|
|
44
|
+
}
|
|
45
|
+
export const PI_ADAPTER_INSTALL_COMMAND = 'pi install npm:pi-mcp-adapter';
|
|
46
|
+
// Matches `npm:pi-mcp-adapter`, a pinned version, and git or path installs ending in the name.
|
|
47
|
+
const ADAPTER_SOURCE = /(?:^|[/:])pi-mcp-adapter(?:@|$)/;
|
|
48
|
+
// pi's PackageSource is a bare source or an object carrying it; `pi install` writes both.
|
|
49
|
+
const packageSourceSchema = z.union([z.string(), z.object({ source: z.string() })]);
|
|
50
|
+
const settingsSchema = z.object({ packages: z.array(packageSourceSchema).optional() });
|
|
51
|
+
function adapterInSettings(file) {
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
// No settings file means nothing is installed; any other read failure tells us nothing.
|
|
58
|
+
return e?.code === 'ENOENT' ? 'absent' : 'unknown';
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(raw);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return 'unknown';
|
|
66
|
+
}
|
|
67
|
+
const settings = settingsSchema.safeParse(parsed);
|
|
68
|
+
if (!settings.success)
|
|
69
|
+
return 'unknown';
|
|
70
|
+
const sources = (settings.data.packages ?? []).map((p) => (typeof p === 'string' ? p : p.source));
|
|
71
|
+
return sources.some((source) => ADAPTER_SOURCE.test(source)) ? 'installed' : 'absent';
|
|
72
|
+
}
|
|
73
|
+
/** Core pi has no MCP, so without this package the entry we write is never read. */
|
|
74
|
+
export function piMcpAdapterState(cwd = process.cwd()) {
|
|
75
|
+
// `pi install -l` records the adapter project-locally, where pi reads it just the same.
|
|
76
|
+
const scopes = [
|
|
77
|
+
path.join(piAgentDir(), 'settings.json'),
|
|
78
|
+
path.join(cwd, '.pi', 'settings.json'), // nosemgrep: path-join-resolve-traversal -- fixed names under the caller's cwd
|
|
79
|
+
].map(adapterInSettings);
|
|
80
|
+
if (scopes.includes('installed'))
|
|
81
|
+
return 'installed';
|
|
82
|
+
return scopes.includes('unknown') ? 'unknown' : 'absent';
|
|
83
|
+
}
|
|
84
|
+
// Cached: detection runs more than once per command and the answer cannot change mid-run.
|
|
85
|
+
let cachedBinaryIsPi;
|
|
86
|
+
/** Tests share one process; the probe result must not. */
|
|
87
|
+
export function resetPiBinaryCache() {
|
|
88
|
+
cachedBinaryIsPi = undefined;
|
|
89
|
+
}
|
|
90
|
+
/** npm ships an unrelated `pi` that prints `3`; only a semver answer is the harness. */
|
|
91
|
+
export function piBinaryIsPi() {
|
|
92
|
+
cachedBinaryIsPi ??= { value: probeVersion() };
|
|
93
|
+
return cachedBinaryIsPi.value;
|
|
94
|
+
}
|
|
95
|
+
function probeVersion() {
|
|
96
|
+
const result = spawn.sync('pi', ['--version'], {
|
|
97
|
+
encoding: 'utf8',
|
|
98
|
+
timeout: HARNESS_PROBE_TIMEOUT_MS,
|
|
99
|
+
});
|
|
100
|
+
if (result.error || result.status !== 0)
|
|
101
|
+
return false;
|
|
102
|
+
return /^\d+\.\d+\.\d+/.test((result.stdout ?? '').trim());
|
|
103
|
+
}
|
|
104
|
+
/** `skills` hardcodes the default dir, so an override leaves the skill where pi will not look. */
|
|
105
|
+
export function piSkillDirMismatch() {
|
|
106
|
+
const agentDir = piAgentDir();
|
|
107
|
+
const fallback = path.join(os.homedir(), '.pi', 'agent');
|
|
108
|
+
if (agentDir === fallback)
|
|
109
|
+
return undefined;
|
|
110
|
+
return { installedTo: path.join(fallback, 'skills'), readFrom: path.join(agentDir, 'skills') };
|
|
111
|
+
}
|
|
@@ -8,6 +8,7 @@ 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';
|
|
@@ -315,6 +316,30 @@ function probeOmp() {
|
|
|
315
316
|
...keyVerdict,
|
|
316
317
|
};
|
|
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();
|
|
322
|
+
if (entry.error) {
|
|
323
|
+
return {
|
|
324
|
+
registered: Registered.Unknown,
|
|
325
|
+
authMode: AuthMode.Unknown,
|
|
326
|
+
reason: 'mcp.json exists but could not be read or parsed',
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (!entry.present)
|
|
330
|
+
return { registered: Registered.No, authMode: AuthMode.Unknown };
|
|
331
|
+
const keyVerdict = entry.keyTemplateVar
|
|
332
|
+
? envKeyVerdict(entry.keyTemplateVar)
|
|
333
|
+
: entry.keyMatchesCliKey
|
|
334
|
+
? { keyMatchesCliKey: true }
|
|
335
|
+
: {};
|
|
336
|
+
return {
|
|
337
|
+
registered: Registered.Yes,
|
|
338
|
+
authMode: entry.hasApiKeyHeader ? AuthMode.ApiKey : AuthMode.Unknown,
|
|
339
|
+
...(entry.url ? { registeredUrl: entry.url } : {}),
|
|
340
|
+
...keyVerdict,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
318
343
|
// Both list commands print this header in every state, including "no servers configured".
|
|
319
344
|
// Exit 0 with output we cannot recognise is not evidence of absence: reporting `no` there would
|
|
320
345
|
// earn a connect repair off a parse failure, which is the bug class this command exists to kill.
|
|
@@ -337,10 +362,11 @@ function fromMcpList(command, output, registeredMode, urlPattern) {
|
|
|
337
362
|
}
|
|
338
363
|
// `hermes mcp list` only pretty-prints this file, so connect's gate reads it too.
|
|
339
364
|
function probeHermes() {
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
365
|
+
const resolved = resolveHermesHome();
|
|
366
|
+
if (typeof resolved !== 'string') {
|
|
342
367
|
return unverified('`hermes dump` did not report a home directory');
|
|
343
368
|
}
|
|
369
|
+
const home = resolved;
|
|
344
370
|
const entry = readHermesEntry(home);
|
|
345
371
|
if (entry.state === 'unreadable' || entry.state === 'unparseable') {
|
|
346
372
|
return unverified(`Hermes' config.yaml could not be ${entry.state === 'unreadable' ? 'read' : 'parsed'}`);
|
|
@@ -479,6 +505,7 @@ const PROBES = {
|
|
|
479
505
|
omp: probeOmp,
|
|
480
506
|
openclaw: probeOpenClaw,
|
|
481
507
|
opencode: probeOpencode,
|
|
508
|
+
pi: probePi,
|
|
482
509
|
};
|
|
483
510
|
/** Never throws; a failed probe is `unknown` with a reason, so no caller can read it as healthy. */
|
|
484
511
|
export function detectRegistrations(harnesses) {
|
|
@@ -29,6 +29,7 @@ declare const harnessResultSchema: z.ZodObject<{
|
|
|
29
29
|
codex: "codex";
|
|
30
30
|
hermes: "hermes";
|
|
31
31
|
opencode: "opencode";
|
|
32
|
+
pi: "pi";
|
|
32
33
|
"claude-code": "claude-code";
|
|
33
34
|
}>;
|
|
34
35
|
detected: z.ZodBoolean;
|
|
@@ -63,6 +64,7 @@ export declare const setupCompletedPayloadSchema: z.ZodObject<{
|
|
|
63
64
|
codex: "codex";
|
|
64
65
|
hermes: "hermes";
|
|
65
66
|
opencode: "opencode";
|
|
67
|
+
pi: "pi";
|
|
66
68
|
"claude-code": "claude-code";
|
|
67
69
|
}>;
|
|
68
70
|
detected: z.ZodBoolean;
|
|
@@ -124,6 +126,7 @@ declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
|
124
126
|
codex: "codex";
|
|
125
127
|
hermes: "hermes";
|
|
126
128
|
opencode: "opencode";
|
|
129
|
+
pi: "pi";
|
|
127
130
|
"claude-code": "claude-code";
|
|
128
131
|
all: "all";
|
|
129
132
|
}>;
|
|
@@ -137,6 +140,7 @@ declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
|
137
140
|
codex: "codex";
|
|
138
141
|
hermes: "hermes";
|
|
139
142
|
opencode: "opencode";
|
|
143
|
+
pi: "pi";
|
|
140
144
|
"claude-code": "claude-code";
|
|
141
145
|
}>>;
|
|
142
146
|
harnesses: z.ZodArray<z.ZodObject<{
|
|
@@ -148,6 +152,7 @@ declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
|
148
152
|
codex: "codex";
|
|
149
153
|
hermes: "hermes";
|
|
150
154
|
opencode: "opencode";
|
|
155
|
+
pi: "pi";
|
|
151
156
|
"claude-code": "claude-code";
|
|
152
157
|
}>;
|
|
153
158
|
detected: z.ZodBoolean;
|
|
@@ -183,6 +188,7 @@ declare const doctorCouldNotRunPayloadSchema: z.ZodObject<{
|
|
|
183
188
|
codex: "codex";
|
|
184
189
|
hermes: "hermes";
|
|
185
190
|
opencode: "opencode";
|
|
191
|
+
pi: "pi";
|
|
186
192
|
"claude-code": "claude-code";
|
|
187
193
|
all: "all";
|
|
188
194
|
}>;
|
|
@@ -219,6 +225,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
219
225
|
codex: "codex";
|
|
220
226
|
hermes: "hermes";
|
|
221
227
|
opencode: "opencode";
|
|
228
|
+
pi: "pi";
|
|
222
229
|
"claude-code": "claude-code";
|
|
223
230
|
all: "all";
|
|
224
231
|
}>;
|
|
@@ -232,6 +239,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
232
239
|
codex: "codex";
|
|
233
240
|
hermes: "hermes";
|
|
234
241
|
opencode: "opencode";
|
|
242
|
+
pi: "pi";
|
|
235
243
|
"claude-code": "claude-code";
|
|
236
244
|
}>>;
|
|
237
245
|
harnesses: z.ZodArray<z.ZodObject<{
|
|
@@ -243,6 +251,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
243
251
|
codex: "codex";
|
|
244
252
|
hermes: "hermes";
|
|
245
253
|
opencode: "opencode";
|
|
254
|
+
pi: "pi";
|
|
246
255
|
"claude-code": "claude-code";
|
|
247
256
|
}>;
|
|
248
257
|
detected: z.ZodBoolean;
|
|
@@ -277,6 +286,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
277
286
|
codex: "codex";
|
|
278
287
|
hermes: "hermes";
|
|
279
288
|
opencode: "opencode";
|
|
289
|
+
pi: "pi";
|
|
280
290
|
"claude-code": "claude-code";
|
|
281
291
|
all: "all";
|
|
282
292
|
}>;
|
|
@@ -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,
|
|
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,27 +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()
|
|
65
|
+
}
|
|
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';
|
|
68
76
|
}
|
|
69
77
|
export function installWebSkill(client, { verbose }) {
|
|
70
78
|
if (!client.skillAgent)
|
|
@@ -74,14 +82,13 @@ export function installWebSkill(client, { verbose }) {
|
|
|
74
82
|
...captureStdio(verbose),
|
|
75
83
|
env: skillSpawnEnv(),
|
|
76
84
|
});
|
|
85
|
+
const output = capturedOutput(add);
|
|
77
86
|
const addFailed = Boolean(add.error) || add.status !== 0;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
if (failed) {
|
|
87
|
+
const absent = !addFailed && !skillOnDisk(client.skillAgent);
|
|
88
|
+
if (addFailed || absent) {
|
|
81
89
|
// Built first: its interrupt check must run before any replay.
|
|
82
|
-
const error = spawnStepError(`Could not install the TinyFish web skill in ${client.displayName}`,
|
|
83
|
-
|
|
84
|
-
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);
|
|
85
92
|
throw error;
|
|
86
93
|
}
|
|
87
94
|
}
|
|
@@ -168,18 +175,18 @@ function reinstallWebSkill(skillAgents, verbose) {
|
|
|
168
175
|
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
169
176
|
});
|
|
170
177
|
const output = capturedOutput(result);
|
|
171
|
-
// Quiet capture stays (#4434); the
|
|
178
|
+
// Quiet capture stays (#4434); the on-disk check replaced prose matching.
|
|
172
179
|
const addFailed = Boolean(result.error) || result.status !== 0;
|
|
173
|
-
const
|
|
174
|
-
const failed = addFailed ||
|
|
180
|
+
const missing = addFailed ? undefined : skillAgents.find((agent) => !skillOnDisk(agent));
|
|
181
|
+
const failed = addFailed || missing !== undefined;
|
|
175
182
|
if (failed)
|
|
176
|
-
throwIfInterrupted(
|
|
183
|
+
throwIfInterrupted(result);
|
|
177
184
|
if (verbose || failed)
|
|
178
185
|
replay(output);
|
|
179
186
|
if (failed) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
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 });
|
|
183
190
|
}
|
|
184
191
|
}
|
|
185
192
|
/** Fallback when no skill-bearing harness is recorded: refresh whatever `skills` tracks. */
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentRunParams, BrowserProfile, Run, RunStatus } from '@tiny-fish/sdk';
|
|
1
|
+
import type { AgentRunParams, BrowserProfile, FetchGetContentsParams, Run, RunStatus } from '@tiny-fish/sdk';
|
|
2
2
|
export type OutputSchema = Record<string, unknown>;
|
|
3
3
|
export interface CliAgentRunParams extends AgentRunParams {
|
|
4
4
|
output_schema?: OutputSchema;
|
|
@@ -91,6 +91,15 @@ export interface RunStepsResponse {
|
|
|
91
91
|
status: RunStatus;
|
|
92
92
|
steps: RunStep[];
|
|
93
93
|
}
|
|
94
|
+
export interface CliFetchHighlightsParams {
|
|
95
|
+
query: string;
|
|
96
|
+
max_snippets?: number;
|
|
97
|
+
max_characters?: number;
|
|
98
|
+
include_full_page_text?: boolean;
|
|
99
|
+
}
|
|
100
|
+
export interface CliFetchGetContentsParams extends FetchGetContentsParams {
|
|
101
|
+
highlights?: CliFetchHighlightsParams;
|
|
102
|
+
}
|
|
94
103
|
export interface CliBrowserSessionCreateParams {
|
|
95
104
|
url?: string;
|
|
96
105
|
browser_profile?: 'lite' | 'stealth';
|