@aident-ai/cli 0.1.2 → 0.1.3-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 +14 -3
  2. package/dist/cli.mjs +590 -205
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,6 +33,14 @@ Use `--oob` for browserless auth environments:
33
33
  aident login --oob
34
34
  ```
35
35
 
36
+ To revoke the stored OAuth token and remove `~/.aident/credentials.json`:
37
+
38
+ ```bash
39
+ aident logout
40
+ ```
41
+
42
+ When using `AIDENT_TOKEN`, unset it in your shell after running `aident logout`.
43
+
36
44
  ## Packages
37
45
 
38
46
  Loadout is always enabled by default. Add Playbook only when an agent needs to create, execute, or manage playbooks.
@@ -50,9 +58,9 @@ aident --packages playbook playbooks execute --playbookId pb_123 --json
50
58
 
51
59
  ```bash
52
60
  aident capabilities search --query "send email"
53
- aident capabilities get --name "gmail_tools.gmail_send_email"
54
- aident capabilities execute --name "gmail_tools.gmail_send_email" --input '{"to":"user@example.com"}'
55
- aident vault status --integrationId github_tools
61
+ aident capabilities get --name "composio:gmail_tools:gmail_send_email"
62
+ aident capabilities execute --name "composio:gmail_tools:gmail_send_email" --input '{"to":"user@example.com"}'
63
+ aident vault status --integrationId composio:github_tools
56
64
  aident integrations migrate-local --json
57
65
  aident integrations migrate-local --apply --integrationIds github_tools,slack_tools --json
58
66
  aident audit recent --limit 20
@@ -86,6 +94,9 @@ Environment overrides:
86
94
  | `AIDENT_PACKAGE` | Focus one package for one invocation. |
87
95
  | `AIDENT_PACKAGES` | Enable add-on packages for one invocation, e.g. `playbook`. |
88
96
 
97
+ OAuth credentials are shared across HTTPS Aident hosts under `aident.ai`, so changing `baseUrl` or `AIDENT_BASE_URL`
98
+ between production, RC, staging, or preview hosts does not require another login. Custom hosts remain isolated.
99
+
89
100
  ## How It Works
90
101
 
91
102
  Discovery fetches `/api/openapi/{package}.json` and reads the package command catalog embedded in the OpenAPI document.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/cli.ts
4
+ import { readFile as readFile6 } from "node:fs/promises";
5
+
3
6
  // src/auth.ts
4
7
  import { spawn } from "node:child_process";
5
8
  import { createHash, randomBytes } from "node:crypto";
@@ -444,16 +447,160 @@ function escapeHtml(s) {
444
447
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
445
448
  }
446
449
 
