loadout-ai 0.5.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +181 -121
  2. package/catalog/discovered.json +28066 -23196
  3. package/catalog/packages.json +2 -2
  4. package/dist/src/cli.js +2 -2
  5. package/dist/src/commands/agents.js +6 -23
  6. package/dist/src/commands/catalog.js +254 -27
  7. package/dist/src/commands/health.js +6 -6
  8. package/dist/src/commands/inventory.js +21 -121
  9. package/dist/src/commands/lifecycle.js +13 -51
  10. package/dist/src/commands/mcp.js +10 -73
  11. package/dist/src/commands/setup.js +5 -5
  12. package/dist/src/commands/sharing.js +11 -108
  13. package/dist/src/commands/support.js +42 -11
  14. package/dist/src/core/{adapters.js → agents/adapters.js} +2 -2
  15. package/dist/src/core/{agent-inspection.js → agents/agent-inspection.js} +26 -6
  16. package/dist/src/core/{codex-mcp.js → agents/codex-mcp.js} +1 -1
  17. package/dist/src/core/{health-score-evidence.js → agents/health-score-evidence.js} +8 -8
  18. package/dist/src/core/{runtime-tools.js → agents/runtime-tools.js} +2 -2
  19. package/dist/src/core/{audit.js → catalog/audit.js} +4 -4
  20. package/dist/src/core/{catalog.js → catalog/catalog.js} +18 -5
  21. package/dist/src/core/{components.js → catalog/components.js} +2 -2
  22. package/dist/src/core/{conformance.js → catalog/conformance.js} +2 -2
  23. package/dist/src/core/{profiles.js → catalog/profiles.js} +2 -2
  24. package/dist/src/core/{provenance.js → catalog/provenance.js} +3 -3
  25. package/dist/src/core/{registry.js → catalog/registry.js} +1 -1
  26. package/dist/src/core/{skill-compare.js → catalog/skill-compare.js} +1 -1
  27. package/dist/src/core/{skill-inventory.js → catalog/skill-inventory.js} +3 -3
  28. package/dist/src/core/{skills.js → catalog/skills.js} +1 -1
  29. package/dist/src/core/{candidate-intelligence.js → discovery/candidate-intelligence.js} +6 -6
  30. package/dist/src/core/{discovery-connector.js → discovery/discovery-connector.js} +2 -2
  31. package/dist/src/core/{evaluate.js → discovery/evaluate.js} +2 -2
  32. package/dist/src/core/{mcp-registry-discovery.js → discovery/mcp-registry-discovery.js} +1 -1
  33. package/dist/src/core/{observations.js → discovery/observations.js} +3 -3
  34. package/dist/src/core/{review-queue.js → discovery/review-queue.js} +9 -4
  35. package/dist/src/core/{skills-sh-discovery.js → discovery/skills-sh-discovery.js} +1 -1
  36. package/dist/src/core/{adopt.js → install/adopt.js} +3 -3
  37. package/dist/src/core/{catalog-install.js → install/catalog-install.js} +11 -11
  38. package/dist/src/core/{file-lock.js → install/file-lock.js} +1 -1
  39. package/dist/src/core/{install.js → install/install.js} +5 -5
  40. package/dist/src/core/{package.js → install/package.js} +3 -3
  41. package/dist/src/core/{reconcile.js → install/reconcile.js} +5 -5
  42. package/dist/src/core/{remove.js → install/remove.js} +4 -4
  43. package/dist/src/core/{snapshot.js → install/snapshot.js} +1 -1
  44. package/dist/src/core/{source.js → install/source.js} +1 -1
  45. package/dist/src/core/{sync.js → install/sync.js} +9 -9
  46. package/dist/src/core/{transaction.js → install/transaction.js} +1 -1
  47. package/dist/src/core/{uninstall.js → install/uninstall.js} +4 -4
  48. package/dist/src/core/{update.js → install/update.js} +4 -4
  49. package/dist/src/core/reporting/cli-guide.js +103 -0
  50. package/dist/src/core/reporting/doctor.js +141 -0
  51. package/dist/src/core/{freshness-alerts.js → reporting/freshness-alerts.js} +5 -5
  52. package/dist/src/core/{health.js → reporting/health.js} +6 -6
  53. package/dist/src/core/{loadout-card.js → reporting/loadout-card.js} +1 -1
  54. package/dist/src/core/{readme-claims.js → reporting/readme-claims.js} +1 -1
  55. package/dist/src/core/{readme-facts.js → reporting/readme-facts.js} +2 -2
  56. package/dist/src/core/{share-report.js → reporting/share-report.js} +3 -3
  57. package/dist/src/core/{upgrade.js → reporting/upgrade.js} +7 -7
  58. package/dist/src/core/routing/first-party-skills.js +118 -0
  59. package/dist/src/core/routing/handoff.js +278 -0
  60. package/dist/src/core/{model-config.js → routing/model-config.js} +4 -4
  61. package/dist/src/core/routing/route.js +539 -0
  62. package/dist/src/core/{api.js → runtime/api.js} +4 -4
  63. package/dist/src/core/{canary.js → runtime/canary.js} +1 -1
  64. package/dist/src/core/{github.js → runtime/github.js} +1 -1
  65. package/dist/src/core/{mcp-recipes.js → runtime/mcp-recipes.js} +1 -1
  66. package/dist/src/core/{mcp.js → runtime/mcp.js} +2 -2
  67. package/dist/src/core/{scheduler.js → runtime/scheduler.js} +4 -4
  68. package/dist/src/core/{active-policy.js → workspace/active-policy.js} +2 -2
  69. package/dist/src/core/{active-set.js → workspace/active-set.js} +2 -2
  70. package/dist/src/core/{improve.js → workspace/improve.js} +3 -3
  71. package/dist/src/core/{manifest.js → workspace/manifest.js} +2 -2
  72. package/dist/src/core/{outcomes.js → workspace/outcomes.js} +3 -3
  73. package/dist/src/core/{portable.js → workspace/portable.js} +3 -3
  74. package/dist/src/core/{profile-state.js → workspace/profile-state.js} +1 -1
  75. package/dist/src/core/{recommend.js → workspace/recommend.js} +1 -1
  76. package/dist/src/core/{state.js → workspace/state.js} +3 -3
  77. package/docs/CATALOG.md +2 -2
  78. package/docs/DISCOVERED.md +249 -247
  79. package/docs/UPSTREAM_LICENSE_DECISIONS.md +19 -12
  80. package/docs/evidence/readme-claims.json +22 -19
  81. package/package.json +2 -1
  82. package/skills/loadout-curator/SKILL.md +105 -0
  83. package/skills/loadout-router/SKILL.md +120 -0
  84. package/dist/src/core/cli-guide.js +0 -118
  85. package/dist/src/core/doctor.js +0 -95
  86. /package/dist/src/core/{agent-health-score.js → agents/agent-health-score.js} +0 -0
  87. /package/dist/src/core/{agent-versions.js → agents/agent-versions.js} +0 -0
  88. /package/dist/src/core/{conversion.js → agents/conversion.js} +0 -0
  89. /package/dist/src/core/{paths.js → agents/paths.js} +0 -0
  90. /package/dist/src/core/{runtime-tool-recipe.js → agents/runtime-tool-recipe.js} +0 -0
  91. /package/dist/src/core/{catalog-coverage.js → catalog/catalog-coverage.js} +0 -0
  92. /package/dist/src/core/{safety.js → catalog/safety.js} +0 -0
  93. /package/dist/src/core/{skill-security.js → catalog/skill-security.js} +0 -0
  94. /package/dist/src/core/{community.js → discovery/community.js} +0 -0
  95. /package/dist/src/core/{github-discovery.js → discovery/github-discovery.js} +0 -0
  96. /package/dist/src/core/{private-discovery.js → discovery/private-discovery.js} +0 -0
  97. /package/dist/src/core/{ranking.js → discovery/ranking.js} +0 -0
  98. /package/dist/src/core/{atomic-file.js → install/atomic-file.js} +0 -0
  99. /package/dist/src/core/{diff.js → install/diff.js} +0 -0
  100. /package/dist/src/core/{update-watch.js → install/update-watch.js} +0 -0
  101. /package/dist/src/core/{completion.js → reporting/completion.js} +0 -0
  102. /package/dist/src/core/{loadout-badge.js → reporting/loadout-badge.js} +0 -0
  103. /package/dist/src/core/{terminal.js → reporting/terminal.js} +0 -0
  104. /package/dist/src/core/{access.js → routing/access.js} +0 -0
  105. /package/dist/src/core/{credentials.js → routing/credentials.js} +0 -0
  106. /package/dist/src/core/{sandbox.js → runtime/sandbox.js} +0 -0
  107. /package/dist/src/core/{active-limit.js → workspace/active-limit.js} +0 -0
  108. /package/dist/src/core/{target-occupancy.js → workspace/target-occupancy.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { cp, lstat, mkdir, readFile, readdir, rm, writeFile, } from "node:fs/promises";
3
3
  import { join, relative, resolve, sep } from "node:path";
4
- import { loadoutHome } from "./paths.js";
4
+ import { loadoutHome } from "../agents/paths.js";
5
5
  import { analyzeUpdateSafety } from "./safety.js";
6
6
  const NAME = /^[a-z0-9][a-z0-9._-]*$/;
7
7
  const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/;
@@ -1,5 +1,5 @@
1
1
  import { basename } from "node:path";
2
- import { explainCatalogScore } from "./ranking.js";
2
+ import { explainCatalogScore } from "../discovery/ranking.js";
3
3
  const STOP_WORDS = new Set([
4
4
  "a",
5
5
  "an",
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, readFile, readdir } from "node:fs/promises";
3
3
  import { join, relative, resolve, sep } from "node:path";
4
- import { DEFAULT_ACTIVE_SKILL_LIMIT } from "./active-limit.js";
5
- import { readInstallState } from "./state.js";
6
- import { listInstalledRuntimeToolSkillTargets, } from "./runtime-tools.js";
4
+ import { DEFAULT_ACTIVE_SKILL_LIMIT } from "../workspace/active-limit.js";
5
+ import { readInstallState } from "../workspace/state.js";
6
+ import { listInstalledRuntimeToolSkillTargets, } from "../agents/runtime-tools.js";
7
7
  const MAX_SCAN_DEPTH = 6;
8
8
  function cleanFrontmatterValue(value) {
9
9
  if (!value)
@@ -1,6 +1,6 @@
1
1
  import { cp, lstat, readdir, readFile, rm } from "node:fs/promises";
2
2
  import { dirname, join, relative, resolve, sep } from "node:path";
3
- import { ensureDirectory } from "./paths.js";
3
+ import { ensureDirectory } from "../agents/paths.js";
4
4
  import { assertSkillSecurity, scanSkillSecurity } from "./skill-security.js";
5
5
  export async function discoverSkillDirectories(root, options = {}) {
6
6
  const result = [];
@@ -4,13 +4,13 @@ import { readFile, stat } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { isDeepStrictEqual } from "node:util";
7
- import { writeFileAtomically } from "./atomic-file.js";
8
- import { loadEffectiveCatalog, validateCatalog } from "./catalog.js";
7
+ import { writeFileAtomically } from "../install/atomic-file.js";
8
+ import { loadEffectiveCatalog, validateCatalog } from "../catalog/catalog.js";
9
9
  import { evaluatePackage } from "./evaluate.js";
10
- import { inspectPackage } from "./package.js";
11
- import { ensureDirectory, loadoutHome } from "./paths.js";
12
- import { fetchRepositorySnapshot, normalizeRepository, } from "./source.js";
13
- import { safeTerminalText } from "./terminal.js";
10
+ import { inspectPackage } from "../install/package.js";
11
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
12
+ import { fetchRepositorySnapshot, normalizeRepository, } from "../install/source.js";
13
+ import { safeTerminalText } from "../reporting/terminal.js";
14
14
  const dossierDirectory = () => join(loadoutHome(), "candidates");
15
15
  const sourceVerifiedDossiers = new WeakMap();
16
16
  function installabilityFor(components) {
@@ -1,7 +1,7 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
- import { writeFileAtomically } from "./atomic-file.js";
4
- import { ensureDirectory } from "./paths.js";
3
+ import { writeFileAtomically } from "../install/atomic-file.js";
4
+ import { ensureDirectory } from "../agents/paths.js";
5
5
  export class ConnectorRequestError extends Error {
6
6
  status;
7
7
  retryAfterSeconds;
@@ -1,5 +1,5 @@
1
- import { inspectPackage } from "./package.js";
2
- import { analyzeUpdateSafety } from "./safety.js";
1
+ import { inspectPackage } from "../install/package.js";
2
+ import { analyzeUpdateSafety } from "../catalog/safety.js";
3
3
  function statusFor(findings) {
4
4
  if (findings.some((finding) => finding.category === "secret" || finding.category === "instruction"))
5
5
  return "blocked";
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { ConnectorRequestError, fetchBoundedJson, rateLimitFromHeaders, readDiscoveryConnectorCache, writeDiscoveryConnectorCache, } from "./discovery-connector.js";
3
- import { loadoutHome } from "./paths.js";
3
+ import { loadoutHome } from "../agents/paths.js";
4
4
  const SOURCE = "official-mcp-registry";
5
5
  const API = "https://registry.modelcontextprotocol.io/v0.1/servers";
6
6
  const ATTRIBUTION = "https://registry.modelcontextprotocol.io/docs";
@@ -1,8 +1,8 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { fetchGitHubReleaseMetadata, } from "./github.js";
4
- import { writeFileAtomically } from "./atomic-file.js";
5
- import { loadoutHome } from "./paths.js";
3
+ import { fetchGitHubReleaseMetadata, } from "../runtime/github.js";
4
+ import { writeFileAtomically } from "../install/atomic-file.js";
5
+ import { loadoutHome } from "../agents/paths.js";
6
6
  const MAX_OBSERVATIONS_PER_REPOSITORY = 90;
7
7
  function observationPath(repository) {
8
8
  return join(loadoutHome(), "observations", `${repository.replace("/", "__")}.json`);
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
- import { writeFileAtomically } from "./atomic-file.js";
4
- import { ensureDirectory, loadoutHome } from "./paths.js";
3
+ import { writeFileAtomically } from "../install/atomic-file.js";
4
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
5
5
  const queuePath = () => join(loadoutHome(), "review-queue.json");
6
6
  function isQueue(value) {
7
7
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -13,7 +13,7 @@ function isQueue(value) {
13
13
  record.items.every((item) => item &&
14
14
  typeof item === "object" &&
15
15
  typeof item.repository === "string" &&
16
- ["pending", "shortlisted", "ignored"].includes(item.decision)));
16
+ ["pending", "shortlisted", "ignored", "promoted"].includes(item.decision)));
17
17
  }
18
18
  export async function readReviewQueue() {
19
19
  try {
@@ -102,7 +102,9 @@ function decisionPriority(decision) {
102
102
  return 0;
103
103
  if (decision === "pending")
104
104
  return 1;
105
- return 2;
105
+ if (decision === "ignored")
106
+ return 2;
107
+ return 3;
106
108
  }
107
109
  function compareQueueItems(left, right) {
108
110
  return (Number(left.alreadyCataloged) - Number(right.alreadyCataloged) ||
@@ -191,6 +193,9 @@ export async function setReviewDecision(repository, decision) {
191
193
  await writeFileAtomically(queuePath(), `${JSON.stringify(queue, null, 2)}\n`);
192
194
  return item;
193
195
  }
196
+ export async function markPromoted(repository) {
197
+ return setReviewDecision(repository, "promoted");
198
+ }
194
199
  export function formatReviewQueue(queue) {
195
200
  const visible = queue.items.filter((item) => !item.alreadyCataloged);
196
201
  return [
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { ConnectorRequestError, fetchBoundedJson, rateLimitFromHeaders, readDiscoveryConnectorCache, writeDiscoveryConnectorCache, } from "./discovery-connector.js";
3
- import { loadoutHome } from "./paths.js";
3
+ import { loadoutHome } from "../agents/paths.js";
4
4
  const SOURCE = "skills-sh";
5
5
  const API = "https://skills.sh/api/v1/skills";
6
6
  const ATTRIBUTION = "https://skills.sh/docs/api";
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, readFile, readdir } from "node:fs/promises";
3
3
  import { basename, isAbsolute, join, posix, relative, sep } from "node:path";
4
- import { enrichInventoryWithProvenance } from "./provenance.js";
5
- import { scanInstalledSkills } from "./skill-inventory.js";
6
- import { installStatePath, recordInstall } from "./state.js";
4
+ import { enrichInventoryWithProvenance } from "../catalog/provenance.js";
5
+ import { scanInstalledSkills } from "../catalog/skill-inventory.js";
6
+ import { installStatePath, recordInstall } from "../workspace/state.js";
7
7
  import { runMutationTransaction } from "./transaction.js";
8
8
  const issuedAdoptionPlans = new WeakMap();
9
9
  function safeRelativePath(root, path) {
@@ -1,13 +1,13 @@
1
- import { DEFAULT_ACTIVE_SKILL_LIMIT } from "./active-limit.js";
2
- import { loadEffectiveCatalog } from "./catalog.js";
3
- import { detectAgents } from "./paths.js";
4
- import { isStableSkillSelected, isPowerSkillSelected, resolveCatalogProfile, } from "./profiles.js";
1
+ import { DEFAULT_ACTIVE_SKILL_LIMIT } from "../workspace/active-limit.js";
2
+ import { loadEffectiveCatalog, } from "../catalog/catalog.js";
3
+ import { detectAgents } from "../agents/paths.js";
4
+ import { isStableSkillSelected, isPowerSkillSelected, resolveCatalogProfile, } from "../catalog/profiles.js";
5
5
  import { applySkillLibraryBatch, applySkillInstallBatch, buildSkillPlan, installedAgents, planManagedProfileReconciliation, } from "./install.js";
6
6
  import { fetchRepositorySnapshot, } from "./source.js";
7
- import { analyzeInstallPlanSafety, } from "./safety.js";
8
- import { formatModelApiAccess } from "./access.js";
9
- import { recordInstalledProfile } from "./profile-state.js";
10
- import { readInstallState } from "./state.js";
7
+ import { analyzeInstallPlanSafety, } from "../catalog/safety.js";
8
+ import { formatModelApiAccess, } from "../routing/access.js";
9
+ import { recordInstalledProfile } from "../workspace/profile-state.js";
10
+ import { readInstallState } from "../workspace/state.js";
11
11
  async function parallelMap(values, concurrency, worker) {
12
12
  const results = new Array(values.length);
13
13
  let cursor = 0;
@@ -240,11 +240,11 @@ export function formatPreparedCatalogInstall(prepared, options = {}) {
240
240
  const quarantined = prepared.skipped.filter((item) => item.kind === "quarantined");
241
241
  const retired = prepared.reconciliation?.obsoleteUnits ?? [];
242
242
  const modeLabel = prepared.selection.mode === "maximum"
243
- ? "Maximum Library"
243
+ ? "Maximum"
244
244
  : prepared.selection.mode === "power"
245
- ? "Power Boost"
245
+ ? "Power"
246
246
  : prepared.selection.mode === "stable"
247
- ? "Stable Boost"
247
+ ? "Stable"
248
248
  : "Custom";
249
249
  const lines = [
250
250
  `Loadout: ${modeLabel}`,
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mkdir, open, readFile, rename, rm, stat, utimes, } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
- import { ensureDirectory } from "./paths.js";
4
+ import { ensureDirectory } from "../agents/paths.js";
5
5
  const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
6
6
  function parseOwner(content) {
7
7
  try {
@@ -1,12 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { cp, lstat, rm } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, posix, relative, win32, } from "node:path";
4
- import { ensureDirectory, loadoutHome } from "./paths.js";
5
- import { planAdapterSkillInstall } from "./adapters.js";
6
- import { applySkillPlan, detectInstallConflicts } from "./skills.js";
7
- import { activationLibraryPath, installStatePath, recordInstall, recordInstallBatch, recordLibraryInstallBatch, readInstallState, hashDirectory, writeInstallState, } from "./state.js";
4
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
5
+ import { planAdapterSkillInstall } from "../agents/adapters.js";
6
+ import { applySkillPlan, detectInstallConflicts } from "../catalog/skills.js";
7
+ import { activationLibraryPath, installStatePath, recordInstall, recordInstallBatch, recordLibraryInstallBatch, readInstallState, hashDirectory, writeInstallState, } from "../workspace/state.js";
8
8
  import { runMutationTransaction } from "./transaction.js";
9
- import { inspectTargetOccupancy } from "./target-occupancy.js";
9
+ import { inspectTargetOccupancy } from "../workspace/target-occupancy.js";
10
10
  export function installedAgents(agents, requested) {
11
11
  const available = agents.filter((agent) => agent.installed);
12
12
  if (!requested || requested.length === 0)
@@ -1,8 +1,8 @@
1
1
  import { readFile, readdir } from "node:fs/promises";
2
2
  import { dirname, join, relative, resolve } from "node:path";
3
- import { discoverSkillDirectories } from "./skills.js";
4
- import { discoverMcpManifests } from "./mcp.js";
5
- import { discoverResources } from "./components.js";
3
+ import { discoverSkillDirectories } from "../catalog/skills.js";
4
+ import { discoverMcpManifests } from "../runtime/mcp.js";
5
+ import { discoverResources } from "../catalog/components.js";
6
6
  const portableRelative = (root, path) => relative(root, path).split(/[\\/]/).join("/") || ".";
7
7
  function frontmatter(path) {
8
8
  return readFile(join(path, "SKILL.md"), "utf8").then((content) => {
@@ -1,13 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { cp, lstat, readFile, readdir, rm } from "node:fs/promises";
3
3
  import { join, relative, sep } from "node:path";
4
- import { analyzeUpdateSafety } from "./safety.js";
5
- import { enrichInventoryWithProvenance, } from "./provenance.js";
6
- import { scanInstalledSkills } from "./skill-inventory.js";
7
- import { installStatePath, recordInstallBatch } from "./state.js";
4
+ import { analyzeUpdateSafety } from "../catalog/safety.js";
5
+ import { enrichInventoryWithProvenance, } from "../catalog/provenance.js";
6
+ import { scanInstalledSkills } from "../catalog/skill-inventory.js";
7
+ import { installStatePath, recordInstallBatch } from "../workspace/state.js";
8
8
  import { runMutationTransaction } from "./transaction.js";
9
9
  import { repositoryCachePath } from "./source.js";
10
- import { validateSkillDirectory } from "./skills.js";
10
+ import { validateSkillDirectory } from "../catalog/skills.js";
11
11
  const issuedPlans = new WeakMap();
12
12
  const IGNORED_TREE_NAMES = new Set([".git", ".cache", "node_modules"]);
13
13
  function slug(value) {
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile, rm } from "node:fs/promises";
3
- import { forgetInstall, installStatePath, readInstallState } from "./state.js";
4
- import { writeMcpConfigPlan } from "./mcp.js";
3
+ import { forgetInstall, installStatePath, readInstallState, } from "../workspace/state.js";
4
+ import { writeMcpConfigPlan } from "../runtime/mcp.js";
5
5
  import { runMutationTransaction } from "./transaction.js";
6
- import { managedFileReadPath } from "./active-set.js";
7
- import { codexMcpServerFingerprint, removeCodexMcpServerBlock, writeCodexMcpConfigContent, } from "./codex-mcp.js";
6
+ import { managedFileReadPath } from "../workspace/active-set.js";
7
+ import { codexMcpServerFingerprint, removeCodexMcpServerBlock, writeCodexMcpConfigContent, } from "../agents/codex-mcp.js";
8
8
  export async function planRemove(packageId) {
9
9
  const state = await readInstallState();
10
10
  const record = state.installs.find((entry) => entry.packageId === packageId);
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { readFile, writeFile, mkdir, readdir, rename, rm, lstat, } from "node:fs/promises";
3
3
  import { dirname, join, resolve, sep } from "node:path";
4
- import { loadoutHome, ensureDirectory, userHome } from "./paths.js";
4
+ import { loadoutHome, ensureDirectory, userHome } from "../agents/paths.js";
5
5
  export async function createSnapshot(paths, options = {}) {
6
6
  const normalizedRoots = [...new Set(paths.map((path) => resolve(path)))];
7
7
  const snapshot = {
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { execFile } from "node:child_process";
6
6
  import { promisify } from "node:util";
7
- import { loadoutHome, ensureDirectory } from "./paths.js";
7
+ import { loadoutHome, ensureDirectory } from "../agents/paths.js";
8
8
  const execFileAsync = promisify(execFile);
9
9
  async function enforceRepositoryBounds(root, options) {
10
10
  if (options.maxBytes === undefined && options.maxFiles === undefined)
@@ -1,17 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { dirname, resolve, sep } from "node:path";
4
- import { loadEffectiveCatalog } from "./catalog.js";
4
+ import { loadEffectiveCatalog } from "../catalog/catalog.js";
5
5
  import { installedAgents } from "./install.js";
6
- import { orderManifestPackages, readManifest, writeLockfile, } from "./manifest.js";
7
- import { detectAgents, userHome } from "./paths.js";
6
+ import { orderManifestPackages, readManifest, writeLockfile, } from "../workspace/manifest.js";
7
+ import { detectAgents, userHome } from "../agents/paths.js";
8
8
  import { fetchGitSnapshot, fetchRepositorySnapshot } from "./source.js";
9
- import { addRootFileExports, buildUniversalPackagePlan } from "./components.js";
10
- import { analyzeInstallPlanSafety, } from "./safety.js";
11
- import { fetchRemoteRegistryPackage, packPackage, resolveRegistryPackage, } from "./registry.js";
12
- import { discoverMcpManifests, planMcpConfigBatch, writeMcpConfigPlan, } from "./mcp.js";
13
- import { applySkillPlan, detectInstallConflicts } from "./skills.js";
14
- import { installStatePath, readInstallState, recordInstallTransaction, } from "./state.js";
9
+ import { addRootFileExports, buildUniversalPackagePlan, } from "../catalog/components.js";
10
+ import { analyzeInstallPlanSafety, } from "../catalog/safety.js";
11
+ import { fetchRemoteRegistryPackage, packPackage, resolveRegistryPackage, } from "../catalog/registry.js";
12
+ import { discoverMcpManifests, planMcpConfigBatch, writeMcpConfigPlan, } from "../runtime/mcp.js";
13
+ import { applySkillPlan, detectInstallConflicts } from "../catalog/skills.js";
14
+ import { installStatePath, readInstallState, recordInstallTransaction, } from "../workspace/state.js";
15
15
  import { runMutationTransaction } from "./transaction.js";
16
16
  function registrySourceKey(source) {
17
17
  if (source.type === "registry")
@@ -1,7 +1,7 @@
1
1
  import { lstat, mkdir, readdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { join } from "node:path";
4
- import { ensureDirectory, loadoutHome } from "./paths.js";
4
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
5
5
  import { createSnapshot, readSnapshot, recordSnapshotPostMutationState, restoreSnapshot, } from "./snapshot.js";
6
6
  import { acquireFileLock, withFileLock, } from "./file-lock.js";
7
7
  export const transactionRoot = () => join(loadoutHome(), "staging");
@@ -2,11 +2,11 @@ import { execFile } from "node:child_process";
2
2
  import { readdir, rm, rmdir } from "node:fs/promises";
3
3
  import { dirname, parse, resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
- import { loadoutHome } from "./paths.js";
5
+ import { loadoutHome } from "../agents/paths.js";
6
6
  import { applyRemove, planRemove } from "./remove.js";
7
- import { applyRuntimeToolPlan, listInstalledRuntimeTools, planRuntimeTool, } from "./runtime-tools.js";
8
- import { applyNativeSchedulerBundle, planNativeScheduler, } from "./scheduler.js";
9
- import { readInstallState } from "./state.js";
7
+ import { applyRuntimeToolPlan, listInstalledRuntimeTools, planRuntimeTool, } from "../agents/runtime-tools.js";
8
+ import { applyNativeSchedulerBundle, planNativeScheduler, } from "../runtime/scheduler.js";
9
+ import { readInstallState } from "../workspace/state.js";
10
10
  export async function buildUninstallPlan(dependencies = {}) {
11
11
  const stateHome = loadoutHome();
12
12
  const state = await readInstallState();
@@ -3,11 +3,11 @@ import { basename, join } from "node:path";
3
3
  import { fetchRepositorySnapshot, resolveRepositoryHead } from "./source.js";
4
4
  import { repositoryCachePath } from "./source.js";
5
5
  import { diffRepositorySnapshots } from "./diff.js";
6
- import { hashDirectory, readInstallState } from "./state.js";
7
- import { analyzeUpdateSafety } from "./safety.js";
8
- import { detectAgents, loadoutHome } from "./paths.js";
6
+ import { hashDirectory, readInstallState } from "../workspace/state.js";
7
+ import { analyzeUpdateSafety } from "../catalog/safety.js";
8
+ import { detectAgents, loadoutHome } from "../agents/paths.js";
9
9
  import { applySkillInstall, buildSkillPlan, installedAgents, } from "./install.js";
10
- import { discoverSkillDirectories, validateSkillDirectory, } from "./skills.js";
10
+ import { discoverSkillDirectories, validateSkillDirectory, } from "../catalog/skills.js";
11
11
  function managedUnitIds(state, packageId) {
12
12
  return [
13
13
  ...new Set((state.activations ?? [])
@@ -0,0 +1,103 @@
1
+ /** Plain-language entry points for people using the CLI, not maintaining it. */
2
+ export const BEGINNER_GUIDE = `
3
+ Loadout — manage the skills, tools, and MCP servers your AI agents use.
4
+
5
+ FIRST TIME
6
+
7
+ loadout doctor which agents you have, and whether they are healthy
8
+ loadout setup --mode stable preview 30 reviewed skills (nothing changes)
9
+ loadout setup --mode stable --yes install them
10
+
11
+ IN A PROJECT
12
+
13
+ loadout recommend --project . what this codebase actually needs
14
+ loadout optimize --project . propose an active set for it
15
+ loadout status health grade and what is managed
16
+
17
+ WHILE YOU WORK
18
+
19
+ loadout route <describe a task> which model tier fits, and what it costs
20
+ loadout route --conserve <task> cheaper tiers when you are low on quota
21
+ loadout handoff send codex "..." give a task to your other agent
22
+
23
+ USE IT INSIDE YOUR AGENT
24
+
25
+ loadout skills install loadout-router --yes
26
+
27
+ Then just ask your agent "which model should I use for this?" instead of
28
+ coming back here.
29
+
30
+ KEEPING CURRENT
31
+
32
+ loadout health safer updates and local drift
33
+ loadout alerts archived, stale, or changed sources
34
+
35
+ Every command that writes previews first and snapshots before it changes
36
+ anything. Undo with: loadout rollback
37
+
38
+ More commands: loadout advanced
39
+ `.trim();
40
+ export const ADVANCED_GUIDE = [
41
+ "ADVANCED COMMANDS",
42
+ "",
43
+ "Hidden from the first screen so daily use stays focused.",
44
+ "",
45
+ "Discovery: candidate, discover, review-queue, evaluate, skill-audit",
46
+ "Sharing: init, lock, export, import, share, card",
47
+ "Integrations: mcp-recipe, mcp-config, credentials, models, handoff, skills, convert",
48
+ "Lifecycle: plan, adopt, uninstall, profiles, autopilot, tool, watch",
49
+ "",
50
+ "`loadout <command> --help` for options. Mutations preview first or require --yes.",
51
+ ].join("\n");
52
+ /**
53
+ * The first-screen help shows only the everyday core (~24 commands). Everything
54
+ * else stays fully available and runnable, just omitted from the default help so
55
+ * the surface reads as a focused product rather than a tool dump. `loadout
56
+ * advanced` lists the hidden surface; `loadout <command> --help` documents each.
57
+ */
58
+ export const HIDDEN_FROM_FIRST_SCREEN = new Set([
59
+ // manifests, authoring, sharing
60
+ "init",
61
+ "lock",
62
+ "export",
63
+ "import",
64
+ "audit",
65
+ "create",
66
+ "add",
67
+ "unadd",
68
+ "share",
69
+ "card",
70
+ "search",
71
+ // analytics / evidence / discovery
72
+ "report",
73
+ "versions",
74
+ "skill-audit",
75
+ "candidate",
76
+ "discover",
77
+ "review-queue",
78
+ "inspect",
79
+ "propose",
80
+ "evaluate",
81
+ // integrations / credentials / runtime
82
+ "credentials",
83
+ "models",
84
+ "handoff",
85
+ "completion",
86
+ "mcp-recipe",
87
+ "mcp-config",
88
+ "convert",
89
+ // lifecycle extras
90
+ "plan",
91
+ "adopt",
92
+ "uninstall",
93
+ "profiles",
94
+ "autopilot",
95
+ "tool",
96
+ "watch",
97
+ "schedule",
98
+ "unschedule",
99
+ "alert-ignore",
100
+ "alert-pin",
101
+ "alert-unpin",
102
+ "alert-pins",
103
+ ]);
@@ -0,0 +1,141 @@
1
+ import { access, constants } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { detectAgents, directoryExists, loadoutHome, userHome, } from "../agents/paths.js";
4
+ import { inspectAgents } from "../agents/agent-inspection.js";
5
+ async function writable(path) {
6
+ // A not-yet-created directory is healthy when any existing ancestor is
7
+ // writable; this handles first-run paths such as ~/.agents/skills.
8
+ let candidate = path;
9
+ while (true) {
10
+ try {
11
+ await access(candidate, constants.W_OK);
12
+ return true;
13
+ }
14
+ catch {
15
+ const parent = dirname(candidate);
16
+ if (parent === candidate)
17
+ return false;
18
+ candidate = parent;
19
+ }
20
+ }
21
+ }
22
+ export async function runDoctor() {
23
+ const home = userHome();
24
+ const stateHome = loadoutHome();
25
+ const loadoutHomeExists = await directoryExists(stateHome);
26
+ const loadoutHomeWritable = await writable(stateHome);
27
+ const agents = await detectAgents();
28
+ const inventories = await inspectAgents(agents);
29
+ const diagnosedAgents = await Promise.all(agents.map(async (agent, index) => {
30
+ const exists = await directoryExists(agent.skillsDirectory);
31
+ const canWrite = await writable(agent.skillsDirectory);
32
+ const issues = [];
33
+ if (!agent.installed)
34
+ issues.push(`Install or configure ${agent.displayName} to enable it.`);
35
+ if (!exists)
36
+ issues.push("Skills directory does not exist yet; Loadout will create it during install.");
37
+ if (!canWrite)
38
+ issues.push("Skills directory is not writable; check permissions or choose another home.");
39
+ return {
40
+ agent,
41
+ inventory: inventories[index],
42
+ skillsRootExists: exists,
43
+ writable: canWrite,
44
+ issues,
45
+ };
46
+ }));
47
+ const issues = [];
48
+ if (!loadoutHomeExists)
49
+ issues.push("Loadout state directory does not exist yet; it will be created on first install.");
50
+ if (!loadoutHomeWritable)
51
+ issues.push("Loadout state directory is not writable; check LOADOUT_HOME permissions.");
52
+ if (!diagnosedAgents.some(({ agent }) => agent.installed)) {
53
+ issues.push("No supported agent executable was detected on PATH.");
54
+ }
55
+ return {
56
+ platform: process.platform,
57
+ userHome: home,
58
+ loadoutHome: stateHome,
59
+ loadoutHomeExists,
60
+ loadoutHomeWritable,
61
+ agents: diagnosedAgents,
62
+ issues,
63
+ };
64
+ }
65
+ export function formatDoctorReport(report, options = {}) {
66
+ const installed = report.agents.filter((a) => a.agent.installed);
67
+ const notInstalled = report.agents.filter((a) => !a.agent.installed);
68
+ const allIssues = [...report.issues, ...installed.flatMap((a) => a.issues)];
69
+ const grade = allIssues.length === 0 && installed.length > 0
70
+ ? "HEALTHY"
71
+ : allIssues.length <= 2
72
+ ? "OK"
73
+ : "NEEDS ATTENTION";
74
+ const lines = [
75
+ `loadout doctor — ${grade}`,
76
+ "",
77
+ `Platform: ${report.platform}`,
78
+ `State: ${report.loadoutHome} ${report.loadoutHomeExists ? "✓" : "⚠ not created"} ${report.loadoutHomeWritable ? "writable" : "⚠ not writable"}`,
79
+ `Agents: ${installed.length} detected, ${notInstalled.length} available`,
80
+ "",
81
+ ];
82
+ // --- Detected agents: compact summary ---
83
+ if (installed.length) {
84
+ lines.push("DETECTED AGENTS");
85
+ for (const entry of installed) {
86
+ const skillCount = entry.inventory.components
87
+ .filter((c) => c.scanned && c.directoryExists)
88
+ .reduce((sum, c) => sum + c.entries.length, 0);
89
+ const supported = entry.inventory.components
90
+ .filter((c) => c.compatibility === "native" || c.compatibility === "adapted")
91
+ .map((c) => c.type);
92
+ lines.push(` ✓ ${entry.agent.displayName}`, ` ${entry.agent.skillsDirectory}`, ` ${skillCount} items | supports: ${supported.join(", ")}`);
93
+ if (entry.issues.length) {
94
+ for (const issue of entry.issues)
95
+ lines.push(` ⚠ ${issue}`);
96
+ }
97
+ if (options.verbose) {
98
+ for (const component of entry.inventory.components) {
99
+ if (component.compatibility === "unsupported")
100
+ continue;
101
+ const detail = component.scanned
102
+ ? component.directoryExists
103
+ ? `${component.entries.length} items`
104
+ : "not created"
105
+ : (component.note ?? "—");
106
+ lines.push(` ${component.type}: ${detail}`);
107
+ }
108
+ }
109
+ }
110
+ lines.push("");
111
+ }
112
+ // --- Not installed: one-liner ---
113
+ if (notInstalled.length) {
114
+ lines.push(`NOT DETECTED: ${notInstalled.map((a) => a.agent.displayName).join(", ")}`, "");
115
+ }
116
+ // --- Issues ---
117
+ if (allIssues.length) {
118
+ lines.push("ISSUES");
119
+ for (const issue of allIssues)
120
+ lines.push(` ⚠ ${issue}`);
121
+ lines.push("");
122
+ }
123
+ // --- Next steps ---
124
+ lines.push("NEXT STEPS");
125
+ if (allIssues.length) {
126
+ lines.push(" Fix the issues above, then run `loadout doctor` again.");
127
+ }
128
+ else if (installed.length === 0) {
129
+ lines.push(" Install an AI coding agent (Claude Code, Codex, Cursor, etc.),", " then run `loadout doctor` again.");
130
+ }
131
+ else {
132
+ lines.push(" loadout status see what's managed", " loadout setup install a curated skill set", " loadout route find the right model for your task", " loadout health check for updates");
133
+ }
134
+ return lines.join("\n");
135
+ }
136
+ /**
137
+ * Legacy-compatible verbose format. Used when --verbose is passed.
138
+ */
139
+ export function formatDoctorReportVerbose(report) {
140
+ return formatDoctorReport(report, { verbose: true });
141
+ }