akm-cli 0.10.261015-beta → 0.10.261016-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/STABILITY.md +2 -1
- package/dist/assets/hints/cli-hints-full.md +6 -4
- package/dist/assets/hints/cli-hints-short.md +4 -2
- package/dist/assets/opencode/akm-plugin.js +16 -8
- package/dist/assets/prompts/workflow-unit-preamble.md +3 -1
- package/dist/commands/health/plugin-staleness.js +86 -22
- package/dist/commands/read/search-cli.js +2 -2
- package/dist/commands/read/show.js +15 -3
- package/dist/core/asset/markdown.js +9 -0
- package/dist/core/config/config-walker.js +35 -32
- package/dist/core/config/schema/connections.js +6 -3
- package/dist/core/config/schema/harnesses.js +8 -1
- package/dist/core/config/schema/primitives.js +12 -1
- package/dist/core/config/schema/sources-bundles.js +2 -2
- package/dist/core/npm-package.js +48 -13
- package/dist/output/renderers.js +2 -2
- package/dist/output/shapes/helpers.js +2 -0
- package/dist/output/text/show-format.js +7 -0
- package/dist/registry/semver.js +12 -0
- package/dist/scripts/akm-migrate-node.js +21629 -5738
- package/dist/scripts/akm-migrate.js +21632 -5741
- package/dist/tasks/backends/cron.js +19 -0
- package/docs/architecture/internals/health-advisories.md +1 -1
- package/docs/architecture/specs/ref.md +4 -3
- package/docs/architecture/testing/testing-workflow.md +7 -8
- package/docs/guides/discover-and-load.md +12 -4
- package/docs/guides/getting-started.md +3 -2
- package/docs/guides/use-with-any-agent.md +4 -1
- package/docs/maintainers/release-checklist.md +15 -8
- package/docs/reference/cli.md +22 -3
- package/package.json +4 -4
- package/schemas/akm-config.json +535 -302
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import { WORKFLOW_MAX_TIMEOUT_MS } from "../../../workflows/resource-limits.js";
|
|
15
|
-
import { connectionEndpoint, engineName, envReferenceApiKey, nonEmptyString, positiveInt, retiredApiKeyFile, } from "./primitives.js";
|
|
15
|
+
import { connectionEndpoint, engineName, envReferenceApiKey, nonEmptyString, OpaqueFieldSchema, positiveInt, retiredApiKeyFile, } from "./primitives.js";
|
|
16
16
|
/**
|
|
17
17
|
* Timeouts share the workflow ceiling: a larger value would pass config
|
|
18
18
|
* validation and then fail every run with an unlocated "invocation is invalid".
|
|
@@ -24,7 +24,10 @@ export const timeoutMsField = z.union([positiveInt.max(WORKFLOW_MAX_TIMEOUT_MS),
|
|
|
24
24
|
* request fields of its own. The object is listed first so a JSON value given to
|
|
25
25
|
* `akm config set` is read as one.
|
|
26
26
|
*/
|
|
27
|
-
export const ModelEntrySchema = z.union([
|
|
27
|
+
export const ModelEntrySchema = z.union([
|
|
28
|
+
z.object({ model: nonEmptyString }).catchall(OpaqueFieldSchema),
|
|
29
|
+
nonEmptyString,
|
|
30
|
+
]);
|
|
28
31
|
/** Alias (`fast`, `balanced`, `reasoning`, or any the user adds) to the model it names. */
|
|
29
32
|
export const ModelAliasesSchema = z.record(nonEmptyString, ModelEntrySchema);
|
|
30
33
|
export const ConnectionSchema = z
|
|
@@ -37,5 +40,5 @@ export const ConnectionSchema = z
|
|
|
37
40
|
timeoutMs: timeoutMsField,
|
|
38
41
|
concurrency: positiveInt.optional(),
|
|
39
42
|
})
|
|
40
|
-
.catchall(
|
|
43
|
+
.catchall(OpaqueFieldSchema);
|
|
41
44
|
export const ConnectionsSchema = z.record(engineName, ConnectionSchema);
|
|
@@ -13,7 +13,14 @@ import { ModelAliasesSchema, timeoutMsField } from "./connections.js";
|
|
|
13
13
|
import { engineName, nonEmptyString } from "./primitives.js";
|
|
14
14
|
export const HarnessSchema = z
|
|
15
15
|
.object({
|
|
16
|
-
|
|
16
|
+
// Zod 4's message for a value outside an enum no longer quotes the value; the warning for a dropped harness should.
|
|
17
|
+
platform: z
|
|
18
|
+
.enum(HARNESS_CONFIG_PLATFORMS, {
|
|
19
|
+
error: (issue) => issue.code === "invalid_value"
|
|
20
|
+
? `${JSON.stringify(issue.input)} is not one of ${HARNESS_CONFIG_PLATFORMS.join(", ")}`
|
|
21
|
+
: undefined,
|
|
22
|
+
})
|
|
23
|
+
.optional(),
|
|
17
24
|
bin: nonEmptyString.optional(),
|
|
18
25
|
args: z.array(z.string()).optional(),
|
|
19
26
|
workspace: nonEmptyString.optional(),
|
|
@@ -12,11 +12,22 @@ import { redactUrlCredentials } from "../../redaction.js";
|
|
|
12
12
|
import { isSecretRefToken } from "../../ref-token.js";
|
|
13
13
|
import { warnOnce } from "../../warn.js";
|
|
14
14
|
import { ENGINE_NAME_PATTERN_SOURCE } from "../engine-semantics.js";
|
|
15
|
+
// Zod 4 reports a map key its key schema rejects as "Invalid key in record" and keeps that schema's own message
|
|
16
|
+
// (the naming rule, the slug rule) inside the issue; config errors and warnings print only the outer message.
|
|
17
|
+
z.config({
|
|
18
|
+
customError: (issue) => issue.code === "invalid_key" ? issue.issues.map((inner) => inner.message).join("; ") : undefined,
|
|
19
|
+
});
|
|
15
20
|
/** Persisted config schema version. Package prerelease/patch versions do not change this value. */
|
|
16
21
|
export const CURRENT_CONFIG_VERSION = "0.9.0";
|
|
17
22
|
// ── Reusable atomic schemas ─────────────────────────────────────────────────
|
|
18
23
|
/** Positive integer (used for tokens, timeouts, batch sizes). */
|
|
19
24
|
export const positiveInt = z.number().int().positive();
|
|
25
|
+
/**
|
|
26
|
+
* The catchall of an object whose extra keys are the endpoint's to define (a connection's request fields):
|
|
27
|
+
* any key, contents left alone. The config walker knows it by identity: a `.passthrough()` object also has an
|
|
28
|
+
* `unknown` catchall, and its extra keys are ones akm does not define.
|
|
29
|
+
*/
|
|
30
|
+
export const OpaqueFieldSchema = z.unknown();
|
|
20
31
|
/** Non-negative finite number (used for scores, weights, days). */
|
|
21
32
|
export const nonNegativeNumber = z.number().finite().min(0);
|
|
22
33
|
/** Non-empty string (rejects "" and whitespace-only). */
|
|
@@ -92,7 +103,7 @@ export const connectionEndpoint = z.string().superRefine((value, ctx) => {
|
|
|
92
103
|
warnOnce(`connectionEndpoint:userinfo:${shown}`, `Config endpoint "${shown}" embeds a username/password; move the credential out of the URL (apiKey: "$VAR", or credentials.json).`);
|
|
93
104
|
}
|
|
94
105
|
});
|
|
95
|
-
export const ExtraParamsSchema = z.record(z.unknown()).superRefine((value, ctx) => {
|
|
106
|
+
export const ExtraParamsSchema = z.record(z.string(), z.unknown()).superRefine((value, ctx) => {
|
|
96
107
|
for (const issue of validateExtraParams(value)) {
|
|
97
108
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: issue.path, message: issue.message });
|
|
98
109
|
}
|
|
@@ -16,7 +16,7 @@ import { isBundleSlug } from "../../asset/asset-ref.js";
|
|
|
16
16
|
import { httpUrl, isApiKeyReference, nonEmptyString, positiveInt } from "./primitives.js";
|
|
17
17
|
const VALID_ADAPTER_IDS_SET = new Set(VALID_ADAPTER_IDS);
|
|
18
18
|
// ── Sources / registries / installed ────────────────────────────────────────
|
|
19
|
-
const SourceConfigEntryOptionsSchema = z.record(z.unknown());
|
|
19
|
+
const SourceConfigEntryOptionsSchema = z.record(z.string(), z.unknown());
|
|
20
20
|
export const SourceConfigEntrySchema = z
|
|
21
21
|
.object({
|
|
22
22
|
type: nonEmptyString,
|
|
@@ -74,7 +74,7 @@ export const RegistryConfigEntrySchema = z
|
|
|
74
74
|
name: z.string().min(1).optional(),
|
|
75
75
|
enabled: z.boolean().optional(),
|
|
76
76
|
provider: z.string().min(1).optional(),
|
|
77
|
-
options: z.record(z.unknown()).optional(),
|
|
77
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
78
78
|
})
|
|
79
79
|
.passthrough();
|
|
80
80
|
// ── Bundles ─────────────────────────────────────────────────────────────────
|
package/dist/core/npm-package.js
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
|
-
import { writeResponseToFileCapped } from "../runtime.js";
|
|
15
|
+
import { sleep, writeResponseToFileCapped } from "../runtime.js";
|
|
16
16
|
import { fetchWithRetry, isRecord } from "./common.js";
|
|
17
|
+
import { releaseLock } from "./file-lock.js";
|
|
18
|
+
import { formatLockHolderPid, tryAcquireRunLock } from "./run-lock.js";
|
|
17
19
|
import { extractTarGzMembers } from "./tar.js";
|
|
18
20
|
export const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
19
21
|
/** The `sha512-<base64>` integrity npm records for a tarball. */
|
|
@@ -47,14 +49,50 @@ async function registryDocument(url) {
|
|
|
47
49
|
}
|
|
48
50
|
return { tarball: dist.tarball, integrity: dist.integrity };
|
|
49
51
|
}
|
|
52
|
+
/** How long a fetch waits for another process's fetch into the same directory: a large tarball on a slow link takes minutes. */
|
|
53
|
+
const FETCH_LOCK_TIMEOUT_MS = 600_000;
|
|
50
54
|
/**
|
|
51
|
-
* Keep members of `name@version`'s tarball in `directory`, each as `file
|
|
52
|
-
*
|
|
53
|
-
* member by temp file and rename (mode 0755), in the order given. Two akm processes fetching at once both finish: a
|
|
54
|
-
* rename that loses to the other leaves the file the other made.
|
|
55
|
+
* Keep members of `name@version`'s tarball in `directory`, each as `file`. One fetch runs per directory at a time: the
|
|
56
|
+
* processes fetching there, this one included, wait on a lock file in it, and a dead holder's lock is reclaimed.
|
|
55
57
|
*/
|
|
56
58
|
export async function fetchNpmPackageFiles(name, version, directory, files) {
|
|
57
59
|
fs.mkdirSync(directory, { recursive: true });
|
|
60
|
+
const lock = path.join(directory, "fetch.lock");
|
|
61
|
+
const deadline = Date.now() + FETCH_LOCK_TIMEOUT_MS;
|
|
62
|
+
for (;;) {
|
|
63
|
+
const attempt = tryAcquireRunLock(lock, { label: "fetch" });
|
|
64
|
+
if (attempt.state === "acquired") {
|
|
65
|
+
try {
|
|
66
|
+
return await fetchHoldingLock(name, version, directory, files);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
releaseLock(attempt.ownership);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (Date.now() >= deadline) {
|
|
73
|
+
throw new Error(`another akm process (pid ${formatLockHolderPid(attempt.holder)}) is still fetching into ${directory} (lock ${lock})`);
|
|
74
|
+
}
|
|
75
|
+
await sleep(100);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Whether `file` is in place whole: files only ever arrive by rename, so an executable regular file is complete. */
|
|
79
|
+
function isExecutableFile(file) {
|
|
80
|
+
try {
|
|
81
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
82
|
+
return fs.statSync(file).isFile();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The registry document of that one version, its tarball, the check against the sha512 integrity the registry lists
|
|
90
|
+
* before anything is extracted, then each member by temp file and rename (mode 0755), in the order given. A fetch that
|
|
91
|
+
* finds every file in place downloads nothing, and a file in place at the size the tarball holds is never renamed over.
|
|
92
|
+
*/
|
|
93
|
+
async function fetchHoldingLock(name, version, directory, files) {
|
|
94
|
+
if (files.every(({ file }) => isExecutableFile(path.join(directory, file))))
|
|
95
|
+
return;
|
|
58
96
|
const download = path.join(directory, `.download-${process.pid}.tgz`);
|
|
59
97
|
const partial = (file) => path.join(directory, `.${file}-${process.pid}.tmp`);
|
|
60
98
|
try {
|
|
@@ -70,15 +108,12 @@ export async function fetchNpmPackageFiles(name, version, directory, files) {
|
|
|
70
108
|
const sizes = await extractTarGzMembers(download, new Map(files.map(({ member, file }) => [member, partial(file)])));
|
|
71
109
|
for (const { member, file } of files) {
|
|
72
110
|
const target = path.join(directory, file);
|
|
111
|
+
// A whole file in place (an akm without this lock may have made it) stands: a rename would unlink a binary that
|
|
112
|
+
// may be running on Linux and fails on Windows.
|
|
113
|
+
if (isExecutableFile(target) && fs.statSync(target).size === sizes.get(member))
|
|
114
|
+
continue;
|
|
73
115
|
fs.chmodSync(partial(file), 0o755);
|
|
74
|
-
|
|
75
|
-
fs.renameSync(partial(file), target);
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
// Where a file in use cannot be replaced, the other process's file stands.
|
|
79
|
-
if (!fs.existsSync(target) || fs.statSync(target).size !== sizes.get(member))
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
116
|
+
fs.renameSync(partial(file), target);
|
|
82
117
|
}
|
|
83
118
|
}
|
|
84
119
|
finally {
|
package/dist/output/renderers.js
CHANGED
|
@@ -217,8 +217,8 @@ const agentMdRenderer = {
|
|
|
217
217
|
},
|
|
218
218
|
};
|
|
219
219
|
// ── 4. knowledge-md ──────────────────────────────────────────────────────────
|
|
220
|
-
const KNOWLEDGE_ACTION = "Reference material - read the content below. For a large document,
|
|
221
|
-
"(
|
|
220
|
+
const KNOWLEDGE_ACTION = "Reference material - read the content below. For a large document, run `akm show <ref> --detail brief` for its " +
|
|
221
|
+
"outline (`toc`, no body), then `akm show <ref>#<slug>` for only the sections you need.";
|
|
222
222
|
const knowledgeMdRenderer = {
|
|
223
223
|
name: "knowledge-md",
|
|
224
224
|
buildShowResponse(ctx) {
|
|
@@ -348,6 +348,7 @@ export function shapeShowOutput(result, detail, shape = "human") {
|
|
|
348
348
|
"editHint",
|
|
349
349
|
"description",
|
|
350
350
|
"action",
|
|
351
|
+
"toc",
|
|
351
352
|
"content",
|
|
352
353
|
"template",
|
|
353
354
|
"prompt",
|
|
@@ -381,6 +382,7 @@ export function shapeShowOutput(result, detail, shape = "human") {
|
|
|
381
382
|
"action",
|
|
382
383
|
"description",
|
|
383
384
|
"tags",
|
|
385
|
+
"toc",
|
|
384
386
|
"content",
|
|
385
387
|
"template",
|
|
386
388
|
"prompt",
|
|
@@ -59,6 +59,13 @@ export function formatShowPlain(r, detail) {
|
|
|
59
59
|
if (r.schemaVersion !== undefined)
|
|
60
60
|
lines.push(`schemaVersion: ${String(r.schemaVersion)}`);
|
|
61
61
|
}
|
|
62
|
+
if (Array.isArray(r.toc) && r.toc.length > 0) {
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push("toc:");
|
|
65
|
+
for (const heading of r.toc) {
|
|
66
|
+
lines.push(` ${"#".repeat(heading.level)} ${heading.text} (#${heading.slug}, line ${heading.startLine})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
62
69
|
const links = typeof r.links === "object" && r.links !== null ? r.links : undefined;
|
|
63
70
|
if (links) {
|
|
64
71
|
lines.push("");
|
package/dist/registry/semver.js
CHANGED
|
@@ -41,6 +41,18 @@ export function isSemverRange(input) {
|
|
|
41
41
|
export function maxSatisfying(versions, range) {
|
|
42
42
|
return semver.maxSatisfying(versions, range) ?? undefined;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Highest of `versions` in semver order, prereleases included (`0.10.1-beta` sorts above `0.9.30`). Invalid entries
|
|
46
|
+
* are ignored; `undefined` when none is valid. Use this to order known versions, not `maxSatisfying(versions, "*")`,
|
|
47
|
+
* which skips every prerelease.
|
|
48
|
+
*/
|
|
49
|
+
export function highestVersion(versions) {
|
|
50
|
+
return semver.rsort(versions.filter((version) => semver.valid(version) !== null))[0];
|
|
51
|
+
}
|
|
52
|
+
/** True when `version` is a valid semver prerelease (`1.2.3-beta`). */
|
|
53
|
+
export function isPrerelease(version) {
|
|
54
|
+
return semver.valid(version) !== null && semver.prerelease(version) !== null;
|
|
55
|
+
}
|
|
44
56
|
/** True when `version` satisfies `range` (both real semver forms). False for an invalid version or range. */
|
|
45
57
|
export function satisfiesRange(version, range) {
|
|
46
58
|
return semver.satisfies(version, range);
|