loadout-ai 0.6.0 → 0.8.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 +72 -0
- package/README.md +129 -26
- package/catalog/discovered.json +28469 -24524
- package/dist/src/cli.js +7 -2
- package/dist/src/commands/agents.js +6 -23
- package/dist/src/commands/catalog.js +186 -102
- package/dist/src/commands/health.js +6 -6
- package/dist/src/commands/inventory.js +19 -120
- package/dist/src/commands/lifecycle.js +13 -51
- package/dist/src/commands/mcp.js +10 -73
- package/dist/src/commands/setup.js +5 -5
- package/dist/src/commands/sharing.js +11 -108
- package/dist/src/commands/support.js +20 -9
- package/dist/src/core/{adapters.js → agents/adapters.js} +2 -2
- package/dist/src/core/{agent-inspection.js → agents/agent-inspection.js} +48 -6
- package/dist/src/core/{codex-mcp.js → agents/codex-mcp.js} +1 -1
- package/dist/src/core/{health-score-evidence.js → agents/health-score-evidence.js} +8 -8
- package/dist/src/core/{runtime-tools.js → agents/runtime-tools.js} +2 -2
- package/dist/src/core/{audit.js → catalog/audit.js} +4 -4
- package/dist/src/core/{catalog.js → catalog/catalog.js} +5 -5
- package/dist/src/core/{components.js → catalog/components.js} +2 -2
- package/dist/src/core/{conformance.js → catalog/conformance.js} +2 -2
- package/dist/src/core/{profiles.js → catalog/profiles.js} +2 -2
- package/dist/src/core/{provenance.js → catalog/provenance.js} +3 -3
- package/dist/src/core/{registry.js → catalog/registry.js} +59 -11
- package/dist/src/core/{safety.js → catalog/safety.js} +36 -7
- package/dist/src/core/{skill-compare.js → catalog/skill-compare.js} +1 -1
- package/dist/src/core/{skill-inventory.js → catalog/skill-inventory.js} +3 -3
- package/dist/src/core/{skills.js → catalog/skills.js} +1 -1
- package/dist/src/core/{candidate-intelligence.js → discovery/candidate-intelligence.js} +6 -6
- package/dist/src/core/{discovery-connector.js → discovery/discovery-connector.js} +2 -2
- package/dist/src/core/{evaluate.js → discovery/evaluate.js} +2 -2
- package/dist/src/core/{mcp-registry-discovery.js → discovery/mcp-registry-discovery.js} +1 -1
- package/dist/src/core/{observations.js → discovery/observations.js} +3 -3
- package/dist/src/core/{review-queue.js → discovery/review-queue.js} +2 -2
- package/dist/src/core/{skills-sh-discovery.js → discovery/skills-sh-discovery.js} +1 -1
- package/dist/src/core/{adopt.js → install/adopt.js} +3 -3
- package/dist/src/core/{catalog-install.js → install/catalog-install.js} +11 -11
- package/dist/src/core/{file-lock.js → install/file-lock.js} +1 -1
- package/dist/src/core/{install.js → install/install.js} +5 -5
- package/dist/src/core/{package.js → install/package.js} +3 -3
- package/dist/src/core/{reconcile.js → install/reconcile.js} +5 -5
- package/dist/src/core/{remove.js → install/remove.js} +4 -4
- package/dist/src/core/{snapshot.js → install/snapshot.js} +1 -1
- package/dist/src/core/{source.js → install/source.js} +22 -8
- package/dist/src/core/{sync.js → install/sync.js} +9 -9
- package/dist/src/core/{transaction.js → install/transaction.js} +1 -1
- package/dist/src/core/{uninstall.js → install/uninstall.js} +4 -4
- package/dist/src/core/{update.js → install/update.js} +4 -4
- package/dist/src/core/{cli-guide.js → reporting/cli-guide.js} +29 -45
- package/dist/src/core/reporting/completion.js +90 -0
- package/dist/src/core/{doctor.js → reporting/doctor.js} +5 -10
- package/dist/src/core/{freshness-alerts.js → reporting/freshness-alerts.js} +5 -5
- package/dist/src/core/{health.js → reporting/health.js} +6 -6
- package/dist/src/core/{loadout-card.js → reporting/loadout-card.js} +1 -1
- package/dist/src/core/{readme-claims.js → reporting/readme-claims.js} +1 -1
- package/dist/src/core/{readme-facts.js → reporting/readme-facts.js} +2 -2
- package/dist/src/core/{share-report.js → reporting/share-report.js} +3 -3
- package/dist/src/core/{upgrade.js → reporting/upgrade.js} +7 -7
- package/dist/src/core/routing/first-party-skills.js +118 -0
- package/dist/src/core/routing/handoff.js +314 -0
- package/dist/src/core/{model-config.js → routing/model-config.js} +4 -4
- package/dist/src/core/routing/policy.js +147 -0
- package/dist/src/core/{route.js → routing/route.js} +202 -39
- package/dist/src/core/{api.js → runtime/api.js} +4 -4
- package/dist/src/core/{canary.js → runtime/canary.js} +1 -1
- package/dist/src/core/{github.js → runtime/github.js} +1 -1
- package/dist/src/core/{mcp-recipes.js → runtime/mcp-recipes.js} +1 -1
- package/dist/src/core/{mcp.js → runtime/mcp.js} +2 -2
- package/dist/src/core/{scheduler.js → runtime/scheduler.js} +4 -4
- package/dist/src/core/{active-policy.js → workspace/active-policy.js} +2 -2
- package/dist/src/core/{active-set.js → workspace/active-set.js} +2 -2
- package/dist/src/core/{improve.js → workspace/improve.js} +3 -3
- package/dist/src/core/{manifest.js → workspace/manifest.js} +2 -2
- package/dist/src/core/{outcomes.js → workspace/outcomes.js} +3 -3
- package/dist/src/core/{portable.js → workspace/portable.js} +3 -3
- package/dist/src/core/{profile-state.js → workspace/profile-state.js} +1 -1
- package/dist/src/core/{recommend.js → workspace/recommend.js} +1 -1
- package/dist/src/core/{state.js → workspace/state.js} +3 -3
- package/docs/CANDIDATE_INTELLIGENCE.md +9 -2
- package/docs/CATALOG.md +1 -1
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +1 -1
- package/docs/DISCOVERED.md +250 -248
- package/docs/FEATURE_TEST_MATRIX.md +7 -260
- package/docs/GITHUB_AUTHORIZATION.md +5 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +1 -1
- package/docs/RELEASE_REVIEW.md +0 -1
- package/docs/evidence/readme-claims.json +22 -19
- package/package.json +7 -4
- package/skills/loadout-curator/SKILL.md +105 -0
- package/skills/loadout-router/SKILL.md +85 -0
- package/MASTER_PLAN.md +0 -2207
- package/dist/src/core/completion.js +0 -143
- package/dist/src/core/handoff.js +0 -161
- package/docs/ACTIVE_SET.md +0 -53
- package/docs/COMPATIBILITY_POLICY.md +0 -22
- package/docs/CONVERSION_AND_SANDBOX.md +0 -27
- package/docs/EVALUATION_PROTOCOL_V1.md +0 -300
- package/docs/HEAD_TO_HEAD_EVALUATION.md +0 -79
- package/docs/PROVIDER_CONFIGURATION.md +0 -45
- package/docs/README_RESEARCH.md +0 -36
- package/docs/REPOSITORY_STABILIZATION.md +0 -190
- package/docs/SAFE_UPDATE_DEMO.md +0 -25
- package/docs/SCHEMA_DECISIONS.md +0 -25
- package/docs/SUBMISSION_COPY.md +0 -90
- package/docs/TEAM_POLICY.md +0 -18
- package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +0 -283
- package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +0 -116
- package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +0 -469
- package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +0 -80
- package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +0 -55
- package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +0 -228
- /package/dist/src/core/{agent-health-score.js → agents/agent-health-score.js} +0 -0
- /package/dist/src/core/{agent-versions.js → agents/agent-versions.js} +0 -0
- /package/dist/src/core/{conversion.js → agents/conversion.js} +0 -0
- /package/dist/src/core/{paths.js → agents/paths.js} +0 -0
- /package/dist/src/core/{runtime-tool-recipe.js → agents/runtime-tool-recipe.js} +0 -0
- /package/dist/src/core/{catalog-coverage.js → catalog/catalog-coverage.js} +0 -0
- /package/dist/src/core/{skill-security.js → catalog/skill-security.js} +0 -0
- /package/dist/src/core/{community.js → discovery/community.js} +0 -0
- /package/dist/src/core/{github-discovery.js → discovery/github-discovery.js} +0 -0
- /package/dist/src/core/{private-discovery.js → discovery/private-discovery.js} +0 -0
- /package/dist/src/core/{ranking.js → discovery/ranking.js} +0 -0
- /package/dist/src/core/{atomic-file.js → install/atomic-file.js} +0 -0
- /package/dist/src/core/{diff.js → install/diff.js} +0 -0
- /package/dist/src/core/{update-watch.js → install/update-watch.js} +0 -0
- /package/dist/src/core/{loadout-badge.js → reporting/loadout-badge.js} +0 -0
- /package/dist/src/core/{terminal.js → reporting/terminal.js} +0 -0
- /package/dist/src/core/{access.js → routing/access.js} +0 -0
- /package/dist/src/core/{credentials.js → routing/credentials.js} +0 -0
- /package/dist/src/core/{sandbox.js → runtime/sandbox.js} +0 -0
- /package/dist/src/core/{active-limit.js → workspace/active-limit.js} +0 -0
- /package/dist/src/core/{target-occupancy.js → workspace/target-occupancy.js} +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { cp, lstat, mkdir, readFile, readdir, rm, writeFile, } from "node:fs/promises";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { loadoutHome } from "
|
|
4
|
+
import { loadoutHome } from "../agents/paths.js";
|
|
5
5
|
import { analyzeUpdateSafety } from "./safety.js";
|
|
6
6
|
const NAME = /^[a-z0-9][a-z0-9._-]*$/;
|
|
7
|
+
/** A SHA-256 digest, and nothing that could traverse a path. */
|
|
8
|
+
const DIGEST = /^[a-f0-9]{64}$/;
|
|
7
9
|
const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/;
|
|
8
10
|
export function parsePackageDescriptor(value) {
|
|
9
11
|
if (!value || typeof value !== "object")
|
|
@@ -266,18 +268,54 @@ function registryEndpoint(base, path) {
|
|
|
266
268
|
endpoint.hash = "";
|
|
267
269
|
return endpoint;
|
|
268
270
|
}
|
|
271
|
+
/** Registry responses are untrusted input: bound the wait and the body. */
|
|
272
|
+
const REGISTRY_TIMEOUT_MS = 30_000;
|
|
273
|
+
const REGISTRY_MAX_BYTES = 32 * 1024 * 1024;
|
|
274
|
+
async function registryJson(input, init = {}) {
|
|
275
|
+
const response = await fetch(input, {
|
|
276
|
+
...init,
|
|
277
|
+
signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
|
|
278
|
+
});
|
|
279
|
+
// Trust the declared length when present, but still cap what is read: a
|
|
280
|
+
// server may understate or omit it.
|
|
281
|
+
const declared = Number(response.headers.get("content-length"));
|
|
282
|
+
if (Number.isFinite(declared) && declared > REGISTRY_MAX_BYTES)
|
|
283
|
+
throw new Error("Remote registry response exceeds the size limit");
|
|
284
|
+
const body = await response.arrayBuffer();
|
|
285
|
+
if (body.byteLength > REGISTRY_MAX_BYTES)
|
|
286
|
+
throw new Error("Remote registry response exceeds the size limit");
|
|
287
|
+
const text = new TextDecoder().decode(body);
|
|
288
|
+
let value;
|
|
289
|
+
try {
|
|
290
|
+
value = JSON.parse(text);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
throw new Error("Remote registry returned a malformed JSON response");
|
|
294
|
+
}
|
|
295
|
+
return { ok: response.ok, status: response.status, value };
|
|
296
|
+
}
|
|
269
297
|
export async function fetchRemoteRegistryPackage(registry, name, version) {
|
|
270
298
|
if (!NAME.test(name) || !VERSION.test(version))
|
|
271
299
|
throw new Error("Invalid remote registry package name or version");
|
|
272
300
|
const base = registryUrl(registry);
|
|
273
|
-
const response = await
|
|
301
|
+
const response = await registryJson(registryEndpoint(base, `/v1/packages/${encodeURIComponent(name)}/${encodeURIComponent(version)}`));
|
|
274
302
|
if (!response.ok)
|
|
275
303
|
throw new Error(`Remote registry returned ${response.status}`);
|
|
276
|
-
const bundle =
|
|
304
|
+
const bundle = response.value;
|
|
305
|
+
// `digest` arrives from a remote server and is used to build a path that is
|
|
306
|
+
// later deleted recursively. Validate its shape before it can become one:
|
|
307
|
+
// a value such as "../../.." would otherwise escape the cache root.
|
|
308
|
+
if (typeof bundle?.digest !== "string" || !DIGEST.test(bundle.digest))
|
|
309
|
+
throw new Error("Remote registry returned a bundle whose digest is not a SHA-256 hex string");
|
|
277
310
|
const key = createHash("sha256")
|
|
278
311
|
.update(base.origin + base.pathname)
|
|
279
312
|
.digest("hex");
|
|
280
|
-
const
|
|
313
|
+
const root = join(loadoutHome(), "cache", "registry", key, name, version);
|
|
314
|
+
const target = resolve(root, bundle.digest);
|
|
315
|
+
// Belt and braces: even with a validated digest, never operate on a path
|
|
316
|
+
// that resolved outside the directory it was meant to live in.
|
|
317
|
+
if (target !== root && !target.startsWith(root + sep))
|
|
318
|
+
throw new Error("Remote registry bundle resolved outside the cache root");
|
|
281
319
|
try {
|
|
282
320
|
const packed = await packPackage(target);
|
|
283
321
|
if (packed.digest === bundle.digest)
|
|
@@ -286,9 +324,19 @@ export async function fetchRemoteRegistryPackage(registry, name, version) {
|
|
|
286
324
|
catch {
|
|
287
325
|
/* import below */
|
|
288
326
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
await
|
|
327
|
+
// Import into a scratch directory and swap it in, so a failed or malicious
|
|
328
|
+
// import cannot leave the cache holding a half-written package.
|
|
329
|
+
await mkdir(root, { recursive: true });
|
|
330
|
+
const staging = `${target}.incoming-${randomUUID().slice(0, 8)}`;
|
|
331
|
+
try {
|
|
332
|
+
await importRegistryBundle(bundle, staging);
|
|
333
|
+
await rm(target, { recursive: true, force: true });
|
|
334
|
+
await rename(staging, target);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
await rm(staging, { recursive: true, force: true });
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
292
340
|
return { path: target, digest: bundle.digest };
|
|
293
341
|
}
|
|
294
342
|
export async function publishRemotePackage(root, registry, token, options = {}) {
|
|
@@ -296,7 +344,7 @@ export async function publishRemotePackage(root, registry, token, options = {})
|
|
|
296
344
|
throw new Error("Remote registry token is required");
|
|
297
345
|
const bundle = await createRegistryBundle(root);
|
|
298
346
|
const base = registryUrl(registry);
|
|
299
|
-
const response = await
|
|
347
|
+
const response = await registryJson(registryEndpoint(base, "/v1/packages"), {
|
|
300
348
|
method: "POST",
|
|
301
349
|
headers: {
|
|
302
350
|
authorization: `Bearer ${token}`,
|
|
@@ -305,7 +353,7 @@ export async function publishRemotePackage(root, registry, token, options = {})
|
|
|
305
353
|
},
|
|
306
354
|
body: JSON.stringify(bundle),
|
|
307
355
|
});
|
|
308
|
-
const result =
|
|
356
|
+
const result = response.value;
|
|
309
357
|
if (!response.ok)
|
|
310
358
|
throw new Error(result.error ?? `Remote registry returned ${response.status}`);
|
|
311
359
|
return {
|
|
@@ -38,27 +38,37 @@ const SUSPICIOUS_INSTRUCTIONS = [
|
|
|
38
38
|
export function detectSecretKinds(content) {
|
|
39
39
|
return SECRET_PATTERNS.filter((check) => check.pattern.test(content)).map((check) => check.name);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
const MAX_INSPECTABLE_BYTES = 2_000_000;
|
|
42
|
+
const MAX_DEPTH = 10;
|
|
43
|
+
async function files(root, gaps = []) {
|
|
42
44
|
const result = new Map();
|
|
43
45
|
const base = resolve(root);
|
|
44
46
|
try {
|
|
45
47
|
const rootInfo = await stat(base);
|
|
46
|
-
if (rootInfo.isFile()
|
|
48
|
+
if (rootInfo.isFile()) {
|
|
49
|
+
if (rootInfo.size > MAX_INSPECTABLE_BYTES) {
|
|
50
|
+
gaps.push({ path: basename(base), reason: "too-large" });
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
47
53
|
result.set(basename(base), await readFile(base));
|
|
48
54
|
return result;
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
catch {
|
|
58
|
+
gaps.push({ path: basename(base), reason: "unreadable" });
|
|
52
59
|
return result;
|
|
53
60
|
}
|
|
54
61
|
async function visit(directory, depth) {
|
|
55
|
-
if (depth >
|
|
62
|
+
if (depth > MAX_DEPTH) {
|
|
63
|
+
gaps.push({ path: relative(base, directory), reason: "too-deep" });
|
|
56
64
|
return;
|
|
65
|
+
}
|
|
57
66
|
let entries;
|
|
58
67
|
try {
|
|
59
68
|
entries = await readdir(directory, { withFileTypes: true });
|
|
60
69
|
}
|
|
61
70
|
catch {
|
|
71
|
+
gaps.push({ path: relative(base, directory), reason: "unreadable" });
|
|
62
72
|
return;
|
|
63
73
|
}
|
|
64
74
|
for (const entry of entries) {
|
|
@@ -73,14 +83,19 @@ async function files(root) {
|
|
|
73
83
|
}
|
|
74
84
|
if (!entry.isFile())
|
|
75
85
|
continue;
|
|
86
|
+
const shown = relative(base, absolute).split("\\").join("/");
|
|
76
87
|
try {
|
|
77
88
|
const info = await stat(absolute);
|
|
78
|
-
if (info.size >
|
|
89
|
+
if (info.size > MAX_INSPECTABLE_BYTES) {
|
|
90
|
+
gaps.push({ path: shown, reason: "too-large" });
|
|
79
91
|
continue;
|
|
80
|
-
|
|
92
|
+
}
|
|
93
|
+
result.set(shown, await readFile(absolute));
|
|
81
94
|
}
|
|
82
95
|
catch {
|
|
83
|
-
|
|
96
|
+
// The repository may change while being inspected; either way this
|
|
97
|
+
// file was not reviewed, so record it rather than passing it silently.
|
|
98
|
+
gaps.push({ path: shown, reason: "unreadable" });
|
|
84
99
|
}
|
|
85
100
|
}
|
|
86
101
|
}
|
|
@@ -131,8 +146,9 @@ function collectEnvironmentNames(content) {
|
|
|
131
146
|
}
|
|
132
147
|
/** Analyze changed repository files without running package code or revealing secret values. */
|
|
133
148
|
export async function analyzeUpdateSafety(oldPath, newPath) {
|
|
149
|
+
const gaps = [];
|
|
134
150
|
const oldFiles = oldPath ? await files(oldPath) : new Map();
|
|
135
|
-
const newFiles = await files(newPath);
|
|
151
|
+
const newFiles = await files(newPath, gaps);
|
|
136
152
|
const changed = changedPaths(oldFiles, newFiles);
|
|
137
153
|
const findings = [];
|
|
138
154
|
const domains = new Set();
|
|
@@ -227,6 +243,19 @@ export async function analyzeUpdateSafety(oldPath, newPath) {
|
|
|
227
243
|
paths: [...new Set(instructionPaths)].sort(),
|
|
228
244
|
names: [...instructionKinds].sort(),
|
|
229
245
|
});
|
|
246
|
+
// Fail closed. Anything the scan could not open is exactly what a reviewer
|
|
247
|
+
// needs told about, so an uninspectable file requires approval rather than
|
|
248
|
+
// producing a clean report.
|
|
249
|
+
if (gaps.length) {
|
|
250
|
+
const reasons = [...new Set(gaps.map((gap) => gap.reason))].sort();
|
|
251
|
+
findings.push({
|
|
252
|
+
severity: "blocking",
|
|
253
|
+
category: "uninspectable",
|
|
254
|
+
message: `Safety scan could not inspect ${gaps.length} path(s) (${reasons.join(", ")}); they were not reviewed.`,
|
|
255
|
+
paths: [...new Set(gaps.map((gap) => gap.path))].sort(),
|
|
256
|
+
names: reasons,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
230
259
|
return {
|
|
231
260
|
approvalRequired: findings.some((finding) => finding.severity === "blocking"),
|
|
232
261
|
findings,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { DEFAULT_ACTIVE_SKILL_LIMIT } from "
|
|
5
|
-
import { readInstallState } from "
|
|
6
|
-
import { listInstalledRuntimeToolSkillTargets, } from "
|
|
4
|
+
import { DEFAULT_ACTIVE_SKILL_LIMIT } from "../workspace/active-limit.js";
|
|
5
|
+
import { readInstallState } from "../workspace/state.js";
|
|
6
|
+
import { listInstalledRuntimeToolSkillTargets, } from "../agents/runtime-tools.js";
|
|
7
7
|
const MAX_SCAN_DEPTH = 6;
|
|
8
8
|
function cleanFrontmatterValue(value) {
|
|
9
9
|
if (!value)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cp, lstat, readdir, readFile, rm } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { ensureDirectory } from "
|
|
3
|
+
import { ensureDirectory } from "../agents/paths.js";
|
|
4
4
|
import { assertSkillSecurity, scanSkillSecurity } from "./skill-security.js";
|
|
5
5
|
export async function discoverSkillDirectories(root, options = {}) {
|
|
6
6
|
const result = [];
|
|
@@ -4,13 +4,13 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { isDeepStrictEqual } from "node:util";
|
|
7
|
-
import { writeFileAtomically } from "
|
|
8
|
-
import { loadEffectiveCatalog, validateCatalog } from "
|
|
7
|
+
import { writeFileAtomically } from "../install/atomic-file.js";
|
|
8
|
+
import { loadEffectiveCatalog, validateCatalog } from "../catalog/catalog.js";
|
|
9
9
|
import { evaluatePackage } from "./evaluate.js";
|
|
10
|
-
import { inspectPackage } from "
|
|
11
|
-
import { ensureDirectory, loadoutHome } from "
|
|
12
|
-
import { fetchRepositorySnapshot, normalizeRepository, } from "
|
|
13
|
-
import { safeTerminalText } from "
|
|
10
|
+
import { inspectPackage } from "../install/package.js";
|
|
11
|
+
import { ensureDirectory, loadoutHome } from "../agents/paths.js";
|
|
12
|
+
import { fetchRepositorySnapshot, normalizeRepository, } from "../install/source.js";
|
|
13
|
+
import { safeTerminalText } from "../reporting/terminal.js";
|
|
14
14
|
const dossierDirectory = () => join(loadoutHome(), "candidates");
|
|
15
15
|
const sourceVerifiedDossiers = new WeakMap();
|
|
16
16
|
function installabilityFor(components) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { writeFileAtomically } from "
|
|
4
|
-
import { ensureDirectory } from "
|
|
3
|
+
import { writeFileAtomically } from "../install/atomic-file.js";
|
|
4
|
+
import { ensureDirectory } from "../agents/paths.js";
|
|
5
5
|
export class ConnectorRequestError extends Error {
|
|
6
6
|
status;
|
|
7
7
|
retryAfterSeconds;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { inspectPackage } from "
|
|
2
|
-
import { analyzeUpdateSafety } from "
|
|
1
|
+
import { inspectPackage } from "../install/package.js";
|
|
2
|
+
import { analyzeUpdateSafety } from "../catalog/safety.js";
|
|
3
3
|
function statusFor(findings) {
|
|
4
4
|
if (findings.some((finding) => finding.category === "secret" || finding.category === "instruction"))
|
|
5
5
|
return "blocked";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { ConnectorRequestError, fetchBoundedJson, rateLimitFromHeaders, readDiscoveryConnectorCache, writeDiscoveryConnectorCache, } from "./discovery-connector.js";
|
|
3
|
-
import { loadoutHome } from "
|
|
3
|
+
import { loadoutHome } from "../agents/paths.js";
|
|
4
4
|
const SOURCE = "official-mcp-registry";
|
|
5
5
|
const API = "https://registry.modelcontextprotocol.io/v0.1/servers";
|
|
6
6
|
const ATTRIBUTION = "https://registry.modelcontextprotocol.io/docs";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { fetchGitHubReleaseMetadata, } from "
|
|
4
|
-
import { writeFileAtomically } from "
|
|
5
|
-
import { loadoutHome } from "
|
|
3
|
+
import { fetchGitHubReleaseMetadata, } from "../runtime/github.js";
|
|
4
|
+
import { writeFileAtomically } from "../install/atomic-file.js";
|
|
5
|
+
import { loadoutHome } from "../agents/paths.js";
|
|
6
6
|
const MAX_OBSERVATIONS_PER_REPOSITORY = 90;
|
|
7
7
|
function observationPath(repository) {
|
|
8
8
|
return join(loadoutHome(), "observations", `${repository.replace("/", "__")}.json`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { writeFileAtomically } from "
|
|
4
|
-
import { ensureDirectory, loadoutHome } from "
|
|
3
|
+
import { writeFileAtomically } from "../install/atomic-file.js";
|
|
4
|
+
import { ensureDirectory, loadoutHome } from "../agents/paths.js";
|
|
5
5
|
const queuePath = () => join(loadoutHome(), "review-queue.json");
|
|
6
6
|
function isQueue(value) {
|
|
7
7
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { ConnectorRequestError, fetchBoundedJson, rateLimitFromHeaders, readDiscoveryConnectorCache, writeDiscoveryConnectorCache, } from "./discovery-connector.js";
|
|
3
|
-
import { loadoutHome } from "
|
|
3
|
+
import { loadoutHome } from "../agents/paths.js";
|
|
4
4
|
const SOURCE = "skills-sh";
|
|
5
5
|
const API = "https://skills.sh/api/v1/skills";
|
|
6
6
|
const ATTRIBUTION = "https://skills.sh/docs/api";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { basename, isAbsolute, join, posix, relative, sep } from "node:path";
|
|
4
|
-
import { enrichInventoryWithProvenance } from "
|
|
5
|
-
import { scanInstalledSkills } from "
|
|
6
|
-
import { installStatePath, recordInstall } from "
|
|
4
|
+
import { enrichInventoryWithProvenance } from "../catalog/provenance.js";
|
|
5
|
+
import { scanInstalledSkills } from "../catalog/skill-inventory.js";
|
|
6
|
+
import { installStatePath, recordInstall } from "../workspace/state.js";
|
|
7
7
|
import { runMutationTransaction } from "./transaction.js";
|
|
8
8
|
const issuedAdoptionPlans = new WeakMap();
|
|
9
9
|
function safeRelativePath(root, path) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { DEFAULT_ACTIVE_SKILL_LIMIT } from "
|
|
2
|
-
import { loadEffectiveCatalog } from "
|
|
3
|
-
import { detectAgents } from "
|
|
4
|
-
import { isStableSkillSelected, isPowerSkillSelected, resolveCatalogProfile, } from "
|
|
1
|
+
import { DEFAULT_ACTIVE_SKILL_LIMIT } from "../workspace/active-limit.js";
|
|
2
|
+
import { loadEffectiveCatalog, } from "../catalog/catalog.js";
|
|
3
|
+
import { detectAgents } from "../agents/paths.js";
|
|
4
|
+
import { isStableSkillSelected, isPowerSkillSelected, resolveCatalogProfile, } from "../catalog/profiles.js";
|
|
5
5
|
import { applySkillLibraryBatch, applySkillInstallBatch, buildSkillPlan, installedAgents, planManagedProfileReconciliation, } from "./install.js";
|
|
6
6
|
import { fetchRepositorySnapshot, } from "./source.js";
|
|
7
|
-
import { analyzeInstallPlanSafety, } from "
|
|
8
|
-
import { formatModelApiAccess } from "
|
|
9
|
-
import { recordInstalledProfile } from "
|
|
10
|
-
import { readInstallState } from "
|
|
7
|
+
import { analyzeInstallPlanSafety, } from "../catalog/safety.js";
|
|
8
|
+
import { formatModelApiAccess, } from "../routing/access.js";
|
|
9
|
+
import { recordInstalledProfile } from "../workspace/profile-state.js";
|
|
10
|
+
import { readInstallState } from "../workspace/state.js";
|
|
11
11
|
async function parallelMap(values, concurrency, worker) {
|
|
12
12
|
const results = new Array(values.length);
|
|
13
13
|
let cursor = 0;
|
|
@@ -240,11 +240,11 @@ export function formatPreparedCatalogInstall(prepared, options = {}) {
|
|
|
240
240
|
const quarantined = prepared.skipped.filter((item) => item.kind === "quarantined");
|
|
241
241
|
const retired = prepared.reconciliation?.obsoleteUnits ?? [];
|
|
242
242
|
const modeLabel = prepared.selection.mode === "maximum"
|
|
243
|
-
? "Maximum
|
|
243
|
+
? "Maximum"
|
|
244
244
|
: prepared.selection.mode === "power"
|
|
245
|
-
? "Power
|
|
245
|
+
? "Power"
|
|
246
246
|
: prepared.selection.mode === "stable"
|
|
247
|
-
? "Stable
|
|
247
|
+
? "Stable"
|
|
248
248
|
: "Custom";
|
|
249
249
|
const lines = [
|
|
250
250
|
`Loadout: ${modeLabel}`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, open, readFile, rename, rm, stat, utimes, } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
-
import { ensureDirectory } from "
|
|
4
|
+
import { ensureDirectory } from "../agents/paths.js";
|
|
5
5
|
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
6
6
|
function parseOwner(content) {
|
|
7
7
|
try {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { cp, lstat, rm } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, posix, relative, win32, } from "node:path";
|
|
4
|
-
import { ensureDirectory, loadoutHome } from "
|
|
5
|
-
import { planAdapterSkillInstall } from "
|
|
6
|
-
import { applySkillPlan, detectInstallConflicts } from "
|
|
7
|
-
import { activationLibraryPath, installStatePath, recordInstall, recordInstallBatch, recordLibraryInstallBatch, readInstallState, hashDirectory, writeInstallState, } from "
|
|
4
|
+
import { ensureDirectory, loadoutHome } from "../agents/paths.js";
|
|
5
|
+
import { planAdapterSkillInstall } from "../agents/adapters.js";
|
|
6
|
+
import { applySkillPlan, detectInstallConflicts } from "../catalog/skills.js";
|
|
7
|
+
import { activationLibraryPath, installStatePath, recordInstall, recordInstallBatch, recordLibraryInstallBatch, readInstallState, hashDirectory, writeInstallState, } from "../workspace/state.js";
|
|
8
8
|
import { runMutationTransaction } from "./transaction.js";
|
|
9
|
-
import { inspectTargetOccupancy } from "
|
|
9
|
+
import { inspectTargetOccupancy } from "../workspace/target-occupancy.js";
|
|
10
10
|
export function installedAgents(agents, requested) {
|
|
11
11
|
const available = agents.filter((agent) => agent.installed);
|
|
12
12
|
if (!requested || requested.length === 0)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile, readdir } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
-
import { discoverSkillDirectories } from "
|
|
4
|
-
import { discoverMcpManifests } from "
|
|
5
|
-
import { discoverResources } from "
|
|
3
|
+
import { discoverSkillDirectories } from "../catalog/skills.js";
|
|
4
|
+
import { discoverMcpManifests } from "../runtime/mcp.js";
|
|
5
|
+
import { discoverResources } from "../catalog/components.js";
|
|
6
6
|
const portableRelative = (root, path) => relative(root, path).split(/[\\/]/).join("/") || ".";
|
|
7
7
|
function frontmatter(path) {
|
|
8
8
|
return readFile(join(path, "SKILL.md"), "utf8").then((content) => {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { cp, lstat, readFile, readdir, rm } from "node:fs/promises";
|
|
3
3
|
import { join, relative, sep } from "node:path";
|
|
4
|
-
import { analyzeUpdateSafety } from "
|
|
5
|
-
import { enrichInventoryWithProvenance, } from "
|
|
6
|
-
import { scanInstalledSkills } from "
|
|
7
|
-
import { installStatePath, recordInstallBatch } from "
|
|
4
|
+
import { analyzeUpdateSafety } from "../catalog/safety.js";
|
|
5
|
+
import { enrichInventoryWithProvenance, } from "../catalog/provenance.js";
|
|
6
|
+
import { scanInstalledSkills } from "../catalog/skill-inventory.js";
|
|
7
|
+
import { installStatePath, recordInstallBatch } from "../workspace/state.js";
|
|
8
8
|
import { runMutationTransaction } from "./transaction.js";
|
|
9
9
|
import { repositoryCachePath } from "./source.js";
|
|
10
|
-
import { validateSkillDirectory } from "
|
|
10
|
+
import { validateSkillDirectory } from "../catalog/skills.js";
|
|
11
11
|
const issuedPlans = new WeakMap();
|
|
12
12
|
const IGNORED_TREE_NAMES = new Set([".git", ".cache", "node_modules"]);
|
|
13
13
|
function slug(value) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile, rm } from "node:fs/promises";
|
|
3
|
-
import { forgetInstall, installStatePath, readInstallState } from "
|
|
4
|
-
import { writeMcpConfigPlan } from "
|
|
3
|
+
import { forgetInstall, installStatePath, readInstallState, } from "../workspace/state.js";
|
|
4
|
+
import { writeMcpConfigPlan } from "../runtime/mcp.js";
|
|
5
5
|
import { runMutationTransaction } from "./transaction.js";
|
|
6
|
-
import { managedFileReadPath } from "
|
|
7
|
-
import { codexMcpServerFingerprint, removeCodexMcpServerBlock, writeCodexMcpConfigContent, } from "
|
|
6
|
+
import { managedFileReadPath } from "../workspace/active-set.js";
|
|
7
|
+
import { codexMcpServerFingerprint, removeCodexMcpServerBlock, writeCodexMcpConfigContent, } from "../agents/codex-mcp.js";
|
|
8
8
|
export async function planRemove(packageId) {
|
|
9
9
|
const state = await readInstallState();
|
|
10
10
|
const record = state.installs.find((entry) => entry.packageId === packageId);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFile, writeFile, mkdir, readdir, rename, rm, lstat, } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
-
import { loadoutHome, ensureDirectory, userHome } from "
|
|
4
|
+
import { loadoutHome, ensureDirectory, userHome } from "../agents/paths.js";
|
|
5
5
|
export async function createSnapshot(paths, options = {}) {
|
|
6
6
|
const normalizedRoots = [...new Set(paths.map((path) => resolve(path)))];
|
|
7
7
|
const snapshot = {
|
|
@@ -4,11 +4,28 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
-
import { loadoutHome, ensureDirectory } from "
|
|
7
|
+
import { loadoutHome, ensureDirectory } from "../agents/paths.js";
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
|
+
/**
|
|
10
|
+
* Bounds apply to every fetch, not only to callers who remember to ask. An
|
|
11
|
+
* unbounded clone of an untrusted repository can hang a Git process forever or
|
|
12
|
+
* fill the disk, so callers may raise a limit but none may omit one.
|
|
13
|
+
*/
|
|
14
|
+
export const REPOSITORY_FETCH_DEFAULTS = {
|
|
15
|
+
/** A reviewed skill repository clones well inside this. */
|
|
16
|
+
timeoutMs: 120_000,
|
|
17
|
+
maxBytes: 256 * 1024 * 1024,
|
|
18
|
+
maxFiles: 20_000,
|
|
19
|
+
};
|
|
20
|
+
function withFetchDefaults(options) {
|
|
21
|
+
return {
|
|
22
|
+
...options,
|
|
23
|
+
timeoutMs: options.timeoutMs ?? REPOSITORY_FETCH_DEFAULTS.timeoutMs,
|
|
24
|
+
maxBytes: options.maxBytes ?? REPOSITORY_FETCH_DEFAULTS.maxBytes,
|
|
25
|
+
maxFiles: options.maxFiles ?? REPOSITORY_FETCH_DEFAULTS.maxFiles,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
9
28
|
async function enforceRepositoryBounds(root, options) {
|
|
10
|
-
if (options.maxBytes === undefined && options.maxFiles === undefined)
|
|
11
|
-
return;
|
|
12
29
|
let bytes = 0;
|
|
13
30
|
let files = 0;
|
|
14
31
|
const pending = [root];
|
|
@@ -35,8 +52,6 @@ async function enforceRepositoryBounds(root, options) {
|
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
54
|
export function validateGitHubTreeBounds(value, options) {
|
|
38
|
-
if (options.maxBytes === undefined && options.maxFiles === undefined)
|
|
39
|
-
return;
|
|
40
55
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
41
56
|
throw new Error("GitHub tree preflight returned an invalid response");
|
|
42
57
|
const response = value;
|
|
@@ -63,8 +78,6 @@ export function validateGitHubTreeBounds(value, options) {
|
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
async function enforceGitHubTreeBounds(repository, commit, options) {
|
|
66
|
-
if (options.maxBytes === undefined && options.maxFiles === undefined)
|
|
67
|
-
return;
|
|
68
81
|
const signal = AbortSignal.timeout(options.timeoutMs ?? 120_000);
|
|
69
82
|
const token = process.env.GITHUB_TOKEN?.trim();
|
|
70
83
|
const response = await (options.fetcher ?? fetch)(`https://api.github.com/repos/${repository}/git/trees/${commit}?recursive=1`, {
|
|
@@ -181,7 +194,8 @@ function normalizeRef(ref) {
|
|
|
181
194
|
throw new Error(`Invalid Git ref: ${ref}`);
|
|
182
195
|
return ref;
|
|
183
196
|
}
|
|
184
|
-
export async function fetchRepositorySnapshot(input,
|
|
197
|
+
export async function fetchRepositorySnapshot(input, requested = {}) {
|
|
198
|
+
const options = withFetchDefaults(requested);
|
|
185
199
|
const repository = normalizeRepository(input);
|
|
186
200
|
const gitEnvironment = await isolatedGitEnvironment(loadoutHome());
|
|
187
201
|
const ref = options.ref ? normalizeRef(options.ref) : undefined;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, resolve, sep } from "node:path";
|
|
4
|
-
import { loadEffectiveCatalog } from "
|
|
4
|
+
import { loadEffectiveCatalog } from "../catalog/catalog.js";
|
|
5
5
|
import { installedAgents } from "./install.js";
|
|
6
|
-
import { orderManifestPackages, readManifest, writeLockfile, } from "
|
|
7
|
-
import { detectAgents, userHome } from "
|
|
6
|
+
import { orderManifestPackages, readManifest, writeLockfile, } from "../workspace/manifest.js";
|
|
7
|
+
import { detectAgents, userHome } from "../agents/paths.js";
|
|
8
8
|
import { fetchGitSnapshot, fetchRepositorySnapshot } from "./source.js";
|
|
9
|
-
import { addRootFileExports, buildUniversalPackagePlan } from "
|
|
10
|
-
import { analyzeInstallPlanSafety, } from "
|
|
11
|
-
import { fetchRemoteRegistryPackage, packPackage, resolveRegistryPackage, } from "
|
|
12
|
-
import { discoverMcpManifests, planMcpConfigBatch, writeMcpConfigPlan, } from "
|
|
13
|
-
import { applySkillPlan, detectInstallConflicts } from "
|
|
14
|
-
import { installStatePath, readInstallState, recordInstallTransaction, } from "
|
|
9
|
+
import { addRootFileExports, buildUniversalPackagePlan, } from "../catalog/components.js";
|
|
10
|
+
import { analyzeInstallPlanSafety, } from "../catalog/safety.js";
|
|
11
|
+
import { fetchRemoteRegistryPackage, packPackage, resolveRegistryPackage, } from "../catalog/registry.js";
|
|
12
|
+
import { discoverMcpManifests, planMcpConfigBatch, writeMcpConfigPlan, } from "../runtime/mcp.js";
|
|
13
|
+
import { applySkillPlan, detectInstallConflicts } from "../catalog/skills.js";
|
|
14
|
+
import { installStatePath, readInstallState, recordInstallTransaction, } from "../workspace/state.js";
|
|
15
15
|
import { runMutationTransaction } from "./transaction.js";
|
|
16
16
|
function registrySourceKey(source) {
|
|
17
17
|
if (source.type === "registry")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { lstat, mkdir, readdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { ensureDirectory, loadoutHome } from "
|
|
4
|
+
import { ensureDirectory, loadoutHome } from "../agents/paths.js";
|
|
5
5
|
import { createSnapshot, readSnapshot, recordSnapshotPostMutationState, restoreSnapshot, } from "./snapshot.js";
|
|
6
6
|
import { acquireFileLock, withFileLock, } from "./file-lock.js";
|
|
7
7
|
export const transactionRoot = () => join(loadoutHome(), "staging");
|
|
@@ -2,11 +2,11 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { readdir, rm, rmdir } from "node:fs/promises";
|
|
3
3
|
import { dirname, parse, resolve } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
-
import { loadoutHome } from "
|
|
5
|
+
import { loadoutHome } from "../agents/paths.js";
|
|
6
6
|
import { applyRemove, planRemove } from "./remove.js";
|
|
7
|
-
import { applyRuntimeToolPlan, listInstalledRuntimeTools, planRuntimeTool, } from "
|
|
8
|
-
import { applyNativeSchedulerBundle, planNativeScheduler, } from "
|
|
9
|
-
import { readInstallState } from "
|
|
7
|
+
import { applyRuntimeToolPlan, listInstalledRuntimeTools, planRuntimeTool, } from "../agents/runtime-tools.js";
|
|
8
|
+
import { applyNativeSchedulerBundle, planNativeScheduler, } from "../runtime/scheduler.js";
|
|
9
|
+
import { readInstallState } from "../workspace/state.js";
|
|
10
10
|
export async function buildUninstallPlan(dependencies = {}) {
|
|
11
11
|
const stateHome = loadoutHome();
|
|
12
12
|
const state = await readInstallState();
|
|
@@ -3,11 +3,11 @@ import { basename, join } from "node:path";
|
|
|
3
3
|
import { fetchRepositorySnapshot, resolveRepositoryHead } from "./source.js";
|
|
4
4
|
import { repositoryCachePath } from "./source.js";
|
|
5
5
|
import { diffRepositorySnapshots } from "./diff.js";
|
|
6
|
-
import { hashDirectory, readInstallState } from "
|
|
7
|
-
import { analyzeUpdateSafety } from "
|
|
8
|
-
import { detectAgents, loadoutHome } from "
|
|
6
|
+
import { hashDirectory, readInstallState } from "../workspace/state.js";
|
|
7
|
+
import { analyzeUpdateSafety } from "../catalog/safety.js";
|
|
8
|
+
import { detectAgents, loadoutHome } from "../agents/paths.js";
|
|
9
9
|
import { applySkillInstall, buildSkillPlan, installedAgents, } from "./install.js";
|
|
10
|
-
import { discoverSkillDirectories, validateSkillDirectory, } from "
|
|
10
|
+
import { discoverSkillDirectories, validateSkillDirectory, } from "../catalog/skills.js";
|
|
11
11
|
function managedUnitIds(state, packageId) {
|
|
12
12
|
return [
|
|
13
13
|
...new Set((state.activations ?? [])
|