@tiny-fish/cli 0.52.4-next.429 → 0.52.4-next.432

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.
@@ -10,6 +10,7 @@ import { AuthMode, RELOAD_ACTION } from './harness/harness-detect.js';
10
10
  import { SIGN_IN_PROBES } from './registration-detect.js';
11
11
  import { errLine, warnLine } from './output.js';
12
12
  import { verifyMcpAuth } from './verify.js';
13
+ import { dotenvMarkerHead, harnessDotenvPath, writeDotenvKey } from './dotenv-file.js';
13
14
  import { apiBaseFromMcpUrl } from './mcp-endpoint.js';
14
15
  import { finishSetupHint, requireKeylessMcp, requireSupportOrRescue, runCliInstallStep, trackPostInstallFailure, walkthroughPhase, } from './connect-steps.js';
15
16
  function removeExistingRegistration(client, removal, verbose) {
@@ -43,12 +44,35 @@ function exportApiKey(apiKey, displayName, deferOutro) {
43
44
  }
44
45
  return true;
45
46
  }
47
+ /** Best-effort: the profile export still covers terminal launches. */
48
+ function seedHarnessDotenv(client, keyed, deferOutro) {
49
+ const { envVar } = keyed.spec;
50
+ const envPath = harnessDotenvPath(client.connectClient);
51
+ if (!envPath || !envVar)
52
+ return false;
53
+ try {
54
+ // Unlike Hermes, nothing else needs this file group-readable.
55
+ writeDotenvKey(envPath, envVar, keyed.key, dotenvMarkerHead(client.connectClient), {
56
+ ownerOnly: true,
57
+ });
58
+ }
59
+ catch (error) {
60
+ warnLine(`Could not write ${envVar} to ${envPath} (${error instanceof Error ? error.message : String(error)}).`);
61
+ return false;
62
+ }
63
+ const line = `Wrote ${envVar} to ${envPath} — ${client.displayName} reads the key from there.`;
64
+ (deferOutro ?? errLine)(line);
65
+ return true;
66
+ }
46
67
  function resolveKeyedAdd(client, storedKey, keyAuthSupported, keyAuthOnly, deferOutro) {
47
68
  if (!storedKey || !keyAuthSupported || !client.keyAuth)
48
69
  return undefined;
49
70
  const keyed = { key: storedKey, spec: client.keyAuth };
71
+ const seededDotenv = seedHarnessDotenv(client, keyed, deferOutro);
50
72
  // Before the removals: a failed export must not strand the old registration.
51
- if (keyed.spec.envVar && !exportApiKey(keyed.key, client.displayName, deferOutro)) {
73
+ if (keyed.spec.envVar &&
74
+ !exportApiKey(keyed.key, client.displayName, deferOutro) &&
75
+ !seededDotenv) {
52
76
  if (keyAuthOnly)
53
77
  refuseFailedKeyExport(client);
54
78
  errLine(`Signing in to ${client.displayName} instead.`);
@@ -116,6 +116,8 @@ function registrationVerdict(status, mcpUrl, keyAuth, cliKeyMissing) {
116
116
  }
117
117
  // Ordered before the key check: the harness's own verdict wins.
118
118
  if (status.connected === false) {
119
+ if (status.keyReason)
120
+ return { status: 'warn', detail: `registered, but ${status.keyReason}` };
119
121
  return {
120
122
  status: 'warn',
121
123
  detail: 'registered, but the harness reports it cannot reach TinyFish; sign in again there',
@@ -0,0 +1,10 @@
1
+ import { type Harness } from './harness/harness-spec.js';
2
+ export declare function harnessDotenvPath(harness: Harness): string | undefined;
3
+ export declare function dotenvMarkerHead(client: string): string;
4
+ export declare function writeDotenvKey(envPath: string, varName: string, key: string, markerHead: string, { ownerOnly }?: {
5
+ ownerOnly?: boolean;
6
+ }): void;
7
+ export declare function readDotenvKey(envPath: string, varName: string): string | undefined;
8
+ /** Restores the exact line writeDotenvKey replaces, quotes and comment included. */
9
+ export declare function captureDotenvKeyRestore(envPath: string, varName: string, markerHead: string): () => void;
10
+ export declare function removeDotenvKey(envPath: string, varName: string, markerHead: string): boolean;
@@ -0,0 +1,124 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { harnessSpec } from './harness/harness-spec.js';
5
+ import { harnessConfigPath } from './harness/harness-detect.js';
6
+ const MARKER_TAIL = '# tinyfish-cli: end';
7
+ // Keys are `sk-tinyfish-<alnum>`; refusing anything else beats shipping unreachable escaping.
8
+ const BARE_SAFE = /^[A-Za-z0-9._\-/:+=]+$/;
9
+ export function harnessDotenvPath(harness) {
10
+ return harnessSpec(harness).keyDotenv ? path.join(harnessConfigPath(harness), '.env') : undefined;
11
+ }
12
+ export function dotenvMarkerHead(client) {
13
+ return `# tinyfish-cli: TinyFish MCP key, managed by \`tinyfish connect ${client}\``;
14
+ }
15
+ /** Mirrors Hermes' _env_line_defines_key: load_env() parses the export form too. */
16
+ function definesKey(line, varName) {
17
+ const stripped = line.trim().replace(/^export\s+/, '');
18
+ return stripped.startsWith(`${varName}=`);
19
+ }
20
+ // A leading BOM would otherwise ride into the first key's name.
21
+ function readLines(envPath) {
22
+ if (!fs.existsSync(envPath))
23
+ return [];
24
+ const raw = fs.readFileSync(envPath);
25
+ // A valid UTF-8 .env has no NUL; every wide encoding does.
26
+ if (raw.includes(0)) {
27
+ throw new Error(`${envPath} contains NUL bytes, so it is a wide encoding or corrupt; TinyFish can only ` +
28
+ `edit a UTF-8 .env. Re-save it as UTF-8 and retry.`);
29
+ }
30
+ return raw
31
+ .toString('utf8')
32
+ .replace(/^\uFEFF/, '')
33
+ .split(/(?<=\n)/);
34
+ }
35
+ // Hermes preserves the original mode so Docker volume mounts survive (config.py:3921-3929).
36
+ function writeAtomic(envPath, lines, ownerOnly = false) {
37
+ let mode = 0o600;
38
+ try {
39
+ mode = fs.statSync(envPath).mode & 0o777;
40
+ }
41
+ catch {
42
+ // No existing file: 0600 is the right default for credentials.
43
+ }
44
+ if (ownerOnly)
45
+ mode &= 0o700;
46
+ const tmp = `${envPath}.${randomUUID()}.tmp`;
47
+ try {
48
+ fs.writeFileSync(tmp, lines.join(''), { encoding: 'utf8', mode });
49
+ // open() applies mode through umask; chmod bypasses it, preserving bits.
50
+ fs.chmodSync(tmp, mode);
51
+ fs.renameSync(tmp, envPath);
52
+ }
53
+ catch (error) {
54
+ fs.rmSync(tmp, { force: true });
55
+ throw error;
56
+ }
57
+ }
58
+ export function writeDotenvKey(envPath, varName, key, markerHead, { ownerOnly = false } = {}) {
59
+ const clean = key.replace(/[\r\n]/g, '').trim();
60
+ // An empty key reads as "bad characters" without its own branch.
61
+ if (!clean)
62
+ throw new Error(`TinyFish API key is empty; nothing to write to ${envPath}`);
63
+ if (!BARE_SAFE.test(clean)) {
64
+ throw new Error('TinyFish API key contains characters that cannot be written to a .env file');
65
+ }
66
+ fs.mkdirSync(path.dirname(envPath), { recursive: true });
67
+ const lines = readLines(envPath);
68
+ const assignment = `${varName}=${clean}\n`;
69
+ const index = lines.findIndex((line) => definesKey(line, varName));
70
+ if (index < 0) {
71
+ if (lines.length && !lines[lines.length - 1].endsWith('\n'))
72
+ lines[lines.length - 1] += '\n';
73
+ lines.push(`${markerHead}\n`, assignment, `${MARKER_TAIL}\n`);
74
+ writeAtomic(envPath, lines, ownerOnly);
75
+ return;
76
+ }
77
+ lines[index] = assignment;
78
+ // Dotenv loaders are last-wins; a surviving duplicate would beat this value.
79
+ writeAtomic(envPath, lines.filter((line, i) => i === index || !definesKey(line, varName)), ownerOnly);
80
+ }
81
+ export function readDotenvKey(envPath, varName) {
82
+ const line = readLines(envPath).find((l) => definesKey(l, varName));
83
+ if (!line)
84
+ return undefined;
85
+ // We never write quoted values, so a quoted one isn't ours.
86
+ const raw = line
87
+ .trim()
88
+ .replace(/^export\s+/, '')
89
+ .slice(varName.length + 1)
90
+ .trim();
91
+ return BARE_SAFE.test(raw) ? raw : undefined;
92
+ }
93
+ /** Restores the exact line writeDotenvKey replaces, quotes and comment included. */
94
+ export function captureDotenvKeyRestore(envPath, varName, markerHead) {
95
+ // Raw, not readDotenvKey: its parse drops quoted and comment-trailed values.
96
+ const previous = readLines(envPath).find((line) => definesKey(line, varName));
97
+ return () => {
98
+ if (previous === undefined) {
99
+ removeDotenvKey(envPath, varName, markerHead);
100
+ return;
101
+ }
102
+ let restored = false;
103
+ const lines = readLines(envPath).flatMap((line) => {
104
+ if (!definesKey(line, varName))
105
+ return [line];
106
+ if (restored)
107
+ return [];
108
+ restored = true;
109
+ return [previous];
110
+ });
111
+ writeAtomic(envPath, restored ? lines : [...lines, previous]);
112
+ };
113
+ }
114
+ export function removeDotenvKey(envPath, varName, markerHead) {
115
+ const lines = readLines(envPath);
116
+ if (!lines.length)
117
+ return false;
118
+ const isMarker = (line) => line.trim() === markerHead || line.trim() === MARKER_TAIL;
119
+ const kept = lines.filter((line) => !definesKey(line, varName) && !isMarker(line));
120
+ if (kept.length === lines.length)
121
+ return false;
122
+ writeAtomic(envPath, kept);
123
+ return true;
124
+ }
@@ -5,6 +5,7 @@ import which from 'which';
5
5
  import { ALL_HARNESSES, harnessSpec } from './harness-spec.js';
6
6
  import { clineConfigDir } from '../config/cline-config.js';
7
7
  import { piAgentDir, piBinaryIsPi } from '../config/pi-config.js';
8
+ import { agentHome } from '../skill-paths.js';
8
9
  export { ALL_HARNESSES };
9
10
  export const HARNESS_DISPLAY_NAMES = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).displayName]));
10
11
  export const RELOAD_ACTION = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).reloadAction]));
