@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmemo/client",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.156",
|
|
4
4
|
"description": "Privacy-first CLI and MCP setup helper for XMemo.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"lint": "node
|
|
17
|
+
"lint": "node scripts/check-js.mjs",
|
|
18
18
|
"test": "node --test",
|
|
19
19
|
"pack:dry-run": "npm pack --dry-run",
|
|
20
20
|
"prepublishOnly": "npm run lint && npm test && npm run pack:dry-run"
|
package/src/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
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { stringValue } from './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 './constants.js';
|
|
14
|
+
import { endpointUrl, postJson } from './http.js';
|
|
15
|
+
import { UsageError } from './errors.js';
|
|
16
|
+
import {
|
|
17
|
+
bestEffortChmod,
|
|
18
|
+
parseJsonConfig,
|
|
19
|
+
readAll,
|
|
20
|
+
readTextIfExists,
|
|
21
|
+
sleep
|
|
22
|
+
} from './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
|
+
}
|
package/src/base-url.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { optionValue } from './args.js';
|
|
2
|
+
import { DEFAULT_SERVICE_URL } from './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
|
+
}
|