@xmemo/client 0.4.135 → 0.4.136

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.
Files changed (5) hide show
  1. package/LICENSE +7 -7
  2. package/README.md +393 -329
  3. package/bin/memory-os.js +12 -12
  4. package/package.json +46 -46
  5. package/src/cli.js +2406 -2244
package/src/cli.js CHANGED
@@ -1,2244 +1,2406 @@
1
- import fs from 'node:fs/promises';
2
- import http from 'node:http';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { spawn } from 'node:child_process';
6
- import { randomUUID } from 'node:crypto';
7
-
8
- const PRODUCT_NAME = 'XMemo';
9
- const PACKAGE_NAME = '@xmemo/client';
10
- const FALLBACK_PACKAGE_NAME = '@yonro/xmemo-client';
11
- const COMMAND_NAME = 'xmemo';
12
- const LEGACY_COMMAND_NAME = 'memory-os';
13
- const CLI_VERSION = '0.4.135';
14
- const DEFAULT_SERVICE_URL = 'https://xmemo.dev';
15
- const TOKEN_ENV_VAR = 'XMEMO_KEY';
16
- const LEGACY_TOKEN_ENV_VAR = 'MEMORY_OS_MCP_TOKEN';
17
- const AGENT_ID_ENV_VAR = 'XMEMO_AGENT_ID';
18
- const AGENT_INSTANCE_ENV_VAR = 'XMEMO_AGENT_INSTANCE_ID';
19
- const AGENT_ID_HEADER = 'X-Memory-OS-Agent-ID';
20
- const AGENT_INSTANCE_HEADER = 'X-Memory-OS-Agent-Instance-ID';
21
- const MCP_SERVER_NAME = 'memory_os';
22
- const CODEX_PROFILE_TARGET = 'AGENTS.md';
23
- const CODEX_PROFILE_MARKER_START = '<!-- memory-os:codex-profile:start -->';
24
- const CODEX_PROFILE_MARKER_END = '<!-- memory-os:codex-profile:end -->';
25
- const DEVICE_LOGIN_START_PATH = '/api/v1/auth/device/start';
26
- const DEVICE_LOGIN_TOKEN_PATH = '/api/v1/auth/device/token';
27
- const DEFAULT_PROXY_HOST = '127.0.0.1';
28
- const DEFAULT_PROXY_PORT = 8765;
29
-
30
- const MCP_CLIENTS = new Map([
31
- ['codex', {
32
- label: 'Codex',
33
- defaultConfigPath: defaultCodexConfigPath,
34
- buildSnippet: codexTomlSnippet,
35
- writeConfig: appendTomlServerConfig,
36
- configKind: 'toml'
37
- }],
38
- ['cursor', {
39
- label: 'Cursor',
40
- defaultConfigPath: defaultCursorConfigPath,
41
- buildSnippet: cursorJsonSnippet,
42
- writeConfig: mergeJsonMcpConfig,
43
- configKind: 'json'
44
- }]
45
- ]);
46
-
47
- const SETUP_CLIENT_ALIASES = new Map([
48
- ['codex', 'codex'],
49
- ['cursor', 'cursor'],
50
- ['copilot', 'copilot-cli'],
51
- ['copilot-cli', 'copilot-cli']
52
- ]);
53
-
54
- class UsageError extends Error {
55
- constructor(message) {
56
- super(message);
57
- this.name = 'UsageError';
58
- }
59
- }
60
-
61
- export async function run(args, io = defaultIo()) {
62
- try {
63
- const command = args[0] ?? 'help';
64
-
65
- if (command === '--help' || command === '-h' || command === 'help') {
66
- writeHelp(io);
67
- return 0;
68
- }
69
-
70
- if (command === '--version' || command === '-v' || command === 'version') {
71
- writeLine(io.stdout, CLI_VERSION);
72
- return 0;
73
- }
74
-
75
- if (command === 'update' || command === '--update') {
76
- return await updateCommand(args.slice(1), io);
77
- }
78
-
79
- if (command === 'doctor') {
80
- return await doctorCommand(args.slice(1), io);
81
- }
82
-
83
- if (command === 'discovery') {
84
- return await discoveryCommand(args.slice(1), io);
85
- }
86
-
87
- if (command === 'status') {
88
- return await statusCommand(args.slice(1), io);
89
- }
90
-
91
- if (command === 'setup') {
92
- return await setupCommand(args.slice(1), io);
93
- }
94
-
95
- if (command === 'login') {
96
- return await loginCommand(args.slice(1), io);
97
- }
98
-
99
- if (command === 'auth') {
100
- return await authCommand(args.slice(1), io);
101
- }
102
-
103
- if (command === 'token') {
104
- return await tokenCommand(args.slice(1), io);
105
- }
106
-
107
- if (command === 'mcp') {
108
- return await mcpCommand(args.slice(1), io);
109
- }
110
-
111
- if (command === 'profile') {
112
- return await profileCommand(args.slice(1), io);
113
- }
114
-
115
- if (command === 'smoke') {
116
- return await smokeCommand(args.slice(1), io);
117
- }
118
-
119
- if (command === 'env') {
120
- return envCommand(args.slice(1), io);
121
- }
122
-
123
- if (command === 'privacy') {
124
- writePrivacy(io);
125
- return 0;
126
- }
127
-
128
- throw new UsageError(`Unknown command: ${command}`);
129
- } catch (error) {
130
- if (error instanceof UsageError) {
131
- writeLine(io.stderr, `Error: ${error.message}`);
132
- writeLine(io.stderr, `Run \`${COMMAND_NAME} help\` for usage.`);
133
- return 2;
134
- }
135
-
136
- writeLine(io.stderr, `Unexpected error: ${error.message}`);
137
- return 1;
138
- }
139
- }
140
-
141
- function defaultIo() {
142
- return {
143
- env: process.env,
144
- stdin: process.stdin,
145
- stdout: process.stdout,
146
- stderr: process.stderr,
147
- fetch: globalThis.fetch,
148
- spawn
149
- };
150
- }
151
-
152
- function writeHelp(io) {
153
- writeLine(io.stdout, `${PRODUCT_NAME} CLI (${PACKAGE_NAME})`);
154
- writeLine(io.stdout, `Fallback npm package: ${FALLBACK_PACKAGE_NAME}; legacy command alias: ${LEGACY_COMMAND_NAME}`);
155
- writeLine(io.stdout, '');
156
- writeLine(io.stdout, 'Usage:');
157
- writeLine(io.stdout, ` ${COMMAND_NAME} update [--dry-run] [--json]`);
158
- writeLine(io.stdout, ` ${COMMAND_NAME} doctor [--base-url <https://api.example.com>] [--json]`);
159
- writeLine(io.stdout, ` ${COMMAND_NAME} discovery show [--base-url <https://api.example.com>] [--json]`);
160
- writeLine(io.stdout, ` ${COMMAND_NAME} setup <codex|cursor|copilot> [--url <https://api.example.com>] [--dry-run] [--json]`);
161
- writeLine(io.stdout, ` ${COMMAND_NAME} login [--from-stdin] [--base-url <url>] [--timeout-ms <ms>] [--http-timeout-ms <ms>] [--json]`);
162
- writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
163
- writeLine(io.stdout, ` ${COMMAND_NAME} status [--url <https://api.example.com>] [--json]`);
164
- writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify]`);
165
- writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
166
- writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
167
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
168
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|generic> [--base-url <url>] [--json]`);
169
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}]`);
170
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp profile codex [--json]`);
171
- writeLine(io.stdout, ` ${COMMAND_NAME} profile install codex [--target AGENTS.md] [--dry-run|--json]`);
172
- writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target AGENTS.md] [--json]`);
173
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <codex|cursor> [--url <https://api.example.com>] [--write] [--config <path>]`);
174
- writeLine(io.stdout, ` ${COMMAND_NAME} smoke --client codex [--config <path>] [--json]`);
175
- writeLine(io.stdout, ` ${COMMAND_NAME} env example [--shell bash|powershell|cmd] [--json]`);
176
- writeLine(io.stdout, ` ${COMMAND_NAME} privacy`);
177
- writeLine(io.stdout, '');
178
- writeLine(io.stdout, `Default service URL: ${DEFAULT_SERVICE_URL}; use --url or XMEMO_URL for private deployments.`);
179
- writeLine(io.stdout, '`login --timeout-ms` controls the full browser approval window; HTTP calls use `--http-timeout-ms`.');
180
- writeLine(io.stdout, '');
181
- writeLine(io.stdout, 'Privacy defaults: no telemetry, no token in project files, and no token is sent by `status`, `doctor`, or `discovery`.');
182
- writeLine(io.stdout, '`login` and `token add` store credentials only in the user-scoped XMemo CLI config directory.');
183
- }
184
-
185
- async function updateCommand(args, io) {
186
- const outputJson = hasFlag(args, '--json');
187
- const dryRun = hasFlag(args, '--dry-run');
188
- const npmCommand = npmExecutable();
189
- const npmArgs = ['install', '-g', `${PACKAGE_NAME}@latest`];
190
- const report = {
191
- package: PACKAGE_NAME,
192
- command: [npmCommand, ...npmArgs],
193
- dryRun,
194
- tokenSent: false,
195
- projectFilesModified: false
196
- };
197
-
198
- if (dryRun) {
199
- if (outputJson) {
200
- writeLine(io.stdout, JSON.stringify(report, null, 2));
201
- } else {
202
- writeLine(io.stdout, `Update command: ${report.command.join(' ')}`);
203
- writeLine(io.stdout, 'Dry run only; no changes made.');
204
- }
205
- return 0;
206
- }
207
-
208
- if (!outputJson) {
209
- writeLine(io.stdout, `Updating ${PACKAGE_NAME} with: ${report.command.join(' ')}`);
210
- }
211
- const result = await runProcess(npmCommand, npmArgs, io, { stream: !outputJson });
212
- report.exitCode = result.code;
213
- report.completed = result.code === 0;
214
-
215
- if (outputJson) {
216
- writeLine(io.stdout, JSON.stringify(report, null, 2));
217
- }
218
- if (result.code !== 0) {
219
- const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
220
- throw new UsageError(`Update failed: ${detail}`);
221
- }
222
- if (!outputJson) {
223
- writeLine(io.stdout, `Update complete. Run \`${COMMAND_NAME} --version\` to confirm.`);
224
- }
225
- return 0;
226
- }
227
-
228
- async function doctorCommand(args, io) {
229
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
230
- const outputJson = hasFlag(args, '--json');
231
- const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
232
- const discoveryUrl = endpointUrl(baseUrl, '/.well-known/agent-discovery.json');
233
- const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
234
- ensureDiscoveryService(discovery, discoveryUrl);
235
-
236
- const rootVersion = await bestEffortRootVersion(discovery, timeoutMs, io);
237
- const mcpUrl = discoveryMcpUrl(discovery, baseUrl);
238
- const checks = [
239
- { name: 'node_version', ok: Number.parseInt(process.versions.node.split('.')[0], 10) >= 20, detail: process.versions.node },
240
- { name: 'discovery_reachable', ok: true, detail: discoveryUrl },
241
- { name: 'mcp_url_present', ok: Boolean(mcpUrl), detail: mcpUrl ?? 'missing' },
242
- { name: 'no_remote_code_execution', ok: booleanValue(discovery, ['security', 'no_remote_code_execution']) === true, detail: String(booleanValue(discovery, ['security', 'no_remote_code_execution'])) },
243
- {
244
- name: 'token_not_in_discovery',
245
- ok: booleanValue(discovery, ['security', 'token_in_discovery']) === false && booleanValue(discovery, ['auth', 'token_in_discovery']) === false,
246
- detail: `security=${booleanValue(discovery, ['security', 'token_in_discovery'])} auth=${booleanValue(discovery, ['auth', 'token_in_discovery'])}`
247
- },
248
- {
249
- name: 'service_version_compatible',
250
- ok: rootVersion.version ? sameMajorMinor(CLI_VERSION, rootVersion.version) : true,
251
- detail: rootVersion.version ? `service=${rootVersion.version} cli=${CLI_VERSION}` : `service version unavailable${rootVersion.error ? `: ${rootVersion.error}` : ''}`
252
- }
253
- ];
254
- const report = {
255
- ok: checks.every((check) => check.ok),
256
- cli: { package: PACKAGE_NAME, version: CLI_VERSION, node: process.versions.node },
257
- discovery: {
258
- url: discoveryUrl,
259
- schemaVersion: stringValue(discovery, ['schema_version']),
260
- protocol: stringValue(discovery, ['protocol']),
261
- service: stringValue(discovery, ['service']),
262
- serviceVersion: rootVersion.version ?? null,
263
- mcpUrl,
264
- supportedClients: agentDiscoveryClientIds(discovery)
265
- },
266
- checks
267
- };
268
-
269
- if (outputJson) {
270
- writeLine(io.stdout, JSON.stringify(report, null, 2));
271
- return report.ok ? 0 : 1;
272
- }
273
-
274
- writeLine(io.stdout, `${PRODUCT_NAME} CLI ${CLI_VERSION}`);
275
- writeLine(io.stdout, `Discovery: ${discoveryUrl}`);
276
- writeLine(io.stdout, `MCP: ${mcpUrl ?? 'missing'}`);
277
- if (rootVersion.version) {
278
- writeLine(io.stdout, `Service version: ${rootVersion.version}`);
279
- }
280
- writeLine(io.stdout, `Supported clients: ${report.discovery.supportedClients.join(', ') || 'unknown'}`);
281
- for (const check of checks) {
282
- writeLine(io.stdout, `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.detail}`);
283
- }
284
- return report.ok ? 0 : 1;
285
- }
286
-
287
- async function discoveryCommand(args, io) {
288
- const subcommand = args[0] ?? 'help';
289
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
290
- writeLine(io.stdout, 'Discovery commands:');
291
- writeLine(io.stdout, ` ${COMMAND_NAME} discovery show [--base-url <https://api.example.com>] [--json]`);
292
- return 0;
293
- }
294
- if (subcommand !== 'show') {
295
- throw new UsageError(`Unknown discovery command: ${subcommand}`);
296
- }
297
-
298
- const baseUrl = normalizeBaseUrl(baseUrlOption(args.slice(1), io.env));
299
- const outputJson = hasFlag(args, '--json');
300
- const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
301
- const discoveryUrl = endpointUrl(baseUrl, '/.well-known/agent-discovery.json');
302
- const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
303
- ensureDiscoveryService(discovery, discoveryUrl);
304
-
305
- if (outputJson) {
306
- writeLine(io.stdout, JSON.stringify(discovery, null, 2));
307
- return 0;
308
- }
309
-
310
- writeLine(io.stdout, `${stringValue(discovery, ['name']) ?? PRODUCT_NAME} discovery`);
311
- writeLine(io.stdout, `URL: ${discoveryUrl}`);
312
- writeLine(io.stdout, `Protocol: ${stringValue(discovery, ['protocol']) ?? 'unknown'}`);
313
- writeLine(io.stdout, `MCP: ${discoveryMcpUrl(discovery, baseUrl) ?? 'missing'}`);
314
- writeLine(io.stdout, `Docs: ${stringValue(discovery, ['urls', 'docs']) ?? 'unknown'}`);
315
- writeLine(io.stdout, `Clients: ${agentDiscoveryClientIds(discovery).join(', ') || 'unknown'}`);
316
- writeLine(io.stdout, 'Security: read-only discovery; tokens are not returned; remote code execution is not advertised.');
317
- return 0;
318
- }
319
-
320
- async function statusCommand(args, io) {
321
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
322
- const outputJson = hasFlag(args, '--json');
323
- const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
324
- const endpoints = [
325
- endpointUrl(baseUrl, '/.well-known/memory-os.json'),
326
- endpointUrl(baseUrl, '/health'),
327
- endpointUrl(baseUrl, '/ready')
328
- ];
329
-
330
- const probes = [];
331
- for (const url of endpoints) {
332
- probes.push(await probe(url, timeoutMs, io));
333
- }
334
-
335
- const result = {
336
- ok: probes.some((item) => item.ok),
337
- baseUrl,
338
- privacy: {
339
- telemetry: false,
340
- tokenSent: false,
341
- tokenSource: 'not-used-by-status'
342
- },
343
- probes
344
- };
345
-
346
- if (outputJson) {
347
- writeLine(io.stdout, JSON.stringify(result, null, 2));
348
- return result.ok ? 0 : 1;
349
- }
350
-
351
- writeLine(io.stdout, `${PRODUCT_NAME} status for ${baseUrl}`);
352
- writeLine(io.stdout, 'Privacy: telemetry disabled; no token sent.');
353
- for (const item of probes) {
354
- if (item.ok) {
355
- writeLine(io.stdout, ` OK ${item.status} ${item.url}`);
356
- } else {
357
- writeLine(io.stdout, ` FAIL ${item.status ?? 'ERR'} ${item.url} ${item.error ?? ''}`.trimEnd());
358
- }
359
- }
360
-
361
- return result.ok ? 0 : 1;
362
- }
363
-
364
- async function setupCommand(args, io) {
365
- const positionalClientId = positionalClientArg(args);
366
- const optionArgs = positionalClientId ? args.slice(1) : args;
367
- const baseUrl = normalizeBaseUrl(baseUrlOption(optionArgs, io.env));
368
- const outputJson = hasFlag(optionArgs, '--json');
369
- const shortClientSetup = Boolean(positionalClientId);
370
- const clientId = normalizeSetupClientId(positionalClientId ?? optionValue(optionArgs, '--client'));
371
- const dryRun = hasFlag(optionArgs, '--dry-run') || hasFlag(optionArgs, '--preview');
372
- const writeConfig = !dryRun && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes') || shortClientSetup);
373
- const timeoutMs = parsePositiveInteger(optionValue(optionArgs, '--timeout-ms') ?? '5000', '--timeout-ms');
374
- const installProfile = shortClientSetup
375
- && clientId === 'codex'
376
- && writeConfig
377
- && !hasFlag(optionArgs, '--no-profile');
378
-
379
- if (writeConfig && !clientId) {
380
- throw new UsageError('Setup --write requires --client <codex|cursor> so the CLI never writes broad config implicitly.');
381
- }
382
-
383
- const discoveryUrl = endpointUrl(baseUrl, '/.well-known/memory-os.json');
384
- const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
385
- ensureDiscoveryService(discovery, discoveryUrl);
386
-
387
- const statusUrl = stringValue(discovery, ['urls', 'onboarding_status'])
388
- ?? stringValue(discovery, ['onboarding_status_url'])
389
- ?? endpointUrl(baseUrl, '/v1/onboarding/status');
390
- const status = await fetchJson(statusUrl, timeoutMs, io);
391
- const setupPlan = buildSetupPlan({ baseUrl, discoveryUrl, statusUrl, discovery, status });
392
-
393
- if (clientId) {
394
- if (clientId === 'copilot-cli') {
395
- const proxyPort = parsePositiveInteger(optionValue(optionArgs, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
396
- setupPlan.selectedClient = copilotSetupPlan(setupPlan.mcpUrl, proxyPort, io.env);
397
- if (writeConfig) {
398
- await mergeCopilotMcpConfig(setupPlan.selectedClient.configPath, setupPlan.selectedClient.proxyUrl);
399
- setupPlan.selectedClient.written = true;
400
- }
401
- } else {
402
- const client = MCP_CLIENTS.get(clientId);
403
- if (!client) {
404
- throw new UsageError(`Unsupported MCP client: ${clientId}. Supported clients: ${supportedSetupClientIds().join(', ')}.`);
405
- }
406
-
407
- const identity = writeConfig ? await agentIdentity(clientId, io.env) : envReferenceIdentity(clientId);
408
- setupPlan.selectedClient = clientSetupPlan(clientId, client, setupPlan.mcpUrl, io.env, identity);
409
- if (writeConfig) {
410
- await client.writeConfig(setupPlan.selectedClient.configPath, setupPlan.mcpUrl, identity);
411
- setupPlan.selectedClient.written = true;
412
- }
413
-
414
- if (clientId === 'codex' && shortClientSetup) {
415
- const profileTarget = optionValue(optionArgs, '--profile-target')
416
- ?? optionValue(optionArgs, '--target')
417
- ?? defaultCodexProfileTarget();
418
- const profileResult = await codexProfileInstallResult(profileTarget, { write: installProfile });
419
- setupPlan.selectedClient.codexProfile = profileResult;
420
- }
421
- }
422
- }
423
-
424
- if (outputJson) {
425
- writeLine(io.stdout, JSON.stringify(setupPlan, null, 2));
426
- return 0;
427
- }
428
-
429
- writeSetupSummary(setupPlan, io);
430
- return 0;
431
- }
432
-
433
- async function profileCommand(args, io) {
434
- const subcommand = args[0] ?? 'help';
435
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
436
- writeLine(io.stdout, 'Profile commands:');
437
- writeLine(io.stdout, ` ${COMMAND_NAME} profile install codex [--target AGENTS.md] [--dry-run|--json]`);
438
- writeLine(io.stdout, ` ${COMMAND_NAME} profile status codex [--target AGENTS.md] [--json]`);
439
- writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target AGENTS.md] [--json]`);
440
- writeLine(io.stdout, '');
441
- writeLine(io.stdout, 'Profile installs are marker-scoped and never write token values.');
442
- return 0;
443
- }
444
-
445
- const clientId = args[1];
446
- if (clientId !== 'codex') {
447
- throw new UsageError(`Unsupported profile client: ${clientId ?? 'missing'}. Supported clients: codex.`);
448
- }
449
-
450
- const optionArgs = args.slice(2);
451
- const outputJson = hasFlag(optionArgs, '--json');
452
- const targetPath = optionValue(optionArgs, '--target') ?? defaultCodexProfileTarget();
453
- let result;
454
-
455
- if (subcommand === 'install') {
456
- result = await codexProfileInstallResult(targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
457
- } else if (subcommand === 'status') {
458
- result = await codexProfileStatusResult(targetPath);
459
- } else if (subcommand === 'uninstall') {
460
- result = await codexProfileUninstallResult(targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
461
- } else {
462
- throw new UsageError(`Unknown profile command: ${subcommand}`);
463
- }
464
-
465
- if (outputJson) {
466
- writeLine(io.stdout, JSON.stringify(result, null, 2));
467
- return 0;
468
- }
469
-
470
- writeProfileResult(subcommand, result, io);
471
- return 0;
472
- }
473
-
474
- async function loginCommand(args, io) {
475
- const outputJson = hasFlag(args, '--json');
476
- const fromStdin = hasFlag(args, '--from-stdin') || hasFlag(args, '--token-stdin');
477
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
478
- const httpTimeoutMs = parsePositiveInteger(optionValue(args, '--http-timeout-ms') ?? '30000', '--http-timeout-ms');
479
- const loginTimeoutOption = optionValue(args, '--timeout-ms');
480
- const pollOnce = hasFlag(args, '--poll-once');
481
-
482
- if (fromStdin) {
483
- const result = await storeTokenFromStdin(io, { source: 'stdin' });
484
- if (outputJson) {
485
- writeLine(io.stdout, JSON.stringify(result, null, 2));
486
- } else {
487
- writeLine(io.stdout, `${PRODUCT_NAME} login complete.`);
488
- writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
489
- writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');
490
- }
491
- return 0;
492
- }
493
-
494
- const start = await startDeviceLogin(baseUrl, httpTimeoutMs, io);
495
- const loginTimeoutMs = loginTimeoutOption
496
- ? parsePositiveInteger(loginTimeoutOption, '--timeout-ms')
497
- : Math.max(1000, start.expiresIn * 1000);
498
- if (!outputJson) {
499
- writeLine(io.stdout, `${PRODUCT_NAME} device login`);
500
- writeLine(io.stdout, `Open: ${start.verificationUriComplete ?? start.verificationUri}`);
501
- if (start.userCode) {
502
- writeLine(io.stdout, `Code: ${start.userCode}`);
503
- }
504
- writeLine(io.stdout, 'Waiting for authorization...');
505
- }
506
-
507
- const token = await pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, { pollOnce });
508
- const result = await storeTokenValue(token.accessToken, { source: 'device-login', account: token.account }, io.env);
509
- const payload = {
510
- ...result,
511
- baseUrl,
512
- verificationUri: start.verificationUri,
513
- account: token.account,
514
- deviceLogin: true
515
- };
516
-
517
- if (outputJson) {
518
- writeLine(io.stdout, JSON.stringify(payload, null, 2));
519
- } else {
520
- writeLine(io.stdout, 'Login complete. Token stored securely in the user-scoped XMemo CLI config directory.');
521
- if (token.account) {
522
- writeLine(io.stdout, `Signed in as: ${formatAccount(token.account)}`);
523
- }
524
- writeLine(io.stdout, `Credential path: ${result.credentialPath}`);
525
- writeLine(io.stdout, 'No extra token configuration is required.');
526
- writeLine(io.stdout, `Optional check: ${COMMAND_NAME} token status --verify`);
527
- }
528
- return 0;
529
- }
530
-
531
- async function authCommand(args, io) {
532
- const subcommand = args[0] ?? 'help';
533
-
534
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
535
- writeLine(io.stdout, 'Auth commands:');
536
- writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
537
- writeLine(io.stdout, '');
538
- writeLine(io.stdout, `Use \`${COMMAND_NAME} login\` to sign in and \`${COMMAND_NAME} token add --from-stdin\` to store an existing token.`);
539
- return 0;
540
- }
541
-
542
- if (subcommand === 'status') {
543
- return await credentialStatusCommand(args.slice(1), io, { mode: 'auth' });
544
- }
545
-
546
- throw new UsageError(`Unknown auth command: ${subcommand}`);
547
- }
548
-
549
- async function tokenCommand(args, io) {
550
- const subcommand = args[0] ?? 'help';
551
-
552
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
553
- writeLine(io.stdout, 'Token commands:');
554
- writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify]`);
555
- writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
556
- writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
557
- writeLine(io.stdout, '');
558
- writeLine(io.stdout, `${COMMAND_NAME} login is the recommended personal-user path.`);
559
- writeLine(io.stdout, `${COMMAND_NAME} token add --from-stdin stores a token in the user-scoped XMemo CLI config directory.`);
560
- return 0;
561
- }
562
-
563
- if (subcommand === 'status') {
564
- return await credentialStatusCommand(args.slice(1), io, { mode: 'token' });
565
- }
566
-
567
- if (subcommand === 'add') {
568
- if (!hasFlag(args, '--from-stdin')) {
569
- throw new UsageError('Refusing command-line token input. Pipe the token through stdin with --from-stdin.');
570
- }
571
- const result = await storeTokenFromStdin(io, { source: 'token-add' });
572
- if (hasFlag(args, '--json')) {
573
- writeLine(io.stdout, JSON.stringify(result, null, 2));
574
- } else {
575
- writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
576
- writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');
577
- }
578
- return 0;
579
- }
580
-
581
- if (subcommand === 'set') {
582
- if (!hasFlag(args, '--from-stdin')) {
583
- throw new UsageError('Refusing command-line token input. Pipe the token through stdin with --from-stdin.');
584
- }
585
- const token = (await readAll(io.stdin)).trim();
586
- validateToken(token);
587
- if (!hasFlag(args, '--allow-plaintext')) {
588
- writeLine(io.stderr, 'Token was read from stdin but was not stored.');
589
- writeLine(io.stderr, 'Enterprise default refuses plaintext token storage without --allow-plaintext.');
590
- writeLine(io.stderr, `Preferred personal-user path: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin.`);
591
- return 2;
592
- }
593
-
594
- const result = await storeTokenValue(token, { source: 'token-set' }, io.env);
595
- writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
596
- writeLine(io.stdout, 'Token value was not printed. Do not commit this file.');
597
- return 0;
598
- }
599
-
600
- throw new UsageError(`Unknown token command: ${subcommand}`);
601
- }
602
-
603
- async function credentialStatusCommand(args, io, { mode }) {
604
- const outputJson = hasFlag(args, '--json');
605
- const verify = hasFlag(args, '--verify');
606
- const credential = await readStoredCredential(io.env);
607
- const environmentToken = io.env[TOKEN_ENV_VAR] ?? io.env[LEGACY_TOKEN_ENV_VAR] ?? '';
608
- const hasEnvironmentToken = Boolean(environmentToken);
609
- const hasUserCredential = Boolean(credential.token);
610
- const tokenSource = hasEnvironmentToken ? 'environment' : hasUserCredential ? 'user-credential-file' : 'missing';
611
- const report = {
612
- loggedIn: hasEnvironmentToken || hasUserCredential,
613
- tokenSource,
614
- environmentToken: {
615
- present: hasEnvironmentToken,
616
- variable: hasEnvironmentToken && io.env[TOKEN_ENV_VAR] ? TOKEN_ENV_VAR : hasEnvironmentToken ? LEGACY_TOKEN_ENV_VAR : TOKEN_ENV_VAR
617
- },
618
- userCredentialFile: {
619
- present: hasUserCredential,
620
- path: credential.path,
621
- storage: credential.storage ?? null
622
- },
623
- account: credential.account ?? null,
624
- privacy: {
625
- tokenPrinted: false,
626
- projectFilesModified: false
627
- }
628
- };
629
-
630
- if (verify) {
631
- const token = await resolveCredentialToken(io.env);
632
- if (!token) {
633
- if (outputJson) {
634
- writeLine(io.stdout, JSON.stringify({ ...report, verification: { ok: false, detail: 'no token found' } }, null, 2));
635
- } else {
636
- writeCredentialStatus(report, io, { mode });
637
- writeLine(io.stderr, `No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\`.`);
638
- }
639
- return 1;
640
- }
641
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
642
- const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '10000', '--timeout-ms');
643
- const verification = await verifyTokenWithMcp(baseUrl, token, timeoutMs, io);
644
- report.verification = verification;
645
- if (outputJson) {
646
- writeLine(io.stdout, JSON.stringify(report, null, 2));
647
- return verification.ok ? 0 : 1;
648
- }
649
- writeCredentialStatus(report, io, { mode });
650
- writeLine(io.stdout, `Remote token verification: ${verification.ok ? 'ok' : 'failed'} (${verification.detail})`);
651
- return verification.ok ? 0 : 1;
652
- }
653
-
654
- if (outputJson) {
655
- writeLine(io.stdout, JSON.stringify(report, null, 2));
656
- } else {
657
- writeCredentialStatus(report, io, { mode });
658
- }
659
- return report.loggedIn ? 0 : 1;
660
- }
661
-
662
- function writeCredentialStatus(report, io, { mode }) {
663
- if (mode === 'auth') {
664
- writeLine(io.stdout, `${PRODUCT_NAME} auth status`);
665
- writeLine(io.stdout, `Logged in: ${report.loggedIn ? 'yes' : 'no'}`);
666
- writeLine(io.stdout, `Credential source: ${report.tokenSource}`);
667
- if (report.account) {
668
- writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);
669
- }
670
- writeLine(io.stdout, report.loggedIn ? 'Credential is ready; token value remains hidden.' : `Run \`${COMMAND_NAME} login\` to sign in.`);
671
- return;
672
- }
673
- writeLine(io.stdout, `Environment token: ${report.environmentToken.present ? 'present' : 'missing'} (${report.environmentToken.variable})`);
674
- writeLine(io.stdout, `User credential file: ${report.userCredentialFile.present ? 'present' : 'missing'} (${report.userCredentialFile.path})`);
675
- if (report.account) {
676
- writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);
677
- }
678
- writeLine(io.stdout, report.loggedIn ? 'Credential is ready; token value remains hidden.' : `Run \`${COMMAND_NAME} login\` to sign in.`);
679
- }
680
-
681
- async function mcpCommand(args, io) {
682
- const subcommand = args[0] ?? 'help';
683
-
684
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
685
- writeLine(io.stdout, 'MCP commands:');
686
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
687
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|generic> [--base-url <url>] [--json]`);
688
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}] [--base-url <url>]`);
689
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp profile codex [--json]`);
690
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <codex|cursor> [--url <https://api.example.com>]`);
691
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <codex|cursor> [--url <https://api.example.com>] --write [--config <path>]`);
692
- return 0;
693
- }
694
-
695
- if (subcommand === 'list') {
696
- if (hasFlag(args, '--json')) {
697
- writeLine(io.stdout, JSON.stringify(supportedMcpClients(), null, 2));
698
- return 0;
699
- }
700
-
701
- writeLine(io.stdout, 'Supported MCP clients:');
702
- for (const client of supportedMcpClients()) {
703
- writeLine(io.stdout, ` ${client.id.padEnd(8)} ${client.label} (${client.configKind})`);
704
- }
705
- writeLine(io.stdout, `All generated configs reference ${TOKEN_ENV_VAR}; token values are never embedded.`);
706
- return 0;
707
- }
708
-
709
- if (subcommand === 'config') {
710
- const clientId = optionValue(args, '--client') ?? args[1] ?? 'generic';
711
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
712
- const mcpUrl = endpointUrl(baseUrl, '/mcp');
713
- const useLocalProxy = clientId === 'copilot-cli' && !hasFlag(args, '--remote-env');
714
- const proxyPort = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
715
- const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`;
716
- const template = useLocalProxy
717
- ? mcpLocalProxyTemplate(clientId, proxyUrl)
718
- : mcpConfigTemplate(clientId, mcpUrl);
719
-
720
- if (hasFlag(args, '--json')) {
721
- writeLine(io.stdout, JSON.stringify(template, null, 2));
722
- return 0;
723
- }
724
-
725
- writeLine(io.stdout, `${PRODUCT_NAME} MCP config template for ${clientId}`);
726
- if (useLocalProxy) {
727
- writeLine(io.stdout, `Requires credential: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin`);
728
- writeLine(io.stdout, `Run local proxy: ${template.requiresLocalCommand}`);
729
- } else {
730
- writeLine(io.stdout, `Requires env: ${TOKEN_ENV_VAR}`);
731
- }
732
- if (typeof template.snippet === 'string') {
733
- writeLine(io.stdout, template.snippet.trimEnd());
734
- } else {
735
- writeLine(io.stdout, JSON.stringify(template.snippet, null, 2));
736
- }
737
- writeLine(io.stdout, 'Review the template before applying it. Token values are not included.');
738
- return 0;
739
- }
740
-
741
- if (subcommand === 'proxy') {
742
- return await mcpProxyCommand(args.slice(1), io);
743
- }
744
-
745
- if (subcommand === 'profile') {
746
- const clientId = args[1] ?? 'codex';
747
- if (clientId !== 'codex') {
748
- throw new UsageError('Only the Codex memory behavior profile is available in this MCP-depth release.');
749
- }
750
-
751
- const profile = codexMemoryProfile();
752
- if (hasFlag(args, '--json')) {
753
- writeLine(io.stdout, JSON.stringify(profile, null, 2));
754
- return 0;
755
- }
756
-
757
- writeCodexMemoryProfile(profile, io);
758
- return 0;
759
- }
760
-
761
- const target = args[1] ?? '';
762
- const client = MCP_CLIENTS.get(target);
763
-
764
- if (subcommand !== 'add' || !client) {
765
- throw new UsageError(`Supported MCP setup command: ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <url>]`);
766
- }
767
-
768
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
769
- const configPath = optionValue(args, '--config') ?? client.defaultConfigPath(io.env);
770
- const mcpUrl = endpointUrl(baseUrl, '/mcp');
771
-
772
- if (hasFlag(args, '--json')) {
773
- const identity = envReferenceIdentity(target);
774
- writeLine(io.stdout, JSON.stringify({
775
- client: target,
776
- label: client.label,
777
- configKind: client.configKind,
778
- configPath,
779
- serverName: MCP_SERVER_NAME,
780
- url: mcpUrl,
781
- tokenEnvVar: TOKEN_ENV_VAR,
782
- agentId: identity.agentId,
783
- agentInstanceId: identity.agentInstanceId,
784
- agentInstanceIdPath: identity.path,
785
- writesTokenValue: false
786
- }, null, 2));
787
- return 0;
788
- }
789
-
790
- const identity = hasFlag(args, '--write') ? await agentIdentity(target, io.env) : envReferenceIdentity(target);
791
- if (hasFlag(args, '--write')) {
792
- await client.writeConfig(configPath, mcpUrl, identity);
793
- writeLine(io.stdout, `Updated ${client.label} MCP config: ${configPath}`);
794
- writeLine(io.stdout, `Token value was not written. ${client.label} will read ${TOKEN_ENV_VAR} from the environment.`);
795
- writeLine(io.stdout, `Agent instance ID stored outside git: ${identity.path}`);
796
- return 0;
797
- }
798
-
799
- const snippet = client.buildSnippet(mcpUrl, identity);
800
- writeLine(io.stdout, `Add this to your ${client.label} config (${configPath}):`);
801
- writeLine(io.stdout, '');
802
- writeLine(io.stdout, snippet.trimEnd());
803
- writeLine(io.stdout, '');
804
- writeLine(io.stdout, `Set ${TOKEN_ENV_VAR} in your user environment or secret manager. The token value is not included here.`);
805
- return 0;
806
- }
807
-
808
- async function mcpProxyCommand(args, io) {
809
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
810
- const mcpUrl = endpointUrl(baseUrl, '/mcp');
811
- const host = optionValue(args, '--host') ?? DEFAULT_PROXY_HOST;
812
- const port = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
813
- const token = await resolveCredentialToken(io.env);
814
- if (!token) {
815
- throw new UsageError(`No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\` first.`);
816
- }
817
- validateToken(token);
818
- const identity = await agentIdentity('copilot-cli', io.env);
819
-
820
- const server = http.createServer(async (request, response) => {
821
- try {
822
- await handleMcpProxyRequest({ request, response, mcpUrl, token, identity, io });
823
- } catch (error) {
824
- response.statusCode = 502;
825
- response.setHeader('content-type', 'application/json');
826
- response.end(JSON.stringify({ error: 'mcp_proxy_error', message: error.message }));
827
- }
828
- });
829
-
830
- await new Promise((resolve, reject) => {
831
- server.once('error', reject);
832
- server.listen(port, host, () => {
833
- server.off('error', reject);
834
- resolve();
835
- });
836
- });
837
-
838
- writeLine(io.stdout, `${PRODUCT_NAME} MCP proxy listening on http://${host}:${port}/mcp`);
839
- writeLine(io.stdout, `Forwarding to ${mcpUrl}`);
840
- writeLine(io.stdout, `Credential source: ${TOKEN_ENV_VAR} or ${credentialsPath(io.env)}`);
841
- return 0;
842
- }
843
-
844
- async function handleMcpProxyRequest({ request, response, mcpUrl, token, identity, io }) {
845
- const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? `${DEFAULT_PROXY_HOST}:${DEFAULT_PROXY_PORT}`}`);
846
- if (request.method !== 'POST' || requestUrl.pathname !== '/mcp') {
847
- response.statusCode = 404;
848
- response.setHeader('content-type', 'application/json');
849
- response.end(JSON.stringify({ error: 'not_found' }));
850
- return;
851
- }
852
-
853
- const body = await readAll(request);
854
- const upstreamHeaders = {
855
- accept: String(request.headers.accept || 'application/json, text/event-stream'),
856
- 'content-type': String(request.headers['content-type'] || 'application/json'),
857
- authorization: `Bearer ${token}`,
858
- [AGENT_ID_HEADER]: identity.agentId,
859
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId,
860
- 'user-agent': `XMemo-CLI-Proxy/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
861
- };
862
- const sessionId = request.headers['mcp-session-id'];
863
- if (sessionId) {
864
- upstreamHeaders['mcp-session-id'] = Array.isArray(sessionId) ? sessionId[0] : sessionId;
865
- }
866
-
867
- const upstream = await io.fetch(mcpUrl, {
868
- method: 'POST',
869
- headers: upstreamHeaders,
870
- body
871
- });
872
-
873
- response.statusCode = upstream.status;
874
- for (const header of ['content-type', 'mcp-session-id']) {
875
- const value = upstream.headers.get(header);
876
- if (value) {
877
- response.setHeader(header, value);
878
- }
879
- }
880
- const buffer = Buffer.from(await upstream.arrayBuffer());
881
- response.end(buffer);
882
- }
883
-
884
- async function smokeCommand(args, io) {
885
- const clientId = optionValue(args, '--client');
886
- const outputJson = hasFlag(args, '--json');
887
- if (!clientId) {
888
- throw new UsageError('Smoke requires --client codex for this MCP-depth release.');
889
- }
890
- if (clientId !== 'codex') {
891
- throw new UsageError('Only Codex smoke checks are available in this MCP-depth release.');
892
- }
893
-
894
- const configPath = optionValue(args, '--config') ?? defaultCodexConfigPath(io.env);
895
- const report = await codexSmokeReport(configPath, io.env);
896
-
897
- if (outputJson) {
898
- writeLine(io.stdout, JSON.stringify(report, null, 2));
899
- return report.ok ? 0 : 1;
900
- }
901
-
902
- writeLine(io.stdout, `${PRODUCT_NAME} Codex MCP smoke: ${report.ok ? 'ok' : 'failed'}`);
903
- writeLine(io.stdout, `Config: ${report.configPath}`);
904
- writeLine(io.stdout, `Token env: ${report.tokenEnvVar}`);
905
- for (const check of report.checks) {
906
- const status = check.ok ? 'OK' : check.required ? 'FAIL' : 'WARN';
907
- writeLine(io.stdout, ` ${status} ${check.name}: ${check.detail}`);
908
- }
909
- return report.ok ? 0 : 1;
910
- }
911
-
912
- function envCommand(args, io) {
913
- const subcommand = args[0] ?? 'help';
914
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
915
- writeLine(io.stdout, 'Env commands:');
916
- writeLine(io.stdout, ` ${COMMAND_NAME} env example [--shell bash|powershell|cmd] [--base-url <url>] [--json]`);
917
- return 0;
918
- }
919
- if (subcommand !== 'example') {
920
- throw new UsageError(`Unknown env command: ${subcommand}`);
921
- }
922
-
923
- const baseUrl = normalizeBaseUrl(baseUrlOption(args.slice(1), io.env));
924
- const outputJson = hasFlag(args, '--json');
925
- const shell = optionValue(args, '--shell') ?? (process.platform === 'win32' ? 'powershell' : 'bash');
926
- const placeholder = '<paste-token-from-your-secret-store>';
927
- const payload = {
928
- XMEMO_URL: baseUrl,
929
- XMEMO_BASE_URL: baseUrl,
930
- MEMORY_OS_URL: baseUrl,
931
- MEMORY_OS_BASE_URL: baseUrl,
932
- [TOKEN_ENV_VAR]: placeholder,
933
- [AGENT_ID_ENV_VAR]: '<agent-family>',
934
- [AGENT_INSTANCE_ENV_VAR]: '<stable-random-id-for-this-local-agent>'
935
- };
936
-
937
- if (outputJson) {
938
- writeLine(io.stdout, JSON.stringify(payload, null, 2));
939
- return 0;
940
- }
941
-
942
- if (shell === 'powershell') {
943
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('XMEMO_URL', '${baseUrl}', 'User')`);
944
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('XMEMO_BASE_URL', '${baseUrl}', 'User')`);
945
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('MEMORY_OS_URL', '${baseUrl}', 'User')`);
946
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('MEMORY_OS_BASE_URL', '${baseUrl}', 'User')`);
947
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${TOKEN_ENV_VAR}', '${placeholder}', 'User')`);
948
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${AGENT_ID_ENV_VAR}', '<agent-family>', 'User')`);
949
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${AGENT_INSTANCE_ENV_VAR}', '<stable-random-id-for-this-local-agent>', 'User')`);
950
- } else if (shell === 'cmd') {
951
- writeLine(io.stdout, `setx XMEMO_URL "${baseUrl}"`);
952
- writeLine(io.stdout, `setx XMEMO_BASE_URL "${baseUrl}"`);
953
- writeLine(io.stdout, `setx MEMORY_OS_URL "${baseUrl}"`);
954
- writeLine(io.stdout, `setx MEMORY_OS_BASE_URL "${baseUrl}"`);
955
- writeLine(io.stdout, `setx ${TOKEN_ENV_VAR} "${placeholder}"`);
956
- writeLine(io.stdout, `setx ${AGENT_ID_ENV_VAR} "<agent-family>"`);
957
- writeLine(io.stdout, `setx ${AGENT_INSTANCE_ENV_VAR} "<stable-random-id-for-this-local-agent>"`);
958
- } else {
959
- writeLine(io.stdout, `export XMEMO_URL="${baseUrl}"`);
960
- writeLine(io.stdout, `export XMEMO_BASE_URL="${baseUrl}"`);
961
- writeLine(io.stdout, `export MEMORY_OS_URL="${baseUrl}"`);
962
- writeLine(io.stdout, `export MEMORY_OS_BASE_URL="${baseUrl}"`);
963
- writeLine(io.stdout, `export ${TOKEN_ENV_VAR}="${placeholder}"`);
964
- writeLine(io.stdout, `export ${AGENT_ID_ENV_VAR}="<agent-family>"`);
965
- writeLine(io.stdout, `export ${AGENT_INSTANCE_ENV_VAR}="<stable-random-id-for-this-local-agent>"`);
966
- }
967
- return 0;
968
- }
969
-
970
- function writePrivacy(io) {
971
- writeLine(io.stdout, `${PRODUCT_NAME} CLI privacy and security defaults:`);
972
- writeLine(io.stdout, '- No telemetry or analytics.');
973
- writeLine(io.stdout, '- `status` does not send tokens.');
974
- writeLine(io.stdout, `- MCP configs reference ${TOKEN_ENV_VAR}; token values are not embedded.`);
975
- writeLine(io.stdout, `- Agent instance IDs are non-secret and stored in user-scoped config outside git.`);
976
- writeLine(io.stdout, '- `login` and `token add` store credentials in the user-scoped XMemo CLI config directory.');
977
- writeLine(io.stdout, '- Legacy `token set` plaintext storage requires explicit --allow-plaintext.');
978
- writeLine(io.stdout, '- npm publishing is restricted by package.json files whitelist.');
979
- }
980
-
981
- async function startDeviceLogin(baseUrl, timeoutMs, io) {
982
- const payload = await postJson(endpointUrl(baseUrl, DEVICE_LOGIN_START_PATH), {
983
- client_id: PACKAGE_NAME,
984
- cli_version: CLI_VERSION,
985
- token_type: 'mcp_token',
986
- scopes: ['memory:read', 'memory:write']
987
- }, timeoutMs, io);
988
-
989
- const deviceCode = stringValue(payload, ['device_code']);
990
- const verificationUri = stringValue(payload, ['verification_uri']);
991
- if (!deviceCode || !verificationUri) {
992
- throw new UsageError(`Device login did not return device_code and verification_uri from ${baseUrl}.`);
993
- }
994
-
995
- return {
996
- deviceCode,
997
- userCode: stringValue(payload, ['user_code']),
998
- verificationUri,
999
- verificationUriComplete: stringValue(payload, ['verification_uri_complete']),
1000
- expiresIn: Number.isFinite(Number(payload.expires_in)) ? Number(payload.expires_in) : 600,
1001
- interval: Number.isFinite(Number(payload.interval)) ? Math.max(1, Number(payload.interval)) : 5
1002
- };
1003
- }
1004
-
1005
- async function pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, options = {}) {
1006
- const deadline = Date.now() + Math.min(start.expiresIn * 1000, loginTimeoutMs);
1007
- const sleepFn = io.sleep ?? sleep;
1008
- let intervalSeconds = start.interval;
1009
- while (Date.now() <= deadline) {
1010
- const payload = await postJson(endpointUrl(baseUrl, DEVICE_LOGIN_TOKEN_PATH), {
1011
- device_code: start.deviceCode,
1012
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
1013
- }, httpTimeoutMs, io, { allowDevicePending: true });
1014
-
1015
- const accessToken = stringValue(payload, ['access_token']) ?? stringValue(payload, ['token']);
1016
- if (accessToken) {
1017
- validateToken(accessToken);
1018
- return {
1019
- accessToken,
1020
- account: accountFromPayload(payload)
1021
- };
1022
- }
1023
-
1024
- const error = stringValue(payload, ['error']);
1025
- if (error && error !== 'authorization_pending' && error !== 'slow_down') {
1026
- throw new UsageError(`Device login failed: ${error}`);
1027
- }
1028
- if (options.pollOnce) {
1029
- throw new UsageError('Device login is still pending.');
1030
- }
1031
- if (error === 'slow_down') {
1032
- intervalSeconds += 5;
1033
- }
1034
- await sleepFn(intervalSeconds * 1000);
1035
- }
1036
-
1037
- throw new UsageError('Device login expired before authorization completed.');
1038
- }
1039
-
1040
- async function storeTokenFromStdin(io, metadata = {}) {
1041
- const token = (await readAll(io.stdin)).trim();
1042
- validateToken(token);
1043
- return await storeTokenValue(token, metadata, io.env);
1044
- }
1045
-
1046
- async function storeTokenValue(token, metadata, env) {
1047
- validateToken(token);
1048
- const credentialPath = credentialsPath(env);
1049
- await writePlaintextCredential(credentialPath, token, metadata);
1050
- return {
1051
- ok: true,
1052
- credentialPath,
1053
- tokenPresent: true,
1054
- tokenPrinted: false,
1055
- projectFilesModified: false,
1056
- storage: 'user-scoped-credential-file'
1057
- };
1058
- }
1059
-
1060
- async function readStoredCredential(env) {
1061
- const credentialPath = credentialsPath(env);
1062
- const content = await readTextIfExists(credentialPath);
1063
- if (!content.trim()) {
1064
- return { path: credentialPath, token: null };
1065
- }
1066
-
1067
- const parsed = parseJsonConfig(content, credentialPath);
1068
- return {
1069
- path: credentialPath,
1070
- token: stringValue(parsed, ['token']),
1071
- storage: stringValue(parsed, ['storage']),
1072
- account: accountFromPayload(parsed.metadata)
1073
- };
1074
- }
1075
-
1076
- function accountFromPayload(payload) {
1077
- const account = payload && typeof payload === 'object'
1078
- ? (payload.user && typeof payload.user === 'object' ? payload.user : payload.account)
1079
- : null;
1080
- if (!account || typeof account !== 'object') {
1081
- return null;
1082
- }
1083
- const userId = stringValue(account, ['user_id']) ?? stringValue(account, ['id']) ?? stringValue(account, ['userId']);
1084
- const email = stringValue(account, ['email']);
1085
- const displayName = stringValue(account, ['display_name']) ?? stringValue(account, ['name']) ?? stringValue(account, ['displayName']);
1086
- if (!userId && !email && !displayName) {
1087
- return null;
1088
- }
1089
- return {
1090
- userId: userId ?? null,
1091
- email: email ?? null,
1092
- displayName: displayName ?? null
1093
- };
1094
- }
1095
-
1096
- function formatAccount(account) {
1097
- const label = account.displayName || account.email || account.userId || 'XMemo account';
1098
- return account.email && account.displayName ? `${account.displayName} <${account.email}>` : label;
1099
- }
1100
-
1101
- async function resolveCredentialToken(env) {
1102
- const environmentToken = env[TOKEN_ENV_VAR] ?? env[LEGACY_TOKEN_ENV_VAR];
1103
- if (environmentToken) {
1104
- return environmentToken;
1105
- }
1106
- const credential = await readStoredCredential(env);
1107
- return credential.token;
1108
- }
1109
-
1110
- async function verifyTokenWithMcp(baseUrl, token, timeoutMs, io) {
1111
- const url = endpointUrl(baseUrl, '/mcp');
1112
- const controller = new AbortController();
1113
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
1114
- try {
1115
- const response = await io.fetch(url, {
1116
- method: 'POST',
1117
- headers: {
1118
- accept: 'application/json, text/event-stream',
1119
- 'content-type': 'application/json',
1120
- authorization: `Bearer ${token}`,
1121
- 'user-agent': `XMemo-CLI/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
1122
- },
1123
- body: JSON.stringify({
1124
- jsonrpc: '2.0',
1125
- id: 1,
1126
- method: 'initialize',
1127
- params: {
1128
- protocolVersion: '2024-11-05',
1129
- capabilities: {},
1130
- clientInfo: { name: COMMAND_NAME, version: CLI_VERSION }
1131
- }
1132
- }),
1133
- signal: controller.signal
1134
- });
1135
- return {
1136
- ok: response.ok,
1137
- detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}`
1138
- };
1139
- } catch (error) {
1140
- return {
1141
- ok: false,
1142
- detail: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
1143
- };
1144
- } finally {
1145
- clearTimeout(timeout);
1146
- }
1147
- }
1148
-
1149
- async function probe(url, timeoutMs, io) {
1150
- if (typeof io.fetch !== 'function') {
1151
- return { url, ok: false, error: 'fetch unavailable in this Node runtime' };
1152
- }
1153
-
1154
- const controller = new AbortController();
1155
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
1156
-
1157
- try {
1158
- const response = await io.fetch(url, {
1159
- headers: { accept: 'application/json' },
1160
- signal: controller.signal
1161
- });
1162
- return { url, ok: response.ok, status: response.status };
1163
- } catch (error) {
1164
- return {
1165
- url,
1166
- ok: false,
1167
- error: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
1168
- };
1169
- } finally {
1170
- clearTimeout(timeout);
1171
- }
1172
- }
1173
-
1174
- async function fetchJson(url, timeoutMs, io) {
1175
- if (typeof io.fetch !== 'function') {
1176
- throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
1177
- }
1178
-
1179
- const controller = new AbortController();
1180
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
1181
-
1182
- try {
1183
- const response = await io.fetch(url, {
1184
- headers: { accept: 'application/json' },
1185
- signal: controller.signal
1186
- });
1187
- if (!response.ok) {
1188
- throw new UsageError(`Discovery request failed with HTTP ${response.status}: ${url}`);
1189
- }
1190
- return await response.json();
1191
- } catch (error) {
1192
- if (error instanceof UsageError) {
1193
- throw error;
1194
- }
1195
- const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
1196
- throw new UsageError(`Discovery request failed: ${url} (${reason})`);
1197
- } finally {
1198
- clearTimeout(timeout);
1199
- }
1200
- }
1201
-
1202
- async function postJson(url, payload, timeoutMs, io, options = {}) {
1203
- if (typeof io.fetch !== 'function') {
1204
- throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
1205
- }
1206
-
1207
- const controller = new AbortController();
1208
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
1209
-
1210
- try {
1211
- const response = await io.fetch(url, {
1212
- method: 'POST',
1213
- headers: {
1214
- accept: 'application/json',
1215
- 'content-type': 'application/json'
1216
- },
1217
- body: JSON.stringify(payload),
1218
- signal: controller.signal
1219
- });
1220
- const responsePayload = await response.json();
1221
- if (!response.ok) {
1222
- const error = stringValue(responsePayload, ['error']) ?? stringValue(responsePayload, ['detail']) ?? `HTTP ${response.status}`;
1223
- if (options.allowDevicePending && (error === 'authorization_pending' || error === 'slow_down')) {
1224
- return { error };
1225
- }
1226
- throw new UsageError(`Request failed with HTTP ${response.status}: ${url} (${error})`);
1227
- }
1228
- return responsePayload;
1229
- } catch (error) {
1230
- if (error instanceof UsageError) {
1231
- throw error;
1232
- }
1233
- const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
1234
- throw new UsageError(`Request failed: ${url} (${reason})`);
1235
- } finally {
1236
- clearTimeout(timeout);
1237
- }
1238
- }
1239
-
1240
- function ensureDiscoveryService(discovery, discoveryUrl) {
1241
- const service = stringValue(discovery, ['service']);
1242
- if (service && service !== 'memory-os') {
1243
- throw new UsageError(`Discovery document at ${discoveryUrl} is for '${service}', not 'memory-os'.`);
1244
- }
1245
- }
1246
-
1247
- function buildSetupPlan({ baseUrl, discoveryUrl, statusUrl, discovery, status }) {
1248
- const apiBase = stringValue(discovery, ['urls', 'api_base'])
1249
- ?? stringValue(discovery, ['api_base_url'])
1250
- ?? baseUrl;
1251
- const mcpUrl = stringValue(discovery, ['urls', 'mcp'])
1252
- ?? stringValue(discovery, ['mcp_url'])
1253
- ?? endpointUrl(apiBase, '/mcp');
1254
- const tokenPortalUrl = stringValue(discovery, ['urls', 'token_portal'])
1255
- ?? stringValue(discovery, ['token_portal_url'])
1256
- ?? stringValue(status, ['requirements', 'token_portal_url']);
1257
- const tokenEnvVar = stringValue(discovery, ['auth', 'token_env_var'])
1258
- ?? stringValue(status, ['requirements', 'token_env_var'])
1259
- ?? TOKEN_ENV_VAR;
1260
-
1261
- return {
1262
- schemaVersion: '1.0',
1263
- baseUrl,
1264
- discoveryUrl,
1265
- statusUrl,
1266
- apiBase,
1267
- mcpUrl,
1268
- guideUrl: stringValue(discovery, ['urls', 'guide']) ?? endpointUrl(apiBase, '/guide'),
1269
- docsUrl: stringValue(discovery, ['urls', 'docs']),
1270
- tokenPortalUrl,
1271
- tokenEnvVar,
1272
- onboardingReady: booleanValue(status, ['ready']),
1273
- supportedClients: discoveryMcpClients(discovery),
1274
- localClients: supportedMcpClients(),
1275
- privacy: {
1276
- telemetry: false,
1277
- tokenSent: false,
1278
- tokenEmbeddedInConfig: false
1279
- },
1280
- boundaries: {
1281
- clientAllowed: arrayValue(discovery, ['agent_boundary', 'client_allowed'])
1282
- ?? arrayValue(status, ['agent_boundary', 'client_allowed'])
1283
- ?? [],
1284
- adminRequired: arrayValue(discovery, ['agent_boundary', 'admin_required'])
1285
- ?? arrayValue(status, ['agent_boundary', 'admin_required'])
1286
- ?? []
1287
- }
1288
- };
1289
- }
1290
-
1291
- async function bestEffortRootVersion(discovery, timeoutMs, io) {
1292
- const rootDiscoveryUrl = stringValue(discovery, ['urls', 'root_discovery']);
1293
- if (!rootDiscoveryUrl) {
1294
- return {};
1295
- }
1296
- try {
1297
- const rootDiscovery = await fetchJson(rootDiscoveryUrl, timeoutMs, io);
1298
- return { version: stringValue(rootDiscovery, ['version']) ?? undefined };
1299
- } catch (error) {
1300
- return { error: error.message };
1301
- }
1302
- }
1303
-
1304
- function discoveryMcpUrl(discovery, baseUrl) {
1305
- return stringValue(discovery, ['api', 'mcp', 'url'])
1306
- ?? stringValue(discovery, ['urls', 'mcp'])
1307
- ?? endpointUrl(baseUrl, '/mcp');
1308
- }
1309
-
1310
- function agentDiscoveryClientIds(discovery) {
1311
- const clients = Array.isArray(discovery?.clients) ? discovery.clients : [];
1312
- const ids = clients
1313
- .filter((client) => isPlainObject(client) && typeof client.id === 'string')
1314
- .map((client) => client.id);
1315
- if (ids.length > 0) {
1316
- return ids;
1317
- }
1318
- const supported = arrayValue(discovery, ['supported_clients']);
1319
- return supported ?? [];
1320
- }
1321
-
1322
- function mcpConfigTemplate(clientId, mcpUrl) {
1323
- if (clientId === 'codex') {
1324
- return {
1325
- client: clientId,
1326
- serverName: MCP_SERVER_NAME,
1327
- snippetFormat: 'toml',
1328
- snippet: codexTomlSnippet(mcpUrl),
1329
- requiresEnv: [TOKEN_ENV_VAR],
1330
- optionalEnv: [AGENT_INSTANCE_ENV_VAR],
1331
- agentIdentity: {
1332
- agentId: 'codex',
1333
- agentIdHeader: AGENT_ID_HEADER,
1334
- agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1335
- agentInstanceHeader: AGENT_INSTANCE_HEADER
1336
- },
1337
- writesTokenValue: false
1338
- };
1339
- }
1340
-
1341
- const serverName = clientId === 'cursor' || clientId === 'gemini-cli' ? 'memory_os' : 'memory-os';
1342
- return {
1343
- client: clientId,
1344
- serverName,
1345
- snippetFormat: 'json',
1346
- snippet: {
1347
- mcpServers: {
1348
- [serverName]: {
1349
- type: 'http',
1350
- url: mcpUrl,
1351
- headers: {
1352
- Authorization: `Bearer \${${TOKEN_ENV_VAR}}`,
1353
- [AGENT_ID_HEADER]: clientId,
1354
- [AGENT_INSTANCE_HEADER]: `\${${AGENT_INSTANCE_ENV_VAR}}`
1355
- }
1356
- }
1357
- }
1358
- },
1359
- requiresEnv: [TOKEN_ENV_VAR],
1360
- optionalEnv: [AGENT_INSTANCE_ENV_VAR],
1361
- agentIdentity: {
1362
- agentId: clientId,
1363
- agentIdHeader: AGENT_ID_HEADER,
1364
- agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1365
- agentInstanceHeader: AGENT_INSTANCE_HEADER
1366
- },
1367
- writesTokenValue: false
1368
- };
1369
- }
1370
-
1371
- function mcpLocalProxyTemplate(clientId, proxyUrl) {
1372
- const serverName = clientId === 'cursor' || clientId === 'gemini-cli' ? 'memory_os' : 'memory-os';
1373
- return {
1374
- client: clientId,
1375
- serverName,
1376
- snippetFormat: 'json',
1377
- snippet: {
1378
- mcpServers: {
1379
- [serverName]: {
1380
- type: 'http',
1381
- url: proxyUrl
1382
- }
1383
- }
1384
- },
1385
- requiresCredential: [`${COMMAND_NAME} login`, `${COMMAND_NAME} token add --from-stdin`],
1386
- requiresLocalCommand: `${COMMAND_NAME} mcp proxy --port ${new URL(proxyUrl).port || DEFAULT_PROXY_PORT}`,
1387
- agentIdentity: {
1388
- agentId: clientId,
1389
- agentIdHeader: AGENT_ID_HEADER,
1390
- agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1391
- agentInstanceHeader: AGENT_INSTANCE_HEADER
1392
- },
1393
- writesTokenValue: false
1394
- };
1395
- }
1396
-
1397
- function sameMajorMinor(left, right) {
1398
- const leftParts = left.split('.');
1399
- const rightParts = right.split('.');
1400
- return leftParts[0] === rightParts[0] && leftParts[1] === rightParts[1];
1401
- }
1402
-
1403
- function baseUrlOption(args, env) {
1404
- return optionValue(args, '--base-url')
1405
- ?? optionValue(args, '--url')
1406
- ?? env.XMEMO_BASE_URL
1407
- ?? env.XMEMO_URL
1408
- ?? env.MEMORY_OS_BASE_URL
1409
- ?? env.MEMORY_OS_URL
1410
- ?? DEFAULT_SERVICE_URL;
1411
- }
1412
-
1413
- function clientSetupPlan(clientId, client, mcpUrl, env, identity) {
1414
- return {
1415
- id: clientId,
1416
- label: client.label,
1417
- configKind: client.configKind,
1418
- configPath: client.defaultConfigPath(env),
1419
- serverName: MCP_SERVER_NAME,
1420
- mcpUrl,
1421
- tokenEnvVar: TOKEN_ENV_VAR,
1422
- agentId: identity.agentId,
1423
- agentInstanceId: identity.agentInstanceId,
1424
- agentInstanceIdPath: identity.path,
1425
- writesTokenValue: false,
1426
- written: false
1427
- };
1428
- }
1429
-
1430
- function copilotSetupPlan(mcpUrl, proxyPort, env) {
1431
- const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`;
1432
- const template = mcpLocalProxyTemplate('copilot-cli', proxyUrl);
1433
- return {
1434
- id: 'copilot-cli',
1435
- label: 'Copilot CLI',
1436
- configKind: 'local-proxy',
1437
- configPath: defaultCopilotConfigPath(env),
1438
- serverName: template.serverName,
1439
- mcpUrl,
1440
- proxyUrl,
1441
- tokenEnvVar: TOKEN_ENV_VAR,
1442
- requiresCredential: template.requiresCredential,
1443
- requiresLocalCommand: template.requiresLocalCommand,
1444
- template: template.snippet,
1445
- agentId: template.agentIdentity.agentId,
1446
- writesTokenValue: false,
1447
- writeSupported: true,
1448
- written: false
1449
- };
1450
- }
1451
-
1452
- function writeSetupSummary(plan, io) {
1453
- writeLine(io.stdout, `${PRODUCT_NAME} setup discovery: ${plan.baseUrl}`);
1454
- writeLine(io.stdout, ` API: ${plan.apiBase}`);
1455
- writeLine(io.stdout, ` MCP: ${plan.mcpUrl}`);
1456
- writeLine(io.stdout, ` Guide: ${plan.guideUrl}`);
1457
- if (plan.docsUrl) {
1458
- writeLine(io.stdout, ` Docs: ${plan.docsUrl}`);
1459
- }
1460
- if (plan.tokenPortalUrl) {
1461
- writeLine(io.stdout, ` Token portal: ${plan.tokenPortalUrl}`);
1462
- }
1463
- writeLine(io.stdout, ` Token env var: ${plan.tokenEnvVar}`);
1464
- writeLine(io.stdout, ` Onboarding ready: ${plan.onboardingReady === null ? 'unknown' : plan.onboardingReady}`);
1465
- writeLine(io.stdout, 'Privacy: telemetry disabled; no token sent; generated config references env vars only.');
1466
-
1467
- if (plan.boundaries.adminRequired.length > 0) {
1468
- writeLine(io.stdout, `Admin-only actions: ${plan.boundaries.adminRequired.join(', ')}`);
1469
- }
1470
-
1471
- if (plan.selectedClient) {
1472
- writeLine(io.stdout, '');
1473
- writeLine(io.stdout, `Selected client: ${plan.selectedClient.label}`);
1474
- writeLine(io.stdout, ` Config path: ${plan.selectedClient.configPath}`);
1475
- writeLine(io.stdout, ` Written: ${plan.selectedClient.written}`);
1476
- writeLine(io.stdout, ` Token value embedded: ${plan.selectedClient.writesTokenValue}`);
1477
- writeLine(io.stdout, ` Agent ID: ${plan.selectedClient.agentId}`);
1478
- if (plan.selectedClient.agentInstanceIdPath) {
1479
- writeLine(io.stdout, ` Agent instance ID stored: ${plan.selectedClient.agentInstanceIdPath}`);
1480
- }
1481
- if (plan.selectedClient.configKind === 'local-proxy') {
1482
- writeLine(io.stdout, ` Local proxy: ${plan.selectedClient.requiresLocalCommand}`);
1483
- if (plan.selectedClient.written) {
1484
- writeLine(io.stdout, ` Next: keep \`${plan.selectedClient.requiresLocalCommand}\` running while you use Copilot CLI.`);
1485
- writeLine(io.stdout, ' If Copilot CLI is already open, reload MCP config or restart Copilot CLI.');
1486
- } else {
1487
- writeLine(io.stdout, ' MCP template:');
1488
- writeLine(io.stdout, JSON.stringify(plan.selectedClient.template, null, 2));
1489
- writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup copilot --url ${plan.baseUrl}`);
1490
- }
1491
- return;
1492
- }
1493
- if (plan.selectedClient.codexProfile) {
1494
- const profile = plan.selectedClient.codexProfile;
1495
- writeLine(io.stdout, ` Codex profile target: ${profile.targetPath}`);
1496
- writeLine(io.stdout, ` Codex profile installed: ${profile.written}`);
1497
- writeLine(io.stdout, ` Codex profile changed: ${profile.changed}`);
1498
- if (!profile.written) {
1499
- writeLine(io.stdout, ` Profile preview: ${COMMAND_NAME} profile install codex --target ${profile.targetPath}`);
1500
- }
1501
- }
1502
- if (!plan.selectedClient.written) {
1503
- writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup ${plan.selectedClient.id} --url ${plan.baseUrl}`);
1504
- }
1505
- return;
1506
- }
1507
-
1508
- writeLine(io.stdout, '');
1509
- writeLine(io.stdout, 'Next steps:');
1510
- writeLine(io.stdout, ` 1. Create a scoped token in the token portal and store it in ${plan.tokenEnvVar}.`);
1511
- writeLine(io.stdout, ` 2. Configure a client, for example: ${COMMAND_NAME} setup codex --url ${plan.baseUrl}`);
1512
- writeLine(io.stdout, ` 3. Run ${COMMAND_NAME} status to smoke-test the service without sending the token.`);
1513
- }
1514
-
1515
- function discoveryMcpClients(discovery) {
1516
- const clients = discovery?.clients?.mcp;
1517
- if (!Array.isArray(clients)) {
1518
- return [];
1519
- }
1520
-
1521
- return clients
1522
- .filter((client) => isPlainObject(client) && typeof client.id === 'string')
1523
- .map((client) => ({
1524
- id: client.id,
1525
- configEndpoint: typeof client.config_endpoint === 'string' ? client.config_endpoint : null
1526
- }));
1527
- }
1528
-
1529
- function normalizeBaseUrl(input) {
1530
- try {
1531
- const parsed = new URL(input);
1532
- if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
1533
- throw new UsageError('URL must use http or https.');
1534
- }
1535
- parsed.hash = '';
1536
- parsed.search = '';
1537
- return parsed.toString().replace(/\/$/, '');
1538
- } catch (error) {
1539
- if (error instanceof UsageError) {
1540
- throw error;
1541
- }
1542
- throw new UsageError(`Invalid URL: ${input}`);
1543
- }
1544
- }
1545
-
1546
- function endpointUrl(baseUrl, pathname) {
1547
- const url = new URL(baseUrl);
1548
- url.pathname = pathname;
1549
- url.hash = '';
1550
- url.search = '';
1551
- return url.toString();
1552
- }
1553
-
1554
- function codexTomlSnippet(mcpUrl) {
1555
- return `[mcp_servers.${MCP_SERVER_NAME}]
1556
- url = "${escapeTomlString(mcpUrl)}"
1557
- bearer_token_env_var = "${TOKEN_ENV_VAR}"
1558
- `;
1559
- }
1560
-
1561
- function codexMemoryProfile() {
1562
- return {
1563
- client: 'codex',
1564
- profileVersion: 'codex-mcp-depth-v1',
1565
- mcpServerName: MCP_SERVER_NAME,
1566
- requiredTokenEnv: TOKEN_ENV_VAR,
1567
- objective: 'Use XMemo deliberately through MCP for project context recall and high-signal write-back.',
1568
- instructions: [
1569
- 'At the start of a non-trivial task, call XMemo recall/search for relevant project decisions, conventions, prior fixes, and active context unless the user explicitly asks not to use memory.',
1570
- 'Use recalled memories as evidence, not as unquestioned truth. Prefer current repository files when memory conflicts with code.',
1571
- 'After meaningful decisions, bug fixes, release steps, or durable conventions, write a concise XMemo memory with scope, source, and no secret values.',
1572
- 'Never store tokens, API keys, cookies, private keys, raw credentials, or sensitive customer data in XMemo.',
1573
- 'For routine or low-signal output, skip durable writes. Prefer summarized procedural or semantic memories over verbose logs.',
1574
- 'Keep XMemo authentication through the XMEMO_KEY environment variable; do not paste token values into prompts, config files, or logs.'
1575
- ],
1576
- setupCommand: `${COMMAND_NAME} setup codex --url "$XMEMO_URL"`,
1577
- smokeCommand: `${COMMAND_NAME} smoke --client codex`
1578
- };
1579
- }
1580
-
1581
- function writeCodexMemoryProfile(profile, io) {
1582
- writeLine(io.stdout, `${PRODUCT_NAME} Codex memory behavior profile`);
1583
- writeLine(io.stdout, `Profile: ${profile.profileVersion}`);
1584
- writeLine(io.stdout, `MCP server: ${profile.mcpServerName}`);
1585
- writeLine(io.stdout, `Token env: ${profile.requiredTokenEnv}`);
1586
- writeLine(io.stdout, '');
1587
- writeLine(io.stdout, 'Recommended Codex instructions:');
1588
- for (const instruction of profile.instructions) {
1589
- writeLine(io.stdout, `- ${instruction}`);
1590
- }
1591
- writeLine(io.stdout, '');
1592
- writeLine(io.stdout, `Setup: ${profile.setupCommand}`);
1593
- writeLine(io.stdout, `Smoke test: ${profile.smokeCommand}`);
1594
- }
1595
-
1596
- function codexProfileInstructionText() {
1597
- const profile = codexMemoryProfile();
1598
- const lines = [
1599
- '## XMemo Codex profile',
1600
- '',
1601
- `MCP server: \`${profile.mcpServerName}\``,
1602
- `Token env var: \`${profile.requiredTokenEnv}\``,
1603
- '',
1604
- profile.objective,
1605
- '',
1606
- 'Recommended Codex behavior:'
1607
- ];
1608
- for (const instruction of profile.instructions) {
1609
- lines.push(`- ${instruction}`);
1610
- }
1611
- lines.push('');
1612
- return `${lines.join('\n')}\n`;
1613
- }
1614
-
1615
- function codexProfileMarkerBlock() {
1616
- return `${CODEX_PROFILE_MARKER_START}\n${codexProfileInstructionText()}${CODEX_PROFILE_MARKER_END}\n`;
1617
- }
1618
-
1619
- function defaultCodexProfileTarget() {
1620
- return path.resolve(process.cwd(), CODEX_PROFILE_TARGET);
1621
- }
1622
-
1623
- async function codexProfileInstallResult(targetPath, options = {}) {
1624
- const resolvedTarget = path.resolve(targetPath);
1625
- const existing = await readTextIfExists(resolvedTarget);
1626
- const marker = markerBounds(existing);
1627
- const block = codexProfileMarkerBlock();
1628
- let nextText;
1629
-
1630
- if (marker.present) {
1631
- nextText = `${existing.slice(0, marker.start)}${block}${existing.slice(marker.end)}`;
1632
- } else if (existing.trim().length === 0) {
1633
- nextText = block;
1634
- } else {
1635
- const separator = existing.endsWith('\n') ? '\n' : '\n\n';
1636
- nextText = `${existing}${separator}${block}`;
1637
- }
1638
-
1639
- const changed = nextText !== existing;
1640
- const write = Boolean(options.write);
1641
- if (write && changed) {
1642
- await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
1643
- await fs.writeFile(resolvedTarget, nextText);
1644
- }
1645
-
1646
- return {
1647
- client: 'codex',
1648
- action: 'install',
1649
- targetPath: resolvedTarget,
1650
- markerStart: CODEX_PROFILE_MARKER_START,
1651
- markerEnd: CODEX_PROFILE_MARKER_END,
1652
- installed: marker.present || (write && changed),
1653
- written: write,
1654
- changed,
1655
- markerPresent: marker.present,
1656
- writesTokenValue: false
1657
- };
1658
- }
1659
-
1660
- async function codexProfileStatusResult(targetPath) {
1661
- const resolvedTarget = path.resolve(targetPath);
1662
- const existing = await readTextIfExists(resolvedTarget);
1663
- const marker = markerBounds(existing);
1664
- return {
1665
- client: 'codex',
1666
- action: 'status',
1667
- targetPath: resolvedTarget,
1668
- installed: marker.present,
1669
- markerPresent: marker.present,
1670
- markerStart: CODEX_PROFILE_MARKER_START,
1671
- markerEnd: CODEX_PROFILE_MARKER_END,
1672
- writesTokenValue: false
1673
- };
1674
- }
1675
-
1676
- async function codexProfileUninstallResult(targetPath, options = {}) {
1677
- const resolvedTarget = path.resolve(targetPath);
1678
- const existing = await readTextIfExists(resolvedTarget);
1679
- const marker = markerBounds(existing);
1680
- const write = Boolean(options.write);
1681
- let changed = false;
1682
-
1683
- if (marker.present) {
1684
- let nextText = `${existing.slice(0, marker.start)}${existing.slice(marker.end)}`;
1685
- nextText = nextText.replace(/\n{3,}/g, '\n\n');
1686
- if (nextText.trim().length === 0) {
1687
- nextText = '';
1688
- } else if (!nextText.endsWith('\n')) {
1689
- nextText = `${nextText}\n`;
1690
- }
1691
- changed = nextText !== existing;
1692
- if (write && changed) {
1693
- await fs.writeFile(resolvedTarget, nextText);
1694
- }
1695
- }
1696
-
1697
- return {
1698
- client: 'codex',
1699
- action: 'uninstall',
1700
- targetPath: resolvedTarget,
1701
- installed: marker.present && !(write && changed),
1702
- written: write,
1703
- changed,
1704
- markerPresent: marker.present,
1705
- markerStart: CODEX_PROFILE_MARKER_START,
1706
- markerEnd: CODEX_PROFILE_MARKER_END,
1707
- writesTokenValue: false
1708
- };
1709
- }
1710
-
1711
- function markerBounds(content) {
1712
- const start = content.indexOf(CODEX_PROFILE_MARKER_START);
1713
- const end = content.indexOf(CODEX_PROFILE_MARKER_END);
1714
- if (start === -1 && end === -1) {
1715
- return { present: false, start: -1, end: -1 };
1716
- }
1717
-
1718
- if (start === -1 || end === -1 || end < start) {
1719
- throw new UsageError('Codex profile markers are incomplete or out of order; edit the target file manually before retrying.');
1720
- }
1721
-
1722
- if (
1723
- content.indexOf(CODEX_PROFILE_MARKER_START, start + CODEX_PROFILE_MARKER_START.length) !== -1
1724
- || content.indexOf(CODEX_PROFILE_MARKER_END, end + CODEX_PROFILE_MARKER_END.length) !== -1
1725
- ) {
1726
- throw new UsageError('Codex profile markers appear more than once; edit the target file manually before retrying.');
1727
- }
1728
-
1729
- const afterEnd = end + CODEX_PROFILE_MARKER_END.length;
1730
- const trailingNewlineLength = content.slice(afterEnd, afterEnd + 2) === '\r\n'
1731
- ? 2
1732
- : content.slice(afterEnd, afterEnd + 1) === '\n'
1733
- ? 1
1734
- : 0;
1735
-
1736
- return {
1737
- present: true,
1738
- start,
1739
- end: afterEnd + trailingNewlineLength
1740
- };
1741
- }
1742
-
1743
- function writeProfileResult(action, result, io) {
1744
- writeLine(io.stdout, `${PRODUCT_NAME} Codex profile ${action}`);
1745
- writeLine(io.stdout, ` Target: ${result.targetPath}`);
1746
- writeLine(io.stdout, ` Installed: ${result.installed}`);
1747
- if ('written' in result) {
1748
- writeLine(io.stdout, ` Written: ${result.written}`);
1749
- writeLine(io.stdout, ` Changed: ${result.changed}`);
1750
- }
1751
- writeLine(io.stdout, ' Token value embedded: false');
1752
- }
1753
-
1754
- async function codexSmokeReport(configPath, env) {
1755
- const configText = await readTextIfExists(configPath);
1756
- const block = tomlServerBlock(configText, MCP_SERVER_NAME);
1757
- const mcpUrl = block ? tomlStringValue(block, 'url') : null;
1758
- const bearerTokenEnvVar = block ? tomlStringValue(block, 'bearer_token_env_var') : null;
1759
- const tokenValue = env[TOKEN_ENV_VAR] ?? '';
1760
- const identityPath = agentInstanceIdentityPath(env, 'codex');
1761
- const identityPresent = await fileExists(identityPath);
1762
- const checks = [
1763
- {
1764
- name: 'config_present',
1765
- ok: configText.trim().length > 0,
1766
- required: true,
1767
- detail: configText.trim().length > 0 ? 'found' : 'missing'
1768
- },
1769
- {
1770
- name: 'memory_os_server_present',
1771
- ok: Boolean(block),
1772
- required: true,
1773
- detail: block ? `[mcp_servers.${MCP_SERVER_NAME}]` : `missing [mcp_servers.${MCP_SERVER_NAME}]`
1774
- },
1775
- {
1776
- name: 'mcp_url_present',
1777
- ok: Boolean(mcpUrl),
1778
- required: true,
1779
- detail: mcpUrl ?? 'missing url'
1780
- },
1781
- {
1782
- name: 'bearer_token_env_var',
1783
- ok: bearerTokenEnvVar === TOKEN_ENV_VAR,
1784
- required: true,
1785
- detail: bearerTokenEnvVar ?? 'missing bearer_token_env_var'
1786
- },
1787
- {
1788
- name: 'token_env_present',
1789
- ok: Boolean(env[TOKEN_ENV_VAR]),
1790
- required: true,
1791
- detail: env[TOKEN_ENV_VAR] ? 'present' : `missing ${TOKEN_ENV_VAR}`
1792
- },
1793
- {
1794
- name: 'token_not_embedded_in_config',
1795
- ok: !tokenValue || !configText.includes(tokenValue),
1796
- required: true,
1797
- detail: 'token value not printed or embedded'
1798
- },
1799
- {
1800
- name: 'agent_instance_identity_file',
1801
- ok: identityPresent,
1802
- required: false,
1803
- detail: identityPresent ? identityPath : `optional; create with ${COMMAND_NAME} mcp add codex --write (${identityPath})`
1804
- }
1805
- ];
1806
-
1807
- return {
1808
- ok: checks.every((check) => !check.required || check.ok),
1809
- client: 'codex',
1810
- configPath,
1811
- serverName: MCP_SERVER_NAME,
1812
- mcpUrl,
1813
- tokenEnvVar: TOKEN_ENV_VAR,
1814
- agentInstanceIdPath: identityPath,
1815
- checks
1816
- };
1817
- }
1818
-
1819
- function tomlServerBlock(content, serverName) {
1820
- const header = `[mcp_servers.${serverName}]`;
1821
- const lines = content.split(/\r?\n/);
1822
- const start = lines.findIndex((line) => line.trim() === header);
1823
- if (start === -1) {
1824
- return '';
1825
- }
1826
-
1827
- const block = [];
1828
- for (let index = start + 1; index < lines.length; index += 1) {
1829
- const line = lines[index];
1830
- if (/^\s*\[/.test(line)) {
1831
- break;
1832
- }
1833
- block.push(line);
1834
- }
1835
- return block.join('\n');
1836
- }
1837
-
1838
- function tomlStringValue(block, key) {
1839
- const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"((?:\\\\.|[^"\\\\])*)"\\s*$`, 'm');
1840
- const match = block.match(pattern);
1841
- return match ? unescapeTomlString(match[1]) : null;
1842
- }
1843
-
1844
- function cursorJsonSnippet(mcpUrl, identity = envReferenceIdentity('cursor')) {
1845
- return `${JSON.stringify(cursorJsonConfig(mcpUrl, identity), null, 2)}\n`;
1846
- }
1847
-
1848
- async function appendTomlServerConfig(configPath, mcpUrl) {
1849
- const snippet = codexTomlSnippet(mcpUrl);
1850
- const existing = await readTextIfExists(configPath);
1851
- if (existing.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
1852
- throw new UsageError(`MCP config already contains [mcp_servers.${MCP_SERVER_NAME}]. Edit ${configPath} manually to avoid duplicate server definitions.`);
1853
- }
1854
-
1855
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
1856
- const prefix = existing.trim().length === 0 ? '' : '\n\n';
1857
- await fs.appendFile(configPath, `${prefix}${snippet}`, { mode: 0o600 });
1858
- await bestEffortChmod(configPath, 0o600);
1859
- }
1860
-
1861
- async function mergeJsonMcpConfig(configPath, mcpUrl, identity) {
1862
- const existing = await readTextIfExists(configPath);
1863
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
1864
-
1865
- if (!isPlainObject(parsed)) {
1866
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
1867
- }
1868
-
1869
- if (!isPlainObject(parsed.mcpServers)) {
1870
- parsed.mcpServers = {};
1871
- }
1872
-
1873
- if (parsed.mcpServers[MCP_SERVER_NAME]) {
1874
- throw new UsageError(`MCP config already contains mcpServers.${MCP_SERVER_NAME}. Edit ${configPath} manually to avoid duplicate server definitions.`);
1875
- }
1876
-
1877
- parsed.mcpServers[MCP_SERVER_NAME] = cursorJsonServerConfig(mcpUrl, identity);
1878
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
1879
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
1880
- await bestEffortChmod(configPath, 0o600);
1881
- }
1882
-
1883
- async function mergeCopilotMcpConfig(configPath, proxyUrl) {
1884
- const existing = await readTextIfExists(configPath);
1885
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
1886
-
1887
- if (!isPlainObject(parsed)) {
1888
- throw new UsageError(`Copilot MCP JSON config must be an object: ${configPath}`);
1889
- }
1890
-
1891
- if (!isPlainObject(parsed.mcpServers)) {
1892
- parsed.mcpServers = {};
1893
- }
1894
-
1895
- parsed.mcpServers['memory-os'] = copilotLocalProxyServerConfig(proxyUrl);
1896
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
1897
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
1898
- await bestEffortChmod(configPath, 0o600);
1899
- }
1900
-
1901
- function copilotLocalProxyServerConfig(proxyUrl) {
1902
- return {
1903
- type: 'http',
1904
- url: proxyUrl
1905
- };
1906
- }
1907
-
1908
- function cursorJsonConfig(mcpUrl, identity = envReferenceIdentity('cursor')) {
1909
- return {
1910
- mcpServers: {
1911
- [MCP_SERVER_NAME]: cursorJsonServerConfig(mcpUrl, identity)
1912
- }
1913
- };
1914
- }
1915
-
1916
- function cursorJsonServerConfig(mcpUrl, identity = envReferenceIdentity('cursor')) {
1917
- return {
1918
- url: mcpUrl,
1919
- headers: {
1920
- Authorization: `Bearer \${env:${TOKEN_ENV_VAR}}`,
1921
- [AGENT_ID_HEADER]: identity.agentId,
1922
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
1923
- }
1924
- };
1925
- }
1926
-
1927
- async function agentIdentity(clientId, env) {
1928
- const configuredInstanceId = env[AGENT_INSTANCE_ENV_VAR];
1929
- if (configuredInstanceId) {
1930
- return {
1931
- agentId: clientId,
1932
- agentInstanceId: configuredInstanceId,
1933
- path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
1934
- };
1935
- }
1936
-
1937
- const identityPath = agentInstanceIdentityPath(env, clientId);
1938
- const existing = await readAgentInstanceIdentity(identityPath);
1939
- if (existing) {
1940
- return { agentId: clientId, agentInstanceId: existing, path: identityPath };
1941
- }
1942
-
1943
- const generated = `xmemo-${clientId}-${randomUUID()}`;
1944
- await fs.mkdir(path.dirname(identityPath), { recursive: true, mode: 0o700 });
1945
- await bestEffortChmod(path.dirname(identityPath), 0o700);
1946
- await fs.writeFile(identityPath, `${JSON.stringify({ version: 1, agentId: clientId, agentInstanceId: generated }, null, 2)}\n`, { mode: 0o600 });
1947
- await bestEffortChmod(identityPath, 0o600);
1948
- return { agentId: clientId, agentInstanceId: generated, path: identityPath };
1949
- }
1950
-
1951
- async function readAgentInstanceIdentity(identityPath) {
1952
- const existing = await readTextIfExists(identityPath);
1953
- if (!existing.trim()) {
1954
- return null;
1955
- }
1956
- const parsed = parseJsonConfig(existing, identityPath);
1957
- const value = stringValue(parsed, ['agentInstanceId']);
1958
- return value || null;
1959
- }
1960
-
1961
- function agentInstanceIdentityPath(env, clientId) {
1962
- return path.join(configRoot(env), 'agent-instances', `${clientId}.json`);
1963
- }
1964
-
1965
- function envReferenceIdentity(clientId) {
1966
- return {
1967
- agentId: clientId,
1968
- agentInstanceId: `\${${AGENT_INSTANCE_ENV_VAR}}`,
1969
- path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
1970
- };
1971
- }
1972
-
1973
- function supportedMcpClients() {
1974
- const clients = Array.from(MCP_CLIENTS.entries()).map(([id, client]) => ({
1975
- id,
1976
- label: client.label,
1977
- configKind: client.configKind
1978
- }));
1979
- clients.push({ id: 'copilot-cli', label: 'Copilot CLI', configKind: 'local-proxy' });
1980
- return clients;
1981
- }
1982
-
1983
- function supportedMcpClientIds() {
1984
- return Array.from(MCP_CLIENTS.keys());
1985
- }
1986
-
1987
- function supportedSetupClientIds() {
1988
- return ['codex', 'cursor', 'copilot'];
1989
- }
1990
-
1991
- function credentialsPath(env) {
1992
- return path.join(configRoot(env), 'credentials.json');
1993
- }
1994
-
1995
- function configRoot(env) {
1996
- if (env.XMEMO_CONFIG_HOME) {
1997
- return env.XMEMO_CONFIG_HOME;
1998
- }
1999
-
2000
- if (env.MEMORY_OS_CONFIG_HOME) {
2001
- return env.MEMORY_OS_CONFIG_HOME;
2002
- }
2003
-
2004
- if (process.platform === 'win32' && env.LOCALAPPDATA) {
2005
- return path.join(env.LOCALAPPDATA, 'XMemo', 'CLI');
2006
- }
2007
-
2008
- if (env.XDG_CONFIG_HOME) {
2009
- return path.join(env.XDG_CONFIG_HOME, 'xmemo');
2010
- }
2011
-
2012
- const home = env.HOME || os.homedir();
2013
- return path.join(home, '.config', 'xmemo');
2014
- }
2015
-
2016
- function defaultCodexConfigPath(env) {
2017
- const home = env.USERPROFILE || env.HOME || os.homedir();
2018
- return path.join(home, '.codex', 'config.toml');
2019
- }
2020
-
2021
- function defaultCursorConfigPath(env) {
2022
- const home = env.USERPROFILE || env.HOME || os.homedir();
2023
- return path.join(home, '.cursor', 'mcp.json');
2024
- }
2025
-
2026
- function defaultCopilotConfigPath(env) {
2027
- const home = env.USERPROFILE || env.HOME || os.homedir();
2028
- return path.join(env.COPILOT_HOME ?? path.join(home, '.copilot'), 'mcp-config.json');
2029
- }
2030
-
2031
- async function writePlaintextCredential(credentialPath, token, metadata = {}) {
2032
- await fs.mkdir(path.dirname(credentialPath), { recursive: true, mode: 0o700 });
2033
- await bestEffortChmod(path.dirname(credentialPath), 0o700);
2034
- const payload = {
2035
- version: 1,
2036
- tokenEnvVar: TOKEN_ENV_VAR,
2037
- storage: 'user-scoped-credential-file',
2038
- createdAt: new Date().toISOString(),
2039
- metadata,
2040
- token
2041
- };
2042
- await fs.writeFile(credentialPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
2043
- await bestEffortChmod(credentialPath, 0o600);
2044
- }
2045
-
2046
- async function bestEffortChmod(filePath, mode) {
2047
- try {
2048
- await fs.chmod(filePath, mode);
2049
- } catch {
2050
- // Windows and managed environments may ignore POSIX chmod.
2051
- }
2052
- }
2053
-
2054
- function validateToken(token) {
2055
- if (!token) {
2056
- throw new UsageError('Token from stdin is empty.');
2057
- }
2058
-
2059
- if (/\s/.test(token)) {
2060
- throw new UsageError('Token must not contain whitespace.');
2061
- }
2062
-
2063
- if (token.length < 16) {
2064
- throw new UsageError('Token is too short to be a production credential.');
2065
- }
2066
- }
2067
-
2068
- function requiredOption(args, name) {
2069
- const value = optionValue(args, name);
2070
- if (!value) {
2071
- throw new UsageError(`Missing required option ${name}.`);
2072
- }
2073
- return value;
2074
- }
2075
-
2076
- function positionalClientArg(args) {
2077
- const candidate = args[0];
2078
- if (!candidate || candidate.startsWith('--')) {
2079
- return null;
2080
- }
2081
-
2082
- return normalizeSetupClientId(candidate);
2083
- }
2084
-
2085
- function normalizeSetupClientId(candidate) {
2086
- if (!candidate) {
2087
- return null;
2088
- }
2089
-
2090
- const normalized = SETUP_CLIENT_ALIASES.get(candidate);
2091
- if (!normalized) {
2092
- throw new UsageError(`Unsupported setup client: ${candidate}. Supported clients: ${supportedSetupClientIds().join(', ')}.`);
2093
- }
2094
-
2095
- return normalized;
2096
- }
2097
-
2098
- function optionValue(args, name) {
2099
- const index = args.indexOf(name);
2100
- if (index === -1) {
2101
- return null;
2102
- }
2103
-
2104
- const value = args[index + 1];
2105
- if (!value || value.startsWith('--')) {
2106
- throw new UsageError(`Option ${name} requires a value.`);
2107
- }
2108
-
2109
- return value;
2110
- }
2111
-
2112
- function stringValue(source, keys) {
2113
- const value = valueAtPath(source, keys);
2114
- return typeof value === 'string' && value.length > 0 ? value : null;
2115
- }
2116
-
2117
- function booleanValue(source, keys) {
2118
- const value = valueAtPath(source, keys);
2119
- return typeof value === 'boolean' ? value : null;
2120
- }
2121
-
2122
- function arrayValue(source, keys) {
2123
- const value = valueAtPath(source, keys);
2124
- return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : null;
2125
- }
2126
-
2127
- function valueAtPath(source, keys) {
2128
- let current = source;
2129
- for (const key of keys) {
2130
- if (!isPlainObject(current) || !(key in current)) {
2131
- return null;
2132
- }
2133
- current = current[key];
2134
- }
2135
- return current;
2136
- }
2137
-
2138
- function hasFlag(args, name) {
2139
- return args.includes(name);
2140
- }
2141
-
2142
- function parsePositiveInteger(value, name) {
2143
- const parsed = Number.parseInt(value, 10);
2144
- if (!Number.isInteger(parsed) || parsed <= 0) {
2145
- throw new UsageError(`${name} must be a positive integer.`);
2146
- }
2147
- return parsed;
2148
- }
2149
-
2150
- async function sleep(ms) {
2151
- await new Promise((resolve) => setTimeout(resolve, ms));
2152
- }
2153
-
2154
-
2155
- function npmExecutable() {
2156
- return os.platform() === 'win32' ? 'npm.cmd' : 'npm';
2157
- }
2158
-
2159
-
2160
- async function runProcess(command, args, io, { stream = true } = {}) {
2161
- const spawnFn = io.spawn ?? spawn;
2162
- return await new Promise((resolve, reject) => {
2163
- const child = spawnFn(command, args, {
2164
- stdio: ['ignore', 'pipe', 'pipe']
2165
- });
2166
- let stdout = '';
2167
- let stderr = '';
2168
- child.stdout?.on('data', (chunk) => {
2169
- const text = String(chunk);
2170
- stdout += text;
2171
- if (stream) {
2172
- io.stdout.write(text);
2173
- }
2174
- });
2175
- child.stderr?.on('data', (chunk) => {
2176
- const text = String(chunk);
2177
- stderr += text;
2178
- if (stream) {
2179
- io.stderr.write(text);
2180
- }
2181
- });
2182
- child.on('error', reject);
2183
- child.on('close', (code) => {
2184
- resolve({ code: code ?? 0, stdout, stderr });
2185
- });
2186
- });
2187
- }
2188
-
2189
-
2190
- async function readAll(stream) {
2191
- let content = '';
2192
- for await (const chunk of stream) {
2193
- content += chunk;
2194
- }
2195
- return content;
2196
- }
2197
-
2198
- async function fileExists(filePath) {
2199
- try {
2200
- await fs.access(filePath);
2201
- return true;
2202
- } catch {
2203
- return false;
2204
- }
2205
- }
2206
-
2207
- async function readTextIfExists(filePath) {
2208
- try {
2209
- return await fs.readFile(filePath, 'utf8');
2210
- } catch (error) {
2211
- if (error.code === 'ENOENT') {
2212
- return '';
2213
- }
2214
- throw error;
2215
- }
2216
- }
2217
-
2218
- function parseJsonConfig(content, configPath) {
2219
- try {
2220
- return JSON.parse(content);
2221
- } catch (error) {
2222
- throw new UsageError(`Invalid JSON in ${configPath}: ${error.message}`);
2223
- }
2224
- }
2225
-
2226
- function isPlainObject(value) {
2227
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
2228
- }
2229
-
2230
- function escapeTomlString(value) {
2231
- return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
2232
- }
2233
-
2234
- function unescapeTomlString(value) {
2235
- return value.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
2236
- }
2237
-
2238
- function escapeRegExp(value) {
2239
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2240
- }
2241
-
2242
- function writeLine(stream, line) {
2243
- stream.write(`${line}\n`);
2244
- }
1
+ import fs from 'node:fs/promises';
2
+ import http from 'node:http';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { randomUUID } from 'node:crypto';
7
+
8
+ const PRODUCT_NAME = 'XMemo';
9
+ const PACKAGE_NAME = '@xmemo/client';
10
+ const FALLBACK_PACKAGE_NAME = '@yonro/xmemo-client';
11
+ const COMMAND_NAME = 'xmemo';
12
+ const LEGACY_COMMAND_NAME = 'memory-os';
13
+ const CLI_VERSION = '0.4.136';
14
+ const DEFAULT_SERVICE_URL = 'https://xmemo.dev';
15
+ const TOKEN_ENV_VAR = 'XMEMO_KEY';
16
+ const LEGACY_TOKEN_ENV_VAR = 'MEMORY_OS_MCP_TOKEN';
17
+ const AGENT_ID_ENV_VAR = 'XMEMO_AGENT_ID';
18
+ const AGENT_INSTANCE_ENV_VAR = 'XMEMO_AGENT_INSTANCE_ID';
19
+ const AGENT_ID_HEADER = 'X-Memory-OS-Agent-ID';
20
+ const AGENT_INSTANCE_HEADER = 'X-Memory-OS-Agent-Instance-ID';
21
+ const MCP_SERVER_NAME = 'memory_os';
22
+ const CODEX_PROFILE_TARGET = 'AGENTS.md';
23
+ const CODEX_PROFILE_MARKER_START = '<!-- memory-os:codex-profile:start -->';
24
+ const CODEX_PROFILE_MARKER_END = '<!-- memory-os:codex-profile:end -->';
25
+ const DEVICE_LOGIN_START_PATH = '/api/v1/auth/device/start';
26
+ const DEVICE_LOGIN_TOKEN_PATH = '/api/v1/auth/device/token';
27
+ const DEFAULT_PROXY_HOST = '127.0.0.1';
28
+ const DEFAULT_PROXY_PORT = 8765;
29
+
30
+ const MCP_CLIENTS = new Map([
31
+ ['codex', {
32
+ label: 'Codex',
33
+ defaultConfigPath: defaultCodexConfigPath,
34
+ buildSnippet: codexTomlSnippet,
35
+ writeConfig: appendTomlServerConfig,
36
+ configKind: 'toml'
37
+ }],
38
+ ['cursor', {
39
+ label: 'Cursor',
40
+ defaultConfigPath: defaultCursorConfigPath,
41
+ buildSnippet: cursorJsonSnippet,
42
+ writeConfig: mergeJsonMcpConfig,
43
+ configKind: 'json'
44
+ }],
45
+ ['gemini-cli', {
46
+ label: 'Gemini CLI',
47
+ defaultConfigPath: defaultGeminiConfigPath,
48
+ buildSnippet: geminiJsonSnippet,
49
+ writeConfig: mergeGeminiMcpConfig,
50
+ configKind: 'json'
51
+ }],
52
+ ['antigravity', {
53
+ label: 'Antigravity',
54
+ defaultConfigPath: defaultAntigravityConfigPath,
55
+ buildSnippet: antigravityJsonSnippet,
56
+ writeConfig: mergeAntigravityMcpConfig,
57
+ configKind: 'json'
58
+ }]
59
+ ]);
60
+
61
+ const SETUP_CLIENT_ALIASES = new Map([
62
+ ['codex', 'codex'],
63
+ ['cursor', 'cursor'],
64
+ ['copilot', 'copilot-cli'],
65
+ ['copilot-cli', 'copilot-cli'],
66
+ ['gemini', 'gemini-cli'],
67
+ ['gemini-cli', 'gemini-cli'],
68
+ ['antigravity', 'antigravity']
69
+ ]);
70
+
71
+ class UsageError extends Error {
72
+ constructor(message) {
73
+ super(message);
74
+ this.name = 'UsageError';
75
+ }
76
+ }
77
+
78
+ export async function run(args, io = defaultIo()) {
79
+ try {
80
+ const command = args[0] ?? 'help';
81
+
82
+ if (command === '--help' || command === '-h' || command === 'help') {
83
+ writeHelp(io);
84
+ return 0;
85
+ }
86
+
87
+ if (command === '--version' || command === '-v' || command === 'version') {
88
+ writeLine(io.stdout, CLI_VERSION);
89
+ return 0;
90
+ }
91
+
92
+ if (command === 'update' || command === '--update') {
93
+ return await updateCommand(args.slice(1), io);
94
+ }
95
+
96
+ if (command === 'doctor') {
97
+ return await doctorCommand(args.slice(1), io);
98
+ }
99
+
100
+ if (command === 'discovery') {
101
+ return await discoveryCommand(args.slice(1), io);
102
+ }
103
+
104
+ if (command === 'status') {
105
+ return await statusCommand(args.slice(1), io);
106
+ }
107
+
108
+ if (command === 'setup') {
109
+ return await setupCommand(args.slice(1), io);
110
+ }
111
+
112
+ if (command === 'login') {
113
+ return await loginCommand(args.slice(1), io);
114
+ }
115
+
116
+ if (command === 'auth') {
117
+ return await authCommand(args.slice(1), io);
118
+ }
119
+
120
+ if (command === 'token') {
121
+ return await tokenCommand(args.slice(1), io);
122
+ }
123
+
124
+ if (command === 'mcp') {
125
+ return await mcpCommand(args.slice(1), io);
126
+ }
127
+
128
+ if (command === 'profile') {
129
+ return await profileCommand(args.slice(1), io);
130
+ }
131
+
132
+ if (command === 'smoke') {
133
+ return await smokeCommand(args.slice(1), io);
134
+ }
135
+
136
+ if (command === 'env') {
137
+ return envCommand(args.slice(1), io);
138
+ }
139
+
140
+ if (command === 'privacy') {
141
+ writePrivacy(io);
142
+ return 0;
143
+ }
144
+
145
+ throw new UsageError(`Unknown command: ${command}`);
146
+ } catch (error) {
147
+ if (error instanceof UsageError) {
148
+ writeLine(io.stderr, `Error: ${error.message}`);
149
+ writeLine(io.stderr, `Run \`${COMMAND_NAME} help\` for usage.`);
150
+ return 2;
151
+ }
152
+
153
+ writeLine(io.stderr, `Unexpected error: ${error.message}`);
154
+ return 1;
155
+ }
156
+ }
157
+
158
+ function defaultIo() {
159
+ return {
160
+ env: process.env,
161
+ stdin: process.stdin,
162
+ stdout: process.stdout,
163
+ stderr: process.stderr,
164
+ fetch: globalThis.fetch,
165
+ spawn
166
+ };
167
+ }
168
+
169
+ function writeHelp(io) {
170
+ writeLine(io.stdout, `${PRODUCT_NAME} CLI (${PACKAGE_NAME})`);
171
+ writeLine(io.stdout, `Fallback npm package: ${FALLBACK_PACKAGE_NAME}; legacy command alias: ${LEGACY_COMMAND_NAME}`);
172
+ writeLine(io.stdout, '');
173
+ writeLine(io.stdout, 'Usage:');
174
+ writeLine(io.stdout, ` ${COMMAND_NAME} update [--dry-run] [--json]`);
175
+ writeLine(io.stdout, ` ${COMMAND_NAME} doctor [--base-url <https://api.example.com>] [--json]`);
176
+ writeLine(io.stdout, ` ${COMMAND_NAME} discovery show [--base-url <https://api.example.com>] [--json]`);
177
+ writeLine(io.stdout, ` ${COMMAND_NAME} setup <codex|cursor|copilot|gemini|antigravity> [--url <https://api.example.com>] [--dry-run] [--json]`);
178
+ writeLine(io.stdout, ` ${COMMAND_NAME} login [--from-stdin] [--base-url <url>] [--timeout-ms <ms>] [--http-timeout-ms <ms>] [--json]`);
179
+ writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
180
+ writeLine(io.stdout, ` ${COMMAND_NAME} status [--url <https://api.example.com>] [--json]`);
181
+ writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify]`);
182
+ writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
183
+ writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
184
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
185
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|antigravity|generic> [--base-url <url>] [--json]`);
186
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}]`);
187
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp profile codex [--json]`);
188
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile install codex [--target AGENTS.md] [--dry-run|--json]`);
189
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target AGENTS.md] [--json]`);
190
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>] [--write] [--config <path>]`);
191
+ writeLine(io.stdout, ` ${COMMAND_NAME} smoke --client codex [--config <path>] [--json]`);
192
+ writeLine(io.stdout, ` ${COMMAND_NAME} env example [--shell bash|powershell|cmd] [--json]`);
193
+ writeLine(io.stdout, ` ${COMMAND_NAME} privacy`);
194
+ writeLine(io.stdout, '');
195
+ writeLine(io.stdout, `Default service URL: ${DEFAULT_SERVICE_URL}; use --url or XMEMO_URL for private deployments.`);
196
+ writeLine(io.stdout, '`login --timeout-ms` controls the full browser approval window; HTTP calls use `--http-timeout-ms`.');
197
+ writeLine(io.stdout, '');
198
+ writeLine(io.stdout, 'Privacy defaults: no telemetry, no token in project files, and no token is sent by `status`, `doctor`, or `discovery`.');
199
+ writeLine(io.stdout, '`login` and `token add` store credentials only in the user-scoped XMemo CLI config directory.');
200
+ }
201
+
202
+ async function updateCommand(args, io) {
203
+ const outputJson = hasFlag(args, '--json');
204
+ const dryRun = hasFlag(args, '--dry-run');
205
+ const npmCommand = npmExecutable();
206
+ const npmArgs = ['install', '-g', `${PACKAGE_NAME}@latest`];
207
+ const report = {
208
+ package: PACKAGE_NAME,
209
+ command: [npmCommand, ...npmArgs],
210
+ dryRun,
211
+ tokenSent: false,
212
+ projectFilesModified: false
213
+ };
214
+
215
+ if (dryRun) {
216
+ if (outputJson) {
217
+ writeLine(io.stdout, JSON.stringify(report, null, 2));
218
+ } else {
219
+ writeLine(io.stdout, `Update command: ${report.command.join(' ')}`);
220
+ writeLine(io.stdout, 'Dry run only; no changes made.');
221
+ }
222
+ return 0;
223
+ }
224
+
225
+ if (!outputJson) {
226
+ writeLine(io.stdout, `Updating ${PACKAGE_NAME} with: ${report.command.join(' ')}`);
227
+ }
228
+ const result = await runProcess(npmCommand, npmArgs, io, { stream: !outputJson });
229
+ report.exitCode = result.code;
230
+ report.completed = result.code === 0;
231
+
232
+ if (outputJson) {
233
+ writeLine(io.stdout, JSON.stringify(report, null, 2));
234
+ }
235
+ if (result.code !== 0) {
236
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
237
+ throw new UsageError(`Update failed: ${detail}`);
238
+ }
239
+ if (!outputJson) {
240
+ writeLine(io.stdout, `Update complete. Run \`${COMMAND_NAME} --version\` to confirm.`);
241
+ }
242
+ return 0;
243
+ }
244
+
245
+ async function doctorCommand(args, io) {
246
+ const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
247
+ const outputJson = hasFlag(args, '--json');
248
+ const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
249
+ const discoveryUrl = endpointUrl(baseUrl, '/.well-known/agent-discovery.json');
250
+ const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
251
+ ensureDiscoveryService(discovery, discoveryUrl);
252
+
253
+ const rootVersion = await bestEffortRootVersion(discovery, timeoutMs, io);
254
+ const mcpUrl = discoveryMcpUrl(discovery, baseUrl);
255
+ const checks = [
256
+ { name: 'node_version', ok: Number.parseInt(process.versions.node.split('.')[0], 10) >= 20, detail: process.versions.node },
257
+ { name: 'discovery_reachable', ok: true, detail: discoveryUrl },
258
+ { name: 'mcp_url_present', ok: Boolean(mcpUrl), detail: mcpUrl ?? 'missing' },
259
+ { name: 'no_remote_code_execution', ok: booleanValue(discovery, ['security', 'no_remote_code_execution']) === true, detail: String(booleanValue(discovery, ['security', 'no_remote_code_execution'])) },
260
+ {
261
+ name: 'token_not_in_discovery',
262
+ ok: booleanValue(discovery, ['security', 'token_in_discovery']) === false && booleanValue(discovery, ['auth', 'token_in_discovery']) === false,
263
+ detail: `security=${booleanValue(discovery, ['security', 'token_in_discovery'])} auth=${booleanValue(discovery, ['auth', 'token_in_discovery'])}`
264
+ },
265
+ {
266
+ name: 'service_version_compatible',
267
+ ok: rootVersion.version ? sameMajorMinor(CLI_VERSION, rootVersion.version) : true,
268
+ detail: rootVersion.version ? `service=${rootVersion.version} cli=${CLI_VERSION}` : `service version unavailable${rootVersion.error ? `: ${rootVersion.error}` : ''}`
269
+ }
270
+ ];
271
+ const report = {
272
+ ok: checks.every((check) => check.ok),
273
+ cli: { package: PACKAGE_NAME, version: CLI_VERSION, node: process.versions.node },
274
+ discovery: {
275
+ url: discoveryUrl,
276
+ schemaVersion: stringValue(discovery, ['schema_version']),
277
+ protocol: stringValue(discovery, ['protocol']),
278
+ service: stringValue(discovery, ['service']),
279
+ serviceVersion: rootVersion.version ?? null,
280
+ mcpUrl,
281
+ supportedClients: agentDiscoveryClientIds(discovery)
282
+ },
283
+ checks
284
+ };
285
+
286
+ if (outputJson) {
287
+ writeLine(io.stdout, JSON.stringify(report, null, 2));
288
+ return report.ok ? 0 : 1;
289
+ }
290
+
291
+ writeLine(io.stdout, `${PRODUCT_NAME} CLI ${CLI_VERSION}`);
292
+ writeLine(io.stdout, `Discovery: ${discoveryUrl}`);
293
+ writeLine(io.stdout, `MCP: ${mcpUrl ?? 'missing'}`);
294
+ if (rootVersion.version) {
295
+ writeLine(io.stdout, `Service version: ${rootVersion.version}`);
296
+ }
297
+ writeLine(io.stdout, `Supported clients: ${report.discovery.supportedClients.join(', ') || 'unknown'}`);
298
+ for (const check of checks) {
299
+ writeLine(io.stdout, `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.detail}`);
300
+ }
301
+ return report.ok ? 0 : 1;
302
+ }
303
+
304
+ async function discoveryCommand(args, io) {
305
+ const subcommand = args[0] ?? 'help';
306
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
307
+ writeLine(io.stdout, 'Discovery commands:');
308
+ writeLine(io.stdout, ` ${COMMAND_NAME} discovery show [--base-url <https://api.example.com>] [--json]`);
309
+ return 0;
310
+ }
311
+ if (subcommand !== 'show') {
312
+ throw new UsageError(`Unknown discovery command: ${subcommand}`);
313
+ }
314
+
315
+ const baseUrl = normalizeBaseUrl(baseUrlOption(args.slice(1), io.env));
316
+ const outputJson = hasFlag(args, '--json');
317
+ const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
318
+ const discoveryUrl = endpointUrl(baseUrl, '/.well-known/agent-discovery.json');
319
+ const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
320
+ ensureDiscoveryService(discovery, discoveryUrl);
321
+
322
+ if (outputJson) {
323
+ writeLine(io.stdout, JSON.stringify(discovery, null, 2));
324
+ return 0;
325
+ }
326
+
327
+ writeLine(io.stdout, `${stringValue(discovery, ['name']) ?? PRODUCT_NAME} discovery`);
328
+ writeLine(io.stdout, `URL: ${discoveryUrl}`);
329
+ writeLine(io.stdout, `Protocol: ${stringValue(discovery, ['protocol']) ?? 'unknown'}`);
330
+ writeLine(io.stdout, `MCP: ${discoveryMcpUrl(discovery, baseUrl) ?? 'missing'}`);
331
+ writeLine(io.stdout, `Docs: ${stringValue(discovery, ['urls', 'docs']) ?? 'unknown'}`);
332
+ writeLine(io.stdout, `Clients: ${agentDiscoveryClientIds(discovery).join(', ') || 'unknown'}`);
333
+ writeLine(io.stdout, 'Security: read-only discovery; tokens are not returned; remote code execution is not advertised.');
334
+ return 0;
335
+ }
336
+
337
+ async function statusCommand(args, io) {
338
+ const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
339
+ const outputJson = hasFlag(args, '--json');
340
+ const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
341
+ const endpoints = [
342
+ endpointUrl(baseUrl, '/.well-known/memory-os.json'),
343
+ endpointUrl(baseUrl, '/health'),
344
+ endpointUrl(baseUrl, '/ready')
345
+ ];
346
+
347
+ const probes = [];
348
+ for (const url of endpoints) {
349
+ probes.push(await probe(url, timeoutMs, io));
350
+ }
351
+
352
+ const result = {
353
+ ok: probes.some((item) => item.ok),
354
+ baseUrl,
355
+ privacy: {
356
+ telemetry: false,
357
+ tokenSent: false,
358
+ tokenSource: 'not-used-by-status'
359
+ },
360
+ probes
361
+ };
362
+
363
+ if (outputJson) {
364
+ writeLine(io.stdout, JSON.stringify(result, null, 2));
365
+ return result.ok ? 0 : 1;
366
+ }
367
+
368
+ writeLine(io.stdout, `${PRODUCT_NAME} status for ${baseUrl}`);
369
+ writeLine(io.stdout, 'Privacy: telemetry disabled; no token sent.');
370
+ for (const item of probes) {
371
+ if (item.ok) {
372
+ writeLine(io.stdout, ` OK ${item.status} ${item.url}`);
373
+ } else {
374
+ writeLine(io.stdout, ` FAIL ${item.status ?? 'ERR'} ${item.url} ${item.error ?? ''}`.trimEnd());
375
+ }
376
+ }
377
+
378
+ return result.ok ? 0 : 1;
379
+ }
380
+
381
+ async function setupCommand(args, io) {
382
+ const positionalClientId = positionalClientArg(args);
383
+ const optionArgs = positionalClientId ? args.slice(1) : args;
384
+ const baseUrl = normalizeBaseUrl(baseUrlOption(optionArgs, io.env));
385
+ const outputJson = hasFlag(optionArgs, '--json');
386
+ const shortClientSetup = Boolean(positionalClientId);
387
+ const clientId = normalizeSetupClientId(positionalClientId ?? optionValue(optionArgs, '--client'));
388
+ const dryRun = hasFlag(optionArgs, '--dry-run') || hasFlag(optionArgs, '--preview');
389
+ const writeConfig = !dryRun && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes') || shortClientSetup);
390
+ const timeoutMs = parsePositiveInteger(optionValue(optionArgs, '--timeout-ms') ?? '5000', '--timeout-ms');
391
+ const installProfile = shortClientSetup
392
+ && clientId === 'codex'
393
+ && writeConfig
394
+ && !hasFlag(optionArgs, '--no-profile');
395
+
396
+ if (writeConfig && !clientId) {
397
+ throw new UsageError(`Setup --write requires --client <${supportedSetupClientIds().join('|')}> so the CLI never writes broad config implicitly.`);
398
+ }
399
+
400
+ const discoveryUrl = endpointUrl(baseUrl, '/.well-known/memory-os.json');
401
+ const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
402
+ ensureDiscoveryService(discovery, discoveryUrl);
403
+
404
+ const statusUrl = stringValue(discovery, ['urls', 'onboarding_status'])
405
+ ?? stringValue(discovery, ['onboarding_status_url'])
406
+ ?? endpointUrl(baseUrl, '/v1/onboarding/status');
407
+ const status = await fetchJson(statusUrl, timeoutMs, io);
408
+ const setupPlan = buildSetupPlan({ baseUrl, discoveryUrl, statusUrl, discovery, status });
409
+
410
+ if (clientId) {
411
+ if (clientId === 'copilot-cli') {
412
+ const proxyPort = parsePositiveInteger(optionValue(optionArgs, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
413
+ setupPlan.selectedClient = copilotSetupPlan(setupPlan.mcpUrl, proxyPort, io.env);
414
+ if (writeConfig) {
415
+ await mergeCopilotMcpConfig(setupPlan.selectedClient.configPath, setupPlan.selectedClient.proxyUrl);
416
+ setupPlan.selectedClient.written = true;
417
+ }
418
+ } else {
419
+ const client = MCP_CLIENTS.get(clientId);
420
+ if (!client) {
421
+ throw new UsageError(`Unsupported MCP client: ${clientId}. Supported clients: ${supportedSetupClientIds().join(', ')}.`);
422
+ }
423
+
424
+ const identity = writeConfig ? await agentIdentity(clientId, io.env) : envReferenceIdentity(clientId);
425
+ setupPlan.selectedClient = clientSetupPlan(clientId, client, setupPlan.mcpUrl, io.env, identity);
426
+ if (writeConfig) {
427
+ await client.writeConfig(setupPlan.selectedClient.configPath, setupPlan.mcpUrl, identity);
428
+ setupPlan.selectedClient.written = true;
429
+ }
430
+
431
+ if (clientId === 'codex' && shortClientSetup) {
432
+ const profileTarget = optionValue(optionArgs, '--profile-target')
433
+ ?? optionValue(optionArgs, '--target')
434
+ ?? defaultCodexProfileTarget();
435
+ const profileResult = await codexProfileInstallResult(profileTarget, { write: installProfile });
436
+ setupPlan.selectedClient.codexProfile = profileResult;
437
+ }
438
+ }
439
+ }
440
+
441
+ if (outputJson) {
442
+ writeLine(io.stdout, JSON.stringify(setupPlan, null, 2));
443
+ return 0;
444
+ }
445
+
446
+ writeSetupSummary(setupPlan, io);
447
+ return 0;
448
+ }
449
+
450
+ async function profileCommand(args, io) {
451
+ const subcommand = args[0] ?? 'help';
452
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
453
+ writeLine(io.stdout, 'Profile commands:');
454
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile install codex [--target AGENTS.md] [--dry-run|--json]`);
455
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile status codex [--target AGENTS.md] [--json]`);
456
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target AGENTS.md] [--json]`);
457
+ writeLine(io.stdout, '');
458
+ writeLine(io.stdout, 'Profile installs are marker-scoped and never write token values.');
459
+ return 0;
460
+ }
461
+
462
+ const clientId = args[1];
463
+ if (clientId !== 'codex') {
464
+ throw new UsageError(`Unsupported profile client: ${clientId ?? 'missing'}. Supported clients: codex.`);
465
+ }
466
+
467
+ const optionArgs = args.slice(2);
468
+ const outputJson = hasFlag(optionArgs, '--json');
469
+ const targetPath = optionValue(optionArgs, '--target') ?? defaultCodexProfileTarget();
470
+ let result;
471
+
472
+ if (subcommand === 'install') {
473
+ result = await codexProfileInstallResult(targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
474
+ } else if (subcommand === 'status') {
475
+ result = await codexProfileStatusResult(targetPath);
476
+ } else if (subcommand === 'uninstall') {
477
+ result = await codexProfileUninstallResult(targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
478
+ } else {
479
+ throw new UsageError(`Unknown profile command: ${subcommand}`);
480
+ }
481
+
482
+ if (outputJson) {
483
+ writeLine(io.stdout, JSON.stringify(result, null, 2));
484
+ return 0;
485
+ }
486
+
487
+ writeProfileResult(subcommand, result, io);
488
+ return 0;
489
+ }
490
+
491
+ async function loginCommand(args, io) {
492
+ const outputJson = hasFlag(args, '--json');
493
+ const fromStdin = hasFlag(args, '--from-stdin') || hasFlag(args, '--token-stdin');
494
+ const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
495
+ const httpTimeoutMs = parsePositiveInteger(optionValue(args, '--http-timeout-ms') ?? '30000', '--http-timeout-ms');
496
+ const loginTimeoutOption = optionValue(args, '--timeout-ms');
497
+ const pollOnce = hasFlag(args, '--poll-once');
498
+
499
+ if (fromStdin) {
500
+ const result = await storeTokenFromStdin(io, { source: 'stdin' });
501
+ if (outputJson) {
502
+ writeLine(io.stdout, JSON.stringify(result, null, 2));
503
+ } else {
504
+ writeLine(io.stdout, `${PRODUCT_NAME} login complete.`);
505
+ writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
506
+ writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');
507
+ }
508
+ return 0;
509
+ }
510
+
511
+ const start = await startDeviceLogin(baseUrl, httpTimeoutMs, io);
512
+ const loginTimeoutMs = loginTimeoutOption
513
+ ? parsePositiveInteger(loginTimeoutOption, '--timeout-ms')
514
+ : Math.max(1000, start.expiresIn * 1000);
515
+ if (!outputJson) {
516
+ writeLine(io.stdout, `${PRODUCT_NAME} device login`);
517
+ writeLine(io.stdout, `Open: ${start.verificationUriComplete ?? start.verificationUri}`);
518
+ if (start.userCode) {
519
+ writeLine(io.stdout, `Code: ${start.userCode}`);
520
+ }
521
+ writeLine(io.stdout, 'Waiting for authorization...');
522
+ }
523
+
524
+ const token = await pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, { pollOnce });
525
+ const result = await storeTokenValue(token.accessToken, { source: 'device-login', account: token.account }, io.env);
526
+ const payload = {
527
+ ...result,
528
+ baseUrl,
529
+ verificationUri: start.verificationUri,
530
+ account: token.account,
531
+ deviceLogin: true
532
+ };
533
+
534
+ if (outputJson) {
535
+ writeLine(io.stdout, JSON.stringify(payload, null, 2));
536
+ } else {
537
+ writeLine(io.stdout, 'Login complete. Token stored securely in the user-scoped XMemo CLI config directory.');
538
+ if (token.account) {
539
+ writeLine(io.stdout, `Signed in as: ${formatAccount(token.account)}`);
540
+ }
541
+ writeLine(io.stdout, `Credential path: ${result.credentialPath}`);
542
+ writeLine(io.stdout, 'No extra token configuration is required.');
543
+ writeLine(io.stdout, `Optional check: ${COMMAND_NAME} token status --verify`);
544
+ }
545
+ return 0;
546
+ }
547
+
548
+ async function authCommand(args, io) {
549
+ const subcommand = args[0] ?? 'help';
550
+
551
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
552
+ writeLine(io.stdout, 'Auth commands:');
553
+ writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
554
+ writeLine(io.stdout, '');
555
+ writeLine(io.stdout, `Use \`${COMMAND_NAME} login\` to sign in and \`${COMMAND_NAME} token add --from-stdin\` to store an existing token.`);
556
+ return 0;
557
+ }
558
+
559
+ if (subcommand === 'status') {
560
+ return await credentialStatusCommand(args.slice(1), io, { mode: 'auth' });
561
+ }
562
+
563
+ throw new UsageError(`Unknown auth command: ${subcommand}`);
564
+ }
565
+
566
+ async function tokenCommand(args, io) {
567
+ const subcommand = args[0] ?? 'help';
568
+
569
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
570
+ writeLine(io.stdout, 'Token commands:');
571
+ writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify]`);
572
+ writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
573
+ writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
574
+ writeLine(io.stdout, '');
575
+ writeLine(io.stdout, `${COMMAND_NAME} login is the recommended personal-user path.`);
576
+ writeLine(io.stdout, `${COMMAND_NAME} token add --from-stdin stores a token in the user-scoped XMemo CLI config directory.`);
577
+ return 0;
578
+ }
579
+
580
+ if (subcommand === 'status') {
581
+ return await credentialStatusCommand(args.slice(1), io, { mode: 'token' });
582
+ }
583
+
584
+ if (subcommand === 'add') {
585
+ if (!hasFlag(args, '--from-stdin')) {
586
+ throw new UsageError('Refusing command-line token input. Pipe the token through stdin with --from-stdin.');
587
+ }
588
+ const result = await storeTokenFromStdin(io, { source: 'token-add' });
589
+ if (hasFlag(args, '--json')) {
590
+ writeLine(io.stdout, JSON.stringify(result, null, 2));
591
+ } else {
592
+ writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
593
+ writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');
594
+ }
595
+ return 0;
596
+ }
597
+
598
+ if (subcommand === 'set') {
599
+ if (!hasFlag(args, '--from-stdin')) {
600
+ throw new UsageError('Refusing command-line token input. Pipe the token through stdin with --from-stdin.');
601
+ }
602
+ const token = (await readAll(io.stdin)).trim();
603
+ validateToken(token);
604
+ if (!hasFlag(args, '--allow-plaintext')) {
605
+ writeLine(io.stderr, 'Token was read from stdin but was not stored.');
606
+ writeLine(io.stderr, 'Enterprise default refuses plaintext token storage without --allow-plaintext.');
607
+ writeLine(io.stderr, `Preferred personal-user path: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin.`);
608
+ return 2;
609
+ }
610
+
611
+ const result = await storeTokenValue(token, { source: 'token-set' }, io.env);
612
+ writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
613
+ writeLine(io.stdout, 'Token value was not printed. Do not commit this file.');
614
+ return 0;
615
+ }
616
+
617
+ throw new UsageError(`Unknown token command: ${subcommand}`);
618
+ }
619
+
620
+ async function credentialStatusCommand(args, io, { mode }) {
621
+ const outputJson = hasFlag(args, '--json');
622
+ const verify = hasFlag(args, '--verify');
623
+ const credential = await readStoredCredential(io.env);
624
+ const environmentToken = io.env[TOKEN_ENV_VAR] ?? io.env[LEGACY_TOKEN_ENV_VAR] ?? '';
625
+ const hasEnvironmentToken = Boolean(environmentToken);
626
+ const hasUserCredential = Boolean(credential.token);
627
+ const tokenSource = hasEnvironmentToken ? 'environment' : hasUserCredential ? 'user-credential-file' : 'missing';
628
+ const report = {
629
+ loggedIn: hasEnvironmentToken || hasUserCredential,
630
+ tokenSource,
631
+ environmentToken: {
632
+ present: hasEnvironmentToken,
633
+ variable: hasEnvironmentToken && io.env[TOKEN_ENV_VAR] ? TOKEN_ENV_VAR : hasEnvironmentToken ? LEGACY_TOKEN_ENV_VAR : TOKEN_ENV_VAR
634
+ },
635
+ userCredentialFile: {
636
+ present: hasUserCredential,
637
+ path: credential.path,
638
+ storage: credential.storage ?? null
639
+ },
640
+ account: credential.account ?? null,
641
+ privacy: {
642
+ tokenPrinted: false,
643
+ projectFilesModified: false
644
+ }
645
+ };
646
+
647
+ if (verify) {
648
+ const token = await resolveCredentialToken(io.env);
649
+ if (!token) {
650
+ if (outputJson) {
651
+ writeLine(io.stdout, JSON.stringify({ ...report, verification: { ok: false, detail: 'no token found' } }, null, 2));
652
+ } else {
653
+ writeCredentialStatus(report, io, { mode });
654
+ writeLine(io.stderr, `No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\`.`);
655
+ }
656
+ return 1;
657
+ }
658
+ const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
659
+ const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '10000', '--timeout-ms');
660
+ const verification = await verifyTokenWithMcp(baseUrl, token, timeoutMs, io);
661
+ report.verification = verification;
662
+ if (outputJson) {
663
+ writeLine(io.stdout, JSON.stringify(report, null, 2));
664
+ return verification.ok ? 0 : 1;
665
+ }
666
+ writeCredentialStatus(report, io, { mode });
667
+ writeLine(io.stdout, `Remote token verification: ${verification.ok ? 'ok' : 'failed'} (${verification.detail})`);
668
+ return verification.ok ? 0 : 1;
669
+ }
670
+
671
+ if (outputJson) {
672
+ writeLine(io.stdout, JSON.stringify(report, null, 2));
673
+ } else {
674
+ writeCredentialStatus(report, io, { mode });
675
+ }
676
+ return report.loggedIn ? 0 : 1;
677
+ }
678
+
679
+ function writeCredentialStatus(report, io, { mode }) {
680
+ if (mode === 'auth') {
681
+ writeLine(io.stdout, `${PRODUCT_NAME} auth status`);
682
+ writeLine(io.stdout, `Logged in: ${report.loggedIn ? 'yes' : 'no'}`);
683
+ writeLine(io.stdout, `Credential source: ${report.tokenSource}`);
684
+ if (report.account) {
685
+ writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);
686
+ }
687
+ writeLine(io.stdout, report.loggedIn ? 'Credential is ready; token value remains hidden.' : `Run \`${COMMAND_NAME} login\` to sign in.`);
688
+ return;
689
+ }
690
+ writeLine(io.stdout, `Environment token: ${report.environmentToken.present ? 'present' : 'missing'} (${report.environmentToken.variable})`);
691
+ writeLine(io.stdout, `User credential file: ${report.userCredentialFile.present ? 'present' : 'missing'} (${report.userCredentialFile.path})`);
692
+ if (report.account) {
693
+ writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);
694
+ }
695
+ writeLine(io.stdout, report.loggedIn ? 'Credential is ready; token value remains hidden.' : `Run \`${COMMAND_NAME} login\` to sign in.`);
696
+ }
697
+
698
+ async function mcpCommand(args, io) {
699
+ const subcommand = args[0] ?? 'help';
700
+
701
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
702
+ writeLine(io.stdout, 'MCP commands:');
703
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
704
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|antigravity|generic> [--base-url <url>] [--json]`);
705
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}] [--base-url <url>]`);
706
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp profile codex [--json]`);
707
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>]`);
708
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>] --write [--config <path>]`);
709
+ return 0;
710
+ }
711
+
712
+ if (subcommand === 'list') {
713
+ if (hasFlag(args, '--json')) {
714
+ writeLine(io.stdout, JSON.stringify(supportedMcpClients(), null, 2));
715
+ return 0;
716
+ }
717
+
718
+ writeLine(io.stdout, 'Supported MCP clients:');
719
+ for (const client of supportedMcpClients()) {
720
+ writeLine(io.stdout, ` ${client.id.padEnd(8)} ${client.label} (${client.configKind})`);
721
+ }
722
+ writeLine(io.stdout, `Generated configs never embed token values; OAuth clients do not require ${TOKEN_ENV_VAR} in their config.`);
723
+ return 0;
724
+ }
725
+
726
+ if (subcommand === 'config') {
727
+ const clientId = optionValue(args, '--client') ?? args[1] ?? 'generic';
728
+ const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
729
+ const mcpUrl = endpointUrl(baseUrl, '/mcp');
730
+ const useLocalProxy = clientId === 'copilot-cli' && !hasFlag(args, '--remote-env');
731
+ const proxyPort = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
732
+ const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`;
733
+ const template = useLocalProxy
734
+ ? mcpLocalProxyTemplate(clientId, proxyUrl)
735
+ : mcpConfigTemplate(clientId, mcpUrl);
736
+
737
+ if (hasFlag(args, '--json')) {
738
+ writeLine(io.stdout, JSON.stringify(template, null, 2));
739
+ return 0;
740
+ }
741
+
742
+ writeLine(io.stdout, `${PRODUCT_NAME} MCP config template for ${clientId}`);
743
+ if (useLocalProxy) {
744
+ writeLine(io.stdout, `Requires credential: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin`);
745
+ writeLine(io.stdout, `Run local proxy: ${template.requiresLocalCommand}`);
746
+ } else {
747
+ if (template.requiresEnv?.length > 0) {
748
+ writeLine(io.stdout, `Requires env: ${template.requiresEnv.join(', ')}`);
749
+ } else if (template.authentication === 'oauth') {
750
+ writeLine(io.stdout, 'Requires auth: complete the client MCP OAuth flow after setup.');
751
+ }
752
+ }
753
+ if (typeof template.snippet === 'string') {
754
+ writeLine(io.stdout, template.snippet.trimEnd());
755
+ } else {
756
+ writeLine(io.stdout, JSON.stringify(template.snippet, null, 2));
757
+ }
758
+ writeLine(io.stdout, 'Review the template before applying it. Token values are not included.');
759
+ return 0;
760
+ }
761
+
762
+ if (subcommand === 'proxy') {
763
+ return await mcpProxyCommand(args.slice(1), io);
764
+ }
765
+
766
+ if (subcommand === 'profile') {
767
+ const clientId = args[1] ?? 'codex';
768
+ if (clientId !== 'codex') {
769
+ throw new UsageError('Only the Codex memory behavior profile is available in this MCP-depth release.');
770
+ }
771
+
772
+ const profile = codexMemoryProfile();
773
+ if (hasFlag(args, '--json')) {
774
+ writeLine(io.stdout, JSON.stringify(profile, null, 2));
775
+ return 0;
776
+ }
777
+
778
+ writeCodexMemoryProfile(profile, io);
779
+ return 0;
780
+ }
781
+
782
+ const target = args[1] ?? '';
783
+ const client = MCP_CLIENTS.get(target);
784
+
785
+ if (subcommand !== 'add' || !client) {
786
+ throw new UsageError(`Supported MCP setup command: ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <url>]`);
787
+ }
788
+
789
+ const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
790
+ const configPath = optionValue(args, '--config') ?? client.defaultConfigPath(io.env);
791
+ const mcpUrl = endpointUrl(baseUrl, '/mcp');
792
+
793
+ if (hasFlag(args, '--json')) {
794
+ const identity = envReferenceIdentity(target);
795
+ const oauthClient = usesClientOAuth(target);
796
+ writeLine(io.stdout, JSON.stringify({
797
+ client: target,
798
+ label: client.label,
799
+ configKind: client.configKind,
800
+ configPath,
801
+ serverName: MCP_SERVER_NAME,
802
+ url: mcpUrl,
803
+ tokenEnvVar: oauthClient ? null : TOKEN_ENV_VAR,
804
+ authentication: oauthClient ? 'oauth' : 'env-bearer',
805
+ agentId: identity.agentId,
806
+ agentInstanceId: identity.agentInstanceId,
807
+ agentInstanceIdPath: identity.path,
808
+ writesTokenValue: false
809
+ }, null, 2));
810
+ return 0;
811
+ }
812
+
813
+ const identity = hasFlag(args, '--write') ? await agentIdentity(target, io.env) : envReferenceIdentity(target);
814
+ if (hasFlag(args, '--write')) {
815
+ await client.writeConfig(configPath, mcpUrl, identity);
816
+ writeLine(io.stdout, `Updated ${client.label} MCP config: ${configPath}`);
817
+ if (usesClientOAuth(target)) {
818
+ writeLine(io.stdout, `Token value was not written. ${client.label} will complete MCP OAuth on first use.`);
819
+ } else {
820
+ writeLine(io.stdout, `Token value was not written. ${client.label} will read ${TOKEN_ENV_VAR} from the environment.`);
821
+ }
822
+ writeLine(io.stdout, `Agent instance ID stored outside git: ${identity.path}`);
823
+ return 0;
824
+ }
825
+
826
+ const snippet = client.buildSnippet(mcpUrl, identity);
827
+ writeLine(io.stdout, `Add this to your ${client.label} config (${configPath}):`);
828
+ writeLine(io.stdout, '');
829
+ writeLine(io.stdout, snippet.trimEnd());
830
+ writeLine(io.stdout, '');
831
+ if (usesClientOAuth(target)) {
832
+ writeLine(io.stdout, `Restart ${client.label} and complete its MCP OAuth flow. No token value is included here.`);
833
+ } else {
834
+ writeLine(io.stdout, `Set ${TOKEN_ENV_VAR} in your user environment or secret manager. The token value is not included here.`);
835
+ }
836
+ return 0;
837
+ }
838
+
839
+ async function mcpProxyCommand(args, io) {
840
+ const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
841
+ const mcpUrl = endpointUrl(baseUrl, '/mcp');
842
+ const host = optionValue(args, '--host') ?? DEFAULT_PROXY_HOST;
843
+ const port = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
844
+ const token = await resolveCredentialToken(io.env);
845
+ if (!token) {
846
+ throw new UsageError(`No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\` first.`);
847
+ }
848
+ validateToken(token);
849
+ const identity = await agentIdentity('copilot-cli', io.env);
850
+
851
+ const server = http.createServer(async (request, response) => {
852
+ try {
853
+ await handleMcpProxyRequest({ request, response, mcpUrl, token, identity, io });
854
+ } catch (error) {
855
+ response.statusCode = 502;
856
+ response.setHeader('content-type', 'application/json');
857
+ response.end(JSON.stringify({ error: 'mcp_proxy_error', message: error.message }));
858
+ }
859
+ });
860
+
861
+ await new Promise((resolve, reject) => {
862
+ server.once('error', reject);
863
+ server.listen(port, host, () => {
864
+ server.off('error', reject);
865
+ resolve();
866
+ });
867
+ });
868
+
869
+ writeLine(io.stdout, `${PRODUCT_NAME} MCP proxy listening on http://${host}:${port}/mcp`);
870
+ writeLine(io.stdout, `Forwarding to ${mcpUrl}`);
871
+ writeLine(io.stdout, `Credential source: ${TOKEN_ENV_VAR} or ${credentialsPath(io.env)}`);
872
+ return 0;
873
+ }
874
+
875
+ async function handleMcpProxyRequest({ request, response, mcpUrl, token, identity, io }) {
876
+ const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? `${DEFAULT_PROXY_HOST}:${DEFAULT_PROXY_PORT}`}`);
877
+ if (request.method !== 'POST' || requestUrl.pathname !== '/mcp') {
878
+ response.statusCode = 404;
879
+ response.setHeader('content-type', 'application/json');
880
+ response.end(JSON.stringify({ error: 'not_found' }));
881
+ return;
882
+ }
883
+
884
+ const body = await readAll(request);
885
+ const upstreamHeaders = {
886
+ accept: String(request.headers.accept || 'application/json, text/event-stream'),
887
+ 'content-type': String(request.headers['content-type'] || 'application/json'),
888
+ authorization: `Bearer ${token}`,
889
+ [AGENT_ID_HEADER]: identity.agentId,
890
+ [AGENT_INSTANCE_HEADER]: identity.agentInstanceId,
891
+ 'user-agent': `XMemo-CLI-Proxy/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
892
+ };
893
+ const sessionId = request.headers['mcp-session-id'];
894
+ if (sessionId) {
895
+ upstreamHeaders['mcp-session-id'] = Array.isArray(sessionId) ? sessionId[0] : sessionId;
896
+ }
897
+
898
+ const upstream = await io.fetch(mcpUrl, {
899
+ method: 'POST',
900
+ headers: upstreamHeaders,
901
+ body
902
+ });
903
+
904
+ response.statusCode = upstream.status;
905
+ for (const header of ['content-type', 'mcp-session-id']) {
906
+ const value = upstream.headers.get(header);
907
+ if (value) {
908
+ response.setHeader(header, value);
909
+ }
910
+ }
911
+ const buffer = Buffer.from(await upstream.arrayBuffer());
912
+ response.end(buffer);
913
+ }
914
+
915
+ async function smokeCommand(args, io) {
916
+ const clientId = optionValue(args, '--client');
917
+ const outputJson = hasFlag(args, '--json');
918
+ if (!clientId) {
919
+ throw new UsageError('Smoke requires --client codex for this MCP-depth release.');
920
+ }
921
+ if (clientId !== 'codex') {
922
+ throw new UsageError('Only Codex smoke checks are available in this MCP-depth release.');
923
+ }
924
+
925
+ const configPath = optionValue(args, '--config') ?? defaultCodexConfigPath(io.env);
926
+ const report = await codexSmokeReport(configPath, io.env);
927
+
928
+ if (outputJson) {
929
+ writeLine(io.stdout, JSON.stringify(report, null, 2));
930
+ return report.ok ? 0 : 1;
931
+ }
932
+
933
+ writeLine(io.stdout, `${PRODUCT_NAME} Codex MCP smoke: ${report.ok ? 'ok' : 'failed'}`);
934
+ writeLine(io.stdout, `Config: ${report.configPath}`);
935
+ writeLine(io.stdout, `Token env: ${report.tokenEnvVar}`);
936
+ for (const check of report.checks) {
937
+ const status = check.ok ? 'OK' : check.required ? 'FAIL' : 'WARN';
938
+ writeLine(io.stdout, ` ${status} ${check.name}: ${check.detail}`);
939
+ }
940
+ return report.ok ? 0 : 1;
941
+ }
942
+
943
+ function envCommand(args, io) {
944
+ const subcommand = args[0] ?? 'help';
945
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
946
+ writeLine(io.stdout, 'Env commands:');
947
+ writeLine(io.stdout, ` ${COMMAND_NAME} env example [--shell bash|powershell|cmd] [--base-url <url>] [--json]`);
948
+ return 0;
949
+ }
950
+ if (subcommand !== 'example') {
951
+ throw new UsageError(`Unknown env command: ${subcommand}`);
952
+ }
953
+
954
+ const baseUrl = normalizeBaseUrl(baseUrlOption(args.slice(1), io.env));
955
+ const outputJson = hasFlag(args, '--json');
956
+ const shell = optionValue(args, '--shell') ?? (process.platform === 'win32' ? 'powershell' : 'bash');
957
+ const placeholder = '<paste-token-from-your-secret-store>';
958
+ const payload = {
959
+ XMEMO_URL: baseUrl,
960
+ XMEMO_BASE_URL: baseUrl,
961
+ MEMORY_OS_URL: baseUrl,
962
+ MEMORY_OS_BASE_URL: baseUrl,
963
+ [TOKEN_ENV_VAR]: placeholder,
964
+ [AGENT_ID_ENV_VAR]: '<agent-family>',
965
+ [AGENT_INSTANCE_ENV_VAR]: '<stable-random-id-for-this-local-agent>'
966
+ };
967
+
968
+ if (outputJson) {
969
+ writeLine(io.stdout, JSON.stringify(payload, null, 2));
970
+ return 0;
971
+ }
972
+
973
+ if (shell === 'powershell') {
974
+ writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('XMEMO_URL', '${baseUrl}', 'User')`);
975
+ writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('XMEMO_BASE_URL', '${baseUrl}', 'User')`);
976
+ writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('MEMORY_OS_URL', '${baseUrl}', 'User')`);
977
+ writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('MEMORY_OS_BASE_URL', '${baseUrl}', 'User')`);
978
+ writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${TOKEN_ENV_VAR}', '${placeholder}', 'User')`);
979
+ writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${AGENT_ID_ENV_VAR}', '<agent-family>', 'User')`);
980
+ writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${AGENT_INSTANCE_ENV_VAR}', '<stable-random-id-for-this-local-agent>', 'User')`);
981
+ } else if (shell === 'cmd') {
982
+ writeLine(io.stdout, `setx XMEMO_URL "${baseUrl}"`);
983
+ writeLine(io.stdout, `setx XMEMO_BASE_URL "${baseUrl}"`);
984
+ writeLine(io.stdout, `setx MEMORY_OS_URL "${baseUrl}"`);
985
+ writeLine(io.stdout, `setx MEMORY_OS_BASE_URL "${baseUrl}"`);
986
+ writeLine(io.stdout, `setx ${TOKEN_ENV_VAR} "${placeholder}"`);
987
+ writeLine(io.stdout, `setx ${AGENT_ID_ENV_VAR} "<agent-family>"`);
988
+ writeLine(io.stdout, `setx ${AGENT_INSTANCE_ENV_VAR} "<stable-random-id-for-this-local-agent>"`);
989
+ } else {
990
+ writeLine(io.stdout, `export XMEMO_URL="${baseUrl}"`);
991
+ writeLine(io.stdout, `export XMEMO_BASE_URL="${baseUrl}"`);
992
+ writeLine(io.stdout, `export MEMORY_OS_URL="${baseUrl}"`);
993
+ writeLine(io.stdout, `export MEMORY_OS_BASE_URL="${baseUrl}"`);
994
+ writeLine(io.stdout, `export ${TOKEN_ENV_VAR}="${placeholder}"`);
995
+ writeLine(io.stdout, `export ${AGENT_ID_ENV_VAR}="<agent-family>"`);
996
+ writeLine(io.stdout, `export ${AGENT_INSTANCE_ENV_VAR}="<stable-random-id-for-this-local-agent>"`);
997
+ }
998
+ return 0;
999
+ }
1000
+
1001
+ function writePrivacy(io) {
1002
+ writeLine(io.stdout, `${PRODUCT_NAME} CLI privacy and security defaults:`);
1003
+ writeLine(io.stdout, '- No telemetry or analytics.');
1004
+ writeLine(io.stdout, '- `status` does not send tokens.');
1005
+ writeLine(io.stdout, `- MCP configs reference ${TOKEN_ENV_VAR}; token values are not embedded.`);
1006
+ writeLine(io.stdout, `- Agent instance IDs are non-secret and stored in user-scoped config outside git.`);
1007
+ writeLine(io.stdout, '- `login` and `token add` store credentials in the user-scoped XMemo CLI config directory.');
1008
+ writeLine(io.stdout, '- Legacy `token set` plaintext storage requires explicit --allow-plaintext.');
1009
+ writeLine(io.stdout, '- npm publishing is restricted by package.json files whitelist.');
1010
+ }
1011
+
1012
+ async function startDeviceLogin(baseUrl, timeoutMs, io) {
1013
+ const payload = await postJson(endpointUrl(baseUrl, DEVICE_LOGIN_START_PATH), {
1014
+ client_id: PACKAGE_NAME,
1015
+ cli_version: CLI_VERSION,
1016
+ token_type: 'mcp_token',
1017
+ scopes: ['memory:read', 'memory:write']
1018
+ }, timeoutMs, io);
1019
+
1020
+ const deviceCode = stringValue(payload, ['device_code']);
1021
+ const verificationUri = stringValue(payload, ['verification_uri']);
1022
+ if (!deviceCode || !verificationUri) {
1023
+ throw new UsageError(`Device login did not return device_code and verification_uri from ${baseUrl}.`);
1024
+ }
1025
+
1026
+ return {
1027
+ deviceCode,
1028
+ userCode: stringValue(payload, ['user_code']),
1029
+ verificationUri,
1030
+ verificationUriComplete: stringValue(payload, ['verification_uri_complete']),
1031
+ expiresIn: Number.isFinite(Number(payload.expires_in)) ? Number(payload.expires_in) : 600,
1032
+ interval: Number.isFinite(Number(payload.interval)) ? Math.max(1, Number(payload.interval)) : 5
1033
+ };
1034
+ }
1035
+
1036
+ async function pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, options = {}) {
1037
+ const deadline = Date.now() + Math.min(start.expiresIn * 1000, loginTimeoutMs);
1038
+ const sleepFn = io.sleep ?? sleep;
1039
+ let intervalSeconds = start.interval;
1040
+ while (Date.now() <= deadline) {
1041
+ const payload = await postJson(endpointUrl(baseUrl, DEVICE_LOGIN_TOKEN_PATH), {
1042
+ device_code: start.deviceCode,
1043
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
1044
+ }, httpTimeoutMs, io, { allowDevicePending: true });
1045
+
1046
+ const accessToken = stringValue(payload, ['access_token']) ?? stringValue(payload, ['token']);
1047
+ if (accessToken) {
1048
+ validateToken(accessToken);
1049
+ return {
1050
+ accessToken,
1051
+ account: accountFromPayload(payload)
1052
+ };
1053
+ }
1054
+
1055
+ const error = stringValue(payload, ['error']);
1056
+ if (error && error !== 'authorization_pending' && error !== 'slow_down') {
1057
+ throw new UsageError(`Device login failed: ${error}`);
1058
+ }
1059
+ if (options.pollOnce) {
1060
+ throw new UsageError('Device login is still pending.');
1061
+ }
1062
+ if (error === 'slow_down') {
1063
+ intervalSeconds += 5;
1064
+ }
1065
+ await sleepFn(intervalSeconds * 1000);
1066
+ }
1067
+
1068
+ throw new UsageError('Device login expired before authorization completed.');
1069
+ }
1070
+
1071
+ async function storeTokenFromStdin(io, metadata = {}) {
1072
+ const token = (await readAll(io.stdin)).trim();
1073
+ validateToken(token);
1074
+ return await storeTokenValue(token, metadata, io.env);
1075
+ }
1076
+
1077
+ async function storeTokenValue(token, metadata, env) {
1078
+ validateToken(token);
1079
+ const credentialPath = credentialsPath(env);
1080
+ await writePlaintextCredential(credentialPath, token, metadata);
1081
+ return {
1082
+ ok: true,
1083
+ credentialPath,
1084
+ tokenPresent: true,
1085
+ tokenPrinted: false,
1086
+ projectFilesModified: false,
1087
+ storage: 'user-scoped-credential-file'
1088
+ };
1089
+ }
1090
+
1091
+ async function readStoredCredential(env) {
1092
+ const credentialPath = credentialsPath(env);
1093
+ const content = await readTextIfExists(credentialPath);
1094
+ if (!content.trim()) {
1095
+ return { path: credentialPath, token: null };
1096
+ }
1097
+
1098
+ const parsed = parseJsonConfig(content, credentialPath);
1099
+ return {
1100
+ path: credentialPath,
1101
+ token: stringValue(parsed, ['token']),
1102
+ storage: stringValue(parsed, ['storage']),
1103
+ account: accountFromPayload(parsed.metadata)
1104
+ };
1105
+ }
1106
+
1107
+ function accountFromPayload(payload) {
1108
+ const account = payload && typeof payload === 'object'
1109
+ ? (payload.user && typeof payload.user === 'object' ? payload.user : payload.account)
1110
+ : null;
1111
+ if (!account || typeof account !== 'object') {
1112
+ return null;
1113
+ }
1114
+ const userId = stringValue(account, ['user_id']) ?? stringValue(account, ['id']) ?? stringValue(account, ['userId']);
1115
+ const email = stringValue(account, ['email']);
1116
+ const displayName = stringValue(account, ['display_name']) ?? stringValue(account, ['name']) ?? stringValue(account, ['displayName']);
1117
+ if (!userId && !email && !displayName) {
1118
+ return null;
1119
+ }
1120
+ return {
1121
+ userId: userId ?? null,
1122
+ email: email ?? null,
1123
+ displayName: displayName ?? null
1124
+ };
1125
+ }
1126
+
1127
+ function formatAccount(account) {
1128
+ const label = account.displayName || account.email || account.userId || 'XMemo account';
1129
+ return account.email && account.displayName ? `${account.displayName} <${account.email}>` : label;
1130
+ }
1131
+
1132
+ async function resolveCredentialToken(env) {
1133
+ const environmentToken = env[TOKEN_ENV_VAR] ?? env[LEGACY_TOKEN_ENV_VAR];
1134
+ if (environmentToken) {
1135
+ return environmentToken;
1136
+ }
1137
+ const credential = await readStoredCredential(env);
1138
+ return credential.token;
1139
+ }
1140
+
1141
+ async function verifyTokenWithMcp(baseUrl, token, timeoutMs, io) {
1142
+ const url = endpointUrl(baseUrl, '/mcp');
1143
+ const controller = new AbortController();
1144
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1145
+ try {
1146
+ const response = await io.fetch(url, {
1147
+ method: 'POST',
1148
+ headers: {
1149
+ accept: 'application/json, text/event-stream',
1150
+ 'content-type': 'application/json',
1151
+ authorization: `Bearer ${token}`,
1152
+ 'user-agent': `XMemo-CLI/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
1153
+ },
1154
+ body: JSON.stringify({
1155
+ jsonrpc: '2.0',
1156
+ id: 1,
1157
+ method: 'initialize',
1158
+ params: {
1159
+ protocolVersion: '2024-11-05',
1160
+ capabilities: {},
1161
+ clientInfo: { name: COMMAND_NAME, version: CLI_VERSION }
1162
+ }
1163
+ }),
1164
+ signal: controller.signal
1165
+ });
1166
+ return {
1167
+ ok: response.ok,
1168
+ detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}`
1169
+ };
1170
+ } catch (error) {
1171
+ return {
1172
+ ok: false,
1173
+ detail: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
1174
+ };
1175
+ } finally {
1176
+ clearTimeout(timeout);
1177
+ }
1178
+ }
1179
+
1180
+ async function probe(url, timeoutMs, io) {
1181
+ if (typeof io.fetch !== 'function') {
1182
+ return { url, ok: false, error: 'fetch unavailable in this Node runtime' };
1183
+ }
1184
+
1185
+ const controller = new AbortController();
1186
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1187
+
1188
+ try {
1189
+ const response = await io.fetch(url, {
1190
+ headers: { accept: 'application/json' },
1191
+ signal: controller.signal
1192
+ });
1193
+ return { url, ok: response.ok, status: response.status };
1194
+ } catch (error) {
1195
+ return {
1196
+ url,
1197
+ ok: false,
1198
+ error: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
1199
+ };
1200
+ } finally {
1201
+ clearTimeout(timeout);
1202
+ }
1203
+ }
1204
+
1205
+ async function fetchJson(url, timeoutMs, io) {
1206
+ if (typeof io.fetch !== 'function') {
1207
+ throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
1208
+ }
1209
+
1210
+ const controller = new AbortController();
1211
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1212
+
1213
+ try {
1214
+ const response = await io.fetch(url, {
1215
+ headers: { accept: 'application/json' },
1216
+ signal: controller.signal
1217
+ });
1218
+ if (!response.ok) {
1219
+ throw new UsageError(`Discovery request failed with HTTP ${response.status}: ${url}`);
1220
+ }
1221
+ return await response.json();
1222
+ } catch (error) {
1223
+ if (error instanceof UsageError) {
1224
+ throw error;
1225
+ }
1226
+ const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
1227
+ throw new UsageError(`Discovery request failed: ${url} (${reason})`);
1228
+ } finally {
1229
+ clearTimeout(timeout);
1230
+ }
1231
+ }
1232
+
1233
+ async function postJson(url, payload, timeoutMs, io, options = {}) {
1234
+ if (typeof io.fetch !== 'function') {
1235
+ throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
1236
+ }
1237
+
1238
+ const controller = new AbortController();
1239
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1240
+
1241
+ try {
1242
+ const response = await io.fetch(url, {
1243
+ method: 'POST',
1244
+ headers: {
1245
+ accept: 'application/json',
1246
+ 'content-type': 'application/json'
1247
+ },
1248
+ body: JSON.stringify(payload),
1249
+ signal: controller.signal
1250
+ });
1251
+ const responsePayload = await response.json();
1252
+ if (!response.ok) {
1253
+ const error = stringValue(responsePayload, ['error']) ?? stringValue(responsePayload, ['detail']) ?? `HTTP ${response.status}`;
1254
+ if (options.allowDevicePending && (error === 'authorization_pending' || error === 'slow_down')) {
1255
+ return { error };
1256
+ }
1257
+ throw new UsageError(`Request failed with HTTP ${response.status}: ${url} (${error})`);
1258
+ }
1259
+ return responsePayload;
1260
+ } catch (error) {
1261
+ if (error instanceof UsageError) {
1262
+ throw error;
1263
+ }
1264
+ const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
1265
+ throw new UsageError(`Request failed: ${url} (${reason})`);
1266
+ } finally {
1267
+ clearTimeout(timeout);
1268
+ }
1269
+ }
1270
+
1271
+ function ensureDiscoveryService(discovery, discoveryUrl) {
1272
+ const service = stringValue(discovery, ['service']);
1273
+ if (service && service !== 'memory-os') {
1274
+ throw new UsageError(`Discovery document at ${discoveryUrl} is for '${service}', not 'memory-os'.`);
1275
+ }
1276
+ }
1277
+
1278
+ function buildSetupPlan({ baseUrl, discoveryUrl, statusUrl, discovery, status }) {
1279
+ const apiBase = stringValue(discovery, ['urls', 'api_base'])
1280
+ ?? stringValue(discovery, ['api_base_url'])
1281
+ ?? baseUrl;
1282
+ const mcpUrl = stringValue(discovery, ['urls', 'mcp'])
1283
+ ?? stringValue(discovery, ['mcp_url'])
1284
+ ?? endpointUrl(apiBase, '/mcp');
1285
+ const tokenPortalUrl = stringValue(discovery, ['urls', 'token_portal'])
1286
+ ?? stringValue(discovery, ['token_portal_url'])
1287
+ ?? stringValue(status, ['requirements', 'token_portal_url']);
1288
+ const tokenEnvVar = stringValue(discovery, ['auth', 'token_env_var'])
1289
+ ?? stringValue(status, ['requirements', 'token_env_var'])
1290
+ ?? TOKEN_ENV_VAR;
1291
+
1292
+ return {
1293
+ schemaVersion: '1.0',
1294
+ baseUrl,
1295
+ discoveryUrl,
1296
+ statusUrl,
1297
+ apiBase,
1298
+ mcpUrl,
1299
+ guideUrl: stringValue(discovery, ['urls', 'guide']) ?? endpointUrl(apiBase, '/guide'),
1300
+ docsUrl: stringValue(discovery, ['urls', 'docs']),
1301
+ tokenPortalUrl,
1302
+ tokenEnvVar,
1303
+ onboardingReady: booleanValue(status, ['ready']),
1304
+ supportedClients: discoveryMcpClients(discovery),
1305
+ localClients: supportedMcpClients(),
1306
+ privacy: {
1307
+ telemetry: false,
1308
+ tokenSent: false,
1309
+ tokenEmbeddedInConfig: false
1310
+ },
1311
+ boundaries: {
1312
+ clientAllowed: arrayValue(discovery, ['agent_boundary', 'client_allowed'])
1313
+ ?? arrayValue(status, ['agent_boundary', 'client_allowed'])
1314
+ ?? [],
1315
+ adminRequired: arrayValue(discovery, ['agent_boundary', 'admin_required'])
1316
+ ?? arrayValue(status, ['agent_boundary', 'admin_required'])
1317
+ ?? []
1318
+ }
1319
+ };
1320
+ }
1321
+
1322
+ async function bestEffortRootVersion(discovery, timeoutMs, io) {
1323
+ const rootDiscoveryUrl = stringValue(discovery, ['urls', 'root_discovery']);
1324
+ if (!rootDiscoveryUrl) {
1325
+ return {};
1326
+ }
1327
+ try {
1328
+ const rootDiscovery = await fetchJson(rootDiscoveryUrl, timeoutMs, io);
1329
+ return { version: stringValue(rootDiscovery, ['version']) ?? undefined };
1330
+ } catch (error) {
1331
+ return { error: error.message };
1332
+ }
1333
+ }
1334
+
1335
+ function discoveryMcpUrl(discovery, baseUrl) {
1336
+ return stringValue(discovery, ['api', 'mcp', 'url'])
1337
+ ?? stringValue(discovery, ['urls', 'mcp'])
1338
+ ?? endpointUrl(baseUrl, '/mcp');
1339
+ }
1340
+
1341
+ function agentDiscoveryClientIds(discovery) {
1342
+ const clients = Array.isArray(discovery?.clients) ? discovery.clients : [];
1343
+ const ids = clients
1344
+ .filter((client) => isPlainObject(client) && typeof client.id === 'string')
1345
+ .map((client) => client.id);
1346
+ if (ids.length > 0) {
1347
+ return ids;
1348
+ }
1349
+ const supported = arrayValue(discovery, ['supported_clients']);
1350
+ return supported ?? [];
1351
+ }
1352
+
1353
+ function mcpConfigTemplate(clientId, mcpUrl) {
1354
+ if (clientId === 'codex') {
1355
+ return {
1356
+ client: clientId,
1357
+ serverName: MCP_SERVER_NAME,
1358
+ snippetFormat: 'toml',
1359
+ snippet: codexTomlSnippet(mcpUrl),
1360
+ requiresEnv: [TOKEN_ENV_VAR],
1361
+ optionalEnv: [AGENT_INSTANCE_ENV_VAR],
1362
+ agentIdentity: {
1363
+ agentId: 'codex',
1364
+ agentIdHeader: AGENT_ID_HEADER,
1365
+ agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1366
+ agentInstanceHeader: AGENT_INSTANCE_HEADER
1367
+ },
1368
+ writesTokenValue: false
1369
+ };
1370
+ }
1371
+
1372
+ if (clientId === 'gemini-cli') {
1373
+ return oauthJsonMcpTemplate(clientId, mcpUrl, geminiJsonConfig(mcpUrl));
1374
+ }
1375
+
1376
+ if (clientId === 'antigravity') {
1377
+ return oauthJsonMcpTemplate(clientId, mcpUrl, antigravityJsonConfig(mcpUrl));
1378
+ }
1379
+
1380
+ const serverName = clientId === 'cursor' || clientId === 'gemini-cli' || clientId === 'antigravity' ? 'memory_os' : 'memory-os';
1381
+ return {
1382
+ client: clientId,
1383
+ serverName,
1384
+ snippetFormat: 'json',
1385
+ snippet: {
1386
+ mcpServers: {
1387
+ [serverName]: {
1388
+ type: 'http',
1389
+ url: mcpUrl,
1390
+ headers: {
1391
+ Authorization: `Bearer \${${TOKEN_ENV_VAR}}`,
1392
+ [AGENT_ID_HEADER]: clientId,
1393
+ [AGENT_INSTANCE_HEADER]: `\${${AGENT_INSTANCE_ENV_VAR}}`
1394
+ }
1395
+ }
1396
+ }
1397
+ },
1398
+ requiresEnv: [TOKEN_ENV_VAR],
1399
+ optionalEnv: [AGENT_INSTANCE_ENV_VAR],
1400
+ agentIdentity: {
1401
+ agentId: clientId,
1402
+ agentIdHeader: AGENT_ID_HEADER,
1403
+ agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1404
+ agentInstanceHeader: AGENT_INSTANCE_HEADER
1405
+ },
1406
+ writesTokenValue: false
1407
+ };
1408
+ }
1409
+
1410
+ function oauthJsonMcpTemplate(clientId, mcpUrl, snippet) {
1411
+ return {
1412
+ client: clientId,
1413
+ serverName: MCP_SERVER_NAME,
1414
+ snippetFormat: 'json',
1415
+ snippet,
1416
+ requiresEnv: [],
1417
+ optionalEnv: [AGENT_INSTANCE_ENV_VAR],
1418
+ authentication: 'oauth',
1419
+ agentIdentity: {
1420
+ agentId: clientId,
1421
+ agentIdHeader: AGENT_ID_HEADER,
1422
+ agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1423
+ agentInstanceHeader: AGENT_INSTANCE_HEADER
1424
+ },
1425
+ mcpUrl,
1426
+ writesTokenValue: false
1427
+ };
1428
+ }
1429
+
1430
+ function mcpLocalProxyTemplate(clientId, proxyUrl) {
1431
+ const serverName = clientId === 'cursor' || clientId === 'gemini-cli' || clientId === 'antigravity' ? 'memory_os' : 'memory-os';
1432
+ return {
1433
+ client: clientId,
1434
+ serverName,
1435
+ snippetFormat: 'json',
1436
+ snippet: {
1437
+ mcpServers: {
1438
+ [serverName]: {
1439
+ type: 'http',
1440
+ url: proxyUrl
1441
+ }
1442
+ }
1443
+ },
1444
+ requiresCredential: [`${COMMAND_NAME} login`, `${COMMAND_NAME} token add --from-stdin`],
1445
+ requiresLocalCommand: `${COMMAND_NAME} mcp proxy --port ${new URL(proxyUrl).port || DEFAULT_PROXY_PORT}`,
1446
+ agentIdentity: {
1447
+ agentId: clientId,
1448
+ agentIdHeader: AGENT_ID_HEADER,
1449
+ agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1450
+ agentInstanceHeader: AGENT_INSTANCE_HEADER
1451
+ },
1452
+ writesTokenValue: false
1453
+ };
1454
+ }
1455
+
1456
+ function sameMajorMinor(left, right) {
1457
+ const leftParts = left.split('.');
1458
+ const rightParts = right.split('.');
1459
+ return leftParts[0] === rightParts[0] && leftParts[1] === rightParts[1];
1460
+ }
1461
+
1462
+ function baseUrlOption(args, env) {
1463
+ return optionValue(args, '--base-url')
1464
+ ?? optionValue(args, '--url')
1465
+ ?? env.XMEMO_BASE_URL
1466
+ ?? env.XMEMO_URL
1467
+ ?? env.MEMORY_OS_BASE_URL
1468
+ ?? env.MEMORY_OS_URL
1469
+ ?? DEFAULT_SERVICE_URL;
1470
+ }
1471
+
1472
+ function clientSetupPlan(clientId, client, mcpUrl, env, identity) {
1473
+ return {
1474
+ id: clientId,
1475
+ label: client.label,
1476
+ configKind: client.configKind,
1477
+ configPath: client.defaultConfigPath(env),
1478
+ serverName: MCP_SERVER_NAME,
1479
+ mcpUrl,
1480
+ tokenEnvVar: TOKEN_ENV_VAR,
1481
+ agentId: identity.agentId,
1482
+ agentInstanceId: identity.agentInstanceId,
1483
+ agentInstanceIdPath: identity.path,
1484
+ writesTokenValue: false,
1485
+ written: false
1486
+ };
1487
+ }
1488
+
1489
+ function copilotSetupPlan(mcpUrl, proxyPort, env) {
1490
+ const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`;
1491
+ const template = mcpLocalProxyTemplate('copilot-cli', proxyUrl);
1492
+ return {
1493
+ id: 'copilot-cli',
1494
+ label: 'Copilot CLI',
1495
+ configKind: 'local-proxy',
1496
+ configPath: defaultCopilotConfigPath(env),
1497
+ serverName: template.serverName,
1498
+ mcpUrl,
1499
+ proxyUrl,
1500
+ tokenEnvVar: TOKEN_ENV_VAR,
1501
+ requiresCredential: template.requiresCredential,
1502
+ requiresLocalCommand: template.requiresLocalCommand,
1503
+ template: template.snippet,
1504
+ agentId: template.agentIdentity.agentId,
1505
+ writesTokenValue: false,
1506
+ writeSupported: true,
1507
+ written: false
1508
+ };
1509
+ }
1510
+
1511
+ function writeSetupSummary(plan, io) {
1512
+ writeLine(io.stdout, `${PRODUCT_NAME} setup discovery: ${plan.baseUrl}`);
1513
+ writeLine(io.stdout, ` API: ${plan.apiBase}`);
1514
+ writeLine(io.stdout, ` MCP: ${plan.mcpUrl}`);
1515
+ writeLine(io.stdout, ` Guide: ${plan.guideUrl}`);
1516
+ if (plan.docsUrl) {
1517
+ writeLine(io.stdout, ` Docs: ${plan.docsUrl}`);
1518
+ }
1519
+ if (plan.tokenPortalUrl) {
1520
+ writeLine(io.stdout, ` Token portal: ${plan.tokenPortalUrl}`);
1521
+ }
1522
+ writeLine(io.stdout, ` Token env var: ${plan.tokenEnvVar}`);
1523
+ writeLine(io.stdout, ` Onboarding ready: ${plan.onboardingReady === null ? 'unknown' : plan.onboardingReady}`);
1524
+ writeLine(io.stdout, 'Privacy: telemetry disabled; no token sent; generated config references env vars only.');
1525
+
1526
+ if (plan.boundaries.adminRequired.length > 0) {
1527
+ writeLine(io.stdout, `Admin-only actions: ${plan.boundaries.adminRequired.join(', ')}`);
1528
+ }
1529
+
1530
+ if (plan.selectedClient) {
1531
+ writeLine(io.stdout, '');
1532
+ writeLine(io.stdout, `Selected client: ${plan.selectedClient.label}`);
1533
+ writeLine(io.stdout, ` Config path: ${plan.selectedClient.configPath}`);
1534
+ writeLine(io.stdout, ` Written: ${plan.selectedClient.written}`);
1535
+ writeLine(io.stdout, ` Token value embedded: ${plan.selectedClient.writesTokenValue}`);
1536
+ writeLine(io.stdout, ` Agent ID: ${plan.selectedClient.agentId}`);
1537
+ if (plan.selectedClient.agentInstanceIdPath) {
1538
+ writeLine(io.stdout, ` Agent instance ID stored: ${plan.selectedClient.agentInstanceIdPath}`);
1539
+ }
1540
+ if (plan.selectedClient.configKind === 'local-proxy') {
1541
+ writeLine(io.stdout, ` Local proxy: ${plan.selectedClient.requiresLocalCommand}`);
1542
+ if (plan.selectedClient.written) {
1543
+ writeLine(io.stdout, ` Next: keep \`${plan.selectedClient.requiresLocalCommand}\` running while you use Copilot CLI.`);
1544
+ writeLine(io.stdout, ' If Copilot CLI is already open, reload MCP config or restart Copilot CLI.');
1545
+ } else {
1546
+ writeLine(io.stdout, ' MCP template:');
1547
+ writeLine(io.stdout, JSON.stringify(plan.selectedClient.template, null, 2));
1548
+ writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup copilot --url ${plan.baseUrl}`);
1549
+ }
1550
+ return;
1551
+ }
1552
+ if (plan.selectedClient.codexProfile) {
1553
+ const profile = plan.selectedClient.codexProfile;
1554
+ writeLine(io.stdout, ` Codex profile target: ${profile.targetPath}`);
1555
+ writeLine(io.stdout, ` Codex profile installed: ${profile.written}`);
1556
+ writeLine(io.stdout, ` Codex profile changed: ${profile.changed}`);
1557
+ if (!profile.written) {
1558
+ writeLine(io.stdout, ` Profile preview: ${COMMAND_NAME} profile install codex --target ${profile.targetPath}`);
1559
+ }
1560
+ }
1561
+ if (!plan.selectedClient.written) {
1562
+ writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup ${plan.selectedClient.id} --url ${plan.baseUrl}`);
1563
+ }
1564
+ return;
1565
+ }
1566
+
1567
+ writeLine(io.stdout, '');
1568
+ writeLine(io.stdout, 'Next steps:');
1569
+ writeLine(io.stdout, ` 1. Create a scoped token in the token portal and store it in ${plan.tokenEnvVar}.`);
1570
+ writeLine(io.stdout, ` 2. Configure a client, for example: ${COMMAND_NAME} setup codex --url ${plan.baseUrl}`);
1571
+ writeLine(io.stdout, ` 3. Run ${COMMAND_NAME} status to smoke-test the service without sending the token.`);
1572
+ }
1573
+
1574
+ function discoveryMcpClients(discovery) {
1575
+ const clients = discovery?.clients?.mcp;
1576
+ if (!Array.isArray(clients)) {
1577
+ return [];
1578
+ }
1579
+
1580
+ return clients
1581
+ .filter((client) => isPlainObject(client) && typeof client.id === 'string')
1582
+ .map((client) => ({
1583
+ id: client.id,
1584
+ configEndpoint: typeof client.config_endpoint === 'string' ? client.config_endpoint : null
1585
+ }));
1586
+ }
1587
+
1588
+ function normalizeBaseUrl(input) {
1589
+ try {
1590
+ const parsed = new URL(input);
1591
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
1592
+ throw new UsageError('URL must use http or https.');
1593
+ }
1594
+ parsed.hash = '';
1595
+ parsed.search = '';
1596
+ return parsed.toString().replace(/\/$/, '');
1597
+ } catch (error) {
1598
+ if (error instanceof UsageError) {
1599
+ throw error;
1600
+ }
1601
+ throw new UsageError(`Invalid URL: ${input}`);
1602
+ }
1603
+ }
1604
+
1605
+ function endpointUrl(baseUrl, pathname) {
1606
+ const url = new URL(baseUrl);
1607
+ url.pathname = pathname;
1608
+ url.hash = '';
1609
+ url.search = '';
1610
+ return url.toString();
1611
+ }
1612
+
1613
+ function codexTomlSnippet(mcpUrl) {
1614
+ return `[mcp_servers.${MCP_SERVER_NAME}]
1615
+ url = "${escapeTomlString(mcpUrl)}"
1616
+ bearer_token_env_var = "${TOKEN_ENV_VAR}"
1617
+ `;
1618
+ }
1619
+
1620
+ function codexMemoryProfile() {
1621
+ return {
1622
+ client: 'codex',
1623
+ profileVersion: 'codex-mcp-depth-v1',
1624
+ mcpServerName: MCP_SERVER_NAME,
1625
+ requiredTokenEnv: TOKEN_ENV_VAR,
1626
+ objective: 'Use XMemo deliberately through MCP for project context recall and high-signal write-back.',
1627
+ instructions: [
1628
+ 'At the start of a non-trivial task, call XMemo recall/search for relevant project decisions, conventions, prior fixes, and active context unless the user explicitly asks not to use memory.',
1629
+ 'Use recalled memories as evidence, not as unquestioned truth. Prefer current repository files when memory conflicts with code.',
1630
+ 'After meaningful decisions, bug fixes, release steps, or durable conventions, write a concise XMemo memory with scope, source, and no secret values.',
1631
+ 'Never store tokens, API keys, cookies, private keys, raw credentials, or sensitive customer data in XMemo.',
1632
+ 'For routine or low-signal output, skip durable writes. Prefer summarized procedural or semantic memories over verbose logs.',
1633
+ 'Keep XMemo authentication through the XMEMO_KEY environment variable; do not paste token values into prompts, config files, or logs.'
1634
+ ],
1635
+ setupCommand: `${COMMAND_NAME} setup codex --url "$XMEMO_URL"`,
1636
+ smokeCommand: `${COMMAND_NAME} smoke --client codex`
1637
+ };
1638
+ }
1639
+
1640
+ function writeCodexMemoryProfile(profile, io) {
1641
+ writeLine(io.stdout, `${PRODUCT_NAME} Codex memory behavior profile`);
1642
+ writeLine(io.stdout, `Profile: ${profile.profileVersion}`);
1643
+ writeLine(io.stdout, `MCP server: ${profile.mcpServerName}`);
1644
+ writeLine(io.stdout, `Token env: ${profile.requiredTokenEnv}`);
1645
+ writeLine(io.stdout, '');
1646
+ writeLine(io.stdout, 'Recommended Codex instructions:');
1647
+ for (const instruction of profile.instructions) {
1648
+ writeLine(io.stdout, `- ${instruction}`);
1649
+ }
1650
+ writeLine(io.stdout, '');
1651
+ writeLine(io.stdout, `Setup: ${profile.setupCommand}`);
1652
+ writeLine(io.stdout, `Smoke test: ${profile.smokeCommand}`);
1653
+ }
1654
+
1655
+ function codexProfileInstructionText() {
1656
+ const profile = codexMemoryProfile();
1657
+ const lines = [
1658
+ '## XMemo Codex profile',
1659
+ '',
1660
+ `MCP server: \`${profile.mcpServerName}\``,
1661
+ `Token env var: \`${profile.requiredTokenEnv}\``,
1662
+ '',
1663
+ profile.objective,
1664
+ '',
1665
+ 'Recommended Codex behavior:'
1666
+ ];
1667
+ for (const instruction of profile.instructions) {
1668
+ lines.push(`- ${instruction}`);
1669
+ }
1670
+ lines.push('');
1671
+ return `${lines.join('\n')}\n`;
1672
+ }
1673
+
1674
+ function codexProfileMarkerBlock() {
1675
+ return `${CODEX_PROFILE_MARKER_START}\n${codexProfileInstructionText()}${CODEX_PROFILE_MARKER_END}\n`;
1676
+ }
1677
+
1678
+ function defaultCodexProfileTarget() {
1679
+ return path.resolve(process.cwd(), CODEX_PROFILE_TARGET);
1680
+ }
1681
+
1682
+ async function codexProfileInstallResult(targetPath, options = {}) {
1683
+ const resolvedTarget = path.resolve(targetPath);
1684
+ const existing = await readTextIfExists(resolvedTarget);
1685
+ const marker = markerBounds(existing);
1686
+ const block = codexProfileMarkerBlock();
1687
+ let nextText;
1688
+
1689
+ if (marker.present) {
1690
+ nextText = `${existing.slice(0, marker.start)}${block}${existing.slice(marker.end)}`;
1691
+ } else if (existing.trim().length === 0) {
1692
+ nextText = block;
1693
+ } else {
1694
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
1695
+ nextText = `${existing}${separator}${block}`;
1696
+ }
1697
+
1698
+ const changed = nextText !== existing;
1699
+ const write = Boolean(options.write);
1700
+ if (write && changed) {
1701
+ await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
1702
+ await fs.writeFile(resolvedTarget, nextText);
1703
+ }
1704
+
1705
+ return {
1706
+ client: 'codex',
1707
+ action: 'install',
1708
+ targetPath: resolvedTarget,
1709
+ markerStart: CODEX_PROFILE_MARKER_START,
1710
+ markerEnd: CODEX_PROFILE_MARKER_END,
1711
+ installed: marker.present || (write && changed),
1712
+ written: write,
1713
+ changed,
1714
+ markerPresent: marker.present,
1715
+ writesTokenValue: false
1716
+ };
1717
+ }
1718
+
1719
+ async function codexProfileStatusResult(targetPath) {
1720
+ const resolvedTarget = path.resolve(targetPath);
1721
+ const existing = await readTextIfExists(resolvedTarget);
1722
+ const marker = markerBounds(existing);
1723
+ return {
1724
+ client: 'codex',
1725
+ action: 'status',
1726
+ targetPath: resolvedTarget,
1727
+ installed: marker.present,
1728
+ markerPresent: marker.present,
1729
+ markerStart: CODEX_PROFILE_MARKER_START,
1730
+ markerEnd: CODEX_PROFILE_MARKER_END,
1731
+ writesTokenValue: false
1732
+ };
1733
+ }
1734
+
1735
+ async function codexProfileUninstallResult(targetPath, options = {}) {
1736
+ const resolvedTarget = path.resolve(targetPath);
1737
+ const existing = await readTextIfExists(resolvedTarget);
1738
+ const marker = markerBounds(existing);
1739
+ const write = Boolean(options.write);
1740
+ let changed = false;
1741
+
1742
+ if (marker.present) {
1743
+ let nextText = `${existing.slice(0, marker.start)}${existing.slice(marker.end)}`;
1744
+ nextText = nextText.replace(/\n{3,}/g, '\n\n');
1745
+ if (nextText.trim().length === 0) {
1746
+ nextText = '';
1747
+ } else if (!nextText.endsWith('\n')) {
1748
+ nextText = `${nextText}\n`;
1749
+ }
1750
+ changed = nextText !== existing;
1751
+ if (write && changed) {
1752
+ await fs.writeFile(resolvedTarget, nextText);
1753
+ }
1754
+ }
1755
+
1756
+ return {
1757
+ client: 'codex',
1758
+ action: 'uninstall',
1759
+ targetPath: resolvedTarget,
1760
+ installed: marker.present && !(write && changed),
1761
+ written: write,
1762
+ changed,
1763
+ markerPresent: marker.present,
1764
+ markerStart: CODEX_PROFILE_MARKER_START,
1765
+ markerEnd: CODEX_PROFILE_MARKER_END,
1766
+ writesTokenValue: false
1767
+ };
1768
+ }
1769
+
1770
+ function markerBounds(content) {
1771
+ const start = content.indexOf(CODEX_PROFILE_MARKER_START);
1772
+ const end = content.indexOf(CODEX_PROFILE_MARKER_END);
1773
+ if (start === -1 && end === -1) {
1774
+ return { present: false, start: -1, end: -1 };
1775
+ }
1776
+
1777
+ if (start === -1 || end === -1 || end < start) {
1778
+ throw new UsageError('Codex profile markers are incomplete or out of order; edit the target file manually before retrying.');
1779
+ }
1780
+
1781
+ if (
1782
+ content.indexOf(CODEX_PROFILE_MARKER_START, start + CODEX_PROFILE_MARKER_START.length) !== -1
1783
+ || content.indexOf(CODEX_PROFILE_MARKER_END, end + CODEX_PROFILE_MARKER_END.length) !== -1
1784
+ ) {
1785
+ throw new UsageError('Codex profile markers appear more than once; edit the target file manually before retrying.');
1786
+ }
1787
+
1788
+ const afterEnd = end + CODEX_PROFILE_MARKER_END.length;
1789
+ const trailingNewlineLength = content.slice(afterEnd, afterEnd + 2) === '\r\n'
1790
+ ? 2
1791
+ : content.slice(afterEnd, afterEnd + 1) === '\n'
1792
+ ? 1
1793
+ : 0;
1794
+
1795
+ return {
1796
+ present: true,
1797
+ start,
1798
+ end: afterEnd + trailingNewlineLength
1799
+ };
1800
+ }
1801
+
1802
+ function writeProfileResult(action, result, io) {
1803
+ writeLine(io.stdout, `${PRODUCT_NAME} Codex profile ${action}`);
1804
+ writeLine(io.stdout, ` Target: ${result.targetPath}`);
1805
+ writeLine(io.stdout, ` Installed: ${result.installed}`);
1806
+ if ('written' in result) {
1807
+ writeLine(io.stdout, ` Written: ${result.written}`);
1808
+ writeLine(io.stdout, ` Changed: ${result.changed}`);
1809
+ }
1810
+ writeLine(io.stdout, ' Token value embedded: false');
1811
+ }
1812
+
1813
+ async function codexSmokeReport(configPath, env) {
1814
+ const configText = await readTextIfExists(configPath);
1815
+ const block = tomlServerBlock(configText, MCP_SERVER_NAME);
1816
+ const mcpUrl = block ? tomlStringValue(block, 'url') : null;
1817
+ const bearerTokenEnvVar = block ? tomlStringValue(block, 'bearer_token_env_var') : null;
1818
+ const tokenValue = env[TOKEN_ENV_VAR] ?? '';
1819
+ const identityPath = agentInstanceIdentityPath(env, 'codex');
1820
+ const identityPresent = await fileExists(identityPath);
1821
+ const checks = [
1822
+ {
1823
+ name: 'config_present',
1824
+ ok: configText.trim().length > 0,
1825
+ required: true,
1826
+ detail: configText.trim().length > 0 ? 'found' : 'missing'
1827
+ },
1828
+ {
1829
+ name: 'memory_os_server_present',
1830
+ ok: Boolean(block),
1831
+ required: true,
1832
+ detail: block ? `[mcp_servers.${MCP_SERVER_NAME}]` : `missing [mcp_servers.${MCP_SERVER_NAME}]`
1833
+ },
1834
+ {
1835
+ name: 'mcp_url_present',
1836
+ ok: Boolean(mcpUrl),
1837
+ required: true,
1838
+ detail: mcpUrl ?? 'missing url'
1839
+ },
1840
+ {
1841
+ name: 'bearer_token_env_var',
1842
+ ok: bearerTokenEnvVar === TOKEN_ENV_VAR,
1843
+ required: true,
1844
+ detail: bearerTokenEnvVar ?? 'missing bearer_token_env_var'
1845
+ },
1846
+ {
1847
+ name: 'token_env_present',
1848
+ ok: Boolean(env[TOKEN_ENV_VAR]),
1849
+ required: true,
1850
+ detail: env[TOKEN_ENV_VAR] ? 'present' : `missing ${TOKEN_ENV_VAR}`
1851
+ },
1852
+ {
1853
+ name: 'token_not_embedded_in_config',
1854
+ ok: !tokenValue || !configText.includes(tokenValue),
1855
+ required: true,
1856
+ detail: 'token value not printed or embedded'
1857
+ },
1858
+ {
1859
+ name: 'agent_instance_identity_file',
1860
+ ok: identityPresent,
1861
+ required: false,
1862
+ detail: identityPresent ? identityPath : `optional; create with ${COMMAND_NAME} mcp add codex --write (${identityPath})`
1863
+ }
1864
+ ];
1865
+
1866
+ return {
1867
+ ok: checks.every((check) => !check.required || check.ok),
1868
+ client: 'codex',
1869
+ configPath,
1870
+ serverName: MCP_SERVER_NAME,
1871
+ mcpUrl,
1872
+ tokenEnvVar: TOKEN_ENV_VAR,
1873
+ agentInstanceIdPath: identityPath,
1874
+ checks
1875
+ };
1876
+ }
1877
+
1878
+ function tomlServerBlock(content, serverName) {
1879
+ const header = `[mcp_servers.${serverName}]`;
1880
+ const lines = content.split(/\r?\n/);
1881
+ const start = lines.findIndex((line) => line.trim() === header);
1882
+ if (start === -1) {
1883
+ return '';
1884
+ }
1885
+
1886
+ const block = [];
1887
+ for (let index = start + 1; index < lines.length; index += 1) {
1888
+ const line = lines[index];
1889
+ if (/^\s*\[/.test(line)) {
1890
+ break;
1891
+ }
1892
+ block.push(line);
1893
+ }
1894
+ return block.join('\n');
1895
+ }
1896
+
1897
+ function tomlStringValue(block, key) {
1898
+ const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"((?:\\\\.|[^"\\\\])*)"\\s*$`, 'm');
1899
+ const match = block.match(pattern);
1900
+ return match ? unescapeTomlString(match[1]) : null;
1901
+ }
1902
+
1903
+ function cursorJsonSnippet(mcpUrl, identity = envReferenceIdentity('cursor')) {
1904
+ return `${JSON.stringify(cursorJsonConfig(mcpUrl, identity), null, 2)}\n`;
1905
+ }
1906
+
1907
+ async function appendTomlServerConfig(configPath, mcpUrl) {
1908
+ const snippet = codexTomlSnippet(mcpUrl);
1909
+ const existing = await readTextIfExists(configPath);
1910
+ if (existing.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
1911
+ throw new UsageError(`MCP config already contains [mcp_servers.${MCP_SERVER_NAME}]. Edit ${configPath} manually to avoid duplicate server definitions.`);
1912
+ }
1913
+
1914
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
1915
+ const prefix = existing.trim().length === 0 ? '' : '\n\n';
1916
+ await fs.appendFile(configPath, `${prefix}${snippet}`, { mode: 0o600 });
1917
+ await bestEffortChmod(configPath, 0o600);
1918
+ }
1919
+
1920
+ async function mergeJsonMcpConfig(configPath, mcpUrl, identity) {
1921
+ const existing = await readTextIfExists(configPath);
1922
+ const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
1923
+
1924
+ if (!isPlainObject(parsed)) {
1925
+ throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
1926
+ }
1927
+
1928
+ if (!isPlainObject(parsed.mcpServers)) {
1929
+ parsed.mcpServers = {};
1930
+ }
1931
+
1932
+ if (parsed.mcpServers[MCP_SERVER_NAME]) {
1933
+ throw new UsageError(`MCP config already contains mcpServers.${MCP_SERVER_NAME}. Edit ${configPath} manually to avoid duplicate server definitions.`);
1934
+ }
1935
+
1936
+ parsed.mcpServers[MCP_SERVER_NAME] = cursorJsonServerConfig(mcpUrl, identity);
1937
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
1938
+ await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
1939
+ await bestEffortChmod(configPath, 0o600);
1940
+ }
1941
+
1942
+ function antigravityJsonServerConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
1943
+ return {
1944
+ serverUrl: mcpUrl,
1945
+ headers: {
1946
+ [AGENT_ID_HEADER]: identity.agentId,
1947
+ [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
1948
+ }
1949
+ };
1950
+ }
1951
+
1952
+ function antigravityJsonConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
1953
+ return {
1954
+ mcpServers: {
1955
+ [MCP_SERVER_NAME]: antigravityJsonServerConfig(mcpUrl, identity)
1956
+ }
1957
+ };
1958
+ }
1959
+
1960
+ function antigravityJsonSnippet(mcpUrl, identity = envReferenceIdentity('antigravity')) {
1961
+ return `${JSON.stringify(antigravityJsonConfig(mcpUrl, identity), null, 2)}\n`;
1962
+ }
1963
+
1964
+ async function mergeAntigravityMcpConfig(configPath, mcpUrl, identity) {
1965
+ const existing = await readTextIfExists(configPath);
1966
+ const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
1967
+
1968
+ if (!isPlainObject(parsed)) {
1969
+ throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
1970
+ }
1971
+
1972
+ if (!isPlainObject(parsed.mcpServers)) {
1973
+ parsed.mcpServers = {};
1974
+ }
1975
+
1976
+ if (parsed.mcpServers[MCP_SERVER_NAME]) {
1977
+ throw new UsageError(`MCP config already contains mcpServers.${MCP_SERVER_NAME}. Edit ${configPath} manually to avoid duplicate server definitions.`);
1978
+ }
1979
+
1980
+ parsed.mcpServers[MCP_SERVER_NAME] = antigravityJsonServerConfig(mcpUrl, identity);
1981
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
1982
+ await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
1983
+ await bestEffortChmod(configPath, 0o600);
1984
+ }
1985
+
1986
+
1987
+ async function mergeGeminiMcpConfig(configPath, mcpUrl, identity) {
1988
+ const existing = await readTextIfExists(configPath);
1989
+ const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
1990
+
1991
+ if (!isPlainObject(parsed)) {
1992
+ throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
1993
+ }
1994
+
1995
+ if (!isPlainObject(parsed.mcpServers)) {
1996
+ parsed.mcpServers = {};
1997
+ }
1998
+
1999
+ if (parsed.mcpServers[MCP_SERVER_NAME]) {
2000
+ throw new UsageError(`MCP config already contains mcpServers.${MCP_SERVER_NAME}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2001
+ }
2002
+
2003
+ parsed.mcpServers[MCP_SERVER_NAME] = geminiJsonServerConfig(mcpUrl, identity);
2004
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2005
+ await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2006
+ await bestEffortChmod(configPath, 0o600);
2007
+ }
2008
+
2009
+ async function mergeCopilotMcpConfig(configPath, proxyUrl) {
2010
+ const existing = await readTextIfExists(configPath);
2011
+ const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
2012
+
2013
+ if (!isPlainObject(parsed)) {
2014
+ throw new UsageError(`Copilot MCP JSON config must be an object: ${configPath}`);
2015
+ }
2016
+
2017
+ if (!isPlainObject(parsed.mcpServers)) {
2018
+ parsed.mcpServers = {};
2019
+ }
2020
+
2021
+ parsed.mcpServers['memory-os'] = copilotLocalProxyServerConfig(proxyUrl);
2022
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2023
+ await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2024
+ await bestEffortChmod(configPath, 0o600);
2025
+ }
2026
+
2027
+ function copilotLocalProxyServerConfig(proxyUrl) {
2028
+ return {
2029
+ type: 'http',
2030
+ url: proxyUrl
2031
+ };
2032
+ }
2033
+
2034
+ function cursorJsonConfig(mcpUrl, identity = envReferenceIdentity('cursor')) {
2035
+ return {
2036
+ mcpServers: {
2037
+ [MCP_SERVER_NAME]: cursorJsonServerConfig(mcpUrl, identity)
2038
+ }
2039
+ };
2040
+ }
2041
+
2042
+ function cursorJsonServerConfig(mcpUrl, identity = envReferenceIdentity('cursor')) {
2043
+ return {
2044
+ url: mcpUrl,
2045
+ headers: {
2046
+ Authorization: `Bearer \${env:${TOKEN_ENV_VAR}}`,
2047
+ [AGENT_ID_HEADER]: identity.agentId,
2048
+ [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
2049
+ }
2050
+ };
2051
+ }
2052
+
2053
+ function geminiJsonServerConfig(mcpUrl, identity = envReferenceIdentity('gemini-cli')) {
2054
+ return {
2055
+ httpUrl: mcpUrl,
2056
+ headers: {
2057
+ [AGENT_ID_HEADER]: identity.agentId,
2058
+ [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
2059
+ }
2060
+ };
2061
+ }
2062
+
2063
+ function geminiJsonConfig(mcpUrl, identity = envReferenceIdentity('gemini-cli')) {
2064
+ return {
2065
+ mcpServers: {
2066
+ [MCP_SERVER_NAME]: geminiJsonServerConfig(mcpUrl, identity)
2067
+ }
2068
+ };
2069
+ }
2070
+
2071
+ function geminiJsonSnippet(mcpUrl, identity = envReferenceIdentity('gemini-cli')) {
2072
+ return `${JSON.stringify(geminiJsonConfig(mcpUrl, identity), null, 2)}\n`;
2073
+ }
2074
+
2075
+ async function agentIdentity(clientId, env) {
2076
+ const configuredInstanceId = env[AGENT_INSTANCE_ENV_VAR];
2077
+ if (configuredInstanceId) {
2078
+ return {
2079
+ agentId: clientId,
2080
+ agentInstanceId: configuredInstanceId,
2081
+ path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
2082
+ };
2083
+ }
2084
+
2085
+ const identityPath = agentInstanceIdentityPath(env, clientId);
2086
+ const existing = await readAgentInstanceIdentity(identityPath);
2087
+ if (existing) {
2088
+ return { agentId: clientId, agentInstanceId: existing, path: identityPath };
2089
+ }
2090
+
2091
+ const generated = `xmemo-${clientId}-${randomUUID()}`;
2092
+ await fs.mkdir(path.dirname(identityPath), { recursive: true, mode: 0o700 });
2093
+ await bestEffortChmod(path.dirname(identityPath), 0o700);
2094
+ await fs.writeFile(identityPath, `${JSON.stringify({ version: 1, agentId: clientId, agentInstanceId: generated }, null, 2)}\n`, { mode: 0o600 });
2095
+ await bestEffortChmod(identityPath, 0o600);
2096
+ return { agentId: clientId, agentInstanceId: generated, path: identityPath };
2097
+ }
2098
+
2099
+ async function readAgentInstanceIdentity(identityPath) {
2100
+ const existing = await readTextIfExists(identityPath);
2101
+ if (!existing.trim()) {
2102
+ return null;
2103
+ }
2104
+ const parsed = parseJsonConfig(existing, identityPath);
2105
+ const value = stringValue(parsed, ['agentInstanceId']);
2106
+ return value || null;
2107
+ }
2108
+
2109
+ function agentInstanceIdentityPath(env, clientId) {
2110
+ return path.join(configRoot(env), 'agent-instances', `${clientId}.json`);
2111
+ }
2112
+
2113
+ function envReferenceIdentity(clientId) {
2114
+ return {
2115
+ agentId: clientId,
2116
+ agentInstanceId: `\${${AGENT_INSTANCE_ENV_VAR}}`,
2117
+ path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
2118
+ };
2119
+ }
2120
+
2121
+ function supportedMcpClients() {
2122
+ const clients = Array.from(MCP_CLIENTS.entries()).map(([id, client]) => ({
2123
+ id,
2124
+ label: client.label,
2125
+ configKind: client.configKind
2126
+ }));
2127
+ clients.push({ id: 'copilot-cli', label: 'Copilot CLI', configKind: 'local-proxy' });
2128
+ return clients;
2129
+ }
2130
+
2131
+ function supportedMcpClientIds() {
2132
+ return Array.from(MCP_CLIENTS.keys());
2133
+ }
2134
+
2135
+ function supportedSetupClientIds() {
2136
+ return ['codex', 'cursor', 'copilot', 'gemini', 'antigravity'];
2137
+ }
2138
+
2139
+ function usesClientOAuth(clientId) {
2140
+ return clientId === 'gemini-cli' || clientId === 'antigravity';
2141
+ }
2142
+
2143
+ function credentialsPath(env) {
2144
+ return path.join(configRoot(env), 'credentials.json');
2145
+ }
2146
+
2147
+ function configRoot(env) {
2148
+ if (env.XMEMO_CONFIG_HOME) {
2149
+ return env.XMEMO_CONFIG_HOME;
2150
+ }
2151
+
2152
+ if (env.MEMORY_OS_CONFIG_HOME) {
2153
+ return env.MEMORY_OS_CONFIG_HOME;
2154
+ }
2155
+
2156
+ if (process.platform === 'win32' && env.LOCALAPPDATA) {
2157
+ return path.join(env.LOCALAPPDATA, 'XMemo', 'CLI');
2158
+ }
2159
+
2160
+ if (env.XDG_CONFIG_HOME) {
2161
+ return path.join(env.XDG_CONFIG_HOME, 'xmemo');
2162
+ }
2163
+
2164
+ const home = env.HOME || os.homedir();
2165
+ return path.join(home, '.config', 'xmemo');
2166
+ }
2167
+
2168
+ function defaultCodexConfigPath(env) {
2169
+ const home = env.USERPROFILE || env.HOME || os.homedir();
2170
+ return path.join(home, '.codex', 'config.toml');
2171
+ }
2172
+
2173
+ function defaultCursorConfigPath(env) {
2174
+ const home = env.USERPROFILE || env.HOME || os.homedir();
2175
+ return path.join(home, '.cursor', 'mcp.json');
2176
+ }
2177
+
2178
+ function defaultGeminiConfigPath(env) {
2179
+ const home = env.USERPROFILE || env.HOME || os.homedir();
2180
+ return path.join(home, '.gemini', 'settings.json');
2181
+ }
2182
+
2183
+ function defaultAntigravityConfigPath(env) {
2184
+ const home = env.USERPROFILE || env.HOME || os.homedir();
2185
+ return path.join(home, '.gemini', 'antigravity', 'mcp_config.json');
2186
+ }
2187
+
2188
+ function defaultCopilotConfigPath(env) {
2189
+ const home = env.USERPROFILE || env.HOME || os.homedir();
2190
+ return path.join(env.COPILOT_HOME ?? path.join(home, '.copilot'), 'mcp-config.json');
2191
+ }
2192
+
2193
+ async function writePlaintextCredential(credentialPath, token, metadata = {}) {
2194
+ await fs.mkdir(path.dirname(credentialPath), { recursive: true, mode: 0o700 });
2195
+ await bestEffortChmod(path.dirname(credentialPath), 0o700);
2196
+ const payload = {
2197
+ version: 1,
2198
+ tokenEnvVar: TOKEN_ENV_VAR,
2199
+ storage: 'user-scoped-credential-file',
2200
+ createdAt: new Date().toISOString(),
2201
+ metadata,
2202
+ token
2203
+ };
2204
+ await fs.writeFile(credentialPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
2205
+ await bestEffortChmod(credentialPath, 0o600);
2206
+ }
2207
+
2208
+ async function bestEffortChmod(filePath, mode) {
2209
+ try {
2210
+ await fs.chmod(filePath, mode);
2211
+ } catch {
2212
+ // Windows and managed environments may ignore POSIX chmod.
2213
+ }
2214
+ }
2215
+
2216
+ function validateToken(token) {
2217
+ if (!token) {
2218
+ throw new UsageError('Token from stdin is empty.');
2219
+ }
2220
+
2221
+ if (/\s/.test(token)) {
2222
+ throw new UsageError('Token must not contain whitespace.');
2223
+ }
2224
+
2225
+ if (token.length < 16) {
2226
+ throw new UsageError('Token is too short to be a production credential.');
2227
+ }
2228
+ }
2229
+
2230
+ function requiredOption(args, name) {
2231
+ const value = optionValue(args, name);
2232
+ if (!value) {
2233
+ throw new UsageError(`Missing required option ${name}.`);
2234
+ }
2235
+ return value;
2236
+ }
2237
+
2238
+ function positionalClientArg(args) {
2239
+ const candidate = args[0];
2240
+ if (!candidate || candidate.startsWith('--')) {
2241
+ return null;
2242
+ }
2243
+
2244
+ return normalizeSetupClientId(candidate);
2245
+ }
2246
+
2247
+ function normalizeSetupClientId(candidate) {
2248
+ if (!candidate) {
2249
+ return null;
2250
+ }
2251
+
2252
+ const normalized = SETUP_CLIENT_ALIASES.get(candidate);
2253
+ if (!normalized) {
2254
+ throw new UsageError(`Unsupported setup client: ${candidate}. Supported clients: ${supportedSetupClientIds().join(', ')}.`);
2255
+ }
2256
+
2257
+ return normalized;
2258
+ }
2259
+
2260
+ function optionValue(args, name) {
2261
+ const index = args.indexOf(name);
2262
+ if (index === -1) {
2263
+ return null;
2264
+ }
2265
+
2266
+ const value = args[index + 1];
2267
+ if (!value || value.startsWith('--')) {
2268
+ throw new UsageError(`Option ${name} requires a value.`);
2269
+ }
2270
+
2271
+ return value;
2272
+ }
2273
+
2274
+ function stringValue(source, keys) {
2275
+ const value = valueAtPath(source, keys);
2276
+ return typeof value === 'string' && value.length > 0 ? value : null;
2277
+ }
2278
+
2279
+ function booleanValue(source, keys) {
2280
+ const value = valueAtPath(source, keys);
2281
+ return typeof value === 'boolean' ? value : null;
2282
+ }
2283
+
2284
+ function arrayValue(source, keys) {
2285
+ const value = valueAtPath(source, keys);
2286
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : null;
2287
+ }
2288
+
2289
+ function valueAtPath(source, keys) {
2290
+ let current = source;
2291
+ for (const key of keys) {
2292
+ if (!isPlainObject(current) || !(key in current)) {
2293
+ return null;
2294
+ }
2295
+ current = current[key];
2296
+ }
2297
+ return current;
2298
+ }
2299
+
2300
+ function hasFlag(args, name) {
2301
+ return args.includes(name);
2302
+ }
2303
+
2304
+ function parsePositiveInteger(value, name) {
2305
+ const parsed = Number.parseInt(value, 10);
2306
+ if (!Number.isInteger(parsed) || parsed <= 0) {
2307
+ throw new UsageError(`${name} must be a positive integer.`);
2308
+ }
2309
+ return parsed;
2310
+ }
2311
+
2312
+ async function sleep(ms) {
2313
+ await new Promise((resolve) => setTimeout(resolve, ms));
2314
+ }
2315
+
2316
+
2317
+ function npmExecutable() {
2318
+ return os.platform() === 'win32' ? 'npm.cmd' : 'npm';
2319
+ }
2320
+
2321
+
2322
+ async function runProcess(command, args, io, { stream = true } = {}) {
2323
+ const spawnFn = io.spawn ?? spawn;
2324
+ return await new Promise((resolve, reject) => {
2325
+ const child = spawnFn(command, args, {
2326
+ stdio: ['ignore', 'pipe', 'pipe']
2327
+ });
2328
+ let stdout = '';
2329
+ let stderr = '';
2330
+ child.stdout?.on('data', (chunk) => {
2331
+ const text = String(chunk);
2332
+ stdout += text;
2333
+ if (stream) {
2334
+ io.stdout.write(text);
2335
+ }
2336
+ });
2337
+ child.stderr?.on('data', (chunk) => {
2338
+ const text = String(chunk);
2339
+ stderr += text;
2340
+ if (stream) {
2341
+ io.stderr.write(text);
2342
+ }
2343
+ });
2344
+ child.on('error', reject);
2345
+ child.on('close', (code) => {
2346
+ resolve({ code: code ?? 0, stdout, stderr });
2347
+ });
2348
+ });
2349
+ }
2350
+
2351
+
2352
+ async function readAll(stream) {
2353
+ let content = '';
2354
+ for await (const chunk of stream) {
2355
+ content += chunk;
2356
+ }
2357
+ return content;
2358
+ }
2359
+
2360
+ async function fileExists(filePath) {
2361
+ try {
2362
+ await fs.access(filePath);
2363
+ return true;
2364
+ } catch {
2365
+ return false;
2366
+ }
2367
+ }
2368
+
2369
+ async function readTextIfExists(filePath) {
2370
+ try {
2371
+ return await fs.readFile(filePath, 'utf8');
2372
+ } catch (error) {
2373
+ if (error.code === 'ENOENT') {
2374
+ return '';
2375
+ }
2376
+ throw error;
2377
+ }
2378
+ }
2379
+
2380
+ function parseJsonConfig(content, configPath) {
2381
+ try {
2382
+ return JSON.parse(content);
2383
+ } catch (error) {
2384
+ throw new UsageError(`Invalid JSON in ${configPath}: ${error.message}`);
2385
+ }
2386
+ }
2387
+
2388
+ function isPlainObject(value) {
2389
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
2390
+ }
2391
+
2392
+ function escapeTomlString(value) {
2393
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
2394
+ }
2395
+
2396
+ function unescapeTomlString(value) {
2397
+ return value.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
2398
+ }
2399
+
2400
+ function escapeRegExp(value) {
2401
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2402
+ }
2403
+
2404
+ function writeLine(stream, line) {
2405
+ stream.write(`${line}\n`);
2406
+ }