@aident-ai/cli 0.0.6 → 0.1.0-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 +47 -73
  2. package/dist/cli.mjs +354 -139
  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.6";
305
+ var VERSION = "0.1.0-rc.1";
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 = {};
@@ -694,7 +824,7 @@ function coerceArgsToSchema(args, schema) {
694
824
  } else {
695
825
  out[key] = [value];
696
826
  }
697
- } else if (types.includes("string") && !types.includes("number") && typeof value === "number") {
827
+ } else if (types.includes("string") && !types.includes("number") && !types.includes("boolean") && (typeof value === "number" || typeof value === "boolean")) {
698
828
  out[key] = String(value);
699
829
  }
700
830
  }
@@ -740,6 +870,8 @@ async function callWithRefresh(client, call, deps = defaultDeps) {
740
870
  }
741
871
  if (first.status !== 401)
742
872
  return first;
873
+ if (!client.canRefresh)
874
+ return first;
743
875
  const creds = await deps.readCredentials();
744
876
  if (!creds)
745
877
  return first;
@@ -769,7 +901,7 @@ async function main() {
769
901
  return;
770
902
  }
771
903
  if (!first || parsed.isHelp && parsed.positional.length === 0) {
772
- await runHelp(parsed.format);
904
+ await runHelp(parsed);
773
905
  return;
774
906
  }
