@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
|
@@ -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. */
|
|
@@ -67,17 +67,17 @@ export interface HarnessSpec {
|
|
|
67
67
|
/** Printed after a successful connect, on both the launch and non-launch paths. */
|
|
68
68
|
postConnectNote?: string;
|
|
69
69
|
/** Sign-in is the CLI's own interactive `tinyfish auth login`. */
|
|
70
|
-
signInViaCliLogin
|
|
70
|
+
signInViaCliLogin?: true;
|
|
71
71
|
/** Stored-key auth verification works against this harness's config. */
|
|
72
|
-
canVerifyAuth
|
|
72
|
+
canVerifyAuth?: true;
|
|
73
73
|
/** The CLI holds or writes the key; no harness `mcp add` carries it. */
|
|
74
|
-
keyHeldByCli
|
|
74
|
+
keyHeldByCli?: true;
|
|
75
75
|
/** Connect writes the MCP config file; no harness binary is spawned. */
|
|
76
|
-
cliWritesConfig
|
|
77
|
-
/**
|
|
78
|
-
|
|
76
|
+
cliWritesConfig?: true;
|
|
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
|
-
authDeferredAtInstall
|
|
80
|
+
authDeferredAtInstall?: true;
|
|
81
81
|
}
|
|
82
82
|
/** One entry per harness; every per-harness list derives from it. */
|
|
83
83
|
export declare const HARNESS_SPECS: {
|
|
@@ -110,11 +110,6 @@ export declare const HARNESS_SPECS: {
|
|
|
110
110
|
args: string[];
|
|
111
111
|
label: string;
|
|
112
112
|
}[];
|
|
113
|
-
signInViaCliLogin: false;
|
|
114
|
-
canVerifyAuth: false;
|
|
115
|
-
keyHeldByCli: false;
|
|
116
|
-
cliWritesConfig: false;
|
|
117
|
-
authDeferredAtInstall: false;
|
|
118
113
|
};
|
|
119
114
|
codex: {
|
|
120
115
|
command: string;
|
|
@@ -136,10 +131,6 @@ export declare const HARNESS_SPECS: {
|
|
|
136
131
|
deferred: string;
|
|
137
132
|
};
|
|
138
133
|
loginArgs: string[];
|
|
139
|
-
signInViaCliLogin: false;
|
|
140
|
-
canVerifyAuth: false;
|
|
141
|
-
keyHeldByCli: false;
|
|
142
|
-
cliWritesConfig: false;
|
|
143
134
|
authDeferredAtInstall: true;
|
|
144
135
|
};
|
|
145
136
|
cursor: {
|
|
@@ -148,11 +139,10 @@ export declare const HARNESS_SPECS: {
|
|
|
148
139
|
configDir: string;
|
|
149
140
|
reloadAction: string;
|
|
150
141
|
skillAgent: "cursor";
|
|
151
|
-
|
|
142
|
+
readableWithoutBinary: true;
|
|
152
143
|
canVerifyAuth: true;
|
|
153
144
|
keyHeldByCli: true;
|
|
154
145
|
cliWritesConfig: true;
|
|
155
|
-
authDeferredAtInstall: false;
|
|
156
146
|
};
|
|
157
147
|
grok: {
|
|
158
148
|
command: string;
|
|
@@ -175,11 +165,6 @@ export declare const HARNESS_SPECS: {
|
|
|
175
165
|
viaEnv: true;
|
|
176
166
|
};
|
|
177
167
|
signInHint: string;
|
|
178
|
-
signInViaCliLogin: false;
|
|
179
|
-
canVerifyAuth: false;
|
|
180
|
-
keyHeldByCli: false;
|
|
181
|
-
cliWritesConfig: false;
|
|
182
|
-
authDeferredAtInstall: false;
|
|
183
168
|
};
|
|
184
169
|
hermes: {
|
|
185
170
|
command: string;
|
|
@@ -201,23 +186,16 @@ export declare const HARNESS_SPECS: {
|
|
|
201
186
|
label: string;
|
|
202
187
|
};
|
|
203
188
|
postConnectNote: string;
|
|
204
|
-
signInViaCliLogin: false;
|
|
205
|
-
canVerifyAuth: false;
|
|
206
189
|
keyHeldByCli: true;
|
|
207
|
-
cliWritesConfig: false;
|
|
208
|
-
authDeferredAtInstall: false;
|
|
209
190
|
};
|
|
210
191
|
omp: {
|
|
211
192
|
command: string;
|
|
212
193
|
displayName: string;
|
|
213
194
|
configDir: string;
|
|
214
195
|
reloadAction: string;
|
|
215
|
-
configPathFromBinary: true;
|
|
216
|
-
signInViaCliLogin: false;
|
|
217
196
|
canVerifyAuth: true;
|
|
218
197
|
keyHeldByCli: true;
|
|
219
198
|
cliWritesConfig: true;
|
|
220
|
-
authDeferredAtInstall: false;
|
|
221
199
|
};
|
|
222
200
|
openclaw: {
|
|
223
201
|
command: string;
|
|
@@ -235,8 +213,6 @@ export declare const HARNESS_SPECS: {
|
|
|
235
213
|
signInViaCliLogin: true;
|
|
236
214
|
canVerifyAuth: true;
|
|
237
215
|
keyHeldByCli: true;
|
|
238
|
-
cliWritesConfig: false;
|
|
239
|
-
authDeferredAtInstall: false;
|
|
240
216
|
};
|
|
241
217
|
opencode: {
|
|
242
218
|
command: string;
|
|
@@ -259,11 +235,16 @@ export declare const HARNESS_SPECS: {
|
|
|
259
235
|
loginArgs: string[];
|
|
260
236
|
removals: never[];
|
|
261
237
|
postConnectNote: string;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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;
|
|
267
248
|
};
|
|
268
249
|
};
|
|
269
250
|
/** Derived from spec keys; one entry extends every union. */
|
|
@@ -275,4 +256,4 @@ export type NonNativeHarness = {
|
|
|
275
256
|
[K in Harness]: 'urlStyle' extends keyof (typeof HARNESS_SPECS)[K] ? never : K;
|
|
276
257
|
}[Harness];
|
|
277
258
|
/** Native MCP harnesses carry add-generation fields; the rest override connect. */
|
|
278
|
-
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
|
@@ -82,11 +82,6 @@ export const HARNESS_SPECS = {
|
|
|
82
82
|
label: 'local TinyFish registration',
|
|
83
83
|
},
|
|
84
84
|
],
|
|
85
|
-
signInViaCliLogin: false,
|
|
86
|
-
canVerifyAuth: false,
|
|
87
|
-
keyHeldByCli: false,
|
|
88
|
-
cliWritesConfig: false,
|
|
89
|
-
authDeferredAtInstall: false,
|
|
90
85
|
},
|
|
91
86
|
codex: {
|
|
92
87
|
command: 'codex',
|
|
@@ -110,10 +105,6 @@ export const HARNESS_SPECS = {
|
|
|
110
105
|
// `--oauth-resource` duplicates the resource Codex discovers; Clerk rejects both.
|
|
111
106
|
// `mcp add` still runs OAuth inline, so `loginArgs` stays a retry.
|
|
112
107
|
loginArgs: ['mcp', 'login', 'tinyfish'],
|
|
113
|
-
signInViaCliLogin: false,
|
|
114
|
-
canVerifyAuth: false,
|
|
115
|
-
keyHeldByCli: false,
|
|
116
|
-
cliWritesConfig: false,
|
|
117
108
|
// TODO(PF-3580): connect now probes Codex sign-in, so this over-warns a signed-in install.
|
|
118
109
|
authDeferredAtInstall: true,
|
|
119
110
|
},
|
|
@@ -123,11 +114,10 @@ export const HARNESS_SPECS = {
|
|
|
123
114
|
configDir: '.cursor',
|
|
124
115
|
reloadAction: 'reload the window',
|
|
125
116
|
skillAgent: 'cursor',
|
|
126
|
-
|
|
117
|
+
readableWithoutBinary: true,
|
|
127
118
|
canVerifyAuth: true,
|
|
128
119
|
keyHeldByCli: true,
|
|
129
120
|
cliWritesConfig: true,
|
|
130
|
-
authDeferredAtInstall: false,
|
|
131
121
|
},
|
|
132
122
|
grok: {
|
|
133
123
|
command: 'grok',
|
|
@@ -148,11 +138,6 @@ export const HARNESS_SPECS = {
|
|
|
148
138
|
// Grok expands `${VAR}` in headers at load, so the key stays out of argv and config.toml.
|
|
149
139
|
header: { name: 'Authorization', sep: ': ', valuePrefix: 'Bearer ', viaEnv: true },
|
|
150
140
|
signInHint: GROK_SIGN_IN_HINT,
|
|
151
|
-
signInViaCliLogin: false,
|
|
152
|
-
canVerifyAuth: false,
|
|
153
|
-
keyHeldByCli: false,
|
|
154
|
-
cliWritesConfig: false,
|
|
155
|
-
authDeferredAtInstall: false,
|
|
156
141
|
},
|
|
157
142
|
hermes: {
|
|
158
143
|
command: 'hermes',
|
|
@@ -175,24 +160,17 @@ export const HARNESS_SPECS = {
|
|
|
175
160
|
label: 'TinyFish web plugin from Hermes',
|
|
176
161
|
},
|
|
177
162
|
postConnectNote: HERMES_RESTART_NOTE,
|
|
178
|
-
|
|
179
|
-
// Stays false: keyed already covers auth; true breaks keyless installs.
|
|
180
|
-
canVerifyAuth: false,
|
|
163
|
+
// No canVerifyAuth: keyed already covers auth; true breaks keyless installs.
|
|
181
164
|
keyHeldByCli: true,
|
|
182
|
-
cliWritesConfig: false,
|
|
183
|
-
authDeferredAtInstall: false,
|
|
184
165
|
},
|
|
185
166
|
omp: {
|
|
186
167
|
command: 'omp',
|
|
187
168
|
displayName: 'omp',
|
|
188
169
|
configDir: '.omp',
|
|
189
170
|
reloadAction: 'restart it',
|
|
190
|
-
configPathFromBinary: true,
|
|
191
|
-
signInViaCliLogin: false,
|
|
192
171
|
canVerifyAuth: true,
|
|
193
172
|
keyHeldByCli: true,
|
|
194
173
|
cliWritesConfig: true,
|
|
195
|
-
authDeferredAtInstall: false,
|
|
196
174
|
},
|
|
197
175
|
openclaw: {
|
|
198
176
|
command: 'openclaw',
|
|
@@ -212,8 +190,6 @@ export const HARNESS_SPECS = {
|
|
|
212
190
|
signInViaCliLogin: true,
|
|
213
191
|
canVerifyAuth: true,
|
|
214
192
|
keyHeldByCli: true,
|
|
215
|
-
cliWritesConfig: false,
|
|
216
|
-
authDeferredAtInstall: false,
|
|
217
193
|
},
|
|
218
194
|
opencode: {
|
|
219
195
|
command: 'opencode',
|
|
@@ -236,11 +212,16 @@ export const HARNESS_SPECS = {
|
|
|
236
212
|
// `opencode mcp add` upserts by name and ships no `mcp remove`.
|
|
237
213
|
removals: [],
|
|
238
214
|
postConnectNote: OPENCODE_MODEL_NOTE,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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,
|
|
244
225
|
},
|
|
245
226
|
};
|
|
246
227
|
export const ALL_HARNESSES = Object.keys(HARNESS_SPECS);
|
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,6 +1,11 @@
|
|
|
1
|
-
|
|
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
|
+
}
|
|
2
7
|
/** Asking Hermes beats reimplementing its profile override, which we'd drift from. */
|
|
3
|
-
export declare function resolveHermesHome(): string |
|
|
8
|
+
export declare function resolveHermesHome(): string | HermesHomeFailure;
|
|
4
9
|
export declare const HERMES_KEY_VAR = "MCP_TINYFISH_API_KEY";
|
|
5
10
|
/** Named in every message about the seeded key, so no caller rebuilds the path. */
|
|
6
11
|
export declare function hermesEnvPath(home: string): string;
|
package/dist/lib/hermes-env.js
CHANGED
|
@@ -7,22 +7,23 @@ import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
|
|
|
7
7
|
import { sanitizeLine } from './output.js';
|
|
8
8
|
// display_hermes_home() abbreviates under $HOME, so the value is not always absolute.
|
|
9
9
|
const HERMES_HOME_LINE = /^\s*hermes_home:\s*(\S.*?)\s*$/m;
|
|
10
|
-
// One value: the next cold-start bump must move both probes together.
|
|
11
|
-
export const HERMES_PROBE_TIMEOUT_MS = HARNESS_PROBE_TIMEOUT_MS;
|
|
12
10
|
/** Asking Hermes beats reimplementing its profile override, which we'd drift from. */
|
|
13
11
|
export function resolveHermesHome() {
|
|
14
12
|
const result = spawn.sync('hermes', ['dump'], {
|
|
15
13
|
encoding: 'utf8',
|
|
16
14
|
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
17
|
-
timeout:
|
|
15
|
+
timeout: HARNESS_PROBE_TIMEOUT_MS,
|
|
18
16
|
});
|
|
17
|
+
if (result.error?.code === 'ETIMEDOUT') {
|
|
18
|
+
return { reason: 'timeout' };
|
|
19
|
+
}
|
|
19
20
|
if (result.error || result.status !== 0)
|
|
20
|
-
return
|
|
21
|
+
return { reason: 'exit' };
|
|
21
22
|
const raw = HERMES_HOME_LINE.exec(sanitizeLine(`${result.stdout ?? ''}\n${result.stderr ?? ''}`))?.[1];
|
|
22
23
|
if (!raw)
|
|
23
|
-
return
|
|
24
|
+
return { reason: 'no_line' };
|
|
24
25
|
const expanded = raw === '~' || raw.startsWith('~/') ? path.join(os.homedir(), raw.slice(2)) : raw;
|
|
25
|
-
return path.isAbsolute(expanded) ? expanded :
|
|
26
|
+
return path.isAbsolute(expanded) ? expanded : { reason: 'relative' };
|
|
26
27
|
}
|
|
27
28
|
export const HERMES_KEY_VAR = 'MCP_TINYFISH_API_KEY';
|
|
28
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,7 +7,8 @@ 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 {
|
|
10
|
+
import { HERMES_HEADER_TEMPLATE, HERMES_MCP_SERVER_KEY } from './hermes-config.js';
|
|
11
|
+
import { errLine, parseJson } from './output.js';
|
|
11
12
|
// Bump per CLI release.
|
|
12
13
|
export const HERMES_PLUGIN_SHA = '496cd63fefd982bbaa8a85ce78ef2270d700984f';
|
|
13
14
|
// Bump with the SHA; the source is `hermes/plugin.yaml` at that commit.
|
|
@@ -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'];
|
|
@@ -127,14 +143,6 @@ const hermesPluginStatusSchema = z.looseObject({
|
|
|
127
143
|
provider_available: z.boolean().nullish(),
|
|
128
144
|
web_backend_configured: z.boolean(),
|
|
129
145
|
});
|
|
130
|
-
function parseJson(text) {
|
|
131
|
-
try {
|
|
132
|
-
return JSON.parse(text);
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
146
|
/** `status --json` always exits 0; a non-zero exit means the subcommand is absent. */
|
|
139
147
|
export function readHermesPluginStatus(home) {
|
|
140
148
|
const result = spawn.sync('hermes', ['tinyfish', 'status', '--json'], {
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -16,3 +16,5 @@ export declare function errLine(line: string): void;
|
|
|
16
16
|
export declare function warnLine(line: string): void;
|
|
17
17
|
export declare function err(data: unknown): void;
|
|
18
18
|
export declare function handleApiError(e: unknown): never;
|
|
19
|
+
/** Undefined, not a throw: callers hand the value to a Zod safeParse. */
|
|
20
|
+
export declare function parseJson(text: string): unknown;
|
package/dist/lib/output.js
CHANGED
|
@@ -70,3 +70,12 @@ export function handleApiError(e) {
|
|
|
70
70
|
}
|
|
71
71
|
process.exit(1);
|
|
72
72
|
}
|
|
73
|
+
/** Undefined, not a throw: callers hand the value to a Zod safeParse. */
|
|
74
|
+
export function parseJson(text) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -20,7 +20,6 @@ export interface RegistrationStatus {
|
|
|
20
20
|
recordedAuthMode?: RecordedAuthMode;
|
|
21
21
|
reason?: string;
|
|
22
22
|
}
|
|
23
|
-
export declare function codexOauthCompleted(): boolean | undefined;
|
|
24
23
|
/** Per-harness sign-in probes; connect composes these rather than the descriptor carrying them. */
|
|
25
24
|
export declare const SIGN_IN_PROBES: Partial<Record<AgentClient, () => boolean | undefined>>;
|
|
26
25
|
/** Never throws; a failed probe is `unknown` with a reason, so no caller can read it as healthy. */
|