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

182 lines (181 loc) • 8.15 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/. const GRAMMAR_HINT = "allowed forms: params.<name>, steps.<id>.output(.<ident>|[<int>])*"; /** * Parse a bare reference string (a whole-value frontmatter field, already * delimiter-free: `map.over`, `route.input`, or one entry of `inputs[]`). */ export function parseReference(source) { const text = source.trim(); if (text === "") return { ok: false, message: `Empty reference; ${GRAMMAR_HINT}.` }; const root = readIdent(text, 0); if (!root) { return { ok: false, message: `Invalid reference "${text}" — must start with an identifier; ${GRAMMAR_HINT}.` }; } switch (root.name) { case "params": { if (text[root.end] !== ".") { return { ok: false, message: `"params" requires a name: params.<name> (got "${text}").` }; } const name = readIdent(text, root.end + 1); if (!name) { return { ok: false, message: `Invalid param name after "params." in "${text}".` }; } if (name.end !== text.length) { return { ok: false, message: `"params.${name.name}" takes exactly one name — found trailing "${text.slice(name.end)}".`, }; } return { ok: true, expr: { kind: "param", name: name.name } }; } case "steps": { if (text[root.end] !== ".") { return { ok: false, message: `"steps" requires a step id: steps.<id>.output (got "${text}").` }; } const stepId = readIdent(text, root.end + 1); if (!stepId) { return { ok: false, message: `Invalid step id after "steps." in "${text}".` }; } if (text[stepId.end] !== ".") { return { ok: false, message: `"steps.${stepId.name}" must be followed by ".output" (got "${text}").` }; } const output = readIdent(text, stepId.end + 1); if (!output || output.name !== "output") { return { ok: false, message: `Expected ".output" after "steps.${stepId.name}" in "${text}".` }; } const path = parsePath(text, output.end); if (!path.ok) return { ok: false, message: path.message }; return { ok: true, expr: { kind: "stepOutput", stepId: stepId.name, path: path.path } }; } default: return { ok: false, message: `Unknown root "${root.name}" in "${text}"; ${GRAMMAR_HINT}.` }; } } /** `<ident>` is `[A-Za-z_][A-Za-z0-9_-]*`. Returns null when no ident starts at `start`. */ function readIdent(text, start) { if (start >= text.length || !/[A-Za-z_]/.test(text[start])) return null; let end = start + 1; while (end < text.length && /[A-Za-z0-9_-]/.test(text[end])) end++; return { name: text.slice(start, end), end }; } /** Parse `( .<ident> | [<non-negative int>] )*` from `start` to end of text. */ function parsePath(text, start) { const path = []; let i = start; while (i < text.length) { const char = text[i]; if (char === ".") { const ident = readIdent(text, i + 1); if (!ident) { return { ok: false, message: `Invalid path segment after "." at position ${i} in "${text}".` }; } path.push(ident.name); i = ident.end; } else if (char === "[") { let j = i + 1; while (j < text.length && /[0-9]/.test(text[j])) j++; if (j === i + 1 || text[j] !== "]") { return { ok: false, message: `Invalid indexer at position ${i} in "${text}" — expected [<non-negative integer>].`, }; } path.push(Number.parseInt(text.slice(i + 1, j), 10)); i = j + 1; } else { return { ok: false, message: `Unexpected character "${char}" at position ${i} in "${text}".` }; } } return { ok: true, path }; } // ── Resolution ─────────────────────────────────────────────────────────────── /** * Resolve a single reference to its RAW value for whole-value contexts * (`map.over`, `route.input`, `inputs[]`): arrays stay arrays, objects stay * objects. null/undefined values and missing paths are errors. */ export function resolveReference(expr, scope) { const reference = formatReference(expr); const fail = (message) => ({ ok: false, error: { reference, message } }); switch (expr.kind) { case "param": { if (!Object.hasOwn(scope.params, expr.name)) { return fail(`${reference} is not defined in the run's params.`); } return finish(scope.params[expr.name], reference, fail); } case "stepOutput": { if (!Object.hasOwn(scope.stepOutputs, expr.stepId)) { return fail(`steps.${expr.stepId}.output is not available — step "${expr.stepId}" has no recorded output.`); } let current = scope.stepOutputs[expr.stepId]; let walked = `steps.${expr.stepId}.output`; for (const segment of expr.path) { if (typeof segment === "number") { if (!Array.isArray(current)) { return fail(`${walked} is not an array — cannot resolve index [${segment}].`); } if (segment >= current.length) { return fail(`${walked}[${segment}] is out of bounds (array length ${current.length}).`); } current = current[segment]; walked += `[${segment}]`; } else { if (typeof current !== "object" || current === null || Array.isArray(current)) { return fail(`${walked} is not an object — cannot resolve property "${segment}".`); } if (!Object.hasOwn(current, segment)) { return fail(`${walked}.${segment} is missing (no such property, resolving ${reference}).`); } current = current[segment]; walked += `.${segment}`; } } return finish(current, reference, fail); } } } /** * Resolve a bare reference STRING directly (parse + resolve in one call) — * the shape every whole-value frontmatter position (`map.over`, `route.input`, * one entry of `inputs[]`) actually needs. */ export function resolveReferenceString(text, scope) { const parsed = parseReference(text); if (!parsed.ok) { return { ok: false, error: { reference: text, message: parsed.message } }; } return resolveReference(parsed.expr, scope); } function finish(value, reference, fail) { if (value === undefined) return fail(`${reference} resolved to undefined.`); if (value === null) return fail(`${reference} resolved to null.`); return { ok: true, value }; } // ── Introspection helpers ──────────────────────────────────────────────────── /** Canonical source spelling of a reference, e.g. `steps.review.output.files[2].name`. */ export function formatReference(expr) { switch (expr.kind) { case "param": return `params.${expr.name}`; case "stepOutput": { let text = `steps.${expr.stepId}.output`; for (const segment of expr.path) { text += typeof segment === "number" ? `[${segment}]` : `.${segment}`; } return text; } } }