@tiny-fish/cli 0.48.1-next.383 → 0.48.1

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
@@ -110,8 +110,7 @@ npx -y @tiny-fish/cli@latest connect command-code --api-key sk-tinyfish-...
110
110
  ```
111
111
 
112
112
  Command Code takes an API key only. `cmd mcp add` opens a browser sign-in even when a header is
113
- passed, so TinyFish registers through `mcp add-json`, which writes `~/.commandcode/mcp.json`
114
- without an auth probe. Without a key the command refuses rather than falling back to a sign-in.
113
+ passed, so TinyFish writes the entry into `~/.commandcode/mcp.json` itself. Without a key the command refuses rather than falling back to a sign-in.
115
114
 
116
115
  ### Connect OpenClaw
117
116
 
@@ -1,3 +1,10 @@
1
- import { type McpJsonServerEntry } from './mcp-json-config.js';
1
+ import { type McpJsonServerEntry, type McpJsonWriteResult } from './mcp-json-config.js';
2
+ export declare function commandCodeMcpPath(): string;
2
3
  /** Reports the header's shape, never its value. */
3
4
  export declare function readCommandCodeTinyfishEntry(): McpJsonServerEntry;
5
+ /** Merges only the `tinyfish` key. */
6
+ export declare function writeCommandCodeMcpConfig(mcpUrl: string, apiKey?: string): McpJsonWriteResult;
7
+ /** Dry-run description of the pending write; touches nothing. */
8
+ export declare function planCommandCodeWrite(mcpUrl: string, apiKey?: string): string;
9
+ /** Removes only the `tinyfish` key. */
10
+ export declare function removeCommandCodeMcpServer(): McpJsonWriteResult;
@@ -1,14 +1,41 @@
1
1
  import * as os from 'os';
2
2
  import * as path from 'path';
3
- import { readTinyfishEntry, } from './mcp-json-config.js';
4
- // Read-only: Command Code writes this file itself, through `cmd mcp add-json --scope user`.
3
+ import { planWrite, readTinyfishEntry, removeServer, writeMcpConfig, } from './mcp-json-config.js';
4
+ // Written directly: `commandcode mcp add-json` mangles the JSON body through the win32 .cmd shim.
5
5
  const COMMAND_CODE_TARGET = {
6
6
  serverKey: 'tinyfish',
7
7
  dir: () => path.join(os.homedir(), '.commandcode'),
8
8
  file: () => path.join(os.homedir(), '.commandcode', 'mcp.json'),
9
9
  keyHeader: { name: 'Authorization', valuePrefix: 'Bearer ' },
10
10
  };
11
+ /** The shape `commandcode mcp add-json --scope user` writes. */
12
+ function commandCodeServerEntry(mcpUrl, apiKey) {
13
+ return {
14
+ transport: 'http',
15
+ enabled: true,
16
+ url: mcpUrl,
17
+ ...(apiKey ? { headers: { Authorization: `Bearer ${apiKey}` } } : {}),
18
+ };
19
+ }
20
+ export function commandCodeMcpPath() {
21
+ return COMMAND_CODE_TARGET.file();
22
+ }
11
23
  /** Reports the header's shape, never its value. */
12
24
  export function readCommandCodeTinyfishEntry() {
13
25
  return readTinyfishEntry(COMMAND_CODE_TARGET);
14
26
  }
27
+ /** Merges only the `tinyfish` key. */
28
+ export function writeCommandCodeMcpConfig(mcpUrl, apiKey) {
29
+ return writeMcpConfig(COMMAND_CODE_TARGET, mcpUrl, apiKey, commandCodeServerEntry(mcpUrl, apiKey));
30
+ }
31
+ /** Dry-run description of the pending write; touches nothing. */
32
+ export function planCommandCodeWrite(mcpUrl, apiKey) {
33
+ // Mirrors connect, which refuses rather than write a keyless entry.
34
+ if (!apiKey)
35
+ return 'would refuse: Command Code needs a TinyFish API key (run: tinyfish auth login)';
36
+ return planWrite(COMMAND_CODE_TARGET, mcpUrl, apiKey, commandCodeServerEntry(mcpUrl, apiKey));
37
+ }
38
+ /** Removes only the `tinyfish` key. */
39
+ export function removeCommandCodeMcpServer() {
40
+ return removeServer(COMMAND_CODE_TARGET);
41
+ }
@@ -1,9 +1,28 @@
1
+ import { type OnWalkthroughDecided, type WalkthroughOutcome } from '../connect-clients.js';
1
2
  import { type ConnectAuthMode } from '../connect-runtime.js';
2
3
  import { cursorMcpPath, writeCursorMcpConfig } from './cursor-config.js';
4
+ import { commandCodeMcpPath, writeCommandCodeMcpConfig } from './command-code-config.js';
3
5
  import { ompMcpPath, writeOmpMcpConfig } from './omp-config.js';
4
6
  import { piMcpAdapterState, type PiAdapterState, piMcpPath, writePiMcpConfig } from './pi-config.js';
5
7
  declare function piPostConnectNotes(adapterState?: PiAdapterState): string | undefined;
6
8
  declare const CONFIG_FILE_HARNESSES: {
9
+ 'command-code': {
10
+ skillTarget: {
11
+ skillAgent: "command-code";
12
+ displayName: string;
13
+ };
14
+ registeredUrl: () => string | undefined;
15
+ write: typeof writeCommandCodeMcpConfig;
16
+ configPath: typeof commandCodeMcpPath;
17
+ tryLaunch: () => false;
18
+ launchWalkthrough: (onDecided?: OnWalkthroughDecided, prompt?: string) => Promise<WalkthroughOutcome>;
19
+ copy: {
20
+ verified: string;
21
+ authFailed: (reason: string) => string;
22
+ launched: string;
23
+ reload: string;
24
+ };
25
+ };
7
26
  cursor: {
8
27
  skillTarget: {
9
28
  readonly skillAgent: "cursor";
@@ -28,6 +47,7 @@ declare const CONFIG_FILE_HARNESSES: {
28
47
  write: typeof writeOmpMcpConfig;
29
48
  configPath: typeof ompMcpPath;
30
49
  tryLaunch: () => false;
50
+ launchWalkthrough: (onDecided?: OnWalkthroughDecided, prompt?: string) => Promise<WalkthroughOutcome>;
31
51
  copy: {
32
52
  verified: string;
33
53
  authFailed: (reason: string) => string;
@@ -44,6 +64,7 @@ declare const CONFIG_FILE_HARNESSES: {
44
64
  write: typeof writePiMcpConfig;
45
65
  configPath: typeof piMcpPath;
46
66
  tryLaunch: () => false;
67
+ launchWalkthrough: (onDecided?: OnWalkthroughDecided, prompt?: string) => Promise<WalkthroughOutcome>;
47
68
  adapterState: typeof piMcpAdapterState;
48
69
  postConnectNote: typeof piPostConnectNotes;
49
70
  copy: {
@@ -58,6 +79,7 @@ declare const CONFIG_FILE_HARNESSES: {
58
79
  export declare function connectConfigFileHarness(harness: keyof typeof CONFIG_FILE_HARNESSES, options: {
59
80
  apiKey?: string;
60
81
  mcpUrl: string;
82
+ launch?: boolean;
61
83
  attemptId?: string;
62
84
  verbose?: boolean;
63
85
  authTimeoutMs?: number;
@@ -1,18 +1,20 @@
1
1
  import { z } from 'zod';
2
2
  import { CONNECT_SOURCE, saveConnectContext, validatedApiKey } from '../auth.js';
3
- import { CURSOR_SKILL_TARGET, openExternalUrl } from '../connect-clients.js';
3
+ import { CURSOR_SKILL_TARGET, KEYLESS_ONBOARDING_PROMPT, launchCommandCodeWalkthrough, launchOmpWalkthrough, launchPiWalkthrough, openExternalUrl, } from '../connect-clients.js';
4
4
  import { ensureCliAuthenticated } from '../connect-auth.js';
5
5
  import { installWebSkill } from '../skill-install.js';
6
6
  import { ConnectStepError, createConnectTelemetry, runGuarded, settle, } from '../connect-runtime.js';
7
7
  import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from './cursor-config.js';
8
+ import { commandCodeMcpPath, readCommandCodeTinyfishEntry, writeCommandCodeMcpConfig, } from './command-code-config.js';
8
9
  import { ompMcpPath, readOmpTinyfishEntry, writeOmpMcpConfig } from './omp-config.js';
9
10
  import { PI_ADAPTER_INSTALL_COMMAND, piMcpAdapterState, piSkillDirMismatch, piMcpPath, readPiTinyfishEntry, writePiMcpConfig, } from './pi-config.js';
10
11
  import { AuthMode, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES, isStaleConfig, } from '../harness/harness-detect.js';
12
+ import { harnessSpec } from '../harness/harness-spec.js';
11
13
  import { detectHumanInitiated } from '../harness.js';
12
14
  import { errLine } from '../output.js';
13
15
  import { verifyMcpAuth } from '../verify.js';
14
16
  import { apiBaseFromMcpUrl } from '../mcp-endpoint.js';
15
- import { finishSetupHint, requireKeylessMcp, trackPostInstallFailure } from '../connect-steps.js';
17
+ import { finishSetupHint, requireKeylessMcp, trackPostInstallFailure, walkthroughPhase, } from '../connect-steps.js';
16
18
  const PI_RELOAD_COPY = 'TinyFish is configured in pi. Restart pi, then run `/mcp-auth tinyfish` there if it asks you ' +
17
19
  'to sign in.';
18
20
  // The skill is the working path either way, so the note must not read as a failure.
@@ -40,7 +42,23 @@ function piPostConnectNotes(adapterState) {
40
42
  // Only interactive omp wires the OAuth handler; headless cannot sign in.
41
43
  const OMP_KEYLESS_COPY = 'TinyFish is configured in omp. Open omp and sign in to TinyFish when prompted, or run ' +
42
44
  '`/mcp reauth tinyfish` there.';
45
+ const COMMAND_CODE_RELOAD_COPY = 'TinyFish is configured in Command Code. Restart it to load TinyFish.';
43
46
  const CONFIG_FILE_HARNESSES = {
47
+ 'command-code': {
48
+ skillTarget: { skillAgent: 'command-code', displayName: HARNESS_DISPLAY_NAMES['command-code'] },
49
+ registeredUrl: () => readCommandCodeTinyfishEntry().url,
50
+ write: writeCommandCodeMcpConfig,
51
+ configPath: commandCodeMcpPath,
52
+ tryLaunch: () => false,
53
+ launchWalkthrough: launchCommandCodeWalkthrough,
54
+ copy: {
55
+ verified: "TinyFish is connected and verified (health+auth). Restart Command Code and it's live.",
56
+ authFailed: (reason) => `TinyFish is configured, but the authenticated check failed (${reason}). ` +
57
+ 'Fix: rotate/re-enter your key with `tinyfish auth login`, then re-run: tinyfish connect command-code',
58
+ launched: COMMAND_CODE_RELOAD_COPY,
59
+ reload: COMMAND_CODE_RELOAD_COPY,
60
+ },
61
+ },
44
62
  cursor: {
45
63
  skillTarget: CURSOR_SKILL_TARGET,
46
64
  registeredUrl: () => readCursorTinyfishEntry().url,
@@ -63,6 +81,7 @@ const CONFIG_FILE_HARNESSES = {
63
81
  configPath: ompMcpPath,
64
82
  // No install deeplink exists; the keyless copy carries the sign-in.
65
83
  tryLaunch: () => false,
84
+ launchWalkthrough: launchOmpWalkthrough,
66
85
  copy: {
67
86
  verified: "TinyFish is connected and verified (health+auth). Restart omp and it's live.",
68
87
  authFailed: (reason) => `TinyFish is configured, but the authenticated check failed (${reason}). ` +
@@ -78,6 +97,7 @@ const CONFIG_FILE_HARNESSES = {
78
97
  configPath: piMcpPath,
79
98
  // No install deeplink exists; the copy carries the restart instead.
80
99
  tryLaunch: () => false,
100
+ launchWalkthrough: launchPiWalkthrough,
81
101
  adapterState: piMcpAdapterState,
82
102
  postConnectNote: piPostConnectNotes,
83
103
  copy: {
@@ -104,6 +124,24 @@ function installSkillPostSettle(spec, displayName, state, telemetry, options) {
104
124
  return true;
105
125
  }
106
126
  }
127
+ /** Post-settle like the native walkthrough: a failed launch never fails the connect. */
128
+ async function launchPostSettle(spec, displayName, keyless, state, telemetry) {
129
+ if (!spec.launchWalkthrough)
130
+ return false;
131
+ state.stage = 'walkthrough_launch';
132
+ try {
133
+ await spec.launchWalkthrough(async (outcome) => {
134
+ telemetry.track('checkpoint', { phase: walkthroughPhase(outcome) });
135
+ // Flushed pre-block; the session-long handover would strand the delivery.
136
+ await telemetry.flush();
137
+ }, keyless ? KEYLESS_ONBOARDING_PROMPT : undefined);
138
+ return false;
139
+ }
140
+ catch (error) {
141
+ trackPostInstallFailure(displayName, state, telemetry, error);
142
+ return true;
143
+ }
144
+ }
107
145
  /** Prints last: the outro must own the final line. */
108
146
  function reportPostInstallFailure(harness, failed, onPostInstallFailed) {
109
147
  if (!failed)
@@ -119,6 +157,10 @@ function requireHarnessInstalled(harness) {
119
157
  throw new ConnectStepError(`${HARNESS_DISPLAY_NAMES[harness]} is not installed (no \`${HARNESS_COMMANDS[harness]}\` on PATH). ` +
120
158
  `Install it, then re-run: tinyfish connect ${harness}`, 'harness_not_installed');
