loadout-ai 0.1.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/LICENSE +21 -0
- package/MASTER_PLAN.md +1711 -0
- package/README.md +453 -0
- package/SECURITY.md +18 -0
- package/SIMPLE_PLAN.md +44 -0
- package/catalog/discovered.json +10469 -0
- package/catalog/packages.json +1150 -0
- package/dashboard/app.js +607 -0
- package/dashboard/index.html +249 -0
- package/dashboard/styles.css +384 -0
- package/dist/src/cli.js +2735 -0
- package/dist/src/core/active-policy.js +233 -0
- package/dist/src/core/active-set.js +382 -0
- package/dist/src/core/adapters.js +254 -0
- package/dist/src/core/adopt.js +92 -0
- package/dist/src/core/agent-health-score.js +475 -0
- package/dist/src/core/agent-inspection.js +152 -0
- package/dist/src/core/agent-versions.js +131 -0
- package/dist/src/core/api.js +90 -0
- package/dist/src/core/atomic-file.js +22 -0
- package/dist/src/core/audit.js +174 -0
- package/dist/src/core/benchmark-campaign.js +496 -0
- package/dist/src/core/benchmark-evidence.js +458 -0
- package/dist/src/core/benchmark-fixtures.js +1157 -0
- package/dist/src/core/benchmark-runner.js +301 -0
- package/dist/src/core/benchmark-trust.js +795 -0
- package/dist/src/core/canary.js +77 -0
- package/dist/src/core/candidate-intelligence.js +661 -0
- package/dist/src/core/catalog-coverage.js +85 -0
- package/dist/src/core/catalog-install.js +186 -0
- package/dist/src/core/catalog-release.js +286 -0
- package/dist/src/core/catalog.js +327 -0
- package/dist/src/core/codex-mcp.js +122 -0
- package/dist/src/core/community.js +86 -0
- package/dist/src/core/compatibility-intelligence.js +465 -0
- package/dist/src/core/completion.js +160 -0
- package/dist/src/core/components.js +138 -0
- package/dist/src/core/conversion.js +56 -0
- package/dist/src/core/credentials.js +377 -0
- package/dist/src/core/demo.js +136 -0
- package/dist/src/core/diff.js +83 -0
- package/dist/src/core/discovery-connector.js +165 -0
- package/dist/src/core/doctor.js +95 -0
- package/dist/src/core/ecosystem-import.js +1072 -0
- package/dist/src/core/evaluate.js +72 -0
- package/dist/src/core/file-lock.js +140 -0
- package/dist/src/core/freshness-alerts.js +230 -0
- package/dist/src/core/github-discovery.js +97 -0
- package/dist/src/core/github.js +148 -0
- package/dist/src/core/head-to-head.js +437 -0
- package/dist/src/core/health-score-evidence.js +169 -0
- package/dist/src/core/health.js +129 -0
- package/dist/src/core/improve.js +223 -0
- package/dist/src/core/install.js +233 -0
- package/dist/src/core/intelligence-feed-build.js +59 -0
- package/dist/src/core/intelligence-feed.js +353 -0
- package/dist/src/core/loadout-badge.js +55 -0
- package/dist/src/core/loadout-card.js +87 -0
- package/dist/src/core/manifest.js +307 -0
- package/dist/src/core/mcp-recipes.js +419 -0
- package/dist/src/core/mcp-registry-discovery.js +455 -0
- package/dist/src/core/mcp.js +287 -0
- package/dist/src/core/model-config.js +102 -0
- package/dist/src/core/observations.js +87 -0
- package/dist/src/core/outcomes.js +164 -0
- package/dist/src/core/package.js +225 -0
- package/dist/src/core/paths.js +214 -0
- package/dist/src/core/portable.js +130 -0
- package/dist/src/core/private-discovery.js +45 -0
- package/dist/src/core/profiles.js +255 -0
- package/dist/src/core/provenance.js +278 -0
- package/dist/src/core/ranking.js +104 -0
- package/dist/src/core/recommend.js +181 -0
- package/dist/src/core/registry-api.js +97 -0
- package/dist/src/core/registry.js +316 -0
- package/dist/src/core/release-claims.js +175 -0
- package/dist/src/core/remove.js +122 -0
- package/dist/src/core/review-queue.js +200 -0
- package/dist/src/core/runtime-tool-recipe.js +315 -0
- package/dist/src/core/runtime-tools.js +453 -0
- package/dist/src/core/safety.js +234 -0
- package/dist/src/core/sandbox.js +78 -0
- package/dist/src/core/scheduler.js +264 -0
- package/dist/src/core/share-report.js +85 -0
- package/dist/src/core/signing.js +115 -0
- package/dist/src/core/skill-compare.js +239 -0
- package/dist/src/core/skill-inventory.js +193 -0
- package/dist/src/core/skill-security.js +340 -0
- package/dist/src/core/skills-sh-discovery.js +336 -0
- package/dist/src/core/skills.js +208 -0
- package/dist/src/core/snapshot.js +210 -0
- package/dist/src/core/source.js +305 -0
- package/dist/src/core/state.js +300 -0
- package/dist/src/core/sync.js +348 -0
- package/dist/src/core/terminal.js +53 -0
- package/dist/src/core/transaction.js +211 -0
- package/dist/src/core/update-watch.js +39 -0
- package/dist/src/core/update.js +216 -0
- package/dist/src/core/upgrade.js +207 -0
- package/dist/src/dashboard.js +418 -0
- package/dist/src/shared/schemas.js +336 -0
- package/dist/src/shared/types.js +1 -0
- package/docs/ACTIVE_SET.md +53 -0
- package/docs/ACTIVE_SET_POLICY.md +45 -0
- package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
- package/docs/CATALOG.md +78 -0
- package/docs/CATALOG_POLICY.md +41 -0
- package/docs/COMMUNITY_DISCOVERY.md +14 -0
- package/docs/COMPATIBILITY_POLICY.md +22 -0
- package/docs/CONVERSION_AND_SANDBOX.md +27 -0
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
- package/docs/DISCOVERED.md +280 -0
- package/docs/EVALUATION_PROTOCOL.md +40 -0
- package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
- package/docs/FEATURE_TEST_MATRIX.md +802 -0
- package/docs/GITHUB_AUTHORIZATION.md +38 -0
- package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
- package/docs/PROVIDER_CONFIGURATION.md +45 -0
- package/docs/RELEASE_REVIEW.md +117 -0
- package/docs/SAFE_UPDATE_DEMO.md +25 -0
- package/docs/SCHEMA_DECISIONS.md +25 -0
- package/docs/TEAM_POLICY.md +18 -0
- package/docs/TESTING.md +190 -0
- package/package.json +82 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { agentIdSchema, componentTypeSchema } from "../shared/schemas.js";
|
|
3
|
+
import { signPayload, verifyEnvelope } from "./signing.js";
|
|
4
|
+
const MAX_COMPATIBILITY_BYTES = 1024 * 1024;
|
|
5
|
+
const identifier = z
|
|
6
|
+
.string()
|
|
7
|
+
.trim()
|
|
8
|
+
.min(1)
|
|
9
|
+
.max(160)
|
|
10
|
+
.regex(/^[a-z0-9][a-z0-9._/-]*$/i, "must be a bounded identifier");
|
|
11
|
+
const safeRelativePath = z
|
|
12
|
+
.string()
|
|
13
|
+
.trim()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(500)
|
|
16
|
+
.refine((value) => !value.startsWith("/") &&
|
|
17
|
+
!value.startsWith("\\") &&
|
|
18
|
+
!/^[A-Za-z]:[\\/]/.test(value) &&
|
|
19
|
+
!value.split(/[\\/]/).includes("..") &&
|
|
20
|
+
!Array.from(value).some((character) => {
|
|
21
|
+
const code = character.charCodeAt(0);
|
|
22
|
+
return code < 32 || code === 127;
|
|
23
|
+
}), "must be a safe agent-relative path");
|
|
24
|
+
const semanticVersion = z
|
|
25
|
+
.string()
|
|
26
|
+
.regex(/^\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/, "must be a bounded semantic version");
|
|
27
|
+
const boundedText = z.string().trim().min(1).max(500);
|
|
28
|
+
const versionRangeSchema = z
|
|
29
|
+
.strictObject({
|
|
30
|
+
minInclusive: semanticVersion.optional(),
|
|
31
|
+
maxExclusive: semanticVersion.optional(),
|
|
32
|
+
includePrerelease: z.boolean().default(false),
|
|
33
|
+
})
|
|
34
|
+
.refine((range) => range.minInclusive !== undefined || range.maxExclusive !== undefined, "version range must declare at least one boundary");
|
|
35
|
+
const affectedSelectorSchema = z.strictObject({
|
|
36
|
+
packageIds: z.array(identifier).max(100).default([]),
|
|
37
|
+
componentTypes: z.array(componentTypeSchema).max(7).default([]),
|
|
38
|
+
pathPrefixes: z.array(safeRelativePath).max(50).default([]),
|
|
39
|
+
recipeIds: z.array(identifier).max(100).default([]),
|
|
40
|
+
providerIds: z.array(identifier).max(50).default([]),
|
|
41
|
+
modelIds: z.array(z.string().trim().min(1).max(200)).max(100).default([]),
|
|
42
|
+
});
|
|
43
|
+
const migrationSchema = z
|
|
44
|
+
.strictObject({
|
|
45
|
+
kind: z.enum(["move", "copy", "rewrite-config", "manual"]),
|
|
46
|
+
fromPath: safeRelativePath.optional(),
|
|
47
|
+
toPath: safeRelativePath.optional(),
|
|
48
|
+
instructions: z.array(boundedText).min(1).max(20),
|
|
49
|
+
automaticEligible: z.boolean().default(false),
|
|
50
|
+
})
|
|
51
|
+
.superRefine((migration, context) => {
|
|
52
|
+
if (migration.kind !== "manual" &&
|
|
53
|
+
(!migration.fromPath || !migration.toPath))
|
|
54
|
+
context.addIssue({
|
|
55
|
+
code: "custom",
|
|
56
|
+
message: "non-manual migrations require fromPath and toPath",
|
|
57
|
+
});
|
|
58
|
+
if (migration.fromPath && migration.fromPath === migration.toPath)
|
|
59
|
+
context.addIssue({
|
|
60
|
+
code: "custom",
|
|
61
|
+
message: "migration source and destination must differ",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
export const compatibilityNoticeSchema = z
|
|
65
|
+
.strictObject({
|
|
66
|
+
schemaVersion: z.literal(1),
|
|
67
|
+
id: identifier,
|
|
68
|
+
agent: agentIdSchema,
|
|
69
|
+
kind: z.enum([
|
|
70
|
+
"path-change",
|
|
71
|
+
"config-format",
|
|
72
|
+
"deprecation",
|
|
73
|
+
"model-provider",
|
|
74
|
+
"recipe-breakage",
|
|
75
|
+
]),
|
|
76
|
+
severity: z.enum(["information", "warning", "breaking"]),
|
|
77
|
+
summary: boundedText,
|
|
78
|
+
issuedAt: z.iso.datetime({ offset: true }),
|
|
79
|
+
expiresAt: z.iso.datetime({ offset: true }),
|
|
80
|
+
versionRange: versionRangeSchema,
|
|
81
|
+
platforms: z
|
|
82
|
+
.array(z.enum(["windows", "macos", "linux"]))
|
|
83
|
+
.min(1)
|
|
84
|
+
.max(3),
|
|
85
|
+
affected: affectedSelectorSchema,
|
|
86
|
+
evidence: z.strictObject({
|
|
87
|
+
sourceUrl: z
|
|
88
|
+
.url()
|
|
89
|
+
.refine((value) => new URL(value).protocol === "https:", "must use HTTPS"),
|
|
90
|
+
observedAt: z.iso.datetime({ offset: true }),
|
|
91
|
+
confidence: z.enum(["verified", "likely", "uncertain"]),
|
|
92
|
+
}),
|
|
93
|
+
migration: migrationSchema.optional(),
|
|
94
|
+
uncertainty: boundedText,
|
|
95
|
+
})
|
|
96
|
+
.superRefine((notice, context) => {
|
|
97
|
+
if (Date.parse(notice.expiresAt) <= Date.parse(notice.issuedAt))
|
|
98
|
+
context.addIssue({
|
|
99
|
+
code: "custom",
|
|
100
|
+
path: ["expiresAt"],
|
|
101
|
+
message: "must be later than issuedAt",
|
|
102
|
+
});
|
|
103
|
+
if (notice.kind === "path-change" && !notice.migration)
|
|
104
|
+
context.addIssue({
|
|
105
|
+
code: "custom",
|
|
106
|
+
path: ["migration"],
|
|
107
|
+
message: "path changes require a migration preview definition",
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
export const compatibilityNoticeSetSchema = z
|
|
111
|
+
.strictObject({
|
|
112
|
+
schemaVersion: z.literal(1),
|
|
113
|
+
generatedAt: z.iso.datetime({ offset: true }),
|
|
114
|
+
expiresAt: z.iso.datetime({ offset: true }),
|
|
115
|
+
notices: z.array(compatibilityNoticeSchema).max(500),
|
|
116
|
+
})
|
|
117
|
+
.superRefine((feed, context) => {
|
|
118
|
+
if (Date.parse(feed.expiresAt) <= Date.parse(feed.generatedAt))
|
|
119
|
+
context.addIssue({
|
|
120
|
+
code: "custom",
|
|
121
|
+
path: ["expiresAt"],
|
|
122
|
+
message: "must be later than generatedAt",
|
|
123
|
+
});
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
feed.notices.forEach((notice, index) => {
|
|
126
|
+
if (seen.has(notice.id))
|
|
127
|
+
context.addIssue({
|
|
128
|
+
code: "custom",
|
|
129
|
+
path: ["notices", index, "id"],
|
|
130
|
+
message: "notice ids must be unique",
|
|
131
|
+
});
|
|
132
|
+
seen.add(notice.id);
|
|
133
|
+
if (Date.parse(notice.issuedAt) > Date.parse(feed.generatedAt))
|
|
134
|
+
context.addIssue({
|
|
135
|
+
code: "custom",
|
|
136
|
+
path: ["notices", index, "issuedAt"],
|
|
137
|
+
message: "notice cannot be issued after feed generation",
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
export function parseCompatibilityNoticeSet(value) {
|
|
142
|
+
const bytes = Buffer.byteLength(JSON.stringify(value));
|
|
143
|
+
if (bytes > MAX_COMPATIBILITY_BYTES)
|
|
144
|
+
throw new Error("Compatibility notice set exceeds the 1 MiB limit");
|
|
145
|
+
return compatibilityNoticeSetSchema.parse(value);
|
|
146
|
+
}
|
|
147
|
+
const signedEnvelopeSchema = z.strictObject({
|
|
148
|
+
schemaVersion: z.literal(1),
|
|
149
|
+
algorithm: z.literal("Ed25519"),
|
|
150
|
+
createdAt: z.iso.datetime({ offset: true }),
|
|
151
|
+
publicKeyFingerprint: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
152
|
+
payload: z.unknown(),
|
|
153
|
+
signature: z.string().min(1).max(1024),
|
|
154
|
+
});
|
|
155
|
+
export function signCompatibilityNoticeSet(value, privateKeyPem, createdAt) {
|
|
156
|
+
const payload = parseCompatibilityNoticeSet(value);
|
|
157
|
+
return signPayload(payload, privateKeyPem, createdAt);
|
|
158
|
+
}
|
|
159
|
+
export function verifySignedCompatibilityNoticeSet(value, publicKeyPem) {
|
|
160
|
+
const bounded = Buffer.byteLength(JSON.stringify(value));
|
|
161
|
+
if (bounded > MAX_COMPATIBILITY_BYTES)
|
|
162
|
+
throw new Error("Signed compatibility notice set exceeds the 1 MiB limit");
|
|
163
|
+
const raw = signedEnvelopeSchema.parse(value);
|
|
164
|
+
const envelope = raw;
|
|
165
|
+
const verification = verifyEnvelope(envelope, publicKeyPem);
|
|
166
|
+
if (!verification.valid)
|
|
167
|
+
throw new Error("Compatibility notice signature is invalid");
|
|
168
|
+
return {
|
|
169
|
+
feed: parseCompatibilityNoticeSet(envelope.payload),
|
|
170
|
+
fingerprint: verification.fingerprint,
|
|
171
|
+
signedAt: envelope.createdAt,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function parsedVersion(value) {
|
|
175
|
+
const matched = value.match(/^(\d+)\.(\d+)(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?$/);
|
|
176
|
+
if (!matched)
|
|
177
|
+
return undefined;
|
|
178
|
+
return {
|
|
179
|
+
numeric: [Number(matched[1]), Number(matched[2]), Number(matched[3] ?? 0)],
|
|
180
|
+
...(matched[4] ? { prerelease: matched[4] } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function compareVersions(left, right) {
|
|
184
|
+
const a = parsedVersion(left);
|
|
185
|
+
const b = parsedVersion(right);
|
|
186
|
+
if (!a || !b)
|
|
187
|
+
return undefined;
|
|
188
|
+
for (let index = 0; index < 3; index++) {
|
|
189
|
+
if (a.numeric[index] !== b.numeric[index])
|
|
190
|
+
return a.numeric[index] < b.numeric[index] ? -1 : 1;
|
|
191
|
+
}
|
|
192
|
+
if (a.prerelease === b.prerelease)
|
|
193
|
+
return 0;
|
|
194
|
+
if (!a.prerelease)
|
|
195
|
+
return 1;
|
|
196
|
+
if (!b.prerelease)
|
|
197
|
+
return -1;
|
|
198
|
+
return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true });
|
|
199
|
+
}
|
|
200
|
+
function versionApplicability(evidence, notice) {
|
|
201
|
+
if (!evidence || evidence.status !== "detected" || !evidence.version)
|
|
202
|
+
return {
|
|
203
|
+
applicability: "potential",
|
|
204
|
+
reason: "The installed version is unavailable, so this notice cannot be excluded safely.",
|
|
205
|
+
};
|
|
206
|
+
const current = evidence.version;
|
|
207
|
+
const range = notice.versionRange;
|
|
208
|
+
const belowMinimum = range.minInclusive
|
|
209
|
+
? compareVersions(current, range.minInclusive)
|
|
210
|
+
: 0;
|
|
211
|
+
const atOrAboveMaximum = range.maxExclusive
|
|
212
|
+
? compareVersions(current, range.maxExclusive)
|
|
213
|
+
: -1;
|
|
214
|
+
if (belowMinimum === undefined || atOrAboveMaximum === undefined)
|
|
215
|
+
return {
|
|
216
|
+
applicability: "potential",
|
|
217
|
+
reason: "The detected version could not be compared to every signed range boundary.",
|
|
218
|
+
};
|
|
219
|
+
if (belowMinimum < 0 || atOrAboveMaximum >= 0)
|
|
220
|
+
return {
|
|
221
|
+
applicability: "not-applicable",
|
|
222
|
+
reason: `Detected version ${current} is outside the signed notice range.`,
|
|
223
|
+
};
|
|
224
|
+
if (evidence.releaseChannel === "prerelease" && !range.includePrerelease)
|
|
225
|
+
return {
|
|
226
|
+
applicability: "potential",
|
|
227
|
+
reason: `Detected version ${current} is a prerelease not explicitly covered by this notice.`,
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
applicability: "applies",
|
|
231
|
+
reason: `Detected version ${current} is inside the signed notice range.`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function platformName(platform) {
|
|
235
|
+
if (platform === "win32")
|
|
236
|
+
return "windows";
|
|
237
|
+
if (platform === "darwin")
|
|
238
|
+
return "macos";
|
|
239
|
+
if (platform === "linux")
|
|
240
|
+
return "linux";
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
function normalizedPath(value) {
|
|
244
|
+
return value.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
245
|
+
}
|
|
246
|
+
function pathMatches(path, relativePrefix) {
|
|
247
|
+
const normalized = normalizedPath(path).toLowerCase();
|
|
248
|
+
const prefix = normalizedPath(relativePrefix)
|
|
249
|
+
.replace(/^\.\//, "")
|
|
250
|
+
.toLowerCase();
|
|
251
|
+
return (normalized === prefix ||
|
|
252
|
+
normalized.endsWith(`/${prefix}`) ||
|
|
253
|
+
normalized.includes(`/${prefix}/`));
|
|
254
|
+
}
|
|
255
|
+
function selectedPaths(paths, notice) {
|
|
256
|
+
if (notice.affected.pathPrefixes.length === 0)
|
|
257
|
+
return [...new Set(paths)].sort();
|
|
258
|
+
return [
|
|
259
|
+
...new Set(paths.filter((path) => notice.affected.pathPrefixes.some((prefix) => pathMatches(path, prefix)))),
|
|
260
|
+
].sort();
|
|
261
|
+
}
|
|
262
|
+
function packageSelected(packageId, notice) {
|
|
263
|
+
const ids = new Set([
|
|
264
|
+
...notice.affected.packageIds,
|
|
265
|
+
...notice.affected.recipeIds,
|
|
266
|
+
]);
|
|
267
|
+
return ids.size === 0 || ids.has(packageId);
|
|
268
|
+
}
|
|
269
|
+
export function affectedManagedContent(notice, state) {
|
|
270
|
+
const results = [];
|
|
271
|
+
const componentTypes = new Set(notice.affected.componentTypes);
|
|
272
|
+
for (const install of state.installs) {
|
|
273
|
+
if (!install.targetAgents.includes(notice.agent) ||
|
|
274
|
+
!packageSelected(install.packageId, notice) ||
|
|
275
|
+
(componentTypes.size > 0 && !componentTypes.has("skill")))
|
|
276
|
+
continue;
|
|
277
|
+
const paths = selectedPaths(install.files.map((file) => file.path), notice);
|
|
278
|
+
if (paths.length || notice.affected.pathPrefixes.length === 0)
|
|
279
|
+
results.push({
|
|
280
|
+
noticeId: notice.id,
|
|
281
|
+
packageId: install.packageId,
|
|
282
|
+
agent: notice.agent,
|
|
283
|
+
source: "install",
|
|
284
|
+
componentType: "skill",
|
|
285
|
+
paths,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
for (const activation of state.activations ?? []) {
|
|
289
|
+
if (activation.agent !== notice.agent ||
|
|
290
|
+
!packageSelected(activation.packageId, notice) ||
|
|
291
|
+
(componentTypes.size > 0 && !componentTypes.has("skill")))
|
|
292
|
+
continue;
|
|
293
|
+
const paths = selectedPaths(activation.targets.map((target) => target.activePath), notice);
|
|
294
|
+
if (paths.length || notice.affected.pathPrefixes.length === 0)
|
|
295
|
+
results.push({
|
|
296
|
+
noticeId: notice.id,
|
|
297
|
+
packageId: activation.packageId,
|
|
298
|
+
agent: notice.agent,
|
|
299
|
+
source: "activation",
|
|
300
|
+
componentType: "skill",
|
|
301
|
+
paths,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
for (const mcp of state.mcpInstalls ?? []) {
|
|
305
|
+
if (!packageSelected(mcp.packageId, notice) ||
|
|
306
|
+
(componentTypes.size > 0 && !componentTypes.has("mcp")))
|
|
307
|
+
continue;
|
|
308
|
+
const paths = selectedPaths([mcp.configPath], notice);
|
|
309
|
+
if (paths.length || notice.affected.pathPrefixes.length === 0)
|
|
310
|
+
results.push({
|
|
311
|
+
noticeId: notice.id,
|
|
312
|
+
packageId: mcp.packageId,
|
|
313
|
+
agent: notice.agent,
|
|
314
|
+
source: "mcp",
|
|
315
|
+
componentType: "mcp",
|
|
316
|
+
paths,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return results.sort((left, right) => left.packageId.localeCompare(right.packageId) ||
|
|
320
|
+
left.source.localeCompare(right.source));
|
|
321
|
+
}
|
|
322
|
+
function migratedPath(path, fromPath, toPath) {
|
|
323
|
+
const normalized = normalizedPath(path);
|
|
324
|
+
const from = normalizedPath(fromPath).replace(/^\.\//, "");
|
|
325
|
+
const lower = normalized.toLowerCase();
|
|
326
|
+
const target = from.toLowerCase();
|
|
327
|
+
const marker = `/${target}`;
|
|
328
|
+
const index = lower.lastIndexOf(marker);
|
|
329
|
+
if (index < 0 && lower !== target)
|
|
330
|
+
return undefined;
|
|
331
|
+
const start = index < 0 ? 0 : index + 1;
|
|
332
|
+
return `${normalized.slice(0, start)}${toPath}${normalized.slice(start + from.length)}`;
|
|
333
|
+
}
|
|
334
|
+
export function previewCompatibilityMigration(notice, affected) {
|
|
335
|
+
const migration = notice.migration;
|
|
336
|
+
if (!migration)
|
|
337
|
+
return [];
|
|
338
|
+
const steps = [];
|
|
339
|
+
for (const content of affected) {
|
|
340
|
+
if (migration.kind === "manual") {
|
|
341
|
+
steps.push({
|
|
342
|
+
noticeId: notice.id,
|
|
343
|
+
packageId: content.packageId,
|
|
344
|
+
agent: content.agent,
|
|
345
|
+
kind: migration.kind,
|
|
346
|
+
instructions: migration.instructions,
|
|
347
|
+
automaticEligible: false,
|
|
348
|
+
requiresApproval: true,
|
|
349
|
+
});
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
for (const sourcePath of content.paths) {
|
|
353
|
+
const targetPath = migratedPath(sourcePath, migration.fromPath, migration.toPath);
|
|
354
|
+
if (!targetPath)
|
|
355
|
+
continue;
|
|
356
|
+
steps.push({
|
|
357
|
+
noticeId: notice.id,
|
|
358
|
+
packageId: content.packageId,
|
|
359
|
+
agent: content.agent,
|
|
360
|
+
kind: migration.kind,
|
|
361
|
+
sourcePath,
|
|
362
|
+
targetPath,
|
|
363
|
+
instructions: migration.instructions,
|
|
364
|
+
automaticEligible: migration.automaticEligible,
|
|
365
|
+
requiresApproval: true,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return steps;
|
|
370
|
+
}
|
|
371
|
+
export function compatibilityFreshness(feed, sourceStatus, now = new Date()) {
|
|
372
|
+
if (sourceStatus === "invalid")
|
|
373
|
+
return {
|
|
374
|
+
status: "invalid",
|
|
375
|
+
sourceStatus,
|
|
376
|
+
message: "Compatibility input failed signature or schema verification; no notice is trusted.",
|
|
377
|
+
};
|
|
378
|
+
if (!feed || sourceStatus === "missing")
|
|
379
|
+
return {
|
|
380
|
+
status: "unavailable",
|
|
381
|
+
sourceStatus: "missing",
|
|
382
|
+
message: "No verified compatibility notice set is available; compatibility is unknown.",
|
|
383
|
+
};
|
|
384
|
+
const ageHours = Math.max(0, (now.getTime() - Date.parse(feed.generatedAt)) / (60 * 60 * 1000));
|
|
385
|
+
if (Date.parse(feed.generatedAt) > now.getTime() + 5 * 60 * 1000)
|
|
386
|
+
return {
|
|
387
|
+
status: "invalid",
|
|
388
|
+
sourceStatus,
|
|
389
|
+
generatedAt: feed.generatedAt,
|
|
390
|
+
expiresAt: feed.expiresAt,
|
|
391
|
+
message: "Compatibility notice generation time is too far in the future for the local clock.",
|
|
392
|
+
};
|
|
393
|
+
const stale = now.getTime() > Date.parse(feed.expiresAt);
|
|
394
|
+
return {
|
|
395
|
+
status: stale ? "stale" : "fresh",
|
|
396
|
+
sourceStatus,
|
|
397
|
+
generatedAt: feed.generatedAt,
|
|
398
|
+
expiresAt: feed.expiresAt,
|
|
399
|
+
ageHours: Number(ageHours.toFixed(2)),
|
|
400
|
+
message: stale
|
|
401
|
+
? "The last verified compatibility notice set is expired; findings are historical and treated as uncertain."
|
|
402
|
+
: sourceStatus === "offline-cache"
|
|
403
|
+
? "A fresh previously verified notice set is available offline; events after its generation time may be missing."
|
|
404
|
+
: "Compatibility notices are verified and within their declared freshness window.",
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
export function buildCompatibilityIntelligence(options) {
|
|
408
|
+
const now = options.now ?? new Date();
|
|
409
|
+
const freshness = compatibilityFreshness(options.feed, options.sourceStatus, now);
|
|
410
|
+
const platform = platformName(options.platform ?? process.platform);
|
|
411
|
+
const versionByAgent = new Map(options.versions.map((evidence) => [evidence.agent, evidence]));
|
|
412
|
+
const assessments = (options.feed?.notices ?? []).map((notice) => {
|
|
413
|
+
let result = versionApplicability(versionByAgent.get(notice.agent), notice);
|
|
414
|
+
if (!platform || !notice.platforms.includes(platform))
|
|
415
|
+
result = {
|
|
416
|
+
applicability: "not-applicable",
|
|
417
|
+
reason: platform
|
|
418
|
+
? `Notice does not target ${platform}.`
|
|
419
|
+
: "This operating system has no declared compatibility mapping.",
|
|
420
|
+
};
|
|
421
|
+
if (result.applicability === "applies" &&
|
|
422
|
+
(freshness.status !== "fresh" ||
|
|
423
|
+
now.getTime() > Date.parse(notice.expiresAt)))
|
|
424
|
+
result = {
|
|
425
|
+
applicability: "potential",
|
|
426
|
+
reason: `${result.reason} ${now.getTime() > Date.parse(notice.expiresAt)
|
|
427
|
+
? "The individual notice has expired"
|
|
428
|
+
: `The verified notice set is ${freshness.status}`}, so current applicability is uncertain.`,
|
|
429
|
+
};
|
|
430
|
+
const affected = result.applicability === "not-applicable"
|
|
431
|
+
? []
|
|
432
|
+
: affectedManagedContent(notice, options.state);
|
|
433
|
+
return {
|
|
434
|
+
notice,
|
|
435
|
+
...result,
|
|
436
|
+
affectedManagedContent: affected,
|
|
437
|
+
migrationPreview: previewCompatibilityMigration(notice, affected),
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
const affected = assessments.flatMap((assessment) => assessment.affectedManagedContent);
|
|
441
|
+
const migrationPreview = assessments.flatMap((assessment) => assessment.migrationPreview);
|
|
442
|
+
const uncertainty = [
|
|
443
|
+
freshness.message,
|
|
444
|
+
...assessments
|
|
445
|
+
.filter((assessment) => assessment.applicability === "potential")
|
|
446
|
+
.map((assessment) => `${assessment.notice.id}: ${assessment.reason}`),
|
|
447
|
+
...assessments.map((assessment) => `${assessment.notice.id}: ${assessment.notice.uncertainty}`),
|
|
448
|
+
...(options.versions.some((version) => version.releaseChannel === "prerelease")
|
|
449
|
+
? [
|
|
450
|
+
"At least one detected agent is a prerelease; absence of a notice is not compatibility evidence.",
|
|
451
|
+
]
|
|
452
|
+
: []),
|
|
453
|
+
];
|
|
454
|
+
return {
|
|
455
|
+
schemaVersion: 1,
|
|
456
|
+
generatedAt: now.toISOString(),
|
|
457
|
+
versions: options.versions,
|
|
458
|
+
freshness,
|
|
459
|
+
assessments,
|
|
460
|
+
affectedManagedContent: affected,
|
|
461
|
+
migrationPreview,
|
|
462
|
+
uncertainty: [...new Set(uncertainty)],
|
|
463
|
+
mutationPerformed: false,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const commands = [
|
|
2
|
+
"setup",
|
|
3
|
+
"upgrade",
|
|
4
|
+
"init",
|
|
5
|
+
"lock",
|
|
6
|
+
"export",
|
|
7
|
+
"import",
|
|
8
|
+
"audit",
|
|
9
|
+
"create",
|
|
10
|
+
"pack",
|
|
11
|
+
"publish",
|
|
12
|
+
"registry-serve",
|
|
13
|
+
"search",
|
|
14
|
+
"add",
|
|
15
|
+
"unadd",
|
|
16
|
+
"list",
|
|
17
|
+
"demo",
|
|
18
|
+
"test-drive",
|
|
19
|
+
"scan",
|
|
20
|
+
"status",
|
|
21
|
+
"versions",
|
|
22
|
+
"intelligence",
|
|
23
|
+
"compatibility",
|
|
24
|
+
"skill-audit",
|
|
25
|
+
"interop",
|
|
26
|
+
"benchmark",
|
|
27
|
+
"doctor",
|
|
28
|
+
"health",
|
|
29
|
+
"compare",
|
|
30
|
+
"optimize",
|
|
31
|
+
"activate",
|
|
32
|
+
"enable",
|
|
33
|
+
"disable",
|
|
34
|
+
"adopt",
|
|
35
|
+
"library",
|
|
36
|
+
"update",
|
|
37
|
+
"rollback",
|
|
38
|
+
"discover",
|
|
39
|
+
"candidate",
|
|
40
|
+
"review-queue",
|
|
41
|
+
"review",
|
|
42
|
+
"alerts",
|
|
43
|
+
"alert-ignore",
|
|
44
|
+
"alert-pin",
|
|
45
|
+
"alert-unpin",
|
|
46
|
+
"alert-pins",
|
|
47
|
+
"mcp",
|
|
48
|
+
"mcp-recipe",
|
|
49
|
+
"mcp-config",
|
|
50
|
+
"models",
|
|
51
|
+
"credentials",
|
|
52
|
+
"schedule",
|
|
53
|
+
"unschedule",
|
|
54
|
+
"autopilot",
|
|
55
|
+
"tool",
|
|
56
|
+
"report",
|
|
57
|
+
"share",
|
|
58
|
+
"card",
|
|
59
|
+
"compare-loadouts",
|
|
60
|
+
"badge",
|
|
61
|
+
"claims",
|
|
62
|
+
"head-to-head",
|
|
63
|
+
"keygen",
|
|
64
|
+
"catalog-sign",
|
|
65
|
+
"catalog-verify",
|
|
66
|
+
"catalog-update",
|
|
67
|
+
"inspect",
|
|
68
|
+
"evaluate",
|
|
69
|
+
"watch",
|
|
70
|
+
"sandbox-run",
|
|
71
|
+
"codex-mcp-config",
|
|
72
|
+
"plan",
|
|
73
|
+
"install",
|
|
74
|
+
"convert",
|
|
75
|
+
"canary",
|
|
76
|
+
"capabilities",
|
|
77
|
+
"catalog",
|
|
78
|
+
"profiles",
|
|
79
|
+
"recommend",
|
|
80
|
+
"improve",
|
|
81
|
+
"improve-feedback",
|
|
82
|
+
"sync",
|
|
83
|
+
"outcome",
|
|
84
|
+
"outcomes",
|
|
85
|
+
"remove",
|
|
86
|
+
"dashboard",
|
|
87
|
+
"serve",
|
|
88
|
+
"completion",
|
|
89
|
+
];
|
|
90
|
+
const modelCommands = ["status", "set", "verify"];
|
|
91
|
+
const credentialCommands = ["status", "set", "check", "delete"];
|
|
92
|
+
const candidateCommands = ["list", "inspect", "propose"];
|
|
93
|
+
const benchmarkCommands = ["plan"];
|
|
94
|
+
export function renderShellCompletion(shell) {
|
|
95
|
+
const words = commands.join(" ");
|
|
96
|
+
switch (shell) {
|
|
97
|
+
case "bash":
|
|
98
|
+
return `# Loadout command completion
|
|
99
|
+
_loadout() {
|
|
100
|
+
local current="\${COMP_WORDS[COMP_CWORD]}"
|
|
101
|
+
local commands="${words}"
|
|
102
|
+
if [[ COMP_CWORD -eq 1 ]]; then
|
|
103
|
+
COMPREPLY=( $(compgen -W "$commands" -- "$current") )
|
|
104
|
+
elif [[ COMP_CWORD -eq 2 && "\${COMP_WORDS[1]}" == "models" ]]; then
|
|
105
|
+
COMPREPLY=( $(compgen -W "${modelCommands.join(" ")}" -- "$current") )
|
|
106
|
+
elif [[ COMP_CWORD -eq 2 && "\${COMP_WORDS[1]}" == "credentials" ]]; then
|
|
107
|
+
COMPREPLY=( $(compgen -W "${credentialCommands.join(" ")}" -- "$current") )
|
|
108
|
+
elif [[ COMP_CWORD -eq 2 && "\${COMP_WORDS[1]}" == "candidate" ]]; then
|
|
109
|
+
COMPREPLY=( $(compgen -W "${candidateCommands.join(" ")}" -- "$current") )
|
|
110
|
+
elif [[ COMP_CWORD -eq 2 && "\${COMP_WORDS[1]}" == "benchmark" ]]; then
|
|
111
|
+
COMPREPLY=( $(compgen -W "${benchmarkCommands.join(" ")}" -- "$current") )
|
|
112
|
+
fi
|
|
113
|
+
}
|
|
114
|
+
complete -F _loadout loadout
|
|
115
|
+
`;
|
|
116
|
+
case "zsh":
|
|
117
|
+
return `#compdef loadout
|
|
118
|
+
typeset -a commands model_commands credential_commands candidate_commands benchmark_commands
|
|
119
|
+
commands=(${commands.map((command) => `'${command}'`).join(" ")})
|
|
120
|
+
model_commands=(${modelCommands.map((command) => `'${command}'`).join(" ")})
|
|
121
|
+
credential_commands=(${credentialCommands.map((command) => `'${command}'`).join(" ")})
|
|
122
|
+
candidate_commands=(${candidateCommands.map((command) => `'${command}'`).join(" ")})
|
|
123
|
+
benchmark_commands=(${benchmarkCommands.map((command) => `'${command}'`).join(" ")})
|
|
124
|
+
if (( CURRENT == 2 )); then
|
|
125
|
+
_describe 'command' commands
|
|
126
|
+
elif (( CURRENT == 3 )) && [[ $words[2] == models ]]; then
|
|
127
|
+
_describe 'models command' model_commands
|
|
128
|
+
elif (( CURRENT == 3 )) && [[ $words[2] == credentials ]]; then
|
|
129
|
+
_describe 'credentials command' credential_commands
|
|
130
|
+
elif (( CURRENT == 3 )) && [[ $words[2] == candidate ]]; then
|
|
131
|
+
_describe 'candidate command' candidate_commands
|
|
132
|
+
elif (( CURRENT == 3 )) && [[ $words[2] == benchmark ]]; then
|
|
133
|
+
_describe 'benchmark command' benchmark_commands
|
|
134
|
+
fi
|
|
135
|
+
`;
|
|
136
|
+
case "fish":
|
|
137
|
+
return [
|
|
138
|
+
...commands.map((command) => `complete -c loadout -f -n '__fish_use_subcommand' -a ${command}`),
|
|
139
|
+
...modelCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from models' -a ${command}`),
|
|
140
|
+
...credentialCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from credentials' -a ${command}`),
|
|
141
|
+
...candidateCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from candidate' -a ${command}`),
|
|
142
|
+
...benchmarkCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from benchmark' -a ${command}`),
|
|
143
|
+
]
|
|
144
|
+
.join("\n")
|
|
145
|
+
.concat("\n");
|
|
146
|
+
case "powershell":
|
|
147
|
+
return `Register-ArgumentCompleter -Native -CommandName loadout -ScriptBlock {
|
|
148
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
149
|
+
$elements = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text })
|
|
150
|
+
$candidates = if ($elements.Count -ge 2 -and $elements[1] -eq 'models') { @(${modelCommands.map((command) => `'${command}'`).join(", ")}) } elseif ($elements.Count -ge 2 -and $elements[1] -eq 'credentials') { @(${credentialCommands.map((command) => `'${command}'`).join(", ")}) } elseif ($elements.Count -ge 2 -and $elements[1] -eq 'candidate') { @(${candidateCommands.map((command) => `'${command}'`).join(", ")}) } elseif ($elements.Count -ge 2 -and $elements[1] -eq 'benchmark') { @(${benchmarkCommands.map((command) => `'${command}'`).join(", ")}) } else { @(${commands.map((command) => `'${command}'`).join(", ")}) }
|
|
151
|
+
$candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
|
|
152
|
+
}
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export function parseCompletionShell(value) {
|
|
157
|
+
if (["bash", "zsh", "fish", "powershell"].includes(value))
|
|
158
|
+
return value;
|
|
159
|
+
throw new Error("Supported shells: bash, zsh, fish, powershell");
|
|
160
|
+
}
|