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
96 lines (95 loc) • 4.23 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/.
import { snapshotStrictRecord } from "./record.js";
function fail(path, detail) {
throw new TypeError(`${path} ${detail}`);
}
/**
* Validate, clone, and freeze a value for durable JSON transport.
*
* `undefined`, non-finite numbers, non-plain objects, and cycles are rejected
* instead of being silently rewritten by `JSON.stringify`. That makes field
* presence meaningful at the execution boundary: omit a key, or provide an
* explicit JSON value (including `false`, `0`, `[]`, `{}`, `""`, or `null`).
*/
export function cloneExecutionJson(value, path = "execution value", ancestors = new Set()) {
if (value === null || typeof value === "string" || typeof value === "boolean")
return value;
if (typeof value === "number") {
if (!Number.isFinite(value))
fail(path, "must contain only finite numbers");
return value;
}
if (value === undefined)
fail(path, "must be omitted rather than set to undefined");
if (typeof value !== "object")
fail(path, "must be JSON-safe");
if (ancestors.has(value))
fail(path, "must not contain a cycle");
const nextAncestors = new Set(ancestors);
nextAncestors.add(value);
if (Array.isArray(value)) {
if (Object.getPrototypeOf(value) !== Array.prototype)
fail(path, "array must use the standard Array prototype");
const ownKeys = Reflect.ownKeys(value);
const lengthDescriptor = Reflect.getOwnPropertyDescriptor(value, "length");
if (!lengthDescriptor ||
!("value" in lengthDescriptor) ||
typeof lengthDescriptor.value !== "number" ||
!Number.isInteger(lengthDescriptor.value) ||
lengthDescriptor.value < 0) {
fail(path, "array length must be a stable nonnegative integer data property");
}
const length = lengthDescriptor.value;
if (ownKeys.length !== length + 1) {
fail(path, "array must be dense and contain no non-index properties");
}
for (const key of ownKeys) {
if (key === "length")
continue;
if (typeof key !== "string" || !/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= length) {
fail(path, "array must contain only canonical index properties");
}
}
const cloned = [];
for (let index = 0; index < length; index++) {
const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
fail(path, "array must be dense enumerable data properties");
}
cloned.push(cloneExecutionJson(descriptor.value, `${path}[${index}]`, nextAncestors));
}
return Object.freeze(cloned);
}
const snapshot = snapshotStrictRecord(value, path);
const cloned = Object.create(null);
for (const [key, child] of Object.entries(snapshot)) {
Object.defineProperty(cloned, key, {
value: cloneExecutionJson(child, `${path}.${key}`, nextAncestors),
enumerable: true,
configurable: false,
writable: false,
});
}
return Object.freeze(cloned);
}
export function cloneExecutionJsonObject(value, path) {
const cloned = cloneExecutionJson(value, path);
if (cloned === null || Array.isArray(cloned) || typeof cloned !== "object") {
fail(path, "must be a JSON object");
}
return cloned;
}
/** Stable object-key ordering for request fingerprints; array order is semantic. */
export function sortExecutionJson(value) {
if (Array.isArray(value))
return value.map(sortExecutionJson);
if (value === null || typeof value !== "object")
return value;
const sorted = Object.create(null);
for (const [key, child] of Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
sorted[key] = sortExecutionJson(child);
}
return sorted;
}