@xmemo/client 0.4.133 → 0.4.135

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 +36 -37
  2. package/package.json +1 -1
  3. package/src/cli.js +55 -16
package/README.md CHANGED
@@ -30,10 +30,11 @@ to print the exact command without changing anything.
30
30
  ```bash
31
31
  xmemo update
32
32
  xmemo setup codex
33
- xmemo setup codex --yes
33
+ xmemo setup codex --dry-run
34
34
  xmemo setup cursor
35
- xmemo setup cursor --yes
35
+ xmemo setup cursor --dry-run
36
36
  xmemo setup copilot
37
+ xmemo setup copilot --dry-run
37
38
  xmemo doctor
38
39
  xmemo discovery show
39
40
  xmemo setup
@@ -142,24 +143,23 @@ accepted as a compatibility alias.
142
143
  Generate and write a client config from discovery:
143
144
 
144
145
  ```bash
145
- xmemo setup codex --yes
146
- xmemo setup codex --url "https://your-private-service.example" --yes
147
- xmemo setup cursor --yes
146
+ xmemo setup codex
147
+ xmemo setup codex --url "https://your-private-service.example"
148
+ xmemo setup cursor
148
149
  xmemo setup copilot
149
150
  ```
150
151
 
151
- `xmemo setup <client>` is the unified setup entry point. Without `--yes` it
152
- previews what will happen. With `--yes`, write-capable clients apply the
153
- user-scoped config. Generated config references `XMEMO_KEY`; it does not embed
154
- the token value. Write-capable client configs also include stable non-secret
155
- agent identity headers where the client format supports them.
152
+ `xmemo setup <client>` is the unified setup entry point. For write-capable
153
+ clients, it applies the user-scoped config directly; use `--dry-run` to preview
154
+ without writing. Generated config references `XMEMO_KEY`; it does not embed the
155
+ token value. Write-capable client configs also include stable non-secret agent
156
+ identity headers where the client format supports them. `--yes` remains accepted
157
+ for Codex and Cursor as a compatibility no-op.
156
158
 
157
- `xmemo setup codex` is the recommended Codex path. Without `--yes` it
158
- previews the Codex MCP config and the project-scoped `AGENTS.md` memory profile.
159
- With `--yes`, it writes the Codex MCP config and installs the profile into the
160
- current project's `AGENTS.md` marker block. Use `--profile-target <path>` to
161
- choose a different project instruction file, or `--no-profile` to configure MCP
162
- only.
159
+ `xmemo setup codex` is the recommended Codex path. It writes the Codex MCP
160
+ config and installs the profile into the current project's `AGENTS.md` marker
161
+ block. Use `--dry-run` to preview, `--profile-target <path>` to choose a
162
+ different project instruction file, or `--no-profile` to configure MCP only.
163
163
 
164
164
  ## MCP setup
165
165
 
@@ -174,24 +174,25 @@ Current write-capable clients:
174
174
  ```text
175
175
  codex ~/.codex/config.toml
176
176
  cursor ~/.cursor/mcp.json
177
+ copilot ~/.copilot/mcp-config.json
177
178
  ```
178
179
 
179
180
  For clients without a verified user-scoped write path, generate a read-only
180
181
  template and apply it manually after review:
181
182
 
182
183
  ```bash
183
- xmemo mcp config --client copilot-cli
184
184
  xmemo mcp config --client generic --base-url "https://your-private-service.example" --json
185
185
  ```
186
186
 
187
- Only Codex and Cursor currently have write-capable helpers. Other client writes
188
- should only be added after their official user-scoped config format is verified.
187
+ Codex, Cursor, and Copilot CLI have write-capable setup helpers. Other client
188
+ writes should only be added after their official user-scoped config format is
189
+ verified.
189
190
 
190
191
  ### Copilot CLI
191
192
 
192
- Copilot CLI has `/mcp` management, but it does not currently document a stable
193
- cross-platform user config file path/format for third-party tools to edit
194
- directly. The recommended personal-user path is therefore local proxy mode:
193
+ Copilot CLI has `/mcp` management and reads user MCP configuration from
194
+ `~/.copilot/mcp-config.json` (or `$COPILOT_HOME/mcp-config.json`). XMemo writes
195
+ a local proxy server entry there:
195
196
 
196
197
  ```bash
197
198
  xmemo login
@@ -199,13 +200,13 @@ xmemo setup copilot
199
200
  xmemo mcp proxy
