@aident-ai/cli 0.1.0 → 0.1.2
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 +10 -5
- package/dist/cli.mjs +1429 -104
- package/package.json +4 -3
package/dist/cli.mjs
CHANGED
|
@@ -26,10 +26,20 @@ function logErr(text) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// src/prompt.ts
|
|
29
|
-
function readLine(prompt) {
|
|
30
|
-
return new Promise((resolve) => {
|
|
29
|
+
function readLine(prompt, options = {}) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
31
|
process.stdout.write(prompt);
|
|
32
32
|
let data = "";
|
|
33
|
+
let timeout;
|
|
34
|
+
let active = true;
|
|
35
|
+
const cleanup = () => {
|
|
36
|
+
active = false;
|
|
37
|
+
process.stdin.removeListener("data", onData);
|
|
38
|
+
process.stdin.pause();
|
|
39
|
+
if (timeout)
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
};
|
|
42
|
+
const timeoutMs = options.timeoutMs;
|
|
33
43
|
const onData = (chunk) => {
|
|
34
44
|
const s = chunk.toString("utf-8");
|
|
35
45
|
const newlineIdx = s.indexOf(`
|
|
@@ -39,10 +49,15 @@ function readLine(prompt) {
|
|
|
39
49
|
return;
|
|
40
50
|
}
|
|
41
51
|
data += s.slice(0, newlineIdx);
|
|
42
|
-
|
|
43
|
-
process.stdin.pause();
|
|
52
|
+
cleanup();
|
|
44
53
|
resolve(data.replace(/\r$/, ""));
|
|
45
54
|
};
|
|
55
|
+
if (timeoutMs) {
|
|
56
|
+
timeout = setTimeout(() => {
|
|
57
|
+
cleanup();
|
|
58
|
+
reject(new Error(options.timeoutMessage ?? `No input received within ${timeoutMs / 1000}s.`));
|
|
59
|
+
}, timeoutMs);
|
|
60
|
+
}
|
|
46
61
|
process.stdin.resume();
|
|
47
62
|
process.stdin.on("data", onData);
|
|
48
63
|
});
|
|
@@ -50,6 +65,10 @@ function readLine(prompt) {
|
|
|
50
65
|
|
|
51
66
|
// src/auth.ts
|
|
52
67
|
var CLIENT_NAME = "aident-cli";
|
|
68
|
+
var LOOPBACK_HOST = "127.0.0.1";
|
|
69
|
+
var LOGIN_OPEN_TIMEOUT_MS = 90 * 1000;
|
|
70
|
+
var LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS = 5 * 60 * 1000;
|
|
71
|
+
var LOGIN_OPENED_MESSAGE = `User opened the Aident login window. They have ${LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS / 60000} minutes to finish; if it fails, run \`aident login --oob\`, share the URL, and ask for the verification code.`;
|
|
53
72
|
async function login(options) {
|
|
54
73
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
55
74
|
if (options.oob)
|
|
@@ -97,24 +116,28 @@ async function loginLoopback(baseUrl) {
|
|
|
97
116
|
const verifier = base64UrlEncode(randomBytes(48));
|
|
98
117
|
const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
|
|
99
118
|
const state = base64UrlEncode(randomBytes(16));
|
|
100
|
-
const { server, port, codePromise } = await startCallbackServer(state);
|
|
101
|
-
const redirectUri = `http
|
|
119
|
+
const { server, port, codePromise, setExpectedOpenRedirectUrl } = await startCallbackServer(state);
|
|
120
|
+
const redirectUri = `http://${LOOPBACK_HOST}:${port}/callback`;
|
|
102
121
|
const clientId = await registerClient(baseUrl, [redirectUri]);
|
|
103
122
|
const authorizeUrl = new URL(`${baseUrl}/api/mcp/oauth/authorize`);
|
|
104
123
|
authorizeUrl.searchParams.set("response_type", "code");
|
|
105
124
|
authorizeUrl.searchParams.set("client_id", clientId);
|
|
106
125
|
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
126
|
+
authorizeUrl.searchParams.set("redirect_uri_exact", base64UrlEncode(Buffer.from(redirectUri, "utf8")));
|
|
107
127
|
authorizeUrl.searchParams.set("code_challenge", challenge);
|
|
108
128
|
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
109
129
|
authorizeUrl.searchParams.set("state", state);
|
|
130
|
+
const authorizeUrlString = authorizeUrl.toString();
|
|
131
|
+
setExpectedOpenRedirectUrl(authorizeUrlString);
|
|
132
|
+
const loginOpenUrl = buildLoginOpenUrl(port, state, authorizeUrlString);
|
|
110
133
|
logInfo(`Opening browser for Aident login...`);
|
|
111
|
-
logInfo(`If the browser does not open, visit: ${
|
|
112
|
-
openBrowser(
|
|
134
|
+
logInfo(`If the browser does not open, visit: ${authorizeUrlString}`);
|
|
135
|
+
openBrowser(shouldUseLoopbackBrowserHandoff() ? loginOpenUrl : authorizeUrlString);
|
|
113
136
|
let code;
|
|
114
137
|
try {
|
|
115
138
|
code = await codePromise;
|
|
116
139
|
} finally {
|
|
117
|
-
server
|
|
140
|
+
closeServer(server);
|
|
118
141
|
}
|
|
119
142
|
const tok = await exchangeCode(baseUrl, clientId, code, redirectUri, verifier);
|
|
120
143
|
return buildCreds(baseUrl, clientId, tok);
|
|
@@ -124,36 +147,54 @@ async function loginOob(baseUrl) {
|
|
|
124
147
|
const verifier = base64UrlEncode(randomBytes(48));
|
|
125
148
|
const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
|
|
126
149
|
const state = base64UrlEncode(randomBytes(16));
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
authorizeUrl.searchParams.set("response_type", "code");
|
|
130
|
-
authorizeUrl.searchParams.set("client_id", clientId);
|
|
131
|
-
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
132
|
-
authorizeUrl.searchParams.set("code_challenge", challenge);
|
|
133
|
-
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
134
|
-
authorizeUrl.searchParams.set("state", state);
|
|
135
|
-
logInfo(`Opening browser for Aident login...`);
|
|
136
|
-
logInfo(`If the browser does not open, visit: ${authorizeUrl.toString()}`);
|
|
137
|
-
openBrowser(authorizeUrl.toString());
|
|
138
|
-
logInfo("After approving, paste the value shown on the Aident page below.");
|
|
139
|
-
logInfo("(It will be either an authorization code — preferred — or a raw access token.)");
|
|
140
|
-
const pasted = (await readLine("Paste here: ")).trim();
|
|
141
|
-
if (!pasted)
|
|
142
|
-
throw new Error("No token provided");
|
|
150
|
+
let clientId = "";
|
|
151
|
+
let valueToExchange = "";
|
|
143
152
|
try {
|
|
144
|
-
|
|
153
|
+
clientId = await registerClient(baseUrl, [redirectUri]);
|
|
154
|
+
const authorizeUrl = new URL(`${baseUrl}/api/mcp/oauth/authorize`);
|
|
155
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
156
|
+
authorizeUrl.searchParams.set("client_id", clientId);
|
|
157
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
158
|
+
authorizeUrl.searchParams.set("code_challenge", challenge);
|
|
159
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
160
|
+
authorizeUrl.searchParams.set("state", state);
|
|
161
|
+
const authorizeUrlString = authorizeUrl.toString();
|
|
162
|
+
logInfo(`Opening browser for Aident login...`);
|
|
163
|
+
logInfo(`If the browser does not open, visit: ${authorizeUrlString}`);
|
|
164
|
+
openBrowser(authorizeUrlString);
|
|
165
|
+
logInfo("After approving, paste the 8-digit verification code shown on the Aident page below.");
|
|
166
|
+
const pastedInput = await readLine("Paste 8-digit code here: ", {
|
|
167
|
+
timeoutMs: LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS,
|
|
168
|
+
timeoutMessage: `Verification code not received within ${LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS / 1000}s; aborting.`
|
|
169
|
+
});
|
|
170
|
+
valueToExchange = normalizeOobLoginInput(pastedInput);
|
|
171
|
+
if (!valueToExchange)
|
|
172
|
+
throw new Error("No verification code provided");
|
|
173
|
+
if (!/^\d{8}$/.test(valueToExchange) && !looksLikeAccessToken(valueToExchange)) {
|
|
174
|
+
throw new Error("Pasted value is not a valid 8-digit verification code. Please retry the login.");
|
|
175
|
+
}
|
|
176
|
+
const tok = await exchangeCode(baseUrl, clientId, valueToExchange, redirectUri, verifier);
|
|
145
177
|
return buildCreds(baseUrl, clientId, tok);
|
|
146
178
|
} catch (err) {
|
|
147
179
|
if (err instanceof OAuthRejectedError) {
|
|
148
|
-
if (!looksLikeAccessToken(
|
|
149
|
-
throw new Error("Pasted
|
|
180
|
+
if (!looksLikeAccessToken(valueToExchange)) {
|
|
181
|
+
throw new Error("Pasted verification code was rejected by the server. Please retry the login.");
|
|
150
182
|
}
|
|
151
183
|
logErr(`Server rejected the pasted value as an authorization code. Treating it as a raw access token. Note: token refresh is unavailable in this mode — when the token expires you'll need to run \`aident login\` again.`);
|
|
152
|
-
return { base_url: baseUrl, client_id: clientId, access_token:
|
|
184
|
+
return { base_url: baseUrl, client_id: clientId, access_token: valueToExchange };
|
|
153
185
|
}
|
|
154
186
|
throw err;
|
|
155
187
|
}
|
|
156
188
|
}
|
|
189
|
+
function normalizeVerificationCodeInput(value) {
|
|
190
|
+
return value.trim().replace(/[\s-]+/g, "");
|
|
191
|
+
}
|
|
192
|
+
function normalizeOobLoginInput(value) {
|
|
193
|
+
const verificationCode = normalizeVerificationCodeInput(value);
|
|
194
|
+
if (/^\d{8}$/.test(verificationCode))
|
|
195
|
+
return verificationCode;
|
|
196
|
+
return value.trim().replace(/\s+/g, "");
|
|
197
|
+
}
|
|
157
198
|
function looksLikeAccessToken(value) {
|
|
158
199
|
if (value.length < 20)
|
|
159
200
|
return false;
|
|
@@ -232,7 +273,6 @@ function updateCredsFromToken(creds, tok) {
|
|
|
232
273
|
expires_at: expiresAt
|
|
233
274
|
};
|
|
234
275
|
}
|
|
235
|
-
var LOOPBACK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
236
276
|
function startCallbackServer(expectedState) {
|
|
237
277
|
return new Promise((resolve, reject) => {
|
|
238
278
|
let codeResolver = () => {
|
|
@@ -241,52 +281,155 @@ function startCallbackServer(expectedState) {
|
|
|
241
281
|
let codeRejecter = () => {
|
|
242
282
|
return;
|
|
243
283
|
};
|
|
284
|
+
let opened = false;
|
|
285
|
+
let expectedOpenRedirectUrl = "";
|
|
286
|
+
let completeTimeout;
|
|
244
287
|
const codePromise = new Promise((res, rej) => {
|
|
245
288
|
codeResolver = res;
|
|
246
289
|
codeRejecter = rej;
|
|
247
290
|
});
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
const url = new URL(req.url, `http://localhost`);
|
|
259
|
-
if (url.pathname !== "/callback") {
|
|
260
|
-
res.writeHead(404);
|
|
261
|
-
res.end("Not found");
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const code = url.searchParams.get("code");
|
|
265
|
-
const state = url.searchParams.get("state");
|
|
266
|
-
const error = url.searchParams.get("error");
|
|
267
|
-
if (error) {
|
|
268
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
269
|
-
res.end(`<html><body><h2>Authentication error</h2><p>${escapeHtml(error)}</p></body></html>`);
|
|
270
|
-
codeRejecter(new Error(`OAuth error: ${error}`));
|
|
271
|
-
return;
|
|
291
|
+
const scheduleCompleteTimeout = (timeoutMs, message) => {
|
|
292
|
+
if (completeTimeout)
|
|
293
|
+
clearTimeout(completeTimeout);
|
|
294
|
+
completeTimeout = setTimeout(() => codeRejecter(new Error(message)), timeoutMs);
|
|
295
|
+
};
|
|
296
|
+
const openTimeout = setTimeout(() => {
|
|
297
|
+
if (!opened) {
|
|
298
|
+
logInfo(`Login window open signal not received within ${LOGIN_OPEN_TIMEOUT_MS / 1000}s; still waiting for login to finish.`);
|
|
272
299
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
300
|
+
}, LOGIN_OPEN_TIMEOUT_MS);
|
|
301
|
+
scheduleCompleteTimeout(LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS, `Authorization callback not received within ${LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS / 1000}s; aborting.`);
|
|
302
|
+
codePromise.then(() => {
|
|
303
|
+
clearTimeout(openTimeout);
|
|
304
|
+
if (completeTimeout)
|
|
305
|
+
clearTimeout(completeTimeout);
|
|
306
|
+
}, () => {
|
|
307
|
+
clearTimeout(openTimeout);
|
|
308
|
+
if (completeTimeout)
|
|
309
|
+
clearTimeout(completeTimeout);
|
|
310
|
+
});
|
|
311
|
+
const markOpened = () => {
|
|
312
|
+
if (opened)
|
|
277
313
|
return;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
314
|
+
opened = true;
|
|
315
|
+
clearTimeout(openTimeout);
|
|
316
|
+
logInfo(LOGIN_OPENED_MESSAGE);
|
|
317
|
+
const timeoutMs = LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS;
|
|
318
|
+
scheduleCompleteTimeout(timeoutMs, `Login page opened but did not complete within ${timeoutMs / 1000}s; aborting.`);
|
|
319
|
+
};
|
|
320
|
+
const server = createServer((req, res) => {
|
|
321
|
+
runLocalCallbackHandler(req, res, () => {
|
|
322
|
+
if (!req.url) {
|
|
323
|
+
res.writeHead(400);
|
|
324
|
+
res.end("Bad request");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const url = new URL(req.url, `http://${LOOPBACK_HOST}`);
|
|
328
|
+
if (handleOpenRedirect(url, res, expectedState, expectedOpenRedirectUrl, markOpened))
|
|
329
|
+
return;
|
|
330
|
+
if (url.pathname !== "/callback") {
|
|
331
|
+
res.writeHead(404);
|
|
332
|
+
res.end("Not found");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const code = url.searchParams.get("code");
|
|
336
|
+
const state = url.searchParams.get("state");
|
|
337
|
+
const error = url.searchParams.get("error");
|
|
338
|
+
if (error) {
|
|
339
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
340
|
+
res.end(`<html><body><h2>Authentication error</h2><p>${escapeHtml(error)}</p></body></html>`);
|
|
341
|
+
codeRejecter(new Error(`OAuth error: ${error}`));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!code || state !== expectedState) {
|
|
345
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
346
|
+
res.end("<html><body><h2>Invalid callback</h2></body></html>");
|
|
347
|
+
codeRejecter(new Error("Invalid callback (missing code or bad state)"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
351
|
+
res.end(`<html><body style="font-family:system-ui;padding:40px;text-align:center"><h2>You're signed in.</h2><p>You can close this window and return to the terminal.</p></body></html>`);
|
|
352
|
+
codeResolver(code);
|
|
353
|
+
}, codeRejecter);
|
|
282
354
|
});
|
|
283
355
|
server.once("error", reject);
|
|
284
|
-
server.listen(0,
|
|
356
|
+
server.listen(0, LOOPBACK_HOST, () => {
|
|
285
357
|
const addr = server.address();
|
|
286
|
-
resolve({
|
|
358
|
+
resolve({
|
|
359
|
+
server,
|
|
360
|
+
port: addr.port,
|
|
361
|
+
codePromise,
|
|
362
|
+
setExpectedOpenRedirectUrl: (url) => {
|
|
363
|
+
expectedOpenRedirectUrl = url;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
287
366
|
});
|
|
288
367
|
});
|
|
289
368
|
}
|
|
369
|
+
function handleOpenRedirect(url, res, expectedState, expectedOpenRedirectUrl, onOpened) {
|
|
370
|
+
if (url.pathname !== "/open")
|
|
371
|
+
return false;
|
|
372
|
+
if (url.searchParams.get("state") !== expectedState) {
|
|
373
|
+
res.writeHead(400);
|
|
374
|
+
res.end("Bad request");
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
const nextUrl = url.searchParams.get("next");
|
|
378
|
+
if (!expectedOpenRedirectUrl || !nextUrl || nextUrl !== expectedOpenRedirectUrl) {
|
|
379
|
+
res.writeHead(400);
|
|
380
|
+
res.end("Bad request");
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
onOpened();
|
|
385
|
+
} catch {
|
|
386
|
+
res.writeHead(500);
|
|
387
|
+
res.end("Internal callback error");
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
res.writeHead(302, { Location: nextUrl });
|
|
391
|
+
res.end();
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
function runLocalCallbackHandler(req, res, handle, onAuthCallbackError) {
|
|
395
|
+
try {
|
|
396
|
+
handle();
|
|
397
|
+
} catch (err) {
|
|
398
|
+
if (!res.writableEnded) {
|
|
399
|
+
if (!res.headersSent)
|
|
400
|
+
res.writeHead(500);
|
|
401
|
+
res.end("Internal callback error");
|
|
402
|
+
}
|
|
403
|
+
if (isAuthCallbackRequest(req.url))
|
|
404
|
+
onAuthCallbackError?.(toError(err));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function isAuthCallbackRequest(url) {
|
|
408
|
+
if (!url)
|
|
409
|
+
return false;
|
|
410
|
+
try {
|
|
411
|
+
return new URL(url, `http://${LOOPBACK_HOST}`).pathname === "/callback";
|
|
412
|
+
} catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function toError(err) {
|
|
417
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
418
|
+
}
|
|
419
|
+
function closeServer(server) {
|
|
420
|
+
server.close();
|
|
421
|
+
server.closeIdleConnections?.();
|
|
422
|
+
server.closeAllConnections?.();
|
|
423
|
+
}
|
|
424
|
+
function buildLoginOpenUrl(port, state, authorizeUrl) {
|
|
425
|
+
const url = new URL(`http://${LOOPBACK_HOST}:${port}/open`);
|
|
426
|
+
url.searchParams.set("state", state);
|
|
427
|
+
url.searchParams.set("next", authorizeUrl);
|
|
428
|
+
return url.toString();
|
|
429
|
+
}
|
|
430
|
+
function shouldUseLoopbackBrowserHandoff(env = process.env) {
|
|
431
|
+
return !env.SSH_CONNECTION && !env.SSH_TTY && !env.CODESPACES && !env.REMOTE_CONTAINERS && !env.DEVCONTAINER;
|
|
432
|
+
}
|
|
290
433
|
function openBrowser(url) {
|
|
291
434
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
292
435
|
const args = process.platform === "win32" ? ["", url] : [url];
|
|
@@ -302,20 +445,22 @@ function escapeHtml(s) {
|
|
|
302
445
|
}
|
|
303
446
|
|
|
304
447
|
// src/version.ts
|
|
305
|
-
var VERSION = "0.1.
|
|
448
|
+
var VERSION = "0.1.2";
|
|
306
449
|
|
|
307
450
|
// src/client.ts
|
|
308
451
|
class CliClient {
|
|
309
452
|
creds;
|
|
310
453
|
credentialSource;
|
|
311
454
|
packages;
|
|
455
|
+
installedSkillVersion;
|
|
312
456
|
commandOperations = new Map;
|
|
313
|
-
constructor(creds, credentialSource = "stored", packages) {
|
|
457
|
+
constructor(creds, credentialSource = "stored", packages, installedSkillVersion = null) {
|
|
314
458
|
this.creds = creds;
|
|
315
459
|
this.credentialSource = credentialSource;
|
|
316
460
|
if (packages.length === 0)
|
|
317
461
|
throw new Error("CliClient packages must not be empty");
|
|
318
462
|
this.packages = [...packages];
|
|
463
|
+
this.installedSkillVersion = installedSkillVersion;
|
|
319
464
|
}
|
|
320
465
|
get baseUrl() {
|
|
321
466
|
return this.creds.base_url.replace(/\/+$/, "");
|
|
@@ -360,8 +505,12 @@ class CliClient {
|
|
|
360
505
|
}
|
|
361
506
|
return this.fetchJson("POST", this.operationPath(operation.packageName, operation.operationId), args);
|
|
362
507
|
}
|
|
508
|
+
async execOperation(packageName, operationId, args) {
|
|
509
|
+
return this.fetchJson("POST", this.operationPath(packageName, operationId), args);
|
|
510
|
+
}
|
|
363
511
|
async fetchOpenApiCatalog(packageName) {
|
|
364
|
-
const
|
|
512
|
+
const headers = packageName === "loadout" && this.installedSkillVersion ? { "x-aident-skill-version": this.installedSkillVersion } : undefined;
|
|
513
|
+
const result = await this.fetchJson("GET", this.catalogPath(packageName), undefined, headers);
|
|
365
514
|
const catalog = result.body["x-aident-command-catalog"];
|
|
366
515
|
if (result.status !== 200 || !catalog) {
|
|
367
516
|
return {
|
|
@@ -369,6 +518,9 @@ class CliClient {
|
|
|
369
518
|
body: invalidResponseFallback(JSON.stringify(result.body))
|
|
370
519
|
};
|
|
371
520
|
}
|
|
521
|
+
if (packageName === "loadout" && result.body["x-aident-loadout-skill"]) {
|
|
522
|
+
catalog.loadoutSkill = result.body["x-aident-loadout-skill"];
|
|
523
|
+
}
|
|
372
524
|
return { status: result.status, body: catalog };
|
|
373
525
|
}
|
|
374
526
|
catalogPath(packageName) {
|
|
@@ -388,10 +540,11 @@ class CliClient {
|
|
|
388
540
|
});
|
|
389
541
|
}
|
|
390
542
|
}
|
|
391
|
-
async fetchJson(method, path, body) {
|
|
543
|
+
async fetchJson(method, path, body, extraHeaders) {
|
|
392
544
|
const headers = {
|
|
393
545
|
Authorization: `Bearer ${this.creds.access_token}`,
|
|
394
|
-
"User-Agent": `@aident-ai/cli/${VERSION}
|
|
546
|
+
"User-Agent": `@aident-ai/cli/${VERSION}`,
|
|
547
|
+
...extraHeaders
|
|
395
548
|
};
|
|
396
549
|
if (body !== undefined)
|
|
397
550
|
headers["Content-Type"] = "application/json";
|
|
@@ -441,10 +594,12 @@ function mergeCatalogs(catalogs, packages, onCommand) {
|
|
|
441
594
|
}
|
|
442
595
|
}
|
|
443
596
|
});
|
|
597
|
+
const loadoutSkill = catalogs.find((catalog) => catalog.loadoutSkill)?.loadoutSkill;
|
|
444
598
|
return {
|
|
445
599
|
packages: [...packageMap.values()],
|
|
446
600
|
domains: [...domainMap.values()],
|
|
447
|
-
commands: [...commandMap.values()]
|
|
601
|
+
commands: [...commandMap.values()],
|
|
602
|
+
...loadoutSkill ? { loadoutSkill } : {}
|
|
448
603
|
};
|
|
449
604
|
}
|
|
450
605
|
|
|
@@ -488,7 +643,7 @@ function getAidentDir() {
|
|
|
488
643
|
function getConfigFile() {
|
|
489
644
|
return join(getAidentDir(), "config.json");
|
|
490
645
|
}
|
|
491
|
-
var DEFAULT_BASE_URL = "https://
|
|
646
|
+
var DEFAULT_BASE_URL = "https://loadout.aident.ai";
|
|
492
647
|
var CONFIG_KEYS = ["baseUrl", "packages"];
|
|
493
648
|
async function readConfig() {
|
|
494
649
|
try {
|
|
@@ -546,6 +701,7 @@ function normalizeBaseUrl(url) {
|
|
|
546
701
|
// src/credentials.ts
|
|
547
702
|
import { mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
548
703
|
import { join as join2 } from "node:path";
|
|
704
|
+
var REFRESH_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
549
705
|
function getCredentialsFile() {
|
|
550
706
|
return join2(getAidentDir(), "credentials.json");
|
|
551
707
|
}
|
|
@@ -573,7 +729,67 @@ function isExpired(creds) {
|
|
|
573
729
|
const expiresAt = Date.parse(creds.expires_at);
|
|
574
730
|
if (Number.isNaN(expiresAt))
|
|
575
731
|
return false;
|
|
576
|
-
return Date.now() >= expiresAt -
|
|
732
|
+
return Date.now() >= expiresAt - REFRESH_WINDOW_MS;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/loadoutSkill.ts
|
|
736
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
737
|
+
import { homedir as homedir2 } from "node:os";
|
|
738
|
+
import { join as join3 } from "node:path";
|
|
739
|
+
async function fetchLoadoutSkillMetadata(baseUrl) {
|
|
740
|
+
const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/.well-known/loadout-skill.json`, {
|
|
741
|
+
method: "GET",
|
|
742
|
+
headers: { "User-Agent": `@aident-ai/cli/${VERSION}` }
|
|
743
|
+
});
|
|
744
|
+
if (res.status !== 200)
|
|
745
|
+
return null;
|
|
746
|
+
const body = await res.json();
|
|
747
|
+
return isLoadoutSkillMetadata(body) ? body : null;
|
|
748
|
+
}
|
|
749
|
+
function isLoadoutSkillMetadata(value) {
|
|
750
|
+
if (!value || typeof value !== "object")
|
|
751
|
+
return false;
|
|
752
|
+
const body = value;
|
|
753
|
+
return body.product === "loadout" && typeof body.skillUrl === "string" && typeof body.skillVersion === "string" && typeof body.updatedAt === "string" && typeof body.setupPrompt === "string" && typeof body.updatePrompt === "string" && typeof body.minCliVersion === "string" && typeof body.recommendedCliVersion === "string" && (body.localIntegrationMigrationPromptEnabled === undefined || typeof body.localIntegrationMigrationPromptEnabled === "boolean") && Array.isArray(body.notices) && body.notices.every(isLoadoutSkillNotice);
|
|
754
|
+
}
|
|
755
|
+
function isLocalIntegrationMigrationPromptEnabled(metadata) {
|
|
756
|
+
return metadata?.localIntegrationMigrationPromptEnabled === true;
|
|
757
|
+
}
|
|
758
|
+
function getInstalledLoadoutSkillCandidates(cwd = process.cwd(), home = homedir2()) {
|
|
759
|
+
const skillFile = join3("skills", "aident-skill", "SKILL.md");
|
|
760
|
+
return [
|
|
761
|
+
join3(cwd, ".claude", skillFile),
|
|
762
|
+
join3(cwd, ".agents", skillFile),
|
|
763
|
+
join3(home, ".claude", skillFile),
|
|
764
|
+
join3(home, ".codex", skillFile),
|
|
765
|
+
join3(home, ".cursor", skillFile),
|
|
766
|
+
join3(home, ".agents", skillFile),
|
|
767
|
+
join3(home, ".gemini", "extensions", "aident-skill", "SKILL.md")
|
|
768
|
+
];
|
|
769
|
+
}
|
|
770
|
+
function formatLoadoutSkillWarnings(metadata) {
|
|
771
|
+
const warnings = metadata?.notices.filter((notice) => notice.severity === "warning") ?? [];
|
|
772
|
+
return warnings.map((notice) => `${colors.yellow}Warning:${colors.reset} ${notice.message}`);
|
|
773
|
+
}
|
|
774
|
+
async function findInstalledLoadoutSkillVersion(candidates = getInstalledLoadoutSkillCandidates()) {
|
|
775
|
+
for (const candidate of candidates) {
|
|
776
|
+
let content;
|
|
777
|
+
try {
|
|
778
|
+
content = await readFile3(candidate, "utf-8");
|
|
779
|
+
} catch {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const match = /^version:\s*(\d+\.\d+\.\d+)\s*$/m.exec(content);
|
|
783
|
+
if (match)
|
|
784
|
+
return match[1];
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
function isLoadoutSkillNotice(value) {
|
|
789
|
+
if (!value || typeof value !== "object")
|
|
790
|
+
return false;
|
|
791
|
+
const body = value;
|
|
792
|
+
return typeof body.id === "string" && (body.severity === "info" || body.severity === "warning") && typeof body.message === "string";
|
|
577
793
|
}
|
|
578
794
|
|
|
579
795
|
// src/doctor.ts
|
|
@@ -583,9 +799,20 @@ async function runDoctor(opts) {
|
|
|
583
799
|
checks.push(await checkConfig());
|
|
584
800
|
checks.push(await checkCredentials(opts.baseUrl));
|
|
585
801
|
checks.push(await checkServerReachable(opts.baseUrl));
|
|
802
|
+
checks.push(await checkLoadoutSkillMetadata(opts.baseUrl));
|
|
586
803
|
const ok = checks.every((c) => c.ok);
|
|
587
804
|
return { ok, checks, ...opts };
|
|
588
805
|
}
|
|
806
|
+
async function checkLoadoutSkillMetadata(baseUrl) {
|
|
807
|
+
const metadata = await fetchLoadoutSkillMetadata(baseUrl);
|
|
808
|
+
if (!metadata)
|
|
809
|
+
return { name: "Loadout skill", ok: true, detail: "freshness metadata unavailable" };
|
|
810
|
+
return {
|
|
811
|
+
name: "Loadout skill",
|
|
812
|
+
ok: true,
|
|
813
|
+
detail: `v${metadata.skillVersion}; ${metadata.updatePrompt}`
|
|
814
|
+
};
|
|
815
|
+
}
|
|
589
816
|
function checkNodeVersion() {
|
|
590
817
|
const version = process.versions.node;
|
|
591
818
|
const major = Number(version.split(".")[0]);
|
|
@@ -633,7 +860,7 @@ async function checkServerReachable(baseUrl) {
|
|
|
633
860
|
return {
|
|
634
861
|
name: "Server reachable",
|
|
635
862
|
ok: false,
|
|
636
|
-
detail: `${baseUrl} requires @aident-ai/cli ≥ ${minVersion} — run \`
|
|
863
|
+
detail: `${baseUrl} requires @aident-ai/cli ≥ ${minVersion} — run \`curl -fsSL https://loadout.aident.ai/cli/install.sh | bash\``
|
|
637
864
|
};
|
|
638
865
|
}
|
|
639
866
|
if (res.status === 401 || res.status === 200) {
|
|
@@ -650,6 +877,54 @@ async function checkServerReachable(baseUrl) {
|
|
|
650
877
|
}
|
|
651
878
|
|
|
652
879
|
// src/help.ts
|
|
880
|
+
var LOCAL_HELP_COMMANDS = [
|
|
881
|
+
{ command: "login", description: "Authenticate with Aident" },
|
|
882
|
+
{ command: "logout", description: "Revoke the current token" },
|
|
883
|
+
{ command: "whoami", description: "Show current user" },
|
|
884
|
+
{ command: "config show", description: "Print persistent config" },
|
|
885
|
+
{ command: "config set <key> <value>", description: "Persist a config value" },
|
|
886
|
+
{ command: "config get <key>", description: "Read a single value" },
|
|
887
|
+
{ command: "packages add <playbook|intern>", description: "Enable an add-on package" },
|
|
888
|
+
{ command: "doctor", description: "Validate installation" },
|
|
889
|
+
{ command: "setup", description: "Interactive setup wizard" },
|
|
890
|
+
{ command: "integrations migrate-local", description: "Plan migration from local MCP configs to Loadout" },
|
|
891
|
+
{ command: "<domain> <command> [--flag value ...] [--json]", description: "Run a catalog command after login" }
|
|
892
|
+
];
|
|
893
|
+
function getLocalHelp(version) {
|
|
894
|
+
return {
|
|
895
|
+
version,
|
|
896
|
+
authenticated: false,
|
|
897
|
+
usage: [
|
|
898
|
+
"aident login [--oob] [--base-url <url>]",
|
|
899
|
+
"aident logout",
|
|
900
|
+
"aident whoami",
|
|
901
|
+
"aident config show",
|
|
902
|
+
"aident config set <key> <value>",
|
|
903
|
+
"aident config get <key>",
|
|
904
|
+
"aident packages add <playbook|intern>",
|
|
905
|
+
"aident doctor",
|
|
906
|
+
"aident setup",
|
|
907
|
+
"aident integrations migrate-local",
|
|
908
|
+
"aident <domain> <command> [--flag value ...] [--json]"
|
|
909
|
+
],
|
|
910
|
+
commands: LOCAL_HELP_COMMANDS,
|
|
911
|
+
note: "Run `aident login` to authenticate. Authenticated help includes the live command catalog."
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function renderLocalHelp(version) {
|
|
915
|
+
const help = getLocalHelp(version);
|
|
916
|
+
const lines = [];
|
|
917
|
+
lines.push(`${colors.bold}Aident CLI v${version}${colors.reset}`);
|
|
918
|
+
lines.push("");
|
|
919
|
+
lines.push(help.note);
|
|
920
|
+
lines.push("");
|
|
921
|
+
lines.push("USAGE:");
|
|
922
|
+
for (const usage of help.usage) {
|
|
923
|
+
lines.push(` ${usage}`);
|
|
924
|
+
}
|
|
925
|
+
return lines.join(`
|
|
926
|
+
`);
|
|
927
|
+
}
|
|
653
928
|
function renderHelp(catalog) {
|
|
654
929
|
const lines = [];
|
|
655
930
|
lines.push(`${colors.bold}AIDENT — Platform Feature CLI${colors.reset}`);
|
|
@@ -674,6 +949,13 @@ function renderHelp(catalog) {
|
|
|
674
949
|
const display = d.isAdmin ? `admin ${d.name.replace("admin:", "")}` : d.name;
|
|
675
950
|
lines.push(` ${display.padEnd(28)} ${d.description} ${colors.dim}(${d.commandCount})${colors.reset}`);
|
|
676
951
|
}
|
|
952
|
+
if (catalog.loadoutSkill?.notices.length) {
|
|
953
|
+
lines.push("");
|
|
954
|
+
lines.push("LOADOUT SKILL:");
|
|
955
|
+
for (const notice of catalog.loadoutSkill.notices) {
|
|
956
|
+
lines.push(` ${notice.message}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
677
959
|
return lines.join(`
|
|
678
960
|
`);
|
|
679
961
|
}
|
|
@@ -754,6 +1036,459 @@ function renderCommandHelp(catalog, domain, command) {
|
|
|
754
1036
|
`);
|
|
755
1037
|
}
|
|
756
1038
|
|
|
1039
|
+
// src/localIntegrationMigration.ts
|
|
1040
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1041
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1042
|
+
import { homedir as homedir3 } from "node:os";
|
|
1043
|
+
import { basename, join as join4 } from "node:path";
|
|
1044
|
+
var LOCAL_INTEGRATION_MIGRATION_PROMPTED_AT_KEY = "localIntegrationMigrationPromptedAt";
|
|
1045
|
+
var LOCAL_INTEGRATION_MIGRATION_SKIPPED_AT_KEY = "localIntegrationMigrationSkippedAt";
|
|
1046
|
+
var LOCAL_INTEGRATION_MIGRATION_COMPLETED_AT_KEY = "localIntegrationMigrationCompletedAt";
|
|
1047
|
+
var SECRET_KEY_PATTERN = /(^|[^a-z0-9])(bearer|pat|token|secret|password|authorization|api[_-]?key|x[_-]?api[_-]?key|apikey|access[_-]?token|refresh[_-]?token|client[_-]?secret|cookie|private[_-]?key|key[_-]?file|keyfile)(?=[^a-z0-9]|$)/i;
|
|
1048
|
+
var SECRET_ASSIGNMENT_PATTERN = /(bearer|pat|token|secret|password|authorization|api[_-]?key|x[_-]?api[_-]?key|apikey|access[_-]?token|refresh[_-]?token|client[_-]?secret|cookie|private[_-]?key|key[_-]?file|keyfile)=([^,\s]+)/gi;
|
|
1049
|
+
var ASSIGNMENT_VALUE_PATTERN = /([A-Za-z0-9_.-]{2,}=)([^,\s]+)/g;
|
|
1050
|
+
var HIGH_ENTROPY_TOKEN_PATTERN = /[A-Za-z0-9_+=.-]{20,}/g;
|
|
1051
|
+
var HIGH_ENTROPY_MIN_LENGTH = 20;
|
|
1052
|
+
var HIGH_ENTROPY_MIN_BITS_PER_CHAR = 3.5;
|
|
1053
|
+
var MAX_JSON_CONFIG_DEPTH = 10;
|
|
1054
|
+
var MIN_INTEGRATION_MATCH_SCORE = 10;
|
|
1055
|
+
var MATCH_STOP_WORDS = new Set([
|
|
1056
|
+
"api",
|
|
1057
|
+
"app",
|
|
1058
|
+
"cli",
|
|
1059
|
+
"command",
|
|
1060
|
+
"config",
|
|
1061
|
+
"desktop",
|
|
1062
|
+
"http",
|
|
1063
|
+
"https",
|
|
1064
|
+
"io",
|
|
1065
|
+
"json",
|
|
1066
|
+
"local",
|
|
1067
|
+
"mcp",
|
|
1068
|
+
"modelcontextprotocol",
|
|
1069
|
+
"npx",
|
|
1070
|
+
"package",
|
|
1071
|
+
"server",
|
|
1072
|
+
"tools",
|
|
1073
|
+
"tool",
|
|
1074
|
+
"url",
|
|
1075
|
+
"www"
|
|
1076
|
+
]);
|
|
1077
|
+
function shouldOfferLocalIntegrationMigration(config, options = {}) {
|
|
1078
|
+
if (options.stdinIsTTY !== true || options.stdoutIsTTY !== true)
|
|
1079
|
+
return false;
|
|
1080
|
+
return typeof config[LOCAL_INTEGRATION_MIGRATION_PROMPTED_AT_KEY] !== "string";
|
|
1081
|
+
}
|
|
1082
|
+
function parseLocalIntegrationSelection(input, plan) {
|
|
1083
|
+
const trimmed = input.trim();
|
|
1084
|
+
if (!trimmed)
|
|
1085
|
+
return [];
|
|
1086
|
+
const connectable = plan.candidates.filter((candidate) => isConnectableCandidate(candidate));
|
|
1087
|
+
if (trimmed.toLowerCase() === "all") {
|
|
1088
|
+
return uniqueStrings(connectable.map((candidate) => candidate.loadoutIntegrationId).filter(isString));
|
|
1089
|
+
}
|
|
1090
|
+
const selected = new Set;
|
|
1091
|
+
for (const token of trimmed.split(/[,\s]+/).filter(Boolean)) {
|
|
1092
|
+
const asNumber = Number(token);
|
|
1093
|
+
const byIndex = Number.isInteger(asNumber) ? plan.candidates[asNumber - 1] : undefined;
|
|
1094
|
+
if (byIndex && isConnectableCandidate(byIndex) && byIndex.loadoutIntegrationId) {
|
|
1095
|
+
selected.add(byIndex.loadoutIntegrationId);
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
const byId = connectable.find((candidate) => candidate.loadoutIntegrationId === token || candidate.candidateId === token);
|
|
1099
|
+
if (byId?.loadoutIntegrationId)
|
|
1100
|
+
selected.add(byId.loadoutIntegrationId);
|
|
1101
|
+
}
|
|
1102
|
+
return [...selected];
|
|
1103
|
+
}
|
|
1104
|
+
function formatLocalIntegrationSelectionFailure(input, plan) {
|
|
1105
|
+
if (!input.trim())
|
|
1106
|
+
return "No Loadout connections started.";
|
|
1107
|
+
if (!plan.candidates.some(isConnectableCandidate)) {
|
|
1108
|
+
return "No connectable Loadout integrations were available in the migration plan.";
|
|
1109
|
+
}
|
|
1110
|
+
return [
|
|
1111
|
+
"Selection did not include any connectable integrations.",
|
|
1112
|
+
"Use `all`, a listed supported candidate number, candidate ID, or Loadout integration ID from the plan."
|
|
1113
|
+
].join(" ");
|
|
1114
|
+
}
|
|
1115
|
+
function isConnectableCandidate(candidate) {
|
|
1116
|
+
return candidate.status !== "unsupported" && candidate.status !== "already_connected" && typeof candidate.loadoutIntegrationId === "string";
|
|
1117
|
+
}
|
|
1118
|
+
async function buildLocalIntegrationMigrationPlan(options = {}) {
|
|
1119
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1120
|
+
const homeDir = options.homeDir ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir3();
|
|
1121
|
+
const files = localConfigFiles(cwd, homeDir);
|
|
1122
|
+
const rawCandidates = [];
|
|
1123
|
+
const scannedFiles = [];
|
|
1124
|
+
for (const file of files) {
|
|
1125
|
+
const text = await readOptionalText(file.path);
|
|
1126
|
+
if (text === null)
|
|
1127
|
+
continue;
|
|
1128
|
+
scannedFiles.push(file.path);
|
|
1129
|
+
if (file.kind === "json") {
|
|
1130
|
+
const parsed = parseJsonObject(text);
|
|
1131
|
+
if (!parsed)
|
|
1132
|
+
continue;
|
|
1133
|
+
rawCandidates.push(...extractJsonMcpCandidates(parsed, file.source, file.path));
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
const candidates = dedupeRawCandidates(rawCandidates);
|
|
1137
|
+
const catalogIntegrations = options.catalogIntegrations ?? (options.catalogIntegrationResolver ? await options.catalogIntegrationResolver(buildCatalogSearchQueries(candidates)) : []);
|
|
1138
|
+
return {
|
|
1139
|
+
candidates: candidates.map((candidate) => resolveCandidate(candidate, catalogIntegrations)),
|
|
1140
|
+
scannedFiles
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function formatLocalIntegrationMigrationPlan(plan) {
|
|
1144
|
+
if (plan.candidates.length === 0)
|
|
1145
|
+
return "No local integration configs found.";
|
|
1146
|
+
return plan.candidates.map((candidate, index) => {
|
|
1147
|
+
const target = candidate.loadoutIntegrationId ? ` -> ${candidate.loadoutIntegrationId}` : "";
|
|
1148
|
+
const match = ` match:${candidate.matchConfidence}${candidate.matchReasons.length > 0 ? `:${candidate.matchReasons.join("+")}` : ""}`;
|
|
1149
|
+
const identity = candidate.platformCredentialSummary ? ` identity:${candidate.platformCredentialSummary}` : "";
|
|
1150
|
+
const reason = candidate.reason ? ` (${candidate.reason})` : "";
|
|
1151
|
+
return `${index + 1}. ${candidate.localLabel} id:${candidate.candidateId}${target} [${candidate.status}] ${candidate.redactedLocator}${match}${identity}${reason}`;
|
|
1152
|
+
}).join(`
|
|
1153
|
+
`);
|
|
1154
|
+
}
|
|
1155
|
+
function localConfigFiles(cwd, homeDir) {
|
|
1156
|
+
return [
|
|
1157
|
+
{ path: join4(cwd, ".mcp.json"), source: "mcp_config", kind: "json" },
|
|
1158
|
+
{
|
|
1159
|
+
path: join4(cwd, ".cursor", "mcp.json"),
|
|
1160
|
+
source: "mcp_config",
|
|
1161
|
+
kind: "json"
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
path: join4(cwd, ".vscode", "mcp.json"),
|
|
1165
|
+
source: "mcp_config",
|
|
1166
|
+
kind: "json"
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
path: join4(homeDir, ".claude.json"),
|
|
1170
|
+
source: "agent_config",
|
|
1171
|
+
kind: "json"
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
path: join4(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
1175
|
+
source: "agent_config",
|
|
1176
|
+
kind: "json"
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
path: join4(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json"),
|
|
1180
|
+
source: "agent_config",
|
|
1181
|
+
kind: "json"
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
path: join4(homeDir, ".config", "Claude", "claude_desktop_config.json"),
|
|
1185
|
+
source: "agent_config",
|
|
1186
|
+
kind: "json"
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
path: join4(homeDir, ".cursor", "mcp.json"),
|
|
1190
|
+
source: "mcp_config",
|
|
1191
|
+
kind: "json"
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
path: join4(homeDir, ".aident", "config.json"),
|
|
1195
|
+
source: "aident_cli_config",
|
|
1196
|
+
kind: "aident-config"
|
|
1197
|
+
}
|
|
1198
|
+
];
|
|
1199
|
+
}
|
|
1200
|
+
async function readOptionalText(path) {
|
|
1201
|
+
try {
|
|
1202
|
+
return await readFile4(path, "utf-8");
|
|
1203
|
+
} catch {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function parseJsonObject(text) {
|
|
1208
|
+
try {
|
|
1209
|
+
const parsed = JSON.parse(text);
|
|
1210
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1211
|
+
} catch {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function extractJsonMcpCandidates(root, source, path) {
|
|
1216
|
+
const candidates = [];
|
|
1217
|
+
const seenObjects = new Set;
|
|
1218
|
+
const visit = (value, depth) => {
|
|
1219
|
+
if (depth > MAX_JSON_CONFIG_DEPTH)
|
|
1220
|
+
return;
|
|
1221
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || seenObjects.has(value))
|
|
1222
|
+
return;
|
|
1223
|
+
seenObjects.add(value);
|
|
1224
|
+
const obj = value;
|
|
1225
|
+
for (const key of ["mcpServers", "servers"]) {
|
|
1226
|
+
const serverMap = obj[key];
|
|
1227
|
+
if (serverMap && typeof serverMap === "object" && !Array.isArray(serverMap)) {
|
|
1228
|
+
candidates.push(...extractServerMap(serverMap, source, path));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
for (const nested of Object.values(obj))
|
|
1232
|
+
visit(nested, depth + 1);
|
|
1233
|
+
};
|
|
1234
|
+
visit(root, 0);
|
|
1235
|
+
return candidates;
|
|
1236
|
+
}
|
|
1237
|
+
function extractServerMap(serverMap, source, path) {
|
|
1238
|
+
return Object.entries(serverMap).filter(([, entry]) => entry && typeof entry === "object" && !Array.isArray(entry)).map(([label, entry]) => {
|
|
1239
|
+
const url = typeof entry.url === "string" ? entry.url : undefined;
|
|
1240
|
+
const command = typeof entry.command === "string" ? entry.command : undefined;
|
|
1241
|
+
const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : [];
|
|
1242
|
+
return {
|
|
1243
|
+
localSource: source,
|
|
1244
|
+
localLabel: label,
|
|
1245
|
+
detectedKind: "mcp_server",
|
|
1246
|
+
redactedLocator: buildRedactedLocator({ url, command, args, path }),
|
|
1247
|
+
matchText: [label, url ?? "", command ?? "", ...args]
|
|
1248
|
+
};
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
function buildRedactedLocator(params) {
|
|
1252
|
+
const parts = [`config:${basename(params.path)}`];
|
|
1253
|
+
if (params.url)
|
|
1254
|
+
parts.push(`url:${redactedUrlOrigin(params.url)}`);
|
|
1255
|
+
if (params.command)
|
|
1256
|
+
parts.push(`command:${basename(params.command)}`);
|
|
1257
|
+
const packageName = firstPackageLikeArg(params.args);
|
|
1258
|
+
if (packageName)
|
|
1259
|
+
parts.push(`package:${redactSecretLike(packageName)}`);
|
|
1260
|
+
return parts.join(" ");
|
|
1261
|
+
}
|
|
1262
|
+
function redactedUrlOrigin(raw) {
|
|
1263
|
+
try {
|
|
1264
|
+
const url = new URL(raw);
|
|
1265
|
+
return url.origin;
|
|
1266
|
+
} catch {
|
|
1267
|
+
return "[invalid-url]";
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
function firstPackageLikeArg(args) {
|
|
1271
|
+
let previousWasSecretFlag = false;
|
|
1272
|
+
for (const arg of args) {
|
|
1273
|
+
if (previousWasSecretFlag) {
|
|
1274
|
+
previousWasSecretFlag = false;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (!arg)
|
|
1278
|
+
continue;
|
|
1279
|
+
if (arg.startsWith("-")) {
|
|
1280
|
+
if (SECRET_KEY_PATTERN.test(arg) && !arg.includes("="))
|
|
1281
|
+
previousWasSecretFlag = true;
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (/^https?:\/\//i.test(arg))
|
|
1285
|
+
continue;
|
|
1286
|
+
if (SECRET_KEY_PATTERN.test(arg))
|
|
1287
|
+
continue;
|
|
1288
|
+
return arg;
|
|
1289
|
+
}
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
function redactSecretLike(value) {
|
|
1293
|
+
if (SECRET_KEY_PATTERN.test(value))
|
|
1294
|
+
return "[redacted]";
|
|
1295
|
+
const redacted = value.replace(SECRET_ASSIGNMENT_PATTERN, "$1=[redacted]").replace(ASSIGNMENT_VALUE_PATTERN, (match, prefix, rawValue) => isHighEntropySecretLike(rawValue) ? `${prefix}[redacted]` : match);
|
|
1296
|
+
if (!/^[A-Za-z0-9_.-]{2,}=/.test(redacted) && isHighEntropySecretLike(redacted))
|
|
1297
|
+
return "[redacted]";
|
|
1298
|
+
return redacted.replace(HIGH_ENTROPY_TOKEN_PATTERN, (token) => isHighEntropySecretLike(token) ? "[redacted]" : token);
|
|
1299
|
+
}
|
|
1300
|
+
function resolveCandidate(candidate, catalogIntegrations) {
|
|
1301
|
+
const matchResult = findBestIntegrationMatch(candidate, catalogIntegrations);
|
|
1302
|
+
if (matchResult.kind === "ambiguous") {
|
|
1303
|
+
return {
|
|
1304
|
+
...withoutMatchText(candidate),
|
|
1305
|
+
matchConfidence: "ambiguous",
|
|
1306
|
+
matchReasons: matchResult.matchReasons,
|
|
1307
|
+
status: "unsupported",
|
|
1308
|
+
reason: "Multiple Loadout integrations matched local evidence."
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
if (matchResult.kind === "none") {
|
|
1312
|
+
return {
|
|
1313
|
+
...withoutMatchText(candidate),
|
|
1314
|
+
matchConfidence: "none",
|
|
1315
|
+
matchReasons: [],
|
|
1316
|
+
status: "unsupported",
|
|
1317
|
+
reason: "No matching Loadout integration found."
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
const match = matchResult.integration;
|
|
1321
|
+
const status = getMatchedStatus(match);
|
|
1322
|
+
return {
|
|
1323
|
+
...withoutMatchText(candidate),
|
|
1324
|
+
loadoutIntegrationId: match.id,
|
|
1325
|
+
matchConfidence: matchResult.matchConfidence,
|
|
1326
|
+
matchReasons: matchResult.matchReasons,
|
|
1327
|
+
platformCredentialSummary: match.platformCredentialSummary,
|
|
1328
|
+
status,
|
|
1329
|
+
reason: getMatchedReason(match, status)
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function getMatchedStatus(match) {
|
|
1333
|
+
if (match.readiness === "ready" && match.platformCredentialSummary)
|
|
1334
|
+
return "already_connected";
|
|
1335
|
+
if (match.readiness === "needs_user_setup" || match.readiness === "ready")
|
|
1336
|
+
return "needs_user_connection";
|
|
1337
|
+
return "supported";
|
|
1338
|
+
}
|
|
1339
|
+
function getMatchedReason(match, status) {
|
|
1340
|
+
if (match.readiness === "ready" && status === "needs_user_connection") {
|
|
1341
|
+
return "Connected Loadout credential needs user-confirmable identity.";
|
|
1342
|
+
}
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
function findBestIntegrationMatch(candidate, catalogIntegrations) {
|
|
1346
|
+
const localTokens = new Set(candidate.matchText.flatMap(tokenizeForMatch));
|
|
1347
|
+
let best = null;
|
|
1348
|
+
let tied = false;
|
|
1349
|
+
for (const integration of catalogIntegrations) {
|
|
1350
|
+
const catalogTokens = new Set([integration.id, integration.name ?? "", ...getDeclaredCatalogEvidence(integration)].flatMap(tokenizeForMatch));
|
|
1351
|
+
const score = scoreTokenMatch(localTokens, catalogTokens);
|
|
1352
|
+
if (score < MIN_INTEGRATION_MATCH_SCORE)
|
|
1353
|
+
continue;
|
|
1354
|
+
const matchConfidence = getMatchConfidence(candidate, integration, score);
|
|
1355
|
+
const matchReasons = getMatchReasons(candidate, integration);
|
|
1356
|
+
if (!best || score > best.score) {
|
|
1357
|
+
best = { integration, score, matchConfidence, matchReasons };
|
|
1358
|
+
tied = false;
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
if (score === best.score)
|
|
1362
|
+
tied = true;
|
|
1363
|
+
}
|
|
1364
|
+
if (!best)
|
|
1365
|
+
return { kind: "none" };
|
|
1366
|
+
if (tied)
|
|
1367
|
+
return { kind: "ambiguous", matchReasons: best.matchReasons };
|
|
1368
|
+
return {
|
|
1369
|
+
kind: "matched",
|
|
1370
|
+
integration: best.integration,
|
|
1371
|
+
matchConfidence: best.matchConfidence,
|
|
1372
|
+
matchReasons: best.matchReasons
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
function scoreTokenMatch(localTokens, catalogTokens) {
|
|
1376
|
+
let score = 0;
|
|
1377
|
+
for (const token of localTokens) {
|
|
1378
|
+
if (catalogTokens.has(token))
|
|
1379
|
+
score += 10;
|
|
1380
|
+
for (const catalogToken of catalogTokens) {
|
|
1381
|
+
if (token.length >= 4 && catalogToken.length >= 4 && token.includes(catalogToken))
|
|
1382
|
+
score += 3;
|
|
1383
|
+
else if (token.length >= 4 && catalogToken.length >= 4 && catalogToken.includes(token))
|
|
1384
|
+
score += 2;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return score;
|
|
1388
|
+
}
|
|
1389
|
+
function getMatchConfidence(candidate, integration, score) {
|
|
1390
|
+
const declaredEvidence = getDeclaredCatalogEvidence(integration);
|
|
1391
|
+
if (hasExactDeclaredCatalogEvidence(candidate, declaredEvidence))
|
|
1392
|
+
return "exact";
|
|
1393
|
+
if (declaredEvidence.length > 0 && score >= MIN_INTEGRATION_MATCH_SCORE)
|
|
1394
|
+
return "high";
|
|
1395
|
+
return "suggested";
|
|
1396
|
+
}
|
|
1397
|
+
function getMatchReasons(candidate, integration) {
|
|
1398
|
+
const reasons = new Set;
|
|
1399
|
+
const catalogValues = [integration.id, integration.name].filter(isString).map(normalizeForDirectMatch);
|
|
1400
|
+
if (candidate.matchText.map(normalizeForDirectMatch).some((value) => catalogValues.includes(value))) {
|
|
1401
|
+
reasons.add("catalog_alias");
|
|
1402
|
+
}
|
|
1403
|
+
if (candidate.redactedLocator.includes("package:"))
|
|
1404
|
+
reasons.add("mcp_package");
|
|
1405
|
+
if (candidate.redactedLocator.includes("command:"))
|
|
1406
|
+
reasons.add("command");
|
|
1407
|
+
if (candidate.redactedLocator.includes("url:"))
|
|
1408
|
+
reasons.add("host");
|
|
1409
|
+
return [...reasons];
|
|
1410
|
+
}
|
|
1411
|
+
function getDeclaredCatalogEvidence(integration) {
|
|
1412
|
+
return [
|
|
1413
|
+
...integration.providerAliases ?? [],
|
|
1414
|
+
...integration.mcpPackages ?? [],
|
|
1415
|
+
...integration.commandAliases ?? [],
|
|
1416
|
+
...integration.hostAllowlist ?? []
|
|
1417
|
+
];
|
|
1418
|
+
}
|
|
1419
|
+
function hasExactDeclaredCatalogEvidence(candidate, evidence) {
|
|
1420
|
+
if (evidence.length === 0)
|
|
1421
|
+
return false;
|
|
1422
|
+
const normalizedEvidence = new Set(evidence.map(normalizeForDirectMatch));
|
|
1423
|
+
return candidate.matchText.map(normalizeForDirectMatch).some((value) => normalizedEvidence.has(value));
|
|
1424
|
+
}
|
|
1425
|
+
function tokenizeForMatch(value) {
|
|
1426
|
+
const normalized = value.toLowerCase().replace(/^(@?modelcontextprotocol\/)?server[-_/]/, "").replace(/^(api|cli|mcp):/, "").replace(/(_tools|_mcp|_api)$/, "");
|
|
1427
|
+
return uniqueStrings(normalized.split(/[^a-z0-9]+/).map((token) => token.replace(/(_tools|_mcp|_api)$/, "")).filter((token) => token.length >= 2 && !MATCH_STOP_WORDS.has(token)));
|
|
1428
|
+
}
|
|
1429
|
+
function normalizeForDirectMatch(value) {
|
|
1430
|
+
return value.trim().toLowerCase();
|
|
1431
|
+
}
|
|
1432
|
+
function withoutMatchText(candidate) {
|
|
1433
|
+
return {
|
|
1434
|
+
candidateId: buildCandidateId(candidate),
|
|
1435
|
+
localSource: candidate.localSource,
|
|
1436
|
+
localLabel: candidate.localLabel,
|
|
1437
|
+
detectedKind: candidate.detectedKind,
|
|
1438
|
+
redactedLocator: candidate.redactedLocator
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
function buildCandidateId(candidate) {
|
|
1442
|
+
return `local_${createHash2("sha256").update([candidate.localSource, candidate.localLabel, candidate.redactedLocator].join("\x00")).digest("hex").slice(0, 12)}`;
|
|
1443
|
+
}
|
|
1444
|
+
function dedupeRawCandidates(candidates) {
|
|
1445
|
+
const byKey = new Map;
|
|
1446
|
+
for (const candidate of candidates) {
|
|
1447
|
+
byKey.set(`${candidate.localSource}:${candidate.localLabel}:${candidate.redactedLocator}`, candidate);
|
|
1448
|
+
}
|
|
1449
|
+
return [...byKey.values()];
|
|
1450
|
+
}
|
|
1451
|
+
function buildCatalogSearchQueries(candidates) {
|
|
1452
|
+
return uniqueStrings(candidates.map((candidate) => {
|
|
1453
|
+
const tokens = tokenizeForMatch(`${candidate.localLabel} ${candidate.redactedLocator}`);
|
|
1454
|
+
return tokens.join(" ");
|
|
1455
|
+
}).filter(isString));
|
|
1456
|
+
}
|
|
1457
|
+
function uniqueStrings(values) {
|
|
1458
|
+
return [...new Set(values)];
|
|
1459
|
+
}
|
|
1460
|
+
function isString(value) {
|
|
1461
|
+
return typeof value === "string" && value.trim() !== "";
|
|
1462
|
+
}
|
|
1463
|
+
function isHighEntropySecretLike(value) {
|
|
1464
|
+
if (value.length < HIGH_ENTROPY_MIN_LENGTH)
|
|
1465
|
+
return false;
|
|
1466
|
+
const classes = [/[a-z]/.test(value), /[A-Z]/.test(value), /\d/.test(value), /[_+=.-]/.test(value)].filter(Boolean).length;
|
|
1467
|
+
if (classes < 3)
|
|
1468
|
+
return false;
|
|
1469
|
+
return shannonEntropy(value) >= HIGH_ENTROPY_MIN_BITS_PER_CHAR;
|
|
1470
|
+
}
|
|
1471
|
+
function shannonEntropy(value) {
|
|
1472
|
+
const counts = new Map;
|
|
1473
|
+
for (const char of value)
|
|
1474
|
+
counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
1475
|
+
return [...counts.values()].reduce((sum, count) => {
|
|
1476
|
+
const probability = count / value.length;
|
|
1477
|
+
return sum - probability * Math.log2(probability);
|
|
1478
|
+
}, 0);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// src/localIntegrationValidation.ts
|
|
1482
|
+
var READ_ONLY_VALIDATION_CAPABILITY_PATTERN = /(^|[._:-])(me|whoami|authenticated[-_]?user|get[-_]?authenticated[-_]?user|current[-_]?user|user[-_]?info|account[-_]?info|account[-_]?metadata|profile|get[-_]?profile|workspace[-_]?info|team[-_]?info|token[-_]?introspect|auth[-_]?test)($|[._:-])/i;
|
|
1483
|
+
var READ_ONLY_VALIDATION_DESCRIPTION_PATTERN = /\b(current user|authenticated user|account metadata|workspace metadata|team metadata|token introspection|profile)\b/i;
|
|
1484
|
+
var UNSAFE_VALIDATION_CAPABILITY_PATTERN = /\b(create|delete|update|send|post|write|modify|invite|remove|upload|charge|payment|transfer)\b/i;
|
|
1485
|
+
function isReadOnlyValidationCapability(capability) {
|
|
1486
|
+
const haystack = `${capability.name} ${capability.description}`;
|
|
1487
|
+
if (UNSAFE_VALIDATION_CAPABILITY_PATTERN.test(haystack))
|
|
1488
|
+
return false;
|
|
1489
|
+
return READ_ONLY_VALIDATION_CAPABILITY_PATTERN.test(capability.name) || READ_ONLY_VALIDATION_DESCRIPTION_PATTERN.test(capability.description);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
757
1492
|
// src/parser.ts
|
|
758
1493
|
var RESERVED_CLI_FLAGS = ["base-url", "json", "tui", "help", "oob", "version", "package", "packages"];
|
|
759
1494
|
function parseArgs(argv) {
|
|
@@ -901,10 +1636,28 @@ async function callWithRefresh(client, call, deps = defaultDeps) {
|
|
|
901
1636
|
}
|
|
902
1637
|
function extractUpgradeMessage(body) {
|
|
903
1638
|
const err = body?.error;
|
|
904
|
-
return err?.message ?? `This @aident-ai/cli version is no longer supported. Run \`
|
|
1639
|
+
return err?.message ?? `This @aident-ai/cli version is no longer supported. Run \`curl -fsSL https://loadout.aident.ai/cli/install.sh | bash\` to upgrade.`;
|
|
905
1640
|
}
|
|
906
1641
|
|
|
907
1642
|
// src/cli.ts
|
|
1643
|
+
var LOADOUT_CAPABILITIES_SEARCH_OPERATION = "loadout_capabilities_search";
|
|
1644
|
+
var LOADOUT_CAPABILITIES_EXECUTE_OPERATION = "loadout_capabilities_execute";
|
|
1645
|
+
var LOADOUT_CAPABILITIES_FEEDBACK_OPERATION = "loadout_capabilities_feedback";
|
|
1646
|
+
var LOADOUT_VAULT_STATUS_OPERATION = "loadout_vault_status";
|
|
1647
|
+
var LOADOUT_VAULT_CONNECT_OPERATION = "loadout_vault_connect";
|
|
1648
|
+
var LOADOUT_LOCAL_MIGRATION_EVENT_OPERATION = "loadout_vault_local_migration_event";
|
|
1649
|
+
var LOCAL_MIGRATION_SEARCH_BATCH_SIZE = 10;
|
|
1650
|
+
var LOCAL_MIGRATION_SEARCH_RESULT_MAX_DEPTH = 4;
|
|
1651
|
+
var LOCAL_MIGRATION_VALIDATION_SEARCH_QUERIES = [
|
|
1652
|
+
"current user",
|
|
1653
|
+
"authenticated user",
|
|
1654
|
+
"profile",
|
|
1655
|
+
"account info",
|
|
1656
|
+
"workspace info",
|
|
1657
|
+
"team info",
|
|
1658
|
+
"token introspect",
|
|
1659
|
+
"auth test"
|
|
1660
|
+
];
|
|
908
1661
|
async function main() {
|
|
909
1662
|
const argv = process.argv.slice(2);
|
|
910
1663
|
const parsed = parseArgs(argv);
|
|
@@ -931,11 +1684,18 @@ async function main() {
|
|
|
931
1684
|
await runDoctorCmd(parsed.format);
|
|
932
1685
|
return;
|
|
933
1686
|
case "setup":
|
|
934
|
-
await runSetup();
|
|
1687
|
+
await runSetup(parsed);
|
|
935
1688
|
return;
|
|
936
1689
|
case "config":
|
|
937
1690
|
await runConfigCmd(parsed);
|
|
938
1691
|
return;
|
|
1692
|
+
case "integrations":
|
|
1693
|
+
if (parsed.positional[1] === "migrate-local") {
|
|
1694
|
+
await runLocalIntegrationMigrationCmd(parsed);
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
await runCommand(parsed);
|
|
1698
|
+
return;
|
|
939
1699
|
case "packages":
|
|
940
1700
|
case "package":
|
|
941
1701
|
await runPackagesCmd(parsed);
|
|
@@ -950,30 +1710,15 @@ async function main() {
|
|
|
950
1710
|
async function runHelp(parsed) {
|
|
951
1711
|
const client = await getAuthenticatedClient(await getRequestedPackages(parsed));
|
|
952
1712
|
if (!client) {
|
|
953
|
-
|
|
954
|
-
logInfo(JSON.stringify({ error: "not-authenticated", message: "Run `aident login` first." }));
|
|
955
|
-
} else {
|
|
956
|
-
logInfo(`${colors.bold}Aident CLI v${VERSION}${colors.reset}`);
|
|
957
|
-
logInfo("");
|
|
958
|
-
logInfo("Run `aident login` to authenticate, then `aident --help` to list commands.");
|
|
959
|
-
logInfo("");
|
|
960
|
-
logInfo("USAGE:");
|
|
961
|
-
logInfo(" aident login [--oob] [--base-url <url>] Authenticate with Aident");
|
|
962
|
-
logInfo(" aident logout Revoke the current token");
|
|
963
|
-
logInfo(" aident whoami Show current user");
|
|
964
|
-
logInfo(" aident config show Print persistent config");
|
|
965
|
-
logInfo(" aident config set <key> <value> Persist a config value");
|
|
966
|
-
logInfo(" aident config get <key> Read a single value");
|
|
967
|
-
logInfo(" aident packages add <playbook|intern> Enable an add-on package");
|
|
968
|
-
logInfo(" aident doctor Validate installation");
|
|
969
|
-
logInfo(" aident setup Interactive setup wizard");
|
|
970
|
-
logInfo(" aident <domain> <command> [--flag value ...] [--json]");
|
|
971
|
-
}
|
|
1713
|
+
emitLocalHelp(parsed.format);
|
|
972
1714
|
return;
|
|
973
1715
|
}
|
|
974
|
-
const catalog = await fetchCatalog(client);
|
|
975
|
-
if (!catalog)
|
|
1716
|
+
const catalog = await fetchCatalog(client, { suppressAuthError: true });
|
|
1717
|
+
if (!catalog) {
|
|
1718
|
+
if (!process.exitCode)
|
|
1719
|
+
emitLocalHelp(parsed.format);
|
|
976
1720
|
return;
|
|
1721
|
+
}
|
|
977
1722
|
if (parsed.format === "json") {
|
|
978
1723
|
logInfo(JSON.stringify(catalog));
|
|
979
1724
|
} else {
|
|
@@ -1192,7 +1937,7 @@ async function runDoctorCmd(format) {
|
|
|
1192
1937
|
if (!report.ok)
|
|
1193
1938
|
process.exitCode = 1;
|
|
1194
1939
|
}
|
|
1195
|
-
async function runSetup() {
|
|
1940
|
+
async function runSetup(parsed) {
|
|
1196
1941
|
logInfo(`${colors.bold}Aident CLI setup${colors.reset}`);
|
|
1197
1942
|
logInfo("");
|
|
1198
1943
|
const current = await resolveDefaultBaseUrl();
|
|
@@ -1216,9 +1961,122 @@ async function runSetup() {
|
|
|
1216
1961
|
logInfo("Skipped login. Run `aident login` whenever you want.");
|
|
1217
1962
|
}
|
|
1218
1963
|
}
|
|
1964
|
+
await maybeOfferLocalIntegrationMigration(parsed);
|
|
1219
1965
|
logInfo("");
|
|
1220
1966
|
logInfo(`Done. Try ${colors.cyan}aident --help${colors.reset} or ${colors.cyan}aident doctor${colors.reset}.`);
|
|
1221
1967
|
}
|
|
1968
|
+
async function maybeOfferLocalIntegrationMigration(parsed) {
|
|
1969
|
+
if (parsed.format !== "tui")
|
|
1970
|
+
return;
|
|
1971
|
+
const config = await readConfig();
|
|
1972
|
+
if (!shouldOfferLocalIntegrationMigration(config, {
|
|
1973
|
+
stdinIsTTY: process.stdin.isTTY === true,
|
|
1974
|
+
stdoutIsTTY: process.stdout.isTTY === true
|
|
1975
|
+
})) {
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
const client = await getAuthenticatedClient(["loadout"]);
|
|
1979
|
+
if (!client)
|
|
1980
|
+
return;
|
|
1981
|
+
if (!await genIsLocalIntegrationMigrationPromptEnabled(client))
|
|
1982
|
+
return;
|
|
1983
|
+
logInfo("");
|
|
1984
|
+
logInfo(`${colors.bold}Local integration migration${colors.reset}`);
|
|
1985
|
+
await tryRecordLocalMigrationEvent(client, "prompt_shown" /* PromptShown */);
|
|
1986
|
+
const answer = (await readLine("Scan local agent/MCP integration configs and offer supported Loadout connections? [y/N]: ")).trim().toLowerCase();
|
|
1987
|
+
const promptedAt = new Date().toISOString();
|
|
1988
|
+
await setConfigValue(LOCAL_INTEGRATION_MIGRATION_PROMPTED_AT_KEY, promptedAt);
|
|
1989
|
+
if (!isAffirmative(answer)) {
|
|
1990
|
+
await tryRecordLocalMigrationEvent(client, "declined" /* Declined */);
|
|
1991
|
+
await setConfigValue(LOCAL_INTEGRATION_MIGRATION_SKIPPED_AT_KEY, promptedAt);
|
|
1992
|
+
logInfo("Skipped local integration migration.");
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
await tryRecordLocalMigrationEvent(client, "accepted" /* Accepted */);
|
|
1996
|
+
const plan = await genLoadoutLocalIntegrationMigrationPlan(client);
|
|
1997
|
+
await tryRecordLocalMigrationEvent(client, "scan_completed" /* ScanCompleted */, getLocalMigrationPlanTelemetry(plan));
|
|
1998
|
+
logInfo(formatLocalIntegrationMigrationPlan(plan));
|
|
1999
|
+
const connectable = plan.candidates.filter(isConnectableCandidate);
|
|
2000
|
+
if (connectable.length === 0) {
|
|
2001
|
+
await tryRecordLocalMigrationEvent(client, "migration_completed" /* MigrationCompleted */, {
|
|
2002
|
+
...getLocalMigrationPlanTelemetry(plan),
|
|
2003
|
+
selectedCount: 0,
|
|
2004
|
+
connectedCount: 0,
|
|
2005
|
+
failedCount: 0,
|
|
2006
|
+
skippedCount: 0
|
|
2007
|
+
});
|
|
2008
|
+
await setConfigValue(LOCAL_INTEGRATION_MIGRATION_COMPLETED_AT_KEY, new Date().toISOString());
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
logInfo("");
|
|
2012
|
+
logInfo("Enter `all`, selected numbers, or selected integration IDs to connect now. Press Enter to skip.");
|
|
2013
|
+
const selection = await readLine("Connect selection: ");
|
|
2014
|
+
const selectedIntegrationIds = parseLocalIntegrationSelection(selection, plan);
|
|
2015
|
+
if (selectedIntegrationIds.length === 0) {
|
|
2016
|
+
await tryRecordLocalMigrationEvent(client, "migration_completed" /* MigrationCompleted */, {
|
|
2017
|
+
...getLocalMigrationPlanTelemetry(plan),
|
|
2018
|
+
selectedCount: 0,
|
|
2019
|
+
connectedCount: 0,
|
|
2020
|
+
failedCount: 0,
|
|
2021
|
+
skippedCount: connectable.length
|
|
2022
|
+
});
|
|
2023
|
+
await setConfigValue(LOCAL_INTEGRATION_MIGRATION_SKIPPED_AT_KEY, new Date().toISOString());
|
|
2024
|
+
logInfo(formatLocalIntegrationSelectionFailure(selection, plan));
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
await tryRecordLocalMigrationEvent(client, "migration_started" /* MigrationStarted */, {
|
|
2028
|
+
...getLocalMigrationPlanTelemetry(plan),
|
|
2029
|
+
selectedCount: selectedIntegrationIds.length
|
|
2030
|
+
});
|
|
2031
|
+
const result = await genApplyLocalIntegrationMigration(client, plan, selectedIntegrationIds);
|
|
2032
|
+
renderLocalMigrationApplyResult(result, "tui");
|
|
2033
|
+
await genRecordLocalMigrationApplyTelemetry(client, result);
|
|
2034
|
+
await setConfigValue(LOCAL_INTEGRATION_MIGRATION_COMPLETED_AT_KEY, new Date().toISOString());
|
|
2035
|
+
}
|
|
2036
|
+
async function genIsLocalIntegrationMigrationPromptEnabled(client) {
|
|
2037
|
+
const result = await callWithRefresh(client, (c) => c.getCatalog());
|
|
2038
|
+
if (result.status !== 200)
|
|
2039
|
+
return false;
|
|
2040
|
+
return isLocalIntegrationMigrationPromptEnabled(result.body.loadoutSkill);
|
|
2041
|
+
}
|
|
2042
|
+
async function runLocalIntegrationMigrationCmd(parsed) {
|
|
2043
|
+
if (parsed.isHelp) {
|
|
2044
|
+
logInfo(renderLocalIntegrationMigrationHelp());
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const client = await getAuthenticatedClient(["loadout"]);
|
|
2048
|
+
if (!client) {
|
|
2049
|
+
emitLocalCommandResult(parsed.format, false, undefined, "not-authenticated", "Run `aident login` before planning local integration migration.");
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
const plan = await genLoadoutLocalIntegrationMigrationPlan(client);
|
|
2053
|
+
await tryRecordLocalMigrationEvent(client, "scan_completed" /* ScanCompleted */, getLocalMigrationPlanTelemetry(plan));
|
|
2054
|
+
const apply = parsed.flags["apply"] === true;
|
|
2055
|
+
if (!apply) {
|
|
2056
|
+
renderLocalMigrationPlan(plan, parsed.format);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
const selectedIntegrationIds = parseIntegrationIdsFlag(parsed, plan);
|
|
2060
|
+
if (selectedIntegrationIds.length === 0) {
|
|
2061
|
+
await tryRecordLocalMigrationEvent(client, "migration_completed" /* MigrationCompleted */, {
|
|
2062
|
+
...getLocalMigrationPlanTelemetry(plan),
|
|
2063
|
+
selectedCount: 0,
|
|
2064
|
+
connectedCount: 0,
|
|
2065
|
+
failedCount: 0,
|
|
2066
|
+
skippedCount: 0,
|
|
2067
|
+
status: "failure"
|
|
2068
|
+
});
|
|
2069
|
+
emitLocalCommandResult(parsed.format, false, undefined, "selection-required", getLocalMigrationSelectionRequiredMessage(parsed, plan));
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
await tryRecordLocalMigrationEvent(client, "migration_started" /* MigrationStarted */, {
|
|
2073
|
+
...getLocalMigrationPlanTelemetry(plan),
|
|
2074
|
+
selectedCount: selectedIntegrationIds.length
|
|
2075
|
+
});
|
|
2076
|
+
const result = await genApplyLocalIntegrationMigration(client, plan, selectedIntegrationIds);
|
|
2077
|
+
renderLocalMigrationApplyResult(result, parsed.format);
|
|
2078
|
+
await genRecordLocalMigrationApplyTelemetry(client, result);
|
|
2079
|
+
}
|
|
1222
2080
|
async function runCatalog(parsed) {
|
|
1223
2081
|
const client = await getAuthenticatedClient(await getRequestedPackages(parsed));
|
|
1224
2082
|
if (!client) {
|
|
@@ -1230,9 +2088,460 @@ async function runCatalog(parsed) {
|
|
|
1230
2088
|
return;
|
|
1231
2089
|
logInfo(JSON.stringify(catalog, null, parsed.format === "json" ? 0 : 2));
|
|
1232
2090
|
}
|
|
2091
|
+
async function genLoadoutLocalIntegrationMigrationPlan(client) {
|
|
2092
|
+
return buildLocalIntegrationMigrationPlan({
|
|
2093
|
+
catalogIntegrationResolver: (queries) => genFetchLoadoutIntegrations(client, queries)
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
async function genFetchLoadoutIntegrations(client, queries) {
|
|
2097
|
+
const byId = new Map;
|
|
2098
|
+
for (const queryBatch of chunkStrings(queries, LOCAL_MIGRATION_SEARCH_BATCH_SIZE)) {
|
|
2099
|
+
const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_CAPABILITIES_SEARCH_OPERATION, {
|
|
2100
|
+
queries: queryBatch,
|
|
2101
|
+
types: ["integration"],
|
|
2102
|
+
mode: "keyword",
|
|
2103
|
+
limit: 10
|
|
2104
|
+
}));
|
|
2105
|
+
if (!result.body.success)
|
|
2106
|
+
continue;
|
|
2107
|
+
for (const integration of getLocalMigrationIntegrationsFromSearch(result.body.data)) {
|
|
2108
|
+
byId.set(integration.id, integration);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
return genEnrichLoadoutIntegrationsWithVaultStatus(client, [...byId.values()]);
|
|
2112
|
+
}
|
|
2113
|
+
async function genEnrichLoadoutIntegrationsWithVaultStatus(client, integrations) {
|
|
2114
|
+
if (integrations.length === 0)
|
|
2115
|
+
return integrations;
|
|
2116
|
+
const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_VAULT_STATUS_OPERATION, {
|
|
2117
|
+
integrationIds: integrations.map((integration) => integration.id)
|
|
2118
|
+
}));
|
|
2119
|
+
if (!result.body.success)
|
|
2120
|
+
return integrations;
|
|
2121
|
+
const status = getLocalMigrationVaultStatus(result.body.data);
|
|
2122
|
+
return integrations.map((integration) => {
|
|
2123
|
+
const state = getLocalMigrationVaultIntegrationState(status, integration.id);
|
|
2124
|
+
const connected = getLocalMigrationVaultIntegrationIds(status, integration.id).some((id) => status.connectionStatus?.[id] === true);
|
|
2125
|
+
return {
|
|
2126
|
+
...integration,
|
|
2127
|
+
readiness: getLocalMigrationReadiness(state?.readiness) ?? (connected ? "ready" : integration.readiness),
|
|
2128
|
+
setupMode: state?.setupMode ?? integration.setupMode
|
|
2129
|
+
};
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
async function genApplyLocalIntegrationMigration(client, plan, selectedIntegrationIds) {
|
|
2133
|
+
const connectableIds = new Set(plan.candidates.filter(isConnectableCandidate).map((candidate) => candidate.loadoutIntegrationId).filter((value) => typeof value === "string"));
|
|
2134
|
+
const connectResults = [];
|
|
2135
|
+
for (const integrationId of selectedIntegrationIds) {
|
|
2136
|
+
if (!connectableIds.has(integrationId)) {
|
|
2137
|
+
connectResults.push({ integrationId, skipped: "not-connectable" });
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_VAULT_CONNECT_OPERATION, { integrationId }));
|
|
2141
|
+
const entry = { integrationId, result: result.body };
|
|
2142
|
+
if (result.body.success) {
|
|
2143
|
+
entry.validation = await genValidateLocalIntegrationConnection(client, integrationId);
|
|
2144
|
+
if (!entry.validation.success) {
|
|
2145
|
+
await trySubmitLocalMigrationFeedback(client, integrationId, formatLocalMigrationValidationFeedback(entry));
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
connectResults.push(entry);
|
|
2149
|
+
}
|
|
2150
|
+
return { plan, selectedIntegrationIds, connectResults };
|
|
2151
|
+
}
|
|
2152
|
+
async function tryRecordLocalMigrationEvent(client, action, properties = {}) {
|
|
2153
|
+
try {
|
|
2154
|
+
await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_LOCAL_MIGRATION_EVENT_OPERATION, {
|
|
2155
|
+
action,
|
|
2156
|
+
...properties
|
|
2157
|
+
}));
|
|
2158
|
+
} catch (error) {
|
|
2159
|
+
if (process.env.AIDENT_CLI_DEBUG === "1") {
|
|
2160
|
+
logErr(`Telemetry skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
async function genValidateLocalIntegrationConnection(client, integrationId) {
|
|
2165
|
+
const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_VAULT_STATUS_OPERATION, { integrationId }));
|
|
2166
|
+
if (!result.body.success) {
|
|
2167
|
+
return {
|
|
2168
|
+
success: false,
|
|
2169
|
+
result: result.body,
|
|
2170
|
+
message: result.body.error?.message ?? "Vault status validation failed."
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
const status = getLocalMigrationVaultStatus(result.body.data);
|
|
2174
|
+
const ids = getLocalMigrationVaultIntegrationIds(status, integrationId);
|
|
2175
|
+
const connected = ids.some((id) => status.connectionStatus?.[id] === true);
|
|
2176
|
+
const ready = ids.some((id) => status.integrationStates?.[id]?.readiness === "ready");
|
|
2177
|
+
const success = connected || ready;
|
|
2178
|
+
if (!success) {
|
|
2179
|
+
return {
|
|
2180
|
+
success: false,
|
|
2181
|
+
result: result.body,
|
|
2182
|
+
message: "Vault status did not confirm a connected or ready integration."
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
const providerValidation = await genTryValidateLocalIntegrationWithReadOnlyCapability(client, integrationId);
|
|
2186
|
+
if (providerValidation.available) {
|
|
2187
|
+
return {
|
|
2188
|
+
success: providerValidation.result.success,
|
|
2189
|
+
result: providerValidation.result,
|
|
2190
|
+
providerCapabilityName: providerValidation.capabilityName,
|
|
2191
|
+
message: providerValidation.result.success ? undefined : providerValidation.result.error?.message ?? "Read-only provider validation failed."
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
return {
|
|
2195
|
+
success: true,
|
|
2196
|
+
result: result.body,
|
|
2197
|
+
message: providerValidation.reason
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
async function genTryValidateLocalIntegrationWithReadOnlyCapability(client, integrationId) {
|
|
2201
|
+
const capabilities = await genListLocalMigrationValidationCapabilities(client, integrationId);
|
|
2202
|
+
const capability = capabilities.find(isReadOnlyValidationCapability);
|
|
2203
|
+
if (!capability) {
|
|
2204
|
+
return {
|
|
2205
|
+
available: false,
|
|
2206
|
+
reason: "No low-risk read-only validation capability was available; Vault status confirmed the connection."
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_CAPABILITIES_EXECUTE_OPERATION, {
|
|
2210
|
+
name: capability.name,
|
|
2211
|
+
input: {}
|
|
2212
|
+
}));
|
|
2213
|
+
return { available: true, capabilityName: capability.name, result: result.body };
|
|
2214
|
+
}
|
|
2215
|
+
async function genListLocalMigrationValidationCapabilities(client, integrationId) {
|
|
2216
|
+
const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_CAPABILITIES_SEARCH_OPERATION, {
|
|
2217
|
+
queries: LOCAL_MIGRATION_VALIDATION_SEARCH_QUERIES,
|
|
2218
|
+
types: ["action"],
|
|
2219
|
+
scope: { integrationId },
|
|
2220
|
+
mode: "keyword",
|
|
2221
|
+
limit: 5
|
|
2222
|
+
}));
|
|
2223
|
+
if (!result.body.success)
|
|
2224
|
+
return [];
|
|
2225
|
+
return getLocalMigrationCapabilitiesFromSearch(result.body.data);
|
|
2226
|
+
}
|
|
2227
|
+
function getLocalMigrationIntegrationsFromSearch(data) {
|
|
2228
|
+
return getLocalMigrationSearchResults(data).map((entry) => {
|
|
2229
|
+
const result = asRecord(entry);
|
|
2230
|
+
if (result?.type !== "integration")
|
|
2231
|
+
return null;
|
|
2232
|
+
const integration = asRecord(result.data);
|
|
2233
|
+
if (!integration)
|
|
2234
|
+
return null;
|
|
2235
|
+
const id = integration?.id;
|
|
2236
|
+
if (typeof id !== "string")
|
|
2237
|
+
return null;
|
|
2238
|
+
const catalogEntry = { id };
|
|
2239
|
+
if (typeof integration.name === "string")
|
|
2240
|
+
catalogEntry.name = integration.name;
|
|
2241
|
+
catalogEntry.providerAliases = getStringArray(integration.providerAliases);
|
|
2242
|
+
catalogEntry.mcpPackages = getStringArray(integration.mcpPackages);
|
|
2243
|
+
catalogEntry.commandAliases = getStringArray(integration.commandAliases);
|
|
2244
|
+
catalogEntry.hostAllowlist = getStringArray(integration.hostAllowlist);
|
|
2245
|
+
if (integration.readiness === "ready" || integration.readiness === "needs_user_setup") {
|
|
2246
|
+
catalogEntry.readiness = integration.readiness;
|
|
2247
|
+
}
|
|
2248
|
+
if (typeof integration.setupMode === "string")
|
|
2249
|
+
catalogEntry.setupMode = integration.setupMode;
|
|
2250
|
+
if (typeof integration.platformCredentialSummary === "string") {
|
|
2251
|
+
catalogEntry.platformCredentialSummary = integration.platformCredentialSummary;
|
|
2252
|
+
}
|
|
2253
|
+
return catalogEntry;
|
|
2254
|
+
}).filter((entry) => entry !== null);
|
|
2255
|
+
}
|
|
2256
|
+
function getLocalMigrationCapabilitiesFromSearch(data) {
|
|
2257
|
+
return getLocalMigrationSearchResults(data).map((entry) => {
|
|
2258
|
+
const result = asRecord(entry);
|
|
2259
|
+
const capability = asRecord(result?.data);
|
|
2260
|
+
const name = capability?.capabilityName ?? capability?.name;
|
|
2261
|
+
const description = capability?.description;
|
|
2262
|
+
if (typeof name !== "string" || typeof description !== "string")
|
|
2263
|
+
return null;
|
|
2264
|
+
return { name, description };
|
|
2265
|
+
}).filter((entry) => entry !== null);
|
|
2266
|
+
}
|
|
2267
|
+
function getLocalMigrationSearchResults(data, depth = 0) {
|
|
2268
|
+
if (depth > LOCAL_MIGRATION_SEARCH_RESULT_MAX_DEPTH)
|
|
2269
|
+
return [];
|
|
2270
|
+
const root = asRecord(data);
|
|
2271
|
+
if (Array.isArray(root?.results))
|
|
2272
|
+
return root.results;
|
|
2273
|
+
const resultsByQuery = asRecord(root?.resultsByQuery);
|
|
2274
|
+
if (!resultsByQuery)
|
|
2275
|
+
return [];
|
|
2276
|
+
return Object.values(resultsByQuery).flatMap((entry) => getLocalMigrationSearchResults(entry, depth + 1));
|
|
2277
|
+
}
|
|
2278
|
+
function asRecord(value) {
|
|
2279
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
2280
|
+
}
|
|
2281
|
+
function getStringArray(value) {
|
|
2282
|
+
if (!Array.isArray(value))
|
|
2283
|
+
return;
|
|
2284
|
+
const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
2285
|
+
return items.length > 0 ? items : undefined;
|
|
2286
|
+
}
|
|
2287
|
+
function chunkStrings(values, size) {
|
|
2288
|
+
const chunks = [];
|
|
2289
|
+
for (let index = 0;index < values.length; index += size) {
|
|
2290
|
+
chunks.push(values.slice(index, index + size));
|
|
2291
|
+
}
|
|
2292
|
+
return chunks;
|
|
2293
|
+
}
|
|
2294
|
+
async function trySubmitLocalMigrationFeedback(client, integrationId, comment) {
|
|
2295
|
+
try {
|
|
2296
|
+
const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_CAPABILITIES_FEEDBACK_OPERATION, {
|
|
2297
|
+
targetType: "capability",
|
|
2298
|
+
capabilityId: integrationId,
|
|
2299
|
+
capabilityType: "integration",
|
|
2300
|
+
tags: ["auth-failed"],
|
|
2301
|
+
comment: redactSecretLike(comment),
|
|
2302
|
+
surface: "aident-cli-local-migration"
|
|
2303
|
+
}));
|
|
2304
|
+
if (result.body.success) {
|
|
2305
|
+
await tryRecordLocalMigrationEvent(client, "feedback_submitted" /* FeedbackSubmitted */, {
|
|
2306
|
+
integrationId,
|
|
2307
|
+
status: "success"
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
if (process.env.AIDENT_CLI_DEBUG === "1") {
|
|
2312
|
+
logErr(`Feedback skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
function formatLocalMigrationValidationFeedback(entry) {
|
|
2317
|
+
const validation = entry.validation;
|
|
2318
|
+
const validationMethod = validation?.providerCapabilityName ? "read_only_capability" : "vault_status";
|
|
2319
|
+
const parts = [
|
|
2320
|
+
"Local integration migration validation failed.",
|
|
2321
|
+
`integrationId=${entry.integrationId}`,
|
|
2322
|
+
`validationMethod=${validationMethod}`
|
|
2323
|
+
];
|
|
2324
|
+
if (validation?.providerCapabilityName)
|
|
2325
|
+
parts.push(`providerCapability=${validation.providerCapabilityName}`);
|
|
2326
|
+
if (validation?.result?.error?.code)
|
|
2327
|
+
parts.push(`errorCode=${validation.result.error.code}`);
|
|
2328
|
+
return parts.join(" ");
|
|
2329
|
+
}
|
|
2330
|
+
function getLocalMigrationVaultStatus(data) {
|
|
2331
|
+
const root = asRecord(data);
|
|
2332
|
+
return {
|
|
2333
|
+
connectionStatus: getBooleanRecord(root?.connectionStatus),
|
|
2334
|
+
integrationStates: getIntegrationStateRecord(root?.integrationStates),
|
|
2335
|
+
normalizedIntegrationIds: getStringRecord(root?.normalizedIntegrationIds)
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
function getBooleanRecord(value) {
|
|
2339
|
+
const record = asRecord(value);
|
|
2340
|
+
if (!record)
|
|
2341
|
+
return;
|
|
2342
|
+
const result = {};
|
|
2343
|
+
for (const [key, item] of Object.entries(record)) {
|
|
2344
|
+
if (typeof item === "boolean")
|
|
2345
|
+
result[key] = item;
|
|
2346
|
+
}
|
|
2347
|
+
return result;
|
|
2348
|
+
}
|
|
2349
|
+
function getStringRecord(value) {
|
|
2350
|
+
const record = asRecord(value);
|
|
2351
|
+
if (!record)
|
|
2352
|
+
return;
|
|
2353
|
+
const result = {};
|
|
2354
|
+
for (const [key, item] of Object.entries(record)) {
|
|
2355
|
+
if (typeof item === "string")
|
|
2356
|
+
result[key] = item;
|
|
2357
|
+
}
|
|
2358
|
+
return result;
|
|
2359
|
+
}
|
|
2360
|
+
function getIntegrationStateRecord(value) {
|
|
2361
|
+
const record = asRecord(value);
|
|
2362
|
+
if (!record)
|
|
2363
|
+
return;
|
|
2364
|
+
const result = {};
|
|
2365
|
+
for (const [key, item] of Object.entries(record)) {
|
|
2366
|
+
const state = asRecord(item);
|
|
2367
|
+
if (!state)
|
|
2368
|
+
continue;
|
|
2369
|
+
result[key] = {
|
|
2370
|
+
readiness: typeof state.readiness === "string" ? state.readiness : undefined,
|
|
2371
|
+
setupMode: typeof state.setupMode === "string" ? state.setupMode : undefined
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
return result;
|
|
2375
|
+
}
|
|
2376
|
+
function getLocalMigrationVaultIntegrationIds(status, integrationId) {
|
|
2377
|
+
return [integrationId, status.normalizedIntegrationIds?.[integrationId]].filter(isString2);
|
|
2378
|
+
}
|
|
2379
|
+
function getLocalMigrationVaultIntegrationState(status, integrationId) {
|
|
2380
|
+
return getLocalMigrationVaultIntegrationIds(status, integrationId).map((id) => status.integrationStates?.[id]).find((state) => state !== undefined);
|
|
2381
|
+
}
|
|
2382
|
+
function getLocalMigrationReadiness(value) {
|
|
2383
|
+
if (value === "ready" || value === "needs_user_setup")
|
|
2384
|
+
return value;
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
async function genRecordLocalMigrationApplyTelemetry(client, result) {
|
|
2388
|
+
for (const entry of result.connectResults) {
|
|
2389
|
+
if (entry.skipped)
|
|
2390
|
+
continue;
|
|
2391
|
+
await tryRecordLocalMigrationEvent(client, "candidate_connected" /* CandidateConnected */, {
|
|
2392
|
+
integrationId: entry.integrationId,
|
|
2393
|
+
status: entry.result?.success ? "success" : "failure"
|
|
2394
|
+
});
|
|
2395
|
+
if (entry.validation) {
|
|
2396
|
+
await tryRecordLocalMigrationEvent(client, entry.validation.success ? "validation_succeeded" /* ValidationSucceeded */ : "validation_failed" /* ValidationFailed */, {
|
|
2397
|
+
integrationId: entry.integrationId,
|
|
2398
|
+
status: entry.validation.success ? "success" : "failure"
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
await tryRecordLocalMigrationEvent(client, "migration_completed" /* MigrationCompleted */, {
|
|
2403
|
+
...getLocalMigrationPlanTelemetry(result.plan),
|
|
2404
|
+
selectedCount: result.selectedIntegrationIds.length,
|
|
2405
|
+
connectedCount: result.connectResults.filter((entry) => entry.validation?.success === true).length,
|
|
2406
|
+
failedCount: result.connectResults.filter(isFailedLocalMigrationConnectResult).length,
|
|
2407
|
+
skippedCount: result.connectResults.filter((entry) => entry.skipped).length,
|
|
2408
|
+
status: result.connectResults.some(isFailedLocalMigrationConnectResult) ? "failure" : "success"
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
function isFailedLocalMigrationConnectResult(entry) {
|
|
2412
|
+
return entry.result?.success === false || entry.validation?.success === false;
|
|
2413
|
+
}
|
|
2414
|
+
function isString2(value) {
|
|
2415
|
+
return typeof value === "string" && value.trim() !== "";
|
|
2416
|
+
}
|
|
2417
|
+
function getLocalMigrationPlanTelemetry(plan) {
|
|
2418
|
+
return {
|
|
2419
|
+
candidateCount: plan.candidates.length,
|
|
2420
|
+
supportedCount: countCandidates(plan.candidates, "supported"),
|
|
2421
|
+
alreadyConnectedCount: countCandidates(plan.candidates, "already_connected"),
|
|
2422
|
+
needsUserConnectionCount: countCandidates(plan.candidates, "needs_user_connection"),
|
|
2423
|
+
unsupportedCount: countCandidates(plan.candidates, "unsupported")
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
function countCandidates(candidates, status) {
|
|
2427
|
+
return candidates.filter((candidate) => candidate.status === status).length;
|
|
2428
|
+
}
|
|
2429
|
+
function renderLocalMigrationPlan(plan, format) {
|
|
2430
|
+
if (format === "json") {
|
|
2431
|
+
logInfo(JSON.stringify({ success: true, data: plan }));
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
logInfo(`${colors.bold}Local integration migration plan${colors.reset}`);
|
|
2435
|
+
logInfo(formatLocalIntegrationMigrationPlan(plan));
|
|
2436
|
+
const connectableIds = plan.candidates.filter(isConnectableCandidate).map((candidate) => candidate.loadoutIntegrationId).filter((value) => typeof value === "string");
|
|
2437
|
+
if (connectableIds.length > 0) {
|
|
2438
|
+
logInfo("");
|
|
2439
|
+
logInfo(`Connect selected integrations with ${colors.cyan}aident integrations migrate-local --apply --integrationIds ${connectableIds.join(",")}${colors.reset}`);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
function renderLocalMigrationApplyResult(result, format) {
|
|
2443
|
+
const failed = result.connectResults.some(isFailedLocalMigrationConnectResult);
|
|
2444
|
+
if (format === "json") {
|
|
2445
|
+
logInfo(JSON.stringify({ success: !failed, data: result }));
|
|
2446
|
+
if (failed)
|
|
2447
|
+
process.exitCode = 1;
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
logInfo(`${colors.bold}Loadout connection results${colors.reset}`);
|
|
2451
|
+
for (const entry of result.connectResults) {
|
|
2452
|
+
if (entry.skipped) {
|
|
2453
|
+
logInfo(` ${colors.yellow}-${colors.reset} ${entry.integrationId}: skipped (${entry.skipped})`);
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
if (entry.result?.success) {
|
|
2457
|
+
const validation = entry.validation?.success === true ? ` ${colors.dim}(${getLocalMigrationValidationLabel(entry.validation)})${colors.reset}` : "";
|
|
2458
|
+
logInfo(` ${colors.green}ok${colors.reset} ${entry.integrationId}${validation}`);
|
|
2459
|
+
if (entry.result.data !== undefined)
|
|
2460
|
+
logInfo(JSON.stringify(entry.result.data, null, 2));
|
|
2461
|
+
if (entry.validation?.success === true && entry.validation.message) {
|
|
2462
|
+
logInfo(` ${colors.dim}${entry.validation.message}${colors.reset}`);
|
|
2463
|
+
}
|
|
2464
|
+
if (entry.validation?.success === false) {
|
|
2465
|
+
logErr(` ${colors.red}x${colors.reset} ${entry.integrationId}: ${entry.validation.message ?? "Validation failed"}`);
|
|
2466
|
+
}
|
|
2467
|
+
} else {
|
|
2468
|
+
logErr(` ${colors.red}x${colors.reset} ${entry.integrationId}: ${entry.result?.error?.message ?? "Unable to connect"}`);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
if (failed)
|
|
2472
|
+
process.exitCode = 1;
|
|
2473
|
+
}
|
|
2474
|
+
function getLocalMigrationValidationLabel(validation) {
|
|
2475
|
+
if (validation.providerCapabilityName)
|
|
2476
|
+
return "provider validated";
|
|
2477
|
+
if (validation.message)
|
|
2478
|
+
return "provider validation skipped";
|
|
2479
|
+
return "connection confirmed";
|
|
2480
|
+
}
|
|
2481
|
+
function parseIntegrationIdsFlag(parsed, plan) {
|
|
2482
|
+
const raw = parsed.flags["integrationIds"];
|
|
2483
|
+
const parseValues = (values) => {
|
|
2484
|
+
const tokens = values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
2485
|
+
if (tokens.some((token) => token.toLowerCase() === "all"))
|
|
2486
|
+
return parseLocalIntegrationSelection("all", plan);
|
|
2487
|
+
return tokens;
|
|
2488
|
+
};
|
|
2489
|
+
if (typeof raw === "string") {
|
|
2490
|
+
return parseValues([raw]);
|
|
2491
|
+
}
|
|
2492
|
+
if (Array.isArray(raw)) {
|
|
2493
|
+
return parseValues(raw.filter((value) => typeof value === "string"));
|
|
2494
|
+
}
|
|
2495
|
+
return [];
|
|
2496
|
+
}
|
|
2497
|
+
function getLocalMigrationSelectionRequiredMessage(parsed, plan) {
|
|
2498
|
+
const raw = parsed.flags["integrationIds"];
|
|
2499
|
+
if (typeof raw === "string")
|
|
2500
|
+
return formatLocalIntegrationSelectionFailure(raw, plan);
|
|
2501
|
+
if (Array.isArray(raw))
|
|
2502
|
+
return formatLocalIntegrationSelectionFailure(raw.join(","), plan);
|
|
2503
|
+
return "Pass --integrationIds with comma-separated integration IDs, or --integrationIds all.";
|
|
2504
|
+
}
|
|
2505
|
+
function emitLocalCommandResult(format, success, data, code, message) {
|
|
2506
|
+
if (format === "json") {
|
|
2507
|
+
logInfo(JSON.stringify({
|
|
2508
|
+
success,
|
|
2509
|
+
data,
|
|
2510
|
+
error: success ? undefined : { code, message }
|
|
2511
|
+
}));
|
|
2512
|
+
} else if (success) {
|
|
2513
|
+
logInfo(JSON.stringify(data, null, 2));
|
|
2514
|
+
} else {
|
|
2515
|
+
logErr(`${colors.red}x ${code}:${colors.reset} ${message}`);
|
|
2516
|
+
}
|
|
2517
|
+
if (!success)
|
|
2518
|
+
process.exitCode = 1;
|
|
2519
|
+
}
|
|
2520
|
+
function renderLocalIntegrationMigrationHelp() {
|
|
2521
|
+
return [
|
|
2522
|
+
`${colors.bold}aident integrations migrate-local${colors.reset}`,
|
|
2523
|
+
"",
|
|
2524
|
+
"Plan or start migration from local MCP/agent integration configs into Aident Loadout.",
|
|
2525
|
+
"",
|
|
2526
|
+
"USAGE:",
|
|
2527
|
+
" aident integrations migrate-local [--json]",
|
|
2528
|
+
" aident integrations migrate-local --apply --integrationIds <ids|all> [--json]",
|
|
2529
|
+
"",
|
|
2530
|
+
"The command scans known local MCP config files, redacts secret-like values, maps supported providers to Loadout,",
|
|
2531
|
+
"and uses existing Vault/OAuth connect flows. Applying a migration requires an explicit selected subset or all."
|
|
2532
|
+
].join(`
|
|
2533
|
+
`);
|
|
2534
|
+
}
|
|
2535
|
+
function isAffirmative(value) {
|
|
2536
|
+
return value === "y" || value === "yes";
|
|
2537
|
+
}
|
|
1233
2538
|
async function runCommand(parsed) {
|
|
1234
2539
|
const client = await getAuthenticatedClient(await getRequestedPackages(parsed));
|
|
1235
2540
|
if (!client) {
|
|
2541
|
+
if (parsed.isHelp) {
|
|
2542
|
+
emitLocalHelp(parsed.format);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
1236
2545
|
if (parsed.format === "json") {
|
|
1237
2546
|
logInfo(JSON.stringify({ success: false, error: { code: "not-authenticated", message: "Run `aident login` first." } }));
|
|
1238
2547
|
} else {
|
|
@@ -1241,9 +2550,12 @@ async function runCommand(parsed) {
|
|
|
1241
2550
|
process.exitCode = 1;
|
|
1242
2551
|
return;
|
|
1243
2552
|
}
|
|
1244
|
-
const catalog = await fetchCatalog(client);
|
|
1245
|
-
if (!catalog)
|
|
2553
|
+
const catalog = await fetchCatalog(client, { suppressAuthError: parsed.isHelp });
|
|
2554
|
+
if (!catalog) {
|
|
2555
|
+
if (parsed.isHelp && !process.exitCode)
|
|
2556
|
+
emitLocalHelp(parsed.format);
|
|
1246
2557
|
return;
|
|
2558
|
+
}
|
|
1247
2559
|
const resolved = resolveCommand(parsed.positional, catalog.commands);
|
|
1248
2560
|
if (!resolved?.command) {
|
|
1249
2561
|
if (resolved && (parsed.isHelp || parsed.format === "tui")) {
|
|
@@ -1334,6 +2646,7 @@ async function getRequestedPackages(parsed) {
|
|
|
1334
2646
|
}
|
|
1335
2647
|
async function getAuthenticatedClient(packages) {
|
|
1336
2648
|
const activePackages = packages ?? await resolveDefaultPackages();
|
|
2649
|
+
const installedSkillVersion = await findInstalledLoadoutSkillVersion();
|
|
1337
2650
|
const envToken = process.env.AIDENT_TOKEN;
|
|
1338
2651
|
const envBaseUrl = process.env.AIDENT_BASE_URL?.trim();
|
|
1339
2652
|
if (envToken) {
|
|
@@ -1342,7 +2655,7 @@ async function getAuthenticatedClient(packages) {
|
|
|
1342
2655
|
base_url: normalizeBaseUrl(baseUrl),
|
|
1343
2656
|
client_id: "",
|
|
1344
2657
|
access_token: envToken
|
|
1345
|
-
}, "env", activePackages);
|
|
2658
|
+
}, "env", activePackages, installedSkillVersion);
|
|
1346
2659
|
}
|
|
1347
2660
|
let creds = await readCredentials();
|
|
1348
2661
|
if (!creds)
|
|
@@ -1357,17 +2670,19 @@ async function getAuthenticatedClient(packages) {
|
|
|
1357
2670
|
creds = refreshed;
|
|
1358
2671
|
}
|
|
1359
2672
|
}
|
|
1360
|
-
return new CliClient(creds, "stored", activePackages);
|
|
2673
|
+
return new CliClient(creds, "stored", activePackages, installedSkillVersion);
|
|
1361
2674
|
}
|
|
1362
2675
|
var catalogCache = null;
|
|
1363
|
-
async function fetchCatalog(client) {
|
|
2676
|
+
async function fetchCatalog(client, options = {}) {
|
|
1364
2677
|
const packagesKey = client.activePackages.join(",");
|
|
1365
2678
|
if (catalogCache && catalogCache.packagesKey === packagesKey)
|
|
1366
2679
|
return catalogCache.catalog;
|
|
1367
2680
|
const res = await callWithRefresh(client, (c) => c.getCatalog());
|
|
1368
2681
|
if (res.status === 401) {
|
|
1369
|
-
|
|
1370
|
-
|
|
2682
|
+
if (!options.suppressAuthError) {
|
|
2683
|
+
logErr(`Not authenticated (HTTP ${res.status}). Run \`aident login\`.`);
|
|
2684
|
+
process.exitCode = 1;
|
|
2685
|
+
}
|
|
1371
2686
|
return null;
|
|
1372
2687
|
}
|
|
1373
2688
|
if (res.status !== 200 || !isCommandCatalog(res.body)) {
|
|
@@ -1376,8 +2691,18 @@ async function fetchCatalog(client) {
|
|
|
1376
2691
|
return null;
|
|
1377
2692
|
}
|
|
1378
2693
|
catalogCache = { packagesKey, catalog: res.body };
|
|
2694
|
+
for (const warning of formatLoadoutSkillWarnings(res.body.loadoutSkill)) {
|
|
2695
|
+
logErr(warning);
|
|
2696
|
+
}
|
|
1379
2697
|
return catalogCache.catalog;
|
|
1380
2698
|
}
|
|
2699
|
+
function emitLocalHelp(format) {
|
|
2700
|
+
if (format === "json") {
|
|
2701
|
+
logInfo(JSON.stringify(getLocalHelp(VERSION)));
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
logInfo(renderLocalHelp(VERSION));
|
|
2705
|
+
}
|
|
1381
2706
|
function formatConfigValue(value) {
|
|
1382
2707
|
return Array.isArray(value) ? value.join(",") : String(value);
|
|
1383
2708
|
}
|