akm-cli 0.10.261014-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.
Files changed (52) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +7 -8
  3. package/SECURITY.md +3 -3
  4. package/STABILITY.md +2 -1
  5. package/dist/assets/hints/cli-hints-full.md +6 -4
  6. package/dist/assets/hints/cli-hints-short.md +4 -2
  7. package/dist/assets/opencode/akm-plugin.js +16 -8
  8. package/dist/assets/prompts/workflow-unit-preamble.md +3 -1
  9. package/dist/cli.js +2 -2
  10. package/dist/commands/health/plugin-staleness.js +86 -22
  11. package/dist/commands/health/semantic-search-check.js +2 -2
  12. package/dist/commands/read/search-cli.js +2 -2
  13. package/dist/commands/read/show.js +15 -3
  14. package/dist/core/asset/markdown.js +9 -0
  15. package/dist/core/config/config-walker.js +35 -32
  16. package/dist/core/config/schema/connections.js +6 -3
  17. package/dist/core/config/schema/harnesses.js +8 -1
  18. package/dist/core/config/schema/primitives.js +12 -1
  19. package/dist/core/config/schema/sources-bundles.js +2 -2
  20. package/dist/core/npm-package.js +124 -0
  21. package/dist/{integrations/harnesses/opencode/runtime → core}/tar.js +25 -16
  22. package/dist/integrations/harnesses/opencode/runtime/binary.js +8 -75
  23. package/dist/integrations/harnesses/opencode/runtime/index.js +5 -1
  24. package/dist/llm/embedders/local.js +23 -58
  25. package/dist/llm/embedders/onnx-runtime.js +121 -0
  26. package/dist/output/renderers.js +2 -2
  27. package/dist/output/shapes/helpers.js +2 -0
  28. package/dist/output/text/show-format.js +7 -0
  29. package/dist/registry/semver.js +12 -0
  30. package/dist/scripts/akm-migrate-node.js +21638 -5760
  31. package/dist/scripts/akm-migrate.js +21645 -5767
  32. package/dist/setup/semantic-assets.js +9 -6
  33. package/dist/setup/setup.js +56 -2
  34. package/dist/setup/steps/embeddings.js +21 -4
  35. package/dist/storage/database.js +17 -57
  36. package/dist/tasks/backends/cron.js +19 -0
  37. package/docs/architecture/akm-core-principles.md +3 -2
  38. package/docs/architecture/internals/health-advisories.md +2 -2
  39. package/docs/architecture/internals/storage-locations.md +1 -0
  40. package/docs/architecture/specs/ref.md +4 -3
  41. package/docs/architecture/testing/testing-workflow.md +28 -20
  42. package/docs/guides/discover-and-load.md +12 -4
  43. package/docs/guides/getting-started.md +23 -20
  44. package/docs/guides/recipes/headless-install.md +13 -8
  45. package/docs/guides/use-with-any-agent.md +4 -1
  46. package/docs/maintainers/release-checklist.md +15 -8
  47. package/docs/migration/v0.9-to-v0.10.md +17 -0
  48. package/docs/reference/cli.md +42 -12
  49. package/docs/reference/configuration.md +37 -15
  50. package/package.json +6 -9
  51. package/schemas/akm-config.json +535 -302
  52. package/dist/standalone/onnxruntime-library.js +0 -52
@@ -2,9 +2,9 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * Local embedder: a BERT-family embedding model run with onnxruntime-node,
6
- * its text encoded by @huggingface/tokenizers, mean-pooled over the attention
7
- * mask and L2-normalised.
5
+ * Local embedder: a BERT-family embedding model run on onnxruntime-node's binding
6
+ * (`./onnx-runtime.ts`), its text encoded by @huggingface/tokenizers, mean-pooled
7
+ * over the attention mask and L2-normalised.
8
8
  *
9
9
  * Encapsulates the model's lifecycle as instance state on a `LocalEmbedder` so
10
10
  * tests can construct fresh instances without leaking loaded models across
@@ -13,8 +13,9 @@
13
13
  */
14
14
  import fs from "node:fs";
15
15
  import path from "node:path";
16
- import { preloadOnnxRuntimeLibrary } from "../../standalone/onnxruntime-library.js";
16
+ import { toErrorMessage } from "../../core/common.js";
17
17
  import { ensureLocalModelFiles } from "./local-model-files.js";
18
+ import { hasLocalEmbeddings, loadOnnxRuntime, noLocalEmbeddingsReason, OnnxTensor } from "./onnx-runtime.js";
18
19
  /**
19
20
  * Default local model for embeddings.
20
21
  * `bge-small-en-v1.5` scores higher on MTEB benchmarks than the previous
@@ -24,10 +25,10 @@ export const DEFAULT_LOCAL_MODEL = "Xenova/bge-small-en-v1.5";
24
25
  /** The runtime the local embedder reports once its model has loaded. */
