@xmemo/client 0.4.155 → 0.4.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -7
- package/package.json +3 -2
- package/plugins/kiro/.kiro-plugin/power.json +35 -0
- package/plugins/kiro/CHANGELOG.md +9 -0
- package/plugins/kiro/LICENSE +7 -0
- package/plugins/kiro/POWER.md +147 -0
- package/plugins/kiro/README.md +31 -0
- package/plugins/kiro/SETUP.md +234 -0
- package/plugins/kiro/assets/logo.svg +27 -0
- package/plugins/kiro/mcp.json +7 -0
- package/plugins/kiro/steering/xmemo-memory.md +32 -0
- package/src/cli.js +23 -3996
- package/src/commands/auth.js +230 -0
- package/src/commands/diagnostics.js +197 -0
- package/src/commands/mcp.js +188 -0
- package/src/commands/profile.js +57 -0
- package/src/commands/setup.js +191 -0
- package/src/commands/update.js +58 -0
- package/src/config/env.js +82 -0
- package/src/config/paths.js +26 -0
- package/src/config/profile.js +533 -0
- package/src/core/args.js +63 -0
- package/src/core/constants.js +32 -0
- package/src/core/errors.js +6 -0
- package/src/core/io.js +16 -0
- package/src/core/runtime.js +144 -0
- package/src/core/version.js +1 -0
- package/src/mcp/clients/detect.js +51 -0
- package/src/mcp/clients/registry.js +68 -0
- package/src/mcp/clients.js +81 -0
- package/src/mcp/core/names.js +13 -0
- package/src/mcp/core/templates.js +156 -0
- package/src/mcp/formats/json.js +355 -0
- package/src/mcp/formats/toml.js +148 -0
- package/src/mcp/formats/yaml.js +72 -0
- package/src/mcp/identity/device.js +78 -0
- package/src/mcp/identity/paths.js +155 -0
- package/src/mcp/proxy/copilot.js +44 -0
- package/src/mcp/proxy/server.js +112 -0
- package/src/network/auth.js +200 -0
- package/src/network/base-url.js +13 -0
- package/src/network/discovery.js +103 -0
- package/src/network/http.js +161 -0
- package/src/ui/help.js +59 -0
- package/src/ui/setup.js +244 -0
|
@@ -0,0 +1,533 @@
|
|
|
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 '../core/constants.js';
|
|
16
|
+
import { UsageError } from '../core/errors.js';
|
|
17
|
+
import { writeLine } from '../core/io.js';
|
|
18
|
+
import { readTextIfExists } from '../core/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
|
+
}
|
|
533
|
+
|
package/src/core/args.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { UsageError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
export function sameMajorMinor(left, right) {
|
|
4
|
+
const leftParts = left.split('.');
|
|
5
|
+
const rightParts = right.split('.');
|
|
6
|
+
return leftParts[0] === rightParts[0] && leftParts[1] === rightParts[1];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function optionValue(args, name) {
|
|
10
|
+
const index = args.indexOf(name);
|
|
11
|
+
if (index === -1) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const value = args[index + 1];
|
|
16
|
+
if (!value || value.startsWith('--')) {
|
|
17
|
+
throw new UsageError(`Option ${name} requires a value.`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function stringValue(source, keys) {
|
|
24
|
+
const value = valueAtPath(source, keys);
|
|
25
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function booleanValue(source, keys) {
|
|
29
|
+
const value = valueAtPath(source, keys);
|
|
30
|
+
return typeof value === 'boolean' ? value : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function arrayValue(source, keys) {
|
|
34
|
+
const value = valueAtPath(source, keys);
|
|
35
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function valueAtPath(source, keys) {
|
|
39
|
+
let current = source;
|
|
40
|
+
for (const key of keys) {
|
|
41
|
+
if (!isPlainObject(current) || !(key in current)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
current = current[key];
|
|
45
|
+
}
|
|
46
|
+
return current;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function hasFlag(args, name) {
|
|
50
|
+
return args.includes(name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parsePositiveInteger(value, name) {
|
|
54
|
+
const parsed = Number.parseInt(value, 10);
|
|
55
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
56
|
+
throw new UsageError(`${name} must be a positive integer.`);
|
|
57
|
+
}
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isPlainObject(value) {
|
|
62
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
63
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const PRODUCT_NAME = 'XMemo';
|
|
2
|
+
export const PACKAGE_NAME = '@xmemo/client';
|
|
3
|
+
export const FALLBACK_PACKAGE_NAME = '@yonro/xmemo-client';
|
|
4
|
+
export const COMMAND_NAME = 'xmemo';
|
|
5
|
+
export const LEGACY_COMMAND_NAME = 'memory-os';
|
|
6
|
+
export { CLI_VERSION } from './version.js';
|
|
7
|
+
export const DEFAULT_SERVICE_URL = 'https://xmemo.dev';
|
|
8
|
+
export const TOKEN_ENV_VAR = 'XMEMO_KEY';
|
|
9
|
+
export const LEGACY_TOKEN_ENV_VAR = 'MEMORY_OS_MCP_TOKEN';
|
|
10
|
+
export const AGENT_ID_ENV_VAR = 'XMEMO_AGENT_ID';
|
|
11
|
+
export const AGENT_INSTANCE_ENV_VAR = 'XMEMO_AGENT_INSTANCE_ID';
|
|
12
|
+
export const AGENT_ID_HEADER = 'X-Memory-OS-Agent-ID';
|
|
13
|
+
export const AGENT_INSTANCE_HEADER = 'X-Memory-OS-Agent-Instance-ID';
|
|
14
|
+
export const MCP_SERVER_NAME = 'XMemo';
|
|
15
|
+
export const LEGACY_MCP_SERVER_NAMES = ['memory_os', 'memory-os'];
|
|
16
|
+
export const CODEX_PROFILE_TARGET = 'AGENTS.md';
|
|
17
|
+
export const CODEX_PROFILE_MARKER_START = '<!-- memory-os:codex-profile:start -->';
|
|
18
|
+
export const CODEX_PROFILE_MARKER_END = '<!-- memory-os:codex-profile:end -->';
|
|
19
|
+
export const CLIENT_PROFILE_TARGETS = {
|
|
20
|
+
cursor: '.cursor/rules/xmemo-memory.md',
|
|
21
|
+
'gemini-cli': 'GEMINI.md',
|
|
22
|
+
antigravity: 'GEMINI.md',
|
|
23
|
+
trae: '.trae/rules/xmemo-memory.md',
|
|
24
|
+
'trae-solo': '.trae/rules/xmemo-memory.md'
|
|
25
|
+
};
|
|
26
|
+
export const CLIENT_PROFILE_MARKER_START = '<!-- xmemo:profile:start -->';
|
|
27
|
+
export const CLIENT_PROFILE_MARKER_END = '<!-- xmemo:profile:end -->';
|
|
28
|
+
export const PROFILE_MARKER_PREFIX = 'memory-os:memory-profile';
|
|
29
|
+
export const DEVICE_LOGIN_START_PATH = '/api/v1/auth/device/start';
|
|
30
|
+
export const DEVICE_LOGIN_TOKEN_PATH = '/api/v1/auth/device/token';
|
|
31
|
+
export const DEFAULT_PROXY_HOST = '127.0.0.1';
|
|
32
|
+
export const DEFAULT_PROXY_PORT = 8765;
|
package/src/core/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
|
+
}
|