diff --git a/README.md b/README.md index be8530b..5c53a44 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,10 @@ rudi search pdf # Search for packages rudi search --all # List all available packages rudi search --stacks # Filter to MCP stacks rudi search --binaries # Filter to CLI tools +rudi search --all --skills --category=web --role=operator +rudi search --all --skills --domain=real-estate +rudi list skills --capability=review +rudi info skill:vercel ``` ### Managing Secrets @@ -501,3 +505,46 @@ chmod 600 ~/.rudi/secrets.json ## License MIT + +## Skill categories and upgrade behavior + +Skills use seven primitive categories: web, code, data, documents, media, +communication and agents. Capability, domain and provider values are read from +`capability:`, `domain:` and `provider:` tags. Search and list accept +`--category`, `--capability`, `--domain`, `--provider` and `--role` filters. +`--role=workflow` refers to a skill's role; `--workflows` still selects the +separate workflow package kind. + +The primary operator role is derived from a stack's `related.operatorSkill`. +Requiring a stack does not make a workflow its operator. Skill JSON includes +`skillRole`, `operatorFor` and `facets`; installed inventory uses local/cached +catalog context without a network request and reports unknown role for external +or unidentified installs. `rudi info skill:` reads the actual skill entrypoint +and supports `--json`. + +Canonical skill packages install as `~/.rudi/skills//SKILL.md`; legacy flat +installs remain readable. Updating an owned skill stages the complete package, +verifies existing content against the installation checksum, replaces the +source and writes the new lock. Failed updates restore the prior source and +lock when recovery is safe. Edited files and missing ownership evidence are +preserved, even when update internally requests reinstall. `rudi update skill: +--dry-run --no-sync-skills` checks ownership and reports the proposed migration +without replacing content. Files excluded from historical checksums also block +replacement until their ownership is reconciled. + +Successful replacements retain the previous file/tree under a hidden transaction +folder, return `backupPath` in the install result and print it during updates. +This preserves late writes through already-open files. Reconcile these backups +before any separately authorized cleanup; the updater never deletes them automatically. + +Same-ID loose-file/folder collisions are explicit errors for path resolution +and native sync; `rudi list skills` exposes their paths for reconciliation. +If a concurrent edit prevents rollback, preserve the reported transaction +folder and resolve its contents and `.install-lock` before retrying. Do not +remove a recovery guard until its prior package and edited replacement have +been accounted for. + +Native projections retain complete trigger descriptions and bundled Codex +metadata. Ownership receipts, exact force scope, conflict preservation and +restartRequired reporting continue to govern native updates. Deploy the +compatible CLI before publishing a registry that migrates existing flat skills. diff --git a/dist/index.cjs b/dist/index.cjs index ed1ef8a..0c3d92b 100755 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -266,16 +266,29 @@ function readSkillCandidates(root) { function discoverSkillPackages(options = {}) { const byName = /* @__PURE__ */ new Map(); for (const root of getSkillDiscoveryRoots(options)) { - for (const candidate of readSkillCandidates(root)) { + const candidates = readSkillCandidates(root).sort((left, right) => left.packagePath.localeCompare(right.packagePath)); + for (const candidate of candidates) { if (!byName.has(candidate.name)) { byName.set(candidate.name, candidate); + } else { + const existing = byName.get(candidate.name); + if (existing.source === candidate.source) { + existing.conflictingPaths = [.../* @__PURE__ */ new Set([ + ...existing.conflictingPaths || [existing.packagePath], + candidate.packagePath + ])].sort(); + } } } } return Array.from(byName.values()); } function findLocalSkillPackage(name) { - return discoverSkillPackages().find((skill) => skill.name === name) || null; + const skill = discoverSkillPackages().find((candidate) => candidate.name === name) || null; + if (skill?.conflictingPaths) { + throw new Error(`Conflicting skill formats for skill:${name}: ${skill.conflictingPaths.join(", ")}`); + } + return skill; } function getPackagePath(id) { const [kind, name] = parsePackageId(id); @@ -1181,6 +1194,60 @@ var init_github_source = __esm({ } }); +// packages/registry-client/src/skill-facets.js +function describeSkill(pkg, index, { catalogIdentity = true } = {}) { + if (pkg.kind !== "skill") return pkg; + const tags = Array.isArray(pkg.tags) ? pkg.tags : Array.isArray(pkg.meta?.tags) ? pkg.meta.tags : []; + const facets = { capabilities: [], domains: [], providers: [] }; + for (const tag of tags) { + if (typeof tag !== "string") continue; + const [namespace, value, extra] = tag.split(":"); + if (Object.hasOwn(FACET_KEYS, namespace) && SLUG.test(value || "") && extra === void 0) { + facets[FACET_KEYS[namespace]].push(value); + } + } + for (const key of Object.values(FACET_KEYS)) facets[key] = [...new Set(facets[key])].sort(); + const registered = catalogIdentity && index?.packages?.[pkg.id]?.kind === "skill" && index.packages[pkg.id].id === pkg.id; + const operatorFor = registered ? Object.entries(index.packages).filter(([id, value]) => id.startsWith("stack:") && value?.id === id && value.kind === "stack" && value.related?.operatorSkill === pkg.id).map(([id]) => id).sort() : []; + return { + ...pkg, + category: pkg.category || pkg.meta?.category, + tags, + facets, + skillRole: registered ? operatorFor.length > 0 ? "operator" : "workflow" : "unknown", + operatorFor + }; +} +function normalizeSkillFilters(options = {}) { + const filters = {}; + for (const key of ["category", "role", ...Object.keys(FACET_KEYS)]) { + if (options[key] === void 0) continue; + if (typeof options[key] !== "string" || !SLUG.test(options[key])) { + throw new Error(`--${key} requires a lowercase category or facet name`); + } + if (key === "role" && !["operator", "workflow", "unknown"].includes(options[key])) { + throw new Error("--role must be operator, workflow, or unknown"); + } + filters[key] = options[key]; + } + return filters; +} +function matchesSkillFilters(pkg, filters) { + if (filters.category && (pkg.category || pkg.meta?.category) !== filters.category) return false; + if (filters.role && pkg.skillRole !== filters.role) return false; + for (const [filter, facet] of Object.entries(FACET_KEYS)) { + if (filters[filter] && !pkg.facets?.[facet]?.includes(filters[filter])) return false; + } + return true; +} +var FACET_KEYS, SLUG; +var init_skill_facets = __esm({ + "packages/registry-client/src/skill-facets.js"() { + FACET_KEYS = { capability: "capabilities", domain: "domains", provider: "providers" }; + SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; + } +}); + // packages/registry-client/src/index.js function assertCommandArg(value, label) { if (typeof value !== "string" || value.length === 0 || value.includes("\0")) { @@ -1397,7 +1464,7 @@ async function fetchRemoteRegistryIndex(url) { detectRegistrySchema(index); return index; } -function getCachedIndex() { +function getCachedIndex({ allowExpired = false } = {}) { const cachePath = PATHS2.registryCache; if (!import_fs2.default.existsSync(cachePath)) { return null; @@ -1405,7 +1472,7 @@ function getCachedIndex() { try { const stat = import_fs2.default.statSync(cachePath); const age = Date.now() - stat.mtimeMs; - if (age > CACHE_TTL) { + if (!allowExpired && age > CACHE_TTL) { return null; } return JSON.parse(import_fs2.default.readFileSync(cachePath, "utf-8")); @@ -1413,6 +1480,16 @@ function getCachedIndex() { return null; } } +function getAvailableRegistryIndex() { + const index = getLocalIndex()?.index || getCachedIndex({ allowExpired: true }); + if (!index) return null; + try { + detectRegistrySchema(index); + return index; + } catch { + return null; + } +} function cacheIndex(index) { const cachePath = PATHS2.registryCache; const cacheDir = import_path3.default.dirname(cachePath); @@ -1453,14 +1530,16 @@ function clearCache() { } async function searchPackages(query, options = {}) { const { kind } = options; + const filters = normalizeSkillFilters(options); const index = await fetchIndex(); const results = []; const queryLower = query.toLowerCase(); const kinds = kind ? [kind] : PACKAGE_KINDS3; for (const k of kinds) { const packages = listRegistryPackages(index, k); - for (const pkg of packages) { - if (matchesQuery(pkg, queryLower)) { + for (const raw of packages) { + const pkg = describeSkill(raw, index); + if (matchesQuery(pkg, queryLower) && matchesSkillFilters(pkg, filters)) { results.push({ ...pkg, kind: k }); } } @@ -1472,6 +1551,7 @@ function matchesQuery(pkg, query) { pkg.id || "", pkg.name || "", pkg.description || "", + pkg.category || "", ...pkg.tags || [] ].join(" ").toLowerCase(); return searchable.includes(query); @@ -1531,9 +1611,10 @@ async function getManifest(pkg) { throw new Error(`Failed to fetch registry manifest ${manifestPath}: ${err.message}`); } } -async function listPackages(kind) { +async function listPackages(kind, options = {}) { + const filters = normalizeSkillFilters(options); const index = await fetchIndex(); - return listRegistryPackages(index, kind); + return listRegistryPackages(index, kind).map((pkg) => describeSkill(pkg, index)).filter((pkg) => matchesSkillFilters(pkg, filters)); } function resolvedBinEntries(bins, packageId) { const entries = Array.isArray(bins) ? bins.map((name) => ({ name, path: name })) : Object.entries(bins || {}).map(([name, config]) => ({ @@ -2261,6 +2342,8 @@ var init_src3 = __esm({ init_src2(); init_registry_contract(); init_github_source(); + init_skill_facets(); + init_skill_facets(); init_registry_contract(); init_github_source(); DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/learnrudi/registry/main/index.json"; @@ -2349,17 +2432,17 @@ var require_visit = __commonJS({ visit.BREAK = BREAK; visit.SKIP = SKIP; visit.REMOVE = REMOVE; - function visit_(key, node, visitor, path55) { - const ctrl = callVisitor(key, node, visitor, path55); + function visit_(key, node, visitor, path56) { + const ctrl = callVisitor(key, node, visitor, path56); if (identity.isNode(ctrl) || identity.isPair(ctrl)) { - replaceNode(key, path55, ctrl); - return visit_(key, ctrl, visitor, path55); + replaceNode(key, path56, ctrl); + return visit_(key, ctrl, visitor, path56); } if (typeof ctrl !== "symbol") { if (identity.isCollection(node)) { - path55 = Object.freeze(path55.concat(node)); + path56 = Object.freeze(path56.concat(node)); for (let i = 0; i < node.items.length; ++i) { - const ci = visit_(i, node.items[i], visitor, path55); + const ci = visit_(i, node.items[i], visitor, path56); if (typeof ci === "number") i = ci - 1; else if (ci === BREAK) @@ -2370,13 +2453,13 @@ var require_visit = __commonJS({ } } } else if (identity.isPair(node)) { - path55 = Object.freeze(path55.concat(node)); - const ck = visit_("key", node.key, visitor, path55); + path56 = Object.freeze(path56.concat(node)); + const ck = visit_("key", node.key, visitor, path56); if (ck === BREAK) return BREAK; else if (ck === REMOVE) node.key = null; - const cv = visit_("value", node.value, visitor, path55); + const cv = visit_("value", node.value, visitor, path56); if (cv === BREAK) return BREAK; else if (cv === REMOVE) @@ -2397,17 +2480,17 @@ var require_visit = __commonJS({ visitAsync.BREAK = BREAK; visitAsync.SKIP = SKIP; visitAsync.REMOVE = REMOVE; - async function visitAsync_(key, node, visitor, path55) { - const ctrl = await callVisitor(key, node, visitor, path55); + async function visitAsync_(key, node, visitor, path56) { + const ctrl = await callVisitor(key, node, visitor, path56); if (identity.isNode(ctrl) || identity.isPair(ctrl)) { - replaceNode(key, path55, ctrl); - return visitAsync_(key, ctrl, visitor, path55); + replaceNode(key, path56, ctrl); + return visitAsync_(key, ctrl, visitor, path56); } if (typeof ctrl !== "symbol") { if (identity.isCollection(node)) { - path55 = Object.freeze(path55.concat(node)); + path56 = Object.freeze(path56.concat(node)); for (let i = 0; i < node.items.length; ++i) { - const ci = await visitAsync_(i, node.items[i], visitor, path55); + const ci = await visitAsync_(i, node.items[i], visitor, path56); if (typeof ci === "number") i = ci - 1; else if (ci === BREAK) @@ -2418,13 +2501,13 @@ var require_visit = __commonJS({ } } } else if (identity.isPair(node)) { - path55 = Object.freeze(path55.concat(node)); - const ck = await visitAsync_("key", node.key, visitor, path55); + path56 = Object.freeze(path56.concat(node)); + const ck = await visitAsync_("key", node.key, visitor, path56); if (ck === BREAK) return BREAK; else if (ck === REMOVE) node.key = null; - const cv = await visitAsync_("value", node.value, visitor, path55); + const cv = await visitAsync_("value", node.value, visitor, path56); if (cv === BREAK) return BREAK; else if (cv === REMOVE) @@ -2451,23 +2534,23 @@ var require_visit = __commonJS({ } return visitor; } - function callVisitor(key, node, visitor, path55) { + function callVisitor(key, node, visitor, path56) { if (typeof visitor === "function") - return visitor(key, node, path55); + return visitor(key, node, path56); if (identity.isMap(node)) - return visitor.Map?.(key, node, path55); + return visitor.Map?.(key, node, path56); if (identity.isSeq(node)) - return visitor.Seq?.(key, node, path55); + return visitor.Seq?.(key, node, path56); if (identity.isPair(node)) - return visitor.Pair?.(key, node, path55); + return visitor.Pair?.(key, node, path56); if (identity.isScalar(node)) - return visitor.Scalar?.(key, node, path55); + return visitor.Scalar?.(key, node, path56); if (identity.isAlias(node)) - return visitor.Alias?.(key, node, path55); + return visitor.Alias?.(key, node, path56); return void 0; } - function replaceNode(key, path55, node) { - const parent = path55[path55.length - 1]; + function replaceNode(key, path56, node) { + const parent = path56[path56.length - 1]; if (identity.isCollection(parent)) { parent.items[key] = node; } else if (identity.isPair(parent)) { @@ -3077,10 +3160,10 @@ var require_Collection = __commonJS({ var createNode = require_createNode(); var identity = require_identity(); var Node = require_Node(); - function collectionFromPath(schema, path55, value) { + function collectionFromPath(schema, path56, value) { let v = value; - for (let i = path55.length - 1; i >= 0; --i) { - const k = path55[i]; + for (let i = path56.length - 1; i >= 0; --i) { + const k = path56[i]; if (typeof k === "number" && Number.isInteger(k) && k >= 0) { const a = []; a[k] = v; @@ -3099,7 +3182,7 @@ var require_Collection = __commonJS({ sourceObjects: /* @__PURE__ */ new Map() }); } - var isEmptyPath = (path55) => path55 == null || typeof path55 === "object" && !!path55[Symbol.iterator]().next().done; + var isEmptyPath = (path56) => path56 == null || typeof path56 === "object" && !!path56[Symbol.iterator]().next().done; var Collection = class extends Node.NodeBase { constructor(type, schema) { super(type); @@ -3129,11 +3212,11 @@ var require_Collection = __commonJS({ * be a Pair instance or a `{ key, value }` object, which may not have a key * that already exists in the map. */ - addIn(path55, value) { - if (isEmptyPath(path55)) + addIn(path56, value) { + if (isEmptyPath(path56)) this.add(value); else { - const [key, ...rest] = path55; + const [key, ...rest] = path56; const node = this.get(key, true); if (identity.isCollection(node)) node.addIn(rest, value); @@ -3147,8 +3230,8 @@ var require_Collection = __commonJS({ * Removes a value from the collection. * @returns `true` if the item was found and removed. */ - deleteIn(path55) { - const [key, ...rest] = path55; + deleteIn(path56) { + const [key, ...rest] = path56; if (rest.length === 0) return this.delete(key); const node = this.get(key, true); @@ -3162,8 +3245,8 @@ var require_Collection = __commonJS({ * scalar values from their surrounding node; to disable set `keepScalar` to * `true` (collections are always returned intact). */ - getIn(path55, keepScalar) { - const [key, ...rest] = path55; + getIn(path56, keepScalar) { + const [key, ...rest] = path56; const node = this.get(key, true); if (rest.length === 0) return !keepScalar && identity.isScalar(node) ? node.value : node; @@ -3181,8 +3264,8 @@ var require_Collection = __commonJS({ /** * Checks if the collection includes a value with the key `key`. */ - hasIn(path55) { - const [key, ...rest] = path55; + hasIn(path56) { + const [key, ...rest] = path56; if (rest.length === 0) return this.has(key); const node = this.get(key, true); @@ -3192,8 +3275,8 @@ var require_Collection = __commonJS({ * Sets a value in this collection. For `!!set`, `value` needs to be a * boolean to add/remove the item from the set. */ - setIn(path55, value) { - const [key, ...rest] = path55; + setIn(path56, value) { + const [key, ...rest] = path56; if (rest.length === 0) { this.set(key, value); } else { @@ -5708,9 +5791,9 @@ var require_Document = __commonJS({ this.contents.add(value); } /** Adds a value to the document. */ - addIn(path55, value) { + addIn(path56, value) { if (assertCollection(this.contents)) - this.contents.addIn(path55, value); + this.contents.addIn(path56, value); } /** * Create a new `Alias` node, ensuring that the target `node` has the required anchor. @@ -5785,14 +5868,14 @@ var require_Document = __commonJS({ * Removes a value from the document. * @returns `true` if the item was found and removed. */ - deleteIn(path55) { - if (Collection.isEmptyPath(path55)) { + deleteIn(path56) { + if (Collection.isEmptyPath(path56)) { if (this.contents == null) return false; this.contents = null; return true; } - return assertCollection(this.contents) ? this.contents.deleteIn(path55) : false; + return assertCollection(this.contents) ? this.contents.deleteIn(path56) : false; } /** * Returns item at `key`, or `undefined` if not found. By default unwraps @@ -5807,10 +5890,10 @@ var require_Document = __commonJS({ * scalar values from their surrounding node; to disable set `keepScalar` to * `true` (collections are always returned intact). */ - getIn(path55, keepScalar) { - if (Collection.isEmptyPath(path55)) + getIn(path56, keepScalar) { + if (Collection.isEmptyPath(path56)) return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents; - return identity.isCollection(this.contents) ? this.contents.getIn(path55, keepScalar) : void 0; + return identity.isCollection(this.contents) ? this.contents.getIn(path56, keepScalar) : void 0; } /** * Checks if the document includes a value with the key `key`. @@ -5821,10 +5904,10 @@ var require_Document = __commonJS({ /** * Checks if the document includes a value at `path`. */ - hasIn(path55) { - if (Collection.isEmptyPath(path55)) + hasIn(path56) { + if (Collection.isEmptyPath(path56)) return this.contents !== void 0; - return identity.isCollection(this.contents) ? this.contents.hasIn(path55) : false; + return identity.isCollection(this.contents) ? this.contents.hasIn(path56) : false; } /** * Sets a value in this document. For `!!set`, `value` needs to be a @@ -5841,13 +5924,13 @@ var require_Document = __commonJS({ * Sets a value in this document. For `!!set`, `value` needs to be a * boolean to add/remove the item from the set. */ - setIn(path55, value) { - if (Collection.isEmptyPath(path55)) { + setIn(path56, value) { + if (Collection.isEmptyPath(path56)) { this.contents = value; } else if (this.contents == null) { - this.contents = Collection.collectionFromPath(this.schema, Array.from(path55), value); + this.contents = Collection.collectionFromPath(this.schema, Array.from(path56), value); } else if (assertCollection(this.contents)) { - this.contents.setIn(path55, value); + this.contents.setIn(path56, value); } } /** @@ -7807,9 +7890,9 @@ var require_cst_visit = __commonJS({ visit.BREAK = BREAK; visit.SKIP = SKIP; visit.REMOVE = REMOVE; - visit.itemAtPath = (cst, path55) => { + visit.itemAtPath = (cst, path56) => { let item = cst; - for (const [field, index] of path55) { + for (const [field, index] of path56) { const tok = item?.[field]; if (tok && "items" in tok) { item = tok.items[index]; @@ -7818,23 +7901,23 @@ var require_cst_visit = __commonJS({ } return item; }; - visit.parentCollection = (cst, path55) => { - const parent = visit.itemAtPath(cst, path55.slice(0, -1)); - const field = path55[path55.length - 1][0]; + visit.parentCollection = (cst, path56) => { + const parent = visit.itemAtPath(cst, path56.slice(0, -1)); + const field = path56[path56.length - 1][0]; const coll = parent?.[field]; if (coll && "items" in coll) return coll; throw new Error("Parent collection not found"); }; - function _visit(path55, item, visitor) { - let ctrl = visitor(item, path55); + function _visit(path56, item, visitor) { + let ctrl = visitor(item, path56); if (typeof ctrl === "symbol") return ctrl; for (const field of ["key", "value"]) { const token = item[field]; if (token && "items" in token) { for (let i = 0; i < token.items.length; ++i) { - const ci = _visit(Object.freeze(path55.concat([[field, i]])), token.items[i], visitor); + const ci = _visit(Object.freeze(path56.concat([[field, i]])), token.items[i], visitor); if (typeof ci === "number") i = ci - 1; else if (ci === BREAK) @@ -7845,10 +7928,10 @@ var require_cst_visit = __commonJS({ } } if (typeof ctrl === "function" && field === "key") - ctrl = ctrl(item, path55); + ctrl = ctrl(item, path56); } } - return typeof ctrl === "function" ? ctrl(item, path55) : ctrl; + return typeof ctrl === "function" ? ctrl(item, path56) : ctrl; } exports2.visit = visit; } @@ -9150,14 +9233,14 @@ var require_parser = __commonJS({ case "scalar": case "single-quoted-scalar": case "double-quoted-scalar": { - const fs52 = this.flowScalar(this.type); + const fs53 = this.flowScalar(this.type); if (atNextItem || it.value) { - map.items.push({ start, key: fs52, sep: [] }); + map.items.push({ start, key: fs53, sep: [] }); this.onKeyLine = true; } else if (it.sep) { - this.stack.push(fs52); + this.stack.push(fs53); } else { - Object.assign(it, { key: fs52, sep: [] }); + Object.assign(it, { key: fs53, sep: [] }); this.onKeyLine = true; } return; @@ -9285,13 +9368,13 @@ var require_parser = __commonJS({ case "scalar": case "single-quoted-scalar": case "double-quoted-scalar": { - const fs52 = this.flowScalar(this.type); + const fs53 = this.flowScalar(this.type); if (!it || it.value) - fc.items.push({ start: [], key: fs52, sep: [] }); + fc.items.push({ start: [], key: fs53, sep: [] }); else if (it.sep) - this.stack.push(fs52); + this.stack.push(fs53); else - Object.assign(it, { key: fs52, sep: [] }); + Object.assign(it, { key: fs53, sep: [] }); return; } case "flow-map-end": @@ -9697,7 +9780,7 @@ async function verifyLockfile(id) { errors }; } -function updateContentHash(hash, rootPath, currentPath) { +function updateContentHash(hash, rootPath, currentPath, includeIgnored) { const relativePath = import_path4.default.relative(rootPath, currentPath).split(import_path4.default.sep).join("/"); const stat = import_fs3.default.lstatSync(currentPath); if (stat.isSymbolicLink()) { @@ -9714,11 +9797,11 @@ function updateContentHash(hash, rootPath, currentPath) { } hash.update(`dir\0${relativePath}\0`); for (const entry of import_fs3.default.readdirSync(currentPath).sort()) { - if (CHECKSUM_IGNORED_NAMES.has(entry) || currentPath === rootPath && CHECKSUM_IGNORED_ROOT_NAMES.has(entry)) continue; - updateContentHash(hash, rootPath, import_path4.default.join(currentPath, entry)); + if (!includeIgnored && (CHECKSUM_IGNORED_NAMES.has(entry) || currentPath === rootPath && CHECKSUM_IGNORED_ROOT_NAMES.has(entry))) continue; + updateContentHash(hash, rootPath, import_path4.default.join(currentPath, entry), includeIgnored); } } -async function computeInstalledContentChecksum(installPath) { +async function computeInstalledContentChecksum(installPath, { includeIgnored = false } = {}) { if (!import_fs3.default.existsSync(installPath)) { throw new Error(`Cannot checksum missing installed package: ${installPath}`); } @@ -9729,7 +9812,7 @@ async function computeInstalledContentChecksum(installPath) { hash.update(`file\0.\0${executable}\0`); hash.update(import_fs3.default.readFileSync(installPath)); } else { - updateContentHash(hash, installPath, installPath); + updateContentHash(hash, installPath, installPath, includeIgnored); } return hash.digest("hex"); } @@ -10194,6 +10277,55 @@ var init_resolver = __esm({ } }); +// packages/core/src/package-metadata.js +function parsePackageMetadata(content) { + const parsed = (0, import_yaml2.parse)(content, { maxAliasCount: 50 }); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("Package metadata must be a mapping"); + } + const metadata = {}; + for (const field of ["name", "description", "version", "category", "icon"]) { + if (parsed[field] === void 0) continue; + if (typeof parsed[field] !== "string") { + throw new Error(`Package metadata ${field} must be a string`); + } + metadata[field] = parsed[field]; + } + const stringList = (value, field) => { + if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) { + throw new Error(`Package metadata ${field} must be a string list`); + } + return value; + }; + if (parsed.tags !== void 0) metadata.tags = stringList(parsed.tags, "tags"); + if (parsed.requires !== void 0) { + if (!parsed.requires || typeof parsed.requires !== "object" || Array.isArray(parsed.requires)) { + throw new Error("Package metadata requires must be a mapping"); + } + metadata.requires = {}; + for (const kind of ["stacks", "skills"]) { + if (parsed.requires[kind] !== void 0) { + metadata.requires[kind] = stringList(parsed.requires[kind], `requires.${kind}`); + } + } + } + return metadata; +} +function parseSkillDocument(content = "") { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!match) return { metadata: {}, body: content.trimStart() }; + return { + metadata: parsePackageMetadata(match[1]), + body: content.slice(match[0].length).trimStart() + }; +} +var import_yaml2; +var init_package_metadata = __esm({ + "packages/core/src/package-metadata.js"() { + import_yaml2 = __toESM(require_dist(), 1); + } +}); + // packages/core/src/shims.js var shims_exports = {}; __export(shims_exports, { @@ -10432,14 +10564,197 @@ var init_shims = __esm({ } }); +// packages/core/src/skill-install.js +function statIfPresent(file) { + try { + return import_node_fs2.default.lstatSync(file); + } catch (error) { + if (error.code === "ENOENT") return null; + throw error; + } +} +function assertRealParents(target) { + const relative4 = import_node_path2.default.relative(PATHS.home, target); + if (relative4.startsWith("..") || import_node_path2.default.isAbsolute(relative4)) { + throw new Error("Skill install path escapes RUDI home"); + } + let current = PATHS.home; + for (const segment of ["", ...relative4.split(import_node_path2.default.sep)]) { + if (segment) current = import_node_path2.default.join(current, segment); + const stat = statIfPresent(current); + if (stat?.isSymbolicLink()) throw new Error(`Refusing symlinked skill install path: ${current}`); + } +} +function inspectPreviousInstall(pkg, destination) { + const slug = pkg.id.slice("skill:".length); + const candidates = [import_node_path2.default.join(PATHS.skills, slug), import_node_path2.default.join(PATHS.skills, `${slug}.md`)]; + for (const candidate of [...candidates, getLockfilePath(pkg.id)]) assertRealParents(candidate); + const existing = candidates.filter((candidate) => statIfPresent(candidate)); + if (existing.length > 1) { + throw new Error(`Conflicting skill formats for ${pkg.id}; preserve and reconcile ${existing.join(" and ")}`); + } + const previousPath = existing[0] || null; + const previousLockfile = readLockfile(pkg.id); + if (previousPath) { + const stat = import_node_fs2.default.lstatSync(previousPath); + const layout = stat.isDirectory() ? "directory" : stat.isFile() ? "file" : null; + if (!layout || previousLockfile?.id !== pkg.id || !/^[a-f0-9]{64}$/i.test(previousLockfile?.checksum || "") || previousLockfile.installLayout && previousLockfile.installLayout !== layout) { + throw new Error(`Cannot prove ownership of ${pkg.id}; preserving ${previousPath}`); + } + } + return { id: pkg.id, destination, previousPath, previousLockfile }; +} +async function assertUnchanged(state, candidate = state.previousPath) { + if (!candidate) return; + const digest = await computeInstalledContentChecksum(candidate); + if (digest !== await computeInstalledContentChecksum(candidate, { includeIgnored: true })) { + throw new Error(`Untracked skill content excluded from the ownership checksum; preserving ${candidate}`); + } + if (digest !== state.previousLockfile.checksum) { + throw new Error(`Modified skill ${state.id}; preserving local content at ${candidate}`); + } +} +async function assertReplacementUnchanged(state) { + assertRealParents(state.destination); + if (await computeInstalledContentChecksum(state.destination, { includeIgnored: true }) !== state.replacementDigest) { + throw new Error(`Replacement changed concurrently; preserving ${state.destination}`); + } +} +async function restorePrevious(state) { + if (state.installed) { + await assertReplacementUnchanged(state); + import_node_fs2.default.rmSync(state.destination, { recursive: true, force: true }); + } + if (state.movedPrevious) { + if (statIfPresent(state.previousPath)) { + throw new Error(`Recovery conflict; previous skill is preserved at ${state.backupPath}`); + } + import_node_fs2.default.renameSync(state.backupPath, state.previousPath); + } + if (state.lockWriteAttempted) restoreLockfile(state.id, state.previousLockfile); +} +async function replaceSkill(pkg, state, onProgress) { + const stage = import_node_path2.default.join(state.transactionRoot, "next"); + await downloadPackage(pkg, stage, { onProgress }); + const directory = !pkg.path.replaceAll("\\", "/").endsWith(".md"); + const entry = directory ? import_node_path2.default.join(stage, "SKILL.md") : stage; + if (!statIfPresent(entry)?.isFile()) throw new Error(`Downloaded ${pkg.id} has no regular skill entrypoint`); + const content = import_node_fs2.default.readFileSync(entry, "utf8"); + const { metadata } = parseSkillDocument(content); + if (!metadata.name?.trim() || !metadata.description?.trim()) { + throw new Error(`Downloaded ${pkg.id} requires name and description metadata`); + } + if (directory) { + const nativeMetadata = import_node_path2.default.join(stage, "agents/openai.yaml"); + if (statIfPresent(nativeMetadata)) { + if (!import_node_fs2.default.lstatSync(nativeMetadata).isFile()) throw new Error("Native metadata must be a regular file"); + const parsed = (0, import_yaml3.parse)(import_node_fs2.default.readFileSync(nativeMetadata, "utf8"), { maxAliasCount: 50 }); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("Native metadata must be a YAML mapping"); + } + } + } + state.replacementDigest = await computeInstalledContentChecksum(stage, { includeIgnored: true }); + await assertUnchanged(state); + if (state.previousPath) { + import_node_fs2.default.renameSync(state.previousPath, state.backupPath); + state.movedPrevious = true; + await assertUnchanged(state, state.backupPath); + } + if (statIfPresent(state.destination)) throw new Error(`Skill destination changed during install: ${state.destination}`); + import_node_fs2.default.renameSync(stage, state.destination); + state.installed = true; + onProgress?.({ phase: "lockfile", package: pkg.id }); + await assertReplacementUnchanged(state); + assertRealParents(getLockfilePath(pkg.id)); + state.lockWriteAttempted = true; + await writeLockfile(pkg, { installPath: state.destination }); + await assertReplacementUnchanged(state); +} +async function inspectRegistrySkillUpdate(pkg, destination) { + if (!/^skill:[a-z0-9]+(?:-[a-z0-9]+)*$/.test(pkg.id) || typeof pkg.path !== "string") { + throw new Error("Invalid registry skill identity or source path"); + } + const guardPath = import_node_path2.default.join(PATHS.skills, `.${pkg.id.slice(6)}.install-lock`); + assertRealParents(guardPath); + if (statIfPresent(guardPath)) throw new Error(`Skill install already active or awaiting recovery: ${guardPath}`); + const state = inspectPreviousInstall(pkg, destination); + await assertUnchanged(state); + return { + id: pkg.id, + from: state.previousPath, + to: destination, + action: state.previousPath && state.previousPath !== destination ? "migrate" : "update" + }; +} +async function installRegistrySkill(pkg, destination, { onProgress } = {}) { + if (!/^skill:[a-z0-9]+(?:-[a-z0-9]+)*$/.test(pkg.id) || typeof pkg.path !== "string") { + throw new Error("Invalid registry skill identity or source path"); + } + assertRealParents(PATHS.skills); + const guardPath = import_node_path2.default.join(PATHS.skills, `.${pkg.id.slice(6)}.install-lock`); + try { + import_node_fs2.default.mkdirSync(guardPath); + } catch (error) { + if (error.code === "EEXIST") throw new Error(`Skill install already active or awaiting recovery: ${guardPath}`); + throw error; + } + let state; + let recoveryFailed = false; + try { + state = inspectPreviousInstall(pkg, destination); + await assertUnchanged(state); + state.transactionRoot = import_node_fs2.default.mkdtempSync(import_node_path2.default.join(PATHS.skills, `.${pkg.id.slice(6)}.install-`)); + state.backupPath = import_node_path2.default.join(state.transactionRoot, "previous"); + await replaceSkill(pkg, state, onProgress); + onProgress?.({ phase: "installed", package: pkg.id, path: destination }); + return { + success: true, + id: pkg.id, + path: destination, + lockfileWritten: true, + ...state.movedPrevious ? { backupPath: state.backupPath } : {} + }; + } catch (error) { + if (state) { + try { + await restorePrevious(state); + } catch (recoveryError) { + recoveryFailed = true; + throw new Error(`${error.message}; recovery failed: ${recoveryError.message}. Preserve ${state.transactionRoot}`); + } + } + throw error; + } finally { + if (!recoveryFailed) { + if (state?.transactionRoot && !statIfPresent(state.backupPath)) { + import_node_fs2.default.rmSync(state.transactionRoot, { recursive: true, force: true }); + } + import_node_fs2.default.rmdirSync(guardPath); + } + } +} +var import_node_fs2, import_yaml3, import_node_path2; +var init_skill_install = __esm({ + "packages/core/src/skill-install.js"() { + import_node_fs2 = __toESM(require("node:fs"), 1); + init_package_metadata(); + import_yaml3 = __toESM(require_dist(), 1); + import_node_path2 = __toESM(require("node:path"), 1); + init_src(); + init_src3(); + init_lockfile(); + } +}); + // packages/core/src/installer.js function getInstallPathForPackage(pkg) { if (!pkg || typeof pkg.id !== "string") { throw new Error("Package metadata requires an id"); } const [kind, name] = parsePackageId(pkg.id); - if (kind === "skill" && typeof pkg.path === "string" && !pkg.path.replaceAll("\\", "/").endsWith(".md")) { - return import_path6.default.join(PATHS.skills, name); + if (kind === "skill" && typeof pkg.path === "string" && pkg.path.length > 0) { + return import_path6.default.join(PATHS.skills, pkg.path.replaceAll("\\", "/").endsWith(".md") ? `${name}.md` : name); } return getPackagePath(pkg.id); } @@ -11003,6 +11318,9 @@ async function installPackage(id, options = {}) { } ensureDirectories(); let toInstall = getInstallOrder(resolved); + if (resolved.kind === "skill" && !import_fs5.default.existsSync(getInstallPathForPackage(resolved)) && !toInstall.some((pkg) => pkg.id === resolved.id)) { + toInstall.push(resolved); + } if (toInstall.length === 0 && !force) { return { success: true, @@ -11059,7 +11377,7 @@ async function installPackage(id, options = {}) { } return { success: false, id: resolved.id, error: error.message }; } - } else if (!transaction) { + } else if (!transaction && !mainResult?.lockfileWritten && !mainResult?.skipped) { onProgress?.({ phase: "lockfile", package: resolved.id }); await writeLockfile(resolved, { installPath: getInstallPathForPackage(resolved) @@ -11069,6 +11387,7 @@ async function installPackage(id, options = {}) { success: true, id: resolved.id, path: getInstallPathForPackage(resolved), + ...mainResult?.backupPath ? { backupPath: mainResult.backupPath } : {}, installed: results.map((r) => r.id), ...transaction && deferFinalize ? { transaction } : {} }; @@ -11201,6 +11520,9 @@ async function installSinglePackage(pkg, options = {}) { if (import_fs5.default.existsSync(installPath) && !force) { return { success: true, id: pkg.id, path: installPath, skipped: true }; } + if (pkg.kind === "skill" && pkg.source?.type !== "github" && pkg.path) { + return installRegistrySkill(pkg, installPath, { onProgress }); + } if (pkg.kind === "runtime" || pkg.kind === "binary") { onProgress?.({ phase: "downloading", package: pkg.id }); if (pkg.installType === "native-installer" && pkg.nativeInstaller) { @@ -11649,9 +11971,9 @@ async function installFromLocal(dir, options = {}) { if (!import_fs5.default.existsSync(manifestPath)) { throw new Error(`No manifest found in ${dir}`); } - const { parse: parseYaml5 } = await Promise.resolve().then(() => __toESM(require_dist(), 1)); + const { parse: parseYaml6 } = await Promise.resolve().then(() => __toESM(require_dist(), 1)); const manifestContent = import_fs5.default.readFileSync(manifestPath, "utf-8"); - const manifest = parseYaml5(manifestContent); + const manifest = parseYaml6(manifestContent); const id = manifest.id.includes(":") ? manifest.id : `stack:${manifest.id}`; const installPath = getPackagePath(id); if (import_fs5.default.existsSync(installPath)) { @@ -11688,63 +12010,14 @@ async function copyDirectory(src, dest) { } } } -function stripQuotes(value) { - return String(value || "").trim().replace(/^["']|["']$/g, ""); -} -function parseListValue(lines, startIndex) { - const values = []; - for (let i = startIndex + 1; i < lines.length; i++) { - const line = lines[i]; - if (!/^\s+/.test(line)) break; - const itemMatch = line.match(/^\s*-\s+(.+?)\s*$/); - if (itemMatch) { - values.push(stripQuotes(itemMatch[1])); - } - } - return values; -} -function parseSimpleYamlMetadata(yaml) { - const metadata = {}; - const lines = yaml.split(/\r?\n/); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const scalarMatch = line.match(/^(name|description|version|category|icon):\s*(.+?)\s*$/); - if (scalarMatch) { - metadata[scalarMatch[1]] = stripQuotes(scalarMatch[2]); - continue; - } - if (/^tags:\s*$/.test(line)) { - metadata.tags = parseListValue(lines, i); - continue; - } - if (/^requires:\s*$/.test(line)) { - const requires = {}; - for (let j = i + 1; j < lines.length; j++) { - const nested = lines[j]; - if (!/^\s+/.test(nested)) break; - const sectionMatch = nested.match(/^\s+(stacks|skills):\s*$/); - if (sectionMatch) { - requires[sectionMatch[1]] = parseListValue(lines, j); - } - } - if (Object.keys(requires).length > 0) { - metadata.requires = requires; - } - } - } - return metadata; -} function extractSingleFileMetadata(filePath, kind) { const content = import_fs5.default.readFileSync(filePath, "utf-8"); if (kind === "workflow" && filePath.endsWith(".json")) { return JSON.parse(content); } - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); - if (frontmatterMatch) { - return parseSimpleYamlMetadata(frontmatterMatch[1]); - } + if (/^---\r?\n/.test(content)) return parseSkillDocument(content).metadata; if (kind === "workflow") { - return parseSimpleYamlMetadata(content); + return parsePackageMetadata(content); } return {}; } @@ -11784,6 +12057,7 @@ async function listInstalled(kind) { format: skill.format, source: getInstalledPackageSource(`${k}:${skill.name}`, skill.source), entryPath: skill.entryPath, + ...skill.conflictingPaths ? { conflictingPaths: skill.conflictingPaths } : {}, path: skill.packagePath }); } catch { @@ -11798,6 +12072,7 @@ async function listInstalled(kind) { format: skill.format, source: getInstalledPackageSource(`${k}:${skill.name}`, skill.source), entryPath: skill.entryPath, + ...skill.conflictingPaths ? { conflictingPaths: skill.conflictingPaths } : {}, path: skill.packagePath }); } @@ -11870,7 +12145,13 @@ async function listInstalled(kind) { } } } - return packages; + const index = packages.some((pkg) => pkg.kind === "skill") ? getAvailableRegistryIndex() : null; + return packages.map((pkg) => { + if (pkg.kind !== "skill") return pkg; + const lock = readLockfile(pkg.id); + const catalogIdentity = pkg.source === "rudi" && lock?.id === pkg.id && /^[a-f0-9]{64}$/i.test(lock.checksum || ""); + return describeSkill(pkg, index, { catalogIdentity }); + }); } async function updatePackage(id, options = {}) { return installPackage(id, { ...options, force: true }); @@ -12116,11 +12397,13 @@ var init_installer = __esm({ import_child_process2 = require("child_process"); import_promises = require("stream/promises"); import_fs6 = require("fs"); + init_package_metadata(); init_src(); init_src3(); init_resolver(); init_lockfile(); init_shims(); + init_skill_install(); SINGLE_FILE_KINDS2 = /* @__PURE__ */ new Set(["skill", "prompt", "workflow"]); WORKFLOW_EXTENSIONS = [".yaml", ".yml", ".json"]; DEFAULT_STACK_STATE_PATHS = ["runs"]; @@ -12372,7 +12655,7 @@ function createLaunchConfig(command, runtime, stackPath) { throw new Error("Binary runtime requires a command"); } const bin2 = command[0].replace(/^\.\//, ""); - const binaryPath = path9.isAbsolute(command[0]) ? command[0] : path9.join(stackPath, bin2); + const binaryPath = path10.isAbsolute(command[0]) ? command[0] : path10.join(stackPath, bin2); return { bin: binaryPath, args: command.slice(1), cwd: stackPath }; } if (!command || command.length === 0) { @@ -12405,31 +12688,31 @@ function createLaunchConfig(command, runtime, stackPath) { function getDefaultRuntimeBin(runtime) { const platform = getPlatform(); if (runtime === "node") { - return platform === "win32" ? path9.join(PATHS.runtimes, "node", "node.exe") : path9.join(PATHS.runtimes, "node", "bin", "node"); + return platform === "win32" ? path10.join(PATHS.runtimes, "node", "node.exe") : path10.join(PATHS.runtimes, "node", "bin", "node"); } if (runtime === "python") { - return platform === "win32" ? path9.join(PATHS.runtimes, "python", "python.exe") : path9.join(PATHS.runtimes, "python", "bin", "python3"); + return platform === "win32" ? path10.join(PATHS.runtimes, "python", "python.exe") : path10.join(PATHS.runtimes, "python", "bin", "python3"); } return runtime; } function getDefaultNpxBin() { const platform = getPlatform(); - return platform === "win32" ? path9.join(PATHS.runtimes, "node", "npx.cmd") : path9.join(PATHS.runtimes, "node", "bin", "npx"); + return platform === "win32" ? path10.join(PATHS.runtimes, "node", "npx.cmd") : path10.join(PATHS.runtimes, "node", "bin", "npx"); } function acquireLock(timeoutMs = LOCK_TIMEOUT_MS) { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { try { - fs8.writeFileSync(RUDI_JSON_LOCK, String(process.pid), { flag: "wx" }); + fs9.writeFileSync(RUDI_JSON_LOCK, String(process.pid), { flag: "wx" }); return true; } catch (err) { if (err.code === "EEXIST") { try { - const pid = parseInt(fs8.readFileSync(RUDI_JSON_LOCK, "utf-8"), 10); + const pid = parseInt(fs9.readFileSync(RUDI_JSON_LOCK, "utf-8"), 10); try { process.kill(pid, 0); } catch { - fs8.unlinkSync(RUDI_JSON_LOCK); + fs9.unlinkSync(RUDI_JSON_LOCK); continue; } } catch { @@ -12447,16 +12730,16 @@ function acquireLock(timeoutMs = LOCK_TIMEOUT_MS) { } function releaseLock() { try { - fs8.unlinkSync(RUDI_JSON_LOCK); + fs9.unlinkSync(RUDI_JSON_LOCK); } catch { } } function rudiConfigExists() { - return fs8.existsSync(RUDI_JSON_PATH); + return fs9.existsSync(RUDI_JSON_PATH); } function readRudiConfig() { try { - const content = fs8.readFileSync(RUDI_JSON_PATH, "utf-8"); + const content = fs9.readFileSync(RUDI_JSON_PATH, "utf-8"); return JSON.parse(content); } catch (err) { if (err.code === "ENOENT") { @@ -12472,9 +12755,9 @@ function writeRudiConfig(config) { } try { const content = JSON.stringify(config, null, 2); - fs8.writeFileSync(RUDI_JSON_TMP, content, { mode: CONFIG_MODE }); - fs8.renameSync(RUDI_JSON_TMP, RUDI_JSON_PATH); - fs8.chmodSync(RUDI_JSON_PATH, CONFIG_MODE); + fs9.writeFileSync(RUDI_JSON_TMP, content, { mode: CONFIG_MODE }); + fs9.renameSync(RUDI_JSON_TMP, RUDI_JSON_PATH); + fs9.chmodSync(RUDI_JSON_PATH, CONFIG_MODE); } finally { releaseLock(); } @@ -12569,7 +12852,7 @@ function addRuntime(runtimeId, runtimeInfo) { } config.runtimes[runtimeId] = { path: runtimeInfo.path, - bin: path9.join(runtimeInfo.path, bin), + bin: path10.join(runtimeInfo.path, bin), version: runtimeInfo.version }; }); @@ -12589,15 +12872,15 @@ function updateSecretStatus(secretName, configured, provider) { } }); } -var fs8, path9, RUDI_JSON_PATH, RUDI_JSON_TMP, RUDI_JSON_LOCK, CONFIG_MODE, LOCK_TIMEOUT_MS; +var fs9, path10, RUDI_JSON_PATH, RUDI_JSON_TMP, RUDI_JSON_LOCK, CONFIG_MODE, LOCK_TIMEOUT_MS; var init_rudi_config = __esm({ "packages/core/src/rudi-config.js"() { - fs8 = __toESM(require("fs"), 1); - path9 = __toESM(require("path"), 1); + fs9 = __toESM(require("fs"), 1); + path10 = __toESM(require("path"), 1); init_src(); - RUDI_JSON_PATH = path9.join(RUDI_HOME, "rudi.json"); - RUDI_JSON_TMP = path9.join(RUDI_HOME, "rudi.json.tmp"); - RUDI_JSON_LOCK = path9.join(RUDI_HOME, "rudi.json.lock"); + RUDI_JSON_PATH = path10.join(RUDI_HOME, "rudi.json"); + RUDI_JSON_TMP = path10.join(RUDI_HOME, "rudi.json.tmp"); + RUDI_JSON_LOCK = path10.join(RUDI_HOME, "rudi.json.lock"); CONFIG_MODE = 384; LOCK_TIMEOUT_MS = 5e3; } @@ -12606,7 +12889,7 @@ var init_rudi_config = __esm({ // packages/core/src/tool-index.js function loadSecrets() { try { - const content = fs9.readFileSync(SECRETS_PATH, "utf-8"); + const content = fs10.readFileSync(SECRETS_PATH, "utf-8"); return JSON.parse(content); } catch { return {}; @@ -12629,23 +12912,23 @@ function getStackSecrets(stackConfig) { return { secrets, missing }; } function existingDirectory(dirPath) { - return typeof dirPath === "string" && fs9.existsSync(dirPath) && fs9.statSync(dirPath).isDirectory(); + return typeof dirPath === "string" && fs10.existsSync(dirPath) && fs10.statSync(dirPath).isDirectory(); } function getRudiExecutionPathEntries() { const entries = [PATHS.bins]; for (const runtimeBin of [ - path10.join(RUDI_HOME, "runtimes", "node", "bin"), - path10.join(RUDI_HOME, "runtimes", "python", "bin") + path11.join(RUDI_HOME, "runtimes", "node", "bin"), + path11.join(RUDI_HOME, "runtimes", "python", "bin") ]) { if (existingDirectory(runtimeBin)) { entries.push(runtimeBin); } } - const binariesRoot = path10.join(RUDI_HOME, "binaries"); + const binariesRoot = path11.join(RUDI_HOME, "binaries"); if (existingDirectory(binariesRoot)) { - for (const entry of fs9.readdirSync(binariesRoot, { withFileTypes: true })) { + for (const entry of fs10.readdirSync(binariesRoot, { withFileTypes: true })) { if (entry.isDirectory()) { - entries.push(path10.join(binariesRoot, entry.name)); + entries.push(path11.join(binariesRoot, entry.name)); } } } @@ -12654,12 +12937,12 @@ function getRudiExecutionPathEntries() { function prependRudiExecutionPath(env) { const seen = /* @__PURE__ */ new Set(); const entries = []; - for (const entry of [...getRudiExecutionPathEntries(), ...(env.PATH || "").split(path10.delimiter)]) { + for (const entry of [...getRudiExecutionPathEntries(), ...(env.PATH || "").split(path11.delimiter)]) { if (!entry || seen.has(entry)) continue; seen.add(entry); entries.push(entry); } - env.PATH = entries.join(path10.delimiter); + env.PATH = entries.join(path11.delimiter); } async function discoverStackTools(stackId, stackConfig, options = {}) { const { timeout = REQUEST_TIMEOUT_MS, log = () => { @@ -12936,21 +13219,21 @@ async function discoverStackTools(stackId, stackConfig, options = {}) { } function readToolIndex() { try { - const content = fs9.readFileSync(TOOL_INDEX_PATH, "utf-8"); + const content = fs10.readFileSync(TOOL_INDEX_PATH, "utf-8"); return JSON.parse(content); } catch { return null; } } function writeToolIndex(index) { - const cacheDir = path10.dirname(TOOL_INDEX_PATH); - if (!fs9.existsSync(cacheDir)) { - fs9.mkdirSync(cacheDir, { recursive: true }); + const cacheDir = path11.dirname(TOOL_INDEX_PATH); + if (!fs10.existsSync(cacheDir)) { + fs10.mkdirSync(cacheDir, { recursive: true }); } index.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); const content = JSON.stringify(index, null, 2); - fs9.writeFileSync(TOOL_INDEX_TMP, content, { mode: 384 }); - fs9.renameSync(TOOL_INDEX_TMP, TOOL_INDEX_PATH); + fs10.writeFileSync(TOOL_INDEX_TMP, content, { mode: 384 }); + fs10.renameSync(TOOL_INDEX_TMP, TOOL_INDEX_PATH); } function createToolIndex() { return { @@ -13013,18 +13296,18 @@ async function indexAllStacks(options = {}) { writeToolIndex(index); return { indexed, failed, index }; } -var import_child_process4, fs9, path10, readline, TOOL_INDEX_PATH, TOOL_INDEX_TMP, SECRETS_PATH, REQUEST_TIMEOUT_MS, PROTOCOL_VERSION, WINDOWS_CLEANUP_TIMEOUT_MS, WINDOWS_DESCENDANT_SWEEP_SCRIPT; +var import_child_process4, fs10, path11, readline, TOOL_INDEX_PATH, TOOL_INDEX_TMP, SECRETS_PATH, REQUEST_TIMEOUT_MS, PROTOCOL_VERSION, WINDOWS_CLEANUP_TIMEOUT_MS, WINDOWS_DESCENDANT_SWEEP_SCRIPT; var init_tool_index = __esm({ "packages/core/src/tool-index.js"() { import_child_process4 = require("child_process"); - fs9 = __toESM(require("fs"), 1); - path10 = __toESM(require("path"), 1); + fs10 = __toESM(require("fs"), 1); + path11 = __toESM(require("path"), 1); readline = __toESM(require("readline"), 1); init_src(); init_rudi_config(); - TOOL_INDEX_PATH = path10.join(RUDI_HOME, "cache", "tool-index.json"); - TOOL_INDEX_TMP = path10.join(RUDI_HOME, "cache", "tool-index.json.tmp"); - SECRETS_PATH = path10.join(RUDI_HOME, "secrets.json"); + TOOL_INDEX_PATH = path11.join(RUDI_HOME, "cache", "tool-index.json"); + TOOL_INDEX_TMP = path11.join(RUDI_HOME, "cache", "tool-index.json.tmp"); + SECRETS_PATH = path11.join(RUDI_HOME, "secrets.json"); REQUEST_TIMEOUT_MS = 15e3; PROTOCOL_VERSION = "2024-11-05"; WINDOWS_CLEANUP_TIMEOUT_MS = 2e3; @@ -13212,15 +13495,15 @@ function getSecretsFilePath() { return SECRETS_FILE; } function ensureSecretsFile() { - const dir = path12.dirname(SECRETS_FILE); - if (!fs11.existsSync(dir)) { - fs11.mkdirSync(dir, { recursive: true }); + const dir = path13.dirname(SECRETS_FILE); + if (!fs12.existsSync(dir)) { + fs12.mkdirSync(dir, { recursive: true }); } - if (!fs11.existsSync(SECRETS_FILE)) { - fs11.writeFileSync(SECRETS_FILE, "{}", { mode: 384 }); + if (!fs12.existsSync(SECRETS_FILE)) { + fs12.writeFileSync(SECRETS_FILE, "{}", { mode: 384 }); } else { try { - fs11.chmodSync(SECRETS_FILE, 384); + fs12.chmodSync(SECRETS_FILE, 384); } catch { } } @@ -13228,7 +13511,7 @@ function ensureSecretsFile() { function loadSecrets2() { ensureSecretsFile(); try { - const content = fs11.readFileSync(SECRETS_FILE, "utf-8"); + const content = fs12.readFileSync(SECRETS_FILE, "utf-8"); const secrets = JSON.parse(content); return isSecretsObject(secrets) ? secrets : {}; } catch { @@ -13237,8 +13520,8 @@ function loadSecrets2() { } function loadSecretsWithoutMutation() { try { - if (!fs11.existsSync(SECRETS_FILE)) return {}; - const content = fs11.readFileSync(SECRETS_FILE, "utf-8"); + if (!fs12.existsSync(SECRETS_FILE)) return {}; + const content = fs12.readFileSync(SECRETS_FILE, "utf-8"); const secrets = JSON.parse(content); return isSecretsObject(secrets) ? secrets : {}; } catch { @@ -13250,18 +13533,18 @@ function saveSecrets(secrets) { const normalized = isSecretsObject(secrets) ? secrets : {}; const tempFile = `${SECRETS_FILE}.${process.pid}.${Date.now()}.tmp`; try { - fs11.writeFileSync(tempFile, JSON.stringify(normalized, null, 2), { + fs12.writeFileSync(tempFile, JSON.stringify(normalized, null, 2), { encoding: "utf-8", mode: 384 }); - fs11.renameSync(tempFile, SECRETS_FILE); + fs12.renameSync(tempFile, SECRETS_FILE); try { - fs11.chmodSync(SECRETS_FILE, 384); + fs12.chmodSync(SECRETS_FILE, 384); } catch { } } finally { - if (fs11.existsSync(tempFile)) { - fs11.rmSync(tempFile, { force: true }); + if (fs12.existsSync(tempFile)) { + fs12.rmSync(tempFile, { force: true }); } } } @@ -13311,13 +13594,13 @@ function getStorageInfo() { permissions: "0600 (owner read/write only)" }; } -var fs11, path12, SECRETS_FILE; +var fs12, path13, SECRETS_FILE; var init_src4 = __esm({ "packages/secrets/src/index.js"() { - fs11 = __toESM(require("fs"), 1); - path12 = __toESM(require("path"), 1); + fs12 = __toESM(require("fs"), 1); + path13 = __toESM(require("path"), 1); init_src(); - SECRETS_FILE = path12.join(PATHS.home, "secrets.json"); + SECRETS_FILE = path13.join(PATHS.home, "secrets.json"); } }); @@ -13332,7 +13615,7 @@ function checkInstalled(stackId, stackConfig) { details: { path: stackConfig.path } }; } - if (!import_node_fs2.default.existsSync(stackConfig.path)) { + if (!import_node_fs3.default.existsSync(stackConfig.path)) { return { passed: false, state: "installed", @@ -13340,8 +13623,8 @@ function checkInstalled(stackId, stackConfig) { details: { path: stackConfig.path } }; } - const manifestPath = import_node_path2.default.join(stackConfig.path, "manifest.json"); - if (!import_node_fs2.default.existsSync(manifestPath)) { + const manifestPath = import_node_path3.default.join(stackConfig.path, "manifest.json"); + if (!import_node_fs3.default.existsSync(manifestPath)) { return { passed: false, state: "installed", @@ -13383,7 +13666,7 @@ function checkLaunchable(stackId, stackConfig) { details: { bin: null, cwd: launch.cwd || stackConfig.path } }; } - if (!import_node_fs2.default.existsSync(launch.bin)) { + if (!import_node_fs3.default.existsSync(launch.bin)) { return { passed: false, state: "launchable", @@ -13392,7 +13675,7 @@ function checkLaunchable(stackId, stackConfig) { }; } try { - import_node_fs2.default.accessSync(launch.bin, import_node_fs2.default.constants.X_OK); + import_node_fs3.default.accessSync(launch.bin, import_node_fs3.default.constants.X_OK); } catch (err) { return { passed: false, @@ -13402,7 +13685,7 @@ function checkLaunchable(stackId, stackConfig) { }; } const cwd = launch.cwd || stackConfig.path; - if (!import_node_fs2.default.existsSync(cwd)) { + if (!import_node_fs3.default.existsSync(cwd)) { return { passed: false, state: "launchable", @@ -13516,7 +13799,7 @@ async function checkMcpReady(stackId, stackConfig, opts = {}) { function checkIndexed(stackId, stackConfig, options = {}) { const indexPath = options.indexPath || TOOL_INDEX_PATH; try { - if (!import_node_fs2.default.existsSync(indexPath)) { + if (!import_node_fs3.default.existsSync(indexPath)) { return { passed: false, state: "indexed", @@ -13524,7 +13807,7 @@ function checkIndexed(stackId, stackConfig, options = {}) { details: { toolCount: 0, indexPath } }; } - const indexContent = import_node_fs2.default.readFileSync(indexPath, "utf8"); + const indexContent = import_node_fs3.default.readFileSync(indexPath, "utf8"); const index = JSON.parse(indexContent); const byStack = index.byStack || index; const entry = byStack[stackId] || byStack[`stack:${stackId}`]; @@ -13619,11 +13902,11 @@ async function checkStackLifecycle(stackId, stackConfig, opts = {}) { fixCommand }; } -var import_node_fs2, import_node_path2; +var import_node_fs3, import_node_path3; var init_stack_lifecycle = __esm({ "packages/core/src/stack-lifecycle.js"() { - import_node_fs2 = __toESM(require("node:fs"), 1); - import_node_path2 = __toESM(require("node:path"), 1); + import_node_fs3 = __toESM(require("node:fs"), 1); + import_node_path3 = __toESM(require("node:path"), 1); init_src4(); init_tool_index(); } @@ -13667,6 +13950,7 @@ __export(src_exports, { createStackDependencyInstallCommand: () => createStackDependencyInstallCommand, createToolIndex: () => createToolIndex, deleteLockfile: () => deleteLockfile, + describeSkill: () => describeSkill, discoverSkillPackages: () => discoverSkillPackages, discoverStackTools: () => discoverStackTools, ensureDirectories: () => ensureDirectories, @@ -13679,6 +13963,7 @@ __export(src_exports, { getAllLockfiles: () => getAllLockfiles, getAllShimOwners: () => getAllShimOwners, getAvailableDeps: () => getAvailableDeps, + getAvailableRegistryIndex: () => getAvailableRegistryIndex, getDefaultNpxBin: () => getDefaultNpxBin, getDefaultRuntimeBin: () => getDefaultRuntimeBin, getDefaultSecretProvider: () => getDefaultSecretProvider, @@ -13696,6 +13981,7 @@ __export(src_exports, { hasLockfile: () => hasLockfile, indexAllStacks: () => indexAllStacks, initRudiConfig: () => initRudiConfig, + inspectRegistrySkillUpdate: () => inspectRegistrySkillUpdate, installFromLocal: () => installFromLocal, installPackage: () => installPackage, isMatchingPinnedGitHubLock: () => isMatchingPinnedGitHubLock, @@ -13704,7 +13990,10 @@ __export(src_exports, { listInstalled: () => listInstalled, listPackages: () => listPackages, listShims: () => listShims, + matchesSkillFilters: () => matchesSkillFilters, + normalizeSkillFilters: () => normalizeSkillFilters, parsePackageId: () => parsePackageId, + parseSkillDocument: () => parseSkillDocument, prepareDeferredInstall: () => prepareDeferredInstall, readLockfile: () => readLockfile, readRudiConfig: () => readRudiConfig, @@ -13749,6 +14038,8 @@ var init_src5 = __esm({ init_shims(); init_system_registry(); init_stack_lifecycle(); + init_package_metadata(); + init_skill_install(); } }); @@ -16980,8 +17271,8 @@ var require_utils = __commonJS({ } return ind; } - function removeDotSegments(path55) { - let input = path55; + function removeDotSegments(path56) { + let input = path56; const output = []; let nextSlash = -1; let len = 0; @@ -17386,8 +17677,8 @@ var require_schemes = __commonJS({ } if (wsComponent.resourceName) { const queryIndex = wsComponent.resourceName.indexOf("?"); - const path55 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex); - wsComponent.path = path55 && path55 !== "/" ? path55 : void 0; + const path56 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex); + wsComponent.path = path56 && path56 !== "/" ? path56 : void 0; wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1); wsComponent.resourceName = void 0; } @@ -20893,12 +21184,12 @@ var require_dist2 = __commonJS({ throw new Error(`Unknown format "${name}"`); return f; }; - function addFormats2(ajv2, list, fs52, exportName) { + function addFormats2(ajv2, list, fs53, exportName) { var _a; var _b; (_a = (_b = ajv2.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`; for (const f of list) - ajv2.addFormat(f, fs52[f]); + ajv2.addFormat(f, fs53[f]); } module2.exports = exports2 = formatsPlugin; Object.defineProperty(exports2, "__esModule", { value: true }); @@ -21363,7 +21654,9 @@ EXAMPLES rudi list binaries rudi list workflows rudi skills - rudi list skills --category=coding + rudi list skills --category=code + rudi search --all --skills --category=web --role=operator + rudi list skills --provider=vercel `, skills: ` rudi skills - List or sync installed RUDI skills @@ -21596,6 +21889,18 @@ function printPackageLifecycle(pkg, indent = "") { } } +// src/commands/skill-display.js +function printSkillDetails(pkg, indent = " ") { + if (pkg.kind !== "skill") return; + if (pkg.category) console.log(`${indent}Category: ${pkg.category}`); + console.log(`${indent}Role: ${pkg.skillRole || "unknown"}`); + if (pkg.operatorFor?.length) console.log(`${indent}Operator for: ${pkg.operatorFor.join(", ")}`); + for (const [field, label] of [["capabilities", "Capabilities"], ["domains", "Domains"], ["providers", "Providers"]]) { + if (pkg.facets?.[field]?.length) console.log(`${indent}${label}: ${pkg.facets[field].join(", ")}`); + } + if (pkg.conflictingPaths?.length) console.log(`${indent}Source conflict: ${pkg.conflictingPaths.join(", ")}`); +} + // src/commands/search.js function pluralizeKind(kind) { if (!kind) return "packages"; @@ -21627,6 +21932,7 @@ function printSearchGuidance(packageKinds) { } async function cmdSearch(args, flags) { const query = args[0]; + const filters = normalizeSkillFilters(flags); const refreshRegistry = flags.fresh || flags["no-cache"] || false; if (refreshRegistry) { await fetchIndex({ force: true }); @@ -21648,19 +21954,19 @@ async function cmdSearch(args, flags) { const binariesFlag = flags.binaries || flags.tools; const kind = flags.stacks ? "stack" : flags.skills || flags.prompts ? "skill" : flags.workflows ? "workflow" : flags.runtimes ? "runtime" : binariesFlag ? "binary" : flags.agents ? "agent" : null; if (flags.prompts && !flags.skills) { - console.log("Note: --prompts has been renamed to --skills. Use --skills instead.\n"); + console.error("Note: --prompts has been renamed to --skills. Use --skills instead.\n"); } - console.log(`Searching for "${query}"...`); + if (!flags.json) console.log(`Searching for "${query}"...`); try { - const results = await searchPackages(query, { kind }); - if (results.length === 0) { - console.log("No packages found matching your query."); - return; - } + const results = await searchPackages(query, { kind, ...filters }); if (flags.json) { console.log(JSON.stringify(results, null, 2)); return; } + if (results.length === 0) { + console.log("No packages found matching your query."); + return; + } console.log(` Found ${results.length} package(s): `); @@ -21684,6 +21990,7 @@ Found ${results.length} package(s): console.log(` v${pkg.version}`); } printPackageLifecycle(pkg, " "); + printSkillDetails(pkg); console.log(); } } @@ -21697,14 +22004,14 @@ async function listAllPackages(flags) { const binariesFlag = flags.binaries || flags.tools; const kind = flags.stacks ? "stack" : flags.skills || flags.prompts ? "skill" : flags.workflows ? "workflow" : flags.runtimes ? "runtime" : binariesFlag ? "binary" : flags.agents ? "agent" : null; if (flags.prompts && !flags.skills) { - console.log("Note: --prompts has been renamed to --skills. Use --skills instead.\n"); + console.error("Note: --prompts has been renamed to --skills. Use --skills instead.\n"); } try { const kinds = kind ? [kind] : ["stack", "skill", "workflow", "runtime", "binary", "agent"]; const allPackages = {}; let totalCount = 0; for (const k of kinds) { - const packages = await listPackages(k); + const packages = await listPackages(k, normalizeSkillFilters(flags)); allPackages[k] = packages; totalCount += packages.length; } @@ -21725,6 +22032,7 @@ ${headingForKind(k)} (${packages.length}):`); console.log(` ${id}${runtime}`); console.log(` ${pkg.description || "No description"}`); printPackageLifecycle(pkg, " "); + printSkillDetails(pkg); } } console.log(` @@ -21737,9 +22045,9 @@ Total: ${totalCount} package(s) available`); } // src/commands/install.js -var fs29 = __toESM(require("fs/promises"), 1); +var fs30 = __toESM(require("fs/promises"), 1); var fsSync = __toESM(require("fs"), 1); -var path31 = __toESM(require("path"), 1); +var path32 = __toESM(require("path"), 1); init_src5(); init_src4(); @@ -21980,28 +22288,28 @@ function getMcpServerSummary() { } // packages/mcp/src/registry.js -var fs14 = __toESM(require("fs/promises"), 1); -var path15 = __toESM(require("path"), 1); +var fs15 = __toESM(require("fs/promises"), 1); +var path16 = __toESM(require("path"), 1); var os5 = __toESM(require("os"), 1); var HOME = os5.homedir(); var AGENT_CONFIGS2 = { - claude: path15.join(HOME, ".claude", "settings.json"), - codex: path15.join(HOME, ".codex", "config.toml"), - gemini: path15.join(HOME, ".gemini", "settings.json") + claude: path16.join(HOME, ".claude", "settings.json"), + codex: path16.join(HOME, ".codex", "config.toml"), + gemini: path16.join(HOME, ".gemini", "settings.json") }; -var RUDI_ROUTER_SHIM = path15.join(HOME, ".rudi", "bins", "rudi-router"); +var RUDI_ROUTER_SHIM = path16.join(HOME, ".rudi", "bins", "rudi-router"); async function readJson(filePath) { try { - const content = await fs14.readFile(filePath, "utf-8"); + const content = await fs15.readFile(filePath, "utf-8"); return JSON.parse(content); } catch { return {}; } } async function writeJson(filePath, data) { - const dir = path15.dirname(filePath); - await fs14.mkdir(dir, { recursive: true }); - await fs14.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8"); + const dir = path16.dirname(filePath); + await fs15.mkdir(dir, { recursive: true }); + await fs15.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8"); } function parseTomlValue(value) { if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) { @@ -22109,16 +22417,16 @@ function stringifyToml(config, prefix = "") { } async function readToml(filePath) { try { - const content = await fs14.readFile(filePath, "utf-8"); + const content = await fs15.readFile(filePath, "utf-8"); return parseToml(content); } catch { return {}; } } async function writeToml(filePath, data) { - const dir = path15.dirname(filePath); - await fs14.mkdir(dir, { recursive: true }); - await fs14.writeFile(filePath, stringifyToml(data), "utf-8"); + const dir = path16.dirname(filePath); + await fs15.mkdir(dir, { recursive: true }); + await fs15.writeFile(filePath, stringifyToml(data), "utf-8"); } async function unregisterMcpCodex(stackId) { const configPath = AGENT_CONFIGS2.codex; @@ -22215,9 +22523,10 @@ function runCommandPlan2(plan, options = {}) { // src/native-skills/lifecycle.js var import_node_crypto2 = __toESM(require("node:crypto"), 1); +init_src5(); var fsp = __toESM(require("node:fs/promises"), 1); var import_node_os = __toESM(require("node:os"), 1); -var import_node_path3 = __toESM(require("node:path"), 1); +var import_node_path4 = __toESM(require("node:path"), 1); var NATIVE_SKILL_HOSTS = Object.freeze([ "codex", "claude", @@ -22251,32 +22560,6 @@ function humanizeSkillDisplayName(value) { function yamlString(value) { return JSON.stringify(String(value || "")); } -function parseSimpleFrontmatter(frontmatter = "") { - const metadata = {}; - for (const line of frontmatter.split("\n")) { - const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); - if (!match) continue; - let value = match[2].trim(); - if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) { - value = value.slice(1, -1); - } - metadata[match[1]] = value; - } - return metadata; -} -function stripFrontmatter(content = "") { - if (!content.startsWith("---\n")) { - return { metadata: {}, body: content.trimStart() }; - } - const end = content.indexOf("\n---\n", 4); - if (end === -1) { - return { metadata: {}, body: content.trimStart() }; - } - return { - metadata: parseSimpleFrontmatter(content.slice(4, end)), - body: content.slice(end + 5).trimStart() - }; -} function normalizeNativeSkillName(skill) { const raw = String(skill?.id || "").replace(/^skill:/, ""); if (!SKILL_NAME_PATTERN.test(raw)) { @@ -22293,12 +22576,11 @@ function defaultPrompt(skillName, description, displayName) { } function buildPortableSkillFiles(skill, sourceContent) { const skillName = normalizeNativeSkillName(skill); - const parsed = stripFrontmatter(sourceContent); + const parsed = parseSkillDocument(sourceContent); const displayName = compactText(parsed.metadata.name || skill.name || skillName, 80); - const description = compactText( - skill.description || parsed.metadata.description || `${displayName} RUDI skill`, - 320 - ); + const description = String( + skill.description || parsed.metadata.description || `${displayName} RUDI skill` + ).replace(/\s+/g, " ").trim(); const body = parsed.body || `Use the installed RUDI skill \`skill:${skillName}\` as the source of truth.`; const skillMd = [ "---", @@ -22314,7 +22596,7 @@ function buildPortableSkillFiles(skill, sourceContent) { function buildCodexSkillFiles(skill, sourceContent) { const baseFiles = buildPortableSkillFiles(skill, sourceContent); const { skillName } = baseFiles; - const parsed = stripFrontmatter(sourceContent); + const parsed = parseSkillDocument(sourceContent); const displayName = humanizeSkillDisplayName(parsed.metadata.name || skill.name || skillName); const description = compactText( skill.description || parsed.metadata.description || `${displayName} RUDI skill`, @@ -22335,30 +22617,30 @@ function assertSupportedHost(host) { } } function taskHome(options = {}) { - return import_node_path3.default.resolve(options.homeDir || options.env?.HOME || import_node_os.default.homedir()); + return import_node_path4.default.resolve(options.homeDir || options.env?.HOME || import_node_os.default.homedir()); } function getNativeSkillRoot(host, options = {}) { assertSupportedHost(host); const env = options.env || process.env; const home = taskHome(options); if (host === "codex") { - return import_node_path3.default.join(import_node_path3.default.resolve(env.CODEX_HOME || import_node_path3.default.join(home, ".codex")), "skills"); + return import_node_path4.default.join(import_node_path4.default.resolve(env.CODEX_HOME || import_node_path4.default.join(home, ".codex")), "skills"); } if (host === "claude") { - return import_node_path3.default.join(import_node_path3.default.resolve(env.CLAUDE_HOME || import_node_path3.default.join(home, ".claude")), "skills"); + return import_node_path4.default.join(import_node_path4.default.resolve(env.CLAUDE_HOME || import_node_path4.default.join(home, ".claude")), "skills"); } if (host === "gemini") { - return import_node_path3.default.join(import_node_path3.default.resolve(env.GEMINI_HOME || import_node_path3.default.join(home, ".gemini")), "skills"); + return import_node_path4.default.join(import_node_path4.default.resolve(env.GEMINI_HOME || import_node_path4.default.join(home, ".gemini")), "skills"); } - return import_node_path3.default.join( - import_node_path3.default.resolve(env.ANTIGRAVITY_HOME || import_node_path3.default.join(home, ".gemini", "antigravity-cli")), + return import_node_path4.default.join( + import_node_path4.default.resolve(env.ANTIGRAVITY_HOME || import_node_path4.default.join(home, ".gemini", "antigravity-cli")), "skills" ); } function getNativeSkillReceiptRoot(options = {}) { const env = options.env || process.env; - const rudiHome = import_node_path3.default.resolve(env.RUDI_HOME || import_node_path3.default.join(taskHome(options), ".rudi")); - return import_node_path3.default.join(rudiHome, "state", "native-skills"); + const rudiHome = import_node_path4.default.resolve(env.RUDI_HOME || import_node_path4.default.join(taskHome(options), ".rudi")); + return import_node_path4.default.join(rudiHome, "state", "native-skills"); } function configuredNativeSkillHosts(installedAgents = []) { const ids = new Set((installedAgents || []).map((agent) => agent?.id).filter(Boolean)); @@ -22370,7 +22652,7 @@ function configuredNativeSkillHosts(installedAgents = []) { return hosts; } function safeRelativePath(relativePath) { - if (!relativePath || import_node_path3.default.isAbsolute(relativePath) || relativePath.includes("\0") || relativePath.split(import_node_path3.default.sep).some((part) => part === "" || part === "." || part === "..")) { + if (!relativePath || import_node_path4.default.isAbsolute(relativePath) || relativePath.includes("\0") || relativePath.split(import_node_path4.default.sep).some((part) => part === "" || part === "." || part === "..")) { throw new Error(`Unsafe native skill resource path: ${relativePath}`); } return relativePath; @@ -22387,12 +22669,12 @@ function assertRealEntry(stat, entryPath, expected) { } } async function assertNoSymlinkPathComponents(candidate, label, options = {}) { - const absolute = import_node_path3.default.resolve(candidate); - const parsed = import_node_path3.default.parse(absolute); - const segments = absolute.slice(parsed.root.length).split(import_node_path3.default.sep).filter(Boolean); + const absolute = import_node_path4.default.resolve(candidate); + const parsed = import_node_path4.default.parse(absolute); + const segments = absolute.slice(parsed.root.length).split(import_node_path4.default.sep).filter(Boolean); let current = parsed.root; for (let index = 0; index < segments.length; index += 1) { - current = import_node_path3.default.join(current, segments[index]); + current = import_node_path4.default.join(current, segments[index]); let stat; try { stat = await fsp.lstat(current); @@ -22421,7 +22703,7 @@ async function assertSafeRoot(root, label) { } } async function collectResourceEntries(sourceRoot, resourceName, entries, sourceEntries) { - const resourceRoot = import_node_path3.default.join(sourceRoot, resourceName); + const resourceRoot = import_node_path4.default.join(sourceRoot, resourceName); let rootStat; try { rootStat = await fsp.lstat(resourceRoot); @@ -22436,8 +22718,8 @@ async function collectResourceEntries(sourceRoot, resourceName, entries, sourceE const children = await fsp.readdir(directory, { withFileTypes: true }); children.sort((a, b) => a.name.localeCompare(b.name)); for (const child of children) { - const childPath = import_node_path3.default.join(directory, child.name); - const relativePath = safeRelativePath(import_node_path3.default.join(relativeDirectory, child.name)); + const childPath = import_node_path4.default.join(directory, child.name); + const relativePath = safeRelativePath(import_node_path4.default.join(relativeDirectory, child.name)); const stat = await fsp.lstat(childPath); if (stat.isSymbolicLink()) { throw new Error(`Native skill resources cannot contain symbolic links: ${childPath}`); @@ -22464,6 +22746,18 @@ async function collectResourceEntries(sourceRoot, resourceName, entries, sourceE } await walk(resourceRoot, resourceName); } +async function readBundledCodexMetadata(sourceRoot) { + const metadataPath = import_node_path4.default.join(sourceRoot, "agents", "openai.yaml"); + let metadataStat; + try { + metadataStat = await fsp.lstat(metadataPath); + } catch (error) { + if (error.code === "ENOENT") return null; + throw error; + } + assertRealEntry(metadataStat, metadataPath, "file"); + return fsp.readFile(metadataPath); +} function manifestEntry(entry) { if (entry.type === "directory") { return { path: entry.relativePath, type: entry.type, mode: entry.mode }; @@ -22488,7 +22782,10 @@ function resolveSourceIdentity(source) { async function buildProjection(host, skill) { assertSupportedHost(host); const skillName = normalizeNativeSkillName(skill); - const sourcePath = import_node_path3.default.resolve(skill.entryPath || skill.path || ""); + if (skill.conflictingPaths?.length) { + throw new Error(`Conflicting skill formats for ${skill.id}; reconcile canonical sources before native sync`); + } + const sourcePath = import_node_path4.default.resolve(skill.entryPath || skill.path || ""); await assertNoSymlinkPathComponents(sourcePath, "Native skill source path"); let sourceStat; try { @@ -22515,29 +22812,33 @@ async function buildProjection(host, skill) { mode: sourceStat.mode & 511, content: sourceContent }]; - if (host === "codex") { - entries.push({ type: "directory", relativePath: "agents", mode: 493 }); - entries.push({ - type: "file", - relativePath: import_node_path3.default.join("agents", "openai.yaml"), - mode: 420, - content: Buffer.from(generated.openaiYaml) - }); - } + let codexMetadata = host === "codex" ? Buffer.from(generated.openaiYaml) : null; let packageDigest; - if (import_node_path3.default.basename(sourcePath) === "SKILL.md") { - const sourceRoot = import_node_path3.default.dirname(sourcePath); + if (import_node_path4.default.basename(sourcePath) === "SKILL.md") { + const sourceRoot = import_node_path4.default.dirname(sourcePath); const completePackage = await inspectTree(sourceRoot); if (!completePackage) { throw new Error(`Source skill package not found: ${sourceRoot}`); } packageDigest = completePackage.digest; + if (host === "codex") { + codexMetadata = await readBundledCodexMetadata(sourceRoot) ?? codexMetadata; + } for (const resourceName of RESOURCE_DIRECTORIES) { await collectResourceEntries(sourceRoot, resourceName, entries, sourceEntries); } } else { packageDigest = digestEntries(sourceEntries).digest; } + if (host === "codex") { + entries.push({ type: "directory", relativePath: "agents", mode: 493 }); + entries.push({ + type: "file", + relativePath: import_node_path4.default.join("agents", "openai.yaml"), + mode: 420, + content: codexMetadata + }); + } const rendered = digestEntries(entries); const sourceIdentity = resolveSourceIdentity(skill.source); return { @@ -22566,9 +22867,9 @@ async function inspectTree(root) { const children = await fsp.readdir(directory, { withFileTypes: true }); children.sort((a, b) => a.name.localeCompare(b.name)); for (const child of children) { - const childPath = import_node_path3.default.join(directory, child.name); + const childPath = import_node_path4.default.join(directory, child.name); const relativePath = safeRelativePath( - relativeDirectory ? import_node_path3.default.join(relativeDirectory, child.name) : child.name + relativeDirectory ? import_node_path4.default.join(relativeDirectory, child.name) : child.name ); const childStat = await fsp.lstat(childPath); if (childStat.isSymbolicLink()) { @@ -22597,7 +22898,7 @@ async function inspectTree(root) { return digestEntries(entries); } function receiptPathFor(receiptRoot, host, skillName) { - return import_node_path3.default.join(import_node_path3.default.resolve(receiptRoot), host, `${skillName}.json`); + return import_node_path4.default.join(import_node_path4.default.resolve(receiptRoot), host, `${skillName}.json`); } function isIsoTimestamp(value) { if (typeof value !== "string") return false; @@ -22641,7 +22942,7 @@ function validateReceipt(receipt, expected = {}) { if (!DIGEST_PATTERN.test(receipt.renderedTreeDigest || "")) { throw new Error("Invalid native skill receipt renderedTreeDigest"); } - if (typeof receipt.targetDir !== "string" || import_node_path3.default.resolve(receipt.targetDir) !== receipt.targetDir) { + if (typeof receipt.targetDir !== "string" || import_node_path4.default.resolve(receipt.targetDir) !== receipt.targetDir) { throw new Error("Invalid native skill receipt targetDir"); } if (!isIsoTimestamp(receipt.createdAt) || !isIsoTimestamp(receipt.updatedAt)) { @@ -22659,7 +22960,7 @@ function validateReceipt(receipt, expected = {}) { if (expected.skillName && receipt.skillName !== expected.skillName) { throw new Error(`Native skill receipt name mismatch: ${receipt.skillName}`); } - if (expected.targetDir && receipt.targetDir !== import_node_path3.default.resolve(expected.targetDir)) { + if (expected.targetDir && receipt.targetDir !== import_node_path4.default.resolve(expected.targetDir)) { throw new Error(`Native skill receipt target mismatch: ${receipt.targetDir}`); } return receipt; @@ -22705,19 +23006,19 @@ async function ensureRealDirectory(directory) { async function writeProjectionStage(targetRoot, projection) { await assertSafeRoot(targetRoot, "Native skill target root"); await ensureRealDirectory(targetRoot); - const stageDir = await fsp.mkdtemp(import_node_path3.default.join(targetRoot, `.${projection.skillName}.rudi-stage-`)); + const stageDir = await fsp.mkdtemp(import_node_path4.default.join(targetRoot, `.${projection.skillName}.rudi-stage-`)); try { for (const entry of projection.entries) { - const destination = import_node_path3.default.join(stageDir, safeRelativePath(entry.relativePath)); - const resolvedDestination = import_node_path3.default.resolve(destination); - if (!resolvedDestination.startsWith(`${import_node_path3.default.resolve(stageDir)}${import_node_path3.default.sep}`)) { + const destination = import_node_path4.default.join(stageDir, safeRelativePath(entry.relativePath)); + const resolvedDestination = import_node_path4.default.resolve(destination); + if (!resolvedDestination.startsWith(`${import_node_path4.default.resolve(stageDir)}${import_node_path4.default.sep}`)) { throw new Error(`Native skill stage path escapes target: ${entry.relativePath}`); } if (entry.type === "directory") { await fsp.mkdir(destination, { recursive: true, mode: entry.mode }); await fsp.chmod(destination, entry.mode); } else { - await fsp.mkdir(import_node_path3.default.dirname(destination), { recursive: true, mode: 493 }); + await fsp.mkdir(import_node_path4.default.dirname(destination), { recursive: true, mode: 493 }); await fsp.writeFile(destination, entry.content, { flag: "wx", mode: entry.mode }); await fsp.chmod(destination, entry.mode); } @@ -22733,12 +23034,12 @@ async function writeProjectionStage(targetRoot, projection) { } } async function atomicWriteReceipt(receiptPath, receipt) { - const directory = import_node_path3.default.dirname(receiptPath); + const directory = import_node_path4.default.dirname(receiptPath); await assertSafeRoot(directory, "Native skill receipt directory"); await ensureRealDirectory(directory); - const temporary = import_node_path3.default.join( + const temporary = import_node_path4.default.join( directory, - `.${import_node_path3.default.basename(receiptPath)}.${process.pid}.${import_node_crypto2.default.randomUUID()}.tmp` + `.${import_node_path4.default.basename(receiptPath)}.${process.pid}.${import_node_crypto2.default.randomUUID()}.tmp` ); try { await fsp.writeFile(temporary, `${JSON.stringify(receipt, null, 2)} @@ -22788,11 +23089,11 @@ async function inspectNativeSkillProjection(options = {}) { const host = options.host; const skill = options.skill; const projection = await buildProjection(host, skill); - const targetRoot = import_node_path3.default.resolve(options.targetRoot || getNativeSkillRoot(host, options)); - const receiptRoot = import_node_path3.default.resolve(options.receiptRoot || getNativeSkillReceiptRoot(options)); + const targetRoot = import_node_path4.default.resolve(options.targetRoot || getNativeSkillRoot(host, options)); + const receiptRoot = import_node_path4.default.resolve(options.receiptRoot || getNativeSkillReceiptRoot(options)); await assertSafeRoot(targetRoot, "Native skill target root"); await assertSafeRoot(receiptRoot, "Native skill receipt root"); - const targetDir = import_node_path3.default.join(targetRoot, projection.skillName); + const targetDir = import_node_path4.default.join(targetRoot, projection.skillName); const receiptPath = receiptPathFor(receiptRoot, host, projection.skillName); const receiptExpectation = { host, @@ -22833,16 +23134,16 @@ async function promoteProjection({ priorActualDigest, writeReceipt = atomicWriteReceipt }) { - const targetRoot = import_node_path3.default.dirname(targetDir); - const backupDir = import_node_path3.default.join( + const targetRoot = import_node_path4.default.dirname(targetDir); + const backupDir = import_node_path4.default.join( targetRoot, - `.${import_node_path3.default.basename(targetDir)}.rudi-backup-${import_node_crypto2.default.randomUUID()}` + `.${import_node_path4.default.basename(targetDir)}.rudi-backup-${import_node_crypto2.default.randomUUID()}` ); let backedUp = false; let promoted = false; try { await assertSafeRoot(targetRoot, "Native skill target root"); - await assertSafeRoot(import_node_path3.default.dirname(receiptPath), "Native skill receipt directory"); + await assertSafeRoot(import_node_path4.default.dirname(receiptPath), "Native skill receipt directory"); const current = await inspectTree(targetDir); if ((current?.digest || null) !== (priorActualDigest || null)) { throw new Error(`Native skill target changed during reconciliation: ${targetDir}`); @@ -22928,8 +23229,8 @@ async function reconcileNativeSkill(options = {}) { if (inspected.state === "unmanaged" && inspected.expectedMatchesActual) { if (dryRun) return { ...base, action: "would_adopt" }; const projection2 = await buildProjection(host, skill); - await assertSafeRoot(import_node_path3.default.dirname(inspected.targetDir), "Native skill target root"); - await assertSafeRoot(import_node_path3.default.dirname(import_node_path3.default.dirname(inspected.receiptPath)), "Native skill receipt root"); + await assertSafeRoot(import_node_path4.default.dirname(inspected.targetDir), "Native skill target root"); + await assertSafeRoot(import_node_path4.default.dirname(import_node_path4.default.dirname(inspected.receiptPath)), "Native skill receipt root"); const current = await inspectTree(inspected.targetDir); if (current?.digest !== projection2.renderedTreeDigest) { throw new Error(`Native skill target changed during adoption: ${inspected.targetDir}`); @@ -22977,7 +23278,7 @@ async function reconcileNativeSkill(options = {}) { await atomicWriteReceipt(inspected.receiptPath, receipt); return { ...base, action, restartRequired: false }; } - const targetRoot = import_node_path3.default.dirname(inspected.targetDir); + const targetRoot = import_node_path4.default.dirname(inspected.targetDir); const stageDir = await writeProjectionStage(targetRoot, projection); try { const warnings = await promoteProjection({ @@ -23063,12 +23364,12 @@ async function removeOrphanReceipt({ targetDir, operations = {} }) { - const receiptDirectory = import_node_path3.default.dirname(receiptPath); + const receiptDirectory = import_node_path4.default.dirname(receiptPath); await assertSafeRoot(receiptDirectory, "Native skill receipt directory"); await assertReceiptUnchanged(receiptPath, receipt, receiptExpectation); - const isolatedReceiptPath = import_node_path3.default.join( + const isolatedReceiptPath = import_node_path4.default.join( receiptDirectory, - `.${import_node_path3.default.basename(receiptPath)}.rudi-orphan-${import_node_crypto2.default.randomUUID()}` + `.${import_node_path4.default.basename(receiptPath)}.rudi-orphan-${import_node_crypto2.default.randomUUID()}` ); await fsp.rename(receiptPath, isolatedReceiptPath); try { @@ -23109,11 +23410,11 @@ async function removeNativeSkillProjection(options = {}) { try { assertSupportedHost(host); const skillName = normalizeNativeSkillName(skill); - const targetRoot = import_node_path3.default.resolve(options.targetRoot || getNativeSkillRoot(host, options)); - const receiptRoot = import_node_path3.default.resolve(options.receiptRoot || getNativeSkillReceiptRoot(options)); + const targetRoot = import_node_path4.default.resolve(options.targetRoot || getNativeSkillRoot(host, options)); + const receiptRoot = import_node_path4.default.resolve(options.receiptRoot || getNativeSkillReceiptRoot(options)); await assertSafeRoot(targetRoot, "Native skill target root"); await assertSafeRoot(receiptRoot, "Native skill receipt root"); - const targetDir = import_node_path3.default.join(targetRoot, skillName); + const targetDir = import_node_path4.default.join(targetRoot, skillName); const receiptPath = receiptPathFor(receiptRoot, host, skillName); const receiptExpectation = { host, skillId: skill.id, skillName, targetDir }; const receipt = await readReceipt(receiptPath, receiptExpectation); @@ -23143,7 +23444,7 @@ async function removeNativeSkillProjection(options = {}) { if (options.dryRun === true) { return { ...base, action: "would_remove", restartRequired: true }; } - const backupDir = import_node_path3.default.join(targetRoot, `.${skillName}.rudi-remove-${import_node_crypto2.default.randomUUID()}`); + const backupDir = import_node_path4.default.join(targetRoot, `.${skillName}.rudi-remove-${import_node_crypto2.default.randomUUID()}`); await assertSafeRoot(targetRoot, "Native skill target root"); await assertSafeRoot(receiptRoot, "Native skill receipt root"); await assertReceiptUnchanged(receiptPath, receipt, receiptExpectation); @@ -23205,8 +23506,8 @@ async function removeNativeSkillProjections(options = {}) { } async function summarizeNativeSkillHost(host, options = {}) { assertSupportedHost(host); - const targetRoot = import_node_path3.default.resolve(options.targetRoot || getNativeSkillRoot(host, options)); - const receiptRoot = import_node_path3.default.resolve(options.receiptRoot || getNativeSkillReceiptRoot(options)); + const targetRoot = import_node_path4.default.resolve(options.targetRoot || getNativeSkillRoot(host, options)); + const receiptRoot = import_node_path4.default.resolve(options.receiptRoot || getNativeSkillReceiptRoot(options)); try { await assertSafeRoot(targetRoot, "Native skill target root"); await assertSafeRoot(receiptRoot, "Native skill receipt root"); @@ -23221,7 +23522,7 @@ async function summarizeNativeSkillHost(host, options = {}) { error: error.message }; } - const hostReceiptRoot = import_node_path3.default.join(receiptRoot, host); + const hostReceiptRoot = import_node_path4.default.join(receiptRoot, host); const summary = { current: 0, drifted: 0, missing: 0, failed: 0, totalManaged: 0 }; let names; try { @@ -23236,8 +23537,8 @@ async function summarizeNativeSkillHost(host, options = {}) { try { const skillName = name.slice(0, -".json".length); if (!SKILL_NAME_PATTERN.test(skillName)) throw new Error(`Invalid receipt name: ${name}`); - const targetDir = import_node_path3.default.join(targetRoot, skillName); - const receipt = await readReceipt(import_node_path3.default.join(hostReceiptRoot, name), { + const targetDir = import_node_path4.default.join(targetRoot, skillName); + const receipt = await readReceipt(import_node_path4.default.join(hostReceiptRoot, name), { host, skillId: `skill:${skillName}`, skillName, @@ -23259,12 +23560,12 @@ async function summarizeNativeSkillHost(host, options = {}) { } async function getManagedNativeSkillHosts(skill, options = {}) { const skillName = normalizeNativeSkillName(skill); - const receiptRoot = import_node_path3.default.resolve(options.receiptRoot || getNativeSkillReceiptRoot(options)); + const receiptRoot = import_node_path4.default.resolve(options.receiptRoot || getNativeSkillReceiptRoot(options)); await assertSafeRoot(receiptRoot, "Native skill receipt root"); const hosts = []; for (const host of NATIVE_SKILL_HOSTS) { const receiptPath = receiptPathFor(receiptRoot, host, skillName); - const targetDir = import_node_path3.default.join(getNativeSkillRoot(host, options), skillName); + const targetDir = import_node_path4.default.join(getNativeSkillRoot(host, options), skillName); const receipt = await readReceipt(receiptPath, { host, skillId: skill.id, @@ -23283,8 +23584,8 @@ init_src5(); init_src5(); // src/agent-host/artifacts.js -var import_node_fs3 = __toESM(require("node:fs"), 1); -var import_node_path4 = __toESM(require("node:path"), 1); +var import_node_fs4 = __toESM(require("node:fs"), 1); +var import_node_path5 = __toESM(require("node:path"), 1); init_src(); var LAUNCH_ID_PATTERN = /^launch_[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; var OWNERSHIP_MARKER = ".rudi-agent-launch.json"; @@ -23302,54 +23603,54 @@ function getAgentHostPaths({ launchId = null, rudiHome = PATHS.home } = {}) { - const home = import_node_path4.default.resolve(rudiHome); - const stateDirectory = import_node_path4.default.join(home, "state"); - const artifactsRoot = import_node_path4.default.join(home, "artifacts", "agent-launches"); + const home = import_node_path5.default.resolve(rudiHome); + const stateDirectory = import_node_path5.default.join(home, "state"); + const artifactsRoot = import_node_path5.default.join(home, "artifacts", "agent-launches"); const result = { artifactsRoot, - stateDatabase: import_node_path4.default.join(stateDirectory, "agent-hosts.db"), + stateDatabase: import_node_path5.default.join(stateDirectory, "agent-hosts.db"), stateDirectory }; if (launchId != null) { assertLaunchId(launchId); - result.launchDirectory = import_node_path4.default.join(artifactsRoot, launchId); - result.workspaceDirectory = import_node_path4.default.join(result.launchDirectory, "workspace"); + result.launchDirectory = import_node_path5.default.join(artifactsRoot, launchId); + result.workspaceDirectory = import_node_path5.default.join(result.launchDirectory, "workspace"); } return result; } function getLaunchArtifactFiles(launchDirectory) { - const directory = import_node_path4.default.resolve(launchDirectory); + const directory = import_node_path5.default.resolve(launchDirectory); return Object.freeze({ - events: import_node_path4.default.join(directory, EVENTS_FILE), - marker: import_node_path4.default.join(directory, OWNERSHIP_MARKER), - stderr: import_node_path4.default.join(directory, STDERR_FILE) + events: import_node_path5.default.join(directory, EVENTS_FILE), + marker: import_node_path5.default.join(directory, OWNERSHIP_MARKER), + stderr: import_node_path5.default.join(directory, STDERR_FILE) }); } function createLaunchOwnershipMarker({ launchDirectory, launchId }) { assertLaunchId(launchId); - const directory = import_node_path4.default.resolve(launchDirectory); - const stat = import_node_fs3.default.statSync(directory); + const directory = import_node_path5.default.resolve(launchDirectory); + const stat = import_node_fs4.default.statSync(directory); if (!stat.isDirectory()) throw new Error(`Launch artifact path is not a directory: ${directory}`); const { marker } = getLaunchArtifactFiles(directory); const payload = `${JSON.stringify({ launchId, schemaVersion: 1 })} `; - const handle = import_node_fs3.default.openSync(marker, "wx", 384); + const handle = import_node_fs4.default.openSync(marker, "wx", 384); try { - import_node_fs3.default.writeFileSync(handle, payload, "utf8"); + import_node_fs4.default.writeFileSync(handle, payload, "utf8"); } finally { - import_node_fs3.default.closeSync(handle); + import_node_fs4.default.closeSync(handle); } return marker; } function assertOwnedLaunchDirectory({ launchDirectory, launchId }) { assertLaunchId(launchId); - const directory = import_node_path4.default.resolve(launchDirectory); + const directory = import_node_path5.default.resolve(launchDirectory); const { marker } = getLaunchArtifactFiles(directory); let parsed; try { - const stat = import_node_fs3.default.lstatSync(marker); + const stat = import_node_fs4.default.lstatSync(marker); if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("marker is not a regular file"); - parsed = JSON.parse(import_node_fs3.default.readFileSync(marker, "utf8")); + parsed = JSON.parse(import_node_fs4.default.readFileSync(marker, "utf8")); } catch (error) { throw new Error(`Launch artifact ownership marker is invalid: ${error.message}`); } @@ -23364,17 +23665,17 @@ function appendLaunchEvent(eventFile, event) { if (Buffer.byteLength(serialized, "utf8") > MAX_EVENT_BYTES) { throw new Error(`Agent event exceeds ${MAX_EVENT_BYTES} bytes`); } - const file = import_node_path4.default.resolve(eventFile); - const handle = import_node_fs3.default.openSync(file, "a", 384); + const file = import_node_path5.default.resolve(eventFile); + const handle = import_node_fs4.default.openSync(file, "a", 384); try { - import_node_fs3.default.writeFileSync(handle, serialized, "utf8"); + import_node_fs4.default.writeFileSync(handle, serialized, "utf8"); } finally { - import_node_fs3.default.closeSync(handle); + import_node_fs4.default.closeSync(handle); } - import_node_fs3.default.chmodSync(file, 384); + import_node_fs4.default.chmodSync(file, 384); } function readLaunchEvents({ eventFile, limitBytes = 1024 * 1024, offset = 0 }) { - const file = import_node_path4.default.resolve(eventFile); + const file = import_node_path5.default.resolve(eventFile); const validOffset = Number(offset); const validLimit = Number(limitBytes); if (!Number.isSafeInteger(validOffset) || validOffset < 0) { @@ -23385,7 +23686,7 @@ function readLaunchEvents({ eventFile, limitBytes = 1024 * 1024, offset = 0 }) { } let stat; try { - stat = import_node_fs3.default.statSync(file); + stat = import_node_fs4.default.statSync(file); } catch (error) { if (error.code === "ENOENT") return { data: "", eof: true, nextOffset: validOffset }; throw error; @@ -23396,12 +23697,12 @@ function readLaunchEvents({ eventFile, limitBytes = 1024 * 1024, offset = 0 }) { const remaining = stat.size - validOffset; const bytesToRead = Math.min(remaining, validLimit + MAX_EVENT_BYTES); const buffer = Buffer.allocUnsafe(bytesToRead); - const handle = import_node_fs3.default.openSync(file, "r"); + const handle = import_node_fs4.default.openSync(file, "r"); let bytesRead; try { - bytesRead = import_node_fs3.default.readSync(handle, buffer, 0, bytesToRead, validOffset); + bytesRead = import_node_fs4.default.readSync(handle, buffer, 0, bytesToRead, validOffset); } finally { - import_node_fs3.default.closeSync(handle); + import_node_fs4.default.closeSync(handle); } let pageBytes = bytesRead; if (remaining > validLimit) { @@ -23423,8 +23724,8 @@ function readLaunchEvents({ eventFile, limitBytes = 1024 * 1024, offset = 0 }) { } // src/agent-host/launch-store.js -var import_node_fs4 = __toESM(require("node:fs"), 1); -var import_node_path5 = __toESM(require("node:path"), 1); +var import_node_fs5 = __toESM(require("node:fs"), 1); +var import_node_path6 = __toESM(require("node:path"), 1); var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1); var LAUNCH_STATUSES = Object.freeze([ "starting", @@ -23586,10 +23887,10 @@ function createLaunchStore({ databasePath = getAgentHostPaths().stateDatabase, now = () => (/* @__PURE__ */ new Date()).toISOString() } = {}) { - const resolvedPath = import_node_path5.default.resolve(databasePath); - import_node_fs4.default.mkdirSync(import_node_path5.default.dirname(resolvedPath), { recursive: true, mode: 448 }); + const resolvedPath = import_node_path6.default.resolve(databasePath); + import_node_fs5.default.mkdirSync(import_node_path6.default.dirname(resolvedPath), { recursive: true, mode: 448 }); const database = new import_better_sqlite3.default(resolvedPath); - import_node_fs4.default.chmodSync(resolvedPath, 384); + import_node_fs5.default.chmodSync(resolvedPath, 384); initialize(database); const getStatement = database.prepare("SELECT * FROM agent_launches WHERE launch_id = ?"); function get(launchId) { @@ -24639,18 +24940,18 @@ async function attachAgentLaunch(launchId, dependencies = {}) { } // src/agent-host/cli-inputs.js -var import_node_fs9 = __toESM(require("node:fs"), 1); -var import_node_path10 = __toESM(require("node:path"), 1); +var import_node_fs10 = __toESM(require("node:fs"), 1); +var import_node_path11 = __toESM(require("node:path"), 1); // src/agent-host/private-automation-profile.js -var import_node_fs6 = __toESM(require("node:fs"), 1); -var import_node_path7 = __toESM(require("node:path"), 1); +var import_node_fs7 = __toESM(require("node:fs"), 1); +var import_node_path8 = __toESM(require("node:path"), 1); var import_node_child_process2 = require("node:child_process"); var import_ajv = __toESM(require_ajv(), 1); // src/agent-host/providers/catalog.js -var import_node_fs5 = require("node:fs"); -var import_node_path6 = require("node:path"); +var import_node_fs6 = require("node:fs"); +var import_node_path7 = require("node:path"); var import_node_os2 = require("node:os"); // src/agent-host/providers/config/claude.json @@ -25596,7 +25897,7 @@ function loadProviderConfig(providerId) { return config; } function canonicalPath(candidate, realpathSyncImpl) { - const absolute = (0, import_node_path6.resolve)(candidate); + const absolute = (0, import_node_path7.resolve)(candidate); try { return realpathSyncImpl(absolute); } catch { @@ -25604,23 +25905,23 @@ function canonicalPath(candidate, realpathSyncImpl) { } } function isInside(root, candidate) { - const child = (0, import_node_path6.relative)(root, candidate); - return child === "" || child !== ".." && !child.startsWith(`..${import_node_path6.sep}`) && !(0, import_node_path6.isAbsolute)(child); + const child = (0, import_node_path7.relative)(root, candidate); + return child === "" || child !== ".." && !child.startsWith(`..${import_node_path7.sep}`) && !(0, import_node_path7.isAbsolute)(child); } function isExternalAgentBinaryPath(candidate, options = {}) { - if (typeof candidate !== "string" || !(0, import_node_path6.isAbsolute)(candidate.trim()) || candidate.length > 4096 || /[\r\n\0]/u.test(candidate)) return false; + if (typeof candidate !== "string" || !(0, import_node_path7.isAbsolute)(candidate.trim()) || candidate.length > 4096 || /[\r\n\0]/u.test(candidate)) return false; const home = options.home || (0, import_node_os2.homedir)(); - const realpathSyncImpl = options.realpathSyncImpl || import_node_fs5.realpathSync; - const lexicalCandidate = (0, import_node_path6.resolve)(candidate.trim()); + const realpathSyncImpl = options.realpathSyncImpl || import_node_fs6.realpathSync; + const lexicalCandidate = (0, import_node_path7.resolve)(candidate.trim()); const canonicalCandidate = canonicalPath(lexicalCandidate, realpathSyncImpl); - const rudiRoots = [(0, import_node_path6.join)(home, ".rudi"), options.rudiHome || process.env.RUDI_HOME].filter((root) => typeof root === "string" && (0, import_node_path6.isAbsolute)(root) && root.length <= 4096 && !/[\r\n\0]/u.test(root)).flatMap((root) => [(0, import_node_path6.resolve)(root), canonicalPath(root, realpathSyncImpl)]); + const rudiRoots = [(0, import_node_path7.join)(home, ".rudi"), options.rudiHome || process.env.RUDI_HOME].filter((root) => typeof root === "string" && (0, import_node_path7.isAbsolute)(root) && root.length <= 4096 && !/[\r\n\0]/u.test(root)).flatMap((root) => [(0, import_node_path7.resolve)(root), canonicalPath(root, realpathSyncImpl)]); return !rudiRoots.some((root) => isInside(root, lexicalCandidate) || isInside(root, canonicalCandidate)); } function resolveProviderBinary(config, dependencies = {}) { const home = dependencies.home || (0, import_node_os2.homedir)(); const arch = process.arch; - const accessSyncImpl = dependencies.accessSyncImpl || import_node_fs5.accessSync; - const existsSyncImpl = dependencies.existsSyncImpl || import_node_fs5.existsSync; + const accessSyncImpl = dependencies.accessSyncImpl || import_node_fs6.accessSync; + const existsSyncImpl = dependencies.existsSyncImpl || import_node_fs6.existsSync; const runCommandPlanImpl = dependencies.runCommandPlanImpl || runCommandPlan2; const externalPathCheck = dependencies.isExternalAgentBinaryPathImpl || ((candidate) => isExternalAgentBinaryPath(candidate, { home, @@ -25629,7 +25930,7 @@ function resolveProviderBinary(config, dependencies = {}) { })); const isExecutable = (candidate) => { try { - accessSyncImpl(candidate, import_node_fs5.constants.X_OK); + accessSyncImpl(candidate, import_node_fs6.constants.X_OK); return true; } catch { return false; @@ -25827,10 +26128,10 @@ function containsSchemaReference(value) { return Object.values(value).some(containsSchemaReference); } function readOutputSchema(outputSchemaPath) { - const requested = import_node_path7.default.resolve(requiredText(outputSchemaPath, "output schema path")); + const requested = import_node_path8.default.resolve(requiredText(outputSchemaPath, "output schema path")); let stat; try { - stat = import_node_fs6.default.lstatSync(requested); + stat = import_node_fs7.default.lstatSync(requested); } catch { throw new Error(`private automation output schema does not exist: ${requested}`); } @@ -25842,7 +26143,7 @@ function readOutputSchema(outputSchemaPath) { } let schema; try { - schema = JSON.parse(import_node_fs6.default.readFileSync(requested, "utf8")); + schema = JSON.parse(import_node_fs7.default.readFileSync(requested, "utf8")); } catch { throw new Error("private automation output schema must contain valid JSON"); } @@ -25869,7 +26170,7 @@ function readOutputSchema(outputSchemaPath) { } return Object.freeze({ canonical: JSON.stringify(schema), - path: import_node_fs6.default.realpathSync(requested), + path: import_node_fs7.default.realpathSync(requested), schema: Object.freeze(schema), validate }); @@ -26065,7 +26366,7 @@ function assertPrivateAutomationHostCapabilities({ binaryPath, profile }, depend "--color", "never", "-C", - import_node_path7.default.dirname(profile.outputSchema.path), + import_node_path8.default.dirname(profile.outputSchema.path), "-m", profile.model, "--output-schema", @@ -26148,9 +26449,9 @@ function assertPrivateAutomationHostCapabilities({ binaryPath, profile }, depend } // src/agent-host/providers/common.js -var import_node_fs7 = __toESM(require("node:fs"), 1); +var import_node_fs8 = __toESM(require("node:fs"), 1); var import_node_os3 = __toESM(require("node:os"), 1); -var import_node_path8 = __toESM(require("node:path"), 1); +var import_node_path9 = __toESM(require("node:path"), 1); var MAX_PROMPT_BYTES = 10 * 1024 * 1024; var PERMISSION_ALIASES = Object.freeze({ "accept-edits": "acceptEdits", @@ -26252,28 +26553,28 @@ function validateImages(images) { return images.map((image, index) => requiredText2(image, `images[${index}]`, 4096)); } function canonicalPath2(candidate) { - const absolute = import_node_path8.default.resolve(candidate); + const absolute = import_node_path9.default.resolve(candidate); try { - return import_node_fs7.default.realpathSync(absolute); + return import_node_fs8.default.realpathSync(absolute); } catch { return absolute; } } function isInsidePath(root, candidate) { - const child = import_node_path8.default.relative(root, candidate); - return child === "" || child !== ".." && !child.startsWith(`..${import_node_path8.default.sep}`) && !import_node_path8.default.isAbsolute(child); + const child = import_node_path9.default.relative(root, candidate); + return child === "" || child !== ".." && !child.startsWith(`..${import_node_path9.default.sep}`) && !import_node_path9.default.isAbsolute(child); } function rudiOwnedPathRoots(environment) { const home = environment.HOME || import_node_os3.default.homedir(); return [ - import_node_path8.default.join(home, ".rudi"), + import_node_path9.default.join(home, ".rudi"), environment.RUDI_HOME, process.env.RUDI_HOME - ].filter((root) => typeof root === "string" && import_node_path8.default.isAbsolute(root)).flatMap((root) => [import_node_path8.default.resolve(root), canonicalPath2(root)]); + ].filter((root) => typeof root === "string" && import_node_path9.default.isAbsolute(root)).flatMap((root) => [import_node_path9.default.resolve(root), canonicalPath2(root)]); } function isRudiOwnedPathEntry(entry, roots) { if (typeof entry !== "string" || entry.length === 0) return false; - const lexicalEntry = import_node_path8.default.resolve(entry); + const lexicalEntry = import_node_path9.default.resolve(entry); const canonicalEntry = canonicalPath2(entry); return roots.some((root) => isInsidePath(root, lexicalEntry) || isInsidePath(root, canonicalEntry)); } @@ -26281,10 +26582,10 @@ function buildAgentExecutableEnvironment(binaryPath, overrides = {}, baseEnviron const merged = { ...baseEnvironment, ...overrides }; const rudiRoots = rudiOwnedPathRoots(merged); const entries = [ - import_node_path8.default.dirname(binaryPath), - ...String(merged.PATH || "").split(import_node_path8.default.delimiter) - ].filter((entry) => entry && import_node_path8.default.isAbsolute(entry) && !isRudiOwnedPathEntry(entry, rudiRoots)); - merged.PATH = [...new Set(entries)].join(import_node_path8.default.delimiter); + import_node_path9.default.dirname(binaryPath), + ...String(merged.PATH || "").split(import_node_path9.default.delimiter) + ].filter((entry) => entry && import_node_path9.default.isAbsolute(entry) && !isRudiOwnedPathEntry(entry, rudiRoots)); + merged.PATH = [...new Set(entries)].join(import_node_path9.default.delimiter); return merged; } var PRIVATE_OPERATIONAL_ENVIRONMENT_KEYS = Object.freeze([ @@ -26312,10 +26613,10 @@ function buildPrivateProviderEnvironment(config, binaryPath, options = {}) { } function buildProviderEnvironment(config, options = {}) { const baseEnvironment = options.baseEnvironment || process.env; - const rudiHome = options.rudiHome || process.env.RUDI_HOME || import_node_path8.default.join(import_node_os3.default.homedir(), ".rudi"); + const rudiHome = options.rudiHome || process.env.RUDI_HOME || import_node_path9.default.join(import_node_os3.default.homedir(), ".rudi"); let storedSecrets = {}; try { - const parsed = JSON.parse(import_node_fs7.default.readFileSync(import_node_path8.default.join(rudiHome, "secrets.json"), "utf8")); + const parsed = JSON.parse(import_node_fs8.default.readFileSync(import_node_path9.default.join(rudiHome, "secrets.json"), "utf8")); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { storedSecrets = Object.fromEntries( Object.entries(parsed).filter(([, value]) => typeof value === "string" && value.length > 0) @@ -26523,8 +26824,8 @@ function buildCodexPlan(options) { } // src/agent-host/providers/gemini.js -var import_node_fs8 = __toESM(require("node:fs"), 1); -var import_node_path9 = __toESM(require("node:path"), 1); +var import_node_fs9 = __toESM(require("node:fs"), 1); +var import_node_path10 = __toESM(require("node:path"), 1); function defaultSystemSettingsPath(platform = process.platform) { if (platform === "darwin") return "/Library/Application Support/GeminiCli/settings.json"; if (platform === "win32") return "C:\\ProgramData\\gemini-cli\\settings.json"; @@ -26536,9 +26837,9 @@ function buildGeminiProviderEnvironment(config, options = {}) { if (!environment.GEMINI_API_KEY || !options.runtimeDirectory) return environment; if (baseEnvironment.GEMINI_CLI_SYSTEM_SETTINGS_PATH) return environment; const systemSettingsPath = options.systemSettingsPath || defaultSystemSettingsPath(options.platform); - if (import_node_fs8.default.existsSync(systemSettingsPath)) return environment; - const settingsPath = import_node_path9.default.join(options.runtimeDirectory, "gemini-system-settings.json"); - import_node_fs8.default.writeFileSync(settingsPath, JSON.stringify({ + if (import_node_fs9.default.existsSync(systemSettingsPath)) return environment; + const settingsPath = import_node_path10.default.join(options.runtimeDirectory, "gemini-system-settings.json"); + import_node_fs9.default.writeFileSync(settingsPath, JSON.stringify({ security: { auth: { selectedType: "gemini-api-key" } } }, null, 2), { encoding: "utf8", mode: 384 }); return { @@ -26649,16 +26950,16 @@ async function resolveAgentPrompt(flags, { prompt = requiredFlagString(inline, "--prompt"); } else if (promptFile != null) { const fileValue = requiredFlagString(promptFile, "--prompt-file"); - const filePath = import_node_path10.default.resolve(originDirectory, fileValue); + const filePath = import_node_path11.default.resolve(originDirectory, fileValue); let stat; try { - stat = import_node_fs9.default.statSync(filePath); + stat = import_node_fs10.default.statSync(filePath); } catch { throw new Error(`Prompt file does not exist: ${filePath}`); } if (!stat.isFile()) throw new Error(`Prompt file is not a regular file: ${filePath}`); if (stat.size > MAX_PROMPT_BYTES2) throw new Error(`Prompt file exceeds ${MAX_PROMPT_BYTES2} bytes`); - prompt = import_node_fs9.default.readFileSync(filePath, "utf8"); + prompt = import_node_fs10.default.readFileSync(filePath, "utf8"); } else if (stdin && stdin.isTTY !== true) { prompt = await readPromptStream( stdin, @@ -26689,10 +26990,10 @@ function parseImages(flags, originDirectory) { const value = flags.image ?? flags.images; if (value == null) return []; return requiredFlagString(value, "--image").split(",").map((item) => item.trim()).filter(Boolean).map((item) => { - const imagePath = import_node_path10.default.resolve(originDirectory, item); + const imagePath = import_node_path11.default.resolve(originDirectory, item); let stat; try { - stat = import_node_fs9.default.statSync(imagePath); + stat = import_node_fs10.default.statSync(imagePath); } catch { throw new Error(`Image attachment does not exist: ${imagePath}`); } @@ -26734,7 +27035,7 @@ function buildLaunchOptions(provider, prompt, flags, passthrough, originDirector flagValue(flags, "output-schema", "outputSchema"), "--output-schema" ); - const outputSchemaPath = import_node_path10.default.resolve(originDirectory, outputSchemaValue); + const outputSchemaPath = import_node_path11.default.resolve(originDirectory, outputSchemaValue); const timeoutMs = parseTimeout(flags); const privateAutomationProfile = createPrivateAutomationProfile({ model: flags.model, @@ -26808,16 +27109,16 @@ function readGroupTaskFiles(taskFlag, originDirectory, common = {}) { } const provider = value.slice(0, separator); resolveAgentProviderId(provider); - const filePath = import_node_path10.default.resolve(originDirectory, value.slice(separator + 1)); + const filePath = import_node_path11.default.resolve(originDirectory, value.slice(separator + 1)); let stat; try { - stat = import_node_fs9.default.statSync(filePath); + stat = import_node_fs10.default.statSync(filePath); } catch { throw new Error(`Task file does not exist: ${filePath}`); } if (!stat.isFile()) throw new Error(`Task file is not a regular file: ${filePath}`); if (stat.size > MAX_PROMPT_BYTES2) throw new Error(`Task file exceeds ${MAX_PROMPT_BYTES2} bytes`); - const prompt = import_node_fs9.default.readFileSync(filePath, "utf8"); + const prompt = import_node_fs10.default.readFileSync(filePath, "utf8"); if (!prompt.trim()) throw new Error(`Task file must not be empty: ${filePath}`); if (prompt.includes("\0")) throw new Error(`Task file must not contain NUL bytes: ${filePath}`); return { ...common, prompt, provider }; @@ -26828,7 +27129,7 @@ function readGroupTaskFiles(taskFlag, originDirectory, common = {}) { var import_node_crypto5 = __toESM(require("node:crypto"), 1); // src/agent-host/detached.js -var import_node_fs13 = __toESM(require("node:fs"), 1); +var import_node_fs14 = __toESM(require("node:fs"), 1); var import_node_child_process6 = require("node:child_process"); // src/agent-host/launch.js @@ -27208,7 +27509,7 @@ function executeForegroundLaunch({ } // src/agent-host/preflight.js -var import_node_path11 = __toESM(require("node:path"), 1); +var import_node_path12 = __toESM(require("node:path"), 1); var import_node_child_process4 = require("node:child_process"); var MCP_AGENT_IDS = Object.freeze({ claude: "claude-code" }); function commandArgs(configuredCommand) { @@ -27229,7 +27530,7 @@ function hasRudiRouter(provider) { const agentId = MCP_AGENT_IDS[provider] || provider; const config = AGENT_CONFIGS.find((item) => item.id === agentId); if (!config) return false; - return readAgentMcpServers(config).some((server) => server.name === "rudi" || import_node_path11.default.basename(String(server.command)) === "rudi-router"); + return readAgentMcpServers(config).some((server) => server.name === "rudi" || import_node_path12.default.basename(String(server.command)) === "rudi-router"); } async function inspectAgentHost(provider, dependencies = {}) { const { spawnSyncImpl = import_node_child_process4.spawnSync } = dependencies; @@ -27292,35 +27593,35 @@ async function assertAgentHostReady({ binaryPath, provider }, dependencies = {}) } // src/agent-host/workspace.js -var import_node_fs11 = __toESM(require("node:fs"), 1); -var import_node_path13 = __toESM(require("node:path"), 1); +var import_node_fs12 = __toESM(require("node:fs"), 1); +var import_node_path14 = __toESM(require("node:path"), 1); var import_node_child_process5 = require("node:child_process"); // src/agent-host/workspace-manifest.js var import_node_crypto3 = __toESM(require("node:crypto"), 1); -var import_node_fs10 = __toESM(require("node:fs"), 1); -var import_node_path12 = __toESM(require("node:path"), 1); +var import_node_fs11 = __toESM(require("node:fs"), 1); +var import_node_path13 = __toESM(require("node:path"), 1); var WORKSPACE_BASELINE_FILE = "workspace-base.json"; function shouldSkip(relativePath) { - const first = relativePath.split(import_node_path12.default.sep)[0]; + const first = relativePath.split(import_node_path13.default.sep)[0]; return first === ".git" || first === ".rudi"; } function portablePath(relativePath) { - return relativePath.split(import_node_path12.default.sep).join("/"); + return relativePath.split(import_node_path13.default.sep).join("/"); } function hashFile(file) { - return import_node_crypto3.default.createHash("sha256").update(import_node_fs10.default.readFileSync(file)).digest("hex"); + return import_node_crypto3.default.createHash("sha256").update(import_node_fs11.default.readFileSync(file)).digest("hex"); } function createWorkspaceManifest(rootDirectory) { - const root = import_node_fs10.default.realpathSync(import_node_path12.default.resolve(rootDirectory)); + const root = import_node_fs11.default.realpathSync(import_node_path13.default.resolve(rootDirectory)); const entries = {}; function visit(directory, prefix = "") { - const children = import_node_fs10.default.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name)); + const children = import_node_fs11.default.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name)); for (const child of children) { - const relative4 = prefix ? import_node_path12.default.join(prefix, child.name) : child.name; + const relative4 = prefix ? import_node_path13.default.join(prefix, child.name) : child.name; if (shouldSkip(relative4)) continue; - const absolute = import_node_path12.default.join(directory, child.name); - const stat = import_node_fs10.default.lstatSync(absolute); + const absolute = import_node_path13.default.join(directory, child.name); + const stat = import_node_fs11.default.lstatSync(absolute); const key = portablePath(relative4); if (stat.isDirectory()) { entries[key] = { mode: stat.mode & 511, type: "directory" }; @@ -27335,7 +27636,7 @@ function createWorkspaceManifest(rootDirectory) { } else if (stat.isSymbolicLink()) { entries[key] = { mode: stat.mode & 511, - target: import_node_fs10.default.readlinkSync(absolute), + target: import_node_fs11.default.readlinkSync(absolute), type: "symlink" }; } else { @@ -27347,24 +27648,24 @@ function createWorkspaceManifest(rootDirectory) { return { entries, schemaVersion: 1 }; } function writeWorkspaceBaseline({ launchDirectory, workspace }) { - const destination = import_node_path12.default.join(import_node_path12.default.resolve(launchDirectory), WORKSPACE_BASELINE_FILE); + const destination = import_node_path13.default.join(import_node_path13.default.resolve(launchDirectory), WORKSPACE_BASELINE_FILE); const manifest = createWorkspaceManifest(workspace); - const handle = import_node_fs10.default.openSync(destination, "wx", 384); + const handle = import_node_fs11.default.openSync(destination, "wx", 384); try { - import_node_fs10.default.writeFileSync(handle, `${JSON.stringify(manifest)} + import_node_fs11.default.writeFileSync(handle, `${JSON.stringify(manifest)} `, "utf8"); } finally { - import_node_fs10.default.closeSync(handle); + import_node_fs11.default.closeSync(handle); } return destination; } function readWorkspaceBaseline(launchDirectory) { - const file = import_node_path12.default.join(import_node_path12.default.resolve(launchDirectory), WORKSPACE_BASELINE_FILE); + const file = import_node_path13.default.join(import_node_path13.default.resolve(launchDirectory), WORKSPACE_BASELINE_FILE); let parsed; try { - const stat = import_node_fs10.default.lstatSync(file); + const stat = import_node_fs11.default.lstatSync(file); if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("baseline is not a regular file"); - parsed = JSON.parse(import_node_fs10.default.readFileSync(file, "utf8")); + parsed = JSON.parse(import_node_fs11.default.readFileSync(file, "utf8")); } catch (error) { throw new Error(`Isolated workspace baseline is unavailable: ${error.message}`); } @@ -27408,21 +27709,21 @@ var WORKSPACE_MODES = Object.freeze({ }); var VALID_MODES = new Set(Object.values(WORKSPACE_MODES)); function existingDirectory2(candidate, label) { - const resolved = import_node_path13.default.resolve(candidate); + const resolved = import_node_path14.default.resolve(candidate); let stat; try { - stat = import_node_fs11.default.statSync(resolved); + stat = import_node_fs12.default.statSync(resolved); } catch { throw new Error(`${label} does not exist: ${resolved}`); } if (!stat.isDirectory()) { throw new Error(`${label} is not a directory: ${resolved}`); } - return import_node_fs11.default.realpathSync(resolved); + return import_node_fs12.default.realpathSync(resolved); } function isInside2(candidate, parent) { - const relative4 = import_node_path13.default.relative(parent, candidate); - return relative4 === "" || !relative4.startsWith(`..${import_node_path13.default.sep}`) && relative4 !== ".." && !import_node_path13.default.isAbsolute(relative4); + const relative4 = import_node_path14.default.relative(parent, candidate); + return relative4 === "" || !relative4.startsWith(`..${import_node_path14.default.sep}`) && relative4 !== ".." && !import_node_path14.default.isAbsolute(relative4); } function findGitProjectRoot(workspace, execFileSyncImpl) { try { @@ -27465,7 +27766,7 @@ function createGitWorktree({ } catch (error) { if (error?.message?.startsWith("Worktree branch already exists:")) throw error; } - import_node_fs11.default.mkdirSync(import_node_path13.default.dirname(destination), { recursive: true, mode: 448 }); + import_node_fs12.default.mkdirSync(import_node_path14.default.dirname(destination), { recursive: true, mode: 448 }); try { execFileSyncImpl("git", ["worktree", "add", "-b", branch, destination, baseRef], { cwd: projectRoot, @@ -27479,7 +27780,7 @@ function createGitWorktree({ }); } catch { } - import_node_fs11.default.rmSync(destination, { recursive: true, force: true }); + import_node_fs12.default.rmSync(destination, { recursive: true, force: true }); try { execFileSyncImpl("git", ["branch", "-D", "--", branch], { cwd: projectRoot, @@ -27496,15 +27797,15 @@ function copyIsolatedWorkspace({ destination, projectRoot }) { throw new Error("Isolated workspace destination cannot be inside the source project"); } try { - import_node_fs11.default.cpSync(projectRoot, destination, { + import_node_fs12.default.cpSync(projectRoot, destination, { errorOnExist: true, filter(candidate) { - const relative4 = import_node_path13.default.relative(projectRoot, candidate); - const firstPart = relative4.split(import_node_path13.default.sep)[0]; + const relative4 = import_node_path14.default.relative(projectRoot, candidate); + const firstPart = relative4.split(import_node_path14.default.sep)[0]; if (firstPart === ".git" || firstPart === ".rudi") return false; - const stat = import_node_fs11.default.lstatSync(candidate); + const stat = import_node_fs12.default.lstatSync(candidate); if (stat.isSymbolicLink()) { - const target = import_node_fs11.default.realpathSync(candidate); + const target = import_node_fs12.default.realpathSync(candidate); if (!isInside2(target, projectRoot)) { throw new Error(`Workspace contains a symlink outside the project: ${candidate}`); } @@ -27515,7 +27816,7 @@ function copyIsolatedWorkspace({ destination, projectRoot }) { recursive: true }); } catch (error) { - import_node_fs11.default.rmSync(destination, { recursive: true, force: true }); + import_node_fs12.default.rmSync(destination, { recursive: true, force: true }); throw new Error(`Unable to create isolated workspace copy: ${error.message}`); } } @@ -27541,12 +27842,12 @@ function resolveAgentWorkspace(options, dependencies = {}) { throw new Error("artifactsRoot is required"); } const resolvedOrigin = existingDirectory2(originDirectory, "Origin directory"); - const requestedWorkspace = workspace == null ? resolvedOrigin : import_node_path13.default.resolve(resolvedOrigin, workspace); + const requestedWorkspace = workspace == null ? resolvedOrigin : import_node_path14.default.resolve(resolvedOrigin, workspace); const validWorkspace = existingDirectory2(requestedWorkspace, "Workspace"); const gitProjectRoot = findGitProjectRoot(validWorkspace, execFileSyncImpl); const projectRoot = gitProjectRoot || validWorkspace; const isGitRepository = Boolean(gitProjectRoot); - const launchDirectory = outputDirectory == null ? import_node_path13.default.resolve(artifactsRoot, launchId) : import_node_path13.default.resolve(resolvedOrigin, outputDirectory); + const launchDirectory = outputDirectory == null ? import_node_path14.default.resolve(artifactsRoot, launchId) : import_node_path14.default.resolve(resolvedOrigin, outputDirectory); let resolvedMode = mode; if (resolvedMode === WORKSPACE_MODES.AUTO) { resolvedMode = isGitRepository ? WORKSPACE_MODES.WORKTREE : WORKSPACE_MODES.ISOLATED_COPY; @@ -27555,21 +27856,21 @@ function resolveAgentWorkspace(options, dependencies = {}) { throw new Error("Workspace mode worktree requires a Git repository"); } assertOutputOutsideProject(launchDirectory, projectRoot); - if (import_node_fs11.default.existsSync(launchDirectory)) { + if (import_node_fs12.default.existsSync(launchDirectory)) { throw new Error(`Output destination already exists: ${launchDirectory}`); } - import_node_fs11.default.mkdirSync(launchDirectory, { recursive: true, mode: 448 }); + import_node_fs12.default.mkdirSync(launchDirectory, { recursive: true, mode: 448 }); createLaunchOwnershipMarker({ launchDirectory, launchId }); let executionWorkspace = projectRoot; let worktreeBranch = null; let baseRef = null; try { if (privateAutomation === true) { - executionWorkspace = import_node_path13.default.join(launchDirectory, "private-workspace"); - import_node_fs11.default.mkdirSync(executionWorkspace, { mode: 320 }); - import_node_fs11.default.chmodSync(executionWorkspace, 320); + executionWorkspace = import_node_path14.default.join(launchDirectory, "private-workspace"); + import_node_fs12.default.mkdirSync(executionWorkspace, { mode: 320 }); + import_node_fs12.default.chmodSync(executionWorkspace, 320); } else if (resolvedMode === WORKSPACE_MODES.WORKTREE) { - executionWorkspace = import_node_path13.default.join(launchDirectory, "workspace"); + executionWorkspace = import_node_path14.default.join(launchDirectory, "workspace"); const created = createGitWorktree({ destination: executionWorkspace, execFileSyncImpl, @@ -27579,12 +27880,12 @@ function resolveAgentWorkspace(options, dependencies = {}) { worktreeBranch = created.branch; baseRef = created.baseRef; } else if (resolvedMode === WORKSPACE_MODES.ISOLATED_COPY) { - executionWorkspace = import_node_path13.default.join(launchDirectory, "workspace"); + executionWorkspace = import_node_path14.default.join(launchDirectory, "workspace"); copyIsolatedWorkspace({ destination: executionWorkspace, projectRoot }); writeWorkspaceBaseline({ launchDirectory, workspace: executionWorkspace }); } } catch (error) { - import_node_fs11.default.rmSync(launchDirectory, { recursive: true, force: true }); + import_node_fs12.default.rmSync(launchDirectory, { recursive: true, force: true }); throw error; } return Object.freeze({ @@ -27602,8 +27903,8 @@ function resolveAgentWorkspace(options, dependencies = {}) { function cleanupUnstartedWorkspace(workspace, dependencies = {}) { if (!workspace || typeof workspace !== "object") return; const { execFileSyncImpl = import_node_child_process5.execFileSync } = dependencies; - const outputDestination = import_node_path13.default.resolve(workspace.outputDestination); - const executionWorkspace = import_node_path13.default.resolve(workspace.executionWorkspace); + const outputDestination = import_node_path14.default.resolve(workspace.outputDestination); + const executionWorkspace = import_node_path14.default.resolve(workspace.executionWorkspace); if (!isInside2(executionWorkspace, outputDestination) && workspace.mode !== WORKSPACE_MODES.READ_ONLY) { throw new Error("Refusing to clean an execution workspace outside its launch output destination"); } @@ -27626,7 +27927,7 @@ function cleanupUnstartedWorkspace(workspace, dependencies = {}) { } catch { } } - import_node_fs11.default.rmSync(outputDestination, { recursive: true, force: true }); + import_node_fs12.default.rmSync(outputDestination, { recursive: true, force: true }); } // src/agent-host/launch.js @@ -27741,11 +28042,11 @@ async function launchAgent(options, dependencies = {}) { } // src/agent-host/resume.js -var import_node_fs12 = __toESM(require("node:fs"), 1); -var import_node_path14 = __toESM(require("node:path"), 1); +var import_node_fs13 = __toESM(require("node:fs"), 1); +var import_node_path15 = __toESM(require("node:path"), 1); function assertWorkspaceStillExists(workspace) { try { - if (import_node_fs12.default.statSync(workspace).isDirectory()) return; + if (import_node_fs13.default.statSync(workspace).isDirectory()) return; } catch { } throw new Error(`Execution workspace no longer exists: ${workspace}`); @@ -27789,11 +28090,11 @@ async function resumeAgentWithStore(options, dependencies) { throw new Error(getMissingAgentProviderMessage(previous.provider)); } await preflightImpl({ binaryPath, provider: previous.provider }); - const outputDestination = dependencies.artifactsRoot ? import_node_path14.default.resolve(artifactsRoot, launchId) : getAgentHostPaths({ launchId, rudiHome: dependencies.rudiHome }).launchDirectory; - if (import_node_fs12.default.existsSync(outputDestination)) { + const outputDestination = dependencies.artifactsRoot ? import_node_path15.default.resolve(artifactsRoot, launchId) : getAgentHostPaths({ launchId, rudiHome: dependencies.rudiHome }).launchDirectory; + if (import_node_fs13.default.existsSync(outputDestination)) { throw new Error(`Output destination already exists: ${outputDestination}`); } - import_node_fs12.default.mkdirSync(outputDestination, { recursive: true, mode: 448 }); + import_node_fs13.default.mkdirSync(outputDestination, { recursive: true, mode: 448 }); createLaunchOwnershipMarker({ launchDirectory: outputDestination, launchId }); const resolvedEventSink = eventSink || ((event) => appendLaunchEvent( getLaunchArtifactFiles(outputDestination).events, @@ -27816,7 +28117,7 @@ async function resumeAgentWithStore(options, dependencies) { workspaceMode: previous.workspaceMode }); } catch (error) { - import_node_fs12.default.rmSync(outputDestination, { recursive: true, force: true }); + import_node_fs13.default.rmSync(outputDestination, { recursive: true, force: true }); throw error; } store.create({ @@ -27874,13 +28175,13 @@ function discardSink() { } }; } function appendPrivateText(file, value) { - const handle = import_node_fs13.default.openSync(file, "a", 384); + const handle = import_node_fs14.default.openSync(file, "a", 384); try { - import_node_fs13.default.writeFileSync(handle, String(value), "utf8"); + import_node_fs14.default.writeFileSync(handle, String(value), "utf8"); } finally { - import_node_fs13.default.closeSync(handle); + import_node_fs14.default.closeSync(handle); } - import_node_fs13.default.chmodSync(file, 384); + import_node_fs14.default.chmodSync(file, 384); } async function dispatchDetachedAgent({ launchId, operation, options }, dependencies = {}) { assertLaunchId(launchId); @@ -28106,8 +28407,8 @@ async function stopAgentLaunch(launchId, dependencies = {}) { } // src/agent-host/workspace-lifecycle.js -var import_node_fs14 = __toESM(require("node:fs"), 1); -var import_node_path15 = __toESM(require("node:path"), 1); +var import_node_fs15 = __toESM(require("node:fs"), 1); +var import_node_path16 = __toESM(require("node:path"), 1); var import_node_child_process8 = require("node:child_process"); var TERMINAL_STATUSES4 = /* @__PURE__ */ new Set(["completed", "failed", "stopped"]); var MAX_DIFF_BYTES = 20 * 1024 * 1024; @@ -28121,7 +28422,7 @@ function git(execFileSyncImpl, cwd, args) { } function noIndexDiff(execFileSyncImpl, left, right) { try { - return git(execFileSyncImpl, import_node_path15.default.dirname(left), [ + return git(execFileSyncImpl, import_node_path16.default.dirname(left), [ "diff", "--no-index", "--binary", @@ -28136,16 +28437,16 @@ function noIndexDiff(execFileSyncImpl, left, right) { } } function isInside3(candidate, parent) { - const relative4 = import_node_path15.default.relative(parent, candidate); - return relative4 === "" || !relative4.startsWith(`..${import_node_path15.default.sep}`) && relative4 !== ".." && !import_node_path15.default.isAbsolute(relative4); + const relative4 = import_node_path16.default.relative(parent, candidate); + return relative4 === "" || !relative4.startsWith(`..${import_node_path16.default.sep}`) && relative4 !== ".." && !import_node_path16.default.isAbsolute(relative4); } function safeRelative(root, relativePath) { if (typeof relativePath !== "string" || relativePath === "" || relativePath.includes("\0")) { throw new Error("Launch change contains an invalid path"); } - const platformPath = relativePath.split("/").join(import_node_path15.default.sep); - const destination = import_node_path15.default.resolve(root, platformPath); - if (!isInside3(destination, import_node_path15.default.resolve(root)) || destination === import_node_path15.default.resolve(root)) { + const platformPath = relativePath.split("/").join(import_node_path16.default.sep); + const destination = import_node_path16.default.resolve(root, platformPath); + if (!isInside3(destination, import_node_path16.default.resolve(root)) || destination === import_node_path16.default.resolve(root)) { throw new Error(`Launch change escapes the workspace: ${relativePath}`); } return destination; @@ -28170,7 +28471,7 @@ function parseNullSeparated(value) { return String(value || "").split("\0").filter(Boolean).sort(); } function getGitChangeSet(launch, execFileSyncImpl) { - if (!import_node_fs14.default.existsSync(launch.executionWorkspace)) { + if (!import_node_fs15.default.existsSync(launch.executionWorkspace)) { throw new Error(`Execution workspace no longer exists: ${launch.executionWorkspace}`); } const trackedPatch = git(execFileSyncImpl, launch.executionWorkspace, [ @@ -28206,19 +28507,19 @@ function getGitChangeSet(launch, execFileSyncImpl) { }; } function assertSafeSymlinks(workspace, relativePaths) { - const root = import_node_fs14.default.realpathSync(workspace); + const root = import_node_fs15.default.realpathSync(workspace); for (const relativePath of relativePaths) { const candidate = safeRelative(root, relativePath); let stat; try { - stat = import_node_fs14.default.lstatSync(candidate); + stat = import_node_fs15.default.lstatSync(candidate); } catch { continue; } if (!stat.isSymbolicLink()) continue; let target; try { - target = import_node_fs14.default.realpathSync(candidate); + target = import_node_fs15.default.realpathSync(candidate); } catch { throw new Error(`Launch change contains a broken symlink: ${relativePath}`); } @@ -28232,7 +28533,7 @@ function cleanupGitWorktree(launch, execFileSyncImpl) { if (launch.worktreeBranch !== expectedBranch) { throw new Error(`Refusing to clean unexpected worktree branch: ${launch.worktreeBranch || "none"}`); } - if (import_node_fs14.default.existsSync(launch.executionWorkspace)) { + if (import_node_fs15.default.existsSync(launch.executionWorkspace)) { git(execFileSyncImpl, launch.projectRoot, [ "worktree", "remove", @@ -28252,33 +28553,33 @@ function copyWorkspaceEntry(sourceRoot, destinationRoot, relativePath, entry) { const source = safeRelative(sourceRoot, relativePath); const destination = safeRelative(destinationRoot, relativePath); if (entry.type === "directory") { - import_node_fs14.default.mkdirSync(destination, { recursive: true, mode: entry.mode }); - import_node_fs14.default.chmodSync(destination, entry.mode); + import_node_fs15.default.mkdirSync(destination, { recursive: true, mode: entry.mode }); + import_node_fs15.default.chmodSync(destination, entry.mode); return; } - import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(destination), { recursive: true }); - const temporary = import_node_path15.default.join( - import_node_path15.default.dirname(destination), - `.${import_node_path15.default.basename(destination)}.rudi-promote-${process.pid}` + import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(destination), { recursive: true }); + const temporary = import_node_path16.default.join( + import_node_path16.default.dirname(destination), + `.${import_node_path16.default.basename(destination)}.rudi-promote-${process.pid}` ); - import_node_fs14.default.rmSync(temporary, { recursive: true, force: true }); + import_node_fs15.default.rmSync(temporary, { recursive: true, force: true }); if (entry.type === "file") { - import_node_fs14.default.copyFileSync(source, temporary, import_node_fs14.default.constants.COPYFILE_EXCL); - import_node_fs14.default.chmodSync(temporary, entry.mode); + import_node_fs15.default.copyFileSync(source, temporary, import_node_fs15.default.constants.COPYFILE_EXCL); + import_node_fs15.default.chmodSync(temporary, entry.mode); } else if (entry.type === "symlink") { - import_node_fs14.default.symlinkSync(entry.target, temporary); + import_node_fs15.default.symlinkSync(entry.target, temporary); } else { throw new Error(`Unsupported promoted entry type: ${entry.type}`); } - import_node_fs14.default.rmSync(destination, { recursive: true, force: true }); - import_node_fs14.default.renameSync(temporary, destination); + import_node_fs15.default.rmSync(destination, { recursive: true, force: true }); + import_node_fs15.default.renameSync(temporary, destination); } function restoreDirectoryFromBackup(projectRoot, backup) { - for (const entry of import_node_fs14.default.readdirSync(projectRoot)) { - import_node_fs14.default.rmSync(import_node_path15.default.join(projectRoot, entry), { recursive: true, force: true }); + for (const entry of import_node_fs15.default.readdirSync(projectRoot)) { + import_node_fs15.default.rmSync(import_node_path16.default.join(projectRoot, entry), { recursive: true, force: true }); } - for (const entry of import_node_fs14.default.readdirSync(backup)) { - import_node_fs14.default.cpSync(import_node_path15.default.join(backup, entry), import_node_path15.default.join(projectRoot, entry), { + for (const entry of import_node_fs15.default.readdirSync(backup)) { + import_node_fs15.default.cpSync(import_node_path16.default.join(backup, entry), import_node_path16.default.join(projectRoot, entry), { errorOnExist: true, force: false, recursive: true @@ -28292,13 +28593,13 @@ function applyIsolatedChanges(launch, baseline, current) { } assertSafeSymlinks(launch.executionWorkspace, Object.keys(current.entries)); const changes = compareWorkspaceManifests(baseline, current); - const backup = import_node_path15.default.join(launch.outputDestination, "promotion-backup"); - if (import_node_fs14.default.existsSync(backup)) throw new Error(`Promotion backup already exists: ${backup}`); - import_node_fs14.default.cpSync(launch.projectRoot, backup, { errorOnExist: true, force: false, recursive: true }); + const backup = import_node_path16.default.join(launch.outputDestination, "promotion-backup"); + if (import_node_fs15.default.existsSync(backup)) throw new Error(`Promotion backup already exists: ${backup}`); + import_node_fs15.default.cpSync(launch.projectRoot, backup, { errorOnExist: true, force: false, recursive: true }); try { const removals = changes.filter((change) => change.after == null).sort((left, right) => right.path.split("/").length - left.path.split("/").length); for (const change of removals) { - import_node_fs14.default.rmSync(safeRelative(launch.projectRoot, change.path), { recursive: true, force: true }); + import_node_fs15.default.rmSync(safeRelative(launch.projectRoot, change.path), { recursive: true, force: true }); } const directories = changes.filter((change) => change.after?.type === "directory"); const otherEntries = changes.filter((change) => change.after && change.after.type !== "directory"); @@ -28329,7 +28630,7 @@ function applyIsolatedChanges(launch, baseline, current) { } throw error; } finally { - import_node_fs14.default.rmSync(backup, { recursive: true, force: true }); + import_node_fs15.default.rmSync(backup, { recursive: true, force: true }); } return changes; } @@ -28399,7 +28700,7 @@ function promoteAgentLaunch(launchId, dependencies = {}) { assertSafeSymlinks(launch.executionWorkspace, [...changedTracked, ...changes.untracked]); for (const relativePath of changes.untracked) { const destination = safeRelative(launch.projectRoot, relativePath); - if (import_node_fs14.default.existsSync(destination)) { + if (import_node_fs15.default.existsSync(destination)) { throw new Error(`Cannot promote untracked file because the destination exists: ${relativePath}`); } } @@ -28422,8 +28723,8 @@ function promoteAgentLaunch(launchId, dependencies = {}) { for (const relativePath of changes.untracked) { const source = safeRelative(launch.executionWorkspace, relativePath); const destination = safeRelative(launch.projectRoot, relativePath); - import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(destination), { recursive: true }); - import_node_fs14.default.cpSync(source, destination, { errorOnExist: true, force: false, recursive: true }); + import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(destination), { recursive: true }); + import_node_fs15.default.cpSync(source, destination, { errorOnExist: true, force: false, recursive: true }); } const updated = store.setDisposition(launchId, "promoted"); cleanupGitWorktree(updated, execFileSyncImpl); @@ -28434,7 +28735,7 @@ function promoteAgentLaunch(launchId, dependencies = {}) { const current = createWorkspaceManifest(launch.executionWorkspace); changes = applyIsolatedChanges(launch, baseline, current); const updated = store.setDisposition(launchId, "promoted"); - import_node_fs14.default.rmSync(updated.executionWorkspace, { recursive: true, force: true }); + import_node_fs15.default.rmSync(updated.executionWorkspace, { recursive: true, force: true }); return { changes, launch: store.get(launchId) }; } throw new Error("Read-only launches have no isolated changes to promote"); @@ -28449,7 +28750,7 @@ function discardAgentLaunch(launchId, dependencies = {}) { const launch = requireManagedLaunch(store, launchId, { terminal: true }); const execFileSyncImpl = dependencies.execFileSyncImpl || import_node_child_process8.execFileSync; if (launch.workspaceMode === "worktree") cleanupGitWorktree(launch, execFileSyncImpl); - import_node_fs14.default.rmSync(launch.outputDestination, { recursive: true, force: true }); + import_node_fs15.default.rmSync(launch.outputDestination, { recursive: true, force: true }); const updated = store.setDisposition(launchId, "discarded"); return { launch: updated }; }); @@ -28554,29 +28855,29 @@ async function stopAgentGroup(groupId, dependencies = {}) { } // src/daemon/runtime/lifecycle.js -var import_node_fs16 = __toESM(require("node:fs"), 1); -var import_node_path17 = __toESM(require("node:path"), 1); +var import_node_fs17 = __toESM(require("node:fs"), 1); +var import_node_path18 = __toESM(require("node:path"), 1); var import_node_child_process9 = require("node:child_process"); init_src(); // src/daemon/client.js -var import_node_fs15 = __toESM(require("node:fs"), 1); -var import_node_path16 = __toESM(require("node:path"), 1); +var import_node_fs16 = __toESM(require("node:fs"), 1); +var import_node_path17 = __toESM(require("node:path"), 1); init_src(); -var DAEMON_PORT_FILE = import_node_path16.default.join(PATHS.home, "daemon.port"); -var DAEMON_TOKEN_FILE = import_node_path16.default.join(PATHS.home, "daemon.token"); +var DAEMON_PORT_FILE = import_node_path17.default.join(PATHS.home, "daemon.port"); +var DAEMON_TOKEN_FILE = import_node_path17.default.join(PATHS.home, "daemon.token"); function readDaemonInfo(options = {}) { const portFile = options.portFile || DAEMON_PORT_FILE; const tokenFile = options.tokenFile || DAEMON_TOKEN_FILE; - if (!import_node_fs15.default.existsSync(portFile) || !import_node_fs15.default.existsSync(tokenFile)) { + if (!import_node_fs16.default.existsSync(portFile) || !import_node_fs16.default.existsSync(tokenFile)) { const error = new Error("RUDI daemon is not running. Start it with: rudi daemon start"); error.code = "DAEMON_NOT_RUNNING"; error.portFile = portFile; error.tokenFile = tokenFile; throw error; } - const portRaw = import_node_fs15.default.readFileSync(portFile, "utf-8").trim(); - const token = import_node_fs15.default.readFileSync(tokenFile, "utf-8").trim(); + const portRaw = import_node_fs16.default.readFileSync(portFile, "utf-8").trim(); + const token = import_node_fs16.default.readFileSync(tokenFile, "utf-8").trim(); const port = Number.parseInt(portRaw, 10); if (!Number.isFinite(port) || port <= 0) { const error = new Error("Invalid daemon port file. Restart it with: rudi daemon restart"); @@ -29155,11 +29456,11 @@ function removeDaemonConnectionFiles({ tokenFile = DAEMON_TOKEN_FILE } = {}) { try { - import_node_fs16.default.unlinkSync(portFile); + import_node_fs17.default.unlinkSync(portFile); } catch { } try { - import_node_fs16.default.unlinkSync(tokenFile); + import_node_fs17.default.unlinkSync(tokenFile); } catch { } } @@ -29183,11 +29484,11 @@ function spawnDaemonProcess({ serveArgs = ["serve"], spawnImpl = import_node_child_process9.spawn } = {}) { - import_node_fs16.default.mkdirSync(logsDir, { recursive: true }); - const stdoutPath = import_node_path17.default.join(logsDir, "daemon.out.log"); - const stderrPath = import_node_path17.default.join(logsDir, "daemon.err.log"); - const stdoutFd = import_node_fs16.default.openSync(stdoutPath, "a"); - const stderrFd = import_node_fs16.default.openSync(stderrPath, "a"); + import_node_fs17.default.mkdirSync(logsDir, { recursive: true }); + const stdoutPath = import_node_path18.default.join(logsDir, "daemon.out.log"); + const stderrPath = import_node_path18.default.join(logsDir, "daemon.err.log"); + const stdoutFd = import_node_fs17.default.openSync(stdoutPath, "a"); + const stderrFd = import_node_fs17.default.openSync(stderrPath, "a"); try { const child = spawnImpl(nodePath, [entrypoint, ...serveArgs], { detached: true, @@ -29198,11 +29499,11 @@ function spawnDaemonProcess({ return { pid: child.pid, stderrPath, stdoutPath }; } finally { try { - import_node_fs16.default.closeSync(stdoutFd); + import_node_fs17.default.closeSync(stdoutFd); } catch { } try { - import_node_fs16.default.closeSync(stderrFd); + import_node_fs17.default.closeSync(stderrFd); } catch { } } @@ -29914,10 +30215,9 @@ Total: ${servers.length} MCP server(s) configured`); } try { let packages = await listInstalled(kind); + const filters = normalizeSkillFilters(flags); const categoryFilter = flags.category; - if (categoryFilter) { - packages = packages.filter((p) => p.category === categoryFilter); - } + packages = packages.filter((pkg) => matchesSkillFilters(pkg, filters)); if (flags.json) { console.log(JSON.stringify(packages, null, 2)); return; @@ -29954,6 +30254,7 @@ SKILLS (${packages.length}):`); console.log(` ${pkg.description}`); } printPackageLifecycle(pkg, " "); + printSkillDetails(pkg, " "); if (pkg.requires && pkg.requires.stacks && pkg.requires.stacks.length > 0) { console.log(` Requires: ${pkg.requires.stacks.join(", ")}`); } @@ -29965,7 +30266,7 @@ SKILLS (${packages.length}):`); console.log(` Total: ${packages.length} skill(s)`); console.log(` -Filter by category: rudi list skills --category=coding`); +Filter by category: rudi list skills --category=code`); return; } const grouped = { @@ -29991,7 +30292,8 @@ ${headingForKind2(pkgKind)} (${pkgs.length}):`); console.log(` ${pkg.description}`); } printPackageLifecycle(pkg, " "); - if (pkg.category) { + printSkillDetails(pkg); + if (pkg.kind !== "skill" && pkg.category) { console.log(` Category: ${pkg.category}`); } if (pkg.tags && pkg.tags.length > 0) { @@ -30283,13 +30585,13 @@ function resolveInstallNativeSkillHosts(flags = {}, installedAgents = []) { return configuredNativeSkillHosts(installedAgents); } function installedSkillDescriptor(resolved, installedPath) { - const packagePath = path31.resolve(installedPath); + const packagePath = path32.resolve(installedPath); const isDirectory = fsSync.existsSync(packagePath) && fsSync.lstatSync(packagePath).isDirectory(); return { ...resolved, source: resolved.source?.type ? resolved.source : "rudi", path: packagePath, - entryPath: isDirectory ? path31.join(packagePath, "SKILL.md") : packagePath + entryPath: isDirectory ? path32.join(packagePath, "SKILL.md") : packagePath }; } async function reconcileInstalledSkillsAfterInstall(skills, flags = {}, dependencies = {}) { @@ -30322,9 +30624,9 @@ function reportInstalledSkillProjection(projection) { } } async function loadManifest(installPath) { - const manifestPath = path31.join(installPath, "manifest.json"); + const manifestPath = path32.join(installPath, "manifest.json"); try { - const content = await fs29.readFile(manifestPath, "utf-8"); + const content = await fs30.readFile(manifestPath, "utf-8"); return JSON.parse(content); } catch { return null; @@ -30332,15 +30634,15 @@ async function loadManifest(installPath) { } function getBundledBinary(runtime, binary) { const platform = process.platform; - const rudiHome = process.env.RUDI_HOME || path31.join(process.env.HOME || process.env.USERPROFILE, ".rudi"); + const rudiHome = process.env.RUDI_HOME || path32.join(process.env.HOME || process.env.USERPROFILE, ".rudi"); if (runtime === "node") { - const npmPath = platform === "win32" ? path31.join(rudiHome, "runtimes", "node", "npm.cmd") : path31.join(rudiHome, "runtimes", "node", "bin", "npm"); + const npmPath = platform === "win32" ? path32.join(rudiHome, "runtimes", "node", "npm.cmd") : path32.join(rudiHome, "runtimes", "node", "bin", "npm"); if (fsSync.existsSync(npmPath)) { return npmPath; } } if (runtime === "python") { - const pipPath = platform === "win32" ? path31.join(rudiHome, "runtimes", "python", "Scripts", "pip.exe") : path31.join(rudiHome, "runtimes", "python", "bin", "pip3"); + const pipPath = platform === "win32" ? path32.join(rudiHome, "runtimes", "python", "Scripts", "pip.exe") : path32.join(rudiHome, "runtimes", "python", "bin", "pip3"); if (fsSync.existsSync(pipPath)) { return pipPath; } @@ -30362,9 +30664,9 @@ function getStackCommand(manifest) { return command; } function getNodeProjectInfo(stackPath) { - const candidates = [stackPath, path31.join(stackPath, "node")]; + const candidates = [stackPath, path32.join(stackPath, "node")]; for (const root of candidates) { - const packageJsonPath = path31.join(root, "package.json"); + const packageJsonPath = path32.join(root, "package.json"); if (!fsSync.existsSync(packageJsonPath)) continue; try { const content = fsSync.readFileSync(packageJsonPath, "utf-8"); @@ -30391,9 +30693,9 @@ async function installDependencies(stackPath, manifest, options = {}) { if (project.error) { return { installed: false, error: `Failed to read package.json: ${project.error}` }; } - const nodeModulesPath = path31.join(project.root, "node_modules"); + const nodeModulesPath = path32.join(project.root, "node_modules"); try { - await fs29.access(nodeModulesPath); + await fs30.access(nodeModulesPath); return { installed: false, reason: "Dependencies already installed" }; } catch { } @@ -30406,15 +30708,15 @@ async function installDependencies(stackPath, manifest, options = {}) { }); return { installed: true }; } else if (runtime === "python") { - let requirementsPath = path31.join(stackPath, "python", "requirements.txt"); - let reqCwd = path31.join(stackPath, "python"); + let requirementsPath = path32.join(stackPath, "python", "requirements.txt"); + let reqCwd = path32.join(stackPath, "python"); try { - await fs29.access(requirementsPath); + await fs30.access(requirementsPath); } catch { - requirementsPath = path31.join(stackPath, "requirements.txt"); + requirementsPath = path32.join(stackPath, "requirements.txt"); reqCwd = stackPath; try { - await fs29.access(requirementsPath); + await fs30.access(requirementsPath); } catch { return { installed: false, reason: "No requirements.txt" }; } @@ -30548,7 +30850,7 @@ async function syncRelatedSkillWrappers(relatedSkills, installResults, installed ); const skills = (relatedSkills || []).filter((skill) => successful.has(skill.id)).map((skill) => { const installed = successful.get(skill.id); - const entryPath = fsSync.existsSync(installed.path) && fsSync.statSync(installed.path).isDirectory() ? path31.join(installed.path, "SKILL.md") : installed.path; + const entryPath = fsSync.existsSync(installed.path) && fsSync.statSync(installed.path).isDirectory() ? path32.join(installed.path, "SKILL.md") : installed.path; return { ...skill, source: "rudi", @@ -30784,12 +31086,12 @@ function getStackEntryPoint(stackPath, manifest) { return { entryArg: null, entryPath: null }; } function resolveContainedStackPath(stackPath, value) { - if (typeof value !== "string" || !value || value.includes("\0") || path31.isAbsolute(value)) { + if (typeof value !== "string" || !value || value.includes("\0") || path32.isAbsolute(value)) { return { error: `Stack command path must be relative to the installed package: ${value}` }; } - const root = path31.resolve(stackPath); - const candidate = path31.resolve(root, value); - if (candidate === root || !candidate.startsWith(`${root}${path31.sep}`)) { + const root = path32.resolve(stackPath); + const candidate = path32.resolve(root, value); + if (candidate === root || !candidate.startsWith(`${root}${path32.sep}`)) { return { error: `Stack command path escapes the installed package: ${value}` }; } return { path: candidate }; @@ -30803,8 +31105,8 @@ function validateExternalStackCommand(stackPath, manifest) { error: "External stack command must reference an entry file inside the pinned package" }; } - const relativeEntry = path31.relative(path31.resolve(stackPath), entryPoint.entryPath); - const rootSegment = relativeEntry.split(path31.sep)[0]; + const relativeEntry = path32.relative(path32.resolve(stackPath), entryPoint.entryPath); + const rootSegment = relativeEntry.split(path32.sep)[0]; if (rootSegment === "runs" || rootSegment === "outputs") { return { valid: false, @@ -30916,9 +31218,9 @@ async function checkSecrets(manifest) { return { found, missing }; } async function parseEnvExample(installPath) { - const examplePath = path31.join(installPath, ".env.example"); + const examplePath = path32.join(installPath, ".env.example"); try { - const content = await fs29.readFile(examplePath, "utf-8"); + const content = await fs30.readFile(examplePath, "utf-8"); const keys = []; for (const line of content.split("\n")) { const trimmed = line.trim(); @@ -30936,7 +31238,7 @@ async function parseEnvExample(installPath) { async function cleanupFailedStackInstall(stackId, stackPath, removeConfig) { if (stackPath) { try { - await fs29.rm(stackPath, { recursive: true, force: true }); + await fs30.rm(stackPath, { recursive: true, force: true }); } catch { } } @@ -31559,11 +31861,11 @@ async function runStack(id, options = {}) { const startTime = Date.now(); const packagePath = getPackagePath(id); const manifestPath = import_path11.default.join(packagePath, "manifest.json"); - const { default: fs52 } = await import("fs"); - if (!fs52.existsSync(manifestPath)) { + const { default: fs53 } = await import("fs"); + if (!fs53.existsSync(manifestPath)) { throw new Error(`Stack manifest not found: ${id}`); } - const manifest = JSON.parse(fs52.readFileSync(manifestPath, "utf-8")); + const manifest = JSON.parse(fs53.readFileSync(manifestPath, "utf-8")); const { command, args } = resolveCommandFromManifest(manifest, packagePath); const secrets = await getSecrets(manifest.requires?.secrets || []); const runEnv = buildStackRunEnv({ @@ -31669,7 +31971,7 @@ function resolveRelativePath(value, basePath) { } // packages/manifest/src/stack.js -var import_yaml2 = __toESM(require_dist(), 1); +var import_yaml4 = __toESM(require_dist(), 1); var import_fs12 = __toESM(require("fs"), 1); var import_path12 = __toESM(require("path"), 1); function parseStackManifest(filePath) { @@ -31677,7 +31979,7 @@ function parseStackManifest(filePath) { return parseStackYaml(content, filePath); } function parseStackYaml(content, source = "stack.yaml") { - const raw = (0, import_yaml2.parse)(content); + const raw = (0, import_yaml4.parse)(content); if (!raw || typeof raw !== "object") { throw new Error(`Invalid stack manifest in ${source}: expected object`); } @@ -31800,13 +32102,13 @@ function findStackManifest(dir) { } // packages/manifest/src/skill.js -var import_yaml3 = __toESM(require_dist(), 1); +var import_yaml5 = __toESM(require_dist(), 1); // packages/manifest/src/prompt.js -var import_yaml4 = __toESM(require_dist(), 1); +var import_yaml6 = __toESM(require_dist(), 1); // packages/manifest/src/runtime.js -var import_yaml5 = __toESM(require_dist(), 1); +var import_yaml7 = __toESM(require_dist(), 1); // packages/manifest/src/validate.js var import_ajv2 = __toESM(require_ajv(), 1); @@ -33755,9 +34057,9 @@ function createShim(shimPath, targetPath) { // src/commands/update.js var import_node_crypto6 = require("node:crypto"); -var import_node_fs17 = require("node:fs"); -var fs37 = __toESM(require("node:fs/promises"), 1); -var path38 = __toESM(require("path"), 1); +var import_node_fs18 = require("node:fs"); +var fs38 = __toESM(require("node:fs/promises"), 1); +var path39 = __toESM(require("path"), 1); init_src5(); init_src(); init_src3(); @@ -33774,24 +34076,24 @@ async function resolveManagedPath(candidate, rootInput, options) { if (typeof candidate !== "string" || candidate.trim() !== candidate || !candidate) { throw new Error(`${candidateLabel} is required for transactional update`); } - const root = path38.resolve(rootInput); - const targetPath = path38.resolve(candidate); - if (targetPath === root || !targetPath.startsWith(`${root}${path38.sep}`)) { + const root = path39.resolve(rootInput); + const targetPath = path39.resolve(candidate); + if (targetPath === root || !targetPath.startsWith(`${root}${path39.sep}`)) { throw new Error(`Refusing to snapshot ${candidateLabel.toLowerCase()} outside the managed ${rootLabel}: ${candidate}`); } if (createRoot) { - await fs37.mkdir(root, { recursive: true }); + await fs38.mkdir(root, { recursive: true }); } - const rootStat = await fs37.lstat(root); + const rootStat = await fs38.lstat(root); if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) { throw new Error(`Managed ${rootLabel} must be a real directory: ${root}`); } - const relative4 = path38.relative(root, targetPath); + const relative4 = path39.relative(root, targetPath); let current = root; - for (const segment of relative4.split(path38.sep)) { - current = path38.join(current, segment); + for (const segment of relative4.split(path39.sep)) { + current = path39.join(current, segment); try { - const stat = await fs37.lstat(current); + const stat = await fs38.lstat(current); if (stat.isSymbolicLink()) { throw new Error(`Refusing symlinked path within managed ${rootLabel}: ${current}`); } @@ -33816,7 +34118,7 @@ function resolveManagedLockfilePath(lockfilePath, locksRoot = PATHS.locks, optio ...options }); } -function resolveManagedStackStatePath(stateRoot, stateStacksRoot = path38.join(PATHS.home, "state", "stacks"), options = {}) { +function resolveManagedStackStatePath(stateRoot, stateStacksRoot = path39.join(PATHS.home, "state", "stacks"), options = {}) { return resolveManagedPath(stateRoot, stateStacksRoot, { candidateLabel: "Stack state path", rootLabel: "stack state root", @@ -33828,28 +34130,28 @@ async function buildTreeManifest(rootPath, prefix = "") { async function visit(currentPath, relativePath) { let stat; try { - stat = await fs37.lstat(currentPath); + stat = await fs38.lstat(currentPath); } catch (error) { if (error.code === "ENOENT" && relativePath === prefix) return; throw error; } const manifestPath = relativePath || "."; if (stat.isSymbolicLink()) { - entries.push([manifestPath, "symlink", await fs37.readlink(currentPath)]); + entries.push([manifestPath, "symlink", await fs38.readlink(currentPath)]); return; } if (stat.isDirectory()) { entries.push([manifestPath, "directory", ""]); - const names = await fs37.readdir(currentPath); + const names = await fs38.readdir(currentPath); names.sort(); for (const name of names) { - const childRelative = relativePath ? path38.join(relativePath, name) : name; - await visit(path38.join(currentPath, name), childRelative); + const childRelative = relativePath ? path39.join(relativePath, name) : name; + await visit(path39.join(currentPath, name), childRelative); } return; } if (stat.isFile()) { - const digest = (0, import_node_crypto6.createHash)("sha256").update(await fs37.readFile(currentPath)).digest("hex"); + const digest = (0, import_node_crypto6.createHash)("sha256").update(await fs38.readFile(currentPath)).digest("hex"); entries.push([manifestPath, "file", digest]); return; } @@ -33881,7 +34183,7 @@ function treeManifestsEqual(left, right) { async function assertSnapshotComponent(componentPath, type, label) { let stat; try { - stat = await fs37.lstat(componentPath); + stat = await fs38.lstat(componentPath); } catch (error) { if (error.code === "ENOENT") throw new Error(`Missing ${label}: ${componentPath}`); throw error; @@ -33892,16 +34194,16 @@ async function assertSnapshotComponent(componentPath, type, label) { } } async function copyPathWithoutOverwrite(sourcePath, destinationPath, label) { - const sourceStat = await fs37.lstat(sourcePath); + const sourceStat = await fs38.lstat(sourcePath); if (sourceStat.isSymbolicLink()) { throw new Error(`Refusing to copy symlinked ${label}: ${sourcePath}`); } - await fs37.mkdir(path38.dirname(destinationPath), { recursive: true }); + await fs38.mkdir(path39.dirname(destinationPath), { recursive: true }); const expectedManifest = await buildTreeManifest(sourcePath); if (sourceStat.isFile()) { - await fs37.copyFile(sourcePath, destinationPath, import_node_fs17.constants.COPYFILE_EXCL); + await fs38.copyFile(sourcePath, destinationPath, import_node_fs18.constants.COPYFILE_EXCL); } else if (sourceStat.isDirectory()) { - await fs37.cp(sourcePath, destinationPath, { + await fs38.cp(sourcePath, destinationPath, { errorOnExist: true, force: false, preserveTimestamps: true, @@ -33931,14 +34233,14 @@ async function validateStackUpdateSnapshot(snapshot, options = {}) { ); const { targetPath: stateRoot } = await resolveManagedStackStatePath( snapshot.stateRoot, - options.stateStacksRoot || snapshot.stateStacksRoot || path38.join(PATHS.home, "state", "stacks") + options.stateStacksRoot || snapshot.stateStacksRoot || path39.join(PATHS.home, "state", "stacks") ); - const backupRoot = path38.resolve(String(snapshot.backupRoot || "")); - const snapshotPath = path38.resolve(String(snapshot.snapshotPath || "")); - const lockfileSnapshotPath = path38.resolve(String(snapshot.lockfileSnapshotPath || "")); - const stateSnapshotPath = path38.resolve(String(snapshot.stateSnapshotPath || "")); - const expectedPrefix = `.${path38.basename(targetPath)}.update-backup-`; - if (path38.dirname(backupRoot) !== root || !path38.basename(backupRoot).startsWith(expectedPrefix) || snapshotPath !== path38.join(backupRoot, "snapshot") || lockfileSnapshotPath !== path38.join(backupRoot, "lockfile") || stateSnapshotPath !== path38.join(backupRoot, "state")) { + const backupRoot = path39.resolve(String(snapshot.backupRoot || "")); + const snapshotPath = path39.resolve(String(snapshot.snapshotPath || "")); + const lockfileSnapshotPath = path39.resolve(String(snapshot.lockfileSnapshotPath || "")); + const stateSnapshotPath = path39.resolve(String(snapshot.stateSnapshotPath || "")); + const expectedPrefix = `.${path39.basename(targetPath)}.update-backup-`; + if (path39.dirname(backupRoot) !== root || !path39.basename(backupRoot).startsWith(expectedPrefix) || snapshotPath !== path39.join(backupRoot, "snapshot") || lockfileSnapshotPath !== path39.join(backupRoot, "lockfile") || stateSnapshotPath !== path39.join(backupRoot, "state")) { throw new Error("Invalid stack update snapshot paths"); } await assertSnapshotComponent(backupRoot, "directory", "stack update backup root"); @@ -33964,62 +34266,62 @@ async function validateStackUpdateSnapshot(snapshot, options = {}) { } async function createStackUpdateSnapshot(stackPath, options = {}) { const { root, targetPath } = await resolveManagedStackPath(stackPath, options.stacksRoot); - const locksRoot = path38.resolve(options.locksRoot || PATHS.locks); + const locksRoot = path39.resolve(options.locksRoot || PATHS.locks); const { targetPath: lockfilePath } = await resolveManagedLockfilePath( options.lockfilePath, locksRoot, { createRoot: true } ); - const stateStacksRoot = path38.resolve( - options.stateStacksRoot || (options.stacksRoot ? path38.join(path38.dirname(root), "state", "stacks") : path38.join(PATHS.home, "state", "stacks")) + const stateStacksRoot = path39.resolve( + options.stateStacksRoot || (options.stacksRoot ? path39.join(path39.dirname(root), "state", "stacks") : path39.join(PATHS.home, "state", "stacks")) ); const { targetPath: stateRoot } = await resolveManagedStackStatePath( - options.stateRoot || path38.join(stateStacksRoot, path38.basename(targetPath)), + options.stateRoot || path39.join(stateStacksRoot, path39.basename(targetPath)), stateStacksRoot, { createRoot: true } ); - const stackStat = await fs37.lstat(targetPath); + const stackStat = await fs38.lstat(targetPath); if (!stackStat.isDirectory() || stackStat.isSymbolicLink()) { throw new Error(`Installed stack path must be a real directory: ${stackPath}`); } - const backupRoot = await fs37.mkdtemp( - path38.join(root, `.${path38.basename(targetPath)}.update-backup-`) + const backupRoot = await fs38.mkdtemp( + path39.join(root, `.${path39.basename(targetPath)}.update-backup-`) ); - const snapshotPath = path38.join(backupRoot, "snapshot"); - const lockfileSnapshotPath = path38.join(backupRoot, "lockfile"); - const stateSnapshotPath = path38.join(backupRoot, "state"); + const snapshotPath = path39.join(backupRoot, "snapshot"); + const lockfileSnapshotPath = path39.join(backupRoot, "lockfile"); + const stateSnapshotPath = path39.join(backupRoot, "state"); let lockfileExisted = false; let stateRootExisted = false; const stateInitialManifest = await buildTreeManifest(stateRoot); - const migratedRunsManifest = await buildTreeManifest(path38.join(targetPath, "runs"), "runs"); + const migratedRunsManifest = await buildTreeManifest(path39.join(targetPath, "runs"), "runs"); const stateExpectedManifest = mergeExpectedStateManifest( stateInitialManifest, migratedRunsManifest ); try { - await fs37.chmod(backupRoot, 448); - await fs37.cp(targetPath, snapshotPath, { + await fs38.chmod(backupRoot, 448); + await fs38.cp(targetPath, snapshotPath, { errorOnExist: true, force: false, preserveTimestamps: true, recursive: true }); try { - const lockfileStat = await fs37.lstat(lockfilePath); + const lockfileStat = await fs38.lstat(lockfilePath); if (!lockfileStat.isFile() || lockfileStat.isSymbolicLink()) { throw new Error(`Stack lockfile path must be a real file: ${lockfilePath}`); } - await fs37.copyFile(lockfilePath, lockfileSnapshotPath); + await fs38.copyFile(lockfilePath, lockfileSnapshotPath); lockfileExisted = true; } catch (error) { if (error.code !== "ENOENT") throw error; } try { - const stateStat = await fs37.lstat(stateRoot); + const stateStat = await fs38.lstat(stateRoot); if (!stateStat.isDirectory() || stateStat.isSymbolicLink()) { throw new Error(`Stack state path must be a real directory: ${stateRoot}`); } - await fs37.cp(stateRoot, stateSnapshotPath, { + await fs38.cp(stateRoot, stateSnapshotPath, { errorOnExist: true, force: false, preserveTimestamps: true, @@ -34030,7 +34332,7 @@ async function createStackUpdateSnapshot(stackPath, options = {}) { if (error.code !== "ENOENT") throw error; } } catch (error) { - await fs37.rm(backupRoot, { force: true, recursive: true }); + await fs38.rm(backupRoot, { force: true, recursive: true }); throw error; } return { @@ -34092,18 +34394,18 @@ async function restoreStackUpdateSnapshot(snapshot, options = {}) { ]; const stateComponent = components[0]; for (const component of components) { - component.stagedPath = path38.join(backupRoot, `failed-${component.label}`); + component.stagedPath = path39.join(backupRoot, `failed-${component.label}`); component.staged = false; component.promoted = false; } try { for (const component of components) { try { - const currentStat = await fs37.lstat(component.currentPath); + const currentStat = await fs38.lstat(component.currentPath); if (currentStat.isSymbolicLink()) { throw new Error(`Refusing to stage symlinked ${component.label}: ${component.currentPath}`); } - await fs37.rename(component.currentPath, component.stagedPath); + await fs38.rename(component.currentPath, component.stagedPath); component.staged = true; } catch (error) { if (error.code !== "ENOENT") throw error; @@ -34117,8 +34419,8 @@ async function restoreStackUpdateSnapshot(snapshot, options = {}) { } for (const component of components) { if (!component.existedBefore) continue; - await fs37.mkdir(path38.dirname(component.currentPath), { recursive: true }); - await fs37.rename(component.snapshotPath, component.currentPath); + await fs38.mkdir(path39.dirname(component.currentPath), { recursive: true }); + await fs38.rename(component.snapshotPath, component.currentPath); component.promoted = true; } } catch (error) { @@ -34159,11 +34461,11 @@ async function restoreStackUpdateSnapshot(snapshot, options = {}) { } throw error; } - await fs37.rm(backupRoot, { force: true, recursive: true }); + await fs38.rm(backupRoot, { force: true, recursive: true }); } async function discardStackUpdateSnapshot(snapshot, options = {}) { const { backupRoot } = await validateStackUpdateSnapshot(snapshot, options); - await fs37.rm(backupRoot, { force: true, recursive: true }); + await fs38.rm(backupRoot, { force: true, recursive: true }); } var defaultDependencies = { fetchIndex, @@ -34306,8 +34608,9 @@ async function updateOnePackage(pkg, flags, deps) { if (!result?.success) { throw new Error(result?.error || `Failed to update ${pkg.id}`); } + if (result.backupPath) deps.log(` Previous skill retained at ${result.backupPath}`); if (kind === "stack") { - if (path38.resolve(result.path) !== path38.resolve(snapshot.targetPath)) { + if (path39.resolve(result.path) !== path39.resolve(snapshot.targetPath)) { throw new Error(`Updated stack path changed unexpectedly for ${pkg.id}`); } const manifest = await deps.loadStackManifest(result.path); @@ -34445,6 +34748,21 @@ async function runUpdate(args = [], flags = {}, deps = defaultDependencies) { flags.force === true && pkgId && target?.kind === "skill" && plannedSkillIds.length === 1 ); if (dryRun) { + const skillMigrations = []; + for (const id of plannedSkillIds) { + try { + const source = refreshedRegistryIndex?.packages?.[id]; + if (!source) throw new Error(`Skill is absent from the refreshed registry: ${id}`); + const candidate = normalizeRegistryPackage(source, "skill"); + const inspect = deps.inspectRegistrySkillUpdate || inspectRegistrySkillUpdate; + const migration = await inspect(candidate, getInstallPathForPackage(candidate)); + skillMigrations.push(migration); + deps.log(` - ${id}: would ${migration.action} ${migration.from || "(new)"} \u2192 ${migration.to}`); + } catch (error) { + failedPackages.push({ id, error: error.message }); + deps.error(` ! ${id}: ${error.message}`); + } + } deps.log(`Dry run: would update ${plannedPackages.length} package(s)`); for (const id of plannedPackages) { deps.log(` - ${id}`); @@ -34462,12 +34780,13 @@ async function runUpdate(args = [], flags = {}, deps = defaultDependencies) { return { dryRun: true, updated: 0, - failed: skillProjection2.failed, - packageFailed: 0, + failed: failedPackages.length + skillProjection2.failed, + packageFailed: failedPackages.length, projectionFailed: skillProjection2.failed, skipped: skippedPackages.length, packages: [], - failures: [], + failures: failedPackages, + skillMigrations, projectionFailures: skillProjection2.failures, skippedPackages, indexedStacks: [], @@ -34589,8 +34908,8 @@ async function cmdUpdate(args, flags, dependencies = {}) { } // src/commands/which.js -var fs38 = __toESM(require("fs/promises"), 1); -var path39 = __toESM(require("path"), 1); +var fs39 = __toESM(require("fs/promises"), 1); +var path40 = __toESM(require("path"), 1); init_src5(); init_src(); init_src4(); @@ -34672,7 +34991,7 @@ Installed stacks:`); if (runtimeInfo.entry) { console.log(""); console.log("Run MCP server directly:"); - const entryPath = path39.join(stackPath, runtimeInfo.entry); + const entryPath = path40.join(stackPath, runtimeInfo.entry); if (runtimeInfo.runtime === "node") { console.log(` echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node ${entryPath}`); } else if (runtimeInfo.runtime === "python") { @@ -34690,32 +35009,32 @@ Installed stacks:`); } async function detectRuntime(stackPath) { const layouts = [ - { runtime: "node", runtimePath: path39.join(stackPath, "node"), entryPrefix: "node/", explicit: true }, - { runtime: "python", runtimePath: path39.join(stackPath, "python"), entryPrefix: "python/", explicit: true }, + { runtime: "node", runtimePath: path40.join(stackPath, "node"), entryPrefix: "node/", explicit: true }, + { runtime: "python", runtimePath: path40.join(stackPath, "python"), entryPrefix: "python/", explicit: true }, { runtime: "node", runtimePath: stackPath, entryPrefix: "", explicit: false }, { runtime: "python", runtimePath: stackPath, entryPrefix: "", explicit: false } ]; for (const { runtime, runtimePath, entryPrefix, explicit } of layouts) { try { - await fs38.access(runtimePath); + await fs39.access(runtimePath); if (runtime === "node") { - const distEntry = path39.join(runtimePath, "dist", "index.js"); - const srcEntry = path39.join(runtimePath, "src", "index.ts"); + const distEntry = path40.join(runtimePath, "dist", "index.js"); + const srcEntry = path40.join(runtimePath, "src", "index.ts"); try { - await fs38.access(distEntry); + await fs39.access(distEntry); return { runtime: "node", entry: `${entryPrefix}dist/index.js` }; } catch { try { - await fs38.access(srcEntry); + await fs39.access(srcEntry); return { runtime: "node", entry: `${entryPrefix}src/index.ts` }; } catch { if (explicit) return { runtime: "node", entry: null }; } } } else if (runtime === "python") { - const entry = path39.join(runtimePath, "src", "index.py"); + const entry = path40.join(runtimePath, "src", "index.py"); try { - await fs38.access(entry); + await fs39.access(entry); return { runtime: "python", entry: `${entryPrefix}src/index.py` }; } catch { if (explicit) return { runtime: "python", entry: null }; @@ -34735,18 +35054,18 @@ async function checkAuth(stackPath, runtime, options = {}) { if (!rootPath || checkedRoots.has(rootPath)) return; checkedRoots.add(rootPath); try { - await fs38.access(path39.join(rootPath, "token.json")); + await fs39.access(path40.join(rootPath, "token.json")); authFiles.push(labelPrefix ? `${labelPrefix}/token.json` : "token.json"); configured = true; } catch { - const accountsPath = path39.join(rootPath, "accounts"); + const accountsPath = path40.join(rootPath, "accounts"); try { - const accounts = await fs38.readdir(accountsPath); + const accounts = await fs39.readdir(accountsPath); for (const account of accounts) { if (account.startsWith(".")) continue; - const accountTokenPath = path39.join(accountsPath, account, "token.json"); + const accountTokenPath = path40.join(accountsPath, account, "token.json"); try { - await fs38.access(accountTokenPath); + await fs39.access(accountTokenPath); const label = labelPrefix ? `${labelPrefix}/accounts/${account}/token.json` : `accounts/${account}/token.json`; authFiles.push(label); configured = true; @@ -34758,19 +35077,19 @@ async function checkAuth(stackPath, runtime, options = {}) { } } if (runtime === "node" || runtime === "python") { - await scanAuthRoot(path39.join(stackPath, runtime), runtime); + await scanAuthRoot(path40.join(stackPath, runtime), runtime); await scanAuthRoot(stackPath, ""); } - const stackName = options.stackName || path39.basename(stackPath); + const stackName = options.stackName || path40.basename(stackPath); const rudiHome = options.rudiHome || PATHS.home; await scanAuthRoot( - path39.join(rudiHome, "state", "stacks", stackName), + path40.join(rudiHome, "state", "stacks", stackName), `state/stacks/${stackName}` ); const envCredentialNames = /* @__PURE__ */ new Set(); - const envPath = path39.join(stackPath, ".env"); + const envPath = path40.join(stackPath, ".env"); try { - const envContent = await fs38.readFile(envPath, "utf-8"); + const envContent = await fs39.readFile(envPath, "utf-8"); for (const line of envContent.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; @@ -34863,8 +35182,8 @@ function checkIfRunning(stackName, options = {}) { } // src/commands/auth.js -var fs39 = __toESM(require("fs/promises"), 1); -var path40 = __toESM(require("path"), 1); +var fs40 = __toESM(require("fs/promises"), 1); +var path41 = __toESM(require("path"), 1); var import_child_process8 = require("child_process"); init_src5(); init_src4(); @@ -34896,31 +35215,31 @@ function isPortAvailable(port) { } async function detectRuntime2(stackPath) { const layouts = [ - { runtime: "node", runtimePath: path40.join(stackPath, "node") }, + { runtime: "node", runtimePath: path41.join(stackPath, "node") }, { runtime: "node", runtimePath: stackPath }, - { runtime: "python", runtimePath: path40.join(stackPath, "python") }, + { runtime: "python", runtimePath: path41.join(stackPath, "python") }, { runtime: "python", runtimePath: stackPath } ]; for (const { runtime, runtimePath } of layouts) { try { - await fs39.access(runtimePath); + await fs40.access(runtimePath); if (runtime === "node") { - const authTs = path40.join(runtimePath, "src", "auth.ts"); - const authJs = path40.join(runtimePath, "dist", "auth.js"); + const authTs = path41.join(runtimePath, "src", "auth.ts"); + const authJs = path41.join(runtimePath, "dist", "auth.js"); try { - await fs39.access(authTs); + await fs40.access(authTs); return { runtime: "node", authScript: authTs, useTsx: true }; } catch { try { - await fs39.access(authJs); + await fs40.access(authJs); return { runtime: "node", authScript: authJs, useTsx: false }; } catch { } } } else if (runtime === "python") { - const authPy = path40.join(runtimePath, "src", "auth.py"); + const authPy = path41.join(runtimePath, "src", "auth.py"); try { - await fs39.access(authPy); + await fs40.access(authPy); return { runtime: "python", authScript: authPy, useTsx: false }; } catch { } @@ -35035,7 +35354,7 @@ function runAuthSubprocess(plan, options = {}) { function getTempAuthScriptPath(authScript, useTsx) { const safeAuthScript = requireSubprocessArg(authScript, "auth script path"); const tempExt = useTsx ? ".ts" : ".mjs"; - return path40.join(path40.dirname(safeAuthScript), `auth-temp${tempExt}`); + return path41.join(path41.dirname(safeAuthScript), `auth-temp${tempExt}`); } async function cmdAuth(args, flags) { const stackId = args[0]; @@ -35076,14 +35395,14 @@ Installed stacks:`); const port = await findAvailablePort(3456); console.log(`Using port: ${port}`); console.log(""); - const cwd = path40.dirname(authInfo.authScript); + const cwd = path41.dirname(authInfo.authScript); if (authInfo.runtime === "node") { - const distAuth = path40.join(cwd, "..", "dist", "auth.js"); + const distAuth = path41.join(cwd, "..", "dist", "auth.js"); let useBuiltInPort = false; let tempAuthScript = null; try { - await fs39.access(distAuth); - const distContent = await fs39.readFile(distAuth, "utf-8"); + await fs40.access(distAuth); + const distContent = await fs40.readFile(distAuth, "utf-8"); if (distContent.includes("findAvailablePort")) { console.log("Using compiled authentication script..."); useBuiltInPort = true; @@ -35091,10 +35410,10 @@ Installed stacks:`); } catch { } if (!useBuiltInPort) { - const authContent = await fs39.readFile(authInfo.authScript, "utf-8"); + const authContent = await fs40.readFile(authInfo.authScript, "utf-8"); tempAuthScript = getTempAuthScriptPath(authInfo.authScript, authInfo.useTsx); const modifiedContent = authContent.replace(/localhost:3456/g, `localhost:${port}`).replace(/server\.listen\(3456/g, `server.listen(${port}`); - await fs39.writeFile(tempAuthScript, modifiedContent); + await fs40.writeFile(tempAuthScript, modifiedContent); } console.log("Starting OAuth flow..."); console.log(""); @@ -35111,12 +35430,12 @@ Installed stacks:`); env: authEnv }); if (tempAuthScript) { - await fs39.unlink(tempAuthScript); + await fs40.unlink(tempAuthScript); } } catch (error) { if (tempAuthScript) { try { - await fs39.unlink(tempAuthScript); + await fs40.unlink(tempAuthScript); } catch { } } @@ -35152,22 +35471,22 @@ Installed stacks:`); } // src/commands/mcp.js -var fs40 = __toESM(require("fs"), 1); -var path41 = __toESM(require("path"), 1); +var fs41 = __toESM(require("fs"), 1); +var path42 = __toESM(require("path"), 1); var import_child_process9 = require("child_process"); init_src(); init_src4(); function getBundledRuntime(runtime) { const platform = process.platform; if (runtime === "node") { - const nodePath = platform === "win32" ? path41.join(PATHS.runtimes, "node", "node.exe") : path41.join(PATHS.runtimes, "node", "bin", "node"); - if (fs40.existsSync(nodePath)) { + const nodePath = platform === "win32" ? path42.join(PATHS.runtimes, "node", "node.exe") : path42.join(PATHS.runtimes, "node", "bin", "node"); + if (fs41.existsSync(nodePath)) { return nodePath; } } if (runtime === "python") { - const pythonPath = platform === "win32" ? path41.join(PATHS.runtimes, "python", "python.exe") : path41.join(PATHS.runtimes, "python", "bin", "python3"); - if (fs40.existsSync(pythonPath)) { + const pythonPath = platform === "win32" ? path42.join(PATHS.runtimes, "python", "python.exe") : path42.join(PATHS.runtimes, "python", "bin", "python3"); + if (fs41.existsSync(pythonPath)) { return pythonPath; } } @@ -35175,18 +35494,18 @@ function getBundledRuntime(runtime) { } function getBundledNpx() { const platform = process.platform; - const npxPath = platform === "win32" ? path41.join(PATHS.runtimes, "node", "npx.cmd") : path41.join(PATHS.runtimes, "node", "bin", "npx"); - if (fs40.existsSync(npxPath)) { + const npxPath = platform === "win32" ? path42.join(PATHS.runtimes, "node", "npx.cmd") : path42.join(PATHS.runtimes, "node", "bin", "npx"); + if (fs41.existsSync(npxPath)) { return npxPath; } return null; } function loadManifest2(stackPath) { - const manifestPath = path41.join(stackPath, "manifest.json"); - if (!fs40.existsSync(manifestPath)) { + const manifestPath = path42.join(stackPath, "manifest.json"); + if (!fs41.existsSync(manifestPath)) { return null; } - return JSON.parse(fs40.readFileSync(manifestPath, "utf-8")); + return JSON.parse(fs41.readFileSync(manifestPath, "utf-8")); } function getRequiredSecrets(manifest) { const secrets = manifest?.requires?.secrets || manifest?.secrets || []; @@ -35219,8 +35538,8 @@ async function cmdMcp(args, flags) { console.error("Example: rudi mcp slack"); process.exit(1); } - const stackPath = path41.join(PATHS.stacks, stackName); - if (!fs40.existsSync(stackPath)) { + const stackPath = path42.join(PATHS.stacks, stackName); + if (!fs41.existsSync(stackPath)) { console.error(`Stack not found: ${stackName}`); console.error(`Expected at: ${stackPath}`); console.error(""); @@ -35269,22 +35588,22 @@ async function cmdMcp(args, flags) { } return part; } - if (part.startsWith("./") || part.startsWith("../") || !path41.isAbsolute(part)) { - const resolved = path41.join(stackPath, part); - if (fs40.existsSync(resolved)) { + if (part.startsWith("./") || part.startsWith("../") || !path42.isAbsolute(part)) { + const resolved = path42.join(stackPath, part); + if (fs41.existsSync(resolved)) { return resolved; } } return part; }); const [cmd, ...cmdArgs] = resolvedCommand; - const bundledNodeBin = path41.join(PATHS.runtimes, "node", "bin"); - const bundledPythonBin = path41.join(PATHS.runtimes, "python", "bin"); - if (fs40.existsSync(bundledNodeBin) || fs40.existsSync(bundledPythonBin)) { + const bundledNodeBin = path42.join(PATHS.runtimes, "node", "bin"); + const bundledPythonBin = path42.join(PATHS.runtimes, "python", "bin"); + if (fs41.existsSync(bundledNodeBin) || fs41.existsSync(bundledPythonBin)) { const runtimePaths = []; - if (fs40.existsSync(bundledNodeBin)) runtimePaths.push(bundledNodeBin); - if (fs40.existsSync(bundledPythonBin)) runtimePaths.push(bundledPythonBin); - env.PATH = runtimePaths.join(path41.delimiter) + path41.delimiter + (env.PATH || ""); + if (fs41.existsSync(bundledNodeBin)) runtimePaths.push(bundledNodeBin); + if (fs41.existsSync(bundledPythonBin)) runtimePaths.push(bundledPythonBin); + env.PATH = runtimePaths.join(path42.delimiter) + path42.delimiter + (env.PATH || ""); } if (flags.debug) { console.error(`[rudi mcp] Stack: ${stackName}`); @@ -35314,47 +35633,47 @@ async function cmdMcp(args, flags) { } // src/commands/integrate.js -var fs41 = __toESM(require("fs"), 1); -var path42 = __toESM(require("path"), 1); +var fs42 = __toESM(require("fs"), 1); +var path43 = __toESM(require("path"), 1); var import_os7 = __toESM(require("os"), 1); init_src(); var HOME2 = import_os7.default.homedir(); -var ROUTER_SHIM_PATH = path42.join(PATHS.bins, "rudi-router"); -var LEGACY_ROUTER_SHIM_PATH = path42.join(PATHS.home, "shims", "rudi-router"); +var ROUTER_SHIM_PATH = path43.join(PATHS.bins, "rudi-router"); +var LEGACY_ROUTER_SHIM_PATH = path43.join(PATHS.home, "shims", "rudi-router"); function checkRouterShim() { - if (fs41.existsSync(ROUTER_SHIM_PATH)) return ROUTER_SHIM_PATH; - if (fs41.existsSync(LEGACY_ROUTER_SHIM_PATH)) return LEGACY_ROUTER_SHIM_PATH; + if (fs42.existsSync(ROUTER_SHIM_PATH)) return ROUTER_SHIM_PATH; + if (fs42.existsSync(LEGACY_ROUTER_SHIM_PATH)) return LEGACY_ROUTER_SHIM_PATH; throw new Error( `Router shim not found at ${ROUTER_SHIM_PATH} Run: rudi shims rebuild` ); } function backupConfig(configPath) { - if (!fs41.existsSync(configPath)) return null; + if (!fs42.existsSync(configPath)) return null; const backupPath = configPath + ".backup." + Date.now(); - fs41.copyFileSync(configPath, backupPath); + fs42.copyFileSync(configPath, backupPath); return backupPath; } function readJsonConfig(configPath) { - if (!fs41.existsSync(configPath)) { + if (!fs42.existsSync(configPath)) { return {}; } try { - return JSON.parse(fs41.readFileSync(configPath, "utf-8")); + return JSON.parse(fs42.readFileSync(configPath, "utf-8")); } catch { return {}; } } function writeJsonConfig(configPath, config) { - const dir = path42.dirname(configPath); - if (!fs41.existsSync(dir)) { - fs41.mkdirSync(dir, { recursive: true }); + const dir = path43.dirname(configPath); + if (!fs42.existsSync(dir)) { + fs42.mkdirSync(dir, { recursive: true }); } - fs41.writeFileSync(configPath, JSON.stringify(config, null, 2)); + fs42.writeFileSync(configPath, JSON.stringify(config, null, 2)); } function getAgentTargetPath(agentConfig) { const configPath = findAgentConfig(agentConfig); - return configPath || path42.join(HOME2, agentConfig.paths[process.platform]?.[0] || agentConfig.paths.darwin[0]); + return configPath || path43.join(HOME2, agentConfig.paths[process.platform]?.[0] || agentConfig.paths.darwin[0]); } function tomlString(value) { return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; @@ -35393,9 +35712,9 @@ function buildCodexRouterTomlBlock(routerPath) { ].join("\n"); } function patchCodexTomlRouter(content, routerPath, options = {}) { - const rudiMcpShimPath = options.rudiMcpShimPath || path42.join(PATHS.bins, "rudi-mcp"); - const legacyMcpShimPath = options.legacyMcpShimPath || path42.join(PATHS.home, "shims", "rudi-mcp"); - const rudiStacksPath = options.rudiStacksPath || path42.join(PATHS.home, "stacks"); + const rudiMcpShimPath = options.rudiMcpShimPath || path43.join(PATHS.bins, "rudi-mcp"); + const legacyMcpShimPath = options.legacyMcpShimPath || path43.join(PATHS.home, "shims", "rudi-mcp"); + const rudiStacksPath = options.rudiStacksPath || path43.join(PATHS.home, "stacks"); const blocks = splitTomlBlocks(content || ""); const removedEntries = []; const removedServers = /* @__PURE__ */ new Set(); @@ -35456,23 +35775,23 @@ async function integrateCodexAgent(agentConfig, targetPath, flags) { ${agentConfig.name}:`); console.log(` Config: ${targetPath}`); const routerPath = checkRouterShim(); - const existing = fs41.existsSync(targetPath) ? fs41.readFileSync(targetPath, "utf-8") : ""; + const existing = fs42.existsSync(targetPath) ? fs42.readFileSync(targetPath, "utf-8") : ""; const result = patchCodexTomlRouter(existing, routerPath); if (result.removed.length > 0) { console.log(` Removed old entries: ${result.removed.join(", ")}`); } if (result.action !== "none" || result.removed.length > 0) { - const dir = path42.dirname(targetPath); - if (!fs41.existsSync(dir)) { - fs41.mkdirSync(dir, { recursive: true }); + const dir = path43.dirname(targetPath); + if (!fs42.existsSync(dir)) { + fs42.mkdirSync(dir, { recursive: true }); } - if (fs41.existsSync(targetPath)) { + if (fs42.existsSync(targetPath)) { const backup = backupConfig(targetPath); if (backup && flags.verbose) { console.log(` Backup: ${backup}`); } } - fs41.writeFileSync(targetPath, result.content); + fs42.writeFileSync(targetPath, result.content); if (result.action !== "none") { console.log(` ${result.action === "added" ? "\u2713 Added" : "\u2713 Updated"} rudi router`); } @@ -35495,7 +35814,7 @@ ${agentConfig.name}:`); console.log(` Config: ${targetPath}`); if (agentId === "codex") { const routerPath = checkRouterShim(); - const existing = fs41.existsSync(targetPath) ? fs41.readFileSync(targetPath, "utf-8") : ""; + const existing = fs42.existsSync(targetPath) ? fs42.readFileSync(targetPath, "utf-8") : ""; const result = patchCodexTomlRouter(existing, routerPath); if (result.removed.length > 0) { console.log(` Would remove old entries: ${result.removed.join(", ")}`); @@ -35530,9 +35849,9 @@ ${agentConfig.name}:`); if (!config[key]) { config[key] = {}; } - const rudiMcpShimPath = path42.join(PATHS.bins, "rudi-mcp"); - const legacyMcpShimPath = path42.join(PATHS.home, "shims", "rudi-mcp"); - const rudiStacksPath = path42.join(PATHS.home, "stacks"); + const rudiMcpShimPath = path43.join(PATHS.bins, "rudi-mcp"); + const legacyMcpShimPath = path43.join(PATHS.home, "shims", "rudi-mcp"); + const rudiStacksPath = path43.join(PATHS.home, "stacks"); const removedEntries = []; for (const [serverName, serverConfig] of Object.entries(config[key])) { if (serverName === "rudi") continue; @@ -35571,7 +35890,7 @@ ${agentConfig.name}:`); action = "updated"; } if (action !== "none" || removedEntries.length > 0) { - if (fs41.existsSync(targetPath)) { + if (fs42.existsSync(targetPath)) { const backup = backupConfig(targetPath); if (backup && flags.verbose) { console.log(` Backup: ${backup}`); @@ -36950,31 +37269,31 @@ var import_fs21 = __toESM(require("fs"), 1); var import_path19 = __toESM(require("path"), 1); // src/runtime-inspection.js -var import_node_fs18 = __toESM(require("node:fs"), 1); -var import_node_path18 = __toESM(require("node:path"), 1); +var import_node_fs19 = __toESM(require("node:fs"), 1); +var import_node_path19 = __toESM(require("node:path"), 1); init_src(); function isWithinRoot(rootPath, candidatePath) { - const relative4 = import_node_path18.default.relative(rootPath, candidatePath); - return relative4 === "" || !relative4.startsWith(`..${import_node_path18.default.sep}`) && relative4 !== ".." && !import_node_path18.default.isAbsolute(relative4); + const relative4 = import_node_path19.default.relative(rootPath, candidatePath); + return relative4 === "" || !relative4.startsWith(`..${import_node_path19.default.sep}`) && relative4 !== ".." && !import_node_path19.default.isAbsolute(relative4); } function declaredRuntimeBins(manifest) { if (Array.isArray(manifest?.bins)) { - return manifest.bins.map((name) => ({ name, relativePath: import_node_path18.default.join("bin", name) })); + return manifest.bins.map((name) => ({ name, relativePath: import_node_path19.default.join("bin", name) })); } if (manifest?.bins && typeof manifest.bins === "object") { return Object.entries(manifest.bins).map(([name, descriptor]) => ({ name, - relativePath: descriptor?.path || import_node_path18.default.join("bin", name) + relativePath: descriptor?.path || import_node_path19.default.join("bin", name) })); } return []; } function inspectRuntimeInstall(packageId) { const installRoot = getPackagePath(packageId); - const manifestPath = import_node_path18.default.join(installRoot, "manifest.json"); - const rootExists = import_node_fs18.default.existsSync(installRoot); - const resolvedInstallRoot = rootExists ? import_node_fs18.default.realpathSync(installRoot) : installRoot; - const manifestPresent = import_node_fs18.default.existsSync(manifestPath); + const manifestPath = import_node_path19.default.join(installRoot, "manifest.json"); + const rootExists = import_node_fs19.default.existsSync(installRoot); + const resolvedInstallRoot = rootExists ? import_node_fs19.default.realpathSync(installRoot) : installRoot; + const manifestPresent = import_node_fs19.default.existsSync(manifestPath); if (!manifestPresent) { return { binaries: [], @@ -36987,7 +37306,7 @@ function inspectRuntimeInstall(packageId) { }; } try { - const manifest = JSON.parse(import_node_fs18.default.readFileSync(manifestPath, "utf8")); + const manifest = JSON.parse(import_node_fs19.default.readFileSync(manifestPath, "utf8")); if (manifest.id !== packageId) { const actualId = Object.hasOwn(manifest, "id") ? JSON.stringify(manifest.id) : "(missing)"; throw new Error(`Installed runtime manifest ID mismatch: expected ${packageId}, got ${actualId}`); @@ -36996,21 +37315,21 @@ function inspectRuntimeInstall(packageId) { if (typeof name !== "string" || !name || typeof relativePath !== "string" || !relativePath) { throw new Error("Installed runtime manifest contains an invalid binary declaration"); } - const binaryPath = import_node_path18.default.resolve(installRoot, relativePath); + const binaryPath = import_node_path19.default.resolve(installRoot, relativePath); if (!isWithinRoot(installRoot, binaryPath)) { throw new Error(`Installed runtime binary escapes its package root: ${name}`); } - if (!import_node_fs18.default.existsSync(binaryPath)) { + if (!import_node_fs19.default.existsSync(binaryPath)) { throw new Error(`Installed runtime binary is missing: ${name}`); } - const resolvedPath = import_node_fs18.default.realpathSync(binaryPath); + const resolvedPath = import_node_fs19.default.realpathSync(binaryPath); if (!isWithinRoot(resolvedInstallRoot, resolvedPath)) { throw new Error(`Installed runtime binary resolves outside its package root: ${name}`); } - if (!import_node_fs18.default.statSync(resolvedPath).isFile()) { + if (!import_node_fs19.default.statSync(resolvedPath).isFile()) { throw new Error(`Installed runtime binary is not a regular file: ${name}`); } - import_node_fs18.default.accessSync(resolvedPath, import_node_fs18.default.constants.X_OK); + import_node_fs19.default.accessSync(resolvedPath, import_node_fs19.default.constants.X_OK); return { name, path: binaryPath, resolvedPath }; }); if (binaries.length === 0) { @@ -37774,6 +38093,23 @@ var import_fs23 = __toESM(require("fs"), 1); var import_path21 = __toESM(require("path"), 1); init_src(); init_src5(); +async function showSkillInfo(id, flags) { + const skill = (await listInstalled("skill")).find((pkg) => pkg.id === id); + if (!skill) throw new Error(`Package not installed: ${id}`); + if (flags.json) { + console.log(JSON.stringify(skill, null, 2)); + return; + } + console.log(` +Package: ${id}`); + console.log(` Name: ${skill.name}`); + console.log(` Kind: skill`); + console.log(` Version: ${skill.version}`); + console.log(` Entrypoint: ${skill.entryPath}`); + console.log(` Description: ${skill.description}`); + printSkillDetails(skill, " "); + if (skill.requires?.stacks?.length) console.log(` Requires: ${skill.requires.stacks.join(", ")}`); +} function resolvesToSameFile(leftPath, rightPath) { try { return import_fs23.default.realpathSync(leftPath) === import_fs23.default.realpathSync(rightPath); @@ -37791,6 +38127,7 @@ async function cmdInfo(args, flags) { } try { const [kind, name] = parsePackageId(pkgId); + if (kind === "skill") return await showSkillInfo(pkgId, flags); const installPath = getPackagePath(pkgId); if (!import_fs23.default.existsSync(installPath)) { console.error(`Package not installed: ${pkgId}`); @@ -38931,7 +39268,7 @@ function buildLocalLlmRoutes(ctx, deps = {}) { } // src/daemon/routes/agent-host-validation.js -var import_node_path19 = __toESM(require("node:path"), 1); +var import_node_path20 = __toESM(require("node:path"), 1); var MAX_AGENT_HOST_BODY_BYTES = 12 * 1024 * 1024; var LAUNCH_FIELDS = /* @__PURE__ */ new Set([ "approvalMode", @@ -39032,7 +39369,7 @@ function validateRequest(body, allowed, { resume = false } = {}) { } if (!resume) { Object.assign(options, { - originDirectory: import_node_path19.default.resolve(requireText(body.originDirectory, "originDirectory")), + originDirectory: import_node_path20.default.resolve(requireText(body.originDirectory, "originDirectory")), outputDirectory: body.outputDirectory == null ? void 0 : requireText(body.outputDirectory, "outputDirectory"), provider: requireText(body.provider, "provider", 64), workspace: body.workspace == null ? void 0 : requireText(body.workspace, "workspace"), @@ -39102,7 +39439,7 @@ function validateAgentGroupRequest(body) { }); return { groupId: assertAgentGroupId(body.groupId), - originDirectory: import_node_path19.default.resolve(requireText(body.originDirectory, "originDirectory")), + originDirectory: import_node_path20.default.resolve(requireText(body.originDirectory, "originDirectory")), tasks, workspace: requireText(body.workspace, "workspace"), workspaceMode: body.workspaceMode == null ? "auto" : requireText(body.workspaceMode, "workspaceMode", 32) @@ -39330,7 +39667,7 @@ function buildAgentHostRoutes(ctx, dependencies = {}) { // src/daemon/routes/packages.js var import_crypto2 = __toESM(require("crypto"), 1); -var fs49 = __toESM(require("fs/promises"), 1); +var fs50 = __toESM(require("fs/promises"), 1); var fsSync2 = __toESM(require("fs"), 1); var import_path23 = __toESM(require("path"), 1); init_src5(); @@ -39402,7 +39739,7 @@ var defaultDeps = { async function loadManifest3(installPath) { const manifestPath = import_path23.default.join(installPath, "manifest.json"); try { - const content = await fs49.readFile(manifestPath, "utf-8"); + const content = await fs50.readFile(manifestPath, "utf-8"); return JSON.parse(content); } catch { return null; @@ -39547,7 +39884,7 @@ async function checkSecrets3(manifest, deps) { async function parseEnvExample2(installPath) { const examplePath = import_path23.default.join(installPath, ".env.example"); try { - const content = await fs49.readFile(examplePath, "utf-8"); + const content = await fs50.readFile(examplePath, "utf-8"); const keys = []; for (const line of content.split("\n")) { const trimmed = line.trim(); @@ -39563,7 +39900,7 @@ async function parseEnvExample2(installPath) { async function cleanupFailedStackInstall2(stackId, stackPath, removeConfig, deps) { if (stackPath) { try { - await fs49.rm(stackPath, { recursive: true, force: true }); + await fs50.rm(stackPath, { recursive: true, force: true }); } catch { } } diff --git a/docs/public-readiness-checklist.md b/docs/public-readiness-checklist.md index 17cf0ab..f8dc6fb 100644 --- a/docs/public-readiness-checklist.md +++ b/docs/public-readiness-checklist.md @@ -15,7 +15,7 @@ RUDI should expose three first-class package kinds: | Kind | Definition | Local install path | Registry path | | --- | --- | --- | --- | | `stack` | Executable MCP/tool package with code, dependencies, tool schemas, and optional secrets | `~/.rudi/stacks//` | `catalog/stacks//` | -| `skill` | Reusable agent instruction/playbook; markdown only, no runtime state | `~/.rudi/skills/.md` | `catalog/skills/.md` | +| `skill` | Reusable agent instruction/playbook; markdown only, no runtime state | `~/.rudi/skills//SKILL.md` (legacy flat files remain readable) | `catalog/skills//SKILL.md` | | `workflow` | Structured repeatable execution with inputs, steps, dependencies, outputs, validation, and permissions | `~/.rudi/workflows/.yaml` | `catalog/workflows/.yaml` | `prompt` is a deprecated compatibility alias for `skill`. It should not remain a first-class public product term. @@ -157,7 +157,7 @@ requires: is legacy migration state available only for explicit removal. - [x] `rudi search` supports `stack`, `skill`, and `workflow`. - [ ] `rudi install stack:` installs to `~/.rudi/stacks//`. -- [x] `rudi install skill:` installs to `~/.rudi/skills/.md`. +- [x] `rudi install skill:` installs to `~/.rudi/skills//SKILL.md` (legacy flat files remain readable). - [x] `rudi install workflow:` installs to `~/.rudi/workflows/.yaml`. - [x] `rudi list workflows` works. - [x] `rudi remove workflow:` works. diff --git a/docs/swe-compliance/2026-09-02-codex-skill-metadata-sync.md b/docs/swe-compliance/2026-09-02-codex-skill-metadata-sync.md new file mode 100644 index 0000000..cb91b84 --- /dev/null +++ b/docs/swe-compliance/2026-09-02-codex-skill-metadata-sync.md @@ -0,0 +1,70 @@ +## Phase 0: Baseline And Manual Lookup + +- Durable issue: `learnrudi/cli#42`, “Preserve bundled Codex metadata during skill sync.” +- Scope: make Codex native-skill reconciliation preserve a canonical package's `agents/openai.yaml` verbatim, including `policy.allow_implicit_invocation: false`, while retaining generated metadata as the fallback when the canonical file is absent. +- Base: isolated worktree `/Users/hoff/RUDI/worktrees/cli/issue-42-codex-skill-metadata`, branch `fix/42-codex-skill-metadata-sync`, based on `origin/main` at `aad3a1d38745eaf52b1623fe92b79123ae72b6cb`. +- Relevant standards: Master Engineering Doctrine; Testing Doctrine and Agent-Assisted Red-Green-Refactor; debugging doctrine; horizontal engineering review standard; repository `AGENTS.md` native-skill lifecycle and generated-artifact requirements. +- Initial evidence: the Registry's canonical `codex-tasks/agents/openai.yaml` contains the non-implicit policy, while current `src/native-skills/lifecycle.js` always renders `agents/openai.yaml` from `buildCodexSkillFiles`; forced reconciliation therefore replaces the authoritative metadata with the generated subset. +- Baseline focused test setup initially failed because the isolated worktree had no workspace links (`ERR_MODULE_NOT_FOUND: @learnrudi/utils`). `pnpm install --frozen-lockfile` restored only lockfile-declared workspace dependencies; this is setup evidence, not the behavior-level red result. +- Risk tier: medium. The change affects persistent, user-visible native host metadata and invocation policy, but is restricted to one projection file and has an explicit fallback. +- Exit criteria: current owner, failing boundary, scope, invariants, proof commands, and authority are recorded before production edits. + +## Phase 1: Scope Lock + +- In scope: `src/native-skills/lifecycle.js`, one focused regression in `src/__tests__/unit/native-skill-lifecycle.test.js`, tracked `dist/index.cjs`, and this evidence record. +- Non-goals: changing canonical package schema or Registry contents; projecting arbitrary `agents/` content to other hosts; changing parsers, dependencies, receipts, CLI syntax, install behavior, release state, live user projections, admin-Mac state, or unrelated saved work. +- Invariants: an existing canonical Codex metadata file is byte-authoritative; absent metadata uses the existing generated fallback; complete-package validation still rejects symlinks; only Codex receives `agents/openai.yaml`; source packages are never mutated; drift and force semantics remain unchanged. +- Trust boundaries: canonical package paths and file types are untrusted. The complete-tree inspection must validate the package before authoritative metadata is read, and no symlink may be followed. +- Designed failure: unsafe or unreadable package metadata fails the projection as the existing lifecycle does for unsafe package content; no partial target promotion is permitted. +- Horizontal scan: current `main` centralizes native projection in `src/native-skills/lifecycle.js`; `src/commands/skills.js` is an adapter and no competing Codex renderer exists. Disposition: no broader obligation; fix the single owner and preserve the existing portable-host boundary. +- Authorized actions: public issue, isolated issue branch/worktree, checklist, implementation, independent review, coherent commits, push, and pull request. Stop before merge. Release, product install/activation, live projection changes, admin sync, and unrelated cleanup remain unauthorized. +- Commit plan: behavior source/test commit; dedicated generated-dist commit; final evidence/checklist commit. Every commit references issue #42. +- Exit criteria: only the listed behavior and files are admitted, with no dependency or schema changes. + +## Phase 2: Red Test + +- Behavior: after a Codex projection is drifted and exact force is supplied, reconciliation must restore the canonical `agents/openai.yaml` byte-for-byte, including `policy.allow_implicit_invocation: false`. +- Red command: `node --test --test-name-pattern='preserves bundled Codex metadata verbatim' src/__tests__/unit/native-skill-lifecycle.test.js`. +- Red result: failed 0/1 at the strict content assertion. Actual metadata was the generated interface-only YAML; expected metadata also contained the canonical policy with `allow_implicit_invocation: false`. This was the intended behavioral failure, not a setup or syntax failure. +- Exit criteria: met; the failing assertion and unchanged rerun command were recorded before production edits. + +## Phase 3: Implementation + +- Implementation rule: after the existing complete-package inspection validates a bundled skill, use its regular-file `agents/openai.yaml` bytes for the Codex projection when present; otherwise use `buildCodexSkillFiles` exactly as before. +- Boundary rule: do not add `agents` to the portable resource-directory allowlist, because that would project Codex-only metadata to Claude, Gemini, and Antigravity. +- Implemented boundary: `readBundledCodexMetadata` accepts only a real regular file and is called only after complete-tree inspection. The projection uses canonical bytes when available and retains generated bytes otherwise. +- Exit criteria: met; the smallest lifecycle change made the unchanged focused test pass without changing source safety or fallback generation. + +## Phase 4: Green Tests And Refactor + +- Green command: the unchanged Phase 2 command passed 1/1. +- Adjacent regression command: `node --test src/__tests__/unit/native-skill-lifecycle.test.js src/__tests__/unit/skills-sync.test.js` passed 36/36. +- Refactor constraint: no unrelated lifecycle restructuring; retain generated metadata as a directly tested fallback. +- Exit criteria: met; focused and adjacent tests pass, the diff remains narrow, and `git diff --check` passes. + +## Phase 5: Full Verification + +- Full suite: `pnpm test` passed 788/788 with zero failures. +- Build: `pnpm build` passed twice; the second build reproduced `dist/index.cjs` at SHA-256 `513d9f274fe28346f18ed55ab82894f3de36d6e2a173119da9c5485c6e3f4a24`. +- Repository debt gate: `node scripts/agent-debt-runner.mjs --changed-since origin/main --no-log` passed with zero findings. +- SWE debt scan: the configured `pr-review` scan through `stack:swe-engineering` passed with zero error, warning, or informational findings. +- Package gate: `npm pack --dry-run --json` passed for six files at version 1.10.26 (328,708 packed bytes; 1,586,154 unpacked bytes). +- Integration proof: an isolated temporary-root projection of the Registry's actual `codex-tasks` bundle was created, deliberately drifted, and force-reconciled. Result: `metadataMatches=true` and `policyPreserved=true`. No live install or host mutation occurred. +- Smoke harness correction: the first probe used a string source identity that the command correctly filters as externally owned, so it selected zero skills and the harness later hit `ENOENT`. The rerun used the installed Registry object-source shape and passed; no production change was made for the harness error. +- Independent review: fresh-context `rudi-code-review` returned Standards pass, Spec pass, Proof pass, and Overall pass with no P0-P3 findings. The reviewer independently reproduced red 0/1, unchanged green 1/1, adjacent 36/36, full 788/788, zero debt, six-file pack, deterministic bundle hash, actual Registry smoke, and unsafe metadata probes. +- Residual review gate: human review of the metadata-precedence boundary remains required before merge. This issue loop stops before merge. +- Exit criteria: met for implementation proof; all gates pass with no accepted implementation debt or proof gap. + +## Phase 6: Docs, Contracts, And Closure + +- Public ledger: issue `learnrudi/cli#42` and its implementation comment point to this checklist and the current lifecycle ownership boundary. +- Pull request: `learnrudi/cli#43` includes `Fixes #42`, medium risk, invariants, proof, this checklist path, the independent-review verdict, and the stop-before-merge boundary. +- CI/review: required `quality` CI passed in 37 seconds on the implementation/evidence head. This final ledger-only commit must receive the same remote gate; its result is recorded on the PR and issue rather than creating another self-referential checklist commit. +- Commit ledger: behavior source/test commit `07e7aa1`; dedicated generated-dist commit `add3f73`; initial evidence commit `e8d6240`; final ledger commit is the commit containing this paragraph. +- Saved-work preservation: the unrelated Registry worktree remains untouched. The earlier duplicate CLI patch in `/Users/hoff/RUDI/apps/platform/cli` may be removed only after this branch safely contains and publishes the accepted fix. +- Administrative closeout: Repo Steward records the non-mutating retained-worktree receipt only after the final commit is pushed, so the receipt can bind the exact published HEAD. Its identifier and version are posted to the issue ledger; no cleanup or archive action is authorized. +- Publication state: branch and PR are published; merge, release, installation, activation, live host reconciliation, admin-Mac synchronization, and branch/worktree cleanup were not performed. +- Human gate: human review of the metadata-precedence boundary remains required before merge. +- Accepted debt: none. +- Proof gaps: none for the implementation or pre-merge delivery. Human merge review is an intentionally outstanding approval gate, not missing implementation proof. +- Final verdict: PASS for the authorized issue-loop boundary; stop before merge. diff --git a/docs/swe-compliance/2026-09-05-skill-catalog-organization.md b/docs/swe-compliance/2026-09-05-skill-catalog-organization.md new file mode 100644 index 0000000..1b7c448 --- /dev/null +++ b/docs/swe-compliance/2026-09-05-skill-catalog-organization.md @@ -0,0 +1,157 @@ +# Skill catalog organization — execution ledger + +## Phase 0: Baseline and manual lookup — complete + +- Task: implement the approved 2026-09-05 catalog impact map using set-goal-and-execute. +- Map: /Users/hoff/.rudi/outputs/skill-catalog-update-map-2026-09-05/change-impact-map.md. +- Evidence home: /Users/hoff/.rudi/outputs/skill-catalog-execution-2026-09-05. +- Registry base: e0f745ea4d42079dc2e63040be0dccd64f61fa5c (refreshed accepted main). +- CLI base: 15161ff744c3010f4a34bb2d3d0cfa50b084ecaf (open PR 43; no merge performed). +- Catalog: 84 skill candidates: 80 accepted main entries plus four preserved local additions. Original checkouts remain intact. +- Baseline carries brand-assets and its binary/stack dependencies, codex-tasks, presentation-design, design-rulebook, and their existing tests/evidence. See baseline-carry.json; these are inherited work, not newly authored here. +- Dwellow local capability changes already occur in newer main; retain the newer accepted version and preserve the original dirty checkout. Plaid unrelated dirty changes remain outside task. +- Manuals: Master Engineering Doctrine, Agent Co-Pilot Operating Standard, Testing Doctrine, Horizontal Engineering and Codebase Stewardship Standard. +- Risk: HIGH for installed format migration and ownership/rollback; medium for discovery; low for catalog documentation. + +## Phase 1: Scope lock — complete + +- Required: seven skill categories, one same-ID folder per skill, namespaced capability/domain/provider tags, role derived from related.operatorSkill, full bundle payload/metadata, safe upgrade and discovery parity. +- Native metadata preservation reuses PR 43. Missing registry identity produces unknown role. Workflow role is distinct from package kind workflow. +- Preserve IDs, supporting files, user edits, legacy-only discovery, optional companion semantics, host ownership receipts and reload reporting. +- Excluded: typed metadata/schema overhaul, new requires.skills model, daemon/UI catalog expansion, stack implementation redesign, broad cleanup. +- Boundaries: YAML source, registry packages and relationship graph, filesystem identities/locks, CLI flags, native host receipts. Reject malformed input; preserve unverifiable or changed content; recover failed writes without silent deletion. +- Horizontal disposition: standardize catalog facet contract across registry and CLI; consolidate CLI facet derivation in registry-client and installed YAML interpretation in core. Reuse existing native lifecycle and transactional install boundaries. Do not create parallel role registries or template generators. +- Planned commit slices (UNAUTHORIZED): (1) CLI preservation/migration, (2) facet discovery, (3) registry metadata/layout/content/docs, (4) generated artifacts. Each needs green evidence. No staging, commits, pushes, PR modifications/merges, release, live install or peer activation authorized. +- Human high-risk acceptance applies before publishing or running the migration against real installations. Local implementation and temporary fixtures are authorized by explicit skill invocation. + +## Phase 2: Red tests — complete + +Actual red failures and unchanged green reruns are retained under the evidence home. +Commands use `node scripts/run-tests.js ` in CLI and `npx vitest run ` +in Registry; each log identifies the test and observed failure. + +| Behavior | Red / green log stem | +|---|---| +| Multiline/CRLF YAML and inline lists | cli-metadata | +| Preserve full trigger description | cli-description | +| Owned flat-to-folder upgrade | cli-migration | +| Recover lock write failure; preserve edits/unowned sources | cli-migration-failure-green (failure-path characterization) | +| Concurrent replacement edit | cli-migration-race | +| Same-ID flat/folder conflict | cli-collision; cli-native-collision-red / cli-preservation-green | +| Graph role and facet filtering | cli-facets; cli-facet-commands; cli-inventory; cli-info | +| Native YAML source decoding | cli-native-yaml | +| Dry-run ownership preview | cli-dry-run | +| Preserve open old-file writer | cli-backup-writer | +| Reject historical checksum exclusions | cli-ignored-content | +| Validate staged source before replacement | cli-staged-yaml | +| Real schema-v2 preview normalization | cli-dry-run-v2 | +| Canonical folder/category/dependency rules | registry layout, policy and dependencies logs | + +The initial missing SQLite binding and missing PyYAML were runner setup failures, +not claimed red proof. Bindings were rebuilt locally; PyYAML was installed only +in an evidence-directory virtual environment. No project dependency was added. +Prose refinements use direct content review rather than implementation-mirroring tests. + +## Phase 3: Implementation — complete + +- 84 same-ID folder entrypoints, 65 moves, 19 existing bundles; seven categories, + capability/domain/provider tags, clearer names/descriptions and patch versions. +- Stack graph remains authoritative for primary operators; hard requirements and + optional companions are distinct. Three text-drafting shortform workflows no + longer require video/publishing installation. +- Public examples parameterized; stale SQLite/session assumptions removed; + focused operator routing/verification added; Reddit writes reconciled before + append; conditional host/review guidance moved verbatim to two references. +- CLI installed YAML and native decoding share core/package-metadata.js. Facet + derivation/filter validation share registry-client/skill-facets.js. +- Skill replacement stages and validates the bundle, checks lock ownership and + checksum (including formerly ignored content), rejects collisions/symlinks, + guards cooperating installers, and restores safely after recoverable failure. +- Successful replacements retain the previous inode/tree and return backupPath. + This intentionally preserves late writes through open file descriptors. Cleanup + of retained backups is separate and never automatic. Crash/recovery guards + remain explicit manual reconciliation boundaries. +- Native projections preserve complete descriptions, bundled Codex policy and + support resources. Existing host ownership/force/reload contracts remain. +- Search/list/info expose categories, graph-derived role and facets. Offline or + external inventory reports unknown role. Dry run normalizes real schema-v2 + registry entries before its read-only ownership/migration preview. + +## Phase 4: Green tests and refactor — complete + +All red behaviors rerun green. The shared YAML parser replaced the native shadow +parser after the native regression was red. Existing Design Rulebook tests moved +intact into src/design-rulebook.test.ts after a size warning; 352 registry tests +remain green. A missing-root-result edge in dependency-only installation was +corrected with optional access and independently exercised by the reviewer. +No assertions were weakened. Fixtures were completed with actual v2 metadata. + +## Phase 5: Full verification — complete + +- Registry: 352 tests, 33 files; validate 173 packages; indexes current; seven + release artifact hashes verified; hygiene zero targets; build and npm pack pass. +- Registry publication-candidate validation passes using an isolated GIT_INDEX_FILE. + The actual index is unchanged. Ordinary validation reports untracked candidate + paths until an authorized commit; this is recorded in registry-public.log. +- Registry debt: zero errors/warnings; focused tool scope and repository runner. +- Preserved Brand Assets stack: five tests and four-tool MCP surface pass using + its package-owned verification hook with preparation. +- CLI: 809 tests across 43 suites pass. Final full test log, build log, package dry-run and focused debt results in + evidence home. cli-debt-final.json records the exact 22-file input including + all untracked new modules and reports zero errors/warnings. +- Built CLI smoke: actual catalog search, install, list, info, dry-run, update, + native sync and removal under isolated RUDI_HOME and four explicit host homes. + Text-only workflows install zero stacks; exact Codex agents/openai.yaml survives. +- 84 projected native entrypoints pass the official local quick_validate.py. +- Fresh-context reviews: review-registry.md and review-cli.md preserve findings + and correction evidence with independent Standards, Spec and Proof axes. + +## Phase 6: Docs, contracts and closure — complete for local delivery + +- Registry guide docs/skill-catalog.md and ADR 0013 define categories, naming, + relationship graph and compatibility. README/CONTRIBUTING/SCHEMA/template and + CLI README/help/info documentation match the candidate. +- All48 original support files remain byte-identical; two linked references added. + No skill deleted, no stack implementation redesigned. The 267-line inherited + design-rulebook test group is preserved in its own test module. +- Horizontal disposition: consolidate metadata parsing and facet derivation now; + use existing registry normalization in dry-run. No third role registry, schema + generation framework or parallel native lifecycle introduced. Registry parser + and native projection are different authoring/delivery boundaries; keep them + separate and verify interoperability with the 84-entry smoke validation. +- Intentional residual: prior skill backups consume disk until explicitly + reconciled. Owner: installer maintainer/user; trigger: post-release accepted + migration review; closing proof: account for late edits and old locks before + any separately authorized cleanup. No universal rollback of unknown concurrent + external writers is claimed. +- Planned commit boundaries remain uncommitted: CLI migration/metadata; CLI + discovery; catalog layout/content/contracts; generated index/bundle artifacts. + PR43 remains a predecessor requiring its own acceptance. No real index staging, + commits, push, PR modifications, merges, release or live migration performed. +- Admin peer read-only state recorded in peer-final-status.log. Its dirty registry + is preserved. Sync is deferred until exact source commits and normal Git + reconciliation are authorized; do not overwrite either dirty peer. +- Release gate: accept compatible CLI (including PR43), assign and verify the + actual release version, then publish it before the moved registry source. + Reconcile and validate admin source before registry publication; run migration + dry runs on both real installations only with that rollout authority. +- Worktree closeout: preserve both uncommitted candidates; record and read back + Repo Steward preservation_required receipts. No cleanup authorized. +- Verdict: READY for authorized publication. Both independent reviews pass + Standards, Spec and Proof. Both Repo Steward receipts are read back at version2, + preservation_required, cleanup ineligible; both leases released. IDs are + skill-catalog-organization-20260905-cli and + skill-catalog-organization-20260905-registry. Ledger root: + /Users/hoff/.rudi/state/repo-steward; readback evidence: closeout-receipts.json. Publication, peer activation and real installed-skill migration remain + explicit subsequent delivery steps, not completed work. + +## Publication authorization — 2026-09-05 + +The user explicitly requested committing, merging and updating main. Feature-branch +push and PR merge are the required repository-policy path to that endpoint. Preserve +unrelated source in local recovery history; update the primary checkouts and admin +source peer after verified integration. Package-manager releases, live skill migration, +service restart, branch deletion and worktree cleanup remain separate operations. +The preceding no-publication entries describe the implementation delivery boundary. +Current publication evidence is recorded outside source under the dated publication +evidence directory and in Git/PR history. diff --git a/packages/core/src/__tests__/unit/bundled-skill-install.test.js b/packages/core/src/__tests__/unit/bundled-skill-install.test.js index 98b099c..40fadc1 100644 --- a/packages/core/src/__tests__/unit/bundled-skill-install.test.js +++ b/packages/core/src/__tests__/unit/bundled-skill-install.test.js @@ -1,6 +1,10 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import path from 'node:path'; +import fs from 'node:fs'; +import os from 'node:os'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; import { PATHS } from '@learnrudi/env'; import { getInstallPathForPackage } from '../../installer.js'; @@ -24,3 +28,194 @@ test('bundled and flat skills use distinct compatible install destinations', () path.join(PATHS.skills, 'flat-path-contract.md') ); }); + +function runSkillUpdate(extra = '', action = 'await updatePackage(initial.id, options)') { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rudi-skill-upgrade-')); + const repoRoot = fileURLToPath(new URL('../../../../../', import.meta.url)); + try { + const script = ` + import fs from 'node:fs'; + import path from 'node:path'; + import { installPackage, updatePackage, listInstalled } from './packages/core/src/installer.js'; + import { readLockfile } from './packages/core/src/lockfile.js'; + const source = path.join(process.env.RUDI_REGISTRY_ROOT, 'catalog/skills'); + fs.mkdirSync(source, { recursive: true }); + fs.writeFileSync(path.join(source, 'upgrade-demo.md'), '---\\nname: Demo\\ndescription: Old skill\\n---\\n'); + const initial = { id: 'skill:upgrade-demo', kind: 'skill', name: 'Demo', version: '1.0.0', path: 'catalog/skills/upgrade-demo.md', dependencies: [] }; + const first = await installPackage(initial.id, { resolvedPackage: initial }); + if (!first.success) throw new Error(first.error); + const oldPath = first.path; + const oldContent = fs.readFileSync(oldPath, 'utf8'); + const oldLock = readLockfile(initial.id); + fs.mkdirSync(path.join(source, 'upgrade-demo', 'agents'), { recursive: true }); + fs.writeFileSync(path.join(source, 'upgrade-demo/SKILL.md'), '---\\nname: Demo\\ndescription: Updated skill\\n---\\n'); + fs.writeFileSync(path.join(source, 'upgrade-demo/agents/openai.yaml'), 'policy:\\n allow_implicit_invocation: false\\n'); + const next = { ...initial, version: '1.1.0', path: 'catalog/skills/upgrade-demo', installed: true }; + const options = { resolvedPackage: next }; + ${extra} + const result = ${action}; + console.log(JSON.stringify({ result, oldExists: fs.existsSync(oldPath), oldContent, + legacyContent: fs.existsSync(oldPath) ? fs.readFileSync(oldPath, 'utf8') : null, + folderExists: fs.existsSync(path.join(process.env.RUDI_HOME, 'skills/upgrade-demo/SKILL.md')), + preservedOutput: fs.existsSync(path.join(process.env.RUDI_HOME, 'skills/upgrade-demo/outputs/user-research.md')) ? fs.readFileSync(path.join(process.env.RUDI_HOME, 'skills/upgrade-demo/outputs/user-research.md'), 'utf8') : null, + lock: readLockfile(initial.id), oldLock, + installed: (await listInstalled('skill')).filter(s => s.id === initial.id), + metadata: result.success ? fs.readFileSync(path.join(result.path, 'agents/openai.yaml'), 'utf8') : null, + savedBackups: fs.readdirSync(path.join(process.env.RUDI_HOME, 'skills')) + .filter(name => name.startsWith('.upgrade-demo.install-')) + .flatMap(name => { + const backup = path.join(process.env.RUDI_HOME, 'skills', name, 'previous'); + return fs.existsSync(backup) && fs.statSync(backup).isFile() ? [fs.readFileSync(backup, 'utf8')] : []; + }), + })); + `; + const output = execFileSync(process.execPath, ['--input-type=module', '-e', script], { + cwd: repoRoot, + env: { ...process.env, RUDI_HOME: path.join(root, 'home'), CLAUDE_HOME: path.join(root, 'claude'), + RUDI_REGISTRY_ROOT: path.join(root, 'registry'), USE_LOCAL_REGISTRY: 'true' }, + encoding: 'utf8', + }); + return JSON.parse(output); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +} + +test('updating an owned flat skill leaves one complete bundle and a directory lock', () => { + const result = runSkillUpdate(); + assert.equal(result.result.success, true); + assert.equal(result.oldExists, false, 'the obsolete owned file must not shadow the bundle'); + assert.equal(result.folderExists, true); + assert.equal(result.installed.length, 1); + assert.equal(result.installed[0].format, 'directory'); + assert.equal(result.lock.installLayout, 'directory'); + assert.match(result.metadata, /allow_implicit_invocation: false/); +}); + +test('an edited canonical skill is preserved instead of being migrated', () => { + const result = runSkillUpdate("fs.appendFileSync(oldPath, 'User-authored instructions.');"); + assert.equal(result.result.success, false); + assert.match(result.result.error, /Modified skill/); + assert.equal(result.legacyContent, result.oldContent + 'User-authored instructions.'); + assert.equal(result.folderExists, false); + assert.deepEqual(result.lock, result.oldLock); +}); + +test('a failed lockfile write restores the previous skill and lock', () => { + const result = runSkillUpdate(` + const writeFile = fs.writeFileSync; + let failed = false; + fs.writeFileSync = (file, ...args) => { + if (!failed && String(file).endsWith('upgrade-demo.lock.yaml')) { + failed = true; + throw new Error('injected lock write failure'); + } + return writeFile(file, ...args); + }; + `); + assert.equal(result.result.success, false); + assert.match(result.result.error, /injected lock write failure/); + assert.equal(result.legacyContent, result.oldContent); + assert.equal(result.folderExists, false); + assert.deepEqual(result.lock, result.oldLock); +}); + +test('a skill without ownership evidence is preserved', () => { + const result = runSkillUpdate("fs.rmSync(path.join(process.env.RUDI_HOME, 'locks/skills/upgrade-demo.lock.yaml'));"); + assert.equal(result.result.success, false); + assert.match(result.result.error, /Cannot prove ownership/); + assert.equal(result.legacyContent, result.oldContent); + assert.equal(result.folderExists, false); + assert.equal(result.lock, null); +}); + +test('a concurrent edit during replacement is never adopted or deleted by rollback', () => { + const result = runSkillUpdate(` + options.onProgress = event => { + if (event.phase === 'lockfile') { + fs.appendFileSync(path.join(process.env.RUDI_HOME, 'skills/upgrade-demo/SKILL.md'), 'Concurrent user edit.'); + } + }; + `); + assert.equal(result.result.success, false); + assert.match(result.result.error, /recovery failed.*Preserve/); + assert.equal(result.folderExists, true); + assert.deepEqual(result.savedBackups, [result.oldContent]); + assert.deepEqual(result.lock, result.oldLock); +}); + + +test('update dry run identifies a modified skill and leaves its file and lock intact', () => { + const result = runSkillUpdate(` + fs.appendFileSync(oldPath, 'User-authored instructions.'); + const { runUpdate } = await import('./src/commands/update.js'); + const deps = { listInstalled, fetchIndex: async () => ({ packages: { [next.id]: next } }), + log() {}, error() {} }; + `, "await runUpdate([initial.id], { 'dry-run': true, 'no-sync-skills': true }, deps)"); + assert.equal(result.result.packageFailed, 1); + assert.match(result.result.failures[0].error, /Modified skill/); + assert.equal(result.legacyContent, result.oldContent + 'User-authored instructions.'); + assert.deepEqual(result.lock, result.oldLock); + assert.equal(result.folderExists, false); +}); + + +test('update dry run reports the file-to-folder destination without installing it', () => { + const result = runSkillUpdate(` + const { runUpdate } = await import('./src/commands/update.js'); + const deps = { listInstalled, fetchIndex: async () => ({ packages: { [next.id]: next } }), + log() {}, error() {} }; + `, "await runUpdate([initial.id], { 'dry-run': true, 'no-sync-skills': true }, deps)"); + assert.equal(result.result.packageFailed, 0, JSON.stringify(result.result.failures)); + assert.equal(result.result.skillMigrations[0].action, 'migrate'); + assert.match(result.result.skillMigrations[0].to, /skills\/upgrade-demo$/); + assert.equal(result.legacyContent, result.oldContent); + assert.deepEqual(result.lock, result.oldLock); + assert.equal(result.folderExists, false); +}); + + +test('a previous file remains recoverable when an open writer edits its moved inode', () => { + const result = runSkillUpdate(` + const oldFd = fs.openSync(oldPath, 'a'); + options.onProgress = event => { + if (event.phase === 'lockfile') fs.writeSync(oldFd, 'Concurrent old-file edit.'); + }; + `); + assert.equal(result.result.success, true); + assert.deepEqual(result.savedBackups, [result.oldContent + 'Concurrent old-file edit.']); +}); + + +test('bundle updates preserve unowned content excluded from historical checksums', () => { + const result = runSkillUpdate(` + const bundle = await updatePackage(initial.id, options); + if (!bundle.success) throw new Error(bundle.error); + fs.mkdirSync(path.join(bundle.path, 'outputs')); + fs.writeFileSync(path.join(bundle.path, 'outputs/user-research.md'), 'User research'); + `); + assert.equal(result.result.success, false); + assert.match(result.result.error, /Untracked skill content/); + assert.equal(result.preservedOutput, 'User research'); +}); + + +test('malformed downloaded skill metadata cannot replace a working skill', () => { + const result = runSkillUpdate("fs.writeFileSync(path.join(source, 'upgrade-demo/SKILL.md'), '---\\nname: [broken YAML\\n---\\n');"); + assert.equal(result.result.success, false); + assert.equal(result.legacyContent, result.oldContent); + assert.deepEqual(result.lock, result.oldLock); + assert.equal(result.folderExists, false); +}); + + +test('update dry run reads canonical schema-v2 install paths', () => { + const result = runSkillUpdate(` + const { runUpdate } = await import('./src/commands/update.js'); + const { path: sourcePath, ...metadata } = next; + const deps = { listInstalled, fetchIndex: async () => ({ packages: { [next.id]: { ...metadata, delivery: 'local', install: { source: 'catalog', path: sourcePath } } } }), log() {}, error() {} }; + `, "await runUpdate([initial.id], { 'dry-run': true, 'no-sync-skills': true }, deps)"); + assert.equal(result.result.packageFailed, 0, JSON.stringify(result.result.failures)); + assert.equal(result.result.skillMigrations[0].action, 'migrate'); + assert.equal(result.legacyContent, result.oldContent); +}); diff --git a/packages/core/src/__tests__/unit/installer-list-installed.test.js b/packages/core/src/__tests__/unit/installer-list-installed.test.js index ea9f800..262a632 100644 --- a/packages/core/src/__tests__/unit/installer-list-installed.test.js +++ b/packages/core/src/__tests__/unit/installer-list-installed.test.js @@ -99,3 +99,39 @@ test('listInstalled includes local and Claude directory skills with metadata', ( fs.rmSync(root, { recursive: true, force: true }); } }); + +test('installed skill metadata preserves YAML facet lists and multiline descriptions', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rudi-skill-metadata-')); + const skillRoot = path.join(root, 'skills', 'web-publisher'); + fs.mkdirSync(skillRoot, { recursive: true }); + fs.writeFileSync(path.join(skillRoot, 'SKILL.md'), [ + '---', + 'name: Web Publisher', + 'description: >-', + ' Publish a website', + ' and verify its deployment.', + 'category: web', + 'tags: ["capability:deploy", "provider:vercel"]', + 'requires:', + ' stacks: ["stack:vercel"]', + '---', + '', + ].join('\r\n')); + try { + const output = execFileSync(process.execPath, ['--input-type=module', '-e', ` + const { listInstalled } = await import(process.argv[1]); + console.log(JSON.stringify((await listInstalled('skill')).find(s => s.id === 'skill:web-publisher'))); + `, installerUrl], { + cwd: repoRoot, + env: { ...process.env, RUDI_HOME: root, CLAUDE_HOME: path.join(root, 'claude') }, + encoding: 'utf8', + }); + const skill = JSON.parse(output); + assert.deepEqual(skill.tags, ['capability:deploy', 'provider:vercel']); + assert.equal(skill.description, 'Publish a website and verify its deployment.'); + assert.deepEqual(skill.requires, { stacks: ['stack:vercel'] }); + assert.equal(skill.category, 'web'); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/core/src/index.js b/packages/core/src/index.js index dc0e3a5..a4f2d91 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -29,6 +29,10 @@ export { searchPackages, getPackage, listPackages, + describeSkill, + getAvailableRegistryIndex, + matchesSkillFilters, + normalizeSkillFilters, clearCache } from '@learnrudi/registry-client'; @@ -42,3 +46,6 @@ export * from './tool-index.js'; export * from './shims.js'; export * from './system-registry.js'; export * from './stack-lifecycle.js'; + +export { parseSkillDocument } from './package-metadata.js'; +export { inspectRegistrySkillUpdate } from './skill-install.js'; diff --git a/packages/core/src/installer.js b/packages/core/src/installer.js index f8ab83a..e5ab6e3 100644 --- a/packages/core/src/installer.js +++ b/packages/core/src/installer.js @@ -10,6 +10,7 @@ import { execFileSync as defaultExecFileSync } from 'child_process'; import { pipeline } from 'stream/promises'; import { createWriteStream } from 'fs'; import { createGunzip } from 'zlib'; +import { parsePackageMetadata, parseSkillDocument } from './package-metadata.js'; import { PATHS, discoverSkillPackages, @@ -25,10 +26,13 @@ import { downloadResolvedPackage, downloadTool, verifyHash, + describeSkill, + getAvailableRegistryIndex, } from '@learnrudi/registry-client'; import { resolvePackage, getInstallOrder } from './resolver.js'; import { readLockfile, restoreLockfile, writeLockfile } from './lockfile.js'; import { createShimsForTool, removeShims } from './shims.js'; +import { installRegistrySkill } from './skill-install.js'; const SINGLE_FILE_KINDS = new Set(['skill', 'prompt', 'workflow']); const WORKFLOW_EXTENSIONS = ['.yaml', '.yml', '.json']; @@ -46,9 +50,9 @@ export function getInstallPathForPackage(pkg) { if ( kind === 'skill' && typeof pkg.path === 'string' && - !pkg.path.replaceAll('\\', '/').endsWith('.md') + pkg.path.length > 0 ) { - return path.join(PATHS.skills, name); + return path.join(PATHS.skills, pkg.path.replaceAll('\\', '/').endsWith('.md') ? `${name}.md` : name); } return getPackagePath(pkg.id); @@ -783,6 +787,12 @@ export async function installPackage(id, options = {}) { // Get install order (dependencies first) let toInstall = getInstallOrder(resolved); + // An installed ID may still need a source-format migration at its new path. + if (resolved.kind === 'skill' && !fs.existsSync(getInstallPathForPackage(resolved)) + && !toInstall.some(pkg => pkg.id === resolved.id)) { + toInstall.push(resolved); + } + // If already installed and not forcing, skip if (toInstall.length === 0 && !force) { return { @@ -851,7 +861,7 @@ export async function installPackage(id, options = {}) { } return { success: false, id: resolved.id, error: error.message }; } - } else if (!transaction) { + } else if (!transaction && !mainResult?.lockfileWritten && !mainResult?.skipped) { onProgress?.({ phase: 'lockfile', package: resolved.id }); await writeLockfile(resolved, { installPath: getInstallPathForPackage(resolved), @@ -862,6 +872,7 @@ export async function installPackage(id, options = {}) { success: true, id: resolved.id, path: getInstallPathForPackage(resolved), + ...(mainResult?.backupPath ? { backupPath: mainResult.backupPath } : {}), installed: results.map(r => r.id), ...(transaction && deferFinalize ? { transaction } : {}), }; @@ -1037,6 +1048,10 @@ async function installSinglePackage(pkg, options = {}) { return { success: true, id: pkg.id, path: installPath, skipped: true }; } + if (pkg.kind === 'skill' && pkg.source?.type !== 'github' && pkg.path) { + return installRegistrySkill(pkg, installPath, { onProgress }); + } + // Handle RUDI-managed runtimes and binaries. if (pkg.kind === 'runtime' || pkg.kind === 'binary') { onProgress?.({ phase: 'downloading', package: pkg.id }); @@ -1650,58 +1665,6 @@ async function copyDirectory(src, dest) { } } -function stripQuotes(value) { - return String(value || '').trim().replace(/^["']|["']$/g, ''); -} - -function parseListValue(lines, startIndex) { - const values = []; - for (let i = startIndex + 1; i < lines.length; i++) { - const line = lines[i]; - if (!/^\s+/.test(line)) break; - const itemMatch = line.match(/^\s*-\s+(.+?)\s*$/); - if (itemMatch) { - values.push(stripQuotes(itemMatch[1])); - } - } - return values; -} - -function parseSimpleYamlMetadata(yaml) { - const metadata = {}; - const lines = yaml.split(/\r?\n/); - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const scalarMatch = line.match(/^(name|description|version|category|icon):\s*(.+?)\s*$/); - if (scalarMatch) { - metadata[scalarMatch[1]] = stripQuotes(scalarMatch[2]); - continue; - } - - if (/^tags:\s*$/.test(line)) { - metadata.tags = parseListValue(lines, i); - continue; - } - - if (/^requires:\s*$/.test(line)) { - const requires = {}; - for (let j = i + 1; j < lines.length; j++) { - const nested = lines[j]; - if (!/^\s+/.test(nested)) break; - const sectionMatch = nested.match(/^\s+(stacks|skills):\s*$/); - if (sectionMatch) { - requires[sectionMatch[1]] = parseListValue(lines, j); - } - } - if (Object.keys(requires).length > 0) { - metadata.requires = requires; - } - } - } - - return metadata; -} function extractSingleFileMetadata(filePath, kind) { const content = fs.readFileSync(filePath, 'utf-8'); @@ -1710,13 +1673,10 @@ function extractSingleFileMetadata(filePath, kind) { return JSON.parse(content); } - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); - if (frontmatterMatch) { - return parseSimpleYamlMetadata(frontmatterMatch[1]); - } + if (/^---\r?\n/.test(content)) return parseSkillDocument(content).metadata; if (kind === 'workflow') { - return parseSimpleYamlMetadata(content); + return parsePackageMetadata(content); } return {}; @@ -1770,6 +1730,7 @@ export async function listInstalled(kind) { format: skill.format, source: getInstalledPackageSource(`${k}:${skill.name}`, skill.source), entryPath: skill.entryPath, + ...(skill.conflictingPaths ? { conflictingPaths: skill.conflictingPaths } : {}), path: skill.packagePath }); } catch { @@ -1785,6 +1746,7 @@ export async function listInstalled(kind) { format: skill.format, source: getInstalledPackageSource(`${k}:${skill.name}`, skill.source), entryPath: skill.entryPath, + ...(skill.conflictingPaths ? { conflictingPaths: skill.conflictingPaths } : {}), path: skill.packagePath }); } @@ -1872,7 +1834,14 @@ export async function listInstalled(kind) { } } - return packages; + const index = packages.some(pkg => pkg.kind === 'skill') ? getAvailableRegistryIndex() : null; + return packages.map(pkg => { + if (pkg.kind !== 'skill') return pkg; + const lock = readLockfile(pkg.id); + const catalogIdentity = pkg.source === 'rudi' && lock?.id === pkg.id + && /^[a-f0-9]{64}$/i.test(lock.checksum || ''); + return describeSkill(pkg, index, { catalogIdentity }); + }); } /** diff --git a/packages/core/src/lockfile.js b/packages/core/src/lockfile.js index 7fb5fd0..5c9a3df 100644 --- a/packages/core/src/lockfile.js +++ b/packages/core/src/lockfile.js @@ -191,7 +191,7 @@ const CHECKSUM_IGNORED_NAMES = new Set([ ]); const CHECKSUM_IGNORED_ROOT_NAMES = new Set(['outputs', 'runs']); -function updateContentHash(hash, rootPath, currentPath) { +function updateContentHash(hash, rootPath, currentPath, includeIgnored) { const relativePath = path.relative(rootPath, currentPath).split(path.sep).join('/'); const stat = fs.lstatSync(currentPath); if (stat.isSymbolicLink()) { @@ -209,14 +209,14 @@ function updateContentHash(hash, rootPath, currentPath) { hash.update(`dir\0${relativePath}\0`); for (const entry of fs.readdirSync(currentPath).sort()) { if ( - CHECKSUM_IGNORED_NAMES.has(entry) || - (currentPath === rootPath && CHECKSUM_IGNORED_ROOT_NAMES.has(entry)) + !includeIgnored && (CHECKSUM_IGNORED_NAMES.has(entry) || + (currentPath === rootPath && CHECKSUM_IGNORED_ROOT_NAMES.has(entry))) ) continue; - updateContentHash(hash, rootPath, path.join(currentPath, entry)); + updateContentHash(hash, rootPath, path.join(currentPath, entry), includeIgnored); } } -export async function computeInstalledContentChecksum(installPath) { +export async function computeInstalledContentChecksum(installPath, { includeIgnored = false } = {}) { if (!fs.existsSync(installPath)) { throw new Error(`Cannot checksum missing installed package: ${installPath}`); } @@ -227,7 +227,7 @@ export async function computeInstalledContentChecksum(installPath) { hash.update(`file\0.\0${executable}\0`); hash.update(fs.readFileSync(installPath)); } else { - updateContentHash(hash, installPath, installPath); + updateContentHash(hash, installPath, installPath, includeIgnored); } return hash.digest('hex'); } diff --git a/packages/core/src/package-metadata.js b/packages/core/src/package-metadata.js new file mode 100644 index 0000000..a1ee323 --- /dev/null +++ b/packages/core/src/package-metadata.js @@ -0,0 +1,46 @@ +import { parse as parseYamlMetadata } from 'yaml'; + +export function parsePackageMetadata(content) { + const parsed = parseYamlMetadata(content, { maxAliasCount: 50 }); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('Package metadata must be a mapping'); + } + const metadata = {}; + for (const field of ['name', 'description', 'version', 'category', 'icon']) { + if (parsed[field] === undefined) continue; + if (typeof parsed[field] !== 'string') { + throw new Error(`Package metadata ${field} must be a string`); + } + metadata[field] = parsed[field]; + } + const stringList = (value, field) => { + if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) { + throw new Error(`Package metadata ${field} must be a string list`); + } + return value; + }; + if (parsed.tags !== undefined) metadata.tags = stringList(parsed.tags, 'tags'); + if (parsed.requires !== undefined) { + if (!parsed.requires || typeof parsed.requires !== 'object' || Array.isArray(parsed.requires)) { + throw new Error('Package metadata requires must be a mapping'); + } + metadata.requires = {}; + for (const kind of ['stacks', 'skills']) { + if (parsed.requires[kind] !== undefined) { + metadata.requires[kind] = stringList(parsed.requires[kind], `requires.${kind}`); + } + } + } + return metadata; +} + + +/** Decode source metadata once for inventory and host projections. */ +export function parseSkillDocument(content = '') { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!match) return { metadata: {}, body: content.trimStart() }; + return { + metadata: parsePackageMetadata(match[1]), + body: content.slice(match[0].length).trimStart(), + }; +} diff --git a/packages/core/src/skill-install.js b/packages/core/src/skill-install.js new file mode 100644 index 0000000..e261001 --- /dev/null +++ b/packages/core/src/skill-install.js @@ -0,0 +1,176 @@ +/** Registry skill replacement owns its package and lockfile as one transaction. */ +import fs from 'node:fs'; +import { parseSkillDocument } from './package-metadata.js'; +import { parse as parseYaml } from 'yaml'; +import path from 'node:path'; +import { PATHS, getLockfilePath } from '@learnrudi/env'; +import { downloadPackage } from '@learnrudi/registry-client'; +import { computeInstalledContentChecksum, readLockfile, restoreLockfile, writeLockfile } from './lockfile.js'; + +function statIfPresent(file) { + try { return fs.lstatSync(file); } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } +} + +function assertRealParents(target) { + const relative = path.relative(PATHS.home, target); + if (relative.startsWith('..') || path.isAbsolute(relative)) { + throw new Error('Skill install path escapes RUDI home'); + } + let current = PATHS.home; + for (const segment of ['', ...relative.split(path.sep)]) { + if (segment) current = path.join(current, segment); + const stat = statIfPresent(current); + if (stat?.isSymbolicLink()) throw new Error(`Refusing symlinked skill install path: ${current}`); + } +} + +function inspectPreviousInstall(pkg, destination) { + const slug = pkg.id.slice('skill:'.length); + const candidates = [path.join(PATHS.skills, slug), path.join(PATHS.skills, `${slug}.md`)]; + for (const candidate of [...candidates, getLockfilePath(pkg.id)]) assertRealParents(candidate); + const existing = candidates.filter(candidate => statIfPresent(candidate)); + if (existing.length > 1) { + throw new Error(`Conflicting skill formats for ${pkg.id}; preserve and reconcile ${existing.join(' and ')}`); + } + const previousPath = existing[0] || null; + const previousLockfile = readLockfile(pkg.id); + if (previousPath) { + const stat = fs.lstatSync(previousPath); + const layout = stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : null; + if (!layout || previousLockfile?.id !== pkg.id + || !/^[a-f0-9]{64}$/i.test(previousLockfile?.checksum || '') + || (previousLockfile.installLayout && previousLockfile.installLayout !== layout)) { + throw new Error(`Cannot prove ownership of ${pkg.id}; preserving ${previousPath}`); + } + } + return { id: pkg.id, destination, previousPath, previousLockfile }; +} + +async function assertUnchanged(state, candidate = state.previousPath) { + if (!candidate) return; + const digest = await computeInstalledContentChecksum(candidate); + if (digest !== await computeInstalledContentChecksum(candidate, { includeIgnored: true })) { + throw new Error(`Untracked skill content excluded from the ownership checksum; preserving ${candidate}`); + } + if (digest !== state.previousLockfile.checksum) { + throw new Error(`Modified skill ${state.id}; preserving local content at ${candidate}`); + } +} + +async function assertReplacementUnchanged(state) { + assertRealParents(state.destination); + if (await computeInstalledContentChecksum(state.destination, { includeIgnored: true }) !== state.replacementDigest) { + throw new Error(`Replacement changed concurrently; preserving ${state.destination}`); + } +} + +async function restorePrevious(state) { + if (state.installed) { + await assertReplacementUnchanged(state); + fs.rmSync(state.destination, { recursive: true, force: true }); + } + if (state.movedPrevious) { + if (statIfPresent(state.previousPath)) { + throw new Error(`Recovery conflict; previous skill is preserved at ${state.backupPath}`); + } + fs.renameSync(state.backupPath, state.previousPath); + } + if (state.lockWriteAttempted) restoreLockfile(state.id, state.previousLockfile); +} + +async function replaceSkill(pkg, state, onProgress) { + const stage = path.join(state.transactionRoot, 'next'); + await downloadPackage(pkg, stage, { onProgress }); + const directory = !pkg.path.replaceAll('\\', '/').endsWith('.md'); + const entry = directory ? path.join(stage, 'SKILL.md') : stage; + if (!statIfPresent(entry)?.isFile()) throw new Error(`Downloaded ${pkg.id} has no regular skill entrypoint`); + const content = fs.readFileSync(entry, 'utf8'); + const { metadata } = parseSkillDocument(content); + if (!metadata.name?.trim() || !metadata.description?.trim()) { + throw new Error(`Downloaded ${pkg.id} requires name and description metadata`); + } + if (directory) { + const nativeMetadata = path.join(stage, 'agents/openai.yaml'); + if (statIfPresent(nativeMetadata)) { + if (!fs.lstatSync(nativeMetadata).isFile()) throw new Error('Native metadata must be a regular file'); + const parsed = parseYaml(fs.readFileSync(nativeMetadata, 'utf8'), { maxAliasCount: 50 }); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('Native metadata must be a YAML mapping'); + } + } + } + state.replacementDigest = await computeInstalledContentChecksum(stage, { includeIgnored: true }); + await assertUnchanged(state); + if (state.previousPath) { + fs.renameSync(state.previousPath, state.backupPath); + state.movedPrevious = true; + await assertUnchanged(state, state.backupPath); + } + if (statIfPresent(state.destination)) throw new Error(`Skill destination changed during install: ${state.destination}`); + fs.renameSync(stage, state.destination); + state.installed = true; + onProgress?.({ phase: 'lockfile', package: pkg.id }); + await assertReplacementUnchanged(state); + assertRealParents(getLockfilePath(pkg.id)); + state.lockWriteAttempted = true; + await writeLockfile(pkg, { installPath: state.destination }); + await assertReplacementUnchanged(state); +} + +/** Read-only migration preview; execution repeats these checks under its guard. */ +export async function inspectRegistrySkillUpdate(pkg, destination) { + if (!/^skill:[a-z0-9]+(?:-[a-z0-9]+)*$/.test(pkg.id) || typeof pkg.path !== 'string') { + throw new Error('Invalid registry skill identity or source path'); + } + const guardPath = path.join(PATHS.skills, `.${pkg.id.slice(6)}.install-lock`); + assertRealParents(guardPath); + if (statIfPresent(guardPath)) throw new Error(`Skill install already active or awaiting recovery: ${guardPath}`); + const state = inspectPreviousInstall(pkg, destination); + await assertUnchanged(state); + return { id: pkg.id, from: state.previousPath, to: destination, + action: state.previousPath && state.previousPath !== destination ? 'migrate' : 'update' }; +} + +export async function installRegistrySkill(pkg, destination, { onProgress } = {}) { + if (!/^skill:[a-z0-9]+(?:-[a-z0-9]+)*$/.test(pkg.id) || typeof pkg.path !== 'string') { + throw new Error('Invalid registry skill identity or source path'); + } + assertRealParents(PATHS.skills); + const guardPath = path.join(PATHS.skills, `.${pkg.id.slice(6)}.install-lock`); + try { fs.mkdirSync(guardPath); } catch (error) { + if (error.code === 'EEXIST') throw new Error(`Skill install already active or awaiting recovery: ${guardPath}`); + throw error; + } + let state; + let recoveryFailed = false; + try { + state = inspectPreviousInstall(pkg, destination); + await assertUnchanged(state); + state.transactionRoot = fs.mkdtempSync(path.join(PATHS.skills, `.${pkg.id.slice(6)}.install-`)); + state.backupPath = path.join(state.transactionRoot, 'previous'); + await replaceSkill(pkg, state, onProgress); + onProgress?.({ phase: 'installed', package: pkg.id, path: destination }); + return { success: true, id: pkg.id, path: destination, lockfileWritten: true, + ...(state.movedPrevious ? { backupPath: state.backupPath } : {}) }; + } catch (error) { + if (state) { + try { await restorePrevious(state); } catch (recoveryError) { + recoveryFailed = true; + throw new Error(`${error.message}; recovery failed: ${recoveryError.message}. Preserve ${state.transactionRoot}`); + } + } + throw error; + } finally { + if (!recoveryFailed) { + // Retain the previous inode/tree: another process may still hold it open. + // Backup deletion requires a separate, explicit reconciliation step. + if (state?.transactionRoot && !statIfPresent(state.backupPath)) { + fs.rmSync(state.transactionRoot, { recursive: true, force: true }); + } + fs.rmdirSync(guardPath); + } + } +} diff --git a/packages/env/src/__tests__/unit/skill-discovery.test.js b/packages/env/src/__tests__/unit/skill-discovery.test.js index 18aa547..9e625aa 100644 --- a/packages/env/src/__tests__/unit/skill-discovery.test.js +++ b/packages/env/src/__tests__/unit/skill-discovery.test.js @@ -91,3 +91,24 @@ test('external Claude skill discovery preserves RUDI precedence', () => { fs.rmSync(root, { recursive: true, force: true }); } }); + +test('same-root duplicate skill formats are reported and exact path resolution refuses ambiguity', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rudi-skill-collision-')); + const skillDir = path.join(root, 'skills', 'demo'); + fs.mkdirSync(skillDir, { recursive: true }); + fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Bundle'); + fs.writeFileSync(path.join(root, 'skills', 'demo.md'), '# Legacy'); + try { + const result = runEnvScript(root, ` + const { discoverSkillPackages, getPackagePath } = await import(process.argv[1]); + let error; + try { getPackagePath('skill:demo'); } catch (cause) { error = cause.message; } + console.log(JSON.stringify({ skills: discoverSkillPackages(), error })); + `); + assert.equal(result.skills.length, 1); + assert.deepEqual(result.skills[0].conflictingPaths.sort(), [skillDir, path.join(root, 'skills', 'demo.md')].sort()); + assert.match(result.error, /Conflicting skill formats/); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/env/src/index.js b/packages/env/src/index.js index 9a77381..a1170ee 100644 --- a/packages/env/src/index.js +++ b/packages/env/src/index.js @@ -491,7 +491,7 @@ function readSkillCandidates(root) { /** * Discover skills in precedence order. RUDI skills win over external skills. - * Within the same root, flat files keep backward-compatible precedence. + * Same-root duplicate formats are visible conflicts, not silent overrides. * @param {{ includeExternal?: boolean }} options * @returns {Array<{name: string, source: string, format: string, packagePath: string, entryPath: string}>} */ @@ -499,9 +499,19 @@ export function discoverSkillPackages(options = {}) { const byName = new Map(); for (const root of getSkillDiscoveryRoots(options)) { - for (const candidate of readSkillCandidates(root)) { + const candidates = readSkillCandidates(root).sort((left, right) => ( + left.packagePath.localeCompare(right.packagePath) + )); + for (const candidate of candidates) { if (!byName.has(candidate.name)) { byName.set(candidate.name, candidate); + } else { + const existing = byName.get(candidate.name); + if (existing.source === candidate.source) { + existing.conflictingPaths = [...new Set([ + ...(existing.conflictingPaths || [existing.packagePath]), candidate.packagePath, + ])].sort(); + } } } } @@ -510,7 +520,11 @@ export function discoverSkillPackages(options = {}) { } function findLocalSkillPackage(name) { - return discoverSkillPackages().find(skill => skill.name === name) || null; + const skill = discoverSkillPackages().find(candidate => candidate.name === name) || null; + if (skill?.conflictingPaths) { + throw new Error(`Conflicting skill formats for skill:${name}: ${skill.conflictingPaths.join(', ')}`); + } + return skill; } /** diff --git a/packages/registry-client/src/__tests__/unit/skill-facets.test.js b/packages/registry-client/src/__tests__/unit/skill-facets.test.js new file mode 100644 index 0000000..2179bc3 --- /dev/null +++ b/packages/registry-client/src/__tests__/unit/skill-facets.test.js @@ -0,0 +1,60 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = fileURLToPath(new URL('../../../../../', import.meta.url)); +const index = { + schemaVersion: '2', + packages: { + 'stack:vercel': { id: 'stack:vercel', kind: 'stack', name: 'Vercel', version: '1.0.0', related: { + operatorSkill: 'skill:vercel', skills: ['skill:vercel', 'skill:publish-site'], + } }, + 'skill:vercel': { id: 'skill:vercel', kind: 'skill', name: 'Vercel Operator', version: '1.0.0', + meta: { category: 'web', description: 'Deploy a site', tags: ['capability:deploy', 'provider:vercel'] }, + requires: { stacks: ['stack:vercel'] } }, + 'skill:publish-site': { id: 'skill:publish-site', kind: 'skill', name: 'Publish Site', version: '1.0.0', + meta: { category: 'web', description: 'Publish project', tags: ['capability:deploy', 'provider:vercel'] }, + requires: { stacks: ['stack:vercel'] } }, + 'skill:other': { id: 'skill:other', kind: 'skill', name: 'Other', version: '1.0.0', meta: { category: 'data', description: 'Analyze information' } }, + }, +}; + +function search(query, options, cliArgs) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rudi-facet-search-')); + try { + fs.writeFileSync(path.join(root, 'index.json'), JSON.stringify(index)); + const commandArgs = cliArgs ? ['src/index.js', ...cliArgs] : ['--input-type=module', '-e', ` + import { searchPackages } from './packages/registry-client/src/index.js'; + console.log(JSON.stringify(await searchPackages(process.argv[1], JSON.parse(process.argv[2])))); + `, query, JSON.stringify(options)]; + const output = execFileSync(process.execPath, commandArgs, { + cwd: repoRoot, encoding: 'utf8', + env: { ...process.env, RUDI_HOME: path.join(root, 'home'), + USE_LOCAL_REGISTRY: 'true', RUDI_REGISTRY_ROOT: root }, + }); + return JSON.parse(output); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +} + +test('search finds skill categories and distinguishes operators from required-stack workflows', () => { + const results = search('web', { kind: 'skill', category: 'web', role: 'operator', provider: 'vercel' }); + assert.deepEqual(results.map(pkg => pkg.id), ['skill:vercel']); + assert.equal(results[0].skillRole, 'operator'); + assert.deepEqual(results[0].operatorFor, ['stack:vercel']); + assert.deepEqual(results[0].facets, { capabilities: ['deploy'], domains: [], providers: ['vercel'] }); + const workflows = search('', { kind: 'skill', role: 'workflow', capability: 'deploy' }); + assert.deepEqual(workflows.map(pkg => pkg.id), ['skill:publish-site']); +}); + +test('CLI query and all-skills JSON searches apply the same facet filters', () => { + const all = search('', {}, ['search', '--all', '--skills', '--category=web', '--role=operator', '--json']); + assert.deepEqual(all.skill.map(pkg => pkg.id), ['skill:vercel']); + const query = search('', {}, ['search', 'web', '--skills', '--role=workflow', '--json']); + assert.deepEqual(query.map(pkg => pkg.id), ['skill:publish-site']); +}); diff --git a/packages/registry-client/src/index.js b/packages/registry-client/src/index.js index 15cc2f1..a52faaa 100644 --- a/packages/registry-client/src/index.js +++ b/packages/registry-client/src/index.js @@ -17,6 +17,9 @@ import { normalizeRegistryPackage, } from './registry-contract.js'; import { downloadGitHubDirectory } from './github-source.js'; +import { describeSkill, matchesSkillFilters, normalizeSkillFilters } from './skill-facets.js'; + +export { describeSkill, matchesSkillFilters, normalizeSkillFilters } from './skill-facets.js'; export { normalizeRegistryPackage, resolveRegistryPackageForPlatform } from './registry-contract.js'; export { @@ -336,7 +339,7 @@ async function fetchRemoteRegistryIndex(url) { * Get cached index if valid * @returns {Object|null} */ -function getCachedIndex() { +function getCachedIndex({ allowExpired = false } = {}) { const cachePath = PATHS.registryCache; if (!fs.existsSync(cachePath)) { @@ -347,7 +350,7 @@ function getCachedIndex() { const stat = fs.statSync(cachePath); const age = Date.now() - stat.mtimeMs; - if (age > CACHE_TTL) { + if (!allowExpired && age > CACHE_TTL) { return null; // Cache expired } @@ -357,6 +360,13 @@ function getCachedIndex() { } } +/** Read-only catalog context for installed inventory; never fetch or refresh cache. */ +export function getAvailableRegistryIndex() { + const index = getLocalIndex()?.index || getCachedIndex({ allowExpired: true }); + if (!index) return null; + try { detectRegistrySchema(index); return index; } catch { return null; } +} + /** * Cache the registry index * @param {Object} index @@ -454,6 +464,7 @@ export const PACKAGE_KINDS = ['stack', 'skill', 'prompt', 'workflow', 'runtime', */ export async function searchPackages(query, options = {}) { const { kind } = options; + const filters = normalizeSkillFilters(options); const index = await fetchIndex(); const results = []; @@ -464,8 +475,9 @@ export async function searchPackages(query, options = {}) { for (const k of kinds) { const packages = listRegistryPackages(index, k); - for (const pkg of packages) { - if (matchesQuery(pkg, queryLower)) { + for (const raw of packages) { + const pkg = describeSkill(raw, index); + if (matchesQuery(pkg, queryLower) && matchesSkillFilters(pkg, filters)) { results.push({ ...pkg, kind: k }); } } @@ -482,6 +494,7 @@ function matchesQuery(pkg, query) { pkg.id || '', pkg.name || '', pkg.description || '', + pkg.category || '', ...(pkg.tags || []) ].join(' ').toLowerCase(); @@ -574,9 +587,11 @@ export async function getManifest(pkg) { * @param {'stack' | 'skill' | 'prompt' | 'workflow' | 'runtime' | 'binary' | 'agent'} kind * @returns {Promise} */ -export async function listPackages(kind) { +export async function listPackages(kind, options = {}) { + const filters = normalizeSkillFilters(options); const index = await fetchIndex(); - return listRegistryPackages(index, kind); + return listRegistryPackages(index, kind).map(pkg => describeSkill(pkg, index)) + .filter(pkg => matchesSkillFilters(pkg, filters)); } /** diff --git a/packages/registry-client/src/skill-facets.js b/packages/registry-client/src/skill-facets.js new file mode 100644 index 0000000..d441da8 --- /dev/null +++ b/packages/registry-client/src/skill-facets.js @@ -0,0 +1,55 @@ +// Catalog tags remain authored data; primary operator roles come from stack relationships. +const FACET_KEYS = { capability: 'capabilities', domain: 'domains', provider: 'providers' }; +const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; + +export function describeSkill(pkg, index, { catalogIdentity = true } = {}) { + if (pkg.kind !== 'skill') return pkg; + const tags = Array.isArray(pkg.tags) ? pkg.tags : Array.isArray(pkg.meta?.tags) ? pkg.meta.tags : []; + const facets = { capabilities: [], domains: [], providers: [] }; + for (const tag of tags) { + if (typeof tag !== 'string') continue; + const [namespace, value, extra] = tag.split(':'); + if (Object.hasOwn(FACET_KEYS, namespace) && SLUG.test(value || '') && extra === undefined) { + facets[FACET_KEYS[namespace]].push(value); + } + } + for (const key of Object.values(FACET_KEYS)) facets[key] = [...new Set(facets[key])].sort(); + const registered = catalogIdentity && index?.packages?.[pkg.id]?.kind === 'skill' + && index.packages[pkg.id].id === pkg.id; + const operatorFor = registered ? Object.entries(index.packages) + .filter(([id, value]) => id.startsWith('stack:') && value?.id === id && value.kind === 'stack' + && value.related?.operatorSkill === pkg.id) + .map(([id]) => id).sort() : []; + return { + ...pkg, + category: pkg.category || pkg.meta?.category, + tags, + facets, + skillRole: registered ? (operatorFor.length > 0 ? 'operator' : 'workflow') : 'unknown', + operatorFor, + }; +} + +export function normalizeSkillFilters(options = {}) { + const filters = {}; + for (const key of ['category', 'role', ...Object.keys(FACET_KEYS)]) { + if (options[key] === undefined) continue; + if (typeof options[key] !== 'string' || !SLUG.test(options[key])) { + throw new Error(`--${key} requires a lowercase category or facet name`); + } + if (key === 'role' && !['operator', 'workflow', 'unknown'].includes(options[key])) { + throw new Error('--role must be operator, workflow, or unknown'); + } + filters[key] = options[key]; + } + return filters; +} + +export function matchesSkillFilters(pkg, filters) { + if (filters.category && (pkg.category || pkg.meta?.category) !== filters.category) return false; + if (filters.role && pkg.skillRole !== filters.role) return false; + for (const [filter, facet] of Object.entries(FACET_KEYS)) { + if (filters[filter] && !pkg.facets?.[facet]?.includes(filters[filter])) return false; + } + return true; +} diff --git a/packages/utils/src/help.js b/packages/utils/src/help.js index 1248cd4..75fb9cc 100644 --- a/packages/utils/src/help.js +++ b/packages/utils/src/help.js @@ -412,7 +412,9 @@ EXAMPLES rudi list binaries rudi list workflows rudi skills - rudi list skills --category=coding + rudi list skills --category=code + rudi search --all --skills --category=web --role=operator + rudi list skills --provider=vercel `, skills: ` rudi skills - List or sync installed RUDI skills diff --git a/src/__tests__/unit/native-skill-lifecycle.test.js b/src/__tests__/unit/native-skill-lifecycle.test.js index c055412..0c928fa 100644 --- a/src/__tests__/unit/native-skill-lifecycle.test.js +++ b/src/__tests__/unit/native-skill-lifecycle.test.js @@ -75,6 +75,108 @@ test('reconcileNativeSkill creates a complete Codex tree and ownership receipt', } }); +test('forced reconciliation preserves bundled Codex metadata verbatim', async () => { + const state = fixture(); + try { + const canonicalMetadata = [ + 'interface:', + ' display_name: Demo Skill', + ' short_description: Canonical metadata fixture', + ' default_prompt: Run the canonical metadata fixture.', + '', + 'policy:', + ' allow_implicit_invocation: false', + '', + ].join('\n'); + const agentsDir = path.join(path.dirname(state.skill.entryPath), 'agents'); + fs.mkdirSync(agentsDir, { recursive: true }); + fs.writeFileSync(path.join(agentsDir, 'openai.yaml'), canonicalMetadata); + + const created = await reconcileNativeSkill({ + host: 'codex', + skill: state.skill, + targetRoot: state.nativeRoot, + receiptRoot: state.receiptRoot, + }); + fs.writeFileSync(path.join(created.targetDir, 'agents', 'openai.yaml'), 'stale metadata\n'); + + const forced = await reconcileNativeSkill({ + host: 'codex', + skill: state.skill, + targetRoot: state.nativeRoot, + receiptRoot: state.receiptRoot, + force: true, + }); + + assert.equal(forced.action, 'updated'); + assert.equal(forced.forced, true); + assert.equal( + fs.readFileSync(path.join(forced.targetDir, 'agents', 'openai.yaml'), 'utf8'), + canonicalMetadata, + ); + } finally { + fs.rmSync(state.root, { recursive: true, force: true }); + } +}); + +test('native projections preserve the complete trigger description independently of UI summaries', async () => { + const state = fixture(); + try { + const description = 'Review the requested project and preserve its task scope. '.repeat(8) + + 'Use only when the user explicitly invokes this skill.'; + state.skill.description = description; + for (const host of ['codex', 'claude', 'gemini', 'antigravity']) { + const result = await reconcileNativeSkill({ + host, skill: state.skill, + targetRoot: path.join(state.root, host, 'skills'), + receiptRoot: state.receiptRoot, + }); + const content = fs.readFileSync(path.join(result.targetDir, 'SKILL.md'), 'utf8'); + assert.ok(content.includes(description), `${host} dropped part of the trigger description`); + } + } finally { + fs.rmSync(state.root, { recursive: true, force: true }); + } +}); + +test('native sync refuses a canonical skill with conflicting source formats', async () => { + const state = fixture(); + state.skill.conflictingPaths = [state.skill.path, `${state.skill.path}.md`]; + try { + const result = await reconcileNativeSkill({ + host: 'codex', skill: state.skill, + targetRoot: state.nativeRoot, receiptRoot: state.receiptRoot, + }); + assert.equal(result.action, 'failed'); + assert.match(result.error, /Conflicting skill formats/); + assert.equal(fs.existsSync(path.join(state.nativeRoot, 'demo-skill')), false); + } finally { + fs.rmSync(state.root, { recursive: true, force: true }); + } +}); + +test('native rendering reads YAML descriptions and strips CRLF source frontmatter once', async () => { + const state = fixture(); + try { + delete state.skill.description; + fs.writeFileSync(state.skill.entryPath, [ + '---', 'name: Demo Skill', 'description: >-', + ' Review the website', ' only when explicitly requested.', + 'category: web', 'tags: ["capability:review"]', '---', '', 'Canonical body.', '', + ].join('\r\n')); + const result = await reconcileNativeSkill({ + host: 'codex', skill: state.skill, targetRoot: state.nativeRoot, receiptRoot: state.receiptRoot, + }); + assert.equal(result.action, 'created'); + const content = fs.readFileSync(path.join(result.targetDir, 'SKILL.md'), 'utf8'); + assert.ok(content.includes('Review the website only when explicitly requested.')); + assert.equal(content.includes('category: web'), false); + assert.equal(content.split('Canonical body.').length, 2); + } finally { + fs.rmSync(state.root, { recursive: true, force: true }); + } +}); + test('managed updates replace the complete tree, prune stale resources, and become idempotent', async () => { const state = fixture(); try { diff --git a/src/__tests__/unit/skill-inventory.test.js b/src/__tests__/unit/skill-inventory.test.js new file mode 100644 index 0000000..ca5667e --- /dev/null +++ b/src/__tests__/unit/skill-inventory.test.js @@ -0,0 +1,78 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)); + +function inventoryCommand(command, { catalog = true } = {}) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rudi-skill-inventory-')); + try { + const script = ` + import fs from 'node:fs'; + import path from 'node:path'; + import { installPackage } from './packages/core/src/installer.js'; + import { cmdList } from './src/commands/list.js'; + import { cmdInfo } from './src/commands/info.js'; + const registryRoot = process.env.RUDI_REGISTRY_ROOT; + const skill = { id: 'skill:web-publisher', kind: 'skill', name: 'Web Publisher', version: '1.2.0', + path: 'catalog/skills/web-publisher', dependencies: [] }; + const source = path.join(registryRoot, skill.path); + fs.mkdirSync(source, { recursive: true }); + fs.writeFileSync(path.join(source, 'SKILL.md'), [ + '---', 'name: Web Publisher', 'version: 1.2.0', 'description: Publish and verify a website.', + 'category: web', 'tags: ["capability:deploy", "provider:vercel"]', + 'requires:', ' stacks: ["stack:vercel"]', '---', '', + ].join('\\n')); + const result = await installPackage(skill.id, { resolvedPackage: skill }); + if (!result.success) throw new Error(result.error); + if (${catalog}) fs.writeFileSync(path.join(registryRoot, 'index.json'), JSON.stringify({ + schemaVersion: '2', packages: { + [skill.id]: skill, + 'stack:vercel': { id: 'stack:vercel', kind: 'stack', name: 'Vercel', version: '1.0.0', + related: { operatorSkill: skill.id, skills: [skill.id] } }, + }, + })); + globalThis.fetch = () => { throw new Error('Installed inventory must not need the network'); }; + ${command} + `; + return execFileSync(process.execPath, ['--input-type=module', '-e', script], { + cwd: repoRoot, encoding: 'utf8', + env: { ...process.env, RUDI_HOME: path.join(root, 'home'), CLAUDE_HOME: path.join(root, 'claude'), + USE_LOCAL_REGISTRY: 'true', RUDI_REGISTRY_ROOT: path.join(root, 'registry') }, + }); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +} + +test('installed skill listing filters facets and derives operator identity without network access', () => { + const skills = JSON.parse(inventoryCommand("await cmdList(['skills'], { json: true, role: 'operator', provider: 'vercel' });")); + assert.equal(skills.length, 1); + assert.equal(skills[0].skillRole, 'operator'); + assert.deepEqual(skills[0].operatorFor, ['stack:vercel']); + assert.deepEqual(skills[0].facets.capabilities, ['deploy']); + const absent = JSON.parse(inventoryCommand("await cmdList(['skills'], { json: true, role: 'workflow' });")); + assert.deepEqual(absent, []); +}); + +test('skill info displays entrypoint metadata and its primary stack relationship', () => { + const output = inventoryCommand("await cmdInfo(['skill:web-publisher'], {});"); + assert.match(output, /Name:\s+Web Publisher/); + assert.match(output, /Version:\s+1\.2\.0/); + assert.match(output, /Category:\s+web/); + assert.match(output, /Role:\s+operator/); + assert.match(output, /Operator for:\s+stack:vercel/); +}); + + +test('offline inventory preserves metadata and explicitly reports unknown role', () => { + const skills = JSON.parse(inventoryCommand("await cmdList(['skills'], { json: true, role: 'unknown' });", { catalog: false })); + assert.equal(skills.length, 1); + assert.equal(skills[0].skillRole, 'unknown'); + assert.equal(skills[0].category, 'web'); + assert.deepEqual(skills[0].operatorFor, []); +}); diff --git a/src/__tests__/unit/update-command.test.js b/src/__tests__/unit/update-command.test.js index 95ba154..74feebd 100644 --- a/src/__tests__/unit/update-command.test.js +++ b/src/__tests__/unit/update-command.test.js @@ -13,7 +13,12 @@ function createDeps(overrides = {}) { calls, async fetchIndex(options) { calls.push(['fetchIndex', options]); - return {}; + return { packages: { 'skill:rudi-engineering-gate': { + id: 'skill:rudi-engineering-gate', name: 'Engineering Gate', version: '1.0.0', kind: 'skill', path: 'catalog/skills/rudi-engineering-gate', + } } }; + }, + async inspectRegistrySkillUpdate(pkg, destination) { + return { id: pkg.id, from: `/tmp/${pkg.id}`, to: destination, action: 'update' }; }, async listInstalled() { calls.push(['listInstalled']); @@ -711,6 +716,7 @@ test('runUpdate dry-run returns the exact suite plan without package or index mu return { schemaVersion: '2', packages: { + 'skill:swe-compliance-checklist': { id: 'skill:swe-compliance-checklist', name: 'SWE Checklist', version: '1.0.0', kind: 'skill', path: 'catalog/skills/swe-compliance-checklist' }, 'stack:swe-engineering': { id: 'stack:swe-engineering', kind: 'stack', @@ -745,6 +751,7 @@ test('runUpdate dry-run returns the exact suite plan without package or index mu ); assert.equal(result.dryRun, true); + assert.equal(result.packageFailed, 0); assert.equal(result.updated, 0); assert.deepEqual(result.plannedPackages, [ 'stack:swe-engineering', @@ -769,6 +776,7 @@ test('runUpdate suite dry-run projects only planned skills to explicitly selecte return { schemaVersion: '2', packages: { + 'skill:swe-compliance-checklist': { id: 'skill:swe-compliance-checklist', name: 'SWE Checklist', version: '1.0.0', kind: 'skill', path: 'catalog/skills/swe-compliance-checklist' }, 'stack:swe-engineering': { id: 'stack:swe-engineering', kind: 'stack', diff --git a/src/commands/info.js b/src/commands/info.js index f08bc09..cffa030 100644 --- a/src/commands/info.js +++ b/src/commands/info.js @@ -13,9 +13,27 @@ import fs from 'fs'; import path from 'path'; import { getPackagePath, parsePackageId, PATHS } from '@learnrudi/env'; -import { getShimOwner, validateShim } from '@learnrudi/core'; +import { getShimOwner, validateShim, listInstalled } from '@learnrudi/core'; import { inspectRuntimeInstall } from '../runtime-inspection.js'; import { printPackageLifecycle } from './package-lifecycle.js'; +import { printSkillDetails } from './skill-display.js'; + +async function showSkillInfo(id, flags) { + const skill = (await listInstalled('skill')).find(pkg => pkg.id === id); + if (!skill) throw new Error(`Package not installed: ${id}`); + if (flags.json) { + console.log(JSON.stringify(skill, null, 2)); + return; + } + console.log(`\nPackage: ${id}`); + console.log(` Name: ${skill.name}`); + console.log(` Kind: skill`); + console.log(` Version: ${skill.version}`); + console.log(` Entrypoint: ${skill.entryPath}`); + console.log(` Description: ${skill.description}`); + printSkillDetails(skill, ' '); + if (skill.requires?.stacks?.length) console.log(` Requires: ${skill.requires.stacks.join(', ')}`); +} function resolvesToSameFile(leftPath, rightPath) { try { @@ -37,6 +55,7 @@ export async function cmdInfo(args, flags) { try { const [kind, name] = parsePackageId(pkgId); + if (kind === 'skill') return await showSkillInfo(pkgId, flags); const installPath = getPackagePath(pkgId); if (!fs.existsSync(installPath)) { diff --git a/src/commands/list.js b/src/commands/list.js index edf34db..05d6fe9 100644 --- a/src/commands/list.js +++ b/src/commands/list.js @@ -5,16 +5,17 @@ * rudi list [kind] List all or filter by kind * rudi list skills List skills * rudi list workflows List workflows - * rudi list skills --category=coding Filter by category + * rudi list skills --category=code Filter by category * rudi list stacks --detected Show MCP servers from agent configs * rudi list --json Output as JSON */ -import { listInstalled } from '@learnrudi/core'; +import { listInstalled, matchesSkillFilters, normalizeSkillFilters } from '@learnrudi/core'; import { detectAllMcpServers, getInstalledAgents, getMcpServerSummary, AGENT_CONFIGS } from '@learnrudi/mcp'; import { cmdAgent } from './agent-host.js'; import { formatOperatorSkillLine, formatRelatedSkillsLine } from './related-skills.js'; import { printPackageLifecycle } from './package-lifecycle.js'; +import { printSkillDetails } from './skill-display.js'; function pluralizeKind(kind) { if (!kind) return 'packages'; @@ -162,12 +163,11 @@ export async function cmdList(args, flags) { try { let packages = await listInstalled(kind); + const filters = normalizeSkillFilters(flags); // Filter by category (mainly for skills/workflows) const categoryFilter = flags.category; - if (categoryFilter) { - packages = packages.filter(p => p.category === categoryFilter); - } + packages = packages.filter(pkg => matchesSkillFilters(pkg, filters)); if (flags.json) { console.log(JSON.stringify(packages, null, 2)); @@ -207,6 +207,7 @@ export async function cmdList(args, flags) { console.log(` ${pkg.description}`); } printPackageLifecycle(pkg, ' '); + printSkillDetails(pkg, ' '); if (pkg.requires && pkg.requires.stacks && pkg.requires.stacks.length > 0) { console.log(` Requires: ${pkg.requires.stacks.join(', ')}`); } @@ -217,7 +218,7 @@ export async function cmdList(args, flags) { } console.log(`\nTotal: ${packages.length} skill(s)`); - console.log(`\nFilter by category: rudi list skills --category=coding`); + console.log(`\nFilter by category: rudi list skills --category=code`); return; } @@ -248,7 +249,8 @@ export async function cmdList(args, flags) { console.log(` ${pkg.description}`); } printPackageLifecycle(pkg, ' '); - if (pkg.category) { + printSkillDetails(pkg); + if (pkg.kind !== 'skill' && pkg.category) { console.log(` Category: ${pkg.category}`); } if (pkg.tags && pkg.tags.length > 0) { diff --git a/src/commands/search.js b/src/commands/search.js index 2fbb837..eefb643 100644 --- a/src/commands/search.js +++ b/src/commands/search.js @@ -2,8 +2,9 @@ * Search command - search registry for packages */ -import { fetchIndex, searchPackages, listPackages } from '@learnrudi/core'; +import { fetchIndex, searchPackages, listPackages, normalizeSkillFilters } from '@learnrudi/core'; import { printPackageLifecycle } from './package-lifecycle.js'; +import { printSkillDetails } from './skill-display.js'; function pluralizeKind(kind) { if (!kind) return 'packages'; @@ -39,6 +40,7 @@ function printSearchGuidance(packageKinds) { export async function cmdSearch(args, flags) { const query = args[0]; + const filters = normalizeSkillFilters(flags); const refreshRegistry = flags.fresh || flags['no-cache'] || false; if (refreshRegistry) { @@ -79,21 +81,21 @@ export async function cmdSearch(args, flags) { // Show deprecation note for --prompts flag if (flags.prompts && !flags.skills) { - console.log('Note: --prompts has been renamed to --skills. Use --skills instead.\n'); + console.error('Note: --prompts has been renamed to --skills. Use --skills instead.\n'); } - console.log(`Searching for "${query}"...`); + if (!flags.json) console.log(`Searching for "${query}"...`); try { - const results = await searchPackages(query, { kind }); + const results = await searchPackages(query, { kind, ...filters }); - if (results.length === 0) { - console.log('No packages found matching your query.'); + if (flags.json) { + console.log(JSON.stringify(results, null, 2)); return; } - if (flags.json) { - console.log(JSON.stringify(results, null, 2)); + if (results.length === 0) { + console.log('No packages found matching your query.'); return; } @@ -122,6 +124,7 @@ export async function cmdSearch(args, flags) { console.log(` v${pkg.version}`); } printPackageLifecycle(pkg, ' '); + printSkillDetails(pkg); console.log(); } } @@ -155,7 +158,7 @@ async function listAllPackages(flags) { // Show deprecation note for --prompts flag if (flags.prompts && !flags.skills) { - console.log('Note: --prompts has been renamed to --skills. Use --skills instead.\n'); + console.error('Note: --prompts has been renamed to --skills. Use --skills instead.\n'); } try { @@ -164,7 +167,7 @@ async function listAllPackages(flags) { let totalCount = 0; for (const k of kinds) { - const packages = await listPackages(k); + const packages = await listPackages(k, normalizeSkillFilters(flags)); allPackages[k] = packages; totalCount += packages.length; } @@ -190,6 +193,7 @@ async function listAllPackages(flags) { console.log(` ${id}${runtime}`); console.log(` ${pkg.description || 'No description'}`); printPackageLifecycle(pkg, ' '); + printSkillDetails(pkg); } } diff --git a/src/commands/skill-display.js b/src/commands/skill-display.js new file mode 100644 index 0000000..93370e6 --- /dev/null +++ b/src/commands/skill-display.js @@ -0,0 +1,10 @@ +export function printSkillDetails(pkg, indent = ' ') { + if (pkg.kind !== 'skill') return; + if (pkg.category) console.log(`${indent}Category: ${pkg.category}`); + console.log(`${indent}Role: ${pkg.skillRole || 'unknown'}`); + if (pkg.operatorFor?.length) console.log(`${indent}Operator for: ${pkg.operatorFor.join(', ')}`); + for (const [field, label] of [['capabilities', 'Capabilities'], ['domains', 'Domains'], ['providers', 'Providers']]) { + if (pkg.facets?.[field]?.length) console.log(`${indent}${label}: ${pkg.facets[field].join(', ')}`); + } + if (pkg.conflictingPaths?.length) console.log(`${indent}Source conflict: ${pkg.conflictingPaths.join(', ')}`); +} diff --git a/src/commands/update.js b/src/commands/update.js index f56539a..8c88217 100644 --- a/src/commands/update.js +++ b/src/commands/update.js @@ -9,13 +9,15 @@ import * as path from 'path'; import { addStack, getLockfilePath, + getInstallPathForPackage, + inspectRegistrySkillUpdate, indexAllStacks, listInstalled, resolvePackage as coreResolvePackage, updatePackage as coreUpdatePackage, } from '@learnrudi/core'; import { PATHS } from '@learnrudi/env'; -import { fetchIndex } from '@learnrudi/registry-client'; +import { fetchIndex, normalizeRegistryPackage } from '@learnrudi/registry-client'; import { getManagedNativeSkillHosts as findManagedNativeSkillHosts, NATIVE_SKILL_HOSTS, @@ -676,6 +678,7 @@ async function updateOnePackage(pkg, flags, deps) { throw new Error(result?.error || `Failed to update ${pkg.id}`); } + if (result.backupPath) deps.log(` Previous skill retained at ${result.backupPath}`); if (kind === 'stack') { if (path.resolve(result.path) !== path.resolve(snapshot.targetPath)) { throw new Error(`Updated stack path changed unexpectedly for ${pkg.id}`); @@ -848,6 +851,21 @@ export async function runUpdate(args = [], flags = {}, deps = defaultDependencie ); if (dryRun) { + const skillMigrations = []; + for (const id of plannedSkillIds) { + try { + const source = refreshedRegistryIndex?.packages?.[id]; + if (!source) throw new Error(`Skill is absent from the refreshed registry: ${id}`); + const candidate = normalizeRegistryPackage(source, 'skill'); + const inspect = deps.inspectRegistrySkillUpdate || inspectRegistrySkillUpdate; + const migration = await inspect(candidate, getInstallPathForPackage(candidate)); + skillMigrations.push(migration); + deps.log(` - ${id}: would ${migration.action} ${migration.from || '(new)'} → ${migration.to}`); + } catch (error) { + failedPackages.push({ id, error: error.message }); + deps.error(` ! ${id}: ${error.message}`); + } + } deps.log(`Dry run: would update ${plannedPackages.length} package(s)`); for (const id of plannedPackages) { deps.log(` - ${id}`); @@ -865,12 +883,13 @@ export async function runUpdate(args = [], flags = {}, deps = defaultDependencie return { dryRun: true, updated: 0, - failed: skillProjection.failed, - packageFailed: 0, + failed: failedPackages.length + skillProjection.failed, + packageFailed: failedPackages.length, projectionFailed: skillProjection.failed, skipped: skippedPackages.length, packages: [], - failures: [], + failures: failedPackages, + skillMigrations, projectionFailures: skillProjection.failures, skippedPackages, indexedStacks: [], diff --git a/src/native-skills/lifecycle.js b/src/native-skills/lifecycle.js index 563cd16..cfbe491 100644 --- a/src/native-skills/lifecycle.js +++ b/src/native-skills/lifecycle.js @@ -1,4 +1,5 @@ import crypto from 'node:crypto'; +import { parseSkillDocument } from '@learnrudi/core'; import fs from 'node:fs'; import * as fsp from 'node:fs/promises'; import os from 'node:os'; @@ -47,37 +48,6 @@ function yamlString(value) { return JSON.stringify(String(value || '')); } -function parseSimpleFrontmatter(frontmatter = '') { - const metadata = {}; - for (const line of frontmatter.split('\n')) { - const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); - if (!match) continue; - let value = match[2].trim(); - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - metadata[match[1]] = value; - } - return metadata; -} - -function stripFrontmatter(content = '') { - if (!content.startsWith('---\n')) { - return { metadata: {}, body: content.trimStart() }; - } - const end = content.indexOf('\n---\n', 4); - if (end === -1) { - return { metadata: {}, body: content.trimStart() }; - } - return { - metadata: parseSimpleFrontmatter(content.slice(4, end)), - body: content.slice(end + 5).trimStart(), - }; -} - export function normalizeNativeSkillName(skill) { const raw = String(skill?.id || '').replace(/^skill:/, ''); if (!SKILL_NAME_PATTERN.test(raw)) { @@ -97,12 +67,12 @@ function defaultPrompt(skillName, description, displayName) { export function buildPortableSkillFiles(skill, sourceContent) { const skillName = normalizeNativeSkillName(skill); - const parsed = stripFrontmatter(sourceContent); + const parsed = parseSkillDocument(sourceContent); const displayName = compactText(parsed.metadata.name || skill.name || skillName, 80); - const description = compactText( + // Trigger conditions can occur at the end; UI summaries have separate limits. + const description = String( skill.description || parsed.metadata.description || `${displayName} RUDI skill`, - 320, - ); + ).replace(/\s+/g, ' ').trim(); const body = parsed.body || `Use the installed RUDI skill \`skill:${skillName}\` as the source of truth.`; const skillMd = [ @@ -120,7 +90,7 @@ export function buildPortableSkillFiles(skill, sourceContent) { export function buildCodexSkillFiles(skill, sourceContent) { const baseFiles = buildPortableSkillFiles(skill, sourceContent); const { skillName } = baseFiles; - const parsed = stripFrontmatter(sourceContent); + const parsed = parseSkillDocument(sourceContent); const displayName = humanizeSkillDisplayName(parsed.metadata.name || skill.name || skillName); const description = compactText( skill.description || parsed.metadata.description || `${displayName} RUDI skill`, @@ -288,6 +258,19 @@ async function collectResourceEntries(sourceRoot, resourceName, entries, sourceE await walk(resourceRoot, resourceName); } +async function readBundledCodexMetadata(sourceRoot) { + const metadataPath = path.join(sourceRoot, 'agents', 'openai.yaml'); + let metadataStat; + try { + metadataStat = await fsp.lstat(metadataPath); + } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } + assertRealEntry(metadataStat, metadataPath, 'file'); + return fsp.readFile(metadataPath); +} + function manifestEntry(entry) { if (entry.type === 'directory') { return { path: entry.relativePath, type: entry.type, mode: entry.mode }; @@ -321,6 +304,9 @@ function resolveSourceIdentity(source) { async function buildProjection(host, skill) { assertSupportedHost(host); const skillName = normalizeNativeSkillName(skill); + if (skill.conflictingPaths?.length) { + throw new Error(`Conflicting skill formats for ${skill.id}; reconcile canonical sources before native sync`); + } const sourcePath = path.resolve(skill.entryPath || skill.path || ''); await assertNoSymlinkPathComponents(sourcePath, 'Native skill source path'); let sourceStat; @@ -350,15 +336,7 @@ async function buildProjection(host, skill) { mode: sourceStat.mode & 0o777, content: sourceContent, }]; - if (host === 'codex') { - entries.push({ type: 'directory', relativePath: 'agents', mode: 0o755 }); - entries.push({ - type: 'file', - relativePath: path.join('agents', 'openai.yaml'), - mode: 0o644, - content: Buffer.from(generated.openaiYaml), - }); - } + let codexMetadata = host === 'codex' ? Buffer.from(generated.openaiYaml) : null; let packageDigest; if (path.basename(sourcePath) === 'SKILL.md') { const sourceRoot = path.dirname(sourcePath); @@ -367,12 +345,24 @@ async function buildProjection(host, skill) { throw new Error(`Source skill package not found: ${sourceRoot}`); } packageDigest = completePackage.digest; + if (host === 'codex') { + codexMetadata = await readBundledCodexMetadata(sourceRoot) ?? codexMetadata; + } for (const resourceName of RESOURCE_DIRECTORIES) { await collectResourceEntries(sourceRoot, resourceName, entries, sourceEntries); } } else { packageDigest = digestEntries(sourceEntries).digest; } + if (host === 'codex') { + entries.push({ type: 'directory', relativePath: 'agents', mode: 0o755 }); + entries.push({ + type: 'file', + relativePath: path.join('agents', 'openai.yaml'), + mode: 0o644, + content: codexMetadata, + }); + } const rendered = digestEntries(entries); const sourceIdentity = resolveSourceIdentity(skill.source); return {