@xmemo/client 0.4.165 → 0.4.168
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/package.json +1 -1
- package/src/commands/setup.js +5 -4
- package/src/mcp/clients/registry.js +4 -4
- package/src/mcp/formats/json.js +15 -6
- package/src/mcp/formats/toml.js +35 -8
- package/src/mcp/formats/yaml.js +5 -2
- package/src/mcp/proxy/copilot.js +3 -3
- package/src/ui/help.js +4 -2
- package/src/ui/setup.js +10 -0
package/package.json
CHANGED
package/src/commands/setup.js
CHANGED
|
@@ -66,6 +66,7 @@ export async function setupCommand(args, io) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const dryRun = hasFlag(optionArgs, '--dry-run') || hasFlag(optionArgs, '--preview');
|
|
69
|
+
const force = hasFlag(optionArgs, '--force');
|
|
69
70
|
const writeConfig = !dryRun && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes') || shortClientSetup || (setupAll && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes'))));
|
|
70
71
|
const timeoutMs = parsePositiveInteger(optionValue(optionArgs, '--timeout-ms') ?? '5000', '--timeout-ms');
|
|
71
72
|
|
|
@@ -102,7 +103,7 @@ export async function setupCommand(args, io) {
|
|
|
102
103
|
clientPlan = copilotSetupPlan(setupPlan.mcpUrl, proxyPort, io.env);
|
|
103
104
|
clientPlan.configPath = detection.path;
|
|
104
105
|
if (writeConfig) {
|
|
105
|
-
await mergeCopilotMcpConfig(clientPlan.configPath, clientPlan.proxyUrl);
|
|
106
|
+
await mergeCopilotMcpConfig(clientPlan.configPath, clientPlan.proxyUrl, force);
|
|
106
107
|
clientPlan.written = true;
|
|
107
108
|
}
|
|
108
109
|
} else {
|
|
@@ -111,7 +112,7 @@ export async function setupCommand(args, io) {
|
|
|
111
112
|
clientPlan = clientSetupPlan(scanId, client, setupPlan.mcpUrl, io.env, identity);
|
|
112
113
|
clientPlan.configPath = detection.path;
|
|
113
114
|
if (writeConfig) {
|
|
114
|
-
await client.writeConfig(clientPlan.configPath, setupPlan.mcpUrl, identity);
|
|
115
|
+
await client.writeConfig(clientPlan.configPath, setupPlan.mcpUrl, identity, { force });
|
|
115
116
|
clientPlan.written = true;
|
|
116
117
|
if (profileClientConfig(scanId)) {
|
|
117
118
|
const installProfile = hasFlag(optionArgs, '--yes') || hasFlag(optionArgs, '--profile');
|
|
@@ -134,7 +135,7 @@ export async function setupCommand(args, io) {
|
|
|
134
135
|
const proxyPort = parsePositiveInteger(optionValue(optionArgs, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
|
|
135
136
|
setupPlan.selectedClient = copilotSetupPlan(setupPlan.mcpUrl, proxyPort, io.env);
|
|
136
137
|
if (writeConfig) {
|
|
137
|
-
await mergeCopilotMcpConfig(setupPlan.selectedClient.configPath, setupPlan.selectedClient.proxyUrl);
|
|
138
|
+
await mergeCopilotMcpConfig(setupPlan.selectedClient.configPath, setupPlan.selectedClient.proxyUrl, force);
|
|
138
139
|
setupPlan.selectedClient.written = true;
|
|
139
140
|
}
|
|
140
141
|
} else {
|
|
@@ -146,7 +147,7 @@ export async function setupCommand(args, io) {
|
|
|
146
147
|
const identity = writeConfig ? await agentIdentity(clientId, io.env) : envReferenceIdentity(clientId);
|
|
147
148
|
setupPlan.selectedClient = clientSetupPlan(clientId, client, setupPlan.mcpUrl, io.env, identity);
|
|
148
149
|
if (writeConfig) {
|
|
149
|
-
await client.writeConfig(setupPlan.selectedClient.configPath, setupPlan.mcpUrl, identity);
|
|
150
|
+
await client.writeConfig(setupPlan.selectedClient.configPath, setupPlan.mcpUrl, identity, { force });
|
|
150
151
|
setupPlan.selectedClient.written = true;
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -7,7 +7,7 @@ export function createMcpClients(deps) {
|
|
|
7
7
|
label: 'Codex',
|
|
8
8
|
defaultConfigPath: deps.defaultCodexConfigPath,
|
|
9
9
|
buildSnippet: deps.codexTomlSnippet,
|
|
10
|
-
writeConfig: deps.appendTomlServerConfig,
|
|
10
|
+
writeConfig: (configPath, mcpUrl, identity, options = {}) => deps.appendTomlServerConfig(configPath, mcpUrl, identity, options.force),
|
|
11
11
|
configKind: 'toml'
|
|
12
12
|
});
|
|
13
13
|
|
|
@@ -15,7 +15,7 @@ export function createMcpClients(deps) {
|
|
|
15
15
|
label: 'Grok',
|
|
16
16
|
defaultConfigPath: deps.defaultGrokConfigPath,
|
|
17
17
|
buildSnippet: deps.grokTomlSnippet,
|
|
18
|
-
writeConfig: deps.appendGrokServerConfig,
|
|
18
|
+
writeConfig: (configPath, mcpUrl, identity, options = {}) => deps.appendGrokServerConfig(configPath, mcpUrl, identity, options.force),
|
|
19
19
|
configKind: 'toml'
|
|
20
20
|
});
|
|
21
21
|
|
|
@@ -38,7 +38,7 @@ function jsonClient(definition, deps) {
|
|
|
38
38
|
label: definition.label,
|
|
39
39
|
defaultConfigPath: deps[definition.defaultConfigPath],
|
|
40
40
|
buildSnippet: (mcpUrl, identity) => deps.jsonClientSnippet(definition.id, mcpUrl, identity),
|
|
41
|
-
writeConfig: (configPath, mcpUrl, identity) => deps.mergeJsonClientMcpConfig(definition.id, configPath, mcpUrl, identity),
|
|
41
|
+
writeConfig: (configPath, mcpUrl, identity, options = {}) => deps.mergeJsonClientMcpConfig(definition.id, configPath, mcpUrl, identity, options.force),
|
|
42
42
|
configKind: definition.configKind,
|
|
43
43
|
authentication: definition.authentication
|
|
44
44
|
};
|
|
@@ -49,7 +49,7 @@ function hermesClient(deps) {
|
|
|
49
49
|
label: 'Hermes',
|
|
50
50
|
defaultConfigPath: deps.defaultHermesConfigPath,
|
|
51
51
|
buildSnippet: deps.hermesYamlSnippet,
|
|
52
|
-
writeConfig: deps.mergeHermesMcpConfig,
|
|
52
|
+
writeConfig: (configPath, mcpUrl, identity, options = {}) => deps.mergeHermesMcpConfig(configPath, mcpUrl, identity, options.force),
|
|
53
53
|
configKind: 'yaml'
|
|
54
54
|
};
|
|
55
55
|
}
|
package/src/mcp/formats/json.js
CHANGED
|
@@ -31,7 +31,7 @@ export const JSON_MCP_CLIENT_DEFINITIONS = Object.freeze([
|
|
|
31
31
|
commandClientDefinition('claude-desktop', 'Claude Desktop', 'defaultClaudeConfigPath'),
|
|
32
32
|
httpClientDefinition('openclaw', 'OpenClaw', 'defaultOpenclawConfigPath', { urlKey: 'url', authentication: 'env-bearer' }),
|
|
33
33
|
commandClientDefinition('kiro', 'Kiro', 'defaultKiroConfigPath'),
|
|
34
|
-
httpClientDefinition('kimi-code', 'Kimi Code', 'defaultKimiCodeConfigPath', { urlKey: 'url', authentication: 'env-
|
|
34
|
+
httpClientDefinition('kimi-code', 'Kimi Code', 'defaultKimiCodeConfigPath', { urlKey: 'url', authentication: 'bearer-token-env-var', bearerTokenEnvVar: 'XMEMO_KEY' }),
|
|
35
35
|
commandClientDefinition('zed', 'Zed', 'defaultZedConfigPath', { section: 'context_servers' }),
|
|
36
36
|
nestedTransportClientDefinition('jetbrains', 'JetBrains', 'defaultJetbrainsConfigPath'),
|
|
37
37
|
remoteClientDefinition('opencode', 'OpenCode', 'defaultOpencodeConfigPath'),
|
|
@@ -118,14 +118,14 @@ export function jsonClientServerConfig(clientId, mcpUrl, identity) {
|
|
|
118
118
|
return serverConfigFromDefinition(definition, mcpUrl, resolvedIdentity);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
export async function mergeJsonClientMcpConfig(clientId, configPath, mcpUrl, identity) {
|
|
121
|
+
export async function mergeJsonClientMcpConfig(clientId, configPath, mcpUrl, identity, force = false) {
|
|
122
122
|
const definition = requireJsonMcpClientDefinition(clientId);
|
|
123
123
|
const serverConfig = serverConfigFromDefinition(definition, mcpUrl, identity);
|
|
124
124
|
await mergeJsonSectionConfig(configPath, definition.section, serverConfig, definition.section, (parsed) => {
|
|
125
125
|
if (definition.mergeExperimentalModelContextProtocolServers && isPlainObject(parsed.experimental)) {
|
|
126
126
|
mergeExperimentalModelContextProtocolServers(parsed, serverConfig, mcpUrl);
|
|
127
127
|
}
|
|
128
|
-
});
|
|
128
|
+
}, force);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
function requireJsonMcpClientDefinition(clientId) {
|
|
@@ -168,6 +168,15 @@ function serverConfigFromDefinition(definition, mcpUrl, identity) {
|
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
if (definition.authentication === 'bearer-token-env-var') {
|
|
172
|
+
return {
|
|
173
|
+
...(definition.extra ?? {}),
|
|
174
|
+
[definition.urlKey]: mcpUrl,
|
|
175
|
+
bearerTokenEnvVar: definition.bearerTokenEnvVar,
|
|
176
|
+
headers: headersForDefinition(definition, identity)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
171
180
|
return {
|
|
172
181
|
...(definition.extra ?? {}),
|
|
173
182
|
[definition.urlKey]: mcpUrl,
|
|
@@ -227,7 +236,7 @@ function mcpRemoteCommandJsonServerConfig(mcpUrl, identity) {
|
|
|
227
236
|
};
|
|
228
237
|
}
|
|
229
238
|
|
|
230
|
-
async function mergeJsonSectionConfig(configPath, sectionName, serverConfig, duplicatePath = sectionName, afterMerge) {
|
|
239
|
+
async function mergeJsonSectionConfig(configPath, sectionName, serverConfig, duplicatePath = sectionName, afterMerge, force = false) {
|
|
231
240
|
const existing = await readTextIfExists(configPath);
|
|
232
241
|
const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
|
|
233
242
|
if (!isPlainObject(parsed)) {
|
|
@@ -237,8 +246,8 @@ async function mergeJsonSectionConfig(configPath, sectionName, serverConfig, dup
|
|
|
237
246
|
parsed[sectionName] = {};
|
|
238
247
|
}
|
|
239
248
|
const existingName = existingJsonMcpServerName(parsed[sectionName]);
|
|
240
|
-
if (existingName) {
|
|
241
|
-
throw new UsageError(`MCP config already contains ${duplicatePath}.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
|
|
249
|
+
if (existingName && !force) {
|
|
250
|
+
throw new UsageError(`MCP config already contains ${duplicatePath}.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions, or use --force to overwrite.`);
|
|
242
251
|
}
|
|
243
252
|
parsed[sectionName][MCP_SERVER_NAME] = serverConfig;
|
|
244
253
|
afterMerge?.(parsed);
|
package/src/mcp/formats/toml.js
CHANGED
|
@@ -47,17 +47,20 @@ ${AGENT_INSTANCE_HEADER} = "${escapeTomlString(agentInstanceId)}"
|
|
|
47
47
|
`;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export async function appendGrokServerConfig(configPath, mcpUrl, identity) {
|
|
50
|
+
export async function appendGrokServerConfig(configPath, mcpUrl, identity, force = false) {
|
|
51
51
|
const snippet = grokTomlSnippet(mcpUrl, identity);
|
|
52
|
-
|
|
52
|
+
let existing = await readTextIfExists(configPath);
|
|
53
53
|
const existingName = existingTomlMcpServerName(existing);
|
|
54
54
|
if (existingName) {
|
|
55
|
-
|
|
55
|
+
if (!force) {
|
|
56
|
+
throw new UsageError(`MCP config already contains [mcp_servers.${existingName}]. Edit ${configPath} manually to avoid duplicate server definitions, or use --force to overwrite.`);
|
|
57
|
+
}
|
|
58
|
+
existing = removeTomlServerBlock(existing, existingName);
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
59
62
|
const prefix = existing.trim().length === 0 ? '' : '\n\n';
|
|
60
|
-
await fs.
|
|
63
|
+
await fs.writeFile(configPath, `${existing.trimEnd()}${prefix}${snippet}\n`, { mode: 0o600 });
|
|
61
64
|
await bestEffortChmod(configPath, 0o600);
|
|
62
65
|
}
|
|
63
66
|
|
|
@@ -127,17 +130,20 @@ export async function codexSmokeReport(configPath, env) {
|
|
|
127
130
|
};
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
export async function appendTomlServerConfig(configPath, mcpUrl, identity) {
|
|
133
|
+
export async function appendTomlServerConfig(configPath, mcpUrl, identity, force = false) {
|
|
131
134
|
const snippet = codexTomlSnippet(mcpUrl, identity);
|
|
132
|
-
|
|
135
|
+
let existing = await readTextIfExists(configPath);
|
|
133
136
|
const existingName = existingTomlMcpServerName(existing);
|
|
134
137
|
if (existingName) {
|
|
135
|
-
|
|
138
|
+
if (!force) {
|
|
139
|
+
throw new UsageError(`MCP config already contains [mcp_servers.${existingName}]. Edit ${configPath} manually to avoid duplicate server definitions, or use --force to overwrite.`);
|
|
140
|
+
}
|
|
141
|
+
existing = removeTomlServerBlock(existing, existingName);
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
139
145
|
const prefix = existing.trim().length === 0 ? '' : '\n\n';
|
|
140
|
-
await fs.
|
|
146
|
+
await fs.writeFile(configPath, `${existing.trimEnd()}${prefix}${snippet}\n`, { mode: 0o600 });
|
|
141
147
|
await bestEffortChmod(configPath, 0o600);
|
|
142
148
|
}
|
|
143
149
|
|
|
@@ -176,6 +182,27 @@ function tomlServerBlock(content, serverName) {
|
|
|
176
182
|
return block.join('\n');
|
|
177
183
|
}
|
|
178
184
|
|
|
185
|
+
function removeTomlServerBlock(content, serverName) {
|
|
186
|
+
const header = `[mcp_servers.${serverName}]`;
|
|
187
|
+
const lines = content.split(/\r?\n/);
|
|
188
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
189
|
+
if (start === -1) {
|
|
190
|
+
return content;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let end = start + 1;
|
|
194
|
+
for (; end < lines.length; end += 1) {
|
|
195
|
+
const line = lines[end];
|
|
196
|
+
if (/^\s*\[/.test(line)) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const before = lines.slice(0, start).join('\n');
|
|
202
|
+
const after = lines.slice(end).join('\n');
|
|
203
|
+
return `${before}\n${after}`.trim();
|
|
204
|
+
}
|
|
205
|
+
|
|
179
206
|
function tomlStringValue(block, key) {
|
|
180
207
|
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"((?:\\\\.|[^"\\\\])*)"\\s*$`, 'm');
|
|
181
208
|
const match = block.match(pattern);
|
package/src/mcp/formats/yaml.js
CHANGED
|
@@ -33,11 +33,14 @@ export function hermesYamlSnippet(mcpUrl, identity = envReferenceIdentity('herme
|
|
|
33
33
|
`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export async function mergeHermesMcpConfig(configPath, mcpUrl, identity) {
|
|
36
|
+
export async function mergeHermesMcpConfig(configPath, mcpUrl, identity, force = false) {
|
|
37
37
|
const existing = await readTextIfExists(configPath);
|
|
38
38
|
|
|
39
39
|
if (existing.includes(`${MCP_SERVER_NAME}:`) || existing.includes('memory_os:') || existing.includes('memory-os:')) {
|
|
40
|
-
|
|
40
|
+
if (!force) {
|
|
41
|
+
throw new UsageError(`MCP config already contains ${MCP_SERVER_NAME} in mcp_servers. Edit ${configPath} manually to avoid duplicate server definitions, or use --force to overwrite.`);
|
|
42
|
+
}
|
|
43
|
+
throw new UsageError(`--force overwrite is not yet supported for Hermes YAML configs. Edit ${configPath} manually.`);
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
package/src/mcp/proxy/copilot.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { existingJsonMcpServerName } from '../core/names.js';
|
|
13
13
|
export { mcpProxyCommand } from './server.js';
|
|
14
14
|
|
|
15
|
-
export async function mergeCopilotMcpConfig(configPath, proxyUrl) {
|
|
15
|
+
export async function mergeCopilotMcpConfig(configPath, proxyUrl, force = false) {
|
|
16
16
|
const existing = await readTextIfExists(configPath);
|
|
17
17
|
const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
|
|
18
18
|
|
|
@@ -25,8 +25,8 @@ export async function mergeCopilotMcpConfig(configPath, proxyUrl) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const existingName = existingJsonMcpServerName(parsed.mcpServers);
|
|
28
|
-
if (existingName) {
|
|
29
|
-
throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions.`);
|
|
28
|
+
if (existingName && !force) {
|
|
29
|
+
throw new UsageError(`MCP config already contains mcpServers.${existingName}. Edit ${configPath} manually to avoid duplicate server definitions, or use --force to overwrite.`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
parsed.mcpServers[MCP_SERVER_NAME] = copilotLocalProxyServerConfig(proxyUrl);
|
package/src/ui/help.js
CHANGED
|
@@ -14,14 +14,16 @@ export function writeHelp(io) {
|
|
|
14
14
|
writeLine(io.stdout, `Official package: ${PACKAGE_NAME} | Legacy command: ${LEGACY_COMMAND_NAME}`);
|
|
15
15
|
writeLine(io.stdout, '');
|
|
16
16
|
writeLine(io.stdout, '💡 CORE ONBOARDING & SETUP COMMANDS:');
|
|
17
|
-
writeLine(io.stdout, ` ${COMMAND_NAME} setup --all [--write] [--profile]`);
|
|
17
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} setup --all [--write] [--profile] [--force]`);
|
|
18
18
|
writeLine(io.stdout, ` Auto-detects all local client installations (Cursor, VS Code, Continue, Trae, etc.).`);
|
|
19
19
|
writeLine(io.stdout, ` Merges XMemo MCP configs. Pass --profile to auto-inject workspace prompt rules.`);
|
|
20
20
|
writeLine(io.stdout, ` *Dry-run by default unless --write (or --yes/-y) is specified for safety.*`);
|
|
21
|
+
writeLine(io.stdout, ` Pass --force to overwrite an existing mcpServers.XMemo entry.`);
|
|
21
22
|
writeLine(io.stdout, '');
|
|
22
|
-
writeLine(io.stdout, ` ${COMMAND_NAME} setup <client-id> [--url <url>] [--no-profile] [--json]`);
|
|
23
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} setup <client-id> [--url <url>] [--no-profile] [--json] [--force]`);
|
|
23
24
|
writeLine(io.stdout, ` Runs interactive setup wizard for a single client (e.g. cursor, gemini, antigravity).`);
|
|
24
25
|
writeLine(io.stdout, ` Detects active workspace to auto-inject project-scoped instruction rules.`);
|
|
26
|
+
writeLine(io.stdout, ` Pass --force to overwrite an existing mcpServers.XMemo entry.`);
|
|
25
27
|
writeLine(io.stdout, '');
|
|
26
28
|
writeLine(io.stdout, ` ${COMMAND_NAME} login [--from-stdin] [--base-url <url>]`);
|
|
27
29
|
writeLine(io.stdout, ` Starts secure OAuth2 browser-based device login flow to register the CLI.`);
|
package/src/ui/setup.js
CHANGED
|
@@ -223,6 +223,16 @@ export function writeSetupSummary(plan, io) {
|
|
|
223
223
|
if (plan.tokenPortalUrl) {
|
|
224
224
|
writeLine(io.stdout, ` (Token portal: ${plan.tokenPortalUrl})`);
|
|
225
225
|
}
|
|
226
|
+
} else if (cid === 'kimi-code') {
|
|
227
|
+
writeLine(io.stdout, `💡 Next steps for ${plan.selectedClient.label}:`);
|
|
228
|
+
writeLine(io.stdout, ' 1. Restart Kimi Code to load the new MCP configuration.');
|
|
229
|
+
writeLine(io.stdout, ` 2. Make sure ${TOKEN_ENV_VAR} is exported in the SAME environment that starts Kimi Code.`);
|
|
230
|
+
writeLine(io.stdout, ` Kimi Code reads the token from process.env['${TOKEN_ENV_VAR}'] via bearerTokenEnvVar.`);
|
|
231
|
+
writeLine(io.stdout, ` Example (PowerShell): $env:${TOKEN_ENV_VAR}='<your-token>'; kimi`);
|
|
232
|
+
writeLine(io.stdout, ` Example (bash): export ${TOKEN_ENV_VAR}=<your-token> && kimi`);
|
|
233
|
+
if (plan.tokenPortalUrl) {
|
|
234
|
+
writeLine(io.stdout, ` (Token portal: ${plan.tokenPortalUrl})`);
|
|
235
|
+
}
|
|
226
236
|
} else if (usesClientOAuth(cid)) {
|
|
227
237
|
writeLine(io.stdout, `💡 Next steps for ${plan.selectedClient.label}:`);
|
|
228
238
|
writeLine(io.stdout, ' 1. When the agent starts or first makes an XMemo tool call, a browser window will automatically pop up requesting OAuth authorization.');
|