@xmemo/client 0.4.136 → 0.4.137

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 +38 -9
  2. package/package.json +1 -1
  3. package/src/cli.js +318 -51
package/README.md CHANGED
@@ -53,6 +53,8 @@ xmemo mcp config --client generic
53
53
  xmemo mcp config --client antigravity
54
54
  xmemo mcp add antigravity --write
55
55
  xmemo profile status codex
56
+ xmemo profile install gemini
57
+ xmemo profile install antigravity
56
58
  xmemo smoke --client codex
57
59
  xmemo privacy
58
60
  ```
@@ -65,6 +67,9 @@ xmemo privacy
65
67
  MCP OAuth flow; it does not write token values into project files.
66
68
  - The CLI generates one stable non-secret `XMEMO_AGENT_INSTANCE_ID` per local
67
69
  client profile and stores it in user-scoped config outside git.
70
+ - `xmemo setup <client>` can install a marker-scoped XMemo memory behavior
71
+ profile for the selected agent. The profile contains instructions only; it
72
+ never embeds token values.
68
73
  - `xmemo login` stores the issued credential in the user-scoped XMemo CLI
69
74
  config directory, shows the approved account when the server provides it,
70
75
  and does not require extra token configuration afterward.
@@ -166,10 +171,27 @@ also include stable non-secret agent identity headers where the client format
166
171
  supports them. `--yes` remains accepted for Codex and Cursor as a compatibility
167
172
  no-op.
168
173
 
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.
174
+ After writing MCP config, `xmemo setup <client>` prompts:
175
+
176
+ ```text
177
+ Write XMemo memory behavior profile to <path>? [Y/n]
178
+ ```
179
+
180
+ The default is `Y`, so pressing Enter writes a marker-scoped profile that nudges
181
+ the agent to recall/search XMemo at the start of non-trivial work and remember
182
+ high-signal decisions after meaningful changes. Use `n` or `--no-profile` to
183
+ configure MCP only. Use `--dry-run` to preview without writing config or profile
184
+ files, and `--profile-target <path>` to choose a different behavior profile
185
+ target.
186
+
187
+ Default behavior profile targets:
188
+
189
+ ```text
190
+ codex ./AGENTS.md
191
+ cursor ~/.cursor/memory-profile.md
192
+ gemini ~/.gemini/GEMINI.md
193
+ antigravity ~/.gemini/antigravity/MEMORY.md
194
+ ```
173
195
 
174
196
  ## MCP setup
175
197
 
@@ -235,9 +257,10 @@ xmemo setup codex
235
257
  xmemo smoke --client codex
236
258
  ```
237
259
 
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.
260
+ `setup codex` writes the MCP config to user-scoped Codex config and, by default,
261
+ installs the XMemo Codex behavior profile into the current project's `AGENTS.md`
262
+ between these markers. Use `xmemo setup codex --dry-run` to preview without
263
+ writing or `xmemo setup codex --no-profile` to skip the behavior profile.
241
264
 
242
265
  ```html
243
266
  <!-- memory-os:codex-profile:start -->
@@ -302,7 +325,9 @@ xmemo mcp add cursor --url "$XMEMO_URL" --write
302
325
  The CLI refuses to overwrite an existing `memory_os` MCP server entry. Edit the
303
326
  config manually if you need to rotate the endpoint. Cursor configs include
304
327
  `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.
328
+ non-secret and stored under the user's XMemo CLI config directory. By default,
329
+ the setup prompt also installs a Cursor behavior profile at
330
+ `~/.cursor/memory-profile.md`; answer `n` or pass `--no-profile` to skip it.
306
331
 
307
332
  ### Gemini CLI
308
333
 
@@ -323,7 +348,9 @@ This is deliberate — Gemini redacts environment variables matching
323
348
  `*KEY*`/`*TOKEN*`/`*AUTH*` during header expansion, so an `${XMEMO_KEY}`
324
349
  reference would not survive. OAuth avoids storing any secret in the config and
325
350
  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.
351
+ run `/mcp` (or the first XMemo tool call) to complete the OAuth login. By
352
+ default, the setup prompt also installs a Gemini behavior profile at
353
+ `~/.gemini/GEMINI.md`; answer `n` or pass `--no-profile` to skip it.
327
354
 
328
355
  The CLI refuses to overwrite an existing `memory_os` MCP server entry. Edit the
329
356
  config manually if you need to rotate the endpoint.
@@ -341,6 +368,8 @@ at `~/.gemini/antigravity/mcp_config.json`. It writes Antigravity's
341
368
  `serverUrl` shape plus `X-Memory-OS-Agent-ID` and
342
369
  `X-Memory-OS-Agent-Instance-ID` headers. Like Gemini CLI, the config carries
343
370
  **no token**: restart Antigravity and complete the MCP OAuth flow on first use.
371
+ By default, the setup prompt also installs an Antigravity behavior profile at
372
+ `~/.gemini/antigravity/MEMORY.md`; answer `n` or pass `--no-profile` to skip it.
344
373
 
345
374
  The lower-level equivalent is:
346
375
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmemo/client",
3
- "version": "0.4.136",
3
+ "version": "0.4.137",
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.137';
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';
@@ -22,6 +22,14 @@ const MCP_SERVER_NAME = 'memory_os';
22
22
  const CODEX_PROFILE_TARGET = 'AGENTS.md';
23
23
  const CODEX_PROFILE_MARKER_START = '<!-- memory-os:codex-profile:start -->';
24
24
  const CODEX_PROFILE_MARKER_END = '<!-- memory-os:codex-profile:end -->';
25
+ const CLIENT_PROFILE_TARGETS = {
26
+ cursor: '.cursor/rules/xmemo-memory.md',
27
+ 'gemini-cli': 'GEMINI.md',
28
+ antigravity: 'GEMINI.md'
29
+ };
30
+ const CLIENT_PROFILE_MARKER_START = '<!-- xmemo:profile:start -->';
31
+ const CLIENT_PROFILE_MARKER_END = '<!-- xmemo:profile:end -->';
32
+ const PROFILE_MARKER_PREFIX = 'memory-os:memory-profile';
25
33
  const DEVICE_LOGIN_START_PATH = '/api/v1/auth/device/start';
26
34
  const DEVICE_LOGIN_TOKEN_PATH = '/api/v1/auth/device/token';
27
35
  const DEFAULT_PROXY_HOST = '127.0.0.1';
@@ -174,7 +182,7 @@ function writeHelp(io) {
174
182
  writeLine(io.stdout, ` ${COMMAND_NAME} update [--dry-run] [--json]`);
175
183
  writeLine(io.stdout, ` ${COMMAND_NAME} doctor [--base-url <https://api.example.com>] [--json]`);
176
184
  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]`);
185
+ writeLine(io.stdout, ` ${COMMAND_NAME} setup <codex|cursor|copilot|gemini|antigravity> [--url <https://api.example.com>] [--dry-run] [--no-profile] [--json]`);
178
186
  writeLine(io.stdout, ` ${COMMAND_NAME} login [--from-stdin] [--base-url <url>] [--timeout-ms <ms>] [--http-timeout-ms <ms>] [--json]`);
179
187
  writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
180
188
  writeLine(io.stdout, ` ${COMMAND_NAME} status [--url <https://api.example.com>] [--json]`);
@@ -185,8 +193,8 @@ function writeHelp(io) {
185
193
  writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|antigravity|generic> [--base-url <url>] [--json]`);
186
194
  writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}]`);
187
195
  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]`);
196
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile install <codex|cursor|gemini|antigravity> [--target <path>] [--dry-run|--json]`);
197
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall <codex|cursor|gemini|antigravity> [--target <path>] [--json]`);
190
198
  writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>] [--write] [--config <path>]`);
191
199
  writeLine(io.stdout, ` ${COMMAND_NAME} smoke --client codex [--config <path>] [--json]`);
