@tiny-fish/cli 0.52.3-next.426 → 0.52.3-next.428
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/lib/connect-runtime.d.ts +3 -2
- package/dist/lib/connect-runtime.js +12 -8
- package/dist/lib/hermes-env.js +2 -2
- package/dist/lib/hermes-plugin.d.ts +1 -1
- package/dist/lib/hermes-plugin.js +38 -14
- package/dist/lib/output.d.ts +1 -0
- package/dist/lib/output.js +6 -0
- package/dist/lib/registration-detect.js +2 -2
- package/package.json +1 -1
|
@@ -42,12 +42,13 @@ export interface SpawnStepResult {
|
|
|
42
42
|
status?: number | null;
|
|
43
43
|
signal?: string | null;
|
|
44
44
|
error?: Error;
|
|
45
|
-
/** Inherited stdio leaves
|
|
45
|
+
/** Inherited stdio leaves these null; only piped runs yield text. */
|
|
46
|
+
stdout?: unknown;
|
|
46
47
|
stderr?: unknown;
|
|
47
48
|
}
|
|
48
49
|
/** Bounded removal spawn shared by install cleanup and --uninstall; Ctrl+C throws. */
|
|
49
50
|
export declare function spawnRemoval(command: string, args: readonly string[], timeoutMs?: number): import("child_process").SpawnSyncReturns<string>;
|
|
50
|
-
/** Outcome plus a bounded
|
|
51
|
+
/** Outcome plus a bounded output tail; the route caps the rest. */
|
|
51
52
|
export declare function spawnFailureDetail(result: SpawnStepResult): string;
|
|
52
53
|
/** Classifies a spawn failure; interrupts throw instead. */
|
|
53
54
|
export declare function spawnStepError(message: string, result: SpawnStepResult, failureDetail?: string): ConnectStepError;
|
|
@@ -3,7 +3,7 @@ import spawn from 'cross-spawn';
|
|
|
3
3
|
import { CONNECT_ATTEMPT_ENV, isAttemptId, resolvedApiKey, takePendingConnectAttempt, } from './auth.js';
|
|
4
4
|
import { CLI_INVOCATION_ID, CLI_VERSION } from './constants.js';
|
|
5
5
|
import { detectHarness, detectHumanInitiated, detectRuntimeEnv } from './harness.js';
|
|
6
|
-
import { boundedVersion, errLine, sanitizeLine } from './output.js';
|
|
6
|
+
import { boundedVersion, errLine, noColourEnv, sanitizeLine } from './output.js';
|
|
7
7
|
import { isOlder } from './cli-presence.js';
|
|
8
8
|
import { postConnectEvent, telemetryDisabled } from './setup-telemetry.js';
|
|
9
9
|
import { installSignalGuard } from './signals.js';
|
|
@@ -78,15 +78,19 @@ export function spawnRemoval(command, args, timeoutMs = NON_INTERACTIVE_TIMEOUT_
|
|
|
78
78
|
return result;
|
|
79
79
|
}
|
|
80
80
|
// Sanitized before the cut so the count is not spent on invisible bytes.
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
? sanitizeLine(result.stderr.trim()).slice(-STDERR_TAIL_MAX_CHARS)
|
|
81
|
+
const OUTPUT_TAIL_MAX_CHARS = 400;
|
|
82
|
+
function outputTail(stream) {
|
|
83
|
+
return typeof stream === 'string'
|
|
84
|
+
? sanitizeLine(stream.trim()).slice(-OUTPUT_TAIL_MAX_CHARS)
|
|
86
85
|
: '';
|
|
86
|
+
}
|
|
87
|
+
/** Outcome plus a bounded output tail; the route caps the rest. */
|
|
88
|
+
export function spawnFailureDetail(result) {
|
|
89
|
+
// Rich-based CLIs (Hermes) print refusals to stdout and leave stderr empty.
|
|
90
|
+
const tail = outputTail(result.stderr) || outputTail(result.stdout);
|
|
87
91
|
// A signal kill leaves status null, where the signal is the diagnosis.
|
|
88
92
|
const outcome = result.signal ? `signal=${result.signal}` : `exit=${result.status}`;
|
|
89
|
-
return `${outcome}${
|
|
93
|
+
return `${outcome}${tail ? ` | ${tail}` : ''}`;
|
|
90
94
|
}
|
|
91
95
|
/** Classifies a spawn failure; interrupts throw instead. */
|
|
92
96
|
export function spawnStepError(message, result, failureDetail) {
|
|
@@ -221,7 +225,7 @@ function supportProbeOutput(client) {
|
|
|
221
225
|
const result = spawn.sync(client.command, client.supportCheck.args, {
|
|
222
226
|
encoding: 'utf8',
|
|
223
227
|
// Agent harnesses export FORCE_COLOR, which makes clients colourise even a piped --help.
|
|
224
|
-
env:
|
|
228
|
+
env: noColourEnv(),
|
|
225
229
|
timeout: probeTimeoutMs(client),
|
|
226
230
|
});
|
|
227
231
|
// 127 is the shell's not-found code; a wrapper shim with no real binary behind it exits that.
|
package/dist/lib/hermes-env.js
CHANGED
|
@@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
5
5
|
import spawn from 'cross-spawn';
|
|
6
6
|
import { HERMES_COLD_START_TIMEOUT_MS } from './constants.js';
|
|
7
7
|
import { throwIfInterrupted } from './connect-runtime.js';
|
|
8
|
-
import { sanitizeLine } from './output.js';
|
|
8
|
+
import { noColourEnv, sanitizeLine } from './output.js';
|
|
9
9
|
// display_hermes_home() abbreviates under $HOME, so the value is not always absolute.
|
|
10
10
|
const HERMES_HOME_LINE = /^\s*hermes_home:\s*(\S.*?)\s*$/m;
|
|
11
11
|
// Consistency, not speed: doctor's second probe timing out would downgrade
|
|
@@ -29,7 +29,7 @@ export function resolveHermesHome() {
|
|
|
29
29
|
function probeHermesHome() {
|
|
30
30
|
const result = spawn.sync('hermes', ['dump'], {
|
|
31
31
|
encoding: 'utf8',
|
|
32
|
-
env:
|
|
32
|
+
env: noColourEnv(),
|
|
33
33
|
timeout: HERMES_COLD_START_TIMEOUT_MS,
|
|
34
34
|
});
|
|
35
35
|
// Ctrl+C would otherwise read as `exit` and be cached.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { type InstallOptions } from './cli-install.js';
|
|
3
|
-
export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish --enable";
|
|
3
|
+
export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish --enable --force";
|
|
4
4
|
export declare function installHermesPlugin(home: string, apiKey: string, { verbose }: Pick<InstallOptions, 'verbose'>): string | undefined;
|
|
5
5
|
/** Absent `enabled` reads as enabled, so never write this field-by-field. */
|
|
6
6
|
export declare function writeHermesMcpEntry(home: string, mcpUrl: string, mode?: 'api-key' | 'keyless'): void;
|
|
@@ -1,26 +1,50 @@
|
|
|
1
1
|
import spawn from 'cross-spawn';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { capturedOutput, replay,
|
|
3
|
+
import { captureStdio, capturedOutput, replay, STEP_MAX_BUFFER, } from './cli-install.js';
|
|
4
4
|
import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
|
|
5
5
|
import { HARNESS_PROBE_TIMEOUT_MS, TINYFISH_ACCESS_MODE_HEADER, TINYFISH_KEYLESS_ACCESS_MODE, } from './constants.js';
|
|
6
6
|
import { HERMES_HEADER_TEMPLATE, HERMES_MCP_SERVER_KEY } from './config/hermes-config.js';
|
|
7
|
-
import { errLine, parseJson } from './output.js';
|
|
8
|
-
export const HERMES_PLUGIN_MANUAL_INSTALL = 'hermes plugins install tinyfish --enable';
|
|
9
|
-
//
|
|
10
|
-
const HERMES_CATALOG_MIN_VERSION = '
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
import { errLine, noColourEnv, parseJson, sanitizeLine } from './output.js';
|
|
8
|
+
export const HERMES_PLUGIN_MANUAL_INSTALL = 'hermes plugins install tinyfish --enable --force';
|
|
9
|
+
// Older Hermes cannot parse our catalog tier, so the name misses.
|
|
10
|
+
const HERMES_CATALOG_MIN_VERSION = 'v0.22.3';
|
|
11
|
+
const HERMES_PLUGIN_REPO = 'tinyfish-io/tinyfish-web-agent-integrations/hermes';
|
|
12
|
+
// Mirrors the catalog pin; bump it with each catalog re-pin.
|
|
13
|
+
const HERMES_PLUGIN_REF = '770a3765fa6249b1f6a6153ddaaf65b76bf8fdd4';
|
|
14
|
+
const HERMES_PLUGIN_REPO_INSTALL = `${HERMES_PLUGIN_REPO} --ref ${HERMES_PLUGIN_REF}`;
|
|
15
|
+
// Catalog-era Hermes, then the community-index era (<= v2026.8.31).
|
|
16
|
+
const CATALOG_MISSES = ['not in the Hermes plugin catalog', 'not found in the community index'];
|
|
17
|
+
function runPluginInstall(home, apiKey, source) {
|
|
18
|
+
return spawn.sync('hermes', ['plugins', 'install', ...source, '--enable', '--force'], {
|
|
19
|
+
...captureStdio(false),
|
|
17
20
|
// Hermes' install-time requires_env check reads os.environ, not our .env seed.
|
|
18
|
-
env: {
|
|
21
|
+
env: noColourEnv({ HERMES_HOME: home, TINYFISH_API_KEY: apiKey }),
|
|
19
22
|
});
|
|
23
|
+
}
|
|
24
|
+
// Rich wraps stdout at 80 columns, which can split the phrase.
|
|
25
|
+
function catalogMiss(result) {
|
|
26
|
+
if (result.error || result.signal || result.status === 0)
|
|
27
|
+
return false;
|
|
28
|
+
const output = sanitizeLine(capturedOutput(result)).replace(/\s+/g, ' ');
|
|
29
|
+
return CATALOG_MISSES.some((miss) => output.includes(miss));
|
|
30
|
+
}
|
|
31
|
+
export function installHermesPlugin(home, apiKey, { verbose }) {
|
|
32
|
+
errLine('Installing the TinyFish web plugin in Hermes...');
|
|
33
|
+
let result = runPluginInstall(home, apiKey, ['tinyfish']);
|
|
34
|
+
throwIfInterrupted(result);
|
|
35
|
+
const fellBack = catalogMiss(result);
|
|
36
|
+
if (fellBack) {
|
|
37
|
+
if (verbose)
|
|
38
|
+
replay(capturedOutput(result));
|
|
39
|
+
result = runPluginInstall(home, apiKey, [HERMES_PLUGIN_REPO, '--ref', HERMES_PLUGIN_REF]);
|
|
40
|
+
}
|
|
20
41
|
if (result.error || result.status !== 0) {
|
|
21
42
|
// A declined security scan exits 1 here, and reconnecting repeats it.
|
|
22
43
|
const error = spawnStepError(`Could not install the TinyFish web plugin in Hermes; to install it yourself run: ` +
|
|
23
|
-
`${HERMES_PLUGIN_MANUAL_INSTALL} (Hermes older than ${HERMES_CATALOG_MIN_VERSION} needs \`hermes update\` first)
|
|
44
|
+
`${HERMES_PLUGIN_MANUAL_INSTALL} (Hermes older than ${HERMES_CATALOG_MIN_VERSION} needs \`hermes update\` first)` +
|
|
45
|
+
(fellBack
|
|
46
|
+
? `, or: hermes plugins install ${HERMES_PLUGIN_REPO_INSTALL} --enable --force`
|
|
47
|
+
: ''), result);
|
|
24
48
|
replay(capturedOutput(result));
|
|
25
49
|
throw error;
|
|
26
50
|
}
|
|
@@ -105,7 +129,7 @@ export function readHermesPluginStatus(home) {
|
|
|
105
129
|
encoding: 'utf8',
|
|
106
130
|
maxBuffer: STEP_MAX_BUFFER,
|
|
107
131
|
timeout: HARNESS_PROBE_TIMEOUT_MS,
|
|
108
|
-
env: {
|
|
132
|
+
env: noColourEnv({ HERMES_HOME: home }),
|
|
109
133
|
});
|
|
110
134
|
// A timeout also lands in result.error, where "not installed" would be a lie.
|
|
111
135
|
if (result.error) {
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare class ApiError extends Error {
|
|
|
4
4
|
constructor(status: number, message: string, code?: string | undefined);
|
|
5
5
|
}
|
|
6
6
|
export declare function out(data: unknown): void;
|
|
7
|
+
export declare function noColourEnv(extra?: Record<string, string>): NodeJS.ProcessEnv;
|
|
7
8
|
/** The only writer for untrusted text: strips ANSI and C0/C1 controls. */
|
|
8
9
|
export declare function sanitizeLine(text: string): string;
|
|
9
10
|
/** Every version the route caps at 32 printable ASCII chars. */
|
package/dist/lib/output.js
CHANGED
|
@@ -12,6 +12,12 @@ export class ApiError extends Error {
|
|
|
12
12
|
export function out(data) {
|
|
13
13
|
process.stdout.write(JSON.stringify(data) + '\n');
|
|
14
14
|
}
|
|
15
|
+
// Rich forces colour on any non-empty FORCE_COLOR, '0' included.
|
|
16
|
+
export function noColourEnv(extra = {}) {
|
|
17
|
+
const env = { ...process.env, NO_COLOR: '1', ...extra };
|
|
18
|
+
delete env['FORCE_COLOR'];
|
|
19
|
+
return env;
|
|
20
|
+
}
|
|
15
21
|
/** The only writer for untrusted text: strips ANSI and C0/C1 controls. */
|
|
16
22
|
export function sanitizeLine(text) {
|
|
17
23
|
// eslint-disable-next-line no-control-regex
|
|
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
import { loadConfig, matchesCliKey } from './auth.js';
|
|
6
6
|
import { NATIVE_BY_HARNESS } from './connect-clients.js';
|
|
7
7
|
import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
|
|
8
|
-
import { errLine } from './output.js';
|
|
8
|
+
import { errLine, noColourEnv } from './output.js';
|
|
9
9
|
import { readClineTinyfishEntry } from './config/cline-config.js';
|
|
10
10
|
import { readCommandCodeTinyfishEntry } from './config/command-code-config.js';
|
|
11
11
|
import { readCursorTinyfishEntry } from './config/cursor-config.js';
|
|
@@ -68,7 +68,7 @@ function runProbe(command, args) {
|
|
|
68
68
|
timeout: HARNESS_PROBE_TIMEOUT_MS,
|
|
69
69
|
// Belt and braces only: agents export FORCE_COLOR, and `NO_COLOR` does not fully silence
|
|
70
70
|
// OpenCode, so the strip below stays load-bearing rather than a fallback.
|
|
71
|
-
env:
|
|
71
|
+
env: noColourEnv(),
|
|
72
72
|
});
|
|
73
73
|
if (result.error) {
|
|
74
74
|
const code = result.error.code;
|