@xmemo/client 0.4.155 → 0.4.157

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 (45) hide show
  1. package/README.md +37 -7
  2. package/package.json +3 -2
  3. package/plugins/kiro/.kiro-plugin/power.json +35 -0
  4. package/plugins/kiro/CHANGELOG.md +9 -0
  5. package/plugins/kiro/LICENSE +7 -0
  6. package/plugins/kiro/POWER.md +147 -0
  7. package/plugins/kiro/README.md +31 -0
  8. package/plugins/kiro/SETUP.md +234 -0
  9. package/plugins/kiro/assets/logo.svg +27 -0
  10. package/plugins/kiro/mcp.json +7 -0
  11. package/plugins/kiro/steering/xmemo-memory.md +32 -0
  12. package/src/cli.js +23 -3996
  13. package/src/commands/auth.js +230 -0
  14. package/src/commands/diagnostics.js +197 -0
  15. package/src/commands/mcp.js +188 -0
  16. package/src/commands/profile.js +57 -0
  17. package/src/commands/setup.js +191 -0
  18. package/src/commands/update.js +58 -0
  19. package/src/config/env.js +82 -0
  20. package/src/config/paths.js +26 -0
  21. package/src/config/profile.js +533 -0
  22. package/src/core/args.js +63 -0
  23. package/src/core/constants.js +32 -0
  24. package/src/core/errors.js +6 -0
  25. package/src/core/io.js +16 -0
  26. package/src/core/runtime.js +144 -0
  27. package/src/core/version.js +1 -0
  28. package/src/mcp/clients/detect.js +51 -0
  29. package/src/mcp/clients/registry.js +68 -0
  30. package/src/mcp/clients.js +81 -0
  31. package/src/mcp/core/names.js +13 -0
  32. package/src/mcp/core/templates.js +156 -0
  33. package/src/mcp/formats/json.js +355 -0
  34. package/src/mcp/formats/toml.js +148 -0
  35. package/src/mcp/formats/yaml.js +72 -0
  36. package/src/mcp/identity/device.js +78 -0
  37. package/src/mcp/identity/paths.js +155 -0
  38. package/src/mcp/proxy/copilot.js +44 -0
  39. package/src/mcp/proxy/server.js +112 -0
  40. package/src/network/auth.js +200 -0
  41. package/src/network/base-url.js +13 -0
  42. package/src/network/discovery.js +103 -0
  43. package/src/network/http.js +161 -0
  44. package/src/ui/help.js +59 -0
  45. package/src/ui/setup.js +244 -0
package/src/cli.js CHANGED
@@ -1,234 +1,26 @@
1
- import fs from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import http from 'node:http';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { spawn } from 'node:child_process';
7
- import { randomUUID } from 'node:crypto';
8
-
9
- const PRODUCT_NAME = 'XMemo';
10
- const PACKAGE_NAME = '@xmemo/client';
11
- const FALLBACK_PACKAGE_NAME = '@yonro/xmemo-client';
12
- const COMMAND_NAME = 'xmemo';
13
- const LEGACY_COMMAND_NAME = 'memory-os';
14
- const CLI_VERSION = '0.4.155';
15
- const DEFAULT_SERVICE_URL = 'https://xmemo.dev';
16
- const TOKEN_ENV_VAR = 'XMEMO_KEY';
17
- const LEGACY_TOKEN_ENV_VAR = 'MEMORY_OS_MCP_TOKEN';
18
- const AGENT_ID_ENV_VAR = 'XMEMO_AGENT_ID';
19
- const AGENT_INSTANCE_ENV_VAR = 'XMEMO_AGENT_INSTANCE_ID';
20
- const AGENT_ID_HEADER = 'X-Memory-OS-Agent-ID';
21
- const AGENT_INSTANCE_HEADER = 'X-Memory-OS-Agent-Instance-ID';
22
- const MCP_SERVER_NAME = 'XMemo';
23
- const LEGACY_MCP_SERVER_NAMES = ['memory_os', 'memory-os'];
24
- const CODEX_PROFILE_TARGET = 'AGENTS.md';
25
- const CODEX_PROFILE_MARKER_START = '<!-- memory-os:codex-profile:start -->';
26
- const CODEX_PROFILE_MARKER_END = '<!-- memory-os:codex-profile:end -->';
27
- const CLIENT_PROFILE_TARGETS = {
28
- cursor: '.cursor/rules/xmemo-memory.md',
29
- 'gemini-cli': 'GEMINI.md',
30
- antigravity: 'GEMINI.md',
31
- trae: '.trae/rules/xmemo-memory.md',
32
- 'trae-solo': '.trae/rules/xmemo-memory.md'
33
- };
34
- const CLIENT_PROFILE_MARKER_START = '<!-- xmemo:profile:start -->';
35
- const CLIENT_PROFILE_MARKER_END = '<!-- xmemo:profile:end -->';
36
- const PROFILE_MARKER_PREFIX = 'memory-os:memory-profile';
37
- const DEVICE_LOGIN_START_PATH = '/api/v1/auth/device/start';
38
- const DEVICE_LOGIN_TOKEN_PATH = '/api/v1/auth/device/token';
39
- const DEFAULT_PROXY_HOST = '127.0.0.1';
40
- const DEFAULT_PROXY_PORT = 8765;
41
-
42
- const MCP_CLIENTS = new Map([
43
- ['codex', {
44
- label: 'Codex',
45
- defaultConfigPath: defaultCodexConfigPath,
46
- buildSnippet: codexTomlSnippet,
47
- writeConfig: appendTomlServerConfig,
48
- configKind: 'toml'
49
- }],
50
- ['cursor', {
51
- label: 'Cursor',
52
- defaultConfigPath: defaultCursorConfigPath,
53
- buildSnippet: cursorJsonSnippet,
54
- writeConfig: mergeJsonMcpConfig,
55
- configKind: 'json'
56
- }],
57
- ['gemini-cli', {
58
- label: 'Gemini CLI',
59
- defaultConfigPath: defaultGeminiConfigPath,
60
- buildSnippet: geminiJsonSnippet,
61
- writeConfig: mergeGeminiMcpConfig,
62
- configKind: 'json'
63
- }],
64
- ['antigravity', {
65
- label: 'Antigravity',
66
- defaultConfigPath: defaultAntigravityConfigPath,
67
- buildSnippet: antigravityJsonSnippet,
68
- writeConfig: mergeAntigravityMcpConfig,
69
- configKind: 'json'
70
- }],
71
- ['antigravity-ide', {
72
- label: 'Antigravity IDE',
73
- defaultConfigPath: defaultAntigravityIdeConfigPath,
74
- buildSnippet: antigravityIdeJsonSnippet,
75
- writeConfig: mergeAntigravityIdeMcpConfig,
76
- configKind: 'json'
77
- }],
78
- ['antigravity2', {
79
- label: 'Antigravity 2.0',
80
- defaultConfigPath: defaultAntigravity2ConfigPath,
81
- buildSnippet: antigravity2JsonSnippet,
82
- writeConfig: mergeAntigravity2McpConfig,
83
- configKind: 'json'
84
- }],
85
- ['antigravity-cli', {
86
- label: 'Antigravity CLI',
87
- defaultConfigPath: defaultAntigravityCliConfigPath,
88
- buildSnippet: antigravityCliJsonSnippet,
89
- writeConfig: mergeAntigravityCliMcpConfig,
90
- configKind: 'json'
91
- }],
92
- ['windsurf', {
93
- label: 'Windsurf',
94
- defaultConfigPath: defaultWindsurfConfigPath,
95
- buildSnippet: windsurfJsonSnippet,
96
- writeConfig: mergeWindsurfMcpConfig,
97
- configKind: 'json'
98
- }],
99
- ['cline', {
100
- label: 'Cline',
101
- defaultConfigPath: defaultClineConfigPath,
102
- buildSnippet: clineJsonSnippet,
103
- writeConfig: mergeClineMcpConfig,
104
- configKind: 'json'
105
- }],
106
- ['continue', {
107
- label: 'Continue',
108
- defaultConfigPath: defaultContinueConfigPath,
109
- buildSnippet: continueJsonSnippet,
110
- writeConfig: mergeContinueMcpConfig,
111
- configKind: 'json'
112
- }],
113
- ['claude-desktop', {
114
- label: 'Claude Desktop',
115
- defaultConfigPath: defaultClaudeConfigPath,
116
- buildSnippet: claudeJsonSnippet,
117
- writeConfig: mergeClaudeMcpConfig,
118
- configKind: 'json'
119
- }],
120
- ['openclaw', {
121
- label: 'OpenClaw',
122
- defaultConfigPath: defaultOpenclawConfigPath,
123
- buildSnippet: openclawJsonSnippet,
124
- writeConfig: mergeOpenclawMcpConfig,
125
- configKind: 'json'
126
- }],
127
- ['kiro', {
128
- label: 'Kiro',
129
- defaultConfigPath: defaultKiroConfigPath,
130
- buildSnippet: kiroJsonSnippet,
131
- writeConfig: mergeKiroMcpConfig,
132
- configKind: 'json'
133
- }],
134
- ['zed', {
135
- label: 'Zed',
136
- defaultConfigPath: defaultZedConfigPath,
137
- buildSnippet: zedJsonSnippet,
138
- writeConfig: mergeZedMcpConfig,
139
- configKind: 'json'
140
- }],
141
- ['jetbrains', {
142
- label: 'JetBrains',
143
- defaultConfigPath: defaultJetbrainsConfigPath,
144
- buildSnippet: jetbrainsJsonSnippet,
145
- writeConfig: mergeJetbrainsMcpConfig,
146
- configKind: 'json'
147
- }],
148
- ['opencode', {
149
- label: 'OpenCode',
150
- defaultConfigPath: defaultOpencodeConfigPath,
151
- buildSnippet: opencodeJsonSnippet,
152
- writeConfig: mergeOpencodeMcpConfig,
153
- configKind: 'json'
154
- }],
155
- ['hermes', {
156
- label: 'Hermes',
157
- defaultConfigPath: defaultHermesConfigPath,
158
- buildSnippet: hermesYamlSnippet,
159
- writeConfig: mergeHermesMcpConfig,
160
- configKind: 'yaml'
161
- }],
162
- ['qwen', {
163
- label: 'Qwen',
164
- defaultConfigPath: defaultQwenConfigPath,
165
- buildSnippet: qwenJsonSnippet,
166
- writeConfig: mergeQwenMcpConfig,
167
- configKind: 'json'
168
- }],
169
- ['trae', {
170
- label: 'Trae',
171
- defaultConfigPath: defaultTraeConfigPath,
172
- buildSnippet: traeJsonSnippet,
173
- writeConfig: mergeTraeMcpConfig,
174
- configKind: 'json'
175
- }],
176
- ['trae-solo', {
177
- label: 'Trae Solo',
178
- defaultConfigPath: defaultTraeSoloConfigPath,
179
- buildSnippet: traeSoloJsonSnippet,
180
- writeConfig: mergeTraeSoloMcpConfig,
181
- configKind: 'json'
182
- }],
183
- ['claude-code', {
184
- label: 'Claude Code',
185
- defaultConfigPath: defaultClaudecodeConfigPath,
186
- buildSnippet: claudecodeJsonSnippet,
187
- writeConfig: mergeClaudecodeMcpConfig,
188
- configKind: 'json'
189
- }]
190
- ]);
191
-
192
- const SETUP_CLIENT_ALIASES = new Map([
193
- ['codex', 'codex'],
194
- ['cursor', 'cursor'],
195
- ['copilot', 'copilot-cli'],
196
- ['copilot-cli', 'copilot-cli'],
197
- ['gemini', 'gemini-cli'],
198
- ['gemini-cli', 'gemini-cli'],
199
- ['antigravity', 'antigravity'],
200
- ['antigravity-ide', 'antigravity-ide'],
201
- ['antigravity2', 'antigravity2'],
202
- ['antigravity-cli', 'antigravity-cli'],
203
- ['windsurf', 'windsurf'],
204
- ['cline', 'cline'],
205
- ['continue', 'continue'],
206
- ['claude', 'claude-desktop'],
207
- ['claude-desktop', 'claude-desktop'],
208
- ['openclaw', 'openclaw'],
209
- ['kiro', 'kiro'],
210
- ['zed', 'zed'],
211
- ['jetbrains', 'jetbrains'],
212
- ['opencode', 'opencode'],
213
- ['hermes', 'hermes'],
214
- ['qwen', 'qwen'],
215
- ['qwencli', 'qwen'],
216
- ['qwen-cli', 'qwen'],
217
- ['trae', 'trae'],
218
- ['traesolo', 'trae-solo'],
219
- ['trae-solo', 'trae-solo'],
220
- ['claude-code', 'claude-code'],
221
- ['claudecode', 'claude-code'],
222
- ['claude-cli', 'claude-code'],
223
- ['claudecode-cli', 'claude-code']
224
- ]);
225
-
226
- class UsageError extends Error {
227
- constructor(message) {
228
- super(message);
229
- this.name = 'UsageError';
230
- }
231
- }
1
+ import {
2
+ CLI_VERSION,
3
+ COMMAND_NAME
4
+ } from './core/constants.js';
5
+ import {
6
+ authCommand,
7
+ loginCommand,
8
+ tokenCommand
9
+ } from './commands/auth.js';
10
+ import {
11
+ discoveryCommand,
12
+ doctorCommand,
13
+ smokeCommand,
14
+ statusCommand
15
+ } from './commands/diagnostics.js';
16
+ import { mcpCommand } from './commands/mcp.js';
17
+ import { profileCommand } from './commands/profile.js';
18
+ import { setupCommand } from './commands/setup.js';
19
+ import { updateCommand } from './commands/update.js';
20
+ import { envCommand, writePrivacy } from './config/env.js';
21
+ import { UsageError } from './core/errors.js';
22
+ import { writeHelp } from './ui/help.js';
23
+ import { defaultIo, writeLine } from './core/io.js';
232
24
 
