@empirical-gauzza/cli 1.0.26 → 1.0.27

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.
@@ -1,47 +1,47 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const { commandExists } = require('../fsUtils');
7
- const { installCodex } = require('./codex');
8
- const { installCopilot } = require('./copilot');
9
- const { installClaude } = require('./claude');
10
-
11
- function detectInstallTargets() {
12
- const home = os.homedir();
13
- const targets = [];
14
-
15
- const codexDetected = commandExists('codex') || fs.existsSync(path.join(home, '.codex'));
16
- if (codexDetected) {
17
- targets.push({
18
- id: 'codex',
19
- label: 'Codex CLI',
20
- install: installCodex
21
- });
22
- }
23
-
24
- const copilotDetected = commandExists('copilot') || fs.existsSync(path.join(home, '.copilot'));
25
- if (copilotDetected) {
26
- targets.push({
27
- id: 'copilot',
28
- label: 'GitHub Copilot CLI',
29
- install: installCopilot
30
- });
31
- }
32
-
33
- const claudeDetected = commandExists('claude') || fs.existsSync(path.join(home, '.claude'));
34
- if (claudeDetected) {
35
- targets.push({
36
- id: 'claude',
37
- label: 'Claude CLI',
38
- install: installClaude
39
- });
40
- }
41
-
42
- return targets;
43
- }
44
-
45
- module.exports = {
46
- detectInstallTargets
47
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { commandExists } = require('../fsUtils');
7
+ const { installCodex } = require('./codex');
8
+ const { installCopilot } = require('./copilot');
9
+ const { installClaude } = require('./claude');
10
+
11
+ function detectInstallTargets() {
12
+ const home = os.homedir();
13
+ const targets = [];
14
+
15
+ const codexDetected = commandExists('codex') || fs.existsSync(path.join(home, '.codex'));
16
+ if (codexDetected) {
17
+ targets.push({
18
+ id: 'codex',
19
+ label: 'Codex CLI',
20
+ install: installCodex
21
+ });
22
+ }
23
+
24
+ const copilotDetected = commandExists('copilot') || fs.existsSync(path.join(home, '.copilot'));
25
+ if (copilotDetected) {
26
+ targets.push({
27
+ id: 'copilot',
28
+ label: 'GitHub Copilot CLI',
29
+ install: installCopilot
30
+ });
31
+ }
32
+
33
+ const claudeDetected = commandExists('claude') || fs.existsSync(path.join(home, '.claude'));
34
+ if (claudeDetected) {
35
+ targets.push({
36
+ id: 'claude',
37
+ label: 'Claude CLI',
38
+ install: installClaude
39
+ });
40
+ }
41
+
42
+ return targets;
43
+ }
44
+
45
+ module.exports = {
46
+ detectInstallTargets
47
+ };
@@ -123,6 +123,18 @@ Two rules that are not optional:
123
123
  codebase, stop: that is work-history or scar-tissue, and filing it here makes it indistinguishable
124
124
  from something the user actually said or lived. Downstream features read these as the user's life.
125
125
 
126
+ **Say which one it is: pass `authorType`.** Same distinction, made explicit in the data instead
127
+ of left to be guessed from which transport you happened to use. `authorType: "user"` when the
128
+ memory is about the user — something they said, decided, prefer, or are working toward.
129
+ `authorType: "agent"` when it is your own work. Do not set `"agent"` merely because you are the
130
+ one calling: an agent writing down a real fact about the user is recording a user-authored fact,
131
+ and that is the whole point of this tool. On the CLI it is `--author-type user|agent`. Omit it
132
+ when you genuinely cannot tell, and it stays unresolved rather than being invented.
133
+
134
+ This is read back at retrieval time to keep an agent's session notes out of answers about the
135
+ user's own life and work, so a wrong value does not show up as a warning, it shows up as the
136
+ wrong memories coming back.
137
+
126
138
  - Append a correction or follow-up to an existing memory with `add_note_by_query` / `empirical memory note`.
127
139
  - Patch an existing memory with `update_memory_by_query` / `empirical memory update`.
128
140
  - Delete only when the user explicitly asks; use the delete tool with confirmation, or `empirical memory delete --match "<memory>" --confirm`.
@@ -115,6 +115,18 @@ Two rules that are not optional:
115
115
  codebase, stop: that is work-history or scar-tissue, and filing it here makes it indistinguishable
116
116
  from something the user actually said or lived. Downstream features read these as the user's life.
117
117
 
118
+ **Say which one it is: pass `authorType`.** Same distinction, made explicit in the data instead
119
+ of left to be guessed from which transport you happened to use. `authorType: "user"` when the
120
+ memory is about the user — something they said, decided, prefer, or are working toward.
121
+ `authorType: "agent"` when it is your own work. Do not set `"agent"` merely because you are the
122
+ one calling: an agent writing down a real fact about the user is recording a user-authored fact,
123
+ and that is the whole point of this tool. On the CLI it is `--author-type user|agent`. Omit it
124
+ when you genuinely cannot tell, and it stays unresolved rather than being invented.
125
+
126
+ This is read back at retrieval time to keep an agent's session notes out of answers about the
127
+ user's own life and work, so a wrong value does not show up as a warning, it shows up as the
128
+ wrong memories coming back.
129
+
118
130
  - Append a correction or follow-up to an existing memory with `empirical memory note`.
119
131
  - Patch an existing memory with `empirical memory update`.
120
132
  - Delete only when the user explicitly asks: `empirical memory delete --match "<memory>" --confirm`.
@@ -92,6 +92,18 @@ Two rules that are not optional:
92
92
  codebase, stop: that is work-history or scar-tissue, and filing it here makes it indistinguishable
93
93
  from something the user actually said or lived. Downstream features read these as the user's life.
94
94
 
95
+ **Say which one it is: pass `authorType`.** Same distinction, made explicit in the data instead
96
+ of left to be guessed from which transport you happened to use. `authorType: "user"` when the
97
+ memory is about the user — something they said, decided, prefer, or are working toward.
98
+ `authorType: "agent"` when it is your own work. Do not set `"agent"` merely because you are the
99
+ one calling: an agent writing down a real fact about the user is recording a user-authored fact,
100
+ and that is the whole point of this tool. On the CLI it is `--author-type user|agent`. Omit it
101
+ when you genuinely cannot tell, and it stays unresolved rather than being invented.
102
+
103
+ This is read back at retrieval time to keep an agent's session notes out of answers about the
104
+ user's own life and work, so a wrong value does not show up as a warning, it shows up as the
105
+ wrong memories coming back.
106
+
95
107
  - Append a correction or follow-up to an existing memory with `add_note_by_query`.
96
108
  - Patch an existing memory with `update_memory_by_query`.
97
109
  - Delete only when the user explicitly asks; use `delete_memory_by_query` with confirmation.
@@ -47,7 +47,7 @@ const MEMORY_SUBCOMMAND_USAGE = {
47
47
  query: 'Usage: empirical memory query --match <query> [--top-k 3] [--category <cat>] [--tags <t1,t2>] [--tags-all <t1,t2>] [--status <s1,s2>] [--pretty] [--format table]',
48
48
  get: 'Usage: empirical memory get --memory-id <id> [--pretty]',
49
49
  list: 'Usage: empirical memory list [--category <cat>] [--tags <t1,t2>] [--start-date <ISO>] [--end-date <ISO>] [--limit 20] [--cursor <cursor>] [--oldest] [--pretty] [--format table]',
50
- record: 'Usage: empirical memory record --category <cat> (--summary <text> | --summary - | --summary-file <path>) [--tags t1,t2] [--node-type observation] [--json-file <path>] [--no-dedupe] [--no-auto-link]',
50
+ record: 'Usage: empirical memory record --category <cat> (--summary <text> | --summary - | --summary-file <path>) [--tags t1,t2] [--node-type observation] [--json-file <path>] [--no-dedupe] [--no-auto-link] [--author-type user|agent]',
51
51
  note: 'Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--min-score 0.35]',
52
52
  update: 'Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>) [--min-score 0.35]',
53
53
  delete: 'Usage: empirical memory delete (--match <query> | --memory-id <id>) [--confirm] [--min-score 0.35]\nUsage: empirical memory delete (--tag <t> | --category <c> | --all) --confirm',
@@ -351,7 +351,7 @@ async function runMemoryRecord(args) {
351
351
  }
352
352
 
353
353
  if (!category || !summary) {
354
- warn('Usage: empirical memory record --category <cat> (--summary <text> | --summary - | --summary-file <path>) [--tags t1,t2] [--node-type observation] [--json-file <path>] [--no-dedupe] [--no-auto-link]');
354
+ warn('Usage: empirical memory record --category <cat> (--summary <text> | --summary - | --summary-file <path>) [--tags t1,t2] [--node-type observation] [--json-file <path>] [--no-dedupe] [--no-auto-link] [--author-type user|agent]');
355
355
  warn('Valid categories: ' + MEMORY_CATEGORIES.join(', '));
356
356
  process.exit(1);
357
357
  }
@@ -395,6 +395,29 @@ async function runMemoryRecord(args) {
395
395
  payload.autoLink = false;
396
396
  }
397
397
 
398
+ // Authorship (2026-09-09). `source` is observable truth: this is the CLI, so say so.
399
+ // Until now the CLI sent nothing, the server fell back to the transport it saw ('api'),
400
+ // and api maps to authorType 'user' because that mapping was written for the web portal.
401
+ // Every agent write through this command was therefore stored as authored by the user.
402
+ // 'cli' resolves to authorType 'unknown', which is the honest answer when nobody says.
403
+ if (payload.source === undefined) payload.source = 'cli';
404
+
405
+ // --author-type lets the caller state what the transport cannot know. An agent recording
406
+ // its own session notes should pass 'agent'; an agent recording something the user said
407
+ // or decided should pass 'user'. Omitted, it stays 'unknown' rather than being guessed.
408
+ const authorType = parseOption(args, '--author-type', '');
409
+ if (authorType) {
410
+ if (!['user', 'agent', 'system'].includes(authorType)) {
411
+ warn(JSON.stringify({
412
+ error: true,
413
+ code: 'invalid_author_type',
414
+ message: `Invalid --author-type "${authorType}". Valid: user, agent, system`
415
+ }));
416
+ process.exit(1);
417
+ }
418
+ payload.authorType = authorType;
419
+ }
420
+
398
421
  const accessToken = requireAccessToken();
399
422
  const baseUrl = resolveApiBaseUrl();
400
423
  const recordUrl = new URL('/api/v1/memories', baseUrl).toString();
@@ -1,222 +1,222 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const { readJsonIfExists, writeFile, ensureDir } = require('./fsUtils');
7
- const { getEnv } = require('./env');
8
- const { runResolvedCommand } = require('./fsUtils');
9
-
10
- function getLocalOAuthClientPath() {
11
- return path.join(os.homedir(), '.empirical', 'oauth-client.json');
12
- }
13
-
14
- function readStoredOAuthClient() {
15
- const stored = readJsonIfExists(getLocalOAuthClientPath());
16
- if (!stored || typeof stored !== 'object') return null;
17
- const clientId = String(stored.clientId || '').trim();
18
- if (!clientId) return null;
19
- return {
20
- clientId,
21
- clientSecret: String(stored.clientSecret || '')
22
- };
23
- }
24
-
25
- function writeStoredOAuthClient(clientId, clientSecret) {
26
- const payload = {
27
- clientId: String(clientId || '').trim(),
28
- clientSecret: String(clientSecret || '').trim(),
29
- updatedAt: new Date().toISOString()
30
- };
31
- writeFile(getLocalOAuthClientPath(), `${JSON.stringify(payload, null, 2)}\n`);
32
- }
33
-
34
- function clearStoredOAuthClient() {
35
- const storePath = getLocalOAuthClientPath();
36
- if (fs.existsSync(storePath)) {
37
- fs.unlinkSync(storePath);
38
- }
39
- }
40
-
41
- function maskSecret(value) {
42
- const raw = String(value || '');
43
- if (!raw) return '(not set)';
44
- if (raw.length <= 8) return '*'.repeat(raw.length);
45
- return `${raw.slice(0, 4)}...${raw.slice(-4)}`;
46
- }
47
-
48
- function generateRandomString(length = 32) {
49
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
50
- let result = '';
51
- for (let i = 0; i < length; i += 1) {
52
- result += chars.charAt(Math.floor(Math.random() * chars.length));
53
- }
54
- return result;
55
- }
56
-
57
- function base64url(buf) {
58
- return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
59
- }
60
-
61
- function generatePKCE() {
62
- const crypto = require('crypto');
63
- const codeVerifier = generateRandomString(128);
64
- const codeChallenge = base64url(crypto.createHash('sha256').update(codeVerifier).digest());
65
- return { codeVerifier, codeChallenge };
66
- }
67
-
68
- function decodeJwtPayload(token) {
69
- try {
70
- const parts = String(token).split('.');
71
- if (parts.length !== 3) return null;
72
- return JSON.parse(Buffer.from(parts[1], 'base64url').toString());
73
- } catch (_error) {
74
- return null;
75
- }
76
- }
77
-
78
- function isJwtToken(token) {
79
- return typeof token === 'string' && token.split('.').length === 3;
80
- }
81
-
82
- function isJwtExpired(token, skewSeconds = 30) {
83
- const payload = decodeJwtPayload(token);
84
- if (!payload || typeof payload.exp !== 'number') return true;
85
- return payload.exp < Math.floor(Date.now() / 1000) + skewSeconds;
86
- }
87
-
88
- function getHeadlessTokenPath() {
89
- return path.resolve(getEnv('EMPIRICAL_TOKEN_OUTPUT', '.empirical/headless-token.json'));
90
- }
91
-
92
- function getOAuthSessionPath() {
93
- return path.join(os.homedir(), '.empirical', 'oauth-session.json');
94
- }
95
-
96
- function readOAuthSession() {
97
- const session = readJsonIfExists(getOAuthSessionPath());
98
- if (!session || typeof session !== 'object') return null;
99
- return session;
100
- }
101
-
102
- function writeOAuthSession(session) {
103
- if (!session?.accessToken) return;
104
- writeFile(getOAuthSessionPath(), `${JSON.stringify(session, null, 2)}\n`);
105
- }
106
-
107
- function clearOAuthSession() {
108
- const sessionPath = getOAuthSessionPath();
109
- if (fs.existsSync(sessionPath)) fs.unlinkSync(sessionPath);
110
- }
111
-
112
- function readHeadlessToken() {
113
- const tokenPath = getHeadlessTokenPath();
114
- if (!fs.existsSync(tokenPath)) return null;
115
- try {
116
- const parsed = JSON.parse(fs.readFileSync(tokenPath, 'utf8'));
117
- if (!parsed || typeof parsed !== 'object') {
118
- return null;
119
- }
120
-
121
- const normalized = { ...parsed };
122
- if (!normalized.clientId && normalized.authClientId) {
123
- normalized.clientId = normalized.authClientId;
124
- }
125
- if (!normalized.clientSecret && normalized.authClientSecret) {
126
- normalized.clientSecret = normalized.authClientSecret;
127
- }
128
- if (!normalized.apiKey) {
129
- normalized.apiKey = normalized.clientSecret || normalized.authClientSecret || null;
130
- }
131
- return normalized;
132
- } catch (_error) {
133
- return null;
134
- }
135
- }
136
-
137
- function getAccessTokenExpiryMs(token) {
138
- if (!token || !token.accessToken) return 0;
139
- const obtainedAtMs = Date.parse(String(token.obtainedAt || ''));
140
- const expiresInSeconds = Number(token.expiresIn || 0);
141
- if (!Number.isFinite(obtainedAtMs) || !Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
142
- return 0;
143
- }
144
- return obtainedAtMs + (expiresInSeconds * 1000);
145
- }
146
-
147
- function hasFreshAccessToken(token, skewSeconds = 120) {
148
- const expiryMs = getAccessTokenExpiryMs(token);
149
- if (!expiryMs) return false;
150
- return expiryMs > (Date.now() + (skewSeconds * 1000));
151
- }
152
-
153
- function persistAccessToken(accessToken) {
154
- if (!accessToken) return;
155
- process.env.EMPIRICAL_ACCESS_TOKEN = accessToken;
156
- if (process.platform !== 'win32') {
157
- return;
158
- }
159
- // `setx` writes permanently into the user's Windows environment, surviving
160
- // every future shell/process. Only do that for a genuinely permanent
161
- // credential (a plain apiKey) — a short-lived session JWT written here
162
- // would silently outlive its own expiry and shadow a valid apiKey in the
163
- // token file for good, forcing a manual env-var reset roughly every hour.
164
- if (isJwtToken(accessToken)) {
165
- return;
166
- }
167
- const escaped = String(accessToken).replace(/\"/g, '""');
168
- runResolvedCommand('setx', ['EMPIRICAL_ACCESS_TOKEN', escaped], { stdio: 'ignore' });
169
- }
170
-
171
- function toEnvSafe(value) {
172
- return String(value ?? '')
173
- .replace(/\\/g, '\\\\')
174
- .replace(/\n/g, '\\n')
175
- .replace(/\r/g, '\\r')
176
- .replace(/"/g, '\\"');
177
- }
178
-
179
- function writeHeadlessEnvFile(result, outputPath) {
180
- ensureDir(path.dirname(path.resolve(outputPath)));
181
- // Prefer the API key as the access token — it never expires and works as a
182
- // permanent Bearer credential with the MCP endpoint.
183
- const permanentToken = result.apiKey || result.accessToken;
184
- const lines = [
185
- '# Generated by empirical cli',
186
- `EMPIRICAL_BASE_URL="${toEnvSafe(result.baseUrl)}"`,
187
- `EMPIRICAL_ACCESS_TOKEN="${toEnvSafe(permanentToken)}"`,
188
- `EMPIRICAL_API_KEY="${toEnvSafe(result.apiKey || '')}"`,
189
- `EMPIRICAL_REFRESH_TOKEN="${toEnvSafe(result.refreshToken || '')}"`,
190
- `EMPIRICAL_CLIENT_ID="${toEnvSafe(result.clientId || '')}"`,
191
- `EMPIRICAL_CLIENT_SECRET="${toEnvSafe(result.clientSecret || '')}"`,
192
- `EMPIRICAL_TOKEN_TYPE="${toEnvSafe(result.tokenType || 'Bearer')}"`,
193
- `EMPIRICAL_TOKEN_OBTAINED_AT="${toEnvSafe(result.obtainedAt || '')}"`,
194
- `EMPIRICAL_TOKEN_EXPIRES_IN="${toEnvSafe(result.expiresIn || '')}"`
195
- ];
196
- fs.writeFileSync(path.resolve(outputPath), `${lines.join('\n')}\n`, 'utf8');
197
- }
198
-
199
- module.exports = {
200
- getLocalOAuthClientPath,
201
- readStoredOAuthClient,
202
- writeStoredOAuthClient,
203
- clearStoredOAuthClient,
204
- maskSecret,
205
- generateRandomString,
206
- base64url,
207
- generatePKCE,
208
- decodeJwtPayload,
209
- isJwtToken,
210
- isJwtExpired,
211
- getHeadlessTokenPath,
212
- getOAuthSessionPath,
213
- readOAuthSession,
214
- writeOAuthSession,
215
- clearOAuthSession,
216
- readHeadlessToken,
217
- getAccessTokenExpiryMs,
218
- hasFreshAccessToken,
219
- persistAccessToken,
220
- toEnvSafe,
221
- writeHeadlessEnvFile
222
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { readJsonIfExists, writeFile, ensureDir } = require('./fsUtils');
7
+ const { getEnv } = require('./env');
8
+ const { runResolvedCommand } = require('./fsUtils');
9
+
10
+ function getLocalOAuthClientPath() {
11
+ return path.join(os.homedir(), '.empirical', 'oauth-client.json');
12
+ }
13
+
14
+ function readStoredOAuthClient() {
15
+ const stored = readJsonIfExists(getLocalOAuthClientPath());
16
+ if (!stored || typeof stored !== 'object') return null;
17
+ const clientId = String(stored.clientId || '').trim();
18
+ if (!clientId) return null;
19
+ return {
20
+ clientId,
21
+ clientSecret: String(stored.clientSecret || '')
22
+ };
23
+ }
24
+
25
+ function writeStoredOAuthClient(clientId, clientSecret) {
26
+ const payload = {
27
+ clientId: String(clientId || '').trim(),
28
+ clientSecret: String(clientSecret || '').trim(),
29
+ updatedAt: new Date().toISOString()
30
+ };
31
+ writeFile(getLocalOAuthClientPath(), `${JSON.stringify(payload, null, 2)}\n`);
32
+ }
33
+
34
+ function clearStoredOAuthClient() {
35
+ const storePath = getLocalOAuthClientPath();
36
+ if (fs.existsSync(storePath)) {
37
+ fs.unlinkSync(storePath);
38
+ }
39
+ }
40
+
41
+ function maskSecret(value) {
42
+ const raw = String(value || '');
43
+ if (!raw) return '(not set)';
44
+ if (raw.length <= 8) return '*'.repeat(raw.length);
45
+ return `${raw.slice(0, 4)}...${raw.slice(-4)}`;
46
+ }
47
+
48
+ function generateRandomString(length = 32) {
49
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
50
+ let result = '';
51
+ for (let i = 0; i < length; i += 1) {
52
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
53
+ }
54
+ return result;
55
+ }
56
+
57
+ function base64url(buf) {
58
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
59
+ }
60
+
61
+ function generatePKCE() {
62
+ const crypto = require('crypto');
63
+ const codeVerifier = generateRandomString(128);
64
+ const codeChallenge = base64url(crypto.createHash('sha256').update(codeVerifier).digest());
65
+ return { codeVerifier, codeChallenge };
66
+ }
67
+
68
+ function decodeJwtPayload(token) {
69
+ try {
70
+ const parts = String(token).split('.');
71
+ if (parts.length !== 3) return null;
72
+ return JSON.parse(Buffer.from(parts[1], 'base64url').toString());
73
+ } catch (_error) {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function isJwtToken(token) {
79
+ return typeof token === 'string' && token.split('.').length === 3;
80
+ }
81
+
82
+ function isJwtExpired(token, skewSeconds = 30) {
83
+ const payload = decodeJwtPayload(token);
84
+ if (!payload || typeof payload.exp !== 'number') return true;
85
+ return payload.exp < Math.floor(Date.now() / 1000) + skewSeconds;
86
+ }
87
+
88
+ function getHeadlessTokenPath() {
89
+ return path.resolve(getEnv('EMPIRICAL_TOKEN_OUTPUT', '.empirical/headless-token.json'));
90
+ }
91
+
92
+ function getOAuthSessionPath() {
93
+ return path.join(os.homedir(), '.empirical', 'oauth-session.json');
94
+ }
95
+
96
+ function readOAuthSession() {
97
+ const session = readJsonIfExists(getOAuthSessionPath());
98
+ if (!session || typeof session !== 'object') return null;
99
+ return session;
100
+ }
101
+
102
+ function writeOAuthSession(session) {
103
+ if (!session?.accessToken) return;
104
+ writeFile(getOAuthSessionPath(), `${JSON.stringify(session, null, 2)}\n`);
105
+ }
106
+
107
+ function clearOAuthSession() {
108
+ const sessionPath = getOAuthSessionPath();
109
+ if (fs.existsSync(sessionPath)) fs.unlinkSync(sessionPath);
110
+ }
111
+
112
+ function readHeadlessToken() {
113
+ const tokenPath = getHeadlessTokenPath();
114
+ if (!fs.existsSync(tokenPath)) return null;
115
+ try {
116
+ const parsed = JSON.parse(fs.readFileSync(tokenPath, 'utf8'));
117
+ if (!parsed || typeof parsed !== 'object') {
118
+ return null;
119
+ }
120
+
121
+ const normalized = { ...parsed };
122
+ if (!normalized.clientId && normalized.authClientId) {
123
+ normalized.clientId = normalized.authClientId;
124
+ }
125
+ if (!normalized.clientSecret && normalized.authClientSecret) {
126
+ normalized.clientSecret = normalized.authClientSecret;
127
+ }
128
+ if (!normalized.apiKey) {
129
+ normalized.apiKey = normalized.clientSecret || normalized.authClientSecret || null;
130
+ }
131
+ return normalized;
132
+ } catch (_error) {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ function getAccessTokenExpiryMs(token) {
138
+ if (!token || !token.accessToken) return 0;
139
+ const obtainedAtMs = Date.parse(String(token.obtainedAt || ''));
140
+ const expiresInSeconds = Number(token.expiresIn || 0);
141
+ if (!Number.isFinite(obtainedAtMs) || !Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
142
+ return 0;
143
+ }
144
+ return obtainedAtMs + (expiresInSeconds * 1000);
145
+ }
146
+
147
+ function hasFreshAccessToken(token, skewSeconds = 120) {
148
+ const expiryMs = getAccessTokenExpiryMs(token);
149
+ if (!expiryMs) return false;
150
+ return expiryMs > (Date.now() + (skewSeconds * 1000));
151
+ }
152
+
153
+ function persistAccessToken(accessToken) {
154
+ if (!accessToken) return;
155
+ process.env.EMPIRICAL_ACCESS_TOKEN = accessToken;
156
+ if (process.platform !== 'win32') {
157
+ return;
158
+ }
159
+ // `setx` writes permanently into the user's Windows environment, surviving
160
+ // every future shell/process. Only do that for a genuinely permanent
161
+ // credential (a plain apiKey) — a short-lived session JWT written here
162
+ // would silently outlive its own expiry and shadow a valid apiKey in the
163
+ // token file for good, forcing a manual env-var reset roughly every hour.
164
+ if (isJwtToken(accessToken)) {
165
+ return;
166
+ }
167
+ const escaped = String(accessToken).replace(/\"/g, '""');
168
+ runResolvedCommand('setx', ['EMPIRICAL_ACCESS_TOKEN', escaped], { stdio: 'ignore' });
169
+ }
170
+
171
+ function toEnvSafe(value) {
172
+ return String(value ?? '')
173
+ .replace(/\\/g, '\\\\')
174
+ .replace(/\n/g, '\\n')
175
+ .replace(/\r/g, '\\r')
176
+ .replace(/"/g, '\\"');
177
+ }
178
+
179
+ function writeHeadlessEnvFile(result, outputPath) {
180
+ ensureDir(path.dirname(path.resolve(outputPath)));
181
+ // Prefer the API key as the access token — it never expires and works as a
182
+ // permanent Bearer credential with the MCP endpoint.
183
+ const permanentToken = result.apiKey || result.accessToken;
184
+ const lines = [
185
+ '# Generated by empirical cli',
186
+ `EMPIRICAL_BASE_URL="${toEnvSafe(result.baseUrl)}"`,
187
+ `EMPIRICAL_ACCESS_TOKEN="${toEnvSafe(permanentToken)}"`,
188
+ `EMPIRICAL_API_KEY="${toEnvSafe(result.apiKey || '')}"`,
189
+ `EMPIRICAL_REFRESH_TOKEN="${toEnvSafe(result.refreshToken || '')}"`,
190
+ `EMPIRICAL_CLIENT_ID="${toEnvSafe(result.clientId || '')}"`,
191
+ `EMPIRICAL_CLIENT_SECRET="${toEnvSafe(result.clientSecret || '')}"`,
192
+ `EMPIRICAL_TOKEN_TYPE="${toEnvSafe(result.tokenType || 'Bearer')}"`,
193
+ `EMPIRICAL_TOKEN_OBTAINED_AT="${toEnvSafe(result.obtainedAt || '')}"`,
194
+ `EMPIRICAL_TOKEN_EXPIRES_IN="${toEnvSafe(result.expiresIn || '')}"`
195
+ ];
196
+ fs.writeFileSync(path.resolve(outputPath), `${lines.join('\n')}\n`, 'utf8');
197
+ }
198
+
199
+ module.exports = {
200
+ getLocalOAuthClientPath,
201
+ readStoredOAuthClient,
202
+ writeStoredOAuthClient,
203
+ clearStoredOAuthClient,
204
+ maskSecret,
205
+ generateRandomString,
206
+ base64url,
207
+ generatePKCE,
208
+ decodeJwtPayload,
209
+ isJwtToken,
210
+ isJwtExpired,
211
+ getHeadlessTokenPath,
212
+ getOAuthSessionPath,
213
+ readOAuthSession,
214
+ writeOAuthSession,
215
+ clearOAuthSession,
216
+ readHeadlessToken,
217
+ getAccessTokenExpiryMs,
218
+ hasFreshAccessToken,
219
+ persistAccessToken,
220
+ toEnvSafe,
221
+ writeHeadlessEnvFile
222
+ };