@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.
- package/README.md +52 -20
- package/package.json +1 -1
- package/src/cli.js +423 -71
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# XMemo CLI
|
|
2
2
|
|
|
3
|
+
[](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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 `
|
|
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
|
|
239
|
-
XMemo Codex behavior profile into the current project's `AGENTS.md`
|
|
240
|
-
these markers. Use `xmemo setup codex --dry-run` to preview without
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
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 `
|
|
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
|
|
303
|
-
config manually if you need to rotate the endpoint.
|
|
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
|
|
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
|
|
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
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.
|
|
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 = '
|
|
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
|
|
189
|
-
writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target
|
|
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 (
|
|
436
|
+
if (shortClientSetup && profileClientConfig(clientId)) {
|
|
432
437
|
const profileTarget = optionValue(optionArgs, '--profile-target')
|
|
433
438
|
?? optionValue(optionArgs, '--target')
|
|
434
|
-
??
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
455
|
-
writeLine(io.stdout, ` ${COMMAND_NAME} profile status codex [--target
|
|
456
|
-
writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target
|
|
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
|
|
464
|
-
throw new UsageError(`Unsupported profile client: ${
|
|
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') ??
|
|
494
|
+
const targetPath = optionValue(optionArgs, '--target') ?? defaultProfileTarget(clientId, io.env);
|
|
470
495
|
let result;
|
|
471
496
|
|
|
472
497
|
if (subcommand === 'install') {
|
|
473
|
-
result = await
|
|
498
|
+
result = await profileInstallResult(clientId, targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
|
|
474
499
|
} else if (subcommand === 'status') {
|
|
475
|
-
result = await
|
|
500
|
+
result = await profileStatusResult(clientId, targetPath);
|
|
476
501
|
} else if (subcommand === 'uninstall') {
|
|
477
|
-
result = await
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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.
|
|
1553
|
-
const profile = plan.selectedClient.
|
|
1554
|
-
|
|
1555
|
-
writeLine(io.stdout, `
|
|
1556
|
-
writeLine(io.stdout, `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1753
|
+
`## XMemo ${profile.label} profile`,
|
|
1659
1754
|
'',
|
|
1660
1755
|
`MCP server: \`${profile.mcpServerName}\``,
|
|
1661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.${
|
|
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
|
-
|
|
1911
|
-
|
|
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
|
-
|
|
1933
|
-
|
|
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
|
-
|
|
1977
|
-
|
|
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
|
-
|
|
2000
|
-
|
|
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
|
-
|
|
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);
|