@tiny-fish/cli 0.44.0 → 0.45.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 +2 -5
- package/dist/commands/doctor.js +2 -6
- package/dist/lib/connect-all-uninstall.js +3 -3
- package/dist/lib/connect-clients.d.ts +1 -3
- package/dist/lib/connect-clients.js +3 -4
- package/dist/lib/doctor-report.d.ts +0 -1
- package/dist/lib/doctor-report.js +0 -1
- package/dist/lib/doctor-telemetry.js +0 -3
- package/dist/lib/hermes-plugin.d.ts +3 -4
- package/dist/lib/hermes-plugin.js +86 -30
- package/dist/lib/setup-telemetry.d.ts +0 -2
- package/dist/lib/setup-telemetry.js +0 -1
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -399,15 +399,12 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
399
399
|
}
|
|
400
400
|
if (client.extraPostInstall && state.authMode !== AuthMode.Keyless) {
|
|
401
401
|
await attempt('plugin_install', () => {
|
|
402
|
-
client.extraPostInstall?.({
|
|
402
|
+
const pluginVersion = client.extraPostInstall?.({
|
|
403
403
|
apiKey: validatedApiKey(options.apiKey),
|
|
404
404
|
verbose: options.verbose ?? false,
|
|
405
405
|
seededHome,
|
|
406
406
|
});
|
|
407
|
-
telemetry.track('checkpoint', {
|
|
408
|
-
phase: 'plugin_installed',
|
|
409
|
-
pluginVersion: client.pluginVersion,
|
|
410
|
-
});
|
|
407
|
+
telemetry.track('checkpoint', { phase: 'plugin_installed', pluginVersion });
|
|
411
408
|
});
|
|
412
409
|
}
|
|
413
410
|
if (state.authMode !== AuthMode.Keyless) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -8,16 +8,12 @@ import { err, errLine, out, outLine } from '../lib/output.js';
|
|
|
8
8
|
import { detectRegistrations } from '../lib/registration-detect.js';
|
|
9
9
|
import { sendDoctorCompleted } from '../lib/setup-telemetry.js';
|
|
10
10
|
import { verifyMcpAuth } from '../lib/verify.js';
|
|
11
|
-
import { HERMES_PLUGIN_VERSION } from '../lib/hermes-plugin.js';
|
|
12
11
|
import { DEFAULT_MCP_URL } from '../lib/mcp-endpoint.js';
|
|
13
|
-
/** Skips mean no Hermes; stamping those buries the
|
|
12
|
+
/** Skips mean no Hermes; stamping those buries the installed share. */
|
|
14
13
|
function hermesPluginVersions(result) {
|
|
15
14
|
if (!result || result.check.status === 'skip')
|
|
16
15
|
return {};
|
|
17
|
-
return {
|
|
18
|
-
hermes_plugin_expected_version: HERMES_PLUGIN_VERSION,
|
|
19
|
-
...(result.observedVersion ? { hermes_plugin_version: result.observedVersion } : {}),
|
|
20
|
-
};
|
|
16
|
+
return result.observedVersion ? { hermes_plugin_version: result.observedVersion } : {};
|
|
21
17
|
}
|
|
22
18
|
export async function runDoctor(options) {
|
|
23
19
|
const statuses = detectRegistrations(options.harness ? [options.harness] : undefined);
|
|
@@ -5,7 +5,7 @@ import { removeCursorMcpServer, planCursorWrite } from './cursor-config.js';
|
|
|
5
5
|
import { planOmpWrite, removeOmpMcpServer } from './omp-config.js';
|
|
6
6
|
import { piMcpPath, planPiWrite, removePiMcpServer } from './pi-config.js';
|
|
7
7
|
import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
|
|
8
|
-
import { clearHermesWebBackends
|
|
8
|
+
import { clearHermesWebBackends } from './hermes-plugin.js';
|
|
9
9
|
import { HARNESS_DISPLAY_NAMES as DISPLAY_NAMES } from './harness-detect.js';
|
|
10
10
|
import { errLine } from './output.js';
|
|
11
11
|
// OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
|
|
@@ -19,8 +19,8 @@ export function planText(harness, mcpUrl, apiKey) {
|
|
|
19
19
|
return planPiWrite(mcpUrl, apiKey);
|
|
20
20
|
if (harness === 'hermes' && apiKey) {
|
|
21
21
|
return (`would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ` +
|
|
22
|
-
`${HERMES_KEY_VAR} to Hermes' .env, installs the tinyfish plugin
|
|
23
|
-
|
|
22
|
+
`${HERMES_KEY_VAR} to Hermes' .env, installs the tinyfish plugin from npm @latest, ` +
|
|
23
|
+
`and points its web backends at tinyfish)`);
|
|
24
24
|
}
|
|
25
25
|
if (harness === 'hermes') {
|
|
26
26
|
return 'would run `tinyfish connect hermes` (writes a keyless MCP header; no credential or plugin write)';
|
|
@@ -65,9 +65,7 @@ interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent'
|
|
|
65
65
|
apiKey?: string;
|
|
66
66
|
verbose: boolean;
|
|
67
67
|
seededHome?: string;
|
|
68
|
-
}) =>
|
|
69
|
-
/** Pin `extraPostInstall` installs, reported on the plugin_installed checkpoint. */
|
|
70
|
-
pluginVersion?: string;
|
|
68
|
+
}) => string | undefined;
|
|
71
69
|
/** Extra --uninstall row beyond `removals`; never runs pre-add. */
|
|
72
70
|
uninstallExtra?: {
|
|
73
71
|
args: string[];
|
|
@@ -8,7 +8,7 @@ import { TINYFISH_ACCESS_MODE_HEADER, TINYFISH_API_KEY_VAR, TINYFISH_KEYLESS_ACC
|
|
|
8
8
|
import { HARNESS_SPECS, NATIVE_HARNESSES, harnessSpec, } from './harness-spec.js';
|
|
9
9
|
import { HERMES_KEY_VAR, captureHermesKeyRestore, hermesEnvPath, resolveHermesHome, writeHermesKey, } from './hermes-env.js';
|
|
10
10
|
import { hermesConfigPath, readHermesEntry } from './hermes-config.js';
|
|
11
|
-
import {
|
|
11
|
+
import { installHermesPlugin, removeHermesMcpEntry, setHermesWebBackends, writeHermesMcpEntry, } from './hermes-plugin.js';
|
|
12
12
|
const HERMES_SEED_TIMEOUT_MS = 120_000;
|
|
13
13
|
const OPENCLAW_SKILL = '@tinyfish/tinyfish';
|
|
14
14
|
// --force makes this an unconditional overwrite, so the same call installs and refreshes.
|
|
@@ -205,8 +205,9 @@ function installHermesWebPlugin({ apiKey, verbose, seededHome, }) {
|
|
|
205
205
|
}
|
|
206
206
|
// Reuses the seed's home: a second `hermes dump` can disagree with the first.
|
|
207
207
|
const home = seededHome ?? requireHermesHome();
|
|
208
|
-
installHermesPlugin(home, apiKey, { verbose });
|
|
208
|
+
const pluginVersion = installHermesPlugin(home, apiKey, { verbose });
|
|
209
209
|
setHermesWebBackends(home);
|
|
210
|
+
return pluginVersion;
|
|
210
211
|
}
|
|
211
212
|
// OpenCode's TUI takes a positional as a project directory, so a bare `opencode "<prompt>"`
|
|
212
213
|
// would be read as a folder. The `--prompt` flag seeds the interactive TUI with a first message
|
|
@@ -275,7 +276,6 @@ const HARNESS_OVERRIDES = {
|
|
|
275
276
|
hermes: {
|
|
276
277
|
launchWalkthrough: launchHermesWalkthrough,
|
|
277
278
|
extraPostInstall: installHermesWebPlugin,
|
|
278
|
-
pluginVersion: HERMES_PLUGIN_VERSION,
|
|
279
279
|
seedKey: seedHermesKey,
|
|
280
280
|
prepareKeylessInstall: prepareHermesKeylessInstall,
|
|
281
281
|
},
|
|
@@ -310,7 +310,6 @@ function toNativeClient(harness) {
|
|
|
310
310
|
detachedLaunch: overrides.detachedLaunch,
|
|
311
311
|
postConnectNote: spec.postConnectNote,
|
|
312
312
|
extraPostInstall: overrides.extraPostInstall,
|
|
313
|
-
pluginVersion: overrides.pluginVersion,
|
|
314
313
|
uninstallExtra: spec.uninstallExtra,
|
|
315
314
|
};
|
|
316
315
|
if (spec.keyRequired) {
|
|
@@ -165,7 +165,6 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
165
165
|
unattended_safe: z.ZodBoolean;
|
|
166
166
|
}, z.core.$strip>>;
|
|
167
167
|
hermes_plugin_version: z.ZodOptional<z.ZodString>;
|
|
168
|
-
hermes_plugin_expected_version: z.ZodOptional<z.ZodString>;
|
|
169
168
|
}, z.core.$strip>;
|
|
170
169
|
export type CheckStatus = z.infer<typeof checkStatusSchema>;
|
|
171
170
|
export type CheckScope = z.infer<typeof checkScopeSchema>;
|
|
@@ -48,7 +48,6 @@ export const doctorReportSchema = z
|
|
|
48
48
|
repairs: z.array(doctorRepairSchema),
|
|
49
49
|
// Unbounded like cli_version: a throw here exits 2 unnecessarily.
|
|
50
50
|
hermes_plugin_version: z.string().optional(),
|
|
51
|
-
hermes_plugin_expected_version: z.string().optional(),
|
|
52
51
|
})
|
|
53
52
|
// Claiming ok while that scope failed is worse than no report.
|
|
54
53
|
.refine((report) => !report.ok_harnesses || !hasFailure(report.checks, 'harness'), {
|
|
@@ -41,9 +41,6 @@ export function telemetryPayload(report, offered, outcomes, options, durationMs)
|
|
|
41
41
|
...(report.hermes_plugin_version
|
|
42
42
|
? { hermes_plugin_version: report.hermes_plugin_version }
|
|
43
43
|
: {}),
|
|
44
|
-
...(report.hermes_plugin_expected_version
|
|
45
|
-
? { hermes_plugin_expected_version: report.hermes_plugin_expected_version }
|
|
46
|
-
: {}),
|
|
47
44
|
};
|
|
48
45
|
}
|
|
49
46
|
/** Both paths send keyless when there is no key; the route records those anonymously. */
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { type InstallOptions } from './cli-install.js';
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const
|
|
5
|
-
export declare
|
|
6
|
-
export declare function installHermesPlugin(home: string, apiKey: string, { verbose }: Pick<InstallOptions, 'verbose'>): void;
|
|
3
|
+
export declare const HERMES_PLUGIN_PACKAGE = "@tiny-fish/hermes";
|
|
4
|
+
export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish-io/tinyfish-web-agent-integrations/hermes --enable";
|
|
5
|
+
export declare function installHermesPlugin(home: string, apiKey: string, { verbose }: Pick<InstallOptions, 'verbose'>): string | undefined;
|
|
7
6
|
/** Absent `enabled` reads as enabled, so never write this field-by-field. */
|
|
8
7
|
export declare function writeHermesMcpEntry(home: string, mcpUrl: string, mode?: 'api-key' | 'keyless'): void;
|
|
9
8
|
/** Retracts our row; an absent `enabled` reads as enabled, so a partial one is live. */
|
|
@@ -9,16 +9,12 @@ import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted,
|
|
|
9
9
|
import { HARNESS_PROBE_TIMEOUT_MS, TINYFISH_ACCESS_MODE_HEADER, TINYFISH_KEYLESS_ACCESS_MODE, } from './constants.js';
|
|
10
10
|
import { HERMES_HEADER_TEMPLATE, HERMES_MCP_SERVER_KEY } from './hermes-config.js';
|
|
11
11
|
import { errLine, parseJson } from './output.js';
|
|
12
|
-
//
|
|
13
|
-
export const
|
|
14
|
-
//
|
|
15
|
-
export const HERMES_PLUGIN_VERSION = '0.1.0';
|
|
16
|
-
// Manifest `name:`, not the repo subdir; `plugins uninstall` keys on it.
|
|
17
|
-
const PLUGIN_SOURCE = 'tinyfish-io/tinyfish-web-agent-integrations/hermes';
|
|
18
|
-
const PLUGIN_REPO_URL = 'https://github.com/tinyfish-io/tinyfish-web-agent-integrations.git';
|
|
19
|
-
export const HERMES_PLUGIN_MANUAL_INSTALL = `hermes plugins install ${PLUGIN_SOURCE} --ref ${HERMES_PLUGIN_SHA} --enable`;
|
|
20
|
-
// Anchored to --ref: another rejected flag would fail the clone path too.
|
|
12
|
+
// Resolved at connect time, so plugin releases need no CLI release.
|
|
13
|
+
export const HERMES_PLUGIN_PACKAGE = '@tiny-fish/hermes';
|
|
14
|
+
// Anchored to --ref: an older Hermes rejects the flag and needs the plain URL.
|
|
21
15
|
const REF_UNSUPPORTED_PATTERN = /unrecognized arguments:[^\n]*--ref/;
|
|
16
|
+
// Tracks the repo head; npm @latest can trail a merge by one publish run.
|
|
17
|
+
export const HERMES_PLUGIN_MANUAL_INSTALL = 'hermes plugins install tinyfish-io/tinyfish-web-agent-integrations/hermes --enable';
|
|
22
18
|
function installSpawnEnv(home, apiKey) {
|
|
23
19
|
// Hermes' install-time requires_env check reads os.environ, not our .env seed.
|
|
24
20
|
return { ...process.env, HERMES_HOME: home, TINYFISH_API_KEY: apiKey };
|
|
@@ -42,42 +38,102 @@ function runGit(args) {
|
|
|
42
38
|
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
43
39
|
});
|
|
44
40
|
if (!result.error && result.status === 0)
|
|
45
|
-
return;
|
|
41
|
+
return result;
|
|
46
42
|
if (commandNotFound(result.error)) {
|
|
47
|
-
throw new ConnectStepError('git is required to stage the
|
|
43
|
+
throw new ConnectStepError('git is required to stage the plugin for Hermes; install git and run connect again', 'command_not_found');
|
|
48
44
|
}
|
|
49
45
|
// Built first: its interrupt check must run before any replay.
|
|
50
46
|
const error = spawnStepError('Could not stage the TinyFish Hermes plugin', result);
|
|
51
47
|
replay(capturedOutput(result));
|
|
52
48
|
throw error;
|
|
53
49
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
50
|
+
function runNpmDownload(tmp) {
|
|
51
|
+
// --prefix both downloads and extracts, so no tar dependency anywhere.
|
|
52
|
+
const result = spawn.sync('npm', [
|
|
53
|
+
'install',
|
|
54
|
+
`${HERMES_PLUGIN_PACKAGE}@latest`,
|
|
55
|
+
'--prefix',
|
|
56
|
+
tmp,
|
|
57
|
+
// Lifecycle scripts would run arbitrary code with the key in env.
|
|
58
|
+
'--ignore-scripts',
|
|
59
|
+
'--no-save',
|
|
60
|
+
'--no-audit',
|
|
61
|
+
'--no-fund',
|
|
62
|
+
'--loglevel=error',
|
|
63
|
+
], { encoding: 'utf8', maxBuffer: STEP_MAX_BUFFER, timeout: SKILL_INSTALL_TIMEOUT_MS });
|
|
64
|
+
if (!result.error && result.status === 0)
|
|
65
|
+
return;
|
|
66
|
+
if (commandNotFound(result.error)) {
|
|
67
|
+
throw new ConnectStepError(`npm is required to download the TinyFish Hermes plugin; to install it yourself run: ${HERMES_PLUGIN_MANUAL_INSTALL}`, 'command_not_found');
|
|
68
|
+
}
|
|
69
|
+
// Built first: its interrupt check must run before any replay.
|
|
70
|
+
const error = spawnStepError(`Could not download ${HERMES_PLUGIN_PACKAGE} from npm; to install it yourself run: ${HERMES_PLUGIN_MANUAL_INSTALL}`, result);
|
|
71
|
+
replay(capturedOutput(result));
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
function stagedPluginVersion(dir) {
|
|
57
75
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return runPluginInstall([`${pathToFileURL(tmp).href}#hermes`], env);
|
|
76
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
77
|
+
const version = parsed.version;
|
|
78
|
+
return typeof version === 'string' && version ? version : undefined;
|
|
62
79
|
}
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
65
82
|
}
|
|
66
83
|
}
|
|
84
|
+
// Hermes installs plugins only by git clone, so the payload gets a throwaway repo.
|
|
85
|
+
function stagePluginFromNpm(tmp) {
|
|
86
|
+
runNpmDownload(tmp);
|
|
87
|
+
const dir = path.join(tmp, 'node_modules', ...HERMES_PLUGIN_PACKAGE.split('/'));
|
|
88
|
+
// Empty hooksPath: a global hooks dir or init template must not run here.
|
|
89
|
+
const hooksDir = path.join(tmp, 'git-hooks');
|
|
90
|
+
fs.mkdirSync(hooksDir);
|
|
91
|
+
runGit(['-C', dir, 'init', '--quiet']);
|
|
92
|
+
runGit(['-C', dir, 'add', '-A']);
|
|
93
|
+
// Inline identity: fresh machines have no git config and commit fails without it.
|
|
94
|
+
runGit([
|
|
95
|
+
'-C',
|
|
96
|
+
dir,
|
|
97
|
+
'-c',
|
|
98
|
+
`core.hooksPath=${hooksDir}`,
|
|
99
|
+
'-c',
|
|
100
|
+
'user.name=tinyfish-cli',
|
|
101
|
+
'-c',
|
|
102
|
+
'user.email=support@tinyfish.io',
|
|
103
|
+
'commit',
|
|
104
|
+
'--quiet',
|
|
105
|
+
// A global commit.gpgSign would summon a signer this commit must not need.
|
|
106
|
+
'--no-gpg-sign',
|
|
107
|
+
'-m',
|
|
108
|
+
'stage @tiny-fish/hermes for install',
|
|
109
|
+
]);
|
|
110
|
+
const sha = String(runGit(['-C', dir, 'rev-parse', 'HEAD']).stdout ?? '').trim();
|
|
111
|
+
return { dir, sha, version: stagedPluginVersion(dir) };
|
|
112
|
+
}
|
|
67
113
|
export function installHermesPlugin(home, apiKey, { verbose }) {
|
|
68
114
|
errLine('Installing the TinyFish web plugin in Hermes...');
|
|
69
115
|
const env = installSpawnEnv(home, apiKey);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
116
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tinyfish-hermes-plugin-'));
|
|
117
|
+
try {
|
|
118
|
+
const staged = stagePluginFromNpm(tmp);
|
|
119
|
+
const url = pathToFileURL(staged.dir).href;
|
|
120
|
+
// --ref: a prior pinned install rejects an unpinned reinstall of the same name.
|
|
121
|
+
const primary = runPluginInstall([url, '--ref', staged.sha], env);
|
|
122
|
+
// A pre---ref Hermes never pins, so the plain URL is safe there.
|
|
123
|
+
const result = refUnsupported(primary) ? runPluginInstall([url], env) : primary;
|
|
124
|
+
if (result.error || result.status !== 0) {
|
|
125
|
+
// A declined security scan exits 1 here, and reconnecting repeats it.
|
|
126
|
+
const error = spawnStepError(`Could not install the TinyFish web plugin in Hermes; to install it yourself run: ${HERMES_PLUGIN_MANUAL_INSTALL}`, result);
|
|
127
|
+
replay(capturedOutput(result));
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (verbose)
|
|
131
|
+
replay(capturedOutput(result));
|
|
132
|
+
return staged.version;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
78
136
|
}
|
|
79
|
-
if (verbose)
|
|
80
|
-
replay(capturedOutput(result));
|
|
81
137
|
}
|
|
82
138
|
function hermesConfig(args, home) {
|
|
83
139
|
return spawn.sync('hermes', ['config', ...args], {
|
|
@@ -173,7 +173,6 @@ declare const doctorCompletedPayloadSchema: z.ZodObject<{
|
|
|
173
173
|
}, z.core.$strip>>;
|
|
174
174
|
duration_ms: z.ZodInt;
|
|
175
175
|
hermes_plugin_version: z.ZodOptional<z.ZodString>;
|
|
176
|
-
hermes_plugin_expected_version: z.ZodOptional<z.ZodString>;
|
|
177
176
|
}, z.core.$strip>;
|
|
178
177
|
declare const doctorCouldNotRunPayloadSchema: z.ZodObject<{
|
|
179
178
|
outcome: z.ZodLiteral<"could_not_run">;
|
|
@@ -282,7 +281,6 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
282
281
|
}, z.core.$strip>>;
|
|
283
282
|
duration_ms: z.ZodInt;
|
|
284
283
|
hermes_plugin_version: z.ZodOptional<z.ZodString>;
|
|
285
|
-
hermes_plugin_expected_version: z.ZodOptional<z.ZodString>;
|
|
286
284
|
}, z.core.$strip>, z.ZodObject<{
|
|
287
285
|
outcome: z.ZodLiteral<"could_not_run">;
|
|
288
286
|
error_class: z.ZodEnum<{
|
|
@@ -272,7 +272,6 @@ const doctorCompletedPayloadSchema = z.object({
|
|
|
272
272
|
duration_ms: z.int().nonnegative().max(3_600_000),
|
|
273
273
|
/** Bounded here, not on the report: a reject drops only telemetry. */
|
|
274
274
|
hermes_plugin_version: versionSchema.optional(),
|
|
275
|
-
hermes_plugin_expected_version: versionSchema.optional(),
|
|
276
275
|
});
|
|
277
276
|
// Detection threw, so there is no report to aggregate: enumerated outcome and timing only.
|
|
278
277
|
const doctorCouldNotRunPayloadSchema = z.object({
|