@xmemo/client 0.4.155 → 0.4.156
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 +2 -2
- package/src/args.js +63 -0
- package/src/auth.js +199 -0
- package/src/base-url.js +12 -0
- package/src/cli.js +23 -3996
- package/src/commands/auth.js +229 -0
- package/src/commands/diagnostics.js +196 -0
- package/src/commands/mcp.js +187 -0
- package/src/commands/profile.js +56 -0
- package/src/commands/setup.js +190 -0
- package/src/commands/update.js +57 -0
- package/src/constants.js +32 -0
- package/src/discovery.js +102 -0
- package/src/env.js +81 -0
- package/src/errors.js +6 -0
- package/src/help.js +58 -0
- package/src/http.js +160 -0
- package/src/io.js +16 -0
- package/src/mcp/clients.js +80 -0
- package/src/mcp/codex.js +147 -0
- package/src/mcp/copilot-proxy.js +43 -0
- package/src/mcp/detect.js +50 -0
- package/src/mcp/hermes.js +71 -0
- package/src/mcp/identity.js +62 -0
- package/src/mcp/json-clients.js +354 -0
- package/src/mcp/names.js +12 -0
- package/src/mcp/paths.js +154 -0
- package/src/mcp/proxy.js +111 -0
- package/src/mcp/registry.js +67 -0
- package/src/mcp/templates.js +155 -0
- package/src/path-config.js +25 -0
- package/src/profile.js +532 -0
- package/src/runtime.js +144 -0
- package/src/setup.js +243 -0
- package/src/version.js +1 -0
package/src/http.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLI_VERSION,
|
|
3
|
+
COMMAND_NAME
|
|
4
|
+
} from './constants.js';
|
|
5
|
+
import { UsageError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
export async function verifyTokenWithMcp(baseUrl, token, timeoutMs, io) {
|
|
8
|
+
const url = endpointUrl(baseUrl, '/mcp');
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
11
|
+
try {
|
|
12
|
+
const response = await io.fetch(url, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: {
|
|
15
|
+
accept: 'application/json, text/event-stream',
|
|
16
|
+
'content-type': 'application/json',
|
|
17
|
+
authorization: `Bearer ${token}`,
|
|
18
|
+
'user-agent': `XMemo-CLI/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
jsonrpc: '2.0',
|
|
22
|
+
id: 1,
|
|
23
|
+
method: 'initialize',
|
|
24
|
+
params: {
|
|
25
|
+
protocolVersion: '2024-11-05',
|
|
26
|
+
capabilities: {},
|
|
27
|
+
clientInfo: { name: COMMAND_NAME, version: CLI_VERSION }
|
|
28
|
+
}
|
|
29
|
+
}),
|
|
30
|
+
signal: controller.signal
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
ok: response.ok,
|
|
34
|
+
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}`
|
|
35
|
+
};
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
detail: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
|
|
40
|
+
};
|
|
41
|
+
} finally {
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function probe(url, timeoutMs, io) {
|
|
47
|
+
if (typeof io.fetch !== 'function') {
|
|
48
|
+
return { url, ok: false, error: 'fetch unavailable in this Node runtime' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const response = await io.fetch(url, {
|
|
56
|
+
headers: { accept: 'application/json' },
|
|
57
|
+
signal: controller.signal
|
|
58
|
+
});
|
|
59
|
+
return { url, ok: response.ok, status: response.status };
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
url,
|
|
63
|
+
ok: false,
|
|
64
|
+
error: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
|
|
65
|
+
};
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(timeout);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function fetchJson(url, timeoutMs, io) {
|
|
72
|
+
if (typeof io.fetch !== 'function') {
|
|
73
|
+
throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await io.fetch(url, {
|
|
81
|
+
headers: { accept: 'application/json' },
|
|
82
|
+
signal: controller.signal
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new UsageError(`Discovery request failed with HTTP ${response.status}: ${url}`);
|
|
86
|
+
}
|
|
87
|
+
return await response.json();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof UsageError) {
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
|
|
93
|
+
throw new UsageError(`Discovery request failed: ${url} (${reason})`);
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function postJson(url, payload, timeoutMs, io, options = {}) {
|
|
100
|
+
if (typeof io.fetch !== 'function') {
|
|
101
|
+
throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const response = await io.fetch(url, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
accept: 'application/json',
|
|
112
|
+
'content-type': 'application/json'
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify(payload),
|
|
115
|
+
signal: controller.signal
|
|
116
|
+
});
|
|
117
|
+
const responsePayload = await response.json();
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const error = responsePayload?.error ?? responsePayload?.detail ?? `HTTP ${response.status}`;
|
|
120
|
+
if (options.allowDevicePending && (error === 'authorization_pending' || error === 'slow_down')) {
|
|
121
|
+
return { error };
|
|
122
|
+
}
|
|
123
|
+
throw new UsageError(`Request failed with HTTP ${response.status}: ${url} (${error})`);
|
|
124
|
+
}
|
|
125
|
+
return responsePayload;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof UsageError) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
|
|
131
|
+
throw new UsageError(`Request failed: ${url} (${reason})`);
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(timeout);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function normalizeBaseUrl(input) {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = new URL(input);
|
|
140
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
141
|
+
throw new UsageError('URL must use http or https.');
|
|
142
|
+
}
|
|
143
|
+
parsed.hash = '';
|
|
144
|
+
parsed.search = '';
|
|
145
|
+
return parsed.toString().replace(/\/$/, '');
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof UsageError) {
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
throw new UsageError(`Invalid URL: ${input}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function endpointUrl(baseUrl, pathname) {
|
|
155
|
+
const url = new URL(baseUrl);
|
|
156
|
+
url.pathname = pathname;
|
|
157
|
+
url.hash = '';
|
|
158
|
+
url.search = '';
|
|
159
|
+
return url.toString();
|
|
160
|
+
}
|
package/src/io.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export function defaultIo() {
|
|
4
|
+
return {
|
|
5
|
+
env: process.env,
|
|
6
|
+
stdin: process.stdin,
|
|
7
|
+
stdout: process.stdout,
|
|
8
|
+
stderr: process.stderr,
|
|
9
|
+
fetch: globalThis.fetch,
|
|
10
|
+
spawn
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function writeLine(stream, line) {
|
|
15
|
+
stream.write(`${line}\n`);
|
|
16
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendTomlServerConfig,
|
|
3
|
+
codexTomlSnippet
|
|
4
|
+
} from './codex.js';
|
|
5
|
+
import {
|
|
6
|
+
hermesYamlSnippet,
|
|
7
|
+
mergeHermesMcpConfig
|
|
8
|
+
} from './hermes.js';
|
|
9
|
+
import {
|
|
10
|
+
JSON_MCP_CLIENT_DEFINITIONS,
|
|
11
|
+
jsonClientSnippet,
|
|
12
|
+
mergeJsonClientMcpConfig
|
|
13
|
+
} from './json-clients.js';
|
|
14
|
+
import {
|
|
15
|
+
defaultAntigravity2ConfigPath,
|
|
16
|
+
defaultAntigravityCliConfigPath,
|
|
17
|
+
defaultAntigravityConfigPath,
|
|
18
|
+
defaultAntigravityIdeConfigPath,
|
|
19
|
+
defaultClaudeConfigPath,
|
|
20
|
+
defaultClaudecodeConfigPath,
|
|
21
|
+
defaultClineConfigPath,
|
|
22
|
+
defaultCodexConfigPath,
|
|
23
|
+
defaultContinueConfigPath,
|
|
24
|
+
defaultCursorConfigPath,
|
|
25
|
+
defaultGeminiConfigPath,
|
|
26
|
+
defaultHermesConfigPath,
|
|
27
|
+
defaultJetbrainsConfigPath,
|
|
28
|
+
defaultKiroConfigPath,
|
|
29
|
+
defaultOpencodeConfigPath,
|
|
30
|
+
defaultOpenclawConfigPath,
|
|
31
|
+
defaultQwenConfigPath,
|
|
32
|
+
defaultTraeConfigPath,
|
|
33
|
+
defaultTraeSoloConfigPath,
|
|
34
|
+
defaultWindsurfConfigPath,
|
|
35
|
+
defaultZedConfigPath
|
|
36
|
+
} from '../path-config.js';
|
|
37
|
+
import {
|
|
38
|
+
createMcpClients,
|
|
39
|
+
supportedMcpClientIds as registrySupportedMcpClientIds,
|
|
40
|
+
supportedMcpClients as registrySupportedMcpClients
|
|
41
|
+
} from './registry.js';
|
|
42
|
+
|
|
43
|
+
export const MCP_CLIENTS = createMcpClients({
|
|
44
|
+
JSON_MCP_CLIENT_DEFINITIONS,
|
|
45
|
+
defaultCodexConfigPath,
|
|
46
|
+
codexTomlSnippet,
|
|
47
|
+
appendTomlServerConfig,
|
|
48
|
+
defaultHermesConfigPath,
|
|
49
|
+
hermesYamlSnippet,
|
|
50
|
+
mergeHermesMcpConfig,
|
|
51
|
+
jsonClientSnippet,
|
|
52
|
+
mergeJsonClientMcpConfig,
|
|
53
|
+
defaultCursorConfigPath,
|
|
54
|
+
defaultGeminiConfigPath,
|
|
55
|
+
defaultAntigravityConfigPath,
|
|
56
|
+
defaultAntigravityIdeConfigPath,
|
|
57
|
+
defaultAntigravity2ConfigPath,
|
|
58
|
+
defaultAntigravityCliConfigPath,
|
|
59
|
+
defaultWindsurfConfigPath,
|
|
60
|
+
defaultClineConfigPath,
|
|
61
|
+
defaultContinueConfigPath,
|
|
62
|
+
defaultClaudeConfigPath,
|
|
63
|
+
defaultOpenclawConfigPath,
|
|
64
|
+
defaultKiroConfigPath,
|
|
65
|
+
defaultZedConfigPath,
|
|
66
|
+
defaultJetbrainsConfigPath,
|
|
67
|
+
defaultOpencodeConfigPath,
|
|
68
|
+
defaultQwenConfigPath,
|
|
69
|
+
defaultTraeConfigPath,
|
|
70
|
+
defaultTraeSoloConfigPath,
|
|
71
|
+
defaultClaudecodeConfigPath
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export function supportedMcpClients() {
|
|
75
|
+
return registrySupportedMcpClients(MCP_CLIENTS);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function supportedMcpClientIds() {
|
|
79
|
+
return registrySupportedMcpClientIds(MCP_CLIENTS);
|
|
80
|
+
}
|
package/src/mcp/codex.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
COMMAND_NAME,
|
|
6
|
+
LEGACY_MCP_SERVER_NAMES,
|
|
7
|
+
MCP_SERVER_NAME,
|
|
8
|
+
TOKEN_ENV_VAR
|
|
9
|
+
} from '../constants.js';
|
|
10
|
+
import { UsageError } from '../errors.js';
|
|
11
|
+
import {
|
|
12
|
+
bestEffortChmod,
|
|
13
|
+
escapeRegExp,
|
|
14
|
+
escapeTomlString,
|
|
15
|
+
fileExists,
|
|
16
|
+
readTextIfExists,
|
|
17
|
+
unescapeTomlString
|
|
18
|
+
} from '../runtime.js';
|
|
19
|
+
import { agentInstanceIdentityPath } from './identity.js';
|
|
20
|
+
|
|
21
|
+
export function codexTomlSnippet(mcpUrl) {
|
|
22
|
+
return `[mcp_servers.${MCP_SERVER_NAME}]
|
|
23
|
+
url = "${escapeTomlString(mcpUrl)}"
|
|
24
|
+
bearer_token_env_var = "${TOKEN_ENV_VAR}"
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function codexSmokeReport(configPath, env) {
|
|
29
|
+
const configText = await readTextIfExists(configPath);
|
|
30
|
+
const serverBlock = findTomlServerBlock(configText);
|
|
31
|
+
const block = serverBlock.block;
|
|
32
|
+
const mcpUrl = block ? tomlStringValue(block, 'url') : null;
|
|
33
|
+
const bearerTokenEnvVar = block ? tomlStringValue(block, 'bearer_token_env_var') : null;
|
|
34
|
+
const tokenValue = env[TOKEN_ENV_VAR] ?? '';
|
|
35
|
+
const identityPath = agentInstanceIdentityPath(env, 'codex');
|
|
36
|
+
const identityPresent = await fileExists(identityPath);
|
|
37
|
+
const checks = [
|
|
38
|
+
{
|
|
39
|
+
name: 'config_present',
|
|
40
|
+
ok: configText.trim().length > 0,
|
|
41
|
+
required: true,
|
|
42
|
+
detail: configText.trim().length > 0 ? 'found' : 'missing'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'memory_os_server_present',
|
|
46
|
+
ok: Boolean(block),
|
|
47
|
+
required: true,
|
|
48
|
+
detail: block ? `[mcp_servers.${serverBlock.name}]` : `missing [mcp_servers.${MCP_SERVER_NAME}]`
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'mcp_url_present',
|
|
52
|
+
ok: Boolean(mcpUrl),
|
|
53
|
+
required: true,
|
|
54
|
+
detail: mcpUrl ?? 'missing url'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'bearer_token_env_var',
|
|
58
|
+
ok: bearerTokenEnvVar === TOKEN_ENV_VAR,
|
|
59
|
+
required: true,
|
|
60
|
+
detail: bearerTokenEnvVar ?? 'missing bearer_token_env_var'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'token_env_present',
|
|
64
|
+
ok: Boolean(env[TOKEN_ENV_VAR]),
|
|
65
|
+
required: true,
|
|
66
|
+
detail: env[TOKEN_ENV_VAR] ? 'present' : `missing ${TOKEN_ENV_VAR}`
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'token_not_embedded_in_config',
|
|
70
|
+
ok: !tokenValue || !configText.includes(tokenValue),
|
|
71
|
+
required: true,
|
|
72
|
+
detail: 'token value not printed or embedded'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'agent_instance_identity_file',
|
|
76
|
+
ok: identityPresent,
|
|
77
|
+
required: false,
|
|
78
|
+
detail: identityPresent ? identityPath : `optional; create with ${COMMAND_NAME} mcp add codex --write (${identityPath})`
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
ok: checks.every((check) => !check.required || check.ok),
|
|
84
|
+
client: 'codex',
|
|
85
|
+
configPath,
|
|
86
|
+
serverName: serverBlock.name ?? MCP_SERVER_NAME,
|
|
87
|
+
mcpUrl,
|
|
88
|
+
tokenEnvVar: TOKEN_ENV_VAR,
|
|
89
|
+
agentInstanceIdPath: identityPath,
|
|
90
|
+
checks
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function appendTomlServerConfig(configPath, mcpUrl) {
|
|
95
|
+
const snippet = codexTomlSnippet(mcpUrl);
|
|
96
|
+
const existing = await readTextIfExists(configPath);
|
|
97
|
+
const existingName = existingTomlMcpServerName(existing);
|
|
98
|
+
if (existingName) {
|
|
99
|
+
throw new UsageError(`MCP config already contains [mcp_servers.${existingName}]. Edit ${configPath} manually to avoid duplicate server definitions.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
103
|
+
const prefix = existing.trim().length === 0 ? '' : '\n\n';
|
|
104
|
+
await fs.appendFile(configPath, `${prefix}${snippet}`, { mode: 0o600 });
|
|
105
|
+
await bestEffortChmod(configPath, 0o600);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function knownMcpServerNames() {
|
|
109
|
+
return [MCP_SERVER_NAME, ...LEGACY_MCP_SERVER_NAMES];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function existingTomlMcpServerName(content) {
|
|
113
|
+
return knownMcpServerNames().find((name) => content.includes(`[mcp_servers.${name}]`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function findTomlServerBlock(content) {
|
|
117
|
+
const name = existingTomlMcpServerName(content);
|
|
118
|
+
return {
|
|
119
|
+
name: name ?? null,
|
|
120
|
+
block: name ? tomlServerBlock(content, name) : ''
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function tomlServerBlock(content, serverName) {
|
|
125
|
+
const header = `[mcp_servers.${serverName}]`;
|
|
126
|
+
const lines = content.split(/\r?\n/);
|
|
127
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
128
|
+
if (start === -1) {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const block = [];
|
|
133
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
134
|
+
const line = lines[index];
|
|
135
|
+
if (/^\s*\[/.test(line)) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
block.push(line);
|
|
139
|
+
}
|
|
140
|
+
return block.join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function tomlStringValue(block, key) {
|
|
144
|
+
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"((?:\\\\.|[^"\\\\])*)"\\s*$`, 'm');
|
|
145
|
+
const match = block.match(pattern);
|
|
146
|
+
return match ? unescapeTomlString(match[1]) : null;
|
|
147
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { MCP_SERVER_NAME } from '../constants.js';
|
|
5
|
+
import { UsageError } from '../errors.js';
|
|
6
|
+
import {
|
|
7
|
+
bestEffortChmod,
|
|
8
|
+
isPlainObject,
|
|
9
|
+
parseJsonConfig,
|
|
10
|
+
readTextIfExists
|
|
11
|
+
} from '../runtime.js';
|
|
12
|
+
import { existingJsonMcpServerName } from './names.js';
|
|
13
|
+
export { mcpProxyCommand } from './proxy.js';
|
|
14
|
+
|
|
15
|
+
export async function mergeCopilotMcpConfig(configPath, proxyUrl) {
|
|
16
|
+
const existing = await readTextIfExists(configPath);
|
|
17
|
+
const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
|
|
18
|
+
|
|
19
|
+
if (!isPlainObject(parsed)) {
|
|
20
|
+
throw new UsageError(`Copilot MCP JSON config must be an object: ${configPath}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!isPlainObject(parsed.mcpServers)) {
|
|
24
|
+
parsed.mcpServers = {};
|
|
25
|
+
}
|
|
26
|
+
|
|
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.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
parsed.mcpServers[MCP_SERVER_NAME] = copilotLocalProxyServerConfig(proxyUrl);
|
|
33
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
34
|
+
await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
|
|
35
|
+
await bestEffortChmod(configPath, 0o600);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function copilotLocalProxyServerConfig(proxyUrl) {
|
|
39
|
+
return {
|
|
40
|
+
type: 'http',
|
|
41
|
+
url: proxyUrl
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { fileExists } from '../runtime.js';
|
|
5
|
+
import { defaultCopilotConfigPath } from './paths.js';
|
|
6
|
+
|
|
7
|
+
export async function detectClient(clientId, env, mcpClients) {
|
|
8
|
+
let filePaths = [];
|
|
9
|
+
if (clientId === 'copilot-cli' || clientId === 'copilot') {
|
|
10
|
+
if (process.platform === 'win32' && env.APPDATA) {
|
|
11
|
+
filePaths.push(path.join(env.APPDATA, 'Code', 'User', 'mcp.json'));
|
|
12
|
+
} else {
|
|
13
|
+
const home = env.HOME || os.homedir();
|
|
14
|
+
if (process.platform === 'darwin') {
|
|
15
|
+
filePaths.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'));
|
|
16
|
+
}
|
|
17
|
+
filePaths.push(path.join(home, '.config', 'Code', 'User', 'mcp.json'));
|
|
18
|
+
}
|
|
19
|
+
filePaths.push(defaultCopilotConfigPath(env));
|
|
20
|
+
} else {
|
|
21
|
+
const client = mcpClients.get(clientId);
|
|
22
|
+
if (client) {
|
|
23
|
+
filePaths.push(client.defaultConfigPath(env));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (clientId === 'cline') {
|
|
28
|
+
if (process.platform === 'win32' && env.APPDATA) {
|
|
29
|
+
filePaths.push(path.join(env.APPDATA, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
|
|
30
|
+
} else {
|
|
31
|
+
const home = env.HOME || os.homedir();
|
|
32
|
+
if (process.platform === 'darwin') {
|
|
33
|
+
filePaths.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
|
|
34
|
+
}
|
|
35
|
+
filePaths.push(path.join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const filePath of filePaths) {
|
|
40
|
+
if (await fileExists(filePath)) {
|
|
41
|
+
return { detected: true, path: filePath };
|
|
42
|
+
}
|
|
43
|
+
const parentDir = path.dirname(filePath);
|
|
44
|
+
if (await fileExists(parentDir)) {
|
|
45
|
+
return { detected: true, path: filePath };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { detected: false };
|
|
50
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AGENT_INSTANCE_ENV_VAR,
|
|
6
|
+
MCP_SERVER_NAME,
|
|
7
|
+
TOKEN_ENV_VAR
|
|
8
|
+
} from '../constants.js';
|
|
9
|
+
import { UsageError } from '../errors.js';
|
|
10
|
+
import {
|
|
11
|
+
bestEffortChmod,
|
|
12
|
+
readTextIfExists
|
|
13
|
+
} from '../runtime.js';
|
|
14
|
+
import { envReferenceIdentity } from './identity.js';
|
|
15
|
+
|
|
16
|
+
export function hermesYamlSnippet(mcpUrl, identity = envReferenceIdentity('hermes')) {
|
|
17
|
+
return `mcp_servers:
|
|
18
|
+
${MCP_SERVER_NAME}:
|
|
19
|
+
command: npx
|
|
20
|
+
args:
|
|
21
|
+
- -y
|
|
22
|
+
- mcp-remote
|
|
23
|
+
- ${mcpUrl}
|
|
24
|
+
- --header
|
|
25
|
+
- "Authorization:Bearer \${${TOKEN_ENV_VAR}}"
|
|
26
|
+
- --header
|
|
27
|
+
- "X-Memory-OS-Agent-ID:${identity.agentId}"
|
|
28
|
+
- --header
|
|
29
|
+
- "X-Memory-OS-Agent-Instance-ID:\${${AGENT_INSTANCE_ENV_VAR}}"
|
|
30
|
+
env:
|
|
31
|
+
${TOKEN_ENV_VAR}: "\${env:${TOKEN_ENV_VAR}}"
|
|
32
|
+
${AGENT_INSTANCE_ENV_VAR}: "${identity.agentInstanceId}"
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function mergeHermesMcpConfig(configPath, mcpUrl, identity) {
|
|
37
|
+
const existing = await readTextIfExists(configPath);
|
|
38
|
+
|
|
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.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
44
|
+
|
|
45
|
+
if (existing.trim().length === 0) {
|
|
46
|
+
await fs.writeFile(configPath, hermesYamlSnippet(mcpUrl, identity), { mode: 0o600 });
|
|
47
|
+
} else if (existing.includes('mcp_servers:')) {
|
|
48
|
+
const replacement = `mcp_servers:
|
|
49
|
+
${MCP_SERVER_NAME}:
|
|
50
|
+
command: npx
|
|
51
|
+
args:
|
|
52
|
+
- -y
|
|
53
|
+
- mcp-remote
|
|
54
|
+
- ${mcpUrl}
|
|
55
|
+
- --header
|
|
56
|
+
- "Authorization:Bearer \${${TOKEN_ENV_VAR}}"
|
|
57
|
+
- --header
|
|
58
|
+
- "X-Memory-OS-Agent-ID:${identity.agentId}"
|
|
59
|
+
- --header
|
|
60
|
+
- "X-Memory-OS-Agent-Instance-ID:\${${AGENT_INSTANCE_ENV_VAR}}"
|
|
61
|
+
env:
|
|
62
|
+
${TOKEN_ENV_VAR}: "\${env:${TOKEN_ENV_VAR}}"
|
|
63
|
+
${AGENT_INSTANCE_ENV_VAR}: "${identity.agentInstanceId}"`;
|
|
64
|
+
const updated = existing.replace('mcp_servers:', replacement);
|
|
65
|
+
await fs.writeFile(configPath, updated, { mode: 0o600 });
|
|
66
|
+
} else {
|
|
67
|
+
const prefix = existing.endsWith('\n') ? '' : '\n';
|
|
68
|
+
await fs.appendFile(configPath, `${prefix}${hermesYamlSnippet(mcpUrl, identity)}`, { mode: 0o600 });
|
|
69
|
+
}
|
|
70
|
+
await bestEffortChmod(configPath, 0o600);
|
|
71
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
import { AGENT_INSTANCE_ENV_VAR } from '../constants.js';
|
|
6
|
+
import { bestEffortChmod, parseJsonConfig, readTextIfExists } from '../runtime.js';
|
|
7
|
+
import { stringValue } from '../args.js';
|
|
8
|
+
import { configRoot } from './paths.js';
|
|
9
|
+
|
|
10
|
+
function identityClientId(clientId) {
|
|
11
|
+
return (clientId === 'antigravity-ide' || clientId === 'antigravity2' || clientId === 'antigravity-cli')
|
|
12
|
+
? 'antigravity'
|
|
13
|
+
: clientId;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function agentIdentity(clientId, env) {
|
|
17
|
+
const targetClientId = identityClientId(clientId);
|
|
18
|
+
const configuredInstanceId = env[AGENT_INSTANCE_ENV_VAR];
|
|
19
|
+
if (configuredInstanceId) {
|
|
20
|
+
return {
|
|
21
|
+
agentId: targetClientId,
|
|
22
|
+
agentInstanceId: configuredInstanceId,
|
|
23
|
+
path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const identityPath = agentInstanceIdentityPath(env, targetClientId);
|
|
28
|
+
const existing = await readAgentInstanceIdentity(identityPath);
|
|
29
|
+
if (existing) {
|
|
30
|
+
return { agentId: targetClientId, agentInstanceId: existing, path: identityPath };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const generated = `xmemo-${targetClientId}-${randomUUID()}`;
|
|
34
|
+
await fs.mkdir(path.dirname(identityPath), { recursive: true, mode: 0o700 });
|
|
35
|
+
await bestEffortChmod(path.dirname(identityPath), 0o700);
|
|
36
|
+
await fs.writeFile(identityPath, `${JSON.stringify({ version: 1, agentId: targetClientId, agentInstanceId: generated }, null, 2)}\n`, { mode: 0o600 });
|
|
37
|
+
await bestEffortChmod(identityPath, 0o600);
|
|
38
|
+
return { agentId: targetClientId, agentInstanceId: generated, path: identityPath };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readAgentInstanceIdentity(identityPath) {
|
|
42
|
+
const existing = await readTextIfExists(identityPath);
|
|
43
|
+
if (!existing.trim()) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const parsed = parseJsonConfig(existing, identityPath);
|
|
47
|
+
const value = stringValue(parsed, ['agentInstanceId']);
|
|
48
|
+
return value || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function agentInstanceIdentityPath(env, clientId) {
|
|
52
|
+
return path.join(configRoot(env), 'agent-instances', `${clientId}.json`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function envReferenceIdentity(clientId) {
|
|
56
|
+
const targetClientId = identityClientId(clientId);
|
|
57
|
+
return {
|
|
58
|
+
agentId: targetClientId,
|
|
59
|
+
agentInstanceId: `\${${AGENT_INSTANCE_ENV_VAR}}`,
|
|
60
|
+
path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
|
|
61
|
+
};
|
|
62
|
+
}
|