@tiny-fish/cli 0.45.2-next.354 → 0.45.2-next.357
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/dist/commands/config-claude.js +1 -1
- package/dist/commands/connect.d.ts +0 -37
- package/dist/commands/connect.js +29 -846
- package/dist/commands/onboard.js +1 -1
- package/dist/lib/auth.d.ts +4 -0
- package/dist/lib/auth.js +26 -4
- package/dist/lib/config/cline-config.d.ts +4 -0
- package/dist/lib/config/cline-config.js +36 -0
- package/dist/lib/config/connect-config-file.d.ts +76 -0
- package/dist/lib/config/connect-config-file.js +241 -0
- package/dist/lib/{hermes-config.js → config/hermes-config.js} +2 -2
- package/dist/lib/{mcp-json-config.d.ts → config/mcp-json-config.d.ts} +2 -0
- package/dist/lib/{mcp-json-config.js → config/mcp-json-config.js} +11 -5
- package/dist/lib/{omp-config.js → config/omp-config.js} +2 -2
- package/dist/lib/{pi-config.js → config/pi-config.js} +1 -1
- package/dist/lib/connect-all-uninstall.js +3 -3
- package/dist/lib/connect-all.js +1 -1
- package/dist/lib/connect-clients.d.ts +1 -1
- package/dist/lib/connect-clients.js +8 -1
- package/dist/lib/connect-harness.d.ts +4 -0
- package/dist/lib/connect-harness.js +76 -0
- package/dist/lib/connect-native.d.ts +4 -0
- package/dist/lib/connect-native.js +389 -0
- package/dist/lib/connect-openclaw.d.ts +3 -0
- package/dist/lib/connect-openclaw.js +71 -0
- package/dist/lib/connect-steps.d.ts +38 -0
- package/dist/lib/connect-steps.js +104 -0
- package/dist/lib/doctor-checks.js +1 -1
- package/dist/lib/doctor-repairs.js +1 -1
- package/dist/lib/doctor-report.d.ts +6 -0
- package/dist/lib/harness-detect.js +7 -3
- package/dist/lib/harness-spec.d.ts +23 -2
- package/dist/lib/harness-spec.js +23 -0
- package/dist/lib/harness.js +5 -0
- package/dist/lib/hermes-plugin.js +1 -1
- package/dist/lib/registration-detect.js +22 -5
- package/dist/lib/setup-telemetry.d.ts +10 -0
- package/dist/lib/skill-paths.js +1 -0
- package/package.json +1 -1
- /package/dist/lib/{claude-config.d.ts → config/claude-config.d.ts} +0 -0
- /package/dist/lib/{claude-config.js → config/claude-config.js} +0 -0
- /package/dist/lib/{command-code-config.d.ts → config/command-code-config.d.ts} +0 -0
- /package/dist/lib/{command-code-config.js → config/command-code-config.js} +0 -0
- /package/dist/lib/{cursor-config.d.ts → config/cursor-config.d.ts} +0 -0
- /package/dist/lib/{cursor-config.js → config/cursor-config.js} +0 -0
- /package/dist/lib/{hermes-config.d.ts → config/hermes-config.d.ts} +0 -0
- /package/dist/lib/{omp-config.d.ts → config/omp-config.d.ts} +0 -0
- /package/dist/lib/{pi-config.d.ts → config/pi-config.d.ts} +0 -0
package/dist/commands/onboard.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getApiKey, saveConfig } from '../lib/auth.js';
|
|
|
3
3
|
import { fetchContentGet, searchQuery } from '../lib/client.js';
|
|
4
4
|
import { BASE_URL, TINYFISH_API_KEY_VAR } from '../lib/constants.js';
|
|
5
5
|
import { errLine, handleApiError, outLine } from '../lib/output.js';
|
|
6
|
-
import { connectHarness, launchAgent } from '
|
|
6
|
+
import { connectHarness, launchAgent } from '../lib/connect-harness.js';
|
|
7
7
|
import { HARNESS_DISPLAY_NAMES } from '../lib/harness-detect.js';
|
|
8
8
|
const EXAMPLE_QUERIES = [
|
|
9
9
|
'What are recent changes in the AI dev stack? Give me the top 5 changes that matter, why they matter, and any action needed.',
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export type RecordedAuthMode = 'api-key' | 'keyless' | 'oauth' | 'deferred';
|
|
|
10
10
|
interface ConnectEntry {
|
|
11
11
|
attempt_id: string;
|
|
12
12
|
auth_mode?: RecordedAuthMode;
|
|
13
|
+
/** `connect cline --config-dir`: uninstall and doctor must find the same store. */
|
|
14
|
+
config_dir?: string;
|
|
13
15
|
}
|
|
14
16
|
interface PendingConnectAttempt {
|
|
15
17
|
id: string;
|
|
@@ -34,6 +36,8 @@ export declare function writeConfig(apiKey: string): void;
|
|
|
34
36
|
export declare function saveConfig(apiKey: string): void;
|
|
35
37
|
/** `authMode` omitted rewrites the entry without one: a reconnect that degraded must not keep a stale key claim. */
|
|
36
38
|
export declare function saveConnectContext(client: string, attemptId: string, authMode?: RecordedAuthMode): void;
|
|
39
|
+
/** Merged after connect wrote the entry; a missing entry means the install never landed. */
|
|
40
|
+
export declare function saveConnectConfigDir(client: string, configDir: string): void;
|
|
37
41
|
/** Distinguishes a missing entry from an unreadable config file. */
|
|
38
42
|
export declare function connectContextState(client: string): 'present' | 'absent' | 'unreadable';
|
|
39
43
|
/** Kept marker after uninstall makes doctor offer to reinstall the harness. */
|
package/dist/lib/auth.js
CHANGED
|
@@ -53,10 +53,14 @@ function parseConnectMap(value) {
|
|
|
53
53
|
if (typeof attemptId !== 'string')
|
|
54
54
|
continue;
|
|
55
55
|
const authMode = record?.auth_mode;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const configDir = record?.config_dir;
|
|
57
|
+
out[client] = {
|
|
58
|
+
attempt_id: attemptId,
|
|
59
|
+
...(typeof authMode === 'string' && RECORDED_AUTH_MODES.includes(authMode)
|
|
60
|
+
? { auth_mode: authMode }
|
|
61
|
+
: {}),
|
|
62
|
+
...(typeof configDir === 'string' ? { config_dir: configDir } : {}),
|
|
63
|
+
};
|
|
60
64
|
}
|
|
61
65
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
62
66
|
}
|
|
@@ -144,6 +148,24 @@ export function saveConnectContext(client, attemptId, authMode) {
|
|
|
144
148
|
errLine(`Warning: could not persist connect context: ${e instanceof Error ? e.message : String(e)}`);
|
|
145
149
|
}
|
|
146
150
|
}
|
|
151
|
+
/** Merged after connect wrote the entry; a missing entry means the install never landed. */
|
|
152
|
+
export function saveConnectConfigDir(client, configDir) {
|
|
153
|
+
try {
|
|
154
|
+
updateConfig((config) => {
|
|
155
|
+
const entry = config.connect?.[client];
|
|
156
|
+
if (!entry)
|
|
157
|
+
return config;
|
|
158
|
+
return {
|
|
159
|
+
...config,
|
|
160
|
+
connect: { ...config.connect, [client]: { ...entry, config_dir: configDir } },
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
// Best-effort, like saveConnectContext: a working install must not fail on this.
|
|
166
|
+
errLine(`Warning: could not persist the config dir: ${e instanceof Error ? e.message : String(e)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
147
169
|
/** Distinguishes a missing entry from an unreadable config file. */
|
|
148
170
|
export function connectContextState(client) {
|
|
149
171
|
let raw;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { loadConfig } from '../auth.js';
|
|
4
|
+
import { readTinyfishEntry, } from './mcp-json-config.js';
|
|
5
|
+
/** A `--config-dir` install outlives the flag, so later runs read it back. */
|
|
6
|
+
function recordedConfigDir() {
|
|
7
|
+
try {
|
|
8
|
+
return loadConfig().connect?.['cline']?.config_dir;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// An unreadable config must degrade to the default, never throw out of a probe.
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Cline reads both raw, so neither is tilde-expanded here either.
|
|
16
|
+
export function clineConfigDir() {
|
|
17
|
+
const override = process.env['CLINE_DIR']?.trim() || recordedConfigDir();
|
|
18
|
+
return override ? path.resolve(override) : path.join(os.homedir(), '.cline'); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
|
|
19
|
+
}
|
|
20
|
+
/** CLINE_DIR moves the whole tree; CLINE_DATA_DIR moves only the data subtree. */
|
|
21
|
+
function clineDataDir() {
|
|
22
|
+
const override = process.env['CLINE_DATA_DIR']?.trim();
|
|
23
|
+
return override ? path.resolve(override) : path.join(clineConfigDir(), 'data');
|
|
24
|
+
}
|
|
25
|
+
// Read-only: Cline writes this file itself, through `cline mcp add --yes`.
|
|
26
|
+
const CLINE_TARGET = {
|
|
27
|
+
serverKey: 'tinyfish',
|
|
28
|
+
dir: clineConfigDir,
|
|
29
|
+
file: () => path.join(clineDataDir(), 'settings', 'cline_mcp_settings.json'),
|
|
30
|
+
keyHeader: { name: 'X-API-Key' },
|
|
31
|
+
transportKey: 'transport',
|
|
32
|
+
};
|
|
33
|
+
/** Reports the header's shape, never its value. */
|
|
34
|
+
export function readClineTinyfishEntry() {
|
|
35
|
+
return readTinyfishEntry(CLINE_TARGET);
|
|
36
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type ConnectAuthMode } from '../connect-runtime.js';
|
|
2
|
+
import { cursorMcpPath, writeCursorMcpConfig } from './cursor-config.js';
|
|
3
|
+
import { ompMcpPath, writeOmpMcpConfig } from './omp-config.js';
|
|
4
|
+
import { piMcpPath, writePiMcpConfig } from './pi-config.js';
|
|
5
|
+
declare function piPostConnectNotes(): string | undefined;
|
|
6
|
+
declare const CONFIG_FILE_HARNESSES: {
|
|
7
|
+
cursor: {
|
|
8
|
+
skillTarget: {
|
|
9
|
+
readonly skillAgent: "cursor";
|
|
10
|
+
readonly displayName: string;
|
|
11
|
+
};
|
|
12
|
+
registeredUrl: () => string | undefined;
|
|
13
|
+
write: typeof writeCursorMcpConfig;
|
|
14
|
+
configPath: typeof cursorMcpPath;
|
|
15
|
+
tryLaunch: typeof launchCursorDeeplink;
|
|
16
|
+
copy: {
|
|
17
|
+
verified: string;
|
|
18
|
+
authFailed: (reason: string) => string;
|
|
19
|
+
launched: string;
|
|
20
|
+
reload: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
omp: {
|
|
24
|
+
skillTarget: {
|
|
25
|
+
displayName: string;
|
|
26
|
+
};
|
|
27
|
+
registeredUrl: () => string | undefined;
|
|
28
|
+
write: typeof writeOmpMcpConfig;
|
|
29
|
+
configPath: typeof ompMcpPath;
|
|
30
|
+
tryLaunch: () => false;
|
|
31
|
+
copy: {
|
|
32
|
+
verified: string;
|
|
33
|
+
authFailed: (reason: string) => string;
|
|
34
|
+
launched: string;
|
|
35
|
+
reload: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
pi: {
|
|
39
|
+
skillTarget: {
|
|
40
|
+
skillAgent: "pi";
|
|
41
|
+
displayName: string;
|
|
42
|
+
};
|
|
43
|
+
registeredUrl: () => string | undefined;
|
|
44
|
+
write: typeof writePiMcpConfig;
|
|
45
|
+
configPath: typeof piMcpPath;
|
|
46
|
+
tryLaunch: () => false;
|
|
47
|
+
postConnectNote: typeof piPostConnectNotes;
|
|
48
|
+
copy: {
|
|
49
|
+
verified: string;
|
|
50
|
+
authFailed: (reason: string) => string;
|
|
51
|
+
launched: string;
|
|
52
|
+
reload: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/** Writes the harness's MCP config file itself; no `mcp add` exists. */
|
|
57
|
+
export declare function connectConfigFileHarness(harness: keyof typeof CONFIG_FILE_HARNESSES, options: {
|
|
58
|
+
apiKey?: string;
|
|
59
|
+
mcpUrl: string;
|
|
60
|
+
attemptId?: string;
|
|
61
|
+
verbose?: boolean;
|
|
62
|
+
authTimeoutMs?: number;
|
|
63
|
+
onPostInstallFailed?: () => void;
|
|
64
|
+
}): Promise<ConnectAuthMode | undefined>;
|
|
65
|
+
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
66
|
+
export declare function connectCursor(options: {
|
|
67
|
+
apiKey?: string;
|
|
68
|
+
mcpUrl: string;
|
|
69
|
+
attemptId?: string;
|
|
70
|
+
verbose?: boolean;
|
|
71
|
+
authTimeoutMs?: number;
|
|
72
|
+
onPostInstallFailed?: () => void;
|
|
73
|
+
}): Promise<void>;
|
|
74
|
+
/** Best-effort: false when no handler/open fails — caller falls back to reload copy. */
|
|
75
|
+
declare function launchCursorDeeplink(mcpUrl: string): boolean;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CONNECT_SOURCE, saveConnectContext, validatedApiKey } from '../auth.js';
|
|
3
|
+
import { CURSOR_SKILL_TARGET, openExternalUrl } from '../connect-clients.js';
|
|
4
|
+
import { ensureCliAuthenticated } from '../connect-auth.js';
|
|
5
|
+
import { installWebSkill } from '../skill-install.js';
|
|
6
|
+
import { ConnectStepError, createConnectTelemetry, runGuarded, settle, } from '../connect-runtime.js';
|
|
7
|
+
import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from './cursor-config.js';
|
|
8
|
+
import { ompMcpPath, readOmpTinyfishEntry, writeOmpMcpConfig } from './omp-config.js';
|
|
9
|
+
import { PI_ADAPTER_INSTALL_COMMAND, piMcpAdapterState, piSkillDirMismatch, piMcpPath, readPiTinyfishEntry, writePiMcpConfig, } from './pi-config.js';
|
|
10
|
+
import { AuthMode, HARNESS_DISPLAY_NAMES } from '../harness-detect.js';
|
|
11
|
+
import { detectHumanInitiated } from '../harness.js';
|
|
12
|
+
import { errLine } from '../output.js';
|
|
13
|
+
import { verifyMcpAuth } from '../verify.js';
|
|
14
|
+
import { apiBaseFromMcpUrl } from '../mcp-endpoint.js';
|
|
15
|
+
import { finishSetupHint, requireKeylessMcp, trackPostInstallFailure } from '../connect-steps.js';
|
|
16
|
+
const PI_RELOAD_COPY = 'TinyFish is configured in pi. Restart pi, then run `/mcp-auth tinyfish` there if it asks you ' +
|
|
17
|
+
'to sign in.';
|
|
18
|
+
// The skill is the working path either way, so the note must not read as a failure.
|
|
19
|
+
const PI_ADAPTER_DORMANT_NOTE = 'Note: pi has no built-in MCP support, so the entry in its mcp.json stays dormant until you ' +
|
|
20
|
+
`run \`${PI_ADAPTER_INSTALL_COMMAND}\`. The TinyFish skill works without it.`;
|
|
21
|
+
// Only the dormant case was spoken for, leaving a working MCP install silent.
|
|
22
|
+
const PI_ADAPTER_LIVE_NOTE = 'pi-mcp-adapter is installed, so pi loads the TinyFish MCP tools as well as the skill once ' +
|
|
23
|
+
'you restart it.';
|
|
24
|
+
function piAdapterNote() {
|
|
25
|
+
const state = piMcpAdapterState();
|
|
26
|
+
if (state === 'installed')
|
|
27
|
+
return PI_ADAPTER_LIVE_NOTE;
|
|
28
|
+
return state === 'absent' ? PI_ADAPTER_DORMANT_NOTE : undefined;
|
|
29
|
+
}
|
|
30
|
+
// The `skills` CLI writes to the default dir even when pi reads another one.
|
|
31
|
+
function piPostConnectNotes() {
|
|
32
|
+
const notes = [piAdapterNote()];
|
|
33
|
+
const skills = piSkillDirMismatch();
|
|
34
|
+
if (skills) {
|
|
35
|
+
notes.push(`Note: PI_CODING_AGENT_DIR points pi at ${skills.readFrom}, but the TinyFish skill ` +
|
|
36
|
+
`installed to ${skills.installedTo}. Copy or symlink it across for pi to load it.`);
|
|
37
|
+
}
|
|
38
|
+
const printable = notes.filter(Boolean);
|
|
39
|
+
return printable.length > 0 ? printable.join('\n') : undefined;
|
|
40
|
+
}
|
|
41
|
+
// Only interactive omp wires the OAuth handler; headless cannot sign in.
|
|
42
|
+
const OMP_KEYLESS_COPY = 'TinyFish is configured in omp. Open omp and sign in to TinyFish when prompted, or run ' +
|
|
43
|
+
'`/mcp reauth tinyfish` there.';
|
|
44
|
+
const CONFIG_FILE_HARNESSES = {
|
|
45
|
+
cursor: {
|
|
46
|
+
skillTarget: CURSOR_SKILL_TARGET,
|
|
47
|
+
registeredUrl: () => readCursorTinyfishEntry().url,
|
|
48
|
+
write: writeCursorMcpConfig,
|
|
49
|
+
configPath: cursorMcpPath,
|
|
50
|
+
tryLaunch: launchCursorDeeplink,
|
|
51
|
+
copy: {
|
|
52
|
+
verified: "TinyFish is connected and verified (health+auth). Reload the Cursor window (or restart cursor-agent) and it's live.",
|
|
53
|
+
authFailed: (reason) => `TinyFish is configured, but the authenticated check failed (${reason}). ` +
|
|
54
|
+
'Fix: rotate/re-enter your key with `tinyfish auth login`, then re-run: tinyfish connect cursor',
|
|
55
|
+
launched: 'Cursor should pop a TinyFish install confirmation — approve it, then sign in when prompted.',
|
|
56
|
+
reload: 'TinyFish is connected. Reload the Cursor window, then approve/sign in under Settings → MCP.',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
omp: {
|
|
60
|
+
// No skill exists for omp; installWebSkill returns without one.
|
|
61
|
+
skillTarget: { displayName: HARNESS_DISPLAY_NAMES.omp },
|
|
62
|
+
registeredUrl: () => readOmpTinyfishEntry()?.url,
|
|
63
|
+
write: writeOmpMcpConfig,
|
|
64
|
+
configPath: ompMcpPath,
|
|
65
|
+
// No install deeplink exists; the keyless copy carries the sign-in.
|
|
66
|
+
tryLaunch: () => false,
|
|
67
|
+
copy: {
|
|
68
|
+
verified: "TinyFish is connected and verified (health+auth). Restart omp and it's live.",
|
|
69
|
+
authFailed: (reason) => `TinyFish is configured, but the authenticated check failed (${reason}). ` +
|
|
70
|
+
'Fix: rotate/re-enter your key with `tinyfish auth login`, then re-run: tinyfish connect omp',
|
|
71
|
+
launched: OMP_KEYLESS_COPY,
|
|
72
|
+
reload: OMP_KEYLESS_COPY,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
pi: {
|
|
76
|
+
skillTarget: { skillAgent: 'pi', displayName: HARNESS_DISPLAY_NAMES.pi },
|
|
77
|
+
registeredUrl: () => readPiTinyfishEntry().url,
|
|
78
|
+
write: writePiMcpConfig,
|
|
79
|
+
configPath: piMcpPath,
|
|
80
|
+
// No install deeplink exists; the copy carries the restart instead.
|
|
81
|
+
tryLaunch: () => false,
|
|
82
|
+
postConnectNote: piPostConnectNotes,
|
|
83
|
+
copy: {
|
|
84
|
+
verified: "TinyFish is connected and verified (health+auth). Restart pi and it's live.",
|
|
85
|
+
authFailed: (reason) => `TinyFish is configured, but the authenticated check failed (${reason}). ` +
|
|
86
|
+
'Fix: rotate/re-enter your key with `tinyfish auth login`, then re-run: tinyfish connect pi',
|
|
87
|
+
launched: PI_RELOAD_COPY,
|
|
88
|
+
reload: PI_RELOAD_COPY,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
/** Cosmetic and post-settle: a skill failure must not fail the connection. */
|
|
93
|
+
function installSkillPostSettle(spec, displayName, state, telemetry, options) {
|
|
94
|
+
if (!spec.skillTarget.skillAgent)
|
|
95
|
+
return false;
|
|
96
|
+
state.stage = 'skill_install';
|
|
97
|
+
try {
|
|
98
|
+
installWebSkill(spec.skillTarget, { verbose: options.verbose ?? false });
|
|
99
|
+
telemetry.track('checkpoint', { phase: 'skill_installed' });
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
trackPostInstallFailure(displayName, state, telemetry, error);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Prints last: the outro must own the final line. */
|
|
108
|
+
function reportPostInstallFailure(harness, failed, onPostInstallFailed) {
|
|
109
|
+
if (!failed)
|
|
110
|
+
return;
|
|
111
|
+
errLine(finishSetupHint(harness));
|
|
112
|
+
onPostInstallFailed?.();
|
|
113
|
+
}
|
|
114
|
+
/** Writes the harness's MCP config file itself; no `mcp add` exists. */
|
|
115
|
+
export async function connectConfigFileHarness(harness, options) {
|
|
116
|
+
const spec = CONFIG_FILE_HARNESSES[harness];
|
|
117
|
+
const displayName = HARNESS_DISPLAY_NAMES[harness];
|
|
118
|
+
let fallbackAttemptId;
|
|
119
|
+
const registeredUrl = spec.registeredUrl();
|
|
120
|
+
if (registeredUrl) {
|
|
121
|
+
try {
|
|
122
|
+
const registered = new URL(registeredUrl);
|
|
123
|
+
const requested = new URL(options.mcpUrl);
|
|
124
|
+
const registeredId = registered.searchParams.get('connect_attempt_id');
|
|
125
|
+
const registeredSource = registered.searchParams.get('source');
|
|
126
|
+
const registeredClient = registered.searchParams.get('client');
|
|
127
|
+
for (const key of ['source', 'client', 'connect_attempt_id']) {
|
|
128
|
+
registered.searchParams.delete(key);
|
|
129
|
+
requested.searchParams.delete(key);
|
|
130
|
+
}
|
|
131
|
+
registered.searchParams.sort();
|
|
132
|
+
requested.searchParams.sort();
|
|
133
|
+
if (registered.toString() === requested.toString() &&
|
|
134
|
+
registeredSource === CONNECT_SOURCE &&
|
|
135
|
+
registeredClient === harness &&
|
|
136
|
+
z.uuid().safeParse(registeredId).success) {
|
|
137
|
+
fallbackAttemptId = registeredId ?? undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// A hand-edited invalid URL is repaired below with a fresh attempt id.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const telemetry = createConnectTelemetry(options.mcpUrl, harness, {
|
|
145
|
+
...options,
|
|
146
|
+
fallbackAttemptId,
|
|
147
|
+
});
|
|
148
|
+
// Never degraded: no sign-in command to be missing. False, not absent, so a `= false`
|
|
149
|
+
// filter still catches these harnesses.
|
|
150
|
+
const state = {
|
|
151
|
+
stage: 'prerequisite_check',
|
|
152
|
+
settled: false,
|
|
153
|
+
harnessDegraded: false,
|
|
154
|
+
};
|
|
155
|
+
await runGuarded(state, telemetry, async () => {
|
|
156
|
+
telemetry.track('started');
|
|
157
|
+
telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
|
|
158
|
+
const resolveAuth = async () => {
|
|
159
|
+
let resolvedKey = validatedApiKey(options.apiKey);
|
|
160
|
+
const keyless = !resolvedKey && harness === 'omp';
|
|
161
|
+
if (keyless) {
|
|
162
|
+
await requireKeylessMcp(options.mcpUrl);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
state.stage = 'authentication';
|
|
166
|
+
ensureCliAuthenticated(harness, options.apiKey, {
|
|
167
|
+
interactiveLogin: options.authTimeoutMs === undefined,
|
|
168
|
+
});
|
|
169
|
+
resolvedKey = validatedApiKey(options.apiKey);
|
|
170
|
+
telemetry.track('checkpoint', { phase: 'authenticated' });
|
|
171
|
+
if (!resolvedKey) {
|
|
172
|
+
errLine('Ignoring the stored API key: invalid format. Run: tinyfish auth login');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { resolvedKey, keyless };
|
|
176
|
+
};
|
|
177
|
+
const { resolvedKey, keyless } = await resolveAuth();
|
|
178
|
+
state.stage = 'registration';
|
|
179
|
+
errLine(`Adding TinyFish to ${displayName}...`);
|
|
180
|
+
const mcpUrl = new URL(options.mcpUrl);
|
|
181
|
+
mcpUrl.searchParams.set('source', CONNECT_SOURCE);
|
|
182
|
+
mcpUrl.searchParams.set('client', harness);
|
|
183
|
+
mcpUrl.searchParams.set('connect_attempt_id', telemetry.attemptId);
|
|
184
|
+
const result = spec.write(mcpUrl.toString(), resolvedKey);
|
|
185
|
+
if (result.status === 'corrupt_skip') {
|
|
186
|
+
// The message fallback would leak the config path; tag instead.
|
|
187
|
+
throw new ConnectStepError(`Could not update ${spec.configPath()}: existing file could not be read or is not valid JSON (${result.error}). ` +
|
|
188
|
+
`Fix the file, then re-run: tinyfish connect ${harness}`, 'invalid_config', { failureDetail: `${harness}_mcp_json_corrupt` });
|
|
189
|
+
}
|
|
190
|
+
if (result.backupPath) {
|
|
191
|
+
errLine(`Backed up existing ${displayName} MCP config to ${result.backupPath}`);
|
|
192
|
+
}
|
|
193
|
+
if (result.repaired) {
|
|
194
|
+
errLine('Repaired the existing TinyFish entry (updated auth/config to current).');
|
|
195
|
+
}
|
|
196
|
+
telemetry.track('checkpoint', { phase: 'registered' });
|
|
197
|
+
let authMode = keyless ? AuthMode.Keyless : 'deferred';
|
|
198
|
+
if (resolvedKey)
|
|
199
|
+
authMode = AuthMode.ApiKey;
|
|
200
|
+
state.authMode = authMode;
|
|
201
|
+
if (keyless)
|
|
202
|
+
saveConnectContext(harness, telemetry.attemptId, AuthMode.Keyless);
|
|
203
|
+
else
|
|
204
|
+
saveConnectContext(harness, telemetry.attemptId);
|
|
205
|
+
settle(state, telemetry, 'completed', { authMode });
|
|
206
|
+
const postInstallFailed = installSkillPostSettle(spec, displayName, state, telemetry, options);
|
|
207
|
+
if (keyless) {
|
|
208
|
+
errLine(`TinyFish keyless Search is connected in ${displayName}.`);
|
|
209
|
+
}
|
|
210
|
+
else if (resolvedKey) {
|
|
211
|
+
// Deeplink would embed the key in a URL (process args, LaunchServices logs) — reload instead.
|
|
212
|
+
const verify = await verifyMcpAuth(resolvedKey, apiBaseFromMcpUrl(options.mcpUrl));
|
|
213
|
+
if (verify.ok) {
|
|
214
|
+
errLine(spec.copy.verified);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
errLine(spec.copy.authFailed(verify.reason ?? 'unknown'));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (detectHumanInitiated() && spec.tryLaunch(mcpUrl.toString())) {
|
|
221
|
+
errLine(spec.copy.launched);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
errLine(spec.copy.reload);
|
|
225
|
+
}
|
|
226
|
+
const note = spec.postConnectNote?.();
|
|
227
|
+
if (note)
|
|
228
|
+
errLine(note);
|
|
229
|
+
reportPostInstallFailure(harness, postInstallFailed, options.onPostInstallFailed);
|
|
230
|
+
});
|
|
231
|
+
return state.authMode;
|
|
232
|
+
}
|
|
233
|
+
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
234
|
+
export async function connectCursor(options) {
|
|
235
|
+
await connectConfigFileHarness('cursor', options);
|
|
236
|
+
}
|
|
237
|
+
/** Best-effort: false when no handler/open fails — caller falls back to reload copy. */
|
|
238
|
+
function launchCursorDeeplink(mcpUrl) {
|
|
239
|
+
const result = openExternalUrl(cursorInstallDeeplink(mcpUrl));
|
|
240
|
+
return !result.error && result.status === 0;
|
|
241
|
+
}
|
|
@@ -2,8 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { TINYFISH_ACCESS_MODE_HEADER, TINYFISH_KEYLESS_ACCESS_MODE } from '
|
|
6
|
-
import { HERMES_KEY_VAR } from '
|
|
5
|
+
import { TINYFISH_ACCESS_MODE_HEADER, TINYFISH_KEYLESS_ACCESS_MODE } from '../constants.js';
|
|
6
|
+
import { HERMES_KEY_VAR } from '../hermes-env.js';
|
|
7
7
|
/** Every message about the entry names this path. */
|
|
8
8
|
export function hermesConfigPath(home) {
|
|
9
9
|
return path.join(home, 'config.yaml');
|
|
@@ -8,6 +8,8 @@ export interface McpJsonTarget {
|
|
|
8
8
|
name: string;
|
|
9
9
|
valuePrefix?: string;
|
|
10
10
|
};
|
|
11
|
+
/** Cline nests url and headers under this key; every other harness is flat. */
|
|
12
|
+
transportKey?: string;
|
|
11
13
|
}
|
|
12
14
|
export interface McpJsonWriteResult {
|
|
13
15
|
status: 'written' | 'unchanged' | 'corrupt_skip';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
-
import { matchesCliKey } from '
|
|
3
|
-
import { TINYFISH_ACCESS_MODE_HEADER, TINYFISH_KEYLESS_ACCESS_MODE } from '
|
|
2
|
+
import { matchesCliKey } from '../auth.js';
|
|
3
|
+
import { TINYFISH_ACCESS_MODE_HEADER, TINYFISH_KEYLESS_ACCESS_MODE } from '../constants.js';
|
|
4
4
|
function isPlainRecord(value) {
|
|
5
5
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
6
6
|
}
|
|
@@ -65,6 +65,11 @@ export function planWrite(target, mcpUrl, apiKey, serverEntry) {
|
|
|
65
65
|
}
|
|
66
66
|
// omp expands `${VAR}` / `${VAR:-default}` header values at load.
|
|
67
67
|
const ENV_TEMPLATE_VALUE = /^\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}$/;
|
|
68
|
+
/** Cline nests url and headers under `transport`; `enabled` stays on the entry itself. */
|
|
69
|
+
function entryFields(entry, transportKey) {
|
|
70
|
+
const nested = transportKey ? entry[transportKey] : undefined;
|
|
71
|
+
return isPlainRecord(nested) ? nested : entry;
|
|
72
|
+
}
|
|
68
73
|
/** Reports the header's shape, never its value. */
|
|
69
74
|
export function readTinyfishEntry(target) {
|
|
70
75
|
const existing = readExisting(target);
|
|
@@ -75,9 +80,10 @@ export function readTinyfishEntry(target) {
|
|
|
75
80
|
const entry = isPlainRecord(servers) ? servers[target.serverKey] : undefined;
|
|
76
81
|
if (!isPlainRecord(entry))
|
|
77
82
|
return { present: false, hasApiKeyHeader: false };
|
|
78
|
-
const
|
|
83
|
+
const fields = entryFields(entry, target.transportKey);
|
|
84
|
+
const key = readKeyHeader(fields.headers, target.keyHeader);
|
|
79
85
|
const templateVar = key ? ENV_TEMPLATE_VALUE.exec(key.value)?.[1] : undefined;
|
|
80
|
-
const accessMode = readKeyHeader(
|
|
86
|
+
const accessMode = readKeyHeader(fields.headers, { name: TINYFISH_ACCESS_MODE_HEADER })?.value;
|
|
81
87
|
const keyless = accessMode?.trim().toLowerCase() === TINYFISH_KEYLESS_ACCESS_MODE;
|
|
82
88
|
return {
|
|
83
89
|
present: true,
|
|
@@ -86,7 +92,7 @@ export function readTinyfishEntry(target) {
|
|
|
86
92
|
...(entry.enabled === false ? { enabled: false } : {}),
|
|
87
93
|
...(matchesCliKey(key?.value) ? { keyMatchesCliKey: true } : {}),
|
|
88
94
|
...(templateVar ? { keyTemplateVar: templateVar } : {}),
|
|
89
|
-
...(typeof
|
|
95
|
+
...(typeof fields.url === 'string' ? { url: fields.url } : {}),
|
|
90
96
|
};
|
|
91
97
|
}
|
|
92
98
|
/** Reports the key header's shape, never its value. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import spawn from 'cross-spawn';
|
|
3
|
-
import { ConnectStepError } from '
|
|
4
|
-
import { HARNESS_PROBE_TIMEOUT_MS } from '
|
|
3
|
+
import { ConnectStepError } from '../connect-runtime.js';
|
|
4
|
+
import { HARNESS_PROBE_TIMEOUT_MS } from '../constants.js';
|
|
5
5
|
import { buildTinyfishKeylessServerEntry, planWrite, readTinyfishEntry, removeServer, writeMcpConfig, } from './mcp-json-config.js';
|
|
6
6
|
const UNRESOLVED_REASON = '`omp config path` did not report a config directory';
|
|
7
7
|
// Cached: every later read must agree with the write.
|
|
@@ -3,7 +3,7 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import spawn from 'cross-spawn';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import { HARNESS_PROBE_TIMEOUT_MS } from '
|
|
6
|
+
import { HARNESS_PROBE_TIMEOUT_MS } from '../constants.js';
|
|
7
7
|
import { planWrite, readTinyfishEntry, removeServer, writeMcpConfig, } from './mcp-json-config.js';
|
|
8
8
|
// pi expands `~` and `~/`; writing one literally would mkdir '~'.
|
|
9
9
|
function expandTilde(dir) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
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
|
-
import { removeCursorMcpServer, planCursorWrite } from './cursor-config.js';
|
|
5
|
-
import { planOmpWrite, removeOmpMcpServer } from './omp-config.js';
|
|
6
|
-
import { piMcpPath, planPiWrite, removePiMcpServer } from './pi-config.js';
|
|
4
|
+
import { removeCursorMcpServer, planCursorWrite } from './config/cursor-config.js';
|
|
5
|
+
import { planOmpWrite, removeOmpMcpServer } from './config/omp-config.js';
|
|
6
|
+
import { piMcpPath, planPiWrite, removePiMcpServer } from './config/pi-config.js';
|
|
7
7
|
import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
|
|
8
8
|
import { clearHermesWebBackends } from './hermes-plugin.js';
|
|
9
9
|
import { HARNESS_DISPLAY_NAMES as DISPLAY_NAMES } from './harness-detect.js';
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as readline from 'node:readline/promises';
|
|
2
2
|
import { harnessSpec } from './harness-spec.js';
|
|
3
3
|
import spawn from 'cross-spawn';
|
|
4
|
-
import { connectHarness } from '
|
|
4
|
+
import { connectHarness } from './connect-harness.js';
|
|
5
5
|
import { loadConfig, validatedApiKey } from './auth.js';
|
|
6
6
|
import { installTinyFishCli, } from './cli-install.js';
|
|
7
7
|
import { authGate, mapConnectError, OAUTH_SIGN_IN_TIMEOUT_MS } from './connect-all-auth.js';
|
|
@@ -104,7 +104,7 @@ export declare function hermesRegistrationEnabled(home: string, mode?: 'api-key'
|
|
|
104
104
|
};
|
|
105
105
|
export declare const NATIVE_MCP_CLIENTS: readonly NativeMcpClient[];
|
|
106
106
|
/** Native descriptor by harness id; Cursor and OpenClaw have none. */
|
|
107
|
-
export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "command-code" | "opencode" | "pi" | "claude-code", NativeMcpClient>;
|
|
107
|
+
export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "command-code" | "opencode" | "pi" | "cline" | "claude-code", NativeMcpClient>;
|
|
108
108
|
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';
|
|
@@ -7,7 +7,7 @@ import { ConnectInterruptedError, ConnectStepError, spawnStepError, } from './co
|
|
|
7
7
|
import { TINYFISH_ACCESS_MODE_HEADER, TINYFISH_API_KEY_VAR, TINYFISH_KEYLESS_ACCESS_MODE, } from './constants.js';
|
|
8
8
|
import { HARNESS_SPECS, NATIVE_HARNESSES, harnessSpec, } from './harness-spec.js';
|
|
9
9
|
import { HERMES_KEY_VAR, captureHermesKeyRestore, hermesEnvPath, resolveHermesHome, writeHermesKey, } from './hermes-env.js';
|
|
10
|
-
import { hermesConfigPath, readHermesEntry } from './hermes-config.js';
|
|
10
|
+
import { hermesConfigPath, readHermesEntry } from './config/hermes-config.js';
|
|
11
11
|
import { installHermesPlugin, removeHermesMcpEntry, setHermesWebBackends, writeHermesMcpEntry, } from './hermes-plugin.js';
|
|
12
12
|
const HERMES_SEED_TIMEOUT_MS = 120_000;
|
|
13
13
|
const OPENCLAW_SKILL = '@tinyfish/tinyfish';
|
|
@@ -215,6 +215,12 @@ function installHermesWebPlugin({ apiKey, verbose, seededHome, }) {
|
|
|
215
215
|
function launchOpencode(prompt) {
|
|
216
216
|
handOverTerminal('opencode', ['--prompt', prompt], 'OpenCode');
|
|
217
217
|
}
|
|
218
|
+
// A bare `cline "<prompt>"` is a one-shot act run that auto-approves every tool, so it would
|
|
219
|
+
// bill TinyFish calls unattended and cannot answer the walkthrough's questions. `-i` is the
|
|
220
|
+
// only interactive mode.
|
|
221
|
+
function launchCline(prompt) {
|
|
222
|
+
handOverTerminal('cline', ['-i', '--auto-approve', 'false', prompt], 'Cline');
|
|
223
|
+
}
|
|
218
224
|
/** "positional": flags before `tinyfish <url>`. "flag": flags after `--url`. */
|
|
219
225
|
function specAddArgs(spec, mcpUrl) {
|
|
220
226
|
const flags = spec.addFlags ?? [];
|
|
@@ -272,6 +278,7 @@ function specKeylessAddArgs(spec) {
|
|
|
272
278
|
}
|
|
273
279
|
/** Descriptor behaviour the spec cannot hold; every other field is spec data. */
|
|
274
280
|
const HARNESS_OVERRIDES = {
|
|
281
|
+
cline: { launchWalkthrough: launchCline },
|
|
275
282
|
codex: { detachedLaunch: launchCodexWalkthrough },
|
|
276
283
|
hermes: {
|
|
277
284
|
launchWalkthrough: launchHermesWalkthrough,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentClient, ConnectAuthMode } from './connect-runtime.js';
|
|
2
|
+
import { type NativeConnectOptions } from './connect-steps.js';
|
|
3
|
+
export declare function launchAgent(client: AgentClient): Promise<void>;
|
|
4
|
+
export declare function connectHarness(harness: AgentClient, options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
|