loadout-ai 0.4.0 → 0.5.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 +65 -0
- package/MASTER_PLAN.md +173 -9
- package/README.md +57 -19
- package/catalog/discovered.json +8422 -6957
- package/catalog/packages.json +26 -0
- package/dist/src/cli.js +140 -68
- package/dist/src/core/active-policy.js +216 -42
- package/dist/src/core/active-set.js +14 -5
- package/dist/src/core/adopt.js +1 -1
- package/dist/src/core/catalog-coverage.js +2 -1
- package/dist/src/core/catalog-install.js +23 -8
- package/dist/src/core/cli-guide.js +5 -2
- package/dist/src/core/codex-mcp.js +49 -5
- package/dist/src/core/completion.js +0 -3
- package/dist/src/core/health.js +19 -11
- package/dist/src/core/install.js +6 -39
- package/dist/src/core/mcp-recipes.js +13 -4
- package/dist/src/core/mcp.js +6 -5
- package/dist/src/core/model-config.js +1 -1
- package/dist/src/core/portable.js +1 -1
- package/dist/src/core/recommend.js +129 -27
- package/dist/src/core/remove.js +28 -2
- package/dist/src/core/runtime-tool-recipe.js +2 -2
- package/dist/src/core/runtime-tools.js +15 -4
- package/dist/src/core/snapshot.js +20 -0
- package/dist/src/core/state.js +9 -0
- package/dist/src/core/sync.js +1 -1
- package/dist/src/core/target-occupancy.js +50 -0
- package/dist/src/core/transaction.js +2 -1
- package/dist/src/shared/schemas.js +1 -0
- package/docs/CATALOG.md +3 -2
- package/docs/DISCOVERED.md +253 -255
- package/docs/FEATURE_TEST_MATRIX.md +19 -32
- package/docs/GITHUB_AUTHORIZATION.md +2 -2
- package/docs/TESTING.md +3 -16
- package/docs/USER_TEST_GUIDE.md +39 -13
- package/docs/assets/loadout-workflow.png +0 -0
- package/docs/evidence/readme-claims.json +0 -8
- package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +116 -0
- package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +469 -0
- package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +55 -0
- package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +228 -0
- package/package.json +4 -6
- package/dashboard/app.js +0 -607
- package/dashboard/index.html +0 -249
- package/dashboard/styles.css +0 -384
- package/dist/src/core/demo.js +0 -136
- package/dist/src/dashboard.js +0 -421
|
@@ -10,6 +10,7 @@ export async function createSnapshot(paths, options = {}) {
|
|
|
10
10
|
.digest("hex")
|
|
11
11
|
.slice(0, 12)}`,
|
|
12
12
|
createdAt: new Date().toISOString(),
|
|
13
|
+
...(options.label ? { label: options.label } : {}),
|
|
13
14
|
roots: normalizedRoots,
|
|
14
15
|
files: [],
|
|
15
16
|
};
|
|
@@ -154,6 +155,22 @@ export async function listSnapshotIds() {
|
|
|
154
155
|
throw error;
|
|
155
156
|
}
|
|
156
157
|
}
|
|
158
|
+
function changedEntryCount(before, after) {
|
|
159
|
+
if (!after)
|
|
160
|
+
return undefined;
|
|
161
|
+
const previous = new Map(before.map((file) => [file.path, file]));
|
|
162
|
+
const next = new Map(after.map((file) => [file.path, file]));
|
|
163
|
+
return [...new Set([...previous.keys(), ...next.keys()])].filter((path) => JSON.stringify(previous.get(path)) !== JSON.stringify(next.get(path))).length;
|
|
164
|
+
}
|
|
165
|
+
export function summarizeSnapshot(snapshot) {
|
|
166
|
+
return {
|
|
167
|
+
id: snapshot.id,
|
|
168
|
+
createdAt: snapshot.createdAt,
|
|
169
|
+
label: snapshot.label ?? "managed change",
|
|
170
|
+
roots: snapshot.roots.length,
|
|
171
|
+
changedEntries: changedEntryCount(snapshot.files, snapshot.postMutationFiles),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
157
174
|
const SNAPSHOT_ID = /^\d{10,}-[a-f0-9]{12}$/;
|
|
158
175
|
function isSnapshotId(value) {
|
|
159
176
|
return SNAPSHOT_ID.test(value);
|
|
@@ -186,6 +203,9 @@ export function validateSnapshot(value) {
|
|
|
186
203
|
if (typeof value.createdAt !== "string" ||
|
|
187
204
|
!Number.isFinite(Date.parse(value.createdAt)))
|
|
188
205
|
throw new Error("Snapshot creation time is invalid");
|
|
206
|
+
if (value.label !== undefined &&
|
|
207
|
+
(typeof value.label !== "string" || value.label.trim().length === 0))
|
|
208
|
+
throw new Error("Snapshot label is invalid");
|
|
189
209
|
if (!Array.isArray(value.roots) ||
|
|
190
210
|
value.roots.length === 0 ||
|
|
191
211
|
!value.roots.every((root) => typeof root === "string" && root.length > 0))
|
package/dist/src/core/state.js
CHANGED
|
@@ -302,6 +302,15 @@ export async function recordInstallTransaction(entries, mcpEntries, snapshotId)
|
|
|
302
302
|
await writeInstallState(state);
|
|
303
303
|
return { installs, mcpInstalls };
|
|
304
304
|
}
|
|
305
|
+
/** Record one externally formatted but Loadout-managed MCP entry. */
|
|
306
|
+
export async function recordMcpInstall(record) {
|
|
307
|
+
const state = await readInstallState();
|
|
308
|
+
state.mcpInstalls = [
|
|
309
|
+
...(state.mcpInstalls ?? []).filter((entry) => entry.packageId !== record.packageId),
|
|
310
|
+
record,
|
|
311
|
+
];
|
|
312
|
+
await writeInstallState(state);
|
|
313
|
+
}
|
|
305
314
|
export async function forgetInstall(packageId) {
|
|
306
315
|
const state = await readInstallState();
|
|
307
316
|
const installs = state.installs.filter((entry) => entry.packageId !== packageId);
|
package/dist/src/core/sync.js
CHANGED
|
@@ -343,6 +343,6 @@ export async function applySyncPlan(plan, lockPath = "loadout.lock", options = {
|
|
|
343
343
|
await writeMcpConfigPlan(entry.plan);
|
|
344
344
|
await recordInstallTransaction(freshPlan.packages, freshPlan.mcpPlans, snapshot.id);
|
|
345
345
|
await writeLockfile(freshPlan.resolvedManifest ?? (await readManifest(freshPlan.manifest)), lockPath);
|
|
346
|
-
});
|
|
346
|
+
}, { label: "synchronize loadout manifest" });
|
|
347
347
|
return { snapshotId: applied.snapshotId, lockfile: lockPath };
|
|
348
348
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Inspect a prospective skill target without following symlinks or executing
|
|
5
|
+
* any content. Missing paths and recursively empty directories are safe to
|
|
6
|
+
* replace; every uncertain state fails closed.
|
|
7
|
+
*/
|
|
8
|
+
export async function inspectTargetOccupancy(path, maximumEntries = 10_000) {
|
|
9
|
+
let root;
|
|
10
|
+
try {
|
|
11
|
+
root = await lstat(path);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error &&
|
|
15
|
+
typeof error === "object" &&
|
|
16
|
+
"code" in error &&
|
|
17
|
+
error.code === "ENOENT")
|
|
18
|
+
return { occupied: false };
|
|
19
|
+
return { occupied: true, reason: "unreadable" };
|
|
20
|
+
}
|
|
21
|
+
if (root.isSymbolicLink())
|
|
22
|
+
return { occupied: true, reason: "symlink" };
|
|
23
|
+
if (!root.isDirectory())
|
|
24
|
+
return { occupied: true, reason: "unsupported" };
|
|
25
|
+
const queue = [path];
|
|
26
|
+
let inspected = 0;
|
|
27
|
+
while (queue.length) {
|
|
28
|
+
const directory = queue.pop();
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { occupied: true, reason: "unreadable" };
|
|
35
|
+
}
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
inspected += 1;
|
|
38
|
+
if (inspected > maximumEntries)
|
|
39
|
+
return { occupied: true, reason: "inspection-limit" };
|
|
40
|
+
if (entry.isDirectory() && !entry.isSymbolicLink())
|
|
41
|
+
queue.push(join(directory, entry.name));
|
|
42
|
+
else
|
|
43
|
+
return {
|
|
44
|
+
occupied: true,
|
|
45
|
+
reason: entry.isSymbolicLink() ? "symlink" : "content",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { occupied: false };
|
|
50
|
+
}
|
|
@@ -123,13 +123,14 @@ export async function withMutationLock(operation) {
|
|
|
123
123
|
* global lock. Network/source preparation should happen before this boundary;
|
|
124
124
|
* every state-dependent precondition belongs in `prepare`.
|
|
125
125
|
*/
|
|
126
|
-
export async function runMutationTransaction(prepare, mutate) {
|
|
126
|
+
export async function runMutationTransaction(prepare, mutate, options = {}) {
|
|
127
127
|
const mutationLock = await acquireMutationLock();
|
|
128
128
|
let transaction;
|
|
129
129
|
try {
|
|
130
130
|
const prepared = await prepare();
|
|
131
131
|
const snapshot = await createSnapshot(prepared.targets, {
|
|
132
132
|
persist: false,
|
|
133
|
+
label: options.label,
|
|
133
134
|
});
|
|
134
135
|
transaction = await beginTransaction(snapshot, prepared.targets, {
|
|
135
136
|
mutationLock,
|
|
@@ -275,6 +275,7 @@ export const installRecordSchema = z
|
|
|
275
275
|
files: z.array(fileHashSchema),
|
|
276
276
|
snapshotId: text,
|
|
277
277
|
installedAt: text,
|
|
278
|
+
configFormat: z.enum(["json", "codex-toml"]).optional(),
|
|
278
279
|
staticAssessment: z
|
|
279
280
|
.object({
|
|
280
281
|
status: z.enum(["clear", "warning", "blocking"]),
|
package/docs/CATALOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Catalog and upstream credits
|
|
2
2
|
|
|
3
|
-
Loadout's bundled catalog indexes
|
|
3
|
+
Loadout's bundled catalog indexes 51 public upstream repositories. This page gives every project a direct source link and exposes the exact reviewed revision used by Loadout.
|
|
4
4
|
|
|
5
5
|
Thank you to every maintainer and contributor whose work appears here. Loadout does not own, endorse for its authors, or relicense these projects. Each project remains governed by its upstream license and current terms.
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ Thank you to every maintainer and contributor whose work appears here. Loadout d
|
|
|
14
14
|
|
|
15
15
|
The machine-readable catalog remains the source of truth. Run `loadout catalog --coverage --json` to inspect coverage and `loadout capabilities` to inspect what each local agent adapter can actually manage.
|
|
16
16
|
|
|
17
|
-
## All
|
|
17
|
+
## All 51 credited repositories
|
|
18
18
|
|
|
19
19
|
| Upstream source | Loadout ID | Category | Reviewed components | License status | Reviewed revision |
|
|
20
20
|
| --------------------------------------------------------------------------------- | -------------------------------- | ------------------------ | -------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -68,6 +68,7 @@ The machine-readable catalog remains the source of truth. Run `loadout catalog -
|
|
|
68
68
|
| [MCP Toolbox for Databases](https://github.com/googleapis/mcp-toolbox) | `mcp-toolbox-for-databases` | database | `mcp` | `Apache-2.0` | [`b574b071fc27`](https://github.com/googleapis/mcp-toolbox/tree/b574b071fc27f717e90b4362864b1086be0c8995) |
|
|
69
69
|
| [Browserbase MCP Server](https://github.com/browserbase/mcp-server-browserbase) | `browserbase-mcp-server` | browser-automation | `mcp` | `Apache-2.0` | [`1e196b3d3c4d`](https://github.com/browserbase/mcp-server-browserbase/tree/1e196b3d3c4dc70944e0d19038dd9eb3608b207a) |
|
|
70
70
|
| [Bright Data MCP](https://github.com/brightdata/brightdata-mcp) | `bright-data-mcp` | web-research | `mcp` | `MIT` | [`adbfdd49b880`](https://github.com/brightdata/brightdata-mcp/tree/adbfdd49b8805136662a264490118c2688da3e29) |
|
|
71
|
+
| [Obsidian Skills](https://github.com/kepano/obsidian-skills) | `obsidian-skills` | knowledge-management | `skill`, `plugin` | `MIT` | [`a1dc48e68138`](https://github.com/kepano/obsidian-skills/tree/a1dc48e68138490d522c04cbf5822214c6eb1202) |
|
|
71
72
|
|
|
72
73
|
## Catalog maintenance
|
|
73
74
|
|