@tiny-fish/cli 0.34.1-next.282 → 0.34.1-next.284
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
CHANGED
|
@@ -346,6 +346,16 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
346
346
|
telemetry.track('checkpoint', { phase: 'skill_installed' });
|
|
347
347
|
});
|
|
348
348
|
}
|
|
349
|
+
const extraPostInstall = client.extraPostInstall;
|
|
350
|
+
if (extraPostInstall) {
|
|
351
|
+
await attempt('plugin_install', () => {
|
|
352
|
+
extraPostInstall({
|
|
353
|
+
apiKey: validatedApiKey(options.apiKey),
|
|
354
|
+
verbose: options.verbose ?? false,
|
|
355
|
+
});
|
|
356
|
+
telemetry.track('checkpoint', { phase: 'plugin_installed' });
|
|
357
|
+
});
|
|
358
|
+
}
|
|
349
359
|
// Before auth: a failed sign-in must not lose the harness record.
|
|
350
360
|
saveConnectContext(client.connectClient, telemetry.attemptId, state.authMode);
|
|
351
361
|
await attempt('authentication', () => {
|
|
@@ -69,6 +69,11 @@ interface BaseMcpClient extends SupportedCommand {
|
|
|
69
69
|
detachedLaunch?: () => WalkthroughOutcome;
|
|
70
70
|
/** Printed after a successful connect, on both the launch and non-launch paths. */
|
|
71
71
|
postConnectNote?: string;
|
|
72
|
+
/** Post-install like the skill step: failures warn, never fail the attempt. */
|
|
73
|
+
extraPostInstall?: (options: {
|
|
74
|
+
apiKey?: string;
|
|
75
|
+
verbose: boolean;
|
|
76
|
+
}) => void;
|
|
72
77
|
}
|
|
73
78
|
/** Keyless installs use this argv; the harness signs itself in. */
|
|
74
79
|
export interface OauthCapableMcpClient extends BaseMcpClient {
|
|
@@ -7,6 +7,7 @@ import { ConnectStepError, spawnStepError, } from './connect-runtime.js';
|
|
|
7
7
|
import { TINYFISH_API_KEY_VAR } from './constants.js';
|
|
8
8
|
import { HERMES_KEY_VAR, captureHermesKeyRestore, hermesEnvPath, resolveHermesHome, writeHermesKey, } from './hermes-env.js';
|
|
9
9
|
import { hermesConfigPath, readHermesEntry } from './hermes-config.js';
|
|
10
|
+
import { installHermesPlugin, setHermesWebBackends } from './hermes-plugin.js';
|
|
10
11
|
const HERMES_SEED_TIMEOUT_MS = 120_000;
|
|
11
12
|
// An old install is only one reason a flag can go unseen, so no message asserts the cause.
|
|
12
13
|
const SUPPORT_CHECK_DEBUG_HINT = ' Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.';
|
|
@@ -259,12 +260,16 @@ export function hermesRegistrationEnabled(home) {
|
|
|
259
260
|
// The tag keeps the path-bearing prose out of telemetry.
|
|
260
261
|
return { ok: false, detail, tag: `hermes_entry_${entry.state}` };
|
|
261
262
|
}
|
|
262
|
-
|
|
263
|
+
// A plain Error settles unexpected_error; cli_connect_stage is load-bearing.
|
|
264
|
+
function requireHermesHome() {
|
|
263
265
|
const home = resolveHermesHome();
|
|
264
|
-
// A plain Error settles unexpected_error; cli_connect_stage is load-bearing.
|
|
265
266
|
if (!home) {
|
|
266
267
|
throw new ConnectStepError("Could not determine Hermes' home directory from `hermes dump`", 'invalid_config', { failureDetail: 'hermes_home_undetermined' });
|
|
267
268
|
}
|
|
269
|
+
return home;
|
|
270
|
+
}
|
|
271
|
+
function seedHermesKey(apiKey) {
|
|
272
|
+
const home = requireHermesHome();
|
|
268
273
|
const restore = captureHermesKeyRestore(home);
|
|
269
274
|
writeHermesKey(home, apiKey);
|
|
270
275
|
return {
|
|
@@ -287,6 +292,15 @@ function rollbackHermesKey(home, restore) {
|
|
|
287
292
|
warnLine(`Could not restore ${HERMES_KEY_VAR} in ${hermesEnvPath(home)}; check it by hand.`);
|
|
288
293
|
}
|
|
289
294
|
}
|
|
295
|
+
// Hermes is keyRequired; a missing key here is a resolution bug.
|
|
296
|
+
function installHermesWebPlugin({ apiKey, verbose }) {
|
|
297
|
+
if (!apiKey) {
|
|
298
|
+
throw new ConnectStepError('No TinyFish API key was available to install the Hermes web plugin', 'invalid_config');
|
|
299
|
+
}
|
|
300
|
+
const home = requireHermesHome();
|
|
301
|
+
installHermesPlugin(home, apiKey, { verbose });
|
|
302
|
+
setHermesWebBackends(home);
|
|
303
|
+
}
|
|
290
304
|
export const HERMES = {
|
|
291
305
|
command: 'hermes',
|
|
292
306
|
connectClient: 'hermes',
|
|
@@ -305,6 +319,7 @@ export const HERMES = {
|
|
|
305
319
|
seedKey: seedHermesKey,
|
|
306
320
|
},
|
|
307
321
|
removals: [{ args: ['mcp', 'remove', 'tinyfish'], label: 'TinyFish registration from Hermes' }],
|
|
322
|
+
extraPostInstall: installHermesWebPlugin,
|
|
308
323
|
launchWalkthrough: launchHermesWalkthrough,
|
|
309
324
|
postConnectNote: 'Restart your Hermes session to pick up TinyFish — Hermes discovers MCP servers at startup.',
|
|
310
325
|
};
|
|
@@ -2,11 +2,11 @@ import { AuthMode } from './harness-detect.js';
|
|
|
2
2
|
export declare const NON_INTERACTIVE_TIMEOUT_MS = 10000;
|
|
3
3
|
export type AgentClient = 'claude-code' | 'codex' | 'cursor' | 'grok' | 'hermes' | 'openclaw' | 'opencode';
|
|
4
4
|
type ConnectStage = 'started' | 'checkpoint' | 'completed' | 'failed' | 'aborted' | 'post_install_failed';
|
|
5
|
-
export type ConnectFailureStage = 'prerequisite_check' | 'registration_cleanup' | 'registration' | 'registration_or_authentication' | 'client_oauth' | 'cli_install' | 'skill_install' | 'authentication' | 'walkthrough_launch' | 'cli_verification';
|
|
5
|
+
export type ConnectFailureStage = 'prerequisite_check' | 'registration_cleanup' | 'registration' | 'registration_or_authentication' | 'client_oauth' | 'cli_install' | 'skill_install' | 'plugin_install' | 'authentication' | 'walkthrough_launch' | 'cli_verification';
|
|
6
6
|
export declare const CONNECT_FAILURE_REASONS: readonly ["harness_not_installed", "harness_command_unsupported", "harness_too_old", "key_auth_unsupported", "command_not_found", "timeout", "spawn_error", "nonzero_exit", "invalid_config", "user_abandoned", "process_terminated", "unexpected_error"];
|
|
7
7
|
export type ConnectFailureReason = (typeof CONNECT_FAILURE_REASONS)[number];
|
|
8
8
|
export type ConnectAuthMode = Exclude<AuthMode, AuthMode.Unknown> | 'deferred';
|
|
9
|
-
export type ConnectCheckpoint = 'prerequisite_ok' | 'registered' | 'oauth_done' | 'cli_installed' | 'skill_installed' | 'authenticated' | 'cli_verified' | 'walkthrough_launched' | 'walkthrough_prompt_printed';
|
|
9
|
+
export type ConnectCheckpoint = 'prerequisite_ok' | 'registered' | 'oauth_done' | 'cli_installed' | 'skill_installed' | 'plugin_installed' | 'authenticated' | 'cli_verified' | 'walkthrough_launched' | 'walkthrough_prompt_printed';
|
|
10
10
|
export type CliFallbackOutcome = 'cli_verified' | 'cli_install_failed' | 'no_credentials' | 'invalid_credentials' | 'authentication_failed' | 'verification_failed' | 'abandoned';
|
|
11
11
|
/** Ctrl+C killed a setup child — abandonment, not error. */
|
|
12
12
|
export declare class ConnectInterruptedError extends Error {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type InstallOptions } from './cli-install.js';
|
|
2
|
+
export declare const HERMES_PLUGIN_SHA = "496cd63fefd982bbaa8a85ce78ef2270d700984f";
|
|
3
|
+
export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish-io/tinyfish-web-agent-integrations/hermes --ref 496cd63fefd982bbaa8a85ce78ef2270d700984f --enable";
|
|
4
|
+
export declare function installHermesPlugin(home: string, apiKey: string, { verbose }: Pick<InstallOptions, 'verbose'>): void;
|
|
5
|
+
/** After install only: backends must never name an absent provider. */
|
|
6
|
+
export declare function setHermesWebBackends(home: string): void;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import spawn from 'cross-spawn';
|
|
6
|
+
import { capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
|
|
7
|
+
import { commandNotFound, ConnectStepError, spawnStepError } from './connect-runtime.js';
|
|
8
|
+
import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
|
|
9
|
+
import { errLine } from './output.js';
|
|
10
|
+
// Bump per CLI release.
|
|
11
|
+
export const HERMES_PLUGIN_SHA = '496cd63fefd982bbaa8a85ce78ef2270d700984f';
|
|
12
|
+
const PLUGIN_SOURCE = 'tinyfish-io/tinyfish-web-agent-integrations/hermes';
|
|
13
|
+
const PLUGIN_REPO_URL = 'https://github.com/tinyfish-io/tinyfish-web-agent-integrations.git';
|
|
14
|
+
export const HERMES_PLUGIN_MANUAL_INSTALL = `hermes plugins install ${PLUGIN_SOURCE} --ref ${HERMES_PLUGIN_SHA} --enable`;
|
|
15
|
+
// Anchored to --ref: another rejected flag would fail the clone path too.
|
|
16
|
+
const REF_UNSUPPORTED_PATTERN = /unrecognized arguments:[^\n]*--ref/;
|
|
17
|
+
function installSpawnEnv(home, apiKey) {
|
|
18
|
+
// Hermes' install-time requires_env check reads os.environ, not our .env seed.
|
|
19
|
+
return { ...process.env, HERMES_HOME: home, TINYFISH_API_KEY: apiKey };
|
|
20
|
+
}
|
|
21
|
+
function runPluginInstall(source, env) {
|
|
22
|
+
// Captured even under --verbose: the fallback matches on stderr.
|
|
23
|
+
return spawn.sync('hermes', ['plugins', 'install', ...source, '--enable', '--force'], {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
maxBuffer: STEP_MAX_BUFFER,
|
|
26
|
+
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
27
|
+
env,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function refUnsupported(result) {
|
|
31
|
+
return result.status !== 0 && REF_UNSUPPORTED_PATTERN.test(String(result.stderr ?? ''));
|
|
32
|
+
}
|
|
33
|
+
function runGit(args) {
|
|
34
|
+
const result = spawn.sync('git', args, {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
maxBuffer: STEP_MAX_BUFFER,
|
|
37
|
+
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
38
|
+
});
|
|
39
|
+
if (!result.error && result.status === 0)
|
|
40
|
+
return;
|
|
41
|
+
if (commandNotFound(result.error)) {
|
|
42
|
+
throw new ConnectStepError('git is required to stage the pinned plugin; install git and run connect again', 'command_not_found');
|
|
43
|
+
}
|
|
44
|
+
// Built first: its interrupt check must run before any replay.
|
|
45
|
+
const error = spawnStepError('Could not stage the TinyFish Hermes plugin', result);
|
|
46
|
+
replay(capturedOutput(result));
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
// Pre-`--ref` Hermes: stage the pin ourselves, mirroring `--ref` internals.
|
|
50
|
+
function installFromClone(env) {
|
|
51
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tinyfish-hermes-plugin-'));
|
|
52
|
+
try {
|
|
53
|
+
runGit(['clone', '--depth', '1', PLUGIN_REPO_URL, tmp]);
|
|
54
|
+
runGit(['-C', tmp, 'fetch', '--depth', '1', 'origin', HERMES_PLUGIN_SHA]);
|
|
55
|
+
runGit(['-C', tmp, 'checkout', '--detach', HERMES_PLUGIN_SHA]);
|
|
56
|
+
return runPluginInstall([`${pathToFileURL(tmp).href}#hermes`], env);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function installHermesPlugin(home, apiKey, { verbose }) {
|
|
63
|
+
errLine('Installing the TinyFish web plugin in Hermes...');
|
|
64
|
+
const env = installSpawnEnv(home, apiKey);
|
|
65
|
+
// --ref always: a same-source reinstall without it keeps the previous pin.
|
|
66
|
+
const primary = runPluginInstall([PLUGIN_SOURCE, '--ref', HERMES_PLUGIN_SHA], env);
|
|
67
|
+
const result = refUnsupported(primary) ? installFromClone(env) : primary;
|
|
68
|
+
if (result.error || result.status !== 0) {
|
|
69
|
+
// A declined security scan exits 1 here, and reconnecting repeats it.
|
|
70
|
+
const error = spawnStepError(`Could not install the TinyFish web plugin in Hermes; to install it yourself run: ${HERMES_PLUGIN_MANUAL_INSTALL}`, result);
|
|
71
|
+
replay(capturedOutput(result));
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
if (verbose)
|
|
75
|
+
replay(capturedOutput(result));
|
|
76
|
+
}
|
|
77
|
+
function hermesConfig(args, home) {
|
|
78
|
+
return spawn.sync('hermes', ['config', ...args], {
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
maxBuffer: STEP_MAX_BUFFER,
|
|
81
|
+
timeout: HARNESS_PROBE_TIMEOUT_MS,
|
|
82
|
+
env: { ...process.env, HERMES_HOME: home },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function configWrite(home, args) {
|
|
86
|
+
const result = hermesConfig(args, home);
|
|
87
|
+
if (!result.error && result.status === 0)
|
|
88
|
+
return;
|
|
89
|
+
// Built first: its interrupt check must run before any replay.
|
|
90
|
+
const error = spawnStepError(`Could not run \`hermes config ${args.join(' ')}\``, result);
|
|
91
|
+
replay(capturedOutput(result));
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
const HERMES_BACKEND_KEYS = ['web.search_backend', 'web.extract_backend'];
|
|
95
|
+
/** After install only: backends must never name an absent provider. */
|
|
96
|
+
export function setHermesWebBackends(home) {
|
|
97
|
+
for (const key of HERMES_BACKEND_KEYS)
|
|
98
|
+
configWrite(home, ['set', key, 'tinyfish']);
|
|
99
|
+
}
|