akm-cli 0.7.4 → 0.8.0-rc.10
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 +224 -1
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2631 -1440
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +2122 -0
- package/dist/commands/curate.js +45 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +1081 -73
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +15 -24
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +1302 -0
- package/dist/commands/help/help-accept.md +12 -0
- package/dist/commands/help/help-improve.md +69 -0
- package/dist/commands/help/help-proposals.md +18 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +11 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +217 -0
- package/dist/commands/improve-profiles.js +166 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +2373 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +196 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +3 -0
- package/dist/commands/proposal.js +67 -12
- package/dist/commands/propose.js +120 -45
- package/dist/commands/reflect.js +1104 -60
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +70 -7
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +158 -60
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +437 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +12 -17
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +67 -1
- package/dist/core/common.js +182 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +534 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +364 -968
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +105 -135
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -8
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +280 -14
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +512 -42
- package/dist/core/state-db.js +1068 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +64 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +198 -489
- package/dist/indexer/db.js +990 -108
- package/dist/indexer/ensure-index.js +136 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -114
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +547 -309
- package/dist/indexer/llm-cache.js +52 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +167 -160
- package/dist/indexer/memory-inference.js +152 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +275 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +6 -17
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +250 -36
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +183 -35
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +69 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +282 -0
- package/dist/integrations/session-logs/providers/opencode.js +258 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +79 -88
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +95 -48
- package/dist/llm/graph-extract.js +676 -72
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +80 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +292 -0
- package/dist/output/cli-hints-short.md +66 -0
- package/dist/output/cli-hints.js +7 -311
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +306 -258
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +102 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -511
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1039 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +11 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1093
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +71 -50
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17750 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +179 -20
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +227 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -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 +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +141 -2
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +91 -89
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +79 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +10 -2
- package/docs/data-and-telemetry.md +225 -0
- 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 +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +29 -11
- package/dist/commands/install-audit.js +0 -381
- package/dist/commands/vault.js +0 -333
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
* Shared JSON parsing utilities for LLM and agent output.
|
|
6
|
+
*
|
|
7
|
+
* Lives in `src/core/` so that both `src/llm/` and `src/integrations/agent/`
|
|
8
|
+
* can import without crossing the one-way boundary defined by v1 spec §9.7
|
|
9
|
+
* (agent/ must not import from llm/).
|
|
10
|
+
*
|
|
11
|
+
* The canonical implementation is ported from `src/llm/client.ts` (most
|
|
12
|
+
* complete version):
|
|
13
|
+
* - Strips `<think>…</think>` reasoning blocks.
|
|
14
|
+
* - Strips markdown code fences (``` or ~~~, optional language tag, with
|
|
15
|
+
* trailing spaces on the fence line).
|
|
16
|
+
* - Escapes unescaped control characters (actual \n, \r, \t bytes) inside
|
|
17
|
+
* JSON string values so `JSON.parse` succeeds on outputs from local LLMs.
|
|
18
|
+
* - Balanced-brace scanner handles both `{…}` and `[…]` top-level
|
|
19
|
+
* structures (spawn.ts v0 only handled `{…}` — that was a bug).
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Strips `<think>…</think>` blocks from LLM output (for reasoning-capable
|
|
23
|
+
* models). Also strips leading/trailing whitespace.
|
|
24
|
+
*/
|
|
25
|
+
export function stripThinkBlocks(raw) {
|
|
26
|
+
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Strips markdown code fences (``` or ~~~, with optional language tag).
|
|
30
|
+
* Handles fences with trailing spaces. Returns trimmed content.
|
|
31
|
+
*/
|
|
32
|
+
export function stripCodeFences(raw) {
|
|
33
|
+
return raw
|
|
34
|
+
.trim()
|
|
35
|
+
.replace(/^```(?:json)?\s*\n?/i, "")
|
|
36
|
+
.replace(/\n?```\s*$/i, "")
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Escapes unescaped control characters (actual \n, \r, \t bytes) inside JSON
|
|
41
|
+
* string values. Prevents `JSON.parse` failures from embedded newlines in
|
|
42
|
+
* local-LLM output.
|
|
43
|
+
*/
|
|
44
|
+
export function escapeJsonStringControls(raw) {
|
|
45
|
+
let out = "";
|
|
46
|
+
let inString = false;
|
|
47
|
+
let escaped = false;
|
|
48
|
+
for (let i = 0; i < raw.length; i++) {
|
|
49
|
+
const ch = raw[i];
|
|
50
|
+
if (escaped) {
|
|
51
|
+
out += ch;
|
|
52
|
+
escaped = false;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === "\\" && inString) {
|
|
56
|
+
out += ch;
|
|
57
|
+
escaped = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ch === '"') {
|
|
61
|
+
inString = !inString;
|
|
62
|
+
out += ch;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (inString) {
|
|
66
|
+
if (ch === "\n") {
|
|
67
|
+
out += "\\n";
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === "\r") {
|
|
71
|
+
out += "\\r";
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === "\t") {
|
|
75
|
+
out += "\\t";
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
out += ch;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Full pipeline: stripThinkBlocks → stripCodeFences → escapeJsonStringControls
|
|
85
|
+
* → JSON.parse. Returns `undefined` on parse failure.
|
|
86
|
+
*/
|
|
87
|
+
export function parseJsonResponse(raw) {
|
|
88
|
+
try {
|
|
89
|
+
const cleaned = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
|
|
90
|
+
return JSON.parse(cleaned);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Attempts `parseJsonResponse` first. On failure, scans for the first
|
|
98
|
+
* balanced `{ }` or `[ ]` structure in the text and attempts to parse that
|
|
99
|
+
* substring. Returns `undefined` if no valid JSON structure is found.
|
|
100
|
+
*
|
|
101
|
+
* Non-array results are preferred: if a `{…}` object is found first, it is
|
|
102
|
+
* returned immediately. Arrays (`[…]`) are captured as a fallback and
|
|
103
|
+
* returned only when no object was found.
|
|
104
|
+
*/
|
|
105
|
+
export function parseEmbeddedJsonResponse(raw) {
|
|
106
|
+
const direct = parseJsonResponse(raw);
|
|
107
|
+
if (direct !== undefined)
|
|
108
|
+
return direct;
|
|
109
|
+
const text = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
|
|
110
|
+
let arrayFallback;
|
|
111
|
+
for (let start = 0; start < text.length; start++) {
|
|
112
|
+
const opener = text[start];
|
|
113
|
+
if (opener !== "{" && opener !== "[")
|
|
114
|
+
continue;
|
|
115
|
+
const closer = opener === "{" ? "}" : "]";
|
|
116
|
+
let depth = 0;
|
|
117
|
+
let inString = false;
|
|
118
|
+
let escaped = false;
|
|
119
|
+
for (let i = start; i < text.length; i++) {
|
|
120
|
+
const ch = text[i];
|
|
121
|
+
if (inString) {
|
|
122
|
+
if (escaped) {
|
|
123
|
+
escaped = false;
|
|
124
|
+
}
|
|
125
|
+
else if (ch === "\\") {
|
|
126
|
+
escaped = true;
|
|
127
|
+
}
|
|
128
|
+
else if (ch === '"') {
|
|
129
|
+
inString = false;
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === '"') {
|
|
134
|
+
inString = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ch === opener)
|
|
138
|
+
depth += 1;
|
|
139
|
+
if (ch === closer) {
|
|
140
|
+
depth -= 1;
|
|
141
|
+
if (depth === 0) {
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(text.slice(start, i + 1));
|
|
144
|
+
if (!Array.isArray(parsed)) {
|
|
145
|
+
return parsed;
|
|
146
|
+
}
|
|
147
|
+
arrayFallback ??= parsed;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return arrayFallback;
|
|
158
|
+
}
|
package/dist/core/paths.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Centralized path resolution for all akm directories.
|
|
3
6
|
*
|
|
@@ -5,14 +8,90 @@
|
|
|
5
8
|
* following XDG Base Directory conventions on Unix and standard locations
|
|
6
9
|
* on Windows.
|
|
7
10
|
*/
|
|
11
|
+
import os from "node:os";
|
|
8
12
|
import path from "node:path";
|
|
13
|
+
import { IS_WINDOWS } from "./common";
|
|
9
14
|
import { ConfigError } from "./errors";
|
|
10
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Returns true when the current process appears to be running under
|
|
17
|
+
* `bun test` (either via the BUN_TEST sentinel Bun sets on the test
|
|
18
|
+
* worker, or via the conventional NODE_ENV=test).
|
|
19
|
+
*
|
|
20
|
+
* Used by getDataDir to enforce that every test which resolves a data
|
|
21
|
+
* directory ALSO sets XDG_DATA_HOME (or the AKM_DATA_DIR override) to a
|
|
22
|
+
* temp directory. Without that pairing, tests silently write SQLite
|
|
23
|
+
* databases, lockfiles, and task history into the developer's real
|
|
24
|
+
* `~/.local/share/akm`.
|
|
25
|
+
*/
|
|
26
|
+
function isUnderBunTest(env) {
|
|
27
|
+
return env.BUN_TEST === "1" || env.NODE_ENV === "test";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns true when the given path is in a directory family the OS may
|
|
31
|
+
* reap (or that the user has clearly designated as a sandbox by virtue
|
|
32
|
+
* of placing it under `/tmp` or a macOS per-user temp dir). Used to
|
|
33
|
+
* decide whether `AKM_STASH_DIR=$tmpdir` should also isolate config +
|
|
34
|
+
* cache writes (so a test harness's `akm setup --yes --dir .` cannot
|
|
35
|
+
* silently clobber the user's `~/.config/akm/config.json`). See
|
|
36
|
+
* `docs/technical/incidents/2026-05-23-setup-clobbers-user-config.md`
|
|
37
|
+
* for the incident that motivated this.
|
|
38
|
+
*
|
|
39
|
+
* Both `/var/folders/*` and `/private/var/folders/*` are matched because
|
|
40
|
+
* `os.tmpdir()` on macOS may return either form depending on whether the
|
|
41
|
+
* caller has canonicalised the path (the realpath of `/var/folders` is
|
|
42
|
+
* `/private/var/folders`, but `path.resolve()` does not follow symlinks).
|
|
43
|
+
*/
|
|
44
|
+
export function isTransientStashPath(p) {
|
|
45
|
+
return (p.startsWith("/tmp/") ||
|
|
46
|
+
p === "/tmp" ||
|
|
47
|
+
p.startsWith("/var/tmp/") ||
|
|
48
|
+
p === "/var/tmp" ||
|
|
49
|
+
p.startsWith("/private/tmp/") ||
|
|
50
|
+
p.startsWith("/private/var/folders/") ||
|
|
51
|
+
p.startsWith("/var/folders/"));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build a TEST_ISOLATION_MISSING ConfigError describing which env var(s)
|
|
55
|
+
* must be set so the data path resolves into a temp dir instead of the
|
|
56
|
+
* user's real XDG home.
|
|
57
|
+
*/
|
|
58
|
+
function testIsolationError() {
|
|
59
|
+
return new ConfigError("Refusing to resolve data directory under bun test: neither XDG_DATA_HOME nor AKM_DATA_DIR is set. " +
|
|
60
|
+
"This guards against tests writing into the developer's real ~/.local/share/akm. " +
|
|
61
|
+
"Set XDG_DATA_HOME (or AKM_DATA_DIR) to a mktemp-d directory in this test's env block.", "TEST_ISOLATION_MISSING");
|
|
62
|
+
}
|
|
11
63
|
// ── Config directory ─────────────────────────────────────────────────────────
|
|
12
64
|
export function getConfigDir(env = process.env, platform = process.platform) {
|
|
13
65
|
const override = env.AKM_CONFIG_DIR?.trim();
|
|
14
66
|
if (override)
|
|
15
67
|
return override;
|
|
68
|
+
// Explicit XDG override wins next — tests and operators that pre-arrange
|
|
69
|
+
// an isolated config dir via XDG_CONFIG_HOME (or %APPDATA% on Windows)
|
|
70
|
+
// must be honored as set, so the AKM_STASH_DIR transient-isolation rule
|
|
71
|
+
// below does not silently move config away from where they pointed it.
|
|
72
|
+
if (platform === "win32") {
|
|
73
|
+
const appData = env.APPDATA?.trim();
|
|
74
|
+
if (appData)
|
|
75
|
+
return path.join(appData, "akm");
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const xdgConfigHome = env.XDG_CONFIG_HOME?.trim();
|
|
79
|
+
if (xdgConfigHome)
|
|
80
|
+
return path.join(xdgConfigHome, "akm");
|
|
81
|
+
}
|
|
82
|
+
// Isolation safety: when AKM_STASH_DIR points at a transient/sandbox path
|
|
83
|
+
// (/tmp, /var/tmp, /private/var/folders) AND no explicit config dir
|
|
84
|
+
// override is set, route config writes into `${AKM_STASH_DIR}/.akm`
|
|
85
|
+
// instead of the user's host ~/.config/akm. This prevents the documented
|
|
86
|
+
// isolation pattern
|
|
87
|
+
// AKM_DATA_DIR=/tmp/x AKM_STASH_DIR=/tmp/x akm setup --yes --dir .
|
|
88
|
+
// from silently clobbering the host config. See
|
|
89
|
+
// docs/technical/incidents/2026-05-23-setup-clobbers-user-config.md for the incident.
|
|
90
|
+
// Daily users with a persistent AKM_STASH_DIR=~/my-stash are unaffected.
|
|
91
|
+
const stashOverride = env.AKM_STASH_DIR?.trim();
|
|
92
|
+
if (stashOverride && isTransientStashPath(stashOverride)) {
|
|
93
|
+
return path.join(stashOverride, ".akm");
|
|
94
|
+
}
|
|
16
95
|
if (platform === "win32") {
|
|
17
96
|
const appData = env.APPDATA?.trim();
|
|
18
97
|
if (appData)
|
|
@@ -40,6 +119,11 @@ export function getCacheDir() {
|
|
|
40
119
|
const override = process.env.AKM_CACHE_DIR?.trim();
|
|
41
120
|
if (override)
|
|
42
121
|
return override;
|
|
122
|
+
// Explicit XDG/platform overrides win before the transient-stash isolation
|
|
123
|
+
// rule below — tests and operators that pre-arrange XDG_CACHE_HOME (or
|
|
124
|
+
// %LOCALAPPDATA% / %USERPROFILE% / %APPDATA% on Windows) must be honored
|
|
125
|
+
// as set, so the AKM_STASH_DIR transient rule does not silently move cache
|
|
126
|
+
// writes away from where they pointed them.
|
|
43
127
|
if (IS_WINDOWS) {
|
|
44
128
|
const localAppData = process.env.LOCALAPPDATA?.trim();
|
|
45
129
|
if (localAppData)
|
|
@@ -48,28 +132,107 @@ export function getCacheDir() {
|
|
|
48
132
|
if (userProfile)
|
|
49
133
|
return path.join(userProfile, "AppData", "Local", "akm");
|
|
50
134
|
const appData = process.env.APPDATA?.trim();
|
|
51
|
-
if (
|
|
52
|
-
|
|
135
|
+
if (appData) {
|
|
136
|
+
// Heuristic fallback: APPDATA points to %APPDATA% (Roaming), so
|
|
137
|
+
// navigate to the sibling "Local" directory. This is typically
|
|
138
|
+
// C:\Users\<name>\AppData\Roaming → C:\Users\<name>\AppData\Local\akm.
|
|
139
|
+
// Preferred: set LOCALAPPDATA to avoid this navigation.
|
|
140
|
+
return path.join(appData, "..", "Local", "akm");
|
|
53
141
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
|
|
145
|
+
if (xdgCacheHome)
|
|
146
|
+
return path.join(xdgCacheHome, "akm");
|
|
147
|
+
}
|
|
148
|
+
// Isolation safety (mirrors getConfigDir): when AKM_STASH_DIR points at a
|
|
149
|
+
// transient path AND no explicit cache override is set, route cache writes
|
|
150
|
+
// into `${AKM_STASH_DIR}/.akm/cache` so that config backups, registry-index
|
|
151
|
+
// cache, and other regenerable artifacts do not pollute the user's host
|
|
152
|
+
// ~/.cache/akm directory.
|
|
153
|
+
const stashOverride = process.env.AKM_STASH_DIR?.trim();
|
|
154
|
+
if (stashOverride && isTransientStashPath(stashOverride)) {
|
|
155
|
+
return path.join(stashOverride, ".akm", "cache");
|
|
156
|
+
}
|
|
157
|
+
if (IS_WINDOWS) {
|
|
158
|
+
// None of LOCALAPPDATA / USERPROFILE / APPDATA were set above.
|
|
159
|
+
throw new ConfigError("Unable to determine cache directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.", "CONFIG_DIR_UNRESOLVABLE");
|
|
160
|
+
}
|
|
63
161
|
const home = process.env.HOME?.trim();
|
|
64
162
|
if (!home)
|
|
65
163
|
return path.join("/tmp", "akm-cache");
|
|
66
164
|
return path.join(home, ".cache", "akm");
|
|
67
165
|
}
|
|
166
|
+
// ── Data directory ───────────────────────────────────────────────────────────
|
|
167
|
+
/**
|
|
168
|
+
* Returns the XDG data directory for akm (`~/.local/share/akm` on Linux/macOS,
|
|
169
|
+
* `%LOCALAPPDATA%\akm\data` on Windows).
|
|
170
|
+
*
|
|
171
|
+
* Holds durable, non-regenerable application data: SQLite databases
|
|
172
|
+
* (index.db, workflow.db, state.db), akm.lock, and config-backups.
|
|
173
|
+
* Losing this directory loses history and installed state.
|
|
174
|
+
*
|
|
175
|
+
* Env overrides (in priority order):
|
|
176
|
+
* AKM_DATA_DIR — point to any directory
|
|
177
|
+
* XDG_DATA_HOME — (Linux/macOS) override the XDG base; akm subdir is appended
|
|
178
|
+
*/
|
|
179
|
+
export function getDataDir(env = process.env, platform = process.platform) {
|
|
180
|
+
const override = env.AKM_DATA_DIR?.trim();
|
|
181
|
+
if (override)
|
|
182
|
+
return override;
|
|
183
|
+
// Defense-in-depth: under `bun test`, refuse to fall through to the
|
|
184
|
+
// user's real $XDG_DATA_HOME / ~/.local/share/akm under any condition.
|
|
185
|
+
// Any test that needs a data dir must point it at a mktemp-d directory
|
|
186
|
+
// via XDG_DATA_HOME (or AKM_DATA_DIR). The previous carve-out that only
|
|
187
|
+
// fired when AKM_STASH_DIR was set was a loophole: tests calling
|
|
188
|
+
// openDatabase() or getDbPath() without overriding any env var silently
|
|
189
|
+
// wrote into ~/.local/share/akm/index.db (observed: 4,183-row
|
|
190
|
+
// registry-cache pollution). Item 5 of the 0.8.x critical-review plan.
|
|
191
|
+
if (isUnderBunTest(env) && !env.XDG_DATA_HOME?.trim()) {
|
|
192
|
+
throw testIsolationError();
|
|
193
|
+
}
|
|
194
|
+
if (platform === "win32") {
|
|
195
|
+
const localAppData = env.LOCALAPPDATA?.trim();
|
|
196
|
+
if (localAppData)
|
|
197
|
+
return path.join(localAppData, "akm", "data");
|
|
198
|
+
const userProfile = env.USERPROFILE?.trim();
|
|
199
|
+
if (userProfile)
|
|
200
|
+
return path.join(userProfile, "AppData", "Local", "akm", "data");
|
|
201
|
+
const appData = env.APPDATA?.trim();
|
|
202
|
+
if (!appData) {
|
|
203
|
+
throw new ConfigError("Unable to determine data directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.", "CONFIG_DIR_UNRESOLVABLE");
|
|
204
|
+
}
|
|
205
|
+
return path.join(appData, "..", "Local", "akm", "data");
|
|
206
|
+
}
|
|
207
|
+
const xdgDataHome = env.XDG_DATA_HOME?.trim();
|
|
208
|
+
if (xdgDataHome)
|
|
209
|
+
return path.join(xdgDataHome, "akm");
|
|
210
|
+
const home = env.HOME?.trim();
|
|
211
|
+
if (!home)
|
|
212
|
+
return path.join("/tmp", "akm-data");
|
|
213
|
+
return path.join(home, ".local", "share", "akm");
|
|
214
|
+
}
|
|
68
215
|
export function getDbPath() {
|
|
69
|
-
return path.join(
|
|
216
|
+
return path.join(getDataDir(), "index.db");
|
|
70
217
|
}
|
|
71
218
|
export function getWorkflowDbPath() {
|
|
72
|
-
return path.join(
|
|
219
|
+
return path.join(getDataDir(), "workflow.db");
|
|
220
|
+
}
|
|
221
|
+
/** Path to the state.db file in $DATA. */
|
|
222
|
+
export function getStateDbPathInDataDir() {
|
|
223
|
+
return path.join(getDataDir(), "state.db");
|
|
224
|
+
}
|
|
225
|
+
/** Path for the task history directory in $DATA. */
|
|
226
|
+
export function getTaskHistoryStateDir() {
|
|
227
|
+
return path.join(getDataDir(), "tasks", "history");
|
|
228
|
+
}
|
|
229
|
+
/** Path to the akm.lock file in $DATA. */
|
|
230
|
+
export function getLockfilePath() {
|
|
231
|
+
return path.join(getDataDir(), "akm.lock");
|
|
232
|
+
}
|
|
233
|
+
/** Path to the akm.lock.lck write-sentinel in $DATA. */
|
|
234
|
+
export function getLockfileLockPath() {
|
|
235
|
+
return path.join(getDataDir(), "akm.lock.lck");
|
|
73
236
|
}
|
|
74
237
|
export function getSemanticStatusPath() {
|
|
75
238
|
return path.join(getCacheDir(), "semantic-status.json");
|
|
@@ -83,6 +246,13 @@ export function getRegistryIndexCacheDir() {
|
|
|
83
246
|
export function getBinDir() {
|
|
84
247
|
return path.join(getCacheDir(), "bin");
|
|
85
248
|
}
|
|
249
|
+
// ── Scheduled-task runtime directories (logs + history) ──────────────────────
|
|
250
|
+
export function getTaskLogDir() {
|
|
251
|
+
return path.join(getCacheDir(), "tasks", "logs");
|
|
252
|
+
}
|
|
253
|
+
export function getTaskHistoryDir() {
|
|
254
|
+
return path.join(getCacheDir(), "tasks", "history");
|
|
255
|
+
}
|
|
86
256
|
// ── Default stash directory ──────────────────────────────────────────────────
|
|
87
257
|
export function getDefaultStashDir() {
|
|
88
258
|
const override = process.env.AKM_STASH_DIR?.trim();
|
|
@@ -100,3 +270,99 @@ export function getDefaultStashDir() {
|
|
|
100
270
|
}
|
|
101
271
|
return path.join(home, "akm");
|
|
102
272
|
}
|
|
273
|
+
// ── Stash directory safety check (#473) ──────────────────────────────────────
|
|
274
|
+
/**
|
|
275
|
+
* Refuse stashDir values that would clobber a sensitive system path or the
|
|
276
|
+
* user's home directory itself. Called from `akm init`, `akm setup`, and the
|
|
277
|
+
* setup-wizard validator before any disk write.
|
|
278
|
+
*
|
|
279
|
+
* Refuses:
|
|
280
|
+
* - The filesystem root (`/` or Windows drive root `C:\`)
|
|
281
|
+
* - Common system roots (`/etc`, `/var`, `/usr`, `/usr/local`, `/opt`,
|
|
282
|
+
* `/sys`, `/proc`, `/boot`, `/bin`, `/sbin`, `/lib`, `/lib64`, `/dev`,
|
|
283
|
+
* `/run`, `/home`, `/root`, `/mnt`, `/media`,
|
|
284
|
+
* `/Library`, `/System`, `/Applications`)
|
|
285
|
+
* - The user's home directory itself (exact match — subdirs are fine)
|
|
286
|
+
* - User-data dotfile parents: `~/.config`, `~/.local`, `~/.cache`,
|
|
287
|
+
* `~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.kube`, `~/.docker`,
|
|
288
|
+
* and the macOS/Windows `~/Documents` and `~/Downloads` parents
|
|
289
|
+
*
|
|
290
|
+
* Subdirectories of any refused path are allowed (so `~/.local/share/akm-test`
|
|
291
|
+
* is fine even though `~/.local` is refused). This catches fat-finger
|
|
292
|
+
* `--dir /` or `--dir ~` without preventing legitimate nested use.
|
|
293
|
+
*/
|
|
294
|
+
export function assertSafeStashDir(stashDir) {
|
|
295
|
+
const resolved = path.resolve(stashDir);
|
|
296
|
+
// Filesystem root — POSIX and Windows drive roots.
|
|
297
|
+
if (resolved === "/" || /^[A-Za-z]:[\\/]?$/.test(resolved)) {
|
|
298
|
+
throw new ConfigError(`Refusing stashDir at filesystem root (${resolved}). Pick a subdirectory like ~/akm.`, "UNSAFE_STASH_DIR");
|
|
299
|
+
}
|
|
300
|
+
// System directories — exact match only.
|
|
301
|
+
const SYSTEM_ROOTS = new Set([
|
|
302
|
+
"/etc",
|
|
303
|
+
"/var",
|
|
304
|
+
"/var/tmp",
|
|
305
|
+
"/usr",
|
|
306
|
+
"/usr/local",
|
|
307
|
+
"/opt",
|
|
308
|
+
"/sys",
|
|
309
|
+
"/proc",
|
|
310
|
+
"/boot",
|
|
311
|
+
"/bin",
|
|
312
|
+
"/sbin",
|
|
313
|
+
"/lib",
|
|
314
|
+
"/lib64",
|
|
315
|
+
"/dev",
|
|
316
|
+
"/run",
|
|
317
|
+
"/home",
|
|
318
|
+
"/root",
|
|
319
|
+
"/mnt",
|
|
320
|
+
"/media",
|
|
321
|
+
"/Library",
|
|
322
|
+
"/System",
|
|
323
|
+
"/Applications",
|
|
324
|
+
]);
|
|
325
|
+
if (SYSTEM_ROOTS.has(resolved)) {
|
|
326
|
+
throw new ConfigError(`Refusing stashDir at system path (${resolved}). Pick a path inside your home directory.`, "UNSAFE_STASH_DIR");
|
|
327
|
+
}
|
|
328
|
+
// User home — exact match only. Subdirs (~/akm, ~/work/stash) are fine.
|
|
329
|
+
// Check BOTH the env-controlled home and the OS-reported home, so the
|
|
330
|
+
// refusal can't be bypassed by unsetting HOME, and so it still fires
|
|
331
|
+
// under bun test (which isolates HOME to a tempdir while os.homedir()
|
|
332
|
+
// still returns the real user's home).
|
|
333
|
+
const candidateHomes = new Set();
|
|
334
|
+
const envHome = (process.env.HOME ?? process.env.USERPROFILE)?.trim();
|
|
335
|
+
if (envHome)
|
|
336
|
+
candidateHomes.add(path.resolve(envHome));
|
|
337
|
+
try {
|
|
338
|
+
const osHome = os.homedir();
|
|
339
|
+
if (osHome)
|
|
340
|
+
candidateHomes.add(path.resolve(osHome));
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// os.homedir() can throw on misconfigured systems; ignore.
|
|
344
|
+
}
|
|
345
|
+
const HIDDEN_USER_PARENTS = [
|
|
346
|
+
".config",
|
|
347
|
+
".local",
|
|
348
|
+
".cache",
|
|
349
|
+
".ssh",
|
|
350
|
+
".gnupg",
|
|
351
|
+
".aws",
|
|
352
|
+
".kube",
|
|
353
|
+
".docker",
|
|
354
|
+
"Documents",
|
|
355
|
+
"Downloads",
|
|
356
|
+
"AppData",
|
|
357
|
+
];
|
|
358
|
+
for (const home of candidateHomes) {
|
|
359
|
+
if (resolved === home) {
|
|
360
|
+
throw new ConfigError(`Refusing stashDir at your home directory (${resolved}). Pick a subdirectory like ~/akm.`, "UNSAFE_STASH_DIR");
|
|
361
|
+
}
|
|
362
|
+
for (const sub of HIDDEN_USER_PARENTS) {
|
|
363
|
+
if (resolved === path.join(home, sub)) {
|
|
364
|
+
throw new ConfigError(`Refusing stashDir at sensitive user directory (${resolved}). Pick a subdirectory or a dedicated workspace.`, "UNSAFE_STASH_DIR");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|