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

350 lines (349 loc) • 15.8 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/. /** The refs an improve run may consider, read from the index, and the small predicates over them. */ import fs from "node:fs"; import path from "node:path"; import { parseBundleRef } from "../../core/asset/asset-ref.js"; import { parseFrontmatter } from "../../core/asset/frontmatter.js"; import { conceptIdFromTypeName, parseRefInput, resolveRef, typeNameFromConceptId } from "../../core/asset/resolve-ref.js"; import { loadConfig } from "../../core/config/config.js"; import { ConfigError, NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js"; import { isPathAbsent } from "../../core/path-access.js"; import { getDbPath } from "../../core/paths.js"; import { deriveInstallations, deriveWritableBundleIds } from "../../indexer/installations.js"; import { resolveSourceEntries } from "../../indexer/search/search-source.js"; import { resolveAssetPath } from "../../indexer/walk/path-resolver.js"; import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js"; import { getAllEntries } from "../../storage/repositories/index-entries-repository.js"; import { hasCurrentEntriesTable } from "../../storage/repositories/index-entry-schema.js"; import { getUtilityScoresByIds } from "../../storage/repositories/index-utility-repository.js"; import { SqliteReadSnapshotUnavailableError } from "../../storage/sqlite-read-snapshot.js"; import { isDistillRefusedInputType } from "./distill.js"; import { isStrategyFilteredForAllPasses } from "./improve-strategies.js"; import { parseMemoryRef } from "./memory/derived-ref.js"; /** The asset type of a short or bundle-qualified conceptId (`""` when unknown). */ export function assetTypeOf(ref) { return typeNameFromConceptId(ref.includes("//") ? ref.slice(ref.indexOf("//") + 2) : ref)?.type ?? ""; } /** * Run `fn` against index.db — the live index, or a non-mutating snapshot for * a dry run. `undefined` when there is none or the read fails (best-effort). */ export function withIndexDb(readOnly, fn) { let db; try { db = readOnly ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : openExistingDatabase(); return db ? fn(db) : undefined; } catch (err) { rethrowIfTestIsolationError(err); return undefined; } finally { if (db) closeDatabase(db); } } /** * The index for candidate selection, or `undefined` when it is absent. An * index without a readable `entries` table needs a rebuild and says so. */ function openEligibilityDb(readOnly) { const db = readOnly ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : isPathAbsent(getDbPath()) ? undefined : openExistingDatabase(); if (db && !hasCurrentEntriesTable(db)) { closeDatabase(db); throw new ConfigError("index.db has no entries table this akm can read, so nothing can be selected for improvement.", "INDEX_SCHEMA_INCOMPATIBLE", "Run `akm index` to rebuild the derived index from the currently materialized sources."); } return db; } /** * How a dry run describes an index it could not read. Only the typed * failures map to an empty plan; any other error still throws. */ function unreadableSnapshot(readOnly, error) { if (!readOnly) return undefined; if (error instanceof SqliteReadSnapshotUnavailableError) { return { status: "incompatible", reason: `index.db cannot provide a stable non-mutating snapshot (${error.message}); dry-run uses an empty snapshot`, }; } if (error instanceof ConfigError && error.code === "INDEX_SCHEMA_INCOMPATIBLE") { return { status: "incompatible", reason: `index.db is incompatible; ${error.hint() ?? error.message} Dry-run uses an empty snapshot and does not migrate it.`, }; } return undefined; } export function resolveImproveScope(scope) { const trimmed = scope?.trim(); if (!trimmed) return { mode: "all" }; try { parseRefInput(trimmed); return { mode: "ref", value: trimmed }; } catch (err) { // A bare word is a `--scope <type>` filter (an unknown type matches // nothing); a ref-shaped value that fails to parse is a real error. if (!/[/:#]/.test(trimmed)) return { mode: "type", value: trimmed }; const message = err instanceof Error ? err.message : String(err); throw new UsageError(`Invalid --scope "${trimmed}": ${message}`, "INVALID_FLAG_VALUE"); } } /** Dedupe by durable identity (item_ref, else ref), keeping the first. */ export function dedupeRefs(refs) { const seen = new Set(); return refs.filter((r) => { const key = r.itemRef ?? r.ref; if (seen.has(key)) return false; seen.add(key); return true; }); } /** * The bundle an improve run reads candidates from and files proposals into: its * write target, the source rooted at `stashDir` (else the working bundle). * Reflect reads a candidate from its owning bundle but the proposal lands in * the write target, so a candidate owned by any other bundle would be read from * one bundle and filed in another (#1000). */ function runBundleIdOf(sources, installations, stashDir) { const root = stashDir === undefined ? undefined : path.resolve(stashDir); const index = sources.findIndex((source) => root === undefined ? source.isDefault === true : path.resolve(source.path) === root); return installations[index]?.id; } export async function collectEligibleRefs(scope, stashDir, improveProfile, config) { return collectEligibleRefsFromIndex(scope, stashDir, improveProfile, false, config); } /** Dry-run selector: query an existing index without creating or mutating it. */ export async function collectEligibleRefsReadOnly(scope, stashDir, improveProfile, config) { return collectEligibleRefsFromIndex(scope, stashDir, improveProfile, true, config); } async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, readOnly, configOverride) { const empty = (indexSnapshot) => ({ plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [], ...(indexSnapshot ? { indexSnapshot } : {}), }); const config = configOverride ?? loadConfig(); let sources; try { sources = resolveSourceEntries(stashDir, config); } catch { return empty(); } if (sources.length === 0) return empty(); const installations = deriveInstallations(sources); const writableBundleIds = deriveWritableBundleIds(sources); const runBundleId = runBundleIdOf(sources, installations, stashDir); // Candidates come from the write target alone, and only while it is writable. const isCandidateBundle = (bundleId) => bundleId !== undefined && bundleId === runBundleId && writableBundleIds.has(bundleId); const ready = { status: "ready", reason: readOnly ? "loaded a non-mutating point-in-time copy of the existing index" : "loaded the prepared index", }; const missing = { status: "missing", reason: readOnly ? "index.db is missing; dry-run uses an empty snapshot and does not create it" : "index.db is missing after index preparation; the selector uses an empty snapshot", }; let db; try { db = openEligibilityDb(readOnly); if (!db) return empty(missing); if (scope.mode === "ref" && scope.value) { const entriesByItemRef = new Map(getAllEntries(db).map((entry) => [entry.itemRef, entry])); let resolved; try { resolved = resolveRef(scope.value, { defaultBundle: config.defaultBundle, bundles: installations.map((installation) => ({ id: installation.id, hasConcept: (conceptId) => entriesByItemRef.has(`${installation.id}//${conceptId}`), })), ...(runBundleId ? { only: runBundleId } : {}), }); } catch (error) { if (!(error instanceof NotFoundError) || !runBundleId) throw error; // Name the bundle that owns the ref; a typo or an unindexed ref keeps the default advice. const { conceptId } = parseBundleRef(scope.value); const owner = installations.find((installation) => installation.id !== runBundleId && entriesByItemRef.has(`${installation.id}//${conceptId}`))?.id; if (!owner) throw error; throw new NotFoundError(error.message, error.code, `"${conceptId}" is in bundle "${owner}"; this run improves bundle "${runBundleId}" only. Run \`akm improve ${owner}//${conceptId}\` (or pass \`--bundle ${owner}\`).`); } const indexed = entriesByItemRef.get(`${resolved.bundle}//${resolved.conceptId}`); if (!indexed?.bundleId || !indexed.conceptId || !fs.existsSync(indexed.filePath)) { if (await findAssetFilePath(scope.value, stashDir)) return empty(ready); throw new NotFoundError(`Asset not found in the selected writable source: ${scope.value}`, "ASSET_NOT_FOUND"); } if (!isCandidateBundle(indexed.bundleId)) return empty(ready); const isMemory = indexed.entry.type === "memory"; return { plannedRefs: [ { ref: indexed.conceptId, itemRef: indexed.itemRef, reason: "scope-ref", filePath: indexed.filePath }, ], memorySummary: { eligible: isMemory ? 1 : 0, derived: isMemory && indexed.entry.name.endsWith(".derived") ? 1 : 0, }, strategyFilteredRefs: [], indexSnapshot: ready, }; } const entries = getAllEntries(db, scope.mode === "type" ? scope.value : undefined).filter((indexed) => isCandidateBundle(indexed.bundleId)); const planned = new Map(); const strategyFiltered = new Map(); let memoryEligible = 0; let memoryDerived = 0; for (const indexed of entries) { const ref = conceptIdFromTypeName(indexed.entry.type, indexed.entry.name); try { parseRefInput(ref); } catch (error) { if (error instanceof UsageError || error instanceof NotFoundError) continue; throw error; } const isDerived = indexed.entry.name.endsWith(".derived"); // `.derived` memories skip reflect; cleanup inspects them on its own. if (!isDerived && !planned.has(ref) && !strategyFiltered.has(ref)) { const candidate = { ref, filePath: indexed.filePath, itemRef: indexed.itemRef }; if (improveProfile && isStrategyFilteredForAllPasses(ref, improveProfile)) { strategyFiltered.set(ref, { ...candidate, reason: "strategy_filtered_all_passes" }); } else { const reason = scope.mode !== "type" && indexed.entry.type === "memory" ? "memory-cleanup" : "scope-type"; planned.set(ref, { ...candidate, reason }); } } if (indexed.entry.type === "memory") { memoryEligible += 1; if (isDerived) memoryDerived += 1; } } return { plannedRefs: [...planned.values()], memorySummary: { eligible: memoryEligible, derived: memoryDerived }, strategyFilteredRefs: [...strategyFiltered.values()], indexSnapshot: ready, }; } catch (error) { rethrowIfTestIsolationError(error); const snapshot = unreadableSnapshot(readOnly, error); if (snapshot) return empty(snapshot); throw error; } finally { if (db) closeDatabase(db); } } /** The parent memory a `--scope <memory ref>` cleanup is restricted to. */ export function memoryCleanupParentRef(scope, stashDir) { if (scope.mode !== "ref" || !scope.value) return undefined; const parsed = parseRefInput(scope.value); if (parsed.type !== "memory") return undefined; if (!parsed.name.endsWith(".derived")) return conceptIdFromTypeName(parsed.type, parsed.name); for (const source of resolveSourceEntries(stashDir)) { const candidate = path.join(source.path, "memories", `${parsed.name}.md`); if (!fs.existsSync(candidate)) continue; const fm = parseFrontmatter(fs.readFileSync(candidate, "utf8")).data; const parent = parseMemoryRef(typeof fm.source === "string" ? fm.source : undefined); if (parent) return parent; } return conceptIdFromTypeName("memory", parsed.name.slice(0, -".derived".length)); } export function isLessonCandidate(ref) { return parseRefInput(ref).type === "lesson"; } /** Should this ref enter the distill queue? Types distill refuses never do. */ export function isDistillCandidateRef(ref, stashDir) { if (isDistillRefusedInputType(parseRefInput(ref).type)) return false; return shouldDistillMemoryRef(ref, stashDir); } /** A non-derived memory that is not marked `quality: proposed`. */ export function shouldDistillMemoryRef(ref, stashDir) { const parsed = parseRefInput(ref); if (parsed.type !== "memory") return false; for (const source of resolveSourceEntries(stashDir)) { const candidate = `${source.path}/memories/${parsed.name}.md`; if (!fs.existsSync(candidate)) continue; if (parseFrontmatter(fs.readFileSync(candidate, "utf8")).data.quality === "proposed") return false; break; } return !parsed.name.endsWith(".derived"); } export function shouldAnalyzeMemoryCleanup(scope, eligibleMemories, primaryStashDir) { if (!primaryStashDir || eligibleMemories === 0) return false; if (scope.mode === "all") return true; if (scope.mode === "type") return scope.value === "memory"; return scope.value ? parseRefInput(scope.value).type === "memory" : false; } export function buildUtilityMap(refs, readOnly = false) { const map = new Map(); if (refs.length === 0) return map; const refSet = new Set(refs.map((r) => r.ref)); withIndexDb(readOnly, (db) => { const idToRef = new Map(); for (const indexed of getAllEntries(db)) { const ref = conceptIdFromTypeName(indexed.entry.type, indexed.entry.name); if (refSet.has(ref)) idToRef.set(indexed.id, ref); } if (idToRef.size === 0) return; for (const [id, score] of getUtilityScoresByIds(db, [...idToRef.keys()])) { const ref = idToRef.get(id); if (ref) map.set(ref, score.utility); } }); return map; } export async function findAssetFilePath(ref, stashDir, writableBundleIds) { return resolveAssetPath(ref, { stashDir, mode: "disk-only", writableBundleIds, directoryIndexNames: ["SKILL.md"], preserveDirectNameFallback: true, honorOrigin: true, }); }