@tiny-fish/cli 0.53.1-next.445 → 0.53.1-next.447
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/connect.js +4 -3
- package/dist/commands/doctor.js +5 -2
- package/dist/lib/auth.d.ts +3 -1
- package/dist/lib/auth.js +7 -4
- package/dist/lib/claude-plugin.d.ts +19 -0
- package/dist/lib/claude-plugin.js +101 -0
- package/dist/lib/config/claude-config.d.ts +3 -0
- package/dist/lib/config/claude-config.js +30 -3
- package/dist/lib/connect-all-uninstall.js +23 -2
- package/dist/lib/connect-all.d.ts +1 -0
- package/dist/lib/connect-all.js +12 -6
- package/dist/lib/connect-clients.d.ts +10 -0
- package/dist/lib/connect-clients.js +13 -2
- package/dist/lib/connect-native.js +42 -10
- package/dist/lib/connect-runtime.d.ts +4 -0
- package/dist/lib/connect-runtime.js +16 -14
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +1 -0
- package/dist/lib/harness/harness-spec.d.ts +6 -3
- package/dist/lib/harness/harness-spec.js +17 -2
- package/dist/lib/skill-install.js +5 -2
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Option } from 'commander';
|
|
|
4
4
|
import { saveConnectConfigDir, validateKeyFormat, validatedApiKey } from '../lib/auth.js';
|
|
5
5
|
import { runConnectAll } from '../lib/connect-all.js';
|
|
6
6
|
import { ALL_HARNESSES } from '../lib/harness/harness-detect.js';
|
|
7
|
+
import { HARNESS_CHOICES, resolveHarnessName } from '../lib/harness/harness-spec.js';
|
|
7
8
|
import { detectHumanInitiated } from '../lib/harness.js';
|
|
8
9
|
import { emitNotice } from '../lib/notice.js';
|
|
9
10
|
import { errLine } from '../lib/output.js';
|
|
@@ -76,7 +77,7 @@ async function connectManyPath(options, mcpUrl, attemptId) {
|
|
|
76
77
|
}
|
|
77
78
|
async function connectSingleClient(client, options, mcpUrl, attemptId) {
|
|
78
79
|
if (!ALL_HARNESSES.includes(client)) {
|
|
79
|
-
throw new Error('Unsupported client: ' + client + '. Supported clients: ' +
|
|
80
|
+
throw new Error('Unsupported client: ' + client + '. Supported clients: ' + HARNESS_CHOICES.join(', '));
|
|
80
81
|
}
|
|
81
82
|
const connectOptions = {
|
|
82
83
|
apiKey: options.apiKey,
|
|
@@ -112,7 +113,7 @@ export function registerConnect(program) {
|
|
|
112
113
|
program
|
|
113
114
|
.command('connect')
|
|
114
115
|
.description('Connect TinyFish to an AI agent')
|
|
115
|
-
.argument('[client]', `Agent client to connect (${
|
|
116
|
+
.argument('[client]', `Agent client to connect (${HARNESS_CHOICES.join(', ')}); omit to pick from the agents detected on this machine`)
|
|
116
117
|
.option('--all', 'Detect and connect every supported harness found on this machine')
|
|
117
118
|
.option('--dry-run', 'Print planned writes per harness without touching anything (--all only)')
|
|
118
119
|
.option('--uninstall', 'Remove TinyFish entries written by connect --all')
|
|
@@ -144,7 +145,7 @@ export function registerConnect(program) {
|
|
|
144
145
|
await connectManyPath(options, mcpUrl, attemptId);
|
|
145
146
|
return;
|
|
146
147
|
}
|
|
147
|
-
await connectSingleClient(client, options, mcpUrl, attemptId);
|
|
148
|
+
await connectSingleClient(resolveHarnessName(client), options, mcpUrl, attemptId);
|
|
148
149
|
// Deferred to here so the notice lands after install output, not interleaved with it.
|
|
149
150
|
emitNotice();
|
|
150
151
|
});
|
package/dist/commands/doctor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DOCTOR_COULD_NOT_RUN, DOCTOR_INVALID_INPUT, DOCTOR_SCHEMA_VERSION, doct
|
|
|
4
4
|
import { applyRepairs, repairsFor } from '../lib/doctor-repairs.js';
|
|
5
5
|
import { couldNotRunPayload, telemetryKey, telemetryPayload } from '../lib/doctor-telemetry.js';
|
|
6
6
|
import { ALL_HARNESSES } from '../lib/harness/harness-detect.js';
|
|
7
|
+
import { HARNESS_CHOICES, resolveHarnessName } from '../lib/harness/harness-spec.js';
|
|
7
8
|
import { err, errLine, out, outLine } from '../lib/output.js';
|
|
8
9
|
import { detectRegistrations } from '../lib/registration-detect.js';
|
|
9
10
|
import { sendDoctorCompleted } from '../lib/setup-telemetry.js';
|
|
@@ -63,13 +64,15 @@ export function registerDoctor(program) {
|
|
|
63
64
|
.command('doctor')
|
|
64
65
|
.description('Diagnose your TinyFish setup')
|
|
65
66
|
.option('--pretty', 'Human-readable check list instead of JSON')
|
|
66
|
-
.option('--harness <name>', `Restrict to one harness (${
|
|
67
|
+
.option('--harness <name>', `Restrict to one harness (${HARNESS_CHOICES.join(', ')})`)
|
|
67
68
|
.option('--url <mcpUrl>', 'MCP endpoint override')
|
|
68
69
|
.option('--fix', 'Repair what can be repaired by re-running tinyfish connect')
|
|
69
70
|
.option('--yes', 'Allow --fix to run without a terminal (unattended-safe repairs only)')
|
|
70
71
|
.action(async (options) => {
|
|
72
|
+
if (options.harness)
|
|
73
|
+
options.harness = resolveHarnessName(options.harness);
|
|
71
74
|
if (options.harness && !ALL_HARNESSES.includes(options.harness)) {
|
|
72
|
-
err({ error: `Unknown --harness value: ${options.harness}`, valid:
|
|
75
|
+
err({ error: `Unknown --harness value: ${options.harness}`, valid: HARNESS_CHOICES });
|
|
73
76
|
process.exitCode = DOCTOR_INVALID_INPUT;
|
|
74
77
|
return;
|
|
75
78
|
}
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ interface ConnectEntry {
|
|
|
12
12
|
auth_mode?: RecordedAuthMode;
|
|
13
13
|
/** `connect cline --config-dir`: uninstall and doctor must find the same store. */
|
|
14
14
|
config_dir?: string;
|
|
15
|
+
/** Claude Code Directory plugin, not `mcp add` + vendored skills. */
|
|
16
|
+
plugin?: true;
|
|
15
17
|
}
|
|
16
18
|
interface PendingConnectAttempt {
|
|
17
19
|
id: string;
|
|
@@ -35,7 +37,7 @@ export declare function takePendingConnectAttempt(): string | undefined;
|
|
|
35
37
|
export declare function writeConfig(apiKey: string): void;
|
|
36
38
|
export declare function saveConfig(apiKey: string): void;
|
|
37
39
|
/** `authMode` omitted rewrites the entry without one: a reconnect that degraded must not keep a stale key claim. */
|
|
38
|
-
export declare function saveConnectContext(client: string, attemptId: string, authMode?: RecordedAuthMode): void;
|
|
40
|
+
export declare function saveConnectContext(client: string, attemptId: string, authMode?: RecordedAuthMode, plugin?: boolean): void;
|
|
39
41
|
/** Merged after connect wrote the entry; a missing entry means the install never landed. */
|
|
40
42
|
export declare function saveConnectConfigDir(client: string, configDir: string): void;
|
|
41
43
|
/** Distinguishes a missing entry from an unreadable config file. */
|
package/dist/lib/auth.js
CHANGED
|
@@ -60,6 +60,7 @@ function parseConnectMap(value) {
|
|
|
60
60
|
? { auth_mode: authMode }
|
|
61
61
|
: {}),
|
|
62
62
|
...(typeof configDir === 'string' ? { config_dir: configDir } : {}),
|
|
63
|
+
...(record?.plugin === true ? { plugin: true } : {}),
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
@@ -132,10 +133,12 @@ export function saveConfig(apiKey) {
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
/** `authMode` omitted rewrites the entry without one: a reconnect that degraded must not keep a stale key claim. */
|
|
135
|
-
export function saveConnectContext(client, attemptId, authMode) {
|
|
136
|
-
const entry =
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
export function saveConnectContext(client, attemptId, authMode, plugin) {
|
|
137
|
+
const entry = {
|
|
138
|
+
attempt_id: attemptId,
|
|
139
|
+
...(authMode ? { auth_mode: authMode } : {}),
|
|
140
|
+
...(plugin ? { plugin: true } : {}),
|
|
141
|
+
};
|
|
139
142
|
try {
|
|
140
143
|
updateConfig((config) => ({
|
|
141
144
|
...config,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const CLAUDE_PLUGIN_SERVER = "plugin:tinyfish:tinyfish";
|
|
2
|
+
export type ClaudePluginInstall = {
|
|
3
|
+
installed: true;
|
|
4
|
+
note?: string;
|
|
5
|
+
} | {
|
|
6
|
+
installed: false;
|
|
7
|
+
fallbackReason: string;
|
|
8
|
+
};
|
|
9
|
+
/** Seeds the key first; a fallback restores the prior value. */
|
|
10
|
+
export declare function installClaudeDirectoryPlugin(options: {
|
|
11
|
+
command: string;
|
|
12
|
+
attemptId: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
verbose: boolean;
|
|
15
|
+
}): ClaudePluginInstall;
|
|
16
|
+
/** Compare-only: a key the user set themselves stays. */
|
|
17
|
+
export declare function removeClaudeSettingsKey(): void;
|
|
18
|
+
/** Recorded by connect; upgrade, uninstall and the walkthrough branch on it. */
|
|
19
|
+
export declare function connectedViaClaudePlugin(): boolean;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { CONNECT_SOURCE, loadConfig, matchesCliKey } from './auth.js';
|
|
4
|
+
import { captureStdio, capturedOutput, replay } from './cli-install.js';
|
|
5
|
+
import { throwIfInterrupted } from './connect-runtime.js';
|
|
6
|
+
import { CLAUDE_PLUGIN_ID, HARNESS_PROBE_TIMEOUT_MS, TINYFISH_API_KEY_VAR } from './constants.js';
|
|
7
|
+
import { claudeSettingsPath, readSettingsEnv, writeSettingsEnv } from './config/claude-config.js';
|
|
8
|
+
import { noColourEnv, parseJson } from './output.js';
|
|
9
|
+
export const CLAUDE_PLUGIN_SERVER = 'plugin:tinyfish:tinyfish';
|
|
10
|
+
const installResultSchema = z.looseObject({
|
|
11
|
+
outcome: z.string(),
|
|
12
|
+
failureCode: z.string().optional(),
|
|
13
|
+
});
|
|
14
|
+
const pluginListSchema = z.array(z.looseObject({ id: z.string(), enabled: z.boolean() }));
|
|
15
|
+
function claudeJson(command, args, timeout) {
|
|
16
|
+
const result = spawn.sync(command, args, {
|
|
17
|
+
...captureStdio(false),
|
|
18
|
+
...(timeout ? { timeout } : {}),
|
|
19
|
+
env: noColourEnv(),
|
|
20
|
+
});
|
|
21
|
+
throwIfInterrupted(result);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
// Directory off fails the install, yet installed plugins keep updating.
|
|
25
|
+
function pluginEnabled(command) {
|
|
26
|
+
const result = claudeJson(command, ['plugin', 'list', '--json'], HARNESS_PROBE_TIMEOUT_MS);
|
|
27
|
+
const parsed = pluginListSchema.safeParse(parseJson(String(result.stdout ?? '')));
|
|
28
|
+
return parsed.success && parsed.data.some((p) => p.id === CLAUDE_PLUGIN_ID && p.enabled);
|
|
29
|
+
}
|
|
30
|
+
function runInstall(command, attemptId, verbose) {
|
|
31
|
+
const config = {
|
|
32
|
+
connect_source: CONNECT_SOURCE,
|
|
33
|
+
connect_client: 'claude-code',
|
|
34
|
+
connect_attempt_id: attemptId,
|
|
35
|
+
};
|
|
36
|
+
const configArgs = Object.entries(config).flatMap(([k, v]) => ['--config', `${k}=${v}`]);
|
|
37
|
+
const result = claudeJson(command, [
|
|
38
|
+
'plugin',
|
|
39
|
+
'install',
|
|
40
|
+
CLAUDE_PLUGIN_ID,
|
|
41
|
+
'--json',
|
|
42
|
+
'-y',
|
|
43
|
+
...configArgs,
|
|
44
|
+
]);
|
|
45
|
+
if (verbose)
|
|
46
|
+
replay(capturedOutput(result));
|
|
47
|
+
const parsed = installResultSchema.safeParse(parseJson(String(result.stdout ?? '').trim()));
|
|
48
|
+
if (parsed.success && parsed.data.outcome === 'ok')
|
|
49
|
+
return { installed: true };
|
|
50
|
+
if (pluginEnabled(command))
|
|
51
|
+
return { installed: true };
|
|
52
|
+
// Older Claude Code prints usage here, not a result line.
|
|
53
|
+
if (!parsed.success)
|
|
54
|
+
return { installed: false, fallbackReason: 'unsupported' };
|
|
55
|
+
// The connect-event route folds codes it cannot hold to `other`.
|
|
56
|
+
return { installed: false, fallbackReason: parsed.data.failureCode ?? 'unknown' };
|
|
57
|
+
}
|
|
58
|
+
/** Seeds the key first; a fallback restores the prior value. */
|
|
59
|
+
export function installClaudeDirectoryPlugin(options) {
|
|
60
|
+
const { command, attemptId, apiKey, verbose } = options;
|
|
61
|
+
let restore;
|
|
62
|
+
if (apiKey) {
|
|
63
|
+
try {
|
|
64
|
+
const previous = readSettingsEnv(TINYFISH_API_KEY_VAR);
|
|
65
|
+
writeSettingsEnv(TINYFISH_API_KEY_VAR, apiKey);
|
|
66
|
+
restore = () => writeSettingsEnv(TINYFISH_API_KEY_VAR, previous);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Malformed settings.json: the header-keyed `mcp add` still works.
|
|
70
|
+
return { installed: false, fallbackReason: 'settings-unwritable' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let outcome;
|
|
74
|
+
try {
|
|
75
|
+
outcome = runInstall(command, attemptId, verbose);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
restore?.();
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
if (!outcome.installed) {
|
|
82
|
+
restore?.();
|
|
83
|
+
return outcome;
|
|
84
|
+
}
|
|
85
|
+
return apiKey
|
|
86
|
+
? {
|
|
87
|
+
installed: true,
|
|
88
|
+
note: `Wrote ${TINYFISH_API_KEY_VAR} to ${claudeSettingsPath()} — Claude Code reads the key from there.`,
|
|
89
|
+
}
|
|
90
|
+
: outcome;
|
|
91
|
+
}
|
|
92
|
+
/** Compare-only: a key the user set themselves stays. */
|
|
93
|
+
export function removeClaudeSettingsKey() {
|
|
94
|
+
if (matchesCliKey(readSettingsEnv(TINYFISH_API_KEY_VAR))) {
|
|
95
|
+
writeSettingsEnv(TINYFISH_API_KEY_VAR, undefined);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Recorded by connect; upgrade, uninstall and the walkthrough branch on it. */
|
|
99
|
+
export function connectedViaClaudePlugin() {
|
|
100
|
+
return loadConfig().connect?.['claude-code']?.plugin === true;
|
|
101
|
+
}
|
|
@@ -4,6 +4,9 @@ export declare const TINYFISH_PERMISSION = "Bash(tinyfish:*)";
|
|
|
4
4
|
export declare const CLAUDE_MD_MARKER = "<!-- tinyfish-configure-claude -->";
|
|
5
5
|
export declare function readSettingsJson(): Record<string, unknown>;
|
|
6
6
|
export declare function writeSettingsJson(settings: Record<string, unknown>): void;
|
|
7
|
+
export declare function readSettingsEnv(name: string): string | undefined;
|
|
8
|
+
/** `undefined` deletes the variable; every other setting survives. */
|
|
9
|
+
export declare function writeSettingsEnv(name: string, value: string | undefined): void;
|
|
7
10
|
export declare function mergeSettings(existing: Record<string, unknown>): Record<string, unknown>;
|
|
8
11
|
export declare function removeFromSettings(existing: Record<string, unknown>): Record<string, unknown>;
|
|
9
12
|
export declare function readClaudeMd(): string;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
-
import * as os from 'os';
|
|
3
2
|
import * as path from 'path';
|
|
3
|
+
import { agentHome } from '../skill-paths.js';
|
|
4
4
|
// ─── Path helpers ─────────────────────────────────────────────────────────────
|
|
5
5
|
function claudeDir() {
|
|
6
|
-
return
|
|
6
|
+
return agentHome('CLAUDE_CONFIG_DIR', '.claude');
|
|
7
7
|
}
|
|
8
8
|
export function claudeSettingsPath() {
|
|
9
9
|
return path.join(claudeDir(), 'settings.json');
|
|
@@ -89,7 +89,34 @@ export function readSettingsJson() {
|
|
|
89
89
|
}
|
|
90
90
|
export function writeSettingsJson(settings) {
|
|
91
91
|
fs.mkdirSync(claudeDir(), { recursive: true });
|
|
92
|
-
|
|
92
|
+
const file = claudeSettingsPath();
|
|
93
|
+
// Settings `env` can hold the key; `mode` only applies on create.
|
|
94
|
+
if (fs.existsSync(file))
|
|
95
|
+
fs.chmodSync(file, 0o600);
|
|
96
|
+
fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n', { mode: 0o600 });
|
|
97
|
+
}
|
|
98
|
+
function settingsEnv(settings) {
|
|
99
|
+
const env = settings.env;
|
|
100
|
+
return env !== null && typeof env === 'object' && !Array.isArray(env)
|
|
101
|
+
? env
|
|
102
|
+
: {};
|
|
103
|
+
}
|
|
104
|
+
export function readSettingsEnv(name) {
|
|
105
|
+
const value = settingsEnv(readSettingsJson())[name];
|
|
106
|
+
return typeof value === 'string' ? value : undefined;
|
|
107
|
+
}
|
|
108
|
+
/** `undefined` deletes the variable; every other setting survives. */
|
|
109
|
+
export function writeSettingsEnv(name, value) {
|
|
110
|
+
const settings = readSettingsJson();
|
|
111
|
+
const env = { ...settingsEnv(settings) };
|
|
112
|
+
if (value === undefined)
|
|
113
|
+
delete env[name];
|
|
114
|
+
else
|
|
115
|
+
env[name] = value;
|
|
116
|
+
const next = { ...settings, env };
|
|
117
|
+
if (Object.keys(env).length === 0)
|
|
118
|
+
delete next.env;
|
|
119
|
+
writeSettingsJson(next);
|
|
93
120
|
}
|
|
94
121
|
// ─── settings.json merge / remove ─────────────────────────────────────────────
|
|
95
122
|
export function mergeSettings(existing) {
|
|
@@ -8,6 +8,8 @@ import { planOmpWrite, removeOmpMcpServer } from './config/omp-config.js';
|
|
|
8
8
|
import { piMcpPath, planPiWrite, removePiMcpServer } from './config/pi-config.js';
|
|
9
9
|
import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
|
|
10
10
|
import { clearHermesWebBackends } from './hermes-plugin.js';
|
|
11
|
+
import { removeClaudeSettingsKey } from './claude-plugin.js';
|
|
12
|
+
import { claudeSettingsPath } from './config/claude-config.js';
|
|
11
13
|
import { HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, harnessDisplayPath, } from './harness/harness-detect.js';
|
|
12
14
|
import { ALL_HARNESSES, harnessSpec } from './harness/harness-spec.js';
|
|
13
15
|
import { skillRootDir } from './skill-paths.js';
|
|
@@ -156,6 +158,23 @@ function hermesKeyKeptAdvisory() {
|
|
|
156
158
|
return (`left ${HERMES_KEY_VAR} in Hermes' .env because the entry was not removed. ` +
|
|
157
159
|
'Run the command above, then re-run `tinyfish connect --all --uninstall` to clear it.');
|
|
158
160
|
}
|
|
161
|
+
/** Every Claude Code subprocess inherits settings `env`. */
|
|
162
|
+
function clearClaudeSettingsKey() {
|
|
163
|
+
try {
|
|
164
|
+
removeClaudeSettingsKey();
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
169
|
+
return `could not clear TINYFISH_API_KEY from ${claudeSettingsPath()} (${detail}); delete it from \`env\` by hand.`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function localStateAdvisory(harness, removed) {
|
|
173
|
+
if (harness === 'hermes')
|
|
174
|
+
return removed ? clearHermesLocalState() : hermesKeyKeptAdvisory();
|
|
175
|
+
// Not removed: the plugin still reads the key.
|
|
176
|
+
return harness === 'claude-code' && removed ? clearClaudeSettingsKey() : undefined;
|
|
177
|
+
}
|
|
159
178
|
function runRemovalCommands(detection, removal, verbose) {
|
|
160
179
|
const { harness } = detection;
|
|
161
180
|
let removed = false;
|
|
@@ -280,8 +299,10 @@ export function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
|
|
|
280
299
|
// Clearing state the entry still references would break a working install.
|
|
281
300
|
const removed = !result.removalFailed &&
|
|
282
301
|
(result.outcome === 'uninstalled' || result.outcome === 'uninstall_noop');
|
|
283
|
-
const
|
|
284
|
-
|
|
302
|
+
const advisory = [
|
|
303
|
+
removed ? removeHarnessSkills(harness) : undefined,
|
|
304
|
+
localStateAdvisory(harness, removed),
|
|
305
|
+
]
|
|
285
306
|
.filter((text) => text !== undefined)
|
|
286
307
|
.join(' ');
|
|
287
308
|
if (!advisory)
|
|
@@ -39,4 +39,5 @@ export interface ConnectAllOptions {
|
|
|
39
39
|
/** The harness has never created its dir, so connecting would build one it does not own. */
|
|
40
40
|
export declare function needsFirstRun(detection: HarnessDetection): boolean;
|
|
41
41
|
export declare function runConnectAll(opts: ConnectAllOptions): Promise<number>;
|
|
42
|
+
export declare const firstTaskPrompt: (server?: string) => string;
|
|
42
43
|
export declare const FIRST_TASK_PROMPT: string;
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLAUDE_PLUGIN_SERVER, connectedViaClaudePlugin } from './claude-plugin.js';
|
|
1
2
|
import * as readline from 'node:readline/promises';
|
|
2
3
|
import { harnessSpec } from './harness/harness-spec.js';
|
|
3
4
|
import spawn from 'cross-spawn';
|
|
@@ -9,7 +10,7 @@ import { authGate, mapConnectError, OAUTH_SIGN_IN_TIMEOUT_MS } from './connect-a
|
|
|
9
10
|
import { actionable, computeExitCode, reloadHint, renderSummary } from './connect-all-summary.js';
|
|
10
11
|
import { planText, uninstallHarness, uninstallPlanText } from './connect-all-uninstall.js';
|
|
11
12
|
import { createStdinPrompt, runCliFallback } from './connect-fallback.js';
|
|
12
|
-
import { KEYLESS_ONBOARDING_PROMPT } from './connect-clients.js';
|
|
13
|
+
import { KEYLESS_ONBOARDING_PROMPT, useServerLine } from './connect-clients.js';
|
|
13
14
|
import { gateApiKey } from './connect-preflight.js';
|
|
14
15
|
import { ConnectInterruptedError } from './connect-runtime.js';
|
|
15
16
|
import { pickHarnesses } from './connect-picker.js';
|
|
@@ -386,13 +387,13 @@ export async function runConnectAll(opts) {
|
|
|
386
387
|
}
|
|
387
388
|
return finishRun(results, opts, isTTY);
|
|
388
389
|
}
|
|
389
|
-
export const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
'top Hacker News story, fetch that page with the TinyFish fetch tool, and give me a two-line ' +
|
|
390
|
+
export const firstTaskPrompt = (server = 'tinyfish') => `My TinyFish setup just finished — verify it with a real run. ${useServerLine(server)} ` +
|
|
391
|
+
"Then use the TinyFish search tool to find today's top Hacker News story," +
|
|
392
|
+
' fetch that page with the TinyFish fetch tool, and give me a two-line ' +
|
|
393
393
|
'summary. Show which tools you call. If a tool call comes back blocked or not approved, tell ' +
|
|
394
394
|
'me which tool needs approval and how to grant it, then wait for me instead of abandoning the ' +
|
|
395
395
|
'task.';
|
|
396
|
+
export const FIRST_TASK_PROMPT = firstTaskPrompt();
|
|
396
397
|
// Launchable seeded agents, best-first — cursor-agent shares ~/.cursor/mcp.json with the IDE.
|
|
397
398
|
const LAUNCHERS = [
|
|
398
399
|
{ harness: 'cursor', args: [FIRST_TASK_PROMPT] },
|
|
@@ -430,7 +431,12 @@ async function launchFirstTask(results) {
|
|
|
430
431
|
outLine(`Starting ${DISPLAY_NAMES[chosen.harness]} with your first TinyFish task...`);
|
|
431
432
|
const keyless = chosen.harness === 'omp' &&
|
|
432
433
|
results.some((result) => result.harness === 'omp' && !result.keyAuthed);
|
|
433
|
-
const
|
|
434
|
+
const pluginServer = chosen.harness === 'claude-code' && connectedViaClaudePlugin();
|
|
435
|
+
const args = keyless
|
|
436
|
+
? [KEYLESS_ONBOARDING_PROMPT]
|
|
437
|
+
: pluginServer
|
|
438
|
+
? [firstTaskPrompt(CLAUDE_PLUGIN_SERVER)]
|
|
439
|
+
: chosen.args;
|
|
434
440
|
const result = spawn.sync(HARNESS_COMMANDS[chosen.harness], args, { stdio: 'inherit' });
|
|
435
441
|
return !result.error;
|
|
436
442
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import spawn from 'cross-spawn';
|
|
2
2
|
import { type AgentClient, type SupportedCommand } from './connect-runtime.js';
|
|
3
3
|
import { type HarnessSpec, type SkillAgent } from './harness/harness-spec.js';
|
|
4
|
+
import { installClaudeDirectoryPlugin } from './claude-plugin.js';
|
|
4
5
|
export declare function openclawSkillInstallArgs(ackFlag: string | undefined): string[];
|
|
5
6
|
/** `openclaw skills` has no uninstall; ClawHub removes from the global dir. */
|
|
6
7
|
export declare function openclawSkillUninstall(): {
|
|
@@ -9,6 +10,8 @@ export declare function openclawSkillUninstall(): {
|
|
|
9
10
|
/** npx fetches the package first; the 10s removal bound starves a cold cache. */
|
|
10
11
|
timeoutMs: number;
|
|
11
12
|
};
|
|
13
|
+
export declare const useServerLine: (server: string) => string;
|
|
14
|
+
export declare const onboardingPrompt: (server?: string) => string;
|
|
12
15
|
export declare const DEFAULT_ONBOARDING_PROMPT: string;
|
|
13
16
|
export declare const OPENCLAW_ONBOARDING_PROMPT: string;
|
|
14
17
|
export declare const KEYLESS_ONBOARDING_PROMPT: string;
|
|
@@ -70,6 +73,13 @@ interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent'
|
|
|
70
73
|
args: string[];
|
|
71
74
|
label: string;
|
|
72
75
|
};
|
|
76
|
+
pluginInstall?: PluginInstall;
|
|
77
|
+
}
|
|
78
|
+
/** Replaces `mcp add`; a fallback reason means the add runs instead. */
|
|
79
|
+
export interface PluginInstall {
|
|
80
|
+
install: typeof installClaudeDirectoryPlugin;
|
|
81
|
+
loginArgs: string[];
|
|
82
|
+
serverName: string;
|
|
73
83
|
}
|
|
74
84
|
/** Keyless installs use this argv; the harness signs itself in. */
|
|
75
85
|
interface OauthCapableMcpClient extends BaseMcpClient {
|
|
@@ -9,6 +9,7 @@ import { HARNESS_SPECS, NATIVE_HARNESSES, harnessSpec, } from './harness/harness
|
|
|
9
9
|
import { HERMES_KEY_VAR, captureHermesKeyRestore, hermesEnvPath, resolveHermesHome, writeHermesKey, } from './hermes-env.js';
|
|
10
10
|
import { hermesConfigPath, readHermesEntry } from './config/hermes-config.js';
|
|
11
11
|
import { installHermesPlugin, removeHermesMcpEntry, setHermesWebBackends, writeHermesMcpEntry, } from './hermes-plugin.js';
|
|
12
|
+
import { CLAUDE_PLUGIN_SERVER, installClaudeDirectoryPlugin } from './claude-plugin.js';
|
|
12
13
|
const HERMES_SEED_TIMEOUT_MS = 120_000;
|
|
13
14
|
const OPENCLAW_SKILL = '@tinyfish/tinyfish';
|
|
14
15
|
// --force makes this an unconditional overwrite, so the same call installs and refreshes.
|
|
@@ -33,13 +34,15 @@ export function openclawSkillUninstall() {
|
|
|
33
34
|
timeoutMs: SKILL_INSTALL_TIMEOUT_MS,
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
|
-
export const
|
|
37
|
-
|
|
37
|
+
export const useServerLine = (server) => `Use the MCP server named \`${server}\` for every TinyFish tool call; if another TinyFish ` +
|
|
38
|
+
`server is also registered, still use \`${server}\` and do not ask me which.`;
|
|
39
|
+
export const onboardingPrompt = (server = 'tinyfish') => `I just connected TinyFish. ${useServerLine(server)} ` +
|
|
38
40
|
'Start by asking me what I want to look up on the web, and wait for my reply. Then call ' +
|
|
39
41
|
'`guide_next_step` and follow its instructions one step at a time, asking for my input and ' +
|
|
40
42
|
'waiting for my reply before each subsequent TinyFish tool call. If a tool call comes back ' +
|
|
41
43
|
'blocked or not approved, tell me which tool needs approval and how to grant it, then wait for ' +
|
|
42
44
|
'me instead of ending the walkthrough.';
|
|
45
|
+
export const DEFAULT_ONBOARDING_PROMPT = onboardingPrompt();
|
|
43
46
|
export const OPENCLAW_ONBOARDING_PROMPT = 'I just installed the TinyFish skill. Guide me through it interactively: first ask what topic I ' +
|
|
44
47
|
'want to search and wait for my reply before using TinyFish Search. Show me the results, ask ' +
|
|
45
48
|
'which result I want to read, and wait before using TinyFish Fetch. Then ask what browser task ' +
|
|
@@ -258,6 +261,13 @@ function specKeyAuth(spec) {
|
|
|
258
261
|
}
|
|
259
262
|
/** Descriptor behaviour the spec cannot hold; every other field is spec data. */
|
|
260
263
|
const HARNESS_OVERRIDES = {
|
|
264
|
+
'claude-code': {
|
|
265
|
+
pluginInstall: {
|
|
266
|
+
install: installClaudeDirectoryPlugin,
|
|
267
|
+
loginArgs: ['mcp', 'login', CLAUDE_PLUGIN_SERVER],
|
|
268
|
+
serverName: CLAUDE_PLUGIN_SERVER,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
261
271
|
cline: { launchWalkthrough: launchCline },
|
|
262
272
|
codex: { detachedLaunch: launchCodexWalkthrough },
|
|
263
273
|
hermes: {
|
|
@@ -301,6 +311,7 @@ function toNativeClient(harness) {
|
|
|
301
311
|
extraPostInstall: overrides.extraPostInstall,
|
|
302
312
|
uninstallExtra: spec.uninstallExtra,
|
|
303
313
|
verifyListed: spec.verifyListed,
|
|
314
|
+
pluginInstall: overrides.pluginInstall,
|
|
304
315
|
};
|
|
305
316
|
if (spec.keyRequired) {
|
|
306
317
|
if (!keyAuth)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import spawn from 'cross-spawn';
|
|
2
2
|
import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validatedApiKey, } from './auth.js';
|
|
3
|
-
import { KEYLESS_ONBOARDING_PROMPT, launchNativeMcpClient, } from './connect-clients.js';
|
|
3
|
+
import { KEYLESS_ONBOARDING_PROMPT, launchNativeMcpClient, onboardingPrompt, } from './connect-clients.js';
|
|
4
4
|
import { captureStdio, capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
|
|
5
5
|
import { ensureCliAuthenticated } from './connect-auth.js';
|
|
6
6
|
import { installWebSkill } from './skill-install.js';
|
|
@@ -14,7 +14,8 @@ import { SIGN_IN_PROBES, TINYFISH_LIST_ROW } from './registration-detect.js';
|
|
|
14
14
|
import { errLine, sanitizeLine, warnLine } from './output.js';
|
|
15
15
|
import { verifyMcpAuth } from './verify.js';
|
|
16
16
|
import { dotenvMarkerHead, harnessDotenvPath, writeDotenvKey } from './dotenv-file.js';
|
|
17
|
-
import { apiBaseFromMcpUrl } from './mcp-endpoint.js';
|
|
17
|
+
import { apiBaseFromMcpUrl, isDefaultEndpoint } from './mcp-endpoint.js';
|
|
18
|
+
import { removeWebSkills } from './skill-vendor.js';
|
|
18
19
|
import { finishSetupHint, requireKeylessMcp, requireSupportOrRescue, runCliInstallStep, trackPostInstallFailure, walkthroughPhase, } from './connect-steps.js';
|
|
19
20
|
/** Several installs can disagree; every later spawn reuses this one. */
|
|
20
21
|
function pinExecutable(client) {
|
|
@@ -243,8 +244,27 @@ function performDirectInstall(client, install, mcpUrl) {
|
|
|
243
244
|
const detail = registered.detail ? `: ${registered.detail}` : '';
|
|
244
245
|
throw new ConnectStepError(`${client.displayName} reported success but TinyFish is not enabled there${detail}.`, 'invalid_config', { failureDetail: registered.tag });
|
|
245
246
|
}
|
|
246
|
-
/**
|
|
247
|
+
/** Directory plugin first; on any fallback reason, the harness add. */
|
|
247
248
|
function performMcpAdd(client, options, plan, attemptId) {
|
|
249
|
+
if (!client.pluginInstall)
|
|
250
|
+
return performHarnessAdd(client, options, plan, attemptId);
|
|
251
|
+
let pluginFallbackReason = 'custom-url';
|
|
252
|
+
// The plugin pins the prod endpoint; `--url` needs the add.
|
|
253
|
+
if (isDefaultEndpoint(options.mcpUrl)) {
|
|
254
|
+
errLine(`Installing the TinyFish plugin in ${client.displayName}...`);
|
|
255
|
+
const plugin = client.pluginInstall.install({
|
|
256
|
+
command: client.command,
|
|
257
|
+
attemptId,
|
|
258
|
+
apiKey: plan.keyedAdd?.key,
|
|
259
|
+
verbose: options.verbose ?? false,
|
|
260
|
+
});
|
|
261
|
+
if (plugin.installed)
|
|
262
|
+
return { note: plugin.note, plugin: true };
|
|
263
|
+
pluginFallbackReason = plugin.fallbackReason;
|
|
264
|
+
}
|
|
265
|
+
return { ...performHarnessAdd(client, options, plan, attemptId), pluginFallbackReason };
|
|
266
|
+
}
|
|
267
|
+
function performHarnessAdd(client, options, plan, attemptId) {
|
|
248
268
|
const { keyedAdd, buildAddArgs, directInstall } = plan;
|
|
249
269
|
const mcpUrl = new URL(options.mcpUrl);
|
|
250
270
|
mcpUrl.searchParams.set('source', CONNECT_SOURCE);
|
|
@@ -363,6 +383,10 @@ async function completeAuthFlow(client, options, state, telemetry, ctx) {
|
|
|
363
383
|
: AuthMode.OAuth;
|
|
364
384
|
return { authMode, signInDeferred };
|
|
365
385
|
}
|
|
386
|
+
/** A degraded harness keeps sign-in deferred on the plugin path too. */
|
|
387
|
+
function authLoginArgs(client, loginArgs, plugin) {
|
|
388
|
+
return plugin ? loginArgs && client.pluginInstall?.loginArgs : loginArgs;
|
|
389
|
+
}
|
|
366
390
|
export async function connectNativeMcpClient(unpinned, options) {
|
|
367
391
|
const client = pinExecutable(unpinned);
|
|
368
392
|
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
|
|
@@ -391,8 +415,9 @@ export async function connectNativeMcpClient(unpinned, options) {
|
|
|
391
415
|
// A probed client authenticates inside `mcp add` too, so a failure there is either.
|
|
392
416
|
state.stage =
|
|
393
417
|
loginArgs && !probeAuthenticated ? 'registration' : 'registration_or_authentication';
|
|
394
|
-
const { note: seededNote, seededHome } = performMcpAdd(client, options, plan, telemetry.attemptId);
|
|
395
|
-
|
|
418
|
+
const { note: seededNote, seededHome, plugin, pluginFallbackReason, } = performMcpAdd(client, options, plan, telemetry.attemptId);
|
|
419
|
+
state.plugin = plugin;
|
|
420
|
+
telemetry.track('checkpoint', { phase: 'registered', pluginFallbackReason });
|
|
396
421
|
// Only 2.x lists a variant, and only 2.x wrote the project file (PF-6613).
|
|
397
422
|
const twoPointX = client.verifyListed && matchedVariant !== undefined;
|
|
398
423
|
for (const stray of twoPointX ? strayProjectKeyFiles() : []) {
|
|
@@ -402,7 +427,7 @@ export async function connectNativeMcpClient(unpinned, options) {
|
|
|
402
427
|
useKeyAuth,
|
|
403
428
|
useKeyless,
|
|
404
429
|
loginDegraded,
|
|
405
|
-
loginArgs,
|
|
430
|
+
loginArgs: authLoginArgs(client, loginArgs, plugin),
|
|
406
431
|
detachedLogin: !!client.detachedLoginVariant && matchedVariant === client.detachedLoginVariant,
|
|
407
432
|
probeAuthenticated,
|
|
408
433
|
seededNote,
|
|
@@ -470,13 +495,18 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
470
495
|
};
|
|
471
496
|
try {
|
|
472
497
|
// Above every step: a Ctrl+C below must not lose a registration already reported (PF-3803).
|
|
473
|
-
saveConnectContext(client.connectClient, telemetry.attemptId, state.authMode);
|
|
498
|
+
saveConnectContext(client.connectClient, telemetry.attemptId, state.authMode, state.plugin);
|
|
474
499
|
if (options.cliInstall !== false) {
|
|
475
500
|
await attempt('cli_install', () => runCliInstallStep(options, telemetry));
|
|
476
501
|
}
|
|
477
|
-
|
|
502
|
+
const { skillAgent } = client;
|
|
503
|
+
if (skillAgent) {
|
|
478
504
|
await attempt('skill_install', () => {
|
|
479
|
-
|
|
505
|
+
// The plugin ships its own skills; vendored copies would duplicate them.
|
|
506
|
+
if (state.plugin)
|
|
507
|
+
removeWebSkills(skillAgent);
|
|
508
|
+
else
|
|
509
|
+
installWebSkill(client, { verbose: options.verbose ?? false });
|
|
480
510
|
telemetry.track('checkpoint', { phase: 'skill_installed' });
|
|
481
511
|
});
|
|
482
512
|
}
|
|
@@ -509,7 +539,9 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
509
539
|
telemetry.track('checkpoint', { phase: walkthroughPhase(outcome) });
|
|
510
540
|
// Flushed pre-block; the session-long handover would strand the delivery.
|
|
511
541
|
await telemetry.flush();
|
|
512
|
-
}, state.authMode === AuthMode.Keyless
|
|
542
|
+
}, state.authMode === AuthMode.Keyless
|
|
543
|
+
? KEYLESS_ONBOARDING_PROMPT
|
|
544
|
+
: onboardingPrompt(state.plugin ? client.pluginInstall?.serverName : undefined));
|
|
513
545
|
});
|
|
514
546
|
}
|
|
515
547
|
}
|
|
@@ -62,6 +62,8 @@ export interface ConnectRunState {
|
|
|
62
62
|
harnessDegraded?: boolean;
|
|
63
63
|
/** Resolved after the probe, so callers read real key use rather than key presence. */
|
|
64
64
|
authMode?: ConnectAuthMode;
|
|
65
|
+
/** The Directory plugin replaced `mcp add` and the vendored skills. */
|
|
66
|
+
plugin?: boolean;
|
|
65
67
|
}
|
|
66
68
|
interface ConnectStageDetail {
|
|
67
69
|
failedStage?: ConnectFailureStage;
|
|
@@ -79,6 +81,8 @@ interface ConnectStageDetail {
|
|
|
79
81
|
failureDetail?: string;
|
|
80
82
|
/** pi only: without the adapter its MCP entry is dormant. */
|
|
81
83
|
piAdapterState?: PiAdapterState;
|
|
84
|
+
/** claude-code only: why connect skipped the Directory plugin. */
|
|
85
|
+
pluginFallbackReason?: string;
|
|
82
86
|
}
|
|
83
87
|
export interface ConnectTelemetry {
|
|
84
88
|
attemptId: string;
|
|
@@ -195,9 +195,9 @@ function isBelowMin(version, minVersion) {
|
|
|
195
195
|
// Flags go unseen for reasons besides age; state facts, not causes.
|
|
196
196
|
function unavailableMessage(client, version) {
|
|
197
197
|
const { minVersion } = client.supportCheck;
|
|
198
|
-
|
|
199
|
-
const required = minVersion &&
|
|
200
|
-
? ` ${client.displayName} ${minVersion} or newer is required
|
|
198
|
+
// A known version below the minimum already failed in requireMinVersion.
|
|
199
|
+
const required = minVersion && version === undefined
|
|
200
|
+
? ` ${client.displayName} ${minVersion} or newer is required.`
|
|
201
201
|
: '';
|
|
202
202
|
return (`${client.supportCheck.unavailableMessage}${required}` +
|
|
203
203
|
' Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.');
|
|
@@ -292,8 +292,8 @@ export function requireCommandSupport(client) {
|
|
|
292
292
|
}
|
|
293
293
|
// Old releases can list the flags yet mishandle them; unknown passes.
|
|
294
294
|
function requireMinVersion(client, harnessVersion) {
|
|
295
|
-
const { minVersion, updateCommand
|
|
296
|
-
if (!
|
|
295
|
+
const { minVersion, updateCommand } = client.supportCheck;
|
|
296
|
+
if (!isBelowMin(harnessVersion, minVersion))
|
|
297
297
|
return;
|
|
298
298
|
const fix = updateCommand ? `Run \`${updateCommand}\`` : `Update ${client.displayName}`;
|
|
299
299
|
throw new ConnectStepError(`${client.displayName} ${minVersion} or newer is required (found ${harnessVersion}). ` +
|
|
@@ -353,22 +353,24 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
|
|
|
353
353
|
});
|
|
354
354
|
}
|
|
355
355
|
function eventBody(stage, detail, stageDurationMs) {
|
|
356
|
+
const d = detail ?? {};
|
|
356
357
|
return JSON.stringify({
|
|
357
358
|
attempt_id: attemptId,
|
|
358
359
|
client,
|
|
359
360
|
stage,
|
|
360
|
-
failed_stage:
|
|
361
|
-
phase:
|
|
362
|
-
failure_reason:
|
|
361
|
+
failed_stage: d.failedStage,
|
|
362
|
+
phase: d.phase,
|
|
363
|
+
failure_reason: d.failureReason,
|
|
363
364
|
// Only with failed_stage: the route refine 400s the event otherwise.
|
|
364
|
-
failure_detail:
|
|
365
|
+
failure_detail: d.failedStage ? d.failureDetail : undefined,
|
|
365
366
|
// Terminal rows only: rescue checkpoints stay wire-identical to normal ones.
|
|
366
367
|
fallback_outcome: stage === 'failed' || stage === 'aborted' ? fallbackOutcome : undefined,
|
|
367
|
-
harness_version:
|
|
368
|
-
plugin_version:
|
|
369
|
-
auth_mode:
|
|
370
|
-
harness_degraded:
|
|
371
|
-
pi_adapter_state:
|
|
368
|
+
harness_version: d.harnessVersion ?? harnessVersion,
|
|
369
|
+
plugin_version: d.pluginVersion,
|
|
370
|
+
auth_mode: d.authMode,
|
|
371
|
+
harness_degraded: d.harnessDegraded,
|
|
372
|
+
pi_adapter_state: d.piAdapterState,
|
|
373
|
+
plugin_fallback_reason: d.pluginFallbackReason,
|
|
372
374
|
stage_duration_ms: stageDurationMs,
|
|
373
375
|
runtime_platform: process.platform,
|
|
374
376
|
node_version: process.version,
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -21,3 +21,4 @@ export declare const HARNESS_PROBE_TIMEOUT_MS = 15000;
|
|
|
21
21
|
export declare const HERMES_COLD_START_TIMEOUT_MS = 60000;
|
|
22
22
|
/** Hermes' own id for the TinyFish web plugin. */
|
|
23
23
|
export declare const HERMES_PLUGIN_NAME = "tinyfish";
|
|
24
|
+
export declare const CLAUDE_PLUGIN_ID = "tinyfish@anthropic-plugin-directory";
|
package/dist/lib/constants.js
CHANGED
|
@@ -66,3 +66,4 @@ export const HARNESS_PROBE_TIMEOUT_MS = 15_000;
|
|
|
66
66
|
export const HERMES_COLD_START_TIMEOUT_MS = 60_000;
|
|
67
67
|
/** Hermes' own id for the TinyFish web plugin. */
|
|
68
68
|
export const HERMES_PLUGIN_NAME = 'tinyfish';
|
|
69
|
+
export const CLAUDE_PLUGIN_ID = 'tinyfish@anthropic-plugin-directory';
|
|
@@ -16,8 +16,6 @@ export interface HarnessSupportCheck {
|
|
|
16
16
|
timeoutMs?: number;
|
|
17
17
|
/** First release with the probed flags; bisected upstream, not derivable. */
|
|
18
18
|
minVersion?: string;
|
|
19
|
-
/** Refuse below minVersion even when the probed flags are listed. */
|
|
20
|
-
enforceMinVersion?: true;
|
|
21
19
|
/** Shown in the below-minVersion error as the one-step fix. */
|
|
22
20
|
updateCommand?: string;
|
|
23
21
|
unavailableMessage: string;
|
|
@@ -125,6 +123,10 @@ export declare const HARNESS_SPECS: {
|
|
|
125
123
|
args: string[];
|
|
126
124
|
label: string;
|
|
127
125
|
}[];
|
|
126
|
+
uninstallExtra: {
|
|
127
|
+
args: string[];
|
|
128
|
+
label: string;
|
|
129
|
+
};
|
|
128
130
|
};
|
|
129
131
|
cline: {
|
|
130
132
|
command: string;
|
|
@@ -229,7 +231,6 @@ export declare const HARNESS_SPECS: {
|
|
|
229
231
|
keyAuthPattern: RegExp;
|
|
230
232
|
timeoutMs: number;
|
|
231
233
|
minVersion: string;
|
|
232
|
-
enforceMinVersion: true;
|
|
233
234
|
updateCommand: string;
|
|
234
235
|
unavailableMessage: string;
|
|
235
236
|
};
|
|
@@ -334,6 +335,8 @@ export declare const HARNESS_SPECS: {
|
|
|
334
335
|
/** Derived from spec keys; one entry extends every union. */
|
|
335
336
|
export type Harness = keyof typeof HARNESS_SPECS;
|
|
336
337
|
export declare const ALL_HARNESSES: readonly Harness[];
|
|
338
|
+
export declare const HARNESS_CHOICES: string[];
|
|
339
|
+
export declare function resolveHarnessName(name: string): string;
|
|
337
340
|
export declare function harnessSpec(harness: Harness): HarnessSpec;
|
|
338
341
|
/** The rest of the union: every one of these must supply a connect and launch override. */
|
|
339
342
|
export type NonNativeHarness = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HERMES_COLD_START_TIMEOUT_MS, HERMES_PLUGIN_NAME } from '../constants.js';
|
|
1
|
+
import { CLAUDE_PLUGIN_ID, HERMES_COLD_START_TIMEOUT_MS, HERMES_PLUGIN_NAME, } from '../constants.js';
|
|
2
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
4
|
/** One entry per harness; every per-harness list derives from it. */
|
|
@@ -35,7 +35,17 @@ export const HARNESS_SPECS = {
|
|
|
35
35
|
args: ['mcp', 'remove', 'tinyfish', '--scope', 'local'],
|
|
36
36
|
label: 'local TinyFish registration',
|
|
37
37
|
},
|
|
38
|
+
// Cookbook plugin server is also plugin:tinyfish:tinyfish; it shadows ours.
|
|
39
|
+
{
|
|
40
|
+
args: ['plugin', 'uninstall', 'tinyfish@tinyfish-marketplace'],
|
|
41
|
+
label: 'TinyFish cookbook plugin',
|
|
42
|
+
},
|
|
38
43
|
],
|
|
44
|
+
// Not a removal: those run pre-add and would undo a Directory-off keep.
|
|
45
|
+
uninstallExtra: {
|
|
46
|
+
args: ['plugin', 'uninstall', CLAUDE_PLUGIN_ID],
|
|
47
|
+
label: 'TinyFish Directory plugin',
|
|
48
|
+
},
|
|
39
49
|
},
|
|
40
50
|
cline: {
|
|
41
51
|
command: 'cline',
|
|
@@ -145,7 +155,6 @@ export const HARNESS_SPECS = {
|
|
|
145
155
|
timeoutMs: HERMES_COLD_START_TIMEOUT_MS,
|
|
146
156
|
// Older `config set` saves our JSON entry as an unusable string.
|
|
147
157
|
minVersion: '0.21.2',
|
|
148
|
-
enforceMinVersion: true,
|
|
149
158
|
updateCommand: 'hermes update',
|
|
150
159
|
unavailableMessage: HERMES_OAUTH_UNAVAILABLE_MESSAGE,
|
|
151
160
|
},
|
|
@@ -257,6 +266,12 @@ export const HARNESS_SPECS = {
|
|
|
257
266
|
},
|
|
258
267
|
};
|
|
259
268
|
export const ALL_HARNESSES = Object.keys(HARNESS_SPECS);
|
|
269
|
+
// Input-only: telemetry, insights and the cookbook doctor skill key on `claude-code`.
|
|
270
|
+
const HARNESS_ALIASES = { claude: 'claude-code' };
|
|
271
|
+
export const HARNESS_CHOICES = [...ALL_HARNESSES, ...Object.keys(HARNESS_ALIASES)];
|
|
272
|
+
export function resolveHarnessName(name) {
|
|
273
|
+
return Object.hasOwn(HARNESS_ALIASES, name) ? HARNESS_ALIASES[name] : name;
|
|
274
|
+
}
|
|
260
275
|
export function harnessSpec(harness) {
|
|
261
276
|
return HARNESS_SPECS[harness];
|
|
262
277
|
}
|
|
@@ -31,13 +31,16 @@ function freshBundleDir() {
|
|
|
31
31
|
export function updateWebSkill({ verbose }) {
|
|
32
32
|
// One entry per `tinyfish connect <client>`, persisted by PF-3169.
|
|
33
33
|
const connected = loadConfig().connect ?? {};
|
|
34
|
-
|
|
34
|
+
// A plugin install ships and updates its own skills.
|
|
35
|
+
const skillAgents = ALL_HARNESSES.filter((h) => connected[h] && !connected[h].plugin)
|
|
35
36
|
.map((harness) => harnessSpec(harness).skillAgent)
|
|
36
37
|
.filter((agent) => agent !== undefined);
|
|
37
38
|
const hasOpenClaw = Boolean(connected['openclaw']);
|
|
38
39
|
// No recorded skill install means no target dirs; connect is the install path.
|
|
39
40
|
if (skillAgents.length === 0 && !hasOpenClaw) {
|
|
40
|
-
|
|
41
|
+
if (!Object.values(connected).some((entry) => entry.plugin)) {
|
|
42
|
+
errLine('No TinyFish skills to refresh. Run `tinyfish connect <client>` to install them.');
|
|
43
|
+
}
|
|
41
44
|
return false;
|
|
42
45
|
}
|
|
43
46
|
let refreshed = false;
|