@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.
Files changed (77) hide show
  1. package/dist/commands/auth.d.ts +1 -1
  2. package/dist/commands/auth.js +52 -52
  3. package/dist/commands/batch.d.ts +1 -1
  4. package/dist/commands/batch.js +54 -46
  5. package/dist/commands/browser.d.ts +1 -1
  6. package/dist/commands/browser.js +14 -14
  7. package/dist/commands/config-claude.d.ts +1 -1
  8. package/dist/commands/config-claude.js +22 -22
  9. package/dist/commands/connect.d.ts +3 -3
  10. package/dist/commands/connect.js +150 -150
  11. package/dist/commands/doctor.d.ts +4 -4
  12. package/dist/commands/doctor.js +104 -104
  13. package/dist/commands/fetch.d.ts +1 -1
  14. package/dist/commands/fetch.js +24 -27
  15. package/dist/commands/onboard.d.ts +1 -1
  16. package/dist/commands/onboard.js +50 -50
  17. package/dist/commands/profile.d.ts +3 -3
  18. package/dist/commands/profile.js +45 -45
  19. package/dist/commands/run.d.ts +1 -1
  20. package/dist/commands/run.js +78 -65
  21. package/dist/commands/runs.d.ts +1 -1
  22. package/dist/commands/runs.js +63 -50
  23. package/dist/commands/search.d.ts +1 -1
  24. package/dist/commands/search.js +16 -16
  25. package/dist/commands/upgrade.d.ts +2 -2
  26. package/dist/commands/upgrade.js +36 -36
  27. package/dist/commands/vault.d.ts +1 -1
  28. package/dist/commands/vault.js +48 -45
  29. package/dist/commands/wallet.d.ts +1 -1
  30. package/dist/commands/wallet.js +14 -14
  31. package/dist/index.js +2 -2
  32. package/dist/lib/auth.d.ts +5 -5
  33. package/dist/lib/auth.js +56 -56
  34. package/dist/lib/batch-store.d.ts +1 -1
  35. package/dist/lib/batch-store.js +4 -4
  36. package/dist/lib/chrome-cookies.d.ts +1 -1
  37. package/dist/lib/chrome-cookies.js +24 -24
  38. package/dist/lib/claude-config.js +37 -37
  39. package/dist/lib/cli-install.js +12 -12
  40. package/dist/lib/client.d.ts +2 -2
  41. package/dist/lib/client.js +61 -61
  42. package/dist/lib/connect-all.js +140 -134
  43. package/dist/lib/connect-auth.d.ts +2 -2
  44. package/dist/lib/connect-auth.js +15 -15
  45. package/dist/lib/connect-clients.d.ts +6 -6
  46. package/dist/lib/connect-clients.js +143 -143
  47. package/dist/lib/connect-fallback.d.ts +1 -1
  48. package/dist/lib/connect-fallback.js +38 -38
  49. package/dist/lib/connect-picker.d.ts +4 -4
  50. package/dist/lib/connect-picker.js +28 -28
  51. package/dist/lib/connect-runtime.d.ts +7 -7
  52. package/dist/lib/connect-runtime.js +61 -61
  53. package/dist/lib/constants.js +13 -13
  54. package/dist/lib/cursor-config.d.ts +1 -1
  55. package/dist/lib/cursor-config.js +30 -30
  56. package/dist/lib/doctor-report.d.ts +2 -2
  57. package/dist/lib/doctor-report.js +22 -22
  58. package/dist/lib/harness-detect.js +39 -39
  59. package/dist/lib/harness.js +9 -9
  60. package/dist/lib/hermes-env.js +26 -23
  61. package/dist/lib/install-root.js +12 -12
  62. package/dist/lib/notice.d.ts +1 -1
  63. package/dist/lib/notice.js +12 -12
  64. package/dist/lib/output.js +14 -14
  65. package/dist/lib/registration-detect.d.ts +3 -3
  66. package/dist/lib/registration-detect.js +82 -82
  67. package/dist/lib/setup-telemetry.d.ts +4 -4
  68. package/dist/lib/setup-telemetry.js +34 -34
  69. package/dist/lib/signals.js +1 -1
  70. package/dist/lib/skill-install.d.ts +3 -3
  71. package/dist/lib/skill-install.js +40 -40
  72. package/dist/lib/types.d.ts +8 -8
  73. package/dist/lib/verify.d.ts +1 -1
  74. package/dist/lib/verify.js +28 -28
  75. package/dist/program.d.ts +1 -1
  76. package/dist/program.js +28 -28
  77. package/package.json +18 -9
@@ -1,35 +1,35 @@
1
- import { createRequire } from "module";
1
+ import { createRequire } from 'module';
2
2
  const _require = createRequire(import.meta.url);
3
- const _pkg = _require("../../package.json");
3
+ const _pkg = _require('../../package.json');
4
4
  export const CLI_VERSION = _pkg.version;
5
5
  /** Unversioned: the install site appends `@latest`, the version read must not. */
6
- export const TINYFISH_CLI_PACKAGE = "@tiny-fish/cli";
6
+ export const TINYFISH_CLI_PACKAGE = '@tiny-fish/cli';
7
7
  // How we identify ourselves to other tools; `skills` echoes it back.