775
907
  switch (first) {
@@ -780,7 +912,7 @@ async function main() {
780
912
  await runLogout();
781
913
  return;
782
914
  case "whoami":
783
- await runWhoami(parsed.format);
915
+ await runWhoami(parsed);
784
916
  return;
785
917
  case "doctor":
786
918
  await runDoctorCmd(parsed.format);
@@ -791,17 +923,21 @@ async function main() {
791
923
  case "config":
792
924
  await runConfigCmd(parsed);
793
925
  return;
926
+ case "packages":
927
+ case "package":
928
+ await runPackagesCmd(parsed);
929
+ return;
794
930
  case "catalog":
795
- await runCatalog(parsed.format);
931
+ await runCatalog(parsed);
796
932
  return;
797
933
  default:
798
934
  await runCommand(parsed);
799
935
  }
800
936
  }
801
- async function runHelp(format) {
802
- const client = await getAuthenticatedClient();
937
+ async function runHelp(parsed) {
938
+ const client = await getAuthenticatedClient(await getRequestedPackages(parsed));
803
939
  if (!client) {
804
- if (format === "json") {
940
+ if (parsed.format === "json") {
805
941
  logInfo(JSON.stringify({ error: "not-authenticated", message: "Run `aident login` first." }));
806
942
  } else {
807
943
  logInfo(`${colors.bold}Aident CLI v${VERSION}${colors.reset}`);
@@ -815,6 +951,7 @@ async function runHelp(format) {
815
951
  logInfo(" aident config show Print persistent config");
816
952
  logInfo(" aident config set <key> <value> Persist a config value");
817
953
  logInfo(" aident config get <key> Read a single value");
954
+ logInfo(" aident packages add <playbook|intern> Enable an add-on package");
818
955
  logInfo(" aident doctor Validate installation");
819
956
  logInfo(" aident setup Interactive setup wizard");
820
957
  logInfo(" aident <domain> <command> [--flag value ...] [--json]");
@@ -824,7 +961,7 @@ async function runHelp(format) {
824
961
  const catalog = await fetchCatalog(client);
825
962
  if (!catalog)
826
963
  return;
827
- if (format === "json") {
964
+ if (parsed.format === "json") {
828
965
  logInfo(JSON.stringify(catalog));
829
966
  } else {
830
967
  logInfo(renderHelp(catalog));
@@ -850,10 +987,10 @@ async function runLogout() {
850
987
  await clearCredentials();
851
988
  logInfo(`${colors.green}Signed out.${colors.reset}`);
852
989
  }
853
- async function runWhoami(format) {
854
- const client = await getAuthenticatedClient();
990
+ async function runWhoami(parsed) {
991
+ const client = await getAuthenticatedClient(["loadout"]);
855
992
  if (!client) {
856
- if (format === "json") {
993
+ if (parsed.format === "json") {
857
994
  logInfo(JSON.stringify({ authenticated: false }));
858
995
  } else {
859
996
  logInfo("Not signed in. Run `aident login`.");
@@ -861,8 +998,9 @@ async function runWhoami(format) {
861
998
  process.exitCode = 1;
862
999
  return;
863
1000
  }
1001
+ await fetchCatalog(client);
864
1002
  const result = await callWithRefresh(client, (c) => c.exec("account", "auth status", {}));
865
- if (format === "json") {
1003
+ if (parsed.format === "json") {
866
1004
  logInfo(JSON.stringify(result.body));
867
1005
  if (!result.body.success)
868
1006
  process.exitCode = 1;
@@ -874,7 +1012,7 @@ async function runWhoami(format) {
874
1012
  return;
875
1013
  }
876
1014
  const err = result.body.error;
877
- logErr(`${colors.red} ${err?.code ?? "error"}:${colors.reset} ${err?.message ?? "Unable to fetch user info."}`);
1015
+ logErr(`${colors.red}x ${err?.code ?? "error"}:${colors.reset} ${err?.message ?? "Unable to fetch user info."}`);
878
1016
  process.exitCode = 1;
879
1017
  }
880
1018
  async function runConfigCmd(parsed) {
@@ -887,8 +1025,9 @@ async function runConfigCmd(parsed) {
887
1025
  }
888
1026
  logInfo(`${colors.bold}Aident config${colors.reset} ${colors.dim}(${getConfigFile()})${colors.reset}`);
889
1027
  if (Object.keys(config).length === 0) {
890
- logInfo(` ${colors.dim}(empty using defaults)${colors.reset}`);
1028
+ logInfo(` ${colors.dim}(empty, using defaults)${colors.reset}`);
891
1029
  logInfo(` ${colors.dim}baseUrl${colors.reset} ${colors.dim}default: ${DEFAULT_BASE_URL}${colors.reset}`);
1030
+ logInfo(` ${colors.dim}packages${colors.reset} ${colors.dim}default: loadout${colors.reset}`);
892
1031
  return;
893
1032
  }
894
1033
  for (const [key, val] of Object.entries(config)) {
@@ -934,14 +1073,18 @@ async function runConfigCmd(parsed) {
934
1073
  process.exitCode = 1;
935
1074
  return;
936
1075
  }
937
- const value = key === "baseUrl" ? normalizeBaseUrl(rawValue) : rawValue;
938
- await setConfigValue(key, value);
939
- logInfo(`${colors.green}Set${colors.reset} ${key} = ${value}`);
940
1076
  if (key === "baseUrl") {
1077
+ const value = normalizeBaseUrl(rawValue);
1078
+ await setConfigValue(key, value);
1079
+ logInfo(`${colors.green}Set${colors.reset} ${key} = ${value}`);
941
1080
  const creds = await readCredentials();
942
1081
  if (creds && creds.base_url !== value) {
943
1082
  logInfo(`${colors.yellow}Note:${colors.reset} existing credentials are for ${creds.base_url}. Run \`aident login\` to authenticate against ${value}.`);
944
1083
  }
1084
+ } else {
1085
+ const value = normalizeCliPackages(rawValue);
1086
+ await setConfigValue(key, value);
1087
+ logInfo(`${colors.green}Set${colors.reset} ${key} = ${formatConfigValue(value)}`);
945
1088
  }
946
1089
  return;
947
1090
  }
@@ -960,6 +1103,54 @@ async function runConfigCmd(parsed) {
960
1103
  logErr("Usage: aident config [show | get <key> | set <key> <value> | unset <key> | path]");
961
1104
  process.exitCode = 1;
962
1105
  }
1106
+ async function runPackagesCmd(parsed) {
1107
+ const sub = parsed.positional[1] ?? "list";
1108
+ if (sub === "list" || sub === "show") {
1109
+ const packages = await resolveDefaultPackages();
1110
+ if (parsed.format === "json") {
1111
+ logInfo(JSON.stringify({ packages, defaultPackage: "loadout" }));
1112
+ } else {
1113
+ logInfo(`${colors.bold}Aident packages${colors.reset}`);
1114
+ logInfo(` ${formatCliPackages(packages)}`);
1115
+ logInfo("");
1116
+ logInfo(`Loadout is always enabled. Add-ons: ${CLI_PACKAGES.filter((p) => p !== "loadout").join(", ")}`);
1117
+ }
1118
+ return;
1119
+ }
1120
+ if (sub === "add" || sub === "enable") {
1121
+ const packageName = parsed.positional[2];
1122
+ if (!packageName) {
1123
+ logErr("Usage: aident packages add <playbook|intern>");
1124
+ process.exitCode = 1;
1125
+ return;
1126
+ }
1127
+ const packages = normalizeCliPackages([...await readConfiguredPackages(), parseCliPackage(packageName)]);
1128
+ await setConfigValue("packages", packages);
1129
+ logInfo(`${colors.green}Enabled packages:${colors.reset} ${formatCliPackages(packages)}`);
1130
+ return;
1131
+ }
1132
+ if (sub === "remove" || sub === "disable") {
1133
+ const packageName = parsed.positional[2];
1134
+ if (!packageName) {
1135
+ logErr("Usage: aident packages remove <playbook|intern>");
1136
+ process.exitCode = 1;
1137
+ return;
1138
+ }
1139
+ const target = parseCliPackage(packageName);
1140
+ if (target === "loadout") {
1141
+ logErr("Loadout is the default package and cannot be removed.");
1142
+ process.exitCode = 1;
1143
+ return;
1144
+ }
1145
+ const packages = normalizeCliPackages((await readConfiguredPackages()).filter((p) => p !== target));
1146
+ await setConfigValue("packages", packages);
1147
+ logInfo(`${colors.green}Enabled packages:${colors.reset} ${formatCliPackages(packages)}`);
1148
+ return;
1149
+ }
1150
+ logErr(`Unknown packages subcommand: ${sub}`);
1151
+ logErr("Usage: aident packages [list | add <playbook|intern> | remove <playbook|intern>]");
1152
+ process.exitCode = 1;
1153
+ }
963
1154
  async function runDoctorCmd(format) {
964
1155
  const baseUrl = await resolveDefaultBaseUrl();
965
1156
  const report = await runDoctor({ baseUrl, configFile: getConfigFile(), credentialsFile: getCredentialsFile() });
@@ -972,7 +1163,7 @@ async function runDoctorCmd(format) {
972
1163
  logInfo(`${colors.bold}Aident CLI doctor${colors.reset}`);
973
1164
  logInfo("");
974
1165
  for (const c of report.checks) {
975
- const mark = c.ok ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
1166
+ const mark = c.ok ? `${colors.green}ok${colors.reset}` : `${colors.red}x${colors.reset}`;
976
1167
  logInfo(` ${mark} ${c.name.padEnd(22)} ${colors.dim}${c.detail}${colors.reset}`);
977
1168
  }
978
1169
  logInfo("");
@@ -1009,8 +1200,8 @@ async function runSetup() {
1009
1200
  logInfo("");
1010
1201
  logInfo(`Done. Try ${colors.cyan}aident --help${colors.reset} or ${colors.cyan}aident doctor${colors.reset}.`);
1011
1202
  }
1012
- async function runCatalog(format) {
1013
- const client = await getAuthenticatedClient();
1203
+ async function runCatalog(parsed) {
1204
+ const client = await getAuthenticatedClient(await getRequestedPackages(parsed));
1014
1205
  if (!client) {
1015
1206
  process.exitCode = 1;
1016
1207
  return;
@@ -1018,10 +1209,10 @@ async function runCatalog(format) {
1018
1209
  const catalog = await fetchCatalog(client);
1019
1210
  if (!catalog)
1020
1211
  return;
1021
- logInfo(JSON.stringify(catalog, null, format === "json" ? 0 : 2));
1212
+ logInfo(JSON.stringify(catalog, null, parsed.format === "json" ? 0 : 2));
1022
1213
  }
1023
1214
  async function runCommand(parsed) {
1024
- const client = await getAuthenticatedClient();
1215
+ const client = await getAuthenticatedClient(await getRequestedPackages(parsed));
1025
1216
  if (!client) {
1026
1217
  if (parsed.format === "json") {
1027
1218
  logInfo(JSON.stringify({ success: false, error: { code: "not-authenticated", message: "Run `aident login` first." } }));
@@ -1097,17 +1288,33 @@ function emitResult(result, format, resolved) {
1097
1288
  return;
1098
1289
  }
1099
1290
  if (body.success) {
1100
- logInfo(`${colors.green}✓${colors.reset} ${resolved.domain} ${resolved.command}`);
1291
+ logInfo(`${colors.green}ok${colors.reset} ${resolved.domain} ${resolved.command}`);
1101
1292
  if (body.data !== undefined)
1102
1293
  logInfo(JSON.stringify(body.data, null, 2));
1103
1294
  if (body.meta)
1104
1295
  logInfo(`${colors.cyan}meta:${colors.reset} ${JSON.stringify(body.meta)}`);
1105
1296
  } else {
1106
- logErr(`${colors.red} ${body.error?.code ?? "error"}:${colors.reset} ${body.error?.message ?? "Unknown error"}`);
1297
+ logErr(`${colors.red}x ${body.error?.code ?? "error"}:${colors.reset} ${body.error?.message ?? "Unknown error"}`);
1107
1298
  process.exitCode = 1;
1108
1299
  }
1109
1300
  }
1110
- async function getAuthenticatedClient() {
1301
+ async function getRequestedPackages(parsed) {
1302
+ const rawPackage = parsed.flags["package"] ?? process.env.AIDENT_PACKAGE;
1303
+ if (rawPackage !== undefined && rawPackage !== null && rawPackage !== false) {
1304
+ if (rawPackage !== true)
1305
+ return [parseCliPackage(rawPackage)];
1306
+ throw new Error("--package requires a value: loadout, playbook, or intern");
1307
+ }
1308
+ const rawPackages = parsed.flags["packages"] ?? process.env.AIDENT_PACKAGES;
1309
+ if (rawPackages !== undefined && rawPackages !== null && rawPackages !== false) {
1310
+ if (rawPackages !== true)
1311
+ return normalizeCliPackages(rawPackages);
1312
+ throw new Error("--packages requires a comma-separated package list");
1313
+ }
1314
+ return resolveDefaultPackages();
1315
+ }
1316
+ async function getAuthenticatedClient(packages) {
1317
+ const activePackages = packages ?? await resolveDefaultPackages();
1111
1318
  const envToken = process.env.AIDENT_TOKEN;
1112
1319
  const envBaseUrl = process.env.AIDENT_BASE_URL?.trim();
1113
1320
  if (envToken) {
@@ -1116,7 +1323,7 @@ async function getAuthenticatedClient() {
1116
1323
  base_url: normalizeBaseUrl(baseUrl),
1117
1324
  client_id: "",
1118
1325
  access_token: envToken
1119
- });
1326
+ }, "env", activePackages);
1120
1327
  }
1121
1328
  let creds = await readCredentials();
1122
1329
  if (!creds)
@@ -1131,12 +1338,13 @@ async function getAuthenticatedClient() {
1131
1338
  creds = refreshed;
1132
1339
  }
1133
1340
  }
1134
- return new CliClient(creds);
1341
+ return new CliClient(creds, "stored", activePackages);
1135
1342
  }
1136
1343
  var catalogCache = null;
1137
1344
  async function fetchCatalog(client) {
1138
- if (catalogCache)
1139
- return catalogCache;
1345
+ const packagesKey = client.activePackages.join(",");
1346
+ if (catalogCache && catalogCache.packagesKey === packagesKey)
1347
+ return catalogCache.catalog;
1140
1348
  const res = await callWithRefresh(client, (c) => c.getCatalog());
1141
1349
  if (res.status === 401) {
1142
1350
  logErr(`Not authenticated (HTTP ${res.status}). Run \`aident login\`.`);
@@ -1148,8 +1356,15 @@ async function fetchCatalog(client) {
1148
1356
  process.exitCode = 1;
1149
1357
  return null;
1150
1358
  }
1151
- catalogCache = res.body;
1152
- return catalogCache;
1359
+ catalogCache = { packagesKey, catalog: res.body };
1360
+ return catalogCache.catalog;
1361
+ }
1362
+ function formatConfigValue(value) {
1363
+ return Array.isArray(value) ? value.join(",") : String(value);
1364
+ }
1365
+ async function readConfiguredPackages() {
1366
+ const config = await readConfig();
1367
+ return normalizeCliPackages(config.packages);
1153
1368
  }
1154
1369
  function isCommandCatalog(body) {
1155
1370
  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.6",
4
- "description": "Aident CLI — agent-friendly access to 1000+ integrations, playbooks, and the Aident automation platform.",
3
+ "version": "0.1.0-rc.1",
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:*",