akm-cli 0.7.4 → 0.8.0-rc.3
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 → .github/CHANGELOG.md} +34 -1
- package/.github/LICENSE +374 -0
- package/dist/cli/parse-args.js +86 -0
- package/dist/cli.js +1223 -650
- package/dist/commands/agent-dispatch.js +107 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +812 -0
- package/dist/commands/curate.js +1 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +224 -39
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +12 -24
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +3 -30
- package/dist/commands/improve.js +1161 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +291 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +145 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/vault-key-rules.js +67 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/migration-help.js +2 -2
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +106 -43
- package/dist/commands/reflect.js +167 -41
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +55 -1
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +135 -55
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +173 -87
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +5 -17
- package/dist/core/asset-spec.js +11 -1
- package/dist/core/common.js +100 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +240 -127
- package/dist/core/events.js +87 -123
- package/dist/core/frontmatter.js +0 -6
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +731 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +86 -472
- package/dist/indexer/db.js +418 -59
- package/dist/indexer/ensure-index.js +133 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +417 -74
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +480 -298
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/matchers.js +124 -160
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +196 -197
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/builders.js +109 -0
- package/dist/integrations/agent/config.js +203 -3
- package/dist/integrations/agent/index.js +5 -2
- package/dist/integrations/agent/model-aliases.js +63 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +114 -29
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +158 -34
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +63 -86
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -64
- package/dist/llm/memory-infer.js +52 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -309
- package/dist/output/renderers.js +226 -257
- package/dist/output/shapes.js +109 -96
- package/dist/output/text.js +274 -36
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/registry/resolve.js +8 -16
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/filesystem.js +16 -23
- package/dist/sources/providers/git.js +45 -4
- package/dist/sources/providers/website.js +15 -22
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/db.js +9 -0
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +73 -88
- package/dist/workflows/scope-key.js +76 -0
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +5 -2
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.4.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +20 -0
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +4 -3
- package/dist/templates/wiki-templates.js +0 -100
package/dist/commands/vault.js
CHANGED
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
* the indexer, the `akm show` renderer, or any structured output channel.
|
|
6
6
|
* The supported load paths are:
|
|
7
7
|
*
|
|
8
|
-
* - `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* file, and emits `. <tmp>; rm -f <tmp>` on stdout. Values reach bash
|
|
12
|
-
* only via the temp file, never via akm's stdout.
|
|
13
|
-
* - `injectIntoEnv(vaultPath, target)` — programmatic API for modules that
|
|
14
|
-
* need values in a process environment.
|
|
8
|
+
* - `source "$(akm vault path vault:<name>)"` — direct shell loading path.
|
|
9
|
+
* - `injectIntoEnv(vaultPath, target)` / `loadEnv(vaultPath)` — programmatic
|
|
10
|
+
* APIs for modules that need values in process memory.
|
|
15
11
|
*
|
|
16
12
|
* Value parsing is delegated to the `dotenv` package — we deliberately do not
|
|
17
13
|
* implement our own quoting/escaping rules for security-sensitive content.
|
|
@@ -19,6 +15,98 @@
|
|
|
19
15
|
import fs from "node:fs";
|
|
20
16
|
import path from "node:path";
|
|
21
17
|
import dotenv from "dotenv";
|
|
18
|
+
import { writeFileAtomic } from "../core/common";
|
|
19
|
+
// ── Write-lock helper ─────────────────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Acquire an exclusive lock file for the given vault path, execute `fn`, then
|
|
22
|
+
* release the lock. Uses O_EXCL creation so two concurrent writers cannot
|
|
23
|
+
* both acquire the lock simultaneously (POSIX atomic guarantee).
|
|
24
|
+
*
|
|
25
|
+
* Retry strategy: if the lock is held we spin for up to 5 s, yielding via
|
|
26
|
+
* `Bun.sleepSync` when available (avoids burning 100 % CPU), otherwise falling
|
|
27
|
+
* back to a busy-wait counter. After the deadline we throw rather than
|
|
28
|
+
* silently proceeding — a timeout here is always a programming error or a
|
|
29
|
+
* stale lock left by a crashed process.
|
|
30
|
+
*
|
|
31
|
+
* Stale lock detection: the current PID is written into the lock file on
|
|
32
|
+
* acquisition. When a lock-acquire attempt fails, the PID stored in the
|
|
33
|
+
* existing lock file is read and tested with `process.kill(pid, 0)`. If the
|
|
34
|
+
* signal call throws (ESRCH — no such process), the lock is stale and is
|
|
35
|
+
* deleted immediately so the next loop iteration can acquire it. If the
|
|
36
|
+
* process is still alive the retry loop continues normally.
|
|
37
|
+
*/
|
|
38
|
+
function withVaultLock(vaultPath, fn) {
|
|
39
|
+
const lockPath = `${vaultPath}.lock`;
|
|
40
|
+
const deadline = Date.now() + 5000;
|
|
41
|
+
let fd = -1;
|
|
42
|
+
while (true) {
|
|
43
|
+
try {
|
|
44
|
+
fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
|
|
45
|
+
// Write our PID so staleness can be detected by other waiters
|
|
46
|
+
fs.writeSync(fd, String(process.pid));
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
if (Date.now() > deadline) {
|
|
51
|
+
throw new Error(`Could not acquire vault lock for ${vaultPath} after 5s. Is another process holding it?`);
|
|
52
|
+
}
|
|
53
|
+
// Check for stale lock: read PID and test if that process is alive
|
|
54
|
+
try {
|
|
55
|
+
const rawPid = fs.readFileSync(lockPath, "utf8").trim();
|
|
56
|
+
const pid = Number.parseInt(rawPid, 10);
|
|
57
|
+
if (!Number.isNaN(pid) && pid > 0) {
|
|
58
|
+
try {
|
|
59
|
+
process.kill(pid, 0); // throws ESRCH if process doesn't exist
|
|
60
|
+
// PID is alive — legitimate holder, wait
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Process is dead — stale lock, remove and retry immediately
|
|
64
|
+
try {
|
|
65
|
+
fs.unlinkSync(lockPath);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* ignore */
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* lock file unreadable, keep waiting */
|
|
76
|
+
}
|
|
77
|
+
// Yield before next attempt
|
|
78
|
+
if (typeof globalThis.Bun
|
|
79
|
+
?.sleepSync === "function") {
|
|
80
|
+
globalThis.Bun.sleepSync(10);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
let spin = 0;
|
|
84
|
+
while (spin++ < 100_000) {
|
|
85
|
+
/* yield */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
return fn();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (fd !== -1) {
|
|
95
|
+
try {
|
|
96
|
+
fs.closeSync(fd);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* ignore */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
fs.unlinkSync(lockPath);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
22
110
|
/** Matches a KEY=value assignment line, capturing only the key. */
|
|
23
111
|
const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
24
112
|
/** Scan lines and return KEY names in file order, without duplicates. */
|
|
@@ -144,8 +232,7 @@ export function injectIntoEnv(vaultPath, target = process.env) {
|
|
|
144
232
|
* non-assignment content, so sourcing the output is safe regardless of what
|
|
145
233
|
* the vault file contains.
|
|
146
234
|
*
|
|
147
|
-
*
|
|
148
|
-
* temp file and emits only the path (never values) on stdout.
|
|
235
|
+
* Retained for programmatic callers/tests that need a literal export script.
|
|
149
236
|
*/
|
|
150
237
|
export function buildShellExportScript(vaultPath) {
|
|
151
238
|
const env = loadEnv(vaultPath);
|
|
@@ -180,91 +267,112 @@ export function buildShellExportScript(vaultPath) {
|
|
|
180
267
|
*/
|
|
181
268
|
export function setKey(vaultPath, key, value, comment) {
|
|
182
269
|
validateKeyName(key);
|
|
270
|
+
if (comment !== undefined && /[\r\n]/.test(comment)) {
|
|
271
|
+
throw new Error("Vault key comment cannot contain newline characters.");
|
|
272
|
+
}
|
|
183
273
|
ensureParentDir(vaultPath);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
274
|
+
withVaultLock(vaultPath, () => {
|
|
275
|
+
const existing = fs.existsSync(vaultPath) ? fs.readFileSync(vaultPath, "utf8") : "";
|
|
276
|
+
const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
|
|
277
|
+
const formatted = `${key}=${quoteValue(value)}`;
|
|
278
|
+
let replaced = false;
|
|
279
|
+
for (let i = 0; i < lines.length; i++) {
|
|
280
|
+
const m = lines[i].match(ASSIGN_RE);
|
|
281
|
+
if (m && m[1] === key) {
|
|
282
|
+
lines[i] = formatted;
|
|
283
|
+
replaced = true;
|
|
284
|
+
if (comment !== undefined) {
|
|
285
|
+
const commentLine = `# ${comment}`;
|
|
286
|
+
const prevIsComment = i > 0 && lines[i - 1].trimStart().startsWith("#");
|
|
287
|
+
if (prevIsComment) {
|
|
288
|
+
lines[i - 1] = commentLine;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
lines.splice(i, 0, commentLine);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!replaced) {
|
|
193
298
|
if (comment !== undefined) {
|
|
194
299
|
const commentLine = `# ${comment}`;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
lines
|
|
300
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
301
|
+
lines[lines.length - 1] = commentLine;
|
|
302
|
+
lines.push(formatted);
|
|
303
|
+
lines.push("");
|
|
198
304
|
}
|
|
199
305
|
else {
|
|
200
|
-
lines.
|
|
306
|
+
lines.push(commentLine);
|
|
307
|
+
lines.push(formatted);
|
|
201
308
|
}
|
|
202
309
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
if (!replaced) {
|
|
207
|
-
if (comment !== undefined) {
|
|
208
|
-
const commentLine = `# ${comment}`;
|
|
209
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
210
|
-
lines[lines.length - 1] = commentLine;
|
|
211
|
-
lines.push(formatted);
|
|
310
|
+
else if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
311
|
+
lines[lines.length - 1] = formatted;
|
|
212
312
|
lines.push("");
|
|
213
313
|
}
|
|
214
314
|
else {
|
|
215
|
-
lines.push(commentLine);
|
|
216
315
|
lines.push(formatted);
|
|
217
316
|
}
|
|
218
317
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
lines.push(formatted);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
let out = lines.join("\n");
|
|
228
|
-
if (!out.endsWith("\n"))
|
|
229
|
-
out += "\n";
|
|
230
|
-
writeFileAtomic(vaultPath, out);
|
|
318
|
+
let out = lines.join("\n");
|
|
319
|
+
if (!out.endsWith("\n"))
|
|
320
|
+
out += "\n";
|
|
321
|
+
writeFileAtomic(vaultPath, out, 0o600);
|
|
322
|
+
});
|
|
231
323
|
}
|
|
232
324
|
/** Remove a key from the vault file. Returns true if the key was present. */
|
|
233
325
|
export function unsetKey(vaultPath, key) {
|
|
234
326
|
if (!fs.existsSync(vaultPath))
|
|
235
327
|
return false;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
328
|
+
return withVaultLock(vaultPath, () => {
|
|
329
|
+
const text = fs.readFileSync(vaultPath, "utf8");
|
|
330
|
+
const lines = text.split(/\r?\n/);
|
|
331
|
+
let keyLineIdx = -1;
|
|
332
|
+
for (let i = 0; i < lines.length; i++) {
|
|
333
|
+
const m = lines[i].match(ASSIGN_RE);
|
|
334
|
+
if (m && m[1] === key) {
|
|
335
|
+
keyLineIdx = i;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
245
338
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
339
|
+
if (keyLineIdx === -1)
|
|
340
|
+
return false;
|
|
341
|
+
// Determine how many consecutive comment lines immediately precede the key.
|
|
342
|
+
// We walk backwards, skipping only comment lines (lines matching /^\s*#/).
|
|
343
|
+
// A blank line between a comment and the key breaks the association — we
|
|
344
|
+
// stop at the first non-comment, non-assignment line (including blank lines).
|
|
345
|
+
let commentStart = keyLineIdx;
|
|
346
|
+
for (let i = keyLineIdx - 1; i >= 0; i--) {
|
|
347
|
+
if (/^\s*#/.test(lines[i])) {
|
|
348
|
+
commentStart = i;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Stop at the first non-comment line (blank or assignment)
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Remove from commentStart through keyLineIdx (inclusive).
|
|
356
|
+
lines.splice(commentStart, keyLineIdx - commentStart + 1);
|
|
357
|
+
let out = lines.join("\n");
|
|
358
|
+
if (out.length > 0 && !out.endsWith("\n"))
|
|
359
|
+
out += "\n";
|
|
360
|
+
writeFileAtomic(vaultPath, out, 0o600);
|
|
361
|
+
return true;
|
|
362
|
+
});
|
|
255
363
|
}
|
|
256
364
|
/** Create an empty vault file (does nothing if it already exists). */
|
|
257
365
|
export function createVault(vaultPath) {
|
|
258
366
|
ensureParentDir(vaultPath);
|
|
259
367
|
if (fs.existsSync(vaultPath))
|
|
260
368
|
return;
|
|
261
|
-
writeFileAtomic(vaultPath, "");
|
|
369
|
+
writeFileAtomic(vaultPath, "", 0o600);
|
|
262
370
|
}
|
|
263
371
|
/**
|
|
264
372
|
* Characters that are safe in an UNquoted dotenv value AND are not
|
|
265
373
|
* metacharacters in POSIX shells. Anything outside this set forces quoting,
|
|
266
374
|
* which is defense-in-depth for any caller that might ever `source` the
|
|
267
|
-
* vault file directly instead of going through `akm vault
|
|
375
|
+
* vault file directly instead of going through `akm vault path`.
|
|
268
376
|
*/
|
|
269
377
|
const UNQUOTED_SAFE_RE = /^[A-Za-z0-9_.:/@%+,-]+$/;
|
|
270
378
|
/**
|
|
@@ -307,27 +415,5 @@ function validateKeyName(key) {
|
|
|
307
415
|
function ensureParentDir(filePath) {
|
|
308
416
|
const dir = path.dirname(filePath);
|
|
309
417
|
if (!fs.existsSync(dir))
|
|
310
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
311
|
-
}
|
|
312
|
-
function writeFileAtomic(filePath, content) {
|
|
313
|
-
const tmp = `${filePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
314
|
-
try {
|
|
315
|
-
fs.writeFileSync(tmp, content, { encoding: "utf8", mode: 0o600 });
|
|
316
|
-
fs.renameSync(tmp, filePath);
|
|
317
|
-
try {
|
|
318
|
-
fs.chmodSync(filePath, 0o600);
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
/* best-effort on platforms without chmod */
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
catch (err) {
|
|
325
|
-
try {
|
|
326
|
-
fs.unlinkSync(tmp);
|
|
327
|
-
}
|
|
328
|
-
catch {
|
|
329
|
-
/* ignore cleanup failure */
|
|
330
|
-
}
|
|
331
|
-
throw err;
|
|
332
|
-
}
|
|
418
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
333
419
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defaultRendererRegistry } from "./asset-registry";
|
|
2
|
+
function registryActionContributor(registry) {
|
|
3
|
+
return {
|
|
4
|
+
name: "registry-action-contributor",
|
|
5
|
+
appliesTo(ctx) {
|
|
6
|
+
return registry.actionBuilderFor(ctx.type) !== undefined;
|
|
7
|
+
},
|
|
8
|
+
buildAction(ctx) {
|
|
9
|
+
return registry.actionBuilderFor(ctx.type)?.(ctx.ref);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function defaultActionContributors(registry = defaultRendererRegistry) {
|
|
14
|
+
return [registryActionContributor(registry)];
|
|
15
|
+
}
|
|
16
|
+
export function buildActionFromContributors(ctx, contributors = defaultActionContributors()) {
|
|
17
|
+
for (const contributor of contributors) {
|
|
18
|
+
if (!contributor.appliesTo(ctx))
|
|
19
|
+
continue;
|
|
20
|
+
const action = contributor.buildAction(ctx);
|
|
21
|
+
if (action !== undefined)
|
|
22
|
+
return action;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
package/dist/core/asset-ref.js
CHANGED
|
@@ -67,6 +67,10 @@ function validateName(name) {
|
|
|
67
67
|
if (normalized === ".." || normalized.startsWith("../")) {
|
|
68
68
|
throw new UsageError("Path traversal in asset name.", "MISSING_REQUIRED_ARGUMENT");
|
|
69
69
|
}
|
|
70
|
+
const segments = normalized.split("/");
|
|
71
|
+
if (segments.some((seg) => seg === "." || seg === "..")) {
|
|
72
|
+
throw new UsageError("Asset name cannot contain relative path segments.", "MISSING_REQUIRED_ARGUMENT");
|
|
73
|
+
}
|
|
70
74
|
}
|
|
71
75
|
function normalizeName(name) {
|
|
72
76
|
return path.posix.normalize(name.replace(/\\/g, "/"));
|
|
@@ -18,10 +18,12 @@ export const TYPE_TO_RENDERER = {
|
|
|
18
18
|
command: "command-md",
|
|
19
19
|
agent: "agent-md",
|
|
20
20
|
knowledge: "knowledge-md",
|
|
21
|
+
lesson: "lesson-md",
|
|
21
22
|
memory: "memory-md",
|
|
22
23
|
workflow: "workflow-md",
|
|
23
24
|
vault: "vault-env",
|
|
24
25
|
wiki: "wiki-md",
|
|
26
|
+
task: "task-md",
|
|
25
27
|
};
|
|
26
28
|
/** Map asset types to action builder functions for search results. */
|
|
27
29
|
export const ACTION_BUILDERS = {
|
|
@@ -30,10 +32,12 @@ export const ACTION_BUILDERS = {
|
|
|
30
32
|
command: (ref) => `akm show ${ref} -> fill placeholders and dispatch`,
|
|
31
33
|
agent: (ref) => `akm show ${ref} -> dispatch with full prompt`,
|
|
32
34
|
knowledge: (ref) => `akm show ${ref} -> read reference material`,
|
|
35
|
+
lesson: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
|
|
33
36
|
memory: (ref) => `akm show ${ref} -> recall context`,
|
|
34
37
|
workflow: (ref) => buildWorkflowAction(ref),
|
|
35
|
-
vault: (ref) => `akm
|
|
38
|
+
vault: (ref) => `akm show ${ref} -> inspect keys; source "$(akm vault path ${ref})" -> load values; akm vault run ${ref} -- <command> -> run with injected env`,
|
|
36
39
|
wiki: (ref) => `akm show ${ref} -> read the wiki page`,
|
|
40
|
+
task: (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`,
|
|
37
41
|
};
|
|
38
42
|
/**
|
|
39
43
|
* Register a type-to-renderer mapping.
|
|
@@ -61,19 +65,3 @@ export const defaultRendererRegistry = {
|
|
|
61
65
|
return ACTION_BUILDERS[type];
|
|
62
66
|
},
|
|
63
67
|
};
|
|
64
|
-
/**
|
|
65
|
-
* Build a registry from explicit maps. Useful for tests that need to assert
|
|
66
|
-
* rendering behavior without touching the global singletons.
|
|
67
|
-
*/
|
|
68
|
-
export function createRendererRegistry(maps) {
|
|
69
|
-
const renderers = maps.renderers ?? {};
|
|
70
|
-
const actionBuilders = maps.actionBuilders ?? {};
|
|
71
|
-
return {
|
|
72
|
-
rendererNameFor(type) {
|
|
73
|
-
return renderers[type];
|
|
74
|
-
},
|
|
75
|
-
actionBuilderFor(type) {
|
|
76
|
-
return actionBuilders[type];
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
package/dist/core/asset-spec.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { buildWorkflowAction } from "../output/renderers";
|
|
3
3
|
import { registerActionBuilder, registerTypeRenderer } from "./asset-registry";
|
|
4
4
|
import { toPosix } from "./common";
|
|
5
|
+
const buildTaskAction = (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`;
|
|
5
6
|
const markdownSpec = {
|
|
6
7
|
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
|
|
7
8
|
toCanonicalName: (typeRoot, filePath) => {
|
|
@@ -82,7 +83,7 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
82
83
|
return path.join(typeRoot, name.endsWith(".env") ? name : `${name}.env`);
|
|
83
84
|
},
|
|
84
85
|
rendererName: "vault-env",
|
|
85
|
-
actionBuilder: (ref) => `akm
|
|
86
|
+
actionBuilder: (ref) => `akm show ${ref} -> inspect keys; source "$(akm vault path ${ref})" -> load values; akm vault run ${ref} -- <command> -> run with injected env`,
|
|
86
87
|
},
|
|
87
88
|
wiki: {
|
|
88
89
|
stashDir: "wikis",
|
|
@@ -102,6 +103,15 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
102
103
|
rendererName: "lesson-md",
|
|
103
104
|
actionBuilder: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
|
|
104
105
|
},
|
|
106
|
+
// Scheduled tasks. A task file pairs a cron-style schedule with a target
|
|
107
|
+
// (workflow ref or prompt) that `akm tasks` registers with the OS-native
|
|
108
|
+
// scheduler (cron / launchd / schtasks). Stored under <stash>/tasks/<id>.md.
|
|
109
|
+
task: {
|
|
110
|
+
stashDir: "tasks",
|
|
111
|
+
...markdownSpec,
|
|
112
|
+
rendererName: "task-md",
|
|
113
|
+
actionBuilder: buildTaskAction,
|
|
114
|
+
},
|
|
105
115
|
};
|
|
106
116
|
export const ASSET_SPECS = ASSET_SPECS_INTERNAL;
|
|
107
117
|
/**
|
package/dist/core/common.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { TYPE_DIRS } from "./asset-spec";
|
|
@@ -8,6 +9,20 @@ export const IS_WINDOWS = process.platform === "win32";
|
|
|
8
9
|
export function isHttpUrl(value) {
|
|
9
10
|
return !!value && /^https?:\/\//.test(value);
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns `true` when `value` looks like a remote URL that a VCS or HTTP
|
|
14
|
+
* fetch can access. Covers http/https, git@, ssh://, and git:// schemes.
|
|
15
|
+
* Consolidates the repeated inline URL-detection pattern in source-manage.ts.
|
|
16
|
+
*/
|
|
17
|
+
export function isRemoteUrl(value) {
|
|
18
|
+
if (!value)
|
|
19
|
+
return false;
|
|
20
|
+
return (value.startsWith("http://") ||
|
|
21
|
+
value.startsWith("https://") ||
|
|
22
|
+
value.startsWith("git@") ||
|
|
23
|
+
value.startsWith("ssh://") ||
|
|
24
|
+
value.startsWith("git://"));
|
|
25
|
+
}
|
|
11
26
|
export function filterNonEmptyStrings(value) {
|
|
12
27
|
if (!Array.isArray(value))
|
|
13
28
|
return undefined;
|
|
@@ -18,6 +33,23 @@ export function isAssetType(type) {
|
|
|
18
33
|
return Object.hasOwn(TYPE_DIRS, type);
|
|
19
34
|
}
|
|
20
35
|
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Write content to a file atomically via a temp file + rename.
|
|
38
|
+
* Prevents partial-write corruption on crash.
|
|
39
|
+
* The temp file is opened with the target `mode` (default 0o600) from the
|
|
40
|
+
* start, so it is never world-readable even briefly.
|
|
41
|
+
*/
|
|
42
|
+
export function writeFileAtomic(target, content, mode) {
|
|
43
|
+
const tmp = `${target}.tmp.${process.pid}.${crypto.randomBytes(8).toString("hex")}`;
|
|
44
|
+
const fd = fs.openSync(tmp, "w", mode ?? 0o600);
|
|
45
|
+
try {
|
|
46
|
+
fs.writeSync(fd, content);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
fs.closeSync(fd);
|
|
50
|
+
}
|
|
51
|
+
fs.renameSync(tmp, target);
|
|
52
|
+
}
|
|
21
53
|
/**
|
|
22
54
|
* Resolve the stash directory using a three-level fallback chain:
|
|
23
55
|
* 1. AKM_STASH_DIR environment variable (override for CI/scripts)
|
|
@@ -329,3 +361,71 @@ function parseRetryAfter(response) {
|
|
|
329
361
|
export function toErrorMessage(error) {
|
|
330
362
|
return error instanceof Error ? error.message : String(error);
|
|
331
363
|
}
|
|
364
|
+
// ── Date / timestamp utilities ───────────────────────────────────────────────
|
|
365
|
+
/**
|
|
366
|
+
* Return today's date in ISO-8601 format (`YYYY-MM-DD`).
|
|
367
|
+
* Consolidates the `new Date().toISOString().slice(0, 10)` pattern that
|
|
368
|
+
* appears at multiple call sites.
|
|
369
|
+
*/
|
|
370
|
+
export function todayIso() {
|
|
371
|
+
return new Date().toISOString().slice(0, 10);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Return a filesystem-safe timestamp string derived from the current instant.
|
|
375
|
+
* Colons and dots are replaced with hyphens so the result is safe as a
|
|
376
|
+
* filename component on all platforms (e.g. `2024-01-15T10-30-00-000Z`).
|
|
377
|
+
*/
|
|
378
|
+
export function timestampForFilename() {
|
|
379
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
380
|
+
}
|
|
381
|
+
// ── String coercion ──────────────────────────────────────────────────────────
|
|
382
|
+
/**
|
|
383
|
+
* Return the trimmed string value if non-empty, otherwise `undefined`.
|
|
384
|
+
* Consolidates `toStringOrUndefined` (frontmatter.ts), `asNonEmptyString`
|
|
385
|
+
* (config.ts), and `firstString` (memory-improve.ts) — all had the same
|
|
386
|
+
* "return a string or undefined" contract with minor semantic differences.
|
|
387
|
+
*/
|
|
388
|
+
export function asNonEmptyString(value) {
|
|
389
|
+
if (typeof value !== "string")
|
|
390
|
+
return undefined;
|
|
391
|
+
const trimmed = value.trim();
|
|
392
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
393
|
+
}
|
|
394
|
+
// ── Generic data utilities ───────────────────────────────────────────────────
|
|
395
|
+
/**
|
|
396
|
+
* Return the trimmed string if non-empty, otherwise `undefined`.
|
|
397
|
+
* Equivalent to `firstString` previously defined in `memory-improve.ts`.
|
|
398
|
+
*/
|
|
399
|
+
export function firstString(value) {
|
|
400
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Coerce an unknown value to a filtered, trimmed string array.
|
|
404
|
+
* Non-strings and empty/whitespace-only entries are dropped.
|
|
405
|
+
*/
|
|
406
|
+
export function stringArray(value) {
|
|
407
|
+
if (!Array.isArray(value))
|
|
408
|
+
return [];
|
|
409
|
+
const out = [];
|
|
410
|
+
for (const item of value) {
|
|
411
|
+
if (typeof item === "string" && item.trim().length > 0)
|
|
412
|
+
out.push(item.trim());
|
|
413
|
+
}
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Group an array of values by a string key derived from each element.
|
|
418
|
+
* Returns a `Map` so insertion order within each group is preserved.
|
|
419
|
+
*/
|
|
420
|
+
export function groupBy(values, keyFn) {
|
|
421
|
+
const groups = new Map();
|
|
422
|
+
for (const value of values) {
|
|
423
|
+
const key = keyFn(value);
|
|
424
|
+
const existing = groups.get(key);
|
|
425
|
+
if (existing)
|
|
426
|
+
existing.push(value);
|
|
427
|
+
else
|
|
428
|
+
groups.set(key, [value]);
|
|
429
|
+
}
|
|
430
|
+
return groups;
|
|
431
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps over items concurrently with a pool size limit.
|
|
3
|
+
* Uses Promise.allSettled semantics — one failure does not cancel others.
|
|
4
|
+
*/
|
|
5
|
+
export async function concurrentMap(items, fn, concurrency = 1) {
|
|
6
|
+
const results = new Array(items.length).fill(undefined);
|
|
7
|
+
let nextIndex = 0;
|
|
8
|
+
async function worker() {
|
|
9
|
+
while (nextIndex < items.length) {
|
|
10
|
+
const i = nextIndex++;
|
|
11
|
+
try {
|
|
12
|
+
results[i] = await fn(items[i], i);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// individual failure: leave undefined, caller checks
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, worker);
|
|
20
|
+
await Promise.all(workers);
|
|
21
|
+
return results;
|
|
22
|
+
}
|