@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.
- package/README.md +38 -9
- package/package.json +1 -1
- 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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
239
|
-
XMemo Codex behavior profile into the current project's `AGENTS.md`
|
|
240
|
-
these markers. Use `xmemo setup codex --dry-run` to preview without
|
|
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
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.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
|
|
189
|
-
writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target
|
|
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 (
|
|
435
|
+
if (shortClientSetup && profileClientConfig(clientId)) {
|
|
432
436
|
const profileTarget = optionValue(optionArgs, '--profile-target')
|
|
433
437
|
?? optionValue(optionArgs, '--target')
|
|
434
|
-
??
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
455
|
-
writeLine(io.stdout, ` ${COMMAND_NAME} profile status codex [--target
|
|
456
|
-
writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target
|
|
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
|
|
464
|
-
throw new UsageError(`Unsupported profile client: ${
|
|
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') ??
|
|
493
|
+
const targetPath = optionValue(optionArgs, '--target') ?? defaultProfileTarget(clientId, io.env);
|
|
470
494
|
let result;
|
|
471
495
|
|
|
472
496
|
if (subcommand === 'install') {
|
|
473
|
-
result = await
|
|
497
|
+
result = await profileInstallResult(clientId, targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
|
|
474
498
|
} else if (subcommand === 'status') {
|
|
475
|
-
result = await
|
|
499
|
+
result = await profileStatusResult(clientId, targetPath);
|
|
476
500
|
} else if (subcommand === 'uninstall') {
|
|
477
|
-
result = await
|
|
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.
|
|
1553
|
-
const profile = plan.selectedClient.
|
|
1554
|
-
|
|
1555
|
-
writeLine(io.stdout, `
|
|
1556
|
-
writeLine(io.stdout, `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1698
|
+
`## XMemo ${profile.label} profile`,
|
|
1659
1699
|
'',
|
|
1660
1700
|
`MCP server: \`${profile.mcpServerName}\``,
|
|
1661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|