@xmemo/client 0.4.170 → 0.4.172

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
@@ -64,6 +64,13 @@ xmemo setup cursor
64
64
  xmemo setup cursor --dry-run
65
65
  xmemo setup copilot
66
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
67
74
  xmemo setup gemini
68
75
  xmemo setup gemini --dry-run
69
76
  xmemo setup antigravity
@@ -194,6 +201,8 @@ xmemo setup codex
194
201
  xmemo setup codex --url "https://your-private-service.example"
195
202
  xmemo setup cursor
196
203
  xmemo setup copilot
204
+ xmemo setup openclaw
205
+ xmemo setup hermes
197
206
  xmemo setup gemini
198
207
  xmemo setup antigravity
199
208
  ```
@@ -202,10 +211,12 @@ xmemo setup antigravity
202
211
  clients, it applies the user-scoped config directly; use `--dry-run` to preview
203
212
  without writing. Codex/Cursor configs reference `XMEMO_KEY`; OAuth-native
204
213
  clients such as Gemini CLI and Antigravity use the client's MCP OAuth flow
205
- instead. No generated config embeds a token value. Write-capable client configs
206
- also include stable non-secret agent identity headers where the client format
207
- supports them. `--yes` remains accepted for Codex and Cursor as a compatibility
208
- 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.
209
220
 
210
221
  After writing MCP config, `xmemo setup <client>` prompts:
211
222
 
@@ -296,12 +307,100 @@ template and apply it manually after review:
296
307
  xmemo mcp config --client generic --base-url "https://your-private-service.example" --json
297
308
  ```
298
309
 
299
- Codex, Cursor, Copilot CLI, Gemini CLI, Antigravity, and Kiro have write-capable setup
300
- helpers. Antigravity 2.0 is write-capable through `xmemo mcp add antigravity2
301
- --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`.
302
313
  Other client writes should only be added after their official user-scoped config
303
314
  format is verified.
304
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
+
305
404
  ### Copilot CLI
306
405
 
307
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.170",
3
+ "version": "0.4.172",
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) {
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}`);