@tiny-fish/cli 0.37.1-next.301 → 0.37.1-next.302

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 CHANGED
@@ -19,7 +19,7 @@ npx -y @tiny-fish/cli@latest connect
19
19
  ```
20
20
 
21
21
  It detects the agent configs on your machine (Claude Code, Codex, Cursor, Grok, Hermes,
22
- OpenClaw, OpenCode) and opens a checklist with everything pre-selected. Press enter to connect
22
+ omp, OpenClaw, OpenCode) and opens a checklist with everything pre-selected. Press enter to connect
23
23
  them all, or use the arrow keys and space to narrow the set first; `q` cancels without touching
24
24
  anything. Terminals without raw-key support get a numbered prompt instead.
25
25
 
@@ -1,6 +1,6 @@
1
1
  import spawn from 'cross-spawn';
2
2
  import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from '../lib/auth.js';
3
- import { CURSOR_SKILL_TARGET, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
3
+ import { CURSOR_SKILL_TARGET, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
4
4
  import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from '../lib/cli-install.js';
5
5
  import { ensureCliAuthenticated } from '../lib/connect-auth.js';
6
6
  import { createStdinPrompt, runCliFallback } from '../lib/connect-fallback.js';
@@ -8,6 +8,7 @@ import { installWebSkill } from '../lib/skill-install.js';
8
8
  import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, stageDurationOf, withStageDuration, spawnRemoval, } from '../lib/connect-runtime.js';
9
9
  import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from '../lib/cursor-config.js';
10
10
  import { runConnectAll } from '../lib/connect-all.js';
11
+ import { ompMcpPath, readOmpTinyfishEntry, writeOmpMcpConfig } from '../lib/omp-config.js';
11
12
  import { ALL_HARNESSES, AuthMode, HARNESS_DISPLAY_NAMES, RELOAD_ACTION, } from '../lib/harness-detect.js';
12
13
  import { detectHumanInitiated } from '../lib/harness.js';
13
14
  import { TINYFISH_API_KEY_VAR } from '../lib/constants.js';
@@ -507,6 +508,9 @@ export async function connectOpenClaw(options) {
507
508
  }
508
509
  });
509
510
  }
511
+ // Only interactive omp wires the OAuth handler; headless cannot sign in.
512
+ const OMP_KEYLESS_COPY = 'TinyFish is configured in omp. Open omp and sign in to TinyFish when prompted, or run ' +
513
+ '`/mcp reauth tinyfish` there.';
510
514
  const CONFIG_FILE_HARNESSES = {
511
515
  cursor: {
512
516
  skillTarget: CURSOR_SKILL_TARGET,
@@ -522,6 +526,22 @@ const CONFIG_FILE_HARNESSES = {
522
526
  reload: 'TinyFish is connected. Reload the Cursor window, then approve/sign in under Settings → MCP.',
523
527
  },
524
528
  },
529
+ omp: {
530
+ // No skill exists for omp; installWebSkill returns without one.
531
+ skillTarget: { displayName: HARNESS_DISPLAY_NAMES.omp },
532
+ registeredUrl: () => readOmpTinyfishEntry()?.url,
533
+ write: writeOmpMcpConfig,
534
+ configPath: ompMcpPath,
535
+ // No install deeplink exists; the keyless copy carries the sign-in.
536
+ tryLaunch: () => false,
537
+ copy: {
538
+ verified: "TinyFish is connected and verified (health+auth). Restart omp and it's live.",
539
+ authFailed: (reason) => `TinyFish is configured, but the authenticated check failed (${reason}). ` +
540
+ 'Fix: rotate/re-enter your key with `tinyfish auth login`, then re-run: tinyfish connect omp',
541
+ launched: OMP_KEYLESS_COPY,
542
+ reload: OMP_KEYLESS_COPY,
543
+ },
544
+ },
525
545
  };
526
546
  /** Writes the harness's MCP config file itself; no `mcp add` exists. */
527
547
  async function connectConfigFileHarness(harness, options) {
@@ -629,6 +649,7 @@ function launchCursorDeeplink(mcpUrl) {
629
649
  }
630
650
  /** Total over the non-native harnesses: a new one fails to compile until wired here. */
631
651
  const LAUNCH_OVERRIDES = {
652
+ omp: launchOmpWalkthrough,
632
653
  openclaw: launchOpenClawWalkthrough,
633
654
  // Cursor has no chat CLI to launch.
634
655
  cursor: () => {
@@ -655,6 +676,20 @@ const CONNECTOR_OVERRIDES = {
655
676
  }
656
677
  return undefined;
657
678
  },
679
+ omp: async (options) => {
680
+ await connectConfigFileHarness('omp', options);
681
+ if (options.launch) {
682
+ try {
683
+ await launchOmpWalkthrough();
684
+ }
685
+ catch (error) {
686
+ // Post-settle, like the native walkthrough step: never fail the connect.
687
+ errLine(error instanceof Error ? error.message : String(error));
688
+ errLine(finishSetupHint('omp'));
689
+ }
690
+ }
691
+ return undefined;
692
+ },
658
693
  };
659
694
  function requireNative(harness) {
660
695
  const client = NATIVE_BY_HARNESS.get(harness);
@@ -764,7 +799,7 @@ export function registerConnect(program) {
764
799
  program
765
800
  .command('connect')
766
801
  .description('Connect TinyFish to an AI agent')
767
- .argument('[client]', 'Agent client to connect (claude-code, codex, cursor, grok, hermes, openclaw, or opencode); omit to pick from the agents detected on this machine')
802
+ .argument('[client]', 'Agent client to connect (claude-code, codex, cursor, grok, hermes, omp, openclaw, or opencode); omit to pick from the agents detected on this machine')
768
803
  .option('--all', 'Detect and connect every supported harness found on this machine')
769
804
  .option('--dry-run', 'Print planned writes per harness without touching anything (--all only)')
770
805
  .option('--uninstall', 'Remove TinyFish entries written by connect --all')
@@ -2,6 +2,7 @@ import { clearConnectContext, connectContextState } from './auth.js';
2
2
  import { NATIVE_BY_HARNESS, openclawSkillUninstall } from './connect-clients.js';
3
3
  import { ConnectInterruptedError, spawnRemoval } from './connect-runtime.js';
4
4
  import { removeCursorMcpServer, planCursorWrite } from './cursor-config.js';
5
+ import { planOmpWrite, removeOmpMcpServer } from './omp-config.js';
5
6
  import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
6
7
  import { clearHermesWebBackends, HERMES_PLUGIN_SHA } from './hermes-plugin.js';
7
8
  import { HARNESS_DISPLAY_NAMES as DISPLAY_NAMES } from './harness-detect.js';
@@ -11,6 +12,8 @@ const OPENCODE_UNINSTALL_POINTER = 'remove the tinyfish entry from ~/.config/ope
11
12
  export function planText(harness, mcpUrl, apiKey) {
12
13
  if (harness === 'cursor')
13
14
  return planCursorWrite(mcpUrl, apiKey);
15
+ if (harness === 'omp')
16
+ return planOmpWrite(mcpUrl, apiKey);
14
17
  // Only the keyed path seeds the .env; a keyless Hermes install writes nothing.
15
18
  if (harness === 'hermes' && apiKey) {
16
19
  return (`would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ` +
@@ -23,6 +26,9 @@ export function uninstallPlanText(harness) {
23
26
  if (harness === 'cursor') {
24
27
  return 'would remove the tinyfish entry from ~/.cursor/mcp.json';
25
28
  }
29
+ if (harness === 'omp') {
30
+ return "would remove the tinyfish entry from omp's mcp.json (located via `omp config path`)";
31
+ }
26
32
  if (!uninstallRuns(harness)) {
27
33
  return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
28
34
  }
@@ -170,10 +176,21 @@ function uninstallViaCommands(detection, verbose, binaryOnPath) {
170
176
  fixCommand: `Retry by hand: ${uninstallPointer(harness)}`,
171
177
  };
172
178
  }
173
- function uninstallCursor(detection) {
179
+ /** The CLI wrote these mcp.json entries, so it removes them. */
180
+ const MCP_JSON_UNINSTALLS = {
181
+ cursor: {
182
+ remove: removeCursorMcpServer,
183
+ fix: 'Fix ~/.cursor/mcp.json by hand, then re-run: tinyfish connect --all --uninstall',
184
+ },
185
+ omp: {
186
+ remove: removeOmpMcpServer,
187
+ fix: "Fix omp's mcp.json (at `omp config path`) by hand, then re-run: tinyfish connect --all --uninstall",
188
+ },
189
+ };
190
+ function uninstallMcpJson(detection, spec) {
174
191
  let result;
175
192
  try {
176
- result = removeCursorMcpServer();
193
+ result = spec.remove();
177
194
  }
178
195
  catch (e) {
179
196
  result = { status: 'corrupt_skip', error: e instanceof Error ? e.message : String(e) };
@@ -183,20 +200,21 @@ function uninstallCursor(detection) {
183
200
  ...detection,
184
201
  outcome: 'uninstalled',
185
202
  removalFailed: true,
186
- errorMessage: `could not update Cursor's mcp.json (${result.error}) — nothing removed.`,
187
- fixCommand: 'Fix ~/.cursor/mcp.json by hand, then re-run: tinyfish connect --all --uninstall',
203
+ errorMessage: `could not update ${DISPLAY_NAMES[detection.harness]}'s mcp.json (${result.error}) — nothing removed.`,
204
+ fixCommand: spec.fix,
188
205
  };
