akm-cli 0.7.5 → 0.8.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +192 -2
  2. package/README.md +22 -6
  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/shared.js +129 -0
  9. package/dist/cli.js +2569 -1449
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +110 -0
  12. package/dist/commands/agent-support.js +68 -0
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +130 -534
  15. package/dist/commands/consolidate.js +2122 -0
  16. package/dist/commands/curate.js +44 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +660 -0
  19. package/dist/commands/distill.js +1075 -77
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +43 -0
  22. package/dist/commands/events.js +5 -23
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +477 -0
  28. package/dist/commands/health.js +1302 -0
  29. package/dist/commands/help/help-accept.md +12 -0
  30. package/dist/commands/help/help-improve.md +69 -0
  31. package/dist/commands/help/help-proposals.md +18 -0
  32. package/dist/commands/help/help-propose.md +17 -0
  33. package/dist/commands/help/help-reject.md +11 -0
  34. package/dist/commands/history.js +54 -46
  35. package/dist/commands/improve-auto-accept.js +97 -0
  36. package/dist/commands/improve-cli.js +217 -0
  37. package/dist/commands/improve-profiles.js +166 -0
  38. package/dist/commands/improve-result-file.js +167 -0
  39. package/dist/commands/improve.js +2373 -0
  40. package/dist/commands/info.js +5 -2
  41. package/dist/commands/init.js +50 -2
  42. package/dist/commands/installed-stashes.js +102 -139
  43. package/dist/commands/knowledge.js +136 -0
  44. package/dist/commands/lint/agent-linter.js +49 -0
  45. package/dist/commands/lint/base-linter.js +479 -0
  46. package/dist/commands/lint/command-linter.js +49 -0
  47. package/dist/commands/lint/default-linter.js +16 -0
  48. package/dist/commands/lint/env-key-rules.js +154 -0
  49. package/dist/commands/lint/index.js +196 -0
  50. package/dist/commands/lint/knowledge-linter.js +16 -0
  51. package/dist/commands/lint/markdown-insertion.js +343 -0
  52. package/dist/commands/lint/memory-linter.js +61 -0
  53. package/dist/commands/lint/registry.js +36 -0
  54. package/dist/commands/lint/skill-linter.js +45 -0
  55. package/dist/commands/lint/task-linter.js +50 -0
  56. package/dist/commands/lint/types.js +4 -0
  57. package/dist/commands/lint/workflow-linter.js +56 -0
  58. package/dist/commands/lint.js +4 -0
  59. package/dist/commands/migration-help.js +5 -2
  60. package/dist/commands/proposal.js +67 -12
  61. package/dist/commands/propose.js +86 -31
  62. package/dist/commands/reflect.js +1091 -73
  63. package/dist/commands/registry-cli.js +150 -0
  64. package/dist/commands/registry-search.js +5 -2
  65. package/dist/commands/remember-cli.js +257 -0
  66. package/dist/commands/remember.js +69 -6
  67. package/dist/commands/schema-repair.js +203 -0
  68. package/dist/commands/search.js +115 -14
  69. package/dist/commands/secret.js +173 -0
  70. package/dist/commands/self-update.js +3 -0
  71. package/dist/commands/show.js +148 -25
  72. package/dist/commands/source-add.js +17 -45
  73. package/dist/commands/source-clone.js +3 -0
  74. package/dist/commands/source-manage.js +14 -19
  75. package/dist/commands/tasks.js +437 -0
  76. package/dist/commands/url-checker.js +42 -0
  77. package/dist/core/action-contributors.js +28 -0
  78. package/dist/core/asset-ref.js +17 -2
  79. package/dist/core/asset-registry.js +12 -17
  80. package/dist/core/asset-serialize.js +88 -0
  81. package/dist/core/asset-spec.js +67 -1
  82. package/dist/core/common.js +182 -0
  83. package/dist/core/concurrent.js +25 -0
  84. package/dist/core/config-io.js +347 -0
  85. package/dist/core/config-migration.js +622 -0
  86. package/dist/core/config-schema.js +534 -0
  87. package/dist/core/config-sources.js +108 -0
  88. package/dist/core/config-types.js +4 -0
  89. package/dist/core/config-walker.js +337 -0
  90. package/dist/core/config.js +364 -981
  91. package/dist/core/errors.js +42 -20
  92. package/dist/core/events.js +91 -138
  93. package/dist/core/file-lock.js +104 -0
  94. package/dist/core/frontmatter.js +75 -8
  95. package/dist/core/lesson-lint.js +3 -0
  96. package/dist/core/markdown.js +20 -0
  97. package/dist/core/memory-belief.js +62 -0
  98. package/dist/core/memory-contradiction-detect.js +274 -0
  99. package/dist/core/memory-improve.js +806 -0
  100. package/dist/core/parse.js +158 -0
  101. package/dist/core/paths.js +280 -14
  102. package/dist/core/proposal-quality-validators.js +380 -0
  103. package/dist/core/proposal-validators.js +69 -0
  104. package/dist/core/proposals.js +512 -42
  105. package/dist/core/state-db.js +1068 -0
  106. package/dist/core/text-truncation.js +107 -0
  107. package/dist/core/time.js +54 -0
  108. package/dist/core/tty.js +59 -0
  109. package/dist/core/warn.js +64 -1
  110. package/dist/core/write-source.js +3 -0
  111. package/dist/indexer/db-backup.js +391 -0
  112. package/dist/indexer/db-search.js +163 -254
  113. package/dist/indexer/db.js +975 -103
  114. package/dist/indexer/ensure-index.js +64 -0
  115. package/dist/indexer/file-context.js +3 -0
  116. package/dist/indexer/graph-boost.js +376 -101
  117. package/dist/indexer/graph-db.js +391 -0
  118. package/dist/indexer/graph-dedup.js +95 -0
  119. package/dist/indexer/graph-extraction.js +550 -124
  120. package/dist/indexer/index-context.js +4 -0
  121. package/dist/indexer/indexer.js +523 -301
  122. package/dist/indexer/llm-cache.js +52 -0
  123. package/dist/indexer/manifest.js +3 -0
  124. package/dist/indexer/matchers.js +167 -160
  125. package/dist/indexer/memory-inference.js +152 -74
  126. package/dist/indexer/metadata-contributors.js +29 -0
  127. package/dist/indexer/metadata.js +275 -196
  128. package/dist/indexer/path-resolver.js +92 -0
  129. package/dist/indexer/project-context.js +192 -0
  130. package/dist/indexer/ranking-contributors.js +331 -0
  131. package/dist/indexer/ranking.js +81 -0
  132. package/dist/indexer/search-fields.js +5 -9
  133. package/dist/indexer/search-hit-enrichers.js +111 -0
  134. package/dist/indexer/search-source.js +44 -10
  135. package/dist/indexer/semantic-status.js +6 -17
  136. package/dist/indexer/staleness-detect.js +447 -0
  137. package/dist/indexer/usage-events.js +12 -9
  138. package/dist/indexer/walker.js +28 -0
  139. package/dist/integrations/agent/builders.js +135 -0
  140. package/dist/integrations/agent/config.js +122 -230
  141. package/dist/integrations/agent/detect.js +3 -0
  142. package/dist/integrations/agent/index.js +7 -13
  143. package/dist/integrations/agent/model-aliases.js +55 -0
  144. package/dist/integrations/agent/profiles.js +70 -5
  145. package/dist/integrations/agent/prompts.js +214 -80
  146. package/dist/integrations/agent/runner.js +151 -0
  147. package/dist/integrations/agent/sdk-runner.js +126 -0
  148. package/dist/integrations/agent/spawn.js +118 -23
  149. package/dist/integrations/github.js +3 -0
  150. package/dist/integrations/lockfile.js +32 -69
  151. package/dist/integrations/session-logs/index.js +69 -0
  152. package/dist/integrations/session-logs/inline-refs.js +35 -0
  153. package/dist/integrations/session-logs/pre-filter.js +152 -0
  154. package/dist/integrations/session-logs/providers/claude-code.js +282 -0
  155. package/dist/integrations/session-logs/providers/opencode.js +258 -0
  156. package/dist/integrations/session-logs/types.js +4 -0
  157. package/dist/llm/call-ai.js +62 -0
  158. package/dist/llm/client.js +77 -124
  159. package/dist/llm/embedder.js +20 -29
  160. package/dist/llm/embedders/cache.js +3 -7
  161. package/dist/llm/embedders/local.js +42 -1
  162. package/dist/llm/embedders/remote.js +20 -8
  163. package/dist/llm/embedders/types.js +3 -7
  164. package/dist/llm/feature-gate.js +95 -48
  165. package/dist/llm/graph-extract.js +676 -70
  166. package/dist/llm/index-passes.js +44 -29
  167. package/dist/llm/memory-infer.js +77 -71
  168. package/dist/llm/metadata-enhance.js +42 -29
  169. package/dist/llm/prompts/extract-session.md +80 -0
  170. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  171. package/dist/output/cli-hints-full.md +292 -0
  172. package/dist/output/cli-hints-short.md +66 -0
  173. package/dist/output/cli-hints.js +7 -320
  174. package/dist/output/context.js +60 -8
  175. package/dist/output/renderers.js +300 -257
  176. package/dist/output/shapes/curate.js +56 -0
  177. package/dist/output/shapes/distill.js +10 -0
  178. package/dist/output/shapes/env-list.js +19 -0
  179. package/dist/output/shapes/events.js +11 -0
  180. package/dist/output/shapes/helpers.js +424 -0
  181. package/dist/output/shapes/history.js +7 -0
  182. package/dist/output/shapes/passthrough.js +102 -0
  183. package/dist/output/shapes/proposal-accept.js +7 -0
  184. package/dist/output/shapes/proposal-diff.js +7 -0
  185. package/dist/output/shapes/proposal-list.js +7 -0
  186. package/dist/output/shapes/proposal-producer.js +11 -0
  187. package/dist/output/shapes/proposal-reject.js +7 -0
  188. package/dist/output/shapes/proposal-show.js +7 -0
  189. package/dist/output/shapes/registry-search.js +6 -0
  190. package/dist/output/shapes/registry.js +30 -0
  191. package/dist/output/shapes/search.js +6 -0
  192. package/dist/output/shapes/secret-list.js +19 -0
  193. package/dist/output/shapes/show.js +6 -0
  194. package/dist/output/shapes/vault-list.js +19 -0
  195. package/dist/output/shapes.js +51 -516
  196. package/dist/output/text/add.js +6 -0
  197. package/dist/output/text/clone.js +6 -0
  198. package/dist/output/text/config.js +6 -0
  199. package/dist/output/text/curate.js +6 -0
  200. package/dist/output/text/distill.js +7 -0
  201. package/dist/output/text/enable-disable.js +7 -0
  202. package/dist/output/text/events.js +10 -0
  203. package/dist/output/text/feedback.js +6 -0
  204. package/dist/output/text/helpers.js +1039 -0
  205. package/dist/output/text/history.js +7 -0
  206. package/dist/output/text/import.js +6 -0
  207. package/dist/output/text/index.js +6 -0
  208. package/dist/output/text/info.js +6 -0
  209. package/dist/output/text/init.js +6 -0
  210. package/dist/output/text/list.js +6 -0
  211. package/dist/output/text/proposal-producer.js +8 -0
  212. package/dist/output/text/proposal.js +11 -0
  213. package/dist/output/text/registry-commands.js +11 -0
  214. package/dist/output/text/registry.js +30 -0
  215. package/dist/output/text/remember.js +6 -0
  216. package/dist/output/text/remove.js +6 -0
  217. package/dist/output/text/save.js +6 -0
  218. package/dist/output/text/search.js +6 -0
  219. package/dist/output/text/show.js +6 -0
  220. package/dist/output/text/update.js +6 -0
  221. package/dist/output/text/upgrade.js +6 -0
  222. package/dist/output/text/vault.js +16 -0
  223. package/dist/output/text/wiki.js +15 -0
  224. package/dist/output/text/workflow.js +14 -0
  225. package/dist/output/text.js +44 -1092
  226. package/dist/registry/build-index.js +3 -0
  227. package/dist/registry/create-provider-registry.js +3 -0
  228. package/dist/registry/factory.js +4 -1
  229. package/dist/registry/origin-resolve.js +3 -0
  230. package/dist/registry/providers/index.js +3 -0
  231. package/dist/registry/providers/skills-sh.js +71 -50
  232. package/dist/registry/providers/static-index.js +53 -48
  233. package/dist/registry/providers/types.js +3 -24
  234. package/dist/registry/resolve.js +11 -16
  235. package/dist/registry/types.js +3 -0
  236. package/dist/scripts/migrate-storage.js +17750 -0
  237. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  238. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  239. package/dist/setup/detect.js +3 -0
  240. package/dist/setup/ripgrep-install.js +3 -0
  241. package/dist/setup/ripgrep-resolve.js +3 -0
  242. package/dist/setup/setup.js +775 -37
  243. package/dist/setup/steps.js +3 -15
  244. package/dist/sources/include.js +3 -0
  245. package/dist/sources/provider-factory.js +5 -12
  246. package/dist/sources/provider.js +3 -20
  247. package/dist/sources/providers/filesystem.js +19 -23
  248. package/dist/sources/providers/git.js +138 -21
  249. package/dist/sources/providers/index.js +3 -0
  250. package/dist/sources/providers/install-types.js +3 -13
  251. package/dist/sources/providers/npm.js +3 -4
  252. package/dist/sources/providers/provider-utils.js +3 -0
  253. package/dist/sources/providers/sync-from-ref.js +3 -11
  254. package/dist/sources/providers/tar-utils.js +3 -0
  255. package/dist/sources/providers/website.js +18 -22
  256. package/dist/sources/resolve.js +3 -0
  257. package/dist/sources/types.js +3 -0
  258. package/dist/sources/website-ingest.js +7 -0
  259. package/dist/tasks/backends/cron.js +203 -0
  260. package/dist/tasks/backends/exec-utils.js +28 -0
  261. package/dist/tasks/backends/index.js +24 -0
  262. package/dist/tasks/backends/launchd-template.xml +19 -0
  263. package/dist/tasks/backends/launchd.js +187 -0
  264. package/dist/tasks/backends/schtasks-template.xml +29 -0
  265. package/dist/tasks/backends/schtasks.js +215 -0
  266. package/dist/tasks/parser.js +211 -0
  267. package/dist/tasks/resolveAkmBin.js +87 -0
  268. package/dist/tasks/runner.js +458 -0
  269. package/dist/tasks/schedule.js +227 -0
  270. package/dist/tasks/schema.js +15 -0
  271. package/dist/tasks/validator.js +62 -0
  272. package/dist/version.js +3 -0
  273. package/dist/wiki/index-template.md +12 -0
  274. package/dist/wiki/ingest-workflow-template.md +54 -0
  275. package/dist/wiki/log-template.md +8 -0
  276. package/dist/wiki/schema-template.md +61 -0
  277. package/dist/wiki/wiki-templates.js +15 -0
  278. package/dist/wiki/wiki.js +13 -61
  279. package/dist/workflows/authoring.js +8 -25
  280. package/dist/workflows/cli.js +3 -0
  281. package/dist/workflows/db.js +140 -10
  282. package/dist/workflows/document-cache.js +3 -10
  283. package/dist/workflows/parser.js +3 -0
  284. package/dist/workflows/renderer.js +11 -3
  285. package/dist/workflows/runs.js +77 -92
  286. package/dist/workflows/schema.js +3 -0
  287. package/dist/workflows/scope-key.js +3 -0
  288. package/dist/workflows/validator.js +4 -8
  289. package/dist/workflows/workflow-template.md +24 -0
  290. package/docs/README.md +10 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.0.md +1 -1
  293. package/docs/migration/release-notes/0.7.5.md +2 -2
  294. package/docs/migration/release-notes/0.8.0.md +48 -0
  295. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  296. package/package.json +30 -12
  297. package/.github/LICENSE +0 -374
  298. package/dist/commands/install-audit.js +0 -381
  299. package/dist/commands/vault.js +0 -328
  300. package/dist/templates/wiki-templates.js +0 -100
