@xmemo/client 0.4.136 → 0.4.138

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 (3) hide show
  1. package/README.md +52 -20
  2. package/package.json +1 -1
  3. package/src/cli.js +423 -71
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # XMemo CLI
2
2
 
3
+ [![smithery badge](https://smithery.ai/badge/xmemo/xmemo)](https://smithery.ai/servers/xmemo/xmemo)
4
+
3
5
  `@xmemo/client` is the privacy-first command line entry point for XMemo client
4
6
  setup. It is intentionally small: the npm package contains only the CLI and
5
7
  setup helper code needed on a user's machine.
@@ -53,6 +55,8 @@ xmemo mcp config --client generic
53
55
  xmemo mcp config --client antigravity
54
56
  xmemo mcp add antigravity --write
55
57
  xmemo profile status codex
58
+ xmemo profile install gemini
59
+ xmemo profile install antigravity
56
60
  xmemo smoke --client codex
57
61
  xmemo privacy
58
62
  ```
@@ -65,6 +69,9 @@ xmemo privacy
65
69
  MCP OAuth flow; it does not write token values into project files.
66
70
  - The CLI generates one stable non-secret `XMEMO_AGENT_INSTANCE_ID` per local
67
71
  client profile and stores it in user-scoped config outside git.
72
+ - `xmemo setup <client>` can install a marker-scoped XMemo memory behavior
73
+ profile for the selected agent. The profile contains instructions only; it
74
+ never embeds token values.
68
75
  - `xmemo login` stores the issued credential in the user-scoped XMemo CLI
69
76
  config directory, shows the approved account when the server provides it,
70
77
  and does not require extra token configuration afterward.
@@ -166,10 +173,27 @@ also include stable non-secret agent identity headers where the client format
166
173
  supports them. `--yes` remains accepted for Codex and Cursor as a compatibility
167
174
  no-op.
168
175
 
169
- `xmemo setup codex` is the recommended Codex path. It writes the Codex MCP
170
- config and installs the profile into the current project's `AGENTS.md` marker
171
- block. Use `--dry-run` to preview, `--profile-target <path>` to choose a
172
- different project instruction file, or `--no-profile` to configure MCP only.
176
+ After writing MCP config, `xmemo setup <client>` prompts:
177
+
178
+ ```text
179
+ Write XMemo memory behavior profile to <path>? [Y/n]
180
+ ```
181
+
182
+ The default is `Y`, so pressing Enter writes a marker-scoped profile that nudges
183
+ the agent to recall/search XMemo at the start of non-trivial work and remember
184
+ high-signal decisions after meaningful changes. Use `n` or `--no-profile` to
185
+ configure MCP only. Use `--dry-run` to preview without writing config or profile
186
+ files, and `--profile-target <path>` to choose a different behavior profile
187
+ target.
188
+
189
+ Default behavior profile targets:
190
+
191
+ ```text
192
+ codex ./AGENTS.md
193
+ cursor ~/.cursor/memory-profile.md
194
+ gemini ~/.gemini/GEMINI.md
195
+ antigravity ~/.gemini/antigravity/MEMORY.md
196
+ ```
173
197
 
174
198
  ## MCP setup
175
199
 
@@ -213,7 +237,7 @@ xmemo setup copilot
213
237
  xmemo mcp proxy
214
238
  ```
215
239
 
216
- `xmemo setup copilot` writes `memory-os` to Copilot CLI's user MCP config and
240
+ `xmemo setup copilot` writes `XMemo` to Copilot CLI's user MCP config and
217
241
  does not include token or identity headers. Use `xmemo setup copilot --dry-run`
218
242
  to preview without writing. `xmemo mcp proxy` reads the token saved by
219
243
  `xmemo login` or `xmemo token add --from-stdin`, adds the XMemo bearer token and
@@ -235,9 +259,10 @@ xmemo setup codex
235
259
  xmemo smoke --client codex
236
260
  ```
237
261
 
238
- `setup codex` writes the MCP config to user-scoped Codex config and installs the
239
- XMemo Codex behavior profile into the current project's `AGENTS.md` between
240
- these markers. Use `xmemo setup codex --dry-run` to preview without writing.
262
+ `setup codex` writes the MCP config to user-scoped Codex config and, by default,
263
+ installs the XMemo Codex behavior profile into the current project's `AGENTS.md`
264
+ between these markers. Use `xmemo setup codex --dry-run` to preview without
265
+ writing or `xmemo setup codex --no-profile` to skip the behavior profile.
241
266
 
242
267
  ```html
243
268
  <!-- memory-os:codex-profile:start -->
@@ -262,9 +287,9 @@ Write it to the default Codex config path:
262
287
  xmemo mcp add codex --url "$XMEMO_URL" --write
263
288
  ```
264
289
 
265
- The generated config references `XMEMO_KEY` and does not include the token
266
- value. Codex custom identity headers are not written until the CLI format is
267
- verified to support them.
290
+ The generated config references `XMEMO_KEY`, includes the non-secret
291
+ `X-Memory-OS-Agent-ID` / `X-Memory-OS-Agent-Instance-ID` attribution headers,
292
+ and does not include the token value.
268
293
 
269
294
  Codex MCP-depth checks:
270
295
 
@@ -279,7 +304,7 @@ xmemo smoke --client codex
279
304
  `xmemo mcp profile codex` prints the recommended memory behavior profile:
280
305
  recall/search at the start of non-trivial tasks, write back high-signal
281
306
  decisions and fixes, and never store secrets. `xmemo smoke --client codex`
282
- checks the local Codex TOML config for the `memory_os` MCP server,
307
+ checks the local Codex TOML config for the `XMemo` MCP server,
283
308
  `bearer_token_env_var = "XMEMO_KEY"`, token presence in the environment, and
284
309
  absence of embedded token values.
285
310
 
@@ -299,10 +324,13 @@ lower-level equivalent remains:
299
324
  xmemo mcp add cursor --url "$XMEMO_URL" --write
300
325
  ```
301
326
 
302
- The CLI refuses to overwrite an existing `memory_os` MCP server entry. Edit the
303
- config manually if you need to rotate the endpoint. Cursor configs include
327
+ The CLI refuses to overwrite an existing `XMemo`, `memory_os`, or `memory-os`
328
+ MCP server entry. Edit the config manually if you need to rotate the endpoint.
329
+ Cursor configs include
304
330
  `X-Memory-OS-Agent-ID` and `X-Memory-OS-Agent-Instance-ID`; the instance ID is
305
- non-secret and stored under the user's XMemo CLI config directory.
331
+ non-secret and stored under the user's XMemo CLI config directory. By default,
332
+ the setup prompt also installs a Cursor behavior profile at
333
+ `~/.cursor/memory-profile.md`; answer `n` or pass `--no-profile` to skip it.
306
334
 
307
335
  ### Gemini CLI
308
336
 
@@ -323,10 +351,12 @@ This is deliberate — Gemini redacts environment variables matching
323
351
  `*KEY*`/`*TOKEN*`/`*AUTH*` during header expansion, so an `${XMEMO_KEY}`
324
352
  reference would not survive. OAuth avoids storing any secret in the config and
325
353
  still grants the full XMemo tool profile. After setup, restart Gemini CLI and
326
- run `/mcp` (or the first XMemo tool call) to complete the OAuth login.
354
+ run `/mcp` (or the first XMemo tool call) to complete the OAuth login. By
355
+ default, the setup prompt also installs a Gemini behavior profile at
356
+ `~/.gemini/GEMINI.md`; answer `n` or pass `--no-profile` to skip it.
327
357
 
328
- The CLI refuses to overwrite an existing `memory_os` MCP server entry. Edit the
329
- config manually if you need to rotate the endpoint.
358
+ The CLI refuses to overwrite an existing `XMemo`, `memory_os`, or `memory-os`
359
+ MCP server entry. Edit the config manually if you need to rotate the endpoint.
330
360
 
331
361
  ### Antigravity
332
362
 
@@ -341,6 +371,8 @@ at `~/.gemini/antigravity/mcp_config.json`. It writes Antigravity's
341
371
  `serverUrl` shape plus `X-Memory-OS-Agent-ID` and
342
372
  `X-Memory-OS-Agent-Instance-ID` headers. Like Gemini CLI, the config carries
343
373
  **no token**: restart Antigravity and complete the MCP OAuth flow on first use.
374
+ By default, the setup prompt also installs an Antigravity behavior profile at
375
+ `~/.gemini/antigravity/MEMORY.md`; answer `n` or pass `--no-profile` to skip it.
344
376
 
345
377
  The lower-level equivalent is:
346
378
 
@@ -352,8 +384,8 @@ Use `xmemo setup antigravity` for normal installs because it performs discovery
352
384
  and chooses the recommended Antigravity path automatically. Use
353
385
  `xmemo mcp add antigravity --write` when you want the generic MCP writer
354
386
  directly, for example with `--url` or `--config` in advanced/multi-client setup.
355
- The CLI refuses to overwrite an existing `memory_os` MCP server entry. Edit the
356
- config manually if you need to rotate the endpoint.
387
+ The CLI refuses to overwrite an existing `XMemo`, `memory_os`, or `memory-os`
388
+ MCP server entry. Edit the config manually if you need to rotate the endpoint.
357
389
 
358
390
  ## Release model
359
391
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmemo/client",
3
- "version": "0.4.136",
3
+ "version": "0.4.138",
4
4
  "description": "Privacy-first CLI and MCP setup helper for XMemo.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -10,7 +10,7 @@ const PACKAGE_NAME = '@xmemo/client';
10
10
  const FALLBACK_PACKAGE_NAME = '@yonro/xmemo-client';
11
11
  const COMMAND_NAME = 'xmemo';
12
12
  const LEGACY_COMMAND_NAME = 'memory-os';
13
- const CLI_VERSION = '0.4.136';
13
+ const CLI_VERSION = '0.4.138';
14
14
  const DEFAULT_SERVICE_URL = 'https://xmemo.dev';
15
15
  const TOKEN_ENV_VAR = 'XMEMO_KEY';
16
16
  const LEGACY_TOKEN_ENV_VAR = 'MEMORY_OS_MCP_TOKEN';
@@ -18,10 +18,19 @@ const AGENT_ID_ENV_VAR = 'XMEMO_AGENT_ID';
18
18
  const AGENT_INSTANCE_ENV_VAR = 'XMEMO_AGENT_INSTANCE_ID';
19
19
  const AGENT_ID_HEADER = 'X-Memory-OS-Agent-ID';
20
20
  const AGENT_INSTANCE_HEADER = 'X-Memory-OS-Agent-Instance-ID';
21
- const MCP_SERVER_NAME = 'memory_os';
21
+ const MCP_SERVER_NAME = 'XMemo';
22
+ const LEGACY_MCP_SERVER_NAMES = ['memory_os', 'memory-os'];
22
23
  const CODEX_PROFILE_TARGET = 'AGENTS.md';
23
24
  const CODEX_PROFILE_MARKER_START = '<!-- memory-os:codex-profile:start -->';
24
25
  const CODEX_PROFILE_MARKER_END = '<!-- memory-os:codex-profile:end -->';
26
+ const CLIENT_PROFILE_TARGETS = {
27
+ cursor: '.cursor/rules/xmemo-memory.md',
28
+ 'gemini-cli': 'GEMINI.md',
29
+ antigravity: 'GEMINI.md'
30
+ };
31
+ const CLIENT_PROFILE_MARKER_START = '<!-- xmemo:profile:start -->';
32
+ const CLIENT_PROFILE_MARKER_END = '<!-- xmemo:profile:end -->';
33
+ const PROFILE_MARKER_PREFIX = 'memory-os:memory-profile';
25
34
  const DEVICE_LOGIN_START_PATH = '/api/v1/auth/device/start';
26
35
  const DEVICE_LOGIN_TOKEN_PATH = '/api/v1/auth/device/token';
27
36
  const DEFAULT_PROXY_HOST = '127.0.0.1';
@@ -174,7 +183,7 @@ function writeHelp(io) {
174
183
  writeLine(io.stdout, ` ${COMMAND_NAME} update [--dry-run] [--json]`);
175
184
  writeLine(io.stdout, ` ${COMMAND_NAME} doctor [--base-url <https://api.example.com>] [--json]`);
176
185
  writeLine(io.stdout, ` ${COMMAND_NAME} discovery show [--base-url <https://api.example.com>] [--json]`);
177
- writeLine(io.stdout, ` ${COMMAND_NAME} setup <codex|cursor|copilot|gemini|antigravity> [--url <https://api.example.com>] [--dry-run] [--json]`);
186
+ writeLine(io.stdout, ` ${COMMAND_NAME} setup <codex|cursor|copilot|gemini|antigravity> [--url <https://api.example.com>] [--dry-run] [--no-profile] [--json]`);
178
187
  writeLine(io.stdout, ` ${COMMAND_NAME} login [--from-stdin] [--base-url <url>] [--timeout-ms <ms>] [--http-timeout-ms <ms>] [--json]`);
179
188
  writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
180
189
  writeLine(io.stdout, ` ${COMMAND_NAME} status [--url <https://api.example.com>] [--json]`);
@@ -182,11 +191,11 @@ function writeHelp(io) {
182
191
  writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
183
192
  writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
184
193
  writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
185
- writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|antigravity|generic> [--base-url <url>] [--json]`);
194
+ writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|gemini-cli|antigravity|generic> [--base-url <url>] [--json]`);
186
195
  writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}]`);
187
196
  writeLine(io.stdout, ` ${COMMAND_NAME} mcp profile codex [--json]`);
188
- writeLine(io.stdout, ` ${COMMAND_NAME} profile install codex [--target AGENTS.md] [--dry-run|--json]`);
189
- writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target AGENTS.md] [--json]`);
197
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile install <codex|cursor|gemini|antigravity> [--target <path>] [--dry-run|--json]`);
198
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall <codex|cursor|gemini|antigravity> [--target <path>] [--json]`);
190
199
  writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>] [--write] [--config <path>]`);
191
200
  writeLine(io.stdout, ` ${COMMAND_NAME} smoke --client codex [--config <path>] [--json]`);
192
201
  writeLine(io.stdout, ` ${COMMAND_NAME} env example [--shell bash|powershell|cmd] [--json]`);
@@ -388,10 +397,6 @@ async function setupCommand(args, io) {
388
397
  const dryRun = hasFlag(optionArgs, '--dry-run') || hasFlag(optionArgs, '--preview');
389
398
  const writeConfig = !dryRun && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes') || shortClientSetup);
390
399
  const timeoutMs = parsePositiveInteger(optionValue(optionArgs, '--timeout-ms') ?? '5000', '--timeout-ms');
391
- const installProfile = shortClientSetup
392
- && clientId === 'codex'
393
- && writeConfig
394
- && !hasFlag(optionArgs, '--no-profile');
395
400
 
396
401
  if (writeConfig && !clientId) {
397
402
  throw new UsageError(`Setup --write requires --client <${supportedSetupClientIds().join('|')}> so the CLI never writes broad config implicitly.`);
@@ -428,12 +433,32 @@ async function setupCommand(args, io) {
428
433
  setupPlan.selectedClient.written = true;
429
434
  }
430
435
 
431
- if (clientId === 'codex' && shortClientSetup) {
436
+ if (shortClientSetup && profileClientConfig(clientId)) {
432
437
  const profileTarget = optionValue(optionArgs, '--profile-target')
433
438
  ?? optionValue(optionArgs, '--target')
434
- ?? defaultCodexProfileTarget();
435
- const profileResult = await codexProfileInstallResult(profileTarget, { write: installProfile });
436
- setupPlan.selectedClient.codexProfile = profileResult;
439
+ ?? defaultProfileTarget(clientId, io.env);
440
+ let installProfile = false;
441
+ let prompted = false;
442
+ let skipped = false;
443
+ if (hasFlag(optionArgs, '--no-profile')) {
444
+ skipped = true;
445
+ } else if (dryRun) {
446
+ installProfile = false;
447
+ } else if (writeConfig) {
448
+ installProfile = outputJson || hasFlag(optionArgs, '--yes') || hasFlag(optionArgs, '--profile');
449
+ if (!installProfile && !outputJson) {
450
+ prompted = true;
451
+ installProfile = await confirmProfileInstall(clientId, profileTarget, io);
452
+ }
453
+ }
454
+ const profileResult = await profileInstallResult(clientId, profileTarget, { write: installProfile });
455
+ profileResult.prompted = prompted;
456
+ profileResult.accepted = installProfile;
457
+ profileResult.skipped = skipped;
458
+ setupPlan.selectedClient.behaviorProfile = profileResult;
459
+ if (clientId === 'codex') {
460
+ setupPlan.selectedClient.codexProfile = profileResult;
461
+ }
437
462
  }
438
463
  }
439
464
  }
@@ -451,30 +476,30 @@ async function profileCommand(args, io) {
451
476
  const subcommand = args[0] ?? 'help';
452
477
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
453
478
  writeLine(io.stdout, 'Profile commands:');
454
- writeLine(io.stdout, ` ${COMMAND_NAME} profile install codex [--target AGENTS.md] [--dry-run|--json]`);
455
- writeLine(io.stdout, ` ${COMMAND_NAME} profile status codex [--target AGENTS.md] [--json]`);
456
- writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target AGENTS.md] [--json]`);
479
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile install <codex|cursor|gemini|antigravity> [--target <path>] [--dry-run|--json]`);
480
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile status <codex|cursor|gemini|antigravity> [--target <path>] [--json]`);
481
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall <codex|cursor|gemini|antigravity> [--target <path>] [--json]`);
457
482
  writeLine(io.stdout, '');
458
483
  writeLine(io.stdout, 'Profile installs are marker-scoped and never write token values.');
459
484
  return 0;
460
485
  }
461
486
 
462
- const clientId = args[1];
463
- if (clientId !== 'codex') {
464
- throw new UsageError(`Unsupported profile client: ${clientId ?? 'missing'}. Supported clients: codex.`);
487
+ const clientId = normalizeSetupClientId(args[1]);
488
+ if (!profileClientConfig(clientId)) {
489
+ throw new UsageError(`Unsupported profile client: ${args[1] ?? 'missing'}. Supported clients: ${supportedProfileClientIds().join(', ')}.`);
465
490
  }
466
491
 
467
492
  const optionArgs = args.slice(2);
468
493
  const outputJson = hasFlag(optionArgs, '--json');
469
- const targetPath = optionValue(optionArgs, '--target') ?? defaultCodexProfileTarget();
494
+ const targetPath = optionValue(optionArgs, '--target') ?? defaultProfileTarget(clientId, io.env);
470
495
  let result;
471
496
 
472
497
  if (subcommand === 'install') {
473
- result = await codexProfileInstallResult(targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
498
+ result = await profileInstallResult(clientId, targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
474
499
  } else if (subcommand === 'status') {
475
- result = await codexProfileStatusResult(targetPath);
500
+ result = await profileStatusResult(clientId, targetPath);
476
501
  } else if (subcommand === 'uninstall') {
477
- result = await codexProfileUninstallResult(targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
502
+ result = await profileUninstallResult(clientId, targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
478
503
  } else {
479
504
  throw new UsageError(`Unknown profile command: ${subcommand}`);
480
505
  }
@@ -755,6 +780,15 @@ async function mcpCommand(args, io) {
755
780
  } else {
756
781
  writeLine(io.stdout, JSON.stringify(template.snippet, null, 2));
757
782
  }
783
+ if (template.optionalEnv?.includes(AGENT_INSTANCE_ENV_VAR)) {
784
+ writeLine(io.stdout, '');
785
+ writeLine(io.stdout, `${AGENT_INSTANCE_ENV_VAR} must be stable per local client install.`);
786
+ if (template.agentInstanceGeneration?.automaticCommand) {
787
+ writeLine(io.stdout, `Use ${template.agentInstanceGeneration.automaticCommand} to generate and persist it, or set it to a unique value such as xmemo-${clientId}-<uuid>.`);
788
+ } else {
789
+ writeLine(io.stdout, `Set it to a unique value such as xmemo-${clientId}-<uuid> and persist it outside git.`);
790
+ }
791
+ }
758
792
  writeLine(io.stdout, 'Review the template before applying it. Token values are not included.');
759
793
  return 0;
760
794
  }
@@ -805,6 +839,7 @@ async function mcpCommand(args, io) {
805
839
  agentId: identity.agentId,
806
840
  agentInstanceId: identity.agentInstanceId,
807
841
  agentInstanceIdPath: identity.path,
842
+ agentInstanceGeneration: agentInstanceGenerationPolicy(target),
808
843
  writesTokenValue: false
809
844
  }, null, 2));
810
845
  return 0;
@@ -833,6 +868,7 @@ async function mcpCommand(args, io) {
833
868
  } else {
834
869
  writeLine(io.stdout, `Set ${TOKEN_ENV_VAR} in your user environment or secret manager. The token value is not included here.`);
835
870
  }
871
+ 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.`);
836
872
  return 0;
