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
JavaScript
// 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;
}
}