192
200
  writeLine(io.stdout, ` ${COMMAND_NAME} env example [--shell bash|powershell|cmd] [--json]`);
@@ -388,10 +396,6 @@ async function setupCommand(args, io) {
388
396
  const dryRun = hasFlag(optionArgs, '--dry-run') || hasFlag(optionArgs, '--preview');
389
397
  const writeConfig = !dryRun && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes') || shortClientSetup);
390
398
  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
399
 
396
400
  if (writeConfig && !clientId) {
397
401
  throw new UsageError(`Setup --write requires --client <${supportedSetupClientIds().join('|')}> so the CLI never writes broad config implicitly.`);
@@ -428,12 +432,32 @@ async function setupCommand(args, io) {
428
432
  setupPlan.selectedClient.written = true;
429
433
  }
430
434
 
431
- if (clientId === 'codex' && shortClientSetup) {
435
+ if (shortClientSetup && profileClientConfig(clientId)) {
432
436
  const profileTarget = optionValue(optionArgs, '--profile-target')
433
437
  ?? optionValue(optionArgs, '--target')
434
- ?? defaultCodexProfileTarget();
435
- const profileResult = await codexProfileInstallResult(profileTarget, { write: installProfile });
436
- setupPlan.selectedClient.codexProfile = profileResult;
438
+ ?? defaultProfileTarget(clientId, io.env);
439
+ let installProfile = false;
440
+ let prompted = false;
441
+ let skipped = false;
442
+ if (hasFlag(optionArgs, '--no-profile')) {
443
+ skipped = true;
444
+ } else if (dryRun) {
445
+ installProfile = false;
446
+ } else if (writeConfig) {
447
+ installProfile = outputJson || hasFlag(optionArgs, '--yes') || hasFlag(optionArgs, '--profile');
448
+ if (!installProfile && !outputJson) {
449
+ prompted = true;
450
+ installProfile = await confirmProfileInstall(clientId, profileTarget, io);
451
+ }
452
+ }
453
+ const profileResult = await profileInstallResult(clientId, profileTarget, { write: installProfile });
454
+ profileResult.prompted = prompted;
455
+ profileResult.accepted = installProfile;
456
+ profileResult.skipped = skipped;
457
+ setupPlan.selectedClient.behaviorProfile = profileResult;
458
+ if (clientId === 'codex') {
459
+ setupPlan.selectedClient.codexProfile = profileResult;
460
+ }
437
461
  }
438
462
  }
439
463
  }
@@ -451,30 +475,30 @@ async function profileCommand(args, io) {
451
475
  const subcommand = args[0] ?? 'help';
452
476
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
453
477
  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]`);
478
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile install <codex|cursor|gemini|antigravity> [--target <path>] [--dry-run|--json]`);
479
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile status <codex|cursor|gemini|antigravity> [--target <path>] [--json]`);
480
+ writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall <codex|cursor|gemini|antigravity> [--target <path>] [--json]`);
457
481
  writeLine(io.stdout, '');
458
482
  writeLine(io.stdout, 'Profile installs are marker-scoped and never write token values.');
459
483
  return 0;
460
484
  }
461
485
 