189
206
  }
190
207
  if (result.status === 'unchanged') {
191
208
  return { ...detection, outcome: 'uninstall_noop' };
192
209
  }
193
- clearConnectContext('cursor');
210
+ clearConnectContext(detection.harness);
194
211
  return { ...detection, outcome: 'uninstalled' };
195
212
  }
196
213
  export function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
197
214
  const detection = { harness, detected: true, configPath, installed: false };
198
- const result = harness === 'cursor'
199
- ? uninstallCursor(detection)
215
+ const mcpJson = MCP_JSON_UNINSTALLS[harness];
216
+ const result = mcpJson
217
+ ? uninstallMcpJson(detection, mcpJson)
200
218
  : uninstallViaCommands(detection, verbose, binaryOnPath);
201
219
  if (harness !== 'hermes')
202
220
  return result;
@@ -143,7 +143,10 @@ function noWorkOutcome(d, uninstall) {
143
143
  }
144
144
  /** A config-writing connect needs no binary; every other harness spawns one. */
145
145
  function isStaleConfig(detection) {
146
- return (detection.detected && !detection.binaryOnPath && !harnessSpec(detection.harness).cliWritesConfig);
146
+ const spec = harnessSpec(detection.harness);
147
+ // Cursor's IDE reads mcp.json binary-less; omp's path needs the binary.
148
+ const binaryless = spec.cliWritesConfig && !spec.configPathFromBinary;
149
+ return detection.detected && !detection.binaryOnPath && !binaryless;
147
150
  }
