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

163 lines (162 loc) • 7.37 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 host's scheduling choice. * * `scheduler.enabled` in config.json is the list of fully-qualified refs * (`bundle//tasks/x`, `bundle//workflows/y`) this host installs in its native * scheduler. Installing a bundle never schedules anything: a task only gets a * native row once its ref is on this list (`akm task enable`, `akm setup`), * and only bundles that are active on this host are eligible. * * A config that predates the list (`scheduler.enabled` absent) is read as * "keep what is installed": `akm task sync` takes the akm-written rows already * in the native scheduler as the host's choice and writes the list once. A * 0.9.17-alpha `{kind, ref, sourceId}` entry is read as its `ref`. */ import fs from "node:fs"; import path from "node:path"; import { assetPathForName, stashDirFor } from "../core/asset/asset-placement.js"; import { bundleRefToString, makeBundleRef, parseBundleRef } from "../core/asset/asset-ref.js"; import { typeNameFromConceptId } from "../core/asset/resolve-ref.js"; import { mutateConfig, schedulerSourceIdFor } from "../core/config/config.js"; import { bundleComponentConfig, bundleContentRoots, isBundleEnabled } from "../core/config/config-sources.js"; import { UsageError } from "../core/errors.js"; import { warnOnce } from "../core/warn.js"; export function canonicalSchedulerRef(ref) { const parsed = parseBundleRef(ref); if (!parsed.bundle || parsed.fragment !== undefined || bundleRefToString(parsed) !== ref) { throw new UsageError(`Scheduling requires a canonical fully-qualified ref, got ${JSON.stringify(ref)}.`, "INVALID_FLAG_VALUE"); } return ref; } /** * Refs this host schedules, or `undefined` when the host has never chosen * (the config predates the list). An entry that is not a canonical ref is * skipped with a warning, never fatal. */ export function schedulerEnabledRefs(config) { const enabled = config.scheduler?.enabled; if (enabled === undefined) return undefined; const refs = new Set(); for (const ref of enabled) { try { refs.add(canonicalSchedulerRef(ref)); } catch { warnOnce(`scheduler:enabled:${ref}`, `Ignoring scheduler.enabled entry ${JSON.stringify(ref)}: not a canonical ref.`); } } return Object.freeze([...refs]); } export function isSchedulerRefEnabled(config, ref) { return (schedulerEnabledRefs(config) ?? []).includes(canonicalSchedulerRef(ref)); } export function revokeSchedulerActivationsForBundle(config, bundleId) { const current = config.scheduler?.enabled; if (current === undefined) return config; const enabled = current.filter((ref) => bundleOf(ref) !== bundleId); if (enabled.length === current.length) return config; return { ...config, scheduler: { ...config.scheduler, enabled } }; } /** Whether refs from this bundle may be scheduled here: configured and enabled, or the implicit `AKM_BUNDLE_DIR` stash. */ export function isSchedulerBundleActive(config, bundleId) { return schedulerSourceIdFor(config, bundleId) !== undefined; } /** * Add or remove one ref on this host's list. Callers must have made the host's * choice explicit first (see `initializeSchedulerChoice` in * `commands/tasks/tasks.ts`): on a config without the list this writes just * the one ref, which is the whole choice from then on. */ export function setSchedulerRefEnabled(ref, enabled) { const canonicalRef = canonicalSchedulerRef(ref); const result = mutateConfig((current) => { const existing = current.scheduler?.enabled ?? []; if (enabled && !isSchedulerBundleActive(current, bundleOf(canonicalRef) ?? "")) { throw new UsageError(`Cannot enable scheduled execution from inactive or unconfigured bundle ${JSON.stringify(bundleOf(canonicalRef))}.`, "INVALID_FLAG_VALUE"); } if (current.scheduler?.enabled !== undefined && existing.includes(canonicalRef) === enabled) return current; const next = existing.filter((entry) => entry !== canonicalRef); if (enabled) next.push(canonicalRef); next.sort((left, right) => left.localeCompare(right)); return { ...current, scheduler: { ...current.scheduler, enabled: next } }; }); return { config: result.config, changed: result.written, ref: canonicalRef }; } /** * The refs behind the akm-written rows currently installed in the native * scheduler: what this host was running before it had a `scheduler.enabled` * list. Rows the backend proves disabled, rows for bundles that are not * active here, and rows whose task file is gone are left out. */ export function enabledRefsFromInstalled(installed, config) { const refs = new Set(); for (const entry of installed) { const ref = installedEntryRef(entry, config); if (ref !== undefined) refs.add(ref); } return Object.freeze([...refs].sort((left, right) => left.localeCompare(right))); } function installedEntryRef(entry, config) { if (entry.enabled === false || !entry.invocation) return undefined; const invocation = entry.invocation; try { if (invocation[0] === "workflow" && invocation[1] === "run" && invocation.length === 3 && invocation[2]) { const ref = canonicalSchedulerRef(invocation[2]); const parsed = parseBundleRef(ref); if (!parsed.bundle || !isBundleEnabled(config, parsed.bundle)) return undefined; return hasBackingFile(config, parsed.bundle, parsed.conceptId) ? ref : undefined; } if (invocation[0] !== "task" || invocation[1] !== "run" || !invocation[2]) return undefined; const bundleIndex = invocation.indexOf("--bundle", 3); const bundle = bundleIndex === -1 ? config.defaultBundle : invocation[bundleIndex + 1]; if (!bundle || !isBundleEnabled(config, bundle)) return undefined; const bundleConfig = config.bundles?.[bundle]; const adapter = bundleConfig ? (bundleComponentConfig(bundleConfig)?.adapter ?? "akm") : "akm"; const conceptId = adapter === "akm-task" ? invocation[2] : `tasks/${invocation[2]}`; if (!hasBackingFile(config, bundle, conceptId)) return undefined; return canonicalSchedulerRef(makeBundleRef(bundle, conceptId)); } catch { return undefined; } } function hasBackingFile(config, bundle, conceptId) { const parts = typeNameFromConceptId(conceptId); if (!parts) return true; const typeDir = stashDirFor(parts.type); if (!typeDir) return true; const contentRoot = bundleContentRoots(config).find((entry) => entry.id === bundle)?.contentRoot; if (!contentRoot) return true; try { const target = assetPathForName(parts.type, path.join(contentRoot, typeDir), parts.name); return fs.existsSync(target) && fs.statSync(target).isFile(); } catch { return false; } } function bundleOf(ref) { try { return parseBundleRef(ref).bundle; } catch { return undefined; } }