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,708 @@
|
|
|
1
|
+
import { explainCatalogScore, loadEffectiveCatalog, loadCatalog, promoteCatalogCandidate, rankCatalog, refreshCatalog, validateCatalog, } from "../core/catalog.js";
|
|
2
|
+
import { parseAgentSelection } from "../core/paths.js";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { allPhaseRoutes, formatCostTable, formatModelCatalog, formatRouteRecommendation, formatRoutingTable, routePhase, routeTask, } from "../core/route.js";
|
|
6
|
+
import { formatHandoffStatus, getHandoffState, initHandoff, isHandoffInitialized, markDone, sendHandoff, } from "../core/handoff.js";
|
|
7
|
+
import { catalogTrustStage, formatCatalogTrustStage, } from "../core/profiles.js";
|
|
8
|
+
import { discoverHackerNewsRepositories } from "../core/community.js";
|
|
9
|
+
import { discoverPrivateRepositories } from "../core/private-discovery.js";
|
|
10
|
+
import { defaultGitHubDiscoveryQueries, discoverGitHubRepositories, } from "../core/github-discovery.js";
|
|
11
|
+
import { formatStarHistory, readCatalogObservations, } from "../core/observations.js";
|
|
12
|
+
import { writeFileAtomically } from "../core/atomic-file.js";
|
|
13
|
+
import { formatReviewQueue, markPromoted, mergeReviewQueue, readReviewQueue, setReviewDecision, } from "../core/review-queue.js";
|
|
14
|
+
import { applyProviderModelSelection, defaultModelConfigurationPath, formatProviderModelConfiguration, planProviderModelSelection, readProviderModelConfiguration, requestOpenRouter, } from "../core/model-config.js";
|
|
15
|
+
import { parseCompletionShell, renderShellCompletion, } from "../core/completion.js";
|
|
16
|
+
import { buildCatalogCoverage, formatCatalogCoverage, } from "../core/catalog-coverage.js";
|
|
17
|
+
import { createCredentialResolver, createOsCredentialStore, } from "../core/credentials.js";
|
|
18
|
+
import { buildCandidateDossier, buildCatalogProposal, formatCandidateDossier, formatCandidateSummaries, listDiscoveryCandidates, readCandidateDossier, verifyCandidateDossierSource, writeCandidateDossier, } from "../core/candidate-intelligence.js";
|
|
19
|
+
import { discoverSkillsSh } from "../core/skills-sh-discovery.js";
|
|
20
|
+
import { discoverOfficialMcpRegistry } from "../core/mcp-registry-discovery.js";
|
|
21
|
+
import { readCredentialFromStdin } from "./support.js";
|
|
22
|
+
export function registerCatalog(program) {
|
|
23
|
+
program
|
|
24
|
+
.command("catalog")
|
|
25
|
+
.description("List the real package catalog")
|
|
26
|
+
.option("--refresh", "fetch current GitHub stars and repository metadata")
|
|
27
|
+
.option("--explain <id>", "print the evidence and guardrails behind one package's ranking")
|
|
28
|
+
.option("--history <id>", "show locally recorded stars and release history")
|
|
29
|
+
.option("--coverage", "show capability, evidence, license, and overlap metrics")
|
|
30
|
+
.option("--json", "emit machine-readable output")
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
if ([options.explain, options.history, options.coverage].filter(Boolean)
|
|
33
|
+
.length > 1)
|
|
34
|
+
throw new Error("Choose one of --explain, --history, or --coverage");
|
|
35
|
+
const base = await loadCatalog();
|
|
36
|
+
const result = options.refresh
|
|
37
|
+
? await refreshCatalog(base, { forceRefresh: true })
|
|
38
|
+
: {
|
|
39
|
+
catalog: await loadEffectiveCatalog(),
|
|
40
|
+
failures: [],
|
|
41
|
+
observationFailures: [],
|
|
42
|
+
};
|
|
43
|
+
if (options.history) {
|
|
44
|
+
const pkg = result.catalog.find((item) => item.id === options.history);
|
|
45
|
+
if (!pkg)
|
|
46
|
+
throw new Error(`Unknown catalog package '${options.history}'`);
|
|
47
|
+
console.log(formatStarHistory(await readCatalogObservations(pkg.repository)));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (options.explain) {
|
|
51
|
+
const pkg = result.catalog.find((item) => item.id === options.explain);
|
|
52
|
+
if (!pkg)
|
|
53
|
+
throw new Error(`Unknown catalog package '${options.explain}'`);
|
|
54
|
+
console.log(JSON.stringify({
|
|
55
|
+
package: {
|
|
56
|
+
id: pkg.id,
|
|
57
|
+
displayName: pkg.displayName,
|
|
58
|
+
category: pkg.category,
|
|
59
|
+
tier: pkg.tier,
|
|
60
|
+
trustStage: catalogTrustStage(pkg),
|
|
61
|
+
evidenceLabel: formatCatalogTrustStage(catalogTrustStage(pkg)),
|
|
62
|
+
},
|
|
63
|
+
ranking: explainCatalogScore(pkg),
|
|
64
|
+
}, null, 2));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (options.coverage) {
|
|
68
|
+
const coverage = buildCatalogCoverage(result.catalog);
|
|
69
|
+
console.log(options.json
|
|
70
|
+
? JSON.stringify(coverage, null, 2)
|
|
71
|
+
: formatCatalogCoverage(coverage));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (options.json) {
|
|
75
|
+
console.log(JSON.stringify(rankCatalog(result.catalog), null, 2));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const pkg of rankCatalog(result.catalog)) {
|
|
79
|
+
const topics = pkg.topics?.length
|
|
80
|
+
? ` — ${pkg.topics.join(", ")}`
|
|
81
|
+
: "";
|
|
82
|
+
const updated = pkg.lastUpdatedAt
|
|
83
|
+
? ` — updated ${pkg.lastUpdatedAt.slice(0, 10)}`
|
|
84
|
+
: "";
|
|
85
|
+
console.log(`${pkg.displayName} [${pkg.tier}; ${formatCatalogTrustStage(catalogTrustStage(pkg))}] ★${pkg.stars ?? "?"} — ${pkg.repository}${topics}${updated}`);
|
|
86
|
+
}
|
|
87
|
+
for (const failure of result.failures)
|
|
88
|
+
console.error(`Warning: could not refresh ${failure.repository}: ${failure.error}`);
|
|
89
|
+
for (const failure of result.observationFailures)
|
|
90
|
+
console.error(`Warning: could not record release observation for ${failure.repository}: ${failure.error}`);
|
|
91
|
+
});
|
|
92
|
+
const candidate = program
|
|
93
|
+
.command("candidate")
|
|
94
|
+
.description("Triage and statically inspect daily discovery candidates; never auto-promotes");
|
|
95
|
+
candidate
|
|
96
|
+
.command("list")
|
|
97
|
+
.allowExcessArguments(false)
|
|
98
|
+
.description("Rank discovery leads for human triage, not as universal quality")
|
|
99
|
+
.option("--limit <count>", "maximum candidates", "20")
|
|
100
|
+
.option("--query <words>", "require all search words")
|
|
101
|
+
.option("--include-reviewed", "include repositories already in the catalog")
|
|
102
|
+
.option("--feed <path>", "alternate discovered.json evidence feed")
|
|
103
|
+
.option("--json", "emit machine-readable JSON")
|
|
104
|
+
.action(async (options) => {
|
|
105
|
+
const limit = Number(options.limit);
|
|
106
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500)
|
|
107
|
+
throw new Error("--limit must be an integer from 1 to 500");
|
|
108
|
+
const result = await listDiscoveryCandidates({
|
|
109
|
+
limit,
|
|
110
|
+
query: options.query,
|
|
111
|
+
includeReviewed: options.includeReviewed,
|
|
112
|
+
path: options.feed,
|
|
113
|
+
});
|
|
114
|
+
console.log(options.json
|
|
115
|
+
? JSON.stringify(result, null, 2)
|
|
116
|
+
: formatCandidateSummaries(result));
|
|
117
|
+
});
|
|
118
|
+
candidate
|
|
119
|
+
.command("inspect")
|
|
120
|
+
.allowExcessArguments(false)
|
|
121
|
+
.description("Clone one lead at an immutable commit and build a static evidence dossier")
|
|
122
|
+
.argument("<repository>", "owner/repository present in the discovery feed")
|
|
123
|
+
.option("--feed <path>", "alternate discovered.json evidence feed")
|
|
124
|
+
.option("--write", "persist the dossier in private Loadout state")
|
|
125
|
+
.option("--output <path>", "persist at an explicit path (implies --write)")
|
|
126
|
+
.option("--json", "emit the complete dossier as JSON")
|
|
127
|
+
.action(async (repository, options) => {
|
|
128
|
+
const dossier = await buildCandidateDossier(repository, {
|
|
129
|
+
discoveryPath: options.feed,
|
|
130
|
+
});
|
|
131
|
+
const path = options.write || options.output
|
|
132
|
+
? await writeCandidateDossier(dossier, options.output)
|
|
133
|
+
: undefined;
|
|
134
|
+
if (options.json)
|
|
135
|
+
return console.log(JSON.stringify({
|
|
136
|
+
dossier,
|
|
137
|
+
persisted: Boolean(path),
|
|
138
|
+
...(path ? { path } : {}),
|
|
139
|
+
}, null, 2));
|
|
140
|
+
console.log(formatCandidateDossier(dossier));
|
|
141
|
+
if (path)
|
|
142
|
+
console.log(`Dossier: ${path}`);
|
|
143
|
+
else {
|
|
144
|
+
console.log("Preview only. Re-run with --write to persist this dossier.");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
candidate
|
|
148
|
+
.command("propose")
|
|
149
|
+
.allowExcessArguments(false)
|
|
150
|
+
.description("Convert a reviewed dossier into a catalog-record proposal; never edits the catalog")
|
|
151
|
+
.argument("<dossier>", "persisted candidate dossier JSON")
|
|
152
|
+
.requiredOption("--id <id>", "lowercase kebab-case catalog id")
|
|
153
|
+
.requiredOption("--category <category>", "inspected catalog category")
|
|
154
|
+
.requiredOption("--platforms <ids>", "explicitly reviewed comma-separated platforms: windows,macos,linux")
|
|
155
|
+
.option("--display-name <name>", "reviewed display name")
|
|
156
|
+
.option("--description <text>", "reviewed description")
|
|
157
|
+
.option("--license <spdx>", "human-reviewed license override")
|
|
158
|
+
.option("--tier <tier>", "official, stable, trending, or community", "community")
|
|
159
|
+
.option("--approve", "confirm human review and write the proposal")
|
|
160
|
+
.option("--output <path>", "proposal JSON output path; required with --approve")
|
|
161
|
+
.option("--json", "emit machine-readable JSON")
|
|
162
|
+
.action(async (dossierPath, options) => {
|
|
163
|
+
const platforms = options.platforms
|
|
164
|
+
.split(",")
|
|
165
|
+
.map((item) => item.trim())
|
|
166
|
+
.filter(Boolean);
|
|
167
|
+
const knownPlatforms = new Set(["windows", "macos", "linux"]);
|
|
168
|
+
if (platforms.some((item) => !knownPlatforms.has(item)))
|
|
169
|
+
throw new Error("--platforms supports only windows, macos, and linux");
|
|
170
|
+
const knownTiers = new Set([
|
|
171
|
+
"official",
|
|
172
|
+
"stable",
|
|
173
|
+
"trending",
|
|
174
|
+
"community",
|
|
175
|
+
]);
|
|
176
|
+
if (!knownTiers.has(options.tier))
|
|
177
|
+
throw new Error("--tier is invalid");
|
|
178
|
+
if (options.approve && !options.output)
|
|
179
|
+
throw new Error("--approve requires --output so catalog mutation stays separate");
|
|
180
|
+
const proposal = buildCatalogProposal(await verifyCandidateDossierSource(await readCandidateDossier(dossierPath)), {
|
|
181
|
+
id: options.id,
|
|
182
|
+
category: options.category,
|
|
183
|
+
operatingSystems: platforms,
|
|
184
|
+
tier: options.tier,
|
|
185
|
+
displayName: options.displayName,
|
|
186
|
+
description: options.description,
|
|
187
|
+
license: options.license,
|
|
188
|
+
}, await loadEffectiveCatalog());
|
|
189
|
+
const output = options.approve ? resolve(options.output) : undefined;
|
|
190
|
+
if (output)
|
|
191
|
+
await writeFileAtomically(output, `${JSON.stringify(proposal, null, 2)}\n`);
|
|
192
|
+
if (options.json)
|
|
193
|
+
return console.log(JSON.stringify({
|
|
194
|
+
proposal,
|
|
195
|
+
approved: Boolean(output),
|
|
196
|
+
catalogMutated: false,
|
|
197
|
+
...(output ? { output } : {}),
|
|
198
|
+
}, null, 2));
|
|
199
|
+
console.log(JSON.stringify(proposal, null, 2));
|
|
200
|
+
if (!output)
|
|
201
|
+
console.log("Proposal preview only. Human review is still required; use --approve --output <path> to persist it.");
|
|
202
|
+
else
|
|
203
|
+
console.log(`Approved proposal written to ${output}.`);
|
|
204
|
+
});
|
|
205
|
+
candidate
|
|
206
|
+
.command("promote")
|
|
207
|
+
.allowExcessArguments(false)
|
|
208
|
+
.description("Merge a reviewed proposal into the local catalog; requires --approve")
|
|
209
|
+
.argument("<proposal>", "proposal JSON file from `candidate propose --approve`")
|
|
210
|
+
.option("--approve", "confirm human review and write to catalog")
|
|
211
|
+
.option("--json", "emit machine-readable JSON")
|
|
212
|
+
.action(async (proposalPath, options) => {
|
|
213
|
+
const proposal = JSON.parse(await readFile(resolve(proposalPath), "utf8"));
|
|
214
|
+
validateCatalog([proposal], { requireEvidence: true });
|
|
215
|
+
if (!options.approve) {
|
|
216
|
+
if (options.json)
|
|
217
|
+
return console.log(JSON.stringify({ proposal, promoted: false, catalogMutated: false }, null, 2));
|
|
218
|
+
console.log(JSON.stringify(proposal, null, 2));
|
|
219
|
+
console.log("Preview only. Re-run with --approve to merge into the catalog.");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const result = await promoteCatalogCandidate(proposal);
|
|
223
|
+
const item = await markPromoted(proposal.repository);
|
|
224
|
+
if (options.json)
|
|
225
|
+
return console.log(JSON.stringify({
|
|
226
|
+
proposal,
|
|
227
|
+
promoted: true,
|
|
228
|
+
catalogMutated: true,
|
|
229
|
+
...result,
|
|
230
|
+
reviewQueue: item,
|
|
231
|
+
}, null, 2));
|
|
232
|
+
console.log(`Promoted '${proposal.id}' into ${result.catalogPath} (${result.totalRecords} records). Review queue: ${item.repository} marked promoted.`);
|
|
233
|
+
});
|
|
234
|
+
program
|
|
235
|
+
.command("discover")
|
|
236
|
+
.description("Find public community leads; discovery never installs anything")
|
|
237
|
+
.option("--source <source>", "source: github, hacker-news, skills-sh, mcp-registry, or all", "hacker-news")
|
|
238
|
+
.option("--limit <count>", "maximum source records or stories", "50")
|
|
239
|
+
.option("--min-score <count>", "minimum Hacker News score", "20")
|
|
240
|
+
.option("--query <words>", "comma-separated words that must appear in a story (for example: codex,mcp,agent)")
|
|
241
|
+
.option("--private", "opt into private GitHub metadata discovery using GITHUB_TOKEN")
|
|
242
|
+
.option("--credential-keychain <service>", "resolve the private GitHub token from the OS credential store")
|
|
243
|
+
.option("--credential-account <account>", "OS credential account")
|
|
244
|
+
.option("--queue", "persist deduplicated public leads for human review; never promotes them")
|
|
245
|
+
.option("--json", "emit source evidence as JSON")
|
|
246
|
+
.action(async (options) => {
|
|
247
|
+
const limit = Number(options.limit);
|
|
248
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
249
|
+
throw new Error("--limit must be a positive integer");
|
|
250
|
+
if (options.credentialKeychain && !options.private)
|
|
251
|
+
throw new Error("--credential-keychain requires --private");
|
|
252
|
+
if (options.private) {
|
|
253
|
+
const token = options.credentialKeychain
|
|
254
|
+
? await createCredentialResolver()({
|
|
255
|
+
kind: "os-keychain",
|
|
256
|
+
service: options.credentialKeychain,
|
|
257
|
+
...(options.credentialAccount
|
|
258
|
+
? { account: options.credentialAccount }
|
|
259
|
+
: {}),
|
|
260
|
+
})
|
|
261
|
+
: undefined;
|
|
262
|
+
if (options.credentialKeychain && !token)
|
|
263
|
+
throw new Error("Private GitHub keychain credential did not resolve");
|
|
264
|
+
const repositories = await discoverPrivateRepositories({ token });
|
|
265
|
+
if (options.json)
|
|
266
|
+
return console.log(JSON.stringify(repositories, null, 2));
|
|
267
|
+
console.log(`Private GitHub repositories: ${repositories.length}`);
|
|
268
|
+
for (const repository of repositories)
|
|
269
|
+
console.log(`${repository.repository} — ${repository.description}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (options.source === "all") {
|
|
273
|
+
const minScore = Number(options.minScore);
|
|
274
|
+
if (!Number.isFinite(minScore) || minScore < 0)
|
|
275
|
+
throw new Error("--min-score must be a non-negative number");
|
|
276
|
+
const [github, hackerNews, skillsSh, mcpRegistry] = await Promise.allSettled([
|
|
277
|
+
discoverGitHubRepositories({
|
|
278
|
+
...(options.query
|
|
279
|
+
? { query: options.query }
|
|
280
|
+
: { queries: defaultGitHubDiscoveryQueries() }),
|
|
281
|
+
limit,
|
|
282
|
+
}),
|
|
283
|
+
discoverHackerNewsRepositories({
|
|
284
|
+
limit,
|
|
285
|
+
minScore,
|
|
286
|
+
keywords: options.query?.split(",") ?? [],
|
|
287
|
+
}),
|
|
288
|
+
discoverSkillsSh({ maxRecords: limit }),
|
|
289
|
+
discoverOfficialMcpRegistry({
|
|
290
|
+
maxRecords: limit,
|
|
291
|
+
...(options.query ? { search: options.query } : {}),
|
|
292
|
+
}),
|
|
293
|
+
]);
|
|
294
|
+
const leads = [
|
|
295
|
+
...(github.status === "fulfilled" ? github.value : []),
|
|
296
|
+
...(hackerNews.status === "fulfilled"
|
|
297
|
+
? hackerNews.value.candidates
|
|
298
|
+
: []),
|
|
299
|
+
...(skillsSh.status === "fulfilled" ? skillsSh.value.records : []),
|
|
300
|
+
...(mcpRegistry.status === "fulfilled"
|
|
301
|
+
? mcpRegistry.value.records
|
|
302
|
+
: []),
|
|
303
|
+
];
|
|
304
|
+
if (!leads.length) {
|
|
305
|
+
const failures = [github, hackerNews, skillsSh, mcpRegistry]
|
|
306
|
+
.filter((result) => result.status === "rejected")
|
|
307
|
+
.map((result) => result.reason instanceof Error
|
|
308
|
+
? result.reason.message
|
|
309
|
+
: String(result.reason));
|
|
310
|
+
const connectorIssues = [skillsSh, mcpRegistry].flatMap((result) => result.status === "fulfilled"
|
|
311
|
+
? result.value.issues.map((issue) => issue.message)
|
|
312
|
+
: []);
|
|
313
|
+
throw new Error(`All discovery sources returned no usable leads: ${[...failures, ...connectorIssues].join("; ")}`);
|
|
314
|
+
}
|
|
315
|
+
const sourceWarnings = [github, hackerNews, skillsSh, mcpRegistry]
|
|
316
|
+
.filter((result) => result.status === "rejected")
|
|
317
|
+
.map((result) => result.reason instanceof Error
|
|
318
|
+
? result.reason.message
|
|
319
|
+
: String(result.reason))
|
|
320
|
+
.concat([skillsSh, mcpRegistry].flatMap((result) => result.status === "fulfilled"
|
|
321
|
+
? result.value.issues.map((issue) => `${result.value.source}: ${issue.message}`)
|
|
322
|
+
: []));
|
|
323
|
+
const queue = options.queue
|
|
324
|
+
? await mergeReviewQueue(leads, await loadEffectiveCatalog())
|
|
325
|
+
: undefined;
|
|
326
|
+
const output = {
|
|
327
|
+
leads,
|
|
328
|
+
queue,
|
|
329
|
+
sourceWarnings,
|
|
330
|
+
connectorStatus: {
|
|
331
|
+
skillsSh: skillsSh.status === "fulfilled"
|
|
332
|
+
? skillsSh.value.status
|
|
333
|
+
: "failed",
|
|
334
|
+
mcpRegistry: mcpRegistry.status === "fulfilled"
|
|
335
|
+
? mcpRegistry.value.status
|
|
336
|
+
: "failed",
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
if (options.json)
|
|
340
|
+
return console.log(JSON.stringify(output, null, 2));
|
|
341
|
+
if (queue)
|
|
342
|
+
console.log(formatReviewQueue(queue));
|
|
343
|
+
else
|
|
344
|
+
console.log(`Multi-source discovery: ${leads.length} public lead(s).`);
|
|
345
|
+
for (const warning of sourceWarnings)
|
|
346
|
+
console.error(`Warning: ${warning}`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (options.source === "github") {
|
|
350
|
+
const repositories = await discoverGitHubRepositories({
|
|
351
|
+
...(options.query
|
|
352
|
+
? { query: options.query }
|
|
353
|
+
: { queries: defaultGitHubDiscoveryQueries() }),
|
|
354
|
+
limit,
|
|
355
|
+
});
|
|
356
|
+
if (options.queue) {
|
|
357
|
+
const queue = await mergeReviewQueue(repositories, await loadEffectiveCatalog());
|
|
358
|
+
if (options.json)
|
|
359
|
+
return console.log(JSON.stringify(queue, null, 2));
|
|
360
|
+
console.log(formatReviewQueue(queue));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (options.json)
|
|
364
|
+
return console.log(JSON.stringify(repositories, null, 2));
|
|
365
|
+
console.log(`GitHub: ${repositories.length} repository lead(s)`);
|
|
366
|
+
for (const repository of repositories)
|
|
367
|
+
console.log(`★${repository.stars} · ${repository.repository} — ${repository.description}`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (options.source === "skills-sh") {
|
|
371
|
+
const result = await discoverSkillsSh({ maxRecords: limit });
|
|
372
|
+
const queue = options.queue
|
|
373
|
+
? await mergeReviewQueue(result.records, await loadEffectiveCatalog())
|
|
374
|
+
: undefined;
|
|
375
|
+
if (options.json)
|
|
376
|
+
return console.log(JSON.stringify({ result, queue }, null, 2));
|
|
377
|
+
if (queue)
|
|
378
|
+
console.log(formatReviewQueue(queue));
|
|
379
|
+
else
|
|
380
|
+
console.log(`skills.sh (${result.status}): ${result.records.length} metadata lead(s); leaderboard popularity is not a safety or quality verdict.`);
|
|
381
|
+
for (const issue of result.issues)
|
|
382
|
+
console.error(`Warning: ${issue.message}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (options.source === "mcp-registry") {
|
|
386
|
+
const result = await discoverOfficialMcpRegistry({
|
|
387
|
+
maxRecords: limit,
|
|
388
|
+
...(options.query ? { search: options.query } : {}),
|
|
389
|
+
});
|
|
390
|
+
const queue = options.queue
|
|
391
|
+
? await mergeReviewQueue(result.records, await loadEffectiveCatalog())
|
|
392
|
+
: undefined;
|
|
393
|
+
if (options.json)
|
|
394
|
+
return console.log(JSON.stringify({ result, queue }, null, 2));
|
|
395
|
+
if (queue)
|
|
396
|
+
console.log(formatReviewQueue(queue));
|
|
397
|
+
else
|
|
398
|
+
console.log(`Official MCP Registry (${result.status}): ${result.records.length} identity/distribution lead(s); registry presence is not a Loadout safety approval.`);
|
|
399
|
+
for (const issue of result.issues)
|
|
400
|
+
console.error(`Warning: ${issue.message}`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (options.source !== "hacker-news")
|
|
404
|
+
throw new Error(`Unsupported discovery source '${options.source}'. Supported: github, hacker-news, skills-sh, mcp-registry, all`);
|
|
405
|
+
const minScore = Number(options.minScore);
|
|
406
|
+
if (!Number.isFinite(minScore) || minScore < 0)
|
|
407
|
+
throw new Error("--min-score must be a non-negative number");
|
|
408
|
+
const result = await discoverHackerNewsRepositories({
|
|
409
|
+
limit,
|
|
410
|
+
minScore,
|
|
411
|
+
keywords: options.query?.split(",") ?? [],
|
|
412
|
+
});
|
|
413
|
+
if (options.queue) {
|
|
414
|
+
const queue = await mergeReviewQueue(result.candidates, await loadEffectiveCatalog());
|
|
415
|
+
if (options.json)
|
|
416
|
+
return console.log(JSON.stringify(queue, null, 2));
|
|
417
|
+
console.log(formatReviewQueue(queue));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (options.json)
|
|
421
|
+
return console.log(JSON.stringify(result, null, 2));
|
|
422
|
+
console.log(`Hacker News: ${result.candidates.length} GitHub repository lead(s) from ${result.storiesScanned} stories.`);
|
|
423
|
+
for (const candidate of result.candidates) {
|
|
424
|
+
console.log(`★${candidate.score} · ${candidate.repository} — ${candidate.title}\n ${candidate.discussionUrl}`);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
program
|
|
428
|
+
.command("review-queue")
|
|
429
|
+
.description("Show deduplicated discovery leads awaiting human review; never installs")
|
|
430
|
+
.option("--decision <value>", "filter: pending, shortlisted, or ignored")
|
|
431
|
+
.option("--json", "emit machine-readable JSON")
|
|
432
|
+
.action(async (options) => {
|
|
433
|
+
const queue = await readReviewQueue();
|
|
434
|
+
if (options.decision &&
|
|
435
|
+
!["pending", "shortlisted", "ignored"].includes(options.decision))
|
|
436
|
+
throw new Error("--decision must be pending, shortlisted, or ignored");
|
|
437
|
+
const filtered = options.decision
|
|
438
|
+
? {
|
|
439
|
+
...queue,
|
|
440
|
+
items: queue.items.filter((item) => item.decision === options.decision),
|
|
441
|
+
}
|
|
442
|
+
: queue;
|
|
443
|
+
console.log(options.json
|
|
444
|
+
? JSON.stringify(filtered, null, 2)
|
|
445
|
+
: formatReviewQueue(filtered));
|
|
446
|
+
});
|
|
447
|
+
program
|
|
448
|
+
.command("review")
|
|
449
|
+
.description("Record a human queue decision; shortlisting still does not promote or install")
|
|
450
|
+
.argument("<repository>", "owner/repository")
|
|
451
|
+
.requiredOption("--decision <value>", "pending, shortlisted, or ignored")
|
|
452
|
+
.action(async (repository, options) => {
|
|
453
|
+
if (!["pending", "shortlisted", "ignored"].includes(options.decision))
|
|
454
|
+
throw new Error("--decision must be pending, shortlisted, or ignored");
|
|
455
|
+
const item = await setReviewDecision(repository, options.decision);
|
|
456
|
+
console.log(`${item.repository}: ${item.decision}. No catalog or agent files changed.`);
|
|
457
|
+
});
|
|
458
|
+
const credentials = program
|
|
459
|
+
.command("credentials")
|
|
460
|
+
.description("Store, inspect, or remove secrets in the native OS credential store");
|
|
461
|
+
credentials
|
|
462
|
+
.command("status")
|
|
463
|
+
.description("Check whether the native OS credential backend is available")
|
|
464
|
+
.option("--json", "emit machine-readable status")
|
|
465
|
+
.action(async (options) => {
|
|
466
|
+
const status = await createOsCredentialStore().status();
|
|
467
|
+
console.log(options.json
|
|
468
|
+
? JSON.stringify(status, null, 2)
|
|
469
|
+
: `${status.backend}: ${status.available ? "available" : "unavailable"}`);
|
|
470
|
+
if (!status.available)
|
|
471
|
+
process.exitCode = 1;
|
|
472
|
+
});
|
|
473
|
+
credentials
|
|
474
|
+
.command("set")
|
|
475
|
+
.description("Store a credential read from stdin; its value is never placed in arguments or output")
|
|
476
|
+
.argument("<service>", "credential service identifier")
|
|
477
|
+
.option("--account <account>", "credential account")
|
|
478
|
+
.requiredOption("--stdin", "require secret input from stdin")
|
|
479
|
+
.action(async (service, options) => {
|
|
480
|
+
await createOsCredentialStore().set({
|
|
481
|
+
kind: "os-keychain",
|
|
482
|
+
service,
|
|
483
|
+
...(options.account ? { account: options.account } : {}),
|
|
484
|
+
}, await readCredentialFromStdin());
|
|
485
|
+
console.log(`Stored '${service}' in the native OS credential store.`);
|
|
486
|
+
});
|
|
487
|
+
credentials
|
|
488
|
+
.command("check")
|
|
489
|
+
.description("Check whether one credential resolves without printing it")
|
|
490
|
+
.argument("<service>", "credential service identifier")
|
|
491
|
+
.option("--account <account>", "credential account")
|
|
492
|
+
.option("--json", "emit machine-readable status")
|
|
493
|
+
.action(async (service, options) => {
|
|
494
|
+
const found = Boolean(await createOsCredentialStore().get({
|
|
495
|
+
kind: "os-keychain",
|
|
496
|
+
service,
|
|
497
|
+
...(options.account ? { account: options.account } : {}),
|
|
498
|
+
}));
|
|
499
|
+
console.log(options.json
|
|
500
|
+
? JSON.stringify({ service, found })
|
|
501
|
+
: `${service}: ${found ? "stored" : "not found"}`);
|
|
502
|
+
if (!found)
|
|
503
|
+
process.exitCode = 1;
|
|
504
|
+
});
|
|
505
|
+
credentials
|
|
506
|
+
.command("delete")
|
|
507
|
+
.description("Remove one credential from the native OS store")
|
|
508
|
+
.argument("<service>", "credential service identifier")
|
|
509
|
+
.option("--account <account>", "credential account")
|
|
510
|
+
.action(async (service, options) => {
|
|
511
|
+
const deleted = await createOsCredentialStore().delete({
|
|
512
|
+
kind: "os-keychain",
|
|
513
|
+
service,
|
|
514
|
+
...(options.account ? { account: options.account } : {}),
|
|
515
|
+
});
|
|
516
|
+
console.log(`${service}: ${deleted ? "deleted" : "not found"}`);
|
|
517
|
+
});
|
|
518
|
+
const models = program
|
|
519
|
+
.command("models")
|
|
520
|
+
.description("Plan, apply, inspect, or verify secret-free provider model selections");
|
|
521
|
+
models
|
|
522
|
+
.command("status")
|
|
523
|
+
.description("Show configured model metadata and credential references")
|
|
524
|
+
.option("--config <path>", "model configuration path")
|
|
525
|
+
.option("--json", "emit machine-readable JSON")
|
|
526
|
+
.action(async (options) => {
|
|
527
|
+
const configuration = await readProviderModelConfiguration(options.config ?? defaultModelConfigurationPath());
|
|
528
|
+
console.log(options.json
|
|
529
|
+
? JSON.stringify(configuration ?? null, null, 2)
|
|
530
|
+
: formatProviderModelConfiguration(configuration));
|
|
531
|
+
});
|
|
532
|
+
models
|
|
533
|
+
.command("set")
|
|
534
|
+
.description("Plan or store one provider selection; never stores a raw key")
|
|
535
|
+
.requiredOption("--id <id>", "selection id")
|
|
536
|
+
.requiredOption("--model <model>", "provider model identifier")
|
|
537
|
+
.option("--provider <provider>", "provider id", "openrouter")
|
|
538
|
+
.option("--endpoint <url>", "provider HTTPS endpoint", "https://openrouter.ai/api/v1")
|
|
539
|
+
.option("--credential-env <name>", "environment variable reference (default: OPENROUTER_API_KEY)")
|
|
540
|
+
.option("--credential-keychain <service>", "native OS credential service reference")
|
|
541
|
+
.option("--credential-account <account>", "native credential account")
|
|
542
|
+
.option("--agents <ids>", "comma-separated target agent ids")
|
|
543
|
+
.option("--config <path>", "model configuration path")
|
|
544
|
+
.option("--yes", "apply after preview")
|
|
545
|
+
.option("--json", "emit machine-readable JSON")
|
|
546
|
+
.action(async (options) => {
|
|
547
|
+
if (options.credentialEnv && options.credentialKeychain)
|
|
548
|
+
throw new Error("Choose either --credential-env or --credential-keychain");
|
|
549
|
+
if (options.credentialAccount && !options.credentialKeychain)
|
|
550
|
+
throw new Error("--credential-account requires --credential-keychain");
|
|
551
|
+
const credential = options.credentialKeychain
|
|
552
|
+
? {
|
|
553
|
+
kind: "os-keychain",
|
|
554
|
+
service: options.credentialKeychain,
|
|
555
|
+
...(options.credentialAccount
|
|
556
|
+
? { account: options.credentialAccount }
|
|
557
|
+
: {}),
|
|
558
|
+
}
|
|
559
|
+
: {
|
|
560
|
+
kind: "environment",
|
|
561
|
+
name: options.credentialEnv ?? "OPENROUTER_API_KEY",
|
|
562
|
+
};
|
|
563
|
+
const plan = await planProviderModelSelection({
|
|
564
|
+
id: options.id,
|
|
565
|
+
provider: options.provider,
|
|
566
|
+
model: options.model,
|
|
567
|
+
endpoint: options.endpoint,
|
|
568
|
+
credential,
|
|
569
|
+
...(options.agents
|
|
570
|
+
? { targetAgents: parseAgentSelection(options.agents) }
|
|
571
|
+
: {}),
|
|
572
|
+
}, options.config ?? defaultModelConfigurationPath());
|
|
573
|
+
if (!options.yes) {
|
|
574
|
+
console.log(options.json
|
|
575
|
+
? JSON.stringify(plan, null, 2)
|
|
576
|
+
: `${formatProviderModelConfiguration(plan.configuration)}\nPath: ${plan.path}\nDry run only. Re-run with --yes to save metadata and the credential reference.`);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const snapshotId = await applyProviderModelSelection(plan);
|
|
580
|
+
console.log(options.json
|
|
581
|
+
? JSON.stringify({ configuration: plan.configuration, snapshotId }, null, 2)
|
|
582
|
+
: `${formatProviderModelConfiguration(plan.configuration)}\nSaved. Snapshot: ${snapshotId}`);
|
|
583
|
+
});
|
|
584
|
+
models
|
|
585
|
+
.command("verify")
|
|
586
|
+
.description("Make one explicit minimal provider request using the referenced credential")
|
|
587
|
+
.argument("<id>", "selection id")
|
|
588
|
+
.option("--config <path>", "model configuration path")
|
|
589
|
+
.action(async (id, options) => {
|
|
590
|
+
const configuration = await readProviderModelConfiguration(options.config ?? defaultModelConfigurationPath());
|
|
591
|
+
if (!configuration)
|
|
592
|
+
throw new Error("No provider model configuration exists");
|
|
593
|
+
await requestOpenRouter(configuration, id, [
|
|
594
|
+
{
|
|
595
|
+
role: "user",
|
|
596
|
+
content: "Reply with the single word OK.",
|
|
597
|
+
},
|
|
598
|
+
], {
|
|
599
|
+
resolveCredential: createCredentialResolver(),
|
|
600
|
+
});
|
|
601
|
+
console.log(`Verified model selection '${id}'. No credential value was stored or printed.`);
|
|
602
|
+
});
|
|
603
|
+
const handoff = program
|
|
604
|
+
.command("handoff")
|
|
605
|
+
.description("Cross-agent file-based task handoff — send work between Claude Code, Codex, and other agents");
|
|
606
|
+
handoff
|
|
607
|
+
.command("init")
|
|
608
|
+
.description("Create the .handoff/ protocol directory in the current project")
|
|
609
|
+
.action(async () => {
|
|
610
|
+
const projectRoot = process.cwd();
|
|
611
|
+
if (await isHandoffInitialized(projectRoot)) {
|
|
612
|
+
console.log("Handoff already initialized at .handoff/");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const dir = await initHandoff(projectRoot);
|
|
616
|
+
console.log(`Initialized handoff protocol at ${dir}`);
|
|
617
|
+
console.log("Add .handoff/ to version control so both agents share the message log.");
|
|
618
|
+
});
|
|
619
|
+
handoff
|
|
620
|
+
.command("send")
|
|
621
|
+
.description("Send a task or message to another agent")
|
|
622
|
+
.argument("<agent>", "target agent (e.g. codex, claude-code)")
|
|
623
|
+
.argument("<description...>", "task description")
|
|
624
|
+
.option("--from <agent>", "sending agent", "user")
|
|
625
|
+
.option("--type <type>", "message type: task, handoff, question, status, error, cancel", "task")
|
|
626
|
+
.option("--context <text>", "additional context for the receiving agent")
|
|
627
|
+
.option("--json", "emit machine-readable JSON")
|
|
628
|
+
.action(async (agent, descriptionWords, options) => {
|
|
629
|
+
const message = await sendHandoff(process.cwd(), agent, descriptionWords.join(" "), {
|
|
630
|
+
from: options.from,
|
|
631
|
+
type: options.type,
|
|
632
|
+
context: options.context,
|
|
633
|
+
});
|
|
634
|
+
console.log(options.json
|
|
635
|
+
? JSON.stringify(message, null, 2)
|
|
636
|
+
: `Sent ${message.type} ${message.id} → ${message.to}: ${message.description}`);
|
|
637
|
+
});
|
|
638
|
+
handoff
|
|
639
|
+
.command("done")
|
|
640
|
+
.description("Mark a handoff task as completed")
|
|
641
|
+
.argument("<id>", "message id to mark done")
|
|
642
|
+
.option("--json", "emit machine-readable JSON")
|
|
643
|
+
.action(async (id, options) => {
|
|
644
|
+
const message = await markDone(process.cwd(), id);
|
|
645
|
+
console.log(options.json
|
|
646
|
+
? JSON.stringify(message, null, 2)
|
|
647
|
+
: `Marked ${id} as done`);
|
|
648
|
+
});
|
|
649
|
+
handoff
|
|
650
|
+
.command("status")
|
|
651
|
+
.description("Show pending and completed handoff tasks")
|
|
652
|
+
.option("--json", "emit machine-readable JSON")
|
|
653
|
+
.action(async (options) => {
|
|
654
|
+
const state = await getHandoffState(process.cwd());
|
|
655
|
+
console.log(options.json
|
|
656
|
+
? JSON.stringify(state, null, 2)
|
|
657
|
+
: formatHandoffStatus(state));
|
|
658
|
+
});
|
|
659
|
+
const route = program
|
|
660
|
+
.command("route")
|
|
661
|
+
.description("Recommend the right model tier and agent for a task — plan, implement, review, test, debug, or document");
|
|
662
|
+
route
|
|
663
|
+
.argument("[description...]", "natural-language task description")
|
|
664
|
+
.option("--phase <phase>", "explicit phase instead of auto-classify")
|
|
665
|
+
.option("--conserve", "recommend cheaper tiers to stretch remaining session quota")
|
|
666
|
+
.option("--cost", "show cost comparison table across all phases")
|
|
667
|
+
.option("--models", "list the full model catalog Loadout knows about")
|
|
668
|
+
.option("--provider <name>", "filter models by provider (anthropic, openai, google, deepseek, meta)")
|
|
669
|
+
.option("--tier <tier>", "filter models by tier (frontier, standard, fast)")
|
|
670
|
+
.option("--json", "emit machine-readable JSON")
|
|
671
|
+
.action(async (descriptionWords, options) => {
|
|
672
|
+
if (options.models) {
|
|
673
|
+
console.log(options.json
|
|
674
|
+
? JSON.stringify((await import("../core/route.js")).MODEL_CATALOG, null, 2)
|
|
675
|
+
: formatModelCatalog({
|
|
676
|
+
provider: options.provider,
|
|
677
|
+
tier: options.tier,
|
|
678
|
+
}));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (options.cost) {
|
|
682
|
+
console.log(options.json
|
|
683
|
+
? JSON.stringify((await import("../core/route.js")).estimateCostSavings(), null, 2)
|
|
684
|
+
: formatCostTable());
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const description = descriptionWords.join(" ").trim();
|
|
688
|
+
if (!description && !options.phase) {
|
|
689
|
+
console.log(options.json
|
|
690
|
+
? JSON.stringify(allPhaseRoutes(options.conserve), null, 2)
|
|
691
|
+
: formatRoutingTable(options.conserve));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const rec = options.phase
|
|
695
|
+
? routePhase(options.phase, options.conserve)
|
|
696
|
+
: routeTask(description, options.conserve);
|
|
697
|
+
console.log(options.json
|
|
698
|
+
? JSON.stringify(rec, null, 2)
|
|
699
|
+
: formatRouteRecommendation(rec));
|
|
700
|
+
});
|
|
701
|
+
program
|
|
702
|
+
.command("completion")
|
|
703
|
+
.description("Print a shell-completion script; redirect it to your shell profile")
|
|
704
|
+
.argument("<shell>", "bash, zsh, fish, or powershell")
|
|
705
|
+
.action((shell) => {
|
|
706
|
+
process.stdout.write(renderShellCompletion(parseCompletionShell(shell)));
|
|
707
|
+
});
|
|
708
|
+
}
|