25
26
  export const LOCAL_EMBEDDING_RUNTIME = "onnxruntime-node";
26
27
  /**
27
- * The embedding runtime is not usable on this machine. `platform`: onnxruntime-node
28
- * has no native library for it, so keyword search is all there is. `load`: it has
29
- * one, which did not load (a missing package or addon, a `noexec` temp or cache
30
- * directory).
28
+ * The embedding runtime is not usable on this machine. `platform`: akm offers no
29
+ * local embeddings here, so keyword search and remote embeddings are what there
30
+ * is. `load`: it does, and the runtime could not be fetched or did not load (no
31
+ * network for the fetch, a `noexec` cache directory).
31
32
  */
32
33
  export class LocalRuntimeUnavailableError extends Error {
33
34
  kind;
@@ -55,50 +56,17 @@ const LOCAL_BATCH_SIZE = 32;
55
56
  function resolveLocalModelName(overrideModel) {
56
57
  return overrideModel || DEFAULT_LOCAL_MODEL;
57
58
  }
58
- /**
59
- * A standalone binary built for a target with no native library (`none`) must not
60
- * bundle the package: its addon for that platform does not exist. The bundler
61
- * drops only the branch a build constant rules out, so the import sits in an
62
- * `else`, not after the `throw`.
63
- */
64
- async function importOnnxRuntime() {
65
- if (typeof AKM_STANDALONE_NATIVE_MODULE !== "undefined" && AKM_STANDALONE_NATIVE_MODULE === "./native/none") {
66
- throw new Error("no native library is available for this platform");
67
- }
68
- else {
69
- await preloadOnnxRuntimeLibrary();
70
- return import("onnxruntime-node");
71
- }
72
- }
73
- /**
74
- * Whether onnxruntime-node has a native library for this machine. A standalone
75
- * binary knows from its build (`none`). The package has one for glibc Linux
76
- * x64/arm64, Apple-silicon macOS and Windows x64/arm64, and none for musl Linux
77
- * or Intel macOS.
78
- */
79
- function hasNativeLibrary() {
80
- if (typeof AKM_STANDALONE_NATIVE_MODULE !== "undefined")
81
- return AKM_STANDALONE_NATIVE_MODULE !== "./native/none";
82
- if (process.platform === "linux") {
83
- const header = process.report?.getReport()?.header;
84
- if (!header?.glibcVersionRuntime)
85
- return false;
86
- }
87
- return ["linux-x64", "linux-arm64", "darwin-arm64", "win32-x64", "win32-arm64"].includes(`${process.platform}-${process.arch}`);
88
- }
89
- async function loadOnnxRuntime() {
59
+ async function loadRuntime() {
60
+ if (!hasLocalEmbeddings())
61
+ throw new LocalRuntimeUnavailableError(noLocalEmbeddingsReason(), "platform");
90
62
  try {
91
- return await importOnnxRuntime();
63
+ return await loadOnnxRuntime();
92
64
  }
93
65
  catch (error) {
94
- const msg = error instanceof Error ? error.message : String(error);
95
- const kind = hasNativeLibrary() ? "load" : "platform";
96
- // Only the package itself missing calls for a reinstall; a platform it ships no addon for (Intel macOS) fails on
97
- // `../bin/napi-v6/<platform>/<arch>/onnxruntime_binding.node` and is reported below.
98
- if (kind === "load" && /Cannot find (?:module|package) ['"]onnxruntime-node['"]/.test(msg)) {
99
- throw new LocalRuntimeUnavailableError("Semantic search requires onnxruntime-node. Reinstall akm-cli.", kind);
100
- }
101
- throw new LocalRuntimeUnavailableError(`Failed to load the embedding runtime (onnxruntime-node): ${msg}. Semantic search with the local model is unavailable; keyword search still works.`, kind);
66
+ const msg = toErrorMessage(error).replace(/\.$/, "");
67
+ // A `noexec` mount maps nothing executable, so the binding cannot load from the cache there.
68
+ const mount = /failed to map segment/.test(msg) ? " akm's cache directory must allow executing files." : "";
69
+ throw new LocalRuntimeUnavailableError(`Failed to load the embedding runtime (onnxruntime-node): ${msg}.${mount} Semantic search with the local model is unavailable; keyword search still works.`, "load");
102
70
  }
103
71
  }
104
72
  /** The mean of one text's token vectors (padding excluded), scaled to unit length. */
@@ -116,17 +84,14 @@ function meanPooled(hidden, offset, tokens, dim) {
116
84
  return vector.map((value) => value / Math.sqrt(norm));
117
85
  }
118
86
  async function loadOnnxPipeline(modelName) {
119
- const ort = await loadOnnxRuntime();
87
+ const ort = await loadRuntime();
120
88
  const { Tokenizer } = await import("@huggingface/tokenizers");
121
89
  const modelDir = await ensureLocalModelFiles(modelName);
122
90
  const readJson = (file) => JSON.parse(fs.readFileSync(path.join(modelDir, file), "utf8"));
123
91
  const tokenizerConfig = readJson("tokenizer_config.json");
124
92
  const tokenizer = new Tokenizer(readJson("tokenizer.json"), tokenizerConfig);
125
93
  const maxLength = typeof tokenizerConfig.model_max_length === "number" ? tokenizerConfig.model_max_length : Number.POSITIVE_INFINITY;
126
- const session = await ort.InferenceSession.create(path.join(modelDir, "onnx", "model.onnx"), {
127
- executionProviders: ["cpu"],
128
- logSeverityLevel: 3,
129
- });
94
+ const session = ort.createSession(path.join(modelDir, "onnx", "model.onnx"));
130
95
  return async (texts) => {
131
96
  // A text longer than the model's maximum is cut to it. Shorter ones are padded
132
97
  // to the longest in the batch; the mask keeps padding out of the pooling.
@@ -142,13 +107,13 @@ async function loadOnnxPipeline(modelName) {
142
107
  });
143
108
  const dims = [texts.length, length];
144
109
  const inputs = {
145
- input_ids: new ort.Tensor("int64", ids, dims),
146
- attention_mask: new ort.Tensor("int64", mask, dims),
147
- token_type_ids: new ort.Tensor("int64", new BigInt64Array(texts.length * length), dims),
110
+ input_ids: new OnnxTensor("int64", ids, dims),
111
+ attention_mask: new OnnxTensor("int64", mask, dims),
112
+ token_type_ids: new OnnxTensor("int64", new BigInt64Array(texts.length * length), dims),
148
113
  };
149
114
  // Models that take fewer inputs (no token_type_ids) are fed only theirs.
150
115
  const feeds = Object.fromEntries(session.inputNames.flatMap((name) => (inputs[name] ? [[name, inputs[name]]] : [])));
151
- const outputs = await session.run(feeds);
116
+ const outputs = session.run(feeds);
152
117
  const hidden = (outputs.last_hidden_state ?? outputs.token_embeddings ?? outputs.logits)?.data;
153
118
  const dim = hidden.length / (texts.length * length);
154
119
  return encoded.map((row, i) => meanPooled(hidden, i * length * dim, row.length, dim));
@@ -0,0 +1,121 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * ONNX Runtime for the local embedder: onnxruntime-node's N-API binding and the
6
+ * library it links, kept in akm's cache and loaded from there. `akm setup`
7
+ * fetches them when local embeddings are chosen, and the first local embedding
8
+ * fetches them when setup did not.
9
+ *
10
+ * akm does not depend on the onnxruntime-node package. Its JavaScript loads the
11
+ * binding from a path inside the package, which a compiled standalone binary can
12
+ * only satisfy by writing an embedded copy to the temp directory, and its install
13
+ * script downloads CUDA providers akm never uses. Here the binding is loaded by
14
+ * absolute path with the library beside it in the same folder, where the system
15
+ * loader finds it (`$ORIGIN` on Linux, `@loader_path` on macOS).
16
+ */
17
+ import fs from "node:fs";
18
+ import { createRequire } from "node:module";
19
+ import path from "node:path";
20
+ import { toErrorMessage } from "../../core/common.js";
21
+ import { fetchNpmPackageFiles, NPM_REGISTRY } from "../../core/npm-package.js";
22
+ import { getCacheDir } from "../../core/paths.js";
23
+ import { info } from "../../core/warn.js";
24
+ /**
25
+ * The onnxruntime-node release whose binding akm loads, and the only one it supports. akm calls the binding through
26
+ * the contract between that package's own JavaScript and its addon, which no release documents or promises and which
27
+ * moves between releases (after 1.24.3, `loadModel` returns a promise and `initOrtOnce` takes a third argument). What
28
+ * akm relies on: `initOrtOnce(logLevel, Tensor)`; `new InferenceSession()`; `loadModel(path, options)`;
29
+ * `inputMetadata` and `outputMetadata` (`{ name }[]`); `run(feeds, fetches)`, which reads a tensor as
30
+ * `{ type, data, dims, location }` and makes each result with `new Tensor(type, data, dims)`. To move it: change this
31
+ * version, check that contract in the new release's `lib/binding.ts` and `lib/backend.ts`, check the library names in
32
+ * {@link LIBRARIES}, and run the reference-vector test in `tests/integration/semantic-search-e2e.test.ts`, the gate for
33
+ * any bump.
34
+ */
35
+ export const ONNXRUNTIME_VERSION = "1.24.3";
36
+ /**
37
+ * The platforms akm offers local embeddings on, each with the library the binding links there. Every other platform
38
+ * has keyword search and remote embeddings. A platform is listed once a real run has verified it; Linux means glibc.
39
+ */
40
+ const LIBRARIES = {
41
+ "linux-x64": "libonnxruntime.so.1",
42
+ "darwin-arm64": `libonnxruntime.${ONNXRUNTIME_VERSION}.dylib`,
43
+ };
44
+ const BINDING = "onnxruntime_binding.node";
45
+ /** This machine as `<platform>-<arch>`, Linux without glibc as `linux-musl-<arch>`. */
46
+ function thisPlatform() {
47
+ const header = process.report?.getReport()?.header;
48
+ const musl = process.platform === "linux" && !header?.glibcVersionRuntime;
49
+ return `${process.platform}${musl ? "-musl" : ""}-${process.arch}`;
50
+ }
51
+ /** Whether akm offers local embeddings on this machine. */
52
+ export function hasLocalEmbeddings() {
53
+ return thisPlatform() in LIBRARIES;
54
+ }
55
+ /** What a machine without local embeddings is told, wherever it is told. */
56
+ export function noLocalEmbeddingsReason() {
57
+ return `Local embeddings are not available on ${thisPlatform()}; choose a remote endpoint or keyword search.`;
58
+ }
59
+ /** A tensor as the binding reads one and makes its results. */
60
+ export class OnnxTensor {
61
+ type;
62
+ data;
63
+ dims;
64
+ location = "cpu";
65
+ constructor(type, data, dims) {
66
+ this.type = type;
67
+ this.data = data;
68
+ this.dims = dims;
69
+ }
70
+ }
71
+ /** Fetch the binding and its library into `directory`, naming the package, its URL and the folder when it cannot. */
72
+ async function fetchOnnxRuntime(directory) {
73
+ const library = LIBRARIES[thisPlatform()];
74
+ if (!library)
75
+ throw new Error(noLocalEmbeddingsReason());
76
+ const source = `bin/napi-v6/${process.platform}/${process.arch}`;
77
+ try {
78
+ info(`Fetching ONNX Runtime ${ONNXRUNTIME_VERSION} for local embeddings (about 86 MB, once) into ${directory}.`);
79
+ // The library first: a binding in the folder means its library is there too.
80
+ await fetchNpmPackageFiles("onnxruntime-node", ONNXRUNTIME_VERSION, directory, [
81
+ { member: `package/${source}/${library}`, file: library },
82
+ { member: `package/${source}/${BINDING}`, file: BINDING },
83
+ ]);
84
+ return directory;
85
+ }
86
+ catch (error) {
87
+ throw new Error(`Could not fetch ONNX Runtime ${ONNXRUNTIME_VERSION} for local embeddings: package onnxruntime-node from ${NPM_REGISTRY}/onnxruntime-node/${ONNXRUNTIME_VERSION} into ${directory}: ${toErrorMessage(error)}. ` +
88
+ `Run \`akm setup --yes\` with network access, or copy ${BINDING} and ${library} from that package's ${source}/ into that folder.`);
89
+ }
90
+ }
91
+ /**
92
+ * The binding, loaded and initialised from `<cache>/onnxruntime/<version>-<platform>-<arch>/`, where it and its library
93
+ * are fetched first when the binding is not there.
94
+ */
95
+ async function loadBinding() {
96
+ const directory = path.join(getCacheDir(), "onnxruntime", `${ONNXRUNTIME_VERSION}-${thisPlatform()}`);
97
+ if (!fs.existsSync(path.join(directory, BINDING)))
98
+ await fetchOnnxRuntime(directory);
99
+ const loaded = createRequire(import.meta.url)(path.join(directory, BINDING));
100
+ // 3: errors only.
101
+ loaded.initOrtOnce(3, OnnxTensor);
102
+ return loaded;
103
+ }
104
+ /** Once per process, however many callers ask meanwhile; a failure is not kept, so the next caller tries again. */
105
+ let loading;
106
+ /** The runtime, ready to load models: fetched first when it is not in the cache, then loaded. */
107
+ export async function loadOnnxRuntime() {
108
+ loading ??= loadBinding().catch((error) => {
109
+ loading = undefined;
110
+ throw error;
111
+ });
112
+ const loaded = await loading;
113
+ return {
114
+ createSession(modelPath) {
115
+ const session = new loaded.InferenceSession();
116
+ session.loadModel(modelPath, { executionProviders: ["cpu"], logSeverityLevel: 3 });
117
+ const outputs = Object.fromEntries(session.outputMetadata.map(({ name }) => [name, null]));
118
+ return { inputNames: session.inputMetadata.map(({ name }) => name), run: (feeds) => session.run(feeds, outputs) };
119
+ },
120
+ };
121
+ }
@@ -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, read one section with `akm show <ref>#fragment` " +
221
- "(an unmatched #fragment lists the available slugs).";
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("");
@@ -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);