@xmemo/client 0.4.166 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmemo/client",
3
- "version": "0.4.166",
3
+ "version": "0.4.168",
4
4
  "description": "Privacy-first CLI and MCP setup helper for XMemo.",
5
5
  "mcpName": "io.github.yonro/xmemo",
6
6
  "type": "module",
@@ -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
  }
@@ -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) {
@@ -236,7 +236,7 @@ function mcpRemoteCommandJsonServerConfig(mcpUrl, identity) {
236
236
  };
237
237
  }
238
238
 
239
- async function mergeJsonSectionConfig(configPath, sectionName, serverConfig, duplicatePath = sectionName, afterMerge) {
239
+ async function mergeJsonSectionConfig(configPath, sectionName, serverConfig, duplicatePath = sectionName, afterMerge, force = false) {
240
240
  const existing = await readTextIfExists(configPath);
241
241
  const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
242
242
  if (!isPlainObject(parsed)) {
@@ -246,8 +246,8 @@ async function mergeJsonSectionConfig(configPath, sectionName, serverConfig, dup
246
246
  parsed[sectionName] = {};
247
247
  }
248
248
  const existingName = existingJsonMcpServerName(parsed[sectionName]);
249
- if (existingName) {
250
- 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.`);
251
251
  }
252
252
  parsed[sectionName][MCP_SERVER_NAME] = serverConfig;
253
253
  afterMerge?.(parsed);
@@ -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
- const existing = await readTextIfExists(configPath);
52
+ let existing = await readTextIfExists(configPath);
53
53
  const existingName = existingTomlMcpServerName(existing);
54
54
  if (existingName) {
55
- throw new UsageError(`MCP config already contains [mcp_servers.${existingName}]. Edit ${configPath} manually to avoid duplicate server definitions.`);
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.appendFile(configPath, `${prefix}${snippet}`, { mode: 0o600 });
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
- const existing = await readTextIfExists(configPath);
135
+ let existing = await readTextIfExists(configPath);
133
136
  const existingName = existingTomlMcpServerName(existing);
134
137
  if (existingName) {
135
- throw new UsageError(`MCP config already contains [mcp_servers.${existingName}]. Edit ${configPath} manually to avoid duplicate server definitions.`);
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.appendFile(configPath, `${prefix}${snippet}`, { mode: 0o600 });
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);
@@ -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
- throw new UsageError(`MCP config already contains ${MCP_SERVER_NAME} in mcp_servers. Edit ${configPath} manually to avoid duplicate server definitions.`);
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 });
@@ -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.');