@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 +106 -7
- package/package.json +1 -1
- package/src/commands/hermes.js +214 -0
- package/src/commands/openclaw.js +153 -0
- package/src/commands/setup.js +22 -1
- package/src/ui/help.js +9 -0
- package/src/ui/setup.js +65 -1
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.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
300
|
-
helpers. Antigravity 2.0 is write-capable through
|
|
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
|
@@ -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
|
+
}
|
package/src/commands/setup.js
CHANGED
|
@@ -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 === '
|
|
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}`);
|