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
JavaScript
// 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 };
}