akm-cli 0.9.3 → 0.9.5
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 +233 -1
- package/README.md +1 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/cli.js +5 -5
- package/dist/commands/health/improve-metrics.js +17 -0
- package/dist/commands/health/windows.js +2 -2
- package/dist/commands/health.js +2 -2
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/improve/preparation.js +8 -1
- package/dist/commands/lint/index.js +3 -7
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/tasks/tasks-cli.js +81 -3
- package/dist/commands/tasks/tasks.js +117 -2
- package/dist/core/adapter/adapters/akm-adapter.js +23 -14
- package/dist/core/adapter/adapters/akm-lint.js +3 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +9 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +13 -11
- package/dist/core/adapter/recognize-match.js +1 -20
- package/dist/core/asset/asset-placement.js +21 -2
- package/dist/core/common.js +21 -1
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +6 -6
- package/dist/core/improve-result.js +35 -14
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/lookup/adapter-concept-owner.js +6 -89
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/db-search.js +6 -0
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/indexer/walk/matchers.js +0 -22
- package/dist/output/shapes/helpers.js +19 -1
- package/dist/output/shapes/passthrough.js +18 -5
- package/dist/output/text/command-format.js +4 -0
- package/dist/registry/pinned-request-helper.js +2 -2
- package/dist/registry/pinned-transport.js +6 -6
- package/dist/scripts/akm-migrate-node.js +12678 -12601
- package/dist/scripts/akm-migrate.js +12678 -12601
- package/dist/storage/repositories/proposals-repository.js +65 -7
- package/dist/storage/repositories/task-history-repository.js +22 -10
- package/dist/tasks/run/task-history.js +23 -3
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +45 -0
- package/dist/tasks/scheduler-sync.js +77 -41
- package/dist/tasks/source/bounded-document.js +1 -1
- package/dist/tasks/source/parse-task-source.js +77 -11
- package/dist/tasks/source/task-source-v3-frozen.js +428 -0
- package/dist/tasks/source/task-to-v3.js +512 -0
- package/dist/tasks/source/task-to-v4.js +457 -0
- package/docs/reference/cli.md +33 -6
- package/docs/reference/configuration.md +27 -6
- package/docs/reference/tasks.md +10 -0
- package/package.json +4 -4
|
@@ -0,0 +1,457 @@
|
|
|
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
|
+
* Pure, byte-producing task-v3 to task-source-v4 migration planner (spec
|
|
6
|
+
* docs/plans/specs/p2b-input-bindings.md §1.3, §1.7 C-N1, §5). Mirrors
|
|
7
|
+
* `task-to-v3.ts`'s fail-closed ladder exactly: the INPUT side is read as a
|
|
8
|
+
* raw record by a vendored bounded-YAML reader (never the typed
|
|
9
|
+
* `parseTaskV3Yaml`, which would normalize away exactly the value bytes this
|
|
10
|
+
* migrator must preserve — a duration string like "5m", or a bare numeric
|
|
11
|
+
* `timeout`, would be converted to milliseconds by the real parser). The
|
|
12
|
+
* OUTPUT side is validated through the REAL `parseTaskSourceV4` before a
|
|
13
|
+
* "changed" outcome is ever handed back (C-N1, B-71).
|
|
14
|
+
*
|
|
15
|
+
* `inputs:` is never invented — the migrator translates structure, not
|
|
16
|
+
* intent (spec §5.3).
|
|
17
|
+
*/
|
|
18
|
+
import crypto from "node:crypto";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
|
|
21
|
+
import { assertBoundedTaskYamlDocument, TASK_V3_MAX_SOURCE_BYTES } from "./bounded-document.js";
|
|
22
|
+
import { classifyTaskV3Uses } from "./task-source-v3-frozen.js";
|
|
23
|
+
import { parseTaskSourceV4 } from "./task-source-v4.js";
|
|
24
|
+
/** The closed v3 top-level key set (`src/tasks/source-v3.ts`'s own, vendored — not exported there). */
|
|
25
|
+
const V3_TOP_LEVEL_KEYS = new Set([
|
|
26
|
+
"version",
|
|
27
|
+
"name",
|
|
28
|
+
"uses",
|
|
29
|
+
"run",
|
|
30
|
+
"with",
|
|
31
|
+
"env",
|
|
32
|
+
"shell",
|
|
33
|
+
"working-directory",
|
|
34
|
+
"akm",
|
|
35
|
+
"on",
|
|
36
|
+
]);
|
|
37
|
+
/** The closed v3 `akm.*` key set, vendored from `src/tasks/source-v3.ts`. */
|
|
38
|
+
const V3_AKM_KEYS = new Set([
|
|
39
|
+
"schedule",
|
|
40
|
+
"enabled",
|
|
41
|
+
"description",
|
|
42
|
+
"when_to_use",
|
|
43
|
+
"tags",
|
|
44
|
+
"agent",
|
|
45
|
+
"engine",
|
|
46
|
+
"model",
|
|
47
|
+
"inference",
|
|
48
|
+
"outputSchema",
|
|
49
|
+
"tools",
|
|
50
|
+
"timeout",
|
|
51
|
+
"redact",
|
|
52
|
+
"maxSteps",
|
|
53
|
+
"maxRetries",
|
|
54
|
+
]);
|
|
55
|
+
/** The closed v3 `on.*` key set, vendored from `src/tasks/source-v3.ts`. */
|
|
56
|
+
const V3_ON_KEYS = new Set(["schedule", "workflow_dispatch"]);
|
|
57
|
+
/** `akm.*` keys hoisted verbatim to the identical top-level v4 key (schedule/enabled handled separately). */
|
|
58
|
+
const AKM_HOIST_KEYS = [
|
|
59
|
+
"description",
|
|
60
|
+
"when_to_use",
|
|
61
|
+
"tags",
|
|
62
|
+
"agent",
|
|
63
|
+
"engine",
|
|
64
|
+
"model",
|
|
65
|
+
"inference",
|
|
66
|
+
"tools",
|
|
67
|
+
"timeout",
|
|
68
|
+
"redact",
|
|
69
|
+
"maxSteps",
|
|
70
|
+
"maxRetries",
|
|
71
|
+
];
|
|
72
|
+
function hash(bytes) {
|
|
73
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
function causeMessage(cause) {
|
|
76
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
77
|
+
}
|
|
78
|
+
function base(input) {
|
|
79
|
+
return {
|
|
80
|
+
filePath: input.filePath,
|
|
81
|
+
before: Buffer.from(input.bytes),
|
|
82
|
+
beforeHash: hash(input.bytes),
|
|
83
|
+
mode: input.mode,
|
|
84
|
+
writable: input.writable,
|
|
85
|
+
...(input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {}),
|
|
86
|
+
...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function blocked(input, reason, detail) {
|
|
90
|
+
return Object.freeze({ status: "blocked", ...base(input), reason, ...(detail ? { detail } : {}) });
|
|
91
|
+
}
|
|
92
|
+
function plainRecord(value, label) {
|
|
93
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
94
|
+
throw new Error(`${label} must be a mapping`);
|
|
95
|
+
}
|
|
96
|
+
const prototype = Object.getPrototypeOf(value);
|
|
97
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
98
|
+
throw new Error(`${label} must use a plain or null prototype`);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function exactString(value, label, nonempty = false) {
|
|
103
|
+
if (typeof value !== "string" || (nonempty && value.trim().length === 0)) {
|
|
104
|
+
throw new Error(`${label} must be ${nonempty ? "a non-empty " : "a "}string`);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Vendored raw-record reader (mirrors `task-to-v3.ts`'s `parseLegacyTaskYaml`
|
|
110
|
+
* exactly). Reading the RAW decoded record — rather than the typed
|
|
111
|
+
* `parseTaskV3Yaml` — keeps every field's original value bytes (a duration
|
|
112
|
+
* string, a bare millisecond integer, an env value's exact type) intact for
|
|
113
|
+
* verbatim re-emission; a typed v3 parse would normalize several of these
|
|
114
|
+
* away (C-N1).
|
|
115
|
+
*/
|
|
116
|
+
function parseV3RawYaml(input) {
|
|
117
|
+
if (input.bytes.byteLength > TASK_V3_MAX_SOURCE_BYTES) {
|
|
118
|
+
throw new Error(`task YAML exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) source resource limit`);
|
|
119
|
+
}
|
|
120
|
+
let source;
|
|
121
|
+
try {
|
|
122
|
+
source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new Error("task YAML contains invalid UTF-8 bytes");
|
|
126
|
+
}
|
|
127
|
+
const lineCounter = new LineCounter();
|
|
128
|
+
let document;
|
|
129
|
+
try {
|
|
130
|
+
document = parseDocument(source, { lineCounter, uniqueKeys: true });
|
|
131
|
+
}
|
|
132
|
+
catch (cause) {
|
|
133
|
+
throw new Error(`invalid YAML: ${causeMessage(cause)}`);
|
|
134
|
+
}
|
|
135
|
+
const [parseError] = document.errors;
|
|
136
|
+
if (parseError)
|
|
137
|
+
throw new Error(`invalid YAML: ${parseError.message.split("\n")[0]}`);
|
|
138
|
+
const [parseWarning] = document.warnings;
|
|
139
|
+
if (parseWarning)
|
|
140
|
+
throw new Error(`unsupported YAML construct: ${parseWarning.message}`);
|
|
141
|
+
assertBoundedTaskYamlDocument(document, {
|
|
142
|
+
filePath: input.filePath,
|
|
143
|
+
sourceLabel: "task v3 migration source",
|
|
144
|
+
lineCounter,
|
|
145
|
+
});
|
|
146
|
+
return { data: plainRecord(document.toJS({ maxAliasCount: 0 }), "task YAML"), source };
|
|
147
|
+
}
|
|
148
|
+
/** Convert one already-validated v3 raw record to final task source v4 bytes. */
|
|
149
|
+
function planV3DataToV4(input, data) {
|
|
150
|
+
const unknownTop = Object.keys(data).filter((key) => !V3_TOP_LEVEL_KEYS.has(key));
|
|
151
|
+
if (unknownTop.length > 0) {
|
|
152
|
+
return blocked(input, "invalid-v3-task", `unknown v3 field(s): ${unknownTop.join(", ")}`);
|
|
153
|
+
}
|
|
154
|
+
const hasUses = Object.hasOwn(data, "uses");
|
|
155
|
+
const hasRun = Object.hasOwn(data, "run");
|
|
156
|
+
if (hasUses === hasRun) {
|
|
157
|
+
return blocked(input, "invalid-v3-task", "requires exactly one executable selector: uses or run");
|
|
158
|
+
}
|
|
159
|
+
let akm;
|
|
160
|
+
if (Object.hasOwn(data, "akm")) {
|
|
161
|
+
try {
|
|
162
|
+
akm = plainRecord(data.akm, "akm");
|
|
163
|
+
}
|
|
164
|
+
catch (cause) {
|
|
165
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
166
|
+
}
|
|
167
|
+
const unknownAkm = Object.keys(akm).filter((key) => !V3_AKM_KEYS.has(key));
|
|
168
|
+
if (unknownAkm.length > 0) {
|
|
169
|
+
return blocked(input, "unrecognized-akm-member", `akm has unknown field(s): ${unknownAkm.join(", ")}`);
|
|
170
|
+
}
|
|
171
|
+
if (Object.hasOwn(akm, "enabled") && typeof akm.enabled !== "boolean") {
|
|
172
|
+
return blocked(input, "invalid-v3-task", "akm.enabled must be a boolean");
|
|
173
|
+
}
|
|
174
|
+
if (Object.hasOwn(akm, "schedule") && typeof akm.schedule !== "string") {
|
|
175
|
+
return blocked(input, "invalid-v3-task", "akm.schedule must be a string");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const hasOn = Object.hasOwn(data, "on");
|
|
179
|
+
let onRecord;
|
|
180
|
+
if (hasOn) {
|
|
181
|
+
try {
|
|
182
|
+
onRecord = plainRecord(data.on, "on");
|
|
183
|
+
}
|
|
184
|
+
catch (cause) {
|
|
185
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
186
|
+
}
|
|
187
|
+
// Mirrors the frozen v3 reader's own `parseOn` gates exactly
|
|
188
|
+
// (task-source-v3-frozen.ts:395-427): an empty `on: {}` declares no
|
|
189
|
+
// trigger at all, and `on.workflow_dispatch` accepts only null or an
|
|
190
|
+
// empty mapping (inputs are unsupported in v3). The frozen parser
|
|
191
|
+
// rejects both shapes outright, so this migrator must too — translating
|
|
192
|
+
// a document the v3 oracle would refuse to parse into runnable v4 bytes
|
|
193
|
+
// would launder invalid input into a valid, schedule-less task.
|
|
194
|
+
if (Object.keys(onRecord).length === 0) {
|
|
195
|
+
return blocked(input, "invalid-v3-task", "on must declare schedule and/or workflow_dispatch.");
|
|
196
|
+
}
|
|
197
|
+
const unknownOn = Object.keys(onRecord).filter((key) => !V3_ON_KEYS.has(key));
|
|
198
|
+
if (unknownOn.length > 0) {
|
|
199
|
+
return blocked(input, "invalid-v3-task", `on has unknown field(s): ${unknownOn.join(", ")}`);
|
|
200
|
+
}
|
|
201
|
+
if (Object.hasOwn(onRecord, "workflow_dispatch") && onRecord.workflow_dispatch !== null) {
|
|
202
|
+
let dispatchMapping;
|
|
203
|
+
try {
|
|
204
|
+
dispatchMapping = plainRecord(onRecord.workflow_dispatch, "on.workflow_dispatch");
|
|
205
|
+
}
|
|
206
|
+
catch (cause) {
|
|
207
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
208
|
+
}
|
|
209
|
+
if (Object.keys(dispatchMapping).length > 0) {
|
|
210
|
+
return blocked(input, "invalid-v3-task", "on.workflow_dispatch must be null or an empty mapping; inputs are unsupported.");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const hasAkmSchedule = akm !== undefined && Object.hasOwn(akm, "schedule");
|
|
215
|
+
if (hasAkmSchedule && hasOn) {
|
|
216
|
+
return blocked(input, "ambiguous-scheduling-source", "declares both akm.schedule and on:; task v3 requires exactly one scheduling source and the migrator will not guess which one wins.");
|
|
217
|
+
}
|
|
218
|
+
if (!hasAkmSchedule && !hasOn) {
|
|
219
|
+
return blocked(input, "invalid-v3-task", "requires exactly one scheduling source: akm.schedule or on.");
|
|
220
|
+
}
|
|
221
|
+
const hasWith = Object.hasOwn(data, "with");
|
|
222
|
+
let usesTarget;
|
|
223
|
+
if (hasUses) {
|
|
224
|
+
let usesValue;
|
|
225
|
+
try {
|
|
226
|
+
usesValue = exactString(data.uses, "uses", true);
|
|
227
|
+
}
|
|
228
|
+
catch (cause) {
|
|
229
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
usesTarget = classifyTaskV3Uses(usesValue);
|
|
233
|
+
}
|
|
234
|
+
catch (cause) {
|
|
235
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
236
|
+
}
|
|
237
|
+
if (usesTarget.kind === "github-action") {
|
|
238
|
+
return blocked(input, "github-action-target-removed", `"${usesValue}" is a github-action target; the github-action uses: variant was removed in task source v4. Use commands/, scripts/, workflows/, or akm/command instead.`);
|
|
239
|
+
}
|
|
240
|
+
if (hasWith && usesTarget.kind !== "builtin-command") {
|
|
241
|
+
return blocked(input, "with-on-non-command-target", `a with: block on "${usesValue}" (a non-akm/command target) has no task source v4 equivalent; task-call inputs are declared and bound separately.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else if (hasWith) {
|
|
245
|
+
return blocked(input, "invalid-v3-task", "with is legal only with uses");
|
|
246
|
+
}
|
|
247
|
+
if (!input.writable || input.onDiskWritable === false) {
|
|
248
|
+
return blocked(input, "read-only-source", !input.writable ? "the owning source is not writable" : "the source file or publication directory is read-only");
|
|
249
|
+
}
|
|
250
|
+
const enabledFalse = akm !== undefined && akm.enabled === false;
|
|
251
|
+
let scheduleField;
|
|
252
|
+
// Several independent translation facts can need reporting on the SAME
|
|
253
|
+
// file (a manual-only trigger AND a dropped output schema, say), so
|
|
254
|
+
// notices accumulate and are joined into the single `notice` string the
|
|
255
|
+
// outcome carries.
|
|
256
|
+
const notices = [];
|
|
257
|
+
if (hasAkmSchedule) {
|
|
258
|
+
const cron = akm.schedule;
|
|
259
|
+
scheduleField = enabledFalse ? [{ cron, enabled: false }] : cron;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
const rawSchedule = onRecord !== undefined && Object.hasOwn(onRecord, "schedule") ? onRecord.schedule : undefined;
|
|
263
|
+
if (rawSchedule !== undefined) {
|
|
264
|
+
if (!Array.isArray(rawSchedule) || rawSchedule.length === 0) {
|
|
265
|
+
return blocked(input, "invalid-v3-task", "on.schedule must be a non-empty list of {cron} records");
|
|
266
|
+
}
|
|
267
|
+
const crons = [];
|
|
268
|
+
for (const entry of rawSchedule) {
|
|
269
|
+
let record;
|
|
270
|
+
try {
|
|
271
|
+
record = plainRecord(entry, "on.schedule[]");
|
|
272
|
+
}
|
|
273
|
+
catch (cause) {
|
|
274
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
275
|
+
}
|
|
276
|
+
const keys = Object.keys(record);
|
|
277
|
+
if (keys.length !== 1 || keys[0] !== "cron" || typeof record.cron !== "string" || record.cron.length === 0) {
|
|
278
|
+
return blocked(input, "invalid-v3-task", "each on.schedule entry must be exactly {cron: <non-empty string>}");
|
|
279
|
+
}
|
|
280
|
+
crons.push(record.cron);
|
|
281
|
+
}
|
|
282
|
+
scheduleField = crons.map((cron) => (enabledFalse ? { cron, enabled: false } : { cron }));
|
|
283
|
+
}
|
|
284
|
+
else if (enabledFalse) {
|
|
285
|
+
return blocked(input, "enabled-false-has-no-schedule-entry", "akm.enabled: false has no schedule entry to attach to (the only trigger is on.workflow_dispatch); task source v4 has no top-level enabled flag.");
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
notices.push("schedule: is absent from the migrated document — the source's only trigger was on.workflow_dispatch (manual dispatch); task source v4 tasks are always runnable manually via `akm task run`, so no schedule: entry was emitted.");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const out = { version: 4 };
|
|
292
|
+
if (Object.hasOwn(data, "name"))
|
|
293
|
+
out.name = data.name;
|
|
294
|
+
if (hasUses)
|
|
295
|
+
out.uses = data.uses;
|
|
296
|
+
else
|
|
297
|
+
out.run = data.run;
|
|
298
|
+
if (Object.hasOwn(data, "shell"))
|
|
299
|
+
out.shell = data.shell;
|
|
300
|
+
if (hasWith)
|
|
301
|
+
out.with = data.with;
|
|
302
|
+
if (Object.hasOwn(data, "env"))
|
|
303
|
+
out.env = data.env;
|
|
304
|
+
if (Object.hasOwn(data, "working-directory"))
|
|
305
|
+
out["working-directory"] = data["working-directory"];
|
|
306
|
+
if (scheduleField !== undefined)
|
|
307
|
+
out.schedule = scheduleField;
|
|
308
|
+
if (akm) {
|
|
309
|
+
for (const key of AKM_HOIST_KEYS) {
|
|
310
|
+
if (Object.hasOwn(akm, key))
|
|
311
|
+
out[key] = akm[key];
|
|
312
|
+
}
|
|
313
|
+
// v3's `akm.outputSchema: null` means "no schema" (accepted verbatim by
|
|
314
|
+
// the frozen v3 reader, task-source-v3-frozen.ts:256-258); v4's
|
|
315
|
+
// `output:` has no null form (parseOutputSchema always requires a
|
|
316
|
+
// mapping). Omitting the key is the faithful v4 equivalent of an
|
|
317
|
+
// explicit v3 null — emitting `output: null` would fail the real
|
|
318
|
+
// parseTaskSourceV4 validation below and block the whole file.
|
|
319
|
+
if (Object.hasOwn(akm, "outputSchema") && akm.outputSchema !== null) {
|
|
320
|
+
// v4 accepts `output:` ONLY on a command target — `uses: commands/<ref>`
|
|
321
|
+
// or `uses: akm/command` (src/tasks/source/task-source-v4.ts's
|
|
322
|
+
// `targetConsumesOutputSchema`). v3 enforced no such rule: the frozen v3
|
|
323
|
+
// reader accepts `akm.outputSchema` on ANY target kind
|
|
324
|
+
// (task-source-v3-frozen.ts:256-263), and on `run:`/`uses: scripts/`/
|
|
325
|
+
// `uses: workflows/` it was equally inert there — nothing ever consumed
|
|
326
|
+
// it. Hoisting it unconditionally would therefore emit bytes the real
|
|
327
|
+
// parseTaskSourceV4 below rejects, blocking a valid, previously-runnable
|
|
328
|
+
// v3 file — and one blocked file aborts the whole plan
|
|
329
|
+
// (`applyTaskToV4MigrationPlan`, ./task-files-to-v4.ts). Dropping an
|
|
330
|
+
// already-inert field and SAYING SO is the faithful translation, and
|
|
331
|
+
// keeps spec row B-66 / §5.3's `changed` guarantee intact.
|
|
332
|
+
if (usesTarget !== undefined && (usesTarget.kind === "command" || usesTarget.kind === "builtin-command")) {
|
|
333
|
+
out.output = akm.outputSchema;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const targetLabel = usesTarget === undefined ? "a run: target" : `the "${usesTarget.ref}" target`;
|
|
337
|
+
notices.push(`akm.outputSchema was dropped rather than hoisted to output: — task source v4 accepts output: only with a command target (uses: commands/<ref> or uses: akm/command), and ${targetLabel} never consumed the schema in v3 either, so nothing enforceable was lost.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const afterYaml = stringifyYaml(out);
|
|
342
|
+
const after = Buffer.from(afterYaml, "utf8");
|
|
343
|
+
try {
|
|
344
|
+
parseTaskSourceV4({
|
|
345
|
+
yaml: afterYaml,
|
|
346
|
+
filePath: input.filePath,
|
|
347
|
+
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
catch (cause) {
|
|
351
|
+
return blocked(input, "generated-v4-validation-failed", causeMessage(cause));
|
|
352
|
+
}
|
|
353
|
+
const notice = notices.join(" ");
|
|
354
|
+
return Object.freeze({
|
|
355
|
+
status: "changed",
|
|
356
|
+
...base(input),
|
|
357
|
+
reason: "task-converted",
|
|
358
|
+
after,
|
|
359
|
+
afterHash: hash(after),
|
|
360
|
+
...(notice ? { notice } : {}),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/** Plan exactly one source file without touching disk. */
|
|
364
|
+
export function planTaskToV4File(input) {
|
|
365
|
+
let data;
|
|
366
|
+
let source;
|
|
367
|
+
try {
|
|
368
|
+
({ data, source } = parseV3RawYaml(input));
|
|
369
|
+
}
|
|
370
|
+
catch (cause) {
|
|
371
|
+
return blocked(input, "invalid-task-yaml", causeMessage(cause));
|
|
372
|
+
}
|
|
373
|
+
if (data.version === 4) {
|
|
374
|
+
try {
|
|
375
|
+
parseTaskSourceV4({
|
|
376
|
+
yaml: source,
|
|
377
|
+
filePath: input.filePath,
|
|
378
|
+
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
379
|
+
});
|
|
380
|
+
return Object.freeze({ status: "skipped", ...base(input), reason: "already-v4" });
|
|
381
|
+
}
|
|
382
|
+
catch (cause) {
|
|
383
|
+
return blocked(input, "invalid-v4-task", causeMessage(cause));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Not this generation's document to validate — v2 grammar is entirely
|
|
387
|
+
// generation 1's domain (task-to-v3.ts). Reported skipped, not blocked
|
|
388
|
+
// (see TaskToV4Skipped's own header).
|
|
389
|
+
if (data.version === 2) {
|
|
390
|
+
return Object.freeze({
|
|
391
|
+
status: "skipped",
|
|
392
|
+
...base(input),
|
|
393
|
+
reason: "pending-v2-to-v3-migration",
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (data.version !== 3) {
|
|
397
|
+
return blocked(input, "unsupported-task-version", `expected version 2, 3, or 4, got ${String(data.version)}`);
|
|
398
|
+
}
|
|
399
|
+
return planV3DataToV4(input, data);
|
|
400
|
+
}
|
|
401
|
+
function generationFor(files) {
|
|
402
|
+
const digest = crypto.createHash("sha256");
|
|
403
|
+
digest.update("akm-task-to-v4-plan-v1\0");
|
|
404
|
+
for (const file of files) {
|
|
405
|
+
digest.update(file.filePath);
|
|
406
|
+
digest.update("\0");
|
|
407
|
+
digest.update(file.status);
|
|
408
|
+
digest.update("\0");
|
|
409
|
+
digest.update(file.reason);
|
|
410
|
+
digest.update("\0");
|
|
411
|
+
digest.update(String(file.mode));
|
|
412
|
+
digest.update("\0");
|
|
413
|
+
digest.update(file.writable ? "writable" : "read-only");
|
|
414
|
+
digest.update("\0");
|
|
415
|
+
digest.update(file.onDiskWritable === false ? "disk-read-only" : "disk-writable-or-unspecified");
|
|
416
|
+
digest.update("\0");
|
|
417
|
+
if (file.containmentRoot)
|
|
418
|
+
digest.update(file.containmentRoot);
|
|
419
|
+
digest.update("\0");
|
|
420
|
+
digest.update(file.beforeHash);
|
|
421
|
+
digest.update("\0");
|
|
422
|
+
if (file.status === "changed")
|
|
423
|
+
digest.update(file.afterHash);
|
|
424
|
+
digest.update("\0");
|
|
425
|
+
if (file.detail)
|
|
426
|
+
digest.update(file.detail);
|
|
427
|
+
digest.update("\0");
|
|
428
|
+
if (file.status === "changed" && file.notice)
|
|
429
|
+
digest.update(file.notice);
|
|
430
|
+
digest.update("\0");
|
|
431
|
+
}
|
|
432
|
+
return digest.digest("hex");
|
|
433
|
+
}
|
|
434
|
+
/** Build/fingerprint a plan from already-derived immutable outcomes. */
|
|
435
|
+
export function taskToV4PlanFromOutcomes(outcomes) {
|
|
436
|
+
const files = [...outcomes].sort((left, right) => left.filePath < right.filePath ? -1 : left.filePath > right.filePath ? 1 : 0);
|
|
437
|
+
for (let index = 1; index < files.length; index += 1) {
|
|
438
|
+
const previous = files[index - 1];
|
|
439
|
+
const current = files[index];
|
|
440
|
+
if (previous && current && path.resolve(previous.filePath) === path.resolve(current.filePath)) {
|
|
441
|
+
throw new Error(`duplicate task migration file path: ${current.filePath}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return Object.freeze({ schemaVersion: 1, generation: generationFor(files), files: Object.freeze(files) });
|
|
445
|
+
}
|
|
446
|
+
/** Plan a complete, stable file set. Input order cannot change the result. */
|
|
447
|
+
export function planTaskToV4Migration(inputs) {
|
|
448
|
+
const sorted = [...inputs].sort((left, right) => left.filePath < right.filePath ? -1 : left.filePath > right.filePath ? 1 : 0);
|
|
449
|
+
let previous;
|
|
450
|
+
for (const current of sorted) {
|
|
451
|
+
if (previous && path.resolve(previous.filePath) === path.resolve(current.filePath)) {
|
|
452
|
+
throw new Error(`duplicate task migration file path: ${current.filePath}`);
|
|
453
|
+
}
|
|
454
|
+
previous = current;
|
|
455
|
+
}
|
|
456
|
+
return taskToV4PlanFromOutcomes(sorted.map(planTaskToV4File));
|
|
457
|
+
}
|
package/docs/reference/cli.md
CHANGED
|
@@ -393,6 +393,11 @@ availability:
|
|
|
393
393
|
- **`origin`** -- The source bundle (e.g. `npm:@scope/pkg`), present only for
|
|
394
394
|
managed source assets; surfaced at `full` only
|
|
395
395
|
- **`id`** -- Registry-level identifier (registry hits only)
|
|
396
|
+
- **`matchStage`** -- Which stage of the progressive AND->OR lexical search
|
|
397
|
+
ladder produced the hit: `exact` (strict AND), `prefix` (prefix AND), or
|
|
398
|
+
`relaxed` (OR/prefix-OR recovery). Omitted for hits with no FTS component
|
|
399
|
+
(e.g. a pure-semantic hybrid match) and for registry hits; surfaced at
|
|
400
|
+
`normal`, `full`, and `--shape agent`
|
|
396
401
|
|
|
397
402
|
The default brief shape is intentionally small. The exact field set per
|
|
398
403
|
detail level (and per `--shape`) is authoritative in
|
|
@@ -402,9 +407,9 @@ assembled into the shape registry by the `src/output/shapes.ts` barrel:
|
|
|
402
407
|
| Level | Local bundle hits | Registry hits |
|
|
403
408
|
| --- | --- | --- |
|
|
404
409
|
| `brief` (default) | `type`, `name`, `ref`, `action`, `estimatedTokens` | `name`, `installRef`, `score` |
|
|
405
|
-
| `normal` | `type`, `name`, `description`, `action`, `score`, `estimatedTokens`, optional `warnings`/`quality`/`keys` | `name`, `description`, `action`, `installRef`, `score`, optional `warnings` |
|
|
406
|
-
| `full` | full hit object (includes `ref`, `origin`, `tags`, `whyMatched`, optional `warnings`, optional `quality`, timings, bundle metadata) | full hit object |
|
|
407
|
-
| `--shape agent` | `name`, `ref`, `type`, `path`, `editable`, conditional `editHint`, `description`, `action`, `score`, optional `estimatedTokens`/`keys` | no local access fields |
|
|
410
|
+
| `normal` | `type`, `name`, `description`, `action`, `score`, `estimatedTokens`, optional `warnings`/`quality`/`keys`/`matchStage` | `name`, `description`, `action`, `installRef`, `score`, optional `warnings` |
|
|
411
|
+
| `full` | full hit object (includes `ref`, `origin`, `tags`, `whyMatched`, optional `warnings`, optional `quality`, optional `matchStage`, timings, bundle metadata) | full hit object |
|
|
412
|
+
| `--shape agent` | `name`, `ref`, `type`, `path`, `editable`, conditional `editHint`, `description`, `action`, `score`, optional `estimatedTokens`/`keys`/`matchStage` | no local access fields |
|
|
408
413
|
|
|
409
414
|
`--shape summary` is **not valid on `search`** — see
|
|
410
415
|
[`--shape summary`](#--shape-summary) above; it is a usage error (exit 2)
|
|
@@ -2418,9 +2423,10 @@ shell commands. It manages on-disk task definitions under
|
|
|
2418
2423
|
(cron / launchd / schtasks). Task source v4 YAML (`version: 4`) is the only
|
|
2419
2424
|
executable source contract this release accepts; `akm task add` writes v4 —
|
|
2420
2425
|
see the canonical [Tasks reference](tasks.md). The
|
|
2421
|
-
group is `add | run | explain | sync | doctor | history` — there is
|
|
2422
|
-
or `remove`; use `akm search --type task` / `akm show tasks/<id>`
|
|
2423
|
-
and edit the file + `akm task sync` to change or remove a
|
|
2426
|
+
group is `add | run | explain | sync | doctor | history | prune` — there is
|
|
2427
|
+
no `list` or `remove`; use `akm search --type task` / `akm show tasks/<id>`
|
|
2428
|
+
to inspect, and edit the file + `akm task sync` to change or remove a
|
|
2429
|
+
schedule.
|
|
2424
2430
|
|
|
2425
2431
|
```sh
|
|
2426
2432
|
akm search --type task # List tasks (cross-bundle)
|
|
@@ -2434,8 +2440,12 @@ akm task run <id> # Execute now (what the scheduler ca
|
|
|
2434
2440
|
akm task explain <ref> # Read-only: declared inputs, target, schedule — spawns nothing
|
|
2435
2441
|
akm task history [--id <id>] [--limit <n>] # Recent runs from state.db
|
|
2436
2442
|
akm task sync # Reconcile on-disk YAML with scheduler
|
|
2443
|
+
akm task sync --dry-run # Preview the reconcile — zero scheduler writes
|
|
2437
2444
|
akm task sync --rebind # Also capture the current installed runtime
|
|
2438
2445
|
akm task doctor # Report scheduler backend + paths
|
|
2446
|
+
akm task prune # Preview orphaned scheduler entries — zero writes
|
|
2447
|
+
akm task prune --yes # Remove every currently-computed orphan
|
|
2448
|
+
akm task prune --id ghost,stale --yes # Remove only the named orphan ids
|
|
2439
2449
|
```
|
|
2440
2450
|
|
|
2441
2451
|
`task add` also accepts `--disabled` (register but leave off in the OS
|
|
@@ -2461,6 +2471,23 @@ schedule-binding) and run `akm task sync`. To remove one, delete its file
|
|
|
2461
2471
|
(`<bundle>/tasks/<id>.yml`) and run `akm task sync` — sync uninstalls the
|
|
2462
2472
|
orphaned scheduler entry.
|
|
2463
2473
|
|
|
2474
|
+
`akm task sync --dry-run` prints the planned adds/updates/removes (removals
|
|
2475
|
+
carry their owning bundle) without touching the scheduler — zero writes.
|
|
2476
|
+
Exits non-zero when removals are pending, so it can gate a CI/health check
|
|
2477
|
+
on "sync would change something."
|
|
2478
|
+
|
|
2479
|
+
`akm task prune` reclaims installed scheduler entries that `sync` can never
|
|
2480
|
+
clean up on its own: entries whose own `--scheduler-context` descriptor no
|
|
2481
|
+
longer resolves to a live bundle (a corrupt/missing descriptor, or the
|
|
2482
|
+
bundle directory it pointed at is gone). It never touches an entry that
|
|
2483
|
+
still resolves to a live bundle — that's `sync`'s job. Like `sync
|
|
2484
|
+
--dry-run`, the default is a dry-run preview (zero scheduler writes) that
|
|
2485
|
+
exits non-zero when there are candidates to remove; `--yes` executes the
|
|
2486
|
+
printed plan, and `--id <id1,id2,...>` narrows a `--yes` run (or a preview)
|
|
2487
|
+
to specific binding ids — naming an id that isn't a current orphan
|
|
2488
|
+
candidate (not installed, or it still resolves to a live bundle) is
|
|
2489
|
+
refused with a usage error and removes nothing.
|
|
2490
|
+
|
|
2464
2491
|
Scheduler activation captures the installed akm runtime. Ordinary `task sync`
|
|
2465
2492
|
reconciles definitions, schedules, and enabled state while preserving that
|
|
2466
2493
|
runtime binding. Use `task sync --rebind` only after intentionally moving or
|
|
@@ -7,12 +7,33 @@ directory. Project `.akm/config.json` files are not merged.
|
|
|
7
7
|
|
|
8
8
|
## Version 0.9
|
|
9
9
|
|
|
10
|
-
A present configuration file must set `configVersion` to
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
A present configuration file must set `configVersion` to a version this
|
|
11
|
+
binary knows: the current `"0.9.0"`, or a known older version it can
|
|
12
|
+
auto-upgrade in memory (see "Version read shim" below). Missing, newer,
|
|
13
|
+
numeric, and any other unrecognized version are rejected by ordinary
|
|
14
|
+
commands without rewriting the file — an older binary never guesses at a
|
|
15
|
+
newer, unknown shape. Pre-0.9 config and database layouts are not runtime
|
|
16
|
+
inputs and are not migrated by `akm upgrade`. Configure the current schema
|
|
17
|
+
directly. The standalone migrator exists only for explicit task migration:
|
|
18
|
+
task v2 to task v3, then task v3 to task source v4, in one pass.
|
|
19
|
+
|
|
20
|
+
### Version read shim
|
|
21
|
+
|
|
22
|
+
Like the task-source v2/v3 auto-shim (`akm migrate apply`'s in-memory
|
|
23
|
+
counterpart, documented under Migration below), a known older `configVersion`
|
|
24
|
+
is converted to the current shape in memory on load — with a one-line stderr
|
|
25
|
+
deprecation warning — rather than hard-failing every command. Nothing is
|
|
26
|
+
written back to disk by the shim itself; the very next config-mutating
|
|
27
|
+
command (`akm config set`, etc.) persists the upgrade for free, since every
|
|
28
|
+
config write already forces `configVersion` to the current value, which
|
|
29
|
+
silences the warning. A `configVersion` this binary does not recognize at
|
|
30
|
+
all — including anything newer than current — still fails closed with
|
|
31
|
+
`UNSUPPORTED_CONFIG_VERSION`.
|
|
32
|
+
|
|
33
|
+
As of this writing `"0.9.0"` is the only `configVersion` akm has ever
|
|
34
|
+
shipped, so there is no real older shape for the shim to convert yet; the
|
|
35
|
+
mechanism (`src/core/config/config-version-shim.ts`) is established ahead of
|
|
36
|
+
the first bump that will need it, per #863.
|
|
16
37
|
|
|
17
38
|
```jsonc
|
|
18
39
|
{
|
package/docs/reference/tasks.md
CHANGED
|
@@ -389,6 +389,16 @@ for full before/after examples and recovery guidance.
|
|
|
389
389
|
- Disable a binding by editing the source and syncing: set that schedule
|
|
390
390
|
entry's own `enabled: false`.
|
|
391
391
|
- Delete the `.yml` source and sync to remove its derived binding(s).
|
|
392
|
+
- `akm task sync --dry-run` previews the reconcile (adds/updates/removes,
|
|
393
|
+
removals annotated with their owning bundle) without writing to the
|
|
394
|
+
scheduler; exits non-zero when removals are pending.
|
|
395
|
+
- `akm task prune` removes installed scheduler entries `sync` cannot reach
|
|
396
|
+
because their own descriptor no longer resolves to a live bundle
|
|
397
|
+
(corrupt/missing `--scheduler-context`, or the owning bundle directory is
|
|
398
|
+
gone). It never touches an entry that still resolves to a live bundle.
|
|
399
|
+
Defaults to a dry-run preview (zero writes); `--yes` executes it; `--id
|
|
400
|
+
<id1,id2,...>` scopes to specific ids and refuses any id that isn't a
|
|
401
|
+
current orphan candidate.
|
|
392
402
|
- Use `akm task sync --rebind` only when deliberately changing the captured
|
|
393
403
|
AKM runtime, then verify with `akm task doctor`.
|
|
394
404
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "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 Cursor.",
|
|
6
6
|
"keywords": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"license": "MPL-2.0",
|
|
37
37
|
"pinNotes": {
|
|
38
38
|
"@opencode-ai/sdk@1.2.20": "Exact pin. The SDK surface we use (createOpencodeClient + session.create/prompt/delete in src/integrations/harnesses/opencode-sdk/sdk-runner.ts) is stable across 1.x, but the SDK has shipped 5+ minor versions of unrelated provider/registry churn. akm-cli is a global CLI install so the pin is isolated from user-project deps. Re-test sdk-runner before bumping. Note this package is an HTTP client only — it declares no dependencies and its own createOpencodeServer spawns `opencode serve` — so it does NOT make the opencode binary available; that install is separate and is what every SDK-path probe checks for.",
|
|
39
|
-
"better-sqlite3@12.11.1": "Exact pin (#790). This is the SQLite driver akm loads on Node (src/storage/database.ts); Bun never touches it. The pin is about PREBUILT BINARIES, not API surface. better-sqlite3 ships one prebuild per Node ABI as a GitHub release asset and its install script is `prebuild-install || node-gyp rebuild` — so any (version, Node ABI) pair with no prebuild silently COMPILES FROM SOURCE against the headers of whatever Node is on the machine that day. The 11.x line predates Node 24 and declares no `engines` at all: its newest release (11.10.0, 2025-05-08) publishes ABI 108/115/127/131 (Node 18/20/22/23) and nothing for ABI 137 (Node 24). Node 24.19.0 then changed the public `node_object_wrap.h` so `node::ObjectWrap`'s ctor/dtor register and unregister an environment cleanup hook; a from-source 11.x build against those headers aborts at teardown in `Statement::~Statement()` with `RemoveEnvironmentCleanupHook ... Assertion (env) != nullptr`, intermittently, depending on GC timing. 12.11.1 publishes ABI 127/137/141/147 (Node 22/24/25/26) and declares `engines: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x`, so
|
|
39
|
+
"better-sqlite3@12.11.1": "Exact pin (#790). This is the SQLite driver akm loads on Node (src/storage/database.ts); Bun never touches it. The pin is about PREBUILT BINARIES, not API surface. better-sqlite3 ships one prebuild per Node ABI as a GitHub release asset and its install script is `prebuild-install || node-gyp rebuild` — so any (version, Node ABI) pair with no prebuild silently COMPILES FROM SOURCE against the headers of whatever Node is on the machine that day. The 11.x line predates Node 24 and declares no `engines` at all: its newest release (11.10.0, 2025-05-08) publishes ABI 108/115/127/131 (Node 18/20/22/23) and nothing for ABI 137 (Node 24). Node 24.19.0 then changed the public `node_object_wrap.h` so `node::ObjectWrap`'s ctor/dtor register and unregister an environment cleanup hook; a from-source 11.x build against those headers aborts at teardown in `Statement::~Statement()` with `RemoveEnvironmentCleanupHook ... Assertion (env) != nullptr`, intermittently, depending on GC timing. 12.11.1 publishes ABI 127/137/141/147 (Node 22/24/25/26) and declares `engines: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x`, so every Node akm supports installs a prebuilt binary and never compiles. Before bumping: confirm the target version publishes a prebuild for EVERY Node major in `engines` (probe https://github.com/WiseLibs/better-sqlite3/releases/download/vX.Y.Z/better-sqlite3-vX.Y.Z-node-vABI-linux-x64.tar.gz), not just that the version is newer. 13.x is the eventual destination — it moved to node-addon-api/N-API with prebuilds bundled in the npm tarball and no install script, which retires this failure mode entirely — but it is a fresh major rewrite of the binding, so it wants its own soak, not a patch release. The CI node-smoke job installs this exact string by reading it back out of this file (.github/workflows/ci.yml), so the two cannot drift.",
|
|
40
40
|
"@huggingface/transformers@4.2.0": "Exact semantic-search dependency pin. Re-run the real-model semantic gate before changing it."
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"akm-migrate": "dist/akm-migrate"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
|
-
"preinstall": "node -e \"var v=(process.versions.node||'0').split('.').map(function(n){return parseInt(n,10)||0});var ok=v[0]>=
|
|
69
|
+
"preinstall": "node -e \"var v=(process.versions.node||'0').split('.').map(function(n){return parseInt(n,10)||0});var ok=v[0]>=22;if(ok){process.exit(0)}console.error('\\n ERROR: the akm-cli npm package requires Node.js >= 22.\\n A working Bun >= 1.0 on PATH is optional and preferred for akm and akm-migrate.\\n Upgrade Node.js (https://nodejs.org), or install the runtime-free standalone binary:\\n curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\\n');process.exit(1)\"",
|
|
70
70
|
"build": "rm -rf dist && bun scripts/gen-config-schema.ts &&bun run tsc --project ./tsconfig.build.json && bun scripts/copy-assets.ts && bun scripts/fix-esm-extensions.ts",
|
|
71
71
|
"check": "bun run lint && bunx tsc --noEmit && bun run test:unit && bun run test:integration",
|
|
72
72
|
"check:fast": "bun run lint && bunx tsc --noEmit && bun run test:unit",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"sqlite-vec": "^0.1.9"
|
|
115
115
|
},
|
|
116
116
|
"engines": {
|
|
117
|
-
"node": ">=
|
|
117
|
+
"node": ">=22"
|
|
118
118
|
},
|
|
119
119
|
"dependencies": {
|
|
120
120
|
"@clack/prompts": "^1.3.0",
|