@tiny-fish/cli 0.42.1 → 0.45.1-next.352
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/README.md +11 -7
- package/dist/commands/auth.js +11 -1
- package/dist/commands/connect.js +112 -50
- package/dist/commands/doctor.js +4 -7
- package/dist/commands/run.js +5 -2
- package/dist/lib/auth.d.ts +2 -1
- package/dist/lib/auth.js +3 -1
- package/dist/lib/client.d.ts +2 -1
- package/dist/lib/client.js +50 -9
- package/dist/lib/connect-all-auth.js +3 -0
- package/dist/lib/connect-all-uninstall.js +6 -4
- package/dist/lib/connect-all.js +10 -4
- package/dist/lib/connect-clients.d.ts +13 -8
- package/dist/lib/connect-clients.js +51 -27
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/doctor-checks.js +4 -0
- package/dist/lib/doctor-report.d.ts +2 -1
- package/dist/lib/doctor-report.js +1 -2
- package/dist/lib/doctor-telemetry.js +0 -3
- package/dist/lib/harness-detect.d.ts +1 -0
- package/dist/lib/harness-detect.js +1 -0
- package/dist/lib/harness-spec.d.ts +4 -0
- package/dist/lib/harness-spec.js +3 -0
- package/dist/lib/hermes-config.d.ts +4 -0
- package/dist/lib/hermes-config.js +12 -3
- package/dist/lib/hermes-plugin.d.ts +4 -5
- package/dist/lib/hermes-plugin.js +92 -36
- package/dist/lib/mcp-json-config.d.ts +4 -2
- package/dist/lib/mcp-json-config.js +20 -10
- package/dist/lib/omp-config.js +5 -3
- package/dist/lib/output.js +7 -1
- package/dist/lib/registration-detect.js +12 -4
- package/dist/lib/setup-telemetry.d.ts +12 -2
- package/dist/lib/setup-telemetry.js +1 -1
- package/dist/lib/verify.d.ts +1 -1
- package/dist/lib/verify.js +29 -9
- package/package.json +2 -2
|
@@ -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.
|
|
@@ -17,11 +17,13 @@ export function planText(harness, mcpUrl, apiKey) {
|
|
|
17
17
|
return planOmpWrite(mcpUrl, apiKey);
|
|
18
18
|
if (harness === 'pi')
|
|
19
19
|
return planPiWrite(mcpUrl, apiKey);
|
|
20
|
-
// Only the keyed path seeds the .env; a keyless Hermes install writes nothing.
|
|
21
20
|
if (harness === 'hermes' && apiKey) {
|
|
22
21
|
return (`would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ` +
|
|
23
|
-
`${HERMES_KEY_VAR} to Hermes' .env, installs the tinyfish plugin
|
|
24
|
-
|
|
22
|
+
`${HERMES_KEY_VAR} to Hermes' .env, installs the tinyfish plugin from npm @latest, ` +
|
|
23
|
+
`and points its web backends at tinyfish)`);
|
|
24
|
+
}
|
|
25
|
+
if (harness === 'hermes') {
|
|
26
|
+
return 'would run `tinyfish connect hermes` (writes a keyless MCP header; no credential or plugin write)';
|
|
25
27
|
}
|
|
26
28
|
return `would run \`tinyfish connect ${harness}\` (harness-owned MCP write; no local file touched by the CLI)`;
|
|
27
29
|
}
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -8,10 +8,11 @@ import { authGate, mapConnectError, OAUTH_SIGN_IN_TIMEOUT_MS } from './connect-a
|
|
|
8
8
|
import { actionable, computeExitCode, reloadHint, renderSummary } from './connect-all-summary.js';
|
|
9
9
|
import { planText, uninstallHarness, uninstallPlanText } from './connect-all-uninstall.js';
|
|
10
10
|
import { createStdinPrompt, runCliFallback } from './connect-fallback.js';
|
|
11
|
+
import { KEYLESS_ONBOARDING_PROMPT } from './connect-clients.js';
|
|
11
12
|
import { gateApiKey } from './connect-preflight.js';
|
|
12
13
|
import { ConnectInterruptedError } from './connect-runtime.js';
|
|
13
14
|
import { pickHarnesses } from './connect-picker.js';
|
|
14
|
-
import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, } from './harness-detect.js';
|
|
15
|
+
import { commandOnPath, detectInstalledHarnesses, AuthMode, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, } from './harness-detect.js';
|
|
15
16
|
import { detectHumanInitiated } from './harness.js';
|
|
16
17
|
import { emitNotice } from './notice.js';
|
|
17
18
|
import { errLine, outLine, setErrIndent } from './output.js';
|
|
@@ -44,7 +45,7 @@ async function verifyHarness(harness, mcpUrl, keyed, apiKey) {
|
|
|
44
45
|
return health;
|
|
45
46
|
if (!apiKey)
|
|
46
47
|
return { depth: 'health+auth', ok: false, reason: 'no stored API key' };
|
|
47
|
-
return verifyMcpAuth(apiKey);
|
|
48
|
+
return verifyMcpAuth(apiKey, new URL(mcpUrl).origin);
|
|
48
49
|
}
|
|
49
50
|
/** The non-connect settlements: undetected, stale, dry-run, uninstall. */
|
|
50
51
|
function settleWithoutConnect(detection, opts, base) {
|
|
@@ -109,7 +110,9 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
|
|
|
109
110
|
}
|
|
110
111
|
// The probe, not the flag, decides: a harness that degraded to OAuth is not key-authed.
|
|
111
112
|
const keyAuthed = authMode ? authMode === 'api-key' : keyed;
|
|
112
|
-
const verify =
|
|
113
|
+
const verify = authMode === AuthMode.Keyless
|
|
114
|
+
? { depth: 'health', ok: true }
|
|
115
|
+
: await verifyHarness(harness, opts.mcpUrl, keyAuthed, apiKey);
|
|
113
116
|
return {
|
|
114
117
|
...base,
|
|
115
118
|
installed: true,
|
|
@@ -426,6 +429,9 @@ async function launchFirstTask(results) {
|
|
|
426
429
|
if (!chosen)
|
|
427
430
|
return false;
|
|
428
431
|
outLine(`Starting ${DISPLAY_NAMES[chosen.harness]} with your first TinyFish task...`);
|
|
429
|
-
const
|
|
432
|
+
const keyless = chosen.harness === 'omp' &&
|
|
433
|
+
results.some((result) => result.harness === 'omp' && !result.keyAuthed);
|
|
434
|
+
const args = keyless ? [KEYLESS_ONBOARDING_PROMPT] : chosen.args;
|
|
435
|
+
const result = spawn.sync(HARNESS_COMMANDS[chosen.harness], args, { stdio: 'inherit' });
|
|
430
436
|
return !result.error;
|
|
431
437
|
}
|
|
@@ -11,6 +11,7 @@ export declare function openclawSkillUninstall(): {
|
|
|
11
11
|
};
|
|
12
12
|
export declare const DEFAULT_ONBOARDING_PROMPT: string;
|
|
13
13
|
export declare const OPENCLAW_ONBOARDING_PROMPT: string;
|
|
14
|
+
export declare const KEYLESS_ONBOARDING_PROMPT: string;
|
|
14
15
|
export type { SkillAgent };
|
|
15
16
|
/** Cursor writes its own MCP config, so it has no descriptor to carry this. */
|
|
16
17
|
export declare const CURSOR_SKILL_TARGET: {
|
|
@@ -48,23 +49,23 @@ interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent'
|
|
|
48
49
|
/** Writes into the harness's store, so connect verifies the key first. */
|
|
49
50
|
seedKey?: (apiKey: string) => SeededInstall;
|
|
50
51
|
};
|
|
52
|
+
keylessAddArgs?: (mcpUrl: string) => string[];
|
|
53
|
+
prepareKeylessInstall?: () => DirectInstall;
|
|
51
54
|
loginArgs?: string[];
|
|
52
55
|
removals: {
|
|
53
56
|
args: string[];
|
|
54
57
|
label: string;
|
|
55
58
|
}[];
|
|
56
59
|
/** Blocking terminal walkthrough; runs only behind the TTY gate. */
|
|
57
|
-
launchWalkthrough?: () => void;
|
|
60
|
+
launchWalkthrough?: (prompt: string) => void;
|
|
58
61
|
/** Own window, never inherits stdio; reports its own outcome. */
|
|
59
|
-
detachedLaunch?: () => WalkthroughOutcome;
|
|
62
|
+
detachedLaunch?: (prompt: string) => WalkthroughOutcome;
|
|
60
63
|
/** Post-install like the skill step: failures warn, never fail the attempt. */
|
|
61
64
|
extraPostInstall?: (options: {
|
|
62
65
|
apiKey?: string;
|
|
63
66
|
verbose: boolean;
|
|
64
67
|
seededHome?: string;
|
|
65
|
-
}) =>
|
|
66
|
-
/** Pin `extraPostInstall` installs, reported on the plugin_installed checkpoint. */
|
|
67
|
-
pluginVersion?: string;
|
|
68
|
+
}) => string | undefined;
|
|
68
69
|
/** Extra --uninstall row beyond `removals`; never runs pre-add. */
|
|
69
70
|
uninstallExtra?: {
|
|
70
71
|
args: string[];
|
|
@@ -85,6 +86,10 @@ interface KeyRequiredMcpClient extends BaseMcpClient {
|
|
|
85
86
|
loginArgs?: never;
|
|
86
87
|
}
|
|
87
88
|
export type NativeMcpClient = OauthCapableMcpClient | KeyRequiredMcpClient;
|
|
89
|
+
export interface DirectInstall {
|
|
90
|
+
home?: string;
|
|
91
|
+
register: (mcpUrl: string) => ReturnType<typeof hermesRegistrationEnabled>;
|
|
92
|
+
}
|
|
88
93
|
/**
|
|
89
94
|
* `cmd` re-parses its command line after `/c`, so an unquoted `&` in the query string ends the
|
|
90
95
|
* command and truncates the URL. Quoting needs windowsVerbatimArguments, or Node escapes the
|
|
@@ -92,7 +97,7 @@ export type NativeMcpClient = OauthCapableMcpClient | KeyRequiredMcpClient;
|
|
|
92
97
|
*/
|
|
93
98
|
export declare function openExternalUrl(url: string): ReturnType<typeof spawn.sync>;
|
|
94
99
|
/** Writes exit 0 having saved nothing, a disabled entry, or a keyless one. */
|
|
95
|
-
export declare function hermesRegistrationEnabled(home: string): {
|
|
100
|
+
export declare function hermesRegistrationEnabled(home: string, mode?: 'api-key' | 'keyless', expectedUrl?: string): {
|
|
96
101
|
ok: boolean;
|
|
97
102
|
detail?: string;
|
|
98
103
|
tag?: string;
|
|
@@ -105,7 +110,7 @@ export declare const OPENCLAW: SupportedCommand;
|
|
|
105
110
|
export type WalkthroughOutcome = 'launched' | 'printed';
|
|
106
111
|
/** Awaited before the handover; track+flush must land pre-block. */
|
|
107
112
|
type OnWalkthroughDecided = (outcome: WalkthroughOutcome) => void | Promise<void>;
|
|
108
|
-
export declare function launchNativeMcpClient(client: NativeMcpClient, onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
109
|
-
export declare function launchOmpWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
113
|
+
export declare function launchNativeMcpClient(client: NativeMcpClient, onDecided?: OnWalkthroughDecided, prompt?: string): Promise<WalkthroughOutcome>;
|
|
114
|
+
export declare function launchOmpWalkthrough(onDecided?: OnWalkthroughDecided, prompt?: string): Promise<WalkthroughOutcome>;
|
|
110
115
|
export declare function launchPiWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
111
116
|
export declare function launchOpenClawWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
@@ -4,11 +4,11 @@ import { detectHumanInitiated } from './harness.js';
|
|
|
4
4
|
import { harnessConfigPath } from './harness-detect.js';
|
|
5
5
|
import { errLine, sanitizeLine, warnLine } from './output.js';
|
|
6
6
|
import { ConnectInterruptedError, ConnectStepError, spawnStepError, } from './connect-runtime.js';
|
|
7
|
-
import { TINYFISH_API_KEY_VAR } from './constants.js';
|
|
7
|
+
import { TINYFISH_ACCESS_MODE_HEADER, TINYFISH_API_KEY_VAR, TINYFISH_KEYLESS_ACCESS_MODE, } from './constants.js';
|
|
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.
|
|
@@ -44,6 +44,8 @@ export const OPENCLAW_ONBOARDING_PROMPT = 'I just installed the TinyFish skill.
|
|
|
44
44
|
'want to search and wait for my reply before using TinyFish Search. Show me the results, ask ' +
|
|
45
45
|
'which result I want to read, and wait before using TinyFish Fetch. Then ask what browser task ' +
|
|
46
46
|
'I want to complete and wait before using TinyFish Agent. Never skip ahead or choose for me.';
|
|
47
|
+
export const KEYLESS_ONBOARDING_PROMPT = 'I just connected TinyFish Search. Ask what I want to look up and wait for my reply. Then search ' +
|
|
48
|
+
'with the `tinyfish` MCP server, show results, and explain signing up unlocks TinyFish Fetch and Agent.';
|
|
47
49
|
/** Cursor writes its own MCP config, so it has no descriptor to carry this. */
|
|
48
50
|
export const CURSOR_SKILL_TARGET = {
|
|
49
51
|
skillAgent: HARNESS_SPECS.cursor.skillAgent,
|
|
@@ -65,14 +67,14 @@ export function openExternalUrl(url) {
|
|
|
65
67
|
}
|
|
66
68
|
return spawn.sync('xdg-open', [url], { stdio: 'ignore' });
|
|
67
69
|
}
|
|
68
|
-
function launchCodexWalkthrough() {
|
|
70
|
+
function launchCodexWalkthrough(prompt) {
|
|
69
71
|
const deepLink = new URL('codex://new');
|
|
70
|
-
deepLink.searchParams.set('prompt',
|
|
72
|
+
deepLink.searchParams.set('prompt', prompt);
|
|
71
73
|
deepLink.searchParams.set('path', process.cwd());
|
|
72
74
|
const result = openExternalUrl(deepLink.toString());
|
|
73
75
|
if (result.error || result.status !== 0) {
|
|
74
76
|
// Headless box: nothing can open a window; recover like the gated clients.
|
|
75
|
-
printPromptForHandoff('Codex', 'codex',
|
|
77
|
+
printPromptForHandoff('Codex', 'codex', prompt, 'Codex could not be opened here (no `codex://` handler)');
|
|
76
78
|
return 'printed';
|
|
77
79
|
}
|
|
78
80
|
errLine('Codex opened with the TinyFish walkthrough ready. Send the prompt to start.');
|
|
@@ -85,8 +87,8 @@ function handOverTerminal(command, args, displayName) {
|
|
|
85
87
|
throw spawnStepError(`Could not launch ${displayName}`, result);
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
|
-
function launchHermesWalkthrough() {
|
|
89
|
-
const seedResult = spawn.sync('hermes', ['chat', '-Q', '-q',
|
|
90
|
+
function launchHermesWalkthrough(prompt) {
|
|
91
|
+
const seedResult = spawn.sync('hermes', ['chat', '-Q', '-q', prompt], {
|
|
90
92
|
encoding: 'utf8',
|
|
91
93
|
timeout: HERMES_SEED_TIMEOUT_MS,
|
|
92
94
|
});
|
|
@@ -100,18 +102,23 @@ function launchHermesWalkthrough() {
|
|
|
100
102
|
}
|
|
101
103
|
handOverTerminal('hermes', ['--resume', sessionId], 'Hermes');
|
|
102
104
|
}
|
|
105
|
+
function failedGate(detail, tag) {
|
|
106
|
+
return { ok: false, detail, tag };
|
|
107
|
+
}
|
|
103
108
|
/** Writes exit 0 having saved nothing, a disabled entry, or a keyless one. */
|
|
104
|
-
export function hermesRegistrationEnabled(home) {
|
|
109
|
+
export function hermesRegistrationEnabled(home, mode = 'api-key', expectedUrl) {
|
|
105
110
|
const entry = readHermesEntry(home);
|
|
106
111
|
if (entry.state === 'enabled') {
|
|
107
|
-
if (entry.
|
|
112
|
+
if (expectedUrl && entry.url !== expectedUrl) {
|
|
113
|
+
return failedGate(`the entry still points at ${entry.url ?? 'an unknown URL'}`, 'hermes_entry_url_mismatch');
|
|
114
|
+
}
|
|
115
|
+
const validAuth = mode === 'keyless' ? entry.keyless : entry.usesKeyHeader;
|
|
116
|
+
if (validAuth)
|
|
108
117
|
return { ok: true };
|
|
109
118
|
// Exit codes carry no signal, so a corrupt entry can only be caught here.
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
tag: 'hermes_entry_no_key_header',
|
|
114
|
-
};
|
|
119
|
+
return mode === 'keyless'
|
|
120
|
+
? failedGate('the entry does not carry the keyless access header', 'hermes_entry_no_keyless_header')
|
|
121
|
+
: failedGate('the entry does not read the seeded key', 'hermes_entry_no_key_header');
|
|
115
122
|
}
|
|
116
123
|
// Each of these is a different repair, so none of them share wording.
|
|
117
124
|
const detail = {
|
|
@@ -141,7 +148,15 @@ function registerHermesEntry(home, mcpUrl) {
|
|
|
141
148
|
throw error;
|
|
142
149
|
return false;
|
|
143
150
|
}
|
|
144
|
-
return hermesRegistrationEnabled(home).ok;
|
|
151
|
+
return hermesRegistrationEnabled(home, 'api-key', mcpUrl).ok;
|
|
152
|
+
}
|
|
153
|
+
function registerHermesKeyless(home, mcpUrl) {
|
|
154
|
+
writeHermesMcpEntry(home, mcpUrl, 'keyless');
|
|
155
|
+
return hermesRegistrationEnabled(home, 'keyless', mcpUrl);
|
|
156
|
+
}
|
|
157
|
+
function prepareHermesKeylessInstall() {
|
|
158
|
+
const home = requireHermesHome();
|
|
159
|
+
return { home, register: (mcpUrl) => registerHermesKeyless(home, mcpUrl) };
|
|
145
160
|
}
|
|
146
161
|
function seedHermesKey(apiKey) {
|
|
147
162
|
const home = requireHermesHome();
|
|
@@ -190,14 +205,15 @@ function installHermesWebPlugin({ apiKey, verbose, seededHome, }) {
|
|
|
190
205
|
}
|
|
191
206
|
// Reuses the seed's home: a second `hermes dump` can disagree with the first.
|
|
192
207
|
const home = seededHome ?? requireHermesHome();
|
|
193
|
-
installHermesPlugin(home, apiKey, { verbose });
|
|
208
|
+
const pluginVersion = installHermesPlugin(home, apiKey, { verbose });
|
|
194
209
|
setHermesWebBackends(home);
|
|
210
|
+
return pluginVersion;
|
|
195
211
|
}
|
|
196
212
|
// OpenCode's TUI takes a positional as a project directory, so a bare `opencode "<prompt>"`
|
|
197
213
|
// would be read as a folder. The `--prompt` flag seeds the interactive TUI with a first message
|
|
198
214
|
// (opencode.ai/docs/cli), so the onboarding guide fires just like the other agents.
|
|
199
|
-
function launchOpencode() {
|
|
200
|
-
handOverTerminal('opencode', ['--prompt',
|
|
215
|
+
function launchOpencode(prompt) {
|
|
216
|
+
handOverTerminal('opencode', ['--prompt', prompt], 'OpenCode');
|
|
201
217
|
}
|
|
202
218
|
/** "positional": flags before `tinyfish <url>`. "flag": flags after `--url`. */
|
|
203
219
|
function specAddArgs(spec, mcpUrl) {
|
|
@@ -247,14 +263,21 @@ function specKeyAuth(spec) {
|
|
|
247
263
|
addArgs: (mcpUrl, apiKey) => [...specAddArgs(spec, mcpUrl), '--header', value(apiKey)],
|
|
248
264
|
};
|
|
249
265
|
}
|
|
266
|
+
function specKeylessAddArgs(spec) {
|
|
267
|
+
const { header } = spec;
|
|
268
|
+
if (!spec.keylessFallback || !header)
|
|
269
|
+
return undefined;
|
|
270
|
+
const value = `${TINYFISH_ACCESS_MODE_HEADER}${header.sep}${TINYFISH_KEYLESS_ACCESS_MODE}`;
|
|
271
|
+
return (mcpUrl) => specAddArgs(spec, mcpUrl).concat('--header', value);
|
|
272
|
+
}
|
|
250
273
|
/** Descriptor behaviour the spec cannot hold; every other field is spec data. */
|
|
251
274
|
const HARNESS_OVERRIDES = {
|
|
252
275
|
codex: { detachedLaunch: launchCodexWalkthrough },
|
|
253
276
|
hermes: {
|
|
254
277
|
launchWalkthrough: launchHermesWalkthrough,
|
|
255
278
|
extraPostInstall: installHermesWebPlugin,
|
|
256
|
-
pluginVersion: HERMES_PLUGIN_VERSION,
|
|
257
279
|
seedKey: seedHermesKey,
|
|
280
|
+
prepareKeylessInstall: prepareHermesKeylessInstall,
|
|
258
281
|
},
|
|
259
282
|
opencode: { launchWalkthrough: launchOpencode },
|
|
260
283
|
};
|
|
@@ -273,6 +296,8 @@ function toNativeClient(harness) {
|
|
|
273
296
|
skillAgent: spec.skillAgent,
|
|
274
297
|
nonInteractiveAdd: spec.nonInteractiveAdd,
|
|
275
298
|
keyAuth,
|
|
299
|
+
keylessAddArgs: specKeylessAddArgs(spec),
|
|
300
|
+
prepareKeylessInstall: overrides.prepareKeylessInstall,
|
|
276
301
|
signInHint: spec.signInHint,
|
|
277
302
|
degradedAuthNotes: spec.degradedAuthNotes,
|
|
278
303
|
removals: spec.removals ?? [
|
|
@@ -285,7 +310,6 @@ function toNativeClient(harness) {
|
|
|
285
310
|
detachedLaunch: overrides.detachedLaunch,
|
|
286
311
|
postConnectNote: spec.postConnectNote,
|
|
287
312
|
extraPostInstall: overrides.extraPostInstall,
|
|
288
|
-
pluginVersion: overrides.pluginVersion,
|
|
289
313
|
uninstallExtra: spec.uninstallExtra,
|
|
290
314
|
};
|
|
291
315
|
if (spec.keyRequired) {
|
|
@@ -329,21 +353,21 @@ async function deliverWalkthrough(displayName, command, prompt, handover, onDeci
|
|
|
329
353
|
handover();
|
|
330
354
|
return 'launched';
|
|
331
355
|
}
|
|
332
|
-
export async function launchNativeMcpClient(client, onDecided) {
|
|
356
|
+
export async function launchNativeMcpClient(client, onDecided, prompt = DEFAULT_ONBOARDING_PROMPT) {
|
|
333
357
|
if (client.detachedLaunch) {
|
|
334
358
|
// Detached: the open attempt never blocks, so decide after it.
|
|
335
359
|
errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
|
|
336
|
-
const outcome = client.detachedLaunch();
|
|
360
|
+
const outcome = client.detachedLaunch(prompt);
|
|
337
361
|
await onDecided?.(outcome);
|
|
338
362
|
return outcome;
|
|
339
363
|
}
|
|
340
|
-
return deliverWalkthrough(client.displayName, client.command,
|
|
341
|
-
? client.launchWalkthrough()
|
|
342
|
-
: handOverTerminal(client.command, [
|
|
364
|
+
return deliverWalkthrough(client.displayName, client.command, prompt, () => client.launchWalkthrough
|
|
365
|
+
? client.launchWalkthrough(prompt)
|
|
366
|
+
: handOverTerminal(client.command, [prompt], client.displayName), onDecided);
|
|
343
367
|
}
|
|
344
368
|
// Via deliverWalkthrough: handOverTerminal alone would wedge a headless --launch.
|
|
345
|
-
export function launchOmpWalkthrough(onDecided) {
|
|
346
|
-
return deliverWalkthrough('omp', 'omp',
|
|
369
|
+
export function launchOmpWalkthrough(onDecided, prompt = DEFAULT_ONBOARDING_PROMPT) {
|
|
370
|
+
return deliverWalkthrough('omp', 'omp', prompt, () => handOverTerminal('omp', [prompt], 'omp'), onDecided);
|
|
347
371
|
}
|
|
348
372
|
export function launchPiWalkthrough(onDecided) {
|
|
349
373
|
return deliverWalkthrough('Pi', 'pi', DEFAULT_ONBOARDING_PROMPT, () => handOverTerminal('pi', [DEFAULT_ONBOARDING_PROMPT], 'Pi'), onDecided);
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare const CLI_AGENT_IDENTITY = "tinyfish-cli";
|
|
|
8
8
|
export declare const WEB_SKILL_NAME = "use-tinyfish";
|
|
9
9
|
/** The descriptor hands this name to harnesses; the probes resolve it. */
|
|
10
10
|
export declare const TINYFISH_API_KEY_VAR = "TINYFISH_API_KEY";
|
|
11
|
+
export declare const TINYFISH_ACCESS_MODE_HEADER = "X-TinyFish-Access-Mode";
|
|
12
|
+
export declare const TINYFISH_KEYLESS_ACCESS_MODE = "keyless";
|
|
11
13
|
export declare const BASE_URL: string;
|
|
12
14
|
export declare const API_URL_OVERRIDE: string | undefined;
|
|
13
15
|
/** URL where users can create and manage API keys */
|
package/dist/lib/constants.js
CHANGED
|
@@ -13,6 +13,8 @@ export const CLI_AGENT_IDENTITY = 'tinyfish-cli';
|
|
|
13
13
|
export const WEB_SKILL_NAME = 'use-tinyfish';
|
|
14
14
|
/** The descriptor hands this name to harnesses; the probes resolve it. */
|
|
15
15
|
export const TINYFISH_API_KEY_VAR = 'TINYFISH_API_KEY';
|
|
16
|
+
export const TINYFISH_ACCESS_MODE_HEADER = 'X-TinyFish-Access-Mode';
|
|
17
|
+
export const TINYFISH_KEYLESS_ACCESS_MODE = 'keyless';
|
|
16
18
|
/** Base URL for the TinyFish API. Override with TINYFISH_API_URL for staging/self-hosted. */
|
|
17
19
|
const apiUrlOverride = process.env['TINYFISH_API_URL']?.trim() || undefined;
|
|
18
20
|
if (apiUrlOverride) {
|
|
@@ -298,6 +298,10 @@ export function checkHermesPlugin(status) {
|
|
|
298
298
|
if (status.registered !== Registered.Yes) {
|
|
299
299
|
return { check: { ...base, status: 'skip', detail: 'TinyFish is not registered in Hermes' } };
|
|
300
300
|
}
|
|
301
|
+
const authMode = status.authMode === AuthMode.Unknown ? status.recordedAuthMode : status.authMode;
|
|
302
|
+
if (authMode === AuthMode.Keyless) {
|
|
303
|
+
return { check: { ...base, status: 'skip', detail: 'not needed for keyless Search' } };
|
|
304
|
+
}
|
|
301
305
|
const resolved = resolveHermesHome();
|
|
302
306
|
if (typeof resolved !== 'string') {
|
|
303
307
|
return {
|
|
@@ -58,6 +58,7 @@ declare const doctorHarnessSchema: z.ZodObject<{
|
|
|
58
58
|
registered: z.ZodEnum<typeof Registered>;
|
|
59
59
|
auth_mode: z.ZodEnum<typeof AuthMode>;
|
|
60
60
|
recorded_auth_mode: z.ZodOptional<z.ZodEnum<{
|
|
61
|
+
keyless: "keyless";
|
|
61
62
|
"api-key": "api-key";
|
|
62
63
|
oauth: "oauth";
|
|
63
64
|
deferred: "deferred";
|
|
@@ -135,6 +136,7 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
135
136
|
registered: z.ZodEnum<typeof Registered>;
|
|
136
137
|
auth_mode: z.ZodEnum<typeof AuthMode>;
|
|
137
138
|
recorded_auth_mode: z.ZodOptional<z.ZodEnum<{
|
|
139
|
+
keyless: "keyless";
|
|
138
140
|
"api-key": "api-key";
|
|
139
141
|
oauth: "oauth";
|
|
140
142
|
deferred: "deferred";
|
|
@@ -163,7 +165,6 @@ export declare const doctorReportSchema: z.ZodObject<{
|
|
|
163
165
|
unattended_safe: z.ZodBoolean;
|
|
164
166
|
}, z.core.$strip>>;
|
|
165
167
|
hermes_plugin_version: z.ZodOptional<z.ZodString>;
|
|
166
|
-
hermes_plugin_expected_version: z.ZodOptional<z.ZodString>;
|
|
167
168
|
}, z.core.$strip>;
|
|
168
169
|
export type CheckStatus = z.infer<typeof checkStatusSchema>;
|
|
169
170
|
export type CheckScope = z.infer<typeof checkScopeSchema>;
|
|
@@ -20,7 +20,7 @@ const doctorHarnessSchema = z.object({
|
|
|
20
20
|
registered: z.enum(Registered),
|
|
21
21
|
auth_mode: z.enum(AuthMode),
|
|
22
22
|
// Additive under schema 3: what connect wrote when the harness cannot echo it back.
|
|
23
|
-
recorded_auth_mode: z.enum(['api-key', 'oauth', 'deferred']).optional(),
|
|
23
|
+
recorded_auth_mode: z.enum(['api-key', 'keyless', 'oauth', 'deferred']).optional(),
|
|
24
24
|
proves_harness_reach: z.boolean(),
|
|
25
25
|
});
|
|
26
26
|
// `action` is what `--fix` dispatches on, not the harness field: keying off a null harness
|
|
@@ -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. */
|
|
@@ -48,6 +48,7 @@ export interface HarnessSpec {
|
|
|
48
48
|
keyedAddJsonFlags?: string[];
|
|
49
49
|
/** No sign-in exists here, so connect refuses a keyless install (Hermes). */
|
|
50
50
|
keyRequired?: true;
|
|
51
|
+
keylessFallback?: true;
|
|
51
52
|
loginArgs?: string[];
|
|
52
53
|
/** Default: one `mcp remove tinyfish` labeled from displayName. */
|
|
53
54
|
removals?: {
|
|
@@ -199,6 +200,7 @@ export declare const HARNESS_SPECS: {
|
|
|
199
200
|
reloadAction: string;
|
|
200
201
|
skillAgent: "hermes-agent";
|
|
201
202
|
keyRequired: true;
|
|
203
|
+
keylessFallback: true;
|
|
202
204
|
supportCheck: {
|
|
203
205
|
args: string[];
|
|
204
206
|
patterns: RegExp[];
|
|
@@ -219,6 +221,7 @@ export declare const HARNESS_SPECS: {
|
|
|
219
221
|
displayName: string;
|
|
220
222
|
configDir: string;
|
|
221
223
|
reloadAction: string;
|
|
224
|
+
keylessFallback: true;
|
|
222
225
|
canVerifyAuth: true;
|
|
223
226
|
keyHeldByCli: true;
|
|
224
227
|
cliWritesConfig: true;
|
|
@@ -247,6 +250,7 @@ export declare const HARNESS_SPECS: {
|
|
|
247
250
|
reloadAction: string;
|
|
248
251
|
skillAgent: "opencode";
|
|
249
252
|
nonInteractiveAdd: true;
|
|
253
|
+
keylessFallback: true;
|
|
250
254
|
supportCheck: {
|
|
251
255
|
args: string[];
|
|
252
256
|
patterns: RegExp[];
|
package/dist/lib/harness-spec.js
CHANGED
|
@@ -177,6 +177,7 @@ export const HARNESS_SPECS = {
|
|
|
177
177
|
reloadAction: 'restart it',
|
|
178
178
|
skillAgent: 'hermes-agent',
|
|
179
179
|
keyRequired: true,
|
|
180
|
+
keylessFallback: true,
|
|
180
181
|
supportCheck: {
|
|
181
182
|
args: ['mcp', 'add', '--help'],
|
|
182
183
|
patterns: [/--auth\s+\{[^}]*oauth[^}]*\}/],
|
|
@@ -199,6 +200,7 @@ export const HARNESS_SPECS = {
|
|
|
199
200
|
displayName: 'omp',
|
|
200
201
|
configDir: '.omp',
|
|
201
202
|
reloadAction: 'restart it',
|
|
203
|
+
keylessFallback: true,
|
|
202
204
|
canVerifyAuth: true,
|
|
203
205
|
keyHeldByCli: true,
|
|
204
206
|
cliWritesConfig: true,
|
|
@@ -229,6 +231,7 @@ export const HARNESS_SPECS = {
|
|
|
229
231
|
reloadAction: 'restart it',
|
|
230
232
|
skillAgent: 'opencode',
|
|
231
233
|
nonInteractiveAdd: true,
|
|
234
|
+
keylessFallback: true,
|
|
232
235
|
supportCheck: {
|
|
233
236
|
args: ['mcp', 'add', '--help'],
|
|
234
237
|
patterns: [/--url(?:\s|$)/m],
|
|
@@ -13,9 +13,13 @@ export type HermesEntry = {
|
|
|
13
13
|
} | {
|
|
14
14
|
state: 'disabled';
|
|
15
15
|
usesKeyHeader: boolean;
|
|
16
|
+
keyless?: true;
|
|
17
|
+
url?: string;
|
|
16
18
|
} | {
|
|
17
19
|
state: 'enabled';
|
|
18
20
|
usesKeyHeader: boolean;
|
|
21
|
+
keyless?: true;
|
|
22
|
+
url?: string;
|
|
19
23
|
};
|
|
20
24
|
/** Connect's gate and doctor's probe share this, so they cannot disagree. */
|
|
21
25
|
export declare function readHermesEntry(home: string): HermesEntry;
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { TINYFISH_ACCESS_MODE_HEADER, TINYFISH_KEYLESS_ACCESS_MODE } from './constants.js';
|
|
5
6
|
import { HERMES_KEY_VAR } from './hermes-env.js';
|
|
6
7
|
/** Every message about the entry names this path. */
|
|
7
8
|
export function hermesConfigPath(home) {
|
|
@@ -16,6 +17,7 @@ const entrySchema = z.looseObject({
|
|
|
16
17
|
// Anything goes: the runtime defaults unrecognised values to enabled.
|
|
17
18
|
enabled: z.unknown().optional(),
|
|
18
19
|
headers: z.record(z.string(), z.unknown()).optional(),
|
|
20
|
+
url: z.unknown().optional(),
|
|
19
21
|
});
|
|
20
22
|
const configSchema = z
|
|
21
23
|
.looseObject({
|
|
@@ -48,6 +50,10 @@ function hasKeyHeader(headers) {
|
|
|
48
50
|
const authorization = Object.entries(headers ?? {}).find(([name]) => name.toLowerCase() === 'authorization');
|
|
49
51
|
return authorization?.[1] === HERMES_HEADER_TEMPLATE;
|
|
50
52
|
}
|
|
53
|
+
function hasKeylessHeader(headers) {
|
|
54
|
+
const value = Object.entries(headers ?? {}).find(([name]) => name.toLowerCase() === TINYFISH_ACCESS_MODE_HEADER.toLowerCase())?.[1];
|
|
55
|
+
return typeof value === 'string' && value.trim().toLowerCase() === TINYFISH_KEYLESS_ACCESS_MODE;
|
|
56
|
+
}
|
|
51
57
|
/** Connect's gate and doctor's probe share this, so they cannot disagree. */
|
|
52
58
|
export function readHermesEntry(home) {
|
|
53
59
|
let raw;
|
|
@@ -68,7 +74,10 @@ export function readHermesEntry(home) {
|
|
|
68
74
|
if (!entry)
|
|
69
75
|
return { state: 'absent' };
|
|
70
76
|
const usesKeyHeader = hasKeyHeader(entry.headers);
|
|
71
|
-
return
|
|
72
|
-
?
|
|
73
|
-
|
|
77
|
+
return {
|
|
78
|
+
state: isEnabled(entry.enabled) ? 'enabled' : 'disabled',
|
|
79
|
+
usesKeyHeader,
|
|
80
|
+
...(hasKeylessHeader(entry.headers) ? { keyless: true } : {}),
|
|
81
|
+
...(typeof entry.url === 'string' ? { url: entry.url } : {}),
|
|
82
|
+
};
|
|
74
83
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
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
|
-
export declare function writeHermesMcpEntry(home: string, mcpUrl: string): void;
|
|
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. */
|
|
10
9
|
export declare function removeHermesMcpEntry(home: string): void;
|
|
11
10
|
/** After install only: backends must never name an absent provider. */
|