@tiny-fish/cli 0.45.2-next.356 → 0.45.2-next.358

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/commands/config-claude.js +1 -1
  2. package/dist/commands/connect.js +29 -4
  3. package/dist/lib/auth.d.ts +4 -0
  4. package/dist/lib/auth.js +26 -4
  5. package/dist/lib/config/cline-config.d.ts +4 -0
  6. package/dist/lib/config/cline-config.js +36 -0
  7. package/dist/lib/{connect-config-file.d.ts → config/connect-config-file.d.ts} +1 -1
  8. package/dist/lib/{connect-config-file.js → config/connect-config-file.js} +11 -11
  9. package/dist/lib/{hermes-config.js → config/hermes-config.js} +2 -2
  10. package/dist/lib/{mcp-json-config.d.ts → config/mcp-json-config.d.ts} +2 -0
  11. package/dist/lib/{mcp-json-config.js → config/mcp-json-config.js} +11 -5
  12. package/dist/lib/{omp-config.js → config/omp-config.js} +2 -2
  13. package/dist/lib/{pi-config.js → config/pi-config.js} +1 -1
  14. package/dist/lib/connect-all-uninstall.js +3 -3
  15. package/dist/lib/connect-clients.d.ts +1 -1
  16. package/dist/lib/connect-clients.js +8 -1
  17. package/dist/lib/connect-harness.js +1 -1
  18. package/dist/lib/doctor-checks.js +1 -1
  19. package/dist/lib/doctor-report.d.ts +6 -0
  20. package/dist/lib/harness-detect.js +7 -3
  21. package/dist/lib/harness-spec.d.ts +23 -2
  22. package/dist/lib/harness-spec.js +23 -0
  23. package/dist/lib/harness.js +7 -0
  24. package/dist/lib/hermes-plugin.js +1 -1
  25. package/dist/lib/registration-detect.js +22 -5
  26. package/dist/lib/setup-telemetry.d.ts +10 -0
  27. package/dist/lib/skill-paths.js +1 -0
  28. package/package.json +1 -1
  29. /package/dist/lib/{claude-config.d.ts → config/claude-config.d.ts} +0 -0
  30. /package/dist/lib/{claude-config.js → config/claude-config.js} +0 -0
  31. /package/dist/lib/{command-code-config.d.ts → config/command-code-config.d.ts} +0 -0
  32. /package/dist/lib/{command-code-config.js → config/command-code-config.js} +0 -0
  33. /package/dist/lib/{cursor-config.d.ts → config/cursor-config.d.ts} +0 -0
  34. /package/dist/lib/{cursor-config.js → config/cursor-config.js} +0 -0
  35. /package/dist/lib/{hermes-config.d.ts → config/hermes-config.d.ts} +0 -0
  36. /package/dist/lib/{omp-config.d.ts → config/omp-config.d.ts} +0 -0
  37. /package/dist/lib/{pi-config.d.ts → config/pi-config.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { err, errLine, out } from '../lib/output.js';
2
2
  import { validatedApiKey } from '../lib/auth.js';
3
- import { readSettingsJson, writeSettingsJson, readClaudeMd, writeClaudeMd, mergeSettings, removeFromSettings, mergeClaudeMd, removeFromClaudeMd, claudeSettingsPath, claudeMdPath, isTinyfishConfiguredInSettings, isTinyfishConfiguredInClaudeMd, } from '../lib/claude-config.js';
3
+ import { readSettingsJson, writeSettingsJson, readClaudeMd, writeClaudeMd, mergeSettings, removeFromSettings, mergeClaudeMd, removeFromClaudeMd, claudeSettingsPath, claudeMdPath, isTinyfishConfiguredInSettings, isTinyfishConfiguredInClaudeMd, } from '../lib/config/claude-config.js';
4
4
  function loadExistingConfig() {
5
5
  let settings;
6
6
  try {
@@ -1,4 +1,6 @@
1
- import { validateKeyFormat, validatedApiKey } from '../lib/auth.js';
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { saveConnectConfigDir, validateKeyFormat, validatedApiKey } from '../lib/auth.js';
2
4
  import { runConnectAll } from '../lib/connect-all.js';
3
5
  import { ALL_HARNESSES } from '../lib/harness-detect.js';
4
6
  import { detectHumanInitiated } from '../lib/harness.js';
@@ -8,6 +10,24 @@ import { z } from 'zod';
8
10
  import { gateApiKey } from '../lib/connect-preflight.js';
9
11
  import { DEFAULT_MCP_URL } from '../lib/mcp-endpoint.js';
10
12
  import { connectHarness } from '../lib/connect-harness.js';
13
+ /** Cline reads CLINE_DIR itself, so setting it steers the add, the remove and our own probe. */
14
+ function applyConfigDir(client, configDir) {
15
+ if (client !== 'cline') {
16
+ throw new Error('--config-dir is supported only by `tinyfish connect cline`.');
17
+ }
18
+ const resolved = path.resolve(configDir);
19
+ let isDir = false;
20
+ try {
21
+ isDir = fs.statSync(resolved).isDirectory();
22
+ }
23
+ catch {
24
+ // Reported below: a typo must fail here, not seed a store nothing reads.
25
+ }
26
+ if (!isDir)
27
+ throw new Error(`--config-dir is not an existing directory: ${resolved}`);
28
+ process.env['CLINE_DIR'] = resolved;
29
+ return resolved;
30
+ }
11
31
  function parseMcpUrlOption(options) {
12
32
  let mcpUrl = options.url ?? DEFAULT_MCP_URL;
13
33
  const parsedUrl = URL.parse(mcpUrl);
@@ -51,10 +71,14 @@ async function connectManyPath(options, mcpUrl, attemptId) {
51
71
  process.exitCode = exitCode;
52
72
  }
53
73
  async function connectSingleClient(client, options, mcpUrl, attemptId) {
74
+ if (!ALL_HARNESSES.includes(client)) {
75
+ throw new Error('Unsupported client: ' + client + '. Supported clients: ' + ALL_HARNESSES.join(', '));
76
+ }
54
77
  const connectOptions = {
55
78
  apiKey: options.apiKey,
56
79
  mcpUrl,
57
80
  launch: options.launch ?? false,
81
+ configDir: options.configDir ? applyConfigDir(client, options.configDir) : undefined,
58
82
  attemptId,
59
83
  // A key that the install cannot use leaves only a browser hop no agent can finish.
60
84
  keyAuthOnly: !detectHumanInitiated() && !!validatedApiKey(options.apiKey),
@@ -63,9 +87,6 @@ async function connectSingleClient(client, options, mcpUrl, attemptId) {
63
87
  fallbackWhenMissing: true,
64
88
  verbose: options.verbose ?? false,
65
89
  };
66
- if (!ALL_HARNESSES.includes(client)) {
67
- throw new Error('Unsupported client: ' + client + '. Supported clients: ' + ALL_HARNESSES.join(', '));
68
- }
69
90
  const gate = await gateApiKey({
70
91
  apiKey: options.apiKey,
71
92
  mcpUrl,
@@ -78,6 +99,9 @@ async function connectSingleClient(client, options, mcpUrl, attemptId) {
78
99
  return;
79
100
  }
80
101
  await connectHarness(client, connectOptions);
102
+ // After the connect: a run that wrote no entry must not record a store it never used.
103
+ if (connectOptions.configDir)
104
+ saveConnectConfigDir(client, connectOptions.configDir);
81
105
  }
82
106
  export function registerConnect(program) {
83
107
  program
@@ -92,6 +116,7 @@ export function registerConnect(program) {
92
116
  .option('--launch', 'Launch the agent and start the TinyFish walkthrough')
93
117
  .option('--url <mcpUrl>', 'MCP endpoint override')
94
118
  .option('--verbose', 'Print the full npm and skills output instead of only on failure')
119
+ .option('--config-dir <path>', 'Cline only: config dir of a relocated install (~/.cline)')
95
120
  .action(async (client, options) => {
96
121
  if (options.apiKey && !validateKeyFormat(options.apiKey)) {
97
122
  throw new Error('Invalid --api-key value');
@@ -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
- out[client] =
57
- typeof authMode === 'string' && RECORDED_AUTH_MODES.includes(authMode)
58
- ? { attempt_id: attemptId, auth_mode: authMode }
59
- : { attempt_id: attemptId };
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,4 @@
1
+ import { type McpJsonServerEntry } from './mcp-json-config.js';
2
+ export declare function clineConfigDir(): string;
3
+ /** Reports the header's shape, never its value. */
4
+ export declare function readClineTinyfishEntry(): McpJsonServerEntry;
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import { type ConnectAuthMode } from './connect-runtime.js';
1
+ import { type ConnectAuthMode } from '../connect-runtime.js';
2
2
  import { cursorMcpPath, writeCursorMcpConfig } from './cursor-config.js';
3
3
  import { ompMcpPath, writeOmpMcpConfig } from './omp-config.js';
4
4
  import { piMcpPath, writePiMcpConfig } from './pi-config.js';
@@ -1,18 +1,18 @@
1
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';
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
7
  import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from './cursor-config.js';
8
8
  import { ompMcpPath, readOmpTinyfishEntry, writeOmpMcpConfig } from './omp-config.js';
9
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';
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
16
  const PI_RELOAD_COPY = 'TinyFish is configured in pi. Restart pi, then run `/mcp-auth tinyfish` there if it asks you ' +
17
17
  'to sign in.';
18
18
  // The skill is the working path either way, so the note must not read as a failure.
@@ -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 './constants.js';
6
- import { HERMES_KEY_VAR } from './hermes-env.js';
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 './auth.js';
3
- import { TINYFISH_ACCESS_MODE_HEADER, TINYFISH_KEYLESS_ACCESS_MODE } from './constants.js';
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 key = readKeyHeader(entry.headers, target.keyHeader);
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(entry.headers, { name: TINYFISH_ACCESS_MODE_HEADER })?.value;
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 entry.url === 'string' ? { url: entry.url } : {}),
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 './connect-runtime.js';
4
- import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
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 './constants.js';
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';
@@ -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,
@@ -3,7 +3,7 @@ import { AuthMode } from './harness-detect.js';
3
3
  import { errLine } from './output.js';
4
4
  import { connectNativeMcpClient } from './connect-native.js';
5
5
  import { connectOpenClaw } from './connect-openclaw.js';
6
- import { connectConfigFileHarness, connectCursor } from './connect-config-file.js';
6
+ import { connectConfigFileHarness, connectCursor } from './config/connect-config-file.js';
7
7
  import { finishSetupHint } from './connect-steps.js';
8
8
  /** Total over the non-native harnesses: a new one fails to compile until wired here. */
9
9
  const LAUNCH_OVERRIDES = {
@@ -4,7 +4,7 @@ import { AuthMode, Registered } from './harness-detect.js';
4
4
  import { resolveHermesHome } from './hermes-env.js';
5
5
  import { hermesWebBackendsOurs, readHermesPluginStatus, } from './hermes-plugin.js';
6
6
  import { endpointOf, isDefaultEndpoint } from './mcp-endpoint.js';
7
- import { PI_ADAPTER_INSTALL_COMMAND, piMcpAdapterState } from './pi-config.js';
7
+ import { PI_ADAPTER_INSTALL_COMMAND, piMcpAdapterState } from './config/pi-config.js';
8
8
  import { boundedVersion } from './output.js';
9
9
  import { verifyMcpHealth } from './verify.js';
10
10
  // A green auth mode with a red auth call proves nothing, so the call's verdict gates the claim.
@@ -43,6 +43,7 @@ declare const doctorCheckSchema: z.ZodObject<{
43
43
  "command-code": "command-code";
44
44
  opencode: "opencode";
45
45
  pi: "pi";
46
+ cline: "cline";
46
47
  "claude-code": "claude-code";
47
48
  }>>;
48
49
  scope: z.ZodEnum<{
@@ -62,6 +63,7 @@ declare const doctorHarnessSchema: z.ZodObject<{
62
63
  "command-code": "command-code";
63
64
  opencode: "opencode";
64
65
  pi: "pi";
66
+ cline: "cline";
65
67
  "claude-code": "claude-code";
66
68
  }>;
67
69
  detected: z.ZodBoolean;
@@ -91,6 +93,7 @@ declare const doctorRepairSchema: z.ZodObject<{
91
93
  "command-code": "command-code";
92
94
  opencode: "opencode";
93
95
  pi: "pi";
96
+ cline: "cline";
94
97
  "claude-code": "claude-code";
95
98
  }>>;
96
99
  command: z.ZodString;
@@ -129,6 +132,7 @@ export declare const doctorReportSchema: z.ZodObject<{
129
132
  "command-code": "command-code";
130
133
  opencode: "opencode";
131
134
  pi: "pi";
135
+ cline: "cline";
132
136
  "claude-code": "claude-code";
133
137
  }>>;
134
138
  scope: z.ZodEnum<{
@@ -148,6 +152,7 @@ export declare const doctorReportSchema: z.ZodObject<{
148
152
  "command-code": "command-code";
149
153
  opencode: "opencode";
150
154
  pi: "pi";
155
+ cline: "cline";
151
156
  "claude-code": "claude-code";
152
157
  }>;
153
158
  detected: z.ZodBoolean;
@@ -177,6 +182,7 @@ export declare const doctorReportSchema: z.ZodObject<{
177
182
  "command-code": "command-code";
178
183
  opencode: "opencode";
179
184
  pi: "pi";
185
+ cline: "cline";
180
186
  "claude-code": "claude-code";
181
187
  }>>;
182
188
  command: z.ZodString;
@@ -3,7 +3,8 @@ import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import which from 'which';
5
5
  import { ALL_HARNESSES, harnessSpec } from './harness-spec.js';
6
- import { piAgentDir, piBinaryIsPi } from './pi-config.js';
6
+ import { clineConfigDir } from './config/cline-config.js';
7
+ import { piAgentDir, piBinaryIsPi } from './config/pi-config.js';
7
8
  export { ALL_HARNESSES };
8
9
  export const HARNESS_DISPLAY_NAMES = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).displayName]));
9
10
  export const RELOAD_ACTION = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).reloadAction]));
@@ -33,8 +34,11 @@ export function commandOnPath(command) {
33
34
  return false;
34
35
  }
35
36
  }
36
- // PI_CODING_AGENT_DIR moves pi's dir; a guessed path would be written unread.
37
- const CONFIG_PATH_RESOLVERS = { pi: piAgentDir };
37
+ // PI_CODING_AGENT_DIR and CLINE_DIR move these dirs; a guessed path would be written unread.
38
+ const CONFIG_PATH_RESOLVERS = {
39
+ cline: clineConfigDir,
40
+ pi: piAgentDir,
41
+ };
38
42
  export function harnessConfigPath(harness) {
39
43
  const resolved = CONFIG_PATH_RESOLVERS[harness]?.();
40
44
  return resolved ?? path.join(os.homedir(), harnessSpec(harness).configDir); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
@@ -1,5 +1,5 @@
1
1
  /** Our skill-path slugs, inherited from the `skills` CLI era. */
2
- export type SkillAgent = 'claude-code' | 'codex' | 'command-code' | 'cursor' | 'hermes-agent' | 'opencode' | 'pi';
2
+ export type SkillAgent = 'claude-code' | 'cline' | 'codex' | 'command-code' | 'cursor' | 'hermes-agent' | 'opencode' | 'pi';
3
3
  export interface HarnessSupportCheck {
4
4
  args: string[];
5
5
  /** Missing → nothing can work; setup fails. */
@@ -114,6 +114,27 @@ export declare const HARNESS_SPECS: {
114
114
  label: string;
115
115
  }[];
116
116
  };
117
+ cline: {
118
+ command: string;
119
+ displayName: string;
120
+ configDir: string;
121
+ reloadAction: string;
122
+ skillAgent: "cline";
123
+ nonInteractiveAdd: true;
124
+ keyRequired: true;
125
+ supportCheck: {
126
+ args: string[];
127
+ patterns: RegExp[];
128
+ keyAuthPattern: RegExp;
129
+ unavailableMessage: string;
130
+ };
131
+ urlStyle: "positional";
132
+ addFlags: string[];
133
+ header: {
134
+ name: string;
135
+ sep: ": ";
136
+ };
137
+ };
117
138
  codex: {
118
139
  command: string;
119
140
  displayName: string;
@@ -286,4 +307,4 @@ export type NonNativeHarness = {
286
307
  [K in Harness]: 'urlStyle' extends keyof (typeof HARNESS_SPECS)[K] ? never : 'keyRequired' extends keyof (typeof HARNESS_SPECS)[K] ? never : K;
287
308
  }[Harness];
288
309
  /** Native MCP harnesses generate an add; key-only ones generate only the keyed form. */
289
- export declare const NATIVE_HARNESSES: ("openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "command-code" | "opencode" | "pi" | "claude-code")[];
310
+ export declare const NATIVE_HARNESSES: ("openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "command-code" | "opencode" | "pi" | "cline" | "claude-code")[];
@@ -46,6 +46,9 @@ const OPENCODE_MODEL_NOTE = 'Note: TinyFish runs on tool calls, so OpenCode need
46
46
  'models (e.g. Nano Banana Pro) will show "No endpoints found that support tool use" — switch ' +
47
47
  "OpenCode's model if the walkthrough can't start.";
48
48
  const HERMES_RESTART_NOTE = 'Restart your Hermes session to pick up TinyFish — Hermes discovers MCP servers at startup.';
49
+ const CLINE_MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this Cline installation supports non-interactive MCP setup: `cline mcp add ' +
50
+ '--help` did not list `--transport` and `--yes`. Update Cline with `cline --update` and retry.' +
51
+ SUPPORT_CHECK_DEBUG_HINT;
49
52
  const COMMAND_CODE_ADD_JSON_UNAVAILABLE_MESSAGE = 'Could not confirm this Command Code installation supports keyed MCP setup: `commandcode mcp ' +
50
53
  '--help` did not list `add-json`. Run `commandcode update` and retry.' +
51
54
  SUPPORT_CHECK_DEBUG_HINT;
@@ -87,6 +90,26 @@ export const HARNESS_SPECS = {
87
90
  },
88
91
  ],
89
92
  },
93
+ cline: {
94
+ command: 'cline',
95
+ displayName: 'Cline',
96
+ configDir: '.cline',
97
+ reloadAction: 'restart it',
98
+ skillAgent: 'cline',
99
+ nonInteractiveAdd: true,
100
+ // Key-only by decision: TinyFish never sends CLI-harness users through OAuth.
101
+ keyRequired: true,
102
+ supportCheck: {
103
+ args: ['mcp', 'add', '--help'],
104
+ // Without `--yes` the add opens a wizard and exits non-zero off a TTY.
105
+ patterns: [/--transport(?:[\s<=]|$)/m, /--yes(?:[\s<=]|$)/m],
106
+ keyAuthPattern: HEADER_FLAG,
107
+ unavailableMessage: CLINE_MCP_ADD_UNAVAILABLE_MESSAGE,
108
+ },
109
+ urlStyle: 'positional',
110
+ addFlags: ['--transport', 'http', '--yes'],
111
+ header: { name: 'X-API-Key', sep: ': ' },
112
+ },
90
113
  codex: {
91
114
  command: 'codex',
92
115
  displayName: 'Codex',
@@ -28,10 +28,17 @@ const HARNESS_FINGERPRINTS = [
28
28
  { name: 'hermes', matches: (env) => hasVarWithPrefix(env, 'HERMES_') },
29
29
  // COMMANDCODE_SCRATCHPAD lands on every child; a disabled scratchpad leaves no marker.
30
30
  { name: 'command-code', matches: (env) => hasVarWithPrefix(env, 'COMMANDCODE_') },
31
+ // Kilo forks opencode and sets OPENCODE=1 too, so it must precede opencode.
32
+ { name: 'kilo', matches: (env) => env['KILO'] === '1' },
31
33
  // Exact key, not a prefix: opencode writes OPENCODE=1 at startup, reads every OPENCODE_* as config.
32
34
  { name: 'opencode', matches: (env) => Boolean(env['OPENCODE']) },
33
35
  // Observed: pi launched from Claude Code inherits CLAUDECODE=1.
34
36
  { name: 'pi', matches: (env) => env['PI_CODING_AGENT'] === 'true' },
37
+ // Runtime-only markers; users export CLINE_API_KEY, so no prefix match.
38
+ {
39
+ name: 'cline',
40
+ matches: (env) => Boolean(env['CLINE_CONNECTOR_CLI_LAUNCH'] || env['CLINE_WRAPPER_PATH']),
41
+ },
35
42
  {
36
43
  name: 'claude-code',
37
44
  matches: (env) => env['CLAUDECODE'] === '1' || Boolean(env['CLAUDE_CODE_ENTRYPOINT']),
@@ -7,7 +7,7 @@ import { z } from 'zod';
7
7
  import { capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
8
8
  import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
9
9
  import { HARNESS_PROBE_TIMEOUT_MS, TINYFISH_ACCESS_MODE_HEADER, TINYFISH_KEYLESS_ACCESS_MODE, } from './constants.js';
10
- import { HERMES_HEADER_TEMPLATE, HERMES_MCP_SERVER_KEY } from './hermes-config.js';
10
+ import { HERMES_HEADER_TEMPLATE, HERMES_MCP_SERVER_KEY } from './config/hermes-config.js';
11
11
  import { errLine, parseJson } from './output.js';
12
12
  // Resolved at connect time, so plugin releases need no CLI release.
13
13
  export const HERMES_PLUGIN_PACKAGE = '@tiny-fish/hermes';
@@ -6,12 +6,13 @@ import { loadConfig, matchesCliKey } from './auth.js';
6
6
  import { NATIVE_BY_HARNESS } from './connect-clients.js';
7
7
  import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
8
8
  import { errLine } from './output.js';
9
- import { readCommandCodeTinyfishEntry } from './command-code-config.js';
10
- import { readCursorTinyfishEntry } from './cursor-config.js';
11
- import { readOmpTinyfishEntry } from './omp-config.js';
12
- import { readPiTinyfishEntry } from './pi-config.js';
9
+ import { readClineTinyfishEntry } from './config/cline-config.js';
10
+ import { readCommandCodeTinyfishEntry } from './config/command-code-config.js';
11
+ import { readCursorTinyfishEntry } from './config/cursor-config.js';
12
+ import { readOmpTinyfishEntry } from './config/omp-config.js';
13
+ import { readPiTinyfishEntry } from './config/pi-config.js';
13
14
  import { readHermesKey, resolveHermesHome } from './hermes-env.js';
14
- import { readHermesEntry } from './hermes-config.js';
15
+ import { readHermesEntry } from './config/hermes-config.js';
15
16
  import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMode, Registered, } from './harness-detect.js';
16
17
  const NOT_REGISTERED = Object.freeze({
17
18
  registered: Registered.No,
@@ -279,6 +280,21 @@ function probeCodex() {
279
280
  ...(envVar ? envKeyVerdict(envVar) : {}),
280
281
  };
281
282
  }
283
+ // No `cline mcp list`, so the settings file is the only registration evidence.
284
+ function probeCline() {
285
+ const entry = readClineTinyfishEntry();
286
+ if (entry.error) {
287
+ return unverified('cline_mcp_settings.json exists but could not be read or parsed');
288
+ }
289
+ if (!entry.present)
290
+ return NOT_REGISTERED;
291
+ return {
292
+ registered: Registered.Yes,
293
+ authMode: entry.hasApiKeyHeader ? AuthMode.ApiKey : AuthMode.Unknown,
294
+ ...(entry.url ? { registeredUrl: entry.url } : {}),
295
+ ...(entry.keyMatchesCliKey ? { keyMatchesCliKey: true } : {}),
296
+ };
297
+ }
282
298
  function probeCommandCode() {
283
299
  const entry = readCommandCodeTinyfishEntry();
284
300
  if (entry.error)
@@ -523,6 +539,7 @@ function probeGrok() {
523
539
  }
524
540
  const PROBES = {
525
541
  'claude-code': () => fromMcpGet('claude'),
542
+ cline: probeCline,
526
543
  codex: probeCodex,
527
544
  'command-code': probeCommandCode,
528
545
  cursor: probeCursor,
@@ -31,6 +31,7 @@ declare const harnessResultSchema: z.ZodObject<{
31
31
  "command-code": "command-code";
32
32
  opencode: "opencode";
33
33
  pi: "pi";
34
+ cline: "cline";
34
35
  "claude-code": "claude-code";
35
36
  }>;
36
37
  detected: z.ZodBoolean;
@@ -67,6 +68,7 @@ export declare const setupCompletedPayloadSchema: z.ZodObject<{
67
68
  "command-code": "command-code";
68
69
  opencode: "opencode";
69
70
  pi: "pi";
71
+ cline: "cline";
70
72
  "claude-code": "claude-code";
71
73
  }>;
72
74
  detected: z.ZodBoolean;
@@ -130,6 +132,7 @@ declare const doctorCompletedPayloadSchema: z.ZodObject<{
130
132
  "command-code": "command-code";
131
133
  opencode: "opencode";
132
134
  pi: "pi";
135
+ cline: "cline";
133
136
  "claude-code": "claude-code";
134
137
  all: "all";
135
138
  }>;
@@ -145,6 +148,7 @@ declare const doctorCompletedPayloadSchema: z.ZodObject<{
145
148
  "command-code": "command-code";
146
149
  opencode: "opencode";
147
150
  pi: "pi";
151
+ cline: "cline";
148
152
  "claude-code": "claude-code";
149
153
  }>>;
150
154
  harnesses: z.ZodArray<z.ZodObject<{
@@ -158,6 +162,7 @@ declare const doctorCompletedPayloadSchema: z.ZodObject<{
158
162
  "command-code": "command-code";
159
163
  opencode: "opencode";
160
164
  pi: "pi";
165
+ cline: "cline";
161
166
  "claude-code": "claude-code";
162
167
  }>;
163
168
  detected: z.ZodBoolean;
@@ -200,6 +205,7 @@ declare const doctorCouldNotRunPayloadSchema: z.ZodObject<{
200
205
  "command-code": "command-code";
201
206
  opencode: "opencode";
202
207
  pi: "pi";
208
+ cline: "cline";
203
209
  "claude-code": "claude-code";
204
210
  all: "all";
205
211
  }>;
@@ -238,6 +244,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
238
244
  "command-code": "command-code";
239
245
  opencode: "opencode";
240
246
  pi: "pi";
247
+ cline: "cline";
241
248
  "claude-code": "claude-code";
242
249
  all: "all";
243
250
  }>;
@@ -253,6 +260,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
253
260
  "command-code": "command-code";
254
261
  opencode: "opencode";
255
262
  pi: "pi";
263
+ cline: "cline";
256
264
  "claude-code": "claude-code";
257
265
  }>>;
258
266
  harnesses: z.ZodArray<z.ZodObject<{
@@ -266,6 +274,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
266
274
  "command-code": "command-code";
267
275
  opencode: "opencode";
268
276
  pi: "pi";
277
+ cline: "cline";
269
278
  "claude-code": "claude-code";
270
279
  }>;
271
280
  detected: z.ZodBoolean;
@@ -307,6 +316,7 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
307
316
  "command-code": "command-code";
308
317
  opencode: "opencode";
309
318
  pi: "pi";
319
+ cline: "cline";
310
320
  "claude-code": "claude-code";
311
321
  all: "all";
312
322
  }>;
@@ -14,6 +14,7 @@ const SKILL_DIR_BY_AGENT = {
14
14
  'claude-code': () => path.join(agentHome('CLAUDE_CONFIG_DIR', '.claude'), 'skills'),
15
15
  // The env value, not resolveHermesHome(): skills@1.5.15 reads $HERMES_HOME directly.
16
16
  'hermes-agent': () => path.join(agentHome('HERMES_HOME', '.hermes'), 'skills'),
17
+ cline: canonicalSkillsDir,
17
18
  codex: canonicalSkillsDir,
18
19
  // Not the canonical dir: `skills` writes this one under the harness's own config dir.
19
20
  'command-code': () => path.join(os.homedir(), '.commandcode', 'skills'), // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.45.2-next.356",
3
+ "version": "0.45.2-next.358",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {