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

197 lines (196 loc) • 8.46 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 { createHash, randomUUID } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { getAdapters } from "../../core/adapter/registry.js"; import { stashDirNames } from "../../core/asset/asset-placement.js"; import { fetchRegistry } from "../../registry/network.js"; import { writeResponseToFileCapped } from "../../runtime.js"; import { copyIncludedPaths, findNearestIncludeConfig } from "../include.js"; const REGISTRY_STASH_DIR_NAMES = new Set(stashDirNames()); /** Strip terminal control characters from untrusted strings. */ export function sanitizeString(value, maxLength = 255) { if (typeof value !== "string") return ""; // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — strip control chars from untrusted remote data return value.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, maxLength); } /** Check whether a cached timestamp has exceeded its TTL. */ export function isExpired(mtimeMs, ttlMs) { return Date.now() - mtimeMs > ttlMs; } /** * True when ANY built-in adapter's install-time `looksLikeRoot` probe claims * `root` — the spec §1.2 ordered probe reduced to "is this the root of a bundle * of some format family?". A probe that throws (unreadable dir, race) does not * claim the root. */ function looksLikeAnyBundleRoot(root) { for (const adapter of getAdapters()) { try { if (adapter.looksLikeRoot?.(root) === true) return true; } catch { // a throwing probe does not claim the root — try the next adapter } } return false; } /** * Find the directory inside `extractedDir` that should be treated as the * bundle/stash root. Probes the top-level dir via the ordered §1.2 registry * probe; if nothing claims it, BFS for the shallowest nested candidate. */ export function detectStashRoot(extractedDir) { const root = path.resolve(extractedDir); // WI-3.1 → wired to the static registry: the top-level root check now routes // through the ordered §1.2 `looksLikeRoot` probe over ALL built-in adapters // (`registry.getAdapters()`), not the single hardcoded `akm` probe. This is // BEHAVIOR-IDENTICAL for akm roots — `akm.looksLikeRoot` reproduces the former // `.stash`-marker + immediate-stash-subdir (`hasStashDirs`) checks VERBATIM // (akm-adapter.looksLikeRoot) and `akm` is in the probe set — so those two // local checks are now subsumed and were removed. It additionally recognizes // NON-akm bundle roots (okf root index / llm-wiki / `.claude` / …) at the top // level, which the single-adapter probe missed but the old final `return root` // fallback also happened to yield. The shallowest-BFS nested-layout discovery // is unchanged. if (looksLikeAnyBundleRoot(root)) { return root; } const shallowest = findShallowestStashRoot(root); if (shallowest) return shallowest; return root; } /** A collision-resistant slug used to isolate cache dirs that lack a stable version. */ function uniqueSlug() { return randomUUID(); } /** * Build a per-source cache directory under `cacheRootDir`. * * Versioned sources get `${source}-${id}/${version}` for cache reuse; * `local` sources get a unique slug so each install is isolated. */ export function buildInstallCacheDir(cacheRootDir, source, id, version) { const safeId = id.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, ""); const identitySuffix = version === "writable" ? `-${createHash("sha256").update(`${source}:${id}`).digest("hex").slice(0, 12)}` : ""; const slug = `${source}-${safeId}${identitySuffix}`; const versionSlug = source === "local" ? uniqueSlug() : (version?.replace(/[^a-zA-Z0-9_.-]+/g, "-") ?? uniqueSlug()); return path.join(cacheRootDir, slug || source, versionSlug); } /** * Apply an `akm.include` config (a `package.json` `akm.include` array — NOT * a `.akm-include` dotfile) if any, by copying the selected paths into a * sibling `selected/` directory and returning that path. Returns undefined * when no include config is found. */ export function applyAkmIncludeConfig(sourceRoot, cacheDir, searchRoot = sourceRoot) { const includeConfig = findNearestIncludeConfig(sourceRoot, searchRoot); if (!includeConfig) return undefined; const selectedDir = path.join(cacheDir, "selected"); fs.rmSync(selectedDir, { recursive: true, force: true }); fs.mkdirSync(selectedDir, { recursive: true }); copyIncludedPaths(includeConfig.include, includeConfig.baseDir, selectedDir); return selectedDir; } /** Stream a remote archive to disk via the runtime boundary's response writer. */ export async function downloadArchive(url, destination) { const response = await fetchRegistry(url, { timeoutMs: 120_000 }); // Stream to disk with an explicit byte cap + body-read deadline so an // unbounded or dribbling body cannot fill the disk or hang the install. await writeResponseToFileCapped(destination, response); } /** SHA-256 of a file, returned as `sha256:<hex>`. */ export async function computeFileHash(filePath) { const data = fs.readFileSync(filePath); const hash = createHash("sha256").update(data).digest("hex"); return `sha256:${hash}`; } /** Recursively copy directory contents, excluding `.git`. */ export function copyDirectoryContents(sourceDir, destinationDir) { for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { if (entry.name === ".git") continue; const src = path.join(sourceDir, entry.name); const dest = path.join(destinationDir, entry.name); copyPathWithoutSymlinks(src, dest); } } function copyPathWithoutSymlinks(sourcePath, destinationPath) { const stat = fs.lstatSync(sourcePath); if (stat.isSymbolicLink()) return; fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); if (stat.isDirectory()) { fs.mkdirSync(destinationPath, { recursive: true }); copyDirectoryContents(sourcePath, destinationPath); return; } fs.copyFileSync(sourcePath, destinationPath); } export function isDirectory(target) { try { return fs.statSync(target).isDirectory(); } catch { return false; } } function countStashDirs(dirPath) { if (!isDirectory(dirPath)) return 0; const entries = fs.readdirSync(dirPath, { withFileTypes: true }); return entries.filter((entry) => entry.isDirectory() && REGISTRY_STASH_DIR_NAMES.has(entry.name)).length; } /** * BFS to find the shallowest directory that looks like a stash root. * Checks for both `.stash` directories and well-known type directories * (scripts/, skills/, etc.), so nested layouts like `project/my-stash/scripts/` * are discovered even without a `.stash` marker. * * Skips `root` itself since the caller already checked it via `hasStashDirs`. */ const BFS_MAX_DEPTH = 5; function findShallowestStashRoot(root) { const queue = [{ dir: root, depth: 0 }]; while (queue.length > 0) { const item = queue.shift(); if (!item) { continue; } const { dir: current, depth } = item; if (current !== root) { // .stash directory is a strong stash marker if (isDirectory(path.join(current, ".stash"))) { return current; } // Require 2+ type dirs for BFS candidates to avoid false positives. // A single "scripts/" is too common (skill dirs, npm packages, etc.). if (countStashDirs(current) >= 2) { return current; } } if (depth >= BFS_MAX_DEPTH) continue; let children; try { children = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; } for (const child of children) { if (!child.isDirectory()) continue; if (child.name === ".git" || child.name === "node_modules") continue; queue.push({ dir: path.join(current, child.name), depth: depth + 1 }); } } return undefined; }