@tiny-fish/cli 0.44.0 → 0.45.1-next.350

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.
@@ -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) {
@@ -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 drift share. */
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, HERMES_PLUGIN_SHA } from './hermes-plugin.js';
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 at ` +
23
- `${HERMES_PLUGIN_SHA.slice(0, 12)}, and points its web backends at tinyfish)`);
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
- }) => void;
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 { HERMES_PLUGIN_VERSION, installHermesPlugin, removeHermesMcpEntry, setHermesWebBackends, writeHermesMcpEntry, } from './hermes-plugin.js';
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 HERMES_PLUGIN_SHA = "496cd63fefd982bbaa8a85ce78ef2270d700984f";
4
- export declare const HERMES_PLUGIN_VERSION = "0.1.0";
5
- export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish-io/tinyfish-web-agent-integrations/hermes --ref 496cd63fefd982bbaa8a85ce78ef2270d700984f --enable";
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
- // Bump per CLI release.
13
- export const HERMES_PLUGIN_SHA = '496cd63fefd982bbaa8a85ce78ef2270d700984f';
14
- // Bump with the SHA; the source is `hermes/plugin.yaml` at that commit.
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 pinned plugin; install git and run connect again', 'command_not_found');
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
- // Pre-`--ref` Hermes: stage the pin ourselves, mirroring `--ref` internals.
55
- function installFromClone(env) {
56
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tinyfish-hermes-plugin-'));
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
- runGit(['clone', '--depth', '1', PLUGIN_REPO_URL, tmp]);
59
- runGit(['-C', tmp, 'fetch', '--depth', '1', 'origin', HERMES_PLUGIN_SHA]);
60
- runGit(['-C', tmp, 'checkout', '--detach', HERMES_PLUGIN_SHA]);
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
- finally {
64
- fs.rmSync(tmp, { recursive: true, force: true });
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
- // --ref always: a same-source reinstall without it keeps the previous pin.
71
- const primary = runPluginInstall([PLUGIN_SOURCE, '--ref', HERMES_PLUGIN_SHA], env);
72
- const result = refUnsupported(primary) ? installFromClone(env) : primary;
73
- if (result.error || result.status !== 0) {
74
- // A declined security scan exits 1 here, and reconnecting repeats it.
75
- const error = spawnStepError(`Could not install the TinyFish web plugin in Hermes; to install it yourself run: ${HERMES_PLUGIN_MANUAL_INSTALL}`, result);
76
- replay(capturedOutput(result));
77
- throw error;
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.44.0",
3
+ "version": "0.45.1-next.350",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {