@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.
Files changed (3) hide show
  1. package/README.md +10 -5
  2. package/dist/cli.mjs +1429 -104
  3. 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
- process.stdin.removeListener("data", onData);
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://localhost:${port}/callback`;
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: ${authorizeUrl.toString()}`);
112
- openBrowser(authorizeUrl.toString());
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.close();
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
- const clientId = await registerClient(baseUrl, [redirectUri]);
128
- const authorizeUrl = new URL(`${baseUrl}/api/mcp/oauth/authorize`);
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
- const tok = await exchangeCode(baseUrl, clientId, pasted, redirectUri, verifier);
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(pasted)) {
149
- throw new Error("Pasted value is neither a valid authorization code nor a recognizable access token. Please retry the login.");
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: pasted };
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 timeout = setTimeout(() => {
249
- codeRejecter(new Error(`No callback received within ${LOOPBACK_TIMEOUT_MS / 1000}s; aborting.`));
250
- }, LOOPBACK_TIMEOUT_MS);
251
- codePromise.finally(() => clearTimeout(timeout));
252
- const server = createServer((req, res) => {
253
- if (!req.url) {
254
- res.writeHead(400);
255
- res.end("Bad request");
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
- if (!code || state !== expectedState) {
274
- res.writeHead(400, { "Content-Type": "text/html" });
275
- res.end("<html><body><h2>Invalid callback</h2></body></html>");
276
- codeRejecter(new Error("Invalid callback (missing code or bad state)"));
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
- res.writeHead(200, { "Content-Type": "text/html" });
280
- 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>`);
281
- codeResolver(code);
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, "127.0.0.1", () => {
356
+ server.listen(0, LOOPBACK_HOST, () => {
285
357
  const addr = server.address();
286
- resolve({ server, port: addr.port, codePromise });
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.0";
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 result = await this.fetchJson("GET", this.catalogPath(packageName));
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://app.aident.ai";
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 - 60000;
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 \`npm install -g @aident-ai/cli@latest\``
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 \`npm install -g @aident-ai/cli@latest\` to upgrade.`;
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
- if (parsed.format === "json") {
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
- logErr(`Not authenticated (HTTP ${res.status}). Run \`aident login\`.`);
1370
- process.exitCode = 1;
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
  }