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,424 @@
|
|
|
1
|
+
import { userHome } from "../core/paths.js";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fetchRepositorySnapshot } from "../core/source.js";
|
|
5
|
+
import { discoverMcpManifests, summarizeMcpManifest, planMcpConfig, summarizeMcpConfigPlan, applyMcpConfigPlan, } from "../core/mcp.js";
|
|
6
|
+
import { REVIEWED_MCP_RECIPES, buildMcpRecipeServer, findMcpRecipe, formatMcpRecipePlan, planMcpRecipe, verifyMcpRecipe, verifyMcpRecipeConnection, } from "../core/mcp-recipes.js";
|
|
7
|
+
import { inspectPackage, formatPackageInspection } from "../core/package.js";
|
|
8
|
+
import { installStatePath, recordInstallTransaction, recordMcpInstall, } from "../core/state.js";
|
|
9
|
+
import { applyCodexMcpConfigPlan, codexMcpServerFingerprint, defaultCodexMcpConfigPath, planCodexMcpConfig, } from "../core/codex-mcp.js";
|
|
10
|
+
import { evaluatePackage, formatPackageEvaluation } from "../core/evaluate.js";
|
|
11
|
+
import { checkForUpdates, startUpdateWatcher } from "../core/update-watch.js";
|
|
12
|
+
import { runDisposableSandbox } from "../core/sandbox.js";
|
|
13
|
+
import { applyNativeScheduler, formatNativeScheduler, planNativeScheduler, } from "../core/scheduler.js";
|
|
14
|
+
import { collectOption, parseMcpCredentialMappings, durableSchedulerLauncher, } from "./support.js";
|
|
15
|
+
export function registerMcp(program) {
|
|
16
|
+
program
|
|
17
|
+
.command("mcp-recipe")
|
|
18
|
+
.description("Preview/configure a reviewed MCP recipe, or explicitly verify its real connection")
|
|
19
|
+
.argument("[id]", "recipe id; omit to list reviewed recipes")
|
|
20
|
+
.option("--agent <id>", "target host: codex or claude-code; selects its default config format and path")
|
|
21
|
+
.option("--config <path>", "target JSON MCP config path")
|
|
22
|
+
.option("--yes", "write the reviewed server entry after preview")
|
|
23
|
+
.option("--verify", "verify the configured entry without starting a server")
|
|
24
|
+
.option("--connect", "launch the exact pinned artifact and perform an MCP initialize handshake")
|
|
25
|
+
.option("--credential <mapping>", "credential mapping NAME=env:VARIABLE or NAME=keychain:SERVICE (repeatable)", collectOption, [])
|
|
26
|
+
.option("--credential-account <account>", "account for keychain mappings")
|
|
27
|
+
.option("--timeout <milliseconds>", "real connection timeout", "30000")
|
|
28
|
+
.option("--approve-risk", "approve launching the reviewed pinned MCP artifact for --connect")
|
|
29
|
+
.option("--json", "emit machine-readable JSON")
|
|
30
|
+
.option("--no-key", "list recipes needing no separately billed AI/model API key")
|
|
31
|
+
.option("--no-model-key", "clear alias for --no-key; service tokens may still be required")
|
|
32
|
+
.option("--credential-free", "list recipes needing no credential of any kind")
|
|
33
|
+
.action(async (id, options) => {
|
|
34
|
+
if (!id) {
|
|
35
|
+
if (options.connect || options.verify || options.yes)
|
|
36
|
+
throw new Error("Select an MCP recipe id for this operation");
|
|
37
|
+
const selected = REVIEWED_MCP_RECIPES.filter((recipe) => !((options.key === false || options.modelKey === false) &&
|
|
38
|
+
recipe.modelApiProviders.length > 0) && !(options.credentialFree && recipe.environment.length > 0));
|
|
39
|
+
const listed = selected.map((recipe) => ({
|
|
40
|
+
id: recipe.id,
|
|
41
|
+
displayName: recipe.displayName,
|
|
42
|
+
source: recipe.source,
|
|
43
|
+
permissions: recipe.permissions,
|
|
44
|
+
environment: recipe.environment,
|
|
45
|
+
modelApiProviders: recipe.modelApiProviders,
|
|
46
|
+
}));
|
|
47
|
+
const output = options.json
|
|
48
|
+
? JSON.stringify(listed, null, 2)
|
|
49
|
+
: listed
|
|
50
|
+
.map((recipe) => `${recipe.id} — ${recipe.displayName} — ${recipe.modelApiProviders.length ? `AI API required: ${recipe.modelApiProviders.join(", ")}` : "No AI API key required"} · ${recipe.environment.length ? `${recipe.environment.includes("GITHUB_PERSONAL_ACCESS_TOKEN") ? "GitHub token required" : `service credential required: ${recipe.environment.join(", ")}`}` : "no other credential"}`)
|
|
51
|
+
.join("\n");
|
|
52
|
+
console.log(output);
|
|
53
|
+
if (!options.json)
|
|
54
|
+
console.log("\nInstall for one host (preview first):\n loadout mcp-recipe playwright --agent codex\n loadout mcp-recipe playwright --agent claude-code");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (options.connect) {
|
|
58
|
+
if (options.verify || options.yes)
|
|
59
|
+
throw new Error("--connect cannot be combined with --verify or --yes");
|
|
60
|
+
const recipe = findMcpRecipe(id);
|
|
61
|
+
const credentialReferences = parseMcpCredentialMappings(recipe, options.credential, options.credentialAccount);
|
|
62
|
+
const timeoutMs = Number(options.timeout);
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const abort = () => controller.abort();
|
|
65
|
+
process.once("SIGINT", abort);
|
|
66
|
+
process.once("SIGTERM", abort);
|
|
67
|
+
try {
|
|
68
|
+
const result = await verifyMcpRecipeConnection(id, {
|
|
69
|
+
approveRisk: Boolean(options.approveRisk),
|
|
70
|
+
credentialReferences,
|
|
71
|
+
timeoutMs,
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
});
|
|
74
|
+
console.log(options.json
|
|
75
|
+
? JSON.stringify(result, null, 2)
|
|
76
|
+
: `Connected: ${result.recipeId} · MCP ${result.protocolVersion}${result.serverInfo ? ` · ${result.serverInfo.name}${result.serverInfo.version ? ` ${result.serverInfo.version}` : ""}` : ""}\n${result.checks.join("\n")}`);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
process.off("SIGINT", abort);
|
|
80
|
+
process.off("SIGTERM", abort);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (options.agent &&
|
|
85
|
+
options.agent !== "codex" &&
|
|
86
|
+
options.agent !== "claude-code")
|
|
87
|
+
throw new Error("--agent must be codex or claude-code");
|
|
88
|
+
if (options.agent && options.config)
|
|
89
|
+
throw new Error("Choose --agent for a managed default path or --config for an explicit JSON path, not both");
|
|
90
|
+
const configPath = options.config ??
|
|
91
|
+
(options.agent === "codex"
|
|
92
|
+
? defaultCodexMcpConfigPath()
|
|
93
|
+
: options.agent === "claude-code"
|
|
94
|
+
? join(userHome(), ".claude.json")
|
|
95
|
+
: undefined);
|
|
96
|
+
if (!configPath)
|
|
97
|
+
throw new Error("Choose --agent codex, --agent claude-code, or an explicit --config path");
|
|
98
|
+
if (options.verify) {
|
|
99
|
+
if (options.yes)
|
|
100
|
+
throw new Error("--verify cannot be combined with --yes");
|
|
101
|
+
if (options.agent === "codex") {
|
|
102
|
+
const recipe = findMcpRecipe(id);
|
|
103
|
+
let content = "";
|
|
104
|
+
try {
|
|
105
|
+
content = await readFile(configPath, "utf8");
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Missing config is rendered as a normal not-configured result.
|
|
109
|
+
}
|
|
110
|
+
const configured = Boolean(codexMcpServerFingerprint(content, recipe.serverName));
|
|
111
|
+
console.log(options.json
|
|
112
|
+
? JSON.stringify({ recipeId: id, configPath, configured }, null, 2)
|
|
113
|
+
: `${configured ? "Configured" : "Not configured"}: ${id}\nCodex config: ${configPath}\nConfiguration presence does not launch the MCP server; add --connect --approve-risk for a bounded handshake.`);
|
|
114
|
+
if (!configured)
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const verification = await verifyMcpRecipe(id, configPath);
|
|
119
|
+
console.log(options.json
|
|
120
|
+
? JSON.stringify(verification, null, 2)
|
|
121
|
+
: `${verification.configured ? "Configured" : "Not configured"}: ${verification.recipeId}\n${[...verification.checks, ...verification.warnings].join("\n")}`);
|
|
122
|
+
if (!verification.configured)
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const recipe = findMcpRecipe(id);
|
|
127
|
+
const credentialReferences = parseMcpCredentialMappings(recipe, options.credential, options.credentialAccount);
|
|
128
|
+
if (options.agent === "codex") {
|
|
129
|
+
const server = buildMcpRecipeServer(id, configPath, {
|
|
130
|
+
credentialReferences,
|
|
131
|
+
requireResolvedCredentials: Boolean(options.yes),
|
|
132
|
+
});
|
|
133
|
+
const plan = await planCodexMcpConfig(configPath, server, server.name);
|
|
134
|
+
const recipe = findMcpRecipe(id);
|
|
135
|
+
const preview = [
|
|
136
|
+
`MCP recipe: ${recipe.displayName}`,
|
|
137
|
+
`Target: Codex`,
|
|
138
|
+
`Config: ${plan.path}`,
|
|
139
|
+
`Change: ${plan.summary}`,
|
|
140
|
+
`Artifact: ${recipe.artifact}`,
|
|
141
|
+
`Permissions: ${recipe.permissions.join(", ")}`,
|
|
142
|
+
recipe.environment.length
|
|
143
|
+
? `Credential references required: ${recipe.environment.join(", ")}`
|
|
144
|
+
: "Credentials: none",
|
|
145
|
+
].join("\n");
|
|
146
|
+
if (!options.yes) {
|
|
147
|
+
console.log(options.json
|
|
148
|
+
? JSON.stringify({
|
|
149
|
+
recipe,
|
|
150
|
+
agent: "codex",
|
|
151
|
+
path: plan.path,
|
|
152
|
+
summary: plan.summary,
|
|
153
|
+
}, null, 2)
|
|
154
|
+
: `${preview}\nDry run only. Re-run with --yes to add this reviewed Codex MCP server.`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const fingerprint = codexMcpServerFingerprint(plan.proposed, recipe.serverName);
|
|
158
|
+
if (!fingerprint)
|
|
159
|
+
throw new Error("Could not fingerprint the applied Codex MCP entry");
|
|
160
|
+
const snapshot = await applyCodexMcpConfigPlan(plan, {
|
|
161
|
+
extraTargets: [installStatePath()],
|
|
162
|
+
afterApply: async (_result, transactionSnapshot) => recordMcpInstall({
|
|
163
|
+
packageId: `mcp-recipe:${id}:codex`,
|
|
164
|
+
configPath: plan.path,
|
|
165
|
+
serverName: recipe.serverName,
|
|
166
|
+
fingerprint,
|
|
167
|
+
snapshotId: transactionSnapshot.id,
|
|
168
|
+
installedAt: new Date().toISOString(),
|
|
169
|
+
configFormat: "codex-toml",
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
console.log(options.json
|
|
173
|
+
? JSON.stringify({
|
|
174
|
+
recipe,
|
|
175
|
+
agent: "codex",
|
|
176
|
+
snapshotId: snapshot.rollbackSnapshotId,
|
|
177
|
+
}, null, 2)
|
|
178
|
+
: `${preview}\nConfigured for Codex. Managed package: mcp-recipe:${id}:codex\nRollback: loadout rollback --snapshot ${snapshot.rollbackSnapshotId}\nNext: loadout mcp-recipe ${id} --agent codex --verify`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const plan = await planMcpRecipe(id, configPath, {
|
|
182
|
+
credentialReferences,
|
|
183
|
+
requireResolvedCredentials: Boolean(options.yes),
|
|
184
|
+
});
|
|
185
|
+
if (!options.yes) {
|
|
186
|
+
const target = options.agent === "claude-code" ? "Target: Claude Code\n" : "";
|
|
187
|
+
console.log(options.json
|
|
188
|
+
? JSON.stringify(options.agent ? { agent: options.agent, ...plan } : plan, null, 2)
|
|
189
|
+
: `${target}${formatMcpRecipePlan(plan)}\nDry run only. Re-run with --yes to write this server entry.`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const managedId = options.agent
|
|
193
|
+
? `mcp-recipe:${id}:${options.agent}`
|
|
194
|
+
: `mcp-recipe:${id}`;
|
|
195
|
+
const snapshot = await applyMcpConfigPlan(plan.config, {
|
|
196
|
+
extraTargets: [installStatePath()],
|
|
197
|
+
afterApply: async (_result, transactionSnapshot) => {
|
|
198
|
+
await recordInstallTransaction([], [{ packageId: managedId, plan: plan.config }], transactionSnapshot.id);
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
console.log(options.json
|
|
202
|
+
? JSON.stringify({ plan, snapshot }, null, 2)
|
|
203
|
+
: `${options.agent === "claude-code" ? "Target: Claude Code\n" : ""}${formatMcpRecipePlan(plan)}\nConfigured${options.agent ? " for Claude Code" : ""}. Managed package: ${managedId}\nRollback: loadout rollback --snapshot ${snapshot.rollbackSnapshotId}\nAuthorize the service separately, then run: loadout mcp-recipe ${id} ${options.agent ? `--agent ${options.agent}` : `--config ${plan.config.path}`} --verify`);
|
|
204
|
+
});
|
|
205
|
+
program
|
|
206
|
+
.command("mcp")
|
|
207
|
+
.description("Inspect MCP manifests without executing servers or scripts")
|
|
208
|
+
.option("--source <directory>", "local repository directory")
|
|
209
|
+
.option("--repository <owner/repo>", "public GitHub repository")
|
|
210
|
+
.option("--json", "emit normalized JSON")
|
|
211
|
+
.action(async (options) => {
|
|
212
|
+
if ((options.source ? 1 : 0) + (options.repository ? 1 : 0) !== 1) {
|
|
213
|
+
throw new Error("Provide exactly one of --source or --repository");
|
|
214
|
+
}
|
|
215
|
+
const source = options.repository
|
|
216
|
+
? (await fetchRepositorySnapshot(options.repository)).path
|
|
217
|
+
: options.source;
|
|
218
|
+
const manifests = await discoverMcpManifests(source);
|
|
219
|
+
if (options.json)
|
|
220
|
+
console.log(JSON.stringify(manifests, null, 2));
|
|
221
|
+
else if (manifests.length === 0)
|
|
222
|
+
console.log("No supported MCP manifests found.");
|
|
223
|
+
else
|
|
224
|
+
for (const manifest of manifests)
|
|
225
|
+
console.log(summarizeMcpManifest(manifest));
|
|
226
|
+
});
|
|
227
|
+
program
|
|
228
|
+
.command("inspect")
|
|
229
|
+
.description("Inspect skills and MCP components in a local directory or public GitHub repository")
|
|
230
|
+
.option("--source <directory>", "local package directory")
|
|
231
|
+
.option("--repository <owner/repo>", "public GitHub repository")
|
|
232
|
+
.option("--json", "emit normalized JSON")
|
|
233
|
+
.action(async (options) => {
|
|
234
|
+
if ((options.source ? 1 : 0) + (options.repository ? 1 : 0) !== 1)
|
|
235
|
+
throw new Error("Provide exactly one of --source or --repository");
|
|
236
|
+
const source = options.repository
|
|
237
|
+
? (await fetchRepositorySnapshot(options.repository)).path
|
|
238
|
+
: options.source;
|
|
239
|
+
const result = await inspectPackage(source);
|
|
240
|
+
console.log(options.json
|
|
241
|
+
? JSON.stringify(result, null, 2)
|
|
242
|
+
: formatPackageInspection(result));
|
|
243
|
+
});
|
|
244
|
+
program
|
|
245
|
+
.command("evaluate")
|
|
246
|
+
.description("Evaluate static skill and MCP evidence without executing package code")
|
|
247
|
+
.option("--source <directory>", "local package directory")
|
|
248
|
+
.option("--repository <owner/repo>", "public GitHub repository")
|
|
249
|
+
.option("--json", "emit evaluation JSON")
|
|
250
|
+
.action(async (options) => {
|
|
251
|
+
if ((options.source ? 1 : 0) + (options.repository ? 1 : 0) !== 1)
|
|
252
|
+
throw new Error("Provide exactly one of --source or --repository");
|
|
253
|
+
const source = options.repository
|
|
254
|
+
? (await fetchRepositorySnapshot(options.repository)).path
|
|
255
|
+
: options.source;
|
|
256
|
+
const result = await evaluatePackage(source);
|
|
257
|
+
console.log(options.json
|
|
258
|
+
? JSON.stringify(result, null, 2)
|
|
259
|
+
: formatPackageEvaluation(result));
|
|
260
|
+
});
|
|
261
|
+
program
|
|
262
|
+
.command("watch")
|
|
263
|
+
.description("Watch for read-only updates; never applies changes automatically")
|
|
264
|
+
.option("--interval <minutes>", "check interval", "1440")
|
|
265
|
+
.option("--once", "check once and exit")
|
|
266
|
+
.option("--json", "emit each notification as JSON")
|
|
267
|
+
.action(async (options) => {
|
|
268
|
+
const minutes = Number(options.interval);
|
|
269
|
+
if (!Number.isFinite(minutes) || minutes <= 0)
|
|
270
|
+
throw new Error("--interval must be a positive number of minutes");
|
|
271
|
+
const notify = (notification) => console.log(options.json
|
|
272
|
+
? JSON.stringify(notification)
|
|
273
|
+
: `${notification.checkedAt}: ${notification.message}`);
|
|
274
|
+
if (options.once) {
|
|
275
|
+
notify(await checkForUpdates());
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const stop = startUpdateWatcher({
|
|
279
|
+
intervalMs: minutes * 60_000,
|
|
280
|
+
notify,
|
|
281
|
+
});
|
|
282
|
+
const shutdown = () => {
|
|
283
|
+
stop();
|
|
284
|
+
process.exit(0);
|
|
285
|
+
};
|
|
286
|
+
process.once("SIGINT", shutdown);
|
|
287
|
+
process.once("SIGTERM", shutdown);
|
|
288
|
+
await new Promise(() => undefined);
|
|
289
|
+
});
|
|
290
|
+
for (const action of [
|
|
291
|
+
"schedule",
|
|
292
|
+
"unschedule",
|
|
293
|
+
]) {
|
|
294
|
+
program
|
|
295
|
+
.command(action)
|
|
296
|
+
.description(`${action === "schedule" ? "Install" : "Remove"} a native daily read-only update or candidate-discovery check`)
|
|
297
|
+
.option("--time <HH:MM>", "local daily check time", "09:00")
|
|
298
|
+
.option("--job <updates|discovery>", "daily read-only job", "updates")
|
|
299
|
+
.option("--yes", "apply the native scheduler change")
|
|
300
|
+
.option("--json", "emit machine-readable JSON")
|
|
301
|
+
.action(async (options) => {
|
|
302
|
+
if (options.job !== "updates" && options.job !== "discovery")
|
|
303
|
+
throw new Error("--job must be updates or discovery");
|
|
304
|
+
const plan = planNativeScheduler(action, {
|
|
305
|
+
time: options.time,
|
|
306
|
+
launcher: durableSchedulerLauncher(),
|
|
307
|
+
job: options.job,
|
|
308
|
+
});
|
|
309
|
+
if (!options.yes) {
|
|
310
|
+
console.log(options.json
|
|
311
|
+
? JSON.stringify(plan, null, 2)
|
|
312
|
+
: `${formatNativeScheduler(plan)}\nDry run only. Re-run with --yes to change the native scheduler.`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const snapshotId = await applyNativeScheduler(plan);
|
|
316
|
+
console.log(options.json
|
|
317
|
+
? JSON.stringify({ plan, snapshotId }, null, 2)
|
|
318
|
+
: `${formatNativeScheduler(plan)}\nApplied. Snapshot: ${snapshotId}`);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
program
|
|
322
|
+
.command("sandbox-run")
|
|
323
|
+
.description("Run an explicitly approved command in a disposable networkless Docker sandbox")
|
|
324
|
+
.requiredOption("--source <directory>", "read-only source directory")
|
|
325
|
+
.requiredOption("--image <image>", "reviewed/pinned Docker image reference")
|
|
326
|
+
.requiredOption("--command <argument>", "command argument (repeatable; first is executable)", (value, previous = []) => [...previous, value], [])
|
|
327
|
+
.requiredOption("--approve-risk", "explicitly approve sandbox execution")
|
|
328
|
+
.option("--timeout <milliseconds>", "execution timeout", "120000")
|
|
329
|
+
.option("--json", "emit result JSON")
|
|
330
|
+
.action(async (options) => {
|
|
331
|
+
const result = await runDisposableSandbox({
|
|
332
|
+
sourceDirectory: options.source,
|
|
333
|
+
image: options.image,
|
|
334
|
+
command: options.command,
|
|
335
|
+
approveRisk: options.approveRisk,
|
|
336
|
+
timeoutMs: Number(options.timeout),
|
|
337
|
+
});
|
|
338
|
+
console.log(options.json
|
|
339
|
+
? JSON.stringify(result, null, 2)
|
|
340
|
+
: `Sandbox exited ${result.exitCode}\n${result.stdout}${result.stderr ? `\n${result.stderr}` : ""}`);
|
|
341
|
+
if (result.exitCode !== 0)
|
|
342
|
+
process.exitCode = result.exitCode;
|
|
343
|
+
});
|
|
344
|
+
program
|
|
345
|
+
.command("mcp-config")
|
|
346
|
+
.description("Plan or apply a safe MCP server configuration change (dry-run by default)")
|
|
347
|
+
.requiredOption("--config <path>", "MCP JSON configuration path")
|
|
348
|
+
.requiredOption("--name <name>", "server name")
|
|
349
|
+
.option("--command <command>", "local server command")
|
|
350
|
+
.option("--url <url>", "remote MCP server URL")
|
|
351
|
+
.option("--arg <value>", "server argument (repeatable)", (value, previous = []) => [...previous, value], [])
|
|
352
|
+
.option("--env <NAME=VALUE>", "environment variable (repeatable; values are never printed)", (value, previous = []) => [...previous, value], [])
|
|
353
|
+
.option("--yes", "apply the change; without this flag only a plan is shown")
|
|
354
|
+
.action(async (options) => {
|
|
355
|
+
if ((options.command ? 1 : 0) + (options.url ? 1 : 0) !== 1)
|
|
356
|
+
throw new Error("Provide exactly one of --command or --url");
|
|
357
|
+
const env = {};
|
|
358
|
+
for (const item of options.env) {
|
|
359
|
+
const separator = item.indexOf("=");
|
|
360
|
+
if (separator <= 0)
|
|
361
|
+
throw new Error(`Invalid --env '${item}'; expected NAME=VALUE`);
|
|
362
|
+
const key = item.slice(0, separator);
|
|
363
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
364
|
+
throw new Error(`Invalid environment variable name '${key}'`);
|
|
365
|
+
env[key] = item.slice(separator + 1);
|
|
366
|
+
}
|
|
367
|
+
const server = {
|
|
368
|
+
name: options.name,
|
|
369
|
+
command: options.command,
|
|
370
|
+
url: options.url,
|
|
371
|
+
args: options.arg,
|
|
372
|
+
env,
|
|
373
|
+
sourcePath: options.config,
|
|
374
|
+
warnings: [],
|
|
375
|
+
};
|
|
376
|
+
const plan = await planMcpConfig(options.config, server);
|
|
377
|
+
console.log(summarizeMcpConfigPlan(plan));
|
|
378
|
+
if (!options.yes) {
|
|
379
|
+
console.log("Dry run only. Re-run with --yes to apply this change.");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const snapshot = await applyMcpConfigPlan(plan);
|
|
383
|
+
console.log(`Applied successfully. Snapshot: ${snapshot.rollbackSnapshotId}`);
|
|
384
|
+
});
|
|
385
|
+
program
|
|
386
|
+
.command("codex-mcp-config")
|
|
387
|
+
.description("Plan or add a Codex TOML MCP server (dry-run by default)")
|
|
388
|
+
.option("--config <path>", "Codex config.toml path", defaultCodexMcpConfigPath())
|
|
389
|
+
.requiredOption("--name <name>", "server name")
|
|
390
|
+
.option("--command <command>", "local server command")
|
|
391
|
+
.option("--url <url>", "remote MCP server URL")
|
|
392
|
+
.option("--arg <value>", "server argument (repeatable)", (value, previous = []) => [...previous, value], [])
|
|
393
|
+
.option("--env <NAME=VALUE>", "environment variable (repeatable; values are never printed)", (value, previous = []) => [...previous, value], [])
|
|
394
|
+
.option("--yes", "apply the change; without this flag only a plan is shown")
|
|
395
|
+
.action(async (options) => {
|
|
396
|
+
if ((options.command ? 1 : 0) + (options.url ? 1 : 0) !== 1)
|
|
397
|
+
throw new Error("Provide exactly one of --command or --url");
|
|
398
|
+
const env = {};
|
|
399
|
+
for (const item of options.env) {
|
|
400
|
+
const separator = item.indexOf("=");
|
|
401
|
+
if (separator <= 0)
|
|
402
|
+
throw new Error(`Invalid --env '${item}'; expected NAME=VALUE`);
|
|
403
|
+
const key = item.slice(0, separator);
|
|
404
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
405
|
+
throw new Error(`Invalid environment variable name '${key}'`);
|
|
406
|
+
env[key] = item.slice(separator + 1);
|
|
407
|
+
}
|
|
408
|
+
const server = {
|
|
409
|
+
name: options.name,
|
|
410
|
+
command: options.command,
|
|
411
|
+
url: options.url,
|
|
412
|
+
args: options.arg,
|
|
413
|
+
env,
|
|
414
|
+
sourcePath: options.config,
|
|
415
|
+
warnings: [],
|
|
416
|
+
};
|
|
417
|
+
const plan = await planCodexMcpConfig(options.config, server);
|
|
418
|
+
console.log(`Codex config: ${plan.path}\n - ${plan.summary}`);
|
|
419
|
+
if (!options.yes)
|
|
420
|
+
return console.log("Dry run only. Re-run with --yes to add this Codex MCP server.");
|
|
421
|
+
const snapshot = await applyCodexMcpConfigPlan(plan);
|
|
422
|
+
console.log(`Applied successfully. Snapshot: ${snapshot.rollbackSnapshotId}`);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { parseAgentSelection } from "../core/paths.js";
|
|
2
|
+
import { formatHealthReport } from "../core/health.js";
|
|
3
|
+
import { applyUpgrade, formatUpgradePlan, planUpgrade, summarizeUpgradePlan, } from "../core/upgrade.js";
|
|
4
|
+
import { parseModelApiAccess } from "../core/access.js";
|
|
5
|
+
import { ADVANCED_GUIDE } from "../core/cli-guide.js";
|
|
6
|
+
import { collectOption, setupSelection, printSetupProgress, runSetup, printBeginnerGuide, } from "./support.js";
|
|
7
|
+
export function registerSetup(program) {
|
|
8
|
+
program
|
|
9
|
+
.command("guide")
|
|
10
|
+
.description("Show the simple, read-only path for using Loadout")
|
|
11
|
+
.action(printBeginnerGuide);
|
|
12
|
+
program
|
|
13
|
+
.command("advanced")
|
|
14
|
+
.description("Explain where to find advanced and maintainer-only commands")
|
|
15
|
+
.action(() => console.log(ADVANCED_GUIDE));
|
|
16
|
+
program
|
|
17
|
+
.command("setup")
|
|
18
|
+
.description("Preview and install a screened skill loadout for detected agents")
|
|
19
|
+
.option("--mode <mode>", "stable, power, maximum, or custom")
|
|
20
|
+
.option("--agents <ids>", "comma-separated target agent ids")
|
|
21
|
+
.option("--package <id>", "package id for custom mode", collectOption, [])
|
|
22
|
+
.option("--api-access <providers>", "separately billed model API access: none, openai, anthropic, openrouter, or other (comma-separated; never a key)")
|
|
23
|
+
.option("-y, --yes", "install after preparing the screened plan")
|
|
24
|
+
.option("--approve-risk", "approve reviewed safety findings in non-interactive mode")
|
|
25
|
+
.option("--details", "show every quarantined and deferred unit")
|
|
26
|
+
.action((options) => runSetup(options));
|
|
27
|
+
program
|
|
28
|
+
.command("upgrade")
|
|
29
|
+
.description("Diagnose, recommend, preview, and transactionally apply one screened upgrade")
|
|
30
|
+
.option("--mode <mode>", "stable, power, maximum, or custom", "stable")
|
|
31
|
+
.option("--project <path>", "project directory", process.cwd())
|
|
32
|
+
.option("--agents <ids>", "comma-separated target agent ids")
|
|
33
|
+
.option("--package <id>", "package id for custom mode", collectOption, [])
|
|
34
|
+
.option("--api-access <providers>", "separately billed model API access: none, openai, anthropic, openrouter, or other (comma-separated; never a key)")
|
|
35
|
+
.option("--yes", "apply the exact displayed upgrade")
|
|
36
|
+
.option("--approve-risk", "approve the displayed reviewed safety findings")
|
|
37
|
+
.option("--json", "emit a machine-readable preview or result")
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
const plan = await planUpgrade(setupSelection(options.mode, options.package), {
|
|
40
|
+
projectPath: options.project,
|
|
41
|
+
requestedAgents: parseAgentSelection(options.agents),
|
|
42
|
+
onProgress: options.json ? undefined : printSetupProgress,
|
|
43
|
+
access: parseModelApiAccess(options.apiAccess),
|
|
44
|
+
});
|
|
45
|
+
if (!options.yes) {
|
|
46
|
+
console.log(options.json
|
|
47
|
+
? JSON.stringify(summarizeUpgradePlan(plan), null, 2)
|
|
48
|
+
: `${formatUpgradePlan(plan)}\n\nPreview complete; nothing was changed. Re-run with --yes${plan.riskApprovalRequired ? " --approve-risk" : ""} to apply this exact upgrade.`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const result = await applyUpgrade(plan, {
|
|
52
|
+
approveRisk: options.approveRisk,
|
|
53
|
+
});
|
|
54
|
+
console.log(options.json
|
|
55
|
+
? JSON.stringify({
|
|
56
|
+
plan: summarizeUpgradePlan(plan),
|
|
57
|
+
result,
|
|
58
|
+
}, null, 2)
|
|
59
|
+
: [
|
|
60
|
+
`Upgrade applied as one transaction. Snapshot: ${result.snapshotId}`,
|
|
61
|
+
formatHealthReport(result.healthAfter),
|
|
62
|
+
...result.healthScoresAfter.map((score) => `Agent Health Score (${score.agent}): ${score.score}/100 (${score.rating}; evidence coverage ${score.evidenceCoverage}%)`),
|
|
63
|
+
"Scores summarize stored evidence only; they do not claim task improvement until benchmark or local outcome evidence exists.",
|
|
64
|
+
"Next: run `loadout rollback` to restore or `loadout outcome` after real use.",
|
|
65
|
+
].join("\n"));
|
|
66
|
+
});
|
|
67
|
+
}
|