@tiny-fish/cli 0.30.2-next.256 → 0.30.2-next.258
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/auth.d.ts +1 -1
- package/dist/commands/auth.js +52 -52
- package/dist/commands/batch.d.ts +1 -1
- package/dist/commands/batch.js +54 -46
- package/dist/commands/browser.d.ts +1 -1
- package/dist/commands/browser.js +14 -14
- package/dist/commands/config-claude.d.ts +1 -1
- package/dist/commands/config-claude.js +22 -22
- package/dist/commands/connect.d.ts +3 -3
- package/dist/commands/connect.js +150 -150
- package/dist/commands/doctor.d.ts +4 -4
- package/dist/commands/doctor.js +104 -104
- package/dist/commands/fetch.d.ts +1 -1
- package/dist/commands/fetch.js +24 -27
- package/dist/commands/onboard.d.ts +1 -1
- package/dist/commands/onboard.js +50 -50
- package/dist/commands/profile.d.ts +3 -3
- package/dist/commands/profile.js +45 -45
- package/dist/commands/run.d.ts +1 -1
- package/dist/commands/run.js +78 -65
- package/dist/commands/runs.d.ts +1 -1
- package/dist/commands/runs.js +63 -50
- package/dist/commands/search.d.ts +1 -1
- package/dist/commands/search.js +16 -16
- package/dist/commands/upgrade.d.ts +2 -2
- package/dist/commands/upgrade.js +36 -36
- package/dist/commands/vault.d.ts +1 -1
- package/dist/commands/vault.js +48 -45
- package/dist/commands/wallet.d.ts +1 -1
- package/dist/commands/wallet.js +14 -14
- package/dist/index.js +2 -2
- package/dist/lib/auth.d.ts +5 -5
- package/dist/lib/auth.js +56 -56
- package/dist/lib/batch-store.d.ts +1 -1
- package/dist/lib/batch-store.js +4 -4
- package/dist/lib/chrome-cookies.d.ts +1 -1
- package/dist/lib/chrome-cookies.js +24 -24
- package/dist/lib/claude-config.js +37 -37
- package/dist/lib/cli-install.js +12 -12
- package/dist/lib/client.d.ts +2 -2
- package/dist/lib/client.js +61 -61
- package/dist/lib/connect-all.js +140 -134
- package/dist/lib/connect-auth.d.ts +2 -2
- package/dist/lib/connect-auth.js +15 -15
- package/dist/lib/connect-clients.d.ts +6 -6
- package/dist/lib/connect-clients.js +143 -143
- package/dist/lib/connect-fallback.d.ts +1 -1
- package/dist/lib/connect-fallback.js +38 -38
- package/dist/lib/connect-picker.d.ts +4 -4
- package/dist/lib/connect-picker.js +28 -28
- package/dist/lib/connect-runtime.d.ts +7 -7
- package/dist/lib/connect-runtime.js +61 -61
- package/dist/lib/constants.js +13 -13
- package/dist/lib/cursor-config.d.ts +1 -1
- package/dist/lib/cursor-config.js +30 -30
- package/dist/lib/doctor-report.d.ts +2 -2
- package/dist/lib/doctor-report.js +22 -22
- package/dist/lib/harness-detect.js +39 -39
- package/dist/lib/harness.js +9 -9
- package/dist/lib/hermes-env.js +26 -23
- package/dist/lib/install-root.js +12 -12
- package/dist/lib/notice.d.ts +1 -1
- package/dist/lib/notice.js +12 -12
- package/dist/lib/output.js +14 -14
- package/dist/lib/registration-detect.d.ts +3 -3
- package/dist/lib/registration-detect.js +82 -82
- package/dist/lib/setup-telemetry.d.ts +4 -4
- package/dist/lib/setup-telemetry.js +34 -34
- package/dist/lib/signals.js +1 -1
- package/dist/lib/skill-install.d.ts +3 -3
- package/dist/lib/skill-install.js +40 -40
- package/dist/lib/types.d.ts +8 -8
- package/dist/lib/verify.d.ts +1 -1
- package/dist/lib/verify.js +28 -28
- package/dist/program.d.ts +1 -1
- package/dist/program.js +28 -28
- package/package.json +18 -9
package/dist/lib/notice.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Command } from
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
2
|
/** Test-only: the capture slot and the once-per-invocation latch are module state. */
|
|
3
3
|
export declare function resetNoticeState(): void;
|
|
4
4
|
/** Records the first notice seen; printing is deferred so it lands at the end of a command. */
|
package/dist/lib/notice.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { CLI_VERSION } from
|
|
2
|
-
import { detectHumanInitiated } from
|
|
3
|
-
import { err, sanitizeLine, warnLine } from
|
|
4
|
-
const NOTICE_HEADER =
|
|
5
|
-
const LEVEL_HEADER =
|
|
1
|
+
import { CLI_VERSION } from './constants.js';
|
|
2
|
+
import { detectHumanInitiated } from './harness.js';
|
|
3
|
+
import { err, sanitizeLine, warnLine } from './output.js';
|
|
4
|
+
const NOTICE_HEADER = 'x-tf-notice';
|
|
5
|
+
const LEVEL_HEADER = 'x-tf-notice-level';
|
|
6
6
|
const MAX_LENGTH = 300;
|
|
7
|
-
const VALID_LEVELS = new Set([
|
|
7
|
+
const VALID_LEVELS = new Set(['required', 'recommended']);
|
|
8
8
|
let captured;
|
|
9
9
|
let printed = false;
|
|
10
10
|
/** Test-only: the capture slot and the once-per-invocation latch are module state. */
|
|
@@ -16,9 +16,9 @@ export function resetNoticeState() {
|
|
|
16
16
|
export function captureNotice(headers) {
|
|
17
17
|
if (captured)
|
|
18
18
|
return;
|
|
19
|
-
if (typeof headers?.get !==
|
|
19
|
+
if (typeof headers?.get !== 'function')
|
|
20
20
|
return;
|
|
21
|
-
const message = sanitizeLine(headers.get(NOTICE_HEADER) ??
|
|
21
|
+
const message = sanitizeLine(headers.get(NOTICE_HEADER) ?? '')
|
|
22
22
|
.trim()
|
|
23
23
|
.slice(0, MAX_LENGTH);
|
|
24
24
|
if (!message)
|
|
@@ -29,7 +29,7 @@ export function captureNotice(headers) {
|
|
|
29
29
|
}
|
|
30
30
|
/** Built here, not by the server, so nothing it sends can reach an agent's context as instructions. */
|
|
31
31
|
function agentMessage(level) {
|
|
32
|
-
return level ===
|
|
32
|
+
return level === 'required'
|
|
33
33
|
? `This TinyFish CLI (${CLI_VERSION}) is below the required minimum. Run: tinyfish upgrade`
|
|
34
34
|
: `A TinyFish CLI update is available (you have ${CLI_VERSION}). Run: tinyfish upgrade`;
|
|
35
35
|
}
|
|
@@ -53,7 +53,7 @@ export function suppressNotice() {
|
|
|
53
53
|
/** postAction covers clean completions, the exit hook covers the process.exit and rejection paths, the latch keeps it to one. */
|
|
54
54
|
export function installNoticeEmit(program) {
|
|
55
55
|
const backstop = () => emitNotice();
|
|
56
|
-
program.hook(
|
|
57
|
-
process.once(
|
|
58
|
-
return () => process.removeListener(
|
|
56
|
+
program.hook('postAction', () => emitNotice());
|
|
57
|
+
process.once('exit', backstop);
|
|
58
|
+
return () => process.removeListener('exit', backstop);
|
|
59
59
|
}
|
package/dist/lib/output.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { stripVTControlCharacters } from
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
2
|
export class ApiError extends Error {
|
|
3
3
|
status;
|
|
4
4
|
code;
|
|
@@ -6,21 +6,21 @@ export class ApiError extends Error {
|
|
|
6
6
|
super(message);
|
|
7
7
|
this.status = status;
|
|
8
8
|
this.code = code;
|
|
9
|
-
this.name =
|
|
9
|
+
this.name = 'ApiError';
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
export function out(data) {
|
|
13
|
-
process.stdout.write(JSON.stringify(data) +
|
|
13
|
+
process.stdout.write(JSON.stringify(data) + '\n');
|
|
14
14
|
}
|
|
15
15
|
/** The only writer for untrusted text: strips ANSI and C0/C1 controls. */
|
|
16
16
|
export function sanitizeLine(text) {
|
|
17
17
|
// eslint-disable-next-line no-control-regex
|
|
18
|
-
return stripVTControlCharacters(text).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g,
|
|
18
|
+
return stripVTControlCharacters(text).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, '');
|
|
19
19
|
}
|
|
20
20
|
export function outLine(line) {
|
|
21
|
-
process.stdout.write(sanitizeLine(line) +
|
|
21
|
+
process.stdout.write(sanitizeLine(line) + '\n');
|
|
22
22
|
}
|
|
23
|
-
let errIndent =
|
|
23
|
+
let errIndent = '';
|
|
24
24
|
/** `connect --all` nests a harness's own lines under its step header. */
|
|
25
25
|
export function setErrIndent(prefix) {
|
|
26
26
|
errIndent = prefix;
|
|
@@ -28,20 +28,20 @@ export function setErrIndent(prefix) {
|
|
|
28
28
|
export function errLine(line) {
|
|
29
29
|
const text = sanitizeLine(line);
|
|
30
30
|
// Per line, not once: a replayed capture carries newlines.
|
|
31
|
-
process.stderr.write((errIndent ? text.replace(/^(?=.)/gm, errIndent) : text) +
|
|
31
|
+
process.stderr.write((errIndent ? text.replace(/^(?=.)/gm, errIndent) : text) + '\n');
|
|
32
32
|
}
|
|
33
|
-
const WARN_ON =
|
|
34
|
-
const WARN_OFF =
|
|
33
|
+
const WARN_ON = '\x1b[33m';
|
|
34
|
+
const WARN_OFF = '\x1b[39m';
|
|
35
35
|
/** Advisory stderr line, yellow on a terminal and plain everywhere else so piped output stays clean. */
|
|
36
36
|
export function warnLine(line) {
|
|
37
37
|
const text = sanitizeLine(line);
|
|
38
|
-
const colour = Boolean(process.stderr.isTTY) && !process.env[
|
|
38
|
+
const colour = Boolean(process.stderr.isTTY) && !process.env['NO_COLOR'];
|
|
39
39
|
process.stderr.write(colour ? `${WARN_ON}${text}${WARN_OFF}\n` : `${text}\n`);
|
|
40
40
|
}
|
|
41
41
|
export function err(data) {
|
|
42
|
-
const payload = typeof data ===
|
|
43
|
-
const serialized = JSON.stringify(payload) ??
|
|
44
|
-
process.stderr.write(sanitizeLine(serialized) +
|
|
42
|
+
const payload = typeof data === 'string' ? { error: data } : data;
|
|
43
|
+
const serialized = JSON.stringify(payload) ?? 'undefined';
|
|
44
|
+
process.stderr.write(sanitizeLine(serialized) + '\n');
|
|
45
45
|
}
|
|
46
46
|
export function handleApiError(e) {
|
|
47
47
|
if (e instanceof ApiError) {
|
|
@@ -50,7 +50,7 @@ export function handleApiError(e) {
|
|
|
50
50
|
payload.code = e.code;
|
|
51
51
|
if (e.status === 401)
|
|
52
52
|
payload.hint =
|
|
53
|
-
|
|
53
|
+
'Clear the TINYFISH_API_KEY environment variable, run `tinyfish auth login`, then try again.';
|
|
54
54
|
err(payload);
|
|
55
55
|
}
|
|
56
56
|
else if (e instanceof Error) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type RecordedAuthMode } from
|
|
2
|
-
import type { AgentClient } from
|
|
3
|
-
import { AuthMode, type Harness, Registered } from
|
|
1
|
+
import { type RecordedAuthMode } from './auth.js';
|
|
2
|
+
import type { AgentClient } from './connect-runtime.js';
|
|
3
|
+
import { AuthMode, type Harness, Registered } from './harness-detect.js';
|
|
4
4
|
export interface RegistrationStatus {
|
|
5
5
|
harness: Harness;
|
|
6
6
|
detected: boolean;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as path from
|
|
3
|
-
import spawn from
|
|
4
|
-
import { z } from
|
|
5
|
-
import { loadConfig, matchesCliKey } from
|
|
6
|
-
import { NATIVE_BY_HARNESS } from
|
|
7
|
-
import { HARNESS_PROBE_TIMEOUT_MS } from
|
|
8
|
-
import { errLine } from
|
|
9
|
-
import { readCursorTinyfishEntry } from
|
|
10
|
-
import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMode, Registered, } from
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import spawn from 'cross-spawn';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { loadConfig, matchesCliKey } from './auth.js';
|
|
6
|
+
import { NATIVE_BY_HARNESS } from './connect-clients.js';
|
|
7
|
+
import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
|
|
8
|
+
import { errLine } from './output.js';
|
|
9
|
+
import { readCursorTinyfishEntry } from './cursor-config.js';
|
|
10
|
+
import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMode, Registered, } from './harness-detect.js';
|
|
11
11
|
// A list, verified against codex 0.146. A name-keyed map was accepted here too, but every
|
|
12
12
|
// field is optional, so that arm parsed *any* object-of-objects: a wrapper like
|
|
13
13
|
// `{"servers": {...}}` decoded as one entry named `servers` and reported TinyFish absent.
|
|
@@ -33,50 +33,50 @@ const ANSI_SGR = /\x1b\[[0-9;]*m/g;
|
|
|
33
33
|
// The report is pasted in public, so every reason is authored here. A spawn error's own
|
|
34
34
|
// message quotes absolute paths, which is how a home directory reaches the payload.
|
|
35
35
|
function spawnFailureReason(command, code) {
|
|
36
|
-
if (code ===
|
|
36
|
+
if (code === 'ENOENT')
|
|
37
37
|
return `\`${command}\` is not on PATH`;
|
|
38
|
-
if (code ===
|
|
38
|
+
if (code === 'EACCES')
|
|
39
39
|
return `\`${command}\` is not executable`;
|
|
40
40
|
return `could not run \`${command}\``;
|
|
41
41
|
}
|
|
42
42
|
function runProbe(command, args) {
|
|
43
43
|
const result = spawn.sync(command, args, {
|
|
44
|
-
encoding:
|
|
44
|
+
encoding: 'utf8',
|
|
45
45
|
timeout: HARNESS_PROBE_TIMEOUT_MS,
|
|
46
46
|
// Belt and braces only: agents export FORCE_COLOR, and `NO_COLOR` does not fully silence
|
|
47
47
|
// OpenCode, so the strip below stays load-bearing rather than a fallback.
|
|
48
|
-
env: { ...process.env, NO_COLOR:
|
|
48
|
+
env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
|
|
49
49
|
});
|
|
50
50
|
if (result.error) {
|
|
51
51
|
const code = result.error.code;
|
|
52
52
|
// A timeout arrives as ETIMEDOUT here, not only as a signal, so it must be named before
|
|
53
53
|
// the generic spawn failure; otherwise a timeout reports "could not run".
|
|
54
|
-
if (code ===
|
|
55
|
-
return { outcome:
|
|
54
|
+
if (code === 'ETIMEDOUT') {
|
|
55
|
+
return { outcome: 'unavailable', reason: `\`${command} ${args.join(' ')}\` timed out` };
|
|
56
56
|
}
|
|
57
|
-
return { outcome:
|
|
57
|
+
return { outcome: 'unavailable', reason: spawnFailureReason(command, code) };
|
|
58
58
|
}
|
|
59
59
|
// Reached only when something else killed the child, so this is a crash, not the timeout.
|
|
60
60
|
if (result.signal) {
|
|
61
|
-
return { outcome:
|
|
61
|
+
return { outcome: 'unavailable', reason: `\`${command}\` was killed by ${result.signal}` };
|
|
62
62
|
}
|
|
63
63
|
// Both streams, since a colour code inside a JSON payload breaks the parse just as it
|
|
64
64
|
// breaks an anchored match. Valid JSON never carries a raw ESC byte, so this cannot corrupt it.
|
|
65
|
-
const stdout = (result.stdout ??
|
|
65
|
+
const stdout = (result.stdout ?? '').replace(ANSI_SGR, '');
|
|
66
66
|
return {
|
|
67
|
-
outcome:
|
|
67
|
+
outcome: 'ran',
|
|
68
68
|
exitCode: result.status ?? 1,
|
|
69
69
|
stdout,
|
|
70
|
-
output: `${stdout}${(result.stderr ??
|
|
70
|
+
output: `${stdout}${(result.stderr ?? '').replace(ANSI_SGR, '')}`,
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
/** Undefined when the probe ran and exited zero. */
|
|
74
74
|
function probeReason(command, args, probe) {
|
|
75
|
-
if (probe.outcome ===
|
|
75
|
+
if (probe.outcome === 'unavailable')
|
|
76
76
|
return probe.reason;
|
|
77
77
|
if (probe.exitCode === 0)
|
|
78
78
|
return undefined;
|
|
79
|
-
return `\`${command} ${args.join(
|
|
79
|
+
return `\`${command} ${args.join(' ')}\` exited ${probe.exitCode}`;
|
|
80
80
|
}
|
|
81
81
|
// A plugin registers its server as `plugin:<plugin>:<server>`, which `mcp get tinyfish` misses.
|
|
82
82
|
// The optional prefix must end in `:`, so `not-tinyfish` is not a match.
|
|
@@ -99,13 +99,13 @@ function connectionState(status) {
|
|
|
99
99
|
}
|
|
100
100
|
/** The plugin ships its own MCP server, so a working install can carry no `tinyfish` entry. */
|
|
101
101
|
function fromPluginRegistration(command) {
|
|
102
|
-
const probe = runProbe(command, [
|
|
103
|
-
if (probe.outcome ===
|
|
102
|
+
const probe = runProbe(command, ['mcp', 'list']);
|
|
103
|
+
if (probe.outcome === 'unavailable' || probe.exitCode !== 0)
|
|
104
104
|
return undefined;
|
|
105
105
|
const match = MCP_LIST_TINYFISH_LINE.exec(probe.output);
|
|
106
106
|
if (!match)
|
|
107
107
|
return undefined;
|
|
108
|
-
const connected = connectionState(match[3] ??
|
|
108
|
+
const connected = connectionState(match[3] ?? '');
|
|
109
109
|
// `mcp list` prints no headers, so the credential behind it is unproven, never assumed.
|
|
110
110
|
return {
|
|
111
111
|
registered: Registered.Yes,
|
|
@@ -119,7 +119,7 @@ function fromPluginRegistration(command) {
|
|
|
119
119
|
function tinyfishServerName(output) {
|
|
120
120
|
const listed = CONFIGURED_SERVERS.exec(output)?.[1];
|
|
121
121
|
return listed
|
|
122
|
-
?.split(
|
|
122
|
+
?.split(',')
|
|
123
123
|
.map((name) => name.trim())
|
|
124
124
|
.find((name) => SERVER_NAMED_TINYFISH.test(name));
|
|
125
125
|
}
|
|
@@ -140,8 +140,8 @@ function parseMcpGet(output) {
|
|
|
140
140
|
}
|
|
141
141
|
// claude-code only. Codex cannot use this: its `mcp get` exits 0 on a missing server.
|
|
142
142
|
function fromMcpGet(command) {
|
|
143
|
-
const probe = runProbe(command, [
|
|
144
|
-
if (probe.outcome ===
|
|
143
|
+
const probe = runProbe(command, ['mcp', 'get', 'tinyfish']);
|
|
144
|
+
if (probe.outcome === 'unavailable') {
|
|
145
145
|
return { registered: Registered.Unknown, authMode: AuthMode.Unknown, reason: probe.reason };
|
|
146
146
|
}
|
|
147
147
|
if (probe.exitCode !== 0) {
|
|
@@ -155,8 +155,8 @@ function fromMcpGet(command) {
|
|
|
155
155
|
// The roster is free; `mcp list` health-checks every server.
|
|
156
156
|
const scoped = tinyfishServerName(probe.output);
|
|
157
157
|
if (scoped) {
|
|
158
|
-
const rescoped = runProbe(command, [
|
|
159
|
-
if (rescoped.outcome ===
|
|
158
|
+
const rescoped = runProbe(command, ['mcp', 'get', scoped]);
|
|
159
|
+
if (rescoped.outcome === 'ran' && rescoped.exitCode === 0) {
|
|
160
160
|
return {
|
|
161
161
|
...parseMcpGet(rescoped.output),
|
|
162
162
|
reason: `registered as ${scoped}`,
|
|
@@ -182,13 +182,13 @@ function fromMcpGet(command) {
|
|
|
182
182
|
// `codex mcp get <missing>` prints an error and exits 0, so presence must never be read from
|
|
183
183
|
// its exit code; `mcp list --json` is the only trustworthy read-back.
|
|
184
184
|
function readCodexEntry() {
|
|
185
|
-
const args = [
|
|
186
|
-
const probe = runProbe(
|
|
187
|
-
if (probe.outcome ===
|
|
185
|
+
const args = ['mcp', 'list', '--json'];
|
|
186
|
+
const probe = runProbe('codex', args);
|
|
187
|
+
if (probe.outcome === 'unavailable')
|
|
188
188
|
return { reason: probe.reason };
|
|
189
189
|
// Presence is never inferred from exit code 0, but a nonzero exit means the output cannot
|
|
190
190
|
// be trusted either — that is `unknown`, not "no server".
|
|
191
|
-
const failure = probeReason(
|
|
191
|
+
const failure = probeReason('codex', args, probe);
|
|
192
192
|
if (failure)
|
|
193
193
|
return { reason: failure };
|
|
194
194
|
let servers;
|
|
@@ -198,25 +198,25 @@ function readCodexEntry() {
|
|
|
198
198
|
servers = JSON.parse(probe.stdout);
|
|
199
199
|
}
|
|
200
200
|
catch {
|
|
201
|
-
return { reason:
|
|
201
|
+
return { reason: '`codex mcp list --json` returned unparseable output' };
|
|
202
202
|
}
|
|
203
203
|
// A scalar or a wrapper object decodes fine and then silently reports "no server", which
|
|
204
204
|
// would suggest a spurious repair. Anything but a list is unknown.
|
|
205
205
|
const shape = codexListSchema.safeParse(servers);
|
|
206
206
|
if (!shape.success)
|
|
207
|
-
return { reason:
|
|
208
|
-
return { entry: shape.data.find((s) => s.name ===
|
|
207
|
+
return { reason: '`codex mcp list --json` returned an unexpected shape' };
|
|
208
|
+
return { entry: shape.data.find((s) => s.name === 'tinyfish') };
|
|
209
209
|
}
|
|
210
210
|
// `mcp add` exits 0 whether or not its inline OAuth finished, so `not_logged_in` is the only
|
|
211
211
|
// evidence of an abandoned sign-in; every other state, older Codex included, is unknown.
|
|
212
212
|
export function codexOauthCompleted() {
|
|
213
213
|
const read = readCodexEntry();
|
|
214
|
-
if (
|
|
214
|
+
if ('reason' in read)
|
|
215
215
|
return undefined;
|
|
216
216
|
const status = read.entry?.auth_status;
|
|
217
|
-
if (status ===
|
|
217
|
+
if (status === 'o_auth')
|
|
218
218
|
return true;
|
|
219
|
-
if (status ===
|
|
219
|
+
if (status === 'not_logged_in')
|
|
220
220
|
return false;
|
|
221
221
|
return undefined;
|
|
222
222
|
}
|
|
@@ -233,7 +233,7 @@ function declaredKeyEnvVar(harness) {
|
|
|
233
233
|
/** Codex stores a variable name; resolving it only answers whether it is the CLI's key. */
|
|
234
234
|
function envKeyVerdict(envVar) {
|
|
235
235
|
if (!ENV_VAR_NAME.test(envVar)) {
|
|
236
|
-
return { keyReason:
|
|
236
|
+
return { keyReason: 'its key comes from an unusable variable name' };
|
|
237
237
|
}
|
|
238
238
|
if (matchesCliKey(process.env[envVar]))
|
|
239
239
|
return { keyMatchesCliKey: true };
|
|
@@ -245,7 +245,7 @@ function envKeyVerdict(envVar) {
|
|
|
245
245
|
}
|
|
246
246
|
function probeCodex() {
|
|
247
247
|
const read = readCodexEntry();
|
|
248
|
-
if (
|
|
248
|
+
if ('reason' in read)
|
|
249
249
|
return { registered: Registered.Unknown, authMode: AuthMode.Unknown, reason: read.reason };
|
|
250
250
|
const entry = read.entry;
|
|
251
251
|
if (!entry)
|
|
@@ -253,14 +253,10 @@ function probeCodex() {
|
|
|
253
253
|
const codexUrl = entry.transport?.url;
|
|
254
254
|
// Read from this entry: codex emits `bearer_token_env_var` on every HTTP server, so testing
|
|
255
255
|
// the whole payload reports api-key for TinyFish because some other server carries a key.
|
|
256
|
-
const envVar = entry.auth_status ===
|
|
256
|
+
const envVar = entry.auth_status === 'o_auth' ? undefined : entry.transport?.bearer_token_env_var;
|
|
257
257
|
return {
|
|
258
258
|
registered: Registered.Yes,
|
|
259
|
-
authMode: entry.auth_status ===
|
|
260
|
-
? AuthMode.OAuth
|
|
261
|
-
: envVar
|
|
262
|
-
? AuthMode.ApiKey
|
|
263
|
-
: AuthMode.Unknown,
|
|
259
|
+
authMode: entry.auth_status === 'o_auth' ? AuthMode.OAuth : envVar ? AuthMode.ApiKey : AuthMode.Unknown,
|
|
264
260
|
...(codexUrl ? { registeredUrl: codexUrl } : {}),
|
|
265
261
|
...(envVar ? envKeyVerdict(envVar) : {}),
|
|
266
262
|
};
|
|
@@ -271,7 +267,7 @@ function probeCursor() {
|
|
|
271
267
|
return {
|
|
272
268
|
registered: Registered.Unknown,
|
|
273
269
|
authMode: AuthMode.Unknown,
|
|
274
|
-
reason:
|
|
270
|
+
reason: 'mcp.json exists but could not be read or parsed',
|
|
275
271
|
};
|
|
276
272
|
}
|
|
277
273
|
if (!entry.present)
|
|
@@ -292,7 +288,11 @@ const OPENCODE_ROW_URL = /^[^\w]*tinyfish(?![\w-]).*\n[^\w]*(https?:\S+)/m;
|
|
|
292
288
|
function fromMcpList(command, output, registeredMode, urlPattern) {
|
|
293
289
|
if (LISTED_AS_TINYFISH.test(output)) {
|
|
294
290
|
const url = urlPattern?.exec(output)?.[1];
|
|
295
|
-
return {
|
|
291
|
+
return {
|
|
292
|
+
registered: Registered.Yes,
|
|
293
|
+
authMode: registeredMode,
|
|
294
|
+
...(url ? { registeredUrl: url } : {}),
|
|
295
|
+
};
|
|
296
296
|
}
|
|
297
297
|
if (!MCP_LIST_HEADER.test(output)) {
|
|
298
298
|
return {
|
|
@@ -305,18 +305,18 @@ function fromMcpList(command, output, registeredMode, urlPattern) {
|
|
|
305
305
|
}
|
|
306
306
|
// connect only ever writes an OAuth Hermes entry, so `oauth` is a fact, not a detection gap.
|
|
307
307
|
function probeHermes() {
|
|
308
|
-
const args = [
|
|
309
|
-
const probe = runProbe(
|
|
310
|
-
if (probe.outcome !==
|
|
311
|
-
const reason = probeReason(
|
|
308
|
+
const args = ['mcp', 'list'];
|
|
309
|
+
const probe = runProbe('hermes', args);
|
|
310
|
+
if (probe.outcome !== 'ran' || probe.exitCode !== 0) {
|
|
311
|
+
const reason = probeReason('hermes', args, probe);
|
|
312
312
|
return { registered: Registered.Unknown, authMode: AuthMode.Unknown, reason };
|
|
313
313
|
}
|
|
314
|
-
return fromMcpList(
|
|
314
|
+
return fromMcpList('hermes', probe.output, AuthMode.OAuth);
|
|
315
315
|
}
|
|
316
316
|
const SKILL_DIR_IS_TINYFISH = /^(?:@tinyfish[/_-])?tinyfish(?![\w-])/i;
|
|
317
317
|
// OpenClaw installs a skill, not an MCP server; the skill shells the CLI, so auth is the CLI key.
|
|
318
318
|
function probeOpenClaw() {
|
|
319
|
-
const skillsDir = path.join(harnessConfigPath(
|
|
319
|
+
const skillsDir = path.join(harnessConfigPath('openclaw'), 'skills');
|
|
320
320
|
let entries;
|
|
321
321
|
try {
|
|
322
322
|
entries = fs.readdirSync(skillsDir);
|
|
@@ -325,7 +325,7 @@ function probeOpenClaw() {
|
|
|
325
325
|
return {
|
|
326
326
|
registered: Registered.Unknown,
|
|
327
327
|
authMode: AuthMode.Unknown,
|
|
328
|
-
reason: `no global skills directory at ${harnessDisplayPath(
|
|
328
|
+
reason: `no global skills directory at ${harnessDisplayPath('openclaw')}/skills; OpenClaw layout is unverified`,
|
|
329
329
|
};
|
|
330
330
|
}
|
|
331
331
|
// A bare substring also matched a skill merely named `not-tinyfish-thing`.
|
|
@@ -335,13 +335,13 @@ function probeOpenClaw() {
|
|
|
335
335
|
}
|
|
336
336
|
// `opencode mcp list` prints no headers, so a key-authed registration is indistinguishable.
|
|
337
337
|
function probeOpencode() {
|
|
338
|
-
const args = [
|
|
339
|
-
const probe = runProbe(
|
|
340
|
-
if (probe.outcome !==
|
|
341
|
-
const reason = probeReason(
|
|
338
|
+
const args = ['mcp', 'list'];
|
|
339
|
+
const probe = runProbe('opencode', args);
|
|
340
|
+
if (probe.outcome !== 'ran' || probe.exitCode !== 0) {
|
|
341
|
+
const reason = probeReason('opencode', args, probe);
|
|
342
342
|
return { registered: Registered.Unknown, authMode: AuthMode.Unknown, reason };
|
|
343
343
|
}
|
|
344
|
-
return fromMcpList(
|
|
344
|
+
return fromMcpList('opencode', probe.output, AuthMode.Unknown, OPENCODE_ROW_URL);
|
|
345
345
|
}
|
|
346
346
|
// Verified against grok 0.0.60: a list. Header values stay `unknown`; only names are read.
|
|
347
347
|
const grokEntrySchema = z.looseObject({
|
|
@@ -354,8 +354,8 @@ const grokListSchema = z.array(grokEntrySchema);
|
|
|
354
354
|
const grokPluginSchema = z.array(z.looseObject({ name: z.string().optional() }));
|
|
355
355
|
/** The Grok plugin ships its own MCP server, and `grok mcp list` does not show it. */
|
|
356
356
|
function fromGrokPlugin() {
|
|
357
|
-
const probe = runProbe(
|
|
358
|
-
if (probe.outcome ===
|
|
357
|
+
const probe = runProbe('grok', ['plugin', 'list', '--json']);
|
|
358
|
+
if (probe.outcome === 'unavailable' || probe.exitCode !== 0)
|
|
359
359
|
return undefined;
|
|
360
360
|
let listed;
|
|
361
361
|
try {
|
|
@@ -372,16 +372,16 @@ function fromGrokPlugin() {
|
|
|
372
372
|
return {
|
|
373
373
|
registered: Registered.Unknown,
|
|
374
374
|
authMode: AuthMode.Unknown,
|
|
375
|
-
reason:
|
|
375
|
+
reason: '`grok plugin list --json` returned an unexpected shape',
|
|
376
376
|
};
|
|
377
377
|
}
|
|
378
|
-
if (!shape.data.some((plugin) => plugin.name ===
|
|
378
|
+
if (!shape.data.some((plugin) => plugin.name === 'tinyfish'))
|
|
379
379
|
return undefined;
|
|
380
380
|
// The plugin manifest carries no headers, so its only credential path is the browser sign-in.
|
|
381
381
|
return {
|
|
382
382
|
registered: Registered.Yes,
|
|
383
383
|
authMode: AuthMode.OAuth,
|
|
384
|
-
reason:
|
|
384
|
+
reason: 'registered by the TinyFish Grok plugin',
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
387
|
// Grok inherits our env, so a header we wrote round-trips to our own value.
|
|
@@ -391,21 +391,21 @@ function grokWroteIt(value, declared) {
|
|
|
391
391
|
}
|
|
392
392
|
/** The descriptor names the variable; envKeyVerdict owns every wording for it. */
|
|
393
393
|
function grokKeyAuth(headers) {
|
|
394
|
-
const authorization = Object.entries(headers).find(([name]) => name.toLowerCase() ===
|
|
394
|
+
const authorization = Object.entries(headers).find(([name]) => name.toLowerCase() === 'authorization');
|
|
395
395
|
if (!authorization)
|
|
396
396
|
return { authMode: AuthMode.OAuth };
|
|
397
|
-
const declared = declaredKeyEnvVar(
|
|
398
|
-
const value = typeof authorization[1] ===
|
|
397
|
+
const declared = declaredKeyEnvVar('grok');
|
|
398
|
+
const value = typeof authorization[1] === 'string' ? authorization[1] : '';
|
|
399
399
|
if (!declared || !grokWroteIt(value, declared)) {
|
|
400
|
-
return { authMode: AuthMode.ApiKey, keyReason:
|
|
400
|
+
return { authMode: AuthMode.ApiKey, keyReason: 'it holds a key this CLI cannot verify' };
|
|
401
401
|
}
|
|
402
402
|
return { authMode: AuthMode.ApiKey, ...envKeyVerdict(declared) };
|
|
403
403
|
}
|
|
404
404
|
// Grok has no `mcp get`; `mcp list --json` is the only machine-readable view.
|
|
405
405
|
function probeGrok() {
|
|
406
|
-
const args = [
|
|
407
|
-
const probe = runProbe(
|
|
408
|
-
if (probe.outcome ===
|
|
406
|
+
const args = ['mcp', 'list', '--json'];
|
|
407
|
+
const probe = runProbe('grok', args);
|
|
408
|
+
if (probe.outcome === 'unavailable') {
|
|
409
409
|
return { registered: Registered.Unknown, authMode: AuthMode.Unknown, reason: probe.reason };
|
|
410
410
|
}
|
|
411
411
|
if (probe.exitCode !== 0) {
|
|
@@ -413,13 +413,13 @@ function probeGrok() {
|
|
|
413
413
|
return {
|
|
414
414
|
registered: Registered.Unknown,
|
|
415
415
|
authMode: AuthMode.Unknown,
|
|
416
|
-
reason:
|
|
416
|
+
reason: 'this Grok build has no `mcp list --json`',
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
419
|
return {
|
|
420
420
|
registered: Registered.Unknown,
|
|
421
421
|
authMode: AuthMode.Unknown,
|
|
422
|
-
reason: probeReason(
|
|
422
|
+
reason: probeReason('grok', args, probe),
|
|
423
423
|
};
|
|
424
424
|
}
|
|
425
425
|
let entries;
|
|
@@ -430,10 +430,10 @@ function probeGrok() {
|
|
|
430
430
|
return {
|
|
431
431
|
registered: Registered.Unknown,
|
|
432
432
|
authMode: AuthMode.Unknown,
|
|
433
|
-
reason:
|
|
433
|
+
reason: '`grok mcp list --json` did not return the expected shape',
|
|
434
434
|
};
|
|
435
435
|
}
|
|
436
|
-
const entry = entries.find((server) => server.name ===
|
|
436
|
+
const entry = entries.find((server) => server.name === 'tinyfish');
|
|
437
437
|
if (!entry)
|
|
438
438
|
return fromGrokPlugin() ?? { registered: Registered.No, authMode: AuthMode.Unknown };
|
|
439
439
|
return {
|
|
@@ -443,7 +443,7 @@ function probeGrok() {
|
|
|
443
443
|
};
|
|
444
444
|
}
|
|
445
445
|
const PROBES = {
|
|
446
|
-
|
|
446
|
+
'claude-code': () => fromMcpGet('claude'),
|
|
447
447
|
codex: probeCodex,
|
|
448
448
|
cursor: probeCursor,
|
|
449
449
|
grok: probeGrok,
|
|
@@ -474,14 +474,14 @@ export function detectRegistrations(harnesses) {
|
|
|
474
474
|
catch (e) {
|
|
475
475
|
// An arbitrary throw is the one message we cannot author, so it never ships. Full
|
|
476
476
|
// fidelity stays on stderr behind --debug, which is not the pasted artifact.
|
|
477
|
-
if (process.env[
|
|
477
|
+
if (process.env['TINYFISH_DEBUG']) {
|
|
478
478
|
errLine(`probe ${detection.harness} threw: ${e instanceof Error ? e.message : String(e)}`);
|
|
479
479
|
}
|
|
480
480
|
return {
|
|
481
481
|
...base,
|
|
482
482
|
registered: Registered.Unknown,
|
|
483
483
|
authMode: AuthMode.Unknown,
|
|
484
|
-
reason:
|
|
484
|
+
reason: 'the probe failed unexpectedly',
|
|
485
485
|
};
|
|
486
486
|
}
|
|
487
487
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { z } from
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
/**
|
|
3
3
|
* `TINYFISH_NO_TELEMETRY` suppresses ALL CLI-emitted network analytics
|
|
4
4
|
* (setup_completed, connect-event, update check). Any value except `0`/`false`
|
|
@@ -68,10 +68,10 @@ export declare const setupCompletedPayloadSchema: z.ZodObject<{
|
|
|
68
68
|
}, z.core.$strip>;
|
|
69
69
|
export type SetupCompletedPayload = z.infer<typeof setupCompletedPayloadSchema>;
|
|
70
70
|
export type HarnessResult = z.infer<typeof harnessResultSchema>;
|
|
71
|
-
export type SetupMode = NonNullable<SetupCompletedPayload[
|
|
72
|
-
export type UpgradeOutcome =
|
|
71
|
+
export type SetupMode = NonNullable<SetupCompletedPayload['mode']>;
|
|
72
|
+
export type UpgradeOutcome = 'updated' | 'failed' | 'interrupted';
|
|
73
73
|
/** Step selection; "updated" means every selected step ran. */
|
|
74
|
-
export type UpgradeScope =
|
|
74
|
+
export type UpgradeScope = 'both' | 'cli' | 'skill';
|
|
75
75
|
/** `toVersion` is null when the run was interrupted or the installed version could not be read. */
|
|
76
76
|
export declare function sendUpgradeCompleted(fromVersion: string, toVersion: string | null, outcome: UpgradeOutcome, scope: UpgradeScope, apiKey?: string): Promise<void>;
|
|
77
77
|
export declare function sendSetupCompleted(mcpUrl: string, harnesses: HarnessResult[], apiKey?: string, mode?: SetupMode): Promise<void>;
|