121
159
  }
160
+ function refuseKeylessInstall(harness) {
161
+ throw new ConnectStepError(`Connecting ${HARNESS_DISPLAY_NAMES[harness]} needs a TinyFish API key, and TinyFish does not ` +
162
+ `sign in to it any other way. Run: tinyfish auth login, then re-run: tinyfish connect ${harness}`, 'invalid_config', { failureDetail: 'keyless_install_refused' });
163
+ }
122
164
  /** The copy after `completed`: what the user does next depends on the auth path. */
123
165
  async function printConnectedCopy(spec, displayName, opts) {
124
166
  if (opts.keyless) {
@@ -141,16 +183,13 @@ async function printConnectedCopy(spec, displayName, opts) {
141
183
  errLine(spec.copy.reload);
142
184
  }
143
185
  }
144
- /** Writes the harness's MCP config file itself; no `mcp add` exists. */
145
- export async function connectConfigFileHarness(harness, options) {
146
- const spec = CONFIG_FILE_HARNESSES[harness];
147
- const displayName = HARNESS_DISPLAY_NAMES[harness];
148
- let fallbackAttemptId;
186
+ /** A re-run keeps the attempt id already in the entry it is repairing. */
187
+ function reusableAttemptId(spec, harness, mcpUrl) {
149
188
  const registeredUrl = spec.registeredUrl();
150
189
  if (registeredUrl) {
151
190
  try {
152
191
  const registered = new URL(registeredUrl);
153
- const requested = new URL(options.mcpUrl);
192
+ const requested = new URL(mcpUrl);
154
193
  const registeredId = registered.searchParams.get('connect_attempt_id');
155
194
  const registeredSource = registered.searchParams.get('source');
156
195
  const registeredClient = registered.searchParams.get('client');
@@ -164,13 +203,20 @@ export async function connectConfigFileHarness(harness, options) {
164
203
  registeredSource === CONNECT_SOURCE &&
165
204
  registeredClient === harness &&
166
205
  z.uuid().safeParse(registeredId).success) {
167
- fallbackAttemptId = registeredId ?? undefined;
206
+ return registeredId ?? undefined;
168
207
  }
169
208
  }
170
209
  catch {
171
- // A hand-edited invalid URL is repaired below with a fresh attempt id.
210
+ // A hand-edited invalid URL gets repaired with a fresh attempt id.
172
211
  }
173
212
  }
213
+ return undefined;
214
+ }
215
+ /** Writes the harness's MCP config file itself; no `mcp add` exists. */
216
+ export async function connectConfigFileHarness(harness, options) {
217
+ const spec = CONFIG_FILE_HARNESSES[harness];
218
+ const displayName = HARNESS_DISPLAY_NAMES[harness];
219
+ const fallbackAttemptId = reusableAttemptId(spec, harness, options.mcpUrl);
174
220
  const telemetry = createConnectTelemetry(options.mcpUrl, harness, {
175
221
  ...options,
176
222
  fallbackAttemptId,
@@ -189,6 +235,9 @@ export async function connectConfigFileHarness(harness, options) {
189
235
  const resolveAuth = async () => {
190
236
  let resolvedKey = validatedApiKey(options.apiKey);
191
237
  const keyless = !resolvedKey && harness === 'omp';
238
+ // Before sign-in, as native refuses: a login here would bypass the refusal.
239
+ if (!resolvedKey && harnessSpec(harness).keyRequired)
240
+ refuseKeylessInstall(harness);
192
241
  if (keyless) {
193
242
  await requireKeylessMcp(options.mcpUrl);
194
243
  }
@@ -245,7 +294,10 @@ export async function connectConfigFileHarness(harness, options) {
245
294
  const note = spec.postConnectNote?.(piAdapterState);
246
295
  if (note)
247
296
  errLine(note);
248
- reportPostInstallFailure(harness, postInstallFailed, options.onPostInstallFailed);
297
+ const launchFailed = options.launch
298
+ ? await launchPostSettle(spec, displayName, keyless, state, telemetry)
299
+ : false;
300
+ reportPostInstallFailure(harness, postInstallFailed || launchFailed, options.onPostInstallFailed);
249
301
  });
250
302
  return state.authMode;
251
303
  }
@@ -3,7 +3,7 @@ export interface McpJsonTarget {
3
3
  /** Functions so paths resolve at call time. */
4
4
  dir(): string;
5
5
  file(): string;
6
- /** Read-side only; defaults to what the write path emits (Command Code writes its own). */
6
+ /** Read-side only; defaults to the `X-API-Key` header buildTinyfishServerEntry emits. */
7
7
  keyHeader?: {
8
8
  name: string;
9
9
  valuePrefix?: string;
@@ -48,10 +48,10 @@ export function planWrite(target, mcpUrl, apiKey, serverEntry) {
48
48
  if ('error' in existing) {
49
49
  return `${filePath}: existing file is corrupt (${existing.error}) — would skip and leave it untouched`;
50
50
  }
51
- const authNote = serverEntry
52
- ? ` with an ${TINYFISH_ACCESS_MODE_HEADER}: ${TINYFISH_KEYLESS_ACCESS_MODE} header`
53
- : apiKey
54
- ? ' with an API-key header — the key is stored in plaintext in that file (value not shown here)'
51
+ const authNote = apiKey
52
+ ? ' with an API-key header — the key is stored in plaintext in that file (value not shown here)'
53
+ : serverEntry
54
+ ? ` with an ${TINYFISH_ACCESS_MODE_HEADER}: ${TINYFISH_KEYLESS_ACCESS_MODE} header`
55
55
  : '';
56
56
  const nextEntry = serverEntry ?? buildTinyfishServerEntry(mcpUrl, apiKey);
57
57
  const servers = existing.parsed.mcpServers;
@@ -22,7 +22,7 @@ const BROWSER_SIGN_IN = new Set(ALL_HARNESSES.filter((harness) => {
22
22
  return !!native?.loginArgs;
23
23
  }));
24
24
  // Derived: these refuse a keyless install, so no sign-in helps.
25
- const KEY_REQUIRED = new Set(ALL_HARNESSES.filter((harness) => !!NATIVE_BY_HARNESS.get(harness)?.keyRequired));
25
+ const KEY_REQUIRED = new Set(ALL_HARNESSES.filter((harness) => !!harnessSpec(harness).keyRequired));
26
26
  function headlessGate(base, oauthBudget, fixCommand) {
27
27
  const { harness } = base;
28
28
  // A CLI-login sign-in is interactive-only — unboundable headless.
@@ -3,6 +3,7 @@ import { clearConnectContext, connectContextState } from './auth.js';
3
3
  import { NATIVE_BY_HARNESS, openclawSkillUninstall } from './connect-clients.js';
4
4
  import { ConnectInterruptedError, spawnRemoval } from './connect-runtime.js';
5
5
  import { removeCursorMcpServer, planCursorWrite } from './config/cursor-config.js';
6
+ import { commandCodeMcpPath, planCommandCodeWrite, removeCommandCodeMcpServer, } from './config/command-code-config.js';
6
7
  import { planOmpWrite, removeOmpMcpServer } from './config/omp-config.js';
7
8
  import { piMcpPath, planPiWrite, removePiMcpServer } from './config/pi-config.js';
8
9
  import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
@@ -17,6 +18,8 @@ export function planText(harness, mcpUrl, apiKey) {
17
18
  return planOmpWrite(mcpUrl, apiKey);
18
19
  if (harness === 'pi')
19
20
  return planPiWrite(mcpUrl, apiKey);
21
+ if (harness === 'command-code')
22
+ return planCommandCodeWrite(mcpUrl, apiKey);
20
23
  if (harness === 'hermes' && apiKey) {
21
24
  return (`would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ` +
22
25
  `${HERMES_KEY_VAR} to Hermes' .env, installs the tinyfish plugin from npm @latest, ` +
@@ -37,6 +40,9 @@ export function uninstallPlanText(harness) {
37
40
  if (harness === 'pi') {
38
41
  return `would remove the tinyfish entry from ${piMcpPath()}`;
39
42
  }
43
+ if (harness === 'command-code') {
44
+ return `would remove the tinyfish entry from ${commandCodeMcpPath()}`;
45
+ }
40
46
  if (!uninstallRuns(harness)) {
41
47
  return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
42
48
  }
@@ -205,6 +211,10 @@ const MCP_JSON_UNINSTALLS = {
205
211
  remove: removePiMcpServer,
206
212
  fix: `Fix ${piMcpPath()} by hand, then re-run: tinyfish connect --all --uninstall`,
207
213
  },
214
+ 'command-code': {
215
+ remove: removeCommandCodeMcpServer,
216
+ fix: `Fix ${commandCodeMcpPath()} by hand, then re-run: tinyfish connect --all --uninstall`,
217
+ },
208
218
  };
209
219
  function uninstallMcpJson(detection, spec) {
210
220
  let result;
@@ -109,8 +109,9 @@ export declare const OPENCLAW: SupportedCommand;
109
109
  /** "printed" = handed to the user to paste; the walkthrough was never started for them. */
110
110
  export type WalkthroughOutcome = 'launched' | 'printed';
111
111
  /** Awaited before the handover; track+flush must land pre-block. */
112
- type OnWalkthroughDecided = (outcome: WalkthroughOutcome) => void | Promise<void>;
112
+ export type OnWalkthroughDecided = (outcome: WalkthroughOutcome) => void | Promise<void>;
113
113
  export declare function launchNativeMcpClient(client: NativeMcpClient, onDecided?: OnWalkthroughDecided, prompt?: string): Promise<WalkthroughOutcome>;
114
- export declare function launchOmpWalkthrough(onDecided?: OnWalkthroughDecided, prompt?: string): Promise<WalkthroughOutcome>;
115
- export declare function launchPiWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
114
+ export declare const launchOmpWalkthrough: (onDecided?: OnWalkthroughDecided, prompt?: string) => Promise<WalkthroughOutcome>;
115
+ export declare const launchPiWalkthrough: (onDecided?: OnWalkthroughDecided, prompt?: string) => Promise<WalkthroughOutcome>;
116
+ export declare const launchCommandCodeWalkthrough: (onDecided?: OnWalkthroughDecided, prompt?: string) => Promise<WalkthroughOutcome>;
116
117
  export declare function launchOpenClawWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
@@ -228,21 +228,8 @@ function specAddArgs(spec, mcpUrl) {
228
228
  ? ['mcp', 'add', ...flags, 'tinyfish', mcpUrl]
229
229
  : ['mcp', 'add', 'tinyfish', '--url', mcpUrl, ...flags];
230
230
  }
231
- /** `mcp add` probes the server and runs OAuth; add-json only writes config. */
232
- function jsonAddArgs(spec, mcpUrl, apiKey) {
233
- const { header } = spec;
234
- const body = {
235
- transport: 'http',
236
- url: mcpUrl,
237
- ...(header ? { headers: { [header.name]: `${header.valuePrefix ?? ''}${apiKey}` } } : {}),
238
- };
239
- return ['mcp', 'add-json', ...(spec.keyedAddJsonFlags ?? []), 'tinyfish', JSON.stringify(body)];
240
- }
241
231
  function specKeyAuth(spec) {
242
232
  const { header, keyEnvFlag, keyAddFlags } = spec;
243
- if (spec.keyedAddJsonFlags) {
244
- return { addArgs: (mcpUrl, apiKey) => jsonAddArgs(spec, mcpUrl, apiKey) };
245
- }
246
233
  if (keyEnvFlag) {
247
234
  return {
248
235
  envVar: TINYFISH_API_KEY_VAR,
@@ -374,12 +361,10 @@ export async function launchNativeMcpClient(client, onDecided, prompt = DEFAULT_
374
361
  : handOverTerminal(client.command, [prompt], client.displayName), onDecided);
375
362
  }
376
363
  // Via deliverWalkthrough: handOverTerminal alone would wedge a headless --launch.
377
- export function launchOmpWalkthrough(onDecided, prompt = DEFAULT_ONBOARDING_PROMPT) {
378
- return deliverWalkthrough('omp', 'omp', prompt, () => handOverTerminal('omp', [prompt], 'omp'), onDecided);
379
- }
380
- export function launchPiWalkthrough(onDecided) {
381
- return deliverWalkthrough('Pi', 'pi', DEFAULT_ONBOARDING_PROMPT, () => handOverTerminal('pi', [DEFAULT_ONBOARDING_PROMPT], 'Pi'), onDecided);
382
- }
364
+ const launchTerminalWalkthrough = (command, displayName) => (onDecided, prompt = DEFAULT_ONBOARDING_PROMPT) => deliverWalkthrough(displayName, command, prompt, () => handOverTerminal(command, [prompt], displayName), onDecided);
365
+ export const launchOmpWalkthrough = launchTerminalWalkthrough('omp', 'omp');
366
+ export const launchPiWalkthrough = launchTerminalWalkthrough('pi', 'Pi');
367
+ export const launchCommandCodeWalkthrough = launchTerminalWalkthrough('commandcode', 'Command Code');
383
368
  export function launchOpenClawWalkthrough(onDecided) {
384
369
  return deliverWalkthrough('OpenClaw', 'openclaw', OPENCLAW_ONBOARDING_PROMPT, () => handOverTerminal('openclaw', ['chat', '--message', OPENCLAW_ONBOARDING_PROMPT], 'OpenClaw'), onDecided);
385
370
  }
@@ -1,4 +1,4 @@
1
1
  import type { AgentClient, ConnectAuthMode } from './connect-runtime.js';
2
- import { type NativeConnectOptions } from './connect-steps.js';
2
+ import type { NativeConnectOptions } from './connect-steps.js';
3
3
  export declare function launchAgent(client: AgentClient): Promise<void>;
4
4
  export declare function connectHarness(harness: AgentClient, options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
@@ -1,14 +1,13 @@
1
- import { KEYLESS_ONBOARDING_PROMPT, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchPiWalkthrough, launchOpenClawWalkthrough, } from './connect-clients.js';
2
- import { AuthMode } from './harness/harness-detect.js';
1
+ import { NATIVE_BY_HARNESS, launchNativeMcpClient, launchCommandCodeWalkthrough, launchOmpWalkthrough, launchPiWalkthrough, launchOpenClawWalkthrough, } from './connect-clients.js';
3
2
  import { errLine } from './output.js';
4
3
  import { connectNativeMcpClient } from './connect-native.js';
5
4
  import { connectOpenClaw } from './connect-openclaw.js';
6
5
  import { connectConfigFileHarness, connectCursor } from './config/connect-config-file.js';
7
- import { finishSetupHint } from './connect-steps.js';
8
6
  /** Total over the non-native harnesses: a new one fails to compile until wired here. */
9
7
  const LAUNCH_OVERRIDES = {
10
- omp: launchOmpWalkthrough,
11
- pi: launchPiWalkthrough,
8
+ omp: () => launchOmpWalkthrough(),
9
+ pi: () => launchPiWalkthrough(),
10
+ 'command-code': () => launchCommandCodeWalkthrough(),
12
11
  openclaw: launchOpenClawWalkthrough,
13
12
  // Cursor has no chat CLI to launch.
14
13
  cursor: () => {
@@ -35,31 +34,13 @@ const CONNECTOR_OVERRIDES = {
35
34
  }
36
35
  return undefined;
37
36
  },
38
- omp: async (options) => {
39
- const authMode = await connectConfigFileHarness('omp', options);
40
- if (options.launch) {
41
- try {
42
- await launchOmpWalkthrough(undefined, authMode === AuthMode.Keyless ? KEYLESS_ONBOARDING_PROMPT : undefined);
43
- }
44
- catch (error) {
45
- // Post-settle, like the native walkthrough step: never fail the connect.
46
- errLine(error instanceof Error ? error.message : String(error));
47
- errLine(finishSetupHint('omp'));
48
- }
49
- }
50
- return authMode;
51
- },
37
+ omp: (options) => connectConfigFileHarness('omp', options),
52
38
  pi: async (options) => {
53
39
  await connectConfigFileHarness('pi', options);
54
- if (options.launch) {
55
- try {
56
- await launchPiWalkthrough();
57
- }
58
- catch (error) {
59
- errLine(error instanceof Error ? error.message : String(error));
60
- errLine(finishSetupHint('pi'));
61
- }
62
- }
40
+ return undefined;
41
+ },
42
+ 'command-code': async (options) => {
43
+ await connectConfigFileHarness('command-code', options);
63
44
  return undefined;
64
45
  },
65
46
  };
@@ -185,10 +185,14 @@ function throwProbeFailure(client, result) {
185
185
  if (result.error?.code === 'ETIMEDOUT') {
186
186
  const probe = `${client.command} ${client.supportCheck.args.join(' ')}`;
187
187
  throw new ConnectStepError(`${client.displayName} did not respond to \`${probe}\` within ` +
188
- `${probeTimeoutMs(client) / 1000}s. Retry.`, 'timeout', { cause: result.error });
188
+ `${probeTimeoutMs(client) / 1000}s. Retry.`, 'timeout', { cause: result.error, failureDetail: spawnFailureDetail(result) });
189
189
  }
190
190
  // It ran and exited non-zero, so the version is still answerable.
191
- throw new ConnectStepError(client.supportCheck.unavailableMessage, 'harness_command_unsupported', { cause: result.error, harnessVersion: probeHarnessVersion(client) });
191
+ throw new ConnectStepError(client.supportCheck.unavailableMessage, 'harness_command_unsupported', {
192
+ cause: result.error,
193
+ harnessVersion: probeHarnessVersion(client),
194
+ failureDetail: spawnFailureDetail(result),
195
+ });
192
196
  }
193
197
  function supportProbeOutput(client) {
194
198
  const result = spawn.sync(client.command, client.supportCheck.args, {
@@ -13,4 +13,3 @@ export declare const OPENCODE_MODEL_NOTE: string;
13
13
  export declare const KILO_MCP_ADD_UNAVAILABLE_MESSAGE: string;
14
14
  export declare const HERMES_RESTART_NOTE = "Restart your Hermes session to pick up TinyFish \u2014 Hermes discovers MCP servers at startup.";
15
15
  export declare const CLINE_MCP_ADD_UNAVAILABLE_MESSAGE: string;
16
- export declare const COMMAND_CODE_ADD_JSON_UNAVAILABLE_MESSAGE: string;
@@ -53,6 +53,3 @@ export const HERMES_RESTART_NOTE = 'Restart your Hermes session to pick up TinyF
53
53
  export const CLINE_MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this Cline installation supports non-interactive MCP setup: `cline mcp add ' +
54
54
  '--help` did not list `--transport` and `--yes`. Update Cline with `cline --update` and retry.' +
55
55
  SUPPORT_CHECK_DEBUG_HINT;
56
- export const COMMAND_CODE_ADD_JSON_UNAVAILABLE_MESSAGE = 'Could not confirm this Command Code installation supports keyed MCP setup: `commandcode mcp ' +
57
- '--help` did not list `add-json`. Run `commandcode update` and retry.' +
58
- SUPPORT_CHECK_DEBUG_HINT;
@@ -44,8 +44,6 @@ export interface HarnessSpec {
44
44
  keyEnvFlag?: string;
45
45
  /** Keyed adds append these flags; the key itself is seeded elsewhere (Hermes). */
46
46
  keyAddFlags?: string[];
47
- /** Keyed adds go through `mcp add-json`, which takes the URL and header in its body. */
48
- keyedAddJsonFlags?: string[];
49
47
  /** No sign-in exists here, so connect refuses a keyless install (Hermes). */
50
48
  keyRequired?: true;
51
49
  keylessFallback?: true;
@@ -163,23 +161,9 @@ export declare const HARNESS_SPECS: {
163
161
  configDir: string;
164
162
  reloadAction: string;
165
163
  skillAgent: "command-code";
166
- supportCheck: {
167
- args: string[];
168
- patterns: RegExp[];
169
- timeoutMs: number;
170
- unavailableMessage: string;
171
- };
172
164
  keyRequired: true;
173
- header: {
174
- name: string;
175
- sep: ": ";
176
- valuePrefix: string;
177
- };
178
- keyedAddJsonFlags: string[];
179
- removals: {
180
- args: string[];
181
- label: string;
182
- }[];
165
+ keyHeldByCli: true;
166
+ cliWritesConfig: true;
183
167
  };
184
168
  cursor: {
185
169
  command: string;
@@ -325,7 +309,7 @@ export declare const ALL_HARNESSES: readonly Harness[];
325
309
  export declare function harnessSpec(harness: Harness): HarnessSpec;
326
310
  /** The rest of the union: every one of these must supply a connect and launch override. */
327
311
  export type NonNativeHarness = {
328
- [K in Harness]: 'urlStyle' extends keyof (typeof HARNESS_SPECS)[K] ? never : 'keyRequired' extends keyof (typeof HARNESS_SPECS)[K] ? never : K;
312
+ [K in Harness]: 'cliWritesConfig' extends keyof (typeof HARNESS_SPECS)[K] ? K : 'urlStyle' extends keyof (typeof HARNESS_SPECS)[K] ? never : 'keyRequired' extends keyof (typeof HARNESS_SPECS)[K] ? never : K;
329
313
  }[Harness];
330
314
  /** Native MCP harnesses generate an add; key-only ones generate only the keyed form. */
331
315
  export declare const NATIVE_HARNESSES: ("openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "command-code" | "kilo" | "opencode" | "pi" | "cline" | "claude-code")[];
@@ -1,7 +1,6 @@
1
1
  import { HERMES_PLUGIN_NAME } from '../constants.js';
2
- import { CLAUDE_CODE_DEFERRED_AUTH_NOTE, CLAUDE_CODE_KEYED_FALLBACK_NOTE, CLINE_MCP_ADD_UNAVAILABLE_MESSAGE, CODEX_DEFERRED_AUTH_NOTE, CODEX_KEYED_FALLBACK_NOTE, CODEX_MCP_URL_UNAVAILABLE_MESSAGE, COMMAND_CODE_ADD_JSON_UNAVAILABLE_MESSAGE, GROK_HTTP_UNAVAILABLE_MESSAGE, GROK_SIGN_IN_HINT, HERMES_OAUTH_UNAVAILABLE_MESSAGE, HERMES_RESTART_NOTE, KILO_MCP_ADD_UNAVAILABLE_MESSAGE, MCP_ADD_UNAVAILABLE_MESSAGE, OPENCLAW_SKILL_UNAVAILABLE_MESSAGE, OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE, OPENCODE_MODEL_NOTE, } from './harness-messages.js';
2
+ import { CLAUDE_CODE_DEFERRED_AUTH_NOTE, CLAUDE_CODE_KEYED_FALLBACK_NOTE, CLINE_MCP_ADD_UNAVAILABLE_MESSAGE, CODEX_DEFERRED_AUTH_NOTE, CODEX_KEYED_FALLBACK_NOTE, CODEX_MCP_URL_UNAVAILABLE_MESSAGE, GROK_HTTP_UNAVAILABLE_MESSAGE, GROK_SIGN_IN_HINT, HERMES_OAUTH_UNAVAILABLE_MESSAGE, HERMES_RESTART_NOTE, KILO_MCP_ADD_UNAVAILABLE_MESSAGE, MCP_ADD_UNAVAILABLE_MESSAGE, OPENCLAW_SKILL_UNAVAILABLE_MESSAGE, OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE, OPENCODE_MODEL_NOTE, } from './harness-messages.js';
3
3
  const HEADER_FLAG = /(?:^|\s)--header(?:[\s<=]|$)/m;
4
- const ADD_JSON_COMMAND = /^\s*add-json(?:\s|\[)/m;
5
4
  /** One entry per harness; every per-harness list derives from it. */
6
5
  export const HARNESS_SPECS = {
7
6
  'claude-code': {
@@ -90,25 +89,10 @@ export const HARNESS_SPECS = {
90
89
  configDir: '.commandcode',
91
90
  reloadAction: 'restart it',
92
91
  skillAgent: 'command-code',
93
- // add-json is the only command this harness uses, so it is what must exist.
94
- supportCheck: {
95
- args: ['mcp', '--help'],
96
- patterns: [ADD_JSON_COMMAND],
97
- // Command Code self-updates inside the probe, which outlasts the default budget.
98
- timeoutMs: 30_000,
99
- unavailableMessage: COMMAND_CODE_ADD_JSON_UNAVAILABLE_MESSAGE,
100
- },
101
- // Key-only: `cmd mcp add` runs browser OAuth even when a header is supplied.
92
+ // `commandcode mcp add` runs browser OAuth even when a header is supplied.
102
93
  keyRequired: true,
103
- header: { name: 'Authorization', sep: ': ', valuePrefix: 'Bearer ' },
104
- // Default scope is `local`, which writes a per-project entry instead of the user's.
105
- keyedAddJsonFlags: ['--scope', 'user'],
106
- removals: [
107
- {
108
- args: ['mcp', 'remove', 'tinyfish', '--scope', 'user'],
109
- label: 'user TinyFish registration',
110
- },
111
- ],
94
+ keyHeldByCli: true,
95
+ cliWritesConfig: true,
112
96
  },
113
97
  cursor: {
114
98
  command: 'cursor-agent',
@@ -258,5 +242,7 @@ export function harnessSpec(harness) {
258
242
  /** Native MCP harnesses generate an add; key-only ones generate only the keyed form. */
259
243
  export const NATIVE_HARNESSES = ALL_HARNESSES.filter((harness) => {
260
244
  const spec = harnessSpec(harness);
245
+ if (spec.cliWritesConfig)
246
+ return false;
261
247
  return spec.urlStyle !== undefined || spec.keyRequired === true;
262
248
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.48.1-next.383",
3
+ "version": "0.48.1",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {