@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/profile.js
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CODEX_PROFILE_MARKER_END,
|
|
8
|
+
CODEX_PROFILE_MARKER_START,
|
|
9
|
+
CODEX_PROFILE_TARGET,
|
|
10
|
+
COMMAND_NAME,
|
|
11
|
+
MCP_SERVER_NAME,
|
|
12
|
+
PRODUCT_NAME,
|
|
13
|
+
PROFILE_MARKER_PREFIX,
|
|
14
|
+
TOKEN_ENV_VAR
|
|
15
|
+
} from './constants.js';
|
|
16
|
+
import { UsageError } from './errors.js';
|
|
17
|
+
import { writeLine } from './io.js';
|
|
18
|
+
import { readTextIfExists } from './runtime.js';
|
|
19
|
+
|
|
20
|
+
export function codexMemoryProfile() {
|
|
21
|
+
return memoryBehaviorProfile('codex');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writeCodexMemoryProfile(profile, io) {
|
|
25
|
+
writeLine(io.stdout, `${PRODUCT_NAME} Codex memory behavior profile`);
|
|
26
|
+
writeLine(io.stdout, `Profile: ${profile.profileVersion}`);
|
|
27
|
+
writeLine(io.stdout, `MCP server: ${profile.mcpServerName}`);
|
|
28
|
+
writeLine(io.stdout, `Token env: ${profile.requiredTokenEnv}`);
|
|
29
|
+
writeLine(io.stdout, '');
|
|
30
|
+
writeLine(io.stdout, 'Recommended Codex instructions:');
|
|
31
|
+
for (const instruction of profile.instructions) {
|
|
32
|
+
writeLine(io.stdout, `- ${instruction}`);
|
|
33
|
+
}
|
|
34
|
+
writeLine(io.stdout, '');
|
|
35
|
+
writeLine(io.stdout, `Setup: ${profile.setupCommand}`);
|
|
36
|
+
writeLine(io.stdout, `Smoke test: ${profile.smokeCommand}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function codexProfileInstructionText() {
|
|
40
|
+
return profileInstructionText('codex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function memoryBehaviorProfile(clientId) {
|
|
44
|
+
const config = profileClientConfig(clientId);
|
|
45
|
+
if (!config) {
|
|
46
|
+
throw new UsageError(`Unsupported profile client: ${clientId}`);
|
|
47
|
+
}
|
|
48
|
+
const instructions = [
|
|
49
|
+
'At the start of a non-trivial task, call XMemo recall/search for relevant project decisions, conventions, prior fixes, and active context unless the user explicitly asks not to use memory.',
|
|
50
|
+
'Use recalled memories as evidence, not as unquestioned truth. Prefer current repository files when memory conflicts with code.',
|
|
51
|
+
'After meaningful decisions, bug fixes, release steps, or durable conventions, write a concise XMemo memory with scope, source, and no secret values.',
|
|
52
|
+
'Never store tokens, API keys, cookies, private keys, raw credentials, or sensitive customer data in XMemo.',
|
|
53
|
+
'For routine or low-signal output, skip durable writes. Prefer summarized procedural or semantic memories over verbose logs.',
|
|
54
|
+
config.authInstruction
|
|
55
|
+
];
|
|
56
|
+
return {
|
|
57
|
+
client: clientId,
|
|
58
|
+
label: config.label,
|
|
59
|
+
profileVersion: config.profileVersion,
|
|
60
|
+
mcpServerName: MCP_SERVER_NAME,
|
|
61
|
+
requiredTokenEnv: config.requiredTokenEnv ?? null,
|
|
62
|
+
objective: 'Use XMemo deliberately through MCP for project context recall and high-signal write-back.',
|
|
63
|
+
instructions,
|
|
64
|
+
setupCommand: `${COMMAND_NAME} setup ${config.setupAlias} --url "$XMEMO_URL"`,
|
|
65
|
+
smokeCommand: clientId === 'codex' ? `${COMMAND_NAME} smoke --client codex` : null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function profileInstructionText(clientId) {
|
|
70
|
+
const profile = memoryBehaviorProfile(clientId);
|
|
71
|
+
const lines = [
|
|
72
|
+
`## XMemo ${profile.label} profile`,
|
|
73
|
+
'',
|
|
74
|
+
`MCP server: \`${profile.mcpServerName}\``,
|
|
75
|
+
];
|
|
76
|
+
if (profile.requiredTokenEnv) {
|
|
77
|
+
lines.push(`Token env var: \`${profile.requiredTokenEnv}\``);
|
|
78
|
+
}
|
|
79
|
+
lines.push(
|
|
80
|
+
'',
|
|
81
|
+
profile.objective,
|
|
82
|
+
'',
|
|
83
|
+
`Recommended ${profile.label} behavior:`
|
|
84
|
+
);
|
|
85
|
+
for (const instruction of profile.instructions) {
|
|
86
|
+
lines.push(`- ${instruction}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push('');
|
|
89
|
+
return `${lines.join('\n')}\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function profileClientConfig(clientId) {
|
|
93
|
+
const profileConfigs = {
|
|
94
|
+
codex: {
|
|
95
|
+
label: 'Codex',
|
|
96
|
+
setupAlias: 'codex',
|
|
97
|
+
profileVersion: 'codex-mcp-depth-v1',
|
|
98
|
+
requiredTokenEnv: TOKEN_ENV_VAR,
|
|
99
|
+
markerStart: CODEX_PROFILE_MARKER_START,
|
|
100
|
+
markerEnd: CODEX_PROFILE_MARKER_END,
|
|
101
|
+
defaultTarget: () => defaultCodexProfileTarget(),
|
|
102
|
+
authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
|
|
103
|
+
},
|
|
104
|
+
cursor: {
|
|
105
|
+
label: 'Cursor',
|
|
106
|
+
setupAlias: 'cursor',
|
|
107
|
+
profileVersion: 'cursor-mcp-depth-v1',
|
|
108
|
+
requiredTokenEnv: TOKEN_ENV_VAR,
|
|
109
|
+
markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:cursor:start -->`,
|
|
110
|
+
markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:cursor:end -->`,
|
|
111
|
+
defaultTarget: (env) => {
|
|
112
|
+
const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
|
|
113
|
+
if (!isTest && (existsSync(path.join(process.cwd(), '.cursor')) || existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
|
|
114
|
+
return path.join(process.cwd(), '.cursor', 'rules', 'xmemo-memory.md');
|
|
115
|
+
}
|
|
116
|
+
return path.join(userHome(env), '.cursor', 'memory-profile.md');
|
|
117
|
+
},
|
|
118
|
+
authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
|
|
119
|
+
},
|
|
120
|
+
'gemini-cli': {
|
|
121
|
+
label: 'Gemini CLI',
|
|
122
|
+
setupAlias: 'gemini',
|
|
123
|
+
profileVersion: 'gemini-cli-mcp-depth-v1',
|
|
124
|
+
markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:gemini-cli:start -->`,
|
|
125
|
+
markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:gemini-cli:end -->`,
|
|
126
|
+
defaultTarget: (env) => {
|
|
127
|
+
const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
|
|
128
|
+
if (!isTest && (existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
|
|
129
|
+
return path.join(process.cwd(), 'GEMINI.md');
|
|
130
|
+
}
|
|
131
|
+
return path.join(userHome(env), '.gemini', 'GEMINI.md');
|
|
132
|
+
},
|
|
133
|
+
authInstruction: 'Use the client-managed MCP OAuth credential; do not paste token values into prompts, config files, or logs.'
|
|
134
|
+
},
|
|
135
|
+
antigravity: {
|
|
136
|
+
label: 'Antigravity',
|
|
137
|
+
setupAlias: 'antigravity',
|
|
138
|
+
profileVersion: 'antigravity-mcp-depth-v1',
|
|
139
|
+
markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:antigravity:start -->`,
|
|
140
|
+
markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:antigravity:end -->`,
|
|
141
|
+
defaultTarget: (env) => {
|
|
142
|
+
const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
|
|
143
|
+
if (!isTest && (existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
|
|
144
|
+
return path.join(process.cwd(), 'GEMINI.md');
|
|
145
|
+
}
|
|
146
|
+
return path.join(userHome(env), '.gemini', 'antigravity', 'MEMORY.md');
|
|
147
|
+
},
|
|
148
|
+
authInstruction: 'Use the client-managed MCP OAuth credential; do not paste token values into prompts, config files, or logs.'
|
|
149
|
+
},
|
|
150
|
+
qwen: {
|
|
151
|
+
label: 'Qwen',
|
|
152
|
+
setupAlias: 'qwen',
|
|
153
|
+
profileVersion: 'qwen-mcp-depth-v1',
|
|
154
|
+
markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:qwen:start -->`,
|
|
155
|
+
markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:qwen:end -->`,
|
|
156
|
+
defaultTarget: (env) => {
|
|
157
|
+
const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
|
|
158
|
+
if (!isTest && (existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
|
|
159
|
+
return path.join(process.cwd(), 'QWEN.md');
|
|
160
|
+
}
|
|
161
|
+
return path.join(userHome(env), '.qwen', 'QWEN.md');
|
|
162
|
+
},
|
|
163
|
+
authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
|
|
164
|
+
},
|
|
165
|
+
opencode: {
|
|
166
|
+
label: 'OpenCode',
|
|
167
|
+
setupAlias: 'opencode',
|
|
168
|
+
profileVersion: 'opencode-mcp-depth-v1',
|
|
169
|
+
markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:opencode:start -->`,
|
|
170
|
+
markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:opencode:end -->`,
|
|
171
|
+
defaultTarget: (env) => {
|
|
172
|
+
const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
|
|
173
|
+
if (!isTest && (existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
|
|
174
|
+
return path.join(process.cwd(), 'AGENTS.md');
|
|
175
|
+
}
|
|
176
|
+
return path.join(userHome(env), '.config', 'opencode', 'AGENTS.md');
|
|
177
|
+
},
|
|
178
|
+
authInstruction: 'Use the client-managed MCP OAuth credential; do not paste token values into prompts, config files, or logs.'
|
|
179
|
+
},
|
|
180
|
+
trae: {
|
|
181
|
+
label: 'Trae',
|
|
182
|
+
setupAlias: 'trae',
|
|
183
|
+
profileVersion: 'trae-mcp-depth-v1',
|
|
184
|
+
requiredTokenEnv: TOKEN_ENV_VAR,
|
|
185
|
+
markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:trae:start -->`,
|
|
186
|
+
markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:trae:end -->`,
|
|
187
|
+
defaultTarget: (env) => {
|
|
188
|
+
const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
|
|
189
|
+
if (!isTest && (existsSync(path.join(process.cwd(), '.trae')) || existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
|
|
190
|
+
return path.join(process.cwd(), '.trae', 'rules', 'xmemo-memory.md');
|
|
191
|
+
}
|
|
192
|
+
return path.join(userHome(env), '.trae', 'memory-profile.md');
|
|
193
|
+
},
|
|
194
|
+
authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
|
|
195
|
+
},
|
|
196
|
+
'trae-solo': {
|
|
197
|
+
label: 'Trae Solo',
|
|
198
|
+
setupAlias: 'trae-solo',
|
|
199
|
+
profileVersion: 'trae-solo-mcp-depth-v1',
|
|
200
|
+
requiredTokenEnv: TOKEN_ENV_VAR,
|
|
201
|
+
markerStart: `<!-- ${PROFILE_MARKER_PREFIX}:trae-solo:start -->`,
|
|
202
|
+
markerEnd: `<!-- ${PROFILE_MARKER_PREFIX}:trae-solo:end -->`,
|
|
203
|
+
defaultTarget: (env) => {
|
|
204
|
+
const isTest = env.HOME && (env.HOME.includes('memory-os-') || env.HOME.includes('test'));
|
|
205
|
+
if (!isTest && (existsSync(path.join(process.cwd(), '.trae')) || existsSync(path.join(process.cwd(), '.git')) || existsSync(path.join(process.cwd(), 'package.json')))) {
|
|
206
|
+
return path.join(process.cwd(), '.trae', 'rules', 'xmemo-memory.md');
|
|
207
|
+
}
|
|
208
|
+
return path.join(userHome(env), '.trae', 'memory-profile.md');
|
|
209
|
+
},
|
|
210
|
+
authInstruction: `Keep XMemo authentication through the ${TOKEN_ENV_VAR} environment variable; do not paste token values into prompts, config files, or logs.`
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
return profileConfigs[clientId] ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function supportedProfileClientIds() {
|
|
217
|
+
return ['codex', 'cursor', 'gemini', 'antigravity', 'qwen', 'opencode', 'trae', 'trae-solo'];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function defaultProfileTarget(clientId, env) {
|
|
221
|
+
const config = profileClientConfig(clientId);
|
|
222
|
+
if (!config) {
|
|
223
|
+
throw new UsageError(`Unsupported profile client: ${clientId}`);
|
|
224
|
+
}
|
|
225
|
+
return config.defaultTarget(env);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function confirmProfileInstall(clientId, targetPath, io) {
|
|
229
|
+
const config = profileClientConfig(clientId);
|
|
230
|
+
writeLine(io.stdout, '');
|
|
231
|
+
writeLine(io.stdout, `Write XMemo memory behavior profile to ${targetPath}? [Y/n]`);
|
|
232
|
+
const answer = (await readLineFromStdin(io.stdin)).trim().toLowerCase();
|
|
233
|
+
if (answer === '' || answer === 'y' || answer === 'yes') {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (answer === 'n' || answer === 'no') {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
throw new UsageError(`Unsupported response for ${config.label} profile prompt: ${answer}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function readLineFromStdin(stdin) {
|
|
243
|
+
let input = '';
|
|
244
|
+
for await (const chunk of stdin) {
|
|
245
|
+
input += chunk;
|
|
246
|
+
if (input.includes('\n')) {
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return input.split(/\r?\n/, 1)[0] ?? '';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function genericProfileMarkerBlock(clientId) {
|
|
254
|
+
const config = profileClientConfig(clientId);
|
|
255
|
+
return `${config.markerStart}\n${profileInstructionText(clientId)}${config.markerEnd}\n`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function profileInstallResult(clientId, targetPath, options = {}) {
|
|
259
|
+
if (clientId === 'codex') {
|
|
260
|
+
return codexProfileInstallResult(targetPath, options);
|
|
261
|
+
}
|
|
262
|
+
const config = profileClientConfig(clientId);
|
|
263
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
264
|
+
const existing = await readTextIfExists(resolvedTarget);
|
|
265
|
+
const marker = profileMarkerBounds(existing, config);
|
|
266
|
+
const block = genericProfileMarkerBlock(clientId);
|
|
267
|
+
let nextText;
|
|
268
|
+
|
|
269
|
+
if (marker.present) {
|
|
270
|
+
nextText = `${existing.slice(0, marker.start)}${block}${existing.slice(marker.end)}`;
|
|
271
|
+
} else if (existing.trim().length === 0) {
|
|
272
|
+
nextText = block;
|
|
273
|
+
} else {
|
|
274
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
275
|
+
nextText = `${existing}${separator}${block}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const changed = nextText !== existing;
|
|
279
|
+
const write = Boolean(options.write);
|
|
280
|
+
if (write && changed) {
|
|
281
|
+
await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
|
|
282
|
+
await fs.writeFile(resolvedTarget, nextText);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
client: clientId,
|
|
287
|
+
action: 'install',
|
|
288
|
+
targetPath: resolvedTarget,
|
|
289
|
+
markerStart: config.markerStart,
|
|
290
|
+
markerEnd: config.markerEnd,
|
|
291
|
+
installed: marker.present || (write && changed),
|
|
292
|
+
written: write,
|
|
293
|
+
changed,
|
|
294
|
+
markerPresent: marker.present,
|
|
295
|
+
writesTokenValue: false
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function profileStatusResult(clientId, targetPath) {
|
|
300
|
+
if (clientId === 'codex') {
|
|
301
|
+
return codexProfileStatusResult(targetPath);
|
|
302
|
+
}
|
|
303
|
+
const config = profileClientConfig(clientId);
|
|
304
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
305
|
+
const existing = await readTextIfExists(resolvedTarget);
|
|
306
|
+
const marker = profileMarkerBounds(existing, config);
|
|
307
|
+
return {
|
|
308
|
+
client: clientId,
|
|
309
|
+
action: 'status',
|
|
310
|
+
targetPath: resolvedTarget,
|
|
311
|
+
installed: marker.present,
|
|
312
|
+
markerPresent: marker.present,
|
|
313
|
+
markerStart: config.markerStart,
|
|
314
|
+
markerEnd: config.markerEnd,
|
|
315
|
+
writesTokenValue: false
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function profileUninstallResult(clientId, targetPath, options = {}) {
|
|
320
|
+
if (clientId === 'codex') {
|
|
321
|
+
return codexProfileUninstallResult(targetPath, options);
|
|
322
|
+
}
|
|
323
|
+
const config = profileClientConfig(clientId);
|
|
324
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
325
|
+
const existing = await readTextIfExists(resolvedTarget);
|
|
326
|
+
const marker = profileMarkerBounds(existing, config);
|
|
327
|
+
const write = Boolean(options.write);
|
|
328
|
+
let changed = false;
|
|
329
|
+
|
|
330
|
+
if (marker.present) {
|
|
331
|
+
let nextText = `${existing.slice(0, marker.start)}${existing.slice(marker.end)}`;
|
|
332
|
+
nextText = nextText.replace(/\n{3,}/g, '\n\n');
|
|
333
|
+
if (nextText.trim().length === 0) {
|
|
334
|
+
nextText = '';
|
|
335
|
+
} else if (!nextText.endsWith('\n')) {
|
|
336
|
+
nextText = `${nextText}\n`;
|
|
337
|
+
}
|
|
338
|
+
changed = nextText !== existing;
|
|
339
|
+
if (write && changed) {
|
|
340
|
+
await fs.writeFile(resolvedTarget, nextText);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
client: clientId,
|
|
346
|
+
action: 'uninstall',
|
|
347
|
+
targetPath: resolvedTarget,
|
|
348
|
+
installed: marker.present && !(write && changed),
|
|
349
|
+
written: write,
|
|
350
|
+
changed,
|
|
351
|
+
markerPresent: marker.present,
|
|
352
|
+
markerStart: config.markerStart,
|
|
353
|
+
markerEnd: config.markerEnd,
|
|
354
|
+
writesTokenValue: false
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function profileMarkerBounds(content, config) {
|
|
359
|
+
const start = content.indexOf(config.markerStart);
|
|
360
|
+
const end = content.indexOf(config.markerEnd);
|
|
361
|
+
if (start === -1 && end === -1) {
|
|
362
|
+
return { present: false, start: -1, end: -1 };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (start === -1 || end === -1 || end < start) {
|
|
366
|
+
throw new UsageError(`${config.label} profile markers are incomplete or out of order; edit the target file manually before retrying.`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
content.indexOf(config.markerStart, start + config.markerStart.length) !== -1
|
|
371
|
+
|| content.indexOf(config.markerEnd, end + config.markerEnd.length) !== -1
|
|
372
|
+
) {
|
|
373
|
+
throw new UsageError(`${config.label} profile markers appear more than once; edit the target file manually before retrying.`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const afterEnd = end + config.markerEnd.length;
|
|
377
|
+
const trailingNewlineLength = content.slice(afterEnd, afterEnd + 2) === '\r\n'
|
|
378
|
+
? 2
|
|
379
|
+
: content.slice(afterEnd, afterEnd + 1) === '\n'
|
|
380
|
+
? 1
|
|
381
|
+
: 0;
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
present: true,
|
|
385
|
+
start,
|
|
386
|
+
end: afterEnd + trailingNewlineLength
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function userHome(env) {
|
|
391
|
+
return env.USERPROFILE || env.HOME || os.homedir();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function codexProfileMarkerBlock() {
|
|
395
|
+
return `${CODEX_PROFILE_MARKER_START}\n${codexProfileInstructionText()}${CODEX_PROFILE_MARKER_END}\n`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function defaultCodexProfileTarget() {
|
|
399
|
+
return path.resolve(process.cwd(), CODEX_PROFILE_TARGET);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function codexProfileInstallResult(targetPath, options = {}) {
|
|
403
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
404
|
+
const existing = await readTextIfExists(resolvedTarget);
|
|
405
|
+
const marker = markerBounds(existing);
|
|
406
|
+
const block = codexProfileMarkerBlock();
|
|
407
|
+
let nextText;
|
|
408
|
+
|
|
409
|
+
if (marker.present) {
|
|
410
|
+
nextText = `${existing.slice(0, marker.start)}${block}${existing.slice(marker.end)}`;
|
|
411
|
+
} else if (existing.trim().length === 0) {
|
|
412
|
+
nextText = block;
|
|
413
|
+
} else {
|
|
414
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
415
|
+
nextText = `${existing}${separator}${block}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const changed = nextText !== existing;
|
|
419
|
+
const write = Boolean(options.write);
|
|
420
|
+
if (write && changed) {
|
|
421
|
+
await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
|
|
422
|
+
await fs.writeFile(resolvedTarget, nextText);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
client: 'codex',
|
|
427
|
+
action: 'install',
|
|
428
|
+
targetPath: resolvedTarget,
|
|
429
|
+
markerStart: CODEX_PROFILE_MARKER_START,
|
|
430
|
+
markerEnd: CODEX_PROFILE_MARKER_END,
|
|
431
|
+
installed: marker.present || (write && changed),
|
|
432
|
+
written: write,
|
|
433
|
+
changed,
|
|
434
|
+
markerPresent: marker.present,
|
|
435
|
+
writesTokenValue: false
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function codexProfileStatusResult(targetPath) {
|
|
440
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
441
|
+
const existing = await readTextIfExists(resolvedTarget);
|
|
442
|
+
const marker = markerBounds(existing);
|
|
443
|
+
return {
|
|
444
|
+
client: 'codex',
|
|
445
|
+
action: 'status',
|
|
446
|
+
targetPath: resolvedTarget,
|
|
447
|
+
installed: marker.present,
|
|
448
|
+
markerPresent: marker.present,
|
|
449
|
+
markerStart: CODEX_PROFILE_MARKER_START,
|
|
450
|
+
markerEnd: CODEX_PROFILE_MARKER_END,
|
|
451
|
+
writesTokenValue: false
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function codexProfileUninstallResult(targetPath, options = {}) {
|
|
456
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
457
|
+
const existing = await readTextIfExists(resolvedTarget);
|
|
458
|
+
const marker = markerBounds(existing);
|
|
459
|
+
const write = Boolean(options.write);
|
|
460
|
+
let changed = false;
|
|
461
|
+
|
|
462
|
+
if (marker.present) {
|
|
463
|
+
let nextText = `${existing.slice(0, marker.start)}${existing.slice(marker.end)}`;
|
|
464
|
+
nextText = nextText.replace(/\n{3,}/g, '\n\n');
|
|
465
|
+
if (nextText.trim().length === 0) {
|
|
466
|
+
nextText = '';
|
|
467
|
+
} else if (!nextText.endsWith('\n')) {
|
|
468
|
+
nextText = `${nextText}\n`;
|
|
469
|
+
}
|
|
470
|
+
changed = nextText !== existing;
|
|
471
|
+
if (write && changed) {
|
|
472
|
+
await fs.writeFile(resolvedTarget, nextText);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
client: 'codex',
|
|
478
|
+
action: 'uninstall',
|
|
479
|
+
targetPath: resolvedTarget,
|
|
480
|
+
installed: marker.present && !(write && changed),
|
|
481
|
+
written: write,
|
|
482
|
+
changed,
|
|
483
|
+
markerPresent: marker.present,
|
|
484
|
+
markerStart: CODEX_PROFILE_MARKER_START,
|
|
485
|
+
markerEnd: CODEX_PROFILE_MARKER_END,
|
|
486
|
+
writesTokenValue: false
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function markerBounds(content) {
|
|
491
|
+
const start = content.indexOf(CODEX_PROFILE_MARKER_START);
|
|
492
|
+
const end = content.indexOf(CODEX_PROFILE_MARKER_END);
|
|
493
|
+
if (start === -1 && end === -1) {
|
|
494
|
+
return { present: false, start: -1, end: -1 };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (start === -1 || end === -1 || end < start) {
|
|
498
|
+
throw new UsageError('Codex profile markers are incomplete or out of order; edit the target file manually before retrying.');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (
|
|
502
|
+
content.indexOf(CODEX_PROFILE_MARKER_START, start + CODEX_PROFILE_MARKER_START.length) !== -1
|
|
503
|
+
|| content.indexOf(CODEX_PROFILE_MARKER_END, end + CODEX_PROFILE_MARKER_END.length) !== -1
|
|
504
|
+
) {
|
|
505
|
+
throw new UsageError('Codex profile markers appear more than once; edit the target file manually before retrying.');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const afterEnd = end + CODEX_PROFILE_MARKER_END.length;
|
|
509
|
+
const trailingNewlineLength = content.slice(afterEnd, afterEnd + 2) === '\r\n'
|
|
510
|
+
? 2
|
|
511
|
+
: content.slice(afterEnd, afterEnd + 1) === '\n'
|
|
512
|
+
? 1
|
|
513
|
+
: 0;
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
present: true,
|
|
517
|
+
start,
|
|
518
|
+
end: afterEnd + trailingNewlineLength
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function writeProfileResult(action, result, io) {
|
|
523
|
+
const config = profileClientConfig(result.client);
|
|
524
|
+
writeLine(io.stdout, `${PRODUCT_NAME} ${config?.label ?? result.client} profile ${action}`);
|
|
525
|
+
writeLine(io.stdout, ` Target: ${result.targetPath}`);
|
|
526
|
+
writeLine(io.stdout, ` Installed: ${result.installed}`);
|
|
527
|
+
if ('written' in result) {
|
|
528
|
+
writeLine(io.stdout, ` Written: ${result.written}`);
|
|
529
|
+
writeLine(io.stdout, ` Changed: ${result.changed}`);
|
|
530
|
+
}
|
|
531
|
+
writeLine(io.stdout, ' Token value embedded: false');
|
|
532
|
+
}
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
import { UsageError } from './errors.js';
|
|
6
|
+
import { writeLine } from './io.js';
|
|
7
|
+
|
|
8
|
+
export function npmExecutable() {
|
|
9
|
+
return os.platform() === 'win32' ? 'npm.cmd' : 'npm';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runProcess(command, args, io, { stream = true } = {}) {
|
|
13
|
+
const spawnFn = io.spawn ?? spawn;
|
|
14
|
+
return await new Promise((resolve, reject) => {
|
|
15
|
+
const child = spawnFn(command, args, {
|
|
16
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
17
|
+
shell: os.platform() === 'win32'
|
|
18
|
+
});
|
|
19
|
+
let stdout = '';
|
|
20
|
+
let stderr = '';
|
|
21
|
+
child.stdout?.on('data', (chunk) => {
|
|
22
|
+
const text = String(chunk);
|
|
23
|
+
stdout += text;
|
|
24
|
+
if (stream) {
|
|
25
|
+
io.stdout.write(text);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
child.stderr?.on('data', (chunk) => {
|
|
29
|
+
const text = String(chunk);
|
|
30
|
+
stderr += text;
|
|
31
|
+
if (stream) {
|
|
32
|
+
io.stderr.write(text);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
child.on('error', reject);
|
|
36
|
+
child.on('close', (code) => {
|
|
37
|
+
resolve({ code: code ?? 0, stdout, stderr });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function waitForShutdown(server, io) {
|
|
43
|
+
await new Promise((resolve) => {
|
|
44
|
+
let resolved = false;
|
|
45
|
+
const finish = async () => {
|
|
46
|
+
if (resolved) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
resolved = true;
|
|
50
|
+
await closeServer(server);
|
|
51
|
+
resolve();
|
|
52
|
+
};
|
|
53
|
+
const onSigint = () => {
|
|
54
|
+
writeLine(io.stdout, 'Shutting down XMemo MCP proxy...');
|
|
55
|
+
void finish();
|
|
56
|
+
};
|
|
57
|
+
process.once('SIGINT', onSigint);
|
|
58
|
+
process.once('SIGTERM', onSigint);
|
|
59
|
+
server.once('close', () => {
|
|
60
|
+
process.off('SIGINT', onSigint);
|
|
61
|
+
process.off('SIGTERM', onSigint);
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
resolved = true;
|
|
64
|
+
resolve();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function closeServer(server) {
|
|
71
|
+
await new Promise((resolve, reject) => {
|
|
72
|
+
server.close((error) => {
|
|
73
|
+
if (error) {
|
|
74
|
+
reject(error);
|
|
75
|
+
} else {
|
|
76
|
+
resolve();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function readAll(stream) {
|
|
83
|
+
let content = '';
|
|
84
|
+
for await (const chunk of stream) {
|
|
85
|
+
content += chunk;
|
|
86
|
+
}
|
|
87
|
+
return content;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function fileExists(filePath) {
|
|
91
|
+
try {
|
|
92
|
+
await fs.access(filePath);
|
|
93
|
+
return true;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function readTextIfExists(filePath) {
|
|
100
|
+
try {
|
|
101
|
+
return await fs.readFile(filePath, 'utf8');
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error.code === 'ENOENT') {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function parseJsonConfig(content, configPath) {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(content);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new UsageError(`Invalid JSON in ${configPath}: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function bestEffortChmod(filePath, mode) {
|
|
119
|
+
try {
|
|
120
|
+
await fs.chmod(filePath, mode);
|
|
121
|
+
} catch {
|
|
122
|
+
// Windows and managed environments may ignore POSIX chmod.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isPlainObject(value) {
|
|
127
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function escapeTomlString(value) {
|
|
131
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function unescapeTomlString(value) {
|
|
135
|
+
return value.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function escapeRegExp(value) {
|
|
139
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function sleep(ms) {
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
144
|
+
}
|