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
227 lines (226 loc) • 10.5 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/.
/**
* Sources / registries / installed entries + the 0.9.0 `bundles` shape.
* Extracted verbatim from the former `config-schema.ts` monolith — no behavior
* change.
*/
import { z } from "zod";
// Dependency-free mirror of the adapter registry's id list (#909) — see
// adapter-ids.ts's header for why config imports this leaf rather than
// `core/adapter/registry.ts` (which would pull in all 11 concrete adapters
// and, transitively, the indexer modules they delegate to).
import { VALID_ADAPTER_IDS } from "../../adapter/adapter-ids.js";
import { isBundleSlug } from "../../asset/asset-ref.js";
import { httpUrl, isApiKeyReference, nonEmptyString, positiveInt } from "./primitives.js";
const VALID_ADAPTER_IDS_SET = new Set(VALID_ADAPTER_IDS);
// ── Sources / registries / installed ────────────────────────────────────────
const SourceConfigEntryOptionsSchema = z.record(z.unknown());
export const SourceConfigEntrySchema = z
.object({
type: nonEmptyString,
path: z.string().min(1).optional(),
url: z.string().min(1).optional(),
name: z.string().min(1).optional(),
enabled: z.boolean().optional(),
writable: z.boolean().optional(),
credential: z.string().min(1).optional(),
primary: z.boolean().optional(),
options: SourceConfigEntryOptionsSchema.optional(),
})
.passthrough()
.superRefine((entry, ctx) => {
if (entry.options && "pushOnCommit" in entry.options) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["options", "pushOnCommit"],
message: "options.pushOnCommit is not supported",
});
}
if (!["filesystem", "git", "website", "npm"].includes(entry.type)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["type"],
message: `unsupported source type "${entry.type}"; expected filesystem, git, website, or npm`,
});
}
if (entry.writable === true && (entry.type === "website" || entry.type === "npm")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `writable: true is only supported on filesystem and git sources (got "${entry.type}"` +
(entry.name ? ` on source "${entry.name}"` : "") +
").",
});
}
if (entry.credential !== undefined && entry.type !== "git") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["credential"],
message: "credential is only supported on git sources",
});
}
if (entry.credential !== undefined && !isApiKeyReference(entry.credential)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["credential"],
message: "git credential must be a $VAR or secret://<name> reference",
});
}
});
export const RegistryConfigEntrySchema = z
.object({
url: httpUrl,
name: z.string().min(1).optional(),
enabled: z.boolean().optional(),
provider: z.string().min(1).optional(),
options: z.record(z.unknown()).optional(),
})
.passthrough();
// ── Bundles (0.9.0 config-shape cutover, spec §10.1 / D-R5) ─────────────────
//
// `bundles` + `defaultBundle` are the desired-configuration shape. Each bundle
// entry carries ONE source descriptor (`path` | `git` | `website` | `npm`
// — mirroring today's source types), an optional `writable`, an optional
// `registryId` locator (the original registry install id, preserved verbatim so
// a non-slug-legal id like `github:owner/repo` is not lost when its slug-legal
// bundle KEY is derived), and an optional single-entry `components` map (spec
// §10.1; the transitional one-component-per-bundle coupling — NOT multi-component
// machinery). The map KEY is the workspace bundle slug (spec §11.1 charset: no
// `/`, `:`, `.`, `#`, whitespace), validated with {@link isBundleSlug}.
//
// `bindings` is not accepted here; the top-level schema rejects it.
/** Website source descriptor for a bundle entry (spec §10.1). */
const BundleWebsiteDescriptorSchema = z
.object({
url: httpUrl,
refresh: z.string().min(1).optional(),
maxPages: positiveInt.optional(),
maxDepth: positiveInt.optional(),
// Default true: crawl-scoped robots.txt compliance (Disallow/Crawl-delay
// for the akm/akm-cli product tokens or "*"). See
// src/sources/snapshot-fetchers/robots.ts. Opt out with `false` to
// restore pre-P1 behavior exactly (no /robots.txt request at all).
respectRobots: z.boolean().optional(),
// Hard wall-clock cap on the whole crawl, in milliseconds. Defaults to
// 10 minutes. Unlike a between-page check, this aborts work already in
// flight — including a `Retry-After` sleep, which a server can otherwise
// make arbitrarily long. Set to 0 to disable the cap entirely for a
// deliberately long-running crawl.
crawlTimeoutMs: z.number().int().min(0).optional(),
})
.passthrough();
/** One component of a bundle (spec §10.1). Single-entry, transitional. */
const BundleComponentConfigSchema = z
.object({
root: z.string().min(1).optional(),
adapter: nonEmptyString.optional(),
writable: z.boolean().optional(),
})
.passthrough();
export const BundleConfigEntrySchema = z
.object({
// Exactly one source descriptor (enforced in superRefine below):
path: z.string().min(1).optional(),
git: z.string().min(1).optional(),
website: BundleWebsiteDescriptorSchema.optional(),
npm: z.string().min(1).optional(),
/** Symbolic HTTPS bearer credential; resolved only at the Git subprocess boundary. */
credential: z.string().min(1).optional(),
writable: z.boolean().optional(),
// Opt a bundle out of indexing, search, refresh, and write targeting
// without deleting it. The runtime honors the derived value in write and
// search source selection.
enabled: z.boolean().optional(),
// The registry install id when the bundle key was slug-derived from it
// (e.g. registryId `github:owner/repo` → key `repo`).
registryId: z.string().min(1).optional(),
components: z.record(z.string().min(1), BundleComponentConfigSchema).optional(),
})
.passthrough()
.superRefine((entry, ctx) => {
const options = entry.options;
if (options && typeof options === "object" && "pushOnCommit" in options) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["options", "pushOnCommit"],
message: "options.pushOnCommit is not supported",
});
}
const descriptors = ["path", "git", "website", "npm"].filter((k) => entry[k] !== undefined);
if (descriptors.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "a bundle entry must carry exactly one source descriptor (path, git, website, or npm)",
});
}
else if (descriptors.length > 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `a bundle entry must carry exactly one source descriptor; got ${descriptors.join(", ")}`,
});
}
if (entry.writable === true && (entry.website !== undefined || entry.npm !== undefined)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["writable"],
message: "writable: true is only supported on path and git bundle sources",
});
}
if (entry.credential !== undefined && entry.git === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["credential"],
message: "credential is only supported on git bundle sources",
});
}
if (entry.credential !== undefined && !isApiKeyReference(entry.credential)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["credential"],
message: "git credential must be a $VAR or secret://<name> reference",
});
}
const componentEntries = entry.components ? Object.entries(entry.components) : [];
if (entry.components !== undefined && componentEntries.length !== 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["components"],
message: "a bundle components map must contain exactly one component",
});
}
const componentEntry = componentEntries[0];
if (componentEntry?.[1].writable === true && (entry.website !== undefined || entry.npm !== undefined)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["components", componentEntry[0], "writable"],
message: "writable: true is only supported on path and git bundle sources",
});
}
// #909: an unrecognized `components.*.adapter` used to silently fall back
// to `akm` at detect-time with no error and no disclosure — a typo
// (`agent_skills`, `akm-native`, …) then silently changed which files got
// indexed. Reject it here instead, the same treatment other enum-valued
// config fields get, listing the accepted values (derived from the
// adapter registry, never hardcoded — see adapter-ids.ts).
if (componentEntry !== undefined) {
const [componentName, componentValue] = componentEntry;
const adapterValue = componentValue.adapter;
if (adapterValue !== undefined && !VALID_ADAPTER_IDS_SET.has(adapterValue)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["components", componentName, "adapter"],
message: `unrecognized adapter "${adapterValue}"; expected one of: ${VALID_ADAPTER_IDS.join(", ")}`,
});
}
}
});
/**
* `bundles` map. Keys are workspace bundle slugs (spec §11.1 / D-R5 charset).
* The key charset is validated with {@link isBundleSlug} so a key can never carry
* `/`, `:`, `.`, `#`, or whitespace (which would break the `bundle//conceptId`
* ref grammar).
*/
export const BundlesConfigSchema = z.record(z.string().min(1).refine(isBundleSlug, {
message: "bundle key must be a legal slug (no '/', ':', '.', '#', or whitespace)",
}), BundleConfigEntrySchema);