@aident-ai/cli 0.0.5 → 0.0.7-rc.0

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 +47 -73
  2. package/dist/cli.mjs +383 -141
  3. package/package.json +7 -5
package/README.md CHANGED
@@ -1,119 +1,93 @@
1
1
  # @aident-ai/cli
2
2
 
3
- The Aident command-line interface — agent-friendly access to 1000+ integrations, automation playbooks, and the full Aident platform.
3
+ Agent-friendly access to Aident Loadout by default, with Playbook and Intern available as packages.
4
4
 
5
5
  ```bash
6
6
  aident login
7
7
  aident capabilities search --query "send email"
8
+ aident packages add playbook
8
9
  aident playbooks list --json
9
10
  ```
10
11
 
11
- This is the recommended way to use Aident from any coding agent (Claude Code, OpenClaw, HermesAgent, Cursor, …) or shell automation. The CLI is a thin wrapper around the Aident command engine every command you run on the platform UI is available here, with the same semantics.
12
+ The CLI is the only npm CLI package Aident ships. It is a thin wrapper around the public OpenAPI package APIs under
13
+ `/api/openapi/...`, so CLI behavior stays aligned with API and MCP behavior.
12
14
 
13
15
  ## Install
14
16
 
15
- ### One-line install (recommended)
16
-
17
- **macOS / Linux:**
18
-
19
- ```bash
20
- curl -fsSL https://app.aident.ai/cli/install.sh | bash
21
- ```
22
-
23
- **Windows (PowerShell, with Git Bash installed):**
24
-
25
- ```powershell
26
- & "C:\Program Files\Git\bin\bash.exe" -c 'curl -fsSL https://app.aident.ai/cli/install.sh | bash'
27
- ```
28
-
29
- The installer requires Node ≥ 18. If you don't have it, install via [nodejs.org](https://nodejs.org) or a version manager (`brew install node`, `nvm install --lts`, `fnm install --lts`) and re-run.
30
-
31
- ### Manual install
32
-
33
17
  ```bash
34
- # One-off (no install)
35
18
  npx -y @aident-ai/cli <command>
36
-
37
- # Global
38
19
  npm install -g @aident-ai/cli
39
20
  ```
40
21
 
41
- ## Get started
22
+ ## Get Started
42
23
 
43
24
  ```bash
44
- aident setup # interactive: pick base URL + log in
45
- aident doctor # validate installation
46
- aident --help # discover packages and domains
25
+ aident setup
26
+ aident doctor
27
+ aident --help
47
28
  ```
48
29
 
49
- The browser-based sign-in uses PKCE on a localhost loopback. If you can't bind a port (locked-down sandboxes, remote shells), pass `--oob`:
30
+ Use `--oob` for browserless auth environments:
50
31
 
51
32
  ```bash
52
33
  aident login --oob
53
34
  ```
54
35
 
55
- That falls back to the OOB copy-paste flow.
36
+ ## Packages
56
37
 
57
- ## Common commands
38
+ Loadout is always enabled by default. Add Playbook only when an agent needs to create, execute, or manage playbooks.
39
+ Intern is admin-gated and should be used only by authenticated admin users.
58
40
 
59
41
  ```bash
60
- aident capabilities --help # list commands in a domain
61
- aident capabilities search --help # show input schema and examples
62
- aident capabilities search --query "send email" # run a command (TUI output)
63
- aident playbooks list --json # JSON output for piping
64
- aident playbooks execute --playbookId pb_123 # run a playbook
65
- aident integrations status # see connected integrations
66
- aident logout # revoke and clear creds
42
+ aident --help
43
+ aident packages add playbook
44
+ aident packages list
45
+ aident --package playbook playbooks list --json
46
+ aident --packages playbook playbooks execute --playbookId pb_123 --json
67
47
  ```
68
48
 
69
- Use `--json` for machine-readable output. The CLI auto-detects whether stdout is a TTY.
70
-
71
- ## Configuration
72
-
73
- The CLI persists settings to `~/.aident/config.json` and credentials to `~/.aident/credentials.json` — the same `~/.aident/` directory used by the Aident skill, the desktop sandbox app, and any other Aident tooling.
49
+ ## Common Loadout Commands
74
50
 
75
51
  ```bash
76
- aident config show # print all config
77
- aident config get baseUrl # print one value
78
- aident config set baseUrl https://... # persist a value
79
- aident config unset baseUrl # remove an override
80
- aident config path # print the config file path
52
+ aident capabilities search --query "send email"
53
+ aident capabilities get --name "gmail.send_email"
54
+ aident capabilities execute --name "gmail.send_email" --input '{"to":"user@example.com"}'
55
+ aident vault status --integrationId github_tools
56
+ aident audit recent --limit 20
81
57
  ```
82
58
 
83
- | Key | Default | Purpose |
84
- | --------- | ----------------------- | ---------------------------------------------------------- |
85
- | `baseUrl` | `https://app.aident.ai` | API host used for new logins. Save once, reuse everywhere. |
86
-
87
- ### Resolution order for the API base URL
59
+ ## Configuration
88
60
 
89
- 1. `AIDENT_BASE_URL` env var (per-invocation override)
90
- 2. `aident login --base-url <url>` (only at login time)
91
- 3. `~/.aident/config.json` `baseUrl`
92
- 4. Active credentials file (after login carries its own host)
93
- 5. Default `https://app.aident.ai`
61
+ Settings live in `~/.aident/config.json`; OAuth credentials live in `~/.aident/credentials.json`.
94
62
 
95
- ### Environment overrides
63
+ | Key | Default | Purpose |
64
+ | ---------- | ----------------------- | ----------------------------------------------------- |
65
+ | `baseUrl` | `https://app.aident.ai` | API host used for logins and command execution. |
66
+ | `packages` | `["loadout"]` | Enabled packages. Loadout is always included locally. |
96
67
 
97
- | Variable | Purpose |
98
- | ----------------- | ------------------------------------------------------------------------- |
99
- | `AIDENT_TOKEN` | Use this Bearer token directly; skips the credentials file. Useful in CI. |
100
- | `AIDENT_BASE_URL` | Override the default server for a single invocation. |
68
+ ```bash
69
+ aident config show
70
+ aident config set baseUrl https://app.aident.ai
71
+ aident config set packages playbook
72
+ aident config unset packages
73
+ ```
101
74
 
102
- ## Where files live
75
+ Environment overrides:
103
76
 
104
- | Path | Owner | Purpose |
105
- | ------------------------------------ | ------------------- | -------------------------------------------- |
106
- | `~/.aident/config.json` | CLI (this package) | Persistent settings (`baseUrl`, …) |
107
- | `~/.aident/credentials.json` | CLI + Aident skill | OAuth tokens for `aident` / agent skill REST |
108
- | `~/.aident/sandbox-credentials.json` | Sandbox desktop app | Tokens used by the local sandbox runtime |
109
- | `~/.aident/sandbox-workspaces.json` | Sandbox desktop app | Tracked workspace directories |
110
- | `~/.aident/sandbox/` | Sandbox daemon | Daemon PID, status, logs |
77
+ | Variable | Purpose |
78
+ | ----------------- | ----------------------------------------------------------- |
79
+ | `AIDENT_TOKEN` | Use this Bearer token directly; skips the credentials file. |
80
+ | `AIDENT_BASE_URL` | Override the API host for one invocation. |
81
+ | `AIDENT_PACKAGE` | Focus one package for one invocation. |
82
+ | `AIDENT_PACKAGES` | Enable add-on packages for one invocation, e.g. `playbook`. |
111
83
 
112
- All Aident tooling shares this directory; nothing escapes outside `~/.aident/`.
84
+ ## How It Works
113
85
 
114
- ## How it works
86
+ Discovery fetches `/api/openapi/{package}.json` and reads the package command catalog embedded in the OpenAPI document.
87
+ Execution calls `/api/openapi/{package}/{operationId}` with the command arguments as JSON.
115
88
 
116
- `aident <domain> <command> [args]` makes a `POST /api/cli/exec` request to the Aident server with `{ domain, command, args }`, the same shape used internally by the platform's CLI engine. Discovery (`--help`, schemas, examples) comes from `GET /api/cli`. The CLI is a thin layer — there is no Aident logic baked in. Anything you can do on the web app, you can do here.
89
+ When multiple packages are enabled, the CLI fetches each package schema, merges the catalogs locally, and routes each
90
+ command back to the package operation that contributed it. Unknown commands are not guessed across packages.
117
91
 
118
92
  ## License
119
93
 
package/dist/cli.mjs CHANGED
@@ -5,100 +5,6 @@ import { spawn } from "node:child_process";
5
5
  import { createHash, randomBytes } from "node:crypto";
6
6
  import { createServer } from "node:http";
7
7
 
8
- // src/credentials.ts
9
- import { mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "node:fs/promises";
10
- import { join as join2 } from "node:path";
11
-
12
- // src/config.ts
13
- import { mkdir, readFile, writeFile } from "node:fs/promises";
14
- import { homedir } from "node:os";
15
- import { join } from "node:path";
16
- function getAidentDir() {
17
- const home = process.env.HOME || process.env.USERPROFILE || homedir();
18
- return join(home, ".aident");
19
- }
20
- function getConfigFile() {
21
- return join(getAidentDir(), "config.json");
22
- }
23
- var DEFAULT_BASE_URL = "https://app.aident.ai";
24
- var CONFIG_KEYS = ["baseUrl"];
25
- async function readConfig() {
26
- try {
27
- const text = await readFile(getConfigFile(), "utf-8");
28
- const parsed = JSON.parse(text);
29
- if (!parsed || typeof parsed !== "object")
30
- return {};
31
- return parsed;
32
- } catch {
33
- return {};
34
- }
35
- }
36
- async function writeConfig(config) {
37
- await mkdir(getAidentDir(), { recursive: true, mode: 448 });
38
- await writeFile(getConfigFile(), JSON.stringify(config, null, 2) + `
39
- `, { mode: 420 });
40
- }
41
- async function setConfigValue(key, value) {
42
- const config = await readConfig();
43
- config[key] = value;
44
- await writeConfig(config);
45
- }
46
- async function unsetConfigValue(key) {
47
- const config = await readConfig();
48
- delete config[key];
49
- await writeConfig(config);
50
- }
51
- async function resolveDefaultBaseUrl() {
52
- const env = process.env.AIDENT_BASE_URL?.trim();
53
- if (env)
54
- return env;
55
- const config = await readConfig();
56
- if (typeof config.baseUrl === "string" && config.baseUrl.trim() !== "") {
57
- return config.baseUrl.trim();
58
- }
59
- return DEFAULT_BASE_URL;
60
- }
61
- function isKnownConfigKey(key) {
62
- return CONFIG_KEYS.includes(key);
63
- }
64
- function normalizeBaseUrl(url) {
65
- let trimmed = url.trim();
66
- if (!/^https?:\/\//i.test(trimmed))
67
- trimmed = `https://${trimmed}`;
68
- return trimmed.replace(/\/+$/, "");
69
- }
70
-
71
- // src/credentials.ts
72
- function getCredentialsFile() {
73
- return join2(getAidentDir(), "credentials.json");
74
- }
75
- async function readCredentials() {
76
- try {
77
- const content = await readFile2(getCredentialsFile(), "utf-8");
78
- const parsed = JSON.parse(content);
79
- if (!parsed.access_token || !parsed.base_url)
80
- return null;
81
- return parsed;
82
- } catch {
83
- return null;
84
- }
85
- }
86
- async function writeCredentials(creds) {
87
- await mkdir2(getAidentDir(), { recursive: true, mode: 448 });
88
- await writeFile2(getCredentialsFile(), JSON.stringify(creds, null, 2), { mode: 384 });
89
- }
90
- async function clearCredentials() {
91
- await rm(getCredentialsFile(), { force: true });
92
- }
93
- function isExpired(creds) {
94
- if (!creds.expires_at)
95
- return false;
96
- const expiresAt = Date.parse(creds.expires_at);
97
- if (Number.isNaN(expiresAt))
98
- return false;
99
- return Date.now() >= expiresAt - 60000;
100
- }
101
-
102
8
  // src/output.ts
103
9
  var isTTY = process.stdout.isTTY === true;
104
10
  var colors = {
@@ -218,7 +124,7 @@ async function loginOob(baseUrl) {
218
124
  const verifier = base64UrlEncode(randomBytes(48));
219
125
  const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
220
126
  const state = base64UrlEncode(randomBytes(16));
221
- const clientId = await reuseOrRegisterClient(baseUrl, [redirectUri]);
127
+ const clientId = await registerClient(baseUrl, [redirectUri]);
222
128
  const authorizeUrl = new URL(`${baseUrl}/api/mcp/oauth/authorize`);
223
129
  authorizeUrl.searchParams.set("response_type", "code");
224
130
  authorizeUrl.searchParams.set("client_id", clientId);
@@ -255,12 +161,6 @@ function looksLikeAccessToken(value) {
255
161
  return false;
256
162
  return /^[A-Za-z0-9._-]+$/.test(value);
257
163
  }
258
- async function reuseOrRegisterClient(baseUrl, redirectUris) {
259
- const existing = await readCredentials();
260
- if (existing && existing.client_id && existing.base_url === baseUrl)
261
- return existing.client_id;
262
- return registerClient(baseUrl, redirectUris);
263
- }
264
164
  async function registerClient(baseUrl, redirectUris) {
265
165
  const res = await fetch(`${baseUrl}/api/mcp/oauth/register`, {
266
166
  method: "POST",
@@ -402,25 +302,91 @@ function escapeHtml(s) {
402
302
  }
403
303
 
404
304
  // src/version.ts
405
- var VERSION = "0.0.5";
305
+ var VERSION = "0.0.7-rc.0";
406
306
 
407
307
  // src/client.ts
408
308
  class CliClient {
409
309
  creds;
410
- constructor(creds) {
310
+ credentialSource;
311
+ packages;
312
+ commandOperations = new Map;
313
+ constructor(creds, credentialSource = "stored", packages) {
411
314
  this.creds = creds;
315
+ this.credentialSource = credentialSource;
316
+ if (packages.length === 0)
317
+ throw new Error("CliClient packages must not be empty");
318
+ this.packages = [...packages];
412
319
  }
413
320
  get baseUrl() {
414
321
  return this.creds.base_url.replace(/\/+$/, "");
415
322
  }
323
+ get canRefresh() {
324
+ return this.credentialSource === "stored";
325
+ }
326
+ get activePackages() {
327
+ return this.packages;
328
+ }
416
329
  updateCredentials(creds) {
417
330
  this.creds = creds;
418
331
  }
419
332
  async getCatalog() {
420
- return this.fetchJson("GET", "/api/cli");
333
+ if (this.packages.length === 1) {
334
+ const packageName = this.packages[0];
335
+ const result = await this.fetchOpenApiCatalog(packageName);
336
+ if (result.status === 200)
337
+ this.indexCatalogCommands(result.body, packageName);
338
+ return result;
339
+ }
340
+ const results = await Promise.all(this.packages.map((packageName) => this.fetchOpenApiCatalog(packageName)));
341
+ const failed = results.find((result) => result.status !== 200);
342
+ if (failed)
343
+ return failed;
344
+ const catalog = mergeCatalogs(results.map((result) => result.body), this.packages, (domain, command, packageName, operationId) => this.commandOperations.set(commandKey(domain, command), { packageName, operationId }));
345
+ return { status: 200, body: catalog };
421
346
  }
422
347
  async exec(domain, command, args) {
423
- return this.fetchJson("POST", "/api/cli/exec", { domain, command, args });
348
+ const operation = this.resolveOperation(domain, command);
349
+ if (operation === null) {
350
+ return {
351
+ status: 404,
352
+ body: {
353
+ success: false,
354
+ error: {
355
+ code: "unknown-command",
356
+ message: `Unknown command: ${domain} ${command}. Run \`aident --help\` to refresh the command catalog.`
357
+ }
358
+ }
359
+ };
360
+ }
361
+ return this.fetchJson("POST", this.operationPath(operation.packageName, operation.operationId), args);
362
+ }
363
+ async fetchOpenApiCatalog(packageName) {
364
+ const result = await this.fetchJson("GET", this.catalogPath(packageName));
365
+ const catalog = result.body["x-aident-command-catalog"];
366
+ if (result.status !== 200 || !catalog) {
367
+ return {
368
+ status: result.status,
369
+ body: invalidResponseFallback(JSON.stringify(result.body))
370
+ };
371
+ }
372
+ return { status: result.status, body: catalog };
373
+ }
374
+ catalogPath(packageName) {
375
+ return `/api/openapi/${encodeURIComponent(packageName)}.json`;
376
+ }
377
+ operationPath(packageName, operationId) {
378
+ return `/api/openapi/${encodeURIComponent(packageName)}/${encodeURIComponent(operationId)}`;
379
+ }
380
+ resolveOperation(domain, command) {
381
+ return this.commandOperations.get(commandKey(domain, command)) ?? null;
382
+ }
383
+ indexCatalogCommands(catalog, packageName) {
384
+ for (const command of catalog.commands) {
385
+ this.commandOperations.set(commandKey(command.domain, command.command), {
386
+ packageName,
387
+ operationId: command.operationId
388
+ });
389
+ }
424
390
  }
425
391
  async fetchJson(method, path, body) {
426
392
  const headers = {
@@ -445,6 +411,170 @@ class CliClient {
445
411
  function invalidResponseFallback(text) {
446
412
  return { success: false, error: { code: "invalid-response", message: text } };
447
413
  }
414
+ function commandKey(domain, command) {
415
+ return `${domain}\x00${command}`;
416
+ }
417
+ function mergeCatalogs(catalogs, packages, onCommand) {
418
+ const packageMap = new Map;
419
+ const domainMap = new Map;
420
+ const commandMap = new Map;
421
+ catalogs.forEach((catalog, index) => {
422
+ const packageName = packages[index];
423
+ for (const pkg of catalog.packages) {
424
+ packageMap.set(pkg.name, {
425
+ ...pkg,
426
+ domainCount: (packageMap.get(pkg.name)?.domainCount ?? 0) + pkg.domainCount
427
+ });
428
+ }
429
+ for (const domain of catalog.domains) {
430
+ const existing = domainMap.get(domain.name);
431
+ domainMap.set(domain.name, {
432
+ ...domain,
433
+ commandCount: (existing?.commandCount ?? 0) + domain.commandCount
434
+ });
435
+ }
436
+ for (const command of catalog.commands) {
437
+ const key = commandKey(command.domain, command.command);
438
+ if (!commandMap.has(key)) {
439
+ commandMap.set(key, command);
440
+ onCommand(command.domain, command.command, packageName, command.operationId);
441
+ }
442
+ }
443
+ });
444
+ return {
445
+ packages: [...packageMap.values()],
446
+ domains: [...domainMap.values()],
447
+ commands: [...commandMap.values()]
448
+ };
449
+ }
450
+
451
+ // src/config.ts
452
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
453
+ import { homedir } from "node:os";
454
+ import { join } from "node:path";
455
+
456
+ // src/packages.ts
457
+ var CLI_PACKAGES = ["loadout", "playbook", "intern"];
458
+ var DEFAULT_CLI_PACKAGES = ["loadout"];
459
+ function parseCliPackage(value) {
460
+ if (typeof value !== "string")
461
+ throw new Error(`Unsupported Aident package: ${String(value)}`);
462
+ const normalized = value.trim().toLowerCase();
463
+ if (!CLI_PACKAGES.includes(normalized))
464
+ throw new Error(`Unsupported Aident package: ${value}`);
465
+ return normalized;
466
+ }
467
+ function normalizeCliPackages(value) {
468
+ const rawPackages = value === undefined || value === null || value === "" ? [] : Array.isArray(value) ? value : String(value).split(/[,\s]+/);
469
+ const packages = ["loadout"];
470
+ for (const rawPackage of rawPackages) {
471
+ if (rawPackage === undefined || rawPackage === null || rawPackage === "")
472
+ continue;
473
+ const cliPackage = parseCliPackage(rawPackage);
474
+ if (!packages.includes(cliPackage))
475
+ packages.push(cliPackage);
476
+ }
477
+ return packages;
478
+ }
479
+ function formatCliPackages(packages) {
480
+ return packages.join(",");
481
+ }
482
+
483
+ // src/config.ts
484
+ function getAidentDir() {
485
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
486
+ return join(home, ".aident");
487
+ }
488
+ function getConfigFile() {
489
+ return join(getAidentDir(), "config.json");
490
+ }
491
+ var DEFAULT_BASE_URL = "https://app.aident.ai";
492
+ var CONFIG_KEYS = ["baseUrl", "packages"];
493
+ async function readConfig() {
494
+ try {
495
+ const text = await readFile(getConfigFile(), "utf-8");
496
+ const parsed = JSON.parse(text);
497
+ if (!parsed || typeof parsed !== "object")
498
+ return {};
499
+ return parsed;
500
+ } catch {
501
+ return {};
502
+ }
503
+ }
504
+ async function writeConfig(config) {
505
+ await mkdir(getAidentDir(), { recursive: true, mode: 448 });
506
+ await writeFile(getConfigFile(), JSON.stringify(config, null, 2) + `
507
+ `, { mode: 420 });
508
+ }
509
+ async function setConfigValue(key, value) {
510
+ const config = await readConfig();
511
+ config[key] = value;
512
+ await writeConfig(config);
513
+ }
514
+ async function unsetConfigValue(key) {
515
+ const config = await readConfig();
516
+ delete config[key];
517
+ await writeConfig(config);
518
+ }
519
+ async function resolveDefaultBaseUrl() {
520
+ const env = process.env.AIDENT_BASE_URL?.trim();
521
+ if (env)
522
+ return env;
523
+ const config = await readConfig();
524
+ if (typeof config.baseUrl === "string" && config.baseUrl.trim() !== "") {
525
+ return config.baseUrl.trim();
526
+ }
527
+ return DEFAULT_BASE_URL;
528
+ }
529
+ async function resolveDefaultPackages() {
530
+ const env = process.env.AIDENT_PACKAGES?.trim() || process.env.AIDENT_PACKAGE?.trim();
531
+ if (env)
532
+ return normalizeCliPackages(env);
533
+ const config = await readConfig();
534
+ return normalizeCliPackages(config.packages ?? DEFAULT_CLI_PACKAGES);
535
+ }
536
+ function isKnownConfigKey(key) {
537
+ return CONFIG_KEYS.includes(key);
538
+ }
539
+ function normalizeBaseUrl(url) {
540
+ let trimmed = url.trim();
541
+ if (!/^https?:\/\//i.test(trimmed))
542
+ trimmed = `https://${trimmed}`;
543
+ return trimmed.replace(/\/+$/, "");
544
+ }
545
+
546
+ // src/credentials.ts
547
+ import { mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "node:fs/promises";
548
+ import { join as join2 } from "node:path";
549
+ function getCredentialsFile() {
550
+ return join2(getAidentDir(), "credentials.json");
551
+ }
552
+ async function readCredentials() {
553
+ try {
554
+ const content = await readFile2(getCredentialsFile(), "utf-8");
555
+ const parsed = JSON.parse(content);
556
+ if (!parsed.access_token || !parsed.base_url)
557
+ return null;
558
+ return parsed;
559
+ } catch {
560
+ return null;
561
+ }
562
+ }
563
+ async function writeCredentials(creds) {
564
+ await mkdir2(getAidentDir(), { recursive: true, mode: 448 });
565
+ await writeFile2(getCredentialsFile(), JSON.stringify(creds, null, 2), { mode: 384 });
566
+ }
567
+ async function clearCredentials() {
568
+ await rm(getCredentialsFile(), { force: true });
569
+ }
570
+ function isExpired(creds) {
571
+ if (!creds.expires_at)
572
+ return false;
573
+ const expiresAt = Date.parse(creds.expires_at);
574
+ if (Number.isNaN(expiresAt))
575
+ return false;
576
+ return Date.now() >= expiresAt - 60000;
577
+ }
448
578
 
449
579
  // src/doctor.ts
450
580
  async function runDoctor(opts) {
@@ -494,7 +624,7 @@ async function checkCredentials(baseUrl) {
494
624
  }
495
625
  async function checkServerReachable(baseUrl) {
496
626
  try {
497
- const res = await fetch(`${baseUrl}/api/cli`, {
627
+ const res = await fetch(`${baseUrl}/api/openapi/loadout.json`, {
498
628
  method: "GET",
499
629
  headers: { "User-Agent": `@aident-ai/cli/${VERSION}` }
500
630
  });
@@ -533,7 +663,7 @@ function renderHelp(catalog) {
533
663
  lines.push(" aident <domain> <command> help Show input schema and examples");
534
664
  lines.push("");
535
665
  if (catalog.packages.length > 0) {
536
- lines.push("PACKAGES:");
666
+ lines.push("COMMAND GROUPS:");
537
667
  for (const pkg of catalog.packages) {
538
668
  lines.push(` ${pkg.name.padEnd(10)} ${pkg.description}`);
539
669
  }
@@ -612,7 +742,7 @@ function renderCommandHelp(catalog, domain, command) {
612
742
  }
613
743
 
614
744
  // src/parser.ts
615
- var RESERVED_CLI_FLAGS = ["base-url", "json", "tui", "help", "oob", "version"];
745
+ var RESERVED_CLI_FLAGS = ["base-url", "json", "tui", "help", "oob", "version", "package", "packages"];
616
746
  function parseArgs(argv) {
617
747
  const positional = [];
618
748
  const flags = {};
@@ -676,6 +806,30 @@ function coerce(val) {
676
806
  }
677
807
  return val;
678
808
  }
809
+ function coerceArgsToSchema(args, schema) {
810
+ const props = schema?.properties;
811
+ if (!props)
812
+ return args;
813
+ const out = { ...args };
814
+ for (const [key, prop] of Object.entries(props)) {
815
+ if (!(key in out))
816
+ continue;
817
+ const value = out[key];
818
+ const types = Array.isArray(prop.type) ? prop.type : prop.type ? [prop.type] : [];
819
+ if (types.includes("array") && !Array.isArray(value)) {
820
+ if (value === undefined || value === null || value === "") {
821
+ out[key] = [];
822
+ } else if (typeof value === "string" && value.includes(",")) {
823
+ out[key] = value.split(",").map((s) => s.trim()).filter(Boolean);
824
+ } else {
825
+ out[key] = [value];
826
+ }
827
+ } else if (types.includes("string") && !types.includes("number") && !types.includes("boolean") && (typeof value === "number" || typeof value === "boolean")) {
828
+ out[key] = String(value);
829
+ }
830
+ }
831
+ return out;
832
+ }
679
833
  function resolveCommand(positional, knownCommands) {
680
834
  if (positional.length === 0)
681
835
  return null;
@@ -716,6 +870,8 @@ async function callWithRefresh(client, call, deps = defaultDeps) {
716
870
  }
717
871
  if (first.status !== 401)
718
872
  return first;
873
+ if (!client.canRefresh)
874
+ return first;
719
875
  const creds = await deps.readCredentials();
720
876
  if (!creds)
721
877
  return first;
@@ -745,7 +901,7 @@ async function main() {
745
901
  return;
746
902
  }
747
903
  if (!first || parsed.isHelp && parsed.positional.length === 0) {
748
- await runHelp(parsed.format);
904
+ await runHelp(parsed);
749
905
  return;
750
906
  }
751
907
  switch (first) {
@@ -756,7 +912,7 @@ async function main() {
756
912
  await runLogout();
757
913
  return;
758
914
  case "whoami":
759
- await runWhoami(parsed.format);
915
+ await runWhoami(parsed);
760
916
  return;
761
917
  case "doctor":
762
918
  await runDoctorCmd(parsed.format);
@@ -767,17 +923,21 @@ async function main() {
767
923
  case "config":
768
924
  await runConfigCmd(parsed);
769
925
  return;
926
+ case "packages":
927
+ case "package":
928
+ await runPackagesCmd(parsed);
929
+ return;
770
930
  case "catalog":
771
- await runCatalog(parsed.format);
931
+ await runCatalog(parsed);
772
932
  return;
773
933
  default:
774
934
  await runCommand(parsed);
775
935
  }
776
936
  }
777
- async function runHelp(format) {
778
- const client = await getAuthenticatedClient();
937
+ async function runHelp(parsed) {
938
+ const client = await getAuthenticatedClient(await getRequestedPackages(parsed));
779
939
  if (!client) {
780
- if (format === "json") {
940
+ if (parsed.format === "json") {
781
941
  logInfo(JSON.stringify({ error: "not-authenticated", message: "Run `aident login` first." }));
782
942
  } else {
783
943
  logInfo(`${colors.bold}Aident CLI v${VERSION}${colors.reset}`);
@@ -791,6 +951,7 @@ async function runHelp(format) {
791
951
  logInfo(" aident config show Print persistent config");
792
952
  logInfo(" aident config set <key> <value> Persist a config value");
793
953
  logInfo(" aident config get <key> Read a single value");
954
+ logInfo(" aident packages add <playbook|intern> Enable an add-on package");
794
955
  logInfo(" aident doctor Validate installation");
795
956
  logInfo(" aident setup Interactive setup wizard");
796
957
  logInfo(" aident <domain> <command> [--flag value ...] [--json]");
@@ -800,7 +961,7 @@ async function runHelp(format) {
800
961
  const catalog = await fetchCatalog(client);
801
962
  if (!catalog)
802
963
  return;
803
- if (format === "json") {
964
+ if (parsed.format === "json") {
804
965
  logInfo(JSON.stringify(catalog));
805
966
  } else {
806
967
  logInfo(renderHelp(catalog));
@@ -826,10 +987,10 @@ async function runLogout() {
826
987
  await clearCredentials();
827
988
  logInfo(`${colors.green}Signed out.${colors.reset}`);
828
989
  }
829
- async function runWhoami(format) {
830
- const client = await getAuthenticatedClient();
990
+ async function runWhoami(parsed) {
991
+ const client = await getAuthenticatedClient(["loadout"]);
831
992
  if (!client) {
832
- if (format === "json") {
993
+ if (parsed.format === "json") {
833
994
  logInfo(JSON.stringify({ authenticated: false }));
834
995
  } else {
835
996
  logInfo("Not signed in. Run `aident login`.");
@@ -837,8 +998,11 @@ async function runWhoami(format) {
837
998
  process.exitCode = 1;
838
999
  return;
839
1000
  }
840
- const result = await callWithRefresh(client, (c) => c.exec("user", "me", {}));
841
- if (format === "json") {
1001
+ const catalog = await fetchCatalog(client);
1002
+ if (!catalog)
1003
+ return;
1004
+ const result = await callWithRefresh(client, (c) => c.exec("account", "auth status", {}));
1005
+ if (parsed.format === "json") {
842
1006
  logInfo(JSON.stringify(result.body));
843
1007
  if (!result.body.success)
844
1008
  process.exitCode = 1;
@@ -850,7 +1014,7 @@ async function runWhoami(format) {
850
1014
  return;
851
1015
  }
852
1016
  const err = result.body.error;
853
- logErr(`${colors.red} ${err?.code ?? "error"}:${colors.reset} ${err?.message ?? "Unable to fetch user info."}`);
1017
+ logErr(`${colors.red}x ${err?.code ?? "error"}:${colors.reset} ${err?.message ?? "Unable to fetch user info."}`);
854
1018
  process.exitCode = 1;
855
1019
  }
856
1020
  async function runConfigCmd(parsed) {
@@ -863,8 +1027,9 @@ async function runConfigCmd(parsed) {
863
1027
  }
864
1028
  logInfo(`${colors.bold}Aident config${colors.reset} ${colors.dim}(${getConfigFile()})${colors.reset}`);
865
1029
  if (Object.keys(config).length === 0) {
866
- logInfo(` ${colors.dim}(empty using defaults)${colors.reset}`);
1030
+ logInfo(` ${colors.dim}(empty, using defaults)${colors.reset}`);
867
1031
  logInfo(` ${colors.dim}baseUrl${colors.reset} ${colors.dim}default: ${DEFAULT_BASE_URL}${colors.reset}`);
1032
+ logInfo(` ${colors.dim}packages${colors.reset} ${colors.dim}default: loadout${colors.reset}`);
868
1033
  return;
869
1034
  }
870
1035
  for (const [key, val] of Object.entries(config)) {
@@ -910,14 +1075,18 @@ async function runConfigCmd(parsed) {
910
1075
  process.exitCode = 1;
911
1076
  return;
912
1077
  }
913
- const value = key === "baseUrl" ? normalizeBaseUrl(rawValue) : rawValue;
914
- await setConfigValue(key, value);
915
- logInfo(`${colors.green}Set${colors.reset} ${key} = ${value}`);
916
1078
  if (key === "baseUrl") {
1079
+ const value = normalizeBaseUrl(rawValue);
1080
+ await setConfigValue(key, value);
1081
+ logInfo(`${colors.green}Set${colors.reset} ${key} = ${value}`);
917
1082
  const creds = await readCredentials();
918
1083
  if (creds && creds.base_url !== value) {
919
1084
  logInfo(`${colors.yellow}Note:${colors.reset} existing credentials are for ${creds.base_url}. Run \`aident login\` to authenticate against ${value}.`);
920
1085
  }
1086
+ } else {
1087
+ const value = normalizeCliPackages(rawValue);
1088
+ await setConfigValue(key, value);
1089
+ logInfo(`${colors.green}Set${colors.reset} ${key} = ${formatConfigValue(value)}`);
921
1090
  }
922
1091
  return;
923
1092
  }
@@ -936,6 +1105,54 @@ async function runConfigCmd(parsed) {
936
1105
  logErr("Usage: aident config [show | get <key> | set <key> <value> | unset <key> | path]");
937
1106
  process.exitCode = 1;
938
1107
  }
1108
+ async function runPackagesCmd(parsed) {
1109
+ const sub = parsed.positional[1] ?? "list";
1110
+ if (sub === "list" || sub === "show") {
1111
+ const packages = await resolveDefaultPackages();
1112
+ if (parsed.format === "json") {
1113
+ logInfo(JSON.stringify({ packages, defaultPackage: "loadout" }));
1114
+ } else {
1115
+ logInfo(`${colors.bold}Aident packages${colors.reset}`);
1116
+ logInfo(` ${formatCliPackages(packages)}`);
1117
+ logInfo("");
1118
+ logInfo(`Loadout is always enabled. Add-ons: ${CLI_PACKAGES.filter((p) => p !== "loadout").join(", ")}`);
1119
+ }
1120
+ return;
1121
+ }
1122
+ if (sub === "add" || sub === "enable") {
1123
+ const packageName = parsed.positional[2];
1124
+ if (!packageName) {
1125
+ logErr("Usage: aident packages add <playbook|intern>");
1126
+ process.exitCode = 1;
1127
+ return;
1128
+ }
1129
+ const packages = normalizeCliPackages([...await readConfiguredPackages(), parseCliPackage(packageName)]);
1130
+ await setConfigValue("packages", packages);
1131
+ logInfo(`${colors.green}Enabled packages:${colors.reset} ${formatCliPackages(packages)}`);
1132
+ return;
1133
+ }
1134
+ if (sub === "remove" || sub === "disable") {
1135
+ const packageName = parsed.positional[2];
1136
+ if (!packageName) {
1137
+ logErr("Usage: aident packages remove <playbook|intern>");
1138
+ process.exitCode = 1;
1139
+ return;
1140
+ }
1141
+ const target = parseCliPackage(packageName);
1142
+ if (target === "loadout") {
1143
+ logErr("Loadout is the default package and cannot be removed.");
1144
+ process.exitCode = 1;
1145
+ return;
1146
+ }
1147
+ const packages = normalizeCliPackages((await readConfiguredPackages()).filter((p) => p !== target));
1148
+ await setConfigValue("packages", packages);
1149
+ logInfo(`${colors.green}Enabled packages:${colors.reset} ${formatCliPackages(packages)}`);
1150
+ return;
1151
+ }
1152
+ logErr(`Unknown packages subcommand: ${sub}`);
1153
+ logErr("Usage: aident packages [list | add <playbook|intern> | remove <playbook|intern>]");
1154
+ process.exitCode = 1;
1155
+ }
939
1156
  async function runDoctorCmd(format) {
940
1157
  const baseUrl = await resolveDefaultBaseUrl();
941
1158
  const report = await runDoctor({ baseUrl, configFile: getConfigFile(), credentialsFile: getCredentialsFile() });
@@ -948,7 +1165,7 @@ async function runDoctorCmd(format) {
948
1165
  logInfo(`${colors.bold}Aident CLI doctor${colors.reset}`);
949
1166
  logInfo("");
950
1167
  for (const c of report.checks) {
951
- const mark = c.ok ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
1168
+ const mark = c.ok ? `${colors.green}ok${colors.reset}` : `${colors.red}x${colors.reset}`;
952
1169
  logInfo(` ${mark} ${c.name.padEnd(22)} ${colors.dim}${c.detail}${colors.reset}`);
953
1170
  }
954
1171
  logInfo("");
@@ -985,8 +1202,8 @@ async function runSetup() {
985
1202
  logInfo("");
986
1203
  logInfo(`Done. Try ${colors.cyan}aident --help${colors.reset} or ${colors.cyan}aident doctor${colors.reset}.`);
987
1204
  }
988
- async function runCatalog(format) {
989
- const client = await getAuthenticatedClient();
1205
+ async function runCatalog(parsed) {
1206
+ const client = await getAuthenticatedClient(await getRequestedPackages(parsed));
990
1207
  if (!client) {
991
1208
  process.exitCode = 1;
992
1209
  return;
@@ -994,10 +1211,10 @@ async function runCatalog(format) {
994
1211
  const catalog = await fetchCatalog(client);
995
1212
  if (!catalog)
996
1213
  return;
997
- logInfo(JSON.stringify(catalog, null, format === "json" ? 0 : 2));
1214
+ logInfo(JSON.stringify(catalog, null, parsed.format === "json" ? 0 : 2));
998
1215
  }
999
1216
  async function runCommand(parsed) {
1000
- const client = await getAuthenticatedClient();
1217
+ const client = await getAuthenticatedClient(await getRequestedPackages(parsed));
1001
1218
  if (!client) {
1002
1219
  if (parsed.format === "json") {
1003
1220
  logInfo(JSON.stringify({ success: false, error: { code: "not-authenticated", message: "Run `aident login` first." } }));
@@ -1042,12 +1259,12 @@ async function runCommand(parsed) {
1042
1259
  logInfo(renderCommandHelp(catalog, resolved.domain, resolved.command));
1043
1260
  return;
1044
1261
  }
1045
- const args = { ...parsed.flags };
1262
+ let args = { ...parsed.flags };
1046
1263
  for (const reserved of RESERVED_CLI_FLAGS) {
1047
1264
  delete args[reserved];
1048
1265
  }
1266
+ const schema = cmdInfo.inputSchema;
1049
1267
  if (resolved.remaining.length > 0) {
1050
- const schema = cmdInfo.inputSchema;
1051
1268
  const required = new Set(schema?.required ?? []);
1052
1269
  const props = schema?.properties ?? {};
1053
1270
  for (const key of Object.keys(props)) {
@@ -1060,6 +1277,7 @@ async function runCommand(parsed) {
1060
1277
  args[key] = resolved.remaining.shift();
1061
1278
  }
1062
1279
  }
1280
+ args = coerceArgsToSchema(args, schema);
1063
1281
  const result = await callWithRefresh(client, (c) => c.exec(resolved.domain, resolved.command, args));
1064
1282
  emitResult(result, parsed.format, resolved);
1065
1283
  }
@@ -1072,17 +1290,33 @@ function emitResult(result, format, resolved) {
1072
1290
  return;
1073
1291
  }
1074
1292
  if (body.success) {
1075
- logInfo(`${colors.green}✓${colors.reset} ${resolved.domain} ${resolved.command}`);
1293
+ logInfo(`${colors.green}ok${colors.reset} ${resolved.domain} ${resolved.command}`);
1076
1294
  if (body.data !== undefined)
1077
1295
  logInfo(JSON.stringify(body.data, null, 2));
1078
1296
  if (body.meta)
1079
1297
  logInfo(`${colors.cyan}meta:${colors.reset} ${JSON.stringify(body.meta)}`);
1080
1298
  } else {
1081
- logErr(`${colors.red} ${body.error?.code ?? "error"}:${colors.reset} ${body.error?.message ?? "Unknown error"}`);
1299
+ logErr(`${colors.red}x ${body.error?.code ?? "error"}:${colors.reset} ${body.error?.message ?? "Unknown error"}`);
1082
1300
  process.exitCode = 1;
1083
1301
  }
1084
1302
  }
1085
- async function getAuthenticatedClient() {
1303
+ async function getRequestedPackages(parsed) {
1304
+ const rawPackage = parsed.flags["package"] ?? process.env.AIDENT_PACKAGE;
1305
+ if (rawPackage !== undefined && rawPackage !== null && rawPackage !== false) {
1306
+ if (rawPackage !== true)
1307
+ return [parseCliPackage(rawPackage)];
1308
+ throw new Error("--package requires a value: loadout, playbook, or intern");
1309
+ }
1310
+ const rawPackages = parsed.flags["packages"] ?? process.env.AIDENT_PACKAGES;
1311
+ if (rawPackages !== undefined && rawPackages !== null && rawPackages !== false) {
1312
+ if (rawPackages !== true)
1313
+ return normalizeCliPackages(rawPackages);
1314
+ throw new Error("--packages requires a comma-separated package list");
1315
+ }
1316
+ return resolveDefaultPackages();
1317
+ }
1318
+ async function getAuthenticatedClient(packages) {
1319
+ const activePackages = packages ?? await resolveDefaultPackages();
1086
1320
  const envToken = process.env.AIDENT_TOKEN;
1087
1321
  const envBaseUrl = process.env.AIDENT_BASE_URL?.trim();
1088
1322
  if (envToken) {
@@ -1091,7 +1325,7 @@ async function getAuthenticatedClient() {
1091
1325
  base_url: normalizeBaseUrl(baseUrl),
1092
1326
  client_id: "",
1093
1327
  access_token: envToken
1094
- });
1328
+ }, "env", activePackages);
1095
1329
  }
1096
1330
  let creds = await readCredentials();
1097
1331
  if (!creds)
@@ -1106,12 +1340,13 @@ async function getAuthenticatedClient() {
1106
1340
  creds = refreshed;
1107
1341
  }
1108
1342
  }
1109
- return new CliClient(creds);
1343
+ return new CliClient(creds, "stored", activePackages);
1110
1344
  }
1111
1345
  var catalogCache = null;
1112
1346
  async function fetchCatalog(client) {
1113
- if (catalogCache)
1114
- return catalogCache;
1347
+ const packagesKey = client.activePackages.join(",");
1348
+ if (catalogCache && catalogCache.packagesKey === packagesKey)
1349
+ return catalogCache.catalog;
1115
1350
  const res = await callWithRefresh(client, (c) => c.getCatalog());
1116
1351
  if (res.status === 401) {
1117
1352
  logErr(`Not authenticated (HTTP ${res.status}). Run \`aident login\`.`);
@@ -1123,8 +1358,15 @@ async function fetchCatalog(client) {
1123
1358
  process.exitCode = 1;
1124
1359
  return null;
1125
1360
  }
1126
- catalogCache = res.body;
1127
- return catalogCache;
1361
+ catalogCache = { packagesKey, catalog: res.body };
1362
+ return catalogCache.catalog;
1363
+ }
1364
+ function formatConfigValue(value) {
1365
+ return Array.isArray(value) ? value.join(",") : String(value);
1366
+ }
1367
+ async function readConfiguredPackages() {
1368
+ const config = await readConfig();
1369
+ return normalizeCliPackages(config.packages);
1128
1370
  }
1129
1371
  function isCommandCatalog(body) {
1130
1372
  if (!body || typeof body !== "object")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aident-ai/cli",
3
- "version": "0.0.5",
4
- "description": "Aident CLI — agent-friendly access to 1000+ integrations, playbooks, and the Aident automation platform.",
3
+ "version": "0.0.7-rc.0",
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": {
7
7
  "type": "git",
@@ -28,14 +28,16 @@
28
28
  "scripts": {
29
29
  "build": "bun run scripts/build.ts",
30
30
  "dev": "bun run src/cli.ts",
31
- "test": "bun test",
31
+ "test": "bun test src/__tests__",
32
+ "test:e2e": "bun test integration",
32
33
  "typecheck": "tsc --noEmit",
33
34
  "format": "pnpm exec prettier . --write",
34
- "lint": "pnpm exec eslint ./src --cache --cache-location ../../.eslintcache",
35
+ "lint": "pnpm exec eslint ./src ./integration --cache --cache-location ../../.eslintcache",
35
36
  "prepublishOnly": "pnpm run build"
36
37
  },
37
38
  "imports": {
38
- "~cli/*": "./src/*"
39
+ "~cli/*": "./src/*",
40
+ "~e2e/*": "./integration/*"
39
41
  },
40
42
  "devDependencies": {
41
43
  "@aident/eslint-config": "workspace:*",