@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.
Files changed (45) hide show
  1. package/README.md +37 -7
  2. package/package.json +3 -2
  3. package/plugins/kiro/.kiro-plugin/power.json +35 -0
  4. package/plugins/kiro/CHANGELOG.md +9 -0
  5. package/plugins/kiro/LICENSE +7 -0
  6. package/plugins/kiro/POWER.md +147 -0
  7. package/plugins/kiro/README.md +31 -0
  8. package/plugins/kiro/SETUP.md +234 -0
  9. package/plugins/kiro/assets/logo.svg +27 -0
  10. package/plugins/kiro/mcp.json +7 -0
  11. package/plugins/kiro/steering/xmemo-memory.md +32 -0
  12. package/src/cli.js +23 -3996
  13. package/src/commands/auth.js +230 -0
  14. package/src/commands/diagnostics.js +197 -0
  15. package/src/commands/mcp.js +188 -0
  16. package/src/commands/profile.js +57 -0
  17. package/src/commands/setup.js +191 -0
  18. package/src/commands/update.js +58 -0
  19. package/src/config/env.js +82 -0
  20. package/src/config/paths.js +26 -0
  21. package/src/config/profile.js +533 -0
  22. package/src/core/args.js +63 -0
  23. package/src/core/constants.js +32 -0
  24. package/src/core/errors.js +6 -0
  25. package/src/core/io.js +16 -0
  26. package/src/core/runtime.js +144 -0
  27. package/src/core/version.js +1 -0
  28. package/src/mcp/clients/detect.js +51 -0
  29. package/src/mcp/clients/registry.js +68 -0
  30. package/src/mcp/clients.js +81 -0
  31. package/src/mcp/core/names.js +13 -0
  32. package/src/mcp/core/templates.js +156 -0
  33. package/src/mcp/formats/json.js +355 -0
  34. package/src/mcp/formats/toml.js +148 -0
  35. package/src/mcp/formats/yaml.js +72 -0
  36. package/src/mcp/identity/device.js +78 -0
  37. package/src/mcp/identity/paths.js +155 -0
  38. package/src/mcp/proxy/copilot.js +44 -0
  39. package/src/mcp/proxy/server.js +112 -0
  40. package/src/network/auth.js +200 -0
  41. package/src/network/base-url.js +13 -0
  42. package/src/network/discovery.js +103 -0
  43. package/src/network/http.js +161 -0
  44. package/src/ui/help.js +59 -0
  45. package/src/ui/setup.js +244 -0
