akm-cli 0.9.0-rc.0 → 0.9.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +339 -2
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +379 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +118 -2
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -50
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/runner-dispatch.js +91 -4
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +141 -20
- package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
- package/dist/integrations/harnesses/aider/index.js +57 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
- package/dist/integrations/harnesses/amazonq/index.js +58 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/index.js +25 -25
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
- package/dist/integrations/harnesses/codex/index.js +62 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
- package/dist/integrations/harnesses/copilot/index.js +59 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +59 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +27 -28
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +15 -22
- package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
- package/dist/integrations/harnesses/openhands/index.js +57 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
- package/dist/integrations/harnesses/pi/index.js +57 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/integrations/session-logs/index.js +24 -11
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/renderers.js +3 -2
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +6 -0
- package/dist/output/text/helpers.js +215 -2
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +16638 -0
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +372 -0
- package/dist/scripts/migrate-storage.js +10944 -8801
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +190 -1
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +200 -13
- package/dist/workflows/exec/brief.js +478 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1034 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1355 -0
- package/dist/workflows/exec/run-workflow.js +609 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1212 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +375 -0
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +545 -0
- package/dist/workflows/parser.js +10 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +869 -0
- package/dist/workflows/program/project.js +104 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +246 -69
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +26 -1
- package/docs/data-and-telemetry.md +4 -3
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -8,105 +8,27 @@
|
|
|
8
8
|
* new config field is one line of Zod schema in `core/config-schema.ts` and
|
|
9
9
|
* zero lines here — the walker handles get/set/unset/coercion uniformly.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - `akm config set embedding.ollamaOptions.numCtx` is sugar for
|
|
15
|
-
* `embedding.ollamaOptions.num_ctx` (camelCase ↔ snake_case bridge).
|
|
11
|
+
* `configVersion` is controlled by the config lifecycle. All execution
|
|
12
|
+
* settings use their canonical engine/strategy paths; retired aliases are not
|
|
13
|
+
* rewritten at this boundary.
|
|
16
14
|
* - `parseConfigValue` returns a Partial<AkmConfig> so it can be merged with
|
|
17
15
|
* the runtime config object via `mergeConfigValue`.
|
|
18
16
|
*/
|
|
19
17
|
import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
|
|
20
18
|
import { resolveStashDir } from "../core/common.js";
|
|
21
|
-
import { DEFAULT_CONFIG,
|
|
19
|
+
import { DEFAULT_CONFIG, loadConfig, mutateConfig } from "../core/config/config.js";
|
|
22
20
|
import { configGet, configSet, configUnset, unknownKeyHint } from "../core/config/config-walker.js";
|
|
23
21
|
import { UsageError } from "../core/errors.js";
|
|
24
22
|
import { getCacheDir, getConfigPath, getDbPath, getDefaultStashDir } from "../core/paths.js";
|
|
25
|
-
// ── Legacy `llm.*` → `profiles.llm.<default>.*` aliasing ────────────────────
|
|
26
|
-
/**
|
|
27
|
-
* Map a legacy top-level `llm.<sub>` path onto the actual schema path. The
|
|
28
|
-
* default profile name is "default" when `defaults.llm` is unset.
|
|
29
|
-
*/
|
|
30
|
-
function rewriteLegacyLlmPath(config, key) {
|
|
31
|
-
if (key !== "llm" && !key.startsWith("llm."))
|
|
32
|
-
return key;
|
|
33
|
-
const sub = key === "llm" ? "" : key.slice("llm.".length);
|
|
34
|
-
const profileName = config.defaults?.llm ?? "default";
|
|
35
|
-
return sub ? `profiles.llm.${profileName}.${sub}` : `profiles.llm.${profileName}`;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Translate the legacy `embedding.ollamaOptions.numCtx` to the actual schema
|
|
39
|
-
* key `embedding.ollamaOptions.num_ctx`.
|
|
40
|
-
*/
|
|
41
|
-
function rewriteEmbeddingPath(key) {
|
|
42
|
-
if (key === "embedding.ollamaOptions.numCtx")
|
|
43
|
-
return "embedding.ollamaOptions.num_ctx";
|
|
44
|
-
return key;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Translate the deprecated `stashes` alias for `sources` (one-way: both read
|
|
48
|
-
* and write go through `sources`).
|
|
49
|
-
*/
|
|
50
|
-
function rewriteSourcesAlias(key) {
|
|
51
|
-
if (key === "stashes")
|
|
52
|
-
return "sources";
|
|
53
|
-
if (key.startsWith("stashes."))
|
|
54
|
-
return `sources.${key.slice("stashes.".length)}`;
|
|
55
|
-
return key;
|
|
56
|
-
}
|
|
57
|
-
function rewriteKey(config, key) {
|
|
58
|
-
let k = rewriteLegacyLlmPath(config, key);
|
|
59
|
-
k = rewriteEmbeddingPath(k);
|
|
60
|
-
k = rewriteSourcesAlias(k);
|
|
61
|
-
return k;
|
|
62
|
-
}
|
|
63
23
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
64
24
|
export function getConfigValue(config, key) {
|
|
65
|
-
|
|
66
|
-
const value = configGet(config, k);
|
|
67
|
-
if (k.split(".").at(-1) === "apiKey")
|
|
68
|
-
return null;
|
|
69
|
-
return omitApiKeysForOutput(value);
|
|
25
|
+
return redactConfigValue(configGet(config, key));
|
|
70
26
|
}
|
|
71
27
|
export function setConfigValue(config, key, rawValue) {
|
|
72
|
-
|
|
73
|
-
// env var the user typed (AKM_LLM_API_KEY) rather than the rewritten profile
|
|
74
|
-
// env var (AKM_PROFILE_DEFAULT_API_KEY) — both work at runtime, but the
|
|
75
|
-
// shorter name matches the user's mental model.
|
|
76
|
-
if (key === "llm.apiKey") {
|
|
77
|
-
throw new UsageError("apiKey cannot be persisted in config; export AKM_LLM_API_KEY instead. (key: llm.apiKey)", "INVALID_FLAG_VALUE", "Storing API keys in config.json leaks them through backups, logs, and version control. " +
|
|
78
|
-
"Use the corresponding environment variable. AKM reads it at request time.");
|
|
79
|
-
}
|
|
80
|
-
if (key === "embedding.apiKey") {
|
|
81
|
-
throw new UsageError("apiKey cannot be persisted in config; export AKM_EMBED_API_KEY instead. (key: embedding.apiKey)", "INVALID_FLAG_VALUE", "Storing API keys in config.json leaks them through backups, logs, and version control. " +
|
|
82
|
-
"Use the corresponding environment variable. AKM reads it at request time.");
|
|
83
|
-
}
|
|
84
|
-
const k = rewriteKey(config, key);
|
|
85
|
-
// Legacy ergonomic: `akm config set semanticSearchMode true|false`
|
|
86
|
-
let coerced = rawValue;
|
|
87
|
-
if (k === "semanticSearchMode") {
|
|
88
|
-
if (rawValue === "true")
|
|
89
|
-
coerced = "auto";
|
|
90
|
-
else if (rawValue === "false")
|
|
91
|
-
coerced = "off";
|
|
92
|
-
}
|
|
93
|
-
let next = configSet(config, k, coerced);
|
|
94
|
-
// Legacy ergonomic shim: when the user sets `llm.<field>` and no
|
|
95
|
-
// `defaults.llm` is set, point it at the freshly-created profile so the
|
|
96
|
-
// value actually takes effect at runtime.
|
|
97
|
-
if (key === "llm" || key.startsWith("llm.")) {
|
|
98
|
-
if (!next.defaults?.llm) {
|
|
99
|
-
next = {
|
|
100
|
-
...next,
|
|
101
|
-
defaults: { ...(next.defaults ?? {}), llm: "default" },
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return next;
|
|
28
|
+
return configSet(config, key, rawValue);
|
|
106
29
|
}
|
|
107
30
|
export function unsetConfigValue(config, key) {
|
|
108
|
-
|
|
109
|
-
return configUnset(config, k);
|
|
31
|
+
return configUnset(config, key);
|
|
110
32
|
}
|
|
111
33
|
/**
|
|
112
34
|
* Compatibility shim: returns a `Partial<AkmConfig>` containing just the
|
|
@@ -128,48 +50,21 @@ export function parseConfigValue(key, value) {
|
|
|
128
50
|
return patch;
|
|
129
51
|
}
|
|
130
52
|
export function listConfig(config) {
|
|
131
|
-
|
|
132
|
-
semanticSearchMode: config.semanticSearchMode,
|
|
133
|
-
registries: config.registries ?? DEFAULT_CONFIG.registries ?? [],
|
|
134
|
-
output: { ...(DEFAULT_CONFIG.output ?? {}), ...(config.output ?? {}) },
|
|
135
|
-
stashDir: config.stashDir ?? null,
|
|
136
|
-
installed: config.installed ?? [],
|
|
137
|
-
sources: getSources(config),
|
|
138
|
-
};
|
|
139
|
-
if (config.defaultWriteTarget)
|
|
140
|
-
result.defaultWriteTarget = config.defaultWriteTarget;
|
|
141
|
-
if (config.embedding)
|
|
142
|
-
result.embedding = config.embedding;
|
|
143
|
-
if (config.profiles)
|
|
144
|
-
result.profiles = config.profiles;
|
|
145
|
-
if (config.defaults)
|
|
146
|
-
result.defaults = config.defaults;
|
|
147
|
-
if (config.search)
|
|
148
|
-
result.search = config.search;
|
|
149
|
-
if (config.index)
|
|
150
|
-
result.index = config.index;
|
|
151
|
-
if (config.feedback)
|
|
152
|
-
result.feedback = config.feedback;
|
|
153
|
-
if (config.improve)
|
|
154
|
-
result.improve = config.improve;
|
|
155
|
-
if (config.archiveRetentionDays !== undefined)
|
|
156
|
-
result.archiveRetentionDays = config.archiveRetentionDays;
|
|
157
|
-
if (config.configVersion !== undefined)
|
|
158
|
-
result.configVersion = config.configVersion;
|
|
159
|
-
return omitApiKeysForOutput(result);
|
|
53
|
+
return redactConfigValue({ ...DEFAULT_CONFIG, ...config, sources: config.sources ?? [] });
|
|
160
54
|
}
|
|
161
|
-
function
|
|
55
|
+
function redactConfigValue(value) {
|
|
162
56
|
if (Array.isArray(value))
|
|
163
|
-
return value.map(
|
|
57
|
+
return value.map(redactConfigValue);
|
|
164
58
|
if (!value || typeof value !== "object")
|
|
165
59
|
return value;
|
|
166
|
-
const
|
|
60
|
+
const result = {};
|
|
167
61
|
for (const [key, child] of Object.entries(value)) {
|
|
168
|
-
if (key
|
|
169
|
-
|
|
170
|
-
|
|
62
|
+
if (key !== "apiKey" ||
|
|
63
|
+
(typeof child === "string" && /^\$(?:[A-Za-z_][A-Za-z0-9_]*|\{[A-Za-z_][A-Za-z0-9_]*\})$/.test(child))) {
|
|
64
|
+
result[key] = redactConfigValue(child);
|
|
65
|
+
}
|
|
171
66
|
}
|
|
172
|
-
return
|
|
67
|
+
return result;
|
|
173
68
|
}
|
|
174
69
|
export { unknownKeyHint };
|
|
175
70
|
// ── `akm config` command surface ────────────────────────────────────────────
|
|
@@ -190,25 +85,21 @@ function normalizeToggleTarget(target) {
|
|
|
190
85
|
throw new UsageError(`Unsupported target "${target}". Supported targets: skills.sh`);
|
|
191
86
|
}
|
|
192
87
|
function toggleSkillsShRegistry(enabled) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled: true });
|
|
210
|
-
saveConfig({ ...config, registries });
|
|
211
|
-
return { changed: true, component: SKILLS_SH_NAME, enabled: true };
|
|
88
|
+
let changed = false;
|
|
89
|
+
mutateConfig((config) => {
|
|
90
|
+
const registries = (config.registries ?? DEFAULT_CONFIG.registries ?? []).map((registry) => ({ ...registry }));
|
|
91
|
+
const idx = registries.findIndex((registry) => registry.provider === SKILLS_SH_PROVIDER || registry.name === SKILLS_SH_NAME || registry.url === SKILLS_SH_URL);
|
|
92
|
+
if (idx >= 0) {
|
|
93
|
+
const wasEnabled = registries[idx].enabled !== false;
|
|
94
|
+
registries[idx].enabled = enabled;
|
|
95
|
+
changed = wasEnabled !== enabled;
|
|
96
|
+
return { ...config, registries };
|
|
97
|
+
}
|
|
98
|
+
registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled });
|
|
99
|
+
changed = true;
|
|
100
|
+
return { ...config, registries };
|
|
101
|
+
});
|
|
102
|
+
return { changed, component: SKILLS_SH_NAME, enabled };
|
|
212
103
|
}
|
|
213
104
|
function toggleComponent(targetRaw, enabled) {
|
|
214
105
|
const target = normalizeToggleTarget(targetRaw);
|
|
@@ -276,7 +167,11 @@ export const configCommand = defineGroupCommand({
|
|
|
276
167
|
set: defineJsonCommand({
|
|
277
168
|
meta: { name: "set", description: "Set a configuration value by key" },
|
|
278
169
|
args: {
|
|
279
|
-
key: {
|
|
170
|
+
key: {
|
|
171
|
+
type: "positional",
|
|
172
|
+
required: true,
|
|
173
|
+
description: "Config key (for example: embedding, engines.default)",
|
|
174
|
+
},
|
|
280
175
|
value: { type: "positional", required: true, description: "Config value" },
|
|
281
176
|
// #463: stable machine-friendly entry point for plugins / hooks.
|
|
282
177
|
// `--silent` suppresses the config dump on stdout so hook-driven
|
|
@@ -297,20 +192,19 @@ export const configCommand = defineGroupCommand({
|
|
|
297
192
|
},
|
|
298
193
|
run({ args }) {
|
|
299
194
|
if (args.layer && args.layer !== "user") {
|
|
300
|
-
throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.
|
|
195
|
+
throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.9.0.`, "INVALID_FLAG_VALUE");
|
|
301
196
|
}
|
|
302
197
|
// Use loadConfig (not loadUserConfig) so the project-config
|
|
303
198
|
// deprecation warning fires consistently with `akm config get`
|
|
304
199
|
// (#457). Effective merged shape is identical post-0.8.0.
|
|
305
|
-
const updated =
|
|
306
|
-
saveConfig(updated);
|
|
200
|
+
const updated = mutateConfig((current) => setConfigValue(current, args.key, args.value)).config;
|
|
307
201
|
if (!args.silent) {
|
|
308
202
|
output("config", listConfig(updated));
|
|
309
203
|
}
|
|
310
204
|
},
|
|
311
205
|
}),
|
|
312
206
|
unset: defineJsonCommand({
|
|
313
|
-
meta: { name: "unset", description: "Unset an optional configuration key or whole embedding/
|
|
207
|
+
meta: { name: "unset", description: "Unset an optional configuration key or whole embedding/engine section" },
|
|
314
208
|
args: {
|
|
315
209
|
key: { type: "positional", required: true, description: "Config key to unset" },
|
|
316
210
|
silent: {
|
|
@@ -326,10 +220,10 @@ export const configCommand = defineGroupCommand({
|
|
|
326
220
|
},
|
|
327
221
|
run({ args }) {
|
|
328
222
|
if (args.layer && args.layer !== "user") {
|
|
329
|
-
throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.
|
|
223
|
+
throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.9.0.`, "INVALID_FLAG_VALUE");
|
|
330
224
|
}
|
|
331
|
-
const
|
|
332
|
-
|
|
225
|
+
const result = mutateConfig((current) => unsetConfigValue(current, args.key), { absentNoop: true });
|
|
226
|
+
const updated = result.config;
|
|
333
227
|
if (!args.silent) {
|
|
334
228
|
output("config", listConfig(updated));
|
|
335
229
|
}
|
|
@@ -348,19 +242,11 @@ export const configCommand = defineGroupCommand({
|
|
|
348
242
|
migrate: defineJsonCommand({
|
|
349
243
|
meta: {
|
|
350
244
|
name: "migrate",
|
|
351
|
-
description: "
|
|
245
|
+
description: "Diagnose whether the user config is already in the current schema. Never writes or translates config.",
|
|
352
246
|
},
|
|
353
|
-
|
|
354
|
-
"dry-run": { type: "boolean", description: "Preview the migration result without writing.", default: false },
|
|
355
|
-
"print-diff": {
|
|
356
|
-
type: "boolean",
|
|
357
|
-
description: "Print a unified diff of old vs new config alongside the migration output.",
|
|
358
|
-
default: false,
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
async run({ args }) {
|
|
247
|
+
async run() {
|
|
362
248
|
const { runConfigMigrate } = await import("../cli/config-migrate.js");
|
|
363
|
-
await runConfigMigrate(
|
|
249
|
+
await runConfigMigrate();
|
|
364
250
|
},
|
|
365
251
|
}),
|
|
366
252
|
enable: defineJsonCommand({
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
* Env-ref → resolved values, shared between `akm env run` and the workflow
|
|
6
|
+
* engine's per-unit env bindings (orchestration plan, *Dispatch context*).
|
|
7
|
+
*
|
|
8
|
+
* Extracted from `env-cli.ts`'s `runEnvInjected` so the native executor
|
|
9
|
+
* REUSES the exact same machinery instead of forking it. Every safety
|
|
10
|
+
* invariant carries over unchanged:
|
|
11
|
+
*
|
|
12
|
+
* - `${secret:NAME}` tokens resolve against the env's own stash; a missing
|
|
13
|
+
* secret is a hard error and NOTHING is injected (no partial injection).
|
|
14
|
+
* - Known process-hijacking keys (LD_PRELOAD, PATH, …) are blocked for
|
|
15
|
+
* third-party-sourced stashes and warned about for first-party ones.
|
|
16
|
+
* - An `env_access` audit event records key NAMES only, never values.
|
|
17
|
+
* - Resolved values must never be written to stdout or logs by callers.
|
|
18
|
+
*/
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { resolveAssetPathFromName } from "../../core/asset/asset-spec.js";
|
|
22
|
+
import { isWithin } from "../../core/common.js";
|
|
23
|
+
import { makeEnvRef, resolveEnvPath } from "../../core/env-secret-ref.js";
|
|
24
|
+
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
25
|
+
import { appendEvent } from "../../core/events.js";
|
|
26
|
+
import { isDangerousEnvKey } from "../lint/env-key-rules.js";
|
|
27
|
+
import { loadEnv, resolveSecretTokens } from "./env.js";
|
|
28
|
+
import { readValue } from "./secret.js";
|
|
29
|
+
/**
|
|
30
|
+
* Resolve an env ref to its injectable values: load the file, apply key
|
|
31
|
+
* filtering, substitute `${secret:NAME}` tokens from the sibling secrets
|
|
32
|
+
* directory, enforce the dangerous-key policy, and append the keys-only
|
|
33
|
+
* `env_access` audit event.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveEnvBinding(target, options = {}) {
|
|
36
|
+
const warn = options.warn ?? ((message) => process.stderr.write(`warning: ${message}\n`));
|
|
37
|
+
const { name, absPath, source } = resolveEnvPath(target);
|
|
38
|
+
const envRef = makeEnvRef(name, source);
|
|
39
|
+
if (!fs.existsSync(absPath)) {
|
|
40
|
+
throw new NotFoundError(`Env not found: ${envRef}`);
|
|
41
|
+
}
|
|
42
|
+
const allValues = loadEnv(absPath);
|
|
43
|
+
// Value-safe key filtering (--only / --except operate on key NAMES only).
|
|
44
|
+
let envValues = allValues;
|
|
45
|
+
if (options.only && options.except) {
|
|
46
|
+
throw new UsageError("Pass only one of --only or --except.", "INVALID_FLAG_VALUE");
|
|
47
|
+
}
|
|
48
|
+
if (options.only) {
|
|
49
|
+
const wanted = new Set(options.only);
|
|
50
|
+
const missing = options.only.filter((k) => !(k in allValues));
|
|
51
|
+
if (missing.length > 0) {
|
|
52
|
+
warn(`--only key(s) not present in ${envRef}: ${missing.join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => wanted.has(k)));
|
|
55
|
+
}
|
|
56
|
+
else if (options.except) {
|
|
57
|
+
const excluded = new Set(options.except);
|
|
58
|
+
envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => !excluded.has(k)));
|
|
59
|
+
}
|
|
60
|
+
// Substitute `${secret:NAME}` tokens with the sibling secret asset in the
|
|
61
|
+
// SAME stash. A missing secret is a hard error — inject NOTHING.
|
|
62
|
+
const secretsRoot = path.join(source.path, "secrets");
|
|
63
|
+
const resolveSecret = (secretName) => {
|
|
64
|
+
const secretPath = resolveAssetPathFromName("secret", secretsRoot, secretName);
|
|
65
|
+
// Defense-in-depth: ensure the resolved path stays inside the secrets dir.
|
|
66
|
+
if (!isWithin(secretPath, secretsRoot)) {
|
|
67
|
+
throw new UsageError(`Secret name "${secretName}" escapes the secrets directory.`);
|
|
68
|
+
}
|
|
69
|
+
if (!fs.existsSync(secretPath))
|
|
70
|
+
return undefined;
|
|
71
|
+
// Match `secret run`: read utf8, do not trim (stay consistent with that path).
|
|
72
|
+
return readValue(secretPath).toString("utf8");
|
|
73
|
+
};
|
|
74
|
+
const { values: substituted, missing } = resolveSecretTokens(envValues, resolveSecret);
|
|
75
|
+
if (missing.length > 0) {
|
|
76
|
+
throw new NotFoundError(`Env "${envRef}" references secret(s) not found in its stash: ${missing.map((n) => `secret:${n}`).join(", ")}. Nothing was injected.`, "FILE_NOT_FOUND", `Create the missing secret, e.g. \`akm secret set secret:${missing[0]}\`.`);
|
|
77
|
+
}
|
|
78
|
+
envValues = substituted;
|
|
79
|
+
const keys = Object.keys(envValues);
|
|
80
|
+
// Scan injected keys for known process-hijacking variables. Block for
|
|
81
|
+
// third-party-sourced stashes (origin has a registryId); warn for the
|
|
82
|
+
// operator's own first-party stash, where they own the file.
|
|
83
|
+
const dangerous = keys.filter(isDangerousEnvKey);
|
|
84
|
+
if (dangerous.length > 0) {
|
|
85
|
+
const detail = `Env "${envRef}" injects process-hijacking variable(s): ${dangerous.join(", ")}.`;
|
|
86
|
+
if (source.registryId) {
|
|
87
|
+
throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
|
|
88
|
+
` Review the file, then copy the values into a first-party env if you trust them.`, "INVALID_FLAG_VALUE");
|
|
89
|
+
}
|
|
90
|
+
warn(`${detail} Injecting anyway (first-party stash).`);
|
|
91
|
+
}
|
|
92
|
+
// Audit trail: keys only, never values.
|
|
93
|
+
appendEvent({ eventType: "env_access", ref: envRef, metadata: { keys } });
|
|
94
|
+
return { ref: envRef, values: envValues, keys };
|
|
95
|
+
}
|
|
@@ -27,7 +27,6 @@ import { isWithin, writeFileAtomic } from "../../core/common.js";
|
|
|
27
27
|
import { loadConfig } from "../../core/config/config.js";
|
|
28
28
|
import { findEnvSource, makeEnvRef, parseEnvRef, resolveEnvPath } from "../../core/env-secret-ref.js";
|
|
29
29
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
30
|
-
import { appendEvent } from "../../core/events.js";
|
|
31
30
|
import { isQuiet } from "../../core/warn.js";
|
|
32
31
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
33
32
|
import { parseFlagValue } from "../../output/context.js";
|
|
@@ -232,64 +231,14 @@ async function runEnvInjected(target, opts) {
|
|
|
232
231
|
}
|
|
233
232
|
throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
|
|
234
233
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const wanted = new Set(opts.only);
|
|
244
|
-
const missing = opts.only.filter((k) => !(k in allValues));
|
|
245
|
-
if (missing.length > 0) {
|
|
246
|
-
process.stderr.write(`warning: --only key(s) not present in ${makeEnvRef(name, source)}: ${missing.join(", ")}\n`);
|
|
247
|
-
}
|
|
248
|
-
envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => wanted.has(k)));
|
|
249
|
-
}
|
|
250
|
-
else if (opts.except) {
|
|
251
|
-
const excluded = new Set(opts.except);
|
|
252
|
-
envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => !excluded.has(k)));
|
|
253
|
-
}
|
|
254
|
-
// Substitute `${secret:NAME}` tokens in values with the value of the sibling
|
|
255
|
-
// secret asset in the SAME stash. The lookup is injected so commands/env.ts
|
|
256
|
-
// keeps its narrow dependency surface; we resolve each name against this env's
|
|
257
|
-
// own `source`. A missing secret is a hard error — inject NOTHING (no partial
|
|
258
|
-
// injection). Resolved values are never logged or printed.
|
|
259
|
-
const { resolveSecretTokens } = await import("./env.js");
|
|
260
|
-
const { readValue } = await import("./secret.js");
|
|
261
|
-
const secretsRoot = path.join(source.path, "secrets");
|
|
262
|
-
const resolveSecret = (secretName) => {
|
|
263
|
-
const secretPath = resolveAssetPathFromName("secret", secretsRoot, secretName);
|
|
264
|
-
// Defense-in-depth: ensure the resolved path stays inside the secrets dir.
|
|
265
|
-
if (!isWithin(secretPath, secretsRoot)) {
|
|
266
|
-
throw new UsageError(`Secret name "${secretName}" escapes the secrets directory.`);
|
|
267
|
-
}
|
|
268
|
-
if (!fs.existsSync(secretPath))
|
|
269
|
-
return undefined;
|
|
270
|
-
// Match `secret run`: read utf8, do not trim (stay consistent with that path).
|
|
271
|
-
return readValue(secretPath).toString("utf8");
|
|
272
|
-
};
|
|
273
|
-
const { values: substituted, missing } = resolveSecretTokens(envValues, resolveSecret);
|
|
274
|
-
if (missing.length > 0) {
|
|
275
|
-
const envRef = makeEnvRef(name, source);
|
|
276
|
-
throw new NotFoundError(`Env "${envRef}" references secret(s) not found in its stash: ${missing.map((n) => `secret:${n}`).join(", ")}. Nothing was injected.`, "FILE_NOT_FOUND", `Create the missing secret, e.g. \`akm secret set secret:${missing[0]}\`.`);
|
|
277
|
-
}
|
|
278
|
-
envValues = substituted;
|
|
279
|
-
const keys = Object.keys(envValues);
|
|
280
|
-
// Scan injected keys for known process-hijacking variables (LD_PRELOAD,
|
|
281
|
-
// PATH, ...). Block for third-party-sourced stashes (origin has a registryId);
|
|
282
|
-
// warn for the operator's own first-party stash, where they own the file.
|
|
283
|
-
const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
|
|
284
|
-
const dangerous = keys.filter(isDangerousEnvKey);
|
|
285
|
-
if (dangerous.length > 0) {
|
|
286
|
-
const detail = `Env "${makeEnvRef(name, source)}" injects process-hijacking variable(s): ${dangerous.join(", ")}.`;
|
|
287
|
-
if (source.registryId) {
|
|
288
|
-
throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
|
|
289
|
-
` Review the file, then copy the values into a first-party env if you trust them.`, "INVALID_FLAG_VALUE");
|
|
290
|
-
}
|
|
291
|
-
process.stderr.write(`warning: ${detail} Injecting anyway (first-party stash).\n`);
|
|
292
|
-
}
|
|
234
|
+
// Load → filter → secret-substitute → dangerous-key policy → keys-only
|
|
235
|
+
// audit event. Shared with the workflow engine's per-unit env bindings —
|
|
236
|
+
// see env-binding.ts for the extracted core and its safety invariants.
|
|
237
|
+
const { resolveEnvBinding } = await import("./env-binding.js");
|
|
238
|
+
const { values: envValues } = resolveEnvBinding(target, {
|
|
239
|
+
only: opts.only,
|
|
240
|
+
except: opts.except,
|
|
241
|
+
});
|
|
293
242
|
const mergedEnv = buildChildEnv(process.env, {
|
|
294
243
|
clean: opts.clean === true,
|
|
295
244
|
inherit: opts.inherit ?? [],
|
|
@@ -297,12 +246,6 @@ async function runEnvInjected(target, opts) {
|
|
|
297
246
|
for (const [envKey, envValue] of Object.entries(envValues)) {
|
|
298
247
|
mergedEnv[envKey] = envValue;
|
|
299
248
|
}
|
|
300
|
-
// Audit trail: keys only, never values.
|
|
301
|
-
appendEvent({
|
|
302
|
-
eventType: "env_access",
|
|
303
|
-
ref: makeEnvRef(name, source),
|
|
304
|
-
metadata: { keys },
|
|
305
|
-
});
|
|
306
249
|
const result = spawnSync(command[0], command.slice(1), {
|
|
307
250
|
stdio: "inherit",
|
|
308
251
|
env: mergedEnv,
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import fs from "node:fs";
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
import { writeFileAtomic } from "../../core/common.js";
|
|
30
|
-
import { probeLock, releaseLock, tryAcquireLockSync } from "../../core/file-lock.js";
|
|
30
|
+
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js";
|
|
31
31
|
import { sleepSync } from "../../runtime.js";
|
|
32
32
|
// ── Write-lock helper ─────────────────────────────────────────────────────────
|
|
33
33
|
/**
|
|
@@ -39,10 +39,13 @@ import { sleepSync } from "../../runtime.js";
|
|
|
39
39
|
export function withSecretLock(secretPath, fn) {
|
|
40
40
|
const lockPath = `${secretPath}.lock`;
|
|
41
41
|
const deadline = Date.now() + 5000;
|
|
42
|
-
|
|
42
|
+
let ownership;
|
|
43
|
+
while (!ownership) {
|
|
44
|
+
ownership = tryAcquireLockSync(lockPath, createLockPayload());
|
|
45
|
+
if (ownership)
|
|
46
|
+
break;
|
|
43
47
|
const probe = probeLock(lockPath);
|
|
44
|
-
if (probe.state === "stale") {
|
|
45
|
-
releaseLock(lockPath);
|
|
48
|
+
if (probe.state === "stale" && reclaimStaleLock(lockPath, probe)) {
|
|
46
49
|
continue;
|
|
47
50
|
}
|
|
48
51
|
if (Date.now() > deadline) {
|
|
@@ -57,7 +60,7 @@ export function withSecretLock(secretPath, fn) {
|
|
|
57
60
|
return fn();
|
|
58
61
|
}
|
|
59
62
|
finally {
|
|
60
|
-
releaseLock(
|
|
63
|
+
releaseLock(ownership);
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
// ── Atomic byte write ──────────────────────────────────────────────────────────
|