@tiny-fish/cli 0.39.1-next.311 → 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.js +86 -19
- package/dist/commands/doctor.js +3 -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 +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/package.json +1 -1
|
@@ -31,6 +31,7 @@ declare const doctorCheckSchema: z.ZodObject<{
|
|
|
31
31
|
codex: "codex";
|
|
32
32
|
hermes: "hermes";
|
|
33
33
|
opencode: "opencode";
|
|
34
|
+
pi: "pi";
|
|
34
35
|
"claude-code": "claude-code";
|
|
35
36
|
}>>;
|
|
36
37
|
scope: z.ZodEnum<{
|
|
@@ -48,6 +49,7 @@ declare const doctorHarnessSchema: z.ZodObject<{
|
|
|
48
49
|
codex: "codex";
|
|
49
50
|
hermes: "hermes";
|
|
50
51
|
opencode: "opencode";
|
|
52
|
+
pi: "pi";
|
|
51
53
|
"claude-code": "claude-code";
|
|
52
54
|
}>;
|
|
53
55
|
detected: z.ZodBoolean;
|
|
@@ -74,6 +76,7 @@ declare const doctorRepairSchema: z.ZodObject<{
|
|
|
74
76
|
codex: "codex";
|
|
75
77
|
hermes: "hermes";
|
|
76
78
|
opencode: "opencode";
|
|
79
|
+
pi: "pi";
|
|
77
80
|
"claude-code": "claude-code";
|
|
78
81
|
}>>;
|
|
79
82
|
command: z.ZodString;
|
|
@@ -102,6 +105,7 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
102
105
|
codex: "codex";
|
|
103
106
|
hermes: "hermes";
|
|
104
107
|
opencode: "opencode";
|
|
108
|
+
pi: "pi";
|
|
105
109
|
"claude-code": "claude-code";
|
|
106
110
|
}>>;
|
|
107
111
|
scope: z.ZodEnum<{
|
|
@@ -119,6 +123,7 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
119
123
|
codex: "codex";
|
|
120
124
|
hermes: "hermes";
|
|
121
125
|
opencode: "opencode";
|
|
126
|
+
pi: "pi";
|
|
122
127
|
"claude-code": "claude-code";
|
|
123
128
|
}>;
|
|
124
129
|
detected: z.ZodBoolean;
|
|
@@ -145,6 +150,7 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
145
150
|
codex: "codex";
|
|
146
151
|
hermes: "hermes";
|
|
147
152
|
opencode: "opencode";
|
|
153
|
+
pi: "pi";
|
|
148
154
|
"claude-code": "claude-code";
|
|
149
155
|
}>>;
|
|
150
156
|
command: z.ZodString;
|
|
@@ -23,5 +23,7 @@ export interface HarnessDetection {
|
|
|
23
23
|
configPath: string;
|
|
24
24
|
/** False with `detected` true means a stale config dir: nothing to run. */
|
|
25
25
|
binaryOnPath: boolean;
|
|
26
|
+
/** True with no config dir means installed but never launched. */
|
|
27
|
+
configDirExists: boolean;
|
|
26
28
|
}
|
|
27
29
|
export declare function detectInstalledHarnesses(): HarnessDetection[];
|
|
@@ -3,6 +3,7 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import which from 'which';
|
|
5
5
|
import { ALL_HARNESSES, harnessSpec } from './harness-spec.js';
|
|
6
|
+
import { piAgentDir, piBinaryIsPi } from './pi-config.js';
|
|
6
7
|
export { ALL_HARNESSES };
|
|
7
8
|
export const HARNESS_DISPLAY_NAMES = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).displayName]));
|
|
8
9
|
export const RELOAD_ACTION = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).reloadAction]));
|
|
@@ -31,13 +32,18 @@ export function commandOnPath(command) {
|
|
|
31
32
|
return false;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
35
|
+
// PI_CODING_AGENT_DIR moves pi's dir; a guessed path would be written unread.
|
|
36
|
+
const CONFIG_PATH_RESOLVERS = { pi: piAgentDir };
|
|
34
37
|
export function harnessConfigPath(harness) {
|
|
35
|
-
|
|
38
|
+
const resolved = CONFIG_PATH_RESOLVERS[harness]?.();
|
|
39
|
+
return resolved ?? path.join(os.homedir(), harnessSpec(harness).configDir); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
|
|
36
40
|
}
|
|
37
41
|
/** For reason strings that name a location; keeps them in sync with the spec. */
|
|
38
42
|
export function harnessDisplayPath(harness) {
|
|
39
43
|
return `~/${harnessSpec(harness).configDir}`;
|
|
40
44
|
}
|
|
45
|
+
// `pi` is a two-letter name an unrelated npm package also claims; confirm before trusting it.
|
|
46
|
+
const BINARY_VERIFIERS = { pi: piBinaryIsPi };
|
|
41
47
|
export function detectInstalledHarnesses() {
|
|
42
48
|
return ALL_HARNESSES.map((harness) => {
|
|
43
49
|
const configPath = harnessConfigPath(harness);
|
|
@@ -48,13 +54,15 @@ export function detectInstalledHarnesses() {
|
|
|
48
54
|
catch {
|
|
49
55
|
configDirExists = false;
|
|
50
56
|
}
|
|
51
|
-
// Config dir means ran once;
|
|
52
|
-
const
|
|
57
|
+
// Config dir means ran once; a verified binary means runnable now.
|
|
58
|
+
const onPath = commandOnPath(HARNESS_COMMANDS[harness]);
|
|
59
|
+
const binaryOnPath = onPath && (BINARY_VERIFIERS[harness]?.() ?? true);
|
|
53
60
|
return {
|
|
54
61
|
harness,
|
|
55
62
|
detected: configDirExists || binaryOnPath,
|
|
56
63
|
configPath,
|
|
57
64
|
binaryOnPath,
|
|
65
|
+
configDirExists,
|
|
58
66
|
};
|
|
59
67
|
});
|
|
60
68
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The `skills` CLI's own agent names, not ours. */
|
|
2
|
-
export type SkillAgent = 'claude-code' | 'codex' | 'cursor' | 'hermes-agent' | 'opencode';
|
|
2
|
+
export type SkillAgent = 'claude-code' | 'codex' | 'cursor' | 'hermes-agent' | 'opencode' | 'pi';
|
|
3
3
|
export interface HarnessSupportCheck {
|
|
4
4
|
args: string[];
|
|
5
5
|
/** Missing → nothing can work; setup fails. */
|
|
@@ -74,8 +74,8 @@ export interface HarnessSpec {
|
|
|
74
74
|
keyHeldByCli?: true;
|
|
75
75
|
/** Connect writes the MCP config file; no harness binary is spawned. */
|
|
76
76
|
cliWritesConfig?: true;
|
|
77
|
-
/**
|
|
78
|
-
|
|
77
|
+
/** A second reader (Cursor's IDE) loads the config with no harness binary on PATH. */
|
|
78
|
+
readableWithoutBinary?: true;
|
|
79
79
|
/** `mcp add` succeeds unauthenticated; OAuth lands at first tool use. */
|
|
80
80
|
authDeferredAtInstall?: true;
|
|
81
81
|
}
|
|
@@ -139,6 +139,7 @@ export declare const HARNESS_SPECS: {
|
|
|
139
139
|
configDir: string;
|
|
140
140
|
reloadAction: string;
|
|
141
141
|
skillAgent: "cursor";
|
|
142
|
+
readableWithoutBinary: true;
|
|
142
143
|
canVerifyAuth: true;
|
|
143
144
|
keyHeldByCli: true;
|
|
144
145
|
cliWritesConfig: true;
|
|
@@ -192,7 +193,6 @@ export declare const HARNESS_SPECS: {
|
|
|
192
193
|
displayName: string;
|
|
193
194
|
configDir: string;
|
|
194
195
|
reloadAction: string;
|
|
195
|
-
configPathFromBinary: true;
|
|
196
196
|
canVerifyAuth: true;
|
|
197
197
|
keyHeldByCli: true;
|
|
198
198
|
cliWritesConfig: true;
|
|
@@ -236,6 +236,16 @@ export declare const HARNESS_SPECS: {
|
|
|
236
236
|
removals: never[];
|
|
237
237
|
postConnectNote: string;
|
|
238
238
|
};
|
|
239
|
+
pi: {
|
|
240
|
+
command: string;
|
|
241
|
+
displayName: string;
|
|
242
|
+
configDir: string;
|
|
243
|
+
reloadAction: string;
|
|
244
|
+
skillAgent: "pi";
|
|
245
|
+
canVerifyAuth: true;
|
|
246
|
+
keyHeldByCli: true;
|
|
247
|
+
cliWritesConfig: true;
|
|
248
|
+
};
|
|
239
249
|
};
|
|
240
250
|
/** Derived from spec keys; one entry extends every union. */
|
|
241
251
|
export type Harness = keyof typeof HARNESS_SPECS;
|
|
@@ -246,4 +256,4 @@ export type NonNativeHarness = {
|
|
|
246
256
|
[K in Harness]: 'urlStyle' extends keyof (typeof HARNESS_SPECS)[K] ? never : K;
|
|
247
257
|
}[Harness];
|
|
248
258
|
/** Native MCP harnesses carry add-generation fields; the rest override connect. */
|
|
249
|
-
export declare const NATIVE_HARNESSES: ("openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code")[];
|
|
259
|
+
export declare const NATIVE_HARNESSES: ("openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "pi" | "claude-code")[];
|
package/dist/lib/harness-spec.js
CHANGED
|
@@ -114,6 +114,7 @@ export const HARNESS_SPECS = {
|
|
|
114
114
|
configDir: '.cursor',
|
|
115
115
|
reloadAction: 'reload the window',
|
|
116
116
|
skillAgent: 'cursor',
|
|
117
|
+
readableWithoutBinary: true,
|
|
117
118
|
canVerifyAuth: true,
|
|
118
119
|
keyHeldByCli: true,
|
|
119
120
|
cliWritesConfig: true,
|
|
@@ -167,7 +168,6 @@ export const HARNESS_SPECS = {
|
|
|
167
168
|
displayName: 'omp',
|
|
168
169
|
configDir: '.omp',
|
|
169
170
|
reloadAction: 'restart it',
|
|
170
|
-
configPathFromBinary: true,
|
|
171
171
|
canVerifyAuth: true,
|
|
172
172
|
keyHeldByCli: true,
|
|
173
173
|
cliWritesConfig: true,
|
|
@@ -213,6 +213,16 @@ export const HARNESS_SPECS = {
|
|
|
213
213
|
removals: [],
|
|
214
214
|
postConnectNote: OPENCODE_MODEL_NOTE,
|
|
215
215
|
},
|
|
216
|
+
pi: {
|
|
217
|
+
command: 'pi',
|
|
218
|
+
displayName: 'Pi',
|
|
219
|
+
configDir: '.pi/agent',
|
|
220
|
+
reloadAction: 'restart it',
|
|
221
|
+
skillAgent: 'pi',
|
|
222
|
+
canVerifyAuth: true,
|
|
223
|
+
keyHeldByCli: true,
|
|
224
|
+
cliWritesConfig: true,
|
|
225
|
+
},
|
|
216
226
|
};
|
|
217
227
|
export const ALL_HARNESSES = Object.keys(HARNESS_SPECS);
|
|
218
228
|
export function harnessSpec(harness) {
|
package/dist/lib/harness.js
CHANGED
|
@@ -28,6 +28,8 @@ const HARNESS_FINGERPRINTS = [
|
|
|
28
28
|
{ name: 'hermes', matches: (env) => hasVarWithPrefix(env, 'HERMES_') },
|
|
29
29
|
// Exact key, not a prefix: opencode writes OPENCODE=1 at startup, reads every OPENCODE_* as config.
|
|
30
30
|
{ name: 'opencode', matches: (env) => Boolean(env['OPENCODE']) },
|
|
31
|
+
// Observed: pi launched from Claude Code inherits CLAUDECODE=1.
|
|
32
|
+
{ name: 'pi', matches: (env) => env['PI_CODING_AGENT'] === 'true' },
|
|
31
33
|
{
|
|
32
34
|
name: 'claude-code',
|
|
33
35
|
matches: (env) => env['CLAUDECODE'] === '1' || Boolean(env['CLAUDE_CODE_ENTRYPOINT']),
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** Every message about the entry names this path. */
|
|
2
2
|
export declare function hermesConfigPath(home: string): string;
|
|
3
|
+
export declare const HERMES_HEADER_TEMPLATE = "Bearer ${MCP_TINYFISH_API_KEY}";
|
|
4
|
+
/** Both the writer and the gate key on this, so they cannot disagree. */
|
|
5
|
+
export declare const HERMES_MCP_SERVER_KEY = "tinyfish";
|
|
3
6
|
/** Callers branch on these; each is a different repair. */
|
|
4
7
|
export type HermesEntry = {
|
|
5
8
|
state: 'unreadable';
|
|
@@ -8,7 +8,9 @@ export function hermesConfigPath(home) {
|
|
|
8
8
|
return path.join(home, 'config.yaml');
|
|
9
9
|
}
|
|
10
10
|
// Hermes persists the key as an interpolation template (mcp_config.py:174-182).
|
|
11
|
-
const
|
|
11
|
+
export const HERMES_HEADER_TEMPLATE = `Bearer \${${HERMES_KEY_VAR}}`;
|
|
12
|
+
/** Both the writer and the gate key on this, so they cannot disagree. */
|
|
13
|
+
export const HERMES_MCP_SERVER_KEY = 'tinyfish';
|
|
12
14
|
// Foreign file: model only what we read, tolerate the rest.
|
|
13
15
|
const entrySchema = z.looseObject({
|
|
14
16
|
// Anything goes: the runtime defaults unrecognised values to enabled.
|
|
@@ -16,7 +18,9 @@ const entrySchema = z.looseObject({
|
|
|
16
18
|
headers: z.record(z.string(), z.unknown()).optional(),
|
|
17
19
|
});
|
|
18
20
|
const configSchema = z
|
|
19
|
-
.looseObject({
|
|
21
|
+
.looseObject({
|
|
22
|
+
mcp_servers: z.looseObject({ [HERMES_MCP_SERVER_KEY]: entrySchema.nullish() }).nullish(),
|
|
23
|
+
})
|
|
20
24
|
.nullish();
|
|
21
25
|
const TRUTHY_STRINGS = new Set(['true', '1', 'yes', 'on']);
|
|
22
26
|
const FALSY_STRINGS = new Set(['false', '0', 'no', 'off']);
|
|
@@ -42,7 +46,7 @@ function isEnabled(value) {
|
|
|
42
46
|
/** Only the header template proves this entry uses the seeded key. */
|
|
43
47
|
function hasKeyHeader(headers) {
|
|
44
48
|
const authorization = Object.entries(headers ?? {}).find(([name]) => name.toLowerCase() === 'authorization');
|
|
45
|
-
return authorization?.[1] ===
|
|
49
|
+
return authorization?.[1] === HERMES_HEADER_TEMPLATE;
|
|
46
50
|
}
|
|
47
51
|
/** Connect's gate and doctor's probe share this, so they cannot disagree. */
|
|
48
52
|
export function readHermesEntry(home) {
|
|
@@ -60,7 +64,7 @@ export function readHermesEntry(home) {
|
|
|
60
64
|
catch {
|
|
61
65
|
return { state: 'unparseable' };
|
|
62
66
|
}
|
|
63
|
-
const entry = config?.mcp_servers?.
|
|
67
|
+
const entry = config?.mcp_servers?.[HERMES_MCP_SERVER_KEY];
|
|
64
68
|
if (!entry)
|
|
65
69
|
return { state: 'absent' };
|
|
66
70
|
const usesKeyHeader = hasKeyHeader(entry.headers);
|
package/dist/lib/hermes-env.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
/** Each reason is a different repair, so telemetry keeps them apart. */
|
|
2
|
+
export type HermesHomeReason = 'timeout' | 'exit' | 'no_line' | 'relative';
|
|
3
|
+
/** Failure carries why, so connect tags it without a second probe. */
|
|
4
|
+
export interface HermesHomeFailure {
|
|
5
|
+
reason: HermesHomeReason;
|
|
6
|
+
}
|
|
1
7
|
/** Asking Hermes beats reimplementing its profile override, which we'd drift from. */
|
|
2
|
-
export declare function resolveHermesHome(): string |
|
|
8
|
+
export declare function resolveHermesHome(): string | HermesHomeFailure;
|
|
3
9
|
export declare const HERMES_KEY_VAR = "MCP_TINYFISH_API_KEY";
|
|
4
10
|
/** Named in every message about the seeded key, so no caller rebuilds the path. */
|
|
5
11
|
export declare function hermesEnvPath(home: string): string;
|
package/dist/lib/hermes-env.js
CHANGED
|
@@ -14,13 +14,16 @@ export function resolveHermesHome() {
|
|
|
14
14
|
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
15
15
|
timeout: HARNESS_PROBE_TIMEOUT_MS,
|
|
16
16
|
});
|
|
17
|
+
if (result.error?.code === 'ETIMEDOUT') {
|
|
18
|
+
return { reason: 'timeout' };
|
|
19
|
+
}
|
|
17
20
|
if (result.error || result.status !== 0)
|
|
18
|
-
return
|
|
21
|
+
return { reason: 'exit' };
|
|
19
22
|
const raw = HERMES_HOME_LINE.exec(sanitizeLine(`${result.stdout ?? ''}\n${result.stderr ?? ''}`))?.[1];
|
|
20
23
|
if (!raw)
|
|
21
|
-
return
|
|
24
|
+
return { reason: 'no_line' };
|
|
22
25
|
const expanded = raw === '~' || raw.startsWith('~/') ? path.join(os.homedir(), raw.slice(2)) : raw;
|
|
23
|
-
return path.isAbsolute(expanded) ? expanded :
|
|
26
|
+
return path.isAbsolute(expanded) ? expanded : { reason: 'relative' };
|
|
24
27
|
}
|
|
25
28
|
export const HERMES_KEY_VAR = 'MCP_TINYFISH_API_KEY';
|
|
26
29
|
/** Named in every message about the seeded key, so no caller rebuilds the path. */
|
|
@@ -4,6 +4,10 @@ export declare const HERMES_PLUGIN_SHA = "496cd63fefd982bbaa8a85ce78ef2270d70098
|
|
|
4
4
|
export declare const HERMES_PLUGIN_VERSION = "0.1.0";
|
|
5
5
|
export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish-io/tinyfish-web-agent-integrations/hermes --ref 496cd63fefd982bbaa8a85ce78ef2270d700984f --enable";
|
|
6
6
|
export declare function installHermesPlugin(home: string, apiKey: string, { verbose }: Pick<InstallOptions, 'verbose'>): void;
|
|
7
|
+
/** Absent `enabled` reads as enabled, so never write this field-by-field. */
|
|
8
|
+
export declare function writeHermesMcpEntry(home: string, mcpUrl: string): void;
|
|
9
|
+
/** Retracts our row; an absent `enabled` reads as enabled, so a partial one is live. */
|
|
10
|
+
export declare function removeHermesMcpEntry(home: string): void;
|
|
7
11
|
/** After install only: backends must never name an absent provider. */
|
|
8
12
|
export declare function setHermesWebBackends(home: string): void;
|
|
9
13
|
/** Ours-only unsets: a user-chosen backend value survives the uninstall. */
|
|
@@ -7,6 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
import { capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
|
|
8
8
|
import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
|
|
9
9
|
import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
|
|
10
|
+
import { HERMES_HEADER_TEMPLATE, HERMES_MCP_SERVER_KEY } from './hermes-config.js';
|
|
10
11
|
import { errLine, parseJson } from './output.js';
|
|
11
12
|
// Bump per CLI release.
|
|
12
13
|
export const HERMES_PLUGIN_SHA = '496cd63fefd982bbaa8a85ce78ef2270d700984f';
|
|
@@ -95,6 +96,21 @@ function configWrite(home, args) {
|
|
|
95
96
|
replay(capturedOutput(result));
|
|
96
97
|
throw error;
|
|
97
98
|
}
|
|
99
|
+
/** Absent `enabled` reads as enabled, so never write this field-by-field. */
|
|
100
|
+
export function writeHermesMcpEntry(home, mcpUrl) {
|
|
101
|
+
const entry = {
|
|
102
|
+
url: mcpUrl,
|
|
103
|
+
headers: { Authorization: HERMES_HEADER_TEMPLATE },
|
|
104
|
+
enabled: true,
|
|
105
|
+
};
|
|
106
|
+
const result = hermesConfig(['set', `mcp_servers.${HERMES_MCP_SERVER_KEY}`, JSON.stringify(entry)], home);
|
|
107
|
+
// `config set` exits 0 on a bad key, so only an interrupt is worth raising here.
|
|
108
|
+
throwIfInterrupted(result);
|
|
109
|
+
}
|
|
110
|
+
/** Retracts our row; an absent `enabled` reads as enabled, so a partial one is live. */
|
|
111
|
+
export function removeHermesMcpEntry(home) {
|
|
112
|
+
configWrite(home, ['unset', `mcp_servers.${HERMES_MCP_SERVER_KEY}`]);
|
|
113
|
+
}
|
|
98
114
|
const HERMES_BACKEND_KEYS = ['web.search_backend', 'web.extract_backend'];
|
|
99
115
|
// Dispatch falls back to web.backend, and the plugin's own setup can set it.
|
|
100
116
|
const HERMES_OWNED_BACKEND_KEYS = [...HERMES_BACKEND_KEYS, 'web.backend'];
|
|
@@ -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;
|