@tiny-fish/cli 0.52.4-next.431 → 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.
- package/dist/lib/connect-native.js +25 -1
- package/dist/lib/doctor-checks.js +2 -0
- package/dist/lib/dotenv-file.d.ts +5 -1
- package/dist/lib/dotenv-file.js +11 -4
- package/dist/lib/harness/harness-detect.js +3 -1
- package/dist/lib/harness/harness-spec.d.ts +3 -0
- package/dist/lib/harness/harness-spec.js +2 -0
- package/dist/lib/registration-detect.js +20 -4
- package/dist/lib/skill-paths.d.ts +1 -0
- package/dist/lib/skill-paths.js +1 -1
- package/package.json +1 -1
|
@@ -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 &&
|
|
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',
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { type Harness } from './harness/harness-spec.js';
|
|
2
|
+
export declare function harnessDotenvPath(harness: Harness): string | undefined;
|
|
1
3
|
export declare function dotenvMarkerHead(client: string): string;
|
|
2
|
-
export declare function writeDotenvKey(envPath: string, varName: string, key: string, markerHead: string
|
|
4
|
+
export declare function writeDotenvKey(envPath: string, varName: string, key: string, markerHead: string, { ownerOnly }?: {
|
|
5
|
+
ownerOnly?: boolean;
|
|
6
|
+
}): void;
|
|
3
7
|
export declare function readDotenvKey(envPath: string, varName: string): string | undefined;
|
|
4
8
|
/** Restores the exact line writeDotenvKey replaces, quotes and comment included. */
|
|
5
9
|
export declare function captureDotenvKeyRestore(envPath: string, varName: string, markerHead: string): () => void;
|
package/dist/lib/dotenv-file.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { harnessSpec } from './harness/harness-spec.js';
|
|
5
|
+
import { harnessConfigPath } from './harness/harness-detect.js';
|
|
4
6
|
const MARKER_TAIL = '# tinyfish-cli: end';
|
|
5
7
|
// Keys are `sk-tinyfish-<alnum>`; refusing anything else beats shipping unreachable escaping.
|
|
6
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
|
+
}
|
|
7
12
|
export function dotenvMarkerHead(client) {
|
|
8
13
|
return `# tinyfish-cli: TinyFish MCP key, managed by \`tinyfish connect ${client}\``;
|
|
9
14
|
}
|
|
@@ -28,7 +33,7 @@ function readLines(envPath) {
|
|
|
28
33
|
.split(/(?<=\n)/);
|
|
29
34
|
}
|
|
30
35
|
// Hermes preserves the original mode so Docker volume mounts survive (config.py:3921-3929).
|
|
31
|
-
function writeAtomic(envPath, lines) {
|
|
36
|
+
function writeAtomic(envPath, lines, ownerOnly = false) {
|
|
32
37
|
let mode = 0o600;
|
|
33
38
|
try {
|
|
34
39
|
mode = fs.statSync(envPath).mode & 0o777;
|
|
@@ -36,6 +41,8 @@ function writeAtomic(envPath, lines) {
|
|
|
36
41
|
catch {
|
|
37
42
|
// No existing file: 0600 is the right default for credentials.
|
|
38
43
|
}
|
|
44
|
+
if (ownerOnly)
|
|
45
|
+
mode &= 0o700;
|
|
39
46
|
const tmp = `${envPath}.${randomUUID()}.tmp`;
|
|
40
47
|
try {
|
|
41
48
|
fs.writeFileSync(tmp, lines.join(''), { encoding: 'utf8', mode });
|
|
@@ -48,7 +55,7 @@ function writeAtomic(envPath, lines) {
|
|
|
48
55
|
throw error;
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
|
-
export function writeDotenvKey(envPath, varName, key, markerHead) {
|
|
58
|
+
export function writeDotenvKey(envPath, varName, key, markerHead, { ownerOnly = false } = {}) {
|
|
52
59
|
const clean = key.replace(/[\r\n]/g, '').trim();
|
|
53
60
|
// An empty key reads as "bad characters" without its own branch.
|
|
54
61
|
if (!clean)
|
|
@@ -64,12 +71,12 @@ export function writeDotenvKey(envPath, varName, key, markerHead) {
|
|
|
64
71
|
if (lines.length && !lines[lines.length - 1].endsWith('\n'))
|
|
65
72
|
lines[lines.length - 1] += '\n';
|
|
66
73
|
lines.push(`${markerHead}\n`, assignment, `${MARKER_TAIL}\n`);
|
|
67
|
-
writeAtomic(envPath, lines);
|
|
74
|
+
writeAtomic(envPath, lines, ownerOnly);
|
|
68
75
|
return;
|
|
69
76
|
}
|
|
70
77
|
lines[index] = assignment;
|
|
71
78
|
// Dotenv loaders are last-wins; a surviving duplicate would beat this value.
|
|
72
|
-
writeAtomic(envPath, lines.filter((line, i) => i === index || !definesKey(line, varName)));
|
|
79
|
+
writeAtomic(envPath, lines.filter((line, i) => i === index || !definesKey(line, varName)), ownerOnly);
|
|
73
80
|
}
|
|
74
81
|
export function readDotenvKey(envPath, varName) {
|
|
75
82
|
const line = readLines(envPath).find((l) => definesKey(l, varName));
|
|
@@ -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
|
-
//
|
|
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,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
|
-
|
|
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:
|
|
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. */
|
package/dist/lib/skill-paths.js
CHANGED
|
@@ -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.
|