@@ -0,0 +1,155 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export function configRoot(env) {
5
+ if (env.XMEMO_CONFIG_HOME) {
6
+ return env.XMEMO_CONFIG_HOME;
7
+ }
8
+
9
+ if (env.MEMORY_OS_CONFIG_HOME) {
10
+ return env.MEMORY_OS_CONFIG_HOME;
11
+ }
12
+
13
+ if (process.platform === 'win32' && env.LOCALAPPDATA) {
14
+ return path.join(env.LOCALAPPDATA, 'XMemo', 'CLI');
15
+ }
16
+
17
+ if (env.XDG_CONFIG_HOME) {
18
+ return path.join(env.XDG_CONFIG_HOME, 'xmemo');
19
+ }
20
+
21
+ const home = env.HOME || os.homedir();
22
+ return path.join(home, '.config', 'xmemo');
23
+ }
24
+
25
+ export function defaultCodexConfigPath(env) {
26
+ const home = env.USERPROFILE || env.HOME || os.homedir();
27
+ return path.join(home, '.codex', 'config.toml');
28
+ }
29
+
30
+ export function defaultCursorConfigPath(env) {
31
+ const home = env.USERPROFILE || env.HOME || os.homedir();
32
+ return path.join(home, '.cursor', 'mcp.json');
33
+ }
34
+
35
+ export function defaultGeminiConfigPath(env) {
36
+ const home = env.USERPROFILE || env.HOME || os.homedir();
37
+ return path.join(home, '.gemini', 'settings.json');
38
+ }
39
+
40
+ export function defaultAntigravityConfigPath(env) {
41
+ const home = env.USERPROFILE || env.HOME || os.homedir();
42
+ return path.join(home, '.gemini', 'config', 'mcp_config.json');
43
+ }
44
+
45
+ export function defaultAntigravityIdeConfigPath(env) {
46
+ const home = env.USERPROFILE || env.HOME || os.homedir();
47
+ return path.join(home, '.gemini', 'config', 'mcp_config.json');
48
+ }
49
+
50
+ export function defaultAntigravity2ConfigPath(env) {
51
+ const home = env.USERPROFILE || env.HOME || os.homedir();
52
+ return path.join(home, '.gemini', 'config', 'mcp_config.json');
53
+ }
54
+
55
+ export function defaultAntigravityCliConfigPath(env) {
56
+ const home = env.USERPROFILE || env.HOME || os.homedir();
57
+ return path.join(home, '.gemini', 'config', 'mcp_config.json');
58
+ }
59
+
60
+ export function defaultCopilotConfigPath(env) {
61
+ const home = env.USERPROFILE || env.HOME || os.homedir();
62
+ return path.join(env.COPILOT_HOME ?? path.join(home, '.copilot'), 'mcp-config.json');
63
+ }
64
+
65
+ export function defaultWindsurfConfigPath(env) {
66
+ const home = env.USERPROFILE || env.HOME || os.homedir();
67
+ return path.join(home, '.codeium', 'windsurf', 'mcp_config.json');
68
+ }
69
+
70
+ export function defaultClineConfigPath(env) {
71
+ const home = env.USERPROFILE || env.HOME || os.homedir();
72
+ return path.join(home, 'Documents', 'Cline', 'MCP', 'cline_mcp_settings.json');
73
+ }
74
+
75
+ export function defaultContinueConfigPath(env) {
76
+ const home = env.USERPROFILE || env.HOME || os.homedir();
77
+ return path.join(home, '.continue', 'config.json');
78
+ }
79
+
80
+ export function defaultClaudeConfigPath(env) {
81
+ if (process.platform === 'win32' && env.APPDATA) {
82
+ return path.join(env.APPDATA, 'Claude', 'claude_desktop_config.json');
83
+ }
84
+ const home = env.HOME || os.homedir();
85
+ if (process.platform === 'darwin') {
86
+ return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
87
+ }
88
+ return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
89
+ }
90
+
91
+ export function defaultOpenclawConfigPath(env) {
92
+ const home = env.USERPROFILE || env.HOME || os.homedir();
93
+ return path.join(home, '.openclaw', 'openclaw.json');
94
+ }
95
+
96
+ export function defaultKiroConfigPath(env) {
97
+ const home = env.USERPROFILE || env.HOME || os.homedir();
98
+ return path.join(home, '.kiro', 'settings', 'mcp.json');
99
+ }
100
+
101
+ export function defaultZedConfigPath(env) {
102
+ if (process.platform === 'win32' && env.APPDATA) {
103
+ return path.join(env.APPDATA, 'Zed', 'settings.json');
104
+ }
105
+ const home = env.HOME || env.USERPROFILE || os.homedir();
106
+ return path.join(home, '.config', 'zed', 'settings.json');
107
+ }
108
+
109
+ export function defaultJetbrainsConfigPath(env) {
110
+ const home = env.USERPROFILE || env.HOME || os.homedir();
111
+ return path.join(home, '.continue', 'config.json');
112
+ }
113
+
114
+ export function defaultOpencodeConfigPath(env) {
115
+ const home = env.USERPROFILE || env.HOME || os.homedir();
116
+ return path.join(home, '.config', 'opencode', 'opencode.json');
117
+ }
118
+
119
+ export function defaultHermesConfigPath(env) {
120
+ const home = env.USERPROFILE || env.HOME || os.homedir();
121
+ return path.join(home, '.hermes', 'config.yaml');
122
+ }
123
+
124
+ export function defaultQwenConfigPath(env) {
125
+ const home = env.USERPROFILE || env.HOME || os.homedir();
126
+ return path.join(home, '.qwen', 'settings.json');
127
+ }
128
+
129
+ export function defaultTraeConfigPath(env) {
130
+ if (process.platform === 'win32' && env.APPDATA) {
131
+ return path.join(env.APPDATA, 'Trae', 'User', 'mcp.json');
132
+ }
133
+ const home = env.USERPROFILE || env.HOME || os.homedir();
134
+ if (process.platform === 'darwin') {
135
+ return path.join(home, 'Library', 'Application Support', 'Trae', 'User', 'mcp.json');
136
+ }
137
+ return path.join(home, '.config', 'Trae', 'User', 'mcp.json');
138
+ }
139
+
140
+ export function defaultTraeSoloConfigPath(env) {
141
+ if (process.platform === 'win32' && env.APPDATA) {
142
+ return path.join(env.APPDATA, 'TRAE SOLO', 'User', 'mcp.json');
143
+ }
144
+ const home = env.USERPROFILE || env.HOME || os.homedir();
145
+ if (process.platform === 'darwin') {
146
+ return path.join(home, 'Library', 'Application Support', 'TRAE SOLO', 'User', 'mcp.json');
147
+ }
148
+ return path.join(home, '.config', 'TRAE SOLO', 'User', 'mcp.json');
149
+ }
150
+
151
+ export function defaultClaudecodeConfigPath(env) {
152
+ const home = env.USERPROFILE || env.HOME || os.homedir();
153
+ return path.join(home, '.claude.json');
154
+ }
155
+
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { MCP_SERVER_NAME } from '../../core/constants.js';
5
+ import { UsageError } from '../../core/errors.js';
6
+ import {
7
+ bestEffortChmod,
8
+ isPlainObject,
9
+ parseJsonConfig,
10
+ readTextIfExists
11
+ } from '../../core/runtime.js';
12
+ import { existingJsonMcpServerName } from '../core/names.js';
13
+ export { mcpProxyCommand } from './server.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
+ }
44
+
@@ -0,0 +1,112 @@
1
+ import http from 'node:http';
2
+
3
+ import { optionValue, parsePositiveInteger } from '../../core/args.js';
4
+ import {
5
+ AGENT_ID_HEADER,
6
+ AGENT_INSTANCE_HEADER,
7
+ CLI_VERSION,
8
+ COMMAND_NAME,
9
+ DEFAULT_SERVICE_URL,
10
+ DEFAULT_PROXY_HOST,
11
+ DEFAULT_PROXY_PORT,
12
+ PRODUCT_NAME,
13
+ TOKEN_ENV_VAR
14
+ } from '../../core/constants.js';
15
+ import { credentialsPath, resolveCredentialToken, validateToken } from '../../network/auth.js';
16
+ import { endpointUrl, normalizeBaseUrl } from '../../network/http.js';
17
+ import { UsageError } from '../../core/errors.js';
18
+ import { writeLine } from '../../core/io.js';
19
+ import { closeServer, readAll, waitForShutdown } from '../../core/runtime.js';
20
+
21
+ export async function mcpProxyCommand(args, io, { agentIdentity }) {
22
+ const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
23
+ const mcpUrl = endpointUrl(baseUrl, '/mcp');
24
+ const host = optionValue(args, '--host') ?? DEFAULT_PROXY_HOST;
25
+ const port = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
26
+ const token = await resolveCredentialToken(io.env);
27
+ if (!token) {
28
+ throw new UsageError(`No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\` first.`);
29
+ }
30
+ validateToken(token);
31
+ const identity = await agentIdentity('copilot-cli', io.env);
32
+
33
+ const server = http.createServer(async (request, response) => {
34
+ try {
35
+ await handleMcpProxyRequest({ request, response, mcpUrl, token, identity, io });
36
+ } catch (error) {
37
+ response.statusCode = 502;
38
+ response.setHeader('content-type', 'application/json');
39
+ response.end(JSON.stringify({ error: 'mcp_proxy_error', message: error.message }));
40
+ }
41
+ });
42
+
43
+ await new Promise((resolve, reject) => {
44
+ server.once('error', reject);
45
+ server.listen(port, host, () => {
46
+ server.off('error', reject);
47
+ resolve();
48
+ });
49
+ });
50
+
51
+ writeLine(io.stdout, `${PRODUCT_NAME} MCP proxy listening on http://${host}:${port}/mcp`);
52
+ writeLine(io.stdout, `Forwarding to ${mcpUrl}`);
53
+ writeLine(io.stdout, `Credential source: ${TOKEN_ENV_VAR} or ${credentialsPath(io.env)}`);
54
+ if (io.keepAlive === false) {
55
+ await closeServer(server);
56
+ return 0;
57
+ }
58
+
59
+ await waitForShutdown(server, io);
60
+ return 0;
61
+ }
62
+
63
+ async function handleMcpProxyRequest({ request, response, mcpUrl, token, identity, io }) {
64
+ const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? `${DEFAULT_PROXY_HOST}:${DEFAULT_PROXY_PORT}`}`);
65
+ if (request.method !== 'POST' || requestUrl.pathname !== '/mcp') {
66
+ response.statusCode = 404;
67
+ response.setHeader('content-type', 'application/json');
68
+ response.end(JSON.stringify({ error: 'not_found' }));
69
+ return;
70
+ }
71
+
72
+ const body = await readAll(request);
73
+ const upstreamHeaders = {
74
+ accept: String(request.headers.accept || 'application/json, text/event-stream'),
75
+ 'content-type': String(request.headers['content-type'] || 'application/json'),
76
+ authorization: `Bearer ${token}`,
77
+ [AGENT_ID_HEADER]: identity.agentId,
78
+ [AGENT_INSTANCE_HEADER]: identity.agentInstanceId,
79
+ 'user-agent': `XMemo-CLI-Proxy/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
80
+ };
81
+ const sessionId = request.headers['mcp-session-id'];
82
+ if (sessionId) {
83
+ upstreamHeaders['mcp-session-id'] = Array.isArray(sessionId) ? sessionId[0] : sessionId;
84
+ }
85
+
86
+ const upstream = await io.fetch(mcpUrl, {
87
+ method: 'POST',
88
+ headers: upstreamHeaders,
89
+ body
90
+ });
91
+
92
+ response.statusCode = upstream.status;
93
+ for (const header of ['content-type', 'mcp-session-id']) {
94
+ const value = upstream.headers.get(header);
95
+ if (value) {
96
+ response.setHeader(header, value);
97
+ }
98
+ }
99
+ const buffer = Buffer.from(await upstream.arrayBuffer());
100
+ response.end(buffer);
101
+ }
102
+
103
+ function baseUrlOption(args, env) {
104
+ return optionValue(args, '--url')
105
+ ?? optionValue(args, '--base-url')
106
+ ?? env.XMEMO_URL
107
+ ?? env.XMEMO_BASE_URL
108
+ ?? env.MEMORY_OS_URL
109
+ ?? env.MEMORY_OS_BASE_URL
110
+ ?? DEFAULT_SERVICE_URL;
111
+ }
112
+
@@ -0,0 +1,200 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { stringValue } from '../core/args.js';
6
+ import {
7
+ CLI_VERSION,
8
+ DEVICE_LOGIN_START_PATH,
9
+ DEVICE_LOGIN_TOKEN_PATH,
10
+ LEGACY_TOKEN_ENV_VAR,
11
+ PACKAGE_NAME,
12
+ TOKEN_ENV_VAR
13
+ } from '../core/constants.js';
14
+ import { endpointUrl, postJson } from './http.js';
15
+ import { UsageError } from '../core/errors.js';
16
+ import {
17
+ bestEffortChmod,
18
+ parseJsonConfig,
19
+ readAll,
20
+ readTextIfExists,
21
+ sleep
22
+ } from '../core/runtime.js';
23
+
24
+ export async function startDeviceLogin(baseUrl, timeoutMs, io) {
25
+ const payload = await postJson(endpointUrl(baseUrl, DEVICE_LOGIN_START_PATH), {
26
+ client_id: PACKAGE_NAME,
27
+ cli_version: CLI_VERSION,
28
+ token_type: 'mcp_token',
29
+ scopes: ['memory:read', 'memory:write']
30
+ }, timeoutMs, io);
31
+
32
+ const deviceCode = stringValue(payload, ['device_code']);
33
+ const verificationUri = stringValue(payload, ['verification_uri']);
34
+ if (!deviceCode || !verificationUri) {
35
+ throw new UsageError(`Device login did not return device_code and verification_uri from ${baseUrl}.`);
36
+ }
37
+
38
+ return {
39
+ deviceCode,
40
+ userCode: stringValue(payload, ['user_code']),
41
+ verificationUri,
42
+ verificationUriComplete: stringValue(payload, ['verification_uri_complete']),
43
+ expiresIn: Number.isFinite(Number(payload.expires_in)) ? Number(payload.expires_in) : 600,
44
+ interval: Number.isFinite(Number(payload.interval)) ? Math.max(1, Number(payload.interval)) : 5
45
+ };
46
+ }
47
+
48
+ export async function pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, options = {}) {
49
+ const deadline = Date.now() + Math.min(start.expiresIn * 1000, loginTimeoutMs);
50
+ const sleepFn = io.sleep ?? sleep;
51
+ let intervalSeconds = start.interval;
52
+ while (Date.now() <= deadline) {
53
+ const payload = await postJson(endpointUrl(baseUrl, DEVICE_LOGIN_TOKEN_PATH), {
54
+ device_code: start.deviceCode,
55
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
56
+ }, httpTimeoutMs, io, { allowDevicePending: true });
57
+
58
+ const accessToken = stringValue(payload, ['access_token']) ?? stringValue(payload, ['token']);
59
+ if (accessToken) {
60
+ validateToken(accessToken);
61
+ return {
62
+ accessToken,
63
+ account: accountFromPayload(payload)
64
+ };
65
+ }
66
+
67
+ const error = stringValue(payload, ['error']);
68
+ if (error && error !== 'authorization_pending' && error !== 'slow_down') {
69
+ throw new UsageError(`Device login failed: ${error}`);
70
+ }
71
+ if (options.pollOnce) {
72
+ throw new UsageError('Device login is still pending.');
73
+ }
74
+ if (error === 'slow_down') {
75
+ intervalSeconds += 5;
76
+ }
77
+ await sleepFn(intervalSeconds * 1000);
78
+ }
79
+
80
+ throw new UsageError('Device login expired before authorization completed.');
81
+ }
82
+
83
+ export async function storeTokenFromStdin(io, metadata = {}) {
84
+ const token = (await readAll(io.stdin)).trim();
85
+ validateToken(token);
86
+ return await storeTokenValue(token, metadata, io.env);
87
+ }
88
+
89
+ export async function storeTokenValue(token, metadata, env) {
90
+ validateToken(token);
91
+ const credentialPath = credentialsPath(env);
92
+ await writePlaintextCredential(credentialPath, token, metadata);
93
+ return {
94
+ credentialPath,
95
+ tokenPresent: true,
96
+ tokenPrinted: false,
97
+ projectFilesModified: false,
98
+ storage: 'user-scoped-credential-file'
99
+ };
100
+ }
101
+
102
+ export async function readStoredCredential(env) {
103
+ const credentialPath = credentialsPath(env);
104
+ const content = await readTextIfExists(credentialPath);
105
+ if (!content.trim()) {
106
+ return { path: credentialPath, token: null };
107
+ }
108
+ const parsed = parseJsonConfig(content, credentialPath);
109
+ return {
110
+ path: credentialPath,
111
+ token: stringValue(parsed, ['token']),
112
+ storage: stringValue(parsed, ['storage']),
113
+ account: accountFromPayload(parsed.metadata)
114
+ };
115
+ }
116
+
117
+ function accountFromPayload(payload) {
118
+ const account = payload && typeof payload === 'object'
119
+ ? (payload.user && typeof payload.user === 'object' ? payload.user : payload.account)
120
+ : null;
121
+ if (!account || typeof account !== 'object') {
122
+ return null;
123
+ }
124
+ const userId = stringValue(account, ['user_id']) ?? stringValue(account, ['id']) ?? stringValue(account, ['userId']);
125
+ const email = stringValue(account, ['email']);
126
+ const displayName = stringValue(account, ['display_name']) ?? stringValue(account, ['name']) ?? stringValue(account, ['displayName']);
127
+ if (!userId && !email && !displayName) {
128
+ return null;
129
+ }
130
+ return {
131
+ userId: userId ?? null,
132
+ email: email ?? null,
133
+ displayName: displayName ?? null
134
+ };
135
+ }
136
+
137
+ export function formatAccount(account) {
138
+ const label = account.displayName || account.email || account.userId || 'XMemo account';
139
+ return account.email && account.displayName ? `${account.displayName} <${account.email}>` : label;
140
+ }
141
+
142
+ export async function resolveCredentialToken(env) {
143
+ const environmentToken = env[TOKEN_ENV_VAR] ?? env[LEGACY_TOKEN_ENV_VAR];
144
+ if (environmentToken) {
145
+ return environmentToken;
146
+ }
147
+ const credential = await readStoredCredential(env);
148
+ return credential.token;
149
+ }
150
+
151
+ export function credentialsPath(env) {
152
+ return path.join(configRoot(env), 'credentials.json');
153
+ }
154
+
155
+ function configRoot(env) {
156
+ if (env.XMEMO_CONFIG_HOME) {
157
+ return env.XMEMO_CONFIG_HOME;
158
+ }
159
+ if (env.MEMORY_OS_CONFIG_HOME) {
160
+ return env.MEMORY_OS_CONFIG_HOME;
161
+ }
162
+ if (process.platform === 'win32' && env.LOCALAPPDATA) {
163
+ return path.join(env.LOCALAPPDATA, 'XMemo', 'CLI');
164
+ }
165
+ if (env.XDG_CONFIG_HOME) {
166
+ return path.join(env.XDG_CONFIG_HOME, 'xmemo');
167
+ }
168
+ const home = env.HOME || os.homedir();
169
+ return path.join(home, '.config', 'xmemo');
170
+ }
171
+
172
+ async function writePlaintextCredential(credentialPath, token, metadata = {}) {
173
+ await fs.mkdir(path.dirname(credentialPath), { recursive: true, mode: 0o700 });
174
+ await bestEffortChmod(path.dirname(credentialPath), 0o700);
175
+ const payload = {
176
+ version: 1,
177
+ tokenEnvVar: TOKEN_ENV_VAR,
178
+ storage: 'user-scoped-credential-file',
179
+ createdAt: new Date().toISOString(),
180
+ metadata,
181
+ token
182
+ };
183
+ await fs.writeFile(credentialPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
184
+ await bestEffortChmod(credentialPath, 0o600);
185
+ }
186
+
187
+ export function validateToken(token) {
188
+ if (!token) {
189
+ throw new UsageError('Token from stdin is empty.');
190
+ }
191
+
192
+ if (/\s/.test(token)) {
193
+ throw new UsageError('Token must not contain whitespace.');
194
+ }
195
+
196
+ if (token.length < 16) {
197
+ throw new UsageError('Token is too short to be a production credential.');
198
+ }
199
+ }
200
+
@@ -0,0 +1,13 @@
1
+ import { optionValue } from '../core/args.js';
2
+ import { DEFAULT_SERVICE_URL } from '../core/constants.js';
3
+
4
+ export function baseUrlOption(args, env) {
5
+ return optionValue(args, '--base-url')
6
+ ?? optionValue(args, '--url')
7
+ ?? env.XMEMO_BASE_URL
8
+ ?? env.XMEMO_URL
9
+ ?? env.MEMORY_OS_BASE_URL
10
+ ?? env.MEMORY_OS_URL
11
+ ?? DEFAULT_SERVICE_URL;
12
+ }
13
+
@@ -0,0 +1,103 @@
1
+ import { arrayValue, booleanValue, stringValue } from '../core/args.js';
2
+ import { TOKEN_ENV_VAR } from '../core/constants.js';
3
+ import { UsageError } from '../core/errors.js';
4
+ import { endpointUrl, fetchJson } from './http.js';
5
+ import { isPlainObject } from '../core/runtime.js';
6
+
7
+ export function ensureDiscoveryService(discovery, discoveryUrl) {
8
+ const service = stringValue(discovery, ['service']);
9
+ if (service && service !== 'memory-os') {
10
+ throw new UsageError(`Discovery document at ${discoveryUrl} is for '${service}', not 'memory-os'.`);
11
+ }
12
+ }
13
+
14
+ export function buildSetupPlan({ baseUrl, discoveryUrl, statusUrl, discovery, status, localClients }) {
15
+ const apiBase = stringValue(discovery, ['urls', 'api_base'])
16
+ ?? stringValue(discovery, ['api_base_url'])
17
+ ?? baseUrl;
18
+ const mcpUrl = stringValue(discovery, ['urls', 'mcp'])
19
+ ?? stringValue(discovery, ['mcp_url'])
20
+ ?? endpointUrl(apiBase, '/mcp');
21
+ const tokenPortalUrl = stringValue(discovery, ['urls', 'token_portal'])
22
+ ?? stringValue(discovery, ['token_portal_url'])
23
+ ?? stringValue(status, ['requirements', 'token_portal_url']);
24
+ const tokenEnvVar = stringValue(discovery, ['auth', 'token_env_var'])
25
+ ?? stringValue(status, ['requirements', 'token_env_var'])
26
+ ?? TOKEN_ENV_VAR;
27
+
28
+ return {
29
+ schemaVersion: '1.0',
30
+ baseUrl,
31
+ discoveryUrl,
32
+ statusUrl,
33
+ apiBase,
34
+ mcpUrl,
35
+ guideUrl: stringValue(discovery, ['urls', 'guide']) ?? endpointUrl(apiBase, '/guide'),
36
+ docsUrl: stringValue(discovery, ['urls', 'docs']),
37
+ tokenPortalUrl,
38
+ tokenEnvVar,
39
+ onboardingReady: booleanValue(status, ['ready']),
40
+ supportedClients: discoveryMcpClients(discovery),
41
+ localClients,
42
+ privacy: {
43
+ telemetry: false,
44
+ tokenSent: false,
45
+ tokenEmbeddedInConfig: false
46
+ },
47
+ boundaries: {
48
+ clientAllowed: arrayValue(discovery, ['agent_boundary', 'client_allowed'])
49
+ ?? arrayValue(status, ['agent_boundary', 'client_allowed'])
50
+ ?? [],
51
+ adminRequired: arrayValue(discovery, ['agent_boundary', 'admin_required'])
52
+ ?? arrayValue(status, ['agent_boundary', 'admin_required'])
53
+ ?? []
54
+ }
55
+ };
56
+ }
57
+
58
+ export async function bestEffortRootVersion(discovery, timeoutMs, io) {
59
+ const rootDiscoveryUrl = stringValue(discovery, ['urls', 'root_discovery']);
60
+ if (!rootDiscoveryUrl) {
61
+ return {};
62
+ }
63
+ try {
64
+ const rootDiscovery = await fetchJson(rootDiscoveryUrl, timeoutMs, io);
65
+ return { version: stringValue(rootDiscovery, ['version']) ?? undefined };
66
+ } catch (error) {
67
+ return { error: error.message };
68
+ }
69
+ }
70
+
71
+ export function discoveryMcpUrl(discovery, baseUrl) {
72
+ return stringValue(discovery, ['api', 'mcp', 'url'])
73
+ ?? stringValue(discovery, ['urls', 'mcp'])
74
+ ?? endpointUrl(baseUrl, '/mcp');
75
+ }
76
+
77
+ export function agentDiscoveryClientIds(discovery) {
78
+ const clients = Array.isArray(discovery?.clients) ? discovery.clients : [];
79
+ const ids = clients
80
+ .filter((client) => isPlainObject(client) && typeof client.id === 'string')
81
+ .map((client) => client.id);
82
+ if (ids.length > 0) {
83
+ return ids;
84
+ }
85
+ const supported = arrayValue(discovery, ['supported_clients']);
86
+ return supported ?? [];
87
+ }
88
+
89
+ export function discoveryMcpClients(discovery) {
90
+ const clients = discovery?.clients?.mcp;
91
+ if (!Array.isArray(clients)) {
92
+ return [];
93
+ }
94
+
95
+ return clients
96
+ .filter((client) => isPlainObject(client) && typeof client.id === 'string')
97
+ .map((client) => ({
98
+ id: client.id,
99
+ configEndpoint: typeof client.config_endpoint === 'string' ? client.config_endpoint : null
100
+ }));
101
+ }
102
+
103
+