loadout-ai 0.5.8 → 0.6.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.
- package/CHANGELOG.md +17 -0
- package/MASTER_PLAN.md +9 -9
- package/README.md +84 -121
- package/catalog/discovered.json +59800 -10065
- package/catalog/packages.json +4 -3
- package/dist/src/cli.js +26 -2979
- package/dist/src/commands/agents.js +168 -0
- package/dist/src/commands/catalog.js +708 -0
- package/dist/src/commands/health.js +98 -0
- package/dist/src/commands/inventory.js +417 -0
- package/dist/src/commands/lifecycle.js +411 -0
- package/dist/src/commands/mcp.js +424 -0
- package/dist/src/commands/setup.js +67 -0
- package/dist/src/commands/sharing.js +358 -0
- package/dist/src/commands/support.js +246 -0
- package/dist/src/core/active-limit.js +2 -0
- package/dist/src/core/active-policy.js +2 -1
- package/dist/src/core/catalog-install.js +47 -10
- package/dist/src/core/catalog.js +15 -96
- package/dist/src/core/cli-guide.js +43 -37
- package/dist/src/core/completion.js +2 -20
- package/dist/src/core/doctor.js +73 -24
- package/dist/src/core/github.js +3 -2
- package/dist/src/core/handoff.js +161 -0
- package/dist/src/core/health-score-evidence.js +2 -2
- package/dist/src/core/health.js +83 -0
- package/dist/src/core/install.js +8 -3
- package/dist/src/core/loadout-card.js +14 -2
- package/dist/src/core/review-queue.js +7 -2
- package/dist/src/core/route.js +248 -0
- package/dist/src/core/skill-inventory.js +3 -3
- package/dist/src/core/skills.js +6 -7
- package/dist/src/core/source.js +10 -3
- package/dist/src/core/upgrade.js +10 -9
- package/docs/ACTIVE_SET.md +1 -1
- package/docs/ACTIVE_SET_POLICY.md +1 -1
- package/docs/CATALOG.md +2 -2
- package/docs/DEMO_SCRIPT.md +5 -5
- package/docs/DISCOVERED.md +250 -253
- package/docs/RELEASE_REVIEW.md +28 -143
- package/docs/SUBMISSION_COPY.md +4 -4
- package/docs/UPSTREAM_LICENSE_DECISIONS.md +19 -12
- package/docs/USER_TEST_GUIDE.md +1 -1
- package/docs/evidence/readme-claims.json +2 -14
- package/package.json +4 -3
- package/dist/src/core/benchmark-campaign.js +0 -496
- package/dist/src/core/benchmark-evidence.js +0 -458
- package/dist/src/core/benchmark-fixtures.js +0 -1157
- package/dist/src/core/benchmark-runner.js +0 -301
- package/dist/src/core/benchmark-trust.js +0 -795
- package/dist/src/core/catalog-release.js +0 -287
- package/dist/src/core/compatibility-intelligence.js +0 -465
- package/dist/src/core/ecosystem-import.js +0 -1072
- package/dist/src/core/head-to-head.js +0 -437
- package/dist/src/core/intelligence-feed-build.js +0 -59
- package/dist/src/core/intelligence-feed.js +0 -353
- package/dist/src/core/registry-api.js +0 -97
- package/dist/src/core/release-claims.js +0 -175
- package/dist/src/core/signing.js +0 -115
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { parseAgentSelection } from "../core/paths.js";
|
|
2
|
+
import { buildUpdatePlan } from "../core/update.js";
|
|
3
|
+
import { buildHealthReport, formatHealthReport } from "../core/health.js";
|
|
4
|
+
import { buildFreshnessAlerts, formatFreshnessAlerts, ignoreFreshnessAlert, pinReplacement, readReplacementPins, unpinReplacement, } from "../core/freshness-alerts.js";
|
|
5
|
+
import { formatAgentHealthScore } from "../core/agent-health-score.js";
|
|
6
|
+
import { buildLocalAgentHealthScores } from "../core/health-score-evidence.js";
|
|
7
|
+
export function registerHealth(program) {
|
|
8
|
+
program
|
|
9
|
+
.command("health")
|
|
10
|
+
.description("Quickly check agents, installed packages, and local file drift")
|
|
11
|
+
.option("--json", "emit machine-readable JSON")
|
|
12
|
+
.option("--updates", "also perform live network update checks")
|
|
13
|
+
.option("--explain", "add deterministic score dimensions, evidence, uncertainty, and remediation")
|
|
14
|
+
.option("--agents <ids>", "limit explained scores to selected agent ids")
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
if (options.updates && !options.json)
|
|
17
|
+
console.error("Checking repository commits (4 at a time; changed sources may take up to 120s for safety review)…");
|
|
18
|
+
const report = await buildHealthReport({
|
|
19
|
+
updates: options.updates
|
|
20
|
+
? () => buildUpdatePlan(undefined, {
|
|
21
|
+
onProgress: options.json
|
|
22
|
+
? undefined
|
|
23
|
+
: ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
|
|
24
|
+
})
|
|
25
|
+
: undefined,
|
|
26
|
+
});
|
|
27
|
+
if (options.agents && !options.explain)
|
|
28
|
+
throw new Error("--agents requires --explain");
|
|
29
|
+
const selectedAgents = parseAgentSelection(options.agents);
|
|
30
|
+
const scores = options.explain
|
|
31
|
+
? (await buildLocalAgentHealthScores()).filter((score) => !selectedAgents || selectedAgents.includes(score.agent))
|
|
32
|
+
: [];
|
|
33
|
+
console.log(options.json
|
|
34
|
+
? JSON.stringify(options.explain ? { report, scores } : report, null, 2)
|
|
35
|
+
: [
|
|
36
|
+
formatHealthReport(report),
|
|
37
|
+
...scores.map((score) => `\n${formatAgentHealthScore(score)}`),
|
|
38
|
+
].join("\n"));
|
|
39
|
+
});
|
|
40
|
+
program
|
|
41
|
+
.command("alerts")
|
|
42
|
+
.description("Explain evidence-backed archive, staleness, reviewed-commit, and permission alerts")
|
|
43
|
+
.option("--updates", "perform live update safety checks")
|
|
44
|
+
.option("--all", "include ignored alerts")
|
|
45
|
+
.option("--json", "emit machine-readable JSON")
|
|
46
|
+
.action(async (options) => {
|
|
47
|
+
const alerts = await buildFreshnessAlerts({
|
|
48
|
+
checkUpdates: options.updates,
|
|
49
|
+
});
|
|
50
|
+
const selected = options.all
|
|
51
|
+
? alerts
|
|
52
|
+
: alerts.filter((alert) => !alert.ignored);
|
|
53
|
+
console.log(options.json
|
|
54
|
+
? JSON.stringify(selected, null, 2)
|
|
55
|
+
: formatFreshnessAlerts(selected));
|
|
56
|
+
});
|
|
57
|
+
program
|
|
58
|
+
.command("alert-ignore")
|
|
59
|
+
.description("Ignore one exact freshness alert id on this machine")
|
|
60
|
+
.argument("<id>", "alert id shown by loadout alerts")
|
|
61
|
+
.action(async (id) => {
|
|
62
|
+
await ignoreFreshnessAlert(id);
|
|
63
|
+
console.log(`Ignored ${id} locally. Re-run loadout alerts --all to inspect it.`);
|
|
64
|
+
});
|
|
65
|
+
program
|
|
66
|
+
.command("alert-pin")
|
|
67
|
+
.description("Pin a reviewed replacement preference after comparing evidence; does not change active skills")
|
|
68
|
+
.argument("<package>", "currently installed package id")
|
|
69
|
+
.argument("<replacement>", "reviewed replacement package id")
|
|
70
|
+
.action(async (packageId, replacementId) => {
|
|
71
|
+
await pinReplacement(packageId, replacementId);
|
|
72
|
+
console.log(`Pinned ${replacementId} as a local replacement preference for ${packageId}. Review and activate it explicitly with loadout compare/enable.`);
|
|
73
|
+
});
|
|
74
|
+
program
|
|
75
|
+
.command("alert-unpin")
|
|
76
|
+
.description("Remove a local replacement preference")
|
|
77
|
+
.argument("<package>", "currently installed package id")
|
|
78
|
+
.action(async (packageId) => {
|
|
79
|
+
const removed = await unpinReplacement(packageId);
|
|
80
|
+
console.log(removed
|
|
81
|
+
? `Removed the replacement preference for ${packageId}.`
|
|
82
|
+
: `No replacement preference exists for ${packageId}.`);
|
|
83
|
+
});
|
|
84
|
+
program
|
|
85
|
+
.command("alert-pins")
|
|
86
|
+
.description("Show local replacement preferences")
|
|
87
|
+
.option("--json", "emit machine-readable JSON")
|
|
88
|
+
.action(async (options) => {
|
|
89
|
+
const pins = await readReplacementPins();
|
|
90
|
+
console.log(options.json
|
|
91
|
+
? JSON.stringify(pins, null, 2)
|
|
92
|
+
: pins.length
|
|
93
|
+
? pins
|
|
94
|
+
.map((pin) => `${pin.packageId} -> ${pin.replacementPackageId}`)
|
|
95
|
+
.join("\n")
|
|
96
|
+
: "No local replacement preferences.");
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { loadEffectiveCatalog } from "../core/catalog.js";
|
|
2
|
+
import { detectAgents, parseAgentSelection } from "../core/paths.js";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { runDoctor, formatDoctorReport } from "../core/doctor.js";
|
|
5
|
+
import { applyProfileToManifest } from "../core/manifest.js";
|
|
6
|
+
import { buildHealthReport, formatStatusScreen, gradeHealth, } from "../core/health.js";
|
|
7
|
+
import { applyRemove, planRemove } from "../core/remove.js";
|
|
8
|
+
import { applyUninstall, buildUninstallPlan, formatUninstallPlan, uninstallGlobalCli, } from "../core/uninstall.js";
|
|
9
|
+
import { formatRecommendations, personalizeRecommendations, profileManifestPackages, recommendPackages, RECOMMENDATION_BOUNDARY, scanProject, TESTED_PROFILES, } from "../core/recommend.js";
|
|
10
|
+
import { buildImprovementCycle, formatImprovementCycle, recordImprovementOutcome, writeImprovementCycle, } from "../core/improve.js";
|
|
11
|
+
import { applySyncPlan, buildSyncPlan } from "../core/sync.js";
|
|
12
|
+
import { ADAPTER_CAPABILITIES, buildAdapterCapabilityGaps, formatAdapterCapabilityGaps, formatCapabilityMatrix, } from "../core/adapters.js";
|
|
13
|
+
import { formatAgentInventory, inspectAgents, } from "../core/agent-inspection.js";
|
|
14
|
+
import { formatInstalledSkillInventory, scanInstalledSkills, } from "../core/skill-inventory.js";
|
|
15
|
+
import { enrichInventoryWithProvenance, formatProvenanceSummary, resolveCatalogSkillIndex, } from "../core/provenance.js";
|
|
16
|
+
import { compareSkill, formatSkillComparison } from "../core/skill-compare.js";
|
|
17
|
+
import { applySkillAdoption, formatAdoptionPlan, planSkillAdoption, } from "../core/adopt.js";
|
|
18
|
+
import { applyReconcilePlan, buildReconcilePlan, formatReconcilePlan, } from "../core/reconcile.js";
|
|
19
|
+
import { readLocalOutcomes } from "../core/outcomes.js";
|
|
20
|
+
import { formatAgentVersions, inspectAgentVersions, } from "../core/agent-versions.js";
|
|
21
|
+
import { scanSkillSecurity } from "../core/skill-security.js";
|
|
22
|
+
import { printProvenanceProgress } from "./support.js";
|
|
23
|
+
export function registerInventory(program) {
|
|
24
|
+
program
|
|
25
|
+
.command("remove")
|
|
26
|
+
.description("Safely remove only files managed for one package")
|
|
27
|
+
.argument("<package>", "managed package id")
|
|
28
|
+
.option("--yes", "apply removal; otherwise show a plan")
|
|
29
|
+
.option("--force", "also remove managed files changed outside Loadout")
|
|
30
|
+
.action(async (packageId, options) => {
|
|
31
|
+
const plan = await planRemove(packageId);
|
|
32
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
33
|
+
if (!options.yes)
|
|
34
|
+
return console.log("Dry run only. Re-run with --yes to apply this removal plan.");
|
|
35
|
+
const snapshot = await applyRemove(plan, { force: options.force });
|
|
36
|
+
console.log(`Removed ${packageId}. Snapshot: ${snapshot}`);
|
|
37
|
+
});
|
|
38
|
+
program
|
|
39
|
+
.command("uninstall")
|
|
40
|
+
.description("Preview or completely remove Loadout-managed data")
|
|
41
|
+
.option("--yes", "remove managed data; otherwise show a preview")
|
|
42
|
+
.option("--force", "also remove managed files changed outside Loadout")
|
|
43
|
+
.option("--remove-cli", "also uninstall the global loadout-ai npm command")
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
const plan = await buildUninstallPlan();
|
|
46
|
+
console.log(formatUninstallPlan(plan, { applying: Boolean(options.yes) }));
|
|
47
|
+
if (!options.yes)
|
|
48
|
+
return;
|
|
49
|
+
const result = await applyUninstall(plan, undefined, {
|
|
50
|
+
force: options.force,
|
|
51
|
+
onProgress: (message) => console.log(`→ ${message}`),
|
|
52
|
+
});
|
|
53
|
+
console.log(`Removed ${result.removedPackages} managed package(s), ${result.removedRuntimeTools} runtime tool(s), daily jobs, cache, library, and Loadout state.`);
|
|
54
|
+
if (options.removeCli) {
|
|
55
|
+
await uninstallGlobalCli();
|
|
56
|
+
console.log("Removed the global loadout-ai CLI. Open a new shell before checking the command again.");
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log("The CLI is still installed. Remove it too with: npm uninstall -g loadout-ai");
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
program
|
|
63
|
+
.command("recommend")
|
|
64
|
+
.description("Recommend catalog packages from local project signals")
|
|
65
|
+
.option("--project <path>", "project directory", process.cwd())
|
|
66
|
+
.option("--agent <id>", "personalize with local-only outcomes recorded for one supported agent")
|
|
67
|
+
.option("--json", "emit machine-readable JSON")
|
|
68
|
+
.action(async (options) => {
|
|
69
|
+
const signals = await scanProject(options.project);
|
|
70
|
+
let recommendations = recommendPackages(signals, await loadEffectiveCatalog());
|
|
71
|
+
if (options.agent) {
|
|
72
|
+
const agents = parseAgentSelection(options.agent);
|
|
73
|
+
if (agents.length !== 1)
|
|
74
|
+
throw new Error("--agent accepts exactly one id");
|
|
75
|
+
recommendations = personalizeRecommendations(recommendations, signals, await readLocalOutcomes(), agents[0]);
|
|
76
|
+
}
|
|
77
|
+
console.log(options.json
|
|
78
|
+
? JSON.stringify({
|
|
79
|
+
signals,
|
|
80
|
+
recommendations,
|
|
81
|
+
recommendationBoundary: RECOMMENDATION_BOUNDARY,
|
|
82
|
+
personalization: options.agent
|
|
83
|
+
? {
|
|
84
|
+
agent: options.agent,
|
|
85
|
+
privacy: "local-only-no-project-or-content",
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
}, null, 2)
|
|
89
|
+
: formatRecommendations(signals, recommendations));
|
|
90
|
+
});
|
|
91
|
+
program
|
|
92
|
+
.command("profiles")
|
|
93
|
+
.description("List Loadout policy profiles or inspect one profile")
|
|
94
|
+
.argument("[name]", "profile name")
|
|
95
|
+
.option("--json", "emit machine-readable JSON")
|
|
96
|
+
.option("--apply-to <path>", "add the selected profile packages to a manifest")
|
|
97
|
+
.action(async (name, options) => {
|
|
98
|
+
if (!name)
|
|
99
|
+
return console.log(options.json
|
|
100
|
+
? JSON.stringify(TESTED_PROFILES, null, 2)
|
|
101
|
+
: Object.entries(TESTED_PROFILES)
|
|
102
|
+
.map(([id, profile]) => `${id} — ${profile.description}`)
|
|
103
|
+
.join("\n"));
|
|
104
|
+
if (name === "custom" && !options.applyTo) {
|
|
105
|
+
const profile = TESTED_PROFILES.custom;
|
|
106
|
+
return console.log(options.json
|
|
107
|
+
? JSON.stringify({ name, ...profile, packages: [] }, null, 2)
|
|
108
|
+
: `custom: ${profile.description}\n Use: loadout setup --mode custom --package <id>`);
|
|
109
|
+
}
|
|
110
|
+
const packages = profileManifestPackages(name, await loadEffectiveCatalog());
|
|
111
|
+
if (options.applyTo) {
|
|
112
|
+
const manifest = await applyProfileToManifest(options.applyTo, name, packages);
|
|
113
|
+
console.log(`Applied profile ${name} to ${options.applyTo}. ${manifest.packages.length} package(s) configured.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(options.json
|
|
117
|
+
? JSON.stringify({ name, ...TESTED_PROFILES[name], packages }, null, 2)
|
|
118
|
+
: `${name}: ${TESTED_PROFILES[name].description}\n${packages.map((pkg) => ` ${pkg.id} — ${pkg.repository}`).join("\n")}`);
|
|
119
|
+
});
|
|
120
|
+
program
|
|
121
|
+
.command("improve")
|
|
122
|
+
.description("Propose the next evidence-backed improvement without changing anything")
|
|
123
|
+
.option("--json", "emit machine-readable JSON")
|
|
124
|
+
.option("--write", "persist the cycle record and reusable prompt locally")
|
|
125
|
+
.option("--output <directory>", "output directory; defaults to private Loadout state")
|
|
126
|
+
.action(async (options) => {
|
|
127
|
+
const cycle = await buildImprovementCycle();
|
|
128
|
+
console.log(options.json
|
|
129
|
+
? JSON.stringify(cycle, null, 2)
|
|
130
|
+
: formatImprovementCycle(cycle));
|
|
131
|
+
if (options.write) {
|
|
132
|
+
const paths = await writeImprovementCycle(cycle, options.output);
|
|
133
|
+
console.log(`Cycle record: ${paths.json}\nLoop prompt: ${paths.prompt}`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
program
|
|
137
|
+
.command("improve-feedback")
|
|
138
|
+
.description("Record a human-reviewed outcome for a persisted improvement cycle")
|
|
139
|
+
.requiredOption("--id <id>", "cycle id")
|
|
140
|
+
.requiredOption("--outcome <outcome>", "success, failure, or partial")
|
|
141
|
+
.option("--note <text>", "short non-secret lesson")
|
|
142
|
+
.option("--directory <path>", "improvement history directory")
|
|
143
|
+
.action(async (options) => {
|
|
144
|
+
if (!["success", "failure", "partial"].includes(options.outcome))
|
|
145
|
+
throw new Error("--outcome must be success, failure, or partial");
|
|
146
|
+
await recordImprovementOutcome(options.id, options.outcome, options.note, options.directory);
|
|
147
|
+
console.log(`Recorded ${options.outcome} outcome for ${options.id}.`);
|
|
148
|
+
});
|
|
149
|
+
program
|
|
150
|
+
.command("sync")
|
|
151
|
+
.description("Reproduce a loadout.json manifest as one safe transaction")
|
|
152
|
+
.option("--manifest <path>", "manifest path", "loadout.json")
|
|
153
|
+
.option("--lock <path>", "lockfile output", "loadout.lock")
|
|
154
|
+
.option("--yes", "apply the plan; otherwise remain read-only")
|
|
155
|
+
.option("--approve-risk", "explicitly approve plans containing scripts, hooks, or binaries")
|
|
156
|
+
.action(async (options) => {
|
|
157
|
+
const plan = await buildSyncPlan(options.manifest);
|
|
158
|
+
console.log(JSON.stringify({
|
|
159
|
+
manifest: plan.manifest,
|
|
160
|
+
packages: plan.packages.map((entry) => ({
|
|
161
|
+
...entry.plan,
|
|
162
|
+
safety: entry.safety,
|
|
163
|
+
})),
|
|
164
|
+
mcpChanges: plan.mcpPlans.map((entry) => ({
|
|
165
|
+
packageId: entry.packageId,
|
|
166
|
+
path: entry.plan.path,
|
|
167
|
+
changes: entry.plan.changes,
|
|
168
|
+
warnings: entry.plan.warnings,
|
|
169
|
+
})),
|
|
170
|
+
skipped: plan.skipped,
|
|
171
|
+
policyViolations: plan.policyViolations,
|
|
172
|
+
}, null, 2));
|
|
173
|
+
if (!options.yes)
|
|
174
|
+
return console.log("Dry run only. Re-run with --yes to synchronize this Loadout.");
|
|
175
|
+
const result = await applySyncPlan(plan, options.lock, {
|
|
176
|
+
approveRisk: options.approveRisk,
|
|
177
|
+
});
|
|
178
|
+
console.log(`Synchronized successfully.${result.snapshotId ? ` Snapshot: ${result.snapshotId}.` : ""} Lockfile: ${result.lockfile}`);
|
|
179
|
+
});
|
|
180
|
+
program
|
|
181
|
+
.command("scan")
|
|
182
|
+
.description("Inventory existing agent skills, ownership, fingerprints, and duplicates without changing anything")
|
|
183
|
+
.option("--agents <ids>", "comma-separated agent ids; defaults to detected agents")
|
|
184
|
+
.option("--refresh-provenance", "fetch exact reviewed commits and rebuild the local catalog skill index")
|
|
185
|
+
.option("--json", "emit the complete machine-readable inventory")
|
|
186
|
+
.action(async (options) => {
|
|
187
|
+
const detected = await detectAgents();
|
|
188
|
+
const requested = parseAgentSelection(options.agents);
|
|
189
|
+
const selected = requested?.length
|
|
190
|
+
? detected.filter((agent) => requested.includes(agent.id))
|
|
191
|
+
: detected.filter((agent) => agent.installed);
|
|
192
|
+
if (!selected.length)
|
|
193
|
+
throw new Error("No detected agent profile is available to scan");
|
|
194
|
+
const report = await scanInstalledSkills(selected);
|
|
195
|
+
const catalog = await loadEffectiveCatalog();
|
|
196
|
+
const resolved = await resolveCatalogSkillIndex({
|
|
197
|
+
refresh: options.refreshProvenance,
|
|
198
|
+
offline: !options.refreshProvenance,
|
|
199
|
+
build: {
|
|
200
|
+
catalog,
|
|
201
|
+
onProgress: options.refreshProvenance
|
|
202
|
+
? printProvenanceProgress
|
|
203
|
+
: undefined,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const enriched = enrichInventoryWithProvenance(report, resolved.index, resolved.source);
|
|
207
|
+
console.log(options.json
|
|
208
|
+
? JSON.stringify(enriched, null, 2)
|
|
209
|
+
: `${formatInstalledSkillInventory(enriched)}\n${formatProvenanceSummary(enriched)}`);
|
|
210
|
+
});
|
|
211
|
+
program
|
|
212
|
+
.command("reconcile")
|
|
213
|
+
.description("Match existing unmanaged skills to pinned sources, group mirrors, and preview safe ownership or updates")
|
|
214
|
+
.option("--agents <ids>", "comma-separated agent ids; defaults to detected agents")
|
|
215
|
+
.option("--refresh", "rebuild the inspected catalog skill index")
|
|
216
|
+
.option("--replace-outdated", "replace unambiguous outdated trees with their reviewed pinned source")
|
|
217
|
+
.option("--approve-risk", "approve the exact safety findings shown for outdated replacements")
|
|
218
|
+
.option("--yes", "apply the displayed transaction; otherwise preview")
|
|
219
|
+
.option("--json", "emit machine-readable output")
|
|
220
|
+
.action(async (options) => {
|
|
221
|
+
const detected = await detectAgents();
|
|
222
|
+
const requested = parseAgentSelection(options.agents);
|
|
223
|
+
const selected = requested?.length
|
|
224
|
+
? detected.filter((agent) => requested.includes(agent.id) && agent.installed)
|
|
225
|
+
: detected.filter((agent) => agent.installed);
|
|
226
|
+
if (!selected.length)
|
|
227
|
+
throw new Error("No detected agent profile is available to reconcile");
|
|
228
|
+
const resolved = await resolveCatalogSkillIndex({
|
|
229
|
+
refresh: options.refresh,
|
|
230
|
+
build: {
|
|
231
|
+
catalog: await loadEffectiveCatalog(),
|
|
232
|
+
onProgress: options.refresh ? printProvenanceProgress : undefined,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
if (!resolved.index)
|
|
236
|
+
throw new Error("No reviewed catalog skill index is available");
|
|
237
|
+
const plan = await buildReconcilePlan(selected, resolved.index);
|
|
238
|
+
if (!options.yes) {
|
|
239
|
+
console.log(options.json
|
|
240
|
+
? JSON.stringify(plan, null, 2)
|
|
241
|
+
: `${formatReconcilePlan(plan)}\n\nDry run only. Re-run with --yes to adopt exact matches.${plan.summary.outdated ? " Add --replace-outdated to include the reviewed updates shown above." : ""}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const result = await applyReconcilePlan(plan, {
|
|
245
|
+
replaceOutdated: options.replaceOutdated,
|
|
246
|
+
approveRisk: options.approveRisk,
|
|
247
|
+
});
|
|
248
|
+
console.log(options.json
|
|
249
|
+
? JSON.stringify({ plan, result }, null, 2)
|
|
250
|
+
: `${formatReconcilePlan(plan)}\n\nReconciled ${result.adopted} exact group(s) and ${result.updated} outdated group(s). Snapshot: ${result.snapshotId}`);
|
|
251
|
+
});
|
|
252
|
+
program
|
|
253
|
+
.command("adopt")
|
|
254
|
+
.description("Take ownership of one explicitly selected installed skill without changing its bytes")
|
|
255
|
+
.argument("<skill>", "installed skill name, directory name, or exact path")
|
|
256
|
+
.requiredOption("--agent <id>", "agent that owns the installed skill")
|
|
257
|
+
.option("--refresh-provenance", "rebuild the inspected catalog skill index")
|
|
258
|
+
.option("--yes", "record ownership; otherwise show a dry-run plan")
|
|
259
|
+
.option("--json", "emit machine-readable JSON")
|
|
260
|
+
.action(async (skill, options) => {
|
|
261
|
+
const detected = await detectAgents();
|
|
262
|
+
const agent = detected.find((item) => item.id === options.agent && item.installed);
|
|
263
|
+
if (!agent)
|
|
264
|
+
throw new Error(`Agent '${options.agent}' is unknown or is not installed`);
|
|
265
|
+
const resolved = await resolveCatalogSkillIndex({
|
|
266
|
+
refresh: options.refreshProvenance,
|
|
267
|
+
offline: !options.refreshProvenance,
|
|
268
|
+
build: {
|
|
269
|
+
catalog: await loadEffectiveCatalog(),
|
|
270
|
+
onProgress: options.refreshProvenance
|
|
271
|
+
? printProvenanceProgress
|
|
272
|
+
: undefined,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
const plan = await planSkillAdoption(skill, agent, resolved.index);
|
|
276
|
+
if (!options.yes) {
|
|
277
|
+
console.log(options.json
|
|
278
|
+
? JSON.stringify(plan, null, 2)
|
|
279
|
+
: `${formatAdoptionPlan(plan)}\nDry run only. Re-run with --yes to adopt this one skill.`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const snapshotId = await applySkillAdoption(plan);
|
|
283
|
+
console.log(options.json
|
|
284
|
+
? JSON.stringify({ plan, snapshotId }, null, 2)
|
|
285
|
+
: `${formatAdoptionPlan(plan)}\nAdopted without changing skill bytes. Snapshot: ${snapshotId}`);
|
|
286
|
+
});
|
|
287
|
+
program
|
|
288
|
+
.command("compare")
|
|
289
|
+
.description("Compare an installed or reviewed skill with evidence-related catalog alternatives without changing anything")
|
|
290
|
+
.argument("<skill>", "installed skill name, directory name, or catalog package id")
|
|
291
|
+
.option("--agent <id>", "select one installed agent when names are ambiguous")
|
|
292
|
+
.option("--refresh", "rebuild the inspected catalog skill index")
|
|
293
|
+
.option("--offline", "never fetch; use the existing local index only")
|
|
294
|
+
.option("--limit <count>", "maximum alternatives to show", "10")
|
|
295
|
+
.option("--json", "emit the complete machine-readable comparison")
|
|
296
|
+
.action(async (skill, options) => {
|
|
297
|
+
if (options.refresh && options.offline)
|
|
298
|
+
throw new Error("--refresh and --offline cannot be used together");
|
|
299
|
+
const limit = Number(options.limit);
|
|
300
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50)
|
|
301
|
+
throw new Error("--limit must be an integer from 1 to 50");
|
|
302
|
+
const detected = await detectAgents();
|
|
303
|
+
const agent = options.agent;
|
|
304
|
+
if (agent && !detected.some((item) => item.id === agent))
|
|
305
|
+
throw new Error(`Unknown agent id: ${options.agent}`);
|
|
306
|
+
const selected = detected.filter((item) => item.installed);
|
|
307
|
+
if (!selected.length)
|
|
308
|
+
throw new Error("No detected agent profile is available to compare");
|
|
309
|
+
const catalog = await loadEffectiveCatalog();
|
|
310
|
+
const resolved = await resolveCatalogSkillIndex({
|
|
311
|
+
refresh: options.refresh,
|
|
312
|
+
offline: options.offline,
|
|
313
|
+
build: {
|
|
314
|
+
catalog,
|
|
315
|
+
onProgress: options.offline ? undefined : printProvenanceProgress,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
if (!resolved.index)
|
|
319
|
+
throw new Error("No local catalog skill index exists. Re-run without --offline or use --refresh.");
|
|
320
|
+
const inventory = enrichInventoryWithProvenance(await scanInstalledSkills(selected), resolved.index, resolved.source);
|
|
321
|
+
const comparison = compareSkill(skill, inventory, resolved.index.records, catalog, {
|
|
322
|
+
...(agent ? { agent } : {}),
|
|
323
|
+
limit,
|
|
324
|
+
indexGeneratedAt: resolved.index.generatedAt,
|
|
325
|
+
failures: resolved.index.failures,
|
|
326
|
+
});
|
|
327
|
+
console.log(options.json
|
|
328
|
+
? JSON.stringify(comparison, null, 2)
|
|
329
|
+
: formatSkillComparison(comparison));
|
|
330
|
+
});
|
|
331
|
+
program
|
|
332
|
+
.command("status")
|
|
333
|
+
.description("Home screen: a health grade, detected agents, and the next thing to do")
|
|
334
|
+
.option("--json", "emit machine-readable status")
|
|
335
|
+
.action(async (options) => {
|
|
336
|
+
const agents = await detectAgents();
|
|
337
|
+
const [inventory, report] = await Promise.all([
|
|
338
|
+
inspectAgents(agents),
|
|
339
|
+
buildHealthReport({}),
|
|
340
|
+
]);
|
|
341
|
+
if (options.json)
|
|
342
|
+
return console.log(JSON.stringify({ grade: gradeHealth(report), report, inventory }, null, 2));
|
|
343
|
+
console.log(formatStatusScreen(report, inventory.map((item) => formatAgentInventory(item))));
|
|
344
|
+
});
|
|
345
|
+
program
|
|
346
|
+
.command("versions")
|
|
347
|
+
.description("Detect installed agent versions with bounded read-only commands")
|
|
348
|
+
.option("--json", "emit machine-readable version evidence")
|
|
349
|
+
.action(async (options) => {
|
|
350
|
+
const evidence = await inspectAgentVersions();
|
|
351
|
+
console.log(options.json
|
|
352
|
+
? JSON.stringify(evidence, null, 2)
|
|
353
|
+
: formatAgentVersions(evidence));
|
|
354
|
+
});
|
|
355
|
+
program
|
|
356
|
+
.command("skill-audit")
|
|
357
|
+
.description("Statically inspect one Agent Skill and emit its security/capability inventory")
|
|
358
|
+
.argument("<directory>", "skill directory containing SKILL.md")
|
|
359
|
+
.option("--json", "emit the complete machine-readable report")
|
|
360
|
+
.action(async (directory, options) => {
|
|
361
|
+
const report = await scanSkillSecurity(resolve(directory));
|
|
362
|
+
console.log(options.json
|
|
363
|
+
? JSON.stringify(report, null, 2)
|
|
364
|
+
: [
|
|
365
|
+
`Skill audit: ${report.specification.name ?? report.rootName}`,
|
|
366
|
+
`Verdict: ${report.verdict}`,
|
|
367
|
+
`Inventory: ${report.inventory.totalFiles} files, ${report.inventory.totalBytes} bytes, sha256:${report.inventory.treeHash}`,
|
|
368
|
+
`Capabilities: ${report.capabilities.executableFiles.length} executable/script files, ${report.capabilities.dependencyNames.length} named dependencies, ${report.capabilities.domains.length} domains, ${report.capabilities.environmentNames.length} environment names`,
|
|
369
|
+
...report.deterministicFindings.map((item) => `${item.severity === "critical" ? "✗" : "!"} ${item.severity}/${item.category}: ${item.message} (${item.paths.join(", ")})`),
|
|
370
|
+
...(!report.deterministicFindings.length
|
|
371
|
+
? [
|
|
372
|
+
"No deterministic findings within the bounded static policy.",
|
|
373
|
+
]
|
|
374
|
+
: []),
|
|
375
|
+
"Static inspection cannot prove runtime safety or task quality.",
|
|
376
|
+
].join("\n"));
|
|
377
|
+
if (report.verdict === "blocked")
|
|
378
|
+
process.exitCode = 2;
|
|
379
|
+
});
|
|
380
|
+
program
|
|
381
|
+
.command("capabilities")
|
|
382
|
+
.description("Show honest native, adapted, and unsupported adapter capabilities")
|
|
383
|
+
.option("--json", "emit machine-readable JSON")
|
|
384
|
+
.option("--inspect", "also inspect managed component directories on this machine")
|
|
385
|
+
.option("--gaps", "show the evidence-gated backlog for unsupported adapter combinations")
|
|
386
|
+
.action(async (options) => {
|
|
387
|
+
if (options.gaps) {
|
|
388
|
+
if (options.inspect)
|
|
389
|
+
throw new Error("Choose either --inspect or --gaps");
|
|
390
|
+
const gaps = buildAdapterCapabilityGaps();
|
|
391
|
+
return console.log(options.json
|
|
392
|
+
? JSON.stringify(gaps, null, 2)
|
|
393
|
+
: formatAdapterCapabilityGaps());
|
|
394
|
+
}
|
|
395
|
+
if (!options.inspect)
|
|
396
|
+
return console.log(options.json
|
|
397
|
+
? JSON.stringify(ADAPTER_CAPABILITIES, null, 2)
|
|
398
|
+
: formatCapabilityMatrix());
|
|
399
|
+
const inventory = await inspectAgents(await detectAgents());
|
|
400
|
+
if (options.json)
|
|
401
|
+
return console.log(JSON.stringify({ capabilities: ADAPTER_CAPABILITIES, inventory }, null, 2));
|
|
402
|
+
console.log(`${formatCapabilityMatrix()}\n\nLocal managed-component inventory:`);
|
|
403
|
+
for (const item of inventory)
|
|
404
|
+
console.log(formatAgentInventory(item));
|
|
405
|
+
});
|
|
406
|
+
program
|
|
407
|
+
.command("doctor")
|
|
408
|
+
.description("Check agents, skill directories, permissions, and Loadout setup")
|
|
409
|
+
.option("--verbose", "show per-component detail for each agent")
|
|
410
|
+
.option("--json", "print a machine-readable report")
|
|
411
|
+
.action(async (options) => {
|
|
412
|
+
const report = await runDoctor();
|
|
413
|
+
console.log(options.json
|
|
414
|
+
? JSON.stringify(report, null, 2)
|
|
415
|
+
: formatDoctorReport(report, { verbose: options.verbose }));
|
|
416
|
+
});
|
|
417
|
+
}
|