akm-cli 0.7.5 → 0.8.0-rc.6

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.
Files changed (236) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
  2. package/README.md +20 -4
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +133 -0
  8. package/dist/cli.js +1995 -551
  9. package/dist/commands/agent-dispatch.js +110 -0
  10. package/dist/commands/agent-support.js +68 -0
  11. package/dist/commands/completions.js +3 -0
  12. package/dist/commands/config-cli.js +130 -534
  13. package/dist/commands/consolidate.js +1531 -0
  14. package/dist/commands/curate.js +44 -3
  15. package/dist/commands/db-cli.js +23 -0
  16. package/dist/commands/distill-promotion-policy.js +660 -0
  17. package/dist/commands/distill.js +990 -75
  18. package/dist/commands/eval-cases.js +43 -0
  19. package/dist/commands/events.js +5 -23
  20. package/dist/commands/graph.js +477 -0
  21. package/dist/commands/health.js +400 -0
  22. package/dist/commands/help/help-accept.md +9 -0
  23. package/dist/commands/help/help-improve.md +77 -0
  24. package/dist/commands/help/help-proposals.md +15 -0
  25. package/dist/commands/help/help-propose.md +17 -0
  26. package/dist/commands/help/help-reject.md +8 -0
  27. package/dist/commands/history.js +54 -46
  28. package/dist/commands/improve-profiles.js +146 -0
  29. package/dist/commands/improve-result-file.js +103 -0
  30. package/dist/commands/improve.js +2175 -0
  31. package/dist/commands/info.js +5 -2
  32. package/dist/commands/init.js +50 -2
  33. package/dist/commands/installed-stashes.js +102 -139
  34. package/dist/commands/knowledge.js +136 -0
  35. package/dist/commands/lint/agent-linter.js +49 -0
  36. package/dist/commands/lint/base-linter.js +479 -0
  37. package/dist/commands/lint/command-linter.js +49 -0
  38. package/dist/commands/lint/default-linter.js +16 -0
  39. package/dist/commands/lint/index.js +183 -0
  40. package/dist/commands/lint/knowledge-linter.js +16 -0
  41. package/dist/commands/lint/markdown-insertion.js +343 -0
  42. package/dist/commands/lint/memory-linter.js +61 -0
  43. package/dist/commands/lint/registry.js +36 -0
  44. package/dist/commands/lint/skill-linter.js +45 -0
  45. package/dist/commands/lint/task-linter.js +50 -0
  46. package/dist/commands/lint/types.js +4 -0
  47. package/dist/commands/lint/vault-key-rules.js +139 -0
  48. package/dist/commands/lint/workflow-linter.js +56 -0
  49. package/dist/commands/lint.js +4 -0
  50. package/dist/commands/migration-help.js +5 -2
  51. package/dist/commands/proposal.js +66 -12
  52. package/dist/commands/propose.js +86 -31
  53. package/dist/commands/reflect.js +1119 -73
  54. package/dist/commands/registry-search.js +5 -2
  55. package/dist/commands/remember.js +69 -6
  56. package/dist/commands/schema-repair.js +203 -0
  57. package/dist/commands/search.js +115 -14
  58. package/dist/commands/self-update.js +3 -0
  59. package/dist/commands/show.js +144 -25
  60. package/dist/commands/source-add.js +17 -45
  61. package/dist/commands/source-clone.js +3 -0
  62. package/dist/commands/source-manage.js +14 -19
  63. package/dist/commands/tasks.js +438 -0
  64. package/dist/commands/url-checker.js +42 -0
  65. package/dist/commands/vault.js +130 -77
  66. package/dist/core/action-contributors.js +28 -0
  67. package/dist/core/asset-ref.js +7 -0
  68. package/dist/core/asset-registry.js +7 -16
  69. package/dist/core/asset-serialize.js +88 -0
  70. package/dist/core/asset-spec.js +22 -0
  71. package/dist/core/common.js +157 -0
  72. package/dist/core/concurrent.js +25 -0
  73. package/dist/core/config-io.js +347 -0
  74. package/dist/core/config-migration.js +625 -0
  75. package/dist/core/config-schema.js +501 -0
  76. package/dist/core/config-sources.js +108 -0
  77. package/dist/core/config-types.js +4 -0
  78. package/dist/core/config-walker.js +337 -0
  79. package/dist/core/config.js +327 -987
  80. package/dist/core/errors.js +40 -19
  81. package/dist/core/events.js +91 -138
  82. package/dist/core/file-lock.js +104 -0
  83. package/dist/core/frontmatter.js +3 -6
  84. package/dist/core/lesson-lint.js +3 -0
  85. package/dist/core/markdown.js +20 -0
  86. package/dist/core/memory-belief.js +62 -0
  87. package/dist/core/memory-contradiction-detect.js +274 -0
  88. package/dist/core/memory-improve.js +806 -0
  89. package/dist/core/parse.js +158 -0
  90. package/dist/core/paths.js +326 -14
  91. package/dist/core/proposal-quality-validators.js +364 -0
  92. package/dist/core/proposal-validators.js +69 -0
  93. package/dist/core/proposals.js +498 -42
  94. package/dist/core/state-db.js +927 -0
  95. package/dist/core/text-truncation.js +107 -0
  96. package/dist/core/time.js +54 -0
  97. package/dist/core/warn.js +62 -1
  98. package/dist/core/write-source.js +3 -0
  99. package/dist/indexer/db-backup.js +391 -0
  100. package/dist/indexer/db-search.js +152 -253
  101. package/dist/indexer/db.js +933 -103
  102. package/dist/indexer/ensure-index.js +64 -0
  103. package/dist/indexer/file-context.js +3 -0
  104. package/dist/indexer/graph-boost.js +376 -101
  105. package/dist/indexer/graph-db.js +391 -0
  106. package/dist/indexer/graph-dedup.js +95 -0
  107. package/dist/indexer/graph-extraction.js +550 -124
  108. package/dist/indexer/index-context.js +4 -0
  109. package/dist/indexer/indexer.js +506 -291
  110. package/dist/indexer/llm-cache.js +47 -0
  111. package/dist/indexer/manifest.js +3 -0
  112. package/dist/indexer/matchers.js +148 -160
  113. package/dist/indexer/memory-inference.js +99 -74
  114. package/dist/indexer/metadata-contributors.js +29 -0
  115. package/dist/indexer/metadata.js +255 -196
  116. package/dist/indexer/path-resolver.js +92 -0
  117. package/dist/indexer/project-context.js +192 -0
  118. package/dist/indexer/ranking-contributors.js +331 -0
  119. package/dist/indexer/ranking.js +81 -0
  120. package/dist/indexer/search-fields.js +5 -9
  121. package/dist/indexer/search-hit-enrichers.js +111 -0
  122. package/dist/indexer/search-source.js +44 -10
  123. package/dist/indexer/semantic-status.js +5 -16
  124. package/dist/indexer/staleness-detect.js +447 -0
  125. package/dist/indexer/usage-events.js +12 -9
  126. package/dist/indexer/walker.js +28 -0
  127. package/dist/integrations/agent/builders.js +135 -0
  128. package/dist/integrations/agent/config.js +122 -230
  129. package/dist/integrations/agent/detect.js +3 -0
  130. package/dist/integrations/agent/index.js +7 -13
  131. package/dist/integrations/agent/model-aliases.js +55 -0
  132. package/dist/integrations/agent/profiles.js +70 -5
  133. package/dist/integrations/agent/prompts.js +150 -74
  134. package/dist/integrations/agent/runner.js +151 -0
  135. package/dist/integrations/agent/sdk-runner.js +126 -0
  136. package/dist/integrations/agent/spawn.js +118 -23
  137. package/dist/integrations/github.js +3 -0
  138. package/dist/integrations/lockfile.js +32 -69
  139. package/dist/integrations/session-logs/index.js +68 -0
  140. package/dist/integrations/session-logs/providers/claude-code.js +59 -0
  141. package/dist/integrations/session-logs/providers/opencode.js +55 -0
  142. package/dist/integrations/session-logs/types.js +4 -0
  143. package/dist/llm/call-ai.js +62 -0
  144. package/dist/llm/client.js +72 -124
  145. package/dist/llm/embedder.js +3 -19
  146. package/dist/llm/embedders/cache.js +3 -7
  147. package/dist/llm/embedders/local.js +3 -0
  148. package/dist/llm/embedders/remote.js +20 -8
  149. package/dist/llm/embedders/types.js +3 -7
  150. package/dist/llm/feature-gate.js +89 -48
  151. package/dist/llm/graph-extract.js +676 -70
  152. package/dist/llm/index-passes.js +9 -23
  153. package/dist/llm/memory-infer.js +52 -71
  154. package/dist/llm/metadata-enhance.js +42 -29
  155. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  156. package/dist/output/cli-hints-full.md +281 -0
  157. package/dist/output/cli-hints-short.md +65 -0
  158. package/dist/output/cli-hints.js +5 -318
  159. package/dist/output/context.js +3 -0
  160. package/dist/output/renderers.js +223 -256
  161. package/dist/output/shapes.js +150 -105
  162. package/dist/output/text.js +318 -30
  163. package/dist/registry/build-index.js +3 -0
  164. package/dist/registry/create-provider-registry.js +3 -0
  165. package/dist/registry/factory.js +3 -0
  166. package/dist/registry/origin-resolve.js +3 -0
  167. package/dist/registry/providers/index.js +3 -0
  168. package/dist/registry/providers/skills-sh.js +70 -49
  169. package/dist/registry/providers/static-index.js +53 -48
  170. package/dist/registry/providers/types.js +3 -24
  171. package/dist/registry/resolve.js +11 -16
  172. package/dist/registry/types.js +3 -0
  173. package/dist/scripts/migrate-storage.js +17307 -0
  174. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
  175. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  176. package/dist/setup/detect.js +3 -0
  177. package/dist/setup/ripgrep-install.js +3 -0
  178. package/dist/setup/ripgrep-resolve.js +3 -0
  179. package/dist/setup/setup.js +775 -37
  180. package/dist/setup/steps.js +3 -15
  181. package/dist/sources/include.js +3 -0
  182. package/dist/sources/provider-factory.js +5 -12
  183. package/dist/sources/provider.js +3 -20
  184. package/dist/sources/providers/filesystem.js +19 -23
  185. package/dist/sources/providers/git.js +7 -5
  186. package/dist/sources/providers/index.js +3 -0
  187. package/dist/sources/providers/install-types.js +3 -13
  188. package/dist/sources/providers/npm.js +3 -4
  189. package/dist/sources/providers/provider-utils.js +3 -0
  190. package/dist/sources/providers/sync-from-ref.js +3 -11
  191. package/dist/sources/providers/tar-utils.js +3 -0
  192. package/dist/sources/providers/website.js +18 -22
  193. package/dist/sources/resolve.js +3 -0
  194. package/dist/sources/types.js +3 -0
  195. package/dist/sources/website-ingest.js +7 -0
  196. package/dist/tasks/backends/cron.js +203 -0
  197. package/dist/tasks/backends/exec-utils.js +28 -0
  198. package/dist/tasks/backends/index.js +24 -0
  199. package/dist/tasks/backends/launchd-template.xml +19 -0
  200. package/dist/tasks/backends/launchd.js +187 -0
  201. package/dist/tasks/backends/schtasks-template.xml +29 -0
  202. package/dist/tasks/backends/schtasks.js +215 -0
  203. package/dist/tasks/parser.js +211 -0
  204. package/dist/tasks/resolveAkmBin.js +87 -0
  205. package/dist/tasks/runner.js +458 -0
  206. package/dist/tasks/schedule.js +211 -0
  207. package/dist/tasks/schema.js +15 -0
  208. package/dist/tasks/validator.js +62 -0
  209. package/dist/version.js +3 -0
  210. package/dist/wiki/index-template.md +12 -0
  211. package/dist/wiki/ingest-workflow-template.md +54 -0
  212. package/dist/wiki/log-template.md +8 -0
  213. package/dist/wiki/schema-template.md +61 -0
  214. package/dist/wiki/wiki-templates.js +15 -0
  215. package/dist/wiki/wiki.js +13 -61
  216. package/dist/workflows/authoring.js +8 -25
  217. package/dist/workflows/cli.js +3 -0
  218. package/dist/workflows/db.js +140 -10
  219. package/dist/workflows/document-cache.js +3 -10
  220. package/dist/workflows/parser.js +3 -0
  221. package/dist/workflows/renderer.js +11 -3
  222. package/dist/workflows/runs.js +62 -91
  223. package/dist/workflows/schema.js +3 -0
  224. package/dist/workflows/scope-key.js +3 -0
  225. package/dist/workflows/validator.js +4 -8
  226. package/dist/workflows/workflow-template.md +24 -0
  227. package/docs/README.md +9 -2
  228. package/docs/data-and-telemetry.md +225 -0
  229. package/docs/migration/release-notes/0.7.0.md +1 -1
  230. package/docs/migration/release-notes/0.7.5.md +2 -2
  231. package/docs/migration/release-notes/0.8.0.md +48 -0
  232. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  233. package/package.json +20 -8
  234. package/.github/LICENSE +0 -374
  235. package/dist/commands/install-audit.js +0 -381
  236. package/dist/templates/wiki-templates.js +0 -100
@@ -0,0 +1,501 @@
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
+ })
76
+ .strict();
77
+ export const LlmProfileConfigSchema = LlmConnectionConfigSchema.extend({
78
+ supportsJsonSchema: z.boolean().optional(),
79
+ }).strict();
80
+ const EmbeddingOllamaOptionsSchema = z
81
+ .object({
82
+ num_ctx: positiveInt.optional(),
83
+ })
84
+ .strict();
85
+ /**
86
+ * Embedding connection config. Both `endpoint` and `model` are optional:
87
+ * - Remote: provide `endpoint` (http/https URL) + `model`.
88
+ * - Local-only: omit `endpoint`/`model`; set `localModel` (or fall back to
89
+ * {@link DEFAULT_LOCAL_MODEL}).
90
+ *
91
+ * Consumers route via `hasRemoteEndpoint()` which checks for an http(s)
92
+ * endpoint — absent fields take the local path naturally, no sentinels needed.
93
+ */
94
+ export const EmbeddingConnectionConfigSchema = z
95
+ .object({
96
+ provider: z.string().optional(),
97
+ endpoint: z.string().optional(),
98
+ model: z.string().optional(),
99
+ apiKey: z.string().optional(),
100
+ dimension: positiveInt.optional(),
101
+ localModel: z.string().min(1).optional(),
102
+ maxTokens: positiveInt.optional(),
103
+ batchSize: positiveInt.optional(),
104
+ chunkSize: positiveInt.optional(),
105
+ contextLength: positiveInt.optional(),
106
+ ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
107
+ })
108
+ .strict();
109
+ // ── Agent profiles ──────────────────────────────────────────────────────────
110
+ const AgentPlatformSchema = z.enum(["opencode", "claude", "opencode-sdk"]);
111
+ export const AgentProfileConfigSchema = z
112
+ .object({
113
+ platform: AgentPlatformSchema,
114
+ bin: z.string().min(1).optional(),
115
+ args: z.array(z.string()).optional(),
116
+ workspace: z.string().min(1).optional(),
117
+ model: z.string().min(1).optional(),
118
+ })
119
+ .strict();
120
+ // ── Improve profile / process ──────────────────────────────────────────────
121
+ export const ImproveProcessConfigSchema = z
122
+ .object({
123
+ enabled: z.boolean().optional(),
124
+ mode: z.enum(["llm", "agent", "sdk"]).optional(),
125
+ profile: z.string().min(1).optional(),
126
+ timeoutMs: z.union([positiveInt, z.null()]).optional(),
127
+ allowedTypes: z.array(z.string().min(1)).optional(),
128
+ cooldownByType: z.record(z.string(), nonNegativeNumber).optional(),
129
+ cooldownDays: nonNegativeNumber.optional(),
130
+ qualityGate: z.object({ enabled: z.boolean().optional() }).strict().optional(),
131
+ contradictionDetection: z.object({ enabled: z.boolean().optional() }).strict().optional(),
132
+ })
133
+ .strict();
134
+ const ImproveProfileProcessesSchema = z
135
+ .object({
136
+ reflect: ImproveProcessConfigSchema.optional(),
137
+ distill: ImproveProcessConfigSchema.optional(),
138
+ consolidate: ImproveProcessConfigSchema.optional(),
139
+ memoryInference: ImproveProcessConfigSchema.optional(),
140
+ graphExtraction: ImproveProcessConfigSchema.optional(),
141
+ feedbackDistillation: ImproveProcessConfigSchema.optional(),
142
+ validation: ImproveProcessConfigSchema.optional(),
143
+ })
144
+ .strict();
145
+ export const ImproveProfileConfigSchema = z
146
+ .object({
147
+ description: z.string().min(1).optional(),
148
+ processes: ImproveProfileProcessesSchema.optional(),
149
+ autoAccept: nonNegativeNumber.optional(),
150
+ limit: positiveInt.optional(),
151
+ })
152
+ .strict();
153
+ // ── Profiles / defaults ────────────────────────────────────────────────────
154
+ export const ProfilesSchema = z
155
+ .object({
156
+ llm: z.record(z.string(), LlmProfileConfigSchema).optional(),
157
+ agent: z.record(z.string(), AgentProfileConfigSchema).optional(),
158
+ improve: z.record(z.string(), ImproveProfileConfigSchema).optional(),
159
+ })
160
+ .strict();
161
+ export const DefaultsSchema = z
162
+ .object({
163
+ llm: z.string().min(1).optional(),
164
+ agent: z.string().min(1).optional(),
165
+ improve: z.string().min(1).optional(),
166
+ })
167
+ .strict();
168
+ // ── Sources / registries / installed ────────────────────────────────────────
169
+ const SourceConfigEntryOptionsSchema = z
170
+ .object({
171
+ pushOnCommit: z.boolean().optional(),
172
+ })
173
+ .passthrough();
174
+ export const SourceConfigEntrySchema = z
175
+ .object({
176
+ type: nonEmptyString,
177
+ path: z.string().min(1).optional(),
178
+ url: z.string().min(1).optional(),
179
+ name: z.string().min(1).optional(),
180
+ enabled: z.boolean().optional(),
181
+ writable: z.boolean().optional(),
182
+ primary: z.boolean().optional(),
183
+ options: SourceConfigEntryOptionsSchema.optional(),
184
+ wikiName: z.string().min(1).optional(),
185
+ })
186
+ .strict()
187
+ .superRefine((entry, ctx) => {
188
+ if (entry.writable === true && (entry.type === "website" || entry.type === "npm")) {
189
+ ctx.addIssue({
190
+ code: z.ZodIssueCode.custom,
191
+ message: `writable: true is only supported on filesystem and git sources (got "${entry.type}"` +
192
+ (entry.name ? ` on source "${entry.name}"` : "") +
193
+ ").",
194
+ });
195
+ }
196
+ });
197
+ export const RegistryConfigEntrySchema = z
198
+ .object({
199
+ url: httpUrl,
200
+ name: z.string().min(1).optional(),
201
+ enabled: z.boolean().optional(),
202
+ provider: z.string().min(1).optional(),
203
+ options: z.record(z.unknown()).optional(),
204
+ })
205
+ .strict();
206
+ const KitSourceSchema = z.enum(["filesystem", "git", "npm", "github", "website", "local"]);
207
+ export const InstalledStashEntrySchema = z
208
+ .object({
209
+ id: nonEmptyString,
210
+ source: KitSourceSchema,
211
+ ref: nonEmptyString,
212
+ artifactUrl: nonEmptyString,
213
+ stashRoot: nonEmptyString,
214
+ cacheDir: nonEmptyString,
215
+ installedAt: nonEmptyString,
216
+ writable: z.boolean().optional(),
217
+ resolvedVersion: z.string().min(1).optional(),
218
+ resolvedRevision: z.string().min(1).optional(),
219
+ wikiName: z.string().min(1).optional(),
220
+ })
221
+ .strict()
222
+ .superRefine((entry, ctx) => {
223
+ if (entry.writable === true && entry.source !== "git" && entry.source !== "filesystem") {
224
+ ctx.addIssue({
225
+ code: z.ZodIssueCode.custom,
226
+ message: `writable: true is only supported on filesystem and git sources (got "${entry.source}" on installed entry "${entry.id}").`,
227
+ });
228
+ }
229
+ });
230
+ // ── Output ──────────────────────────────────────────────────────────────────
231
+ export const OutputConfigSchema = z
232
+ .object({
233
+ format: z.enum(["json", "yaml", "text"]).optional(),
234
+ detail: z.enum(["brief", "normal", "full"]).optional(),
235
+ })
236
+ .strict();
237
+ // ── Search ──────────────────────────────────────────────────────────────────
238
+ const SearchGraphBoostSchema = z
239
+ .object({
240
+ directBoostPerEntity: nonNegativeNumber.optional(),
241
+ directBoostCap: nonNegativeNumber.optional(),
242
+ hopBoostPerEntity: nonNegativeNumber.optional(),
243
+ hopBoostCap: nonNegativeNumber.optional(),
244
+ /** Hard-capped at 3; values > 3 hard-error so users see the typo. */
245
+ maxHops: positiveInt.max(3).optional(),
246
+ confidenceMode: z.enum(["off", "blend", "multiply"]).default("blend").optional(),
247
+ /** Range [0, 1]; values > 1 hard-error (no silent clamp). */
248
+ confidenceWeight: z.number().finite().min(0).max(1).default(0.2).optional(),
249
+ })
250
+ .strict();
251
+ export const SearchConfigSchema = z
252
+ .object({
253
+ minScore: nonNegativeNumber.optional(),
254
+ curateRerank: z.object({ enabled: z.boolean().optional() }).strict().optional(),
255
+ graphBoost: SearchGraphBoostSchema.optional(),
256
+ })
257
+ .strict();
258
+ // ── Feedback ────────────────────────────────────────────────────────────────
259
+ export const FeedbackConfigSchema = z
260
+ .object({
261
+ requireReason: z.boolean().optional(),
262
+ allowedFailureModes: z.array(nonEmptyString).optional(),
263
+ })
264
+ .strict();
265
+ // ── Improve top-level (utility decay, event retention) ─────────────────────
266
+ const ImproveUtilityDecaySchema = z
267
+ .object({
268
+ halfLifeDays: z.number().finite().min(0.1).optional(),
269
+ feedbackStabilityBoost: z.number().finite().min(1).optional(),
270
+ })
271
+ .strict();
272
+ export const ImproveConfigSchema = z
273
+ .object({
274
+ utilityDecay: ImproveUtilityDecaySchema.optional(),
275
+ eventRetentionDays: nonNegativeNumber.optional(),
276
+ })
277
+ .strict();
278
+ // ── Index / per-pass ────────────────────────────────────────────────────────
279
+ const GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED = [
280
+ "memory",
281
+ "knowledge",
282
+ "skill",
283
+ "command",
284
+ "agent",
285
+ "workflow",
286
+ "lesson",
287
+ "task",
288
+ "wiki",
289
+ ];
290
+ const INDEX_PASS_PROVIDER_KEYS = new Set([
291
+ "endpoint",
292
+ "model",
293
+ "provider",
294
+ "apiKey",
295
+ "baseUrl",
296
+ "temperature",
297
+ "maxTokens",
298
+ "capabilities",
299
+ ]);
300
+ const INDEX_PASS_KNOWN_KEYS = new Set([
301
+ "llm",
302
+ "graphExtractionBatchSize",
303
+ "graphExtractionIncludeTypes",
304
+ "memoryInferenceBatchSize",
305
+ ]);
306
+ /**
307
+ * Per-pass `index.<pass>` entry. Uses preprocess + manual validation so we can
308
+ * emit the legacy parser's targeted error messages ("Duplicate LLM provider
309
+ * configuration", "Unknown key `index.<pass>.<key>`", "expected a boolean")
310
+ * instead of Zod's generic `Unrecognized key` / `Expected boolean, received
311
+ * string` strings — keeps `akm` startup errors actionable.
312
+ */
313
+ export const IndexPassConfigSchema = z.preprocess((raw, ctx) => {
314
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
315
+ return raw; // let z.object below produce the type error
316
+ }
317
+ const obj = raw;
318
+ for (const key of Object.keys(obj)) {
319
+ if (INDEX_PASS_PROVIDER_KEYS.has(key)) {
320
+ ctx.addIssue({
321
+ code: z.ZodIssueCode.custom,
322
+ message: `Duplicate LLM provider configuration: \`${[...(ctx.path ?? []), key].join(".")}\` is not allowed. ` +
323
+ "Configure provider/model/endpoint under `profiles.llm` only; per-pass entries support `{ llm: false }` opt-out.",
324
+ });
325
+ return raw;
326
+ }
327
+ if (!INDEX_PASS_KNOWN_KEYS.has(key)) {
328
+ ctx.addIssue({
329
+ code: z.ZodIssueCode.custom,
330
+ message: `Unknown key \`${[...(ctx.path ?? []), key].join(".")}\`. Per-pass entries support \`llm\` ` +
331
+ "(boolean opt-out), `graphExtractionBatchSize`, `graphExtractionIncludeTypes`, and " +
332
+ "`memoryInferenceBatchSize`.",
333
+ });
334
+ return raw;
335
+ }
336
+ }
337
+ if ("llm" in obj && typeof obj.llm !== "boolean") {
338
+ ctx.addIssue({
339
+ code: z.ZodIssueCode.custom,
340
+ message: `Invalid \`${[...(ctx.path ?? []), "llm"].join(".")}\`: expected a boolean (true to use the default LLM profile, false to opt out). Got ${typeof obj.llm}.`,
341
+ });
342
+ return raw;
343
+ }
344
+ return raw;
345
+ }, z
346
+ .object({
347
+ llm: z.boolean().optional(),
348
+ graphExtractionBatchSize: positiveInt.optional(),
349
+ graphExtractionIncludeTypes: z.array(z.enum(GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED)).nonempty().optional(),
350
+ memoryInferenceBatchSize: positiveInt.optional(),
351
+ })
352
+ .passthrough());
353
+ const MetadataEnhanceSchema = z.object({ enabled: z.boolean().optional() }).strict();
354
+ const StalenessDetectionSchema = z
355
+ .object({
356
+ enabled: z.boolean().optional(),
357
+ thresholdDays: positiveInt.optional(),
358
+ })
359
+ .strict();
360
+ /**
361
+ * Index config is a union of reserved feature sections and per-pass entries.
362
+ * Passthrough so per-pass entries (keyed by arbitrary pass names like `graph`,
363
+ * `enrichment`) can live next to the reserved keys.
364
+ *
365
+ * The outer preprocess emits the legacy parser's actionable error messages
366
+ * for the two most common type-shape mistakes:
367
+ * - An array at the `index` block.
368
+ * - A non-object at `index.<passName>`.
369
+ * Inner field validation (graphExtractionIncludeTypes enum, llm boolean,
370
+ * provider-key rejection) is delegated to {@link IndexPassConfigSchema}.
371
+ */
372
+ export const IndexConfigSchema = z.preprocess((raw, ctx) => {
373
+ if (raw === undefined || raw === null)
374
+ return raw;
375
+ if (Array.isArray(raw)) {
376
+ ctx.addIssue({
377
+ code: z.ZodIssueCode.custom,
378
+ message: 'Invalid `index` config: expected an object keyed by pass name (e.g. `{ "enrichment": { "llm": false } }`).',
379
+ });
380
+ return raw;
381
+ }
382
+ if (typeof raw !== "object")
383
+ return raw;
384
+ for (const [passName, value] of Object.entries(raw)) {
385
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
386
+ ctx.addIssue({
387
+ code: z.ZodIssueCode.custom,
388
+ message: `Invalid \`index.${passName}\` config: expected an object like \`{ "llm": false }\`.`,
389
+ });
390
+ return raw;
391
+ }
392
+ if (passName !== "metadataEnhance" &&
393
+ passName !== "stalenessDetection" &&
394
+ Array.isArray(value.graphExtractionIncludeTypes)) {
395
+ const arr = value.graphExtractionIncludeTypes;
396
+ const invalid = [];
397
+ for (const t of arr) {
398
+ if (typeof t === "string" &&
399
+ !GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED.includes(t.toLowerCase())) {
400
+ invalid.push(t);
401
+ }
402
+ }
403
+ if (invalid.length > 0) {
404
+ ctx.addIssue({
405
+ code: z.ZodIssueCode.custom,
406
+ message: `Invalid \`index.${passName}.graphExtractionIncludeTypes\`: unsupported type(s): ${invalid.join(", ")}.`,
407
+ });
408
+ return raw;
409
+ }
410
+ }
411
+ }
412
+ return raw;
413
+ }, z
414
+ .object({
415
+ metadataEnhance: MetadataEnhanceSchema.optional(),
416
+ stalenessDetection: StalenessDetectionSchema.optional(),
417
+ })
418
+ .catchall(IndexPassConfigSchema));
419
+ // ── Top-level AkmConfig ────────────────────────────────────────────────────
420
+ /**
421
+ * Base object schema used both as the top-level shape and as the source of
422
+ * truth for {@link listTopLevelConfigKeys}. {@link AkmConfigSchema} wraps this
423
+ * with cross-field refinements (`.superRefine()`).
424
+ *
425
+ * All fields validate loudly — typos and shape errors throw at load time. The
426
+ * legacy parser's warn-and-drop tolerance was a frequent source of silent
427
+ * configuration loss; the migration module ({@link migrateConfigShape}) handles
428
+ * one-time 0.7→0.8 input transforms before the schema sees the value.
429
+ */
430
+ export const AkmConfigShape = {
431
+ configVersion: z.union([z.string().min(1), z.number()]).optional(),
432
+ profiles: ProfilesSchema.optional(),
433
+ defaults: DefaultsSchema.optional(),
434
+ stashDir: nonEmptyString.optional(),
435
+ semanticSearchMode: z.enum(["off", "auto"]).default("auto"),
436
+ embedding: EmbeddingConnectionConfigSchema.optional(),
437
+ index: IndexConfigSchema.optional(),
438
+ installed: z.array(InstalledStashEntrySchema).optional(),
439
+ registries: z.array(RegistryConfigEntrySchema).optional(),
440
+ sources: z.array(SourceConfigEntrySchema).optional(),
441
+ output: OutputConfigSchema.optional(),
442
+ writable: z.boolean().optional(),
443
+ defaultWriteTarget: nonEmptyString.optional(),
444
+ search: SearchConfigSchema.optional(),
445
+ feedback: FeedbackConfigSchema.optional(),
446
+ archiveRetentionDays: nonNegativeNumber.optional(),
447
+ improve: ImproveConfigSchema.optional(),
448
+ };
449
+ export const AkmConfigBaseSchema = z.object(AkmConfigShape).strict();
450
+ export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) => {
451
+ // #464.a: defaultWriteTarget must name a configured source when sources
452
+ // are present. With no sources configured, error out instead of silently
453
+ // accepting (no implicit "first writable" fallback — see locked decision 3).
454
+ if (config.defaultWriteTarget !== undefined) {
455
+ const knownNames = (config.sources ?? [])
456
+ .map((s) => s.name)
457
+ .filter((n) => typeof n === "string" && n.length > 0);
458
+ if (knownNames.length === 0) {
459
+ ctx.addIssue({
460
+ code: z.ZodIssueCode.custom,
461
+ path: ["defaultWriteTarget"],
462
+ message: `defaultWriteTarget "${config.defaultWriteTarget}" cannot be resolved: no sources configured. ` +
463
+ "Add at least one entry to `sources` with a matching `name` first.",
464
+ });
465
+ }
466
+ else if (!knownNames.includes(config.defaultWriteTarget)) {
467
+ ctx.addIssue({
468
+ code: z.ZodIssueCode.custom,
469
+ path: ["defaultWriteTarget"],
470
+ message: `defaultWriteTarget "${config.defaultWriteTarget}" does not match any configured source name: ${knownNames.map((n) => `"${n}"`).join(", ")}.`,
471
+ });
472
+ }
473
+ }
474
+ });
475
+ /**
476
+ * Validate a raw object against {@link AkmConfigSchema}. Returns a structured
477
+ * result so callers can render errors as a list (instead of throwing on the
478
+ * first issue).
479
+ */
480
+ export function validateConfigShape(raw) {
481
+ const result = AkmConfigSchema.safeParse(raw);
482
+ if (result.success) {
483
+ return { ok: true, value: result.data, errors: [] };
484
+ }
485
+ return {
486
+ ok: false,
487
+ errors: result.error.issues.map((issue) => ({
488
+ path: issue.path.join("."),
489
+ message: issue.message,
490
+ })),
491
+ };
492
+ }
493
+ // ── Top-level key listing (for hint messages) ───────────────────────────────
494
+ /**
495
+ * Return the sorted list of top-level config keys recognized by the schema.
496
+ * Used by error hints so the list stays in sync with the schema automatically
497
+ * (#460).
498
+ */
499
+ export function listTopLevelConfigKeys() {
500
+ return Object.keys(AkmConfigShape).sort();
501
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
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
+ export {};