@tiny-fish/cli 0.30.1 → 0.30.2-next.256

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.
@@ -9,3 +9,4 @@ export declare const API_URL_OVERRIDE: string | undefined;
9
9
  /** URL where users can create and manage API keys */
10
10
  export declare function getDashboardUrl(source?: string): string;
11
11
  export declare const DASHBOARD_URL: string;
12
+ export declare const HARNESS_PROBE_TIMEOUT_MS = 15000;
@@ -45,3 +45,4 @@ export function getDashboardUrl(source = DEFAULT_DASHBOARD_SOURCE) {
45
45
  return url.toString();
46
46
  }
47
47
  export const DASHBOARD_URL = getDashboardUrl();
48
+ export const HARNESS_PROBE_TIMEOUT_MS = 15_000;
@@ -0,0 +1,6 @@
1
+ export declare const HERMES_PROBE_TIMEOUT_MS = 15000;
2
+ /** Asking Hermes beats reimplementing its profile override, which we'd drift from. */
3
+ export declare function resolveHermesHome(): string | undefined;
4
+ export declare const HERMES_KEY_VAR = "MCP_TINYFISH_API_KEY";
5
+ export declare function writeHermesKey(home: string, key: string): void;
6
+ export declare function removeHermesKey(home: string): boolean;
@@ -0,0 +1,101 @@
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
+ // display_hermes_home() abbreviates under $HOME, so the value is not always absolute.
9
+ const HERMES_HOME_LINE = /^\s*hermes_home:\s*(\S.*?)\s*$/m;
10
+ // One value: the next cold-start bump must move both probes together.
11
+ export const HERMES_PROBE_TIMEOUT_MS = HARNESS_PROBE_TIMEOUT_MS;
12
+ /** Asking Hermes beats reimplementing its profile override, which we'd drift from. */
13
+ export function resolveHermesHome() {
14
+ const result = spawn.sync("hermes", ["dump"], {
15
+ encoding: "utf8",
16
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
17
+ timeout: HERMES_PROBE_TIMEOUT_MS,
18
+ });
19
+ if (result.error || result.status !== 0)
20
+ return undefined;
21
+ const raw = HERMES_HOME_LINE.exec(sanitizeLine(`${result.stdout ?? ""}\n${result.stderr ?? ""}`))?.[1];
22
+ if (!raw)
23
+ return undefined;
24
+ const expanded = raw === "~" || raw.startsWith("~/") ? path.join(os.homedir(), raw.slice(2)) : raw;
25
+ return path.isAbsolute(expanded) ? expanded : undefined;
26
+ }
27
+ export const HERMES_KEY_VAR = "MCP_TINYFISH_API_KEY";
28
+ const MARKER_HEAD = `# tinyfish-cli: TinyFish MCP key, managed by \`tinyfish connect hermes\``;
29
+ const MARKER_TAIL = "# tinyfish-cli: end";
30
+ // Keys are `sk-tinyfish-<alnum>`; refusing anything else beats shipping unreachable escaping.
31
+ const BARE_SAFE = /^[A-Za-z0-9._\-/:+=]+$/;
32
+ /** Mirrors Hermes' _env_line_defines_key: load_env() parses the export form too. */
33
+ function definesKey(line) {
34
+ const stripped = line.trim().replace(/^export\s+/, "");
35
+ return stripped.startsWith(`${HERMES_KEY_VAR}=`);
36
+ }
37
+ function isMarker(line) {
38
+ return line.trim() === MARKER_HEAD || line.trim() === MARKER_TAIL;
39
+ }
40
+ // utf-8 read keeps the BOM; rewriting it breaks Hermes' first key.
41
+ function readLines(envPath) {
42
+ if (!fs.existsSync(envPath))
43
+ return [];
44
+ return fs.readFileSync(envPath, "utf8").replace(/^\uFEFF/, "").split(/(?<=\n)/);
45
+ }
46
+ // Hermes preserves the original mode so Docker volume mounts survive (config.py:3921-3929).
47
+ function writeAtomic(envPath, lines) {
48
+ let mode = 0o600;
49
+ try {
50
+ mode = fs.statSync(envPath).mode & 0o777;
51
+ }
52
+ catch {
53
+ // No existing file: 0600 is the right default for credentials.
54
+ }
55
+ const tmp = `${envPath}.${randomUUID()}.tmp`;
56
+ try {
57
+ fs.writeFileSync(tmp, lines.join(""), { encoding: "utf8", mode });
58
+ // open() applies mode through umask; chmod bypasses it, preserving bits.
59
+ fs.chmodSync(tmp, mode);
60
+ fs.renameSync(tmp, envPath);
61
+ }
62
+ catch (error) {
63
+ fs.rmSync(tmp, { force: true });
64
+ throw error;
65
+ }
66
+ }
67
+ export function writeHermesKey(home, key) {
68
+ const clean = key.replace(/[\r\n]/g, "").trim();
69
+ // An empty key reads as "bad characters" without its own branch.
70
+ if (!clean)
71
+ throw new Error("TinyFish API key is empty; nothing to write to Hermes' .env");
72
+ if (!BARE_SAFE.test(clean)) {
73
+ throw new Error("TinyFish API key contains characters that cannot be written to a .env file");
74
+ }
75
+ const envPath = path.join(home, ".env");
76
+ fs.mkdirSync(home, { recursive: true });
77
+ const lines = readLines(envPath);
78
+ const assignment = `${HERMES_KEY_VAR}=${clean}\n`;
79
+ const index = lines.findIndex(definesKey);
80
+ if (index < 0) {
81
+ if (lines.length && !lines[lines.length - 1].endsWith("\n"))
82
+ lines[lines.length - 1] += "\n";
83
+ lines.push(`${MARKER_HEAD}\n`, assignment, `${MARKER_TAIL}\n`);
84
+ writeAtomic(envPath, lines);
85
+ return;
86
+ }
87
+ lines[index] = assignment;
88
+ // load_env() is last-wins; a surviving duplicate would beat this value.
89
+ writeAtomic(envPath, lines.filter((line, i) => i === index || !definesKey(line)));
90
+ }
91
+ export function removeHermesKey(home) {
92
+ const envPath = path.join(home, ".env");
93
+ const lines = readLines(envPath);
94
+ if (!lines.length)
95
+ return false;
96
+ const kept = lines.filter((line) => !definesKey(line) && !isMarker(line));
97
+ if (kept.length === lines.length)
98
+ return false;
99
+ writeAtomic(envPath, kept);
100
+ return true;
101
+ }
@@ -4,11 +4,10 @@ import spawn from "cross-spawn";
4
4
  import { z } from "zod";
