@tiny-fish/cli 0.48.1-next.383 → 0.48.2-next.386
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 +1 -2
- package/dist/lib/config/command-code-config.d.ts +8 -1
- package/dist/lib/config/command-code-config.js +29 -2
- package/dist/lib/config/connect-config-file.d.ts +22 -0
- package/dist/lib/config/connect-config-file.js +63 -11
- package/dist/lib/config/mcp-json-config.d.ts +1 -1
- package/dist/lib/config/mcp-json-config.js +4 -4
- package/dist/lib/connect-all-auth.js +1 -1
- package/dist/lib/connect-all-uninstall.js +10 -0
- package/dist/lib/connect-clients.d.ts +4 -3
- package/dist/lib/connect-clients.js +4 -19
- package/dist/lib/connect-harness.d.ts +1 -1
- package/dist/lib/connect-harness.js +9 -28
- package/dist/lib/connect-runtime.js +6 -2
- package/dist/lib/harness/harness-messages.d.ts +0 -1
- package/dist/lib/harness/harness-messages.js +0 -3
- package/dist/lib/harness/harness-spec.d.ts +3 -19
- package/dist/lib/harness/harness-spec.js +6 -20
- package/package.json +1 -1
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
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
145
|
-
|
|
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(
|
|
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
|
-
|
|
206
|
+
return registeredId ?? undefined;
|
|
168
207
|
}
|
|
169
208
|
}
|
|
170
209
|
catch {
|
|
171
|
-
// A hand-edited invalid URL
|
|
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
|
-
|
|
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
|
|
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 =
|
|
52
|
-
?
|
|
53
|
-
:
|
|
54
|
-
?
|
|
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) => !!
|
|
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
|
|
115
|
-
export declare
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
export
|
|
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 {
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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', {
|
|
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
|
-
|
|
174
|
-
|
|
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,
|
|
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
|
-
//
|
|
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
|
-
|
|
104
|
-
|
|
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
|
});
|