akm-cli 0.9.0 → 0.9.1-beta.2
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 +724 -0
- package/README.md +28 -63
- package/STABILITY.md +4 -2
- package/dist/cli/parse-args.js +7 -1
- package/dist/commands/agent/contribute-cli.js +1 -1
- package/dist/commands/env/child-env.js +14 -0
- package/dist/commands/feedback-cli.js +7 -1
- package/dist/commands/health/llm-usage.js +2 -1
- package/dist/commands/health/surfaces.js +4 -77
- package/dist/commands/health.js +65 -11
- package/dist/commands/improve/distill/quality-gate.js +6 -1
- package/dist/commands/improve/eligibility.js +7 -1
- package/dist/commands/improve/eval-cases.js +2 -0
- package/dist/commands/improve/improve.js +126 -10
- package/dist/commands/improve/locks.js +7 -0
- package/dist/commands/improve/memory/memory-improve.js +9 -0
- package/dist/commands/improve/run-context.js +5 -0
- package/dist/commands/improve/session-asset.js +4 -0
- package/dist/commands/lint/base-linter.js +31 -7
- package/dist/commands/lint/index.js +205 -51
- package/dist/commands/lint/types.js +22 -1
- package/dist/commands/proposal/repository.js +17 -1
- package/dist/commands/sources/add-cli.js +8 -2
- package/dist/commands/sources/info.js +12 -2
- package/dist/commands/sources/installed-stashes.js +6 -1
- package/dist/commands/sources/migration-help.js +12 -3
- package/dist/commands/sources/self-update.js +9 -1
- package/dist/commands/tasks/tasks.js +8 -2
- package/dist/commands/workflow-cli.js +17 -11
- package/dist/core/abort-deadline.js +28 -0
- package/dist/core/adapter/adapters/agent-skills-adapter.js +83 -5
- package/dist/core/adapter/adapters/akm-adapter.js +13 -10
- package/dist/core/adapter/adapters/akm-lint.js +78 -22
- package/dist/core/adapter/adapters/akm-task-adapter.js +43 -20
- package/dist/core/adapter/adapters/dotenv-adapter.js +21 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +5 -3
- package/dist/core/asset/frontmatter.js +10 -1
- package/dist/core/common.js +147 -9
- package/dist/core/concurrent.js +32 -0
- package/dist/core/config/config-io.js +5 -45
- package/dist/core/config/schema/engines.js +14 -3
- package/dist/core/config/schema/workflow.js +11 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/events.js +30 -24
- package/dist/core/extra-params.js +11 -0
- package/dist/core/file-lock.js +7 -1
- package/dist/core/fs-txn.js +15 -2
- package/dist/core/improve-result.js +5 -0
- package/dist/core/json-schema.js +344 -9
- package/dist/core/loopback.js +89 -0
- package/dist/core/migration-operation.js +17 -2
- package/dist/core/path-access.js +107 -0
- package/dist/core/paths.js +16 -2
- package/dist/core/redaction.js +86 -18
- package/dist/core/spawn-env.js +234 -0
- package/dist/core/state-db-scope.js +134 -0
- package/dist/core/state-db.js +1 -0
- package/dist/core/subprocess.js +181 -37
- package/dist/core/write-provenance.js +85 -0
- package/dist/core/write-source.js +33 -2
- package/dist/indexer/db/graph-db.js +17 -6
- package/dist/indexer/ensure-index.js +10 -3
- package/dist/indexer/index-written-assets.js +17 -2
- package/dist/indexer/indexer.js +86 -21
- package/dist/indexer/passes/memory-inference.js +4 -0
- package/dist/indexer/search/db-search.js +25 -17
- package/dist/indexer/walk/walker.js +6 -1
- package/dist/integrations/agent/detect.js +13 -1
- package/dist/integrations/agent/engine-resolution.js +24 -11
- package/dist/integrations/agent/model-aliases.js +1 -1
- package/dist/integrations/agent/profiles.js +9 -1
- package/dist/integrations/agent/spawn.js +15 -87
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +21 -0
- package/dist/integrations/lockfile.js +55 -2
- package/dist/llm/client.js +14 -19
- package/dist/llm/embedder.js +23 -3
- package/dist/llm/embedders/remote.js +27 -2
- package/dist/output/html-render.js +40 -1
- package/dist/output/text/lint-format.js +17 -4
- package/dist/runtime.js +23 -1
- package/dist/scripts/akm-migrate-node.js +1714 -836
- package/dist/scripts/akm-migrate.js +1682 -804
- package/dist/setup/setup.js +22 -7
- package/dist/sources/providers/git-install.js +25 -2
- package/dist/sources/providers/git-stash.js +19 -0
- package/dist/sources/providers/git.js +1 -1
- package/dist/sources/snapshot-fetchers/content-extract.js +63 -1
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -20
- package/dist/storage/database.js +71 -7
- package/dist/storage/engines/sqlite-migrations.js +61 -2
- package/dist/storage/managed-db.js +19 -0
- package/dist/storage/repositories/index-connection.js +39 -4
- package/dist/storage/repositories/index-entries-repository.js +6 -1
- package/dist/storage/repositories/index-meta-repository.js +11 -0
- package/dist/storage/repositories/index-schema.js +17 -2
- package/dist/storage/repositories/index-vec-repository.js +43 -5
- package/dist/storage/repositories/workflow-runs-repository.js +66 -13
- package/dist/storage/sqlite-pragmas.js +12 -1
- package/dist/tasks/log-redaction.js +156 -0
- package/dist/tasks/parser.js +82 -5
- package/dist/tasks/runner.js +222 -17
- package/dist/tasks/scheduler-invocation.js +19 -0
- package/dist/tasks/schema.js +86 -1
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/concurrency-policy.js +95 -1
- package/dist/workflows/exec/dispatch-redaction.js +114 -0
- package/dist/workflows/exec/exec-unit.js +542 -0
- package/dist/workflows/exec/frozen-judge.js +114 -42
- package/dist/workflows/exec/native-executor.js +465 -238
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +424 -219
- package/dist/workflows/exec/step-work.js +506 -167
- package/dist/workflows/exec/unit-dispatch.js +31 -1
- package/dist/workflows/exec/unit-writer.js +53 -13
- package/dist/workflows/exec/worktree.js +454 -41
- package/dist/workflows/ir/compile.js +26 -2
- package/dist/workflows/ir/freeze.js +82 -15
- package/dist/workflows/ir/schema.js +105 -20
- package/dist/workflows/parser.js +242 -19
- package/dist/workflows/program/schema.js +24 -0
- package/dist/workflows/renderer.js +32 -4
- package/dist/workflows/resource-limits.js +182 -0
- package/dist/workflows/runtime/runs.js +146 -6
- package/dist/workflows/validate-summary.js +17 -2
- package/docs/README.md +74 -32
- package/docs/migration/release-notes/0.9.0.md +2 -1
- package/docs/migration/v0.7-to-v0.8.md +2 -1
- package/docs/migration/v0.8-to-v0.9.md +3 -1
- package/docs/reference/README.md +11 -4
- package/docs/reference/bundle-types.md +19 -0
- package/docs/reference/cli.md +105 -16
- package/docs/reference/configuration.md +15 -2
- package/docs/reference/data-and-telemetry.md +30 -10
- package/docs/reference/supported-formats.md +50 -0
- package/docs/reference/workflow-schema.md +1014 -0
- package/docs/reference/workflows.md +37 -633
- package/package.json +13 -6
- package/schemas/akm-config.json +18 -5
- package/schemas/akm-task.json +27 -5
- package/schemas/akm-workflow.json +92 -13
|
@@ -7,6 +7,7 @@ import { writeFileAtomic } from "../core/common.js";
|
|
|
7
7
|
import { ConfigError, rethrowIfTestIsolationError } from "../core/errors.js";
|
|
8
8
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "../core/file-lock.js";
|
|
9
9
|
import { acquireMaintenanceBarrier } from "../core/maintenance-barrier.js";
|
|
10
|
+
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
10
11
|
import { getDataDir, getLockfileLockPath, getLockfilePath } from "../core/paths.js";
|
|
11
12
|
// ── Lock sentinel ────────────────────────────────────────────────────────────
|
|
12
13
|
const LOCK_MAX_RETRIES = 3;
|
|
@@ -54,6 +55,29 @@ export function readLockfile() {
|
|
|
54
55
|
return [];
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Refuse to treat an UNREADABLE lockfile (or data dir) as an absent one (#791).
|
|
60
|
+
*
|
|
61
|
+
* Every write path here is read-modify-WRITE: it loads the current entries and
|
|
62
|
+
* writes the whole array back. An unreadable `akm.lock` that reads as `[]`
|
|
63
|
+
* therefore does not merely lose information — the very next
|
|
64
|
+
* `writeFileAtomic` replaces the operator's entire lock record with the single
|
|
65
|
+
* entry this call happened to be adding. That is the same catastrophe R-012
|
|
66
|
+
* guards against for a *corrupt* file, reached instead through a permission
|
|
67
|
+
* fault, and `fs.existsSync`/a swallowed `readFileSync` could not tell the two
|
|
68
|
+
* apart from "the file was never created".
|
|
69
|
+
*
|
|
70
|
+
* No-op when the path is genuinely absent — that case really does have nothing
|
|
71
|
+
* to preserve.
|
|
72
|
+
*/
|
|
73
|
+
function assertLockfilePathReadable(target) {
|
|
74
|
+
const { access, code } = classifyPathAccess(target);
|
|
75
|
+
if (access !== "inaccessible")
|
|
76
|
+
return;
|
|
77
|
+
throw new ConfigError(`Refusing to modify the lockfile: ${describeInaccessiblePath(target, code)}. akm cannot read the existing lock ` +
|
|
78
|
+
"records, and writing over them would destroy every bundle they track. Fix the ownership or mode of that " +
|
|
79
|
+
"path (or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user owns) and retry.", "DATA_DIR_UNREADABLE");
|
|
80
|
+
}
|
|
57
81
|
/**
|
|
58
82
|
* Like {@link readLockfile}, but THROWS instead of silently degrading to `[]`
|
|
59
83
|
* when the on-disk lockfile exists yet is not parseable JSON or not a JSON
|
|
@@ -82,7 +106,15 @@ function readLockfileOrThrow() {
|
|
|
82
106
|
}
|
|
83
107
|
catch (err) {
|
|
84
108
|
rethrowIfTestIsolationError(err);
|
|
85
|
-
|
|
109
|
+
// "Missing file" is the only failure with nothing to preserve. An
|
|
110
|
+
// UNREADABLE lockfile has everything to preserve and we cannot see it —
|
|
111
|
+
// degrading it to `[]` here is precisely the destructive overwrite this
|
|
112
|
+
// function was written to prevent, only triggered by a permission fault
|
|
113
|
+
// instead of a corrupt file (#791). Classify AFTER the failed read so the
|
|
114
|
+
// happy path costs no extra syscall and the answer describes the failure
|
|
115
|
+
// we actually got.
|
|
116
|
+
assertLockfilePathReadable(lockfilePath);
|
|
117
|
+
return [];
|
|
86
118
|
}
|
|
87
119
|
let parsed;
|
|
88
120
|
try {
|
|
@@ -94,6 +126,16 @@ function readLockfileOrThrow() {
|
|
|
94
126
|
if (!Array.isArray(parsed)) {
|
|
95
127
|
throw new ConfigError(`Refusing to modify lockfile ${lockfilePath}: existing content is not a JSON array. Fix or remove the file by hand before retrying — every existing lock entry would otherwise be lost.`, "INVALID_CONFIG_FILE");
|
|
96
128
|
}
|
|
129
|
+
// Refuse rather than filter. This is the WRITE path's read: everything it
|
|
130
|
+
// returns is what gets written back, so silently dropping entries that fail
|
|
131
|
+
// per-entry validation destroyed them on the next write — the same
|
|
132
|
+
// data-losing overwrite the two refusals above exist to prevent, just at
|
|
133
|
+
// entry granularity instead of file granularity.
|
|
134
|
+
const invalid = parsed.filter((entry) => !isValidLockfileEntry(entry));
|
|
135
|
+
if (invalid.length > 0) {
|
|
136
|
+
throw new ConfigError(`Refusing to modify lockfile ${lockfilePath}: ${invalid.length} existing entr${invalid.length === 1 ? "y is" : "ies are"} malformed. ` +
|
|
137
|
+
"Fix or remove the file by hand before retrying — those entries would otherwise be lost.", "INVALID_CONFIG_FILE");
|
|
138
|
+
}
|
|
97
139
|
return parsed.filter(isValidLockfileEntry);
|
|
98
140
|
}
|
|
99
141
|
/**
|
|
@@ -172,6 +214,11 @@ export async function upsertLockEntry(entry) {
|
|
|
172
214
|
function readLockEntriesForMigration() {
|
|
173
215
|
let existing = [];
|
|
174
216
|
const lockfilePath = getLockfilePath();
|
|
217
|
+
// `mergeLockEntriesSync` writes `existing` straight back out, so an
|
|
218
|
+
// unreadable lockfile read as absent would be overwritten with just the
|
|
219
|
+
// migrator's sparse entries (#791). This is also what
|
|
220
|
+
// `assertMigrationLockfileReadable` promises to have checked.
|
|
221
|
+
assertLockfilePathReadable(lockfilePath);
|
|
175
222
|
if (fs.existsSync(lockfilePath)) {
|
|
176
223
|
let raw;
|
|
177
224
|
try {
|
|
@@ -211,7 +258,13 @@ export function mergeLockEntriesSync(entries) {
|
|
|
211
258
|
writeLockfileUnlocked([...existing.filter((e) => !incomingIds.has(e.id)), ...merged]);
|
|
212
259
|
}
|
|
213
260
|
export async function removeLockEntry(id) {
|
|
214
|
-
|
|
261
|
+
// Returning early says "there is no lock record to remove", and the uninstall
|
|
262
|
+
// that called us reports success on that basis. Only an absent data dir earns
|
|
263
|
+
// it — one we cannot read may hold the very entry we were asked to drop, and
|
|
264
|
+
// silently leaving it behind is how a bundle stays "installed" forever (#791).
|
|
265
|
+
const dataDir = getDataDir();
|
|
266
|
+
assertLockfilePathReadable(dataDir);
|
|
267
|
+
if (!fs.existsSync(dataDir))
|
|
215
268
|
return;
|
|
216
269
|
const release = await acquireLockSentinel();
|
|
217
270
|
try {
|
package/dist/llm/client.js
CHANGED
|
@@ -9,34 +9,22 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { fetchWithTimeout, readBodyWithByteCap } from "../core/common.js";
|
|
11
11
|
import { resolveSecret } from "../core/config/config.js";
|
|
12
|
+
import { ENV_REFERENCE_PATTERN } from "../core/config/schema/primitives.js";
|
|
12
13
|
import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-params.js";
|
|
13
14
|
import { parseJsonResponse } from "../core/parse.js";
|
|
14
|
-
import {
|
|
15
|
+
import { redactErrorBody, redactSensitiveText } from "../core/redaction.js";
|
|
15
16
|
import { warnVerbose } from "../core/warn.js";
|
|
16
17
|
import { DEFAULT_LLM_TIMEOUT_MS } from "../integrations/agent/config.js";
|
|
17
18
|
import { emitLlmUsage, extractUsageTokens, } from "./usage-telemetry.js";
|
|
18
|
-
/** Maximum length of an
|
|
19
|
+
/** Maximum length of an upstream response excerpt included in thrown errors. */
|
|
19
20
|
const ERROR_BODY_MAX_LEN = 200;
|
|
20
21
|
/** Stable OpenAI-compatible response-schema name used for every structured call. */
|
|
21
22
|
const JSON_SCHEMA_RESPONSE_NAME = "akm_response";
|
|
22
23
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* so that a verbose provider response cannot leak large amounts of context.
|
|
26
|
-
*
|
|
27
|
-
* The pattern set itself lives in {@link redactCredentialPatterns}
|
|
28
|
-
* (src/core/redaction.ts) so other output paths (e.g. task run logs) can
|
|
29
|
-
* reuse it without this function's length cap.
|
|
24
|
+
* Re-exported from src/core/redaction.ts, where it now lives so every HTTP
|
|
25
|
+
* transport can apply the same hardening — the embeddings client needs it too.
|
|
30
26
|
*/
|
|
31
|
-
export
|
|
32
|
-
if (!input)
|
|
33
|
-
return "";
|
|
34
|
-
let out = redactCredentialPatterns(input);
|
|
35
|
-
if (out.length > ERROR_BODY_MAX_LEN) {
|
|
36
|
-
out = `${out.slice(0, ERROR_BODY_MAX_LEN)}…`;
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
27
|
+
export { redactErrorBody } from "../core/redaction.js";
|
|
40
28
|
/**
|
|
41
29
|
* Detect a response body that is an HTML document rather than the expected
|
|
42
30
|
* JSON. LM Studio (and similar local providers) can serve their web UI on
|
|
@@ -223,7 +211,14 @@ async function chatCompletionAttempt(config, messages, options, timeoutMs) {
|
|
|
223
211
|
throw new Error(formatExtraParamsIssue("LLM extraParams", issue));
|
|
224
212
|
}
|
|
225
213
|
const headers = { "Content-Type": "application/json" };
|
|
226
|
-
|
|
214
|
+
// Resolve ONLY a whole-string env reference. Every live caller already hands
|
|
215
|
+
// us the materialized credential (materializeLlmConnection / materializeFrozenLlm
|
|
216
|
+
// resolve `$VAR` upstream, and engine config REQUIRES the symbolic form), so
|
|
217
|
+
// re-running the substitution over a literal key mangled any credential
|
|
218
|
+
// containing `$` — `sk-live$ecret` lost everything from the `$` onward, and
|
|
219
|
+
// the request failed with an opaque 401. The narrow check keeps the symbolic
|
|
220
|
+
// form working for any direct caller that still passes one.
|
|
221
|
+
const resolvedKey = ENV_REFERENCE_PATTERN.test(config.apiKey ?? "") ? resolveSecret(config.apiKey) : config.apiKey;
|
|
227
222
|
if (resolvedKey) {
|
|
228
223
|
headers.Authorization = `Bearer ${resolvedKey}`;
|
|
229
224
|
}
|
package/dist/llm/embedder.js
CHANGED
|
@@ -65,12 +65,32 @@ export async function embed(text, embeddingConfig, signal) {
|
|
|
65
65
|
const cached = getCachedEmbedding(key);
|
|
66
66
|
if (cached)
|
|
67
67
|
return cached;
|
|
68
|
-
const result =
|
|
69
|
-
? await new RemoteEmbedder(embeddingConfig).embed(text, signal)
|
|
70
|
-
: await getLocalEmbedder().embed(text, signal);
|
|
68
|
+
const result = await embedOnce(text, embeddingConfig, signal);
|
|
71
69
|
setCachedEmbedding(key, result);
|
|
72
70
|
return result;
|
|
73
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a single embedding through the configured provider.
|
|
74
|
+
*
|
|
75
|
+
* The local branch must honour `localModel` exactly as {@link embedBatch}
|
|
76
|
+
* does. The singleton is constructed with no default model, so routing through
|
|
77
|
+
* `getLocalEmbedder().embed()` silently used DEFAULT_LOCAL_MODEL: queries were
|
|
78
|
+
* embedded with a different model than the index was built with. Nothing
|
|
79
|
+
* detected it, because the provider fingerprint keys on `localModel`, so no
|
|
80
|
+
* purge or "pending" status ever fired — a dimension mismatch made semantic
|
|
81
|
+
* ranking contribute nothing, and a same-dimension override silently produced
|
|
82
|
+
* meaningless cross-model scores.
|
|
83
|
+
*/
|
|
84
|
+
async function embedOnce(text, embeddingConfig, signal) {
|
|
85
|
+
if (embeddingConfig && hasRemoteEndpoint(embeddingConfig)) {
|
|
86
|
+
return new RemoteEmbedder(embeddingConfig).embed(text, signal);
|
|
87
|
+
}
|
|
88
|
+
const localModel = embeddingConfig?.localModel;
|
|
89
|
+
if (localModel) {
|
|
90
|
+
return getLocalEmbedder().embedWithModel(text, localModel);
|
|
91
|
+
}
|
|
92
|
+
return getLocalEmbedder().embed(text, signal);
|
|
93
|
+
}
|
|
74
94
|
/**
|
|
75
95
|
* Generate embeddings for multiple texts in batch.
|
|
76
96
|
* Uses the OpenAI-compatible batch API for remote endpoints (batches of 100).
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { fetchWithTimeout, isHttpUrl, readBodyWithByteCap } from "../../core/common.js";
|
|
11
11
|
import { resolveSecret } from "../../core/config/config.js";
|
|
12
|
+
import { redactErrorBody, redactSensitiveText } from "../../core/redaction.js";
|
|
12
13
|
const DEFAULT_REMOTE_BATCH_SIZE = 100;
|
|
13
14
|
/** Cheap token estimator: 4 chars ≈ 1 token. Used in verbose logging and error messages. */
|
|
14
15
|
export function estimateTokenCount(text) {
|
|
@@ -54,7 +55,7 @@ export class RemoteEmbedder {
|
|
|
54
55
|
throw err;
|
|
55
56
|
return "";
|
|
56
57
|
});
|
|
57
|
-
throw new Error(`Embedding request failed (${response.status}): ${errBody}`);
|
|
58
|
+
throw new Error(`Embedding request failed (${response.status}): ${this.safeErrorBody(errBody)}`);
|
|
58
59
|
}
|
|
59
60
|
const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: 30_000, signal }));
|
|
60
61
|
if (!json.data?.[0]?.embedding) {
|
|
@@ -94,7 +95,7 @@ export class RemoteEmbedder {
|
|
|
94
95
|
throw err;
|
|
95
96
|
return "";
|
|
96
97
|
});
|
|
97
|
-
throw new Error(`Embedding batch request failed (${response.status}): ${respBody}`);
|
|
98
|
+
throw new Error(`Embedding batch request failed (${response.status}): ${this.safeErrorBody(respBody)}`);
|
|
98
99
|
}
|
|
99
100
|
const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: 30_000, signal }));
|
|
100
101
|
if (!json.data || json.data.length !== batch.length) {
|
|
@@ -119,6 +120,21 @@ export class RemoteEmbedder {
|
|
|
119
120
|
}
|
|
120
121
|
return headers;
|
|
121
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Make a provider error body safe to embed in a thrown Error, matching the
|
|
125
|
+
* hardening llm/client.ts applies on the identical path: pattern-redact
|
|
126
|
+
* credential shapes, exact-scrub this connection's own key, and clip.
|
|
127
|
+
*
|
|
128
|
+
* These messages are durable — generateEmbeddingsForDb surfaces them as
|
|
129
|
+
* `embeddingResult.message`, which is written to semantic-status.json and
|
|
130
|
+
* replayed by `akm info` (including `--json`) until the next successful
|
|
131
|
+
* index, and printed on every vector-search attempt. Raw bodies reached that
|
|
132
|
+
* far unredacted and uncapped, at readBodyWithByteCap's 10 MB default.
|
|
133
|
+
*/
|
|
134
|
+
safeErrorBody(body) {
|
|
135
|
+
const resolvedKey = resolveSecret(this.config.apiKey);
|
|
136
|
+
return redactSensitiveText(redactErrorBody(body), resolvedKey ? [resolvedKey] : []);
|
|
137
|
+
}
|
|
122
138
|
}
|
|
123
139
|
/**
|
|
124
140
|
* L2-normalize a vector to unit length.
|
|
@@ -144,6 +160,15 @@ export function normalizeEmbeddingEndpoint(endpoint) {
|
|
|
144
160
|
if (normalizedPath.endsWith("/embeddings")) {
|
|
145
161
|
return parsed.toString();
|
|
146
162
|
}
|
|
163
|
+
// Ollama's NATIVE embedding route is `/api/embed` (and the older
|
|
164
|
+
// `/api/embeddings`). Appending "/embeddings" to it produced
|
|
165
|
+
// `/api/embed/embeddings`, a 404 — so pointing akm at the native endpoint,
|
|
166
|
+
// which is what its own options like ollamaOptions and contextLength are
|
|
167
|
+
// for, could never work. An explicit path that is already an embedding route
|
|
168
|
+
// is left alone.
|
|
169
|
+
if (normalizedPath.endsWith("/embed")) {
|
|
170
|
+
return parsed.toString();
|
|
171
|
+
}
|
|
147
172
|
parsed.pathname = normalizedPath ? `${normalizedPath}/embeddings` : "/embeddings";
|
|
148
173
|
return parsed.toString();
|
|
149
174
|
}
|
|
@@ -14,8 +14,28 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
|
+
import healthTemplate from "../assets/templates/html/health.html" with { type: "text" };
|
|
17
18
|
import { getDirname } from "../runtime.js";
|
|
18
19
|
const TEMPLATES_DIR = path.join(getDirname(import.meta.url), "../assets/templates/html");
|
|
20
|
+
/**
|
|
21
|
+
* Templates embedded at build time, keyed by command name.
|
|
22
|
+
*
|
|
23
|
+
* `bun build --compile` embeds only what is imported `with { type: "text" }`;
|
|
24
|
+
* a plain `readFileSync` from a path relative to `import.meta.url` resolves
|
|
25
|
+
* into the virtual `/$bunfs` tree and misses. `akm health --report --format
|
|
26
|
+
* html` therefore crashed with ENOENT (exit 70) on the standalone binary that
|
|
27
|
+
* the CLI's own install error and the CHANGELOG promote as the runtime-free
|
|
28
|
+
* option. The text import works on all three runtimes: natively on Bun, via
|
|
29
|
+
* scripts/node-runtime/text-import-hook.mjs on Node, and embedded in the
|
|
30
|
+
* compiled binary.
|
|
31
|
+
*/
|
|
32
|
+
const EMBEDDED_TEMPLATES = {
|
|
33
|
+
// bun-types declares `*.html` as an `HTMLBundle` (its HTML-bundler entrypoint
|
|
34
|
+
// feature), which is not what a `type: "text"` import yields — the value is
|
|
35
|
+
// the file's contents as a string on every runtime. The cast reconciles the
|
|
36
|
+
// ambient declaration with the actual import attribute.
|
|
37
|
+
health: healthTemplate,
|
|
38
|
+
};
|
|
19
39
|
/**
|
|
20
40
|
* Resolve the on-disk template path for a command's bespoke `<command>.html`.
|
|
21
41
|
* The command name is sanitized to a bare basename so a hostile command
|
|
@@ -37,9 +57,28 @@ const TOKEN_RE = /%%[A-Z_]+%%/g;
|
|
|
37
57
|
* matching the skill renderer's behaviour.
|
|
38
58
|
*/
|
|
39
59
|
export function renderHtml(templatePath, replacements) {
|
|
40
|
-
const html =
|
|
60
|
+
const html = readTemplate(templatePath);
|
|
41
61
|
return html.replace(TOKEN_RE, (token) => replacements[token] ?? token);
|
|
42
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Read a template from disk, falling back to the embedded copy.
|
|
65
|
+
*
|
|
66
|
+
* Disk stays primary so an operator (or a test) editing
|
|
67
|
+
* `src/assets/templates/html/<name>.html` sees the change without a rebuild.
|
|
68
|
+
* The fallback covers the standalone binary, where the file does not exist on
|
|
69
|
+
* any real filesystem.
|
|
70
|
+
*/
|
|
71
|
+
function readTemplate(templatePath) {
|
|
72
|
+
try {
|
|
73
|
+
return fs.readFileSync(templatePath, "utf8");
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const embedded = EMBEDDED_TEMPLATES[path.basename(templatePath, ".html")];
|
|
77
|
+
if (embedded !== undefined)
|
|
78
|
+
return embedded;
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
43
82
|
/**
|
|
44
83
|
* Minimal HTML entity escaping for text interpolated into templates. Escapes
|
|
45
84
|
* the single quote as well as the double quote so escaped values are safe in
|
|
@@ -16,9 +16,18 @@ function glyphFor(fixed) {
|
|
|
16
16
|
return { glyph: "✓", severityRank: 2 };
|
|
17
17
|
return { glyph: "⚠", severityRank: 1 };
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* `file:line` when the finding is line-anchored (workflow parse/compile
|
|
21
|
+
* errors), bare `file` otherwise. `LintIssue.line` is optional precisely
|
|
22
|
+
* because most lint sources are whole-file, so their headline is byte-identical
|
|
23
|
+
* to what it has always been.
|
|
24
|
+
*/
|
|
25
|
+
function locationOf(issue) {
|
|
26
|
+
return typeof issue.line === "number" ? `${issue.file}:${issue.line}` : issue.file;
|
|
27
|
+
}
|
|
19
28
|
function issueEntry(issue) {
|
|
20
29
|
const { glyph, severityRank } = glyphFor(issue.fixed);
|
|
21
|
-
return { severityRank, glyph, headline: `${issue
|
|
30
|
+
return { severityRank, glyph, headline: `${locationOf(issue)} [${issue.issue}] ${issue.detail}` };
|
|
22
31
|
}
|
|
23
32
|
function renderIssueSection(title, issues) {
|
|
24
33
|
if (issues.length === 0)
|
|
@@ -30,14 +39,18 @@ export function formatLintPlain(r) {
|
|
|
30
39
|
return null;
|
|
31
40
|
const fixed = Array.isArray(r.fixed) ? r.fixed : [];
|
|
32
41
|
const flagged = Array.isArray(r.flagged) ? r.flagged : [];
|
|
42
|
+
const warnings = Array.isArray(r.warnings) ? r.warnings : [];
|
|
33
43
|
const summary = r.summary;
|
|
34
44
|
const lines = [];
|
|
35
45
|
if (typeof r.ok === "boolean")
|
|
36
46
|
lines.push(`ok: ${r.ok}`);
|
|
37
|
-
lines.push(`summary: fixed=${summary?.fixed ?? fixed.length} flagged=${summary?.flagged ?? flagged.length}`
|
|
38
|
-
|
|
39
|
-
//
|
|
47
|
+
lines.push(`summary: fixed=${summary?.fixed ?? fixed.length} flagged=${summary?.flagged ?? flagged.length}` +
|
|
48
|
+
` warnings=${summary?.warnings ?? warnings.length}`);
|
|
49
|
+
// Flagged (still needs attention) surfaces before warnings (advisory,
|
|
50
|
+
// non-fatal) and fixed (already handled), so a scan of the output hits the
|
|
51
|
+
// actionable items first.
|
|
40
52
|
lines.push("", ...renderIssueSection("flagged", flagged));
|
|
53
|
+
lines.push("", ...renderIssueSection("warnings", warnings));
|
|
41
54
|
lines.push("", ...renderIssueSection("fixed", fixed));
|
|
42
55
|
return lines.join("\n").trim();
|
|
43
56
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -78,8 +78,17 @@ function nodeSpawnAdapter(cmd, options) {
|
|
|
78
78
|
detached: options.detached,
|
|
79
79
|
stdio: [stdioFor(options.stdin), stdioFor(options.stdout), stdioFor(options.stderr)],
|
|
80
80
|
});
|
|
81
|
+
// Node's 'exit' fires (null, signal) when the child dies from a signal.
|
|
82
|
+
// Resolving `code ?? 0` reported that as SUCCESS, so an OOM-killed or
|
|
83
|
+
// segfaulted child came back exit 0 with partial stdout. Bun resolves
|
|
84
|
+
// 128 + signum for the same case; match it so both runtimes agree and
|
|
85
|
+
// callers that only check `exitCode !== 0` classify signal deaths correctly.
|
|
86
|
+
let signalCode = null;
|
|
81
87
|
const exited = new Promise((resolve, reject) => {
|
|
82
|
-
child.once("exit", (code) =>
|
|
88
|
+
child.once("exit", (code, signal) => {
|
|
89
|
+
signalCode = signal;
|
|
90
|
+
resolve(code ?? (signal ? 128 + signalNumber(signal) : 0));
|
|
91
|
+
});
|
|
83
92
|
child.once("error", reject);
|
|
84
93
|
});
|
|
85
94
|
return {
|
|
@@ -96,12 +105,25 @@ function nodeSpawnAdapter(cmd, options) {
|
|
|
96
105
|
get exitCode() {
|
|
97
106
|
return child.exitCode;
|
|
98
107
|
},
|
|
108
|
+
get signalCode() {
|
|
109
|
+
return signalCode ?? child.signalCode;
|
|
110
|
+
},
|
|
99
111
|
pid: child.pid,
|
|
100
112
|
kill(signal) {
|
|
101
113
|
child.kill(signal);
|
|
102
114
|
},
|
|
103
115
|
};
|
|
104
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Map a signal name to its number so a signal death can be reported as the
|
|
119
|
+
* conventional 128 + signum exit status. Falls back to SIGKILL's 9 for a name
|
|
120
|
+
* this platform does not define, which still yields a non-zero status — the
|
|
121
|
+
* property that matters for classifying the run as failed.
|
|
122
|
+
*/
|
|
123
|
+
function signalNumber(signal) {
|
|
124
|
+
const { constants } = nodeRequire("node:os");
|
|
125
|
+
return constants.signals[signal] ?? 9;
|
|
126
|
+
}
|
|
105
127
|
// `node:stream`'s Writable.toWeb is available on Node >=17; referenced via the
|
|
106
128
|
// class to avoid a static import that Bun's typings may not expose.
|
|
107
129
|
function Writable_toWeb(w) {
|