837
873
  }
838
874
 
@@ -1365,10 +1401,15 @@ function mcpConfigTemplate(clientId, mcpUrl) {
1365
1401
  agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1366
1402
  agentInstanceHeader: AGENT_INSTANCE_HEADER
1367
1403
  },
1404
+ agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1368
1405
  writesTokenValue: false
1369
1406
  };
1370
1407
  }
1371
1408
 
1409
+ if (clientId === 'cursor') {
1410
+ return bearerJsonMcpTemplate(clientId, mcpUrl, cursorJsonConfig(mcpUrl));
1411
+ }
1412
+
1372
1413
  if (clientId === 'gemini-cli') {
1373
1414
  return oauthJsonMcpTemplate(clientId, mcpUrl, geminiJsonConfig(mcpUrl));
1374
1415
  }
@@ -1377,14 +1418,13 @@ function mcpConfigTemplate(clientId, mcpUrl) {
1377
1418
  return oauthJsonMcpTemplate(clientId, mcpUrl, antigravityJsonConfig(mcpUrl));
1378
1419
  }
1379
1420
 
1380
- const serverName = clientId === 'cursor' || clientId === 'gemini-cli' || clientId === 'antigravity' ? 'memory_os' : 'memory-os';
1381
1421
  return {
1382
1422
  client: clientId,
1383
- serverName,
1423
+ serverName: MCP_SERVER_NAME,
1384
1424
  snippetFormat: 'json',
1385
1425
  snippet: {
1386
1426
  mcpServers: {
1387
- [serverName]: {
1427
+ [MCP_SERVER_NAME]: {
1388
1428
  type: 'http',
1389
1429
  url: mcpUrl,
1390
1430
  headers: {
@@ -1403,6 +1443,28 @@ function mcpConfigTemplate(clientId, mcpUrl) {
1403
1443
  agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1404
1444
  agentInstanceHeader: AGENT_INSTANCE_HEADER
1405
1445
  },
1446
+ agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1447
+ writesTokenValue: false
1448
+ };
1449
+ }
1450
+
1451
+ function bearerJsonMcpTemplate(clientId, mcpUrl, snippet) {
1452
+ return {
1453
+ client: clientId,
1454
+ serverName: MCP_SERVER_NAME,
1455
+ snippetFormat: 'json',
1456
+ snippet,
1457
+ requiresEnv: [TOKEN_ENV_VAR],
1458
+ optionalEnv: [AGENT_INSTANCE_ENV_VAR],
1459
+ authentication: 'env-bearer',
1460
+ agentIdentity: {
1461
+ agentId: clientId,
1462
+ agentIdHeader: AGENT_ID_HEADER,
1463
+ agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1464
+ agentInstanceHeader: AGENT_INSTANCE_HEADER
1465
+ },
1466
+ agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1467
+ mcpUrl,
1406
1468
  writesTokenValue: false
1407
1469
  };
1408
1470
  }
@@ -1422,20 +1484,20 @@ function oauthJsonMcpTemplate(clientId, mcpUrl, snippet) {
1422
1484
  agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1423
1485
  agentInstanceHeader: AGENT_INSTANCE_HEADER
1424
1486
  },
1487
+ agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1425
1488
  mcpUrl,
1426
1489
  writesTokenValue: false
1427
1490
  };
1428
1491
  }
1429
1492
 
1430
1493
  function mcpLocalProxyTemplate(clientId, proxyUrl) {
1431
- const serverName = clientId === 'cursor' || clientId === 'gemini-cli' || clientId === 'antigravity' ? 'memory_os' : 'memory-os';
1432
1494
  return {
1433
1495
  client: clientId,
1434
- serverName,
1496
+ serverName: MCP_SERVER_NAME,
1435
1497
  snippetFormat: 'json',
1436
1498
  snippet: {
1437
1499
  mcpServers: {
1438
- [serverName]: {
1500
+ [MCP_SERVER_NAME]: {
1439
1501
  type: 'http',
1440
1502
  url: proxyUrl
1441
1503
  }
@@ -1449,10 +1511,27 @@ function mcpLocalProxyTemplate(clientId, proxyUrl) {
1449
1511
  agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1450
1512
  agentInstanceHeader: AGENT_INSTANCE_HEADER
1451
1513
  },
1514
+ agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1452
1515
  writesTokenValue: false
1453
1516
  };
1454
1517
  }
1455
1518
 
1519
+ function agentInstanceGenerationPolicy(clientId) {
1520
+ const automaticCommand = MCP_CLIENTS.has(clientId)
1521
+ ? `${COMMAND_NAME} mcp add ${clientId} --write`
1522
+ : clientId === 'copilot-cli'
1523
+ ? `${COMMAND_NAME} setup copilot --write`
1524
+ : null;
1525
+ return {
1526
+ requiredForHeaders: true,
1527
+ stablePerInstall: true,
1528
+ automaticCommand,
1529
+ generatedPattern: `xmemo-${clientId}-<uuid>`,
1530
+ storagePath: `~/.config/xmemo/agent-instances/${clientId}.json`,
1531
+ manualEnvVar: AGENT_INSTANCE_ENV_VAR
1532
+ };
1533
+ }
1534
+
1456
1535
  function sameMajorMinor(left, right) {
1457
1536
  const leftParts = left.split('.');
1458
1537
  const rightParts = right.split('.');
@@ -1549,13 +1628,15 @@ function writeSetupSummary(plan, io) {
1549
1628
  }
1550
1629
  return;
1551
1630
  }
1552
- if (plan.selectedClient.codexProfile) {
1553
- const profile = plan.selectedClient.codexProfile;
1554
- writeLine(io.stdout, ` Codex profile target: ${profile.targetPath}`);
1555
- writeLine(io.stdout, ` Codex profile installed: ${profile.written}`);
1556
- writeLine(io.stdout, ` Codex profile changed: ${profile.changed}`);
1631
+ if (plan.selectedClient.behaviorProfile) {
1632
+ const profile = plan.selectedClient.behaviorProfile;
1633
+ const profileClient = profileClientConfig(profile.client);
1634
+ writeLine(io.stdout, ` Behavior profile target: ${profile.targetPath}`);
1635
+ writeLine(io.stdout, ` Behavior profile client: ${profileClient?.label ?? profile.client}`);
1636
+ writeLine(io.stdout, ` Behavior profile installed: ${profile.written}`);
1637
+ writeLine(io.stdout, ` Behavior profile changed: ${profile.changed}`);
1557
1638
  if (!profile.written) {
1558
- writeLine(io.stdout, ` Profile preview: ${COMMAND_NAME} profile install codex --target ${profile.targetPath}`);
1639
+ writeLine(io.stdout, ` Profile preview: ${COMMAND_NAME} profile install ${profile.client} --target ${profile.targetPath}`);
1559
1640
  }
1560
1641
  }
1561
1642
  if (!plan.selectedClient.written) {
@@ -1618,23 +1699,7 @@ bearer_token_env_var = "${TOKEN_ENV_VAR}"
1618
1699
  }
1619
1700
 
1620
1701
  function codexMemoryProfile() {
1621
- return {
1622
- client: 'codex',
1623
- profileVersion: 'codex-mcp-depth-v1',
1624
- mcpServerName: MCP_SERVER_NAME,
1625
- requiredTokenEnv: TOKEN_ENV_VAR,
1626
- objective: 'Use XMemo deliberately through MCP for project context recall and high-signal write-back.',
1627
- instructions: [
1628
- 'At the start of a non-trivial task, call XMemo recall/search for relevant project decisions, conventions, prior fixes, and active context unless the user explicitly asks not to use memory.',
1629
- 'Use recalled memories as evidence, not as unquestioned truth. Prefer current repository files when memory conflicts with code.',
1630
- 'After meaningful decisions, bug fixes, release steps, or durable conventions, write a concise XMemo memory with scope, source, and no secret values.',
1631
- 'Never store tokens, API keys, cookies, private keys, raw credentials, or sensitive customer data in XMemo.',
1632
- 'For routine or low-signal output, skip durable writes. Prefer summarized procedural or semantic memories over verbose logs.',
1633
- 'Keep XMemo authentication through the XMEMO_KEY environment variable; do not paste token values into prompts, config files, or logs.'
1634
- ],
1635
- setupCommand: `${COMMAND_NAME} setup codex --url "$XMEMO_URL"`,
1636
- smokeCommand: `${COMMAND_NAME} smoke --client codex`
1637
- };
1702
+ return memoryBehaviorProfile('codex');
1638
1703
  }
1639
1704
 
1640
1705
  function writeCodexMemoryProfile(profile, io) {
@@ -1653,17 +1718,51 @@ function writeCodexMemoryProfile(profile, io) {
1653
1718
  }
1654
1719
 
1655
1720
  function codexProfileInstructionText() {
1656
- const profile = codexMemoryProfile();
1721
+ return profileInstructionText('codex');
1722
+ }
1723
+
1724
+ function memoryBehaviorProfile(clientId) {
1725
+ const config = profileClientConfig(clientId);
1726
+ if (!config) {
1727
+ throw new UsageError(`Unsupported profile client: ${clientId}`);
1728
+ }
1729
+ const instructions = [
1730
+ '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.',
1731
+ 'Use recalled memories as evidence, not as unquestioned truth. Prefer current repository files when memory conflicts with code.',
1732
+ 'After meaningful decisions, bug fixes, release steps, or durable conventions, write a concise XMemo memory with scope, source, and no secret values.',
1733
+ 'Never store tokens, API keys, cookies, private keys, raw credentials, or sensitive customer data in XMemo.',
1734
+ 'For routine or low-signal output, skip durable writes. Prefer summarized procedural or semantic memories over verbose logs.',
1735
+ config.authInstruction
1736
+ ];
1737
+ return {
1738
+ client: clientId,
1739
+ label: config.label,
1740
+ profileVersion: config.profileVersion,
1741
+ mcpServerName: MCP_SERVER_NAME,
1742
+ requiredTokenEnv: config.requiredTokenEnv ?? null,
1743
+ objective: 'Use XMemo deliberately through MCP for project context recall and high-signal write-back.',
1744
+ instructions,
1745
+ setupCommand: `${COMMAND_NAME} setup ${config.setupAlias} --url "$XMEMO_URL"`,
1746
+ smokeCommand: clientId === 'codex' ? `${COMMAND_NAME} smoke --client codex` : null
1747
+ };
1748
+ }
1749
+
1750
+ function profileInstructionText(clientId) {
1751
+ const profile = memoryBehaviorProfile(clientId);
1657
1752
  const lines = [
1658
- '## XMemo Codex profile',
1753
+ `## XMemo ${profile.label} profile`,
1659
1754
  '',
1660
1755
  `MCP server: \`${profile.mcpServerName}\``,
1661
- `Token env var: \`${profile.requiredTokenEnv}\``,
1756
+ ];
1757
+ if (profile.requiredTokenEnv) {
1758
+ lines.push(`Token env var: \`${profile.requiredTokenEnv}\``);
1759
+ }
1760
+ lines.push(
1662
1761
  '',
1663
1762
  profile.objective,
1664
1763
  '',
1665
- 'Recommended Codex behavior:'
1666
- ];
1764
+ `Recommended ${profile.label} behavior:`
1765
+ );
1667
1766
  for (const instruction of profile.instructions) {
1668
1767
  lines.push(`- ${instruction}`);
1669
1768
  }
@@ -1671,6 +1770,228 @@ function codexProfileInstructionText() {
1671
1770
  return `${lines.join('\n')}\n`;
1672
1771
  }
1673
1772
 
1773
+ function profileClientConfig(clientId) {
1774
+ const profileConfigs = {
1775
+ codex: {
1776
+ label: 'Codex',
1777
+ setupAlias: 'codex',
1778
+ profileVersion: 'codex-mcp-depth-v1',
1779
+ requiredTokenEnv: TOKEN_ENV_VAR,
1780
+ markerStart: CODEX_PROFILE_MARKER_START,
1781
+ markerEnd: CODEX_PROFILE_MARKER_END,
1782
+ defaultTarget: (env) => defaultCodexProfileTarget(env),
1783
+ authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
1784
+ },
1785
+ cursor: {
1786
+ label: 'Cursor',
1787
+ setupAlias: 'cursor',
1788
+ profileVersion: 'cursor-mcp-depth-v1',
1789
+ requiredTokenEnv: TOKEN_ENV_VAR,
1790
+ markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:cursor:start -->`,
1791
+ markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:cursor:end -->`,
1792
+ defaultTarget: (env) => path.join(userHome(env), '.cursor', 'memory-profile.md'),
1793
+ authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
1794
+ },
1795
+ 'gemini-cli': {
1796
+ label: 'Gemini CLI',
1797
+ setupAlias: 'gemini',
1798
+ profileVersion: 'gemini-cli-mcp-depth-v1',
1799
+ markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:gemini-cli:start -->`,
1800
+ markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:gemini-cli:end -->`,
1801
+ defaultTarget: (env) => path.join(userHome(env), '.gemini', 'GEMINI.md'),
1802
+ authInstruction: 'Use the client-managed MCP OAuth credential; do not paste token values into prompts, config files, or logs.'
1803
+ },
1804
+ antigravity: {
1805
+ label: 'Antigravity',
1806
+ setupAlias: 'antigravity',
1807
+ profileVersion: 'antigravity-mcp-depth-v1',
1808
+ markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:antigravity:start -->`,
1809
+ markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:antigravity:end -->`,
1810
+ defaultTarget: (env) => path.join(userHome(env), '.gemini', 'antigravity', 'MEMORY.md'),
1811
+ authInstruction: 'Use the client-managed MCP OAuth credential; do not paste token values into prompts, config files, or logs.'
1812
+ }
1813
+ };
1814
+ return profileConfigs[clientId] ?? null;
1815
+ }
1816
+
1817
+ function supportedProfileClientIds() {
1818
+ return ['codex', 'cursor', 'gemini', 'antigravity'];
1819
+ }
1820
+
1821
+ function defaultProfileTarget(clientId, env) {
1822
+ const config = profileClientConfig(clientId);
1823
+ if (!config) {
1824
+ throw new UsageError(`Unsupported profile client: ${clientId}`);
1825
+ }
1826
+ return config.defaultTarget(env);
1827
+ }
1828
+
1829
+ async function confirmProfileInstall(clientId, targetPath, io) {
1830
+ const config = profileClientConfig(clientId);
1831
+ writeLine(io.stdout, '');
1832
+ writeLine(io.stdout, `Write XMemo memory behavior profile to ${targetPath}? [Y/n]`);
1833
+ const answer = (await readLineFromStdin(io.stdin)).trim().toLowerCase();
1834
+ if (answer === '' || answer === 'y' || answer === 'yes') {
1835
+ return true;
1836
+ }
1837
+ if (answer === 'n' || answer === 'no') {
1838
+ return false;
1839
+ }
1840
+ throw new UsageError(`Unsupported response for ${config.label} profile prompt: ${answer}`);
1841
+ }
1842
+
1843
+ async function readLineFromStdin(stdin) {
1844
+ let input = '';
1845
+ for await (const chunk of stdin) {
1846
+ input += chunk;
1847
+ if (input.includes('\n')) {
1848
+ break;
1849
+ }
1850
+ }
1851
+ return input.split(/\r?\n/, 1)[0] ?? '';
1852
+ }
1853
+
1854
+ function genericProfileMarkerBlock(clientId) {
1855
+ const config = profileClientConfig(clientId);
1856
+ return `${config.markerStart}\n${profileInstructionText(clientId)}${config.markerEnd}\n`;
1857
+ }
1858
+
1859
+ async function profileInstallResult(clientId, targetPath, options = {}) {
1860
+ if (clientId === 'codex') {
1861
+ return codexProfileInstallResult(targetPath, options);
1862
+ }
1863
+ const config = profileClientConfig(clientId);
1864
+ const resolvedTarget = path.resolve(targetPath);
1865
+ const existing = await readTextIfExists(resolvedTarget);
1866
+ const marker = profileMarkerBounds(existing, config);
1867
+ const block = genericProfileMarkerBlock(clientId);
1868
+ let nextText;
1869
+
1870
+ if (marker.present) {
1871
+ nextText = `${existing.slice(0, marker.start)}${block}${existing.slice(marker.end)}`;
1872
+ } else if (existing.trim().length === 0) {
1873
+ nextText = block;
1874
+ } else {
1875
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
1876
+ nextText = `${existing}${separator}${block}`;
1877
+ }
1878
+
1879
+ const changed = nextText !== existing;
1880
+ const write = Boolean(options.write);
1881
+ if (write && changed) {
1882
+ await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
1883
+ await fs.writeFile(resolvedTarget, nextText);
1884
+ }
1885
+
1886
+ return {
1887
+ client: clientId,
1888
+ action: 'install',
1889
+ targetPath: resolvedTarget,
1890
+ markerStart: config.markerStart,
1891
+ markerEnd: config.markerEnd,
1892
+ installed: marker.present || (write && changed),
1893
+ written: write,
1894
+ changed,
1895
+ markerPresent: marker.present,
1896
+ writesTokenValue: false
1897
+ };
1898
+ }
1899
+
1900
+ async function profileStatusResult(clientId, targetPath) {
1901
+ if (clientId === 'codex') {
1902
+ return codexProfileStatusResult(targetPath);
1903
+ }
1904
+ const config = profileClientConfig(clientId);
1905
+ const resolvedTarget = path.resolve(targetPath);
1906
+ const existing = await readTextIfExists(resolvedTarget);
1907
+ const marker = profileMarkerBounds(existing, config);
1908
+ return {
1909
+ client: clientId,
1910
+ action: 'status',
1911
+ targetPath: resolvedTarget,
1912
+ installed: marker.present,
1913
+ markerPresent: marker.present,
1914
+ markerStart: config.markerStart,
1915
+ markerEnd: config.markerEnd,
1916
+ writesTokenValue: false
1917
+ };
1918
+ }
1919
+
1920
+ async function profileUninstallResult(clientId, targetPath, options = {}) {
1921
+ if (clientId === 'codex') {
1922
+ return codexProfileUninstallResult(targetPath, options);
1923
+ }
1924
+ const config = profileClientConfig(clientId);
1925
+ const resolvedTarget = path.resolve(targetPath);
1926
+ const existing = await readTextIfExists(resolvedTarget);
1927
+ const marker = profileMarkerBounds(existing, config);
1928
+ const write = Boolean(options.write);
1929
+ let changed = false;
1930
+
1931
+ if (marker.present) {
1932
+ let nextText = `${existing.slice(0, marker.start)}${existing.slice(marker.end)}`;
1933
+ nextText = nextText.replace(/\n{3,}/g, '\n\n');
1934
+ if (nextText.trim().length === 0) {
1935
+ nextText = '';
1936
+ } else if (!nextText.endsWith('\n')) {
1937
+ nextText = `${nextText}\n`;
1938
+ }
1939
+ changed = nextText !== existing;
1940
+ if (write && changed) {
1941
+ await fs.writeFile(resolvedTarget, nextText);
1942
+ }
1943
+ }
1944
+
1945
+ return {
1946
+ client: clientId,
1947
+ action: 'uninstall',
1948
+ targetPath: resolvedTarget,
1949
+ installed: marker.present && !(write && changed),
1950
+ written: write,
1951
+ changed,
1952
+ markerPresent: marker.present,
1953
+ markerStart: config.markerStart,
1954
+ markerEnd: config.markerEnd,
1955
+ writesTokenValue: false
1956
+ };
1957
+ }
1958
+
1959
+ function profileMarkerBounds(content, config) {
1960
+ const start = content.indexOf(config.markerStart);
1961
+ const end = content.indexOf(config.markerEnd);
1962
+ if (start === -1 && end === -1) {
1963
+ return { present: false, start: -1, end: -1 };
1964
+ }
1965
+
1966
+ if (start === -1 || end === -1 || end < start) {
1967
+ throw new UsageError(`${config.label} profile markers are incomplete or out of order; edit the target file manually before retrying.`);
1968
+ }
1969
+
1970
+ if (
1971
+ content.indexOf(config.markerStart, start + config.markerStart.length) !== -1
1972
+ || content.indexOf(config.markerEnd, end + config.markerEnd.length) !== -1
1973
+ ) {
1974
+ throw new UsageError(`${config.label} profile markers appear more than once; edit the target file manually before retrying.`);
1975
+ }
1976
+
1977
+ const afterEnd = end + config.markerEnd.length;
1978
+ const trailingNewlineLength = content.slice(afterEnd, afterEnd + 2) === '\r\n'
1979
+ ? 2
1980
+ : content.slice(afterEnd, afterEnd + 1) === '\n'
1981
+ ? 1
1982
+ : 0;
1983
+
1984
+ return {
1985
+ present: true,
1986
+ start,
1987
+ end: afterEnd + trailingNewlineLength
1988
+ };
1989
+ }
1990
+
1991
+ function userHome(env) {
1992
+ return env.USERPROFILE || env.HOME || os.homedir();
1993
+ }
1994
+
1674
1995
  function codexProfileMarkerBlock() {
1675
1996
  return `${CODEX_PROFILE_MARKER_START}\n${codexProfileInstructionText()}${CODEX_PROFILE_MARKER_END}\n`;
1676
1997
  }
@@ -1800,7 +2121,8 @@ function markerBounds(content) {
1800
2121
  }
1801
2122
 
1802
2123
  function writeProfileResult(action, result, io) {
1803
- writeLine(io.stdout, `${PRODUCT_NAME} Codex profile ${action}`);
2124
+ const config = profileClientConfig(result.client);
2125
+ writeLine(io.stdout, `${PRODUCT_NAME} ${config?.label ?? result.client} profile ${action}`);
1804
2126
  writeLine(io.stdout, ` Target: ${result.targetPath}`);
1805
2127
  writeLine(io.stdout, ` Installed: ${result.installed}`);
1806
2128
  if ('written' in result) {
@@ -1812,7 +2134,8 @@ function writeProfileResult(action, result, io) {
1812
2134
 
1813
2135
  async function codexSmokeReport(configPath, env) {
1814
2136
  const configText = await readTextIfExists(configPath);
1815
- const block = tomlServerBlock(configText, MCP_SERVER_NAME);
2137
+ const serverBlock = findTomlServerBlock(configText);
2138
+ const block = serverBlock.block;
1816
2139
  const mcpUrl = block ? tomlStringValue(block, 'url') : null;
1817
2140
  const bearerTokenEnvVar = block ? tomlStringValue(block, 'bearer_token_env_var') : null;
1818
2141
  const tokenValue = env[TOKEN_ENV_VAR] ?? '';
@@ -1829,7 +2152,7 @@ async function codexSmokeReport(configPath, env) {
1829
2152
  name: 'memory_os_server_present',
1830
2153
  ok: Boolean(block),
1831
2154
  required: true,
1832
- detail: block ? `[mcp_servers.${MCP_SERVER_NAME}]` : `missing [mcp_servers.${MCP_SERVER_NAME}]`
2155
+ detail: block ? `[mcp_servers.${serverBlock.name}]` : `missing [mcp_servers.${MCP_SERVER_NAME}]`
1833
2156
  },
1834
2157
  {
1835
2158
  name: 'mcp_url_present',
@@ -1867,7 +2190,7 @@ async function codexSmokeReport(configPath, env) {
1867
2190
  ok: checks.every((check) => !check.required || check.ok),
1868
2191
  client: 'codex',
1869
2192
  configPath,
1870
- serverName: MCP_SERVER_NAME,
2193
+ serverName: serverBlock.name ?? MCP_SERVER_NAME,
1871
2194
  mcpUrl,
1872
2195
  tokenEnvVar: TOKEN_ENV_VAR,
1873
2196
  agentInstanceIdPath: identityPath,
@@ -1875,6 +2198,26 @@ async function codexSmokeReport(configPath, env) {
1875
2198
  };
1876
2199
  }
1877
2200
 
2201
+ function knownMcpServerNames() {
2202
+ return [MCP_SERVER_NAME, ...LEGACY_MCP_SERVER_NAMES];
2203
+ }
2204
+
2205
+ function existingJsonMcpServerName(mcpServers) {
2206
+ return knownMcpServerNames().find((name) => mcpServers[name]);
2207
+ }
2208
+
2209
+ function existingTomlMcpServerName(content) {
2210
+ return knownMcpServerNames().find((name) => content.includes(`[mcp_servers.${name}]`));
2211
+ }
2212
+
2213
+ function findTomlServerBlock(content) {
2214
+ const name = existingTomlMcpServerName(content);
2215
+ return {
2216
+ name: name ?? null,
2217
+ block: name ? tomlServerBlock(content, name) : ''
2218
+ };
2219
+ }
2220
+
1878
2221
  function tomlServerBlock(content, serverName) {
1879
2222
  const header = `[mcp_servers.${serverName}]`;
1880
2223
  const lines = content.split(/\r?\n/);
@@ -1907,8 +2250,9 @@ function cursorJsonSnippet(mcpUrl, identity = envReferenceIdentity('cursor')) {
1907
2250
  async function appendTomlServerConfig(configPath, mcpUrl) {
1908
2251
  const snippet = codexTomlSnippet(mcpUrl);
1909
2252
  const existing = await readTextIfExists(configPath);
1910
- if (existing.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
1911
- throw new UsageError(`MCP config already contains [mcp_servers.${MCP_SERVER_NAME}]. Edit ${configPath} manually to avoid duplicate server definitions.`);
2253
+ const existingName = existingTomlMcpServerName(existing);
2254
+ if (existingName) {
2255
+ throw new UsageError(`MCP config already contains [mcp_servers.${existingName}]. Edit ${configPath} manually to avoid duplicate server definitions.`);
1912
2256
  }
1913
2257
 
1914
2258
  await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
@@ -1929,8 +2273,9 @@ async function mergeJsonMcpConfig(configPath, mcpUrl, identity) {
1929
2273
  parsed.mcpServers = {};
1930
2274
  }
1931
2275
 
1932
- if (parsed.mcpServers[MCP_SERVER_NAME]) {
1933
- throw new UsageError(`MCP config already contains mcpServers.${MCP_SERVER_NAME}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2276
+ const existingName = existingJsonMcpServerName(parsed.mcpServers);
2277
+ if (existingName) {
2278
+ throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
1934
2279
  }
1935
2280
 
1936
2281
  parsed.mcpServers[MCP_SERVER_NAME] = cursorJsonServerConfig(mcpUrl, identity);
@@ -1973,8 +2318,9 @@ async function mergeAntigravityMcpConfig(configPath, mcpUrl, identity) {
1973
2318
  parsed.mcpServers = {};
1974
2319
  }
1975
2320
 
1976
- if (parsed.mcpServers[MCP_SERVER_NAME]) {
1977
- throw new UsageError(`MCP config already contains mcpServers.${MCP_SERVER_NAME}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2321
+ const existingName = existingJsonMcpServerName(parsed.mcpServers);
2322
+ if (existingName) {
2323
+ throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
1978
2324
  }
1979
2325
 
1980
2326
  parsed.mcpServers[MCP_SERVER_NAME] = antigravityJsonServerConfig(mcpUrl, identity);
@@ -1996,8 +2342,9 @@ async function mergeGeminiMcpConfig(configPath, mcpUrl, identity) {
1996
2342
  parsed.mcpServers = {};
1997
2343
  }
1998
2344
 
1999
- if (parsed.mcpServers[MCP_SERVER_NAME]) {
2000
- throw new UsageError(`MCP config already contains mcpServers.${MCP_SERVER_NAME}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2345
+ const existingName = existingJsonMcpServerName(parsed.mcpServers);
2346
+ if (existingName) {
2347
+ throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2001
2348
  }
2002
2349
 
2003
2350
  parsed.mcpServers[MCP_SERVER_NAME] = geminiJsonServerConfig(mcpUrl, identity);
@@ -2018,7 +2365,12 @@ async function mergeCopilotMcpConfig(configPath, proxyUrl) {
2018
2365
  parsed.mcpServers = {};
2019
2366
  }
2020
2367
 
2021
- parsed.mcpServers['memory-os'] = copilotLocalProxyServerConfig(proxyUrl);
2368
+ const existingName = existingJsonMcpServerName(parsed.mcpServers);
2369
+ if (existingName) {
2370
+ throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
2371
+ }
2372
+
2373
+ parsed.mcpServers[MCP_SERVER_NAME] = copilotLocalProxyServerConfig(proxyUrl);
2022
2374
  await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2023
2375
  await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2024
2376
  await bestEffortChmod(configPath, 0o600);