@@ -34,9 +35,10 @@ export function commandOnPath(command) {
34
35
  return false;
35
36
  }
36
37
  }
37
- // PI_CODING_AGENT_DIR and CLINE_DIR move these dirs; a guessed path would be written unread.
38
+ // Env vars move these dirs; a guessed path would go unread.
38
39
  const CONFIG_PATH_RESOLVERS = {
39
40
  cline: clineConfigDir,
41
+ codex: () => agentHome('CODEX_HOME', harnessSpec('codex').configDir),
40
42
  pi: piAgentDir,
41
43
  };
42
44
  export function harnessConfigPath(harness) {
@@ -48,6 +48,8 @@ export interface HarnessSpec {
48
48
  };
49
49
  /** Keyed adds append this flag plus the env var name (Codex). */
50
50
  keyEnvFlag?: string;
51
+ /** The harness loads `<config dir>/.env` at launch (Codex). */
52
+ keyDotenv?: boolean;
51
53
  /** Keyed adds append these flags; the key itself is seeded elsewhere (Hermes). */
52
54
  keyAddFlags?: string[];
53
55
  /** No sign-in exists here, so connect refuses a keyless install (Hermes). */
@@ -156,6 +158,7 @@ export declare const HARNESS_SPECS: {
156
158
  };
157
159
  urlStyle: "flag";
158
160
  keyEnvFlag: string;
161
+ keyDotenv: true;
159
162
  degradedAuthNotes: {
160
163
  keyed: string;
161
164
  deferred: string;
@@ -78,6 +78,8 @@ export const HARNESS_SPECS = {
78
78
  urlStyle: 'flag',
79
79
  // Codex has no `--header`; the MCP server takes a key as `Bearer sk-…`.
80
80
  keyEnvFlag: '--bearer-token-env-var',
81
+ // GUI launches skip the shell profile; unset variable drops the server.
82
+ keyDotenv: true,
81
83
  degradedAuthNotes: { keyed: CODEX_KEYED_FALLBACK_NOTE, deferred: CODEX_DEFERRED_AUTH_NOTE },
82
84
  // `--oauth-resource` duplicates the resource Codex discovers; Clerk rejects both.
83
85
  // `mcp add` still runs OAuth inline, so `loginArgs` stays a retry.
@@ -14,6 +14,5 @@ export declare function hermesEnvPath(home: string): string;
14
14
  export declare function writeHermesKey(home: string, key: string): void;
15
15
  /** Doctor needs this to tell key auth from OAuth. */
16
16
  export declare function readHermesKey(home: string): string | undefined;
17
- /** Restores the exact line writeHermesKey replaces, quotes and comment included. */
18
17
  export declare function captureHermesKeyRestore(home: string): () => void;
19
18
  export declare function removeHermesKey(home: string): boolean;
@@ -1,11 +1,10 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
- import fs from 'node:fs';
4
- import { randomUUID } from 'node:crypto';
5
3
  import spawn from 'cross-spawn';
6
4
  import { HERMES_COLD_START_TIMEOUT_MS } from './constants.js';
7
5
  import { throwIfInterrupted } from './connect-runtime.js';
8
6
  import { noColourEnv, sanitizeLine } from './output.js';
7
+ import { captureDotenvKeyRestore, dotenvMarkerHead, readDotenvKey, removeDotenvKey, writeDotenvKey, } from './dotenv-file.js';
9
8
  // display_hermes_home() abbreviates under $HOME, so the value is not always absolute.
10
9
  const HERMES_HOME_LINE = /^\s*hermes_home:\s*(\S.*?)\s*$/m;
11
10
  // Consistency, not speed: doctor's second probe timing out would downgrade
@@ -46,125 +45,21 @@ function probeHermesHome() {
46
45
  return path.isAbsolute(expanded) ? expanded : { reason: 'relative' };
47
46
  }
48
47
  export const HERMES_KEY_VAR = 'MCP_TINYFISH_API_KEY';
48
+ const MARKER_HEAD = dotenvMarkerHead('hermes');
49
49
  /** Named in every message about the seeded key, so no caller rebuilds the path. */
50
50
  export function hermesEnvPath(home) {
51
51
  return path.join(home, '.env');
52
52
  }
53
- const MARKER_HEAD = `# tinyfish-cli: TinyFish MCP key, managed by \`tinyfish connect hermes\``;
54
- const MARKER_TAIL = '# tinyfish-cli: end';
55
- // Keys are `sk-tinyfish-<alnum>`; refusing anything else beats shipping unreachable escaping.
56
- const BARE_SAFE = /^[A-Za-z0-9._\-/:+=]+$/;
57
- /** Mirrors Hermes' _env_line_defines_key: load_env() parses the export form too. */
58
- function definesKey(line) {
59
- const stripped = line.trim().replace(/^export\s+/, '');
60
- return stripped.startsWith(`${HERMES_KEY_VAR}=`);
61
- }
62
- function isMarker(line) {
63
- return line.trim() === MARKER_HEAD || line.trim() === MARKER_TAIL;
64
- }
65
- // A leading BOM would otherwise ride into the first key's name.
66
- function readLines(envPath) {
67
- if (!fs.existsSync(envPath))
68
- return [];
69
- const raw = fs.readFileSync(envPath);
70
- // A valid UTF-8 .env has no NUL; every wide encoding does.
71
- if (raw.includes(0)) {
72
- throw new Error(`${envPath} contains NUL bytes, so it is a wide encoding or corrupt; TinyFish can only ` +
73
- `edit a UTF-8 .env. Re-save it as UTF-8 and retry.`);
74
- }
75
- return raw
76
- .toString('utf8')
77
- .replace(/^\uFEFF/, '')
78
- .split(/(?<=\n)/);
79
- }
80
- // Hermes preserves the original mode so Docker volume mounts survive (config.py:3921-3929).
81
- function writeAtomic(envPath, lines) {
82
- let mode = 0o600;
83
- try {
84
- mode = fs.statSync(envPath).mode & 0o777;
85
- }
86
- catch {
87
- // No existing file: 0600 is the right default for credentials.
88
- }
89
- const tmp = `${envPath}.${randomUUID()}.tmp`;
90
- try {
91
- fs.writeFileSync(tmp, lines.join(''), { encoding: 'utf8', mode });
92
- // open() applies mode through umask; chmod bypasses it, preserving bits.
93
- fs.chmodSync(tmp, mode);
94
- fs.renameSync(tmp, envPath);
95
- }
96
- catch (error) {
97
- fs.rmSync(tmp, { force: true });
98
- throw error;
99
- }
100
- }
101
53
  export function writeHermesKey(home, key) {
102
- const clean = key.replace(/[\r\n]/g, '').trim();
103
- // An empty key reads as "bad characters" without its own branch.
104
- if (!clean)
105
- throw new Error("TinyFish API key is empty; nothing to write to Hermes' .env");
106
- if (!BARE_SAFE.test(clean)) {
107
- throw new Error('TinyFish API key contains characters that cannot be written to a .env file');
108
- }
109
- const envPath = hermesEnvPath(home);
110
- fs.mkdirSync(home, { recursive: true });
111
- const lines = readLines(envPath);
112
- const assignment = `${HERMES_KEY_VAR}=${clean}\n`;
113
- const index = lines.findIndex(definesKey);
114
- if (index < 0) {
115
- if (lines.length && !lines[lines.length - 1].endsWith('\n'))
116
- lines[lines.length - 1] += '\n';
117
- lines.push(`${MARKER_HEAD}\n`, assignment, `${MARKER_TAIL}\n`);
118
- writeAtomic(envPath, lines);
119
- return;
120
- }
121
- lines[index] = assignment;
122
- // load_env() is last-wins; a surviving duplicate would beat this value.
123
- writeAtomic(envPath, lines.filter((line, i) => i === index || !definesKey(line)));
54
+ writeDotenvKey(hermesEnvPath(home), HERMES_KEY_VAR, key, MARKER_HEAD);
124
55
  }
125
56
  /** Doctor needs this to tell key auth from OAuth. */
126
57
  export function readHermesKey(home) {
127
- const line = readLines(hermesEnvPath(home)).find(definesKey);
128
- if (!line)
129
- return undefined;
130
- // We never write quoted values, so a quoted one isn't ours.
131
- const raw = line
132
- .trim()
133
- .replace(/^export\s+/, '')
134
- .slice(HERMES_KEY_VAR.length + 1)
135
- .trim();
136
- return BARE_SAFE.test(raw) ? raw : undefined;
58
+ return readDotenvKey(hermesEnvPath(home), HERMES_KEY_VAR);
137
59
  }
138
- /** Restores the exact line writeHermesKey replaces, quotes and comment included. */
139
60
  export function captureHermesKeyRestore(home) {
140
- // Raw, not readHermesKey: its parse drops quoted and comment-trailed values.
141
- const previous = readLines(hermesEnvPath(home)).find(definesKey);
142
- return () => {
143
- if (previous === undefined) {
144
- removeHermesKey(home);
145
- return;
146
- }
147
- const envPath = hermesEnvPath(home);
148
- let restored = false;
149
- const lines = readLines(envPath).flatMap((line) => {
150
- if (!definesKey(line))
151
- return [line];
152
- if (restored)
153
- return [];
154
- restored = true;
155
- return [previous];
156
- });
157
- writeAtomic(envPath, restored ? lines : [...lines, previous]);
158
- };
61
+ return captureDotenvKeyRestore(hermesEnvPath(home), HERMES_KEY_VAR, MARKER_HEAD);
159
62
  }
160
63
  export function removeHermesKey(home) {
161
- const envPath = hermesEnvPath(home);
162
- const lines = readLines(envPath);
163
- if (!lines.length)
164
- return false;
165
- const kept = lines.filter((line) => !definesKey(line) && !isMarker(line));
166
- if (kept.length === lines.length)
167
- return false;
168
- writeAtomic(envPath, kept);
169
- return true;
64
+ return removeDotenvKey(hermesEnvPath(home), HERMES_KEY_VAR, MARKER_HEAD);
170
65
  }
@@ -14,6 +14,7 @@ import { readOpencodeStyleEntry } from './config/opencode-config.js';
14
14
  import { readPiTinyfishEntry } from './config/pi-config.js';
15
15
  import { harnessSpec } from './harness/harness-spec.js';
16
16
  import { readHermesKey, resolveHermesHome } from './hermes-env.js';
17
+ import { harnessDotenvPath, readDotenvKey } from './dotenv-file.js';
17
18
  import { readHermesEntry } from './config/hermes-config.js';
18
19
  import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMode, Registered, } from './harness/harness-detect.js';
19
20
  const NOT_REGISTERED = Object.freeze({
@@ -245,15 +246,30 @@ const ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
245
246
  function declaredKeyEnvVar(harness) {
246
247
  return NATIVE_BY_HARNESS.get(harness)?.keyAuth?.envVar;
247
248
  }
249
+ /** An unreadable .env reads as unset, not as a failed probe. */
250
+ function readDotenvKeySafe(dotenvPath, envVar) {
251
+ try {
252
+ return readDotenvKey(dotenvPath, envVar);
253
+ }
254
+ catch {
255
+ return undefined;
256
+ }
257
+ }
248
258
  /** Codex stores a variable name; resolving it only answers whether it is the CLI's key. */
249
- function envKeyVerdict(envVar) {
259
+ function envKeyVerdict(envVar, dotenvPath) {
250
260
  if (!ENV_VAR_NAME.test(envVar)) {
251
261
  return { keyReason: 'its key comes from an unusable variable name' };
252
262
  }
253
- if (matchesCliKey(process.env[envVar]))
263
+ const fromDotenv = dotenvPath ? readDotenvKeySafe(dotenvPath, envVar) : undefined;
264
+ // GUI launches read only the .env; a shell export masks that.
265
+ if (dotenvPath && !fromDotenv) {
266
+ return { connected: false, keyReason: `$${envVar} is not in ${dotenvPath}` };
267
+ }
268
+ const value = process.env[envVar] || fromDotenv;
269
+ if (matchesCliKey(value))
254
270
  return { keyMatchesCliKey: true };
255
271
  return {
256
- keyReason: process.env[envVar]
272
+ keyReason: value
257
273
  ? `$${envVar} holds a key this CLI cannot verify`
258
274
  : `$${envVar} is not set here`,
259
275
  };
@@ -279,7 +295,7 @@ function probeCodex() {
279
295
  // Codex's own verdict: a stale `oauth_resource` strands installs here.
280
296
  ...(entry.auth_status === CodexAuthStatus.NotLoggedIn && !envVar ? { connected: false } : {}),
281
297
  ...(codexUrl ? { registeredUrl: codexUrl } : {}),
282
- ...(envVar ? envKeyVerdict(envVar) : {}),
298
+ ...(envVar ? envKeyVerdict(envVar, harnessDotenvPath('codex')) : {}),
283
299
  };
284
300
  }
285
301
  // No `cline mcp list`, so the settings file is the only registration evidence.
@@ -1,4 +1,5 @@
1
1
  import type { SkillAgent } from './harness/harness-spec.js';
2
+ export declare function agentHome(override: string, fallback: string): string;
2
3
  /** Where skills@1.5.15 wrote skill dirs for this agent. */
3
4
  export declare function skillRootDir(agent: SkillAgent): string;
4
5
  /** Mirrors skills@1.5.15 getSkillLockPath(); missing the XDG branch strands entries. */
@@ -1,7 +1,7 @@
1
1
  import * as os from 'node:os';
2
2
  import * as path from 'node:path';
3
3
  // Mirrors skills@1.5.15 exactly: it trims and falls back on empty values.
4
- function agentHome(override, fallback) {
4
+ export function agentHome(override, fallback) {
5
5
  return process.env[override]?.trim() || path.join(os.homedir(), fallback); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
6
6
  }
7
7
  // `skills` writes here for the universal agents, whose own config dirs it never touches.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.52.4-next.429",
3
+ "version": "0.52.4-next.432",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {