@aident-ai/cli 0.1.7 → 0.1.8-rc.1

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 +5 -0
  2. package/dist/cli.mjs +165 -62
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -28,6 +28,11 @@ aident update --check
28
28
  aident --help
29
29
  ```
30
30
 
31
+ `aident setup` installs or updates the verified Aident Skill globally, migrates recognized clean project copies that
32
+ could shadow it, reuses or opens Aident authentication, verifies Loadout access, and records completion. Add
33
+ `--client-name <name>` to record the agent client and `--json` for a single machine-readable report; a configured
34
+ `AIDENT_TOKEN` makes the flow non-interactive.
35
+
31
36
  Use `--oob` for browserless auth environments:
32
37
 
33
38
  ```bash
package/dist/cli.mjs CHANGED
@@ -45,7 +45,7 @@ function logErr(text) {
45
45
  // src/prompt.ts
46
46
  function readLine(prompt, options = {}) {
47
47
  return new Promise((resolve, reject) => {
48
- process.stdout.write(prompt);
48
+ (options.output ?? process.stdout).write(prompt);
49
49
  let data = "";
50
50
  let timeout;
51
51
  const cleanup = () => {
@@ -86,13 +86,14 @@ var LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS = 5 * 60 * 1000;
86
86
  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.`;
87
87
  async function login(options) {
88
88
  const baseUrl = options.baseUrl.replace(/\/+$/, "");
89
+ const writeInfo = options.format === "json" ? logErr : logInfo;
89
90
  if (options.oob)
90
- return loginOob(baseUrl);
91
+ return loginOob(baseUrl, writeInfo, options.format === "json" ? process.stderr : process.stdout);
91
92
  try {
92
- return await loginLoopback(baseUrl);
93
+ return await loginLoopback(baseUrl, writeInfo);
93
94
  } catch (err) {
94
95
  logErr(`Loopback OAuth failed (${err instanceof Error ? err.message : String(err)}). Falling back to OOB flow.`);
95
- return loginOob(baseUrl);
96
+ return loginOob(baseUrl, writeInfo, options.format === "json" ? process.stderr : process.stdout);
96
97
  }
97
98
  }
98
99
  async function refreshToken(creds) {
@@ -127,11 +128,11 @@ async function logout(creds) {
127
128
  return;
128
129
  });
129
130
  }
130
- async function loginLoopback(baseUrl) {
131
+ async function loginLoopback(baseUrl, writeInfo) {
131
132
  const verifier = base64UrlEncode(randomBytes(48));
132
133
  const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
133
134
  const state = base64UrlEncode(randomBytes(16));
134
- const { server, port, codePromise, setExpectedOpenRedirectUrl } = await startCallbackServer(state);
135
+ const { server, port, codePromise, setExpectedOpenRedirectUrl } = await startCallbackServer(state, writeInfo);
135
136
  const redirectUri = `http://${LOOPBACK_HOST}:${port}/callback`;
136
137
  const clientId = await registerClient(baseUrl, [redirectUri]);
137
138
  const authorizeUrl = new URL(`${baseUrl}/api/mcp/oauth/authorize`);
@@ -145,8 +146,8 @@ async function loginLoopback(baseUrl) {
145
146
  const authorizeUrlString = authorizeUrl.toString();
146
147
  setExpectedOpenRedirectUrl(authorizeUrlString);
147
148
  const loginOpenUrl = buildLoginOpenUrl(port, state, authorizeUrlString);
148
- logInfo(`Opening browser for Aident login...`);
149
- logInfo(`If the browser does not open, visit: ${authorizeUrlString}`);
149
+ writeInfo(`Opening browser for Aident login...`);
150
+ writeInfo(`If the browser does not open, visit: ${authorizeUrlString}`);
150
151
  openBrowser(shouldUseLoopbackBrowserHandoff() ? loginOpenUrl : authorizeUrlString);
151
152
  let code;
152
153
  try {
@@ -157,7 +158,7 @@ async function loginLoopback(baseUrl) {
157
158
  const tok = await exchangeCode(baseUrl, clientId, code, redirectUri, verifier);
158
159
  return buildCreds(baseUrl, clientId, tok);
159
160
  }
160
- async function loginOob(baseUrl) {
161
+ async function loginOob(baseUrl, writeInfo, promptOutput) {
161
162
  const redirectUri = `${baseUrl}/mcp/oob`;
162
163
  const verifier = base64UrlEncode(randomBytes(48));
163
164
  const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
@@ -174,11 +175,12 @@ async function loginOob(baseUrl) {
174
175
  authorizeUrl.searchParams.set("code_challenge_method", "S256");
175
176
  authorizeUrl.searchParams.set("state", state);
176
177
  const authorizeUrlString = authorizeUrl.toString();
177
- logInfo(`Opening browser for Aident login...`);
178
- logInfo(`If the browser does not open, visit: ${authorizeUrlString}`);
178
+ writeInfo(`Opening browser for Aident login...`);
179
+ writeInfo(`If the browser does not open, visit: ${authorizeUrlString}`);
179
180
  openBrowser(authorizeUrlString);
180
- logInfo("After approving, paste the 8-digit verification code shown on the Aident page below.");
181
+ writeInfo("After approving, paste the 8-digit verification code shown on the Aident page below.");
181
182
  const pastedInput = await readLine("Paste 8-digit code here: ", {
183
+ output: promptOutput,
182
184
  timeoutMs: LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS,
183
185
  timeoutMessage: `Verification code not received within ${LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS / 1000}s; aborting.`
184
186
  });
@@ -288,7 +290,7 @@ function updateCredsFromToken(creds, tok) {
288
290
  expires_at: expiresAt
289
291
  };
290
292
  }
291
- function startCallbackServer(expectedState) {
293
+ function startCallbackServer(expectedState, writeInfo = logInfo) {
292
294
  return new Promise((resolve, reject) => {
293
295
  let codeResolver = () => {
294
296
  return;
@@ -310,7 +312,7 @@ function startCallbackServer(expectedState) {
310
312
  };
311
313
  const openTimeout = setTimeout(() => {
312
314
  if (!opened) {
313
- logInfo(`Login window open signal not received within ${LOGIN_OPEN_TIMEOUT_MS / 1000}s; still waiting for login to finish.`);
315
+ writeInfo(`Login window open signal not received within ${LOGIN_OPEN_TIMEOUT_MS / 1000}s; still waiting for login to finish.`);
314
316
  }
315
317
  }, LOGIN_OPEN_TIMEOUT_MS);
316
318
  scheduleCompleteTimeout(LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS, `Authorization callback not received within ${LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS / 1000}s; aborting.`);
@@ -328,7 +330,7 @@ function startCallbackServer(expectedState) {
328
330
  return;
329
331
  opened = true;
330
332
  clearTimeout(openTimeout);
331
- logInfo(LOGIN_OPENED_MESSAGE);
333
+ writeInfo(LOGIN_OPENED_MESSAGE);
332
334
  const timeoutMs = LOGIN_COMPLETE_TIMEOUT_AFTER_OPENED_MS;
333
335
  scheduleCompleteTimeout(timeoutMs, `Login page opened but did not complete within ${timeoutMs / 1000}s; aborting.`);
334
336
  };
@@ -596,7 +598,7 @@ function normalizeBaseUrl(url) {
596
598
  }
597
599
 
598
600
  // src/version.ts
599
- var VERSION = "0.1.7";
601
+ var VERSION = "0.1.8-rc.1";
600
602
 
601
603
  // src/catalogCache.ts
602
604
  var CACHE_TTL_MS = 5 * 60 * 1000;
@@ -6177,6 +6179,30 @@ function mergeCatalogs(catalogs, packages, onCommand) {
6177
6179
  // src/credentials.ts
6178
6180
  import { mkdir as mkdir5, readFile as readFile6, rm, writeFile as writeFile5 } from "node:fs/promises";
6179
6181
  import { join as join7 } from "node:path";
6182
+
6183
+ // ../web/shared/loadout/AidentCredentialOrigin.mjs
6184
+ function getSharedAidentOrigin(value) {
6185
+ try {
6186
+ const url = new URL(value);
6187
+ if (url.protocol !== "https:" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "" || url.hostname !== "aident.ai" && !url.hostname.endsWith(".aident.ai")) {
6188
+ return null;
6189
+ }
6190
+ return url.origin;
6191
+ } catch {
6192
+ return null;
6193
+ }
6194
+ }
6195
+ function resolveAidentCredentialBaseUrl(credentialsBaseUrl, requestedBaseUrl) {
6196
+ const credentialsUrl = credentialsBaseUrl.replace(/\/+$/, "");
6197
+ const requestedUrl = requestedBaseUrl.replace(/\/+$/, "");
6198
+ if (credentialsUrl === requestedUrl)
6199
+ return requestedUrl;
6200
+ if (!getSharedAidentOrigin(credentialsUrl))
6201
+ return null;
6202
+ return getSharedAidentOrigin(requestedUrl);
6203
+ }
6204
+
6205
+ // src/credentials.ts
6180
6206
  var REFRESH_WINDOW_MS = 24 * 60 * 60 * 1000;
6181
6207
  function getCredentialsFile() {
6182
6208
  return join7(getAidentDir(), "credentials.json");
@@ -6200,16 +6226,8 @@ async function clearCredentials() {
6200
6226
  await rm(getCredentialsFile(), { force: true });
6201
6227
  }
6202
6228
  function credentialsForBaseUrl(creds, baseUrl) {
6203
- const credentialsBaseUrl = creds.base_url.replace(/\/+$/, "");
6204
- const requestedBaseUrl = baseUrl.replace(/\/+$/, "");
6205
- if (credentialsBaseUrl === requestedBaseUrl)
6206
- return { ...creds, base_url: requestedBaseUrl };
6207
- if (!getSharedAidentBaseUrl(credentialsBaseUrl))
6208
- return null;
6209
- const sharedBaseUrl = getSharedAidentBaseUrl(requestedBaseUrl);
6210
- if (!sharedBaseUrl)
6211
- return null;
6212
- return { ...creds, base_url: sharedBaseUrl };
6229
+ const resolvedBaseUrl = resolveAidentCredentialBaseUrl(creds.base_url, baseUrl);
6230
+ return resolvedBaseUrl ? { ...creds, base_url: resolvedBaseUrl } : null;
6213
6231
  }
6214
6232
  async function clearCredentialsForIncompatibleBaseUrl(baseUrl) {
6215
6233
  const creds = await readCredentials();
@@ -6226,17 +6244,6 @@ function isExpired(creds) {
6226
6244
  return false;
6227
6245
  return Date.now() >= expiresAt - REFRESH_WINDOW_MS;
6228
6246
  }
6229
- function getSharedAidentBaseUrl(baseUrl) {
6230
- try {
6231
- const url = new URL(baseUrl);
6232
- if (url.protocol !== "https:" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "" || url.hostname !== "aident.ai" && !url.hostname.endsWith(".aident.ai")) {
6233
- return null;
6234
- }
6235
- return url.origin;
6236
- } catch {
6237
- return null;
6238
- }
6239
- }
6240
6247
 
6241
6248
  // src/doctor.ts
6242
6249
  import { existsSync as existsSync2 } from "node:fs";
@@ -6449,7 +6456,7 @@ var LOCAL_HELP_COMMANDS = [
6449
6456
  { command: "config get <key>", description: "Read a single value" },
6450
6457
  { command: "packages add <playbook|intern>", description: "Enable an add-on package" },
6451
6458
  { command: "doctor", description: "Validate installation" },
6452
- { command: "setup", description: "Interactive setup wizard" },
6459
+ { command: "setup", description: "Install Aident globally, authenticate, and verify Loadout" },
6453
6460
  { command: "integrations migrate-local", description: "Plan migration from local MCP configs to Loadout" },
6454
6461
  { command: "<domain> <command> [--flag value ...] [--json]", description: "Run a catalog command after login" }
6455
6462
  ];
@@ -6467,7 +6474,7 @@ function getLocalHelp(version) {
6467
6474
  "aident config get <key>",
6468
6475
  "aident packages add <playbook|intern>",
6469
6476
  "aident doctor",
6470
- "aident setup",
6477
+ "aident setup [--client-name <name>]",
6471
6478
  "aident integrations migrate-local",
6472
6479
  "aident <domain> <command> [--flag value ...] [--json]"
6473
6480
  ],
@@ -9432,32 +9439,128 @@ function formatAidentUpdateReport(report) {
9432
9439
  `);
9433
9440
  }
9434
9441
  async function runSetup(parsed) {
9435
- logInfo(`${colors.bold}Aident CLI setup${colors.reset}`);
9436
- logInfo("");
9437
- const current = await resolveDefaultBaseUrl();
9438
- const input = (await readLine(`Base URL [${current}]: `)).trim();
9439
- const chosen = input ? normalizeBaseUrl(input) : current;
9440
- if (chosen !== current) {
9441
- await setConfigValue("baseUrl", chosen);
9442
- logInfo(`${colors.green}Saved${colors.reset} baseUrl = ${chosen}`);
9442
+ if (parsed.isHelp) {
9443
+ logInfo(renderSetupHelp());
9444
+ return;
9443
9445
  }
9444
- logInfo("");
9445
- const existing = await readCredentials();
9446
- if (existing && credentialsForBaseUrl(existing, chosen)) {
9447
- logInfo(`${colors.green}Already signed in${colors.reset} to ${chosen}.`);
9448
- } else {
9449
- const proceed = (await readLine(`Open browser to authenticate now? [Y/n]: `)).trim().toLowerCase();
9450
- if (proceed === "" || proceed === "y" || proceed === "yes") {
9451
- const creds = await login({ baseUrl: chosen });
9452
- await writeCredentials(creds);
9453
- logInfo(`${colors.green}Signed in to ${creds.base_url}${colors.reset}`);
9454
- } else {
9455
- logInfo("Skipped login. Run `aident login` whenever you want.");
9446
+ const report = { schemaVersion: 1, success: false };
9447
+ let errorCode = "invalid-input";
9448
+ try {
9449
+ const options = await getSetupOptions(parsed);
9450
+ errorCode = "setup-failed";
9451
+ report.baseUrl = options.baseUrl;
9452
+ report.clientName = options.clientName;
9453
+ if (options.persistBaseUrl)
9454
+ await setConfigValue("baseUrl", options.baseUrl);
9455
+ const update = await runAidentUpdate({
9456
+ baseUrl: options.baseUrl,
9457
+ currentVersion: VERSION,
9458
+ mode: "apply" /* Apply */,
9459
+ includeCli: true,
9460
+ includeSkill: true,
9461
+ includeProject: true
9462
+ });
9463
+ recordLoadoutUpdateTelemetry(options.baseUrl, getLoadoutUpdateCompletionTelemetry(update));
9464
+ report.update = update;
9465
+ if (!update.success) {
9466
+ throw new Error(update.errors?.[0]?.message ?? update.skill?.detail ?? "Aident installation was incomplete");
9467
+ }
9468
+ const skillVersion = update.skill?.targetVersion;
9469
+ let credentials = await readCredentials();
9470
+ const hasEnvironmentToken = !!process.env.AIDENT_TOKEN?.trim();
9471
+ if ((!credentials || !credentialsForBaseUrl(credentials, options.baseUrl)) && !hasEnvironmentToken) {
9472
+ credentials = await login({ baseUrl: options.baseUrl, format: parsed.format, oob: options.oob });
9473
+ await writeCredentials(credentials);
9474
+ }
9475
+ const client = await getAuthenticatedClient(["loadout"]);
9476
+ if (!client)
9477
+ throw new Error("Aident credentials could not be loaded after sign-in");
9478
+ const catalog = await fetchCatalog(client);
9479
+ if (!catalog)
9480
+ throw new Error("Aident Loadout command catalog is unavailable");
9481
+ const identity = await callWithRefresh(client, (activeClient) => activeClient.exec("account", "auth status", {}));
9482
+ if (!identity.body.success) {
9483
+ throw new Error(identity.body.error?.message ?? "Aident account verification failed");
9484
+ }
9485
+ report.authenticated = true;
9486
+ report.account = identity.body.data;
9487
+ const doctor = await runDoctor({
9488
+ baseUrl: options.baseUrl,
9489
+ configFile: getConfigFile(),
9490
+ credentialsFile: getCredentialsFile()
9491
+ });
9492
+ report.doctor = doctor;
9493
+ if (!doctor.ok)
9494
+ throw new Error("Aident installation verification failed");
9495
+ const completion = await callWithRefresh(client, (activeClient) => activeClient.exec("loadout", "setup complete", {
9496
+ clientName: options.clientName,
9497
+ ...skillVersion ? { skillVersion } : {}
9498
+ }));
9499
+ if (!completion.body.success || !completion.body.data?.recorded) {
9500
+ throw new Error(completion.body.error?.message ?? "Aident setup completion was not recorded");
9456
9501
  }
9502
+ report.completion = completion.body.data;
9503
+ report.success = true;
9504
+ await maybeOfferLocalIntegrationMigration(parsed);
9505
+ emitSetupReport(parsed.format, report);
9506
+ } catch (error) {
9507
+ report.error = {
9508
+ code: errorCode,
9509
+ message: error instanceof Error ? error.message : String(error)
9510
+ };
9511
+ emitSetupReport(parsed.format, report);
9512
+ process.exitCode = 1;
9457
9513
  }
9458
- await maybeOfferLocalIntegrationMigration(parsed);
9459
- logInfo("");
9460
- logInfo(`Done. Try ${colors.cyan}aident --help${colors.reset} or ${colors.cyan}aident doctor${colors.reset}.`);
9514
+ }
9515
+ async function getSetupOptions(parsed) {
9516
+ if (parsed.positional.length !== 1)
9517
+ throw new Error("Usage: aident setup [flags]");
9518
+ const allowedFlags = new Set(["base-url", "client-name", "oob"]);
9519
+ const unknownFlag = Object.keys(parsed.flags).find((flag) => !allowedFlags.has(flag));
9520
+ if (unknownFlag)
9521
+ throw new Error(`Unknown setup flag: --${unknownFlag}`);
9522
+ const rawBaseUrl = getOptionalStringFlag(parsed.flags, "base-url");
9523
+ const clientName = getOptionalStringFlag(parsed.flags, "client-name") ?? "Aident CLI";
9524
+ if (clientName.length > 160)
9525
+ throw new Error("--client-name must be at most 160 characters");
9526
+ return {
9527
+ baseUrl: rawBaseUrl ? normalizeBaseUrl(rawBaseUrl) : await resolveDefaultBaseUrl(),
9528
+ clientName,
9529
+ oob: getBooleanUpdateFlag(parsed.flags, "oob"),
9530
+ persistBaseUrl: !!rawBaseUrl
9531
+ };
9532
+ }
9533
+ function getOptionalStringFlag(flags, name) {
9534
+ const value = flags[name];
9535
+ if (value === undefined)
9536
+ return;
9537
+ if (typeof value !== "string" || !value.trim())
9538
+ throw new Error(`--${name} requires a value`);
9539
+ return value.trim();
9540
+ }
9541
+ function emitSetupReport(format, report) {
9542
+ if (format === "json") {
9543
+ logInfo(JSON.stringify(report));
9544
+ return;
9545
+ }
9546
+ if (report.success) {
9547
+ logInfo(`${colors.green}Aident Loadout setup is complete.${colors.reset}`);
9548
+ return;
9549
+ }
9550
+ const error = report.error;
9551
+ logErr(`${colors.red}Aident Loadout setup failed:${colors.reset} ${error?.message ?? "Unknown error"}`);
9552
+ }
9553
+ function renderSetupHelp() {
9554
+ return [
9555
+ `${colors.bold}AIDENT SETUP${colors.reset}`,
9556
+ "",
9557
+ "USAGE:",
9558
+ " aident setup [--client-name <name>] [--base-url <url>] [--oob] [--json]",
9559
+ "",
9560
+ "The setup command updates the CLI, installs or reconciles the verified Aident Skill globally, and opens sign-in when needed.",
9561
+ "It verifies Loadout access and records completion. AIDENT_TOKEN enables non-interactive authentication."
9562
+ ].join(`
9563
+ `);
9461
9564
  }
9462
9565
  async function maybeOfferLocalIntegrationMigration(parsed) {
9463
9566
  if (parsed.format !== "tui")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aident-ai/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8-rc.1",
4
4
  "description": "Aident CLI — umbrella access to Loadout integrations, Playbook automation, Intern tools, and the Aident platform.",
5
5
  "homepage": "https://aident.ai",
6
6
  "repository": {