@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,230 @@
|
|
|
1
|
+
import { hasFlag, optionValue, parsePositiveInteger } from '../core/args.js';
|
|
2
|
+
import {
|
|
3
|
+
formatAccount,
|
|
4
|
+
pollDeviceLogin,
|
|
5
|
+
readStoredCredential,
|
|
6
|
+
resolveCredentialToken,
|
|
7
|
+
startDeviceLogin,
|
|
8
|
+
storeTokenFromStdin,
|
|
9
|
+
storeTokenValue,
|
|
10
|
+
validateToken
|
|
11
|
+
} from '../network/auth.js';
|
|
12
|
+
import { baseUrlOption } from '../network/base-url.js';
|
|
13
|
+
import {
|
|
14
|
+
COMMAND_NAME,
|
|
15
|
+
LEGACY_TOKEN_ENV_VAR,
|
|
16
|
+
PRODUCT_NAME,
|
|
17
|
+
TOKEN_ENV_VAR
|
|
18
|
+
} from '../core/constants.js';
|
|
19
|
+
import { UsageError } from '../core/errors.js';
|
|
20
|
+
import { normalizeBaseUrl, verifyTokenWithMcp } from '../network/http.js';
|
|
21
|
+
import { writeLine } from '../core/io.js';
|
|
22
|
+
import { readAll } from '../core/runtime.js';
|
|
23
|
+
|
|
24
|
+
export async function loginCommand(args, io) {
|
|
25
|
+
const outputJson = hasFlag(args, '--json');
|
|
26
|
+
const fromStdin = hasFlag(args, '--from-stdin') || hasFlag(args, '--token-stdin');
|
|
27
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
28
|
+
const httpTimeoutMs = parsePositiveInteger(optionValue(args, '--http-timeout-ms') ?? '30000', '--http-timeout-ms');
|
|
29
|
+
const loginTimeoutOption = optionValue(args, '--timeout-ms');
|
|
30
|
+
const pollOnce = hasFlag(args, '--poll-once');
|
|
31
|
+
|
|
32
|
+
if (fromStdin) {
|
|
33
|
+
const result = await storeTokenFromStdin(io, { source: 'stdin' });
|
|
34
|
+
if (outputJson) {
|
|
35
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
36
|
+
} else {
|
|
37
|
+
writeLine(io.stdout, `${PRODUCT_NAME} login complete.`);
|
|
38
|
+
writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
|
|
39
|
+
writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const start = await startDeviceLogin(baseUrl, httpTimeoutMs, io);
|
|
45
|
+
const loginTimeoutMs = loginTimeoutOption
|
|
46
|
+
? parsePositiveInteger(loginTimeoutOption, '--timeout-ms')
|
|
47
|
+
: Math.max(1000, start.expiresIn * 1000);
|
|
48
|
+
if (!outputJson) {
|
|
49
|
+
writeLine(io.stdout, `${PRODUCT_NAME} device login`);
|
|
50
|
+
writeLine(io.stdout, `Open: ${start.verificationUriComplete ?? start.verificationUri}`);
|
|
51
|
+
if (start.userCode) {
|
|
52
|
+
writeLine(io.stdout, `Code: ${start.userCode}`);
|
|
53
|
+
}
|
|
54
|
+
writeLine(io.stdout, 'Waiting for authorization...');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const token = await pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, { pollOnce });
|
|
58
|
+
const result = await storeTokenValue(token.accessToken, { source: 'device-login', account: token.account }, io.env);
|
|
59
|
+
const payload = {
|
|
60
|
+
...result,
|
|
61
|
+
baseUrl,
|
|
62
|
+
verificationUri: start.verificationUri,
|
|
63
|
+
account: token.account,
|
|
64
|
+
deviceLogin: true
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (outputJson) {
|
|
68
|
+
writeLine(io.stdout, JSON.stringify(payload, null, 2));
|
|
69
|
+
} else {
|
|
70
|
+
writeLine(io.stdout, 'Login complete. Token stored securely in the user-scoped XMemo CLI config directory.');
|
|
71
|
+
if (token.account) {
|
|
72
|
+
writeLine(io.stdout, `Signed in as: ${formatAccount(token.account)}`);
|
|
73
|
+
}
|
|
74
|
+
writeLine(io.stdout, `Credential path: ${result.credentialPath}`);
|
|
75
|
+
writeLine(io.stdout, 'No extra token configuration is required.');
|
|
76
|
+
writeLine(io.stdout, `Optional check: ${COMMAND_NAME} token status --verify`);
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function authCommand(args, io) {
|
|
82
|
+
const subcommand = args[0] ?? 'help';
|
|
83
|
+
|
|
84
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
85
|
+
writeLine(io.stdout, 'Auth commands:');
|
|
86
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
|
|
87
|
+
writeLine(io.stdout, '');
|
|
88
|
+
writeLine(io.stdout, `Use \`${COMMAND_NAME} login\` to sign in and \`${COMMAND_NAME} token add --from-stdin\` to store an existing token.`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (subcommand === 'status') {
|
|
93
|
+
return await credentialStatusCommand(args.slice(1), io, { mode: 'auth' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw new UsageError(`Unknown auth command: ${subcommand}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function tokenCommand(args, io) {
|
|
100
|
+
const subcommand = args[0] ?? 'help';
|
|
101
|
+
|
|
102
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
103
|
+
writeLine(io.stdout, 'Token commands:');
|
|
104
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify]`);
|
|
105
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
|
|
106
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
|
|
107
|
+
writeLine(io.stdout, '');
|
|
108
|
+
writeLine(io.stdout, `${COMMAND_NAME} login is the recommended personal-user path.`);
|
|
109
|
+
writeLine(io.stdout, `${COMMAND_NAME} token add --from-stdin stores a token in the user-scoped XMemo CLI config directory.`);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (subcommand === 'status') {
|
|
114
|
+
return await credentialStatusCommand(args.slice(1), io, { mode: 'token' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (subcommand === 'add') {
|
|
118
|
+
if (!hasFlag(args, '--from-stdin')) {
|
|
119
|
+
throw new UsageError('Refusing command-line token input. Pipe the token through stdin with --from-stdin.');
|
|
120
|
+
}
|
|
121
|
+
const result = await storeTokenFromStdin(io, { source: 'token-add' });
|
|
122
|
+
if (hasFlag(args, '--json')) {
|
|
123
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
124
|
+
} else {
|
|
125
|
+
writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
|
|
126
|
+
writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');
|
|
127
|
+
}
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (subcommand === 'set') {
|
|
132
|
+
if (!hasFlag(args, '--from-stdin')) {
|
|
133
|
+
throw new UsageError('Refusing command-line token input. Pipe the token through stdin with --from-stdin.');
|
|
134
|
+
}
|
|
135
|
+
const token = (await readAll(io.stdin)).trim();
|
|
136
|
+
validateToken(token);
|
|
137
|
+
if (!hasFlag(args, '--allow-plaintext')) {
|
|
138
|
+
writeLine(io.stderr, 'Token was read from stdin but was not stored.');
|
|
139
|
+
writeLine(io.stderr, 'Enterprise default refuses plaintext token storage without --allow-plaintext.');
|
|
140
|
+
writeLine(io.stderr, `Preferred personal-user path: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin.`);
|
|
141
|
+
return 2;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = await storeTokenValue(token, { source: 'token-set' }, io.env);
|
|
145
|
+
writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
|
|
146
|
+
writeLine(io.stdout, 'Token value was not printed. Do not commit this file.');
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw new UsageError(`Unknown token command: ${subcommand}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function credentialStatusCommand(args, io, { mode }) {
|
|
154
|
+
const outputJson = hasFlag(args, '--json');
|
|
155
|
+
const verify = hasFlag(args, '--verify');
|
|
156
|
+
const credential = await readStoredCredential(io.env);
|
|
157
|
+
const environmentToken = io.env[TOKEN_ENV_VAR] ?? io.env[LEGACY_TOKEN_ENV_VAR] ?? '';
|
|
158
|
+
const hasEnvironmentToken = Boolean(environmentToken);
|
|
159
|
+
const hasUserCredential = Boolean(credential.token);
|
|
160
|
+
const tokenSource = hasEnvironmentToken ? 'environment' : hasUserCredential ? 'user-credential-file' : 'missing';
|
|
161
|
+
const report = {
|
|
162
|
+
loggedIn: hasEnvironmentToken || hasUserCredential,
|
|
163
|
+
tokenSource,
|
|
164
|
+
environmentToken: {
|
|
165
|
+
present: hasEnvironmentToken,
|
|
166
|
+
variable: hasEnvironmentToken && io.env[TOKEN_ENV_VAR] ? TOKEN_ENV_VAR : hasEnvironmentToken ? LEGACY_TOKEN_ENV_VAR : TOKEN_ENV_VAR
|
|
167
|
+
},
|
|
168
|
+
userCredentialFile: {
|
|
169
|
+
present: hasUserCredential,
|
|
170
|
+
path: credential.path,
|
|
171
|
+
storage: credential.storage ?? null
|
|
172
|
+
},
|
|
173
|
+
account: credential.account ?? null,
|
|
174
|
+
privacy: {
|
|
175
|
+
tokenPrinted: false,
|
|
176
|
+
projectFilesModified: false
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (verify) {
|
|
181
|
+
const token = await resolveCredentialToken(io.env);
|
|
182
|
+
if (!token) {
|
|
183
|
+
if (outputJson) {
|
|
184
|
+
writeLine(io.stdout, JSON.stringify({ ...report, verification: { ok: false, detail: 'no token found' } }, null, 2));
|
|
185
|
+
} else {
|
|
186
|
+
writeCredentialStatus(report, io, { mode });
|
|
187
|
+
writeLine(io.stderr, `No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\`.`);
|
|
188
|
+
}
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
192
|
+
const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '10000', '--timeout-ms');
|
|
193
|
+
const verification = await verifyTokenWithMcp(baseUrl, token, timeoutMs, io);
|
|
194
|
+
report.verification = verification;
|
|
195
|
+
if (outputJson) {
|
|
196
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
197
|
+
return verification.ok ? 0 : 1;
|
|
198
|
+
}
|
|
199
|
+
writeCredentialStatus(report, io, { mode });
|
|
200
|
+
writeLine(io.stdout, `Remote token verification: ${verification.ok ? 'ok' : 'failed'} (${verification.detail})`);
|
|
201
|
+
return verification.ok ? 0 : 1;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (outputJson) {
|
|
205
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
206
|
+
} else {
|
|
207
|
+
writeCredentialStatus(report, io, { mode });
|
|
208
|
+
}
|
|
209
|
+
return report.loggedIn ? 0 : 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function writeCredentialStatus(report, io, { mode }) {
|
|
213
|
+
if (mode === 'auth') {
|
|
214
|
+
writeLine(io.stdout, `${PRODUCT_NAME} auth status`);
|
|
215
|
+
writeLine(io.stdout, `Logged in: ${report.loggedIn ? 'yes' : 'no'}`);
|
|
216
|
+
writeLine(io.stdout, `Credential source: ${report.tokenSource}`);
|
|
217
|
+
if (report.account) {
|
|
218
|
+
writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);
|
|
219
|
+
}
|
|
220
|
+
writeLine(io.stdout, report.loggedIn ? 'Credential is ready; token value remains hidden.' : `Run \`${COMMAND_NAME} login\` to sign in.`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
writeLine(io.stdout, `Environment token: ${report.environmentToken.present ? 'present' : 'missing'} (${report.environmentToken.variable})`);
|
|
224
|
+
writeLine(io.stdout, `User credential file: ${report.userCredentialFile.present ? 'present' : 'missing'} (${report.userCredentialFile.path})`);
|
|
225
|
+
if (report.account) {
|
|
226
|
+
writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);
|
|
227
|
+
}
|
|
228
|
+
writeLine(io.stdout, report.loggedIn ? 'Credential is ready; token value remains hidden.' : `Run \`${COMMAND_NAME} login\` to sign in.`);
|
|
229
|
+
}
|
|
230
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
booleanValue,
|
|
3
|
+
hasFlag,
|
|
4
|
+
optionValue,
|
|
5
|
+
parsePositiveInteger,
|
|
6
|
+
sameMajorMinor,
|
|
7
|
+
stringValue
|
|
8
|
+
} from '../core/args.js';
|
|
9
|
+
import { baseUrlOption } from '../network/base-url.js';
|
|
10
|
+
import {
|
|
11
|
+
CLI_VERSION,
|
|
12
|
+
COMMAND_NAME,
|
|
13
|
+
PACKAGE_NAME,
|
|
14
|
+
PRODUCT_NAME
|
|
15
|
+
} from '../core/constants.js';
|
|
16
|
+
import { UsageError } from '../core/errors.js';
|
|
17
|
+
import {
|
|
18
|
+
agentDiscoveryClientIds,
|
|
19
|
+
bestEffortRootVersion,
|
|
20
|
+
discoveryMcpUrl,
|
|
21
|
+
ensureDiscoveryService
|
|
22
|
+
} from '../network/discovery.js';
|
|
23
|
+
import {
|
|
24
|
+
endpointUrl,
|
|
25
|
+
fetchJson,
|
|
26
|
+
normalizeBaseUrl,
|
|
27
|
+
probe
|
|
28
|
+
} from '../network/http.js';
|
|
29
|
+
import { writeLine } from '../core/io.js';
|
|
30
|
+
import { codexSmokeReport } from '../mcp/formats/toml.js';
|
|
31
|
+
import { defaultCodexConfigPath } from '../config/paths.js';
|
|
32
|
+
|
|
33
|
+
export async function doctorCommand(args, io) {
|
|
34
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
35
|
+
const outputJson = hasFlag(args, '--json');
|
|
36
|
+
const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
|
|
37
|
+
const nodeVersion = io.nodeVersion ?? process.versions.node;
|
|
38
|
+
const discoveryUrl = endpointUrl(baseUrl, '/.well-known/agent-discovery.json');
|
|
39
|
+
const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
|
|
40
|
+
ensureDiscoveryService(discovery, discoveryUrl);
|
|
41
|
+
|
|
42
|
+
const rootVersion = await bestEffortRootVersion(discovery, timeoutMs, io);
|
|
43
|
+
const mcpUrl = discoveryMcpUrl(discovery, baseUrl);
|
|
44
|
+
const checks = [
|
|
45
|
+
{ name: 'node_version', ok: Number.parseInt(nodeVersion.split('.')[0], 10) >= 20, detail: nodeVersion },
|
|
46
|
+
{ name: 'discovery_reachable', ok: true, detail: discoveryUrl },
|
|
47
|
+
{ name: 'mcp_url_present', ok: Boolean(mcpUrl), detail: mcpUrl ?? 'missing' },
|
|
48
|
+
{ name: 'no_remote_code_execution', ok: booleanValue(discovery, ['security', 'no_remote_code_execution']) === true, detail: String(booleanValue(discovery, ['security', 'no_remote_code_execution'])) },
|
|
49
|
+
{
|
|
50
|
+
name: 'token_not_in_discovery',
|
|
51
|
+
ok: booleanValue(discovery, ['security', 'token_in_discovery']) === false && booleanValue(discovery, ['auth', 'token_in_discovery']) === false,
|
|
52
|
+
detail: `security=${booleanValue(discovery, ['security', 'token_in_discovery'])} auth=${booleanValue(discovery, ['auth', 'token_in_discovery'])}`
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'service_version_compatible',
|
|
56
|
+
ok: rootVersion.version ? sameMajorMinor(CLI_VERSION, rootVersion.version) : true,
|
|
57
|
+
detail: rootVersion.version ? `service=${rootVersion.version} cli=${CLI_VERSION}` : `service version unavailable${rootVersion.error ? `: ${rootVersion.error}` : ''}`
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
const report = {
|
|
61
|
+
ok: checks.every((check) => check.ok),
|
|
62
|
+
cli: { package: PACKAGE_NAME, version: CLI_VERSION, node: nodeVersion },
|
|
63
|
+
discovery: {
|
|
64
|
+
url: discoveryUrl,
|
|
65
|
+
schemaVersion: stringValue(discovery, ['schema_version']),
|
|
66
|
+
protocol: stringValue(discovery, ['protocol']),
|
|
67
|
+
service: stringValue(discovery, ['service']),
|
|
68
|
+
serviceVersion: rootVersion.version ?? null,
|
|
69
|
+
mcpUrl,
|
|
70
|
+
supportedClients: agentDiscoveryClientIds(discovery)
|
|
71
|
+
},
|
|
72
|
+
checks
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (outputJson) {
|
|
76
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
77
|
+
return report.ok ? 0 : 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
writeLine(io.stdout, `${PRODUCT_NAME} CLI ${CLI_VERSION}`);
|
|
81
|
+
writeLine(io.stdout, `Discovery: ${discoveryUrl}`);
|
|
82
|
+
writeLine(io.stdout, `MCP: ${mcpUrl ?? 'missing'}`);
|
|
83
|
+
if (rootVersion.version) {
|
|
84
|
+
writeLine(io.stdout, `Service version: ${rootVersion.version}`);
|
|
85
|
+
}
|
|
86
|
+
writeLine(io.stdout, `Supported clients: ${report.discovery.supportedClients.join(', ') || 'unknown'}`);
|
|
87
|
+
for (const check of checks) {
|
|
88
|
+
writeLine(io.stdout, `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.detail}`);
|
|
89
|
+
}
|
|
90
|
+
return report.ok ? 0 : 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function discoveryCommand(args, io) {
|
|
94
|
+
const subcommand = args[0] ?? 'help';
|
|
95
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
96
|
+
writeLine(io.stdout, 'Discovery commands:');
|
|
97
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} discovery show [--base-url <https://api.example.com>] [--json]`);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
if (subcommand !== 'show') {
|
|
101
|
+
throw new UsageError(`Unknown discovery command: ${subcommand}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args.slice(1), io.env));
|
|
105
|
+
const outputJson = hasFlag(args, '--json');
|
|
106
|
+
const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
|
|
107
|
+
const discoveryUrl = endpointUrl(baseUrl, '/.well-known/agent-discovery.json');
|
|
108
|
+
const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
|
|
109
|
+
ensureDiscoveryService(discovery, discoveryUrl);
|
|
110
|
+
|
|
111
|
+
if (outputJson) {
|
|
112
|
+
writeLine(io.stdout, JSON.stringify(discovery, null, 2));
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
writeLine(io.stdout, `${stringValue(discovery, ['name']) ?? PRODUCT_NAME} discovery`);
|
|
117
|
+
writeLine(io.stdout, `URL: ${discoveryUrl}`);
|
|
118
|
+
writeLine(io.stdout, `Protocol: ${stringValue(discovery, ['protocol']) ?? 'unknown'}`);
|
|
119
|
+
writeLine(io.stdout, `MCP: ${discoveryMcpUrl(discovery, baseUrl) ?? 'missing'}`);
|
|
120
|
+
writeLine(io.stdout, `Docs: ${stringValue(discovery, ['urls', 'docs']) ?? 'unknown'}`);
|
|
121
|
+
writeLine(io.stdout, `Clients: ${agentDiscoveryClientIds(discovery).join(', ') || 'unknown'}`);
|
|
122
|
+
writeLine(io.stdout, 'Security: read-only discovery; tokens are not returned; remote code execution is not advertised.');
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function statusCommand(args, io) {
|
|
127
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
128
|
+
const outputJson = hasFlag(args, '--json');
|
|
129
|
+
const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
|
|
130
|
+
const endpoints = [
|
|
131
|
+
endpointUrl(baseUrl, '/.well-known/memory-os.json'),
|
|
132
|
+
endpointUrl(baseUrl, '/health'),
|
|
133
|
+
endpointUrl(baseUrl, '/ready')
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const probes = [];
|
|
137
|
+
for (const url of endpoints) {
|
|
138
|
+
probes.push(await probe(url, timeoutMs, io));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result = {
|
|
142
|
+
ok: probes.some((item) => item.ok),
|
|
143
|
+
baseUrl,
|
|
144
|
+
privacy: {
|
|
145
|
+
telemetry: false,
|
|
146
|
+
tokenSent: false,
|
|
147
|
+
tokenSource: 'not-used-by-status'
|
|
148
|
+
},
|
|
149
|
+
probes
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (outputJson) {
|
|
153
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
154
|
+
return result.ok ? 0 : 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
writeLine(io.stdout, `${PRODUCT_NAME} status for ${baseUrl}`);
|
|
158
|
+
writeLine(io.stdout, 'Privacy: telemetry disabled; no token sent.');
|
|
159
|
+
for (const item of probes) {
|
|
160
|
+
if (item.ok) {
|
|
161
|
+
writeLine(io.stdout, ` OK ${item.status} ${item.url}`);
|
|
162
|
+
} else {
|
|
163
|
+
writeLine(io.stdout, ` FAIL ${item.status ?? 'ERR'} ${item.url} ${item.error ?? ''}`.trimEnd());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result.ok ? 0 : 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function smokeCommand(args, io) {
|
|
171
|
+
const clientId = optionValue(args, '--client');
|
|
172
|
+
const outputJson = hasFlag(args, '--json');
|
|
173
|
+
if (!clientId) {
|
|
174
|
+
throw new UsageError('Smoke requires --client codex for this MCP-depth release.');
|
|
175
|
+
}
|
|
176
|
+
if (clientId !== 'codex') {
|
|
177
|
+
throw new UsageError('Only Codex smoke checks are available in this MCP-depth release.');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const configPath = optionValue(args, '--config') ?? defaultCodexConfigPath(io.env);
|
|
181
|
+
const report = await codexSmokeReport(configPath, io.env);
|
|
182
|
+
|
|
183
|
+
if (outputJson) {
|
|
184
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
185
|
+
return report.ok ? 0 : 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
writeLine(io.stdout, `${PRODUCT_NAME} Codex MCP smoke: ${report.ok ? 'ok' : 'failed'}`);
|
|
189
|
+
writeLine(io.stdout, `Config: ${report.configPath}`);
|
|
190
|
+
writeLine(io.stdout, `Token env: ${report.tokenEnvVar}`);
|
|
191
|
+
for (const check of report.checks) {
|
|
192
|
+
const status = check.ok ? 'OK' : check.required ? 'FAIL' : 'WARN';
|
|
193
|
+
writeLine(io.stdout, ` ${status} ${check.name}: ${check.detail}`);
|
|
194
|
+
}
|
|
195
|
+
return report.ok ? 0 : 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { hasFlag, optionValue, parsePositiveInteger } from '../core/args.js';
|
|
2
|
+
import { baseUrlOption } from '../network/base-url.js';
|
|
3
|
+
import {
|
|
4
|
+
AGENT_INSTANCE_ENV_VAR,
|
|
5
|
+
COMMAND_NAME,
|
|
6
|
+
DEFAULT_PROXY_HOST,
|
|
7
|
+
DEFAULT_PROXY_PORT,
|
|
8
|
+
MCP_SERVER_NAME,
|
|
9
|
+
PRODUCT_NAME,
|
|
10
|
+
TOKEN_ENV_VAR
|
|
11
|
+
} from '../core/constants.js';
|
|
12
|
+
import { UsageError } from '../core/errors.js';
|
|
13
|
+
import { endpointUrl, normalizeBaseUrl } from '../network/http.js';
|
|
14
|
+
import { writeLine } from '../core/io.js';
|
|
15
|
+
import {
|
|
16
|
+
MCP_CLIENTS,
|
|
17
|
+
supportedMcpClientIds,
|
|
18
|
+
supportedMcpClients
|
|
19
|
+
} from '../mcp/clients.js';
|
|
20
|
+
import { mcpProxyCommand } from '../mcp/proxy/copilot.js';
|
|
21
|
+
import {
|
|
22
|
+
agentIdentity,
|
|
23
|
+
envReferenceIdentity
|
|
24
|
+
} from '../mcp/identity/device.js';
|
|
25
|
+
import {
|
|
26
|
+
agentInstanceGenerationPolicy,
|
|
27
|
+
mcpConfigTemplate,
|
|
28
|
+
mcpLocalProxyTemplate
|
|
29
|
+
} from '../mcp/core/templates.js';
|
|
30
|
+
import { usesClientOAuth } from '../mcp/clients/registry.js';
|
|
31
|
+
import {
|
|
32
|
+
codexMemoryProfile,
|
|
33
|
+
writeCodexMemoryProfile
|
|
34
|
+
} from '../config/profile.js';
|
|
35
|
+
|
|
36
|
+
export async function mcpCommand(args, io) {
|
|
37
|
+
const subcommand = args[0] ?? 'help';
|
|
38
|
+
|
|
39
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
40
|
+
writeLine(io.stdout, 'MCP commands:');
|
|
41
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
|
|
42
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|antigravity|generic> [--base-url <url>] [--json]`);
|
|
43
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}] [--base-url <url>]`);
|
|
44
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp profile codex [--json]`);
|
|
45
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>]`);
|
|
46
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>] --write [--config <path>]`);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (subcommand === 'list') {
|
|
51
|
+
if (hasFlag(args, '--json')) {
|
|
52
|
+
writeLine(io.stdout, JSON.stringify(supportedMcpClients(), null, 2));
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
writeLine(io.stdout, 'Supported MCP clients:');
|
|
57
|
+
for (const client of supportedMcpClients()) {
|
|
58
|
+
writeLine(io.stdout, ` ${client.id.padEnd(8)} ${client.label} (${client.configKind})`);
|
|
59
|
+
}
|
|
60
|
+
writeLine(io.stdout, `Generated configs never embed token values; OAuth clients do not require ${TOKEN_ENV_VAR} in their config.`);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (subcommand === 'config') {
|
|
65
|
+
const clientId = optionValue(args, '--client') ?? args[1] ?? 'generic';
|
|
66
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
67
|
+
const mcpUrl = endpointUrl(baseUrl, '/mcp');
|
|
68
|
+
const useLocalProxy = clientId === 'copilot-cli' && !hasFlag(args, '--remote-env');
|
|
69
|
+
const proxyPort = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
|
|
70
|
+
const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`;
|
|
71
|
+
const templateOptions = { mcpClients: MCP_CLIENTS };
|
|
72
|
+
const template = useLocalProxy
|
|
73
|
+
? mcpLocalProxyTemplate(clientId, proxyUrl, templateOptions)
|
|
74
|
+
: mcpConfigTemplate(clientId, mcpUrl, templateOptions);
|
|
75
|
+
|
|
76
|
+
if (hasFlag(args, '--json')) {
|
|
77
|
+
writeLine(io.stdout, JSON.stringify(template, null, 2));
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
writeLine(io.stdout, `${PRODUCT_NAME} MCP config template for ${clientId}`);
|
|
82
|
+
if (useLocalProxy) {
|
|
83
|
+
writeLine(io.stdout, `Requires credential: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin`);
|
|
84
|
+
writeLine(io.stdout, `Run local proxy: ${template.requiresLocalCommand}`);
|
|
85
|
+
} else {
|
|
86
|
+
if (template.requiresEnv?.length > 0) {
|
|
87
|
+
writeLine(io.stdout, `Requires env: ${template.requiresEnv.join(', ')}`);
|
|
88
|
+
} else if (template.authentication === 'oauth') {
|
|
89
|
+
writeLine(io.stdout, 'Requires auth: complete the client MCP OAuth flow after setup.');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (typeof template.snippet === 'string') {
|
|
93
|
+
writeLine(io.stdout, template.snippet.trimEnd());
|
|
94
|
+
} else {
|
|
95
|
+
writeLine(io.stdout, JSON.stringify(template.snippet, null, 2));
|
|
96
|
+
}
|
|
97
|
+
if (template.optionalEnv?.includes(AGENT_INSTANCE_ENV_VAR)) {
|
|
98
|
+
writeLine(io.stdout, '');
|
|
99
|
+
writeLine(io.stdout, `${AGENT_INSTANCE_ENV_VAR} must be stable per local client install.`);
|
|
100
|
+
if (template.agentInstanceGeneration?.automaticCommand) {
|
|
101
|
+
writeLine(io.stdout, `Use ${template.agentInstanceGeneration.automaticCommand} to generate and persist it, or set it to a unique value such as xmemo-${clientId}-<uuid>.`);
|
|
102
|
+
} else {
|
|
103
|
+
writeLine(io.stdout, `Set it to a unique value such as xmemo-${clientId}-<uuid> and persist it outside git.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
writeLine(io.stdout, 'Review the template before applying it. Token values are not included.');
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (subcommand === 'proxy') {
|
|
111
|
+
return await mcpProxyCommand(args.slice(1), io, { agentIdentity });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (subcommand === 'profile') {
|
|
115
|
+
const clientId = args[1] ?? 'codex';
|
|
116
|
+
if (clientId !== 'codex') {
|
|
117
|
+
throw new UsageError('Only the Codex memory behavior profile is available in this MCP-depth release.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const profile = codexMemoryProfile();
|
|
121
|
+
if (hasFlag(args, '--json')) {
|
|
122
|
+
writeLine(io.stdout, JSON.stringify(profile, null, 2));
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
writeCodexMemoryProfile(profile, io);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const target = args[1] ?? '';
|
|
131
|
+
const client = MCP_CLIENTS.get(target);
|
|
132
|
+
|
|
133
|
+
if (subcommand !== 'add' || !client) {
|
|
134
|
+
throw new UsageError(`Supported MCP setup command: ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <url>]`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
138
|
+
const configPath = optionValue(args, '--config') ?? client.defaultConfigPath(io.env);
|
|
139
|
+
const mcpUrl = endpointUrl(baseUrl, '/mcp');
|
|
140
|
+
|
|
141
|
+
if (hasFlag(args, '--json')) {
|
|
142
|
+
const identity = envReferenceIdentity(target);
|
|
143
|
+
const oauthClient = usesClientOAuth(target);
|
|
144
|
+
writeLine(io.stdout, JSON.stringify({
|
|
145
|
+
client: target,
|
|
146
|
+
label: client.label,
|
|
147
|
+
configKind: client.configKind,
|
|
148
|
+
configPath,
|
|
149
|
+
serverName: MCP_SERVER_NAME,
|
|
150
|
+
url: mcpUrl,
|
|
151
|
+
tokenEnvVar: oauthClient ? null : TOKEN_ENV_VAR,
|
|
152
|
+
authentication: oauthClient ? 'oauth' : 'env-bearer',
|
|
153
|
+
agentId: identity.agentId,
|
|
154
|
+
agentInstanceId: identity.agentInstanceId,
|
|
155
|
+
agentInstanceIdPath: identity.path,
|
|
156
|
+
agentInstanceGeneration: agentInstanceGenerationPolicy(target, { mcpClients: MCP_CLIENTS }),
|
|
157
|
+
writesTokenValue: false
|
|
158
|
+
}, null, 2));
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const identity = hasFlag(args, '--write') ? await agentIdentity(target, io.env) : envReferenceIdentity(target);
|
|
163
|
+
if (hasFlag(args, '--write')) {
|
|
164
|
+
await client.writeConfig(configPath, mcpUrl, identity);
|
|
165
|
+
writeLine(io.stdout, `Updated ${client.label} MCP config: ${configPath}`);
|
|
166
|
+
if (usesClientOAuth(target)) {
|
|
167
|
+
writeLine(io.stdout, `Token value was not written. ${client.label} will complete MCP OAuth on first use.`);
|
|
168
|
+
} else {
|
|
169
|
+
writeLine(io.stdout, `Token value was not written. ${client.label} will read ${TOKEN_ENV_VAR} from the environment.`);
|
|
170
|
+
}
|
|
171
|
+
writeLine(io.stdout, `Agent instance ID stored outside git: ${identity.path}`);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const snippet = client.buildSnippet(mcpUrl, identity);
|
|
176
|
+
writeLine(io.stdout, `Add this to your ${client.label} config (${configPath}):`);
|
|
177
|
+
writeLine(io.stdout, '');
|
|
178
|
+
writeLine(io.stdout, snippet.trimEnd());
|
|
179
|
+
writeLine(io.stdout, '');
|
|
180
|
+
if (usesClientOAuth(target)) {
|
|
181
|
+
writeLine(io.stdout, `Restart ${client.label} and complete its MCP OAuth flow. No token value is included here.`);
|
|
182
|
+
} else {
|
|
183
|
+
writeLine(io.stdout, `Set ${TOKEN_ENV_VAR} in your user environment or secret manager. The token value is not included here.`);
|
|
184
|
+
}
|
|
185
|
+
writeLine(io.stdout, `${AGENT_INSTANCE_ENV_VAR} must be stable per local ${client.label} install; run ${COMMAND_NAME} mcp add ${target} --write to generate it automatically.`);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { hasFlag, optionValue } from '../core/args.js';
|
|
2
|
+
import { COMMAND_NAME } from '../core/constants.js';
|
|
3
|
+
import { UsageError } from '../core/errors.js';
|
|
4
|
+
import { writeLine } from '../core/io.js';
|
|
5
|
+
import { MCP_CLIENTS } from '../mcp/clients.js';
|
|
6
|
+
import {
|
|
7
|
+
defaultProfileTarget,
|
|
8
|
+
profileClientConfig,
|
|
9
|
+
profileInstallResult,
|
|
10
|
+
profileStatusResult,
|
|
11
|
+
profileUninstallResult,
|
|
12
|
+
supportedProfileClientIds,
|
|
13
|
+
writeProfileResult
|
|
14
|
+
} from '../config/profile.js';
|
|
15
|
+
import { normalizeSetupClientId } from '../ui/setup.js';
|
|
16
|
+
|
|
17
|
+
export async function profileCommand(args, io) {
|
|
18
|
+
const subcommand = args[0] ?? 'help';
|
|
19
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
20
|
+
writeLine(io.stdout, 'Profile commands:');
|
|
21
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} profile install <codex|cursor|gemini|antigravity|qwen|opencode> [--target <path>] [--dry-run|--json]`);
|
|
22
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} profile status <codex|cursor|gemini|antigravity|qwen|opencode> [--target <path>] [--json]`);
|
|
23
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall <codex|cursor|gemini|antigravity|qwen|opencode> [--target <path>] [--json]`);
|
|
24
|
+
writeLine(io.stdout, '');
|
|
25
|
+
writeLine(io.stdout, 'Profile installs are marker-scoped and never write token values.');
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const clientId = normalizeSetupClientId(args[1], MCP_CLIENTS);
|
|
30
|
+
if (!profileClientConfig(clientId)) {
|
|
31
|
+
throw new UsageError(`Unsupported profile client: ${args[1] ?? 'missing'}. Supported clients: ${supportedProfileClientIds().join(', ')}.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const optionArgs = args.slice(2);
|
|
35
|
+
const outputJson = hasFlag(optionArgs, '--json');
|
|
36
|
+
const targetPath = optionValue(optionArgs, '--target') ?? defaultProfileTarget(clientId, io.env);
|
|
37
|
+
let result;
|
|
38
|
+
|
|
39
|
+
if (subcommand === 'install') {
|
|
40
|
+
result = await profileInstallResult(clientId, targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
|
|
41
|
+
} else if (subcommand === 'status') {
|
|
42
|
+
result = await profileStatusResult(clientId, targetPath);
|
|
43
|
+
} else if (subcommand === 'uninstall') {
|
|
44
|
+
result = await profileUninstallResult(clientId, targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
|
|
45
|
+
} else {
|
|
46
|
+
throw new UsageError(`Unknown profile command: ${subcommand}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (outputJson) {
|
|
50
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
writeProfileResult(subcommand, result, io);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|