@@ -1,559 +1,155 @@
1
- import { DEFAULT_CONFIG, } from "../core/config";
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
+ * Config CLI commands — `akm config get/set/unset/list`.
6
+ *
7
+ * Thin wrappers around the schema walker in `core/config-walker.ts`. Adding a
8
+ * new config field is one line of Zod schema in `core/config-schema.ts` and
9
+ * zero lines here — the walker handles get/set/unset/coercion uniformly.
10
+ *
11
+ * Legacy behaviour preserved:
12
+ * - `akm config set llm.<x>` writes to `profiles.llm.<defaults.llm>` (or
13
+ * auto-creates a "default" profile), mirroring the pre-rewrite shim.
14
+ * - `akm config set embedding.ollamaOptions.numCtx` is sugar for
15
+ * `embedding.ollamaOptions.num_ctx` (camelCase ↔ snake_case bridge).
16
+ * - `parseConfigValue` returns a Partial<AkmConfig> so it can be merged with
17
+ * the runtime config object via `mergeConfigValue`.
18
+ */
19
+ import { DEFAULT_CONFIG, getSources } from "../core/config";
20
+ import { configGet, configSet, configUnset, unknownKeyHint } from "../core/config-walker";
2
21
  import { UsageError } from "../core/errors";
3
- // ── Merge helpers for LLM/embedding subkey set ───────────────────────────────
4
- function mergeLlmLike(base, patch) {
5
- return { endpoint: "", model: "", ...(base ?? {}), ...patch };
6
- }
7
- function mergeLlmLikeEmbedding(base, patch) {
8
- return { endpoint: "", model: "", ...(base ?? {}), ...patch };
9
- }
10
- export function parseConfigValue(key, value) {
11
- switch (key) {
12
- case "stashDir":
13
- return { stashDir: requireNonEmptyString(value, key) };
14
- case "defaultWriteTarget":
15
- return { defaultWriteTarget: requireNonEmptyString(value, key) };
16
- case "semanticSearchMode":
17
- // Accept legacy boolean-style strings from CLI
18
- if (value === "true")
19
- return { semanticSearchMode: "auto" };
20
- if (value === "false")
21
- return { semanticSearchMode: "off" };
22
- if (value !== "off" && value !== "auto") {
23
- throw new UsageError(`Invalid value for semanticSearchMode: expected "off" or "auto"`);
24
- }
25
- return { semanticSearchMode: value };
26
- case "embedding":
27
- return { embedding: parseEmbeddingConnectionValue(value) };
28
- case "embedding.endpoint":
29
- return { embedding: mergeLlmLikeEmbedding(undefined, { endpoint: requireNonEmptyString(value, key) }) };
30
- case "embedding.model":
31
- return { embedding: mergeLlmLikeEmbedding(undefined, { model: requireNonEmptyString(value, key) }) };
32
- case "embedding.apiKey":
33
- return { embedding: mergeLlmLikeEmbedding(undefined, { apiKey: requireNonEmptyString(value, key) }) };
34
- case "embedding.contextLength":
35
- return { embedding: mergeLlmLikeEmbedding(undefined, { contextLength: parsePositiveInteger(value, key) }) };
36
- case "embedding.ollamaOptions.numCtx":
37
- return {
38
- embedding: mergeLlmLikeEmbedding(undefined, { ollamaOptions: { num_ctx: parsePositiveInteger(value, key) } }),
39
- };
40
- case "llm":
41
- return { llm: parseLlmConnectionValue(value) };
42
- case "llm.endpoint":
43
- return { llm: mergeLlmLike(undefined, { endpoint: requireNonEmptyString(value, key) }) };
44
- case "llm.model":
45
- return { llm: mergeLlmLike(undefined, { model: requireNonEmptyString(value, key) }) };
46
- case "llm.apiKey":
47
- return { llm: mergeLlmLike(undefined, { apiKey: requireNonEmptyString(value, key) }) };
48
- case "registries":
49
- return { registries: parseRegistriesValue(value) };
50
- case "sources":
51
- case "stashes":
52
- // "stashes" is kept as an alias for backwards-compat; both write to `sources`.
53
- return { sources: parseStashesValue(value) };
54
- case "output.format":
55
- return { output: { format: parseOutputFormat(value) } };
56
- case "output.detail":
57
- return { output: { detail: parseOutputDetail(value) } };
58
- case "security.installAudit.enabled":
59
- return { security: { installAudit: { enabled: parseBooleanValue(value, key) } } };
60
- case "security.installAudit.blockOnCritical":
61
- return { security: { installAudit: { blockOnCritical: parseBooleanValue(value, key) } } };
62
- case "security.installAudit.blockUnlistedRegistries":
63
- return { security: { installAudit: { blockUnlistedRegistries: parseBooleanValue(value, key) } } };
64
- case "security.installAudit.registryAllowlist":
65
- return { security: { installAudit: { registryAllowlist: parseStringArrayValue(value, key) } } };
66
- case "security.installAudit.registryWhitelist":
67
- return { security: { installAudit: { registryAllowlist: parseStringArrayValue(value, key) } } };
68
- case "security.installAudit.allowedFindings":
69
- return { security: { installAudit: { allowedFindings: parseAllowedFindingsValue(value, key) } } };
70
- default:
71
- throw new UsageError(`Unknown config key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
72
- }
73
- }
74
- const UNKNOWN_CONFIG_KEY_HINT = "Valid top-level keys: stashDir, embedding, llm, registries, sources, agent, output, semanticSearchMode. Use dotted paths like `embedding.endpoint` or `output.format` for nested values.";
22
+ // ── Legacy `llm.*` `profiles.llm.<default>.*` aliasing ────────────────────
23
+ /**
24
+ * Map a legacy top-level `llm.<sub>` path onto the actual schema path. The
25
+ * default profile name is "default" when `defaults.llm` is unset.
26
+ */
27
+ function rewriteLegacyLlmPath(config, key) {
28
+ if (key !== "llm" && !key.startsWith("llm."))
29
+ return key;
30
+ const sub = key === "llm" ? "" : key.slice("llm.".length);
31
+ const profileName = config.defaults?.llm ?? "default";
32
+ return sub ? `profiles.llm.${profileName}.${sub}` : `profiles.llm.${profileName}`;
33
+ }
34
+ /**
35
+ * Translate the legacy `embedding.ollamaOptions.numCtx` to the actual schema
36
+ * key `embedding.ollamaOptions.num_ctx`.
37
+ */
38
+ function rewriteEmbeddingPath(key) {
39
+ if (key === "embedding.ollamaOptions.numCtx")
40
+ return "embedding.ollamaOptions.num_ctx";
41
+ return key;
42
+ }
43
+ /**
44
+ * Translate the deprecated `stashes` alias for `sources` (one-way: both read
45
+ * and write go through `sources`).
46
+ */
47
+ function rewriteSourcesAlias(key) {
48
+ if (key === "stashes")
49
+ return "sources";
50
+ if (key.startsWith("stashes."))
51
+ return `sources.${key.slice("stashes.".length)}`;
52
+ return key;
53
+ }
54
+ function rewriteKey(config, key) {
55
+ let k = rewriteLegacyLlmPath(config, key);
56
+ k = rewriteEmbeddingPath(k);
57
+ k = rewriteSourcesAlias(k);
58
+ return k;
59
+ }
60
+ // ── Public API ──────────────────────────────────────────────────────────────
75
61
  export function getConfigValue(config, key) {
76
- switch (key) {
77
- case "stashDir":
78
- return config.stashDir ?? null;
79
- case "defaultWriteTarget":
80
- return config.defaultWriteTarget ?? null;
81
- case "semanticSearchMode":
82
- return config.semanticSearchMode;
83
- case "embedding":
84
- return config.embedding ?? null;
85
- case "embedding.endpoint":
86
- return config.embedding?.endpoint ?? null;
87
- case "embedding.model":
88
- return config.embedding?.model ?? null;
89
- case "embedding.apiKey":
90
- return config.embedding?.apiKey ?? null;
91
- case "embedding.contextLength":
92
- return config.embedding?.contextLength ?? null;
93
- case "embedding.ollamaOptions.numCtx":
94
- return config.embedding?.ollamaOptions?.num_ctx ?? null;
95
- case "llm":
96
- return config.llm ?? null;
97
- case "llm.endpoint":
98
- return config.llm?.endpoint ?? null;
99
- case "llm.model":
100
- return config.llm?.model ?? null;
101
- case "llm.apiKey":
102
- return config.llm?.apiKey ?? null;
103
- case "registries":
104
- return config.registries ?? DEFAULT_CONFIG.registries ?? [];
105
- case "sources":
106
- case "stashes":
107
- // "stashes" is an alias for "sources" for backwards-compat.
108
- return config.sources ?? config.stashes ?? [];
109
- case "output.format":
110
- return config.output?.format ?? null;
111
- case "output.detail":
112
- return config.output?.detail ?? null;
113
- case "security":
114
- return config.security ?? null;
115
- case "security.installAudit.enabled":
116
- return config.security?.installAudit?.enabled ?? null;
117
- case "security.installAudit.blockOnCritical":
118
- return config.security?.installAudit?.blockOnCritical ?? null;
119
- case "security.installAudit.blockUnlistedRegistries":
120
- return config.security?.installAudit?.blockUnlistedRegistries ?? null;
121
- case "security.installAudit.registryAllowlist":
122
- return getInstallAuditAllowlist(config);
123
- case "security.installAudit.registryWhitelist":
124
- return getInstallAuditAllowlist(config);
125
- case "security.installAudit.allowedFindings":
126
- return config.security?.installAudit?.allowedFindings ?? null;
127
- default:
128
- throw new UsageError(`Unknown config key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
129
- }
62
+ const k = rewriteKey(config, key);
63
+ return configGet(config, k);
130
64
  }
131
65
  export function setConfigValue(config, key, rawValue) {
132
- switch (key) {
133
- case "stashDir":
134
- case "semanticSearchMode":
135
- case "embedding":
136
- case "llm":
137
- case "registries":
138
- case "sources":
139
- case "stashes":
140
- case "output.format":
141
- case "output.detail":
142
- case "security.installAudit.enabled":
143
- case "security.installAudit.blockOnCritical":
144
- case "security.installAudit.blockUnlistedRegistries":
145
- case "security.installAudit.registryAllowlist":
146
- case "security.installAudit.registryWhitelist":
147
- case "security.installAudit.allowedFindings":
148
- return mergeConfigValue(config, parseConfigValue(key, rawValue));
149
- // Subkey setters use deep-merge so sibling fields are preserved
150
- case "embedding.endpoint":
151
- return {
152
- ...config,
153
- embedding: mergeLlmLikeEmbedding(config.embedding, { endpoint: requireNonEmptyString(rawValue, key) }),
154
- };
155
- case "embedding.model":
156
- return {
157
- ...config,
158
- embedding: mergeLlmLikeEmbedding(config.embedding, { model: requireNonEmptyString(rawValue, key) }),
159
- };
160
- case "embedding.apiKey":
161
- return {
162
- ...config,
163
- embedding: mergeLlmLikeEmbedding(config.embedding, { apiKey: requireNonEmptyString(rawValue, key) }),
164
- };
165
- case "embedding.contextLength":
166
- return {
167
- ...config,
168
- embedding: mergeLlmLikeEmbedding(config.embedding, { contextLength: parsePositiveInteger(rawValue, key) }),
66
+ // #454: reject the legacy aliases up front so the error message names the
67
+ // env var the user typed (AKM_LLM_API_KEY) rather than the rewritten profile
68
+ // env var (AKM_PROFILE_DEFAULT_API_KEY) — both work at runtime, but the
69
+ // shorter name matches the user's mental model.
70
+ if (key === "llm.apiKey") {
71
+ 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. " +
72
+ "Use the corresponding environment variable. AKM reads it at request time.");
73
+ }
74
+ if (key === "embedding.apiKey") {
75
+ 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. " +
76
+ "Use the corresponding environment variable. AKM reads it at request time.");
77
+ }
78
+ const k = rewriteKey(config, key);
79
+ // Legacy ergonomic: `akm config set semanticSearchMode true|false`
80
+ let coerced = rawValue;
81
+ if (k === "semanticSearchMode") {
82
+ if (rawValue === "true")
83
+ coerced = "auto";
84
+ else if (rawValue === "false")
85
+ coerced = "off";
86
+ }
87
+ let next = configSet(config, k, coerced);
88
+ // Legacy ergonomic shim: when the user sets `llm.<field>` and no
89
+ // `defaults.llm` is set, point it at the freshly-created profile so the
90
+ // value actually takes effect at runtime.
91
+ if (key === "llm" || key.startsWith("llm.")) {
92
+ if (!next.defaults?.llm) {
93
+ next = {
94
+ ...next,
95
+ defaults: { ...(next.defaults ?? {}), llm: "default" },
169
96
  };
170
- case "embedding.ollamaOptions.numCtx":
171
- return {
172
- ...config,
173
- embedding: mergeLlmLikeEmbedding(config.embedding, {
174
- ollamaOptions: { ...(config.embedding?.ollamaOptions ?? {}), num_ctx: parsePositiveInteger(rawValue, key) },
175
- }),
176
- };
177
- case "llm.endpoint":
178
- return { ...config, llm: mergeLlmLike(config.llm, { endpoint: requireNonEmptyString(rawValue, key) }) };
179
- case "llm.model":
180
- return { ...config, llm: mergeLlmLike(config.llm, { model: requireNonEmptyString(rawValue, key) }) };
181
- case "llm.apiKey":
182
- return { ...config, llm: mergeLlmLike(config.llm, { apiKey: requireNonEmptyString(rawValue, key) }) };
183
- case "defaultWriteTarget": {
184
- const name = requireNonEmptyString(rawValue, key);
185
- const knownNames = (config.sources ?? config.stashes ?? [])
186
- .map((s) => s.name)
187
- .filter((n) => typeof n === "string");
188
- if (knownNames.length > 0 && !knownNames.includes(name)) {
189
- throw new UsageError(`Unknown source name "${name}" for defaultWriteTarget; configured source names: ${knownNames.map((n) => `"${n}"`).join(", ")}`);
190
- }
191
- return { ...config, defaultWriteTarget: name };
192
97
  }
193
- default:
194
- throw new UsageError(`Unknown config key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
195
98
  }
99
+ return next;
196
100
  }
197
101
  export function unsetConfigValue(config, key) {
198
- switch (key) {
199
- case "stashDir":
200
- return { ...config, stashDir: undefined };
201
- case "defaultWriteTarget":
202
- return { ...config, defaultWriteTarget: undefined };
203
- case "embedding":
204
- return { ...config, embedding: undefined };
205
- case "embedding.endpoint":
206
- return { ...config, embedding: mergeLlmLikeEmbedding(config.embedding, { endpoint: "" }) };
207
- case "embedding.model":
208
- return { ...config, embedding: mergeLlmLikeEmbedding(config.embedding, { model: "" }) };
209
- case "embedding.apiKey": {
210
- if (!config.embedding)
211
- return config;
212
- const { apiKey: _a, ...rest } = config.embedding;
213
- return { ...config, embedding: rest };
214
- }
215
- case "embedding.contextLength": {
216
- if (!config.embedding)
217
- return config;
218
- const { contextLength: _cl, ...rest } = config.embedding;
219
- return { ...config, embedding: rest };
220
- }
221
- case "embedding.ollamaOptions.numCtx": {
222
- if (!config.embedding?.ollamaOptions)
223
- return config;
224
- const { num_ctx: _nc, ...restOpts } = config.embedding.ollamaOptions;
225
- const ollamaOptions = Object.keys(restOpts).length > 0 ? restOpts : undefined;
226
- return { ...config, embedding: { ...config.embedding, ollamaOptions } };
227
- }
228
- case "llm":
229
- return { ...config, llm: undefined };
230
- case "llm.endpoint":
231
- return { ...config, llm: mergeLlmLike(config.llm, { endpoint: "" }) };
232
- case "llm.model":
233
- return { ...config, llm: mergeLlmLike(config.llm, { model: "" }) };
234
- case "llm.apiKey": {
235
- if (!config.llm)
236
- return config;
237
- const { apiKey: _b, ...restLlm } = config.llm;
238
- return { ...config, llm: restLlm };
102
+ const k = rewriteKey(config, key);
103
+ return configUnset(config, k);
104
+ }
105
+ /**
106
+ * Compatibility shim: returns a `Partial<AkmConfig>` containing just the
107
+ * change. Older code merged this onto the live config — new code should call
108
+ * `setConfigValue` directly (which returns the full merged config).
109
+ */
110
+ export function parseConfigValue(key, value) {
111
+ // Use a "marker" base so we can detect which top-level fields actually got
112
+ // touched by the set call. Anything still equal to the marker is untouched.
113
+ const SENTINEL = Symbol("untouched");
114
+ const base = { semanticSearchMode: SENTINEL };
115
+ const next = setConfigValue(base, key, value);
116
+ const patch = {};
117
+ for (const k of Object.keys(next)) {
118
+ if (next[k] !== SENTINEL) {
119
+ patch[k] = next[k];
239
120
  }
240
- case "registries":
241
- return { ...config, registries: undefined };
242
- case "sources":
243
- case "stashes":
244
- // "stashes" is kept as an alias for backwards-compat; both clear `sources`.
245
- return { ...config, sources: undefined, stashes: undefined };
246
- case "output.format":
247
- return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
248
- case "output.detail":
249
- return { ...config, output: mergeOutputConfig(config.output, { detail: undefined }) };
250
- case "security":
251
- return { ...config, security: undefined };
252
- case "security.installAudit.enabled":
253
- return { ...config, security: mergeSecurityConfig(config.security, { installAudit: { enabled: undefined } }) };
254
- case "security.installAudit.blockOnCritical":
255
- return {
256
- ...config,
257
- security: mergeSecurityConfig(config.security, { installAudit: { blockOnCritical: undefined } }),
258
- };
259
- case "security.installAudit.blockUnlistedRegistries":
260
- return {
261
- ...config,
262
- security: mergeSecurityConfig(config.security, { installAudit: { blockUnlistedRegistries: undefined } }),
263
- };
264
- case "security.installAudit.registryAllowlist":
265
- case "security.installAudit.registryWhitelist":
266
- return {
267
- ...config,
268
- security: mergeSecurityConfig(config.security, {
269
- installAudit: { registryAllowlist: undefined, registryWhitelist: undefined },
270
- }),
271
- };
272
- case "security.installAudit.allowedFindings":
273
- return {
274
- ...config,
275
- security: mergeSecurityConfig(config.security, {
276
- installAudit: { allowedFindings: undefined },
277
- }),
278
- };
279
- default:
280
- throw new UsageError(`Unknown or unsupported unset key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
281
121
  }
122
+ return patch;
282
123
  }
283
124
  export function listConfig(config) {
284
125
  const result = {
285
126
  semanticSearchMode: config.semanticSearchMode,
286
127
  registries: config.registries ?? DEFAULT_CONFIG.registries ?? [],
287
- output: mergeOutputConfig(DEFAULT_CONFIG.output, config.output) ?? null,
128
+ output: { ...(DEFAULT_CONFIG.output ?? {}), ...(config.output ?? {}) },
288
129
  stashDir: config.stashDir ?? null,
289
130
  installed: config.installed ?? [],
290
- sources: config.sources ?? config.stashes ?? [],
131
+ sources: getSources(config),
291
132
  };
292
133
  if (config.defaultWriteTarget)
293
134
  result.defaultWriteTarget = config.defaultWriteTarget;
294
135
  if (config.embedding)
295
136
  result.embedding = config.embedding;
296
- if (config.llm)
297
- result.llm = config.llm;
298
- if (config.security)
299
- result.security = config.security;
300
- return result;
301
- }
302
- function mergeConfigValue(config, partial) {
303
- return {
304
- ...config,
305
- ...partial,
306
- output: mergeOutputConfig(config.output, partial.output),
307
- security: mergeSecurityConfig(config.security, partial.security),
308
- };
309
- }
310
- function mergeOutputConfig(base, override) {
311
- const merged = {
312
- ...(base ?? {}),
313
- ...(override ?? {}),
314
- };
315
- return merged.format || merged.detail ? merged : undefined;
316
- }
317
- function mergeSecurityConfig(base, override) {
318
- const mergedInstallAudit = mergeInstallAuditConfig(base?.installAudit, override?.installAudit);
319
- return mergedInstallAudit ? { installAudit: mergedInstallAudit } : undefined;
320
- }
321
- function mergeInstallAuditConfig(base, override) {
322
- const merged = {
323
- ...(base ?? {}),
324
- ...(override ?? {}),
325
- };
326
- const hasValue = Object.values(merged).some((value) => value !== undefined);
327
- return hasValue ? merged : undefined;
328
- }
329
- function parseOutputFormat(value) {
330
- if (value === "json" || value === "yaml" || value === "text")
331
- return value;
332
- throw new UsageError(`Invalid value for output.format: expected one of json|yaml|text`);
333
- }
334
- function parseOutputDetail(value) {
335
- if (value === "brief" || value === "normal" || value === "full")
336
- return value;
337
- throw new UsageError(`Invalid value for output.detail: expected one of brief|normal|full`);
338
- }
339
- function parseBooleanValue(value, key) {
340
- if (value === "true")
341
- return true;
342
- if (value === "false")
343
- return false;
344
- throw new UsageError(`Invalid value for ${key}: expected true or false`);
345
- }
346
- function parseStringArrayValue(value, key) {
347
- let parsed;
348
- try {
349
- parsed = JSON.parse(value);
350
- }
351
- catch {
352
- throw new UsageError(`Invalid value for ${key}: expected a JSON array of strings`);
353
- }
354
- if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
355
- throw new UsageError(`Invalid value for ${key}: expected a JSON array of strings`);
356
- }
357
- return parsed;
358
- }
359
- function getInstallAuditAllowlist(config) {
360
- return config.security?.installAudit?.registryAllowlist ?? config.security?.installAudit?.registryWhitelist ?? null;
361
- }
362
- function parseAllowedFindingsValue(value, key) {
363
- let parsed;
364
- try {
365
- parsed = JSON.parse(value);
366
- }
367
- catch {
368
- throw new UsageError(`Invalid value for ${key}: expected a JSON array of {id, ref?, path?, reason?} objects`);
369
- }
370
- if (!Array.isArray(parsed)) {
371
- throw new UsageError(`Invalid value for ${key}: expected a JSON array`);
372
- }
373
- return parsed.map((entry, i) => {
374
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
375
- throw new UsageError(`Invalid value for ${key}[${i}]: expected an object with an "id" field`);
376
- }
377
- const obj = entry;
378
- if (typeof obj.id !== "string" || !obj.id) {
379
- throw new UsageError(`Invalid value for ${key}[${i}]: "id" is required`);
380
- }
381
- const result = { id: obj.id };
382
- if (typeof obj.ref === "string" && obj.ref)
383
- result.ref = obj.ref;
384
- if (typeof obj.path === "string" && obj.path)
385
- result.path = obj.path;
386
- if (typeof obj.reason === "string" && obj.reason)
387
- result.reason = obj.reason;
388
- return result;
389
- });
390
- }
391
- function parseRegistriesValue(value) {
392
- if (value === "null" || value === "")
393
- return undefined;
394
- let parsed;
395
- try {
396
- parsed = JSON.parse(value);
397
- }
398
- catch {
399
- throw new UsageError(`Invalid value for registries: expected JSON array of {url, name?, enabled?, provider?, options?} objects` +
400
- ` (e.g. '[{"url":"https://example.com/index.json","name":"my-registry"}]')`);
401
- }
402
- if (!Array.isArray(parsed)) {
403
- throw new UsageError(`Invalid value for registries: expected a JSON array`);
404
- }
405
- return parsed.map((entry, i) => {
406
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
407
- throw new UsageError(`Invalid value for registries[${i}]: expected an object with a "url" field`);
408
- }
409
- const obj = entry;
410
- if (typeof obj.url !== "string" || !obj.url) {
411
- throw new UsageError(`Invalid value for registries[${i}]: "url" is required`);
412
- }
413
- const result = { url: obj.url };
414
- if (typeof obj.name === "string" && obj.name)
415
- result.name = obj.name;
416
- if (typeof obj.enabled === "boolean")
417
- result.enabled = obj.enabled;
418
- if (typeof obj.provider === "string" && obj.provider)
419
- result.provider = obj.provider;
420
- if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
421
- result.options = obj.options;
422
- }
423
- return result;
424
- });
425
- }
426
- function parseEmbeddingConnectionValue(value) {
427
- if (value === "null" || value === "")
428
- return undefined;
429
- const parsed = parseJsonObject(value, "embedding", {
430
- endpoint: "http://localhost:11434/v1/embeddings",
431
- model: "nomic-embed-text",
432
- });
433
- const localModel = typeof parsed.localModel === "string" && parsed.localModel ? parsed.localModel : undefined;
434
- const endpoint = typeof parsed.endpoint === "string" ? parsed.endpoint : "";
435
- if (!endpoint) {
436
- if (!localModel) {
437
- throw new UsageError(`Invalid value for embedding: endpoint/model are required for remote embeddings, or provide localModel`);
438
- }
439
- const localOnly = { endpoint: "", model: "", localModel };
440
- if (typeof parsed.provider === "string" && parsed.provider)
441
- localOnly.provider = parsed.provider;
442
- return localOnly;
443
- }
444
- const result = {
445
- endpoint: asRequiredString(parsed.endpoint, "embedding", "endpoint"),
446
- model: asRequiredString(parsed.model, "embedding", "model"),
447
- };
448
- if (typeof parsed.provider === "string" && parsed.provider)
449
- result.provider = parsed.provider;
450
- if (parsed.dimension !== undefined)
451
- result.dimension = parseUnknownPositiveInteger(parsed.dimension, "embedding.dimension");
452
- if (typeof parsed.apiKey === "string" && parsed.apiKey)
453
- result.apiKey = parsed.apiKey;
454
- if (localModel)
455
- result.localModel = localModel;
456
- return result;
457
- }
458
- function parseLlmConnectionValue(value) {
459
- if (value === "null" || value === "")
460
- return undefined;
461
- const parsed = parseJsonObject(value, "llm", {
462
- endpoint: "http://localhost:11434/v1/chat/completions",
463
- model: "llama3.2",
464
- });
465
- const result = {
466
- endpoint: asRequiredString(parsed.endpoint, "llm", "endpoint"),
467
- model: asRequiredString(parsed.model, "llm", "model"),
468
- };
469
- if (typeof parsed.provider === "string" && parsed.provider)
470
- result.provider = parsed.provider;
471
- if (parsed.temperature !== undefined)
472
- result.temperature = parseUnknownNumber(parsed.temperature, "llm.temperature");
473
- if (parsed.maxTokens !== undefined)
474
- result.maxTokens = parseUnknownPositiveInteger(parsed.maxTokens, "llm.maxTokens");
475
- if (typeof parsed.apiKey === "string" && parsed.apiKey)
476
- result.apiKey = parsed.apiKey;
137
+ if (config.profiles)
138
+ result.profiles = config.profiles;
139
+ if (config.defaults)
140
+ result.defaults = config.defaults;
141
+ if (config.search)
142
+ result.search = config.search;
143
+ if (config.index)
144
+ result.index = config.index;
145
+ if (config.feedback)
146
+ result.feedback = config.feedback;
147
+ if (config.improve)
148
+ result.improve = config.improve;
149
+ if (config.archiveRetentionDays !== undefined)
150
+ result.archiveRetentionDays = config.archiveRetentionDays;
151
+ if (config.configVersion !== undefined)
152
+ result.configVersion = config.configVersion;
477
153
  return result;
478
154
  }
479
- function parseJsonObject(value, key, example) {
480
- let parsed;
481
- try {
482
- parsed = JSON.parse(value);
483
- }
484
- catch {
485
- throw new UsageError(`Invalid value for ${key}: expected JSON object with endpoint and model` +
486
- ` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`, "INVALID_JSON_CONFIG_VALUE");
487
- }
488
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
489
- throw new UsageError(`Invalid value for ${key}: expected a JSON object`);
490
- }
491
- return parsed;
492
- }
493
- function asRequiredString(value, key, field) {
494
- if (typeof value !== "string" || !value) {
495
- throw new UsageError(`Invalid value for ${key}: "${field}" is a required string field`);
496
- }
497
- return value;
498
- }
499
- function requireNonEmptyString(value, key) {
500
- if (!value) {
501
- throw new UsageError(`Invalid value for ${key}: expected a non-empty string`);
502
- }
503
- return value;
504
- }
505
- function parseUnknownNumber(value, key) {
506
- if (typeof value !== "number" || !Number.isFinite(value)) {
507
- throw new UsageError(`Invalid value for ${key}: expected a number`);
508
- }
509
- return value;
510
- }
511
- function parseUnknownPositiveInteger(value, key) {
512
- if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
513
- throw new UsageError(`Invalid value for ${key}: expected a positive integer`);
514
- }
515
- return value;
516
- }
517
- function parsePositiveInteger(value, key) {
518
- const n = Number(value);
519
- if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
520
- throw new UsageError(`Invalid value for ${key}: expected a positive integer`);
521
- }
522
- return n;
523
- }
524
- function parseStashesValue(value) {
525
- if (value === "null" || value === "")
526
- return undefined;
527
- let parsed;
528
- try {
529
- parsed = JSON.parse(value);
530
- }
531
- catch {
532
- throw new UsageError(`Invalid value for sources: expected JSON array of {type, path?, url?, name?, enabled?, options?} objects`);
533
- }
534
- if (!Array.isArray(parsed)) {
535
- throw new UsageError(`Invalid value for sources: expected a JSON array`);
536
- }
537
- return parsed.map((entry, i) => {
538
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
539
- throw new UsageError(`Invalid value for sources[${i}]: expected an object with a "type" field`);
540
- }
541
- const obj = entry;
542
- if (typeof obj.type !== "string" || !obj.type) {
543
- throw new UsageError(`Invalid value for sources[${i}]: "type" is required`);
544
- }
545
- const result = { type: obj.type };
546
- if (typeof obj.path === "string" && obj.path)
547
- result.path = obj.path;
548
- if (typeof obj.url === "string" && obj.url)
549
- result.url = obj.url;
550
- if (typeof obj.name === "string" && obj.name)
551
- result.name = obj.name;
552
- if (typeof obj.enabled === "boolean")
553
- result.enabled = obj.enabled;
554
- if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
555
- result.options = obj.options;
556
- }
557
- return result;
558
- });
559
- }
155
+ export { unknownKeyHint };