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

142 lines (141 loc) • 6.61 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 } from "node:crypto"; import fs from "node:fs"; import { detectAdapterId } from "../../core/adapter/detect-adapter.js"; import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js"; import { loadConfig } from "../../core/config/config.js"; import { COMPOSITION_INVALID_MULTI_JOB_HINT, NotFoundError, UsageError } from "../../core/errors.js"; import { getDbPath } from "../../core/paths.js"; import { canonicalizeWorkflowName } from "../../core/recognition-util.js"; import { deriveInstallations } from "../../indexer/installations.js"; import { resolveAdapterConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js"; import { resolveSourceEntries } from "../../indexer/search/search-source.js"; import { withIndexDb } from "../../storage/repositories/index-db.js"; import { compileWorkflowSource } from "../compile.js"; /** * Build the canonical `workflow_runs.workflow_ref` for an AKM workflow asset. */ export function canonicalWorkflowRunRef(bundle, canonicalName) { return makeBundleRef(bundle, `workflows/${canonicalName}`); } /** Parse a workflow ref using the canonical `[bundle//]conceptId` grammar. */ export function parseWorkflowRefInput(ref) { const parsed = parseBundleRef(ref.trim()); if (parsed.fragment !== undefined) { throw new UsageError(`Export fragment "#${parsed.fragment}" is not accepted in a workflow ref.`, "TARGET_REF_INVALID"); } if (parsed.conceptId.startsWith("workflow:")) { throw new UsageError(`Invalid workflow ref "${ref.trim()}". Use [bundle//]conceptId, such as workflows/release.`, "TARGET_REF_INVALID"); } return parsed; } /** Resolve input sugar to the workflow's canonical run identity. */ export async function canonicalizeWorkflowRefInput(ref) { return (await loadWorkflowAsset(ref)).ref; } /** * Resolve a workflow ref and compile its authored bytes into a plan. */ export async function loadWorkflowAsset(ref) { const bundleRef = parseWorkflowRefInput(ref); const config = loadConfig(); const allSources = resolveSourceEntries(undefined, config); const installations = deriveInstallations(allSources); const searchSources = allSources.flatMap((source, index) => { const bundleId = installations[index]?.id; if (!bundleId || (bundleRef.bundle !== undefined && bundleRef.bundle !== bundleId)) return []; return [{ source, bundleId }]; }); if (bundleRef.bundle && searchSources.length === 0) { throw new UsageError(`Bundle "${bundleRef.bundle}" was not found among configured sources.`, "WORKFLOW_SOURCE_INVALID"); } let assetPath; let sourcePath; let sourceBundleId; let workflowName; let workflowAdapterId; let rejectedSource; for (const candidateSource of searchSources) { const { source, bundleId } = candidateSource; const adapterId = source.adapterId ?? detectAdapterId(source.path); const ownedSource = source.adapterId ? source : { ...source, adapterId }; const owner = resolveAdapterConceptOwner(source.path, adapterId, bundleRef.conceptId); if (!owner) continue; if (!ownsNativeWorkflowRuntime(ownedSource) || !owner.workflowSource) { rejectedSource ??= { source: ownedSource, bundleId }; break; } assetPath = owner.path; sourcePath = source.path; sourceBundleId = bundleId; workflowName = owner.workflowSource.canonicalName; workflowAdapterId = adapterId; break; } if (!assetPath) { if (rejectedSource) { const sourceName = rejectedSource.bundleId; const adapterId = rejectedSource.source.adapterId ?? "unassigned"; throw new UsageError(`Bundle "${sourceName}" uses adapter "${adapterId}", which does not support native workflow execution.`, "WORKFLOW_SOURCE_INVALID"); } throw new NotFoundError(`Workflow not found for ref: ${ref}`); } const resolvedSourcePath = sourcePath; // Canonicalize the stored ref: `workflows/foo.md` and `workflows/foo` resolve // to the same file, so they MUST share one run identity. const canonicalName = canonicalizeWorkflowName(workflowName); const fullRef = workflowAdapterId === "akm-workflow" ? makeBundleRef(sourceBundleId, canonicalName) : canonicalWorkflowRunRef(sourceBundleId, canonicalName); const title = canonicalName.split("/").pop() || canonicalName; const bytes = fs.readFileSync(assetPath); const result = compileWorkflowSource(bytes.toString("utf8"), { path: assetPath, workspaceRoot: resolvedSourcePath, title, }); if (!result.ok) { const details = result.errors.map((error) => ` ${error.path}:${error.line} — ${error.message}`).join("\n"); const isMultiJob = result.errors.length === 1 && result.errors[0]?.code === "multi-job-unsupported"; throw new UsageError(`Workflow source has ${result.errors.length} error(s):\n${details}`, isMultiJob ? "COMPOSITION_INVALID" : "WORKFLOW_SOURCE_INVALID", isMultiJob ? COMPOSITION_INVALID_MULTI_JOB_HINT : undefined); } return { ref: fullRef, path: assetPath, sourcePath: resolvedSourcePath, adapterId: workflowAdapterId, title, plan: result.plan, sourceHash: createHash("sha256").update(bytes).digest("hex"), }; } function ownsNativeWorkflowRuntime(source) { return source.adapterId === "akm" || source.adapterId === "akm-workflow"; } /** * Resolve the `entries.id` for an indexed workflow, or null when the index * database does not yet exist or has no matching entry. */ export function resolveWorkflowEntryId(_sourcePath, ref, adapterId) { if (!fs.existsSync(getDbPath())) return null; const parsed = parseBundleRef(ref); if (!parsed.bundle) throw new UsageError(`Expected a bundle-qualified workflow ref, got "${ref}".`); const itemRef = makeBundleRef(parsed.bundle, parsed.conceptId); return withIndexDb((db) => { const row = db .prepare(`SELECT id FROM entries WHERE type = 'workflow' AND item_ref = ? ${adapterId ? "AND adapter_id = ?" : ""} LIMIT 1`) .get(itemRef, ...(adapterId ? [adapterId] : [])); return row?.id ?? null; }); }