462
- const clientId = args[1];
463
- if (clientId !== 'codex') {
464
- throw new UsageError(`Unsupported profile client: ${clientId ?? 'missing'}. Supported clients: codex.`);
486
+ const clientId = normalizeSetupClientId(args[1]);
487
+ if (!profileClientConfig(clientId)) {
488
+ throw new UsageError(`Unsupported profile client: ${args[1] ?? 'missing'}. Supported clients: ${supportedProfileClientIds().join(', ')}.`);
465
489
  }
466
490
 
467
491
  const optionArgs = args.slice(2);
468
492
  const outputJson = hasFlag(optionArgs, '--json');
469
- const targetPath = optionValue(optionArgs, '--target') ?? defaultCodexProfileTarget();
493
+ const targetPath = optionValue(optionArgs, '--target') ?? defaultProfileTarget(clientId, io.env);
470
494
  let result;
471
495
 
472
496
  if (subcommand === 'install') {
473
- result = await codexProfileInstallResult(targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
497
+ result = await profileInstallResult(clientId, targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
474
498
  } else if (subcommand === 'status') {
475
- result = await codexProfileStatusResult(targetPath);
499
+ result = await profileStatusResult(clientId, targetPath);
476
500
  } else if (subcommand === 'uninstall') {
477
- result = await codexProfileUninstallResult(targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
501
+ result = await profileUninstallResult(clientId, targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
478
502
  } else {
479
503
  throw new UsageError(`Unknown profile command: ${subcommand}`);
480
504
  }
@@ -1549,13 +1573,15 @@ function writeSetupSummary(plan, io) {
1549
1573
  }
1550
1574
  return;
1551
1575
  }
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}`);
1576
+ if (plan.selectedClient.behaviorProfile) {
1577
+ const profile = plan.selectedClient.behaviorProfile;
1578
+ const profileClient = profileClientConfig(profile.client);
1579
+ writeLine(io.stdout, ` Behavior profile target: ${profile.targetPath}`);
1580
+ writeLine(io.stdout, ` Behavior profile client: ${profileClient?.label ?? profile.client}`);
1581
+ writeLine(io.stdout, ` Behavior profile installed: ${profile.written}`);
1582
+ writeLine(io.stdout, ` Behavior profile changed: ${profile.changed}`);
1557
1583
  if (!profile.written) {
1558
- writeLine(io.stdout, ` Profile preview: ${COMMAND_NAME} profile install codex --target ${profile.targetPath}`);
1584
+ writeLine(io.stdout, ` Profile preview: ${COMMAND_NAME} profile install ${profile.client} --target ${profile.targetPath}`);
1559
1585
  }
1560
1586
  }
1561
1587
  if (!plan.selectedClient.written) {
@@ -1618,23 +1644,7 @@ bearer_token_env_var = "${TOKEN_ENV_VAR}"
1618
1644
  }
1619
1645
 
1620
1646
  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
- };
1647
+ return memoryBehaviorProfile('codex');
1638
1648
  }
1639
1649
 
1640
1650
  function writeCodexMemoryProfile(profile, io) {
@@ -1653,17 +1663,51 @@ function writeCodexMemoryProfile(profile, io) {
1653
1663
  }
1654
1664
 
1655
1665
  function codexProfileInstructionText() {
1656
- const profile = codexMemoryProfile();
1666
+ return profileInstructionText('codex');
1667
+ }
1668
+
1669
+ function memoryBehaviorProfile(clientId) {
1670
+ const config = profileClientConfig(clientId);
1671
+ if (!config) {
1672
+ throw new UsageError(`Unsupported profile client: ${clientId}`);
1673
+ }
1674
+ const instructions = [
1675
+ '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.',
1676
+ 'Use recalled memories as evidence, not as unquestioned truth. Prefer current repository files when memory conflicts with code.',
1677
+ 'After meaningful decisions, bug fixes, release steps, or durable conventions, write a concise XMemo memory with scope, source, and no secret values.',
1678
+ 'Never store tokens, API keys, cookies, private keys, raw credentials, or sensitive customer data in XMemo.',
1679
+ 'For routine or low-signal output, skip durable writes. Prefer summarized procedural or semantic memories over verbose logs.',
1680
+ config.authInstruction
1681
+ ];
1682
+ return {
1683
+ client: clientId,
1684
+ label: config.label,
1685
+ profileVersion: config.profileVersion,
1686
+ mcpServerName: MCP_SERVER_NAME,
1687
+ requiredTokenEnv: config.requiredTokenEnv ?? null,
1688
+ objective: 'Use XMemo deliberately through MCP for project context recall and high-signal write-back.',
1689
+ instructions,
1690
+ setupCommand: `${COMMAND_NAME} setup ${config.setupAlias} --url "$XMEMO_URL"`,
1691
+ smokeCommand: clientId === 'codex' ? `${COMMAND_NAME} smoke --client codex` : null
1692
+ };
1693
+ }
1694
+
1695
+ function profileInstructionText(clientId) {
1696
+ const profile = memoryBehaviorProfile(clientId);
1657
1697
  const lines = [
1658
- '## XMemo Codex profile',
1698
+ `## XMemo ${profile.label} profile`,
1659
1699
  '',
1660
1700
  `MCP server: \`${profile.mcpServerName}\``,
1661
- `Token env var: \`${profile.requiredTokenEnv}\``,
1701
+ ];
1702
+ if (profile.requiredTokenEnv) {
1703
+ lines.push(`Token env var: \`${profile.requiredTokenEnv}\``);
1704
+ }
1705
+ lines.push(
1662
1706
  '',
1663
1707
  profile.objective,
1664
1708
  '',
1665
- 'Recommended Codex behavior:'
1666
- ];
1709
+ `Recommended ${profile.label} behavior:`
1710
+ );
1667
1711
  for (const instruction of profile.instructions) {
1668
1712
  lines.push(`- ${instruction}`);
1669
1713
  }
@@ -1671,6 +1715,228 @@ function codexProfileInstructionText() {
1671
1715
  return `${lines.join('\n')}\n`;
1672
1716
  }
1673
1717
 
1718
+ function profileClientConfig(clientId) {
1719
+ const profileConfigs = {
1720
+ codex: {
1721
+ label: 'Codex',
1722
+ setupAlias: 'codex',
1723
+ profileVersion: 'codex-mcp-depth-v1',
1724
+ requiredTokenEnv: TOKEN_ENV_VAR,
1725
+ markerStart: CODEX_PROFILE_MARKER_START,
1726
+ markerEnd: CODEX_PROFILE_MARKER_END,
1727
+ defaultTarget: (env) => defaultCodexProfileTarget(env),
1728
+ authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
1729
+ },
1730
+ cursor: {
1731
+ label: 'Cursor',
1732
+ setupAlias: 'cursor',
1733
+ profileVersion: 'cursor-mcp-depth-v1',
1734
+ requiredTokenEnv: TOKEN_ENV_VAR,
1735
+ markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:cursor:start -->`,
1736
+ markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:cursor:end -->`,
1737
+ defaultTarget: (env) => path.join(userHome(env), '.cursor', 'memory-profile.md'),
1738
+ authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
1739
+ },
1740
+ 'gemini-cli': {
1741
+ label: 'Gemini CLI',
1742
+ setupAlias: 'gemini',
1743
+ profileVersion: 'gemini-cli-mcp-depth-v1',
1744
+ markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:gemini-cli:start -->`,
1745
+ markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:gemini-cli:end -->`,
1746
+ defaultTarget: (env) => path.join(userHome(env), '.gemini', 'GEMINI.md'),
1747
+ authInstruction: 'Use the client-managed MCP OAuth credential; do not paste token values into prompts, config files, or logs.'
1748
+ },
1749
+ antigravity: {
1750
+ label: 'Antigravity',
1751
+ setupAlias: 'antigravity',
1752
+ profileVersion: 'antigravity-mcp-depth-v1',
1753
+ markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:antigravity:start -->`,
1754
+ markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:antigravity:end -->`,
1755
+ defaultTarget: (env) => path.join(userHome(env), '.gemini', 'antigravity', 'MEMORY.md'),
1756
+ authInstruction: 'Use the client-managed MCP OAuth credential; do not paste token values into prompts, config files, or logs.'
1757
+ }
1758
+ };
1759
+ return profileConfigs[clientId] ?? null;
1760
+ }
1761
+
1762
+ function supportedProfileClientIds() {
1763
+ return ['codex', 'cursor', 'gemini', 'antigravity'];
1764
+ }
1765
+
1766
+ function defaultProfileTarget(clientId, env) {
1767
+ const config = profileClientConfig(clientId);
1768
+ if (!config) {
1769
+ throw new UsageError(`Unsupported profile client: ${clientId}`);
1770
+ }
1771
+ return config.defaultTarget(env);
1772
+ }
1773
+
1774
+ async function confirmProfileInstall(clientId, targetPath, io) {
1775
+ const config = profileClientConfig(clientId);
1776
+ writeLine(io.stdout, '');
1777
+ writeLine(io.stdout, `Write XMemo memory behavior profile to ${targetPath}? [Y/n]`);
1778
+ const answer = (await readLineFromStdin(io.stdin)).trim().toLowerCase();
1779
+ if (answer === '' || answer === 'y' || answer === 'yes') {
1780
+ return true;
1781
+ }
1782
+ if (answer === 'n' || answer === 'no') {
1783
+ return false;
1784
+ }
1785
+ throw new UsageError(`Unsupported response for ${config.label} profile prompt: ${answer}`);
1786
+ }
1787
+
1788
+ async function readLineFromStdin(stdin) {
1789
+ let input = '';
1790
+ for await (const chunk of stdin) {
1791
+ input += chunk;
1792
+ if (input.includes('\n')) {
1793
+ break;
1794
+ }
1795
+ }
1796
+ return input.split(/\r?\n/, 1)[0] ?? '';
1797
+ }
1798
+
1799
+ function genericProfileMarkerBlock(clientId) {
1800
+ const config = profileClientConfig(clientId);
1801
+ return `${config.markerStart}\n${profileInstructionText(clientId)}${config.markerEnd}\n`;
1802
+ }
1803
+
1804
+ async function profileInstallResult(clientId, targetPath, options = {}) {
1805
+ if (clientId === 'codex') {
1806
+ return codexProfileInstallResult(targetPath, options);
1807
+ }
1808
+ const config = profileClientConfig(clientId);
1809
+ const resolvedTarget = path.resolve(targetPath);
1810
+ const existing = await readTextIfExists(resolvedTarget);
1811
+ const marker = profileMarkerBounds(existing, config);
1812
+ const block = genericProfileMarkerBlock(clientId);
1813
+ let nextText;
1814
+
1815
+ if (marker.present) {
1816
+ nextText = `${existing.slice(0, marker.start)}${block}${existing.slice(marker.end)}`;
1817
+ } else if (existing.trim().length === 0) {
1818
+ nextText = block;
1819
+ } else {
1820
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
1821
+ nextText = `${existing}${separator}${block}`;
1822
+ }
1823
+
1824
+ const changed = nextText !== existing;
1825
+ const write = Boolean(options.write);
1826
+ if (write && changed) {
1827
+ await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
1828
+ await fs.writeFile(resolvedTarget, nextText);
1829
+ }
1830
+
1831
+ return {
1832
+ client: clientId,
1833
+ action: 'install',
1834
+ targetPath: resolvedTarget,
1835
+ markerStart: config.markerStart,
1836
+ markerEnd: config.markerEnd,
1837
+ installed: marker.present || (write && changed),
1838
+ written: write,
1839
+ changed,
1840
+ markerPresent: marker.present,
1841
+ writesTokenValue: false
1842
+ };
1843
+ }
1844
+
1845
+ async function profileStatusResult(clientId, targetPath) {
1846
+ if (clientId === 'codex') {
1847
+ return codexProfileStatusResult(targetPath);
1848
+ }
1849
+ const config = profileClientConfig(clientId);
1850
+ const resolvedTarget = path.resolve(targetPath);
1851
+ const existing = await readTextIfExists(resolvedTarget);
1852
+ const marker = profileMarkerBounds(existing, config);
1853
+ return {
1854
+ client: clientId,
1855
+ action: 'status',
1856
+ targetPath: resolvedTarget,
1857
+ installed: marker.present,
1858
+ markerPresent: marker.present,
1859
+ markerStart: config.markerStart,
1860
+ markerEnd: config.markerEnd,
1861
+ writesTokenValue: false
1862
+ };
1863
+ }
1864
+
1865
+ async function profileUninstallResult(clientId, targetPath, options = {}) {
1866
+ if (clientId === 'codex') {
1867
+ return codexProfileUninstallResult(targetPath, options);
1868
+ }
1869
+ const config = profileClientConfig(clientId);
1870
+ const resolvedTarget = path.resolve(targetPath);
1871
+ const existing = await readTextIfExists(resolvedTarget);
1872
+ const marker = profileMarkerBounds(existing, config);
1873
+ const write = Boolean(options.write);
1874
+ let changed = false;
1875
+
1876
+ if (marker.present) {
1877
+ let nextText = `${existing.slice(0, marker.start)}${existing.slice(marker.end)}`;
1878
+ nextText = nextText.replace(/\n{3,}/g, '\n\n');
1879
+ if (nextText.trim().length === 0) {
1880
+ nextText = '';
1881
+ } else if (!nextText.endsWith('\n')) {
1882
+ nextText = `${nextText}\n`;
1883
+ }
1884
+ changed = nextText !== existing;
1885
+ if (write && changed) {
1886
+ await fs.writeFile(resolvedTarget, nextText);
1887
+ }
1888
+ }
1889
+
1890
+ return {
1891
+ client: clientId,
1892
+ action: 'uninstall',
1893
+ targetPath: resolvedTarget,
1894
+ installed: marker.present && !(write && changed),
1895
+ written: write,
1896
+ changed,
1897
+ markerPresent: marker.present,
1898
+ markerStart: config.markerStart,
1899
+ markerEnd: config.markerEnd,
1900
+ writesTokenValue: false
1901
+ };
1902
+ }
1903
+
1904
+ function profileMarkerBounds(content, config) {
1905
+ const start = content.indexOf(config.markerStart);
1906
+ const end = content.indexOf(config.markerEnd);
1907
+ if (start === -1 && end === -1) {
1908
+ return { present: false, start: -1, end: -1 };
1909
+ }
1910
+
1911
+ if (start === -1 || end === -1 || end < start) {
1912
+ throw new UsageError(`${config.label} profile markers are incomplete or out of order; edit the target file manually before retrying.`);
1913
+ }
1914
+
1915
+ if (
1916
+ content.indexOf(config.markerStart, start + config.markerStart.length) !== -1
1917
+ || content.indexOf(config.markerEnd, end + config.markerEnd.length) !== -1
1918
+ ) {
1919
+ throw new UsageError(`${config.label} profile markers appear more than once; edit the target file manually before retrying.`);
1920
+ }
1921
+
1922
+ const afterEnd = end + config.markerEnd.length;
1923
+ const trailingNewlineLength = content.slice(afterEnd, afterEnd + 2) === '\r\n'
1924
+ ? 2
1925
+ : content.slice(afterEnd, afterEnd + 1) === '\n'
1926
+ ? 1
1927
+ : 0;
1928
+
1929
+ return {
1930
+ present: true,
1931
+ start,
1932
+ end: afterEnd + trailingNewlineLength
1933
+ };
1934
+ }
1935
+
1936
+ function userHome(env) {
1937
+ return env.USERPROFILE || env.HOME || os.homedir();
1938
+ }
1939
+
1674
1940
  function codexProfileMarkerBlock() {
1675
1941
  return `${CODEX_PROFILE_MARKER_START}\n${codexProfileInstructionText()}${CODEX_PROFILE_MARKER_END}\n`;
1676
1942
  }
@@ -1800,7 +2066,8 @@ function markerBounds(content) {
1800
2066
  }
1801
2067
 
1802
2068
  function writeProfileResult(action, result, io) {
1803
- writeLine(io.stdout, `${PRODUCT_NAME} Codex profile ${action}`);
2069
+ const config = profileClientConfig(result.client);
2070
+ writeLine(io.stdout, `${PRODUCT_NAME} ${config?.label ?? result.client} profile ${action}`);
1804
2071
  writeLine(io.stdout, ` Target: ${result.targetPath}`);
1805
2072
  writeLine(io.stdout, ` Installed: ${result.installed}`);
1806
2073
  if ('written' in result) {