148
151
  async function resolvePick(detections, opts, prompt) {
149
152
  let deselected = new Set();
@@ -404,6 +407,7 @@ export const FIRST_TASK_PROMPT = 'My TinyFish setup just finished — verify it
404
407
  const LAUNCHERS = [
405
408
  { harness: 'cursor', args: [FIRST_TASK_PROMPT] },
406
409
  { harness: 'claude-code', args: [FIRST_TASK_PROMPT] },
410
+ { harness: 'omp', args: [FIRST_TASK_PROMPT] },
407
411
  ];
408
412
  async function launchFirstTask(results) {
409
413
  const candidates = LAUNCHERS.filter((l) => results.some((r) => r.harness === l.harness && r.installed) &&
@@ -95,11 +95,12 @@ export declare function hermesRegistrationEnabled(home: string): {
95
95
  };
96
96
  export declare const NATIVE_MCP_CLIENTS: readonly NativeMcpClient[];
97
97
  /** Native descriptor by harness id; Cursor and OpenClaw have none. */
98
- export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code", NativeMcpClient>;
98
+ export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code", NativeMcpClient>;
99
99
  export declare const OPENCLAW: SupportedCommand;
100
100
  /** "printed" = handed to the user to paste; the walkthrough was never started for them. */
101
101
  export type WalkthroughOutcome = 'launched' | 'printed';
102
102
  /** Awaited before the handover; track+flush must land pre-block. */
103
103
  type OnWalkthroughDecided = (outcome: WalkthroughOutcome) => void | Promise<void>;
104
104
  export declare function launchNativeMcpClient(client: NativeMcpClient, onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
105
+ export declare function launchOmpWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
105
106
  export declare function launchOpenClawWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
@@ -293,6 +293,10 @@ export async function launchNativeMcpClient(client, onDecided) {
293
293
  ? client.launchWalkthrough()
294
294
  : handOverTerminal(client.command, [DEFAULT_ONBOARDING_PROMPT], client.displayName), onDecided);
295
295
  }
296
+ // Via deliverWalkthrough: handOverTerminal alone would wedge a headless --launch.
297
+ export function launchOmpWalkthrough(onDecided) {
298
+ return deliverWalkthrough('omp', 'omp', DEFAULT_ONBOARDING_PROMPT, () => handOverTerminal('omp', [DEFAULT_ONBOARDING_PROMPT], 'omp'), onDecided);
299
+ }
296
300
  export function launchOpenClawWalkthrough(onDecided) {
297
301
  return deliverWalkthrough('OpenClaw', 'openclaw', OPENCLAW_ONBOARDING_PROMPT, () => handOverTerminal('openclaw', ['chat', '--message', OPENCLAW_ONBOARDING_PROMPT], 'OpenClaw'), onDecided);
298
302
  }
@@ -1,10 +1,11 @@
1
1
  import spawn from 'cross-spawn';
2
2
  import { connectHarness } from '../commands/connect.js';
3
- import { Registered } from './harness-detect.js';
3
+ import { ALL_HARNESSES, Registered } from './harness-detect.js';
4
+ import { harnessSpec } from './harness-spec.js';
4
5
  import { detectHumanInitiated } from './harness.js';
5
6
  import { errLine } from './output.js';
6
- // Only Cursor's repair is a pure local file write; every other one needs a browser sign-in.
7
- const UNATTENDED_SAFE = new Set(['cursor']);
7
+ // Config-file repairs are local writes; the rest need browser sign-in.
8
+ const UNATTENDED_SAFE = new Set(ALL_HARNESSES.filter((harness) => harnessSpec(harness).cliWritesConfig));
8
9
  export function repairsFor(checks, statuses) {
9
10
  const repairs = [];
10
11
  // A revoked-but-well-formed key passes the credential check and fails the call; both need login.
@@ -25,6 +25,7 @@ declare const doctorCheckSchema: z.ZodObject<{
25
25
  detail: z.ZodString;
26
26
  harness: z.ZodNullable<z.ZodEnum<{
27
27
  openclaw: "openclaw";
28
+ omp: "omp";
28
29
  grok: "grok";
29
30
  cursor: "cursor";
30
31
  codex: "codex";
@@ -41,6 +42,7 @@ declare const doctorCheckSchema: z.ZodObject<{
41
42
  declare const doctorHarnessSchema: z.ZodObject<{
42
43
  harness: z.ZodEnum<{
43
44
  openclaw: "openclaw";
45
+ omp: "omp";
44
46
  grok: "grok";
45
47
  cursor: "cursor";
46
48
  codex: "codex";
@@ -66,6 +68,7 @@ declare const doctorRepairSchema: z.ZodObject<{
66
68
  }>;
67
69
  harness: z.ZodNullable<z.ZodEnum<{
68
70
  openclaw: "openclaw";
71
+ omp: "omp";
69
72
  grok: "grok";
70
73
  cursor: "cursor";
71
74
  codex: "codex";
@@ -93,6 +96,7 @@ export declare const doctorReportSchema: z.ZodObject<{
93
96
  detail: z.ZodString;
94
97
  harness: z.ZodNullable<z.ZodEnum<{
95
98
  openclaw: "openclaw";
99
+ omp: "omp";
96
100
  grok: "grok";
97
101
  cursor: "cursor";
98
102
  codex: "codex";
@@ -109,6 +113,7 @@ export declare const doctorReportSchema: z.ZodObject<{
109
113
  harnesses: z.ZodArray<z.ZodObject<{
110
114
  harness: z.ZodEnum<{
111
115
  openclaw: "openclaw";
116
+ omp: "omp";
112
117
  grok: "grok";
113
118
  cursor: "cursor";
114
119
  codex: "codex";
@@ -134,6 +139,7 @@ export declare const doctorReportSchema: z.ZodObject<{
134
139
  }>;
135
140
  harness: z.ZodNullable<z.ZodEnum<{
136
141
  openclaw: "openclaw";
142
+ omp: "omp";
137
143
  grok: "grok";
138
144
  cursor: "cursor";
139
145
  codex: "codex";
@@ -68,6 +68,8 @@ export interface HarnessSpec {
68
68
  keyHeldByCli: boolean;
69
69
  /** Connect writes the MCP config file; no harness binary is spawned. */
70
70
  cliWritesConfig: boolean;
71
+ /** Binary resolves the config path; absent binary, nothing reads it. */
72
+ configPathFromBinary?: true;
71
73
  /** `mcp add` succeeds unauthenticated; OAuth lands at first tool use. */
72
74
  authDeferredAtInstall: boolean;
73
75
  }
@@ -199,6 +201,18 @@ export declare const HARNESS_SPECS: {
199
201
  cliWritesConfig: false;
200
202
  authDeferredAtInstall: false;
201
203
  };
204
+ omp: {
205
+ command: string;
206
+ displayName: string;
207
+ configDir: string;
208
+ reloadAction: string;
209
+ configPathFromBinary: true;
210
+ signInViaCliLogin: false;
211
+ canVerifyAuth: true;
212
+ keyHeldByCli: true;
213
+ cliWritesConfig: true;
214
+ authDeferredAtInstall: false;
215
+ };
202
216
  openclaw: {
203
217
  command: string;
204
218
  displayName: string;
@@ -252,4 +266,4 @@ export type NonNativeHarness = {
252
266
  [K in Harness]: 'urlStyle' extends keyof (typeof HARNESS_SPECS)[K] ? never : K;
253
267
  }[Harness];
254
268
  /** Native MCP harnesses carry add-generation fields; the rest override connect. */
255
- export declare const NATIVE_HARNESSES: ("openclaw" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code")[];
269
+ export declare const NATIVE_HARNESSES: ("openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code")[];
@@ -183,6 +183,18 @@ export const HARNESS_SPECS = {
183
183
  cliWritesConfig: false,
184
184
  authDeferredAtInstall: false,
185
185
  },
186
+ omp: {
187
+ command: 'omp',
188
+ displayName: 'omp',
189
+ configDir: '.omp',
190
+ reloadAction: 'restart it',
191
+ configPathFromBinary: true,
192
+ signInViaCliLogin: false,
193
+ canVerifyAuth: true,
194
+ keyHeldByCli: true,
195
+ cliWritesConfig: true,
196
+ authDeferredAtInstall: false,
197
+ },
186
198
  openclaw: {
187
199
  command: 'openclaw',
188
200
  displayName: 'OpenClaw',
@@ -20,6 +20,8 @@ export interface McpJsonServerEntry {
20
20
  /** Registered endpoint, so a caller can tell "registered" from "registered at the right place". */
21
21
  url?: string;
22
22
  keyMatchesCliKey?: boolean;
23
+ /** Whole-value `${VAR}` template: the variable name, never a key. */
24
+ keyTemplateVar?: string;
23
25
  error?: string;
24
26
  }
25
27
  /** Reports the header's shape, never its value. */
@@ -56,6 +56,8 @@ export function planWrite(target, mcpUrl, apiKey) {
56
56
  ? `${filePath}: would create with a "${target.serverKey}" MCP server entry${authNote}`
57
57
  : `${filePath}: would back up to a timestamped copy, then merge in the "${target.serverKey}" MCP server entry${authNote}`;
58
58
  }
59
+ // omp expands `${VAR}` / `${VAR:-default}` header values at load.
60
+ const ENV_TEMPLATE_VALUE = /^\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}$/;
59
61
  /** Reports the header's shape, never its value. */
60
62
  export function readTinyfishEntry(target) {
61
63
  const existing = readExisting(target);
@@ -71,10 +73,12 @@ export function readTinyfishEntry(target) {
71
73
  const keyHeader = isPlainRecord(headers)
72
74
  ? Object.entries(headers).find((entry) => entry[0].toLowerCase() === 'x-api-key' && typeof entry[1] === 'string')
73
75
  : undefined;
76
+ const templateVar = keyHeader ? ENV_TEMPLATE_VALUE.exec(keyHeader[1])?.[1] : undefined;
74
77
  return {
75
78
  present: true,
76
79
  hasApiKeyHeader: keyHeader !== undefined,
77
80
  ...(matchesCliKey(keyHeader?.[1]) ? { keyMatchesCliKey: true } : {}),
81
+ ...(templateVar ? { keyTemplateVar: templateVar } : {}),
78
82
  ...(typeof entry.url === 'string' ? { url: entry.url } : {}),
79
83
  };
80
84
  }
@@ -0,0 +1,16 @@
1
+ import { type McpJsonServerEntry, type McpJsonTarget, type McpJsonWriteResult } from './mcp-json-config.js';
2
+ /** Tests share one process; the resolver result must not. */
3
+ export declare function resetOmpAgentDirCache(): void;
4
+ /** Profiles move the dir; a guessed path is written unread. */
5
+ export declare function ompAgentDir(): string | undefined;
6
+ /** Undefined means unresolved; connect fails closed via requireOmpTarget. */
7
+ export declare function ompMcpTarget(): McpJsonTarget | undefined;
8
+ export declare function ompMcpPath(): string;
9
+ /** Reports the header's shape, never its value; undefined means unresolved. */
10
+ export declare function readOmpTinyfishEntry(): McpJsonServerEntry | undefined;
11
+ /** Merges only the `tinyfish` key; throws when the dir is unresolved. */
12
+ export declare function writeOmpMcpConfig(mcpUrl: string, apiKey?: string): McpJsonWriteResult;
13
+ /** Dry-run description of the pending write; touches nothing. */
14
+ export declare function planOmpWrite(mcpUrl: string, apiKey?: string): string;
15
+ /** Removes only the `tinyfish` key. */
16
+ export declare function removeOmpMcpServer(): McpJsonWriteResult;
@@ -0,0 +1,71 @@
1
+ import * as path from 'path';
2
+ import spawn from 'cross-spawn';
3
+ import { ConnectStepError } from './connect-runtime.js';
4
+ import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
5
+ import { planWrite, readTinyfishEntry, removeServer, writeMcpConfig, } from './mcp-json-config.js';
6
+ const UNRESOLVED_REASON = '`omp config path` did not report a config directory';
7
+ // Cached: every later read must agree with the write.
8
+ let cachedAgentDir;
9
+ /** Tests share one process; the resolver result must not. */
10
+ export function resetOmpAgentDirCache() {
11
+ cachedAgentDir = undefined;
12
+ }
13
+ /** Profiles move the dir; a guessed path is written unread. */
14
+ export function ompAgentDir() {
15
+ cachedAgentDir ??= { dir: resolveAgentDir() };
16
+ return cachedAgentDir.dir;
17
+ }
18
+ function resolveAgentDir() {
19
+ const result = spawn.sync('omp', ['config', 'path'], {
20
+ encoding: 'utf8',
21
+ timeout: HARNESS_PROBE_TIMEOUT_MS,
22
+ });
23
+ if (result.error || result.status !== 0)
24
+ return undefined;
25
+ const dir = (result.stdout ?? '')
26
+ .split('\n')
27
+ .map((line) => line.trim())
28
+ .filter(Boolean)
29
+ .at(-1);
30
+ return dir && path.isAbsolute(dir) ? dir : undefined;
31
+ }
32
+ /** Undefined means unresolved; connect fails closed via requireOmpTarget. */
33
+ export function ompMcpTarget() {
34
+ const dir = ompAgentDir();
35
+ if (!dir)
36
+ return undefined;
37
+ return { serverKey: 'tinyfish', dir: () => dir, file: () => path.join(dir, 'mcp.json') };
38
+ }
39
+ function requireOmpTarget() {
40
+ const target = ompMcpTarget();
41
+ if (target)
42
+ return target;
43
+ throw new ConnectStepError("Could not locate omp's config directory: `omp config path` did not print one. " +
44
+ 'Check that omp is installed and `omp config path` works, then re-run: tinyfish connect omp', 'invalid_config', { failureDetail: 'omp_agent_dir_unresolved' });
45
+ }
46
+ export function ompMcpPath() {
47
+ return requireOmpTarget().file();
48
+ }
49
+ /** Reports the header's shape, never its value; undefined means unresolved. */
50
+ export function readOmpTinyfishEntry() {
51
+ const target = ompMcpTarget();
52
+ return target ? readTinyfishEntry(target) : undefined;
53
+ }
54
+ /** Merges only the `tinyfish` key; throws when the dir is unresolved. */
55
+ export function writeOmpMcpConfig(mcpUrl, apiKey) {
56
+ return writeMcpConfig(requireOmpTarget(), mcpUrl, apiKey);
57
+ }
58
+ /** Dry-run description of the pending write; touches nothing. */
59
+ export function planOmpWrite(mcpUrl, apiKey) {
60
+ const target = ompMcpTarget();
61
+ if (!target)
62
+ return `${UNRESOLVED_REASON} — connect omp would fail the same way`;
63
+ return planWrite(target, mcpUrl, apiKey);
64
+ }
65
+ /** Removes only the `tinyfish` key. */
66
+ export function removeOmpMcpServer() {
67
+ const target = ompMcpTarget();
68
+ if (!target)
69
+ return { status: 'corrupt_skip', error: UNRESOLVED_REASON };
70
+ return removeServer(target);
71
+ }
@@ -7,6 +7,7 @@ import { NATIVE_BY_HARNESS } from './connect-clients.js';
7
7
  import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
8
8
  import { errLine } from './output.js';
9
9
  import { readCursorTinyfishEntry } from './cursor-config.js';
10
+ import { readOmpTinyfishEntry } from './omp-config.js';
10
11
  import { readHermesKey, resolveHermesHome } from './hermes-env.js';
11
12
  import { readHermesEntry } from './hermes-config.js';
12
13
  import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMode, Registered, } from './harness-detect.js';
@@ -293,6 +294,38 @@ function probeCursor() {
293
294
  ...(entry.keyMatchesCliKey ? { keyMatchesCliKey: true } : {}),
294
295
  };
295
296
  }
297
+ // omp shares Cursor's mcp.json shape; only `omp config path` resolves it.
298
+ function probeOmp() {
299
+ const entry = readOmpTinyfishEntry();
300
+ if (!entry) {
301
+ return {
302
+ registered: Registered.Unknown,
303
+ authMode: AuthMode.Unknown,
304
+ reason: '`omp config path` did not report a config directory',
305
+ };
306
+ }
307
+ if (entry.error) {
308
+ return {
309
+ registered: Registered.Unknown,
310
+ authMode: AuthMode.Unknown,
311
+ reason: 'mcp.json exists but could not be read or parsed',
312
+ };
313
+ }
314
+ if (!entry.present)
315
+ return { registered: Registered.No, authMode: AuthMode.Unknown };
316
+ // A hand-written `${VAR}` header resolves like Codex's env-var key.
317
+ const keyVerdict = entry.keyTemplateVar
318
+ ? envKeyVerdict(entry.keyTemplateVar)
319
+ : entry.keyMatchesCliKey
320
+ ? { keyMatchesCliKey: true }
321
+ : {};
322
+ return {
323
+ registered: Registered.Yes,
324
+ authMode: entry.hasApiKeyHeader ? AuthMode.ApiKey : AuthMode.Unknown,
325
+ ...(entry.url ? { registeredUrl: entry.url } : {}),
326
+ ...keyVerdict,
327
+ };
328
+ }
296
329
  // Both list commands print this header in every state, including "no servers configured".
297
330
  // Exit 0 with output we cannot recognise is not evidence of absence: reporting `no` there would
298
331
  // earn a connect repair off a parse failure, which is the bug class this command exists to kill.
@@ -486,6 +519,7 @@ const PROBES = {
486
519
  cursor: probeCursor,
487
520
  grok: probeGrok,
488
521
  hermes: probeHermes,
522
+ omp: probeOmp,
489
523
  openclaw: probeOpenClaw,
490
524
  opencode: probeOpencode,
491
525
  };
@@ -26,6 +26,7 @@ export declare function postConnectEvent(options: {
26
26
  declare const harnessResultSchema: z.ZodObject<{
27
27
  harness: z.ZodEnum<{
28
28
  openclaw: "openclaw";
29
+ omp: "omp";
29
30
  grok: "grok";
30
31
  cursor: "cursor";
31
32
  codex: "codex";
@@ -59,6 +60,7 @@ export declare const setupCompletedPayloadSchema: z.ZodObject<{
59
60
  harnesses: z.ZodArray<z.ZodObject<{
60
61
  harness: z.ZodEnum<{
61
62
  openclaw: "openclaw";
63
+ omp: "omp";
62
64
  grok: "grok";
63
65
  cursor: "cursor";
64
66
  codex: "codex";
@@ -91,7 +93,7 @@ export type UpgradeScope = 'both' | 'cli' | 'skill';
91
93
  /** `toVersion` is null when the run was interrupted or the installed version could not be read. */
92
94
  export declare function sendUpgradeCompleted(fromVersion: string, toVersion: string | null, outcome: UpgradeOutcome, scope: UpgradeScope, apiKey?: string): Promise<void>;
93
95
  export declare function sendSetupCompleted(mcpUrl: string, harnesses: HarnessResult[], apiKey?: string, mode?: SetupMode, attemptId?: string): Promise<void>;
94
- export declare const DOCTOR_HARNESS_SCOPES: readonly ["all", ...("openclaw" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code")[]];
96
+ export declare const DOCTOR_HARNESS_SCOPES: readonly ["all", ...("openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code")[]];
95
97
  export declare const DOCTOR_ERROR_CLASSES: readonly ["command_not_found", "timeout", "spawn_error", "nonzero_exit", "unexpected_error"];
96
98
  export type DoctorErrorClass = (typeof DOCTOR_ERROR_CLASSES)[number];
97
99
  /** Derived, not read: the catch is generic, so only the code is trustworthy. */
@@ -120,6 +122,7 @@ export declare const doctorCompletedPayloadSchema: z.ZodObject<{
120
122
  }>;
121
123
  harness_scope: z.ZodEnum<{
122
124
  openclaw: "openclaw";
125
+ omp: "omp";
123
126
  grok: "grok";
124
127
  cursor: "cursor";
125
128
  codex: "codex";
@@ -132,6 +135,7 @@ export declare const doctorCompletedPayloadSchema: z.ZodObject<{
132
135
  failed_check_ids: z.ZodArray<z.ZodString>;
133
136
  failed_harnesses: z.ZodArray<z.ZodEnum<{
134
137
  openclaw: "openclaw";
138
+ omp: "omp";
135
139
  grok: "grok";
136
140
  cursor: "cursor";
137
141
  codex: "codex";
@@ -142,6 +146,7 @@ export declare const doctorCompletedPayloadSchema: z.ZodObject<{
142
146
  harnesses: z.ZodArray<z.ZodObject<{
143
147
  harness: z.ZodEnum<{
144
148
  openclaw: "openclaw";
149
+ omp: "omp";
145
150
  grok: "grok";
146
151
  cursor: "cursor";
147
152
  codex: "codex";
@@ -174,6 +179,7 @@ export declare const doctorCouldNotRunPayloadSchema: z.ZodObject<{
174
179
  }>;
175
180
  harness_scope: z.ZodEnum<{
176
181
  openclaw: "openclaw";
182
+ omp: "omp";
177
183
  grok: "grok";
178
184
  cursor: "cursor";
179
185
  codex: "codex";
@@ -209,6 +215,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
209
215
  }>;
210
216
  harness_scope: z.ZodEnum<{
211
217
  openclaw: "openclaw";
218
+ omp: "omp";
212
219
  grok: "grok";
213
220
  cursor: "cursor";
214
221
  codex: "codex";
@@ -221,6 +228,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
221
228
  failed_check_ids: z.ZodArray<z.ZodString>;
222
229
  failed_harnesses: z.ZodArray<z.ZodEnum<{
223
230
  openclaw: "openclaw";
231
+ omp: "omp";
224
232
  grok: "grok";
225
233
  cursor: "cursor";
226
234
  codex: "codex";
@@ -231,6 +239,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
231
239
  harnesses: z.ZodArray<z.ZodObject<{
232
240
  harness: z.ZodEnum<{
233
241
  openclaw: "openclaw";
242
+ omp: "omp";
234
243
  grok: "grok";
235
244
  cursor: "cursor";
236
245
  codex: "codex";
@@ -262,6 +271,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
262
271
  }>;
263
272
  harness_scope: z.ZodEnum<{
264
273
  openclaw: "openclaw";
274
+ omp: "omp";
265
275
  grok: "grok";
266
276
  cursor: "cursor";
267
277
  codex: "codex";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.37.1-next.301",
3
+ "version": "0.37.1-next.302",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {