akm-cli 0.8.0-rc1 → 0.8.0
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/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
- 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 +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2162 -1258
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1533 -144
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- 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 +260 -5
- package/dist/commands/health.js +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +233 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -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 +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +662 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +84 -14
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +114 -48
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -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 +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- 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 +92 -56
- package/dist/llm/graph-extract.js +401 -30
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- 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 +105 -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 -549
- 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 +1059 -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 +12 -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 -1329
- 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 +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- 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 +17767 -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 +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- 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 +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -307
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
|
@@ -0,0 +1,558 @@
|
|
|
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
|
+
* Zod schema for AkmConfig — the single source of truth for the on-disk shape.
|
|
6
|
+
*
|
|
7
|
+
* Two responsibilities:
|
|
8
|
+
* 1. **Validate + transform** the raw JSON-parsed config object into the runtime
|
|
9
|
+
* `AkmConfig` shape consumed by the rest of the codebase. Replaces the
|
|
10
|
+
* ~1.4k LOC of legacy per-shape parsers (parseLlmConfig, parseEmbeddingConfig,
|
|
11
|
+
* parseIndexConfig, etc.) — see `loadConfig` in `./config.ts`.
|
|
12
|
+
* 2. **Reject hard-errored values** (openviking source type, legacy
|
|
13
|
+
* `stashes[]` key) at load time via `superRefine`.
|
|
14
|
+
*
|
|
15
|
+
* Design rules:
|
|
16
|
+
* - Top-level uses `.passthrough()` so unknown future keys round-trip intact on
|
|
17
|
+
* read; `sanitizeConfigForWrite` decides what to persist.
|
|
18
|
+
* - Most nested sub-objects use `.catch(undefined)` so malformed entries are
|
|
19
|
+
* silently dropped (matches the legacy parser's warn-and-ignore semantics for
|
|
20
|
+
* field-level shape errors — keeps cold-start working when a user has a
|
|
21
|
+
* typo in their config).
|
|
22
|
+
* - Two exceptions (hard-rejected): openviking source type and legacy
|
|
23
|
+
* `stashes[]` key. Both have explicit migration paths; silently dropping
|
|
24
|
+
* would mask user data loss.
|
|
25
|
+
* - `.strict()` walls still gate `registries[]`, `sources[]`, `profiles.*`
|
|
26
|
+
* sub-shapes so typos in those structured records are caught (#462).
|
|
27
|
+
* - `defaultWriteTarget` resolution and similar cross-field invariants are
|
|
28
|
+
* enforced at save time via `superRefine` on the top-level schema.
|
|
29
|
+
*/
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
// ── Reusable atomic schemas ─────────────────────────────────────────────────
|
|
32
|
+
/** Positive integer (used for tokens, timeouts, batch sizes). */
|
|
33
|
+
const positiveInt = z.number().int().positive();
|
|
34
|
+
/** Non-negative finite number (used for scores, weights, days). */
|
|
35
|
+
const nonNegativeNumber = z.number().finite().min(0);
|
|
36
|
+
/** Non-empty string (rejects "" and whitespace-only). */
|
|
37
|
+
const nonEmptyString = z
|
|
38
|
+
.string()
|
|
39
|
+
.min(1)
|
|
40
|
+
.refine((v) => v.trim().length > 0, { message: "expected a non-empty string" });
|
|
41
|
+
/** HTTP(S) URL string. */
|
|
42
|
+
const httpUrl = z.string().refine((v) => v.startsWith("http://") || v.startsWith("https://"), {
|
|
43
|
+
message: "endpoint must start with http:// or https://",
|
|
44
|
+
});
|
|
45
|
+
// ── Feedback failure modes ──────────────────────────────────────────────────
|
|
46
|
+
export const FEEDBACK_FAILURE_MODES = ["incorrect", "outdated", "dangerous", "incomplete", "redundant"];
|
|
47
|
+
// ── Connection configs (LLM / embedding) ────────────────────────────────────
|
|
48
|
+
const LlmCapabilitiesSchema = z
|
|
49
|
+
.object({
|
|
50
|
+
structuredOutput: z.boolean().optional(),
|
|
51
|
+
})
|
|
52
|
+
.strict();
|
|
53
|
+
/**
|
|
54
|
+
* Connection config used for both top-level `llm` (after migration) and
|
|
55
|
+
* `profiles.llm[*]`. `model` is required at schema level — partial entries
|
|
56
|
+
* created by `akm config set llm.endpoint <url>` (where model is left absent)
|
|
57
|
+
* are normalized to `model: ""` *before* Zod sees them by the load-time
|
|
58
|
+
* pre-Zod migrator hook, so this strict shape gates CLI writes without
|
|
59
|
+
* breaking legacy load-time partial configs.
|
|
60
|
+
*/
|
|
61
|
+
export const LlmConnectionConfigSchema = z
|
|
62
|
+
.object({
|
|
63
|
+
provider: z.string().optional(),
|
|
64
|
+
endpoint: z.string(),
|
|
65
|
+
model: z.string(),
|
|
66
|
+
apiKey: z.string().optional(),
|
|
67
|
+
temperature: z.number().finite().optional(),
|
|
68
|
+
maxTokens: positiveInt.optional(),
|
|
69
|
+
timeoutMs: positiveInt.optional(),
|
|
70
|
+
concurrency: positiveInt.optional(),
|
|
71
|
+
capabilities: LlmCapabilitiesSchema.optional(),
|
|
72
|
+
extraParams: z.record(z.unknown()).optional(),
|
|
73
|
+
contextLength: positiveInt.optional(),
|
|
74
|
+
judgeModel: z.string().min(1).optional(),
|
|
75
|
+
enableThinking: z.boolean().optional(),
|
|
76
|
+
})
|
|
77
|
+
.strict();
|
|
78
|
+
export const LlmProfileConfigSchema = LlmConnectionConfigSchema.extend({
|
|
79
|
+
supportsJsonSchema: z.boolean().optional(),
|
|
80
|
+
}).strict();
|
|
81
|
+
const EmbeddingOllamaOptionsSchema = z
|
|
82
|
+
.object({
|
|
83
|
+
num_ctx: positiveInt.optional(),
|
|
84
|
+
})
|
|
85
|
+
.strict();
|
|
86
|
+
/**
|
|
87
|
+
* Embedding connection config. Both `endpoint` and `model` are optional:
|
|
88
|
+
* - Remote: provide `endpoint` (http/https URL) + `model`.
|
|
89
|
+
* - Local-only: omit `endpoint`/`model`; set `localModel` (or fall back to
|
|
90
|
+
* {@link DEFAULT_LOCAL_MODEL}).
|
|
91
|
+
*
|
|
92
|
+
* Consumers route via `hasRemoteEndpoint()` which checks for an http(s)
|
|
93
|
+
* endpoint — absent fields take the local path naturally, no sentinels needed.
|
|
94
|
+
*/
|
|
95
|
+
export const EmbeddingConnectionConfigSchema = z
|
|
96
|
+
.object({
|
|
97
|
+
provider: z.string().optional(),
|
|
98
|
+
endpoint: z.string().optional(),
|
|
99
|
+
model: z.string().optional(),
|
|
100
|
+
apiKey: z.string().optional(),
|
|
101
|
+
dimension: positiveInt.optional(),
|
|
102
|
+
localModel: z.string().min(1).optional(),
|
|
103
|
+
maxTokens: positiveInt.optional(),
|
|
104
|
+
batchSize: positiveInt.optional(),
|
|
105
|
+
chunkSize: positiveInt.optional(),
|
|
106
|
+
contextLength: positiveInt.optional(),
|
|
107
|
+
ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
// ── Agent profiles ──────────────────────────────────────────────────────────
|
|
111
|
+
const AgentPlatformSchema = z.enum(["opencode", "claude", "opencode-sdk"]);
|
|
112
|
+
export const AgentProfileConfigSchema = z
|
|
113
|
+
.object({
|
|
114
|
+
platform: AgentPlatformSchema,
|
|
115
|
+
bin: z.string().min(1).optional(),
|
|
116
|
+
args: z.array(z.string()).optional(),
|
|
117
|
+
workspace: z.string().min(1).optional(),
|
|
118
|
+
model: z.string().min(1).optional(),
|
|
119
|
+
})
|
|
120
|
+
.strict();
|
|
121
|
+
// ── Improve profile / process ──────────────────────────────────────────────
|
|
122
|
+
export const ImproveProcessConfigSchema = z
|
|
123
|
+
.object({
|
|
124
|
+
enabled: z.boolean().optional(),
|
|
125
|
+
mode: z.enum(["llm", "agent", "sdk"]).optional(),
|
|
126
|
+
profile: z.string().min(1).optional(),
|
|
127
|
+
timeoutMs: z.union([positiveInt, z.null()]).optional(),
|
|
128
|
+
allowedTypes: z.array(z.string().min(1)).optional(),
|
|
129
|
+
qualityGate: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
130
|
+
contradictionDetection: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
131
|
+
// Extract process config (only meaningful for extract process)
|
|
132
|
+
defaultSince: z.string().min(1).optional(),
|
|
133
|
+
maxTotalChars: positiveInt.optional(),
|
|
134
|
+
maxChunkSize: z.number().int().min(1).max(50).optional(),
|
|
135
|
+
// Triage process config (only meaningful for the `triage` process)
|
|
136
|
+
applyMode: z.enum(["queue", "promote"]).optional(),
|
|
137
|
+
policy: z.string().min(1).optional(),
|
|
138
|
+
maxAcceptsPerRun: positiveInt.optional(),
|
|
139
|
+
maxDiffLines: positiveInt.optional(),
|
|
140
|
+
rejectEmpty: z.boolean().optional(),
|
|
141
|
+
judgment: z
|
|
142
|
+
.object({
|
|
143
|
+
mode: z.enum(["llm", "agent", "sdk"]).optional(),
|
|
144
|
+
profile: z.string().min(1).optional(),
|
|
145
|
+
timeoutMs: z.union([positiveInt, z.null()]).optional(),
|
|
146
|
+
})
|
|
147
|
+
.strict()
|
|
148
|
+
.optional(),
|
|
149
|
+
})
|
|
150
|
+
.strict();
|
|
151
|
+
const ImproveProfileProcessesSchema = z
|
|
152
|
+
.object({
|
|
153
|
+
reflect: ImproveProcessConfigSchema.optional(),
|
|
154
|
+
distill: ImproveProcessConfigSchema.optional(),
|
|
155
|
+
consolidate: ImproveProcessConfigSchema.optional(),
|
|
156
|
+
memoryInference: ImproveProcessConfigSchema.optional(),
|
|
157
|
+
graphExtraction: ImproveProcessConfigSchema.optional(),
|
|
158
|
+
validation: ImproveProcessConfigSchema.optional(),
|
|
159
|
+
triage: ImproveProcessConfigSchema.optional(),
|
|
160
|
+
})
|
|
161
|
+
.passthrough()
|
|
162
|
+
.superRefine((val, ctx) => {
|
|
163
|
+
// 0.8.0 removed the duplicated `feedbackDistillation` process key — it was
|
|
164
|
+
// a thin wrapper around `processes.distill.enabled`. Single source of truth.
|
|
165
|
+
const raw = val;
|
|
166
|
+
if ("feedbackDistillation" in raw) {
|
|
167
|
+
ctx.addIssue({
|
|
168
|
+
code: z.ZodIssueCode.custom,
|
|
169
|
+
message: "feedbackDistillation was removed in 0.8.0 — use processes.distill.enabled instead. " +
|
|
170
|
+
"It now controls both the orchestration gate and the LLM-call gate.",
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const allowed = new Set([
|
|
175
|
+
"reflect",
|
|
176
|
+
"distill",
|
|
177
|
+
"consolidate",
|
|
178
|
+
"memoryInference",
|
|
179
|
+
"graphExtraction",
|
|
180
|
+
"validation",
|
|
181
|
+
"extract",
|
|
182
|
+
"triage",
|
|
183
|
+
]);
|
|
184
|
+
for (const k of Object.keys(raw)) {
|
|
185
|
+
if (!allowed.has(k)) {
|
|
186
|
+
ctx.addIssue({
|
|
187
|
+
code: z.ZodIssueCode.unrecognized_keys,
|
|
188
|
+
keys: [k],
|
|
189
|
+
message: `Unrecognized improve process key: "${k}".`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
export const ImproveProfileConfigSchema = z
|
|
195
|
+
.object({
|
|
196
|
+
description: z.string().min(1).optional(),
|
|
197
|
+
processes: ImproveProfileProcessesSchema.optional(),
|
|
198
|
+
autoAccept: nonNegativeNumber.optional(),
|
|
199
|
+
limit: positiveInt.optional(),
|
|
200
|
+
sync: z
|
|
201
|
+
.object({
|
|
202
|
+
enabled: z.boolean().optional(),
|
|
203
|
+
push: z.boolean().optional(),
|
|
204
|
+
message: z.string().min(1).optional(),
|
|
205
|
+
})
|
|
206
|
+
.strict()
|
|
207
|
+
.optional(),
|
|
208
|
+
})
|
|
209
|
+
.strict();
|
|
210
|
+
// ── Profiles / defaults ────────────────────────────────────────────────────
|
|
211
|
+
export const ProfilesSchema = z
|
|
212
|
+
.object({
|
|
213
|
+
llm: z.record(z.string(), LlmProfileConfigSchema).optional(),
|
|
214
|
+
agent: z.record(z.string(), AgentProfileConfigSchema).optional(),
|
|
215
|
+
improve: z.record(z.string(), ImproveProfileConfigSchema).optional(),
|
|
216
|
+
})
|
|
217
|
+
.strict();
|
|
218
|
+
export const DefaultsSchema = z
|
|
219
|
+
.object({
|
|
220
|
+
llm: z.string().min(1).optional(),
|
|
221
|
+
agent: z.string().min(1).optional(),
|
|
222
|
+
improve: z.string().min(1).optional(),
|
|
223
|
+
})
|
|
224
|
+
.strict();
|
|
225
|
+
// ── Sources / registries / installed ────────────────────────────────────────
|
|
226
|
+
const SourceConfigEntryOptionsSchema = z
|
|
227
|
+
.object({
|
|
228
|
+
pushOnCommit: z.boolean().optional(),
|
|
229
|
+
})
|
|
230
|
+
.passthrough();
|
|
231
|
+
export const SourceConfigEntrySchema = z
|
|
232
|
+
.object({
|
|
233
|
+
type: nonEmptyString,
|
|
234
|
+
path: z.string().min(1).optional(),
|
|
235
|
+
url: z.string().min(1).optional(),
|
|
236
|
+
name: z.string().min(1).optional(),
|
|
237
|
+
enabled: z.boolean().optional(),
|
|
238
|
+
writable: z.boolean().optional(),
|
|
239
|
+
primary: z.boolean().optional(),
|
|
240
|
+
options: SourceConfigEntryOptionsSchema.optional(),
|
|
241
|
+
wikiName: z.string().min(1).optional(),
|
|
242
|
+
})
|
|
243
|
+
.strict()
|
|
244
|
+
.superRefine((entry, ctx) => {
|
|
245
|
+
if (entry.writable === true && (entry.type === "website" || entry.type === "npm")) {
|
|
246
|
+
ctx.addIssue({
|
|
247
|
+
code: z.ZodIssueCode.custom,
|
|
248
|
+
message: `writable: true is only supported on filesystem and git sources (got "${entry.type}"` +
|
|
249
|
+
(entry.name ? ` on source "${entry.name}"` : "") +
|
|
250
|
+
").",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
export const RegistryConfigEntrySchema = z
|
|
255
|
+
.object({
|
|
256
|
+
url: httpUrl,
|
|
257
|
+
name: z.string().min(1).optional(),
|
|
258
|
+
enabled: z.boolean().optional(),
|
|
259
|
+
provider: z.string().min(1).optional(),
|
|
260
|
+
options: z.record(z.unknown()).optional(),
|
|
261
|
+
})
|
|
262
|
+
.strict();
|
|
263
|
+
const KitSourceSchema = z.enum(["filesystem", "git", "npm", "github", "website", "local"]);
|
|
264
|
+
export const InstalledStashEntrySchema = z
|
|
265
|
+
.object({
|
|
266
|
+
id: nonEmptyString,
|
|
267
|
+
source: KitSourceSchema,
|
|
268
|
+
ref: nonEmptyString,
|
|
269
|
+
artifactUrl: nonEmptyString,
|
|
270
|
+
stashRoot: nonEmptyString,
|
|
271
|
+
cacheDir: nonEmptyString,
|
|
272
|
+
installedAt: nonEmptyString,
|
|
273
|
+
writable: z.boolean().optional(),
|
|
274
|
+
resolvedVersion: z.string().min(1).optional(),
|
|
275
|
+
resolvedRevision: z.string().min(1).optional(),
|
|
276
|
+
wikiName: z.string().min(1).optional(),
|
|
277
|
+
})
|
|
278
|
+
.strict()
|
|
279
|
+
.superRefine((entry, ctx) => {
|
|
280
|
+
if (entry.writable === true && entry.source !== "git" && entry.source !== "filesystem") {
|
|
281
|
+
ctx.addIssue({
|
|
282
|
+
code: z.ZodIssueCode.custom,
|
|
283
|
+
message: `writable: true is only supported on filesystem and git sources (got "${entry.source}" on installed entry "${entry.id}").`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
// ── Output ──────────────────────────────────────────────────────────────────
|
|
288
|
+
export const OutputConfigSchema = z
|
|
289
|
+
.object({
|
|
290
|
+
format: z.enum(["json", "yaml", "text"]).optional(),
|
|
291
|
+
detail: z.enum(["brief", "normal", "full"]).optional(),
|
|
292
|
+
})
|
|
293
|
+
.strict();
|
|
294
|
+
// ── Search ──────────────────────────────────────────────────────────────────
|
|
295
|
+
const SearchGraphBoostSchema = z
|
|
296
|
+
.object({
|
|
297
|
+
directBoostPerEntity: nonNegativeNumber.optional(),
|
|
298
|
+
directBoostCap: nonNegativeNumber.optional(),
|
|
299
|
+
hopBoostPerEntity: nonNegativeNumber.optional(),
|
|
300
|
+
hopBoostCap: nonNegativeNumber.optional(),
|
|
301
|
+
/** Hard-capped at 3; values > 3 hard-error so users see the typo. */
|
|
302
|
+
maxHops: positiveInt.max(3).optional(),
|
|
303
|
+
confidenceMode: z.enum(["off", "blend", "multiply"]).default("blend").optional(),
|
|
304
|
+
/** Range [0, 1]; values > 1 hard-error (no silent clamp). */
|
|
305
|
+
confidenceWeight: z.number().finite().min(0).max(1).default(0.2).optional(),
|
|
306
|
+
})
|
|
307
|
+
.strict();
|
|
308
|
+
export const SearchConfigSchema = z
|
|
309
|
+
.object({
|
|
310
|
+
minScore: nonNegativeNumber.optional(),
|
|
311
|
+
curateRerank: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
312
|
+
graphBoost: SearchGraphBoostSchema.optional(),
|
|
313
|
+
})
|
|
314
|
+
.strict();
|
|
315
|
+
// ── Feedback ────────────────────────────────────────────────────────────────
|
|
316
|
+
export const FeedbackConfigSchema = z
|
|
317
|
+
.object({
|
|
318
|
+
requireReason: z.boolean().optional(),
|
|
319
|
+
allowedFailureModes: z.array(nonEmptyString).optional(),
|
|
320
|
+
})
|
|
321
|
+
.strict();
|
|
322
|
+
// ── Improve top-level (utility decay, event retention) ─────────────────────
|
|
323
|
+
const ImproveUtilityDecaySchema = z
|
|
324
|
+
.object({
|
|
325
|
+
halfLifeDays: z.number().finite().min(0.1).optional(),
|
|
326
|
+
feedbackStabilityBoost: z.number().finite().min(1).optional(),
|
|
327
|
+
})
|
|
328
|
+
.strict();
|
|
329
|
+
export const ImproveConfigSchema = z
|
|
330
|
+
.object({
|
|
331
|
+
utilityDecay: ImproveUtilityDecaySchema.optional(),
|
|
332
|
+
eventRetentionDays: nonNegativeNumber.optional(),
|
|
333
|
+
})
|
|
334
|
+
.strict();
|
|
335
|
+
// ── Index / per-pass ────────────────────────────────────────────────────────
|
|
336
|
+
const GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED = [
|
|
337
|
+
"memory",
|
|
338
|
+
"knowledge",
|
|
339
|
+
"skill",
|
|
340
|
+
"command",
|
|
341
|
+
"agent",
|
|
342
|
+
"workflow",
|
|
343
|
+
"lesson",
|
|
344
|
+
"task",
|
|
345
|
+
"wiki",
|
|
346
|
+
];
|
|
347
|
+
const INDEX_PASS_PROVIDER_KEYS = new Set([
|
|
348
|
+
"endpoint",
|
|
349
|
+
"model",
|
|
350
|
+
"provider",
|
|
351
|
+
"apiKey",
|
|
352
|
+
"baseUrl",
|
|
353
|
+
"temperature",
|
|
354
|
+
"maxTokens",
|
|
355
|
+
"capabilities",
|
|
356
|
+
]);
|
|
357
|
+
const INDEX_PASS_KNOWN_KEYS = new Set([
|
|
358
|
+
"llm",
|
|
359
|
+
"graphExtractionBatchSize",
|
|
360
|
+
"graphExtractionIncludeTypes",
|
|
361
|
+
"memoryInferenceBatchSize",
|
|
362
|
+
]);
|
|
363
|
+
/**
|
|
364
|
+
* Per-pass `index.<pass>` entry. Uses preprocess + manual validation so we can
|
|
365
|
+
* emit the legacy parser's targeted error messages ("Duplicate LLM provider
|
|
366
|
+
* configuration", "Unknown key `index.<pass>.<key>`", "expected a boolean")
|
|
367
|
+
* instead of Zod's generic `Unrecognized key` / `Expected boolean, received
|
|
368
|
+
* string` strings — keeps `akm` startup errors actionable.
|
|
369
|
+
*/
|
|
370
|
+
export const IndexPassConfigSchema = z.preprocess((raw, ctx) => {
|
|
371
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
372
|
+
return raw; // let z.object below produce the type error
|
|
373
|
+
}
|
|
374
|
+
const obj = raw;
|
|
375
|
+
for (const key of Object.keys(obj)) {
|
|
376
|
+
if (INDEX_PASS_PROVIDER_KEYS.has(key)) {
|
|
377
|
+
ctx.addIssue({
|
|
378
|
+
code: z.ZodIssueCode.custom,
|
|
379
|
+
message: `Duplicate LLM provider configuration: \`${[...(ctx.path ?? []), key].join(".")}\` is not allowed. ` +
|
|
380
|
+
"Configure provider/model/endpoint under `profiles.llm` only; per-pass entries support `{ llm: false }` opt-out.",
|
|
381
|
+
});
|
|
382
|
+
return raw;
|
|
383
|
+
}
|
|
384
|
+
if (!INDEX_PASS_KNOWN_KEYS.has(key)) {
|
|
385
|
+
ctx.addIssue({
|
|
386
|
+
code: z.ZodIssueCode.custom,
|
|
387
|
+
message: `Unknown key \`${[...(ctx.path ?? []), key].join(".")}\`. Per-pass entries support \`llm\` ` +
|
|
388
|
+
"(boolean opt-out), `graphExtractionBatchSize`, `graphExtractionIncludeTypes`, and " +
|
|
389
|
+
"`memoryInferenceBatchSize`.",
|
|
390
|
+
});
|
|
391
|
+
return raw;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if ("llm" in obj && typeof obj.llm !== "boolean") {
|
|
395
|
+
ctx.addIssue({
|
|
396
|
+
code: z.ZodIssueCode.custom,
|
|
397
|
+
message: `Invalid \`${[...(ctx.path ?? []), "llm"].join(".")}\`: expected a boolean (true to use the default LLM profile, false to opt out). Got ${typeof obj.llm}.`,
|
|
398
|
+
});
|
|
399
|
+
return raw;
|
|
400
|
+
}
|
|
401
|
+
return raw;
|
|
402
|
+
}, z
|
|
403
|
+
.object({
|
|
404
|
+
llm: z.boolean().optional(),
|
|
405
|
+
graphExtractionBatchSize: positiveInt.optional(),
|
|
406
|
+
graphExtractionIncludeTypes: z.array(z.enum(GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED)).nonempty().optional(),
|
|
407
|
+
memoryInferenceBatchSize: positiveInt.optional(),
|
|
408
|
+
})
|
|
409
|
+
.passthrough());
|
|
410
|
+
const MetadataEnhanceSchema = z.object({ enabled: z.boolean().optional() }).strict();
|
|
411
|
+
const StalenessDetectionSchema = z
|
|
412
|
+
.object({
|
|
413
|
+
enabled: z.boolean().optional(),
|
|
414
|
+
thresholdDays: positiveInt.optional(),
|
|
415
|
+
})
|
|
416
|
+
.strict();
|
|
417
|
+
/**
|
|
418
|
+
* Index config is a union of reserved feature sections and per-pass entries.
|
|
419
|
+
* Passthrough so per-pass entries (keyed by arbitrary pass names like `graph`,
|
|
420
|
+
* `enrichment`) can live next to the reserved keys.
|
|
421
|
+
*
|
|
422
|
+
* The outer preprocess emits the legacy parser's actionable error messages
|
|
423
|
+
* for the two most common type-shape mistakes:
|
|
424
|
+
* - An array at the `index` block.
|
|
425
|
+
* - A non-object at `index.<passName>`.
|
|
426
|
+
* Inner field validation (graphExtractionIncludeTypes enum, llm boolean,
|
|
427
|
+
* provider-key rejection) is delegated to {@link IndexPassConfigSchema}.
|
|
428
|
+
*/
|
|
429
|
+
export const IndexConfigSchema = z.preprocess((raw, ctx) => {
|
|
430
|
+
if (raw === undefined || raw === null)
|
|
431
|
+
return raw;
|
|
432
|
+
if (Array.isArray(raw)) {
|
|
433
|
+
ctx.addIssue({
|
|
434
|
+
code: z.ZodIssueCode.custom,
|
|
435
|
+
message: 'Invalid `index` config: expected an object keyed by pass name (e.g. `{ "enrichment": { "llm": false } }`).',
|
|
436
|
+
});
|
|
437
|
+
return raw;
|
|
438
|
+
}
|
|
439
|
+
if (typeof raw !== "object")
|
|
440
|
+
return raw;
|
|
441
|
+
for (const [passName, value] of Object.entries(raw)) {
|
|
442
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
443
|
+
ctx.addIssue({
|
|
444
|
+
code: z.ZodIssueCode.custom,
|
|
445
|
+
message: `Invalid \`index.${passName}\` config: expected an object like \`{ "llm": false }\`.`,
|
|
446
|
+
});
|
|
447
|
+
return raw;
|
|
448
|
+
}
|
|
449
|
+
if (passName !== "metadataEnhance" &&
|
|
450
|
+
passName !== "stalenessDetection" &&
|
|
451
|
+
Array.isArray(value.graphExtractionIncludeTypes)) {
|
|
452
|
+
const arr = value.graphExtractionIncludeTypes;
|
|
453
|
+
const invalid = [];
|
|
454
|
+
for (const t of arr) {
|
|
455
|
+
if (typeof t === "string" &&
|
|
456
|
+
!GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED.includes(t.toLowerCase())) {
|
|
457
|
+
invalid.push(t);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (invalid.length > 0) {
|
|
461
|
+
ctx.addIssue({
|
|
462
|
+
code: z.ZodIssueCode.custom,
|
|
463
|
+
message: `Invalid \`index.${passName}.graphExtractionIncludeTypes\`: unsupported type(s): ${invalid.join(", ")}.`,
|
|
464
|
+
});
|
|
465
|
+
return raw;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return raw;
|
|
470
|
+
}, z
|
|
471
|
+
.object({
|
|
472
|
+
metadataEnhance: MetadataEnhanceSchema.optional(),
|
|
473
|
+
stalenessDetection: StalenessDetectionSchema.optional(),
|
|
474
|
+
})
|
|
475
|
+
.catchall(IndexPassConfigSchema));
|
|
476
|
+
// ── Top-level AkmConfig ────────────────────────────────────────────────────
|
|
477
|
+
/**
|
|
478
|
+
* Base object schema used both as the top-level shape and as the source of
|
|
479
|
+
* truth for {@link listTopLevelConfigKeys}. {@link AkmConfigSchema} wraps this
|
|
480
|
+
* with cross-field refinements (`.superRefine()`).
|
|
481
|
+
*
|
|
482
|
+
* All fields validate loudly — typos and shape errors throw at load time. The
|
|
483
|
+
* legacy parser's warn-and-drop tolerance was a frequent source of silent
|
|
484
|
+
* configuration loss; the migration module ({@link migrateConfigShape}) handles
|
|
485
|
+
* one-time 0.7→0.8 input transforms before the schema sees the value.
|
|
486
|
+
*/
|
|
487
|
+
export const AkmConfigShape = {
|
|
488
|
+
configVersion: z.union([z.string().min(1), z.number()]).optional(),
|
|
489
|
+
profiles: ProfilesSchema.optional(),
|
|
490
|
+
defaults: DefaultsSchema.optional(),
|
|
491
|
+
stashDir: nonEmptyString.optional(),
|
|
492
|
+
semanticSearchMode: z.enum(["off", "auto"]).default("auto"),
|
|
493
|
+
embedding: EmbeddingConnectionConfigSchema.optional(),
|
|
494
|
+
index: IndexConfigSchema.optional(),
|
|
495
|
+
installed: z.array(InstalledStashEntrySchema).optional(),
|
|
496
|
+
registries: z.array(RegistryConfigEntrySchema).optional(),
|
|
497
|
+
sources: z.array(SourceConfigEntrySchema).optional(),
|
|
498
|
+
output: OutputConfigSchema.optional(),
|
|
499
|
+
writable: z.boolean().optional(),
|
|
500
|
+
defaultWriteTarget: nonEmptyString.optional(),
|
|
501
|
+
search: SearchConfigSchema.optional(),
|
|
502
|
+
feedback: FeedbackConfigSchema.optional(),
|
|
503
|
+
archiveRetentionDays: nonNegativeNumber.optional(),
|
|
504
|
+
improve: ImproveConfigSchema.optional(),
|
|
505
|
+
};
|
|
506
|
+
export const AkmConfigBaseSchema = z.object(AkmConfigShape).strict();
|
|
507
|
+
export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) => {
|
|
508
|
+
// #464.a: defaultWriteTarget must name a configured source when sources
|
|
509
|
+
// are present. With no sources configured, error out instead of silently
|
|
510
|
+
// accepting (no implicit "first writable" fallback — see locked decision 3).
|
|
511
|
+
if (config.defaultWriteTarget !== undefined) {
|
|
512
|
+
const knownNames = (config.sources ?? [])
|
|
513
|
+
.map((s) => s.name)
|
|
514
|
+
.filter((n) => typeof n === "string" && n.length > 0);
|
|
515
|
+
if (knownNames.length === 0) {
|
|
516
|
+
ctx.addIssue({
|
|
517
|
+
code: z.ZodIssueCode.custom,
|
|
518
|
+
path: ["defaultWriteTarget"],
|
|
519
|
+
message: `defaultWriteTarget "${config.defaultWriteTarget}" cannot be resolved: no sources configured. ` +
|
|
520
|
+
"Add at least one entry to `sources` with a matching `name` first.",
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
else if (!knownNames.includes(config.defaultWriteTarget)) {
|
|
524
|
+
ctx.addIssue({
|
|
525
|
+
code: z.ZodIssueCode.custom,
|
|
526
|
+
path: ["defaultWriteTarget"],
|
|
527
|
+
message: `defaultWriteTarget "${config.defaultWriteTarget}" does not match any configured source name: ${knownNames.map((n) => `"${n}"`).join(", ")}.`,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
/**
|
|
533
|
+
* Validate a raw object against {@link AkmConfigSchema}. Returns a structured
|
|
534
|
+
* result so callers can render errors as a list (instead of throwing on the
|
|
535
|
+
* first issue).
|
|
536
|
+
*/
|
|
537
|
+
export function validateConfigShape(raw) {
|
|
538
|
+
const result = AkmConfigSchema.safeParse(raw);
|
|
539
|
+
if (result.success) {
|
|
540
|
+
return { ok: true, value: result.data, errors: [] };
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
ok: false,
|
|
544
|
+
errors: result.error.issues.map((issue) => ({
|
|
545
|
+
path: issue.path.join("."),
|
|
546
|
+
message: issue.message,
|
|
547
|
+
})),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
// ── Top-level key listing (for hint messages) ───────────────────────────────
|
|
551
|
+
/**
|
|
552
|
+
* Return the sorted list of top-level config keys recognized by the schema.
|
|
553
|
+
* Used by error hints so the list stays in sync with the schema automatically
|
|
554
|
+
* (#460).
|
|
555
|
+
*/
|
|
556
|
+
export function listTopLevelConfigKeys() {
|
|
557
|
+
return Object.keys(AkmConfigShape).sort();
|
|
558
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
* Runtime helpers that derive {@link ConfiguredSource} values from the
|
|
6
|
+
* persisted {@link SourceConfigEntry} / {@link InstalledStashEntry} shapes
|
|
7
|
+
* in an {@link AkmConfig}.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
/**
|
|
11
|
+
* Synthesize a stable identifier when a {@link SourceConfigEntry} omits its
|
|
12
|
+
* `name`. Uses a short hash of the discriminating fields so two equivalent
|
|
13
|
+
* entries collapse to the same generated name.
|
|
14
|
+
*/
|
|
15
|
+
function deriveStashEntryName(entry) {
|
|
16
|
+
if (entry.name)
|
|
17
|
+
return entry.name;
|
|
18
|
+
const seed = JSON.stringify({
|
|
19
|
+
type: entry.type,
|
|
20
|
+
path: entry.path ?? null,
|
|
21
|
+
url: entry.url ?? null,
|
|
22
|
+
});
|
|
23
|
+
const hash = createHash("sha256").update(seed).digest("hex").slice(0, 8);
|
|
24
|
+
return `${entry.type}-${hash}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Convert a persisted {@link SourceConfigEntry} into the runtime
|
|
28
|
+
* {@link SourceSpec} discriminated union. Returns `undefined` when the entry
|
|
29
|
+
* is missing the fields its provider type requires (e.g. a `filesystem`
|
|
30
|
+
* entry with no `path`); callers should drop or warn for those.
|
|
31
|
+
*/
|
|
32
|
+
export function parseSourceSpec(entry) {
|
|
33
|
+
switch (entry.type) {
|
|
34
|
+
case "filesystem":
|
|
35
|
+
return entry.path ? { type: "filesystem", path: entry.path } : undefined;
|
|
36
|
+
case "git":
|
|
37
|
+
return entry.url ? { type: "git", url: entry.url } : undefined;
|
|
38
|
+
case "website":
|
|
39
|
+
return entry.url
|
|
40
|
+
? {
|
|
41
|
+
type: "website",
|
|
42
|
+
url: entry.url,
|
|
43
|
+
...(typeof entry.options?.maxPages === "number" ? { maxPages: entry.options.maxPages } : {}),
|
|
44
|
+
}
|
|
45
|
+
: undefined;
|
|
46
|
+
case "npm":
|
|
47
|
+
return entry.path ? { type: "npm", package: entry.path } : undefined;
|
|
48
|
+
default:
|
|
49
|
+
// Unknown provider — best-effort fallback so callers still get something.
|
|
50
|
+
return entry.path ? { type: "filesystem", path: entry.path } : undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the full ordered list of runtime {@link ConfiguredSource} values from
|
|
55
|
+
* a loaded {@link AkmConfig}:
|
|
56
|
+
* 1. The entry marked `primary: true` (or a synthetic entry from `stashDir`).
|
|
57
|
+
* 2. Remaining `sources[]` entries in declared order.
|
|
58
|
+
* 3. Legacy `installed[]` entries, mapped into runtime entries.
|
|
59
|
+
*
|
|
60
|
+
* Entries with `enabled: false` are still emitted — callers decide whether to
|
|
61
|
+
* honour the flag. Entries that fail {@link parseSourceSpec} drop silently.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveConfiguredSources(config) {
|
|
64
|
+
const entries = [];
|
|
65
|
+
const sources = config.sources ?? [];
|
|
66
|
+
let primary = sources.find((entry) => entry.primary === true);
|
|
67
|
+
if (!primary && config.stashDir) {
|
|
68
|
+
primary = { type: "filesystem", path: config.stashDir, primary: true };
|
|
69
|
+
}
|
|
70
|
+
if (primary) {
|
|
71
|
+
const runtime = toConfiguredSource(primary, true);
|
|
72
|
+
if (runtime)
|
|
73
|
+
entries.push(runtime);
|
|
74
|
+
}
|
|
75
|
+
for (const entry of sources) {
|
|
76
|
+
if (entry === primary)
|
|
77
|
+
continue;
|
|
78
|
+
const runtime = toConfiguredSource(entry, false);
|
|
79
|
+
if (runtime)
|
|
80
|
+
entries.push(runtime);
|
|
81
|
+
}
|
|
82
|
+
for (const installed of config.installed ?? []) {
|
|
83
|
+
entries.push({
|
|
84
|
+
name: installed.id,
|
|
85
|
+
type: "filesystem",
|
|
86
|
+
source: { type: "filesystem", path: installed.stashRoot },
|
|
87
|
+
enabled: true,
|
|
88
|
+
writable: installed.writable,
|
|
89
|
+
...(installed.wikiName ? { wikiName: installed.wikiName } : {}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return entries;
|
|
93
|
+
}
|
|
94
|
+
function toConfiguredSource(persisted, isPrimary) {
|
|
95
|
+
const source = parseSourceSpec(persisted);
|
|
96
|
+
if (!source)
|
|
97
|
+
return undefined;
|
|
98
|
+
return {
|
|
99
|
+
name: deriveStashEntryName(persisted),
|
|
100
|
+
type: persisted.type,
|
|
101
|
+
source,
|
|
102
|
+
...(persisted.enabled !== undefined ? { enabled: persisted.enabled } : {}),
|
|
103
|
+
...(persisted.writable !== undefined ? { writable: persisted.writable } : {}),
|
|
104
|
+
...(isPrimary || persisted.primary ? { primary: true } : {}),
|
|
105
|
+
...(persisted.options ? { options: persisted.options } : {}),
|
|
106
|
+
...(persisted.wikiName ? { wikiName: persisted.wikiName } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|