@iseoane/agent-depot 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 +37 -0
- package/README.es.md +969 -0
- package/README.md +254 -25
- package/dist/src/app-flow.js +52 -51
- package/dist/src/app-recipe-removal.js +63 -0
- package/dist/src/cli.js +122 -11
- package/dist/src/executable-resolution.js +54 -0
- package/dist/src/git-source.js +129 -6
- package/dist/src/package-bundles.js +614 -0
- package/dist/src/package-cli.js +412 -0
- package/dist/src/package-content.js +158 -0
- package/dist/src/package-flow.js +636 -0
- package/dist/src/package-host-plans.js +240 -0
- package/dist/src/package-host-state.js +349 -0
- package/dist/src/package-model.js +33 -0
- package/dist/src/package-owned-skills.js +122 -0
- package/dist/src/package-state.js +404 -0
- package/dist/src/profile-cli.js +15 -9
- package/dist/src/profile-export.js +44 -9
- package/dist/src/profile-import.js +80 -17
- package/dist/src/profile.js +78 -3
- package/dist/src/skill-discovery.js +2 -2
- package/dist/src/skill-install.js +16 -0
- package/dist/src/sources.js +11 -0
- package/dist/src/tui/app-actions.js +1 -1
- package/dist/src/tui/app.js +5 -6
- package/dist/src/tui/catalog-browse.js +10 -3
- package/dist/src/tui/catalog-panel.js +7 -3
- package/dist/src/tui/catalog-state.js +57 -6
- package/dist/src/tui/catalog-tree.js +52 -10
- package/dist/src/tui/catalog-view.js +27 -4
- package/dist/src/tui/environment.js +8 -1
- package/dist/src/tui/installations-browse.js +20 -2
- package/dist/src/tui/installations-rows.js +2 -1
- package/dist/src/tui/installations-tree.js +14 -1
- package/dist/src/tui/installations-view.js +8 -2
- package/dist/src/tui/installations.js +12 -2
- package/dist/src/tui/manage-actions.js +2 -0
- package/dist/src/tui/manage-panel.js +5 -0
- package/dist/src/tui/package-actions.js +165 -0
- package/dist/src/tui/package-rows.js +87 -0
- package/dist/src/tui/profile-view.js +36 -14
- package/dist/src/tui/sources-mode.js +2 -0
- package/dist/src/tui/sources-recipes.js +116 -53
- package/dist/src/tui/sources-view.js +57 -19
- package/dist/src/tui/tree.js +3 -4
- package/dist/src/tui/updates-flow.js +4 -1
- package/dist/src/tui/updates-panel.js +2 -2
- package/dist/src/tui/updates.js +72 -4
- package/dist/src/user-global-skill-inventory.js +7 -2
- package/package.json +2 -1
- package/skills/agent-depot-apprecipe/SKILL.md +29 -5
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { EXCLUDED_LIFECYCLE_SEGMENTS, } from "./skill-discovery.js";
|
|
4
|
+
/** A malformed manifest crossing the boundary fails closed instead of guessing. */
|
|
5
|
+
export class PackageManifestError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "PackageManifestError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Reads the `pi` manifest key, or the conventional `extensions/`, `skills/`,
|
|
13
|
+
* `prompts/`, and `themes/` directories when no `pi` key is present. Component
|
|
14
|
+
* paths are relative to the package root; the conventional fallback declares
|
|
15
|
+
* all four slots and lets discovery drop the ones with no files.
|
|
16
|
+
*/
|
|
17
|
+
export function parsePiPackageManifest(raw, root) {
|
|
18
|
+
const packageRoot = normalizeRoot(root);
|
|
19
|
+
const manifest = parseJsonObject(raw);
|
|
20
|
+
const components = resolveComponentPaths(packageRoot, buildComponentInventory("pi", manifest));
|
|
21
|
+
const version = manifestVersionEvidence(manifest.version, "bundle");
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
host: "pi",
|
|
24
|
+
coordinates: Object.freeze({ host: "pi", root: packageRoot }),
|
|
25
|
+
name: manifestName(manifest, packageRoot, "package"),
|
|
26
|
+
...(version === undefined ? {} : { version }),
|
|
27
|
+
components,
|
|
28
|
+
warnings: Object.freeze([]),
|
|
29
|
+
installable: true,
|
|
30
|
+
manifestDigest: manifestDigest([{ path: manifestPathFor(packageRoot, "package.json"), content: raw }]),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads one Claude Code plugin manifest. `name` is required. Component paths
|
|
35
|
+
* are relative to the plugin root. The marketplace binding is added by
|
|
36
|
+
* `parseClaudeMarketplace` or `discoverBundlesInSnapshot`, which own the
|
|
37
|
+
* `marketplaceRoot` coordinate.
|
|
38
|
+
*/
|
|
39
|
+
export function parseClaudePluginManifest(raw, root) {
|
|
40
|
+
const pluginRoot = normalizeRoot(root);
|
|
41
|
+
const manifest = parseJsonObject(raw);
|
|
42
|
+
const name = requiredName(manifest, "plugin.json");
|
|
43
|
+
const components = resolveComponentPaths(pluginRoot, buildComponentInventory("claude", manifest));
|
|
44
|
+
const version = manifestVersionEvidence(manifest.version, "bundle");
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
host: "claude",
|
|
47
|
+
coordinates: Object.freeze({ host: "claude", marketplaceRoot: pluginRoot, pluginName: name }),
|
|
48
|
+
name,
|
|
49
|
+
...(version === undefined ? {} : { version }),
|
|
50
|
+
components,
|
|
51
|
+
warnings: Object.freeze([]),
|
|
52
|
+
installable: true,
|
|
53
|
+
manifestDigest: manifestDigest([{ path: manifestPathFor(pluginRoot, ".claude-plugin/plugin.json"), content: raw }]),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reads one marketplace and returns a descriptor per `plugins` entry. The
|
|
58
|
+
* `readPluginManifest` resolver supplies the raw plugin.json for an in-repo
|
|
59
|
+
* entry, because the entry version overrides the plugin manifest version and
|
|
60
|
+
* the plugin manifest owns the component inventory (agents, hooks, and so on).
|
|
61
|
+
* A `command` source and any source outside the repository yield a descriptor
|
|
62
|
+
* with a warning rather than a throw.
|
|
63
|
+
*/
|
|
64
|
+
export function parseClaudeMarketplace(raw, root, readPluginManifest) {
|
|
65
|
+
const marketplaceRoot = normalizeRoot(root);
|
|
66
|
+
const marketplace = parseJsonObject(raw);
|
|
67
|
+
const entries = marketplace.plugins;
|
|
68
|
+
if (!Array.isArray(entries)) {
|
|
69
|
+
throw new PackageManifestError("marketplace.json must define a plugins array");
|
|
70
|
+
}
|
|
71
|
+
const metadataPluginRoot = isRecord(marketplace.metadata) && typeof marketplace.metadata.pluginRoot === "string"
|
|
72
|
+
? normalizeComponentPath(marketplace.metadata.pluginRoot)
|
|
73
|
+
: "";
|
|
74
|
+
const descriptors = [];
|
|
75
|
+
for (const [index, entry] of entries.entries()) {
|
|
76
|
+
if (!isRecord(entry)) {
|
|
77
|
+
throw new PackageManifestError(`marketplace.json plugins[${index}] must be an object`);
|
|
78
|
+
}
|
|
79
|
+
descriptors.push(buildMarketplaceDescriptor(entry, `plugins[${index}]`, marketplaceRoot, metadataPluginRoot, raw, readPluginManifest));
|
|
80
|
+
}
|
|
81
|
+
return Object.freeze(descriptors);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The `name` a Claude marketplace manifest declares. `planHostSteps` needs it to
|
|
85
|
+
* build `<plugin>@<marketplace>` and to name a marketplace refresh, because the
|
|
86
|
+
* name lives only in the Source's `.claude-plugin/marketplace.json`.
|
|
87
|
+
*/
|
|
88
|
+
export function claudeMarketplaceName(raw) {
|
|
89
|
+
const manifest = parseJsonObject(raw);
|
|
90
|
+
return typeof manifest.name === "string" && manifest.name.trim() !== "" ? manifest.name.trim() : undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The typed component inventory for one host manifest. Pi reads the `pi` key or
|
|
94
|
+
* the conventional directories; Claude reads the plugin component keys plus the
|
|
95
|
+
* default `skills/` directory. Kinds are normalized once; ownership is derived
|
|
96
|
+
* from the kind, and the deferred kinds are referenced only from
|
|
97
|
+
* `DEFERRED_COMPONENT_KINDS`, never re-listed here.
|
|
98
|
+
*/
|
|
99
|
+
export function buildComponentInventory(host, manifest) {
|
|
100
|
+
if (!isRecord(manifest)) {
|
|
101
|
+
return Object.freeze([]);
|
|
102
|
+
}
|
|
103
|
+
return host === "pi" ? buildPiInventory(manifest) : buildClaudeInventory(manifest);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One pass over the snapshot. Indexes `package.json` at the scope root and
|
|
107
|
+
* `.claude-plugin/*.json` anywhere under it. A dual-format repository yields
|
|
108
|
+
* one Pi descriptor and one Claude descriptor over the same skills directory.
|
|
109
|
+
* Refusals are warnings, not throws: a scoped Source URL refuses a Pi package,
|
|
110
|
+
* a Pi manifest below the scope root is not a bundle, and a Claude plugin with
|
|
111
|
+
* no in-repo marketplace binding is reported but not installable. A
|
|
112
|
+
* conventional Pi bundle must show Pi intent, because `skills/` and `themes/`
|
|
113
|
+
* are also other Hosts' default locations; see `conventionalPiBundle`.
|
|
114
|
+
*/
|
|
115
|
+
export function discoverBundlesInSnapshot(files, scope = {}) {
|
|
116
|
+
const scopeRoot = normalizeRoot(scope.directory ?? "");
|
|
117
|
+
const descriptors = [];
|
|
118
|
+
const rootPackage = files.find((file) => file.path === manifestPathFor(scopeRoot, "package.json"));
|
|
119
|
+
if (rootPackage !== undefined) {
|
|
120
|
+
const bundle = hasPiManifestKey(rootPackage.content)
|
|
121
|
+
? parsePiPackageManifest(rootPackage.content, scopeRoot)
|
|
122
|
+
: conventionalPiBundle(rootPackage.content, scopeRoot, files);
|
|
123
|
+
if (bundle !== undefined) {
|
|
124
|
+
descriptors.push(scope.directory === undefined
|
|
125
|
+
? bundle
|
|
126
|
+
: { ...withWarnings(bundle, ["Pi packages are not installable from a scoped Source; register the repository root"]), installable: false });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const pluginManifests = new Map();
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
const pluginRoot = pluginRootForPath(file.path);
|
|
132
|
+
if (pluginRoot !== undefined) {
|
|
133
|
+
pluginManifests.set(pluginRoot, file.content);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const boundPluginRoots = new Set();
|
|
137
|
+
const readPluginManifest = (pluginRoot) => {
|
|
138
|
+
boundPluginRoots.add(pluginRoot);
|
|
139
|
+
return pluginManifests.get(pluginRoot);
|
|
140
|
+
};
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
const marketplaceRoot = marketplaceRootForPath(file.path);
|
|
143
|
+
if (marketplaceRoot === undefined) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
descriptors.push(...parseClaudeMarketplace(file.content, marketplaceRoot, readPluginManifest));
|
|
147
|
+
}
|
|
148
|
+
for (const [pluginRoot, raw] of pluginManifests) {
|
|
149
|
+
if (boundPluginRoots.has(pluginRoot)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const descriptor = parseClaudePluginManifest(raw, pluginRoot);
|
|
153
|
+
descriptors.push({ ...withWarnings(descriptor, [
|
|
154
|
+
`Claude plugin ${JSON.stringify(pluginRoot)} has no in-repo marketplace binding; it is not installable`,
|
|
155
|
+
]), installable: false });
|
|
156
|
+
}
|
|
157
|
+
return Object.freeze(descriptors.map((descriptor) => populateSkillNames(descriptor, files)));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The owned-skill matcher shared by the guard module. A skill path is owned by
|
|
161
|
+
* a bundle when it sits under a `skill-category` component path, and is not
|
|
162
|
+
* claimed when it crosses an excluded lifecycle segment, matching the filter
|
|
163
|
+
* skill discovery applies.
|
|
164
|
+
*/
|
|
165
|
+
export function bundleOwnedSkillMatcher(descriptors) {
|
|
166
|
+
const owners = skillCategoryOwners(descriptors);
|
|
167
|
+
return (skillPath) => {
|
|
168
|
+
const normalized = normalizeSkillDirectory(skillPath);
|
|
169
|
+
if (normalized === undefined || hasLifecycleSegment(normalized)) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
for (const owner of owners) {
|
|
173
|
+
if (owner.roots.some((root) => matchesBundlePath(root, normalized))) {
|
|
174
|
+
return owner.descriptor;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The owned-skill matcher for one bare Skill directory name. Names come from
|
|
182
|
+
* `populateSkillNames`, so they are read from the same snapshot pass that fed
|
|
183
|
+
* discovery rather than from a hand-synced list, and the lifecycle filter is
|
|
184
|
+
* already applied where they are collected.
|
|
185
|
+
*/
|
|
186
|
+
export function bundleOwnedSkillNameMatcher(descriptors) {
|
|
187
|
+
const owners = skillCategoryOwners(descriptors);
|
|
188
|
+
return (skillName) => {
|
|
189
|
+
if (skillName === "" || skillName.includes("/") || skillName.includes("\\")) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
for (const owner of owners) {
|
|
193
|
+
if (owner.names.has(skillName)) {
|
|
194
|
+
return owner.descriptor;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function skillCategoryOwners(descriptors) {
|
|
201
|
+
return descriptors.map((descriptor) => {
|
|
202
|
+
const components = descriptor.components.filter((component) => component.kind === "skills" && component.ownership === "skill-category");
|
|
203
|
+
return {
|
|
204
|
+
descriptor,
|
|
205
|
+
roots: components.flatMap((component) => component.paths),
|
|
206
|
+
names: new Set(components.flatMap((component) => component.skillNames ?? [])),
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const COMPONENT_KIND_ORDER = [
|
|
211
|
+
"skills", "extensions", "prompts", "themes", "agents", "commands", "hooks",
|
|
212
|
+
"mcp-servers", "lsp-servers", "output-styles", "workflows", "monitors",
|
|
213
|
+
];
|
|
214
|
+
const COMPONENT_EFFECTS = Object.freeze({
|
|
215
|
+
skills: "instruction",
|
|
216
|
+
extensions: "executable",
|
|
217
|
+
prompts: "data",
|
|
218
|
+
themes: "data",
|
|
219
|
+
agents: "instruction",
|
|
220
|
+
commands: "instruction",
|
|
221
|
+
hooks: "executable",
|
|
222
|
+
"mcp-servers": "executable",
|
|
223
|
+
"lsp-servers": "executable",
|
|
224
|
+
"output-styles": "data",
|
|
225
|
+
workflows: "instruction",
|
|
226
|
+
monitors: "executable",
|
|
227
|
+
});
|
|
228
|
+
/**
|
|
229
|
+
* Conventional directories that show Pi intent on their own. `skills/` and
|
|
230
|
+
* `themes/` are also Claude Code default locations, so neither is evidence of
|
|
231
|
+
* Pi packaging by itself. `extensions/` and `prompts/` have no other Host's
|
|
232
|
+
* conventional meaning at a package root.
|
|
233
|
+
*/
|
|
234
|
+
const PI_INTENT_COMPONENT_KINDS = new Set(["extensions", "prompts"]);
|
|
235
|
+
const PI_COMPONENT_KEYS = Object.freeze([
|
|
236
|
+
["skills", "skills"],
|
|
237
|
+
["extensions", "extensions"],
|
|
238
|
+
["prompts", "prompts"],
|
|
239
|
+
["themes", "themes"],
|
|
240
|
+
]);
|
|
241
|
+
const CLAUDE_COMPONENT_KEYS = Object.freeze([
|
|
242
|
+
["agents", "agents"],
|
|
243
|
+
["commands", "commands"],
|
|
244
|
+
["hooks", "hooks"],
|
|
245
|
+
["mcpServers", "mcp-servers"],
|
|
246
|
+
["lspServers", "lsp-servers"],
|
|
247
|
+
["outputStyles", "output-styles"],
|
|
248
|
+
["workflows", "workflows"],
|
|
249
|
+
]);
|
|
250
|
+
function componentOwnership(kind) {
|
|
251
|
+
return kind === "skills" ? "skill-category" : "host-only";
|
|
252
|
+
}
|
|
253
|
+
function makeComponent(kind, paths) {
|
|
254
|
+
return Object.freeze({
|
|
255
|
+
kind,
|
|
256
|
+
ownership: componentOwnership(kind),
|
|
257
|
+
effect: COMPONENT_EFFECTS[kind],
|
|
258
|
+
paths: Object.freeze(dedupeStrings(paths)),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function buildPiInventory(manifest) {
|
|
262
|
+
const pi = isRecord(manifest.pi) ? manifest.pi : undefined;
|
|
263
|
+
if (pi !== undefined) {
|
|
264
|
+
const components = [];
|
|
265
|
+
for (const [key, kind] of PI_COMPONENT_KEYS) {
|
|
266
|
+
const paths = stringPaths(pi[key]);
|
|
267
|
+
if (paths.length > 0) {
|
|
268
|
+
components.push(makeComponent(kind, paths));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return Object.freeze(sortComponents(components));
|
|
272
|
+
}
|
|
273
|
+
// Conventional fallback: the four slots, pruned against the snapshot by discovery.
|
|
274
|
+
return Object.freeze(sortComponents(PI_COMPONENT_KEYS.map(([key, kind]) => makeComponent(kind, [key]))));
|
|
275
|
+
}
|
|
276
|
+
function buildClaudeInventory(manifest) {
|
|
277
|
+
const components = [];
|
|
278
|
+
// Skills add to the default skills/ directory rather than replacing it.
|
|
279
|
+
components.push(makeComponent("skills", ["skills", ...stringPaths(manifest.skills)]));
|
|
280
|
+
for (const [key, kind] of CLAUDE_COMPONENT_KEYS) {
|
|
281
|
+
const paths = stringPaths(manifest[key]);
|
|
282
|
+
if (paths.length > 0) {
|
|
283
|
+
components.push(makeComponent(kind, paths));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (isRecord(manifest.experimental)) {
|
|
287
|
+
const themes = stringPaths(manifest.experimental.themes);
|
|
288
|
+
if (themes.length > 0) {
|
|
289
|
+
components.push(makeComponent("themes", themes));
|
|
290
|
+
}
|
|
291
|
+
const monitors = stringPaths(manifest.experimental.monitors);
|
|
292
|
+
if (monitors.length > 0) {
|
|
293
|
+
components.push(makeComponent("monitors", monitors));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return Object.freeze(sortComponents(components));
|
|
297
|
+
}
|
|
298
|
+
function buildMarketplaceDescriptor(entry, label, marketplaceRoot, metadataPluginRoot, marketplaceRaw, readPluginManifest) {
|
|
299
|
+
const entryName = requiredName(entry, label);
|
|
300
|
+
if (entry.source === undefined) {
|
|
301
|
+
throw new PackageManifestError(`marketplace.json ${label}.source is required`);
|
|
302
|
+
}
|
|
303
|
+
const resolution = resolveMarketplaceSource(entry.source, marketplaceRoot, metadataPluginRoot);
|
|
304
|
+
if (resolution.kind === "command") {
|
|
305
|
+
return marketplaceOnlyDescriptor({
|
|
306
|
+
entry, entryName, marketplaceRoot, marketplaceRaw,
|
|
307
|
+
warnings: [`marketplace entry ${JSON.stringify(entryName)} uses a command source, which Agent Depot will not run; it is not installable`],
|
|
308
|
+
installable: false,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (resolution.kind === "external") {
|
|
312
|
+
return marketplaceOnlyDescriptor({
|
|
313
|
+
entry, entryName, marketplaceRoot, marketplaceRaw,
|
|
314
|
+
warnings: [`marketplace entry ${JSON.stringify(entryName)} points outside the registered repository; it is not installable`],
|
|
315
|
+
installable: false,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const pluginRoot = resolution.pluginRoot;
|
|
319
|
+
const pluginRaw = readPluginManifest(pluginRoot);
|
|
320
|
+
if (pluginRaw === undefined) {
|
|
321
|
+
return marketplaceOnlyDescriptor({
|
|
322
|
+
entry, entryName, marketplaceRoot, marketplaceRaw,
|
|
323
|
+
warnings: [`marketplace entry ${JSON.stringify(entryName)} has no plugin manifest at ${manifestPathFor(pluginRoot, ".claude-plugin/plugin.json")}`],
|
|
324
|
+
installable: true,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const plugin = parseClaudePluginManifest(pluginRaw, pluginRoot);
|
|
328
|
+
const warnings = [];
|
|
329
|
+
if (plugin.name !== entryName) {
|
|
330
|
+
warnings.push(`marketplace entry name ${JSON.stringify(entryName)} differs from plugin manifest name ${JSON.stringify(plugin.name)}`);
|
|
331
|
+
}
|
|
332
|
+
const entryVersion = manifestVersionEvidence(entry.version, "marketplace-entry");
|
|
333
|
+
const version = entryVersion ?? plugin.version;
|
|
334
|
+
return Object.freeze({
|
|
335
|
+
host: "claude",
|
|
336
|
+
coordinates: Object.freeze({ host: "claude", marketplaceRoot, pluginName: entryName }),
|
|
337
|
+
name: entryName,
|
|
338
|
+
...(version === undefined ? {} : { version }),
|
|
339
|
+
components: plugin.components,
|
|
340
|
+
warnings: Object.freeze(warnings),
|
|
341
|
+
installable: true,
|
|
342
|
+
manifestDigest: manifestDigest([
|
|
343
|
+
{ path: manifestPathFor(marketplaceRoot, ".claude-plugin/marketplace.json"), content: marketplaceRaw },
|
|
344
|
+
{ path: manifestPathFor(pluginRoot, ".claude-plugin/plugin.json"), content: pluginRaw },
|
|
345
|
+
]),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function marketplaceOnlyDescriptor(parts) {
|
|
349
|
+
const version = manifestVersionEvidence(parts.entry.version, "marketplace-entry");
|
|
350
|
+
return Object.freeze({
|
|
351
|
+
host: "claude",
|
|
352
|
+
coordinates: Object.freeze({ host: "claude", marketplaceRoot: parts.marketplaceRoot, pluginName: parts.entryName }),
|
|
353
|
+
name: parts.entryName,
|
|
354
|
+
...(version === undefined ? {} : { version }),
|
|
355
|
+
components: resolveComponentPaths(parts.marketplaceRoot, buildComponentInventory("claude", parts.entry)),
|
|
356
|
+
warnings: Object.freeze(parts.warnings),
|
|
357
|
+
installable: parts.installable,
|
|
358
|
+
manifestDigest: manifestDigest([{ path: manifestPathFor(parts.marketplaceRoot, ".claude-plugin/marketplace.json"), content: parts.marketplaceRaw }]),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function resolveMarketplaceSource(source, marketplaceRoot, metadataPluginRoot) {
|
|
362
|
+
if (typeof source === "string") {
|
|
363
|
+
if (source.startsWith("./") || source === ".") {
|
|
364
|
+
// `"."` and `"./"` name the marketplace root, so the plugin is the repository itself.
|
|
365
|
+
const relative = normalizeComponentPath(source);
|
|
366
|
+
return relative === ""
|
|
367
|
+
? { kind: "in-repo", pluginRoot: marketplaceRoot }
|
|
368
|
+
: { kind: "in-repo", pluginRoot: resolveBundlePath(marketplaceRoot, relative) };
|
|
369
|
+
}
|
|
370
|
+
const bare = normalizeComponentPath(source);
|
|
371
|
+
if (bare !== "" && metadataPluginRoot !== "") {
|
|
372
|
+
return { kind: "in-repo", pluginRoot: resolveBundlePath(marketplaceRoot, `${metadataPluginRoot}/${bare}`) };
|
|
373
|
+
}
|
|
374
|
+
return { kind: "external" };
|
|
375
|
+
}
|
|
376
|
+
if (isRecord(source)) {
|
|
377
|
+
return Object.hasOwn(source, "command") ? { kind: "command" } : { kind: "external" };
|
|
378
|
+
}
|
|
379
|
+
return { kind: "external" };
|
|
380
|
+
}
|
|
381
|
+
function resolveComponentPaths(root, components) {
|
|
382
|
+
const base = normalizeRoot(root);
|
|
383
|
+
if (base === "") {
|
|
384
|
+
return components;
|
|
385
|
+
}
|
|
386
|
+
return Object.freeze(components.map((component) => Object.freeze({
|
|
387
|
+
...component,
|
|
388
|
+
paths: Object.freeze(component.paths.map((entry) => `${base}/${entry}`)),
|
|
389
|
+
})));
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* A Pi bundle with no `pi` key is recognized only when the repository shows Pi
|
|
393
|
+
* intent: the `pi-package` keyword, or a Pi-native `extensions/` or `prompts/`
|
|
394
|
+
* directory that holds files in the snapshot. Returns undefined when the only
|
|
395
|
+
* conventional directory is shared with another Host, so an ordinary skill
|
|
396
|
+
* repository with a `package.json` is not reported as a Pi package.
|
|
397
|
+
*/
|
|
398
|
+
function conventionalPiBundle(raw, root, files) {
|
|
399
|
+
const pruned = pruneConventionalComponents(parsePiPackageManifest(raw, root), files);
|
|
400
|
+
if (pruned.components.length === 0) {
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
const intended = hasPiPackageKeyword(raw)
|
|
404
|
+
|| pruned.components.some((component) => PI_INTENT_COMPONENT_KINDS.has(component.kind));
|
|
405
|
+
return intended ? pruned : undefined;
|
|
406
|
+
}
|
|
407
|
+
function hasPiPackageKeyword(raw) {
|
|
408
|
+
const keywords = parseJsonObject(raw).keywords;
|
|
409
|
+
return Array.isArray(keywords) && keywords.some((keyword) => keyword === "pi-package");
|
|
410
|
+
}
|
|
411
|
+
function pruneConventionalComponents(descriptor, files) {
|
|
412
|
+
const conventionalKinds = new Set(["skills", "extensions", "prompts", "themes"]);
|
|
413
|
+
const components = descriptor.components.filter((component) => {
|
|
414
|
+
if (!conventionalKinds.has(component.kind)) {
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
return component.paths.some((directory) => files.some((file) => file.path === directory || file.path.startsWith(`${directory}/`)));
|
|
418
|
+
});
|
|
419
|
+
return Object.freeze({ ...descriptor, components: Object.freeze(components) });
|
|
420
|
+
}
|
|
421
|
+
function populateSkillNames(descriptor, files) {
|
|
422
|
+
const components = descriptor.components.map((component) => {
|
|
423
|
+
if (component.kind !== "skills") {
|
|
424
|
+
return component;
|
|
425
|
+
}
|
|
426
|
+
const names = new Set();
|
|
427
|
+
for (const file of files) {
|
|
428
|
+
if (path.posix.basename(file.path) !== "SKILL.md") {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const directory = path.posix.dirname(file.path);
|
|
432
|
+
if (directory === "." || hasLifecycleSegment(directory)) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (component.paths.some((spec) => matchesBundlePath(spec, directory))) {
|
|
436
|
+
names.add(path.posix.basename(directory));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (names.size === 0) {
|
|
440
|
+
return component;
|
|
441
|
+
}
|
|
442
|
+
return Object.freeze({ ...component, skillNames: Object.freeze([...names].sort()) });
|
|
443
|
+
});
|
|
444
|
+
return Object.freeze({ ...descriptor, components: Object.freeze(components) });
|
|
445
|
+
}
|
|
446
|
+
function matchesBundlePath(spec, skillDirectory) {
|
|
447
|
+
if (skillDirectory === spec) {
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
if (spec.includes("*")) {
|
|
451
|
+
return globToRegExp(spec).test(skillDirectory) || globToRegExp(`${spec}/*`).test(skillDirectory);
|
|
452
|
+
}
|
|
453
|
+
return skillDirectory.startsWith(`${spec}/`);
|
|
454
|
+
}
|
|
455
|
+
function globToRegExp(pattern) {
|
|
456
|
+
let out = "^";
|
|
457
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
458
|
+
const character = pattern[index];
|
|
459
|
+
if (character === "*") {
|
|
460
|
+
out += pattern[index + 1] === "*" ? ".*" : "[^/]*";
|
|
461
|
+
if (pattern[index + 1] === "*") {
|
|
462
|
+
index += 1;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
else if ("\\^$.|+()[]{}-".includes(character)) {
|
|
466
|
+
out += `\\${character}`;
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
out += character;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return new RegExp(`${out}$`, "u");
|
|
473
|
+
}
|
|
474
|
+
function manifestDigest(files) {
|
|
475
|
+
const sorted = [...files].sort((left, right) => left.path.localeCompare(right.path));
|
|
476
|
+
const hash = createHash("sha256");
|
|
477
|
+
hash.update("agent-depot-package-manifest-v1\0", "utf8");
|
|
478
|
+
for (const file of sorted) {
|
|
479
|
+
const pathBytes = Buffer.from(file.path, "utf8");
|
|
480
|
+
const contentBytes = Buffer.from(file.content, "utf8");
|
|
481
|
+
hash.update(`${pathBytes.byteLength}:`, "utf8");
|
|
482
|
+
hash.update(pathBytes);
|
|
483
|
+
hash.update(`${contentBytes.byteLength}:`, "utf8");
|
|
484
|
+
hash.update(contentBytes);
|
|
485
|
+
hash.update("\0", "utf8");
|
|
486
|
+
}
|
|
487
|
+
return hash.digest("hex");
|
|
488
|
+
}
|
|
489
|
+
function manifestVersionEvidence(value, declaredBy) {
|
|
490
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
return Object.freeze({ kind: "manifest-version", version: value, declaredBy });
|
|
494
|
+
}
|
|
495
|
+
function manifestName(manifest, root, fallback) {
|
|
496
|
+
if (typeof manifest.name === "string" && manifest.name.trim() !== "") {
|
|
497
|
+
return manifest.name.trim();
|
|
498
|
+
}
|
|
499
|
+
const base = path.posix.basename(root);
|
|
500
|
+
return base === "" || base === "." ? fallback : base;
|
|
501
|
+
}
|
|
502
|
+
function requiredName(manifest, label) {
|
|
503
|
+
if (typeof manifest.name !== "string" || manifest.name.trim() === "") {
|
|
504
|
+
throw new PackageManifestError(`${label} must define a non-empty name`);
|
|
505
|
+
}
|
|
506
|
+
return manifest.name.trim();
|
|
507
|
+
}
|
|
508
|
+
function stringPaths(value) {
|
|
509
|
+
if (typeof value === "string") {
|
|
510
|
+
const normalized = normalizeComponentPath(value);
|
|
511
|
+
return normalized === "" ? Object.freeze([]) : Object.freeze([normalized]);
|
|
512
|
+
}
|
|
513
|
+
if (!Array.isArray(value)) {
|
|
514
|
+
return Object.freeze([]);
|
|
515
|
+
}
|
|
516
|
+
const paths = [];
|
|
517
|
+
for (const entry of value) {
|
|
518
|
+
if (typeof entry !== "string" || entry.trim() === "" || entry.startsWith("!")) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const normalized = normalizeComponentPath(entry);
|
|
522
|
+
if (normalized !== "") {
|
|
523
|
+
paths.push(normalized);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return Object.freeze(paths);
|
|
527
|
+
}
|
|
528
|
+
function normalizeComponentPath(entry) {
|
|
529
|
+
const trimmed = entry.trim();
|
|
530
|
+
if (trimmed === "" || trimmed.includes("\\")) {
|
|
531
|
+
return "";
|
|
532
|
+
}
|
|
533
|
+
const normalized = path.posix.normalize(trimmed.replace(/^\.\//u, ""));
|
|
534
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/")) {
|
|
535
|
+
return "";
|
|
536
|
+
}
|
|
537
|
+
return normalized;
|
|
538
|
+
}
|
|
539
|
+
function normalizeRoot(root) {
|
|
540
|
+
const trimmed = root.trim();
|
|
541
|
+
if (trimmed === "" || trimmed === ".") {
|
|
542
|
+
return "";
|
|
543
|
+
}
|
|
544
|
+
const normalized = path.posix.normalize(trimmed.replace(/^\.\//u, ""));
|
|
545
|
+
return normalized === "." ? "" : normalized.replace(/\/+$/u, "");
|
|
546
|
+
}
|
|
547
|
+
function manifestPathFor(root, filename) {
|
|
548
|
+
const base = normalizeRoot(root);
|
|
549
|
+
return base === "" ? filename : `${base}/${filename}`;
|
|
550
|
+
}
|
|
551
|
+
function resolveBundlePath(root, componentPath) {
|
|
552
|
+
const base = normalizeRoot(root);
|
|
553
|
+
return base === "" ? componentPath : `${base}/${componentPath}`;
|
|
554
|
+
}
|
|
555
|
+
function marketplaceRootForPath(filePath) {
|
|
556
|
+
const marker = ".claude-plugin/marketplace.json";
|
|
557
|
+
if (filePath === marker) {
|
|
558
|
+
return "";
|
|
559
|
+
}
|
|
560
|
+
const suffix = `/${marker}`;
|
|
561
|
+
return filePath.endsWith(suffix) ? normalizeRoot(filePath.slice(0, -suffix.length)) : undefined;
|
|
562
|
+
}
|
|
563
|
+
function pluginRootForPath(filePath) {
|
|
564
|
+
const marker = ".claude-plugin/plugin.json";
|
|
565
|
+
if (filePath === marker) {
|
|
566
|
+
return "";
|
|
567
|
+
}
|
|
568
|
+
const suffix = `/${marker}`;
|
|
569
|
+
return filePath.endsWith(suffix) ? normalizeRoot(filePath.slice(0, -suffix.length)) : undefined;
|
|
570
|
+
}
|
|
571
|
+
function normalizeSkillDirectory(candidate) {
|
|
572
|
+
if (typeof candidate !== "string" || candidate.length === 0 || candidate.includes("\\")) {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
const normalized = path.posix.normalize(candidate);
|
|
576
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/")) {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
return normalized;
|
|
580
|
+
}
|
|
581
|
+
function hasLifecycleSegment(skillDirectory) {
|
|
582
|
+
return skillDirectory.split("/").some((segment) => EXCLUDED_LIFECYCLE_SEGMENTS.includes(segment));
|
|
583
|
+
}
|
|
584
|
+
function hasPiManifestKey(raw) {
|
|
585
|
+
const manifest = parseJsonObject(raw);
|
|
586
|
+
return isRecord(manifest.pi);
|
|
587
|
+
}
|
|
588
|
+
function withWarnings(descriptor, warnings) {
|
|
589
|
+
return Object.freeze({ ...descriptor, warnings: Object.freeze([...descriptor.warnings, ...warnings]) });
|
|
590
|
+
}
|
|
591
|
+
function dedupeStrings(values) {
|
|
592
|
+
return Object.freeze([...new Set(values)]);
|
|
593
|
+
}
|
|
594
|
+
function sortComponents(components) {
|
|
595
|
+
const order = new Map(COMPONENT_KIND_ORDER.map((kind, index) => [kind, index]));
|
|
596
|
+
return Object.freeze([...components].sort((left, right) => (order.get(left.kind) ?? 0) - (order.get(right.kind) ?? 0)));
|
|
597
|
+
}
|
|
598
|
+
function isRecord(value) {
|
|
599
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
600
|
+
}
|
|
601
|
+
function parseJsonObject(raw) {
|
|
602
|
+
let value;
|
|
603
|
+
try {
|
|
604
|
+
value = JSON.parse(raw);
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
const reason = error instanceof Error ? error.message : "the JSON could not be parsed";
|
|
608
|
+
throw new PackageManifestError(`manifest is not valid JSON (${reason})`);
|
|
609
|
+
}
|
|
610
|
+
if (!isRecord(value)) {
|
|
611
|
+
throw new PackageManifestError("manifest must be a JSON object");
|
|
612
|
+
}
|
|
613
|
+
return value;
|
|
614
|
+
}
|