@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.
Files changed (38) hide show
  1. package/README.md +11 -7
  2. package/dist/commands/auth.js +11 -1
  3. package/dist/commands/connect.js +112 -50
  4. package/dist/commands/doctor.js +4 -7
  5. package/dist/commands/run.js +5 -2
  6. package/dist/lib/auth.d.ts +2 -1
  7. package/dist/lib/auth.js +3 -1
  8. package/dist/lib/client.d.ts +2 -1
  9. package/dist/lib/client.js +50 -9
  10. package/dist/lib/connect-all-auth.js +3 -0
  11. package/dist/lib/connect-all-uninstall.js +6 -4
  12. package/dist/lib/connect-all.js +10 -4
  13. package/dist/lib/connect-clients.d.ts +13 -8
  14. package/dist/lib/connect-clients.js +51 -27
  15. package/dist/lib/constants.d.ts +2 -0
  16. package/dist/lib/constants.js +2 -0
  17. package/dist/lib/doctor-checks.js +4 -0
  18. package/dist/lib/doctor-report.d.ts +2 -1
  19. package/dist/lib/doctor-report.js +1 -2
  20. package/dist/lib/doctor-telemetry.js +0 -3
  21. package/dist/lib/harness-detect.d.ts +1 -0
  22. package/dist/lib/harness-detect.js +1 -0
  23. package/dist/lib/harness-spec.d.ts +4 -0
  24. package/dist/lib/harness-spec.js +3 -0
  25. package/dist/lib/hermes-config.d.ts +4 -0
  26. package/dist/lib/hermes-config.js +12 -3
  27. package/dist/lib/hermes-plugin.d.ts +4 -5
  28. package/dist/lib/hermes-plugin.js +92 -36
  29. package/dist/lib/mcp-json-config.d.ts +4 -2
  30. package/dist/lib/mcp-json-config.js +20 -10
  31. package/dist/lib/omp-config.js +5 -3
  32. package/dist/lib/output.js +7 -1
  33. package/dist/lib/registration-detect.js +12 -4
  34. package/dist/lib/setup-telemetry.d.ts +12 -2
  35. package/dist/lib/setup-telemetry.js +1 -1
  36. package/dist/lib/verify.d.ts +1 -1
  37. package/dist/lib/verify.js +29 -9
  38. 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, 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.
@@ -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 at ` +
24
- `${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
+ }
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
  }
@@ -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 = await verifyHarness(harness, opts.mcpUrl, keyAuthed, apiKey);
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 result = spawn.sync(HARNESS_COMMANDS[chosen.harness], chosen.args, { stdio: 'inherit' });
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
- }) => void;
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 { 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.
@@ -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', DEFAULT_ONBOARDING_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', DEFAULT_ONBOARDING_PROMPT, 'Codex could not be opened here (no `codex://` handler)');
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', DEFAULT_ONBOARDING_PROMPT], {
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.usesKeyHeader)
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
- ok: false,
112
- detail: 'the entry does not read the seeded key',
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', DEFAULT_ONBOARDING_PROMPT], 'OpenCode');
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, DEFAULT_ONBOARDING_PROMPT, () => client.launchWalkthrough
341
- ? client.launchWalkthrough()
342
- : handOverTerminal(client.command, [DEFAULT_ONBOARDING_PROMPT], client.displayName), onDecided);
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', DEFAULT_ONBOARDING_PROMPT, () => handOverTerminal('omp', [DEFAULT_ONBOARDING_PROMPT], 'omp'), onDecided);
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);
@@ -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 */
@@ -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. */
@@ -9,6 +9,7 @@ export declare enum Registered {
9
9
  }
10
10
  export declare enum AuthMode {
11
11
  ApiKey = "api-key",
12
+ Keyless = "keyless",
12
13
  OAuth = "oauth",
13
14
  Unknown = "unknown"
14
15
  }
@@ -17,6 +17,7 @@ export var Registered;
17
17
  export var AuthMode;
18
18
  (function (AuthMode) {
19
19
  AuthMode["ApiKey"] = "api-key";
20
+ AuthMode["Keyless"] = "keyless";
20
21
  AuthMode["OAuth"] = "oauth";
21
22
  AuthMode["Unknown"] = "unknown";
22
23
  })(AuthMode || (AuthMode = {}));
@@ -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[];
@@ -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 isEnabled(entry.enabled)
72
- ? { state: 'enabled', usesKeyHeader }
73
- : { state: 'disabled', usesKeyHeader };
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 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
- 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. */