@empirical-gauzza/cli 1.0.9

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.
@@ -0,0 +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
+ };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const { log, warn, uiHeader, uiSuccess } = require('./ui');
4
+ const { detectInstallTargets } = require('./installers/detect');
5
+ const {
6
+ installNativePlugin,
7
+ OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo']
8
+ } = require('./installers/nativePlugin');
9
+ const { checkNativePluginUpdates } = require('./pluginUpdateCheck');
10
+
11
+ const SUPPORTED_TARGETS = ['codex', 'copilot', 'claude'];
12
+
13
+ function isPluginCommand(args = []) {
14
+ return args[0] === 'plugin';
15
+ }
16
+
17
+ function resolveTargets(targetArg) {
18
+ if (targetArg === 'all') return SUPPORTED_TARGETS;
19
+ if (targetArg && SUPPORTED_TARGETS.includes(targetArg)) return [targetArg];
20
+ if (targetArg) {
21
+ throw new Error(`Unsupported plugin target "${targetArg}". Use codex, copilot, claude, or all.`);
22
+ }
23
+
24
+ const detected = detectInstallTargets().map((target) => target.id);
25
+ if (detected.length === 0) {
26
+ throw new Error('No supported CLI detected. Specify a target explicitly: codex, copilot, claude, or all.');
27
+ }
28
+ return detected;
29
+ }
30
+
31
+ function resolvePluginNames(args = []) {
32
+ const names = ['empirical-memory'];
33
+ if (args.includes('--all-optional') || args.includes('--work-history')) names.push('empirical-work-history');
34
+ if (args.includes('--all-optional') || args.includes('--scar-tissue')) names.push('empirical-scar-tissue');
35
+ if (args.includes('--all-optional') || args.includes('--todo')) names.push('empirical-todo');
36
+ return names.filter((name) => name === 'empirical-memory' || OPTIONAL_PLUGIN_NAMES.includes(name));
37
+ }
38
+
39
+ async function runPluginCommand(args = []) {
40
+ const action = args[0];
41
+ if (action === 'check') {
42
+ const result = await checkNativePluginUpdates({
43
+ force: args.includes('--force'),
44
+ interactive: Boolean(process.stdin.isTTY)
45
+ });
46
+ if (result.skipped && result.reason === 'rate_limited') {
47
+ log('Native plugin update check was recently completed.');
48
+ } else if (result.outdated?.length) {
49
+ log(uiSuccess('Native plugin update check complete.'));
50
+ } else if (!result.skipped) {
51
+ log('Native plugins are up to date.');
52
+ }
53
+ return;
54
+ }
55
+ if (action !== 'install') {
56
+ throw new Error('Usage: empirical plugin install <codex|copilot|claude|all> or empirical plugin check [--force]');
57
+ }
58
+
59
+ const targets = resolveTargets(args[1]);
60
+ const pluginNames = resolvePluginNames(args.slice(2));
61
+ log(uiHeader('Empirical native plugin installer'));
62
+ log('');
63
+ for (const target of targets) {
64
+ log(`Installing empirical-memory for ${target}...`);
65
+ if (pluginNames.length === 1) installNativePlugin(target);
66
+ else installNativePlugin(target, pluginNames);
67
+ log(uiSuccess(`${target} plugins installed through its marketplace: ${pluginNames.join(', ')}.`));
68
+ }
69
+ log('');
70
+ log('Native plugin installation does not configure MCP or credentials.');
71
+ log('Optional packages: --work-history, --scar-tissue, --todo, or --all-optional.');
72
+ log('Use `empirical auth login` for normal CLI OAuth, or the full `empirical install` flow for MCP setup.');
73
+ }
74
+
75
+ module.exports = {
76
+ SUPPORTED_TARGETS,
77
+ isPluginCommand,
78
+ resolveTargets,
79
+ resolvePluginNames,
80
+ runPluginCommand
81
+ };
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ const { askYesNo, ask, log, warn, uiHeader, parseSelection } = require('./ui');
4
+ const { runResolvedCommand, commandExists, summarizeProcessOutput } = require('./fsUtils');
5
+ const { readDeviceConfig, writeDeviceConfig } = require('./deviceConfig');
6
+ const { installNativePlugin } = require('./installers/nativePlugin');
7
+
8
+ const PLUGIN_METADATA_URL = 'https://raw.githubusercontent.com/mattgauzza/empirical-memory-plugin/main/.claude-plugin/marketplace.json';
9
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
10
+ const SUPPORTED_TARGETS = ['codex', 'claude', 'copilot'];
11
+
12
+ function parseVersion(value) {
13
+ const match = String(value || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/i);
14
+ return match ? match.slice(1).map(Number) : null;
15
+ }
16
+
17
+ function compareVersions(left, right) {
18
+ const a = parseVersion(left);
19
+ const b = parseVersion(right);
20
+ if (!a || !b) return null;
21
+ for (let i = 0; i < 3; i += 1) {
22
+ if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1;
23
+ }
24
+ return 0;
25
+ }
26
+
27
+ function extractVersion(output, pluginName = 'empirical-memory') {
28
+ const text = String(output || '');
29
+ const jsonCandidates = [text, ...text.split(/\r?\n/).map((line) => line.trim())];
30
+ for (const candidate of jsonCandidates) {
31
+ try {
32
+ const value = JSON.parse(candidate);
33
+ const rows = Array.isArray(value) ? value : [value, ...(Array.isArray(value?.plugins) ? value.plugins : [])];
34
+ const match = rows.find((row) => row && (row.name === pluginName || row.plugin === pluginName));
35
+ if (match?.version) return String(match.version);
36
+ } catch (_error) {
37
+ // Native CLIs may print a table instead of JSON.
38
+ }
39
+ }
40
+ const line = text.split(/\r?\n/).find((value) => value.toLowerCase().includes(pluginName.toLowerCase()));
41
+ const match = line && line.match(new RegExp(`${pluginName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^\\d]*(\\d+\\.\\d+\\.\\d+)`, 'i'));
42
+ return match ? match[1] : null;
43
+ }
44
+
45
+ function readInstalledVersions(target, pluginNames) {
46
+ const commands = {
47
+ codex: ['codex', ['plugin', 'list']],
48
+ claude: ['claude', ['plugin', 'list']],
49
+ copilot: ['copilot', ['plugin', 'list']]
50
+ };
51
+ const [command, args] = commands[target];
52
+ if (!commandExists(command)) return null;
53
+ const result = runResolvedCommand(command, args, { stdio: 'pipe' });
54
+ if (!result || result.status !== 0) return null;
55
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
56
+ return Object.fromEntries(pluginNames.map((name) => [name, extractVersion(output, name)]));
57
+ }
58
+
59
+ async function fetchLatestVersions(fetchImpl = globalThis.fetch) {
60
+ if (typeof fetchImpl !== 'function') return null;
61
+ const response = await fetchImpl(PLUGIN_METADATA_URL, { signal: AbortSignal.timeout(5000) });
62
+ if (!response.ok) throw new Error(`marketplace metadata returned HTTP ${response.status}`);
63
+ const data = await response.json();
64
+ if (Array.isArray(data?.plugins)) {
65
+ return Object.fromEntries(data.plugins.map((plugin) => [plugin.name, plugin.version]).filter(([, version]) => version));
66
+ }
67
+ return data?.version ? { 'empirical-memory': data.version } : {};
68
+ }
69
+
70
+ function shouldCheck(cache, now = Date.now(), force = false) {
71
+ return force || !cache?.checkedAt || now - Number(cache.checkedAt) >= CHECK_INTERVAL_MS;
72
+ }
73
+
74
+ async function checkNativePluginUpdates({ force = false, interactive = process.stdin.isTTY, fetchImpl } = {}) {
75
+ if (!interactive && !force) return { skipped: true, reason: 'noninteractive' };
76
+ const config = readDeviceConfig();
77
+ const cache = config.pluginUpdates || {};
78
+ if (!shouldCheck(cache, Date.now(), force)) return { skipped: true, reason: 'rate_limited' };
79
+
80
+ let latestVersion;
81
+ try {
82
+ latestVersion = await fetchLatestVersions(fetchImpl);
83
+ } catch (error) {
84
+ config.pluginUpdates = { ...cache, checkedAt: Date.now(), lastError: error.message };
85
+ writeDeviceConfig(config);
86
+ return { skipped: true, reason: 'marketplace_unavailable' };
87
+ }
88
+ const pluginNames = Object.keys(latestVersion);
89
+ const installed = {};
90
+ for (const target of SUPPORTED_TARGETS) installed[target] = readInstalledVersions(target, pluginNames);
91
+ config.pluginUpdates = { checkedAt: Date.now(), latestVersions: latestVersion, installed };
92
+ writeDeviceConfig(config);
93
+
94
+ const outdated = SUPPORTED_TARGETS.flatMap((target) => pluginNames
95
+ .filter((name) => compareVersions(latestVersion[name], installed[target][name]) === 1)
96
+ .map((name) => ({ target, name, installed: installed[target][name], latest: latestVersion[name] })));
97
+ const latestSignature = JSON.stringify(latestVersion);
98
+ if (outdated.length === 0 || cache.offeredVersion === latestSignature) {
99
+ return { latestVersion, installed, outdated };
100
+ }
101
+
102
+ if (!interactive) {
103
+ return { latestVersion, installed, outdated, promptSuppressed: true };
104
+ }
105
+
106
+ log(uiHeader(`A newer empirical-memory plugin is available (${latestVersion}).`));
107
+ log(`Installed versions: ${outdated.map((item) => `${item.target} ${item.name} ${item.installed} -> ${item.latest}`).join(', ')}`);
108
+ const update = await askYesNo('Update the native plugin now?', true);
109
+ if (!update) {
110
+ config.pluginUpdates = { ...config.pluginUpdates, offeredVersion: latestSignature, outcome: 'declined' };
111
+ writeDeviceConfig(config);
112
+ return { latestVersion, installed, outdated, declined: true };
113
+ }
114
+ const selection = (await ask('Which agents should be updated? (all, or numbers 1-' + outdated.length + ') ')).trim().toLowerCase();
115
+ const outdatedTargets = [...new Set(outdated.map((item) => item.target))];
116
+ const selectedTargets = selection === 'all' || !selection
117
+ ? outdatedTargets
118
+ : parseSelection(selection, outdatedTargets.length).map((index) => outdatedTargets[index]);
119
+ const results = {};
120
+ for (const target of selectedTargets) {
121
+ try {
122
+ installNativePlugin(target, [...new Set(outdated.filter((item) => item.target === target).map((item) => item.name))]);
123
+ results[target] = 'updated';
124
+ } catch (error) {
125
+ results[target] = `failed: ${summarizeProcessOutput({ stderr: error.message })}`;
126
+ warn(`Could not update ${target}: ${error.message}`);
127
+ }
128
+ }
129
+ config.pluginUpdates = { ...config.pluginUpdates, offeredVersion: latestSignature, outcome: results };
130
+ writeDeviceConfig(config);
131
+ return { latestVersion, installed, outdated, results };
132
+ }
133
+
134
+ module.exports = {
135
+ CHECK_INTERVAL_MS,
136
+ SUPPORTED_TARGETS,
137
+ parseVersion,
138
+ compareVersions,
139
+ extractVersion,
140
+ shouldCheck,
141
+ fetchLatestVersions,
142
+ checkNativePluginUpdates
143
+ };
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ const { log, warn } = require('./ui');
4
+ const { httpJson, printApiError } = require('./http');
5
+ const { parseOption } = require('./env');
6
+ const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
7
+ const { readPolicyCache, writePolicyCache } = require('./deviceConfig');
8
+
9
+ const POLICY_CACHE_TTL_MS = 15 * 60 * 1000;
10
+
11
+ function policyUrl(baseUrl) {
12
+ return new URL('/api/v1/users/me/empirical-policy', baseUrl).toString();
13
+ }
14
+
15
+ function isPolicyCacheFresh(cache, now = Date.now()) {
16
+ return Boolean(
17
+ cache &&
18
+ cache.policy &&
19
+ cache.updatedAt &&
20
+ now - new Date(cache.cachedAt || 0).getTime() < POLICY_CACHE_TTL_MS
21
+ );
22
+ }
23
+
24
+ async function fetchPolicy({ force = false } = {}) {
25
+ const cached = readPolicyCache();
26
+ if (!force && isPolicyCacheFresh(cached)) return { ...cached, source: 'cache' };
27
+
28
+ const accessToken = requireAccessToken();
29
+ const response = await httpJson(
30
+ policyUrl(resolveApiBaseUrl()),
31
+ 'GET',
32
+ null,
33
+ buildApiAuthHeaders(accessToken)
34
+ );
35
+ const data = response?.data || response;
36
+ const next = {
37
+ policy: data.policy || {},
38
+ policyVersion: data.policyVersion || 1,
39
+ updatedAt: data.updatedAt || null,
40
+ cachedAt: new Date().toISOString()
41
+ };
42
+ writePolicyCache(next);
43
+ return { ...next, source: 'remote' };
44
+ }
45
+
46
+ function policyFromInstallerOptions(options = {}) {
47
+ return {
48
+ memoryFirstWorkflows: options.requireMemoryInWorkflows === false
49
+ ? []
50
+ : (options.requiredWorkflows || []),
51
+ allowDiscretionaryUse: options.allowDiscretionaryMemoryUsage !== false,
52
+ stopOnMemoryFailure: options.stopOnMemoryToolFailure !== false,
53
+ rememberCodingStyle: options.rememberCodingStyle !== false,
54
+ rememberArchitectureDecisions: options.rememberArchitectureDecisions !== false,
55
+ recordWorkHistory: options.recordWorkHistory !== false,
56
+ recordScarTissue: options.recordScarTissue !== false,
57
+ recordTodos: options.recordTodos === true,
58
+ storeInstructionArtifacts: options.storeInstructionArtifacts === true,
59
+ checkMemoryBeforeAskingContext: options.checkMemoryBeforeAskingContext !== false,
60
+ durableWritesOnly: options.durableWritesOnly !== false
61
+ };
62
+ }
63
+
64
+ async function savePolicy(policy, accessTokenOverride = null) {
65
+ const accessToken = accessTokenOverride || requireAccessToken();
66
+ const response = await httpJson(
67
+ policyUrl(resolveApiBaseUrl()),
68
+ 'PATCH',
69
+ { policy },
70
+ buildApiAuthHeaders(accessToken)
71
+ );
72
+ const data = response?.data || response;
73
+ writePolicyCache({
74
+ policy: data.policy || {},
75
+ policyVersion: data.policyVersion || 1,
76
+ updatedAt: data.updatedAt || null,
77
+ cachedAt: new Date().toISOString()
78
+ });
79
+ return data;
80
+ }
81
+
82
+ async function runPolicyCommand(args = []) {
83
+ const subcommand = args[0] || 'show';
84
+ if (subcommand === 'show') {
85
+ try {
86
+ const result = await fetchPolicy({ force: args.includes('--refresh') });
87
+ log(JSON.stringify(result, null, 2));
88
+ } catch (error) {
89
+ printApiError(error);
90
+ process.exitCode = 1;
91
+ }
92
+ return;
93
+ }
94
+
95
+ if (subcommand === 'set') {
96
+ const raw = parseOption(args, '--json', '');
97
+ if (!raw) {
98
+ warn('Usage: empirical policy set --json \'{"allowDiscretionaryUse":false}\'');
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+ let policy;
103
+ try {
104
+ policy = JSON.parse(raw);
105
+ } catch (error) {
106
+ warn(`Invalid policy JSON: ${error.message}`);
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+ try {
111
+ const accessToken = requireAccessToken();
112
+ const response = await httpJson(
113
+ policyUrl(resolveApiBaseUrl()),
114
+ 'PATCH',
115
+ { policy },
116
+ buildApiAuthHeaders(accessToken)
117
+ );
118
+ const data = response?.data || response;
119
+ writePolicyCache({
120
+ policy: data.policy || {},
121
+ policyVersion: data.policyVersion || 1,
122
+ updatedAt: data.updatedAt || null,
123
+ cachedAt: new Date().toISOString()
124
+ });
125
+ log(JSON.stringify(data, null, 2));
126
+ } catch (error) {
127
+ printApiError(error);
128
+ process.exitCode = 1;
129
+ }
130
+ return;
131
+ }
132
+
133
+ warn('Usage: empirical policy <show [--refresh]|set --json <policy>>');
134
+ process.exitCode = 1;
135
+ }
136
+
137
+ module.exports = {
138
+ POLICY_CACHE_TTL_MS,
139
+ isPolicyCacheFresh,
140
+ policyFromInstallerOptions,
141
+ fetchPolicy,
142
+ savePolicy,
143
+ runPolicyCommand
144
+ };