@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.
- package/bin/empirical.js +125 -0
- package/lib/auth.js +804 -0
- package/lib/constants.js +491 -0
- package/lib/deviceConfig.js +94 -0
- package/lib/doc.js +50 -0
- package/lib/doctor.js +116 -0
- package/lib/env.js +49 -0
- package/lib/fsUtils.js +90 -0
- package/lib/help.js +113 -0
- package/lib/http.js +179 -0
- package/lib/installWizard.js +383 -0
- package/lib/installers/claude.js +89 -0
- package/lib/installers/codex.js +78 -0
- package/lib/installers/copilot.js +77 -0
- package/lib/installers/detect.js +47 -0
- package/lib/installers/hooks.js +128 -0
- package/lib/installers/nativePlugin.js +87 -0
- package/lib/installers/promptBlocks.js +403 -0
- package/lib/kitCommands.js +622 -0
- package/lib/memoryCommands.js +528 -0
- package/lib/oauthClient.js +222 -0
- package/lib/pluginCommands.js +81 -0
- package/lib/pluginUpdateCheck.js +143 -0
- package/lib/policyCommands.js +144 -0
- package/lib/scheduledSync.js +331 -0
- package/lib/skillSync.js +508 -0
- package/lib/ui.js +280 -0
- package/lib/workspaceCommands.js +323 -0
- package/lib/workspaceContext.js +76 -0
- package/package.json +25 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { log, warn, uiHeader, uiQuestion, uiSuccess, uiWarn, uiDim, uiLink, ask, askYesNo, parseSelection, formatHeadlessErrorHint } = require('./ui');
|
|
4
|
+
const { getEnv } = require('./env');
|
|
5
|
+
const { summarizeProcessOutput } = require('./fsUtils');
|
|
6
|
+
const {
|
|
7
|
+
EMPIRICAL_MCP_NAME, EMPIRICAL_MCP_URL,
|
|
8
|
+
getCliVersion, deriveBaseUrlFromMcpUrl
|
|
9
|
+
} = require('./constants');
|
|
10
|
+
const {
|
|
11
|
+
readStoredOAuthClient, writeStoredOAuthClient, getLocalOAuthClientPath,
|
|
12
|
+
persistAccessToken, getHeadlessTokenPath
|
|
13
|
+
} = require('./oauthClient');
|
|
14
|
+
const {
|
|
15
|
+
oauthPreflight, ensureHeadlessToken, selfProvisionHeadlessCredentials,
|
|
16
|
+
runMcpOAuthFlow, updateClaudeMcpServerAuth, resolveAccessToken
|
|
17
|
+
} = require('./auth');
|
|
18
|
+
const { performSync, printSyncSummary } = require('./skillSync');
|
|
19
|
+
const {
|
|
20
|
+
INSTALL_PROMPT_OPTIONS_DEFAULTS, collectInstallerPromptOptions,
|
|
21
|
+
} = require('./installers/promptBlocks');
|
|
22
|
+
const { detectInstallTargets } = require('./installers/detect');
|
|
23
|
+
const { installClaudeHooks } = require('./installers/hooks');
|
|
24
|
+
const { runCodex } = require('./installers/codex');
|
|
25
|
+
const { installNativePlugin, OPTIONAL_PLUGIN_NAMES } = require('./installers/nativePlugin');
|
|
26
|
+
const { policyFromInstallerOptions, savePolicy: persistPolicy } = require('./policyCommands');
|
|
27
|
+
|
|
28
|
+
function pluginNamesForInstallerOptions(options = {}) {
|
|
29
|
+
const names = ['empirical-memory'];
|
|
30
|
+
if (options.recordWorkHistory !== false) names.push('empirical-work-history');
|
|
31
|
+
if (options.recordScarTissue !== false) names.push('empirical-scar-tissue');
|
|
32
|
+
if (options.recordTodos === true) names.push('empirical-todo');
|
|
33
|
+
return names.filter((name) => name === 'empirical-memory' || OPTIONAL_PLUGIN_NAMES.includes(name));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runInstallFlow() {
|
|
37
|
+
log(uiHeader(`Empirical installer v${getCliVersion()}`));
|
|
38
|
+
log('');
|
|
39
|
+
|
|
40
|
+
const targets = detectInstallTargets();
|
|
41
|
+
if (targets.length === 0) {
|
|
42
|
+
log('No supported CLI configs detected.');
|
|
43
|
+
log('Supported installers: Codex CLI, GitHub Copilot CLI, Claude CLI.');
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
log(uiHeader('Detected CLIs (all selected by default):'));
|
|
49
|
+
targets.forEach((target, index) => {
|
|
50
|
+
log(` [x] ${index + 1}. ${target.label}`);
|
|
51
|
+
});
|
|
52
|
+
log('');
|
|
53
|
+
|
|
54
|
+
const answer = await ask(uiQuestion('Press Enter for all, or select numbers (e.g. 1,2): '));
|
|
55
|
+
const selectedIndexes = parseSelection(answer, targets.length);
|
|
56
|
+
|
|
57
|
+
if (selectedIndexes.length === 0) {
|
|
58
|
+
log('No valid selection; nothing installed.');
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
log('');
|
|
64
|
+
|
|
65
|
+
// Integration mode selection
|
|
66
|
+
let integrationMode = 1; // default: MCP
|
|
67
|
+
if (process.stdin.isTTY) {
|
|
68
|
+
log(uiQuestion('How should agents interact with Empirical?'));
|
|
69
|
+
log(' 1. Via MCP tool (recommended)');
|
|
70
|
+
log(' 2. Via CLI commands');
|
|
71
|
+
log(' 3. Both (MCP primary, CLI fallback)');
|
|
72
|
+
const modeAnswer = (await ask(uiDim('Enter 1, 2, or 3 [default: 1]: '))).trim();
|
|
73
|
+
integrationMode = modeAnswer === '2' ? 2 : modeAnswer === '3' ? 3 : 1;
|
|
74
|
+
log('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let promptOptions;
|
|
78
|
+
let savePolicyEnabled;
|
|
79
|
+
|
|
80
|
+
if (integrationMode === 2) {
|
|
81
|
+
// CLI-direct mode: skip MCP-specific installer questions
|
|
82
|
+
promptOptions = { ...INSTALL_PROMPT_OPTIONS_DEFAULTS };
|
|
83
|
+
savePolicyEnabled = true;
|
|
84
|
+
} else {
|
|
85
|
+
const promptSetup = await collectInstallerPromptOptions();
|
|
86
|
+
promptOptions = promptSetup.promptOptions;
|
|
87
|
+
savePolicyEnabled = promptSetup.savePolicy !== false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
log(uiHeader('Installer auth setup'));
|
|
91
|
+
log('');
|
|
92
|
+
const installerBaseUrl = getEnv('EMPIRICAL_BASE_URL', '') || deriveBaseUrlFromMcpUrl() || 'https://empirical.gauzza.com';
|
|
93
|
+
if (!getEnv('EMPIRICAL_BASE_URL')) {
|
|
94
|
+
process.env.EMPIRICAL_BASE_URL = installerBaseUrl;
|
|
95
|
+
}
|
|
96
|
+
const useHeadlessDefault = getEnv('EMPIRICAL_HEADLESS_DEFAULT', 'true').toLowerCase() !== 'false';
|
|
97
|
+
const settingsUrl = `${installerBaseUrl.replace(/\/$/, '')}/app/settings`;
|
|
98
|
+
let headlessToken = null;
|
|
99
|
+
let policyAccessToken = null;
|
|
100
|
+
try {
|
|
101
|
+
const existingClient = readStoredOAuthClient();
|
|
102
|
+
if (getEnv('EMPIRICAL_CLIENT_ID')) {
|
|
103
|
+
log('Step 1/2: OAuth client credentials found in environment (EMPIRICAL_CLIENT_ID).');
|
|
104
|
+
} else if (existingClient?.clientId) {
|
|
105
|
+
log(`Step 1/2: OAuth client credentials found at ${getLocalOAuthClientPath()}.`);
|
|
106
|
+
} else {
|
|
107
|
+
if (process.stdin.isTTY) {
|
|
108
|
+
log('Step 1/2: No saved headless credentials found. Enter client id/secret or press Enter to self-provision from your account.');
|
|
109
|
+
const manualClientId = (await ask('OAuth client ID (blank to self-provision): ')).trim();
|
|
110
|
+
if (manualClientId) {
|
|
111
|
+
const manualSecret = (await ask('OAuth client secret (optional): ')).trim();
|
|
112
|
+
writeStoredOAuthClient(manualClientId, manualSecret);
|
|
113
|
+
log(` - Saved OAuth client credentials to ${getLocalOAuthClientPath()}.`);
|
|
114
|
+
} else {
|
|
115
|
+
log(' - Self-provisioning credentials from your account...');
|
|
116
|
+
const provisioned = await selfProvisionHeadlessCredentials({ baseUrl: installerBaseUrl });
|
|
117
|
+
if (provisioned?.clientId) {
|
|
118
|
+
writeStoredOAuthClient(provisioned.clientId, provisioned.clientSecret || '');
|
|
119
|
+
log(` - Saved credentials to ${getLocalOAuthClientPath()}.`);
|
|
120
|
+
} else {
|
|
121
|
+
log(' - Skipped (declined, or no existing session to identify the account). Interactive OAuth will be used when required.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
log('Step 1/2: No saved headless credentials found. Self-provisioning from your account...');
|
|
126
|
+
const provisioned = await selfProvisionHeadlessCredentials({ baseUrl: installerBaseUrl, allowBrowserLogin: false });
|
|
127
|
+
if (provisioned?.clientId) {
|
|
128
|
+
writeStoredOAuthClient(provisioned.clientId, provisioned.clientSecret || '');
|
|
129
|
+
log(` - Saved credentials to ${getLocalOAuthClientPath()}.`);
|
|
130
|
+
} else {
|
|
131
|
+
log(' - Skipped (no existing session/token found to identify the account non-interactively).');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
warn(`Step 1/2 warning: OAuth client provisioning failed: ${error.message}`);
|
|
137
|
+
warn('Continuing install. You can run "empirical oauth client set" manually.');
|
|
138
|
+
}
|
|
139
|
+
log('');
|
|
140
|
+
|
|
141
|
+
let shouldBootstrapHeadless = useHeadlessDefault;
|
|
142
|
+
if (process.stdin.isTTY) {
|
|
143
|
+
log('Optional credential setup:');
|
|
144
|
+
log(' Browser OAuth is the normal choice for interactive CLI use.');
|
|
145
|
+
log(' Headless bootstrap creates reusable credentials for CLI use, automation, containers, and external runtimes.');
|
|
146
|
+
log(' Headless bootstrap uses Settings client credentials (client ID + client secret).');
|
|
147
|
+
log(' Client credentials are loaded from env/saved oauth-client.json, with prompt fallback.');
|
|
148
|
+
log(` Settings: ${uiLink(settingsUrl)}`);
|
|
149
|
+
shouldBootstrapHeadless = await askYesNo('Bootstrap headless token now?', useHeadlessDefault);
|
|
150
|
+
log('');
|
|
151
|
+
} else {
|
|
152
|
+
log(`Settings: ${uiLink(settingsUrl)}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (shouldBootstrapHeadless) {
|
|
156
|
+
let bootstrapAttempts = 0;
|
|
157
|
+
while (bootstrapAttempts < 3) {
|
|
158
|
+
try {
|
|
159
|
+
bootstrapAttempts += 1;
|
|
160
|
+
log(`Headless auth: bootstrapping access token for CLI clients (attempt ${bootstrapAttempts}/3)...`);
|
|
161
|
+
// Step 1/2 above already loaded or self-provisioned credentials into
|
|
162
|
+
// oauth-client.json, so don't force a re-prompt here — that just makes
|
|
163
|
+
// the user paste back the client ID/secret we already have. Only
|
|
164
|
+
// runHeadlessBootstrap's own fallback (no clientId at all) should prompt.
|
|
165
|
+
headlessToken = await ensureHeadlessToken({ promptCredentials: false });
|
|
166
|
+
const persistedToken = headlessToken?.apiKey || headlessToken?.accessToken;
|
|
167
|
+
if (persistedToken) {
|
|
168
|
+
persistAccessToken(persistedToken);
|
|
169
|
+
log(uiSuccess(`Headless token saved at ${getHeadlessTokenPath()}.`));
|
|
170
|
+
}
|
|
171
|
+
log('');
|
|
172
|
+
break;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const hint = formatHeadlessErrorHint(error.message);
|
|
175
|
+
warn(uiWarn(`Headless auth failed: ${error.message}`));
|
|
176
|
+
if (hint) {
|
|
177
|
+
warn(` ${hint}`);
|
|
178
|
+
}
|
|
179
|
+
warn(` Settings: ${settingsUrl}`);
|
|
180
|
+
if (!process.stdin.isTTY || bootstrapAttempts >= 3) {
|
|
181
|
+
warn('Continuing install with interactive OAuth if required.');
|
|
182
|
+
log('');
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const retry = await askYesNo('Retry headless bootstrap now?', true);
|
|
187
|
+
log('');
|
|
188
|
+
if (!retry) {
|
|
189
|
+
warn('Continuing install with interactive OAuth if required.');
|
|
190
|
+
log('');
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
log('Headless auth: skipped by user choice. Interactive OAuth will be used when required.');
|
|
197
|
+
log('');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if ((integrationMode === 2 || integrationMode === 3) && !resolveAccessToken()) {
|
|
201
|
+
try {
|
|
202
|
+
log('CLI auth: no reusable credential found. Opening browser OAuth login...');
|
|
203
|
+
await runMcpOAuthFlow(installerBaseUrl);
|
|
204
|
+
log(uiSuccess('CLI OAuth session saved. Empirical CLI commands are ready to use.'));
|
|
205
|
+
log('');
|
|
206
|
+
} catch (error) {
|
|
207
|
+
warn(uiWarn(`CLI OAuth login failed: ${error.message}`));
|
|
208
|
+
warn(' CLI commands will need a credential later: run "empirical auth login".');
|
|
209
|
+
log('');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (integrationMode !== 2) {
|
|
214
|
+
const preflight = await oauthPreflight(EMPIRICAL_MCP_URL);
|
|
215
|
+
if (!preflight.ok) {
|
|
216
|
+
warn(`OAuth preflight warning: ${preflight.message}`);
|
|
217
|
+
warn('Codex/Claude setup may fail until the hosted OAuth login page is updated.');
|
|
218
|
+
log('');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let failures = 0;
|
|
223
|
+
let claudeInstalled = false;
|
|
224
|
+
let codexInstalled = false;
|
|
225
|
+
let codexNeedsPostLogin = false;
|
|
226
|
+
|
|
227
|
+
for (const index of selectedIndexes) {
|
|
228
|
+
const target = targets[index];
|
|
229
|
+
log(uiHeader(`Step 2/2: Installing for ${target.label}...`));
|
|
230
|
+
try {
|
|
231
|
+
if (integrationMode === 2) {
|
|
232
|
+
// CLI-direct: skip MCP registration; the native plugin owns guidance.
|
|
233
|
+
const pluginNames = pluginNamesForInstallerOptions(promptOptions);
|
|
234
|
+
installNativePlugin(target.id, pluginNames);
|
|
235
|
+
log(` - ${target.label} native plugins installed: ${pluginNames.join(', ')}.`);
|
|
236
|
+
if (target.id === 'claude') {
|
|
237
|
+
try {
|
|
238
|
+
installClaudeHooks();
|
|
239
|
+
} catch (error) {
|
|
240
|
+
warn(` - Could not install Claude hooks for deterministic memory lookups: ${error.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
log(uiSuccess(`Done: ${target.label} (CLI-direct mode)`));
|
|
244
|
+
} else {
|
|
245
|
+
// Mode 1 or 3: run MCP install normally
|
|
246
|
+
const pluginNames = pluginNamesForInstallerOptions(promptOptions);
|
|
247
|
+
const installResult = target.install(headlessToken, promptOptions, {
|
|
248
|
+
writeGlobalInstructions: false,
|
|
249
|
+
pluginNames
|
|
250
|
+
});
|
|
251
|
+
log(uiSuccess(`Done: ${target.label}`));
|
|
252
|
+
if (target.id === 'codex') {
|
|
253
|
+
codexInstalled = true;
|
|
254
|
+
codexNeedsPostLogin = Boolean(installResult?.needsPostLogin);
|
|
255
|
+
}
|
|
256
|
+
if (target.id === 'claude') {
|
|
257
|
+
claudeInstalled = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
failures += 1;
|
|
262
|
+
warn(uiWarn(`Failed: ${target.label} - ${error.message}`));
|
|
263
|
+
}
|
|
264
|
+
log('');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (failures > 0) {
|
|
268
|
+
warn(`Completed with ${failures} failure(s).`);
|
|
269
|
+
process.exitCode = 1;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Authenticate Empirical MCP for Codex automatically after installation.
|
|
274
|
+
if (codexInstalled && codexNeedsPostLogin && process.stdin.isTTY) {
|
|
275
|
+
log(uiHeader('Empirical MCP authentication (Codex)'));
|
|
276
|
+
try {
|
|
277
|
+
log('');
|
|
278
|
+
log(uiQuestion('Authenticating Empirical MCP in Codex...'));
|
|
279
|
+
const loginResult = runCodex(['mcp', 'login', EMPIRICAL_MCP_NAME], { stdio: 'inherit' });
|
|
280
|
+
if (loginResult.status === 0) {
|
|
281
|
+
log(uiSuccess('Empirical MCP authenticated successfully in Codex!'));
|
|
282
|
+
} else {
|
|
283
|
+
const detail = summarizeProcessOutput(loginResult);
|
|
284
|
+
warn(uiWarn(`Codex MCP authentication returned a non-zero status${detail ? `: ${detail}` : '.'}`));
|
|
285
|
+
warn('Re-run `empirical install` to retry automatic authentication.');
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
warn(uiWarn(`Codex MCP authentication failed: ${error.message}`));
|
|
289
|
+
warn('Re-run `empirical install` to retry automatic authentication.');
|
|
290
|
+
}
|
|
291
|
+
log('');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Offer to authenticate Empirical MCP after Claude installation.
|
|
295
|
+
// Prefer the permanent API key from the headless bootstrap (never expires)
|
|
296
|
+
// over a short-lived JWT from the browser OAuth flow.
|
|
297
|
+
if (claudeInstalled && process.stdin.isTTY) {
|
|
298
|
+
log(uiHeader('Empirical MCP authentication'));
|
|
299
|
+
const permanentToken = headlessToken?.apiKey || '';
|
|
300
|
+
if (permanentToken) {
|
|
301
|
+
// Headless bootstrap already ran — write the API key as the Bearer credential.
|
|
302
|
+
// API keys never expire, so Claude sessions stay authenticated indefinitely.
|
|
303
|
+
try {
|
|
304
|
+
updateClaudeMcpServerAuth(EMPIRICAL_MCP_NAME, permanentToken);
|
|
305
|
+
log(uiSuccess('Empirical MCP configured with a permanent API key — no re-auth needed!'));
|
|
306
|
+
} catch (error) {
|
|
307
|
+
warn(uiWarn(`Could not write Claude MCP auth: ${error.message}`));
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
try {
|
|
311
|
+
log('');
|
|
312
|
+
log(uiQuestion('Authenticating Empirical MCP...'));
|
|
313
|
+
const mcpToken = await runMcpOAuthFlow(installerBaseUrl);
|
|
314
|
+
policyAccessToken = mcpToken.accessToken;
|
|
315
|
+
updateClaudeMcpServerAuth(EMPIRICAL_MCP_NAME, mcpToken.accessToken);
|
|
316
|
+
log(uiSuccess('Empirical MCP authenticated successfully in Claude!'));
|
|
317
|
+
log(' Note: this OAuth token expires in ~30 days. Re-run the installer with');
|
|
318
|
+
log(' your Settings client credentials to get a permanent credential instead.');
|
|
319
|
+
} catch (error) {
|
|
320
|
+
warn(uiWarn(`MCP authentication failed: ${error.message}`));
|
|
321
|
+
warn('You can authenticate later from Claude Code by using the /mcp command.');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
log('');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Persist the selected behavior after the installer has had a chance to complete OAuth/MCP
|
|
328
|
+
// authentication. In MCP-only mode, authentication happens after native installation.
|
|
329
|
+
try {
|
|
330
|
+
const accessToken = resolveAccessToken() || policyAccessToken;
|
|
331
|
+
if (accessToken && savePolicyEnabled) {
|
|
332
|
+
await persistPolicy(policyFromInstallerOptions(promptOptions), accessToken);
|
|
333
|
+
log(uiSuccess('Empirical behavior policy saved for native skills.'));
|
|
334
|
+
} else if (accessToken) {
|
|
335
|
+
log('Empirical behavior policy not saved by user choice; native skills will use safe defaults.');
|
|
336
|
+
} else {
|
|
337
|
+
warn('Empirical behavior policy was not saved: no authenticated CLI credential is available yet.');
|
|
338
|
+
}
|
|
339
|
+
log('');
|
|
340
|
+
} catch (error) {
|
|
341
|
+
warn(uiWarn(`Empirical behavior policy could not be saved: ${error.message}`));
|
|
342
|
+
log('');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// US-011: independent of whether any Kit exists or `kit use` has ever been run, also sync
|
|
346
|
+
// the active device's resolved (builtin ∪ Default Kit ∪ active named Kit) Skill set into
|
|
347
|
+
// every detected agent's skill directory now, so a brand-new device gets the Default Kit's
|
|
348
|
+
// authored Skills (if any exist yet — likely none for a fresh account, which is fine; the
|
|
349
|
+
// sync just writes zero non-builtin Skills) immediately. This does NOT touch the
|
|
350
|
+
// Native plugin installation above owns the empirical-memory Skill. The generalized Kit
|
|
351
|
+
// writer below must continue to exclude the builtin placeholder with the same name.
|
|
352
|
+
try {
|
|
353
|
+
const syncAccessToken = resolveAccessToken();
|
|
354
|
+
if (syncAccessToken) {
|
|
355
|
+
log(uiHeader('Syncing Kit skills...'));
|
|
356
|
+
const syncResult = await performSync({
|
|
357
|
+
accessToken: syncAccessToken,
|
|
358
|
+
baseUrl: installerBaseUrl,
|
|
359
|
+
dryRun: false,
|
|
360
|
+
force: false
|
|
361
|
+
});
|
|
362
|
+
printSyncSummary(syncResult, false);
|
|
363
|
+
log('');
|
|
364
|
+
} else {
|
|
365
|
+
warn(' - Skipped Kit skill sync: no access token available.');
|
|
366
|
+
log('');
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
warn(uiWarn(`Kit skill sync failed: ${error.message}`));
|
|
370
|
+
log('');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const completionMessage = integrationMode === 2
|
|
374
|
+
? 'Install complete. Next time you open the selected CLI(s), Empirical CLI commands should be available.'
|
|
375
|
+
: integrationMode === 3
|
|
376
|
+
? 'Install complete. Next time you open the selected CLI(s), Empirical MCP and CLI commands should be available.'
|
|
377
|
+
: 'Install complete. Next time you open the selected CLI(s), Empirical MCP should be available.';
|
|
378
|
+
log(uiSuccess(completionMessage));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
module.exports = {
|
|
382
|
+
runInstallFlow
|
|
383
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { log, warn } = require('../ui');
|
|
4
|
+
const { commandExists, runResolvedCommand, summarizeProcessOutput } = require('../fsUtils');
|
|
5
|
+
const { readStoredOAuthClient } = require('../oauthClient');
|
|
6
|
+
const {
|
|
7
|
+
EMPIRICAL_MCP_NAME, EMPIRICAL_MCP_LEGACY_NAMES, EMPIRICAL_MCP_URL
|
|
8
|
+
} = require('../constants');
|
|
9
|
+
const { INSTALL_PROMPT_OPTIONS_DEFAULTS } = require('./promptBlocks');
|
|
10
|
+
const { installClaudeHooks } = require('./hooks');
|
|
11
|
+
const { installNativePlugin } = require('./nativePlugin');
|
|
12
|
+
|
|
13
|
+
function runClaude(args, options = {}) {
|
|
14
|
+
return runResolvedCommand('claude', args, options);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isClaudeServerConfigured(name) {
|
|
18
|
+
const result = runClaude(['mcp', 'get', name], { stdio: 'ignore' });
|
|
19
|
+
return result.status === 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function installClaude(headlessToken, promptOptions = INSTALL_PROMPT_OPTIONS_DEFAULTS, installOptions = {}) {
|
|
23
|
+
log(' - Claude global guidance file unchanged; native plugin guidance is authoritative.');
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
installClaudeHooks();
|
|
27
|
+
} catch (error) {
|
|
28
|
+
warn(` - Could not install Claude hooks for deterministic memory lookups: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
installNativePlugin('claude', installOptions.pluginNames);
|
|
32
|
+
log(` - Claude native plugins installed: ${(installOptions.pluginNames || ['empirical-memory']).join(', ')}.`);
|
|
33
|
+
|
|
34
|
+
if (!commandExists('claude')) {
|
|
35
|
+
warn(' - Claude CLI not found on PATH. Skipped Claude MCP registration.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const storedClient = readStoredOAuthClient();
|
|
40
|
+
|
|
41
|
+
// Keep the server definition deterministic across canonical and legacy names.
|
|
42
|
+
for (const serverName of [EMPIRICAL_MCP_NAME, ...EMPIRICAL_MCP_LEGACY_NAMES]) {
|
|
43
|
+
runClaude(['mcp', 'remove', serverName], { stdio: 'ignore' });
|
|
44
|
+
runClaude(['mcp', 'remove', serverName, '-s', 'user'], { stdio: 'ignore' });
|
|
45
|
+
runClaude(['mcp', 'remove', serverName, '-s', 'local'], { stdio: 'ignore' });
|
|
46
|
+
runClaude(['mcp', 'remove', serverName, '-s', 'project'], { stdio: 'ignore' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let lastFailureSummary = '';
|
|
50
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
51
|
+
if (attempt > 1) {
|
|
52
|
+
log(' - Retrying Claude MCP registration...');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const addArgs = [
|
|
56
|
+
'mcp',
|
|
57
|
+
'add',
|
|
58
|
+
'--scope',
|
|
59
|
+
'user',
|
|
60
|
+
'--transport',
|
|
61
|
+
'http',
|
|
62
|
+
EMPIRICAL_MCP_NAME,
|
|
63
|
+
EMPIRICAL_MCP_URL
|
|
64
|
+
];
|
|
65
|
+
if (storedClient?.clientId) {
|
|
66
|
+
addArgs.push('--client-id', storedClient.clientId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const addResult = runClaude(addArgs, { stdio: attempt === 1 ? 'inherit' : 'pipe' });
|
|
70
|
+
if (addResult.status === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
lastFailureSummary = summarizeProcessOutput(addResult);
|
|
74
|
+
|
|
75
|
+
if (isClaudeServerConfigured(EMPIRICAL_MCP_NAME)) {
|
|
76
|
+
warn(' - Claude returned a non-zero status, but MCP server is present. Continuing.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const detail = lastFailureSummary ? ` Details: ${lastFailureSummary}` : '';
|
|
82
|
+
throw new Error(`claude mcp add failed after retry.${detail}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
runClaude,
|
|
87
|
+
isClaudeServerConfigured,
|
|
88
|
+
installClaude
|
|
89
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { log, warn } = require('../ui');
|
|
4
|
+
const { commandExists, runResolvedCommand, summarizeProcessOutput } = require('../fsUtils');
|
|
5
|
+
const { persistAccessToken } = require('../oauthClient');
|
|
6
|
+
const {
|
|
7
|
+
EMPIRICAL_MCP_NAME, EMPIRICAL_MCP_LEGACY_NAMES, EMPIRICAL_MCP_URL
|
|
8
|
+
} = require('../constants');
|
|
9
|
+
const { INSTALL_PROMPT_OPTIONS_DEFAULTS } = require('./promptBlocks');
|
|
10
|
+
const { installNativePlugin } = require('./nativePlugin');
|
|
11
|
+
|
|
12
|
+
function runCodex(args, options = {}) {
|
|
13
|
+
return runResolvedCommand('codex', args, options);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isCodexServerConfigured(name) {
|
|
17
|
+
const result = runCodex(['mcp', 'get', name], { stdio: 'ignore' });
|
|
18
|
+
return result.status === 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function installCodex(headlessToken, promptOptions = INSTALL_PROMPT_OPTIONS_DEFAULTS, installOptions = {}) {
|
|
22
|
+
log(' - Codex global AGENTS instructions unchanged; native plugin guidance is authoritative.');
|
|
23
|
+
|
|
24
|
+
installNativePlugin('codex', installOptions.pluginNames);
|
|
25
|
+
log(` - Codex native plugins installed: ${(installOptions.pluginNames || ['empirical-memory']).join(', ')}.`);
|
|
26
|
+
|
|
27
|
+
if (!commandExists('codex')) {
|
|
28
|
+
warn(' - Codex CLI not found on PATH. Skipped Codex MCP registration.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const serverName of [EMPIRICAL_MCP_NAME, ...EMPIRICAL_MCP_LEGACY_NAMES]) {
|
|
33
|
+
if (isCodexServerConfigured(serverName)) {
|
|
34
|
+
runCodex(['mcp', 'remove', serverName], { stdio: 'ignore' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const addArgs = ['mcp', 'add', EMPIRICAL_MCP_NAME, '--url', EMPIRICAL_MCP_URL];
|
|
39
|
+
const permanentToken = headlessToken?.apiKey || '';
|
|
40
|
+
if (permanentToken) {
|
|
41
|
+
persistAccessToken(permanentToken);
|
|
42
|
+
addArgs.push('--bearer-token-env-var', 'EMPIRICAL_ACCESS_TOKEN');
|
|
43
|
+
log(' - Registering Codex MCP server with permanent environment-backed authentication...');
|
|
44
|
+
} else {
|
|
45
|
+
log(' - Registering Codex MCP server (OAuth flow may open in browser)...');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let lastFailureSummary = '';
|
|
49
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
50
|
+
if (attempt > 1) {
|
|
51
|
+
log(' - Retrying Codex MCP registration...');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const addResult = runCodex(
|
|
55
|
+
addArgs,
|
|
56
|
+
{ stdio: attempt === 1 ? 'inherit' : 'pipe' }
|
|
57
|
+
);
|
|
58
|
+
if (addResult.status === 0) {
|
|
59
|
+
return { needsPostLogin: false };
|
|
60
|
+
}
|
|
61
|
+
lastFailureSummary = summarizeProcessOutput(addResult);
|
|
62
|
+
|
|
63
|
+
// Codex OAuth flow can return non-zero even when the server is persisted.
|
|
64
|
+
if (isCodexServerConfigured(EMPIRICAL_MCP_NAME)) {
|
|
65
|
+
warn(' - Codex returned a non-zero status, but MCP server is present. Continuing.');
|
|
66
|
+
return { needsPostLogin: true };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const detail = lastFailureSummary ? ` Details: ${lastFailureSummary}` : '';
|
|
71
|
+
throw new Error(`codex mcp add failed after retry.${detail}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
runCodex,
|
|
76
|
+
isCodexServerConfigured,
|
|
77
|
+
installCodex
|
|
78
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { log, warn } = require('../ui');
|
|
6
|
+
const { ensureDir, readJsonIfExists, readUtf8IfExists, writeFile } = require('../fsUtils');
|
|
7
|
+
const { getEnv } = require('../env');
|
|
8
|
+
const { readStoredOAuthClient, getLocalOAuthClientPath } = require('../oauthClient');
|
|
9
|
+
const {
|
|
10
|
+
EMPIRICAL_MCP_NAME, EMPIRICAL_MCP_LEGACY_NAMES, EMPIRICAL_MCP_URL, COPILOT_OAUTH_CLIENT_ID
|
|
11
|
+
} = require('../constants');
|
|
12
|
+
const { INSTALL_PROMPT_OPTIONS_DEFAULTS } = require('./promptBlocks');
|
|
13
|
+
const { installNativePlugin } = require('./nativePlugin');
|
|
14
|
+
|
|
15
|
+
function installCopilot(headlessToken, promptOptions = INSTALL_PROMPT_OPTIONS_DEFAULTS, installOptions = {}) {
|
|
16
|
+
const home = os.homedir();
|
|
17
|
+
const copilotDir = path.join(home, '.copilot');
|
|
18
|
+
const mcpConfigPath = path.join(copilotDir, 'mcp-config.json');
|
|
19
|
+
const instructionsPath = path.join(copilotDir, 'copilot-instructions.md');
|
|
20
|
+
|
|
21
|
+
ensureDir(copilotDir);
|
|
22
|
+
|
|
23
|
+
const mcpConfig = readJsonIfExists(mcpConfigPath) || {};
|
|
24
|
+
if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== 'object') {
|
|
25
|
+
mcpConfig.mcpServers = {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const legacyName of EMPIRICAL_MCP_LEGACY_NAMES) {
|
|
29
|
+
if (legacyName && legacyName in mcpConfig.mcpServers) {
|
|
30
|
+
delete mcpConfig.mcpServers[legacyName];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const storedClient = readStoredOAuthClient();
|
|
35
|
+
const oauthClientId = getEnv(
|
|
36
|
+
'EMPIRICAL_COPILOT_OAUTH_CLIENT_ID',
|
|
37
|
+
storedClient?.clientId || COPILOT_OAUTH_CLIENT_ID
|
|
38
|
+
);
|
|
39
|
+
const oauthClientSecret = getEnv(
|
|
40
|
+
'EMPIRICAL_COPILOT_OAUTH_CLIENT_SECRET',
|
|
41
|
+
storedClient?.clientSecret || ''
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
mcpConfig.mcpServers[EMPIRICAL_MCP_NAME] = {
|
|
45
|
+
oauthPublicClient: !oauthClientSecret,
|
|
46
|
+
type: 'http',
|
|
47
|
+
tools: ['*'],
|
|
48
|
+
url: EMPIRICAL_MCP_URL,
|
|
49
|
+
oauthClientId
|
|
50
|
+
};
|
|
51
|
+
if (oauthClientSecret) {
|
|
52
|
+
mcpConfig.mcpServers[EMPIRICAL_MCP_NAME].oauthClientSecret = oauthClientSecret;
|
|
53
|
+
} else if ('oauthClientSecret' in mcpConfig.mcpServers[EMPIRICAL_MCP_NAME]) {
|
|
54
|
+
delete mcpConfig.mcpServers[EMPIRICAL_MCP_NAME].oauthClientSecret;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if ('httpHeaders' in mcpConfig.mcpServers[EMPIRICAL_MCP_NAME]) {
|
|
58
|
+
delete mcpConfig.mcpServers[EMPIRICAL_MCP_NAME].httpHeaders;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
writeFile(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`);
|
|
62
|
+
|
|
63
|
+
log(' - Copilot global instructions unchanged; native plugin guidance is authoritative.');
|
|
64
|
+
|
|
65
|
+
installNativePlugin('copilot', installOptions.pluginNames);
|
|
66
|
+
log(` - Copilot native plugins installed: ${(installOptions.pluginNames || ['empirical-memory']).join(', ')}.`);
|
|
67
|
+
|
|
68
|
+
log(' - Copilot MCP config updated.');
|
|
69
|
+
if (storedClient?.clientId) {
|
|
70
|
+
log(` - Copilot OAuth client loaded from ${getLocalOAuthClientPath()}.`);
|
|
71
|
+
}
|
|
72
|
+
log(' - Copilot OAuth is typically completed on first MCP use (no standalone `copilot mcp login` command).');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
installCopilot
|
|
77
|
+
};
|
|
@@ -0,0 +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
|
+
};
|