@xmemo/client 0.4.137 → 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 +14 -11
  2. package/package.json +1 -1
  3. package/src/cli.js +106 -21
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.
@@ -235,7 +237,7 @@ xmemo setup copilot
235
237
  xmemo mcp proxy
236
238
  ```
237
239
 
238
- `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
239
241
  does not include token or identity headers. Use `xmemo setup copilot --dry-run`
240
242
  to preview without writing. `xmemo mcp proxy` reads the token saved by
241
243
  `xmemo login` or `xmemo token add --from-stdin`, adds the XMemo bearer token and
@@ -285,9 +287,9 @@ Write it to the default Codex config path:
285
287
  xmemo mcp add codex --url "$XMEMO_URL" --write
286
288
  ```
287
289
 
288
- The generated config references `XMEMO_KEY` and does not include the token
289
- value. Codex custom identity headers are not written until the CLI format is
290
- 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.
291
293
 
292
294
  Codex MCP-depth checks:
293
295
 
@@ -302,7 +304,7 @@ xmemo smoke --client codex
302
304
  `xmemo mcp profile codex` prints the recommended memory behavior profile:
303
305
  recall/search at the start of non-trivial tasks, write back high-signal
304
306
  decisions and fixes, and never store secrets. `xmemo smoke --client codex`
305
- checks the local Codex TOML config for the `memory_os` MCP server,
307
+ checks the local Codex TOML config for the `XMemo` MCP server,
306
308
  `bearer_token_env_var = "XMEMO_KEY"`, token presence in the environment, and
307
309
  absence of embedded token values.
308
310
 
@@ -322,8 +324,9 @@ lower-level equivalent remains:
322
324
  xmemo mcp add cursor --url "$XMEMO_URL" --write
323
325
  ```
324
326
 
325
- The CLI refuses to overwrite an existing `memory_os` MCP server entry. Edit the
326
- 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
327
330
  `X-Memory-OS-Agent-ID` and `X-Memory-OS-Agent-Instance-ID`; the instance ID is
328
331
  non-secret and stored under the user's XMemo CLI config directory. By default,
329
332
  the setup prompt also installs a Cursor behavior profile at
@@ -352,8 +355,8 @@ run `/mcp` (or the first XMemo tool call) to complete the OAuth login. By
352
355
  default, the setup prompt also installs a Gemini behavior profile at
353
356
  `~/.gemini/GEMINI.md`; answer `n` or pass `--no-profile` to skip it.
354
357
 
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.
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.
357
360
 
358
361
  ### Antigravity
359
362
 
@@ -381,8 +384,8 @@ Use `xmemo setup antigravity` for normal installs because it performs discovery
381
384
  and chooses the recommended Antigravity path automatically. Use
382
385
  `xmemo mcp add antigravity --write` when you want the generic MCP writer
383
386
  directly, for example with `--url` or `--config` in advanced/multi-client setup.
384
- The CLI refuses to overwrite an existing `memory_os` MCP server entry. Edit the
385
- 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.
386
389
 
387
390
  ## Release model
388
391
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmemo/client",
3
- "version": "0.4.137",
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.137';
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,7 +18,8 @@ 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 -->';
@@ -190,7 +191,7 @@ function writeHelp(io) {
190
191
  writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
191
192
  writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
192
193
  writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
193
- 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]`);
194
195
  writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}]`);
195
196
  writeLine(io.stdout, ` ${COMMAND_NAME} mcp profile codex [--json]`);
196
197
  writeLine(io.stdout, ` ${COMMAND_NAME} profile install <codex|cursor|gemini|antigravity> [--target <path>] [--dry-run|--json]`);
@@ -779,6 +780,15 @@ async function mcpCommand(args, io) {
779
780
  } else {
780
781
  writeLine(io.stdout, JSON.stringify(template.snippet, null, 2));
781
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
+ }
782
792
  writeLine(io.stdout, 'Review the template before applying it. Token values are not included.');
783
793
  return 0;
784
794
  }
@@ -829,6 +839,7 @@ async function mcpCommand(args, io) {
829
839
  agentId: identity.agentId,
830
840
  agentInstanceId: identity.agentInstanceId,
831
841
  agentInstanceIdPath: identity.path,
842
+ agentInstanceGeneration: agentInstanceGenerationPolicy(target),
832
843
  writesTokenValue: false
833
844
  }, null, 2));