8
- export const CLI_AGENT_IDENTITY = "tinyfish-cli";
8
+ export const CLI_AGENT_IDENTITY = 'tinyfish-cli';
9
9
  /** The descriptor hands this name to harnesses; the probes resolve it. */
10
- export const TINYFISH_API_KEY_VAR = "TINYFISH_API_KEY";
10
+ export const TINYFISH_API_KEY_VAR = 'TINYFISH_API_KEY';
11
11
  /** Base URL for the TinyFish API. Override with TINYFISH_API_URL for staging/self-hosted. */
12
- const apiUrlOverride = process.env["TINYFISH_API_URL"]?.trim() || undefined;
12
+ const apiUrlOverride = process.env['TINYFISH_API_URL']?.trim() || undefined;
13
13
  if (apiUrlOverride) {
14
14
  let parsed;
15
15
  try {
16
16
  parsed = new URL(apiUrlOverride);
17
17
  }
18
18
  catch {
19
- process.stderr.write("Error: TINYFISH_API_URL must be a valid https URL\n");
19
+ process.stderr.write('Error: TINYFISH_API_URL must be a valid https URL\n');
20
20
  process.exit(1);
21
21
  }
22
- const loopbackHttp = parsed.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
23
- if (parsed.protocol !== "https:" && !loopbackHttp) {
24
- process.stderr.write(`Error: TINYFISH_API_URL must use https:// (got protocol "${parsed.protocol || "unknown"}")\n`);
22
+ const loopbackHttp = parsed.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(parsed.hostname);
23
+ if (parsed.protocol !== 'https:' && !loopbackHttp) {
24
+ process.stderr.write(`Error: TINYFISH_API_URL must use https:// (got protocol "${parsed.protocol || 'unknown'}")\n`);
25
25
  process.exit(1);
26
26
  }
27
27
  }
28
- export const BASE_URL = apiUrlOverride ?? "https://agent.tinyfish.ai";
28
+ export const BASE_URL = apiUrlOverride ?? 'https://agent.tinyfish.ai';
29
29
  // Left undefined, the SDK sends Search and Fetch to their own hosts, which are the ones
30
30
  // behind the API gateway.
31
31
  export const API_URL_OVERRIDE = apiUrlOverride;
32
- const DEFAULT_DASHBOARD_SOURCE = "cli";
32
+ const DEFAULT_DASHBOARD_SOURCE = 'cli';
33
33
  const DASHBOARD_SOURCE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
34
34
  function normalizeDashboardSource(source) {
35
35
  const normalized = source?.trim().toLowerCase();
@@ -41,7 +41,7 @@ function normalizeDashboardSource(source) {
41
41
  /** URL where users can create and manage API keys */
42
42
  export function getDashboardUrl(source = DEFAULT_DASHBOARD_SOURCE) {
43
43
  const url = new URL(`${BASE_URL}/api-keys`);
44
- url.searchParams.set("source", normalizeDashboardSource(source));
44
+ url.searchParams.set('source', normalizeDashboardSource(source));
45
45
  return url.toString();
46
46
  }
47
47
  export const DASHBOARD_URL = getDashboardUrl();
@@ -1,7 +1,7 @@
1
1
  export declare function cursorConfigDir(): string;
2
2
  export declare function cursorMcpPath(): string;
3
3
  export interface CursorMcpWriteResult {
4
- status: "written" | "unchanged" | "corrupt_skip";
4
+ status: 'written' | 'unchanged' | 'corrupt_skip';
5
5
  backupPath?: string;
6
6
  error?: string;
7
7
  /** A pre-existing tinyfish entry was replaced — a repair, not a fresh install. */
@@ -1,29 +1,29 @@
1
- import * as fs from "fs";
2
- import * as os from "os";
3
- import * as path from "path";
4
- import { matchesCliKey } from "./auth.js";
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { matchesCliKey } from './auth.js';
5
5
  // The one config file the CLI writes directly — hence backup/merge rigor.
6
- const TINYFISH_SERVER_KEY = "tinyfish";
6
+ const TINYFISH_SERVER_KEY = 'tinyfish';
7
7
  export function cursorConfigDir() {
8
- return path.join(os.homedir(), ".cursor");
8
+ return path.join(os.homedir(), '.cursor');
9
9
  }
10
10
  export function cursorMcpPath() {
11
- return path.join(cursorConfigDir(), "mcp.json");
11
+ return path.join(cursorConfigDir(), 'mcp.json');
12
12
  }
13
13
  function isPlainRecord(value) {
14
- return value !== null && typeof value === "object" && !Array.isArray(value);
14
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
15
15
  }
16
16
  // Footgun: the key lands plaintext in mcp.json (0600) — per-device keys are the GA hardening.
17
17
  function buildTinyfishServerEntry(mcpUrl, apiKey) {
18
- return apiKey ? { url: mcpUrl, headers: { "X-API-Key": apiKey } } : { url: mcpUrl };
18
+ return apiKey ? { url: mcpUrl, headers: { 'X-API-Key': apiKey } } : { url: mcpUrl };
19
19
  }
20
20
  function parseMcpJson(raw) {
21
21
  try {
22
22
  const parsed = JSON.parse(raw);
23
23
  if (!isPlainRecord(parsed))
24
- return { ok: false, error: "mcp.json root is not a JSON object" };
25
- if ("mcpServers" in parsed && !isPlainRecord(parsed.mcpServers)) {
26
- return { ok: false, error: "mcp.json mcpServers is not a JSON object" };
24
+ return { ok: false, error: 'mcp.json root is not a JSON object' };
25
+ if ('mcpServers' in parsed && !isPlainRecord(parsed.mcpServers)) {
26
+ return { ok: false, error: 'mcp.json mcpServers is not a JSON object' };
27
27
  }
28
28
  return { ok: true, value: parsed };
29
29
  }
@@ -35,10 +35,10 @@ function parseMcpJson(raw) {
35
35
  function readExisting() {
36
36
  let raw;
37
37
  try {
38
- raw = fs.readFileSync(cursorMcpPath(), "utf8");
38
+ raw = fs.readFileSync(cursorMcpPath(), 'utf8');
39
39
  }
40
40
  catch (e) {
41
- if (e?.code === "ENOENT")
41
+ if (e?.code === 'ENOENT')
42
42
  return { parsed: {} };
43
43
  return { error: e instanceof Error ? e.message : String(e) };
44
44
  }
@@ -51,12 +51,12 @@ function readExisting() {
51
51
  export function planCursorWrite(mcpUrl, apiKey) {
52
52
  const existing = readExisting();
53
53
  const filePath = cursorMcpPath();
54
- if ("error" in existing) {
54
+ if ('error' in existing) {
55
55
  return `${filePath}: existing file is corrupt (${existing.error}) — would skip and leave it untouched`;
56
56
  }
57
57
  const authNote = apiKey
58
- ? " with an API-key header — the key is stored in plaintext in that file (value not shown here)"
59
- : "";
58
+ ? ' with an API-key header — the key is stored in plaintext in that file (value not shown here)'
59
+ : '';
60
60
  const servers = existing.parsed.mcpServers;
61
61
  const current = isPlainRecord(servers) ? servers[TINYFISH_SERVER_KEY] : undefined;
62
62
  if (JSON.stringify(current) === JSON.stringify(buildTinyfishServerEntry(mcpUrl, apiKey))) {
@@ -69,7 +69,7 @@ export function planCursorWrite(mcpUrl, apiKey) {
69
69
  /** Reports the header's shape, never its value. */
70
70
  export function readCursorTinyfishEntry() {
71
71
  const existing = readExisting();
72
- if ("error" in existing)
72
+ if ('error' in existing)
73
73
  return { present: false, hasApiKeyHeader: false, error: existing.error };
74
74
  const servers = existing.parsed.mcpServers;
75
75
  const entry = isPlainRecord(servers) ? servers[TINYFISH_SERVER_KEY] : undefined;
@@ -79,13 +79,13 @@ export function readCursorTinyfishEntry() {
79
79
  // Header names are case-insensitive, and this file is hand-editable — matching only the
80
80
  // casing we write would understate auth mode for a user who typed it differently.
81
81
  const keyHeader = isPlainRecord(headers)
82
- ? Object.entries(headers).find((entry) => entry[0].toLowerCase() === "x-api-key" && typeof entry[1] === "string")
82
+ ? Object.entries(headers).find((entry) => entry[0].toLowerCase() === 'x-api-key' && typeof entry[1] === 'string')
83
83
  : undefined;
84
84
  return {
85
85
  present: true,
86
86
  hasApiKeyHeader: keyHeader !== undefined,
87
87
  ...(matchesCliKey(keyHeader?.[1]) ? { keyMatchesCliKey: true } : {}),
88
- ...(typeof entry.url === "string" ? { url: entry.url } : {}),
88
+ ...(typeof entry.url === 'string' ? { url: entry.url } : {}),
89
89
  };
90
90
  }
91
91
  // Backup, then atomic temp+rename; pid avoids same-millisecond backup collisions.
@@ -98,7 +98,7 @@ function commitServers(filePath, existing, servers) {
98
98
  }
99
99
  const next = { ...existing.parsed, mcpServers: servers };
100
100
  const tmpPath = `${filePath}.tmp-${process.pid}`;
101
- fs.writeFileSync(tmpPath, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
101
+ fs.writeFileSync(tmpPath, JSON.stringify(next, null, 2) + '\n', { mode: 0o600 });
102
102
  try {
103
103
  fs.renameSync(tmpPath, filePath);
104
104
  }
@@ -106,20 +106,20 @@ function commitServers(filePath, existing, servers) {
106
106
  fs.rmSync(tmpPath, { force: true });
107
107
  throw e;
108
108
  }
109
- return { status: "written", backupPath };
109
+ return { status: 'written', backupPath };
110
110
  }
111
111
  /** Merges only the `tinyfish` key; skips unreadable/corrupt files rather than clobber. */
112
112
  export function writeCursorMcpConfig(mcpUrl, apiKey) {
113
113
  const filePath = cursorMcpPath();
114
114
  const existing = readExisting();
115
- if ("error" in existing)
116
- return { status: "corrupt_skip", error: existing.error };
115
+ if ('error' in existing)
116
+ return { status: 'corrupt_skip', error: existing.error };
117
117
  const servers = isPlainRecord(existing.parsed.mcpServers)
118
118
  ? { ...existing.parsed.mcpServers }
119
119
  : {};
120
120
  const nextEntry = buildTinyfishServerEntry(mcpUrl, apiKey);
121
121
  if (JSON.stringify(servers[TINYFISH_SERVER_KEY]) === JSON.stringify(nextEntry)) {
122
- return { status: "unchanged" };
122
+ return { status: 'unchanged' };
123
123
  }
124
124
  const repaired = TINYFISH_SERVER_KEY in servers;
125
125
  servers[TINYFISH_SERVER_KEY] = nextEntry;
@@ -127,22 +127,22 @@ export function writeCursorMcpConfig(mcpUrl, apiKey) {
127
127
  }
128
128
  /** Cursor deeplink format: cursor://anysphere.cursor-deeplink/mcp/install?name=$NAME&config=$BASE64 */
129
129
  export function cursorInstallDeeplink(mcpUrl) {
130
- const config = Buffer.from(JSON.stringify(buildTinyfishServerEntry(mcpUrl))).toString("base64");
130
+ const config = Buffer.from(JSON.stringify(buildTinyfishServerEntry(mcpUrl))).toString('base64');
131
131
  return `cursor://anysphere.cursor-deeplink/mcp/install?name=${TINYFISH_SERVER_KEY}&config=${encodeURIComponent(config)}`;
132
132
  }
133
133
  /** Removes only the `tinyfish` key. */
134
134
  export function removeCursorMcpServer() {
135
135
  const filePath = cursorMcpPath();
136
136
  const existing = readExisting();
137
- if ("error" in existing)
138
- return { status: "corrupt_skip", error: existing.error };
137
+ if ('error' in existing)
138
+ return { status: 'corrupt_skip', error: existing.error };
139
139
  if (existing.raw === undefined)
140
- return { status: "unchanged" };
140
+ return { status: 'unchanged' };
141
141
  const servers = isPlainRecord(existing.parsed.mcpServers)
142
142
  ? { ...existing.parsed.mcpServers }
143
143
  : {};
144
144
  if (!(TINYFISH_SERVER_KEY in servers))
145
- return { status: "unchanged" };
145
+ return { status: 'unchanged' };
146
146
  delete servers[TINYFISH_SERVER_KEY];
147
147
  return commitServers(filePath, existing, servers);
148
148
  }
@@ -1,5 +1,5 @@
1
- import { z } from "zod";
2
- import { AuthMode, Registered } from "./harness-detect.js";
1
+ import { z } from 'zod';
2
+ import { AuthMode, Registered } from './harness-detect.js';
3
3
  /** Bumped whenever a consumer could misread the payload; the cookbook skill releases separately. */
4
4
  export declare const DOCTOR_SCHEMA_VERSION = 3;
5
5
  declare const checkStatusSchema: z.ZodEnum<{
@@ -1,11 +1,11 @@
1
- import { z } from "zod";
2
- import { ALL_HARNESSES, AuthMode, Registered } from "./harness-detect.js";
1
+ import { z } from 'zod';
2
+ import { ALL_HARNESSES, AuthMode, Registered } from './harness-detect.js';
3
3
  /** Bumped whenever a consumer could misread the payload; the cookbook skill releases separately. */
4
4
  export const DOCTOR_SCHEMA_VERSION = 3;
5
- const checkStatusSchema = z.enum(["pass", "fail", "warn", "skip"]);
5
+ const checkStatusSchema = z.enum(['pass', 'fail', 'warn', 'skip']);
6
6
  const harnessSchema = z.enum(ALL_HARNESSES);
7
7
  // Not derived from `harness`: connectivity is harness-null yet harness-scoped.
8
- const checkScopeSchema = z.enum(["harness", "cli", "info"]);
8
+ const checkScopeSchema = z.enum(['harness', 'cli', 'info']);
9
9
  const doctorCheckSchema = z.object({
10
10
  id: z.string(),
11
11
  title: z.string(),
@@ -20,20 +20,20 @@ const doctorHarnessSchema = z.object({
20
20
  registered: z.enum(Registered),
21
21
  auth_mode: z.enum(AuthMode),
22
22
  // Additive under schema 3: what connect wrote when the harness cannot echo it back.
23
- recorded_auth_mode: z.enum(["api-key", "oauth", "deferred"]).optional(),
23
+ recorded_auth_mode: z.enum(['api-key', 'oauth', 'deferred']).optional(),
24
24
  proves_harness_reach: z.boolean(),
25
25
  });
26
26
  // `action` is what `--fix` dispatches on, not the harness field: keying off a null harness
27
27
  // conflated "run auth login" with a non-executable repair and the credential fix never ran.
28
28
  const doctorRepairSchema = z.object({
29
29
  for: z.string(),
30
- action: z.enum(["connect", "auth-login"]),
30
+ action: z.enum(['connect', 'auth-login']),
31
31
  harness: harnessSchema.nullable(),
32
32
  command: z.string(),
33
33
  unattended_safe: z.boolean(),
34
34
  });
35
35
  function hasFailure(checks, scope) {
36
- return checks.some((check) => check.scope === scope && check.status === "fail");
36
+ return checks.some((check) => check.scope === scope && check.status === 'fail');
37
37
  }
38
38
  // One `ok` could not answer both questions, and answered neither.
39
39
  export const doctorReportSchema = z
@@ -48,20 +48,20 @@ export const doctorReportSchema = z
48
48
  repairs: z.array(doctorRepairSchema),
49
49
  })
50
50
  // Claiming ok while that scope failed is worse than no report.
51
- .refine((report) => !report.ok_harnesses || !hasFailure(report.checks, "harness"), {
52
- message: "ok_harnesses cannot be true while a harness check has failed",
53
- path: ["ok_harnesses"],
51
+ .refine((report) => !report.ok_harnesses || !hasFailure(report.checks, 'harness'), {
52
+ message: 'ok_harnesses cannot be true while a harness check has failed',
53
+ path: ['ok_harnesses'],
54
54
  })
55
- .refine((report) => !report.ok_cli || !hasFailure(report.checks, "cli"), {
56
- message: "ok_cli cannot be true while a CLI check has failed",
57
- path: ["ok_cli"],
55
+ .refine((report) => !report.ok_cli || !hasFailure(report.checks, 'cli'), {
56
+ message: 'ok_cli cannot be true while a CLI check has failed',
57
+ path: ['ok_cli'],
58
58
  });
59
- const GLYPHS = { pass: "✓", fail: "✗", warn: "⚠", skip: "…" };
59
+ const GLYPHS = { pass: '✓', fail: '✗', warn: '⚠', skip: '…' };
60
60
  /** Doctor itself broke, which is a different claim from "your setup is broken". */
61
61
  export const DOCTOR_COULD_NOT_RUN = 2;
62
62
  /** Skips never fail the run; a skip means not applicable, not broken. */
63
63
  export function verdictFor(checks) {
64
- return { ok_harnesses: !hasFailure(checks, "harness"), ok_cli: !hasFailure(checks, "cli") };
64
+ return { ok_harnesses: !hasFailure(checks, 'harness'), ok_cli: !hasFailure(checks, 'cli') };
65
65
  }
66
66
  /** Harness scope only: the caller asked about its agent, not us. */
67
67
  export function exitCodeFor(checks) {
@@ -71,25 +71,25 @@ export function exitCodeFor(checks) {
71
71
  function footnotes(report) {
72
72
  const notes = [];
73
73
  // Alongside exit 1 this is noise; something did fail.
74
- if (hasFailure(report.checks, "cli") && report.ok_harnesses) {
74
+ if (hasFailure(report.checks, 'cli') && report.ok_harnesses) {
75
75
  notes.push("The CLI's own credential is broken; that does not affect the exit code.");
76
76
  }
77
77
  // Per-harness rows, not the scope: connectivity is harness-scoped and never skips.
78
78
  const harnessChecks = report.checks.filter((c) => c.harness !== null);
79
- if (harnessChecks.length > 0 && harnessChecks.every((c) => c.status === "skip")) {
80
- notes.push("No harness was checked here, so a green harness verdict proves nothing.");
79
+ if (harnessChecks.length > 0 && harnessChecks.every((c) => c.status === 'skip')) {
80
+ notes.push('No harness was checked here, so a green harness verdict proves nothing.');
81
81
  }
82
82
  return notes;
83
83
  }
84
84
  export function renderPretty(report) {
85
- const lines = report.checks.map((c) => `${GLYPHS[c.status]} ${c.title}${c.detail ? ` — ${c.detail}` : ""}`);
85
+ const lines = report.checks.map((c) => `${GLYPHS[c.status]} ${c.title}${c.detail ? ` — ${c.detail}` : ''}`);
86
86
  if (report.repairs.length > 0) {
87
- lines.push("", "Repairs:");
87
+ lines.push('', 'Repairs:');
88
88
  for (const repair of report.repairs)
89
89
  lines.push(` ${repair.command}`);
90
90
  }
91
91
  const notes = footnotes(report);
92
92
  if (notes.length > 0)
93
- lines.push("", ...notes);
94
- return lines.join("\n");
93
+ lines.push('', ...notes);
94
+ return lines.join('\n');
95
95
  }
@@ -1,35 +1,35 @@
1
- import * as fs from "fs";
2
- import * as os from "os";
3
- import * as path from "path";
4
- import which from "which";
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import which from 'which';
5
5
  export const ALL_HARNESSES = [
6
- "claude-code",
7
- "codex",
8
- "cursor",
9
- "grok",
10
- "hermes",
11
- "openclaw",
12
- "opencode",
6
+ 'claude-code',
7
+ 'codex',
8
+ 'cursor',
9
+ 'grok',
10
+ 'hermes',
11
+ 'openclaw',
12
+ 'opencode',
13
13
  ];
14
14
  export const HARNESS_DISPLAY_NAMES = {
15
- "claude-code": "Claude Code",
16
- codex: "Codex",
17
- cursor: "Cursor",
18
- grok: "Grok",
19
- hermes: "Hermes",
20
- openclaw: "OpenClaw",
21
- opencode: "OpenCode",
15
+ 'claude-code': 'Claude Code',
16
+ codex: 'Codex',
17
+ cursor: 'Cursor',
18
+ grok: 'Grok',
19
+ hermes: 'Hermes',
20
+ openclaw: 'OpenClaw',
21
+ opencode: 'OpenCode',
22
22
  };
23
23
  /** MCP config is read at startup, so a running harness needs this to see TinyFish. */
24
24
  export const RELOAD_ACTION = {
25
- "claude-code": "restart it",
26
- codex: "restart it",
25
+ 'claude-code': 'restart it',
26
+ codex: 'restart it',
27
27
  // docs.x.ai/build/features/mcp-servers: `r` in /mcps rereads the config.
28
- grok: "run /mcps and press r",
29
- hermes: "restart it",
30
- openclaw: "restart it",
31
- opencode: "restart it",
32
- cursor: "reload the window",
28
+ grok: 'run /mcps and press r',
29
+ hermes: 'restart it',
30
+ openclaw: 'restart it',
31
+ opencode: 'restart it',
32
+ cursor: 'reload the window',
33
33
  };
34
34
  // Values are the wire contract; the report schema derives from these.
35
35
  export var Registered;
@@ -45,24 +45,24 @@ export var AuthMode;
45
45
  AuthMode["Unknown"] = "unknown";
46
46
  })(AuthMode || (AuthMode = {}));
47
47
  const CONFIG_DIRS = {
48
- "claude-code": ".claude",
49
- codex: ".codex",
50
- cursor: ".cursor",
51
- grok: ".grok",
52
- hermes: ".hermes",
53
- openclaw: ".openclaw",
54
- opencode: ".config/opencode",
48
+ 'claude-code': '.claude',
49
+ codex: '.codex',
50
+ cursor: '.cursor',
51
+ grok: '.grok',
52
+ hermes: '.hermes',
53
+ openclaw: '.openclaw',
54
+ opencode: '.config/opencode',
55
55
  };
56
56
  // Harness id is not the binary name (cursor-agent).
57
57
  // TODO(PF-3668): a same-named non-harness binary still reads as installed.
58
58
  export const HARNESS_COMMANDS = {
59
- "claude-code": "claude",
60
- codex: "codex",
61
- cursor: "cursor-agent",
62
- grok: "grok",
63
- hermes: "hermes",
64
- openclaw: "openclaw",
65
- opencode: "opencode",
59
+ 'claude-code': 'claude',
60
+ codex: 'codex',
61
+ cursor: 'cursor-agent',
62
+ grok: 'grok',
63
+ hermes: 'hermes',
64
+ openclaw: 'openclaw',
65
+ opencode: 'opencode',
66
66
  };
67
67
  // cross-spawn resolves the same way, so detection cannot disagree with the spawn.
68
68
  export function commandOnPath(command) {
@@ -12,22 +12,22 @@ const hasVarWithPrefix = (env, prefix) => Object.keys(env).some((key) => key.sta
12
12
  // frontend/app/api/cli/connect-event/route.ts, or connect telemetry buckets it as `other`.
13
13
  // First match wins. Base agents (claude-code) go last so embedding wrappers win.
14
14
  const HARNESS_FINGERPRINTS = [
15
- { name: "openclaw", matches: (env) => hasVarWithPrefix(env, "OPENCLAW_MCP_") },
16
- { name: "grok", matches: (env) => env["GROK_AGENT"] === "1" },
15
+ { name: 'openclaw', matches: (env) => hasVarWithPrefix(env, 'OPENCLAW_MCP_') },
16
+ { name: 'grok', matches: (env) => env['GROK_AGENT'] === '1' },
17
17
  // Cursor's VS Code host sets CURSOR_* broadly; CURSOR_AGENT=1 flags the
18
18
  // agent specifically, so gate on it rather than the prefix.
19
- { name: "cursor", matches: (env) => env["CURSOR_AGENT"] === "1" },
19
+ { name: 'cursor', matches: (env) => env['CURSOR_AGENT'] === '1' },
20
20
  // Antigravity is Google's successor to the (deprecated) gemini-cli.
21
- { name: "antigravity", matches: (env) => env["ANTIGRAVITY_AGENT"] === "1" },
21
+ { name: 'antigravity', matches: (env) => env['ANTIGRAVITY_AGENT'] === '1' },
22
22
  // CODEX_CI seen in probe; CODEX_SANDBOX* appear under seatbelt. Match the
23
23
  // prefix so we catch a Codex run regardless of which of its vars are set.
24
- { name: "codex", matches: (env) => hasVarWithPrefix(env, "CODEX_") },
25
- { name: "hermes", matches: (env) => hasVarWithPrefix(env, "HERMES_") },
24
+ { name: 'codex', matches: (env) => hasVarWithPrefix(env, 'CODEX_') },
25
+ { name: 'hermes', matches: (env) => hasVarWithPrefix(env, 'HERMES_') },
26
26
  // Exact key, not a prefix: opencode writes OPENCODE=1 at startup, reads every OPENCODE_* as config.
27
- { name: "opencode", matches: (env) => Boolean(env["OPENCODE"]) },
27
+ { name: 'opencode', matches: (env) => Boolean(env['OPENCODE']) },
28
28
  {
29
- name: "claude-code",
30
- matches: (env) => env["CLAUDECODE"] === "1" || Boolean(env["CLAUDE_CODE_ENTRYPOINT"]),
29
+ name: 'claude-code',
30
+ matches: (env) => env['CLAUDECODE'] === '1' || Boolean(env['CLAUDE_CODE_ENTRYPOINT']),
31
31
  },
32
32
  ];
33
33
  /** Returns the detected harness name, or null when none match. */
@@ -1,37 +1,37 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import fs from "node:fs";
4
- import { randomUUID } from "node:crypto";
5
- import spawn from "cross-spawn";
6
- import { HARNESS_PROBE_TIMEOUT_MS } from "./constants.js";
7
- import { sanitizeLine } from "./output.js";
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { randomUUID } from 'node:crypto';
5
+ import spawn from 'cross-spawn';
6
+ import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
7
+ import { sanitizeLine } from './output.js';
8
8
  // display_hermes_home() abbreviates under $HOME, so the value is not always absolute.
9
9
  const HERMES_HOME_LINE = /^\s*hermes_home:\s*(\S.*?)\s*$/m;
10
10
  // One value: the next cold-start bump must move both probes together.
11
11
  export const HERMES_PROBE_TIMEOUT_MS = HARNESS_PROBE_TIMEOUT_MS;
12
12
  /** Asking Hermes beats reimplementing its profile override, which we'd drift from. */
13
13
  export function resolveHermesHome() {
14
- const result = spawn.sync("hermes", ["dump"], {
15
- encoding: "utf8",
16
- env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
14
+ const result = spawn.sync('hermes', ['dump'], {
15
+ encoding: 'utf8',
16
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
17
17
  timeout: HERMES_PROBE_TIMEOUT_MS,
18
18
  });
19
19
  if (result.error || result.status !== 0)
20
20
  return undefined;
21
- const raw = HERMES_HOME_LINE.exec(sanitizeLine(`${result.stdout ?? ""}\n${result.stderr ?? ""}`))?.[1];
21
+ const raw = HERMES_HOME_LINE.exec(sanitizeLine(`${result.stdout ?? ''}\n${result.stderr ?? ''}`))?.[1];
22
22
  if (!raw)
23
23
  return undefined;
24
- const expanded = raw === "~" || raw.startsWith("~/") ? path.join(os.homedir(), raw.slice(2)) : raw;
24
+ const expanded = raw === '~' || raw.startsWith('~/') ? path.join(os.homedir(), raw.slice(2)) : raw;
25
25
  return path.isAbsolute(expanded) ? expanded : undefined;
26
26
  }
27
- export const HERMES_KEY_VAR = "MCP_TINYFISH_API_KEY";
27
+ export const HERMES_KEY_VAR = 'MCP_TINYFISH_API_KEY';
28
28
  const MARKER_HEAD = `# tinyfish-cli: TinyFish MCP key, managed by \`tinyfish connect hermes\``;
29
- const MARKER_TAIL = "# tinyfish-cli: end";
29
+ const MARKER_TAIL = '# tinyfish-cli: end';
30
30
  // Keys are `sk-tinyfish-<alnum>`; refusing anything else beats shipping unreachable escaping.
31
31
  const BARE_SAFE = /^[A-Za-z0-9._\-/:+=]+$/;
32
32
  /** Mirrors Hermes' _env_line_defines_key: load_env() parses the export form too. */
33
33
  function definesKey(line) {
34
- const stripped = line.trim().replace(/^export\s+/, "");
34
+ const stripped = line.trim().replace(/^export\s+/, '');
35
35
  return stripped.startsWith(`${HERMES_KEY_VAR}=`);
36
36
  }
37
37
  function isMarker(line) {
@@ -41,7 +41,10 @@ function isMarker(line) {
41
41
  function readLines(envPath) {
42
42
  if (!fs.existsSync(envPath))
43
43
  return [];
44
- return fs.readFileSync(envPath, "utf8").replace(/^\uFEFF/, "").split(/(?<=\n)/);
44
+ return fs
45
+ .readFileSync(envPath, 'utf8')
46
+ .replace(/^\uFEFF/, '')
47
+ .split(/(?<=\n)/);
45
48
  }
46
49
  // Hermes preserves the original mode so Docker volume mounts survive (config.py:3921-3929).
47
50
  function writeAtomic(envPath, lines) {
@@ -54,7 +57,7 @@ function writeAtomic(envPath, lines) {
54
57
  }
55
58
  const tmp = `${envPath}.${randomUUID()}.tmp`;
56
59
  try {
57
- fs.writeFileSync(tmp, lines.join(""), { encoding: "utf8", mode });
60
+ fs.writeFileSync(tmp, lines.join(''), { encoding: 'utf8', mode });
58
61
  // open() applies mode through umask; chmod bypasses it, preserving bits.
59
62
  fs.chmodSync(tmp, mode);
60
63
  fs.renameSync(tmp, envPath);
@@ -65,21 +68,21 @@ function writeAtomic(envPath, lines) {
65
68
  }
66
69
  }
67
70
  export function writeHermesKey(home, key) {
68
- const clean = key.replace(/[\r\n]/g, "").trim();
71
+ const clean = key.replace(/[\r\n]/g, '').trim();
69
72
  // An empty key reads as "bad characters" without its own branch.
70
73
  if (!clean)
71
74
  throw new Error("TinyFish API key is empty; nothing to write to Hermes' .env");
72
75
  if (!BARE_SAFE.test(clean)) {
73
- throw new Error("TinyFish API key contains characters that cannot be written to a .env file");
76
+ throw new Error('TinyFish API key contains characters that cannot be written to a .env file');
74
77
  }
75
- const envPath = path.join(home, ".env");
78
+ const envPath = path.join(home, '.env');
76
79
  fs.mkdirSync(home, { recursive: true });
77
80
  const lines = readLines(envPath);
78
81
  const assignment = `${HERMES_KEY_VAR}=${clean}\n`;
79
82
  const index = lines.findIndex(definesKey);
80
83
  if (index < 0) {
81
- if (lines.length && !lines[lines.length - 1].endsWith("\n"))
82
- lines[lines.length - 1] += "\n";
84
+ if (lines.length && !lines[lines.length - 1].endsWith('\n'))
85
+ lines[lines.length - 1] += '\n';
83
86
  lines.push(`${MARKER_HEAD}\n`, assignment, `${MARKER_TAIL}\n`);
84
87
  writeAtomic(envPath, lines);
85
88
  return;
@@ -89,7 +92,7 @@ export function writeHermesKey(home, key) {
89
92
  writeAtomic(envPath, lines.filter((line, i) => i === index || !definesKey(line)));
90
93
  }
91
94
  export function removeHermesKey(home) {
92
- const envPath = path.join(home, ".env");
95
+ const envPath = path.join(home, '.env');
93
96
  const lines = readLines(envPath);
94
97
  if (!lines.length)
95
98
  return false;
@@ -1,7 +1,7 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { TINYFISH_CLI_PACKAGE } from "./constants.js";
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { TINYFISH_CLI_PACKAGE } from './constants.js';
5
5
  function isDirectory(candidate) {
6
6
  try {
7
7
  return fs.statSync(candidate).isDirectory();
@@ -17,30 +17,30 @@ function isDirectory(candidate) {
17
17
  * installing into their trees would report a success the PATH binary never sees.
18
18
  */
19
19
  export function resolveInstallRoot(packageRoot, platform, onDisk = isDirectory) {
20
- const p = platform === "win32" ? path.win32 : path.posix;
20
+ const p = platform === 'win32' ? path.win32 : path.posix;
21
21
  // npx's win32 cache uses the same bare layout as a global install.
22
- if (packageRoot.split(p.sep).includes("_npx"))
22
+ if (packageRoot.split(p.sep).includes('_npx'))
23
23
  return null;
24
- const nodeModules = p.resolve(packageRoot, "..", "..");
25
- if (p.basename(nodeModules) !== "node_modules")
24
+ const nodeModules = p.resolve(packageRoot, '..', '..');
25
+ if (p.basename(nodeModules) !== 'node_modules')
26
26
  return null;
27
27
  const container = p.dirname(nodeModules);
28
28
  // Only win32 puts node_modules directly under the prefix.
29
- if (platform !== "win32" && p.basename(container) !== "lib")
29
+ if (platform !== 'win32' && p.basename(container) !== 'lib')
30
30
  return null;
31
31
  if (!onDisk(nodeModules))
32
32
  return null;
33
- return { prefix: platform === "win32" ? container : p.dirname(container), nodeModules };
33
+ return { prefix: platform === 'win32' ? container : p.dirname(container), nodeModules };
34
34
  }
35
35
  export function installRoot() {
36
36
  // Two levels up from dist/lib/ is the package root, and tsc mirrors src/ into dist/.
37
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
37
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
38
38
  return resolveInstallRoot(packageRoot, process.platform);
39
39
  }
40
40
  /** The version on disk under a prefix, which is the only account of what `@latest` resolved to. */
41
41
  export function readInstalledVersion(nodeModules) {
42
42
  try {
43
- const manifest = fs.readFileSync(path.join(nodeModules, ...TINYFISH_CLI_PACKAGE.split("/"), "package.json"), "utf8");
43
+ const manifest = fs.readFileSync(path.join(nodeModules, ...TINYFISH_CLI_PACKAGE.split('/'), 'package.json'), 'utf8');
44
44
  return JSON.parse(manifest).version ?? null;
45
45
  }
46
46
  catch {