@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.
- package/dist/lib/connect-native.js +25 -1
- package/dist/lib/doctor-checks.js +2 -0
- package/dist/lib/dotenv-file.d.ts +10 -0
- package/dist/lib/dotenv-file.js +124 -0
- 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/hermes-env.d.ts +0 -1
- package/dist/lib/hermes-env.js +6 -111
- 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',
|
|
@@ -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
|
-
//
|
|
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.
|
package/dist/lib/hermes-env.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/hermes-env.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|