450
+ // src/catalogCache.ts
451
+ import crypto from "node:crypto";
452
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
453
+ import { join as join2 } from "node:path";
454
+
455
+ // src/config.ts
456
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
457
+ import { homedir } from "node:os";
458
+ import { join } from "node:path";
459
+
460
+ // src/packages.ts
461
+ var CLI_PACKAGES = ["loadout", "playbook", "intern"];
462
+ var DEFAULT_CLI_PACKAGES = ["loadout"];
463
+ function parseCliPackage(value) {
464
+ if (typeof value !== "string")
465
+ throw new Error(`Unsupported Aident package: ${String(value)}`);
466
+ const normalized = value.trim().toLowerCase();
467
+ if (!CLI_PACKAGES.includes(normalized))
468
+ throw new Error(`Unsupported Aident package: ${value}`);
469
+ return normalized;
470
+ }
471
+ function normalizeCliPackages(value) {
472
+ const rawPackages = value === undefined || value === null || value === "" ? [] : Array.isArray(value) ? value : String(value).split(/[,\s]+/);
473
+ const packages = ["loadout"];
474
+ for (const rawPackage of rawPackages) {
475
+ if (rawPackage === undefined || rawPackage === null || rawPackage === "")
476
+ continue;
477
+ const cliPackage = parseCliPackage(rawPackage);
478
+ if (!packages.includes(cliPackage))
479
+ packages.push(cliPackage);
480
+ }
481
+ return packages;
482
+ }
483
+ function formatCliPackages(packages) {
484
+ return packages.join(",");
485
+ }
486
+
487
+ // src/config.ts
488
+ function getAidentDir() {
489
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
490
+ return join(home, ".aident");
491
+ }
492
+ function getConfigFile() {
493
+ return join(getAidentDir(), "config.json");
494
+ }
495
+ var DEFAULT_BASE_URL = "https://loadout.aident.ai";
496
+ var CONFIG_KEYS = ["baseUrl", "packages"];
497
+ async function readConfig() {
498
+ try {
499
+ const text = await readFile(getConfigFile(), "utf-8");
500
+ const parsed = JSON.parse(text);
501
+ if (!parsed || typeof parsed !== "object")
502
+ return {};
503
+ return parsed;
504
+ } catch {
505
+ return {};
506
+ }
507
+ }
508
+ async function writeConfig(config) {
509
+ await mkdir(getAidentDir(), { recursive: true, mode: 448 });
510
+ await writeFile(getConfigFile(), JSON.stringify(config, null, 2) + `
511
+ `, { mode: 420 });
512
+ }
513
+ async function setConfigValue(key, value) {
514
+ const config = await readConfig();
515
+ config[key] = value;
516
+ await writeConfig(config);
517
+ }
518
+ async function unsetConfigValue(key) {
519
+ const config = await readConfig();
520
+ delete config[key];
521
+ await writeConfig(config);
522
+ }
523
+ async function resolveDefaultBaseUrl() {
524
+ const env = process.env.AIDENT_BASE_URL?.trim();
525
+ if (env)
526
+ return env;
527
+ const config = await readConfig();
528
+ if (typeof config.baseUrl === "string" && config.baseUrl.trim() !== "") {
529
+ return config.baseUrl.trim();
530
+ }
531
+ return DEFAULT_BASE_URL;
532
+ }
533
+ async function resolveDefaultPackages() {
534
+ const env = process.env.AIDENT_PACKAGES?.trim() || process.env.AIDENT_PACKAGE?.trim();
535
+ if (env)
536
+ return normalizeCliPackages(env);
537
+ const config = await readConfig();
538
+ return normalizeCliPackages(config.packages ?? DEFAULT_CLI_PACKAGES);
539
+ }
540
+ function isKnownConfigKey(key) {
541
+ return CONFIG_KEYS.includes(key);
542
+ }
543
+ function normalizeBaseUrl(url) {
544
+ let trimmed = url.trim();
545
+ if (!/^https?:\/\//i.test(trimmed))
546
+ trimmed = `https://${trimmed}`;
547
+ return trimmed.replace(/\/+$/, "");
548
+ }
549
+
447
550
  // src/version.ts
448
- var VERSION = "0.1.2";
551
+ var VERSION = "0.1.3-rc.1";
552
+
553
+ // src/catalogCache.ts
554
+ var CACHE_TTL_MS = 5 * 60 * 1000;
555
+ async function genReadCachedCatalog(params) {
556
+ if (process.env.AIDENT_CLI_DISABLE_CATALOG_CACHE === "1")
557
+ return null;
558
+ try {
559
+ const cached = JSON.parse(await readFile2(getCacheFile(params), "utf8"));
560
+ if (cached.credentialFingerprint !== getCredentialFingerprint(params.accessToken) || Date.now() - cached.cachedAt > CACHE_TTL_MS || !isCommandCatalog(cached.catalog))
561
+ return null;
562
+ return cached.catalog;
563
+ } catch {
564
+ return null;
565
+ }
566
+ }
567
+ async function genWriteCachedCatalog(params, catalog) {
568
+ if (process.env.AIDENT_CLI_DISABLE_CATALOG_CACHE === "1")
569
+ return;
570
+ try {
571
+ const cacheDir = join2(getAidentDir(), "cache");
572
+ await mkdir2(cacheDir, { recursive: true, mode: 448 });
573
+ await writeFile2(getCacheFile(params), JSON.stringify({
574
+ cachedAt: Date.now(),
575
+ catalog,
576
+ credentialFingerprint: getCredentialFingerprint(params.accessToken)
577
+ }), { mode: 384 });
578
+ } catch {}
579
+ }
580
+ function getCacheFile(params) {
581
+ const key = crypto.createHash("sha256").update([params.baseUrl, params.packageName, params.installedSkillVersion ?? "", VERSION].join("\x00")).digest("hex").slice(0, 16);
582
+ return join2(getAidentDir(), "cache", `catalog-${key}.json`);
583
+ }
584
+ function getCredentialFingerprint(accessToken) {
585
+ return crypto.createHash("sha256").update(accessToken).digest("hex").slice(0, 16);
586
+ }
587
+ function isCommandCatalog(value) {
588
+ if (!value || typeof value !== "object")
589
+ return false;
590
+ const catalog = value;
591
+ return Array.isArray(catalog.packages) && Array.isArray(catalog.domains) && Array.isArray(catalog.commands);
592
+ }
449
593
 
450
594
  // src/client.ts
595
+ var CLI_CATALOG_DURATION_HEADER = "x-aident-cli-catalog-duration-ms";
596
+
451
597
  class CliClient {
452
598
  creds;
453
599
  credentialSource;
454
600
  packages;
455
601
  installedSkillVersion;
456
602
  commandOperations = new Map;
603
+ catalogDurationMs = null;
457
604
  constructor(creds, credentialSource = "stored", packages, installedSkillVersion = null) {
458
605
  this.creds = creds;
459
606
  this.credentialSource = credentialSource;
@@ -475,6 +622,14 @@ class CliClient {
475
622
  this.creds = creds;
476
623
  }
477
624
  async getCatalog() {
625
+ const startedAt = Date.now();
626
+ try {
627
+ return await this.fetchCatalog();
628
+ } finally {
629
+ this.catalogDurationMs = Date.now() - startedAt;
630
+ }
631
+ }
632
+ async fetchCatalog() {
478
633
  if (this.packages.length === 1) {
479
634
  const packageName = this.packages[0];
480
635
  const result = await this.fetchOpenApiCatalog(packageName);
@@ -503,14 +658,37 @@ class CliClient {
503
658
  }
504
659
  };
505
660
  }
506
- return this.fetchJson("POST", this.operationPath(operation.packageName, operation.operationId), args);
661
+ return this.fetchJson("POST", this.operationPath(operation.packageName, operation.operationId), args, this.takeCatalogTimingHeaders());
507
662
  }
508
663
  async execOperation(packageName, operationId, args) {
509
- return this.fetchJson("POST", this.operationPath(packageName, operationId), args);
664
+ return this.fetchJson("POST", this.operationPath(packageName, operationId), args, this.takeCatalogTimingHeaders());
665
+ }
666
+ takeCatalogTimingHeaders() {
667
+ if (this.catalogDurationMs === null)
668
+ return;
669
+ const headers = { [CLI_CATALOG_DURATION_HEADER]: String(this.catalogDurationMs) };
670
+ this.catalogDurationMs = null;
671
+ return headers;
510
672
  }
511
673
  async fetchOpenApiCatalog(packageName) {
674
+ const cacheParams = {
675
+ accessToken: this.creds.access_token,
676
+ baseUrl: this.baseUrl,
677
+ packageName,
678
+ installedSkillVersion: this.installedSkillVersion
679
+ };
680
+ const cached = await genReadCachedCatalog(cacheParams);
681
+ if (cached)
682
+ return { status: 200, body: cached };
512
683
  const headers = packageName === "loadout" && this.installedSkillVersion ? { "x-aident-skill-version": this.installedSkillVersion } : undefined;
513
- const result = await this.fetchJson("GET", this.catalogPath(packageName), undefined, headers);
684
+ const path = this.catalogPath(packageName);
685
+ let result;
686
+ try {
687
+ result = await this.fetchJson("GET", path, undefined, headers);
688
+ } catch (error) {
689
+ const detail = error instanceof Error ? error.message : String(error);
690
+ throw new Error(`Unable to reach the Aident ${packageName} command catalog at ${this.baseUrl}${path}. Allow outbound HTTPS access to ${this.baseUrl} and retry. (${detail})`);
691
+ }
514
692
  const catalog = result.body["x-aident-command-catalog"];
515
693
  if (result.status !== 200 || !catalog) {
516
694
  return {
@@ -521,6 +699,7 @@ class CliClient {
521
699
  if (packageName === "loadout" && result.body["x-aident-loadout-skill"]) {
522
700
  catalog.loadoutSkill = result.body["x-aident-loadout-skill"];
523
701
  }
702
+ await genWriteCachedCatalog(cacheParams, catalog);
524
703
  return { status: result.status, body: catalog };
525
704
  }
526
705
  catalogPath(packageName) {
@@ -603,111 +782,16 @@ function mergeCatalogs(catalogs, packages, onCommand) {
603
782
  };
604
783
  }
605
784
 
606
- // src/config.ts
607
- import { mkdir, readFile, writeFile } from "node:fs/promises";
608
- import { homedir } from "node:os";
609
- import { join } from "node:path";
610
-
611
- // src/packages.ts
612
- var CLI_PACKAGES = ["loadout", "playbook", "intern"];
613
- var DEFAULT_CLI_PACKAGES = ["loadout"];
614
- function parseCliPackage(value) {
615
- if (typeof value !== "string")
616
- throw new Error(`Unsupported Aident package: ${String(value)}`);
617
- const normalized = value.trim().toLowerCase();
618
- if (!CLI_PACKAGES.includes(normalized))
619
- throw new Error(`Unsupported Aident package: ${value}`);
620
- return normalized;
621
- }
622
- function normalizeCliPackages(value) {
623
- const rawPackages = value === undefined || value === null || value === "" ? [] : Array.isArray(value) ? value : String(value).split(/[,\s]+/);
624
- const packages = ["loadout"];
625
- for (const rawPackage of rawPackages) {
626
- if (rawPackage === undefined || rawPackage === null || rawPackage === "")
627
- continue;
628
- const cliPackage = parseCliPackage(rawPackage);
629
- if (!packages.includes(cliPackage))
630
- packages.push(cliPackage);
631
- }
632
- return packages;
633
- }
634
- function formatCliPackages(packages) {
635
- return packages.join(",");
636
- }
637
-
638
- // src/config.ts
639
- function getAidentDir() {
640
- const home = process.env.HOME || process.env.USERPROFILE || homedir();
641
- return join(home, ".aident");
642
- }
643
- function getConfigFile() {
644
- return join(getAidentDir(), "config.json");
645
- }
646
- var DEFAULT_BASE_URL = "https://loadout.aident.ai";
647
- var CONFIG_KEYS = ["baseUrl", "packages"];
648
- async function readConfig() {
649
- try {
650
- const text = await readFile(getConfigFile(), "utf-8");
651
- const parsed = JSON.parse(text);
652
- if (!parsed || typeof parsed !== "object")
653
- return {};
654
- return parsed;
655
- } catch {
656
- return {};
657
- }
658
- }
659
- async function writeConfig(config) {
660
- await mkdir(getAidentDir(), { recursive: true, mode: 448 });
661
- await writeFile(getConfigFile(), JSON.stringify(config, null, 2) + `
662
- `, { mode: 420 });
663
- }
664
- async function setConfigValue(key, value) {
665
- const config = await readConfig();
666
- config[key] = value;
667
- await writeConfig(config);
668
- }
669
- async function unsetConfigValue(key) {
670
- const config = await readConfig();
671
- delete config[key];
672
- await writeConfig(config);
673
- }
674
- async function resolveDefaultBaseUrl() {
675
- const env = process.env.AIDENT_BASE_URL?.trim();
676
- if (env)
677
- return env;
678
- const config = await readConfig();
679
- if (typeof config.baseUrl === "string" && config.baseUrl.trim() !== "") {
680
- return config.baseUrl.trim();
681
- }
682
- return DEFAULT_BASE_URL;
683
- }
684
- async function resolveDefaultPackages() {
685
- const env = process.env.AIDENT_PACKAGES?.trim() || process.env.AIDENT_PACKAGE?.trim();
686
- if (env)
687
- return normalizeCliPackages(env);
688
- const config = await readConfig();
689
- return normalizeCliPackages(config.packages ?? DEFAULT_CLI_PACKAGES);
690
- }
691
- function isKnownConfigKey(key) {
692
- return CONFIG_KEYS.includes(key);
693
- }
694
- function normalizeBaseUrl(url) {
695
- let trimmed = url.trim();
696
- if (!/^https?:\/\//i.test(trimmed))
697
- trimmed = `https://${trimmed}`;
698
- return trimmed.replace(/\/+$/, "");
699
- }
700
-
701
785
  // src/credentials.ts
702
- import { mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "node:fs/promises";
703
- import { join as join2 } from "node:path";
786
+ import { mkdir as mkdir3, readFile as readFile3, rm, writeFile as writeFile3 } from "node:fs/promises";
787
+ import { join as join3 } from "node:path";
704
788
  var REFRESH_WINDOW_MS = 24 * 60 * 60 * 1000;
705
789
  function getCredentialsFile() {
706
- return join2(getAidentDir(), "credentials.json");
790
+ return join3(getAidentDir(), "credentials.json");
707
791
  }
708
792
  async function readCredentials() {
709
793
  try {
710
- const content = await readFile2(getCredentialsFile(), "utf-8");
794
+ const content = await readFile3(getCredentialsFile(), "utf-8");
711
795
  const parsed = JSON.parse(content);
712
796
  if (!parsed.access_token || !parsed.base_url)
713
797
  return null;
@@ -717,12 +801,31 @@ async function readCredentials() {
717
801
  }
718
802
  }
719
803
  async function writeCredentials(creds) {
720
- await mkdir2(getAidentDir(), { recursive: true, mode: 448 });
721
- await writeFile2(getCredentialsFile(), JSON.stringify(creds, null, 2), { mode: 384 });
804
+ await mkdir3(getAidentDir(), { recursive: true, mode: 448 });
805
+ await writeFile3(getCredentialsFile(), JSON.stringify(creds, null, 2), { mode: 384 });
722
806
  }
723
807
  async function clearCredentials() {
724
808
  await rm(getCredentialsFile(), { force: true });
725
809
  }
810
+ function credentialsForBaseUrl(creds, baseUrl) {
811
+ const credentialsBaseUrl = creds.base_url.replace(/\/+$/, "");
812
+ const requestedBaseUrl = baseUrl.replace(/\/+$/, "");
813
+ if (credentialsBaseUrl === requestedBaseUrl)
814
+ return { ...creds, base_url: requestedBaseUrl };
815
+ if (!getSharedAidentBaseUrl(credentialsBaseUrl))
816
+ return null;
817
+ const sharedBaseUrl = getSharedAidentBaseUrl(requestedBaseUrl);
818
+ if (!sharedBaseUrl)
819
+ return null;
820
+ return { ...creds, base_url: sharedBaseUrl };
821
+ }
822
+ async function clearCredentialsForIncompatibleBaseUrl(baseUrl) {
823
+ const creds = await readCredentials();
824
+ if (!creds || credentialsForBaseUrl(creds, baseUrl))
825
+ return null;
826
+ await clearCredentials();
827
+ return creds.base_url;
828
+ }
726
829
  function isExpired(creds) {
727
830
  if (!creds.expires_at)
728
831
  return false;
@@ -731,11 +834,23 @@ function isExpired(creds) {
731
834
  return false;
732
835
  return Date.now() >= expiresAt - REFRESH_WINDOW_MS;
733
836
  }
837
+ function getSharedAidentBaseUrl(baseUrl) {
838
+ try {
839
+ const url = new URL(baseUrl);
840
+ 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")) {
841
+ return null;
842
+ }
843
+ return url.origin;
844
+ } catch {
845
+ return null;
846
+ }
847
+ }
734
848
 
735
849
  // src/loadoutSkill.ts
736
- import { readFile as readFile3 } from "node:fs/promises";
850
+ import { existsSync } from "node:fs";
851
+ import { readFile as readFile4, realpath } from "node:fs/promises";
737
852
  import { homedir as homedir2 } from "node:os";
738
- import { join as join3 } from "node:path";
853
+ import { dirname, join as join4, resolve } from "node:path";
739
854
  async function fetchLoadoutSkillMetadata(baseUrl) {
740
855
  const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/.well-known/loadout-skill.json`, {
741
856
  method: "GET",
@@ -756,34 +871,104 @@ function isLocalIntegrationMigrationPromptEnabled(metadata) {
756
871
  return metadata?.localIntegrationMigrationPromptEnabled === true;
757
872
  }
758
873
  function getInstalledLoadoutSkillCandidates(cwd = process.cwd(), home = homedir2()) {
759
- const skillFile = join3("skills", "aident-skill", "SKILL.md");
760
- return [
761
- join3(cwd, ".claude", skillFile),
762
- join3(cwd, ".agents", skillFile),
763
- join3(home, ".claude", skillFile),
764
- join3(home, ".codex", skillFile),
765
- join3(home, ".cursor", skillFile),
766
- join3(home, ".agents", skillFile),
767
- join3(home, ".gemini", "extensions", "aident-skill", "SKILL.md")
768
- ];
874
+ const skillFile = join4("skills", "aident-skill", "SKILL.md");
875
+ const candidates = [];
876
+ const paths = new Set;
877
+ const add = (agent, scope, root, folder = "") => {
878
+ const path = join4(root, folder, skillFile);
879
+ if (paths.has(path))
880
+ return;
881
+ paths.add(path);
882
+ candidates.push({ agent, scope, path });
883
+ };
884
+ for (const root of getProjectRoots(cwd)) {
885
+ add("shared" /* Shared */, "project" /* Project */, root, ".agents");
886
+ add("claude-code" /* ClaudeCode */, "project" /* Project */, root, ".claude");
887
+ add("cursor" /* Cursor */, "project" /* Project */, root, ".cursor");
888
+ add("gemini" /* Gemini */, "project" /* Project */, root, ".gemini");
889
+ add("workbuddy" /* WorkBuddy */, "project" /* Project */, root, ".workbuddy");
890
+ }
891
+ add("shared" /* Shared */, "global" /* Global */, home, ".agents");
892
+ add("shared" /* Shared */, "global" /* Global */, home, join4(".config", "agents"));
893
+ add("claude-code" /* ClaudeCode */, "global" /* Global */, home, ".claude");
894
+ add("codex" /* Codex */, "global" /* Global */, home, ".codex");
895
+ add("cursor" /* Cursor */, "global" /* Global */, home, ".cursor");
896
+ add("gemini" /* Gemini */, "global" /* Global */, home, ".gemini");
897
+ add("workbuddy" /* WorkBuddy */, "global" /* Global */, home, ".workbuddy");
898
+ add("codex" /* Codex */, "admin" /* Admin */, "/etc/codex");
899
+ return candidates;
769
900
  }
770
901
  function formatLoadoutSkillWarnings(metadata) {
771
902
  const warnings = metadata?.notices.filter((notice) => notice.severity === "warning") ?? [];
772
903
  return warnings.map((notice) => `${colors.yellow}Warning:${colors.reset} ${notice.message}`);
773
904
  }
774
- async function findInstalledLoadoutSkillVersion(candidates = getInstalledLoadoutSkillCandidates()) {
775
- for (const candidate of candidates) {
776
- let content;
905
+ function isLoadoutSkillVersionBehind(installed, current) {
906
+ const installedParts = parseSkillVersion(installed);
907
+ const currentParts = parseSkillVersion(current);
908
+ if (!installedParts || !currentParts)
909
+ return false;
910
+ for (let i = 0;i < installedParts.length; i++) {
911
+ if (installedParts[i] !== currentParts[i])
912
+ return installedParts[i] < currentParts[i];
913
+ }
914
+ return false;
915
+ }
916
+ async function scanInstalledLoadoutSkills(candidates = getInstalledLoadoutSkillCandidates()) {
917
+ const matches = await Promise.all(candidates.map(async (candidate) => {
777
918
  try {
778
- content = await readFile3(candidate, "utf-8");
919
+ const [content, resolvedPath] = await Promise.all([
920
+ readFile4(candidate.path, "utf-8"),
921
+ realpath(candidate.path)
922
+ ]);
923
+ const match = /^version:\s*(\d+\.\d+\.\d+)\s*$/m.exec(content);
924
+ return match ? { candidate, resolvedPath, version: match[1] } : null;
779
925
  } catch {
780
- continue;
926
+ return null;
781
927
  }
782
- const match = /^version:\s*(\d+\.\d+\.\d+)\s*$/m.exec(content);
783
- if (match)
784
- return match[1];
928
+ }));
929
+ const installed = [];
930
+ const resolvedPaths = new Set;
931
+ for (const match of matches) {
932
+ if (!match || resolvedPaths.has(match.resolvedPath))
933
+ continue;
934
+ resolvedPaths.add(match.resolvedPath);
935
+ installed.push({ ...match.candidate, version: match.version });
936
+ }
937
+ return installed;
938
+ }
939
+ async function findInstalledLoadoutSkillVersion(candidates = getInstalledLoadoutSkillCandidates()) {
940
+ const installed = await scanInstalledLoadoutSkills(candidates);
941
+ return installed.reduce((oldest, skill) => !oldest || isLoadoutSkillVersionBehind(skill.version, oldest) ? skill.version : oldest, null);
942
+ }
943
+ function formatLoadoutAgentHost(agent) {
944
+ switch (agent) {
945
+ case "claude-code" /* ClaudeCode */:
946
+ return "Claude Code";
947
+ case "codex" /* Codex */:
948
+ return "Codex";
949
+ case "cursor" /* Cursor */:
950
+ return "Cursor";
951
+ case "gemini" /* Gemini */:
952
+ return "Gemini";
953
+ case "workbuddy" /* WorkBuddy */:
954
+ return "WorkBuddy";
955
+ case "shared" /* Shared */:
956
+ return "Shared agents";
957
+ }
958
+ }
959
+ function getProjectRoots(cwd) {
960
+ const start = resolve(cwd);
961
+ const roots = [];
962
+ let current = start;
963
+ while (true) {
964
+ roots.push(current);
965
+ if (existsSync(join4(current, ".git")))
966
+ return roots;
967
+ const parent = dirname(current);
968
+ if (parent === current)
969
+ return [start];
970
+ current = parent;
785
971
  }
786
- return null;
787
972
  }
788
973
  function isLoadoutSkillNotice(value) {
789
974
  if (!value || typeof value !== "object")
@@ -791,27 +976,56 @@ function isLoadoutSkillNotice(value) {
791
976
  const body = value;
792
977
  return typeof body.id === "string" && (body.severity === "info" || body.severity === "warning") && typeof body.message === "string";
793
978
  }
979
+ function parseSkillVersion(version) {
980
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
981
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
982
+ }
794
983
 
795
984
  // src/doctor.ts
796
- async function runDoctor(opts) {
985
+ async function runDoctor(opts, skillCandidates = getInstalledLoadoutSkillCandidates()) {
797
986
  const checks = [];
798
987
  checks.push(checkNodeVersion());
799
988
  checks.push(await checkConfig());
800
989
  checks.push(await checkCredentials(opts.baseUrl));
801
990
  checks.push(await checkServerReachable(opts.baseUrl));
802
- checks.push(await checkLoadoutSkillMetadata(opts.baseUrl));
991
+ checks.push(...await checkLoadoutSkills(opts.baseUrl, skillCandidates));
803
992
  const ok = checks.every((c) => c.ok);
804
993
  return { ok, checks, ...opts };
805
994
  }
806
- async function checkLoadoutSkillMetadata(baseUrl) {
807
- const metadata = await fetchLoadoutSkillMetadata(baseUrl);
808
- if (!metadata)
809
- return { name: "Loadout skill", ok: true, detail: "freshness metadata unavailable" };
810
- return {
811
- name: "Loadout skill",
812
- ok: true,
813
- detail: `v${metadata.skillVersion}; ${metadata.updatePrompt}`
814
- };
995
+ async function checkLoadoutSkills(baseUrl, candidates) {
996
+ const [metadata, installedSkills] = await Promise.all([
997
+ fetchLoadoutSkillMetadata(baseUrl),
998
+ scanInstalledLoadoutSkills(candidates)
999
+ ]);
1000
+ if (installedSkills.length === 0) {
1001
+ return [
1002
+ {
1003
+ name: "Loadout skills",
1004
+ ok: true,
1005
+ detail: metadata ? `none found; latest v${metadata.skillVersion}; ${metadata.updatePrompt}` : "none found; freshness metadata unavailable"
1006
+ }
1007
+ ];
1008
+ }
1009
+ return installedSkills.map((skill) => {
1010
+ const name = `Loadout skill (${formatLoadoutAgentHost(skill.agent)})`;
1011
+ const location = `${skill.scope}; ${skill.path}`;
1012
+ if (!metadata) {
1013
+ return { name, ok: true, detail: `installed v${skill.version}; latest unavailable; ${location}` };
1014
+ }
1015
+ if (isLoadoutSkillVersionBehind(skill.version, metadata.skillVersion)) {
1016
+ return {
1017
+ name,
1018
+ ok: true,
1019
+ warning: true,
1020
+ detail: `installed v${skill.version}; latest v${metadata.skillVersion}; ${location}; ${metadata.updatePrompt}`
1021
+ };
1022
+ }
1023
+ return {
1024
+ name,
1025
+ ok: true,
1026
+ detail: skill.version === metadata.skillVersion ? `installed v${skill.version} (latest); ${location}` : `installed v${skill.version}; published v${metadata.skillVersion}; ${location}`
1027
+ };
1028
+ });
815
1029
  }
816
1030
  function checkNodeVersion() {
817
1031
  const version = process.versions.node;
@@ -840,14 +1054,14 @@ async function checkCredentials(baseUrl) {
840
1054
  if (!creds) {
841
1055
  return { name: "Authenticated", ok: false, detail: "no credentials — run `aident login`" };
842
1056
  }
843
- if (creds.base_url !== baseUrl) {
1057
+ if (!credentialsForBaseUrl(creds, baseUrl)) {
844
1058
  return {
845
1059
  name: "Authenticated",
846
1060
  ok: false,
847
1061
  detail: `credentials are for ${creds.base_url} but config baseUrl is ${baseUrl} — run \`aident login\``
848
1062
  };
849
1063
  }
850
- return { name: "Authenticated", ok: true, detail: `signed in to ${creds.base_url}` };
1064
+ return { name: "Authenticated", ok: true, detail: `signed in to ${baseUrl}` };
851
1065
  }
852
1066
  async function checkServerReachable(baseUrl) {
853
1067
  try {
@@ -879,7 +1093,7 @@ async function checkServerReachable(baseUrl) {
879
1093
  // src/help.ts
880
1094
  var LOCAL_HELP_COMMANDS = [
881
1095
  { command: "login", description: "Authenticate with Aident" },
882
- { command: "logout", description: "Revoke the current token" },
1096
+ { command: "logout", description: "Sign out and remove local credentials" },
883
1097
  { command: "whoami", description: "Show current user" },
884
1098
  { command: "config show", description: "Print persistent config" },
885
1099
  { command: "config set <key> <value>", description: "Persist a config value" },
@@ -932,7 +1146,7 @@ function renderHelp(catalog) {
932
1146
  lines.push("USAGE:");
933
1147
  lines.push(" aident <domain> <command> [--flag value ...] [--json]");
934
1148
  lines.push(" aident login Authenticate with Aident");
935
- lines.push(" aident logout Revoke the current token");
1149
+ lines.push(" aident logout Sign out and remove local credentials");
936
1150
  lines.push(" aident whoami Show current user");
937
1151
  lines.push(" aident <domain> help List commands in a domain");
938
1152
  lines.push(" aident <domain> <command> help Show input schema and examples");
@@ -972,6 +1186,18 @@ function renderLoginHelp() {
972
1186
  ].join(`
973
1187
  `);
974
1188
  }
1189
+ function renderLogoutHelp() {
1190
+ return [
1191
+ `${colors.bold}AIDENT LOGOUT${colors.reset}`,
1192
+ "",
1193
+ "USAGE:",
1194
+ " aident logout",
1195
+ "",
1196
+ "Revokes the stored OAuth token and removes ~/.aident/credentials.json.",
1197
+ "If AIDENT_TOKEN is set, unset it in your shell to finish signing out."
1198
+ ].join(`
1199
+ `);
1200
+ }
975
1201
  function renderDomainHelp(catalog, domain) {
976
1202
  const cmds = catalog.commands.filter((c) => c.domain === domain);
977
1203
  if (cmds.length === 0)
@@ -987,20 +1213,27 @@ function renderDomainHelp(catalog, domain) {
987
1213
  lines.push("");
988
1214
  lines.push("COMMANDS:");
989
1215
  for (const c of cmds) {
990
- lines.push(` ${c.command.padEnd(28)} ${c.description}`);
1216
+ if (c.command === domain && c.cliActionSubcommands?.length) {
1217
+ for (const subcommand of c.cliActionSubcommands) {
1218
+ lines.push(` ${subcommand.padEnd(28)} ${c.description}`);
1219
+ }
1220
+ } else {
1221
+ lines.push(` ${c.command.padEnd(28)} ${c.description}`);
1222
+ }
991
1223
  }
992
1224
  lines.push("");
993
1225
  lines.push(`Run "aident ${display} <command> help" for input schema and examples.`);
994
1226
  return lines.join(`
995
1227
  `);
996
1228
  }
997
- function renderCommandHelp(catalog, domain, command) {
1229
+ function renderCommandHelp(catalog, domain, command, subcommand) {
998
1230
  const cmd = catalog.commands.find((c) => c.domain === domain && c.command === command);
999
1231
  if (!cmd)
1000
1232
  return `Unknown command: ${domain} ${command}`;
1001
1233
  const display = domain.startsWith("admin:") ? `admin ${domain.replace("admin:", "")}` : domain;
1234
+ const commandSuffix = subcommand ? ` ${subcommand}` : domain === command ? "" : ` ${command}`;
1002
1235
  const lines = [];
1003
- lines.push(`${colors.bold}AIDENT ${display.toUpperCase()} ${command.toUpperCase()}${colors.reset}`);
1236
+ lines.push(`${colors.bold}AIDENT ${display.toUpperCase()}${commandSuffix.toUpperCase()}${colors.reset}`);
1004
1237
  lines.push("");
1005
1238
  lines.push(cmd.description);
1006
1239
  if (cmd.longDescription) {
@@ -1013,10 +1246,16 @@ function renderCommandHelp(catalog, domain, command) {
1013
1246
  lines.push("");
1014
1247
  lines.push("INPUT:");
1015
1248
  for (const [key, val] of Object.entries(schema.properties)) {
1249
+ if (key === "action" && cmd.cliActionSubcommands?.length)
1250
+ continue;
1016
1251
  const tag = required.has(key) ? "" : " (optional)";
1017
1252
  const type = val.type ? ` ${colors.dim}<${val.type}>${colors.reset}` : "";
1018
1253
  lines.push(` --${key.padEnd(24)}${type} ${val.description ?? ""}${tag}`);
1019
1254
  }
1255
+ if (Object.values(schema.properties).some((val) => val.type === "string")) {
1256
+ lines.push("");
1257
+ lines.push(` ${colors.dim}String flags also accept --<flag>-file <path> to read the value from a local file.${colors.reset}`);
1258
+ }
1020
1259
  }
1021
1260
  if (cmd.outputDescription) {
1022
1261
  lines.push("");
@@ -1025,10 +1264,13 @@ function renderCommandHelp(catalog, domain, command) {
1025
1264
  if (cmd.examples && cmd.examples.length > 0) {
1026
1265
  lines.push("");
1027
1266
  lines.push("EXAMPLES:");
1028
- for (const ex of cmd.examples) {
1267
+ const examples = subcommand ? cmd.examples.filter((example) => example.args.action === subcommand) : cmd.examples;
1268
+ for (const ex of examples) {
1029
1269
  lines.push(` ${colors.dim}# ${ex.description}${colors.reset}`);
1030
- const argStr = Object.entries(ex.args).map(([k, v]) => `--${k} ${typeof v === "string" ? `"${v}"` : JSON.stringify(v)}`).join(" ");
1031
- lines.push(` aident ${display} ${command} ${argStr}`);
1270
+ const action = typeof ex.args.action === "string" && cmd.cliActionSubcommands?.includes(ex.args.action) ? ex.args.action : undefined;
1271
+ const argStr = Object.entries(ex.args).filter(([key]) => key !== "action" || !action).map(([k, v]) => `--${k} ${typeof v === "string" ? `"${v}"` : JSON.stringify(v)}`).join(" ");
1272
+ const exampleSuffix = action ? ` ${action}` : commandSuffix;
1273
+ lines.push(` aident ${display}${exampleSuffix} ${argStr}`);
1032
1274
  lines.push("");
1033
1275
  }
1034
1276
  }
@@ -1038,9 +1280,9 @@ function renderCommandHelp(catalog, domain, command) {
1038
1280
 
1039
1281
  // src/localIntegrationMigration.ts
1040
1282
  import { createHash as createHash2 } from "node:crypto";
1041
- import { readFile as readFile4 } from "node:fs/promises";
1283
+ import { readFile as readFile5 } from "node:fs/promises";
1042
1284
  import { homedir as homedir3 } from "node:os";
1043
- import { basename, join as join4 } from "node:path";
1285
+ import { basename, join as join5 } from "node:path";
1044
1286
  var LOCAL_INTEGRATION_MIGRATION_PROMPTED_AT_KEY = "localIntegrationMigrationPromptedAt";
1045
1287
  var LOCAL_INTEGRATION_MIGRATION_SKIPPED_AT_KEY = "localIntegrationMigrationSkippedAt";
1046
1288
  var LOCAL_INTEGRATION_MIGRATION_COMPLETED_AT_KEY = "localIntegrationMigrationCompletedAt";
@@ -1154,44 +1396,44 @@ function formatLocalIntegrationMigrationPlan(plan) {
1154
1396
  }
1155
1397
  function localConfigFiles(cwd, homeDir) {
1156
1398
  return [
1157
- { path: join4(cwd, ".mcp.json"), source: "mcp_config", kind: "json" },
1399
+ { path: join5(cwd, ".mcp.json"), source: "mcp_config", kind: "json" },
1158
1400
  {
1159
- path: join4(cwd, ".cursor", "mcp.json"),
1401
+ path: join5(cwd, ".cursor", "mcp.json"),
1160
1402
  source: "mcp_config",
1161
1403
  kind: "json"
1162
1404
  },
1163
1405
  {
1164
- path: join4(cwd, ".vscode", "mcp.json"),
1406
+ path: join5(cwd, ".vscode", "mcp.json"),
1165
1407
  source: "mcp_config",
1166
1408
  kind: "json"
1167
1409
  },
1168
1410
  {
1169
- path: join4(homeDir, ".claude.json"),
1411
+ path: join5(homeDir, ".claude.json"),
1170
1412
  source: "agent_config",
1171
1413
  kind: "json"
1172
1414
  },
1173
1415
  {
1174
- path: join4(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
1416
+ path: join5(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
1175
1417
  source: "agent_config",
1176
1418
  kind: "json"
1177
1419
  },
1178
1420
  {
1179
- path: join4(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json"),
1421
+ path: join5(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json"),
1180
1422
  source: "agent_config",
1181
1423
  kind: "json"
1182
1424
  },
1183
1425
  {
1184
- path: join4(homeDir, ".config", "Claude", "claude_desktop_config.json"),
1426
+ path: join5(homeDir, ".config", "Claude", "claude_desktop_config.json"),
1185
1427
  source: "agent_config",
1186
1428
  kind: "json"
1187
1429
  },
1188
1430
  {
1189
- path: join4(homeDir, ".cursor", "mcp.json"),
1431
+ path: join5(homeDir, ".cursor", "mcp.json"),
1190
1432
  source: "mcp_config",
1191
1433
  kind: "json"
1192
1434
  },
1193
1435
  {
1194
- path: join4(homeDir, ".aident", "config.json"),
1436
+ path: join5(homeDir, ".aident", "config.json"),
1195
1437
  source: "aident_cli_config",
1196
1438
  kind: "aident-config"
1197
1439
  }
@@ -1199,7 +1441,7 @@ function localConfigFiles(cwd, homeDir) {
1199
1441
  }
1200
1442
  async function readOptionalText(path) {
1201
1443
  try {
1202
- return await readFile4(path, "utf-8");
1444
+ return await readFile5(path, "utf-8");
1203
1445
  } catch {
1204
1446
  return null;
1205
1447
  }
@@ -1554,6 +1796,38 @@ function coerce(val) {
1554
1796
  }
1555
1797
  return val;
1556
1798
  }
1799
+ async function applyFileValueFlags(flags, schema, readFile6) {
1800
+ const props = schema?.properties ?? {};
1801
+ const out = { ...flags };
1802
+ for (const key of Object.keys(flags)) {
1803
+ if (!key.endsWith("-file") || key in props)
1804
+ continue;
1805
+ const base = key.slice(0, -"-file".length);
1806
+ const prop = base ? props[base] : undefined;
1807
+ const types = Array.isArray(prop?.type) ? prop.type : prop?.type ? [prop.type] : [];
1808
+ if (!types.includes("string"))
1809
+ continue;
1810
+ if (base in out) {
1811
+ return { error: { code: "invalid-input", message: `Pass either --${base} or --${key}, not both.` } };
1812
+ }
1813
+ const path = out[key];
1814
+ if (typeof path !== "string" || !path.trim()) {
1815
+ return { error: { code: "invalid-input", message: `--${key} requires a file path.` } };
1816
+ }
1817
+ try {
1818
+ out[base] = await readFile6(path);
1819
+ } catch (error) {
1820
+ return {
1821
+ error: {
1822
+ code: "invalid-input",
1823
+ message: `Unable to read --${key} ${path}: ${error instanceof Error ? error.message : String(error)}`
1824
+ }
1825
+ };
1826
+ }
1827
+ delete out[key];
1828
+ }
1829
+ return { flags: out };
1830
+ }
1557
1831
  function coerceArgsToSchema(args, schema) {
1558
1832
  const props = schema?.properties;
1559
1833
  if (!props)
@@ -1578,6 +1852,74 @@ function coerceArgsToSchema(args, schema) {
1578
1852
  }
1579
1853
  return out;
1580
1854
  }
1855
+ function shouldRejectRemainingArgs(domain, command) {
1856
+ return domain === "audit" && command === "audit" || domain === "billing" && command === "billing" || domain === "vault" && command === "vault";
1857
+ }
1858
+ function parseJsonObjectArg(value) {
1859
+ const trimmed = value.trim();
1860
+ if (!trimmed.startsWith("{"))
1861
+ return null;
1862
+ try {
1863
+ const parsed = JSON.parse(trimmed);
1864
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1865
+ return { error: { code: "invalid-input", message: "JSON command input must be an object." } };
1866
+ }
1867
+ return { args: parsed };
1868
+ } catch (error) {
1869
+ return {
1870
+ error: {
1871
+ code: "invalid-input",
1872
+ message: `Invalid JSON command input: ${error instanceof Error ? error.message : String(error)}`
1873
+ }
1874
+ };
1875
+ }
1876
+ }
1877
+ function buildCommandArgs(params) {
1878
+ const remaining = [...params.remaining];
1879
+ const positionalJson = remaining.length === 1 ? parseJsonObjectArg(remaining[0]) : null;
1880
+ if (positionalJson?.error)
1881
+ return positionalJson;
1882
+ const args = { ...positionalJson?.args ?? {}, ...params.flags };
1883
+ const props = params.schema?.properties ?? {};
1884
+ for (const reserved of RESERVED_CLI_FLAGS) {
1885
+ if (reserved === "version" && props.version !== undefined)
1886
+ continue;
1887
+ delete args[reserved];
1888
+ }
1889
+ if (positionalJson?.args)
1890
+ remaining.pop();
1891
+ if (params.subcommand) {
1892
+ if (args.action !== undefined) {
1893
+ return {
1894
+ error: {
1895
+ code: "invalid-input",
1896
+ message: `Do not combine ${params.domain} ${params.subcommand} with --action.`
1897
+ }
1898
+ };
1899
+ }
1900
+ args.action = params.subcommand;
1901
+ }
1902
+ const required = new Set(params.schema?.required ?? []);
1903
+ for (const key of Object.keys(props)) {
1904
+ if (remaining.length === 0)
1905
+ break;
1906
+ if (!required.has(key))
1907
+ continue;
1908
+ if (key in args)
1909
+ continue;
1910
+ args[key] = remaining.shift();
1911
+ }
1912
+ if (remaining.length > 0 && shouldRejectRemainingArgs(params.domain, params.command)) {
1913
+ const commandLabel = params.subcommand ? `${params.domain} ${params.subcommand}` : params.domain === params.command ? params.domain : `${params.domain} ${params.command}`;
1914
+ return {
1915
+ error: {
1916
+ code: "invalid-input",
1917
+ message: `Unexpected arguments for ${commandLabel}: ${remaining.join(" ")}`
1918
+ }
1919
+ };
1920
+ }
1921
+ return { args: coerceArgsToSchema(args, params.schema) };
1922
+ }
1581
1923
  function resolveCommand(positional, knownCommands) {
1582
1924
  if (positional.length === 0)
1583
1925
  return null;
@@ -1587,16 +1929,32 @@ function resolveCommand(positional, knownCommands) {
1587
1929
  domain = `admin:${positional[1]}`;
1588
1930
  consumed = 2;
1589
1931
  }
1590
- const remaining = positional.slice(consumed);
1591
- if (remaining.length === 0)
1592
- return { domain, command: "", remaining: [] };
1593
1932
  const domainCommands = knownCommands.filter((c) => c.domain === domain);
1933
+ const remaining = positional.slice(consumed);
1934
+ if (remaining.length === 0) {
1935
+ const selfCommand2 = domainCommands.find((candidate) => candidate.command === domain);
1936
+ return {
1937
+ domain,
1938
+ command: selfCommand2?.cliActionSubcommands?.length ? "" : selfCommand2?.command ?? "",
1939
+ remaining: []
1940
+ };
1941
+ }
1594
1942
  for (let len = Math.min(remaining.length, 5);len >= 1; len--) {
1595
1943
  const candidate = remaining.slice(0, len).join(" ");
1596
1944
  if (domainCommands.some((c) => c.command === candidate)) {
1597
1945
  return { domain, command: candidate, remaining: remaining.slice(len) };
1598
1946
  }
1599
1947
  }
1948
+ const selfCommand = domainCommands.find((candidate) => candidate.command === domain);
1949
+ const subcommand = remaining[0];
1950
+ if (selfCommand?.cliActionSubcommands?.includes(subcommand)) {
1951
+ return {
1952
+ domain,
1953
+ command: selfCommand.command,
1954
+ subcommand,
1955
+ remaining: remaining.slice(1)
1956
+ };
1957
+ }
1600
1958
  return { domain, command: remaining[0], remaining: remaining.slice(1) };
1601
1959
  }
1602
1960
 
@@ -1623,11 +1981,17 @@ async function callWithRefresh(client, call, deps = defaultDeps) {
1623
1981
  const creds = await deps.readCredentials();
1624
1982
  if (!creds)
1625
1983
  return first;
1626
- const refreshed = await deps.refreshToken(creds);
1984
+ const activeCredentials = credentialsForBaseUrl(creds, client.baseUrl);
1985
+ if (!activeCredentials)
1986
+ return first;
1987
+ const refreshed = await deps.refreshToken(activeCredentials);
1627
1988
  if (!refreshed)
1628
1989
  return first;
1629
- await deps.writeCredentials(refreshed);
1630
- client.updateCredentials(refreshed);
1990
+ const updatedCredentials = credentialsForBaseUrl(refreshed, client.baseUrl);
1991
+ if (!updatedCredentials)
1992
+ return first;
1993
+ await deps.writeCredentials(updatedCredentials);
1994
+ client.updateCredentials(updatedCredentials);
1631
1995
  const retried = await call(client);
1632
1996
  if (retried.status === 426) {
1633
1997
  deps.onUpgradeRequired(extractUpgradeMessage(retried.body));
@@ -1643,8 +2007,7 @@ function extractUpgradeMessage(body) {
1643
2007
  var LOADOUT_CAPABILITIES_SEARCH_OPERATION = "loadout_capabilities_search";
1644
2008
  var LOADOUT_CAPABILITIES_EXECUTE_OPERATION = "loadout_capabilities_execute";
1645
2009
  var LOADOUT_CAPABILITIES_FEEDBACK_OPERATION = "loadout_capabilities_feedback";
1646
- var LOADOUT_VAULT_STATUS_OPERATION = "loadout_vault_status";
1647
- var LOADOUT_VAULT_CONNECT_OPERATION = "loadout_vault_connect";
2010
+ var LOADOUT_VAULT_OPERATION = "loadout_vault";
1648
2011
  var LOADOUT_LOCAL_MIGRATION_EVENT_OPERATION = "loadout_vault_local_migration_event";
1649
2012
  var LOCAL_MIGRATION_SEARCH_BATCH_SIZE = 10;
1650
2013
  var LOCAL_MIGRATION_SEARCH_RESULT_MAX_DEPTH = 4;
@@ -1675,7 +2038,7 @@ async function main() {
1675
2038
  await runLogin(parsed);
1676
2039
  return;
1677
2040
  case "logout":
1678
- await runLogout();
2041
+ await runLogout(parsed);
1679
2042
  return;
1680
2043
  case "whoami":
1681
2044
  await runWhoami(parsed);
@@ -1739,14 +2102,24 @@ async function runLogin(parsed) {
1739
2102
  logInfo(`${colors.green}Signed in to ${creds.base_url}${colors.reset}`);
1740
2103
  await setConfigValue("baseUrl", creds.base_url);
1741
2104
  }
1742
- async function runLogout() {
2105
+ async function runLogout(parsed) {
2106
+ if (parsed.isHelp) {
2107
+ logInfo(renderLogoutHelp());
2108
+ return;
2109
+ }
2110
+ const envToken = process.env.AIDENT_TOKEN;
1743
2111
  const creds = await readCredentials();
1744
2112
  if (!creds) {
1745
- logInfo("Not signed in.");
2113
+ await clearCredentials();
2114
+ logInfo(envToken ? "AIDENT_TOKEN is set. Unset it in your shell to sign out." : "Not signed in.");
1746
2115
  return;
1747
2116
  }
1748
2117
  await logout(creds);
1749
2118
  await clearCredentials();
2119
+ if (envToken) {
2120
+ logInfo(`${colors.green}Stored credentials removed.${colors.reset} AIDENT_TOKEN is still set; unset it in your shell to sign out.`);
2121
+ return;
2122
+ }
1750
2123
  logInfo(`${colors.green}Signed out.${colors.reset}`);
1751
2124
  }
1752
2125
  async function runWhoami(parsed) {
@@ -1841,9 +2214,9 @@ async function runConfigCmd(parsed) {
1841
2214
  const value = normalizeBaseUrl(rawValue);
1842
2215
  await setConfigValue(key, value);
1843
2216
  logInfo(`${colors.green}Set${colors.reset} ${key} = ${value}`);
1844
- const creds = await readCredentials();
1845
- if (creds && creds.base_url !== value) {
1846
- logInfo(`${colors.yellow}Note:${colors.reset} existing credentials are for ${creds.base_url}. Run \`aident login\` to authenticate against ${value}.`);
2217
+ const clearedCredentialsBaseUrl = await clearCredentialsForIncompatibleBaseUrl(value);
2218
+ if (clearedCredentialsBaseUrl) {
2219
+ logInfo(`${colors.yellow}Cleared credentials for ${clearedCredentialsBaseUrl}.${colors.reset} Run \`aident login\` to authenticate against ${value}.`);
1847
2220
  }
1848
2221
  } else {
1849
2222
  const value = normalizeCliPackages(rawValue);
@@ -1861,6 +2234,13 @@ async function runConfigCmd(parsed) {
1861
2234
  }
1862
2235
  await unsetConfigValue(key);
1863
2236
  logInfo(`${colors.green}Unset${colors.reset} ${key}`);
2237
+ if (key === "baseUrl") {
2238
+ const baseUrl = await resolveDefaultBaseUrl();
2239
+ const clearedCredentialsBaseUrl = await clearCredentialsForIncompatibleBaseUrl(baseUrl);
2240
+ if (clearedCredentialsBaseUrl) {
2241
+ logInfo(`${colors.yellow}Cleared credentials for ${clearedCredentialsBaseUrl}.${colors.reset} Run \`aident login\` to authenticate against ${baseUrl}.`);
2242
+ }
2243
+ }
1864
2244
  return;
1865
2245
  }
1866
2246
  logErr(`Unknown config subcommand: ${sub}`);
@@ -1927,7 +2307,7 @@ async function runDoctorCmd(format) {
1927
2307
  logInfo(`${colors.bold}Aident CLI doctor${colors.reset}`);
1928
2308
  logInfo("");
1929
2309
  for (const c of report.checks) {
1930
- const mark = c.ok ? `${colors.green}ok${colors.reset}` : `${colors.red}x${colors.reset}`;
2310
+ const mark = !c.ok ? `${colors.red}x${colors.reset}` : c.warning ? `${colors.yellow}warning${colors.reset}` : `${colors.green}ok${colors.reset}`;
1931
2311
  logInfo(` ${mark} ${c.name.padEnd(22)} ${colors.dim}${c.detail}${colors.reset}`);
1932
2312
  }
1933
2313
  logInfo("");
@@ -1949,7 +2329,7 @@ async function runSetup(parsed) {
1949
2329
  }
1950
2330
  logInfo("");
1951
2331
  const existing = await readCredentials();
1952
- if (existing && existing.base_url === chosen) {
2332
+ if (existing && credentialsForBaseUrl(existing, chosen)) {
1953
2333
  logInfo(`${colors.green}Already signed in${colors.reset} to ${chosen}.`);
1954
2334
  } else {
1955
2335
  const proceed = (await readLine(`Open browser to authenticate now? [Y/n]: `)).trim().toLowerCase();
@@ -2113,7 +2493,8 @@ async function genFetchLoadoutIntegrations(client, queries) {
2113
2493
  async function genEnrichLoadoutIntegrationsWithVaultStatus(client, integrations) {
2114
2494
  if (integrations.length === 0)
2115
2495
  return integrations;
2116
- const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_VAULT_STATUS_OPERATION, {
2496
+ const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_VAULT_OPERATION, {
2497
+ action: "status",
2117
2498
  integrationIds: integrations.map((integration) => integration.id)
2118
2499
  }));
2119
2500
  if (!result.body.success)
@@ -2137,7 +2518,7 @@ async function genApplyLocalIntegrationMigration(client, plan, selectedIntegrati
2137
2518
  connectResults.push({ integrationId, skipped: "not-connectable" });
2138
2519
  continue;
2139
2520
  }
2140
- const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_VAULT_CONNECT_OPERATION, { integrationId }));
2521
+ const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_VAULT_OPERATION, { action: "connect", integrationId }));
2141
2522
  const entry = { integrationId, result: result.body };
2142
2523
  if (result.body.success) {
2143
2524
  entry.validation = await genValidateLocalIntegrationConnection(client, integrationId);
@@ -2162,7 +2543,7 @@ async function tryRecordLocalMigrationEvent(client, action, properties = {}) {
2162
2543
  }
2163
2544
  }
2164
2545
  async function genValidateLocalIntegrationConnection(client, integrationId) {
2165
- const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_VAULT_STATUS_OPERATION, { integrationId }));
2546
+ const result = await callWithRefresh(client, (c) => c.execOperation("loadout", LOADOUT_VAULT_OPERATION, { action: "status", integrationId }));
2166
2547
  if (!result.body.success) {
2167
2548
  return {
2168
2549
  success: false,
@@ -2315,7 +2696,7 @@ async function trySubmitLocalMigrationFeedback(client, integrationId, comment) {
2315
2696
  }
2316
2697
  function formatLocalMigrationValidationFeedback(entry) {
2317
2698
  const validation = entry.validation;
2318
- const validationMethod = validation?.providerCapabilityName ? "read_only_capability" : "vault_status";
2699
+ const validationMethod = validation?.providerCapabilityName ? "read_only_capability" : "vault";
2319
2700
  const parts = [
2320
2701
  "Local integration migration validation failed.",
2321
2702
  `integrationId=${entry.integrationId}`,
@@ -2585,29 +2966,28 @@ async function runCommand(parsed) {
2585
2966
  return;
2586
2967
  }
2587
2968
  if (parsed.isHelp) {
2588
- logInfo(renderCommandHelp(catalog, resolved.domain, resolved.command));
2969
+ logInfo(renderCommandHelp(catalog, resolved.domain, resolved.command, resolved.subcommand));
2589
2970
  return;
2590
2971
  }
2591
- let args = { ...parsed.flags };
2592
- for (const reserved of RESERVED_CLI_FLAGS) {
2593
- delete args[reserved];
2594
- }
2595
2972
  const schema = cmdInfo.inputSchema;
2596
- if (resolved.remaining.length > 0) {
2597
- const required = new Set(schema?.required ?? []);
2598
- const props = schema?.properties ?? {};
2599
- for (const key of Object.keys(props)) {
2600
- if (resolved.remaining.length === 0)
2601
- break;
2602
- if (!required.has(key))
2603
- continue;
2604
- if (key in args)
2605
- continue;
2606
- args[key] = resolved.remaining.shift();
2607
- }
2973
+ const fileFlags = await applyFileValueFlags(parsed.flags, schema, (path) => readFile6(path, "utf8"));
2974
+ if (fileFlags.error) {
2975
+ emitLocalCommandResult(parsed.format, false, undefined, fileFlags.error.code, fileFlags.error.message);
2976
+ return;
2608
2977
  }
2609
- args = coerceArgsToSchema(args, schema);
2610
- const result = await callWithRefresh(client, (c) => c.exec(resolved.domain, resolved.command, args));
2978
+ const builtArgs = buildCommandArgs({
2979
+ flags: fileFlags.flags,
2980
+ subcommand: resolved.subcommand,
2981
+ remaining: resolved.remaining,
2982
+ schema,
2983
+ domain: resolved.domain,
2984
+ command: resolved.command
2985
+ });
2986
+ if (builtArgs.error) {
2987
+ emitLocalCommandResult(parsed.format, false, undefined, builtArgs.error.code, builtArgs.error.message);
2988
+ return;
2989
+ }
2990
+ const result = await callWithRefresh(client, (c) => c.exec(resolved.domain, resolved.command, builtArgs.args ?? {}));
2611
2991
  emitResult(result, parsed.format, resolved);
2612
2992
  }
2613
2993
  function emitResult(result, format, resolved) {
@@ -2619,7 +2999,8 @@ function emitResult(result, format, resolved) {
2619
2999
  return;
2620
3000
  }
2621
3001
  if (body.success) {
2622
- logInfo(`${colors.green}ok${colors.reset} ${resolved.domain} ${resolved.command}`);
3002
+ const commandLabel = resolved.subcommand ? `${resolved.domain} ${resolved.subcommand}` : resolved.domain === resolved.command ? resolved.domain : `${resolved.domain} ${resolved.command}`;
3003
+ logInfo(`${colors.green}ok${colors.reset} ${commandLabel}`);
2623
3004
  if (body.data !== undefined)
2624
3005
  logInfo(JSON.stringify(body.data, null, 2));
2625
3006
  if (body.meta)
@@ -2650,9 +3031,9 @@ async function getAuthenticatedClient(packages) {
2650
3031
  const envToken = process.env.AIDENT_TOKEN;
2651
3032
  const envBaseUrl = process.env.AIDENT_BASE_URL?.trim();
2652
3033
  if (envToken) {
2653
- const baseUrl = envBaseUrl || (await readCredentials())?.base_url || await resolveDefaultBaseUrl();
3034
+ const baseUrl2 = envBaseUrl || (await readCredentials())?.base_url || await resolveDefaultBaseUrl();
2654
3035
  return new CliClient({
2655
- base_url: normalizeBaseUrl(baseUrl),
3036
+ base_url: normalizeBaseUrl(baseUrl2),
2656
3037
  client_id: "",
2657
3038
  access_token: envToken
2658
3039
  }, "env", activePackages, installedSkillVersion);
@@ -2660,9 +3041,13 @@ async function getAuthenticatedClient(packages) {
2660
3041
  let creds = await readCredentials();
2661
3042
  if (!creds)
2662
3043
  return null;
2663
- if (envBaseUrl && normalizeBaseUrl(envBaseUrl) !== creds.base_url) {
2664
- logErr(`${colors.yellow}Warning:${colors.reset} AIDENT_BASE_URL=${envBaseUrl} does not match the host your token was issued for (${creds.base_url}). Ignoring the env override; run \`aident login --base-url ${envBaseUrl}\` to authenticate against the new host.`);
3044
+ const baseUrl = normalizeBaseUrl(envBaseUrl || await resolveDefaultBaseUrl());
3045
+ const activeCredentials = credentialsForBaseUrl(creds, baseUrl);
3046
+ if (!activeCredentials) {
3047
+ logErr(`${colors.yellow}Warning:${colors.reset} ${baseUrl} does not match the host your token was issued for (${creds.base_url}). Run \`aident login --base-url ${baseUrl}\` to authenticate against the new host.`);
3048
+ return null;
2665
3049
  }
3050
+ creds = activeCredentials;
2666
3051
  if (isExpired(creds)) {
2667
3052
  const refreshed = await refreshToken(creds);
2668
3053
  if (refreshed) {
@@ -2685,7 +3070,7 @@ async function fetchCatalog(client, options = {}) {
2685
3070
  }
2686
3071
  return null;
2687
3072
  }
2688
- if (res.status !== 200 || !isCommandCatalog(res.body)) {
3073
+ if (res.status !== 200 || !isCommandCatalog2(res.body)) {
2689
3074
  logErr(`Failed to fetch command catalog (HTTP ${res.status}): ${JSON.stringify(res.body)}`);
2690
3075
  process.exitCode = 1;
2691
3076
  return null;
@@ -2710,7 +3095,7 @@ async function readConfiguredPackages() {
2710
3095
  const config = await readConfig();
2711
3096
  return normalizeCliPackages(config.packages);
2712
3097
  }
2713
- function isCommandCatalog(body) {
3098
+ function isCommandCatalog2(body) {
2714
3099
  if (!body || typeof body !== "object")
2715
3100
  return false;
2716
3101
  const b = body;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aident-ai/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3-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": {