233
25
  export async function run(args, io = defaultIo()) {
234
26
  try {
@@ -310,3768 +102,3 @@ export async function run(args, io = defaultIo()) {
310
102
  }
311
103
  }
312
104
 
313
- function defaultIo() {
314
- return {
315
- env: process.env,
316
- stdin: process.stdin,
317
- stdout: process.stdout,
318
- stderr: process.stderr,
319
- fetch: globalThis.fetch,
320
- spawn
321
- };
322
- }
323
-
324
- function writeHelp(io) {
325
- writeLine(io.stdout, `======================================================================`);
326
- writeLine(io.stdout, ` 🧠 ${PRODUCT_NAME} CLI (Version ${CLI_VERSION}) — Cloud Memory Orchestration Utility`);
327
- writeLine(io.stdout, `======================================================================`);
328
- writeLine(io.stdout, `Official package: ${PACKAGE_NAME} | Legacy command: ${LEGACY_COMMAND_NAME}`);
329
- writeLine(io.stdout, '');
330
- writeLine(io.stdout, '💡 CORE ONBOARDING & SETUP COMMANDS:');
331
- writeLine(io.stdout, ` ${COMMAND_NAME} setup --all [--write] [--profile]`);
332
- writeLine(io.stdout, ` Auto-detects all local client installations (Cursor, VS Code, Continue, Trae, etc.).`);
333
- writeLine(io.stdout, ` Merges XMemo MCP configs. Pass --profile to auto-inject workspace prompt rules.`);
334
- writeLine(io.stdout, ` *Dry-run by default unless --write (or --yes/-y) is specified for safety.*`);
335
- writeLine(io.stdout, '');
336
- writeLine(io.stdout, ` ${COMMAND_NAME} setup <client-id> [--url <url>] [--no-profile] [--json]`);
337
- writeLine(io.stdout, ` Runs interactive setup wizard for a single client (e.g. cursor, gemini, antigravity).`);
338
- writeLine(io.stdout, ` Detects active workspace to auto-inject project-scoped instruction rules.`);
339
- writeLine(io.stdout, '');
340
- writeLine(io.stdout, ` ${COMMAND_NAME} login [--from-stdin] [--base-url <url>]`);
341
- writeLine(io.stdout, ` Starts secure OAuth2 browser-based device login flow to register the CLI.`);
342
- writeLine(io.stdout, '');
343
- writeLine(io.stdout, '🛡️ DIAGNOSTICS & SYSTEM AUDIT:');
344
- writeLine(io.stdout, ` ${COMMAND_NAME} doctor [--base-url <url>] [--json]`);
345
- writeLine(io.stdout, ` Performs structural diagnostics (Node version, Cloud connectivity, API compatibility, security).`);
346
- writeLine(io.stdout, '');
347
- writeLine(io.stdout, ` ${COMMAND_NAME} status [--url <url>] [--json]`);
348
- writeLine(io.stdout, ` Probes and audits XMemo core service endpoints, readiness states, and network health.`);
349
- writeLine(io.stdout, '');
350
- writeLine(io.stdout, '📋 MCP & CREDENTIAL MANAGEMENT:');
351
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
352
- writeLine(io.stdout, ` Lists all natively supported client integrations and configurations.`);
353
- writeLine(io.stdout, '');
354
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <client-id> [--base-url <url>] [--json]`);
355
- writeLine(io.stdout, ` Generates and outputs raw MCP config snippet templates without writing to files.`);
356
- writeLine(io.stdout, '');
357
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <client-id> [--write] [--config <path>]`);
358
- writeLine(io.stdout, ` Directly adds XMemo MCP server config snippet to the specified client settings file.`);
359
- writeLine(io.stdout, '');
360
- writeLine(io.stdout, ` ${COMMAND_NAME} profile install <client-id> [--target <path>] [--dry-run]`);
361
- writeLine(io.stdout, ` Injects/updates instruction rules prompt in target workspace rules files (Cursor/Gemini).`);
362
- writeLine(io.stdout, '');
363
- writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify] | ${COMMAND_NAME} token add --from-stdin`);
364
- writeLine(io.stdout, ` Checks local static credential states or manually saves XMEMO_KEY for key-auth fallbacks.`);
365
- writeLine(io.stdout, '');
366
- writeLine(io.stdout, '🔐 SECURITY & PRIVACY BY DESIGN:');
367
- writeLine(io.stdout, ' - ZERO Telemetry: We never collect private workspace data or usage metrics.');
368
- writeLine(io.stdout, ' - Git Protection: API tokens are kept securely in system environment variables (XMEMO_KEY)');
369
- writeLine(io.stdout, ' or in user-scoped credentials.json file. They are never written to project configs.');
370
- writeLine(io.stdout, ' - AST Merge Safety: Config writes only touch and append the XMemo keys, preserving all other servers.');
371
- writeLine(io.stdout, '======================================================================');
372
- }
373
-
374
- async function updateCommand(args, io) {
375
- const outputJson = hasFlag(args, '--json');
376
- const dryRun = hasFlag(args, '--dry-run');
377
- const npmCommand = npmExecutable();
378
- const npmArgs = ['install', '-g', `${PACKAGE_NAME}@latest`];
379
- const report = {
380
- package: PACKAGE_NAME,
381
- command: [npmCommand, ...npmArgs],
382
- dryRun,
383
- tokenSent: false,
384
- projectFilesModified: false
385
- };
386
-
387
- const commandToDisplay = report.command.map(c => c === 'npm.cmd' ? 'npm' : c).join(' ');
388
-
389
- if (dryRun) {
390
- if (outputJson) {
391
- writeLine(io.stdout, JSON.stringify(report, null, 2));
392
- } else {
393
- writeLine(io.stdout, `Update command: ${commandToDisplay}`);
394
- writeLine(io.stdout, 'Dry run only; no changes made.');
395
- }
396
- return 0;
397
- }
398
-
399
- if (!outputJson) {
400
- writeLine(io.stdout, `Updating ${PACKAGE_NAME} to the latest version...`);
401
- writeLine(io.stdout, `Running: ${commandToDisplay}`);
402
- }
403
- const result = await runProcess(npmCommand, npmArgs, io, { stream: !outputJson });
404
- report.exitCode = result.code;
405
- report.completed = result.code === 0;
406
-
407
- if (outputJson) {
408
- writeLine(io.stdout, JSON.stringify(report, null, 2));
409
- }
410
- if (result.code !== 0) {
411
- const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
412
- throw new UsageError(`Update failed: ${detail}`);
413
- }
414
- if (!outputJson) {
415
- writeLine(io.stdout, `Update complete. Run \`${COMMAND_NAME} --version\` to confirm.`);
416
- }
417
- return 0;
418
- }
419
-
420
- async function doctorCommand(args, io) {
421
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
422
- const outputJson = hasFlag(args, '--json');
423
- const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
424
- const discoveryUrl = endpointUrl(baseUrl, '/.well-known/agent-discovery.json');
425
- const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
426
- ensureDiscoveryService(discovery, discoveryUrl);
427
-
428
- const rootVersion = await bestEffortRootVersion(discovery, timeoutMs, io);
429
- const mcpUrl = discoveryMcpUrl(discovery, baseUrl);
430
- const checks = [
431
- { name: 'node_version', ok: Number.parseInt(process.versions.node.split('.')[0], 10) >= 20, detail: process.versions.node },
432
- { name: 'discovery_reachable', ok: true, detail: discoveryUrl },
433
- { name: 'mcp_url_present', ok: Boolean(mcpUrl), detail: mcpUrl ?? 'missing' },
434
- { name: 'no_remote_code_execution', ok: booleanValue(discovery, ['security', 'no_remote_code_execution']) === true, detail: String(booleanValue(discovery, ['security', 'no_remote_code_execution'])) },
435
- {
436
- name: 'token_not_in_discovery',
437
- ok: booleanValue(discovery, ['security', 'token_in_discovery']) === false && booleanValue(discovery, ['auth', 'token_in_discovery']) === false,
438
- detail: `security=${booleanValue(discovery, ['security', 'token_in_discovery'])} auth=${booleanValue(discovery, ['auth', 'token_in_discovery'])}`
439
- },
440
- {
441
- name: 'service_version_compatible',
442
- ok: rootVersion.version ? sameMajorMinor(CLI_VERSION, rootVersion.version) : true,
443
- detail: rootVersion.version ? `service=${rootVersion.version} cli=${CLI_VERSION}` : `service version unavailable${rootVersion.error ? `: ${rootVersion.error}` : ''}`
444
- }
445
- ];
446
- const report = {
447
- ok: checks.every((check) => check.ok),
448
- cli: { package: PACKAGE_NAME, version: CLI_VERSION, node: process.versions.node },
449
- discovery: {
450
- url: discoveryUrl,
451
- schemaVersion: stringValue(discovery, ['schema_version']),
452
- protocol: stringValue(discovery, ['protocol']),
453
- service: stringValue(discovery, ['service']),
454
- serviceVersion: rootVersion.version ?? null,
455
- mcpUrl,
456
- supportedClients: agentDiscoveryClientIds(discovery)
457
- },
458
- checks
459
- };
460
-
461
- if (outputJson) {
462
- writeLine(io.stdout, JSON.stringify(report, null, 2));
463
- return report.ok ? 0 : 1;
464
- }
465
-
466
- writeLine(io.stdout, `${PRODUCT_NAME} CLI ${CLI_VERSION}`);
467
- writeLine(io.stdout, `Discovery: ${discoveryUrl}`);
468
- writeLine(io.stdout, `MCP: ${mcpUrl ?? 'missing'}`);
469
- if (rootVersion.version) {
470
- writeLine(io.stdout, `Service version: ${rootVersion.version}`);
471
- }
472
- writeLine(io.stdout, `Supported clients: ${report.discovery.supportedClients.join(', ') || 'unknown'}`);
473
- for (const check of checks) {
474
- writeLine(io.stdout, `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.detail}`);
475
- }
476
- return report.ok ? 0 : 1;
477
- }
478
-
479
- async function discoveryCommand(args, io) {
480
- const subcommand = args[0] ?? 'help';
481
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
482
- writeLine(io.stdout, 'Discovery commands:');
483
- writeLine(io.stdout, ` ${COMMAND_NAME} discovery show [--base-url <https://api.example.com>] [--json]`);
484
- return 0;
485
- }
486
- if (subcommand !== 'show') {
487
- throw new UsageError(`Unknown discovery command: ${subcommand}`);
488
- }
489
-
490
- const baseUrl = normalizeBaseUrl(baseUrlOption(args.slice(1), io.env));
491
- const outputJson = hasFlag(args, '--json');
492
- const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
493
- const discoveryUrl = endpointUrl(baseUrl, '/.well-known/agent-discovery.json');
494
- const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
495
- ensureDiscoveryService(discovery, discoveryUrl);
496
-
497
- if (outputJson) {
498
- writeLine(io.stdout, JSON.stringify(discovery, null, 2));
499
- return 0;
500
- }
501
-
502
- writeLine(io.stdout, `${stringValue(discovery, ['name']) ?? PRODUCT_NAME} discovery`);
503
- writeLine(io.stdout, `URL: ${discoveryUrl}`);
504
- writeLine(io.stdout, `Protocol: ${stringValue(discovery, ['protocol']) ?? 'unknown'}`);
505
- writeLine(io.stdout, `MCP: ${discoveryMcpUrl(discovery, baseUrl) ?? 'missing'}`);
506
- writeLine(io.stdout, `Docs: ${stringValue(discovery, ['urls', 'docs']) ?? 'unknown'}`);
507
- writeLine(io.stdout, `Clients: ${agentDiscoveryClientIds(discovery).join(', ') || 'unknown'}`);
508
- writeLine(io.stdout, 'Security: read-only discovery; tokens are not returned; remote code execution is not advertised.');
509
- return 0;
510
- }
511
-
512
- async function statusCommand(args, io) {
513
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
514
- const outputJson = hasFlag(args, '--json');
515
- const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
516
- const endpoints = [
517
- endpointUrl(baseUrl, '/.well-known/memory-os.json'),
518
- endpointUrl(baseUrl, '/health'),
519
- endpointUrl(baseUrl, '/ready')
520
- ];
521
-
522
- const probes = [];
523
- for (const url of endpoints) {
524
- probes.push(await probe(url, timeoutMs, io));
525
- }
526
-
527
- const result = {
528
- ok: probes.some((item) => item.ok),
529
- baseUrl,
530
- privacy: {
531
- telemetry: false,
532
- tokenSent: false,
533
- tokenSource: 'not-used-by-status'
534
- },
535
- probes
536
- };
537
-
538
- if (outputJson) {
539
- writeLine(io.stdout, JSON.stringify(result, null, 2));
540
- return result.ok ? 0 : 1;
541
- }
542
-
543
- writeLine(io.stdout, `${PRODUCT_NAME} status for ${baseUrl}`);
544
- writeLine(io.stdout, 'Privacy: telemetry disabled; no token sent.');
545
- for (const item of probes) {
546
- if (item.ok) {
547
- writeLine(io.stdout, ` OK ${item.status} ${item.url}`);
548
- } else {
549
- writeLine(io.stdout, ` FAIL ${item.status ?? 'ERR'} ${item.url} ${item.error ?? ''}`.trimEnd());
550
- }
551
- }
552
-
553
- return result.ok ? 0 : 1;
554
- }
555
-
556
- async function setupCommand(args, io) {
557
- const positionalClientId = positionalClientArg(args);
558
- const optionArgs = positionalClientId ? args.slice(1) : args;
559
- const baseUrl = normalizeBaseUrl(baseUrlOption(optionArgs, io.env));
560
- const outputJson = hasFlag(optionArgs, '--json');
561
- const shortClientSetup = Boolean(positionalClientId);
562
- const setupAll = hasFlag(optionArgs, '--all');
563
-
564
- let clientId = null;
565
- try {
566
- clientId = normalizeSetupClientId(positionalClientId ?? optionValue(optionArgs, '--client'));
567
- } catch (error) {
568
- if (!setupAll) {
569
- throw error;
570
- }
571
- }
572
-
573
- if (setupAll && clientId) {
574
- throw new UsageError('Cannot specify both --all and a specific client.');
575
- }
576
-
577
- const dryRun = hasFlag(optionArgs, '--dry-run') || hasFlag(optionArgs, '--preview');
578
- const writeConfig = !dryRun && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes') || shortClientSetup || (setupAll && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes'))));
579
- const timeoutMs = parsePositiveInteger(optionValue(optionArgs, '--timeout-ms') ?? '5000', '--timeout-ms');
580
-
581
- if (writeConfig && !clientId && !setupAll) {
582
- throw new UsageError(`Setup --write requires --client <${supportedSetupClientIds().join('|')}> or --all so the CLI never writes broad config implicitly.`);
583
- }
584
-
585
- const discoveryUrl = endpointUrl(baseUrl, '/.well-known/memory-os.json');
586
- const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
587
- ensureDiscoveryService(discovery, discoveryUrl);
588
-
589
- const statusUrl = stringValue(discovery, ['urls', 'onboarding_status'])
590
- ?? stringValue(discovery, ['onboarding_status_url'])
591
- ?? endpointUrl(baseUrl, '/v1/onboarding/status');
592
- const status = await fetchJson(statusUrl, timeoutMs, io);
593
- const setupPlan = buildSetupPlan({ baseUrl, discoveryUrl, statusUrl, discovery, status });
594
-
595
- if (setupAll) {
596
- setupPlan.detectedClients = [];
597
- const scanIds = ['codex', 'cursor', 'copilot-cli', 'gemini-cli', 'antigravity', 'antigravity-ide', 'antigravity2', 'antigravity-cli', 'windsurf', 'cline', 'continue', 'claude-desktop', 'qwen', 'opencode', 'trae', 'trae-solo'];
598
- for (const scanId of scanIds) {
599
- const detection = await detectClient(scanId, io.env);
600
- if (detection.detected) {
601
- let clientPlan;
602
- if (scanId === 'copilot-cli') {
603
- const proxyPort = parsePositiveInteger(optionValue(optionArgs, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
604
- clientPlan = copilotSetupPlan(setupPlan.mcpUrl, proxyPort, io.env);
605
- clientPlan.configPath = detection.path;
606
- if (writeConfig) {
607
- await mergeCopilotMcpConfig(clientPlan.configPath, clientPlan.proxyUrl);
608
- clientPlan.written = true;
609
- }
610
- } else {
611
- const client = MCP_CLIENTS.get(scanId);
612
- const identity = writeConfig ? await agentIdentity(scanId, io.env) : envReferenceIdentity(scanId);
613
- clientPlan = clientSetupPlan(scanId, client, setupPlan.mcpUrl, io.env, identity);
614
- clientPlan.configPath = detection.path;
615
- if (writeConfig) {
616
- await client.writeConfig(clientPlan.configPath, setupPlan.mcpUrl, identity);
617
- clientPlan.written = true;
618
- if (profileClientConfig(scanId)) {
619
- const installProfile = hasFlag(optionArgs, '--yes') || hasFlag(optionArgs, '--profile');
620
- if (installProfile) {
621
- const profileTarget = defaultProfileTarget(scanId, io.env);
622
- const profileResult = await profileInstallResult(scanId, profileTarget, { write: true });
623
- clientPlan.behaviorProfile = profileResult;
624
- if (scanId === 'codex') {
625
- clientPlan.codexProfile = profileResult;
626
- }
627
- }
628
- }
629
- }
630
- }
631
- setupPlan.detectedClients.push(clientPlan);
632
- }
633
- }
634
- } else if (clientId) {
635
- if (clientId === 'copilot-cli') {
636
- const proxyPort = parsePositiveInteger(optionValue(optionArgs, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
637
- setupPlan.selectedClient = copilotSetupPlan(setupPlan.mcpUrl, proxyPort, io.env);
638
- if (writeConfig) {
639
- await mergeCopilotMcpConfig(setupPlan.selectedClient.configPath, setupPlan.selectedClient.proxyUrl);
640
- setupPlan.selectedClient.written = true;
641
- }
642
- } else {
643
- const client = MCP_CLIENTS.get(clientId);
644
- if (!client) {
645
- throw new UsageError(`Unsupported MCP client: ${clientId}. Supported clients: ${supportedSetupClientIds().join(', ')}.`);
646
- }
647
-
648
- const identity = writeConfig ? await agentIdentity(clientId, io.env) : envReferenceIdentity(clientId);
649
- setupPlan.selectedClient = clientSetupPlan(clientId, client, setupPlan.mcpUrl, io.env, identity);
650
- if (writeConfig) {
651
- await client.writeConfig(setupPlan.selectedClient.configPath, setupPlan.mcpUrl, identity);
652
- setupPlan.selectedClient.written = true;
653
- }
654
-
655
- if (shortClientSetup && profileClientConfig(clientId)) {
656
- const profileTarget = optionValue(optionArgs, '--profile-target')
657
- ?? optionValue(optionArgs, '--target')
658
- ?? defaultProfileTarget(clientId, io.env);
659
- let installProfile = false;
660
- let prompted = false;
661
- let skipped = false;
662
- if (hasFlag(optionArgs, '--no-profile')) {
663
- skipped = true;
664
- } else if (dryRun) {
665
- installProfile = false;
666
- } else if (writeConfig) {
667
- installProfile = outputJson || hasFlag(optionArgs, '--yes') || hasFlag(optionArgs, '--profile');
668
- if (!installProfile && !outputJson) {
669
- prompted = true;
670
- installProfile = await confirmProfileInstall(clientId, profileTarget, io);
671
- }
672
- }
673
- const profileResult = await profileInstallResult(clientId, profileTarget, { write: installProfile });
674
- profileResult.prompted = prompted;
675
- profileResult.accepted = installProfile;
676
- profileResult.skipped = skipped;
677
- setupPlan.selectedClient.behaviorProfile = profileResult;
678
- if (clientId === 'codex') {
679
- setupPlan.selectedClient.codexProfile = profileResult;
680
- }
681
- }
682
- }
683
- }
684
-
685
- if (outputJson) {
686
- writeLine(io.stdout, JSON.stringify(setupPlan, null, 2));
687
- return 0;
688
- }
689
-
690
- writeSetupSummary(setupPlan, io);
691
- return 0;
692
- }
693
-
694
- async function profileCommand(args, io) {
695
- const subcommand = args[0] ?? 'help';
696
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
697
- writeLine(io.stdout, 'Profile commands:');
698
- writeLine(io.stdout, ` ${COMMAND_NAME} profile install <codex|cursor|gemini|antigravity|qwen|opencode> [--target <path>] [--dry-run|--json]`);
699
- writeLine(io.stdout, ` ${COMMAND_NAME} profile status <codex|cursor|gemini|antigravity|qwen|opencode> [--target <path>] [--json]`);
700
- writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall <codex|cursor|gemini|antigravity|qwen|opencode> [--target <path>] [--json]`);
701
- writeLine(io.stdout, '');
702
- writeLine(io.stdout, 'Profile installs are marker-scoped and never write token values.');
703
- return 0;
704
- }
705
-
706
- const clientId = normalizeSetupClientId(args[1]);
707
- if (!profileClientConfig(clientId)) {
708
- throw new UsageError(`Unsupported profile client: ${args[1] ?? 'missing'}. Supported clients: ${supportedProfileClientIds().join(', ')}.`);
709
- }
710
-
711
- const optionArgs = args.slice(2);
712
- const outputJson = hasFlag(optionArgs, '--json');
713
- const targetPath = optionValue(optionArgs, '--target') ?? defaultProfileTarget(clientId, io.env);
714
- let result;
715
-
716
- if (subcommand === 'install') {
717
- result = await profileInstallResult(clientId, targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
718
- } else if (subcommand === 'status') {
719
- result = await profileStatusResult(clientId, targetPath);
720
- } else if (subcommand === 'uninstall') {
721
- result = await profileUninstallResult(clientId, targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
722
- } else {
723
- throw new UsageError(`Unknown profile command: ${subcommand}`);
724
- }
725
-
726
- if (outputJson) {
727
- writeLine(io.stdout, JSON.stringify(result, null, 2));
728
- return 0;
729
- }
730
-
731
- writeProfileResult(subcommand, result, io);
732
- return 0;
733
- }
734
-
735
- async function loginCommand(args, io) {
736
- const outputJson = hasFlag(args, '--json');
737
- const fromStdin = hasFlag(args, '--from-stdin') || hasFlag(args, '--token-stdin');
738
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
739
- const httpTimeoutMs = parsePositiveInteger(optionValue(args, '--http-timeout-ms') ?? '30000', '--http-timeout-ms');
740
- const loginTimeoutOption = optionValue(args, '--timeout-ms');
741
- const pollOnce = hasFlag(args, '--poll-once');
742
-
743
- if (fromStdin) {
744
- const result = await storeTokenFromStdin(io, { source: 'stdin' });
745
- if (outputJson) {
746
- writeLine(io.stdout, JSON.stringify(result, null, 2));
747
- } else {
748
- writeLine(io.stdout, `${PRODUCT_NAME} login complete.`);
749
- writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
750
- writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');
751
- }
752
- return 0;
753
- }
754
-
755
- const start = await startDeviceLogin(baseUrl, httpTimeoutMs, io);
756
- const loginTimeoutMs = loginTimeoutOption
757
- ? parsePositiveInteger(loginTimeoutOption, '--timeout-ms')
758
- : Math.max(1000, start.expiresIn * 1000);
759
- if (!outputJson) {
760
- writeLine(io.stdout, `${PRODUCT_NAME} device login`);
761
- writeLine(io.stdout, `Open: ${start.verificationUriComplete ?? start.verificationUri}`);
762
- if (start.userCode) {
763
- writeLine(io.stdout, `Code: ${start.userCode}`);
764
- }
765
- writeLine(io.stdout, 'Waiting for authorization...');
766
- }
767
-
768
- const token = await pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, { pollOnce });
769
- const result = await storeTokenValue(token.accessToken, { source: 'device-login', account: token.account }, io.env);
770
- const payload = {
771
- ...result,
772
- baseUrl,
773
- verificationUri: start.verificationUri,
774
- account: token.account,
775
- deviceLogin: true
776
- };
777
-
778
- if (outputJson) {
779
- writeLine(io.stdout, JSON.stringify(payload, null, 2));
780
- } else {
781
- writeLine(io.stdout, 'Login complete. Token stored securely in the user-scoped XMemo CLI config directory.');
782
- if (token.account) {
783
- writeLine(io.stdout, `Signed in as: ${formatAccount(token.account)}`);
784
- }
785
- writeLine(io.stdout, `Credential path: ${result.credentialPath}`);
786
- writeLine(io.stdout, 'No extra token configuration is required.');
787
- writeLine(io.stdout, `Optional check: ${COMMAND_NAME} token status --verify`);
788
- }
789
- return 0;
790
- }
791
-
792
- async function authCommand(args, io) {
793
- const subcommand = args[0] ?? 'help';
794
-
795
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
796
- writeLine(io.stdout, 'Auth commands:');
797
- writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
798
- writeLine(io.stdout, '');
799
- writeLine(io.stdout, `Use \`${COMMAND_NAME} login\` to sign in and \`${COMMAND_NAME} token add --from-stdin\` to store an existing token.`);
800
- return 0;
801
- }
802
-
803
- if (subcommand === 'status') {
804
- return await credentialStatusCommand(args.slice(1), io, { mode: 'auth' });
805
- }
806
-
807
- throw new UsageError(`Unknown auth command: ${subcommand}`);
808
- }
809
-
810
- async function tokenCommand(args, io) {
811
- const subcommand = args[0] ?? 'help';
812
-
813
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
814
- writeLine(io.stdout, 'Token commands:');
815
- writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify]`);
816
- writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
817
- writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
818
- writeLine(io.stdout, '');
819
- writeLine(io.stdout, `${COMMAND_NAME} login is the recommended personal-user path.`);
820
- writeLine(io.stdout, `${COMMAND_NAME} token add --from-stdin stores a token in the user-scoped XMemo CLI config directory.`);
821
- return 0;
822
- }
823
-
824
- if (subcommand === 'status') {
825
- return await credentialStatusCommand(args.slice(1), io, { mode: 'token' });
826
- }
827
-
828
- if (subcommand === 'add') {
829
- if (!hasFlag(args, '--from-stdin')) {
830
- throw new UsageError('Refusing command-line token input. Pipe the token through stdin with --from-stdin.');
831
- }
832
- const result = await storeTokenFromStdin(io, { source: 'token-add' });
833
- if (hasFlag(args, '--json')) {
834
- writeLine(io.stdout, JSON.stringify(result, null, 2));
835
- } else {
836
- writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
837
- writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');
838
- }
839
- return 0;
840
- }
841
-
842
- if (subcommand === 'set') {
843
- if (!hasFlag(args, '--from-stdin')) {
844
- throw new UsageError('Refusing command-line token input. Pipe the token through stdin with --from-stdin.');
845
- }
846
- const token = (await readAll(io.stdin)).trim();
847
- validateToken(token);
848
- if (!hasFlag(args, '--allow-plaintext')) {
849
- writeLine(io.stderr, 'Token was read from stdin but was not stored.');
850
- writeLine(io.stderr, 'Enterprise default refuses plaintext token storage without --allow-plaintext.');
851
- writeLine(io.stderr, `Preferred personal-user path: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin.`);
852
- return 2;
853
- }
854
-
855
- const result = await storeTokenValue(token, { source: 'token-set' }, io.env);
856
- writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
857
- writeLine(io.stdout, 'Token value was not printed. Do not commit this file.');
858
- return 0;
859
- }
860
-
861
- throw new UsageError(`Unknown token command: ${subcommand}`);
862
- }
863
-
864
- async function credentialStatusCommand(args, io, { mode }) {
865
- const outputJson = hasFlag(args, '--json');
866
- const verify = hasFlag(args, '--verify');
867
- const credential = await readStoredCredential(io.env);
868
- const environmentToken = io.env[TOKEN_ENV_VAR] ?? io.env[LEGACY_TOKEN_ENV_VAR] ?? '';
869
- const hasEnvironmentToken = Boolean(environmentToken);
870
- const hasUserCredential = Boolean(credential.token);
871
- const tokenSource = hasEnvironmentToken ? 'environment' : hasUserCredential ? 'user-credential-file' : 'missing';
872
- const report = {
873
- loggedIn: hasEnvironmentToken || hasUserCredential,
874
- tokenSource,
875
- environmentToken: {
876
- present: hasEnvironmentToken,
877
- variable: hasEnvironmentToken && io.env[TOKEN_ENV_VAR] ? TOKEN_ENV_VAR : hasEnvironmentToken ? LEGACY_TOKEN_ENV_VAR : TOKEN_ENV_VAR
878
- },
879
- userCredentialFile: {
880
- present: hasUserCredential,
881
- path: credential.path,
882
- storage: credential.storage ?? null
883
- },
884
- account: credential.account ?? null,
885
- privacy: {
886
- tokenPrinted: false,
887
- projectFilesModified: false
888
- }
889
- };
890
-
891
- if (verify) {
892
- const token = await resolveCredentialToken(io.env);
893
- if (!token) {
894
- if (outputJson) {
895
- writeLine(io.stdout, JSON.stringify({ ...report, verification: { ok: false, detail: 'no token found' } }, null, 2));
896
- } else {
897
- writeCredentialStatus(report, io, { mode });
898
- writeLine(io.stderr, `No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\`.`);
899
- }
900
- return 1;
901
- }
902
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
903
- const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '10000', '--timeout-ms');
904
- const verification = await verifyTokenWithMcp(baseUrl, token, timeoutMs, io);
905
- report.verification = verification;
906
- if (outputJson) {
907
- writeLine(io.stdout, JSON.stringify(report, null, 2));
908
- return verification.ok ? 0 : 1;
909
- }
910
- writeCredentialStatus(report, io, { mode });
911
- writeLine(io.stdout, `Remote token verification: ${verification.ok ? 'ok' : 'failed'} (${verification.detail})`);
912
- return verification.ok ? 0 : 1;
913
- }
914
-
915
- if (outputJson) {
916
- writeLine(io.stdout, JSON.stringify(report, null, 2));
917
- } else {
918
- writeCredentialStatus(report, io, { mode });
919
- }
920
- return report.loggedIn ? 0 : 1;
921
- }
922
-
923
- function writeCredentialStatus(report, io, { mode }) {
924
- if (mode === 'auth') {
925
- writeLine(io.stdout, `${PRODUCT_NAME} auth status`);
926
- writeLine(io.stdout, `Logged in: ${report.loggedIn ? 'yes' : 'no'}`);
927
- writeLine(io.stdout, `Credential source: ${report.tokenSource}`);
928
- if (report.account) {
929
- writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);
930
- }
931
- writeLine(io.stdout, report.loggedIn ? 'Credential is ready; token value remains hidden.' : `Run \`${COMMAND_NAME} login\` to sign in.`);
932
- return;
933
- }
934
- writeLine(io.stdout, `Environment token: ${report.environmentToken.present ? 'present' : 'missing'} (${report.environmentToken.variable})`);
935
- writeLine(io.stdout, `User credential file: ${report.userCredentialFile.present ? 'present' : 'missing'} (${report.userCredentialFile.path})`);
936
- if (report.account) {
937
- writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);
938
- }
939
- writeLine(io.stdout, report.loggedIn ? 'Credential is ready; token value remains hidden.' : `Run \`${COMMAND_NAME} login\` to sign in.`);
940
- }
941
-
942
- async function mcpCommand(args, io) {
943
- const subcommand = args[0] ?? 'help';
944
-
945
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
946
- writeLine(io.stdout, 'MCP commands:');
947
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
948
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|antigravity|generic> [--base-url <url>] [--json]`);
949
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}] [--base-url <url>]`);
950
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp profile codex [--json]`);
951
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>]`);
952
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>] --write [--config <path>]`);
953
- return 0;
954
- }
955
-
956
- if (subcommand === 'list') {
957
- if (hasFlag(args, '--json')) {
958
- writeLine(io.stdout, JSON.stringify(supportedMcpClients(), null, 2));
959
- return 0;
960
- }
961
-
962
- writeLine(io.stdout, 'Supported MCP clients:');
963
- for (const client of supportedMcpClients()) {
964
- writeLine(io.stdout, ` ${client.id.padEnd(8)} ${client.label} (${client.configKind})`);
965
- }
966
- writeLine(io.stdout, `Generated configs never embed token values; OAuth clients do not require ${TOKEN_ENV_VAR} in their config.`);
967
- return 0;
968
- }
969
-
970
- if (subcommand === 'config') {
971
- const clientId = optionValue(args, '--client') ?? args[1] ?? 'generic';
972
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
973
- const mcpUrl = endpointUrl(baseUrl, '/mcp');
974
- const useLocalProxy = clientId === 'copilot-cli' && !hasFlag(args, '--remote-env');
975
- const proxyPort = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
976
- const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`;
977
- const template = useLocalProxy
978
- ? mcpLocalProxyTemplate(clientId, proxyUrl)
979
- : mcpConfigTemplate(clientId, mcpUrl);
980
-
981
- if (hasFlag(args, '--json')) {
982
- writeLine(io.stdout, JSON.stringify(template, null, 2));
983
- return 0;
984
- }
985
-
986
- writeLine(io.stdout, `${PRODUCT_NAME} MCP config template for ${clientId}`);
987
- if (useLocalProxy) {
988
- writeLine(io.stdout, `Requires credential: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin`);
989
- writeLine(io.stdout, `Run local proxy: ${template.requiresLocalCommand}`);
990
- } else {
991
- if (template.requiresEnv?.length > 0) {
992
- writeLine(io.stdout, `Requires env: ${template.requiresEnv.join(', ')}`);
993
- } else if (template.authentication === 'oauth') {
994
- writeLine(io.stdout, 'Requires auth: complete the client MCP OAuth flow after setup.');
995
- }
996
- }
997
- if (typeof template.snippet === 'string') {
998
- writeLine(io.stdout, template.snippet.trimEnd());
999
- } else {
1000
- writeLine(io.stdout, JSON.stringify(template.snippet, null, 2));
1001
- }
1002
- if (template.optionalEnv?.includes(AGENT_INSTANCE_ENV_VAR)) {
1003
- writeLine(io.stdout, '');
1004
- writeLine(io.stdout, `${AGENT_INSTANCE_ENV_VAR} must be stable per local client install.`);
1005
- if (template.agentInstanceGeneration?.automaticCommand) {
1006
- writeLine(io.stdout, `Use ${template.agentInstanceGeneration.automaticCommand} to generate and persist it, or set it to a unique value such as xmemo-${clientId}-<uuid>.`);
1007
- } else {
1008
- writeLine(io.stdout, `Set it to a unique value such as xmemo-${clientId}-<uuid> and persist it outside git.`);
1009
- }
1010
- }
1011
- writeLine(io.stdout, 'Review the template before applying it. Token values are not included.');
1012
- return 0;
1013
- }
1014
-
1015
- if (subcommand === 'proxy') {
1016
- return await mcpProxyCommand(args.slice(1), io);
1017
- }
1018
-
1019
- if (subcommand === 'profile') {
1020
- const clientId = args[1] ?? 'codex';
1021
- if (clientId !== 'codex') {
1022
- throw new UsageError('Only the Codex memory behavior profile is available in this MCP-depth release.');
1023
- }
1024
-
1025
- const profile = codexMemoryProfile();
1026
- if (hasFlag(args, '--json')) {
1027
- writeLine(io.stdout, JSON.stringify(profile, null, 2));
1028
- return 0;
1029
- }
1030
-
1031
- writeCodexMemoryProfile(profile, io);
1032
- return 0;
1033
- }
1034
-
1035
- const target = args[1] ?? '';
1036
- const client = MCP_CLIENTS.get(target);
1037
-
1038
- if (subcommand !== 'add' || !client) {
1039
- throw new UsageError(`Supported MCP setup command: ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <url>]`);
1040
- }
1041
-
1042
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
1043
- const configPath = optionValue(args, '--config') ?? client.defaultConfigPath(io.env);
1044
- const mcpUrl = endpointUrl(baseUrl, '/mcp');
1045
-
1046
- if (hasFlag(args, '--json')) {
1047
- const identity = envReferenceIdentity(target);
1048
- const oauthClient = usesClientOAuth(target);
1049
- writeLine(io.stdout, JSON.stringify({
1050
- client: target,
1051
- label: client.label,
1052
- configKind: client.configKind,
1053
- configPath,
1054
- serverName: MCP_SERVER_NAME,
1055
- url: mcpUrl,
1056
- tokenEnvVar: oauthClient ? null : TOKEN_ENV_VAR,
1057
- authentication: oauthClient ? 'oauth' : 'env-bearer',
1058
- agentId: identity.agentId,
1059
- agentInstanceId: identity.agentInstanceId,
1060
- agentInstanceIdPath: identity.path,
1061
- agentInstanceGeneration: agentInstanceGenerationPolicy(target),
1062
- writesTokenValue: false
1063
- }, null, 2));
1064
- return 0;
1065
- }
1066
-
1067
- const identity = hasFlag(args, '--write') ? await agentIdentity(target, io.env) : envReferenceIdentity(target);
1068
- if (hasFlag(args, '--write')) {
1069
- await client.writeConfig(configPath, mcpUrl, identity);
1070
- writeLine(io.stdout, `Updated ${client.label} MCP config: ${configPath}`);
1071
- if (usesClientOAuth(target)) {
1072
- writeLine(io.stdout, `Token value was not written. ${client.label} will complete MCP OAuth on first use.`);
1073
- } else {
1074
- writeLine(io.stdout, `Token value was not written. ${client.label} will read ${TOKEN_ENV_VAR} from the environment.`);
1075
- }
1076
- writeLine(io.stdout, `Agent instance ID stored outside git: ${identity.path}`);
1077
- return 0;
1078
- }
1079
-
1080
- const snippet = client.buildSnippet(mcpUrl, identity);
1081
- writeLine(io.stdout, `Add this to your ${client.label} config (${configPath}):`);
1082
- writeLine(io.stdout, '');
1083
- writeLine(io.stdout, snippet.trimEnd());
1084
- writeLine(io.stdout, '');
1085
- if (usesClientOAuth(target)) {
1086
- writeLine(io.stdout, `Restart ${client.label} and complete its MCP OAuth flow. No token value is included here.`);
1087
- } else {
1088
- writeLine(io.stdout, `Set ${TOKEN_ENV_VAR} in your user environment or secret manager. The token value is not included here.`);
1089
- }
1090
- writeLine(io.stdout, `${AGENT_INSTANCE_ENV_VAR} must be stable per local ${client.label} install; run ${COMMAND_NAME} mcp add ${target} --write to generate it automatically.`);
1091
- return 0;
1092
- }
1093
-
1094
- async function mcpProxyCommand(args, io) {
1095
- const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
1096
- const mcpUrl = endpointUrl(baseUrl, '/mcp');
1097
- const host = optionValue(args, '--host') ?? DEFAULT_PROXY_HOST;
1098
- const port = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
1099
- const token = await resolveCredentialToken(io.env);
1100
- if (!token) {
1101
- throw new UsageError(`No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\` first.`);
1102
- }
1103
- validateToken(token);
1104
- const identity = await agentIdentity('copilot-cli', io.env);
1105
-
1106
- const server = http.createServer(async (request, response) => {
1107
- try {
1108
- await handleMcpProxyRequest({ request, response, mcpUrl, token, identity, io });
1109
- } catch (error) {
1110
- response.statusCode = 502;
1111
- response.setHeader('content-type', 'application/json');
1112
- response.end(JSON.stringify({ error: 'mcp_proxy_error', message: error.message }));
1113
- }
1114
- });
1115
-
1116
- await new Promise((resolve, reject) => {
1117
- server.once('error', reject);
1118
- server.listen(port, host, () => {
1119
- server.off('error', reject);
1120
- resolve();
1121
- });
1122
- });
1123
-
1124
- writeLine(io.stdout, `${PRODUCT_NAME} MCP proxy listening on http://${host}:${port}/mcp`);
1125
- writeLine(io.stdout, `Forwarding to ${mcpUrl}`);
1126
- writeLine(io.stdout, `Credential source: ${TOKEN_ENV_VAR} or ${credentialsPath(io.env)}`);
1127
- return 0;
1128
- }
1129
-
1130
- async function handleMcpProxyRequest({ request, response, mcpUrl, token, identity, io }) {
1131
- const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? `${DEFAULT_PROXY_HOST}:${DEFAULT_PROXY_PORT}`}`);
1132
- if (request.method !== 'POST' || requestUrl.pathname !== '/mcp') {
1133
- response.statusCode = 404;
1134
- response.setHeader('content-type', 'application/json');
1135
- response.end(JSON.stringify({ error: 'not_found' }));
1136
- return;
1137
- }
1138
-
1139
- const body = await readAll(request);
1140
- const upstreamHeaders = {
1141
- accept: String(request.headers.accept || 'application/json, text/event-stream'),
1142
- 'content-type': String(request.headers['content-type'] || 'application/json'),
1143
- authorization: `Bearer ${token}`,
1144
- [AGENT_ID_HEADER]: identity.agentId,
1145
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId,
1146
- 'user-agent': `XMemo-CLI-Proxy/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
1147
- };
1148
- const sessionId = request.headers['mcp-session-id'];
1149
- if (sessionId) {
1150
- upstreamHeaders['mcp-session-id'] = Array.isArray(sessionId) ? sessionId[0] : sessionId;
1151
- }
1152
-
1153
- const upstream = await io.fetch(mcpUrl, {
1154
- method: 'POST',
1155
- headers: upstreamHeaders,
1156
- body
1157
- });
1158
-
1159
- response.statusCode = upstream.status;
1160
- for (const header of ['content-type', 'mcp-session-id']) {
1161
- const value = upstream.headers.get(header);
1162
- if (value) {
1163
- response.setHeader(header, value);
1164
- }
1165
- }
1166
- const buffer = Buffer.from(await upstream.arrayBuffer());
1167
- response.end(buffer);
1168
- }
1169
-
1170
- async function smokeCommand(args, io) {
1171
- const clientId = optionValue(args, '--client');
1172
- const outputJson = hasFlag(args, '--json');
1173
- if (!clientId) {
1174
- throw new UsageError('Smoke requires --client codex for this MCP-depth release.');
1175
- }
1176
- if (clientId !== 'codex') {
1177
- throw new UsageError('Only Codex smoke checks are available in this MCP-depth release.');
1178
- }
1179
-
1180
- const configPath = optionValue(args, '--config') ?? defaultCodexConfigPath(io.env);
1181
- const report = await codexSmokeReport(configPath, io.env);
1182
-
1183
- if (outputJson) {
1184
- writeLine(io.stdout, JSON.stringify(report, null, 2));
1185
- return report.ok ? 0 : 1;
1186
- }
1187
-
1188
- writeLine(io.stdout, `${PRODUCT_NAME} Codex MCP smoke: ${report.ok ? 'ok' : 'failed'}`);
1189
- writeLine(io.stdout, `Config: ${report.configPath}`);
1190
- writeLine(io.stdout, `Token env: ${report.tokenEnvVar}`);
1191
- for (const check of report.checks) {
1192
- const status = check.ok ? 'OK' : check.required ? 'FAIL' : 'WARN';
1193
- writeLine(io.stdout, ` ${status} ${check.name}: ${check.detail}`);
1194
- }
1195
- return report.ok ? 0 : 1;
1196
- }
1197
-
1198
- function envCommand(args, io) {
1199
- const subcommand = args[0] ?? 'help';
1200
- if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
1201
- writeLine(io.stdout, 'Env commands:');
1202
- writeLine(io.stdout, ` ${COMMAND_NAME} env example [--shell bash|powershell|cmd] [--base-url <url>] [--json]`);
1203
- return 0;
1204
- }
1205
- if (subcommand !== 'example') {
1206
- throw new UsageError(`Unknown env command: ${subcommand}`);
1207
- }
1208
-
1209
- const baseUrl = normalizeBaseUrl(baseUrlOption(args.slice(1), io.env));
1210
- const outputJson = hasFlag(args, '--json');
1211
- const shell = optionValue(args, '--shell') ?? (process.platform === 'win32' ? 'powershell' : 'bash');
1212
- const placeholder = '<paste-token-from-your-secret-store>';
1213
- const payload = {
1214
- XMEMO_URL: baseUrl,
1215
- XMEMO_BASE_URL: baseUrl,
1216
- MEMORY_OS_URL: baseUrl,
1217
- MEMORY_OS_BASE_URL: baseUrl,
1218
- [TOKEN_ENV_VAR]: placeholder,
1219
- [AGENT_ID_ENV_VAR]: '<agent-family>',
1220
- [AGENT_INSTANCE_ENV_VAR]: '<stable-random-id-for-this-local-agent>'
1221
- };
1222
-
1223
- if (outputJson) {
1224
- writeLine(io.stdout, JSON.stringify(payload, null, 2));
1225
- return 0;
1226
- }
1227
-
1228
- if (shell === 'powershell') {
1229
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('XMEMO_URL', '${baseUrl}', 'User')`);
1230
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('XMEMO_BASE_URL', '${baseUrl}', 'User')`);
1231
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('MEMORY_OS_URL', '${baseUrl}', 'User')`);
1232
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('MEMORY_OS_BASE_URL', '${baseUrl}', 'User')`);
1233
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${TOKEN_ENV_VAR}', '${placeholder}', 'User')`);
1234
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${AGENT_ID_ENV_VAR}', '<agent-family>', 'User')`);
1235
- writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${AGENT_INSTANCE_ENV_VAR}', '<stable-random-id-for-this-local-agent>', 'User')`);
1236
- } else if (shell === 'cmd') {
1237
- writeLine(io.stdout, `setx XMEMO_URL "${baseUrl}"`);
1238
- writeLine(io.stdout, `setx XMEMO_BASE_URL "${baseUrl}"`);
1239
- writeLine(io.stdout, `setx MEMORY_OS_URL "${baseUrl}"`);
1240
- writeLine(io.stdout, `setx MEMORY_OS_BASE_URL "${baseUrl}"`);
1241
- writeLine(io.stdout, `setx ${TOKEN_ENV_VAR} "${placeholder}"`);
1242
- writeLine(io.stdout, `setx ${AGENT_ID_ENV_VAR} "<agent-family>"`);
1243
- writeLine(io.stdout, `setx ${AGENT_INSTANCE_ENV_VAR} "<stable-random-id-for-this-local-agent>"`);
1244
- } else {
1245
- writeLine(io.stdout, `export XMEMO_URL="${baseUrl}"`);
1246
- writeLine(io.stdout, `export XMEMO_BASE_URL="${baseUrl}"`);
1247
- writeLine(io.stdout, `export MEMORY_OS_URL="${baseUrl}"`);
1248
- writeLine(io.stdout, `export MEMORY_OS_BASE_URL="${baseUrl}"`);
1249
- writeLine(io.stdout, `export ${TOKEN_ENV_VAR}="${placeholder}"`);
1250
- writeLine(io.stdout, `export ${AGENT_ID_ENV_VAR}="<agent-family>"`);
1251
- writeLine(io.stdout, `export ${AGENT_INSTANCE_ENV_VAR}="<stable-random-id-for-this-local-agent>"`);
1252
- }
1253
- return 0;
1254
- }
1255
-
1256
- function writePrivacy(io) {
1257
- writeLine(io.stdout, `${PRODUCT_NAME} CLI privacy and security defaults:`);
1258
- writeLine(io.stdout, '- No telemetry or analytics.');
1259
- writeLine(io.stdout, '- `status` does not send tokens.');
1260
- writeLine(io.stdout, `- MCP configs reference ${TOKEN_ENV_VAR}; token values are not embedded.`);
1261
- writeLine(io.stdout, `- Agent instance IDs are non-secret and stored in user-scoped config outside git.`);
1262
- writeLine(io.stdout, '- `login` and `token add` store credentials in the user-scoped XMemo CLI config directory.');
1263
- writeLine(io.stdout, '- Legacy `token set` plaintext storage requires explicit --allow-plaintext.');
1264
- writeLine(io.stdout, '- npm publishing is restricted by package.json files whitelist.');
1265
- }
1266
-
1267
- async function startDeviceLogin(baseUrl, timeoutMs, io) {
1268
- const payload = await postJson(endpointUrl(baseUrl, DEVICE_LOGIN_START_PATH), {
1269
- client_id: PACKAGE_NAME,
1270
- cli_version: CLI_VERSION,
1271
- token_type: 'mcp_token',
1272
- scopes: ['memory:read', 'memory:write']
1273
- }, timeoutMs, io);
1274
-
1275
- const deviceCode = stringValue(payload, ['device_code']);
1276
- const verificationUri = stringValue(payload, ['verification_uri']);
1277
- if (!deviceCode || !verificationUri) {
1278
- throw new UsageError(`Device login did not return device_code and verification_uri from ${baseUrl}.`);
1279
- }
1280
-
1281
- return {
1282
- deviceCode,
1283
- userCode: stringValue(payload, ['user_code']),
1284
- verificationUri,
1285
- verificationUriComplete: stringValue(payload, ['verification_uri_complete']),
1286
- expiresIn: Number.isFinite(Number(payload.expires_in)) ? Number(payload.expires_in) : 600,
1287
- interval: Number.isFinite(Number(payload.interval)) ? Math.max(1, Number(payload.interval)) : 5
1288
- };
1289
- }
1290
-
1291
- async function pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, options = {}) {
1292
- const deadline = Date.now() + Math.min(start.expiresIn * 1000, loginTimeoutMs);
1293
- const sleepFn = io.sleep ?? sleep;
1294
- let intervalSeconds = start.interval;
1295
- while (Date.now() <= deadline) {
1296
- const payload = await postJson(endpointUrl(baseUrl, DEVICE_LOGIN_TOKEN_PATH), {
1297
- device_code: start.deviceCode,
1298
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
1299
- }, httpTimeoutMs, io, { allowDevicePending: true });
1300
-
1301
- const accessToken = stringValue(payload, ['access_token']) ?? stringValue(payload, ['token']);
1302
- if (accessToken) {
1303
- validateToken(accessToken);
1304
- return {
1305
- accessToken,
1306
- account: accountFromPayload(payload)
1307
- };
1308
- }
1309
-
1310
- const error = stringValue(payload, ['error']);
1311
- if (error && error !== 'authorization_pending' && error !== 'slow_down') {
1312
- throw new UsageError(`Device login failed: ${error}`);
1313
- }
1314
- if (options.pollOnce) {
1315
- throw new UsageError('Device login is still pending.');
1316
- }
1317
- if (error === 'slow_down') {
1318
- intervalSeconds += 5;
1319
- }
1320
- await sleepFn(intervalSeconds * 1000);
1321
- }
1322
-
1323
- throw new UsageError('Device login expired before authorization completed.');
1324
- }
1325
-
1326
- async function storeTokenFromStdin(io, metadata = {}) {
1327
- const token = (await readAll(io.stdin)).trim();
1328
- validateToken(token);
1329
- return await storeTokenValue(token, metadata, io.env);
1330
- }
1331
-
1332
- async function storeTokenValue(token, metadata, env) {
1333
- validateToken(token);
1334
- const credentialPath = credentialsPath(env);
1335
- await writePlaintextCredential(credentialPath, token, metadata);
1336
- return {
1337
- ok: true,
1338
- credentialPath,
1339
- tokenPresent: true,
1340
- tokenPrinted: false,
1341
- projectFilesModified: false,
1342
- storage: 'user-scoped-credential-file'
1343
- };
1344
- }
1345
-
1346
- async function readStoredCredential(env) {
1347
- const credentialPath = credentialsPath(env);
1348
- const content = await readTextIfExists(credentialPath);
1349
- if (!content.trim()) {
1350
- return { path: credentialPath, token: null };
1351
- }
1352
-
1353
- const parsed = parseJsonConfig(content, credentialPath);
1354
- return {
1355
- path: credentialPath,
1356
- token: stringValue(parsed, ['token']),
1357
- storage: stringValue(parsed, ['storage']),
1358
- account: accountFromPayload(parsed.metadata)
1359
- };
1360
- }
1361
-
1362
- function accountFromPayload(payload) {
1363
- const account = payload && typeof payload === 'object'
1364
- ? (payload.user && typeof payload.user === 'object' ? payload.user : payload.account)
1365
- : null;
1366
- if (!account || typeof account !== 'object') {
1367
- return null;
1368
- }
1369
- const userId = stringValue(account, ['user_id']) ?? stringValue(account, ['id']) ?? stringValue(account, ['userId']);
1370
- const email = stringValue(account, ['email']);
1371
- const displayName = stringValue(account, ['display_name']) ?? stringValue(account, ['name']) ?? stringValue(account, ['displayName']);
1372
- if (!userId && !email && !displayName) {
1373
- return null;
1374
- }
1375
- return {
1376
- userId: userId ?? null,
1377
- email: email ?? null,
1378
- displayName: displayName ?? null
1379
- };
1380
- }
1381
-
1382
- function formatAccount(account) {
1383
- const label = account.displayName || account.email || account.userId || 'XMemo account';
1384
- return account.email && account.displayName ? `${account.displayName} <${account.email}>` : label;
1385
- }
1386
-
1387
- async function resolveCredentialToken(env) {
1388
- const environmentToken = env[TOKEN_ENV_VAR] ?? env[LEGACY_TOKEN_ENV_VAR];
1389
- if (environmentToken) {
1390
- return environmentToken;
1391
- }
1392
- const credential = await readStoredCredential(env);
1393
- return credential.token;
1394
- }
1395
-
1396
- async function verifyTokenWithMcp(baseUrl, token, timeoutMs, io) {
1397
- const url = endpointUrl(baseUrl, '/mcp');
1398
- const controller = new AbortController();
1399
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
1400
- try {
1401
- const response = await io.fetch(url, {
1402
- method: 'POST',
1403
- headers: {
1404
- accept: 'application/json, text/event-stream',
1405
- 'content-type': 'application/json',
1406
- authorization: `Bearer ${token}`,
1407
- 'user-agent': `XMemo-CLI/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
1408
- },
1409
- body: JSON.stringify({
1410
- jsonrpc: '2.0',
1411
- id: 1,
1412
- method: 'initialize',
1413
- params: {
1414
- protocolVersion: '2024-11-05',
1415
- capabilities: {},
1416
- clientInfo: { name: COMMAND_NAME, version: CLI_VERSION }
1417
- }
1418
- }),
1419
- signal: controller.signal
1420
- });
1421
- return {
1422
- ok: response.ok,
1423
- detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}`
1424
- };
1425
- } catch (error) {
1426
- return {
1427
- ok: false,
1428
- detail: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
1429
- };
1430
- } finally {
1431
- clearTimeout(timeout);
1432
- }
1433
- }
1434
-
1435
- async function probe(url, timeoutMs, io) {
1436
- if (typeof io.fetch !== 'function') {
1437
- return { url, ok: false, error: 'fetch unavailable in this Node runtime' };
1438
- }
1439
-
1440
- const controller = new AbortController();
1441
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
1442
-
1443
- try {
1444
- const response = await io.fetch(url, {
1445
- headers: { accept: 'application/json' },
1446
- signal: controller.signal
1447
- });
1448
- return { url, ok: response.ok, status: response.status };
1449
- } catch (error) {
1450
- return {
1451
- url,
1452
- ok: false,
1453
- error: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
1454
- };
1455
- } finally {
1456
- clearTimeout(timeout);
1457
- }
1458
- }
1459
-
1460
- async function fetchJson(url, timeoutMs, io) {
1461
- if (typeof io.fetch !== 'function') {
1462
- throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
1463
- }
1464
-
1465
- const controller = new AbortController();
1466
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
1467
-
1468
- try {
1469
- const response = await io.fetch(url, {
1470
- headers: { accept: 'application/json' },
1471
- signal: controller.signal
1472
- });
1473
- if (!response.ok) {
1474
- throw new UsageError(`Discovery request failed with HTTP ${response.status}: ${url}`);
1475
- }
1476
- return await response.json();
1477
- } catch (error) {
1478
- if (error instanceof UsageError) {
1479
- throw error;
1480
- }
1481
- const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
1482
- throw new UsageError(`Discovery request failed: ${url} (${reason})`);
1483
- } finally {
1484
- clearTimeout(timeout);
1485
- }
1486
- }
1487
-
1488
- async function postJson(url, payload, timeoutMs, io, options = {}) {
1489
- if (typeof io.fetch !== 'function') {
1490
- throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
1491
- }
1492
-
1493
- const controller = new AbortController();
1494
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
1495
-
1496
- try {
1497
- const response = await io.fetch(url, {
1498
- method: 'POST',
1499
- headers: {
1500
- accept: 'application/json',
1501
- 'content-type': 'application/json'
1502
- },
1503
- body: JSON.stringify(payload),
1504
- signal: controller.signal
1505
- });
1506
- const responsePayload = await response.json();
1507
- if (!response.ok) {
1508
- const error = stringValue(responsePayload, ['error']) ?? stringValue(responsePayload, ['detail']) ?? `HTTP ${response.status}`;
1509
- if (options.allowDevicePending && (error === 'authorization_pending' || error === 'slow_down')) {
1510
- return { error };
1511
- }
1512
- throw new UsageError(`Request failed with HTTP ${response.status}: ${url} (${error})`);
1513
- }
1514
- return responsePayload;
1515
- } catch (error) {
1516
- if (error instanceof UsageError) {
1517
- throw error;
1518
- }
1519
- const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
1520
- throw new UsageError(`Request failed: ${url} (${reason})`);
1521
- } finally {
1522
- clearTimeout(timeout);
1523
- }
1524
- }
1525
-
1526
- function ensureDiscoveryService(discovery, discoveryUrl) {
1527
- const service = stringValue(discovery, ['service']);
1528
- if (service && service !== 'memory-os') {
1529
- throw new UsageError(`Discovery document at ${discoveryUrl} is for '${service}', not 'memory-os'.`);
1530
- }
1531
- }
1532
-
1533
- function buildSetupPlan({ baseUrl, discoveryUrl, statusUrl, discovery, status }) {
1534
- const apiBase = stringValue(discovery, ['urls', 'api_base'])
1535
- ?? stringValue(discovery, ['api_base_url'])
1536
- ?? baseUrl;
1537
- const mcpUrl = stringValue(discovery, ['urls', 'mcp'])
1538
- ?? stringValue(discovery, ['mcp_url'])
1539
- ?? endpointUrl(apiBase, '/mcp');
1540
- const tokenPortalUrl = stringValue(discovery, ['urls', 'token_portal'])
1541
- ?? stringValue(discovery, ['token_portal_url'])
1542
- ?? stringValue(status, ['requirements', 'token_portal_url']);
1543
- const tokenEnvVar = stringValue(discovery, ['auth', 'token_env_var'])
1544
- ?? stringValue(status, ['requirements', 'token_env_var'])
1545
- ?? TOKEN_ENV_VAR;
1546
-
1547
- return {
1548
- schemaVersion: '1.0',
1549
- baseUrl,
1550
- discoveryUrl,
1551
- statusUrl,
1552
- apiBase,
1553
- mcpUrl,
1554
- guideUrl: stringValue(discovery, ['urls', 'guide']) ?? endpointUrl(apiBase, '/guide'),
1555
- docsUrl: stringValue(discovery, ['urls', 'docs']),
1556
- tokenPortalUrl,
1557
- tokenEnvVar,
1558
- onboardingReady: booleanValue(status, ['ready']),
1559
- supportedClients: discoveryMcpClients(discovery),
1560
- localClients: supportedMcpClients(),
1561
- privacy: {
1562
- telemetry: false,
1563
- tokenSent: false,
1564
- tokenEmbeddedInConfig: false
1565
- },
1566
- boundaries: {
1567
- clientAllowed: arrayValue(discovery, ['agent_boundary', 'client_allowed'])
1568
- ?? arrayValue(status, ['agent_boundary', 'client_allowed'])
1569
- ?? [],
1570
- adminRequired: arrayValue(discovery, ['agent_boundary', 'admin_required'])
1571
- ?? arrayValue(status, ['agent_boundary', 'admin_required'])
1572
- ?? []
1573
- }
1574
- };
1575
- }
1576
-
1577
- async function bestEffortRootVersion(discovery, timeoutMs, io) {
1578
- const rootDiscoveryUrl = stringValue(discovery, ['urls', 'root_discovery']);
1579
- if (!rootDiscoveryUrl) {
1580
- return {};
1581
- }
1582
- try {
1583
- const rootDiscovery = await fetchJson(rootDiscoveryUrl, timeoutMs, io);
1584
- return { version: stringValue(rootDiscovery, ['version']) ?? undefined };
1585
- } catch (error) {
1586
- return { error: error.message };
1587
- }
1588
- }
1589
-
1590
- function discoveryMcpUrl(discovery, baseUrl) {
1591
- return stringValue(discovery, ['api', 'mcp', 'url'])
1592
- ?? stringValue(discovery, ['urls', 'mcp'])
1593
- ?? endpointUrl(baseUrl, '/mcp');
1594
- }
1595
-
1596
- function agentDiscoveryClientIds(discovery) {
1597
- const clients = Array.isArray(discovery?.clients) ? discovery.clients : [];
1598
- const ids = clients
1599
- .filter((client) => isPlainObject(client) && typeof client.id === 'string')
1600
- .map((client) => client.id);
1601
- if (ids.length > 0) {
1602
- return ids;
1603
- }
1604
- const supported = arrayValue(discovery, ['supported_clients']);
1605
- return supported ?? [];
1606
- }
1607
-
1608
- function mcpConfigTemplate(clientId, mcpUrl) {
1609
- if (clientId === 'codex') {
1610
- return {
1611
- client: clientId,
1612
- serverName: MCP_SERVER_NAME,
1613
- snippetFormat: 'toml',
1614
- snippet: codexTomlSnippet(mcpUrl),
1615
- requiresEnv: [TOKEN_ENV_VAR],
1616
- optionalEnv: [AGENT_INSTANCE_ENV_VAR],
1617
- agentIdentity: {
1618
- agentId: 'codex',
1619
- agentIdHeader: AGENT_ID_HEADER,
1620
- agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1621
- agentInstanceHeader: AGENT_INSTANCE_HEADER
1622
- },
1623
- agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1624
- writesTokenValue: false
1625
- };
1626
- }
1627
-
1628
- if (clientId === 'cursor') {
1629
- return bearerJsonMcpTemplate(clientId, mcpUrl, cursorJsonConfig(mcpUrl));
1630
- }
1631
-
1632
- if (clientId === 'gemini-cli') {
1633
- return oauthJsonMcpTemplate(clientId, mcpUrl, geminiJsonConfig(mcpUrl));
1634
- }
1635
-
1636
- if (clientId === 'antigravity') {
1637
- return oauthJsonMcpTemplate(clientId, mcpUrl, antigravityJsonConfig(mcpUrl));
1638
- }
1639
-
1640
- if (clientId === 'antigravity-ide') {
1641
- return oauthJsonMcpTemplate(clientId, mcpUrl, antigravityIdeJsonConfig(mcpUrl));
1642
- }
1643
-
1644
- if (clientId === 'antigravity2') {
1645
- return oauthJsonMcpTemplate(clientId, mcpUrl, antigravity2JsonConfig(mcpUrl));
1646
- }
1647
-
1648
- if (clientId === 'antigravity-cli') {
1649
- return oauthJsonMcpTemplate(clientId, mcpUrl, antigravityCliJsonConfig(mcpUrl));
1650
- }
1651
-
1652
- if (clientId === 'windsurf') {
1653
- return bearerJsonMcpTemplate(clientId, mcpUrl, windsurfJsonConfig(mcpUrl));
1654
- }
1655
-
1656
- if (clientId === 'cline') {
1657
- return bearerJsonMcpTemplate(clientId, mcpUrl, clineJsonConfig(mcpUrl));
1658
- }
1659
-
1660
- if (clientId === 'continue') {
1661
- return bearerJsonMcpTemplate(clientId, mcpUrl, continueJsonConfig(mcpUrl));
1662
- }
1663
-
1664
- if (clientId === 'claude-desktop') {
1665
- return bearerJsonMcpTemplate(clientId, mcpUrl, claudeJsonConfig(mcpUrl));
1666
- }
1667
-
1668
- if (clientId === 'qwen') {
1669
- return oauthJsonMcpTemplate(clientId, mcpUrl, qwenJsonConfig(mcpUrl));
1670
- }
1671
-
1672
- if (clientId === 'opencode') {
1673
- return oauthJsonMcpTemplate(clientId, mcpUrl, opencodeJsonConfig(mcpUrl));
1674
- }
1675
-
1676
- return {
1677
- client: clientId,
1678
- serverName: MCP_SERVER_NAME,
1679
- snippetFormat: 'json',
1680
- snippet: {
1681
- mcpServers: {
1682
- [MCP_SERVER_NAME]: {
1683
- type: 'http',
1684
- url: mcpUrl,
1685
- headers: {
1686
- Authorization: `Bearer \${${TOKEN_ENV_VAR}}`,
1687
- [AGENT_ID_HEADER]: clientId,
1688
- [AGENT_INSTANCE_HEADER]: `\${${AGENT_INSTANCE_ENV_VAR}}`
1689
- }
1690
- }
1691
- }
1692
- },
1693
- requiresEnv: [TOKEN_ENV_VAR],
1694
- optionalEnv: [AGENT_INSTANCE_ENV_VAR],
1695
- agentIdentity: {
1696
- agentId: clientId,
1697
- agentIdHeader: AGENT_ID_HEADER,
1698
- agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1699
- agentInstanceHeader: AGENT_INSTANCE_HEADER
1700
- },
1701
- agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1702
- writesTokenValue: false
1703
- };
1704
- }
1705
-
1706
- function bearerJsonMcpTemplate(clientId, mcpUrl, snippet) {
1707
- return {
1708
- client: clientId,
1709
- serverName: MCP_SERVER_NAME,
1710
- snippetFormat: 'json',
1711
- snippet,
1712
- requiresEnv: [TOKEN_ENV_VAR],
1713
- optionalEnv: [AGENT_INSTANCE_ENV_VAR],
1714
- authentication: 'env-bearer',
1715
- agentIdentity: {
1716
- agentId: clientId,
1717
- agentIdHeader: AGENT_ID_HEADER,
1718
- agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1719
- agentInstanceHeader: AGENT_INSTANCE_HEADER
1720
- },
1721
- agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1722
- mcpUrl,
1723
- writesTokenValue: false
1724
- };
1725
- }
1726
-
1727
- function oauthJsonMcpTemplate(clientId, mcpUrl, snippet) {
1728
- return {
1729
- client: clientId,
1730
- serverName: MCP_SERVER_NAME,
1731
- snippetFormat: 'json',
1732
- snippet,
1733
- requiresEnv: [],
1734
- optionalEnv: [AGENT_INSTANCE_ENV_VAR],
1735
- authentication: 'oauth',
1736
- agentIdentity: {
1737
- agentId: clientId,
1738
- agentIdHeader: AGENT_ID_HEADER,
1739
- agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1740
- agentInstanceHeader: AGENT_INSTANCE_HEADER
1741
- },
1742
- agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1743
- mcpUrl,
1744
- writesTokenValue: false
1745
- };
1746
- }
1747
-
1748
- function mcpLocalProxyTemplate(clientId, proxyUrl) {
1749
- return {
1750
- client: clientId,
1751
- serverName: MCP_SERVER_NAME,
1752
- snippetFormat: 'json',
1753
- snippet: {
1754
- mcpServers: {
1755
- [MCP_SERVER_NAME]: {
1756
- type: 'http',
1757
- url: proxyUrl
1758
- }
1759
- }
1760
- },
1761
- requiresCredential: [`${COMMAND_NAME} login`, `${COMMAND_NAME} token add --from-stdin`],
1762
- requiresLocalCommand: `${COMMAND_NAME} mcp proxy --port ${new URL(proxyUrl).port || DEFAULT_PROXY_PORT}`,
1763
- agentIdentity: {
1764
- agentId: clientId,
1765
- agentIdHeader: AGENT_ID_HEADER,
1766
- agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1767
- agentInstanceHeader: AGENT_INSTANCE_HEADER
1768
- },
1769
- agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1770
- writesTokenValue: false
1771
- };
1772
- }
1773
-
1774
- function agentInstanceGenerationPolicy(clientId) {
1775
- const automaticCommand = MCP_CLIENTS.has(clientId)
1776
- ? `${COMMAND_NAME} mcp add ${clientId} --write`
1777
- : clientId === 'copilot-cli'
1778
- ? `${COMMAND_NAME} setup copilot --write`
1779
- : null;
1780
- return {
1781
- requiredForHeaders: true,
1782
- stablePerInstall: true,
1783
- automaticCommand,
1784
- generatedPattern: `xmemo-${clientId}-<uuid>`,
1785
- storagePath: `~/.config/xmemo/agent-instances/${clientId}.json`,
1786
- manualEnvVar: AGENT_INSTANCE_ENV_VAR
1787
- };
1788
- }
1789
-
1790
- function sameMajorMinor(left, right) {
1791
- const leftParts = left.split('.');
1792
- const rightParts = right.split('.');
1793
- return leftParts[0] === rightParts[0] && leftParts[1] === rightParts[1];
1794
- }
1795
-
1796
- function baseUrlOption(args, env) {
1797
- return optionValue(args, '--base-url')
1798
- ?? optionValue(args, '--url')
1799
- ?? env.XMEMO_BASE_URL
1800
- ?? env.XMEMO_URL
1801
- ?? env.MEMORY_OS_BASE_URL
1802
- ?? env.MEMORY_OS_URL
1803
- ?? DEFAULT_SERVICE_URL;
1804
- }
1805
-
1806
- function clientSetupPlan(clientId, client, mcpUrl, env, identity) {
1807
- return {
1808
- id: clientId,
1809
- label: client.label,
1810
- configKind: client.configKind,
1811
- configPath: client.defaultConfigPath(env),
1812
- serverName: MCP_SERVER_NAME,
1813
- mcpUrl,
1814
- tokenEnvVar: TOKEN_ENV_VAR,
1815
- agentId: identity.agentId,
1816
- agentInstanceId: identity.agentInstanceId,
1817
- agentInstanceIdPath: identity.path,
1818
- writesTokenValue: false,
1819
- written: false
1820
- };
1821
- }
1822
-
1823
- function copilotSetupPlan(mcpUrl, proxyPort, env) {
1824
- const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`;
1825
- const template = mcpLocalProxyTemplate('copilot-cli', proxyUrl);
1826
- return {
1827
- id: 'copilot-cli',
1828
- label: 'Copilot CLI',
1829
- configKind: 'local-proxy',
1830
- configPath: defaultCopilotConfigPath(env),
1831
- serverName: template.serverName,
1832
- mcpUrl,
1833
- proxyUrl,
1834
- tokenEnvVar: TOKEN_ENV_VAR,
1835
- requiresCredential: template.requiresCredential,
1836
- requiresLocalCommand: template.requiresLocalCommand,
1837
- template: template.snippet,
1838
- agentId: template.agentIdentity.agentId,
1839
- writesTokenValue: false,
1840
- writeSupported: true,
1841
- written: false
1842
- };
1843
- }
1844
-
1845
- function writeSetupSummary(plan, io) {
1846
- writeLine(io.stdout, `${PRODUCT_NAME} setup discovery: ${plan.baseUrl}`);
1847
- writeLine(io.stdout, ` API: ${plan.apiBase}`);
1848
- writeLine(io.stdout, ` MCP: ${plan.mcpUrl}`);
1849
- writeLine(io.stdout, ` Guide: ${plan.guideUrl}`);
1850
- if (plan.docsUrl) {
1851
- writeLine(io.stdout, ` Docs: ${plan.docsUrl}`);
1852
- }
1853
- if (plan.tokenPortalUrl) {
1854
- writeLine(io.stdout, ` Token portal: ${plan.tokenPortalUrl}`);
1855
- }
1856
- writeLine(io.stdout, ` Token env var: ${plan.tokenEnvVar}`);
1857
- writeLine(io.stdout, ` Onboarding ready: ${plan.onboardingReady === null ? 'unknown' : plan.onboardingReady}`);
1858
- writeLine(io.stdout, 'Privacy: telemetry disabled; no token sent; generated config references env vars only.');
1859
-
1860
- if (plan.boundaries.adminRequired.length > 0) {
1861
- writeLine(io.stdout, `Admin-only actions: ${plan.boundaries.adminRequired.join(', ')}`);
1862
- }
1863
-
1864
- if (plan.detectedClients) {
1865
- writeLine(io.stdout, '');
1866
- if (plan.detectedClients.length === 0) {
1867
- writeLine(io.stdout, 'No local IDE or CLI client configurations were detected.');
1868
- writeLine(io.stdout, `Run \`${COMMAND_NAME} setup <client>\` to configure a client manually.`);
1869
- } else {
1870
- writeLine(io.stdout, `Auto-detected ${plan.detectedClients.length} client(s):`);
1871
- for (const client of plan.detectedClients) {
1872
- writeLine(io.stdout, ` [${client.written ? '✔' : ' '}] ${client.label}`);
1873
- writeLine(io.stdout, ` Config: ${client.configPath}`);
1874
- writeLine(io.stdout, ` Agent ID: ${client.agentId}`);
1875
- }
1876
- if (plan.detectedClients.some(c => c.written)) {
1877
- writeLine(io.stdout, '');
1878
- writeLine(io.stdout, 'Successfully applied XMemo MCP configuration to all detected clients!');
1879
- writeLine(io.stdout, 'Restart your IDEs or reload their MCP configurations to apply the changes.');
1880
- } else {
1881
- writeLine(io.stdout, '');
1882
- writeLine(io.stdout, `Run \`${COMMAND_NAME} setup --all --write\` to write configurations for all detected clients.`);
1883
- }
1884
- }
1885
- return;
1886
- }
1887
-
1888
- if (plan.selectedClient) {
1889
- writeLine(io.stdout, '');
1890
- writeLine(io.stdout, `Selected client: ${plan.selectedClient.label}`);
1891
- writeLine(io.stdout, ` Config path: ${plan.selectedClient.configPath}`);
1892
- writeLine(io.stdout, ` Written: ${plan.selectedClient.written}`);
1893
- writeLine(io.stdout, ` Token value embedded: ${plan.selectedClient.writesTokenValue}`);
1894
- writeLine(io.stdout, ` Agent ID: ${plan.selectedClient.agentId}`);
1895
- if (plan.selectedClient.agentInstanceIdPath) {
1896
- writeLine(io.stdout, ` Agent instance ID stored: ${plan.selectedClient.agentInstanceIdPath}`);
1897
- }
1898
- if (plan.selectedClient.configKind === 'local-proxy') {
1899
- writeLine(io.stdout, ` Local proxy: ${plan.selectedClient.requiresLocalCommand}`);
1900
- if (plan.selectedClient.written) {
1901
- writeLine(io.stdout, ` Next: keep \`${plan.selectedClient.requiresLocalCommand}\` running while you use Copilot CLI.`);
1902
- writeLine(io.stdout, ' If Copilot CLI is already open, reload MCP config or restart Copilot CLI.');
1903
- } else {
1904
- writeLine(io.stdout, ' MCP template:');
1905
- writeLine(io.stdout, JSON.stringify(plan.selectedClient.template, null, 2));
1906
- writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup copilot --url ${plan.baseUrl}`);
1907
- }
1908
- return;
1909
- }
1910
- if (plan.selectedClient.behaviorProfile) {
1911
- const profile = plan.selectedClient.behaviorProfile;
1912
- const profileClient = profileClientConfig(profile.client);
1913
- writeLine(io.stdout, ` Behavior profile target: ${profile.targetPath}`);
1914
- writeLine(io.stdout, ` Behavior profile client: ${profileClient?.label ?? profile.client}`);
1915
- writeLine(io.stdout, ` Behavior profile installed: ${profile.written}`);
1916
- writeLine(io.stdout, ` Behavior profile changed: ${profile.changed}`);
1917
- if (!profile.written) {
1918
- writeLine(io.stdout, ` Profile preview: ${COMMAND_NAME} profile install ${profile.client} --target ${profile.targetPath}`);
1919
- }
1920
- }
1921
- if (plan.selectedClient.written) {
1922
- writeLine(io.stdout, '');
1923
- const cid = plan.selectedClient.id;
1924
- if (cid === 'opencode') {
1925
- writeLine(io.stdout, '💡 Next steps for OpenCode:');
1926
- writeLine(io.stdout, ' 1. Open or restart OpenCode.');
1927
- writeLine(io.stdout, ' 2. Trigger any XMemo tool call, or manually run `opencode mcp auth XMemo` in your terminal.');
1928
- writeLine(io.stdout, ' 3. A browser window will automatically pop up requesting XMemo OAuth authorization.');
1929
- writeLine(io.stdout, ' 4. Log in or register on the webpage, then click "Authorize" to link OpenCode.');
1930
- } else if (cid === 'qwen') {
1931
- writeLine(io.stdout, '💡 Next steps for Qwen:');
1932
- writeLine(io.stdout, ' 1. Open or restart Qwen.');
1933
- writeLine(io.stdout, ' 2. When Qwen connects to XMemo MCP, a browser window will automatically pop up requesting OAuth authorization.');
1934
- writeLine(io.stdout, ' 3. Follow the page prompts to sign in and click "Authorize" to link Qwen.');
1935
- } else if (cid === 'trae' || cid === 'trae-solo') {
1936
- writeLine(io.stdout, `💡 Next steps for ${plan.selectedClient.label}:`);
1937
- writeLine(io.stdout, ` 1. Restart ${plan.selectedClient.label} to load the new MCP configuration.`);
1938
- writeLine(io.stdout, ` 2. Make sure the ${TOKEN_ENV_VAR} environment variable is set in your user environment.`);
1939
- if (plan.tokenPortalUrl) {
1940
- writeLine(io.stdout, ` (Token portal: ${plan.tokenPortalUrl})`);
1941
- }
1942
- } else if (usesClientOAuth(cid)) {
1943
- writeLine(io.stdout, `💡 Next steps for ${plan.selectedClient.label}:`);
1944
- writeLine(io.stdout, ' 1. When the agent starts or first makes an XMemo tool call, a browser window will automatically pop up requesting OAuth authorization.');
1945
- writeLine(io.stdout, ' 2. Follow the page prompts to sign in and click "Authorize".');
1946
- } else {
1947
- writeLine(io.stdout, `💡 Next steps for ${plan.selectedClient.label}:`);
1948
- writeLine(io.stdout, ' 1. Restart your editor/client to load the new MCP configuration.');
1949
- writeLine(io.stdout, ` 2. Make sure the ${TOKEN_ENV_VAR} environment variable is set in your user environment.`);
1950
- if (plan.tokenPortalUrl) {
1951
- writeLine(io.stdout, ` (Token portal: ${plan.tokenPortalUrl})`);
1952
- }
1953
- }
1954
- } else {
1955
- writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup ${plan.selectedClient.id} --url ${plan.baseUrl}`);
1956
- }
1957
- return;
1958
- }
1959
-
1960
- writeLine(io.stdout, '');
1961
- writeLine(io.stdout, 'Next steps:');
1962
- writeLine(io.stdout, ` 1. Create a scoped token in the token portal and store it in ${plan.tokenEnvVar}.`);
1963
- writeLine(io.stdout, ` 2. Configure a client, for example: ${COMMAND_NAME} setup codex --url ${plan.baseUrl}`);
1964
- writeLine(io.stdout, ` 3. Run ${COMMAND_NAME} status to smoke-test the service without sending the token.`);
1965
- }
1966
-
1967
- function discoveryMcpClients(discovery) {
1968
- const clients = discovery?.clients?.mcp;
1969
- if (!Array.isArray(clients)) {
1970
- return [];
1971
- }
1972
-
1973
- return clients
1974
- .filter((client) => isPlainObject(client) && typeof client.id === 'string')
1975
- .map((client) => ({
1976
- id: client.id,
1977
- configEndpoint: typeof client.config_endpoint === 'string' ? client.config_endpoint : null
1978
- }));
1979
- }
1980
-
1981
- function normalizeBaseUrl(input) {
1982
- try {
1983
- const parsed = new URL(input);
1984
- if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
1985
- throw new UsageError('URL must use http or https.');
1986
- }
1987
- parsed.hash = '';
1988
- parsed.search = '';
1989
- return parsed.toString().replace(/\/$/, '');
1990
- } catch (error) {
1991
- if (error instanceof UsageError) {
1992
- throw error;
1993
- }
1994
- throw new UsageError(`Invalid URL: ${input}`);
1995
- }
1996
- }
1997
-
1998
- function endpointUrl(baseUrl, pathname) {
1999
- const url = new URL(baseUrl);
2000
- url.pathname = pathname;
2001
- url.hash = '';
2002
- url.search = '';
2003
- return url.toString();
2004
- }
2005
-
2006
- function codexTomlSnippet(mcpUrl) {
2007
- return `[mcp_servers.${MCP_SERVER_NAME}]
2008
- url = "${escapeTomlString(mcpUrl)}"
2009
- bearer_token_env_var = "${TOKEN_ENV_VAR}"
2010
- `;
2011
- }
2012
-
2013
- function codexMemoryProfile() {
2014
- return memoryBehaviorProfile('codex');
2015
- }
2016
-
2017
- function writeCodexMemoryProfile(profile, io) {
2018
- writeLine(io.stdout, `${PRODUCT_NAME} Codex memory behavior profile`);
2019
- writeLine(io.stdout, `Profile: ${profile.profileVersion}`);
2020
- writeLine(io.stdout, `MCP server: ${profile.mcpServerName}`);
2021
- writeLine(io.stdout, `Token env: ${profile.requiredTokenEnv}`);
2022
- writeLine(io.stdout, '');
2023
- writeLine(io.stdout, 'Recommended Codex instructions:');
2024
- for (const instruction of profile.instructions) {
2025
- writeLine(io.stdout, `- ${instruction}`);
2026
- }
2027
- writeLine(io.stdout, '');
2028
- writeLine(io.stdout, `Setup: ${profile.setupCommand}`);
2029
- writeLine(io.stdout, `Smoke test: ${profile.smokeCommand}`);
2030
- }
2031
-
2032
- function codexProfileInstructionText() {
2033
- return profileInstructionText('codex');
2034
- }
2035
-
2036
- function memoryBehaviorProfile(clientId) {
2037
- const config = profileClientConfig(clientId);
2038
- if (!config) {
2039
- throw new UsageError(`Unsupported profile client: ${clientId}`);
2040
- }
2041
- const instructions = [
2042
- '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.',
2043
- 'Use recalled memories as evidence, not as unquestioned truth. Prefer current repository files when memory conflicts with code.',
2044
- 'After meaningful decisions, bug fixes, release steps, or durable conventions, write a concise XMemo memory with scope, source, and no secret values.',
2045
- 'Never store tokens, API keys, cookies, private keys, raw credentials, or sensitive customer data in XMemo.',
2046
- 'For routine or low-signal output, skip durable writes. Prefer summarized procedural or semantic memories over verbose logs.',
2047
- config.authInstruction
2048
- ];
2049
- return {
2050
- client: clientId,
2051
- label: config.label,
2052
- profileVersion: config.profileVersion,
2053
- mcpServerName: MCP_SERVER_NAME,
2054
- requiredTokenEnv: config.requiredTokenEnv ?? null,
2055
- objective: 'Use XMemo deliberately through MCP for project context recall and high-signal write-back.',
2056
- instructions,
2057
- setupCommand: `${COMMAND_NAME} setup ${config.setupAlias} --url "$XMEMO_URL"`,
2058
- smokeCommand: clientId === 'codex' ? `${COMMAND_NAME} smoke --client codex` : null
2059
- };
2060
- }
2061
-
2062
- function profileInstructionText(clientId) {
2063
- const profile = memoryBehaviorProfile(clientId);
2064
- const lines = [
2065
- `## XMemo ${profile.label} profile`,
2066
- '',
2067
- `MCP server: \`${profile.mcpServerName}\``,
2068
- ];
2069
- if (profile.requiredTokenEnv) {
2070
- lines.push(`Token env var: \`${profile.requiredTokenEnv}\``);
2071
- }
2072
- lines.push(
2073
- '',
2074
- profile.objective,
2075
- '',
2076
- `Recommended ${profile.label} behavior:`
2077
- );
2078
- for (const instruction of profile.instructions) {
2079
- lines.push(`- ${instruction}`);
2080
- }
2081
- lines.push('');
2082
- return `${lines.join('\n')}\n`;
2083
- }
2084
-
2085
- function profileClientConfig(clientId) {
2086
- const profileConfigs = {
2087
- codex: {
2088
- label: 'Codex',
2089
- setupAlias: 'codex',
2090
- profileVersion: 'codex-mcp-depth-v1',
2091
- requiredTokenEnv: TOKEN_ENV_VAR,
2092
- markerStart: CODEX_PROFILE_MARKER_START,
2093
- markerEnd: CODEX_PROFILE_MARKER_END,
2094
- defaultTarget: (env) => defaultCodexProfileTarget(env),
2095
- authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
2096
- },
2097
- cursor: {
2098
- label: 'Cursor',
2099
- setupAlias: 'cursor',
2100
- profileVersion: 'cursor-mcp-depth-v1',
2101
- requiredTokenEnv: TOKEN_ENV_VAR,
2102
- markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:cursor:start -->`,
2103
- markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:cursor:end -->`,
2104
- defaultTarget: (env) => {
2105
- const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
2106
- if (!isTest && (existsSync(path.join(process.cwd(), '.cursor')) || existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
2107
- return path.join(process.cwd(), '.cursor', 'rules', 'xmemo-memory.md');
2108
- }
2109
- return path.join(userHome(env), '.cursor', 'memory-profile.md');
2110
- },
2111
- authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
2112
- },
2113
- 'gemini-cli': {
2114
- label: 'Gemini CLI',
2115
- setupAlias: 'gemini',
2116
- profileVersion: 'gemini-cli-mcp-depth-v1',
2117
- markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:gemini-cli:start -->`,
2118
- markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:gemini-cli:end -->`,
2119
- defaultTarget: (env) => {
2120
- const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
2121
- if (!isTest && (existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
2122
- return path.join(process.cwd(), 'GEMINI.md');
2123
- }
2124
- return path.join(userHome(env), '.gemini', 'GEMINI.md');
2125
- },
2126
- authInstruction: 'Use the client-managed MCP OAuth credential; do not paste token values into prompts, config files, or logs.'
2127
- },
2128
- antigravity: {
2129
- label: 'Antigravity',
2130
- setupAlias: 'antigravity',
2131
- profileVersion: 'antigravity-mcp-depth-v1',
2132
- markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:antigravity:start -->`,
2133
- markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:antigravity:end -->`,
2134
- defaultTarget: (env) => {
2135
- const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
2136
- if (!isTest && (existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
2137
- return path.join(process.cwd(), 'GEMINI.md');
2138
- }
2139
- return path.join(userHome(env), '.gemini', 'antigravity', 'MEMORY.md');
2140
- },
2141
- authInstruction: 'Use the client-managed MCP OAuth credential; do not paste token values into prompts, config files, or logs.'
2142
- },
2143
- qwen: {
2144
- label: 'Qwen',
2145
- setupAlias: 'qwen',
2146
- profileVersion: 'qwen-mcp-depth-v1',
2147
- markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:qwen:start -->`,
2148
- markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:qwen:end -->`,
2149
- defaultTarget: (env) => {
2150
- const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
2151
- if (!isTest && (existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
2152
- return path.join(process.cwd(), 'QWEN.md');
2153
- }
2154
- return path.join(userHome(env), '.qwen', 'QWEN.md');
2155
- },
2156
- authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
2157
- },
2158
- opencode: {
2159
- label: 'OpenCode',
2160
- setupAlias: 'opencode',
2161
- profileVersion: 'opencode-mcp-depth-v1',
2162
- markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:opencode:start -->`,
2163
- markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:opencode:end -->`,
2164
- defaultTarget: (env) => {
2165
- const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
2166
- if (!isTest && (existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
2167
- return path.join(process.cwd(), 'AGENTS.md');
2168
- }
2169
- return path.join(userHome(env), '.config', 'opencode', 'AGENTS.md');
2170
- },
2171
- authInstruction: 'Use the client-managed MCP OAuth credential; do not paste token values into prompts, config files, or logs.'
2172
- },
2173
- trae: {
2174
- label: 'Trae',
2175
- setupAlias: 'trae',
2176
- profileVersion: 'trae-mcp-depth-v1',
2177
- requiredTokenEnv: TOKEN_ENV_VAR,
2178
- markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:trae:start -->`,
2179
- markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:trae:end -->`,
2180
- defaultTarget: (env) => {
2181
- const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
2182
- if (!isTest && (existsSync(path.join(process.cwd(), '.trae')) || existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
2183
- return path.join(process.cwd(), '.trae', 'rules', 'xmemo-memory.md');
2184
- }
2185
- return path.join(userHome(env), '.trae', 'memory-profile.md');
2186
- },
2187
- authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
2188
- },
2189
- 'trae-solo': {
2190
- label: 'Trae Solo',
2191
- setupAlias: 'trae-solo',
2192
- profileVersion: 'trae-solo-mcp-depth-v1',
2193
- requiredTokenEnv: TOKEN_ENV_VAR,
2194
- markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:trae-solo:start -->`,
2195
- markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:trae-solo:end -->`,
2196
- defaultTarget: (env) => {
2197
- const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
2198
- if (!isTest && (existsSync(path.join(process.cwd(), '.trae')) || existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
2199
- return path.join(process.cwd(), '.trae', 'rules', 'xmemo-memory.md');
2200
- }
2201
- return path.join(userHome(env), '.trae', 'memory-profile.md');
2202
- },
2203
- authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
2204
- }
2205
- };
2206
- return profileConfigs[clientId] ?? null;
2207
- }
2208
-
2209
- function supportedProfileClientIds() {
2210
- return ['codex', 'cursor', 'gemini', 'antigravity', 'qwen', 'opencode', 'trae', 'trae-solo'];
2211
- }
2212
-
2213
- function defaultProfileTarget(clientId, env) {
2214
- const config = profileClientConfig(clientId);
2215
- if (!config) {
2216
- throw new UsageError(`Unsupported profile client: ${clientId}`);
2217
- }
2218
- return config.defaultTarget(env);
2219
- }
2220
-
2221
- async function confirmProfileInstall(clientId, targetPath, io) {
2222
- const config = profileClientConfig(clientId);
2223
- writeLine(io.stdout, '');
2224
- writeLine(io.stdout, `Write XMemo memory behavior profile to ${targetPath}? [Y/n]`);
2225
- const answer = (await readLineFromStdin(io.stdin)).trim().toLowerCase();
2226
- if (answer === '' || answer === 'y' || answer === 'yes') {
2227
- return true;
2228
- }
2229
- if (answer === 'n' || answer === 'no') {
2230
- return false;
2231
- }
2232
- throw new UsageError(`Unsupported response for ${config.label} profile prompt: ${answer}`);
2233
- }
2234
-
2235
- async function readLineFromStdin(stdin) {
2236
- let input = '';
2237
- for await (const chunk of stdin) {
2238
- input += chunk;
2239
- if (input.includes('\n')) {
2240
- break;
2241
- }
2242
- }
2243
- return input.split(/\r?\n/, 1)[0] ?? '';
2244
- }
2245
-
2246
- function genericProfileMarkerBlock(clientId) {
2247
- const config = profileClientConfig(clientId);
2248
- return `${config.markerStart}\n${profileInstructionText(clientId)}${config.markerEnd}\n`;
2249
- }
2250
-
2251
- async function profileInstallResult(clientId, targetPath, options = {}) {
2252
- if (clientId === 'codex') {
2253
- return codexProfileInstallResult(targetPath, options);
2254
- }
2255
- const config = profileClientConfig(clientId);
2256
- const resolvedTarget = path.resolve(targetPath);
2257
- const existing = await readTextIfExists(resolvedTarget);
2258
- const marker = profileMarkerBounds(existing, config);
2259
- const block = genericProfileMarkerBlock(clientId);
2260
- let nextText;
2261
-
2262
- if (marker.present) {
2263
- nextText = `${existing.slice(0, marker.start)}${block}${existing.slice(marker.end)}`;
2264
- } else if (existing.trim().length === 0) {
2265
- nextText = block;
2266
- } else {
2267
- const separator = existing.endsWith('\n') ? '\n' : '\n\n';
2268
- nextText = `${existing}${separator}${block}`;
2269
- }
2270
-
2271
- const changed = nextText !== existing;
2272
- const write = Boolean(options.write);
2273
- if (write && changed) {
2274
- await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
2275
- await fs.writeFile(resolvedTarget, nextText);
2276
- }
2277
-
2278
- return {
2279
- client: clientId,
2280
- action: 'install',
2281
- targetPath: resolvedTarget,
2282
- markerStart: config.markerStart,
2283
- markerEnd: config.markerEnd,
2284
- installed: marker.present || (write && changed),
2285
- written: write,
2286
- changed,
2287
- markerPresent: marker.present,
2288
- writesTokenValue: false
2289
- };
2290
- }
2291
-
2292
- async function profileStatusResult(clientId, targetPath) {
2293
- if (clientId === 'codex') {
2294
- return codexProfileStatusResult(targetPath);
2295
- }
2296
- const config = profileClientConfig(clientId);
2297
- const resolvedTarget = path.resolve(targetPath);
2298
- const existing = await readTextIfExists(resolvedTarget);
2299
- const marker = profileMarkerBounds(existing, config);
2300
- return {
2301
- client: clientId,
2302
- action: 'status',
2303
- targetPath: resolvedTarget,
2304
- installed: marker.present,
2305
- markerPresent: marker.present,
2306
- markerStart: config.markerStart,
2307
- markerEnd: config.markerEnd,
2308
- writesTokenValue: false
2309
- };
2310
- }
2311
-
2312
- async function profileUninstallResult(clientId, targetPath, options = {}) {
2313
- if (clientId === 'codex') {
2314
- return codexProfileUninstallResult(targetPath, options);
2315
- }
2316
- const config = profileClientConfig(clientId);
2317
- const resolvedTarget = path.resolve(targetPath);
2318
- const existing = await readTextIfExists(resolvedTarget);
2319
- const marker = profileMarkerBounds(existing, config);
2320
- const write = Boolean(options.write);
2321
- let changed = false;
2322
-
2323
- if (marker.present) {
2324
- let nextText = `${existing.slice(0, marker.start)}${existing.slice(marker.end)}`;
2325
- nextText = nextText.replace(/\n{3,}/g, '\n\n');
2326
- if (nextText.trim().length === 0) {
2327
- nextText = '';
2328
- } else if (!nextText.endsWith('\n')) {
2329
- nextText = `${nextText}\n`;
2330
- }
2331
- changed = nextText !== existing;
2332
- if (write && changed) {
2333
- await fs.writeFile(resolvedTarget, nextText);
2334
- }
2335
- }
2336
-
2337
- return {
2338
- client: clientId,
2339
- action: 'uninstall',
2340
- targetPath: resolvedTarget,
2341
- installed: marker.present && !(write && changed),
2342
- written: write,
2343
- changed,
2344
- markerPresent: marker.present,
2345
- markerStart: config.markerStart,
2346
- markerEnd: config.markerEnd,
2347
- writesTokenValue: false
2348
- };
2349
- }
2350
-
2351
- function profileMarkerBounds(content, config) {
2352
- const start = content.indexOf(config.markerStart);
2353
- const end = content.indexOf(config.markerEnd);
2354
- if (start === -1 && end === -1) {
2355
- return { present: false, start: -1, end: -1 };
2356
- }
2357
-
2358
- if (start === -1 || end === -1 || end < start) {
2359
- throw new UsageError(`${config.label} profile markers are incomplete or out of order; edit the target file manually before retrying.`);
2360
- }
2361
-
2362
- if (
2363
- content.indexOf(config.markerStart, start + config.markerStart.length) !== -1
2364
- || content.indexOf(config.markerEnd, end + config.markerEnd.length) !== -1
2365
- ) {
2366
- throw new UsageError(`${config.label} profile markers appear more than once; edit the target file manually before retrying.`);
2367
- }
2368
-
2369
- const afterEnd = end + config.markerEnd.length;
2370
- const trailingNewlineLength = content.slice(afterEnd, afterEnd + 2) === '\r\n'
2371
- ? 2
2372
- : content.slice(afterEnd, afterEnd + 1) === '\n'
2373
- ? 1
2374
- : 0;
2375
-
2376
- return {
2377
- present: true,
2378
- start,
2379
- end: afterEnd + trailingNewlineLength
2380
- };
2381
- }
2382
-
2383
- function userHome(env) {
2384
- return env.USERPROFILE || env.HOME || os.homedir();
2385
- }
2386
-
2387
- function codexProfileMarkerBlock() {
2388
- return `${CODEX_PROFILE_MARKER_START}\n${codexProfileInstructionText()}${CODEX_PROFILE_MARKER_END}\n`;
2389
- }
2390
-
2391
- function defaultCodexProfileTarget() {
2392
- return path.resolve(process.cwd(), CODEX_PROFILE_TARGET);
2393
- }
2394
-
2395
- async function codexProfileInstallResult(targetPath, options = {}) {
2396
- const resolvedTarget = path.resolve(targetPath);
2397
- const existing = await readTextIfExists(resolvedTarget);
2398
- const marker = markerBounds(existing);
2399
- const block = codexProfileMarkerBlock();
2400
- let nextText;
2401
-
2402
- if (marker.present) {
2403
- nextText = `${existing.slice(0, marker.start)}${block}${existing.slice(marker.end)}`;
2404
- } else if (existing.trim().length === 0) {
2405
- nextText = block;
2406
- } else {
2407
- const separator = existing.endsWith('\n') ? '\n' : '\n\n';
2408
- nextText = `${existing}${separator}${block}`;
2409
- }
2410
-
2411
- const changed = nextText !== existing;
2412
- const write = Boolean(options.write);
2413
- if (write && changed) {
2414
- await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
2415
- await fs.writeFile(resolvedTarget, nextText);
2416
- }
2417
-
2418
- return {
2419
- client: 'codex',
2420
- action: 'install',
2421
- targetPath: resolvedTarget,
2422
- markerStart: CODEX_PROFILE_MARKER_START,
2423
- markerEnd: CODEX_PROFILE_MARKER_END,
2424
- installed: marker.present || (write && changed),
2425
- written: write,
2426
- changed,
2427
- markerPresent: marker.present,
2428
- writesTokenValue: false
2429
- };
2430
- }
2431
-
2432
- async function codexProfileStatusResult(targetPath) {
2433
- const resolvedTarget = path.resolve(targetPath);
2434
- const existing = await readTextIfExists(resolvedTarget);
2435
- const marker = markerBounds(existing);
2436
- return {
2437
- client: 'codex',
2438
- action: 'status',
2439
- targetPath: resolvedTarget,
2440
- installed: marker.present,
2441
- markerPresent: marker.present,
2442
- markerStart: CODEX_PROFILE_MARKER_START,
2443
- markerEnd: CODEX_PROFILE_MARKER_END,
2444
- writesTokenValue: false
2445
- };
2446
- }
2447
-
2448
- async function codexProfileUninstallResult(targetPath, options = {}) {
2449
- const resolvedTarget = path.resolve(targetPath);
2450
- const existing = await readTextIfExists(resolvedTarget);
2451
- const marker = markerBounds(existing);
2452
- const write = Boolean(options.write);
2453
- let changed = false;
2454
-
2455
- if (marker.present) {
2456
- let nextText = `${existing.slice(0, marker.start)}${existing.slice(marker.end)}`;
2457
- nextText = nextText.replace(/\n{3,}/g, '\n\n');
2458
- if (nextText.trim().length === 0) {
2459
- nextText = '';
2460
- } else if (!nextText.endsWith('\n')) {
2461
- nextText = `${nextText}\n`;
2462
- }
2463
- changed = nextText !== existing;
2464
- if (write && changed) {
2465
- await fs.writeFile(resolvedTarget, nextText);
2466
- }
2467
- }
2468
-
2469
- return {
2470
- client: 'codex',
2471
- action: 'uninstall',
2472
- targetPath: resolvedTarget,
2473
- installed: marker.present && !(write && changed),
2474
- written: write,
2475
- changed,
2476
- markerPresent: marker.present,
2477
- markerStart: CODEX_PROFILE_MARKER_START,
2478
- markerEnd: CODEX_PROFILE_MARKER_END,
2479
- writesTokenValue: false
2480
- };
2481
- }
2482
-
2483
- function markerBounds(content) {
2484
- const start = content.indexOf(CODEX_PROFILE_MARKER_START);
2485
- const end = content.indexOf(CODEX_PROFILE_MARKER_END);
2486
- if (start === -1 && end === -1) {
2487
- return { present: false, start: -1, end: -1 };
2488
- }
2489
-
2490
- if (start === -1 || end === -1 || end < start) {
2491
- throw new UsageError('Codex profile markers are incomplete or out of order; edit the target file manually before retrying.');
2492
- }
2493
-
2494
- if (
2495
- content.indexOf(CODEX_PROFILE_MARKER_START, start + CODEX_PROFILE_MARKER_START.length) !== -1
2496
- || content.indexOf(CODEX_PROFILE_MARKER_END, end + CODEX_PROFILE_MARKER_END.length) !== -1
2497
- ) {
2498
- throw new UsageError('Codex profile markers appear more than once; edit the target file manually before retrying.');
2499
- }
2500
-
2501
- const afterEnd = end + CODEX_PROFILE_MARKER_END.length;
2502
- const trailingNewlineLength = content.slice(afterEnd, afterEnd + 2) === '\r\n'
2503
- ? 2
2504
- : content.slice(afterEnd, afterEnd + 1) === '\n'
2505
- ? 1
2506
- : 0;
2507
-
2508
- return {
2509
- present: true,
2510
- start,
2511
- end: afterEnd + trailingNewlineLength
2512
- };
2513
- }
2514
-
2515
- function writeProfileResult(action, result, io) {
2516
- const config = profileClientConfig(result.client);
2517
- writeLine(io.stdout, `${PRODUCT_NAME} ${config?.label ?? result.client} profile ${action}`);
2518
- writeLine(io.stdout, ` Target: ${result.targetPath}`);
2519
- writeLine(io.stdout, ` Installed: ${result.installed}`);
2520
- if ('written' in result) {
2521
- writeLine(io.stdout, ` Written: ${result.written}`);
2522
- writeLine(io.stdout, ` Changed: ${result.changed}`);
2523
- }
2524
- writeLine(io.stdout, ' Token value embedded: false');
2525
- }
2526
-
2527
- async function codexSmokeReport(configPath, env) {
2528
- const configText = await readTextIfExists(configPath);
2529
- const serverBlock = findTomlServerBlock(configText);
2530
- const block = serverBlock.block;
2531
- const mcpUrl = block ? tomlStringValue(block, 'url') : null;
2532
- const bearerTokenEnvVar = block ? tomlStringValue(block, 'bearer_token_env_var') : null;
2533
- const tokenValue = env[TOKEN_ENV_VAR] ?? '';
2534
- const identityPath = agentInstanceIdentityPath(env, 'codex');
2535
- const identityPresent = await fileExists(identityPath);
2536
- const checks = [
2537
- {
2538
- name: 'config_present',
2539
- ok: configText.trim().length > 0,
2540
- required: true,
2541
- detail: configText.trim().length > 0 ? 'found' : 'missing'
2542
- },
2543
- {
2544
- name: 'memory_os_server_present',
2545
- ok: Boolean(block),
2546
- required: true,
2547
- detail: block ? `[mcp_servers.${serverBlock.name}]` : `missing [mcp_servers.${MCP_SERVER_NAME}]`
2548
- },
2549
- {
2550
- name: 'mcp_url_present',
2551
- ok: Boolean(mcpUrl),
2552
- required: true,
2553
- detail: mcpUrl ?? 'missing url'
2554
- },
2555
- {
2556
- name: 'bearer_token_env_var',
2557
- ok: bearerTokenEnvVar === TOKEN_ENV_VAR,
2558
- required: true,
2559
- detail: bearerTokenEnvVar ?? 'missing bearer_token_env_var'
2560
- },
2561
- {
2562
- name: 'token_env_present',
2563
- ok: Boolean(env[TOKEN_ENV_VAR]),
2564
- required: true,
2565
- detail: env[TOKEN_ENV_VAR] ? 'present' : `missing ${TOKEN_ENV_VAR}`
2566
- },
2567
- {
2568
- name: 'token_not_embedded_in_config',
2569
- ok: !tokenValue || !configText.includes(tokenValue),
2570
- required: true,
2571
- detail: 'token value not printed or embedded'
2572
- },
2573
- {
2574
- name: 'agent_instance_identity_file',
2575
- ok: identityPresent,
2576
- required: false,
2577
- detail: identityPresent ? identityPath : `optional; create with ${COMMAND_NAME} mcp add codex --write (${identityPath})`
2578
- }
2579
- ];
2580
-
2581
- return {
2582
- ok: checks.every((check) => !check.required || check.ok),
2583
- client: 'codex',
2584
- configPath,
2585
- serverName: serverBlock.name ?? MCP_SERVER_NAME,
2586
- mcpUrl,
2587
- tokenEnvVar: TOKEN_ENV_VAR,
2588
- agentInstanceIdPath: identityPath,
2589
- checks
2590
- };
2591
- }
2592
-
2593
- function knownMcpServerNames() {
2594
- return [MCP_SERVER_NAME, ...LEGACY_MCP_SERVER_NAMES];
2595
- }
2596
-
2597
- function existingJsonMcpServerName(mcpServers) {
2598
- return knownMcpServerNames().find((name) => mcpServers[name]);
2599
- }
2600
-
2601
- function existingTomlMcpServerName(content) {
2602
- return knownMcpServerNames().find((name) => content.includes(`[mcp_servers.${name}]`));
2603
- }
2604
-
2605
- function findTomlServerBlock(content) {
2606
- const name = existingTomlMcpServerName(content);
2607
- return {
2608
- name: name ?? null,
2609
- block: name ? tomlServerBlock(content, name) : ''
2610
- };
2611
- }
2612
-
2613
- function tomlServerBlock(content, serverName) {
2614
- const header = `[mcp_servers.${serverName}]`;
2615
- const lines = content.split(/\r?\n/);
2616
- const start = lines.findIndex((line) => line.trim() === header);
2617
- if (start === -1) {
2618
- return '';
2619
- }
2620
-
2621
- const block = [];
2622
- for (let index = start + 1; index < lines.length; index += 1) {
2623
- const line = lines[index];
2624
- if (/^\s*\[/.test(line)) {
2625
- break;
2626
- }
2627
- block.push(line);
2628
- }
2629
- return block.join('\n');
2630
- }
2631
-
2632
- function tomlStringValue(block, key) {
2633
- const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"((?:\\\\.|[^"\\\\])*)"\\s*$`, 'm');
2634
- const match = block.match(pattern);
2635
- return match ? unescapeTomlString(match[1]) : null;
2636
- }
2637
-
2638
- function cursorJsonSnippet(mcpUrl, identity = envReferenceIdentity('cursor')) {
2639
- return `${JSON.stringify(cursorJsonConfig(mcpUrl, identity), null, 2)}\n`;
2640
- }
2641
-
2642
- async function appendTomlServerConfig(configPath, mcpUrl) {
2643
- const snippet = codexTomlSnippet(mcpUrl);
2644
- const existing = await readTextIfExists(configPath);
2645
- const existingName = existingTomlMcpServerName(existing);
2646
- if (existingName) {
2647
- throw new UsageError(`MCP config already contains [mcp_servers.${existingName}]. Edit ${configPath} manually to avoid duplicate server definitions.`);
2648
- }
2649
-
2650
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2651
- const prefix = existing.trim().length === 0 ? '' : '\n\n';
2652
- await fs.appendFile(configPath, `${prefix}${snippet}`, { mode: 0o600 });
2653
- await bestEffortChmod(configPath, 0o600);
2654
- }
2655
-
2656
- async function mergeJsonMcpConfig(configPath, mcpUrl, identity) {
2657
- const existing = await readTextIfExists(configPath);
2658
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
2659
-
2660
- if (!isPlainObject(parsed)) {
2661
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
2662
- }
2663
-
2664
- if (!isPlainObject(parsed.mcpServers)) {
2665
- parsed.mcpServers = {};
2666
- }
2667
-
2668
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
2669
- if (existingName) {
2670
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2671
- }
2672
-
2673
- parsed.mcpServers[MCP_SERVER_NAME] = cursorJsonServerConfig(mcpUrl, identity);
2674
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2675
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2676
- await bestEffortChmod(configPath, 0o600);
2677
- }
2678
-
2679
- function antigravityJsonServerConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2680
- return {
2681
- serverUrl: mcpUrl,
2682
- headers: {
2683
- [AGENT_ID_HEADER]: identity.agentId,
2684
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
2685
- }
2686
- };
2687
- }
2688
-
2689
- function antigravityJsonConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2690
- return {
2691
- mcpServers: {
2692
- [MCP_SERVER_NAME]: antigravityJsonServerConfig(mcpUrl, identity)
2693
- }
2694
- };
2695
- }
2696
-
2697
- function antigravityJsonSnippet(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2698
- return `${JSON.stringify(antigravityJsonConfig(mcpUrl, identity), null, 2)}\n`;
2699
- }
2700
-
2701
- async function mergeAntigravityMcpConfig(configPath, mcpUrl, identity) {
2702
- const existing = await readTextIfExists(configPath);
2703
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
2704
-
2705
- if (!isPlainObject(parsed)) {
2706
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
2707
- }
2708
-
2709
- if (!isPlainObject(parsed.mcpServers)) {
2710
- parsed.mcpServers = {};
2711
- }
2712
-
2713
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
2714
- if (existingName) {
2715
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2716
- }
2717
-
2718
- parsed.mcpServers[MCP_SERVER_NAME] = antigravityJsonServerConfig(mcpUrl, identity);
2719
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2720
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2721
- await bestEffortChmod(configPath, 0o600);
2722
- }
2723
-
2724
- function antigravityIdeJsonServerConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2725
- return {
2726
- type: 'http',
2727
- url: mcpUrl,
2728
- headers: {
2729
- [AGENT_ID_HEADER]: identity.agentId,
2730
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
2731
- }
2732
- };
2733
- }
2734
-
2735
- function antigravityIdeJsonConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2736
- return {
2737
- mcpServers: {
2738
- [MCP_SERVER_NAME]: antigravityIdeJsonServerConfig(mcpUrl, identity)
2739
- }
2740
- };
2741
- }
2742
-
2743
- function antigravityIdeJsonSnippet(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2744
- return `${JSON.stringify(antigravityIdeJsonConfig(mcpUrl, identity), null, 2)}\n`;
2745
- }
2746
-
2747
- async function mergeAntigravityIdeMcpConfig(configPath, mcpUrl, identity) {
2748
- const existing = await readTextIfExists(configPath);
2749
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
2750
-
2751
- if (!isPlainObject(parsed)) {
2752
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
2753
- }
2754
-
2755
- if (!isPlainObject(parsed.mcpServers)) {
2756
- parsed.mcpServers = {};
2757
- }
2758
-
2759
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
2760
- if (existingName) {
2761
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2762
- }
2763
-
2764
- parsed.mcpServers[MCP_SERVER_NAME] = antigravityIdeJsonServerConfig(mcpUrl, identity);
2765
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2766
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2767
- await bestEffortChmod(configPath, 0o600);
2768
- }
2769
-
2770
- function antigravity2JsonServerConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2771
- return {
2772
- type: 'http',
2773
- url: mcpUrl,
2774
- headers: {
2775
- [AGENT_ID_HEADER]: identity.agentId,
2776
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
2777
- }
2778
- };
2779
- }
2780
-
2781
- function antigravity2JsonConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2782
- return {
2783
- mcpServers: {
2784
- [MCP_SERVER_NAME]: antigravity2JsonServerConfig(mcpUrl, identity)
2785
- }
2786
- };
2787
- }
2788
-
2789
- function antigravity2JsonSnippet(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2790
- return `${JSON.stringify(antigravity2JsonConfig(mcpUrl, identity), null, 2)}\n`;
2791
- }
2792
-
2793
- async function mergeAntigravity2McpConfig(configPath, mcpUrl, identity) {
2794
- const existing = await readTextIfExists(configPath);
2795
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
2796
-
2797
- if (!isPlainObject(parsed)) {
2798
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
2799
- }
2800
-
2801
- if (!isPlainObject(parsed.mcpServers)) {
2802
- parsed.mcpServers = {};
2803
- }
2804
-
2805
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
2806
- if (existingName) {
2807
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2808
- }
2809
-
2810
- parsed.mcpServers[MCP_SERVER_NAME] = antigravity2JsonServerConfig(mcpUrl, identity);
2811
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2812
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2813
- await bestEffortChmod(configPath, 0o600);
2814
- }
2815
-
2816
- function antigravityCliJsonServerConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2817
- return {
2818
- httpUrl: mcpUrl,
2819
- headers: {
2820
- [AGENT_ID_HEADER]: identity.agentId,
2821
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
2822
- }
2823
- };
2824
- }
2825
-
2826
- function antigravityCliJsonConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2827
- return {
2828
- mcpServers: {
2829
- [MCP_SERVER_NAME]: antigravityCliJsonServerConfig(mcpUrl, identity)
2830
- }
2831
- };
2832
- }
2833
-
2834
- function antigravityCliJsonSnippet(mcpUrl, identity = envReferenceIdentity('antigravity')) {
2835
- return `${JSON.stringify(antigravityCliJsonConfig(mcpUrl, identity), null, 2)}\n`;
2836
- }
2837
-
2838
- async function mergeAntigravityCliMcpConfig(configPath, mcpUrl, identity) {
2839
- const existing = await readTextIfExists(configPath);
2840
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
2841
-
2842
- if (!isPlainObject(parsed)) {
2843
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
2844
- }
2845
-
2846
- if (!isPlainObject(parsed.mcpServers)) {
2847
- parsed.mcpServers = {};
2848
- }
2849
-
2850
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
2851
- if (existingName) {
2852
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2853
- }
2854
-
2855
- parsed.mcpServers[MCP_SERVER_NAME] = antigravityCliJsonServerConfig(mcpUrl, identity);
2856
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2857
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2858
- await bestEffortChmod(configPath, 0o600);
2859
- }
2860
-
2861
-
2862
- async function mergeGeminiMcpConfig(configPath, mcpUrl, identity) {
2863
- const existing = await readTextIfExists(configPath);
2864
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
2865
-
2866
- if (!isPlainObject(parsed)) {
2867
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
2868
- }
2869
-
2870
- if (!isPlainObject(parsed.mcpServers)) {
2871
- parsed.mcpServers = {};
2872
- }
2873
-
2874
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
2875
- if (existingName) {
2876
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2877
- }
2878
-
2879
- parsed.mcpServers[MCP_SERVER_NAME] = geminiJsonServerConfig(mcpUrl, identity);
2880
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2881
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2882
- await bestEffortChmod(configPath, 0o600);
2883
- }
2884
-
2885
- async function mergeCopilotMcpConfig(configPath, proxyUrl) {
2886
- const existing = await readTextIfExists(configPath);
2887
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
2888
-
2889
- if (!isPlainObject(parsed)) {
2890
- throw new UsageError(`Copilot MCP JSON config must be an object: ${configPath}`);
2891
- }
2892
-
2893
- if (!isPlainObject(parsed.mcpServers)) {
2894
- parsed.mcpServers = {};
2895
- }
2896
-
2897
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
2898
- if (existingName) {
2899
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2900
- }
2901
-
2902
- parsed.mcpServers[MCP_SERVER_NAME] = copilotLocalProxyServerConfig(proxyUrl);
2903
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2904
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2905
- await bestEffortChmod(configPath, 0o600);
2906
- }
2907
-
2908
- function copilotLocalProxyServerConfig(proxyUrl) {
2909
- return {
2910
- type: 'http',
2911
- url: proxyUrl
2912
- };
2913
- }
2914
-
2915
- function cursorJsonConfig(mcpUrl, identity = envReferenceIdentity('cursor')) {
2916
- return {
2917
- mcpServers: {
2918
- [MCP_SERVER_NAME]: cursorJsonServerConfig(mcpUrl, identity)
2919
- }
2920
- };
2921
- }
2922
-
2923
- function cursorJsonServerConfig(mcpUrl, identity = envReferenceIdentity('cursor')) {
2924
- return {
2925
- url: mcpUrl,
2926
- headers: {
2927
- Authorization: `Bearer \${env:${TOKEN_ENV_VAR}}`,
2928
- [AGENT_ID_HEADER]: identity.agentId,
2929
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
2930
- }
2931
- };
2932
- }
2933
-
2934
- function geminiJsonServerConfig(mcpUrl, identity = envReferenceIdentity('gemini-cli')) {
2935
- return {
2936
- httpUrl: mcpUrl,
2937
- headers: {
2938
- [AGENT_ID_HEADER]: identity.agentId,
2939
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
2940
- }
2941
- };
2942
- }
2943
-
2944
- function geminiJsonConfig(mcpUrl, identity = envReferenceIdentity('gemini-cli')) {
2945
- return {
2946
- mcpServers: {
2947
- [MCP_SERVER_NAME]: geminiJsonServerConfig(mcpUrl, identity)
2948
- }
2949
- };
2950
- }
2951
-
2952
- function geminiJsonSnippet(mcpUrl, identity = envReferenceIdentity('gemini-cli')) {
2953
- return `${JSON.stringify(geminiJsonConfig(mcpUrl, identity), null, 2)}\n`;
2954
- }
2955
-
2956
- async function agentIdentity(clientId, env) {
2957
- const targetClientId = (clientId === 'antigravity-ide' || clientId === 'antigravity2' || clientId === 'antigravity-cli') ? 'antigravity' : clientId;
2958
- const configuredInstanceId = env[AGENT_INSTANCE_ENV_VAR];
2959
- if (configuredInstanceId) {
2960
- return {
2961
- agentId: targetClientId,
2962
- agentInstanceId: configuredInstanceId,
2963
- path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
2964
- };
2965
- }
2966
-
2967
- const identityPath = agentInstanceIdentityPath(env, targetClientId);
2968
- const existing = await readAgentInstanceIdentity(identityPath);
2969
- if (existing) {
2970
- return { agentId: targetClientId, agentInstanceId: existing, path: identityPath };
2971
- }
2972
-
2973
- const generated = `xmemo-${targetClientId}-${randomUUID()}`;
2974
- await fs.mkdir(path.dirname(identityPath), { recursive: true, mode: 0o700 });
2975
- await bestEffortChmod(path.dirname(identityPath), 0o700);
2976
- await fs.writeFile(identityPath, `${JSON.stringify({ version: 1, agentId: targetClientId, agentInstanceId: generated }, null, 2)}\n`, { mode: 0o600 });
2977
- await bestEffortChmod(identityPath, 0o600);
2978
- return { agentId: targetClientId, agentInstanceId: generated, path: identityPath };
2979
- }
2980
-
2981
- async function readAgentInstanceIdentity(identityPath) {
2982
- const existing = await readTextIfExists(identityPath);
2983
- if (!existing.trim()) {
2984
- return null;
2985
- }
2986
- const parsed = parseJsonConfig(existing, identityPath);
2987
- const value = stringValue(parsed, ['agentInstanceId']);
2988
- return value || null;
2989
- }
2990
-
2991
- function agentInstanceIdentityPath(env, clientId) {
2992
- return path.join(configRoot(env), 'agent-instances', `${clientId}.json`);
2993
- }
2994
-
2995
- function envReferenceIdentity(clientId) {
2996
- const targetClientId = (clientId === 'antigravity-ide' || clientId === 'antigravity2' || clientId === 'antigravity-cli') ? 'antigravity' : clientId;
2997
- return {
2998
- agentId: targetClientId,
2999
- agentInstanceId: `\${${AGENT_INSTANCE_ENV_VAR}}`,
3000
- path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
3001
- };
3002
- }
3003
-
3004
- function supportedMcpClients() {
3005
- const clients = Array.from(MCP_CLIENTS.entries()).map(([id, client]) => ({
3006
- id,
3007
- label: client.label,
3008
- configKind: client.configKind
3009
- }));
3010
- clients.push({ id: 'copilot-cli', label: 'Copilot CLI', configKind: 'local-proxy' });
3011
- return clients;
3012
- }
3013
-
3014
- function supportedMcpClientIds() {
3015
- return Array.from(MCP_CLIENTS.keys());
3016
- }
3017
-
3018
- function supportedSetupClientIds() {
3019
- return ['codex', 'cursor', 'copilot', 'gemini', 'antigravity', 'antigravity-ide', 'antigravity2', 'antigravity-cli', 'windsurf', 'cline', 'continue', 'claude', 'qwen', 'opencode', 'trae', 'trae-solo'];
3020
- }
3021
-
3022
- function usesClientOAuth(clientId) {
3023
- return clientId === 'gemini-cli' || clientId === 'antigravity' || clientId === 'antigravity-ide' || clientId === 'antigravity2' || clientId === 'antigravity-cli' || clientId === 'qwen' || clientId === 'opencode';
3024
- }
3025
-
3026
- function credentialsPath(env) {
3027
- return path.join(configRoot(env), 'credentials.json');
3028
- }
3029
-
3030
- function configRoot(env) {
3031
- if (env.XMEMO_CONFIG_HOME) {
3032
- return env.XMEMO_CONFIG_HOME;
3033
- }
3034
-
3035
- if (env.MEMORY_OS_CONFIG_HOME) {
3036
- return env.MEMORY_OS_CONFIG_HOME;
3037
- }
3038
-
3039
- if (process.platform === 'win32' && env.LOCALAPPDATA) {
3040
- return path.join(env.LOCALAPPDATA, 'XMemo', 'CLI');
3041
- }
3042
-
3043
- if (env.XDG_CONFIG_HOME) {
3044
- return path.join(env.XDG_CONFIG_HOME, 'xmemo');
3045
- }
3046
-
3047
- const home = env.HOME || os.homedir();
3048
- return path.join(home, '.config', 'xmemo');
3049
- }
3050
-
3051
- function defaultCodexConfigPath(env) {
3052
- const home = env.USERPROFILE || env.HOME || os.homedir();
3053
- return path.join(home, '.codex', 'config.toml');
3054
- }
3055
-
3056
- function defaultCursorConfigPath(env) {
3057
- const home = env.USERPROFILE || env.HOME || os.homedir();
3058
- return path.join(home, '.cursor', 'mcp.json');
3059
- }
3060
-
3061
- function defaultGeminiConfigPath(env) {
3062
- const home = env.USERPROFILE || env.HOME || os.homedir();
3063
- return path.join(home, '.gemini', 'settings.json');
3064
- }
3065
-
3066
- function defaultAntigravityConfigPath(env) {
3067
- const home = env.USERPROFILE || env.HOME || os.homedir();
3068
- return path.join(home, '.gemini', 'config', 'mcp_config.json');
3069
- }
3070
-
3071
- function defaultAntigravityIdeConfigPath(env) {
3072
- const home = env.USERPROFILE || env.HOME || os.homedir();
3073
- return path.join(home, '.gemini', 'config', 'mcp_config.json');
3074
- }
3075
-
3076
- function defaultAntigravity2ConfigPath(env) {
3077
- const home = env.USERPROFILE || env.HOME || os.homedir();
3078
- return path.join(home, '.gemini', 'config', 'mcp_config.json');
3079
- }
3080
-
3081
- function defaultAntigravityCliConfigPath(env) {
3082
- const home = env.USERPROFILE || env.HOME || os.homedir();
3083
- return path.join(home, '.gemini', 'config', 'mcp_config.json');
3084
- }
3085
-
3086
- function defaultCopilotConfigPath(env) {
3087
- const home = env.USERPROFILE || env.HOME || os.homedir();
3088
- return path.join(env.COPILOT_HOME ?? path.join(home, '.copilot'), 'mcp-config.json');
3089
- }
3090
-
3091
- async function writePlaintextCredential(credentialPath, token, metadata = {}) {
3092
- await fs.mkdir(path.dirname(credentialPath), { recursive: true, mode: 0o700 });
3093
- await bestEffortChmod(path.dirname(credentialPath), 0o700);
3094
- const payload = {
3095
- version: 1,
3096
- tokenEnvVar: TOKEN_ENV_VAR,
3097
- storage: 'user-scoped-credential-file',
3098
- createdAt: new Date().toISOString(),
3099
- metadata,
3100
- token
3101
- };
3102
- await fs.writeFile(credentialPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
3103
- await bestEffortChmod(credentialPath, 0o600);
3104
- }
3105
-
3106
- async function bestEffortChmod(filePath, mode) {
3107
- try {
3108
- await fs.chmod(filePath, mode);
3109
- } catch {
3110
- // Windows and managed environments may ignore POSIX chmod.
3111
- }
3112
- }
3113
-
3114
- function validateToken(token) {
3115
- if (!token) {
3116
- throw new UsageError('Token from stdin is empty.');
3117
- }
3118
-
3119
- if (/\s/.test(token)) {
3120
- throw new UsageError('Token must not contain whitespace.');
3121
- }
3122
-
3123
- if (token.length < 16) {
3124
- throw new UsageError('Token is too short to be a production credential.');
3125
- }
3126
- }
3127
-
3128
- function requiredOption(args, name) {
3129
- const value = optionValue(args, name);
3130
- if (!value) {
3131
- throw new UsageError(`Missing required option ${name}.`);
3132
- }
3133
- return value;
3134
- }
3135
-
3136
- function positionalClientArg(args) {
3137
- const candidate = args[0];
3138
- if (!candidate || candidate.startsWith('--')) {
3139
- return null;
3140
- }
3141
-
3142
- return normalizeSetupClientId(candidate);
3143
- }
3144
-
3145
- function normalizeSetupClientId(candidate) {
3146
- if (!candidate) {
3147
- return null;
3148
- }
3149
-
3150
- const normalized = SETUP_CLIENT_ALIASES.get(candidate);
3151
- if (!normalized) {
3152
- throw new UsageError(`Unsupported setup client: ${candidate}. Supported clients: ${supportedSetupClientIds().join(', ')}.`);
3153
- }
3154
-
3155
- return normalized;
3156
- }
3157
-
3158
- function optionValue(args, name) {
3159
- const index = args.indexOf(name);
3160
- if (index === -1) {
3161
- return null;
3162
- }
3163
-
3164
- const value = args[index + 1];
3165
- if (!value || value.startsWith('--')) {
3166
- throw new UsageError(`Option ${name} requires a value.`);
3167
- }
3168
-
3169
- return value;
3170
- }
3171
-
3172
- function stringValue(source, keys) {
3173
- const value = valueAtPath(source, keys);
3174
- return typeof value === 'string' && value.length > 0 ? value : null;
3175
- }
3176
-
3177
- function booleanValue(source, keys) {
3178
- const value = valueAtPath(source, keys);
3179
- return typeof value === 'boolean' ? value : null;
3180
- }
3181
-
3182
- function arrayValue(source, keys) {
3183
- const value = valueAtPath(source, keys);
3184
- return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : null;
3185
- }
3186
-
3187
- function valueAtPath(source, keys) {
3188
- let current = source;
3189
- for (const key of keys) {
3190
- if (!isPlainObject(current) || !(key in current)) {
3191
- return null;
3192
- }
3193
- current = current[key];
3194
- }
3195
- return current;
3196
- }
3197
-
3198
- function hasFlag(args, name) {
3199
- return args.includes(name);
3200
- }
3201
-
3202
- function parsePositiveInteger(value, name) {
3203
- const parsed = Number.parseInt(value, 10);
3204
- if (!Number.isInteger(parsed) || parsed <= 0) {
3205
- throw new UsageError(`${name} must be a positive integer.`);
3206
- }
3207
- return parsed;
3208
- }
3209
-
3210
- async function sleep(ms) {
3211
- await new Promise((resolve) => setTimeout(resolve, ms));
3212
- }
3213
-
3214
-
3215
- async function detectClient(clientId, env) {
3216
- let filePaths = [];
3217
- if (clientId === 'copilot-cli' || clientId === 'copilot') {
3218
- if (process.platform === 'win32' && env.APPDATA) {
3219
- filePaths.push(path.join(env.APPDATA, 'Code', 'User', 'mcp.json'));
3220
- } else {
3221
- const home = env.HOME || os.homedir();
3222
- if (process.platform === 'darwin') {
3223
- filePaths.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'));
3224
- }
3225
- filePaths.push(path.join(home, '.config', 'Code', 'User', 'mcp.json'));
3226
- }
3227
- filePaths.push(defaultCopilotConfigPath(env));
3228
- } else {
3229
- const client = MCP_CLIENTS.get(clientId);
3230
- if (client) {
3231
- filePaths.push(client.defaultConfigPath(env));
3232
- }
3233
- }
3234
-
3235
- if (clientId === 'cline') {
3236
- if (process.platform === 'win32' && env.APPDATA) {
3237
- filePaths.push(path.join(env.APPDATA, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
3238
- } else {
3239
- const home = env.HOME || os.homedir();
3240
- if (process.platform === 'darwin') {
3241
- filePaths.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
3242
- }
3243
- filePaths.push(path.join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
3244
- }
3245
- }
3246
-
3247
- for (const filePath of filePaths) {
3248
- if (await fileExists(filePath)) {
3249
- return { detected: true, path: filePath };
3250
- }
3251
- const parentDir = path.dirname(filePath);
3252
- if (await fileExists(parentDir)) {
3253
- return { detected: true, path: filePath };
3254
- }
3255
- }
3256
-
3257
- return { detected: false };
3258
- }
3259
-
3260
- function npmExecutable() {
3261
- return os.platform() === 'win32' ? 'npm.cmd' : 'npm';
3262
- }
3263
-
3264
-
3265
- async function runProcess(command, args, io, { stream = true } = {}) {
3266
- const spawnFn = io.spawn ?? spawn;
3267
- return await new Promise((resolve, reject) => {
3268
- const child = spawnFn(command, args, {
3269
- stdio: ['ignore', 'pipe', 'pipe'],
3270
- shell: os.platform() === 'win32'
3271
- });
3272
- let stdout = '';
3273
- let stderr = '';
3274
- child.stdout?.on('data', (chunk) => {
3275
- const text = String(chunk);
3276
- stdout += text;
3277
- if (stream) {
3278
- io.stdout.write(text);
3279
- }
3280
- });
3281
- child.stderr?.on('data', (chunk) => {
3282
- const text = String(chunk);
3283
- stderr += text;
3284
- if (stream) {
3285
- io.stderr.write(text);
3286
- }
3287
- });
3288
- child.on('error', reject);
3289
- child.on('close', (code) => {
3290
- resolve({ code: code ?? 0, stdout, stderr });
3291
- });
3292
- });
3293
- }
3294
-
3295
-
3296
- async function readAll(stream) {
3297
- let content = '';
3298
- for await (const chunk of stream) {
3299
- content += chunk;
3300
- }
3301
- return content;
3302
- }
3303
-
3304
- async function fileExists(filePath) {
3305
- try {
3306
- await fs.access(filePath);
3307
- return true;
3308
- } catch {
3309
- return false;
3310
- }
3311
- }
3312
-
3313
- async function readTextIfExists(filePath) {
3314
- try {
3315
- return await fs.readFile(filePath, 'utf8');
3316
- } catch (error) {
3317
- if (error.code === 'ENOENT') {
3318
- return '';
3319
- }
3320
- throw error;
3321
- }
3322
- }
3323
-
3324
- function parseJsonConfig(content, configPath) {
3325
- try {
3326
- return JSON.parse(content);
3327
- } catch (error) {
3328
- throw new UsageError(`Invalid JSON in ${configPath}: ${error.message}`);
3329
- }
3330
- }
3331
-
3332
- function isPlainObject(value) {
3333
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
3334
- }
3335
-
3336
- function escapeTomlString(value) {
3337
- return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
3338
- }
3339
-
3340
- function unescapeTomlString(value) {
3341
- return value.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
3342
- }
3343
-
3344
- function escapeRegExp(value) {
3345
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3346
- }
3347
-
3348
- function defaultWindsurfConfigPath(env) {
3349
- const home = env.USERPROFILE || env.HOME || os.homedir();
3350
- return path.join(home, '.codeium', 'windsurf', 'mcp_config.json');
3351
- }
3352
-
3353
- function defaultClineConfigPath(env) {
3354
- const home = env.USERPROFILE || env.HOME || os.homedir();
3355
- return path.join(home, 'Documents', 'Cline', 'MCP', 'cline_mcp_settings.json');
3356
- }
3357
-
3358
- function defaultContinueConfigPath(env) {
3359
- const home = env.USERPROFILE || env.HOME || os.homedir();
3360
- return path.join(home, '.continue', 'config.json');
3361
- }
3362
-
3363
- function defaultClaudeConfigPath(env) {
3364
- if (process.platform === 'win32' && env.APPDATA) {
3365
- return path.join(env.APPDATA, 'Claude', 'claude_desktop_config.json');
3366
- }
3367
- const home = env.HOME || os.homedir();
3368
- if (process.platform === 'darwin') {
3369
- return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
3370
- }
3371
- return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
3372
- }
3373
-
3374
- function windsurfJsonServerConfig(mcpUrl, identity = envReferenceIdentity('windsurf')) {
3375
- return {
3376
- serverUrl: mcpUrl,
3377
- headers: {
3378
- Authorization: `Bearer \${env:${TOKEN_ENV_VAR}}`,
3379
- [AGENT_ID_HEADER]: identity.agentId,
3380
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
3381
- }
3382
- };
3383
- }
3384
-
3385
- function windsurfJsonConfig(mcpUrl, identity = envReferenceIdentity('windsurf')) {
3386
- return {
3387
- mcpServers: {
3388
- [MCP_SERVER_NAME]: windsurfJsonServerConfig(mcpUrl, identity)
3389
- }
3390
- };
3391
- }
3392
-
3393
- function windsurfJsonSnippet(mcpUrl, identity = envReferenceIdentity('windsurf')) {
3394
- return `${JSON.stringify(windsurfJsonConfig(mcpUrl, identity), null, 2)}\n`;
3395
- }
3396
-
3397
- async function mergeWindsurfMcpConfig(configPath, mcpUrl, identity) {
3398
- const existing = await readTextIfExists(configPath);
3399
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
3400
- if (!isPlainObject(parsed)) {
3401
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
3402
- }
3403
- if (!isPlainObject(parsed.mcpServers)) {
3404
- parsed.mcpServers = {};
3405
- }
3406
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
3407
- if (existingName) {
3408
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
3409
- }
3410
- parsed.mcpServers[MCP_SERVER_NAME] = windsurfJsonServerConfig(mcpUrl, identity);
3411
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3412
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
3413
- await bestEffortChmod(configPath, 0o600);
3414
- }
3415
-
3416
- function clineJsonServerConfig(mcpUrl, identity = envReferenceIdentity('cline')) {
3417
- return {
3418
- httpUrl: mcpUrl,
3419
- headers: {
3420
- Authorization: `Bearer \${env:${TOKEN_ENV_VAR}}`,
3421
- [AGENT_ID_HEADER]: identity.agentId,
3422
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
3423
- }
3424
- };
3425
- }
3426
-
3427
- function clineJsonConfig(mcpUrl, identity = envReferenceIdentity('cline')) {
3428
- return {
3429
- mcpServers: {
3430
- [MCP_SERVER_NAME]: clineJsonServerConfig(mcpUrl, identity)
3431
- }
3432
- };
3433
- }
3434
-
3435
- function clineJsonSnippet(mcpUrl, identity = envReferenceIdentity('cline')) {
3436
- return `${JSON.stringify(clineJsonConfig(mcpUrl, identity), null, 2)}\n`;
3437
- }
3438
-
3439
- async function mergeClineMcpConfig(configPath, mcpUrl, identity) {
3440
- const existing = await readTextIfExists(configPath);
3441
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
3442
- if (!isPlainObject(parsed)) {
3443
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
3444
- }
3445
- if (!isPlainObject(parsed.mcpServers)) {
3446
- parsed.mcpServers = {};
3447
- }
3448
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
3449
- if (existingName) {
3450
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
3451
- }
3452
- parsed.mcpServers[MCP_SERVER_NAME] = clineJsonServerConfig(mcpUrl, identity);
3453
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3454
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
3455
- await bestEffortChmod(configPath, 0o600);
3456
- }
3457
-
3458
- function continueJsonServerConfig(mcpUrl, identity = envReferenceIdentity('continue')) {
3459
- return {
3460
- transport: {
3461
- type: 'streamable-http',
3462
- url: mcpUrl,
3463
- headers: {
3464
- Authorization: `Bearer \${${TOKEN_ENV_VAR}}`,
3465
- [AGENT_ID_HEADER]: identity.agentId,
3466
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
3467
- }
3468
- }
3469
- };
3470
- }
3471
-
3472
- function continueJsonConfig(mcpUrl, identity = envReferenceIdentity('continue')) {
3473
- return {
3474
- mcpServers: {
3475
- [MCP_SERVER_NAME]: continueJsonServerConfig(mcpUrl, identity)
3476
- }
3477
- };
3478
- }
3479
-
3480
- function continueJsonSnippet(mcpUrl, identity = envReferenceIdentity('continue')) {
3481
- return `${JSON.stringify(continueJsonConfig(mcpUrl, identity), null, 2)}\n`;
3482
- }
3483
-
3484
- async function mergeContinueMcpConfig(configPath, mcpUrl, identity) {
3485
- const existing = await readTextIfExists(configPath);
3486
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
3487
- if (!isPlainObject(parsed)) {
3488
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
3489
- }
3490
- if (!isPlainObject(parsed.mcpServers)) {
3491
- parsed.mcpServers = {};
3492
- }
3493
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
3494
- if (existingName) {
3495
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
3496
- }
3497
- parsed.mcpServers[MCP_SERVER_NAME] = continueJsonServerConfig(mcpUrl, identity);
3498
-
3499
- if (isPlainObject(parsed.experimental)) {
3500
- if (!Array.isArray(parsed.experimental.modelContextProtocolServers)) {
3501
- parsed.experimental.modelContextProtocolServers = [];
3502
- }
3503
- const hasXMemo = parsed.experimental.modelContextProtocolServers.some(
3504
- (srv) => srv.transport && srv.transport.url === mcpUrl
3505
- );
3506
- if (!hasXMemo) {
3507
- parsed.experimental.modelContextProtocolServers.push(continueJsonServerConfig(mcpUrl, identity));
3508
- }
3509
- }
3510
-
3511
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3512
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
3513
- await bestEffortChmod(configPath, 0o600);
3514
- }
3515
-
3516
- function claudeJsonServerConfig(mcpUrl, identity = envReferenceIdentity('claude-desktop')) {
3517
- return {
3518
- command: 'npx',
3519
- args: [
3520
- '-y',
3521
- 'mcp-remote',
3522
- mcpUrl,
3523
- '--header',
3524
- `Authorization:Bearer \${${TOKEN_ENV_VAR}}`,
3525
- '--header',
3526
- `X-Memory-OS-Agent-ID:${identity.agentId}`,
3527
- '--header',
3528
- `X-Memory-OS-Agent-Instance-ID:\${${AGENT_INSTANCE_ENV_VAR}}`
3529
- ],
3530
- env: {
3531
- [TOKEN_ENV_VAR]: `\${env:${TOKEN_ENV_VAR}}`,
3532
- [AGENT_INSTANCE_ENV_VAR]: identity.agentInstanceId
3533
- }
3534
- };
3535
- }
3536
-
3537
- function claudeJsonConfig(mcpUrl, identity = envReferenceIdentity('claude-desktop')) {
3538
- return {
3539
- mcpServers: {
3540
- [MCP_SERVER_NAME]: claudeJsonServerConfig(mcpUrl, identity)
3541
- }
3542
- };
3543
- }
3544
-
3545
- function claudeJsonSnippet(mcpUrl, identity = envReferenceIdentity('claude-desktop')) {
3546
- return `${JSON.stringify(claudeJsonConfig(mcpUrl, identity), null, 2)}\n`;
3547
- }
3548
-
3549
- async function mergeClaudeMcpConfig(configPath, mcpUrl, identity) {
3550
- const existing = await readTextIfExists(configPath);
3551
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
3552
- if (!isPlainObject(parsed)) {
3553
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
3554
- }
3555
- if (!isPlainObject(parsed.mcpServers)) {
3556
- parsed.mcpServers = {};
3557
- }
3558
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
3559
- if (existingName) {
3560
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
3561
- }
3562
- parsed.mcpServers[MCP_SERVER_NAME] = claudeJsonServerConfig(mcpUrl, identity);
3563
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3564
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
3565
- await bestEffortChmod(configPath, 0o600);
3566
- }
3567
-
3568
- function defaultOpenclawConfigPath(env) {
3569
- const home = env.USERPROFILE || env.HOME || os.homedir();
3570
- return path.join(home, '.openclaw', 'openclaw.json');
3571
- }
3572
-
3573
- function openclawJsonServerConfig(mcpUrl, identity = envReferenceIdentity('openclaw')) {
3574
- return {
3575
- url: mcpUrl,
3576
- headers: {
3577
- Authorization: `Bearer \${env:${TOKEN_ENV_VAR}}`,
3578
- [AGENT_ID_HEADER]: identity.agentId,
3579
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
3580
- }
3581
- };
3582
- }
3583
-
3584
- function openclawJsonConfig(mcpUrl, identity = envReferenceIdentity('openclaw')) {
3585
- return {
3586
- mcpServers: {
3587
- [MCP_SERVER_NAME]: openclawJsonServerConfig(mcpUrl, identity)
3588
- }
3589
- };
3590
- }
3591
-
3592
- function openclawJsonSnippet(mcpUrl, identity = envReferenceIdentity('openclaw')) {
3593
- return `${JSON.stringify(openclawJsonConfig(mcpUrl, identity), null, 2)}\n`;
3594
- }
3595
-
3596
- async function mergeOpenclawMcpConfig(configPath, mcpUrl, identity) {
3597
- const existing = await readTextIfExists(configPath);
3598
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
3599
- if (!isPlainObject(parsed)) {
3600
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
3601
- }
3602
- if (!isPlainObject(parsed.mcpServers)) {
3603
- parsed.mcpServers = {};
3604
- }
3605
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
3606
- if (existingName) {
3607
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
3608
- }
3609
- parsed.mcpServers[MCP_SERVER_NAME] = openclawJsonServerConfig(mcpUrl, identity);
3610
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3611
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
3612
- await bestEffortChmod(configPath, 0o600);
3613
- }
3614
-
3615
- function defaultKiroConfigPath(env) {
3616
- const home = env.USERPROFILE || env.HOME || os.homedir();
3617
- return path.join(home, '.kiro', 'settings', 'mcp.json');
3618
- }
3619
-
3620
- function kiroJsonServerConfig(mcpUrl, identity = envReferenceIdentity('kiro')) {
3621
- return {
3622
- url: mcpUrl,
3623
- headers: {
3624
- Authorization: `Bearer \${env:${TOKEN_ENV_VAR}}`,
3625
- [AGENT_ID_HEADER]: identity.agentId,
3626
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
3627
- }
3628
- };
3629
- }
3630
-
3631
- function kiroJsonConfig(mcpUrl, identity = envReferenceIdentity('kiro')) {
3632
- return {
3633
- mcpServers: {
3634
- [MCP_SERVER_NAME]: kiroJsonServerConfig(mcpUrl, identity)
3635
- }
3636
- };
3637
- }
3638
-
3639
- function kiroJsonSnippet(mcpUrl, identity = envReferenceIdentity('kiro')) {
3640
- return `${JSON.stringify(kiroJsonConfig(mcpUrl, identity), null, 2)}\n`;
3641
- }
3642
-
3643
- async function mergeKiroMcpConfig(configPath, mcpUrl, identity) {
3644
- const existing = await readTextIfExists(configPath);
3645
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
3646
- if (!isPlainObject(parsed)) {
3647
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
3648
- }
3649
- if (!isPlainObject(parsed.mcpServers)) {
3650
- parsed.mcpServers = {};
3651
- }
3652
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
3653
- if (existingName) {
3654
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
3655
- }
3656
- parsed.mcpServers[MCP_SERVER_NAME] = kiroJsonServerConfig(mcpUrl, identity);
3657
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3658
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
3659
- await bestEffortChmod(configPath, 0o600);
3660
- }
3661
-
3662
- function defaultZedConfigPath(env) {
3663
- if (process.platform === 'win32' && env.APPDATA) {
3664
- return path.join(env.APPDATA, 'Zed', 'settings.json');
3665
- }
3666
- const home = env.HOME || env.USERPROFILE || os.homedir();
3667
- return path.join(home, '.config', 'zed', 'settings.json');
3668
- }
3669
-
3670
- function zedJsonServerConfig(mcpUrl, identity = envReferenceIdentity('zed')) {
3671
- return {
3672
- command: 'npx',
3673
- args: [
3674
- '-y',
3675
- 'mcp-remote',
3676
- mcpUrl,
3677
- '--header',
3678
- `Authorization:Bearer \${${TOKEN_ENV_VAR}}`,
3679
- '--header',
3680
- `X-Memory-OS-Agent-ID:${identity.agentId}`,
3681
- '--header',
3682
- `X-Memory-OS-Agent-Instance-ID:\${${AGENT_INSTANCE_ENV_VAR}}`
3683
- ],
3684
- env: {
3685
- [TOKEN_ENV_VAR]: `\${env:${TOKEN_ENV_VAR}}`,
3686
- [AGENT_INSTANCE_ENV_VAR]: identity.agentInstanceId
3687
- }
3688
- };
3689
- }
3690
-
3691
- function zedJsonConfig(mcpUrl, identity = envReferenceIdentity('zed')) {
3692
- return {
3693
- context_servers: {
3694
- [MCP_SERVER_NAME]: zedJsonServerConfig(mcpUrl, identity)
3695
- }
3696
- };
3697
- }
3698
-
3699
- function zedJsonSnippet(mcpUrl, identity = envReferenceIdentity('zed')) {
3700
- return `${JSON.stringify(zedJsonConfig(mcpUrl, identity), null, 2)}\n`;
3701
- }
3702
-
3703
- async function mergeZedMcpConfig(configPath, mcpUrl, identity) {
3704
- const existing = await readTextIfExists(configPath);
3705
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
3706
- if (!isPlainObject(parsed)) {
3707
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
3708
- }
3709
- if (!isPlainObject(parsed.context_servers)) {
3710
- parsed.context_servers = {};
3711
- }
3712
- const existingName = existingJsonMcpServerName(parsed.context_servers);
3713
- if (existingName) {
3714
- throw new UsageError(`MCP config already contains context_servers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
3715
- }
3716
- parsed.context_servers[MCP_SERVER_NAME] = zedJsonServerConfig(mcpUrl, identity);
3717
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3718
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
3719
- await bestEffortChmod(configPath, 0o600);
3720
- }
3721
-
3722
- function defaultJetbrainsConfigPath(env) {
3723
- const home = env.USERPROFILE || env.HOME || os.homedir();
3724
- return path.join(home, '.continue', 'config.json');
3725
- }
3726
-
3727
- function jetbrainsJsonServerConfig(mcpUrl, identity = envReferenceIdentity('jetbrains')) {
3728
- return continueJsonServerConfig(mcpUrl, identity);
3729
- }
3730
-
3731
- function jetbrainsJsonConfig(mcpUrl, identity = envReferenceIdentity('jetbrains')) {
3732
- return continueJsonConfig(mcpUrl, identity);
3733
- }
3734
-
3735
- function jetbrainsJsonSnippet(mcpUrl, identity = envReferenceIdentity('jetbrains')) {
3736
- return continueJsonSnippet(mcpUrl, identity);
3737
- }
3738
-
3739
- async function mergeJetbrainsMcpConfig(configPath, mcpUrl, identity) {
3740
- await mergeContinueMcpConfig(configPath, mcpUrl, identity);
3741
- }
3742
-
3743
- function defaultOpencodeConfigPath(env) {
3744
- const home = env.USERPROFILE || env.HOME || os.homedir();
3745
- return path.join(home, '.config', 'opencode', 'opencode.json');
3746
- }
3747
-
3748
- function opencodeJsonServerConfig(mcpUrl, identity = envReferenceIdentity('opencode')) {
3749
- return {
3750
- type: 'remote',
3751
- url: mcpUrl,
3752
- enabled: true,
3753
- headers: {
3754
- [AGENT_ID_HEADER]: identity.agentId,
3755
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
3756
- }
3757
- };
3758
- }
3759
-
3760
- function opencodeJsonConfig(mcpUrl, identity = envReferenceIdentity('opencode')) {
3761
- return {
3762
- mcp: {
3763
- [MCP_SERVER_NAME]: opencodeJsonServerConfig(mcpUrl, identity)
3764
- }
3765
- };
3766
- }
3767
-
3768
- function opencodeJsonSnippet(mcpUrl, identity = envReferenceIdentity('opencode')) {
3769
- return `${JSON.stringify(opencodeJsonConfig(mcpUrl, identity), null, 2)}\n`;
3770
- }
3771
-
3772
- async function mergeOpencodeMcpConfig(configPath, mcpUrl, identity) {
3773
- const existing = await readTextIfExists(configPath);
3774
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
3775
- if (!isPlainObject(parsed)) {
3776
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
3777
- }
3778
- if (!isPlainObject(parsed.mcp)) {
3779
- parsed.mcp = {};
3780
- }
3781
- const existingName = existingJsonMcpServerName(parsed.mcp);
3782
- if (existingName) {
3783
- throw new UsageError(`MCP config already contains mcp.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
3784
- }
3785
- parsed.mcp[MCP_SERVER_NAME] = opencodeJsonServerConfig(mcpUrl, identity);
3786
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3787
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
3788
- await bestEffortChmod(configPath, 0o600);
3789
- }
3790
-
3791
- function defaultHermesConfigPath(env) {
3792
- const home = env.USERPROFILE || env.HOME || os.homedir();
3793
- return path.join(home, '.hermes', 'config.yaml');
3794
- }
3795
-
3796
- function hermesYamlSnippet(mcpUrl, identity = envReferenceIdentity('hermes')) {
3797
- return `mcp_servers:
3798
- ${MCP_SERVER_NAME}:
3799
- command: npx
3800
- args:
3801
- - -y
3802
- - mcp-remote
3803
- - ${mcpUrl}
3804
- - --header
3805
- - "Authorization:Bearer \${${TOKEN_ENV_VAR}}"
3806
- - --header
3807
- - "X-Memory-OS-Agent-ID:${identity.agentId}"
3808
- - --header
3809
- - "X-Memory-OS-Agent-Instance-ID:\${${AGENT_INSTANCE_ENV_VAR}}"
3810
- env:
3811
- ${TOKEN_ENV_VAR}: "\${env:${TOKEN_ENV_VAR}}"
3812
- ${AGENT_INSTANCE_ENV_VAR}: "${identity.agentInstanceId}"
3813
- `;
3814
- }
3815
-
3816
- async function mergeHermesMcpConfig(configPath, mcpUrl, identity) {
3817
- const existing = await readTextIfExists(configPath);
3818
-
3819
- if (existing.includes(`${MCP_SERVER_NAME}:`) || existing.includes('memory_os:') || existing.includes('memory-os:')) {
3820
- throw new UsageError(`MCP config already contains ${MCP_SERVER_NAME} in mcp_servers. Edit ${configPath} manually to avoid duplicate server definitions.`);
3821
- }
3822
-
3823
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3824
-
3825
- if (existing.trim().length === 0) {
3826
- await fs.writeFile(configPath, hermesYamlSnippet(mcpUrl, identity), { mode: 0o600 });
3827
- } else if (existing.includes('mcp_servers:')) {
3828
- const replacement = `mcp_servers:
3829
- ${MCP_SERVER_NAME}:
3830
- command: npx
3831
- args:
3832
- - -y
3833
- - mcp-remote
3834
- - ${mcpUrl}
3835
- env:
3836
- ${TOKEN_ENV_VAR}: "\${env:${TOKEN_ENV_VAR}}"
3837
- ${AGENT_INSTANCE_ENV_VAR}: "${identity.agentInstanceId}"`;
3838
- const updated = existing.replace('mcp_servers:', replacement);
3839
- await fs.writeFile(configPath, updated, { mode: 0o600 });
3840
- } else {
3841
- const prefix = existing.endsWith('\n') ? '' : '\n';
3842
- await fs.appendFile(configPath, `${prefix}${hermesYamlSnippet(mcpUrl, identity)}`, { mode: 0o600 });
3843
- }
3844
- await bestEffortChmod(configPath, 0o600);
3845
- }
3846
-
3847
- function defaultQwenConfigPath(env) {
3848
- const home = env.USERPROFILE || env.HOME || os.homedir();
3849
- return path.join(home, '.qwen', 'settings.json');
3850
- }
3851
-
3852
- function qwenJsonServerConfig(mcpUrl, identity = envReferenceIdentity('qwen')) {
3853
- return {
3854
- httpUrl: mcpUrl,
3855
- headers: {
3856
- [AGENT_ID_HEADER]: identity.agentId,
3857
- [AGENT_INSTANCE_HEADER]: identity.agentInstanceId
3858
- }
3859
- };
3860
- }
3861
-
3862
- // Reuse the trae config layout which uses mcpServers
3863
- function qwenJsonConfig(mcpUrl, identity = envReferenceIdentity('qwen')) {
3864
- return {
3865
- mcpServers: {
3866
- [MCP_SERVER_NAME]: qwenJsonServerConfig(mcpUrl, identity)
3867
- }
3868
- };
3869
- }
3870
-
3871
- function qwenJsonSnippet(mcpUrl, identity = envReferenceIdentity('qwen')) {
3872
- return `${JSON.stringify(qwenJsonConfig(mcpUrl, identity), null, 2)}\n`;
3873
- }
3874
-
3875
- async function mergeQwenMcpConfig(configPath, mcpUrl, identity) {
3876
- const existing = await readTextIfExists(configPath);
3877
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
3878
- if (!isPlainObject(parsed)) {
3879
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
3880
- }
3881
- if (!isPlainObject(parsed.mcpServers)) {
3882
- parsed.mcpServers = {};
3883
- }
3884
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
3885
- if (existingName) {
3886
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
3887
- }
3888
- parsed.mcpServers[MCP_SERVER_NAME] = qwenJsonServerConfig(mcpUrl, identity);
3889
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3890
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
3891
- await bestEffortChmod(configPath, 0o600);
3892
- }
3893
-
3894
- function defaultTraeConfigPath(env) {
3895
- if (process.platform === 'win32' && env.APPDATA) {
3896
- return path.join(env.APPDATA, 'Trae', 'User', 'mcp.json');
3897
- }
3898
- const home = env.USERPROFILE || env.HOME || os.homedir();
3899
- if (process.platform === 'darwin') {
3900
- return path.join(home, 'Library', 'Application Support', 'Trae', 'User', 'mcp.json');
3901
- }
3902
- return path.join(home, '.config', 'Trae', 'User', 'mcp.json');
3903
- }
3904
-
3905
- function traeJsonServerConfig(mcpUrl, identity = envReferenceIdentity('trae')) {
3906
- return {
3907
- command: 'npx',
3908
- args: [
3909
- '-y',
3910
- 'mcp-remote',
3911
- mcpUrl,
3912
- '--header',
3913
- `Authorization:Bearer \${${TOKEN_ENV_VAR}}`,
3914
- '--header',
3915
- `X-Memory-OS-Agent-ID:${identity.agentId}`,
3916
- '--header',
3917
- `X-Memory-OS-Agent-Instance-ID:\${${AGENT_INSTANCE_ENV_VAR}}`
3918
- ],
3919
- env: {
3920
- [TOKEN_ENV_VAR]: `\${env:${TOKEN_ENV_VAR}}`,
3921
- [AGENT_INSTANCE_ENV_VAR]: identity.agentInstanceId
3922
- }
3923
- };
3924
- }
3925
-
3926
- function traeJsonConfig(mcpUrl, identity = envReferenceIdentity('trae')) {
3927
- return {
3928
- mcpServers: {
3929
- [MCP_SERVER_NAME]: traeJsonServerConfig(mcpUrl, identity)
3930
- }
3931
- };
3932
- }
3933
-
3934
- // Return Trae MCP config snippet
3935
- function traeJsonSnippet(mcpUrl, identity = envReferenceIdentity('trae')) {
3936
- return `${JSON.stringify(traeJsonConfig(mcpUrl, identity), null, 2)}\n`;
3937
- }
3938
-
3939
- async function mergeTraeMcpConfig(configPath, mcpUrl, identity) {
3940
- const existing = await readTextIfExists(configPath);
3941
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
3942
- if (!isPlainObject(parsed)) {
3943
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
3944
- }
3945
- if (!isPlainObject(parsed.mcpServers)) {
3946
- parsed.mcpServers = {};
3947
- }
3948
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
3949
- if (existingName) {
3950
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
3951
- }
3952
- parsed.mcpServers[MCP_SERVER_NAME] = traeJsonServerConfig(mcpUrl, identity);
3953
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
3954
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
3955
- await bestEffortChmod(configPath, 0o600);
3956
- }
3957
-
3958
- function defaultTraeSoloConfigPath(env) {
3959
- if (process.platform === 'win32' && env.APPDATA) {
3960
- return path.join(env.APPDATA, 'TRAE SOLO', 'User', 'mcp.json');
3961
- }
3962
- const home = env.USERPROFILE || env.HOME || os.homedir();
3963
- if (process.platform === 'darwin') {
3964
- return path.join(home, 'Library', 'Application Support', 'TRAE SOLO', 'User', 'mcp.json');
3965
- }
3966
- return path.join(home, '.config', 'TRAE SOLO', 'User', 'mcp.json');
3967
- }
3968
-
3969
- function traeSoloJsonServerConfig(mcpUrl, identity = envReferenceIdentity('trae-solo')) {
3970
- return {
3971
- command: 'npx',
3972
- args: [
3973
- '-y',
3974
- 'mcp-remote',
3975
- mcpUrl,
3976
- '--header',
3977
- `Authorization:Bearer \${${TOKEN_ENV_VAR}}`,
3978
- '--header',
3979
- `X-Memory-OS-Agent-ID:${identity.agentId}`,
3980
- '--header',
3981
- `X-Memory-OS-Agent-Instance-ID:\${${AGENT_INSTANCE_ENV_VAR}}`
3982
- ],
3983
- env: {
3984
- [TOKEN_ENV_VAR]: `\${env:${TOKEN_ENV_VAR}}`,
3985
- [AGENT_INSTANCE_ENV_VAR]: identity.agentInstanceId
3986
- }
3987
- };
3988
- }
3989
-
3990
- function traeSoloJsonConfig(mcpUrl, identity = envReferenceIdentity('trae-solo')) {
3991
- return {
3992
- mcpServers: {
3993
- [MCP_SERVER_NAME]: traeSoloJsonServerConfig(mcpUrl, identity)
3994
- }
3995
- };
3996
- }
3997
-
3998
- function traeSoloJsonSnippet(mcpUrl, identity = envReferenceIdentity('trae-solo')) {
3999
- return `${JSON.stringify(traeSoloJsonConfig(mcpUrl, identity), null, 2)}\n`;
4000
- }
4001
-
4002
- async function mergeTraeSoloMcpConfig(configPath, mcpUrl, identity) {
4003
- const existing = await readTextIfExists(configPath);
4004
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
4005
- if (!isPlainObject(parsed)) {
4006
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
4007
- }
4008
- if (!isPlainObject(parsed.mcpServers)) {
4009
- parsed.mcpServers = {};
4010
- }
4011
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
4012
- if (existingName) {
4013
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
4014
- }
4015
- parsed.mcpServers[MCP_SERVER_NAME] = traeSoloJsonServerConfig(mcpUrl, identity);
4016
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
4017
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
4018
- await bestEffortChmod(configPath, 0o600);
4019
- }
4020
-
4021
- function defaultClaudecodeConfigPath(env) {
4022
- const home = env.USERPROFILE || env.HOME || os.homedir();
4023
- return path.join(home, '.claude.json');
4024
- }
4025
-
4026
- function claudecodeJsonServerConfig(mcpUrl, identity = envReferenceIdentity('claude-code')) {
4027
- return {
4028
- command: 'npx',
4029
- args: [
4030
- '-y',
4031
- 'mcp-remote',
4032
- mcpUrl
4033
- ],
4034
- env: {
4035
- [TOKEN_ENV_VAR]: `\${env:${TOKEN_ENV_VAR}}`,
4036
- [AGENT_INSTANCE_ENV_VAR]: identity.agentInstanceId
4037
- }
4038
- };
4039
- }
4040
-
4041
- function claudecodeJsonConfig(mcpUrl, identity = envReferenceIdentity('claude-code')) {
4042
- return {
4043
- mcpServers: {
4044
- [MCP_SERVER_NAME]: claudecodeJsonServerConfig(mcpUrl, identity)
4045
- }
4046
- };
4047
- }
4048
-
4049
- function claudecodeJsonSnippet(mcpUrl, identity = envReferenceIdentity('claude-code')) {
4050
- return `${JSON.stringify(claudecodeJsonConfig(mcpUrl, identity), null, 2)}\n`;
4051
- }
4052
-
4053
- async function mergeClaudecodeMcpConfig(configPath, mcpUrl, identity) {
4054
- const existing = await readTextIfExists(configPath);
4055
- const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
4056
- if (!isPlainObject(parsed)) {
4057
- throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
4058
- }
4059
- if (!isPlainObject(parsed.mcpServers)) {
4060
- parsed.mcpServers = {};
4061
- }
4062
- const existingName = existingJsonMcpServerName(parsed.mcpServers);
4063
- if (existingName) {
4064
- throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
4065
- }
4066
- parsed.mcpServers[MCP_SERVER_NAME] = claudecodeJsonServerConfig(mcpUrl, identity);
4067
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
4068
- await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
4069
- await bestEffortChmod(configPath, 0o600);
4070
- }
4071
-
4072
- function writeLine(stream, line) {
4073
- stream.write(`${line}\n`);
4074
- }
4075
-
4076
-
4077
-