200
201
  ```
201
202
 
202
- The generated Copilot CLI template points at `http://127.0.0.1:8765/mcp` and
203
- does not include token or identity headers. `xmemo mcp proxy` reads the token
204
- saved by `xmemo login` or `xmemo token add --from-stdin`, adds the XMemo bearer
205
- token and local agent identity, then forwards requests to `https://xmemo.dev/mcp`.
206
- Copilot CLI does not currently document a stable cross-platform config file for
207
- third-party tools to edit, so `xmemo setup copilot` prints the template and next
208
- command instead of writing directly.
203
+ `xmemo setup copilot` writes `memory-os` to Copilot CLI's user MCP config and
204
+ does not include token or identity headers. Use `xmemo setup copilot --dry-run`
205
+ to preview without writing. `xmemo mcp proxy` reads the token saved by
206
+ `xmemo login` or `xmemo token add --from-stdin`, adds the XMemo bearer token and
207
+ local agent identity, then forwards requests to `https://xmemo.dev/mcp`. If
208
+ Copilot CLI is already open, reload MCP config or restart Copilot CLI after
209
+ setup.
209
210
  If you specifically want the older environment-variable template, run:
210
211
 
211
212
  ```bash
@@ -218,14 +219,12 @@ Recommended Codex setup:
218
219
 
219
220
  ```bash
220
221
  xmemo setup codex
221
- xmemo setup codex --yes
222
222
  xmemo smoke --client codex
223
223
  ```
224
224
 
225
- `setup codex` is visible and opt-in: the first command previews the writes, and
226
- `--yes` performs them. The MCP config stays in user-scoped Codex config, while
227
- the XMemo Codex behavior profile is installed into the current project's
228
- `AGENTS.md` between these markers:
225
+ `setup codex` writes the MCP config to user-scoped Codex config and installs the
226
+ XMemo Codex behavior profile into the current project's `AGENTS.md` between
227
+ these markers. Use `xmemo setup codex --dry-run` to preview without writing.
229
228
 
230
229
  ```html
231
230
  <!-- memory-os:codex-profile:start -->
@@ -277,11 +276,11 @@ Recommended Cursor setup:
277
276
 
278
277
  ```bash
279
278
  xmemo setup cursor
280
- xmemo setup cursor --yes
281
279
  ```
282
280
 
283
- `setup cursor` previews the Cursor MCP config. `setup cursor --yes` merges it
284
- into the default Cursor user config path. The lower-level equivalent remains:
281
+ `setup cursor` merges the Cursor MCP config into the default Cursor user config
282
+ path. Use `xmemo setup cursor --dry-run` to preview without writing. The
283
+ lower-level equivalent remains:
285
284
 
286
285
  ```bash
287
286
  xmemo mcp add cursor --url "$XMEMO_URL" --write
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmemo/client",
3
- "version": "0.4.133",
3
+ "version": "0.4.135",
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.133';
13
+ const CLI_VERSION = '0.4.135';
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';
@@ -157,7 +157,7 @@ function writeHelp(io) {
157
157
  writeLine(io.stdout, ` ${COMMAND_NAME} update [--dry-run] [--json]`);
158
158
  writeLine(io.stdout, ` ${COMMAND_NAME} doctor [--base-url <https://api.example.com>] [--json]`);
159
159
  writeLine(io.stdout, ` ${COMMAND_NAME} discovery show [--base-url <https://api.example.com>] [--json]`);
160
- writeLine(io.stdout, ` ${COMMAND_NAME} setup <codex|cursor|copilot> [--url <https://api.example.com>] [--write|--yes] [--json]`);
160
+ writeLine(io.stdout, ` ${COMMAND_NAME} setup <codex|cursor|copilot> [--url <https://api.example.com>] [--dry-run] [--json]`);
161
161
  writeLine(io.stdout, ` ${COMMAND_NAME} login [--from-stdin] [--base-url <url>] [--timeout-ms <ms>] [--http-timeout-ms <ms>] [--json]`);
162
162
  writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
163
163
  writeLine(io.stdout, ` ${COMMAND_NAME} status [--url <https://api.example.com>] [--json]`);
@@ -367,8 +367,9 @@ async function setupCommand(args, io) {
367
367
  const baseUrl = normalizeBaseUrl(baseUrlOption(optionArgs, io.env));
368
368
  const outputJson = hasFlag(optionArgs, '--json');
369
369
  const shortClientSetup = Boolean(positionalClientId);
370
- const writeConfig = hasFlag(optionArgs, '--write') || (shortClientSetup && hasFlag(optionArgs, '--yes'));
371
370
  const clientId = normalizeSetupClientId(positionalClientId ?? optionValue(optionArgs, '--client'));
371
+ const dryRun = hasFlag(optionArgs, '--dry-run') || hasFlag(optionArgs, '--preview');
372
+ const writeConfig = !dryRun && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes') || shortClientSetup);
372
373
  const timeoutMs = parsePositiveInteger(optionValue(optionArgs, '--timeout-ms') ?? '5000', '--timeout-ms');
373
374
  const installProfile = shortClientSetup
374
375
  && clientId === 'codex'
@@ -391,11 +392,12 @@ async function setupCommand(args, io) {
391
392
 
392
393
  if (clientId) {
393
394
  if (clientId === 'copilot-cli') {
395
+ const proxyPort = parsePositiveInteger(optionValue(optionArgs, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
396
+ setupPlan.selectedClient = copilotSetupPlan(setupPlan.mcpUrl, proxyPort, io.env);
394
397
  if (writeConfig) {
395
- throw new UsageError(`Copilot CLI setup cannot be written automatically yet. Run \`${COMMAND_NAME} setup copilot\` to print the local proxy template, then add it with Copilot CLI MCP management.`);
398
+ await mergeCopilotMcpConfig(setupPlan.selectedClient.configPath, setupPlan.selectedClient.proxyUrl);
399
+ setupPlan.selectedClient.written = true;
396
400
  }
397
- const proxyPort = parsePositiveInteger(optionValue(optionArgs, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
398
- setupPlan.selectedClient = copilotSetupPlan(setupPlan.mcpUrl, proxyPort);
399
401
  } else {
400
402
  const client = MCP_CLIENTS.get(clientId);
401
403
  if (!client) {
@@ -1425,14 +1427,14 @@ function clientSetupPlan(clientId, client, mcpUrl, env, identity) {
1425
1427
  };
1426
1428
  }
1427
1429
 
1428
- function copilotSetupPlan(mcpUrl, proxyPort) {
1430
+ function copilotSetupPlan(mcpUrl, proxyPort, env) {
1429
1431
  const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`;
1430
1432
  const template = mcpLocalProxyTemplate('copilot-cli', proxyUrl);
1431
1433
  return {
1432
1434
  id: 'copilot-cli',
1433
1435
  label: 'Copilot CLI',
1434
1436
  configKind: 'local-proxy',
1435
- configPath: 'Copilot CLI MCP config',
1437
+ configPath: defaultCopilotConfigPath(env),
1436
1438
  serverName: template.serverName,
1437
1439
  mcpUrl,
1438
1440
  proxyUrl,
@@ -1442,7 +1444,7 @@ function copilotSetupPlan(mcpUrl, proxyPort) {
1442
1444
  template: template.snippet,
1443
1445
  agentId: template.agentIdentity.agentId,
1444
1446
  writesTokenValue: false,
1445
- writeSupported: false,
1447
+ writeSupported: true,
1446
1448
  written: false
1447
1449
  };
1448
1450
  }
@@ -1478,9 +1480,14 @@ function writeSetupSummary(plan, io) {
1478
1480
  }
1479
1481
  if (plan.selectedClient.configKind === 'local-proxy') {
1480
1482
  writeLine(io.stdout, ` Local proxy: ${plan.selectedClient.requiresLocalCommand}`);
1481
- writeLine(io.stdout, ' MCP template:');
1482
- writeLine(io.stdout, JSON.stringify(plan.selectedClient.template, null, 2));
1483
- writeLine(io.stdout, ` Next: add the template with Copilot CLI MCP management, then keep \`${plan.selectedClient.requiresLocalCommand}\` running while you use Copilot CLI.`);
1483
+ if (plan.selectedClient.written) {
1484
+ writeLine(io.stdout, ` Next: keep \`${plan.selectedClient.requiresLocalCommand}\` running while you use Copilot CLI.`);
1485
+ writeLine(io.stdout, ' If Copilot CLI is already open, reload MCP config or restart Copilot CLI.');
1486
+ } else {
1487
+ writeLine(io.stdout, ' MCP template:');
1488
+ writeLine(io.stdout, JSON.stringify(plan.selectedClient.template, null, 2));
1489
+ writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup copilot --url ${plan.baseUrl}`);
1490
+ }
1484
1491
  return;
1485
1492
  }
1486
1493
  if (plan.selectedClient.codexProfile) {
@@ -1493,7 +1500,7 @@ function writeSetupSummary(plan, io) {
1493
1500
  }
1494
1501
  }
1495
1502
  if (!plan.selectedClient.written) {
1496
- writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup ${plan.selectedClient.id} --url ${plan.baseUrl} --yes`);
1503
+ writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup ${plan.selectedClient.id} --url ${plan.baseUrl}`);
1497
1504
  }
1498
1505
  return;
1499
1506
  }
@@ -1501,7 +1508,7 @@ function writeSetupSummary(plan, io) {
1501
1508
  writeLine(io.stdout, '');
1502
1509
  writeLine(io.stdout, 'Next steps:');
1503
1510
  writeLine(io.stdout, ` 1. Create a scoped token in the token portal and store it in ${plan.tokenEnvVar}.`);
1504
- writeLine(io.stdout, ` 2. Configure a client, for example: ${COMMAND_NAME} setup codex --url ${plan.baseUrl} --yes`);
1511
+ writeLine(io.stdout, ` 2. Configure a client, for example: ${COMMAND_NAME} setup codex --url ${plan.baseUrl}`);
1505
1512
  writeLine(io.stdout, ` 3. Run ${COMMAND_NAME} status to smoke-test the service without sending the token.`);
1506
1513
  }
1507
1514
 
@@ -1566,7 +1573,7 @@ function codexMemoryProfile() {
1566
1573
  'For routine or low-signal output, skip durable writes. Prefer summarized procedural or semantic memories over verbose logs.',
1567
1574
  'Keep XMemo authentication through the XMEMO_KEY environment variable; do not paste token values into prompts, config files, or logs.'
1568
1575
  ],
1569
- setupCommand: `${COMMAND_NAME} setup codex --url "$XMEMO_URL" --yes`,
1576
+ setupCommand: `${COMMAND_NAME} setup codex --url "$XMEMO_URL"`,
1570
1577
  smokeCommand: `${COMMAND_NAME} smoke --client codex`
1571
1578
  };
1572
1579
  }
@@ -1873,6 +1880,31 @@ async function mergeJsonMcpConfig(configPath, mcpUrl, identity) {
1873
1880
  await bestEffortChmod(configPath, 0o600);
1874
1881
  }
1875
1882
 
1883
+ async function mergeCopilotMcpConfig(configPath, proxyUrl) {
1884
+ const existing = await readTextIfExists(configPath);
1885
+ const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
1886
+
1887
+ if (!isPlainObject(parsed)) {
1888
+ throw new UsageError(`Copilot MCP JSON config must be an object: ${configPath}`);
1889
+ }
1890
+
1891
+ if (!isPlainObject(parsed.mcpServers)) {
1892
+ parsed.mcpServers = {};
1893
+ }
1894
+
1895
+ parsed.mcpServers['memory-os'] = copilotLocalProxyServerConfig(proxyUrl);
1896
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
1897
+ await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
1898
+ await bestEffortChmod(configPath, 0o600);
1899
+ }
1900
+
1901
+ function copilotLocalProxyServerConfig(proxyUrl) {
1902
+ return {
1903
+ type: 'http',
1904
+ url: proxyUrl
1905
+ };
1906
+ }
1907
+
1876
1908
  function cursorJsonConfig(mcpUrl, identity = envReferenceIdentity('cursor')) {
1877
1909
  return {
1878
1910
  mcpServers: {
@@ -1939,11 +1971,13 @@ function envReferenceIdentity(clientId) {
1939
1971
  }
1940
1972
 
1941
1973
  function supportedMcpClients() {
1942
- return Array.from(MCP_CLIENTS.entries()).map(([id, client]) => ({
1974
+ const clients = Array.from(MCP_CLIENTS.entries()).map(([id, client]) => ({
1943
1975
  id,
1944
1976
  label: client.label,
1945
1977
  configKind: client.configKind
1946
1978
  }));
1979
+ clients.push({ id: 'copilot-cli', label: 'Copilot CLI', configKind: 'local-proxy' });
1980
+ return clients;
1947
1981
  }
1948
1982
 
1949
1983
  function supportedMcpClientIds() {
@@ -1989,6 +2023,11 @@ function defaultCursorConfigPath(env) {
1989
2023
  return path.join(home, '.cursor', 'mcp.json');
1990
2024
  }
1991
2025
 
2026
+ function defaultCopilotConfigPath(env) {
2027
+ const home = env.USERPROFILE || env.HOME || os.homedir();
2028
+ return path.join(env.COPILOT_HOME ?? path.join(home, '.copilot'), 'mcp-config.json');
2029
+ }
2030
+
1992
2031
  async function writePlaintextCredential(credentialPath, token, metadata = {}) {
1993
2032
  await fs.mkdir(path.dirname(credentialPath), { recursive: true, mode: 0o700 });
1994
2033
  await bestEffortChmod(path.dirname(credentialPath), 0o700);