UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

137 lines (136 loc) • 6.59 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. import path from "node:path"; import { UsageError } from "../errors.js"; // ── Validation ────────────────────────────────────────────────────────────── function validateName(name) { if (!name) throw new UsageError("Empty asset name.", "MISSING_REQUIRED_ARGUMENT"); if (name.includes("\0")) throw new UsageError("Null byte in asset name.", "MISSING_REQUIRED_ARGUMENT"); if (/^[A-Za-z]:/.test(name)) throw new UsageError("Windows drive path in asset name.", "MISSING_REQUIRED_ARGUMENT"); const slashName = name.replace(/\\/g, "/"); if (slashName === ".." || slashName.startsWith("../")) { throw new UsageError("Path traversal in asset name.", "MISSING_REQUIRED_ARGUMENT"); } if (slashName.split("/").some((seg) => seg === "." || seg === "..")) { throw new UsageError("Asset name cannot contain relative path segments.", "MISSING_REQUIRED_ARGUMENT"); } const normalized = path.posix.normalize(slashName); if (path.posix.isAbsolute(normalized)) throw new UsageError("Absolute path in asset name.", "MISSING_REQUIRED_ARGUMENT"); if (normalized === ".." || normalized.startsWith("../")) { throw new UsageError("Path traversal in asset name.", "MISSING_REQUIRED_ARGUMENT"); } } function normalizeName(name) { return path.posix.normalize(name.replace(/\\/g, "/")); } /** * Bundle slug charset (spec §3.4): non-empty, and excluding `:`/`.`/`#`/`/` * plus whitespace. Excluding `:`/`.` is what keeps `bundle//conceptId` lexically * distinguishable from a URL in prose; excluding `/` is what makes the first * `//` an unambiguous bundle boundary; excluding `#` reserves it for fragments. */ const BUNDLE_SLUG_RE = /^[^\s:.#/]+$/; /** * True when `s` is a legal bundle slug (spec §11.1 / D-R5 charset: non-empty, * no `:`/`.`/`#`/`/` or whitespace). Exported for input boundaries that need * to distinguish bundle-qualified refs from source locators. */ export function isBundleSlug(s) { return BUNDLE_SLUG_RE.test(s); } /** * Body-ref recognition (prose): the FULLY-QUALIFIED anchored form ONLY * (`<bundle>//<concept-id>[#<fragment>]`, spec §11.1 — the short form is never * recognized in prose). `g`/`m` for scanning; group 1 = the whole ref token. * * The leading `//` must be preceded by a bundle slug, so a URL (`https://…`, * `//cdn.example.com/…`) never matches: a scheme carries a `:` before `//` * (excluded from the slug), and a scheme-relative `//host` has no slug before * its `//` at all. The bundle-slug charset here additionally excludes the prose * boundary punctuation (brackets/parens/quotes/backtick/comma/angle) so a * leading boundary char (e.g. the `[` of a markdown link) is not absorbed into * the slug. The concept segment reuses the same terminator charset as the * body-ref scan (whitespace/quotes/brackets/comma/nl), and admits `/`, * `.`, and a trailing `#fragment`. */ export const BUNDLE_REF_RE = /(?:^|[\s`"'(,[])([^\s:.#/`"'()[\],<>]+\/\/[^\s"'`)\]>,\n]+)/gm; /** NFC-normalize + traversal/null-byte/drive-letter guard + path-normalize a concept id. */ function normalizeConceptId(raw) { const nfc = raw.normalize("NFC"); if (nfc.includes("#")) { throw new UsageError("`#` is reserved for the export fragment in a concept id.", "MISSING_REQUIRED_ARGUMENT"); } validateName(nfc); return normalizeName(nfc); } /** * Build a fully-qualified (or short) bundle ref string from its components. * * Examples: * makeBundleRef(undefined, "knowledge/http-caching") → "knowledge/http-caching" * makeBundleRef("core", "knowledge/http-caching") → "core//knowledge/http-caching" * makeBundleRef("core", "skills/review", "usage") → "core//skills/review#usage" */ export function makeBundleRef(bundle, conceptId, fragment) { const normalized = normalizeConceptId(conceptId); let out = normalized; if (bundle) { if (!BUNDLE_SLUG_RE.test(bundle)) { throw new UsageError(`Invalid bundle slug "${bundle}". A bundle slug may not contain ':', '.', '#', '/', or whitespace.`, "MISSING_REQUIRED_ARGUMENT"); } out = `${bundle}//${normalized}`; } if (fragment) out = `${out}#${fragment}`; return out; } /** * Serialize a parsed {@link BundleRef} back to its canonical * `[bundle//]conceptId[#fragment]` string form — the single formatter for the * 0.9.0 grammar (call this instead of hand-building `${bundle}//${conceptId}`). * `bundleRefToString(parseBundleRef(s))` round-trips for any `s` that * `parseBundleRef` accepts. */ export function bundleRefToString(ref) { return makeBundleRef(ref.bundle, ref.conceptId, ref.fragment); } /** * Parse a ref string in the 0.9.0 format `[<bundle>//]<concept-id>[#<fragment>]`. * The bundle prefix and the export fragment are both optional; the short form * (no `bundle//`) leaves `bundle` undefined for the caller to resolve against * the containing bundle (§11.1). */ export function parseBundleRef(ref) { const trimmed = ref.trim(); if (!trimmed) throw new UsageError("Empty ref.", "MISSING_REQUIRED_ARGUMENT"); let bundle; let body = trimmed; const boundary = trimmed.indexOf("//"); if (boundary >= 0) { bundle = trimmed.slice(0, boundary); body = trimmed.slice(boundary + 2); if (!bundle) throw new UsageError("Empty bundle in ref.", "MISSING_REQUIRED_ARGUMENT"); if (!BUNDLE_SLUG_RE.test(bundle)) { throw new UsageError(`Invalid bundle slug "${bundle}" in ref "${trimmed}". A bundle slug may not contain ':', '.', '#', '/', or whitespace.`, "MISSING_REQUIRED_ARGUMENT"); } } // Export fragment: everything after the first `#` in the concept body. let fragment; const hash = body.indexOf("#"); if (hash >= 0) { fragment = body.slice(hash + 1) || undefined; body = body.slice(0, hash); } if (!body) { throw new UsageError(`Invalid ref "${trimmed}". Expected [bundle//]conceptId, e.g. knowledge/guide or core//skills/review`, "MISSING_REQUIRED_ARGUMENT"); } const conceptId = normalizeConceptId(body); return { bundle: bundle || undefined, conceptId, fragment }; }