834
845
  return 0;
@@ -857,6 +868,7 @@ async function mcpCommand(args, io) {
857
868
  } else {
858
869
  writeLine(io.stdout, `Set ${TOKEN_ENV_VAR} in your user environment or secret manager. The token value is not included here.`);
859
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.`);
860
872
  return 0;
861
873
  }
862
874
 
@@ -1389,10 +1401,15 @@ function mcpConfigTemplate(clientId, mcpUrl) {
1389
1401
  agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1390
1402
  agentInstanceHeader: AGENT_INSTANCE_HEADER
1391
1403
  },
1404
+ agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1392
1405
  writesTokenValue: false
1393
1406
  };
1394
1407
  }
1395
1408
 
1409
+ if (clientId === 'cursor') {
1410
+ return bearerJsonMcpTemplate(clientId, mcpUrl, cursorJsonConfig(mcpUrl));
1411
+ }
1412
+
1396
1413
  if (clientId === 'gemini-cli') {
1397
1414
  return oauthJsonMcpTemplate(clientId, mcpUrl, geminiJsonConfig(mcpUrl));
1398
1415
  }
@@ -1401,14 +1418,13 @@ function mcpConfigTemplate(clientId, mcpUrl) {
1401
1418
  return oauthJsonMcpTemplate(clientId, mcpUrl, antigravityJsonConfig(mcpUrl));
1402
1419
  }
1403
1420
 
1404
- const serverName = clientId === 'cursor' || clientId === 'gemini-cli' || clientId === 'antigravity' ? 'memory_os' : 'memory-os';
1405
1421
  return {
1406
1422
  client: clientId,
1407
- serverName,
1423
+ serverName: MCP_SERVER_NAME,
1408
1424
  snippetFormat: 'json',
1409
1425
  snippet: {
1410
1426
  mcpServers: {
1411
- [serverName]: {
1427
+ [MCP_SERVER_NAME]: {
1412
1428
  type: 'http',
1413
1429
  url: mcpUrl,
1414
1430
  headers: {
@@ -1427,6 +1443,28 @@ function mcpConfigTemplate(clientId, mcpUrl) {
1427
1443
  agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1428
1444
  agentInstanceHeader: AGENT_INSTANCE_HEADER
1429
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,
1430
1468
  writesTokenValue: false
1431
1469
  };
1432
1470
  }
@@ -1446,20 +1484,20 @@ function oauthJsonMcpTemplate(clientId, mcpUrl, snippet) {
1446
1484
  agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1447
1485
  agentInstanceHeader: AGENT_INSTANCE_HEADER
1448
1486
  },
1487
+ agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1449
1488
  mcpUrl,
1450
1489
  writesTokenValue: false
1451
1490
  };
1452
1491
  }
1453
1492
 
1454
1493
  function mcpLocalProxyTemplate(clientId, proxyUrl) {
1455
- const serverName = clientId === 'cursor' || clientId === 'gemini-cli' || clientId === 'antigravity' ? 'memory_os' : 'memory-os';
1456
1494
  return {
1457
1495
  client: clientId,
1458
- serverName,
1496
+ serverName: MCP_SERVER_NAME,
1459
1497
  snippetFormat: 'json',
1460
1498
  snippet: {
1461
1499
  mcpServers: {
1462
- [serverName]: {
1500
+ [MCP_SERVER_NAME]: {
1463
1501
  type: 'http',
1464
1502
  url: proxyUrl
1465
1503
  }
@@ -1473,10 +1511,27 @@ function mcpLocalProxyTemplate(clientId, proxyUrl) {
1473
1511
  agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
1474
1512
  agentInstanceHeader: AGENT_INSTANCE_HEADER
1475
1513
  },
1514
+ agentInstanceGeneration: agentInstanceGenerationPolicy(clientId),
1476
1515
  writesTokenValue: false
1477
1516
  };
1478
1517
  }
1479
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
+
1480
1535
  function sameMajorMinor(left, right) {
1481
1536
  const leftParts = left.split('.');
1482
1537
  const rightParts = right.split('.');
@@ -2079,7 +2134,8 @@ function writeProfileResult(action, result, io) {
2079
2134
 
2080
2135
  async function codexSmokeReport(configPath, env) {
2081
2136
  const configText = await readTextIfExists(configPath);
2082
- const block = tomlServerBlock(configText, MCP_SERVER_NAME);
2137
+ const serverBlock = findTomlServerBlock(configText);
2138
+ const block = serverBlock.block;
2083
2139
  const mcpUrl = block ? tomlStringValue(block, 'url') : null;
2084
2140
  const bearerTokenEnvVar = block ? tomlStringValue(block, 'bearer_token_env_var') : null;
2085
2141
  const tokenValue = env[TOKEN_ENV_VAR] ?? '';
@@ -2096,7 +2152,7 @@ async function codexSmokeReport(configPath, env) {
2096
2152
  name: 'memory_os_server_present',
2097
2153
  ok: Boolean(block),
2098
2154
  required: true,
2099
- 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}]`
2100
2156
  },
2101
2157
  {
2102
2158
  name: 'mcp_url_present',
@@ -2134,7 +2190,7 @@ async function codexSmokeReport(configPath, env) {
2134
2190
  ok: checks.every((check) => !check.required || check.ok),
2135
2191
  client: 'codex',
2136
2192
  configPath,
2137
- serverName: MCP_SERVER_NAME,
2193
+ serverName: serverBlock.name ?? MCP_SERVER_NAME,
2138
2194
  mcpUrl,
2139
2195
  tokenEnvVar: TOKEN_ENV_VAR,
2140
2196
  agentInstanceIdPath: identityPath,
@@ -2142,6 +2198,26 @@ async function codexSmokeReport(configPath, env) {
2142
2198
  };
2143
2199
  }
2144
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
+
2145
2221
  function tomlServerBlock(content, serverName) {
2146
2222
  const header = `[mcp_servers.${serverName}]`;
2147
2223
  const lines = content.split(/\r?\n/);
@@ -2174,8 +2250,9 @@ function cursorJsonSnippet(mcpUrl, identity = envReferenceIdentity('cursor')) {
2174
2250
  async function appendTomlServerConfig(configPath, mcpUrl) {
2175
2251
  const snippet = codexTomlSnippet(mcpUrl);
2176
2252
  const existing = await readTextIfExists(configPath);
2177
- if (existing.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
2178
- 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.`);
2179
2256
  }
2180
2257
 
2181
2258
  await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
@@ -2196,8 +2273,9 @@ async function mergeJsonMcpConfig(configPath, mcpUrl, identity) {
2196
2273
  parsed.mcpServers = {};
2197
2274
  }
2198
2275
 
2199
- if (parsed.mcpServers[MCP_SERVER_NAME]) {
2200
- 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.`);
2201
2279
  }
2202
2280
 
2203
2281
  parsed.mcpServers[MCP_SERVER_NAME] = cursorJsonServerConfig(mcpUrl, identity);
@@ -2240,8 +2318,9 @@ async function mergeAntigravityMcpConfig(configPath, mcpUrl, identity) {
2240
2318
  parsed.mcpServers = {};
2241
2319
  }
2242
2320
 
2243
- if (parsed.mcpServers[MCP_SERVER_NAME]) {
2244
- 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.`);
2245
2324
  }
2246
2325
 
2247
2326
  parsed.mcpServers[MCP_SERVER_NAME] = antigravityJsonServerConfig(mcpUrl, identity);
@@ -2263,8 +2342,9 @@ async function mergeGeminiMcpConfig(configPath, mcpUrl, identity) {
2263
2342
  parsed.mcpServers = {};
2264
2343
  }
2265
2344
 
2266
- if (parsed.mcpServers[MCP_SERVER_NAME]) {
2267
- 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.`);
2268
2348
  }
2269
2349
 
2270
2350
  parsed.mcpServers[MCP_SERVER_NAME] = geminiJsonServerConfig(mcpUrl, identity);
@@ -2285,7 +2365,12 @@ async function mergeCopilotMcpConfig(configPath, proxyUrl) {
2285
2365
  parsed.mcpServers = {};
2286
2366
  }
2287
2367
 
2288
- 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);
2289
2374
  await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
2290
2375
  await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
2291
2376
  await bestEffortChmod(configPath, 0o600);