5
5
  import { loadConfig, matchesCliKey } from "./auth.js";
6
6
  import { NATIVE_BY_HARNESS } from "./connect-clients.js";
7
+ import { HARNESS_PROBE_TIMEOUT_MS } from "./constants.js";
7
8
  import { errLine } from "./output.js";
8
9
  import { readCursorTinyfishEntry } from "./cursor-config.js";
9
10
  import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMode, Registered, } from "./harness-detect.js";
10
- // 6s tripped on a cold `hermes mcp list`, measured at 6.34s.
11
- const PROBE_TIMEOUT_MS = 15_000;
12
11
  // A list, verified against codex 0.146. A name-keyed map was accepted here too, but every
13
12
  // field is optional, so that arm parsed *any* object-of-objects: a wrapper like
14
13
  // `{"servers": {...}}` decoded as one entry named `servers` and reported TinyFish absent.
@@ -43,7 +42,7 @@ function spawnFailureReason(command, code) {
43
42
  function runProbe(command, args) {
44
43
  const result = spawn.sync(command, args, {
45
44
  encoding: "utf8",
46
- timeout: PROBE_TIMEOUT_MS,
45
+ timeout: HARNESS_PROBE_TIMEOUT_MS,
47
46
  // Belt and braces only: agents export FORCE_COLOR, and `NO_COLOR` does not fully silence
48
47
  // OpenCode, so the strip below stays load-bearing rather than a fallback.
49
48
  env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.30.1",
3
+ "version": "0.30.2-next.256",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {