@xmemo/client 0.4.169 → 0.4.171

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 CHANGED
@@ -12,6 +12,33 @@ setup helper code needed on a user's machine.
12
12
  The XMemo server, database, token registry, deployment files, logs, and
13
13
  internal scripts are not part of this npm package.
14
14
 
15
+ > 🧠 **XMemo is also an MCP Server** — give your AI agents persistent memory across sessions. See [MCP Setup](#mcp-setup) below.
16
+
17
+ ## MCP Server Overview
18
+
19
+ **XMemo** is a user-owned, hosted MCP memory service that lets AI agents persistently store, search, recall, update, and manage notes and memory fragments across sessions, projects, and tools.
20
+
21
+ - **MCP Endpoint**: `https://xmemo.dev/mcp` (Streamable HTTP)
22
+ - **Auth**: Bearer Token (`XMEMO_KEY`) or MCP OAuth
23
+ - **Tools**: `remember`, `recall`, `search_memory`, `update_memory`, `forget`, `redact_memory`, `explain_memory`, `create_memory_todo`, `list_memory_todos`, `complete_memory_todo`, `record_event`, `get_timeline`, `add_expense`
24
+ - **Clients**: Kimi, Claude, Cursor, Copilot, Gemini, Grok, Windsurf, Cline, Trae, Zed, Qwen, and more
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "XMemo": {
30
+ "type": "streamable-http",
31
+ "url": "https://xmemo.dev/mcp",
32
+ "headers": {
33
+ "Authorization": "Bearer ${XMEMO_KEY}"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ See [MCP Setup](#mcp-setup) for detailed client configuration.
41
+
15
42
  ## Install
16
43
 
17
44
  ```bash
@@ -37,6 +64,13 @@ xmemo setup cursor
37
64
  xmemo setup cursor --dry-run
38
65
  xmemo setup copilot
39
66
  xmemo setup copilot --dry-run
67
+ xmemo setup openclaw
68
+ xmemo setup openclaw --dry-run
69
+ xmemo setup openclaw --with-mcp
70
+ xmemo setup openclaw --mcp-only
71
+ xmemo setup hermes
72
+ xmemo setup hermes --with-mcp
73
+ xmemo setup hermes --mcp-only
40
74
  xmemo setup gemini
41
75
  xmemo setup gemini --dry-run
42
76
  xmemo setup antigravity
@@ -167,6 +201,8 @@ xmemo setup codex
167
201
  xmemo setup codex --url "https://your-private-service.example"
168
202
  xmemo setup cursor
169
203
  xmemo setup copilot
204
+ xmemo setup openclaw
205
+ xmemo setup hermes
170
206
  xmemo setup gemini
171
207
  xmemo setup antigravity
172
208
  ```
@@ -175,10 +211,12 @@ xmemo setup antigravity
175
211
  clients, it applies the user-scoped config directly; use `--dry-run` to preview
176
212
  without writing. Codex/Cursor configs reference `XMEMO_KEY`; OAuth-native
177
213
  clients such as Gemini CLI and Antigravity use the client's MCP OAuth flow
178
- instead. No generated config embeds a token value. Write-capable client configs
179
- also include stable non-secret agent identity headers where the client format
180
- supports them. `--yes` remains accepted for Codex and Cursor as a compatibility
181
- no-op.
214
+ instead. `xmemo setup openclaw` is a custom OpenClaw installer: it installs or
215
+ updates the native `@xmemo/openclaw-memory` plugin and the XMemo Skill, and does
216
+ not add the hosted MCP server unless `--with-mcp` is passed. No generated config
217
+ embeds a token value. Write-capable client configs also include stable
218
+ non-secret agent identity headers where the client format supports them.
219
+ `--yes` remains accepted for Codex and Cursor as a compatibility no-op.
182
220
 
183
221
  After writing MCP config, `xmemo setup <client>` prompts:
184
222
 
@@ -269,12 +307,100 @@ template and apply it manually after review:
269
307
  xmemo mcp config --client generic --base-url "https://your-private-service.example" --json
270
308
  ```
271
309
 
272
- Codex, Cursor, Copilot CLI, Gemini CLI, Antigravity, and Kiro have write-capable setup
273
- helpers. Antigravity 2.0 is write-capable through `xmemo mcp add antigravity2
274
- --write`.
310
+ Codex, Cursor, Copilot CLI, Gemini CLI, Antigravity, OpenClaw, Hermes, and Kiro
311
+ have write-capable setup helpers. Antigravity 2.0 is write-capable through
312
+ `xmemo mcp add antigravity2 --write`.
275
313
  Other client writes should only be added after their official user-scoped config
276
314
  format is verified.
277
315
 
316
+ OpenClaw and Hermes use the same setup modes:
317
+
318
+ | Command | Result |
319
+ |---------|--------|
320
+ | `xmemo setup <openclaw|hermes>` | Install/update the native integration and sync credentials. MCP is not installed. |
321
+ | `xmemo setup <openclaw|hermes> --with-mcp` | Install/update the native integration, sync credentials, and add hosted MCP fallback. |
322
+ | `xmemo setup <openclaw|hermes> --mcp-only` | Add hosted MCP fallback only. Native plugin/Skill install and native credential sync are skipped. |
323
+
324
+ ### OpenClaw
325
+
326
+ Recommended OpenClaw setup:
327
+
328
+ ```bash
329
+ xmemo login
330
+ xmemo setup openclaw
331
+ openclaw xmemo status
332
+ ```
333
+
334
+ `xmemo setup openclaw` installs or updates OpenClaw's native XMemo memory plugin
335
+ from `@xmemo/openclaw-memory`, installs the XMemo Skill with `openclaw skills
336
+ install xmemo --force`, and then runs `openclaw xmemo status --json` so the user
337
+ can see whether credentials are available. The native plugin reads the same
338
+ user-scoped XMemo credential used by `xmemo login` and `xmemo token add
339
+ --from-stdin`, so normal users do not need to separately configure an OpenClaw
340
+ API key.
341
+
342
+ Hosted MCP is not installed by default because it creates a second XMemo tool
343
+ surface beside the native memory plugin. If you intentionally want that fallback
344
+ too, run:
345
+
346
+ ```bash
347
+ xmemo setup openclaw --with-mcp
348
+ ```
349
+
350
+ The fallback MCP entry references `Authorization: Bearer ${XMEMO_KEY}` and does
351
+ not embed the token value. To install only the hosted MCP fallback without the
352
+ native plugin or Skill, run:
353
+
354
+ ```bash
355
+ xmemo setup openclaw --mcp-only
356
+ ```
357
+
358
+ Use `--no-skill` only when the XMemo Skill is managed by another deployment
359
+ path. Use `--openclaw-bin <path>` for custom OpenClaw binary locations.
360
+
361
+ ### Hermes
362
+
363
+ Recommended Hermes setup:
364
+
365
+ ```bash
366
+ xmemo login
367
+ xmemo setup hermes
368
+ ```
369
+
370
+ `xmemo setup hermes` is Hermes-aware rather than a raw MCP writer. It installs
371
+ or updates the native `hermes-xmemo` package with `python -m pip install -U
372
+ hermes-xmemo`, runs `hermes-xmemo install`, then resolves the XMemo token from,
373
+ in order, process environment (`XMEMO_KEY`, `MEMORY_OS_API_KEY`, or
374
+ `MEMORY_OS_MCP_TOKEN`), the shared `@xmemo/client` credential written by
375
+ `xmemo login` / `xmemo token add --from-stdin`, and an existing
376
+ `$HERMES_HOME/.env`. It syncs the token to Hermes' `$HERMES_HOME/.env` as
377
+ `XMEMO_KEY`, which keeps the native `hermes-xmemo` plugin and any optional
378
+ Hermes MCP fallback on the same credential.
379
+
380
+ Hosted MCP is not installed by default because it creates a second XMemo tool
381
+ surface beside the native memory provider. If you intentionally want that
382
+ fallback too, run:
383
+
384
+ ```bash
385
+ xmemo setup hermes --with-mcp
386
+ ```
387
+
388
+ The fallback MCP entry references `Authorization:Bearer ${XMEMO_KEY}` and does
389
+ not embed the token value. To install only the hosted MCP fallback without the
390
+ native plugin or native credential sync, run:
391
+
392
+ ```bash
393
+ xmemo setup hermes --mcp-only
394
+ ```
395
+
396
+ If an older Hermes plugin setup already created `$HERMES_HOME/.env`,
397
+ `xmemo setup hermes` can backfill the same token into the shared `@xmemo/client`
398
+ credential file so future XMemo CLI and MCP flows reuse it. Use `--no-plugin`
399
+ only when the native Hermes plugin is managed by another deployment path. Use
400
+ `--hermes-home <path>` for non-default Hermes homes. The legacy
401
+ `hermes memory setup xmemo` flow remains supported by the plugin and can reuse
402
+ the shared credential when present.
403
+
278
404
  ### Copilot CLI
279
405
 
280
406
  Copilot CLI has `/mcp` management and reads user MCP configuration from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmemo/client",
3
- "version": "0.4.169",
3
+ "version": "0.4.171",
4
4
  "description": "Privacy-first CLI and MCP setup helper for XMemo.",
5
5
  "mcpName": "io.github.yonro/xmemo",
6
6
  "type": "module",
@@ -0,0 +1,214 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { hasFlag, optionValue } from '../core/args.js';
5
+ import { TOKEN_ENV_VAR, LEGACY_TOKEN_ENV_VAR } from '../core/constants.js';
6
+ import { UsageError } from '../core/errors.js';
7
+ import {
8
+ bestEffortChmod,
9
+ readTextIfExists,
10
+ runProcess
11
+ } from '../core/runtime.js';
12
+ import {
13
+ readStoredCredential,
14
+ resolveCredentialToken,
15
+ storeTokenValue
16
+ } from '../network/auth.js';
17
+
18
+ const HERMES_MCP_NAME = 'XMemo';
19
+ const HERMES_PLUGIN_PACKAGE = 'hermes-xmemo';
20
+ const DEFAULT_PYTHON_BIN = process.platform === 'win32' ? 'python' : 'python3';
21
+
22
+ export function defaultHermesHome(env) {
23
+ if (env.HERMES_HOME) {
24
+ return env.HERMES_HOME;
25
+ }
26
+ const home = env.USERPROFILE || env.HOME;
27
+ if (!home) {
28
+ throw new UsageError('Cannot determine Hermes home. Set HERMES_HOME or HOME.');
29
+ }
30
+ return path.join(home, '.hermes');
31
+ }
32
+
33
+ function parseEnvContent(content) {
34
+ const values = new Map();
35
+ for (const line of content.split(/\r?\n/)) {
36
+ const trimmed = line.trim();
37
+ if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) {
38
+ continue;
39
+ }
40
+ const [rawKey, ...rest] = trimmed.split('=');
41
+ const key = rawKey.trim();
42
+ let value = rest.join('=').trim();
43
+ if (
44
+ (value.startsWith('"') && value.endsWith('"'))
45
+ || (value.startsWith("'") && value.endsWith("'"))
46
+ ) {
47
+ value = value.slice(1, -1);
48
+ }
49
+ values.set(key, value);
50
+ }
51
+ return values;
52
+ }
53
+
54
+ async function readHermesEnvToken(envPath) {
55
+ const content = await readTextIfExists(envPath);
56
+ if (!content.trim()) {
57
+ return null;
58
+ }
59
+ const values = parseEnvContent(content);
60
+ return values.get(TOKEN_ENV_VAR) || values.get('MEMORY_OS_API_KEY') || values.get(LEGACY_TOKEN_ENV_VAR) || null;
61
+ }
62
+
63
+ async function writeHermesEnvToken(envPath, token) {
64
+ const existing = await readTextIfExists(envPath);
65
+ const lines = existing ? existing.split(/\r?\n/) : [];
66
+ const output = [];
67
+ let wrote = false;
68
+
69
+ for (const line of lines) {
70
+ const key = line.includes('=') ? line.split('=', 1)[0].trim() : '';
71
+ if (key === TOKEN_ENV_VAR) {
72
+ output.push(`${TOKEN_ENV_VAR}=${token}`);
73
+ wrote = true;
74
+ } else {
75
+ output.push(line);
76
+ }
77
+ }
78
+
79
+ if (!wrote) {
80
+ output.push(`${TOKEN_ENV_VAR}=${token}`);
81
+ }
82
+
83
+ const text = `${output.join('\n').replace(/\n*$/, '')}\n`;
84
+ await fs.mkdir(path.dirname(envPath), { recursive: true, mode: 0o700 });
85
+ await fs.writeFile(envPath, text, { mode: 0o600 });
86
+ await bestEffortChmod(path.dirname(envPath), 0o700);
87
+ await bestEffortChmod(envPath, 0o600);
88
+ }
89
+
90
+ async function resolveHermesCredential(io, hermesEnvPath) {
91
+ const envToken = io.env[TOKEN_ENV_VAR] || io.env.MEMORY_OS_API_KEY || io.env[LEGACY_TOKEN_ENV_VAR];
92
+ if (envToken) {
93
+ return { token: envToken, source: 'process-env' };
94
+ }
95
+
96
+ const sharedToken = await resolveCredentialToken(io.env);
97
+ if (sharedToken) {
98
+ return { token: sharedToken, source: 'shared-credential' };
99
+ }
100
+
101
+ const hermesToken = await readHermesEnvToken(hermesEnvPath);
102
+ if (hermesToken) {
103
+ return { token: hermesToken, source: 'hermes-env' };
104
+ }
105
+
106
+ return { token: null, source: 'missing' };
107
+ }
108
+
109
+ function commandText(command, args) {
110
+ return [command, ...args].join(' ');
111
+ }
112
+
113
+ async function runHermesCommand(command, args, io) {
114
+ const result = await runProcess(command, args, io, { stream: false });
115
+ if (result.code !== 0) {
116
+ throw new UsageError(
117
+ `Hermes setup command failed (${result.code}): ${commandText(command, args)}\n${result.stderr || result.stdout}`,
118
+ );
119
+ }
120
+ return result;
121
+ }
122
+
123
+ export async function hermesSetupPlan({ setupPlan, optionArgs, io, dryRun, identity, client, force }) {
124
+ const hermesHome = optionValue(optionArgs, '--hermes-home') ?? defaultHermesHome(io.env);
125
+ const hermesEnvPath = path.join(hermesHome, '.env');
126
+ const configPath = optionValue(optionArgs, '--config') ?? path.join(hermesHome, 'config.yaml');
127
+ const mcpOnly = hasFlag(optionArgs, '--mcp-only');
128
+ const withMcp = mcpOnly || hasFlag(optionArgs, '--with-mcp');
129
+ const noPlugin = hasFlag(optionArgs, '--no-plugin');
130
+ const noEnvSync = hasFlag(optionArgs, '--no-env-sync');
131
+ const pythonBin = optionValue(optionArgs, '--python') ?? DEFAULT_PYTHON_BIN;
132
+ const hermesXmemoBin = optionValue(optionArgs, '--hermes-xmemo-bin') ?? 'hermes-xmemo';
133
+
134
+ const credential = await resolveHermesCredential(io, hermesEnvPath);
135
+ const storedCredential = await readStoredCredential(io.env);
136
+ const shouldBackfillSharedCredential = credential.token
137
+ && credential.source === 'hermes-env'
138
+ && !storedCredential.token;
139
+ const shouldWriteHermesEnv = Boolean(credential.token) && !noEnvSync;
140
+
141
+ const selectedClient = {
142
+ id: 'hermes',
143
+ label: 'Hermes',
144
+ configKind: 'hermes-native-and-mcp',
145
+ setupMode: mcpOnly ? 'mcp-only' : withMcp ? 'native-with-mcp' : 'native',
146
+ configPath,
147
+ hermesHome,
148
+ hermesEnvPath,
149
+ written: false,
150
+ writesTokenValue: shouldWriteHermesEnv,
151
+ tokenValueEmbeddedInMcpConfig: false,
152
+ credential: {
153
+ ready: Boolean(credential.token),
154
+ source: credential.source,
155
+ sharedCredentialBackfilled: false,
156
+ hermesEnvSynced: false,
157
+ },
158
+ nativePlugin: {
159
+ package: HERMES_PLUGIN_PACKAGE,
160
+ installCommand: commandText(pythonBin, ['-m', 'pip', 'install', '-U', HERMES_PLUGIN_PACKAGE]),
161
+ activateCommand: commandText(hermesXmemoBin, ['install', '--hermes-home', hermesHome]),
162
+ installed: false,
163
+ skipped: noPlugin || mcpOnly,
164
+ note: noPlugin || mcpOnly
165
+ ? mcpOnly
166
+ ? 'Native Hermes plugin install skipped by --mcp-only.'
167
+ : 'Native Hermes plugin install skipped by --no-plugin.'
168
+ : 'Installs/updates the native Hermes XMemo plugin before syncing credentials.',
169
+ },
170
+ mcp: {
171
+ enabled: withMcp,
172
+ serverName: HERMES_MCP_NAME,
173
+ mcpUrl: setupPlan.mcpUrl,
174
+ written: false,
175
+ only: mcpOnly,
176
+ note: withMcp
177
+ ? mcpOnly
178
+ ? `Hosted MCP references ${TOKEN_ENV_VAR}; native Hermes plugin install and credential sync are skipped by --mcp-only.`
179
+ : `Hosted MCP references ${TOKEN_ENV_VAR} from Hermes .env/process env; token value is not embedded in config.yaml.`
180
+ : 'Hosted MCP is not installed by default; use --with-mcp for an explicit fallback.',
181
+ },
182
+ dryRun,
183
+ };
184
+
185
+ if (dryRun) {
186
+ return selectedClient;
187
+ }
188
+
189
+ if (!noPlugin && !mcpOnly) {
190
+ await runHermesCommand(pythonBin, ['-m', 'pip', 'install', '-U', HERMES_PLUGIN_PACKAGE], io);
191
+ await runHermesCommand(hermesXmemoBin, ['install', '--hermes-home', hermesHome], io);
192
+ selectedClient.nativePlugin.installed = true;
193
+ }
194
+
195
+ if (shouldBackfillSharedCredential && !mcpOnly) {
196
+ await storeTokenValue(credential.token, { source: 'hermes-env-sync' }, io.env);
197
+ selectedClient.credential.sharedCredentialBackfilled = true;
198
+ }
199
+
200
+ if (shouldWriteHermesEnv && !mcpOnly) {
201
+ await writeHermesEnvToken(hermesEnvPath, credential.token);
202
+ selectedClient.credential.hermesEnvSynced = true;
203
+ }
204
+
205
+ if (withMcp) {
206
+ await client.writeConfig(configPath, setupPlan.mcpUrl, identity, { force });
207
+ selectedClient.mcp.written = true;
208
+ }
209
+
210
+ selectedClient.written = selectedClient.nativePlugin.installed
211
+ || selectedClient.credential.hermesEnvSynced
212
+ || selectedClient.mcp.written;
213
+ return selectedClient;
214
+ }
@@ -0,0 +1,153 @@
1
+ import { hasFlag, optionValue } from '../core/args.js';
2
+ import { TOKEN_ENV_VAR } from '../core/constants.js';
3
+ import { UsageError } from '../core/errors.js';
4
+ import { runProcess } from '../core/runtime.js';
5
+ import { resolveCredentialToken } from '../network/auth.js';
6
+
7
+ const DEFAULT_OPENCLAW_BIN = 'openclaw';
8
+ const OPENCLAW_PLUGIN_SPEC = '@xmemo/openclaw-memory';
9
+ const OPENCLAW_SKILL_REF = 'xmemo';
10
+ const OPENCLAW_MCP_NAME = 'xmemo';
11
+
12
+ function commandText(command, args) {
13
+ return [command, ...args].join(' ');
14
+ }
15
+
16
+ function extractLastJsonObject(text) {
17
+ const trimmed = text.trim();
18
+ if (!trimmed) {
19
+ return null;
20
+ }
21
+ for (let index = trimmed.lastIndexOf('{'); index >= 0; index = trimmed.lastIndexOf('{', index - 1)) {
22
+ const candidate = trimmed.slice(index);
23
+ try {
24
+ return JSON.parse(candidate);
25
+ } catch {
26
+ // Keep scanning; OpenClaw may print warnings before the final JSON object.
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ async function runOpenClaw(openclawBin, args, io) {
33
+ const result = await runProcess(openclawBin, args, io, { stream: false });
34
+ if (result.code !== 0) {
35
+ throw new UsageError(
36
+ `OpenClaw command failed (${result.code}): ${commandText(openclawBin, args)}\n${result.stderr || result.stdout}`,
37
+ );
38
+ }
39
+ return result;
40
+ }
41
+
42
+ function credentialPlan(env) {
43
+ if (env[TOKEN_ENV_VAR]) {
44
+ return { ready: true, source: 'env', variable: TOKEN_ENV_VAR };
45
+ }
46
+ if (env.MEMORY_OS_MCP_TOKEN) {
47
+ return { ready: true, source: 'env', variable: 'MEMORY_OS_MCP_TOKEN' };
48
+ }
49
+ return { ready: false, source: 'shared-credential-or-missing', variable: null };
50
+ }
51
+
52
+ export async function openclawSetupPlan({ setupPlan, optionArgs, io, dryRun }) {
53
+ const openclawBin = optionValue(optionArgs, '--openclaw-bin') ?? DEFAULT_OPENCLAW_BIN;
54
+ const mcpOnly = hasFlag(optionArgs, '--mcp-only');
55
+ const withMcp = mcpOnly || hasFlag(optionArgs, '--with-mcp');
56
+ const noSkill = hasFlag(optionArgs, '--no-skill');
57
+ const credential = credentialPlan(io.env);
58
+ const sharedToken = await resolveCredentialToken(io.env);
59
+ if (sharedToken && !credential.ready) {
60
+ credential.ready = true;
61
+ credential.source = 'shared-credential';
62
+ }
63
+
64
+ const selectedClient = {
65
+ id: 'openclaw',
66
+ label: 'OpenClaw',
67
+ configKind: 'native-plugin',
68
+ setupMode: mcpOnly ? 'mcp-only' : withMcp ? 'native-with-mcp' : 'native',
69
+ written: false,
70
+ writesTokenValue: false,
71
+ openclawBin,
72
+ credential,
73
+ nativePlugin: {
74
+ package: OPENCLAW_PLUGIN_SPEC,
75
+ command: commandText(openclawBin, ['plugins', 'install', OPENCLAW_PLUGIN_SPEC, '--force']),
76
+ installed: false,
77
+ skipped: mcpOnly,
78
+ },
79
+ skill: {
80
+ ref: OPENCLAW_SKILL_REF,
81
+ command: commandText(openclawBin, ['skills', 'install', OPENCLAW_SKILL_REF, '--force']),
82
+ installed: false,
83
+ skipped: noSkill || mcpOnly,
84
+ },
85
+ mcp: {
86
+ enabled: withMcp,
87
+ serverName: OPENCLAW_MCP_NAME,
88
+ mcpUrl: setupPlan.mcpUrl,
89
+ command: commandText(openclawBin, [
90
+ 'mcp',
91
+ 'add',
92
+ OPENCLAW_MCP_NAME,
93
+ '--url',
94
+ setupPlan.mcpUrl,
95
+ '--transport',
96
+ 'streamable-http',
97
+ '--header',
98
+ `Authorization=Bearer \${${TOKEN_ENV_VAR}}`,
99
+ '--no-probe',
100
+ ]),
101
+ written: false,
102
+ only: mcpOnly,
103
+ note: withMcp
104
+ ? mcpOnly
105
+ ? `Hosted MCP references ${TOKEN_ENV_VAR}; native plugin and Skill are skipped by --mcp-only.`
106
+ : `Hosted MCP fallback references ${TOKEN_ENV_VAR}; native plugin remains primary.`
107
+ : 'Hosted MCP is not installed by default; use --with-mcp for an explicit fallback.',
108
+ },
109
+ status: null,
110
+ dryRun,
111
+ };
112
+
113
+ if (dryRun) {
114
+ return selectedClient;
115
+ }
116
+
117
+ if (!mcpOnly) {
118
+ await runOpenClaw(openclawBin, ['plugins', 'install', OPENCLAW_PLUGIN_SPEC, '--force'], io);
119
+ selectedClient.nativePlugin.installed = true;
120
+
121
+ if (!noSkill) {
122
+ await runOpenClaw(openclawBin, ['skills', 'install', OPENCLAW_SKILL_REF, '--force'], io);
123
+ selectedClient.skill.installed = true;
124
+ }
125
+ }
126
+
127
+ if (withMcp) {
128
+ await runOpenClaw(
129
+ openclawBin,
130
+ [
131
+ 'mcp',
132
+ 'add',
133
+ OPENCLAW_MCP_NAME,
134
+ '--url',
135
+ setupPlan.mcpUrl,
136
+ '--transport',
137
+ 'streamable-http',
138
+ '--header',
139
+ `Authorization=Bearer \${${TOKEN_ENV_VAR}}`,
140
+ '--no-probe',
141
+ ],
142
+ io,
143
+ );
144
+ selectedClient.mcp.written = true;
145
+ }
146
+
147
+ if (!mcpOnly) {
148
+ const statusResult = await runOpenClaw(openclawBin, ['xmemo', 'status', '--json'], io);
149
+ selectedClient.status = extractLastJsonObject(statusResult.stdout);
150
+ }
151
+ selectedClient.written = true;
152
+ return selectedClient;
153
+ }
@@ -28,6 +28,8 @@ import {
28
28
  supportedMcpClients
29
29
  } from '../mcp/clients.js';
30
30
  import { mergeCopilotMcpConfig } from '../mcp/proxy/copilot.js';
31
+ import { hermesSetupPlan } from './hermes.js';
32
+ import { openclawSetupPlan } from './openclaw.js';
31
33
 
32
34
  import {
33
35
  agentIdentity,
@@ -134,7 +136,26 @@ export async function setupCommand(args, io) {
134
136
  setupPlan.detectedClients.push(clientPlan);
135
137
  }
136
138
  } else if (clientId) {
137
- if (clientId === 'copilot-cli') {
139
+ if (clientId === 'hermes') {
140
+ const client = MCP_CLIENTS.get(clientId);
141
+ const identity = writeConfig ? await agentIdentity(clientId, io.env) : envReferenceIdentity(clientId);
142
+ setupPlan.selectedClient = await hermesSetupPlan({
143
+ setupPlan,
144
+ optionArgs,
145
+ io,
146
+ dryRun,
147
+ identity,
148
+ client,
149
+ force
150
+ });
151
+ } else if (clientId === 'openclaw') {
152
+ setupPlan.selectedClient = await openclawSetupPlan({
153
+ setupPlan,
154
+ optionArgs,
155
+ io,
156
+ dryRun
157
+ });
158
+ } else if (clientId === 'copilot-cli') {
138
159
  const proxyPort = parsePositiveInteger(optionValue(optionArgs, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
139
160
  setupPlan.selectedClient = copilotSetupPlan(setupPlan.mcpUrl, proxyPort, io.env);
140
161
  if (writeConfig) {
@@ -35,9 +35,10 @@ export function mcpConfigTemplate(clientId, mcpUrl, options = {}) {
35
35
 
36
36
  const jsonDefinition = jsonMcpClientDefinition(clientId);
37
37
  if (jsonDefinition) {
38
+ const identityClientId = jsonDefinition.defaultIdentityId ?? clientId;
38
39
  return jsonDefinition.authentication === 'oauth'
39
- ? oauthJsonMcpTemplate(clientId, mcpUrl, jsonClientConfig(clientId, mcpUrl), options)
40
- : bearerJsonMcpTemplate(clientId, mcpUrl, jsonClientConfig(clientId, mcpUrl), options);
40
+ ? oauthJsonMcpTemplate(clientId, identityClientId, mcpUrl, jsonClientConfig(clientId, mcpUrl), options)
41
+ : bearerJsonMcpTemplate(clientId, identityClientId, mcpUrl, jsonClientConfig(clientId, mcpUrl), options);
41
42
  }
42
43
 
43
44
  return {
@@ -112,7 +113,7 @@ export function agentInstanceGenerationPolicy(clientId, options = {}) {
112
113
  };
113
114
  }
114
115
 
115
- function bearerJsonMcpTemplate(clientId, mcpUrl, snippet, options) {
116
+ function bearerJsonMcpTemplate(clientId, identityClientId, mcpUrl, snippet, options) {
116
117
  return {
117
118
  client: clientId,
118
119
  serverName: MCP_SERVER_NAME,
@@ -122,7 +123,7 @@ function bearerJsonMcpTemplate(clientId, mcpUrl, snippet, options) {
122
123
  optionalEnv: [AGENT_INSTANCE_ENV_VAR],
123
124
  authentication: 'env-bearer',
124
125
  agentIdentity: {
125
- agentId: clientId,
126
+ agentId: identityClientId,
126
127
  agentIdHeader: AGENT_ID_HEADER,
127
128
  agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
128
129
  agentInstanceHeader: AGENT_INSTANCE_HEADER
@@ -133,7 +134,7 @@ function bearerJsonMcpTemplate(clientId, mcpUrl, snippet, options) {
133
134
  };
134
135
  }
135
136
 
136
- function oauthJsonMcpTemplate(clientId, mcpUrl, snippet, options) {
137
+ function oauthJsonMcpTemplate(clientId, identityClientId, mcpUrl, snippet, options) {
137
138
  return {
138
139
  client: clientId,
139
140
  serverName: MCP_SERVER_NAME,
@@ -143,7 +144,7 @@ function oauthJsonMcpTemplate(clientId, mcpUrl, snippet, options) {
143
144
  optionalEnv: [AGENT_INSTANCE_ENV_VAR],
144
145
  authentication: 'oauth',
145
146
  agentIdentity: {
146
- agentId: clientId,
147
+ agentId: identityClientId,
147
148
  agentIdHeader: AGENT_ID_HEADER,
148
149
  agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
149
150
  agentInstanceHeader: AGENT_INSTANCE_HEADER
@@ -153,4 +154,3 @@ function oauthJsonMcpTemplate(clientId, mcpUrl, snippet, options) {
153
154
  writesTokenValue: false
154
155
  };
155
156
  }
156
-
@@ -22,9 +22,9 @@ export const JSON_MCP_CLIENT_DEFINITIONS = Object.freeze([
22
22
  httpClientDefinition('cursor', 'Cursor', 'defaultCursorConfigPath', { urlKey: 'url', authentication: 'env-bearer' }),
23
23
  httpClientDefinition('gemini-cli', 'Gemini CLI', 'defaultGeminiConfigPath', { urlKey: 'httpUrl', authentication: 'oauth' }),
24
24
  httpClientDefinition('antigravity', 'Antigravity', 'defaultAntigravityConfigPath', { urlKey: 'serverUrl', authentication: 'oauth' }),
25
- httpClientDefinition('antigravity-ide', 'Antigravity IDE', 'defaultAntigravityIdeConfigPath', { urlKey: 'url', authentication: 'oauth', defaultIdentityId: 'antigravity', extra: { type: 'http' } }),
26
- httpClientDefinition('antigravity2', 'Antigravity 2.0', 'defaultAntigravity2ConfigPath', { urlKey: 'url', authentication: 'oauth', defaultIdentityId: 'antigravity', extra: { type: 'http' } }),
27
- httpClientDefinition('antigravity-cli', 'Antigravity CLI', 'defaultAntigravityCliConfigPath', { urlKey: 'httpUrl', authentication: 'oauth', defaultIdentityId: 'antigravity' }),
25
+ httpClientDefinition('antigravity-ide', 'Antigravity IDE', 'defaultAntigravityIdeConfigPath', { urlKey: 'serverUrl', authentication: 'oauth', defaultIdentityId: 'antigravity' }),
26
+ httpClientDefinition('antigravity2', 'Antigravity 2.0', 'defaultAntigravity2ConfigPath', { urlKey: 'serverUrl', authentication: 'oauth', defaultIdentityId: 'antigravity' }),
27
+ httpClientDefinition('antigravity-cli', 'Antigravity CLI', 'defaultAntigravityCliConfigPath', { urlKey: 'serverUrl', authentication: 'oauth', defaultIdentityId: 'antigravity' }),
28
28
  httpClientDefinition('windsurf', 'Windsurf', 'defaultWindsurfConfigPath', { urlKey: 'serverUrl', authentication: 'env-bearer' }),
29
29
  httpClientDefinition('cline', 'Cline', 'defaultClineConfigPath', { urlKey: 'httpUrl', authentication: 'env-bearer' }),
30
30
  nestedTransportClientDefinition('continue', 'Continue', 'defaultContinueConfigPath'),
package/src/ui/help.js CHANGED
@@ -25,6 +25,15 @@ export function writeHelp(io) {
25
25
  writeLine(io.stdout, ` Detects active workspace to auto-inject project-scoped instruction rules.`);
26
26
  writeLine(io.stdout, ` Pass --force to overwrite an existing mcpServers.XMemo entry.`);
27
27
  writeLine(io.stdout, '');
28
+ writeLine(io.stdout, ` ${COMMAND_NAME} setup openclaw [--with-mcp|--mcp-only] [--no-skill] [--dry-run] [--json]`);
29
+ writeLine(io.stdout, ` Installs/updates the native OpenClaw memory plugin and XMemo Skill.`);
30
+ writeLine(io.stdout, ` Use --with-mcp for native+MCP, or --mcp-only for MCP without native plugin/Skill.`);
31
+ writeLine(io.stdout, '');
32
+ writeLine(io.stdout, ` ${COMMAND_NAME} setup hermes [--with-mcp|--mcp-only] [--no-plugin] [--hermes-home <path>] [--dry-run] [--json]`);
33
+ writeLine(io.stdout, ` Installs/updates the native Hermes plugin and syncs the shared XMemo credential.`);
34
+ writeLine(io.stdout, ` Use --with-mcp for native+MCP, or --mcp-only for MCP without native plugin.`);
35
+ writeLine(io.stdout, ` Compatible with existing hermes-xmemo plugin .env setup.`);
36
+ writeLine(io.stdout, '');
28
37
  writeLine(io.stdout, ` ${COMMAND_NAME} uninstall --all [--yes] [--profiles] [--dry-run]`);
29
38
  writeLine(io.stdout, ` Removes the XMemo MCP server entry from every detected client config.`);
30
39
  writeLine(io.stdout, ` Use --profiles to also remove installed behavior profiles.`);
package/src/ui/setup.js CHANGED
@@ -54,7 +54,7 @@ const SETUP_CLIENT_ALIASES = new Map([
54
54
  ]);
55
55
 
56
56
  export function supportedSetupClientIds(mcpClients) {
57
- return [...supportedMcpClientIds(mcpClients), 'copilot-cli'];
57
+ return [...supportedMcpClientIds(mcpClients), 'copilot-cli', 'openclaw'];
58
58
  }
59
59
 
60
60
  export function requiredOption(args, name) {
@@ -172,6 +172,70 @@ export function writeSetupSummary(plan, io) {
172
172
  if (plan.selectedClient) {
173
173
  writeLine(io.stdout, '');
174
174
  writeLine(io.stdout, `Selected client: ${plan.selectedClient.label}`);
175
+ if (plan.selectedClient.id === 'openclaw') {
176
+ writeLine(io.stdout, ` Setup kind: ${plan.selectedClient.configKind}`);
177
+ writeLine(io.stdout, ` Setup mode: ${plan.selectedClient.setupMode}`);
178
+ writeLine(io.stdout, ` Plugin: ${plan.selectedClient.nativePlugin.package}`);
179
+ writeLine(io.stdout, ` Plugin installed: ${plan.selectedClient.nativePlugin.installed}`);
180
+ writeLine(io.stdout, ` Skill: ${plan.selectedClient.skill.ref}`);
181
+ writeLine(io.stdout, ` Skill installed: ${plan.selectedClient.skill.installed}`);
182
+ writeLine(io.stdout, ` Skill skipped: ${plan.selectedClient.skill.skipped}`);
183
+ writeLine(io.stdout, ` Credential source: ${plan.selectedClient.status?.credentialSource ?? plan.selectedClient.credential.source}`);
184
+ writeLine(io.stdout, ` Connected: ${plan.selectedClient.status?.connected ?? 'unknown'}`);
185
+ writeLine(io.stdout, ` Hosted MCP fallback: ${plan.selectedClient.mcp.enabled ? 'enabled' : 'not installed'}`);
186
+ if (plan.selectedClient.mcp.enabled) {
187
+ writeLine(io.stdout, ` MCP written: ${plan.selectedClient.mcp.written}`);
188
+ writeLine(io.stdout, ` MCP note: ${plan.selectedClient.mcp.note}`);
189
+ }
190
+ if (!plan.selectedClient.credential.ready) {
191
+ writeLine(io.stdout, ` Next credential step: ${COMMAND_NAME} login`);
192
+ }
193
+ if (plan.selectedClient.dryRun) {
194
+ writeLine(io.stdout, ' Dry run commands:');
195
+ writeLine(io.stdout, ` ${plan.selectedClient.nativePlugin.command}`);
196
+ if (!plan.selectedClient.skill.skipped) {
197
+ writeLine(io.stdout, ` ${plan.selectedClient.skill.command}`);
198
+ }
199
+ if (plan.selectedClient.mcp.enabled) {
200
+ writeLine(io.stdout, ` ${plan.selectedClient.mcp.command}`);
201
+ }
202
+ }
203
+ return;
204
+ }
205
+ if (plan.selectedClient.id === 'hermes') {
206
+ writeLine(io.stdout, ` Setup kind: ${plan.selectedClient.configKind}`);
207
+ writeLine(io.stdout, ` Setup mode: ${plan.selectedClient.setupMode}`);
208
+ writeLine(io.stdout, ` Config path: ${plan.selectedClient.configPath}`);
209
+ writeLine(io.stdout, ` Hermes home: ${plan.selectedClient.hermesHome}`);
210
+ writeLine(io.stdout, ` Hermes env: ${plan.selectedClient.hermesEnvPath}`);
211
+ writeLine(io.stdout, ` Credential ready: ${plan.selectedClient.credential.ready}`);
212
+ writeLine(io.stdout, ` Credential source: ${plan.selectedClient.credential.source}`);
213
+ writeLine(io.stdout, ` Hermes env synced: ${plan.selectedClient.credential.hermesEnvSynced}`);
214
+ writeLine(io.stdout, ` Shared credential backfilled: ${plan.selectedClient.credential.sharedCredentialBackfilled}`);
215
+ writeLine(io.stdout, ` Native plugin package: ${plan.selectedClient.nativePlugin.package}`);
216
+ writeLine(io.stdout, ` Native plugin installed: ${plan.selectedClient.nativePlugin.installed}`);
217
+ writeLine(io.stdout, ` Native plugin skipped: ${plan.selectedClient.nativePlugin.skipped}`);
218
+ writeLine(io.stdout, ` Native plugin note: ${plan.selectedClient.nativePlugin.note}`);
219
+ writeLine(io.stdout, ` Hosted MCP fallback: ${plan.selectedClient.mcp.enabled ? 'enabled' : 'not installed'}`);
220
+ writeLine(io.stdout, ` MCP written: ${plan.selectedClient.mcp.written}`);
221
+ writeLine(io.stdout, ` MCP note: ${plan.selectedClient.mcp.note}`);
222
+ writeLine(io.stdout, ` Token value embedded in MCP config: ${plan.selectedClient.tokenValueEmbeddedInMcpConfig}`);
223
+ if (!plan.selectedClient.credential.ready) {
224
+ writeLine(io.stdout, ` Next credential step: ${COMMAND_NAME} login`);
225
+ }
226
+ if (plan.selectedClient.dryRun) {
227
+ writeLine(io.stdout, ' Dry run actions:');
228
+ if (!plan.selectedClient.nativePlugin.skipped) {
229
+ writeLine(io.stdout, ` ${plan.selectedClient.nativePlugin.installCommand}`);
230
+ writeLine(io.stdout, ` ${plan.selectedClient.nativePlugin.activateCommand}`);
231
+ }
232
+ writeLine(io.stdout, ` sync XMemo credential to ${plan.selectedClient.hermesEnvPath}`);
233
+ if (plan.selectedClient.mcp.enabled) {
234
+ writeLine(io.stdout, ` write Hermes MCP config to ${plan.selectedClient.configPath}`);
235
+ }
236
+ }
237
+ return;
238
+ }
175
239
  writeLine(io.stdout, ` Config path: ${plan.selectedClient.configPath}`);
176
240
  writeLine(io.stdout, ` Written: ${plan.selectedClient.written}`);
177
241
  writeLine(io.stdout, ` Token value embedded: ${plan.selectedClient.writesTokenValue}`);