akm-cli 0.8.0-rc2 → 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.
Files changed (295) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
  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 +93 -3
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2141 -1268
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +20 -12
  12. package/dist/commands/agent-support.js +11 -5
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +129 -517
  15. package/dist/commands/consolidate.js +1533 -144
  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 +5 -3
  19. package/dist/commands/distill.js +906 -100
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +3 -0
  22. package/dist/commands/events.js +3 -0
  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 +260 -5
  28. package/dist/commands/health.js +977 -51
  29. package/dist/commands/help/help-accept.md +6 -3
  30. package/dist/commands/help/help-improve.md +36 -8
  31. package/dist/commands/help/help-proposals.md +7 -4
  32. package/dist/commands/help/help-reject.md +5 -2
  33. package/dist/commands/history.js +51 -16
  34. package/dist/commands/improve-auto-accept.js +97 -0
  35. package/dist/commands/improve-cli.js +236 -0
  36. package/dist/commands/improve-profiles.js +184 -0
  37. package/dist/commands/improve-result-file.js +167 -0
  38. package/dist/commands/improve.js +1725 -332
  39. package/dist/commands/info.js +3 -0
  40. package/dist/commands/init.js +49 -1
  41. package/dist/commands/installed-stashes.js +6 -23
  42. package/dist/commands/knowledge.js +3 -0
  43. package/dist/commands/lint/agent-linter.js +3 -0
  44. package/dist/commands/lint/base-linter.js +199 -5
  45. package/dist/commands/lint/command-linter.js +3 -0
  46. package/dist/commands/lint/default-linter.js +3 -0
  47. package/dist/commands/lint/env-key-rules.js +154 -0
  48. package/dist/commands/lint/index.js +92 -3
  49. package/dist/commands/lint/knowledge-linter.js +3 -0
  50. package/dist/commands/lint/markdown-insertion.js +343 -0
  51. package/dist/commands/lint/memory-linter.js +3 -0
  52. package/dist/commands/lint/registry.js +3 -0
  53. package/dist/commands/lint/skill-linter.js +3 -0
  54. package/dist/commands/lint/task-linter.js +15 -12
  55. package/dist/commands/lint/types.js +3 -0
  56. package/dist/commands/lint/workflow-linter.js +3 -0
  57. package/dist/commands/lint.js +3 -0
  58. package/dist/commands/migration-help.js +5 -2
  59. package/dist/commands/proposal-drain-policies.js +128 -0
  60. package/dist/commands/proposal-drain.js +477 -0
  61. package/dist/commands/proposal.js +60 -6
  62. package/dist/commands/propose.js +24 -19
  63. package/dist/commands/reflect.js +1004 -94
  64. package/dist/commands/registry-cli.js +150 -0
  65. package/dist/commands/registry-search.js +3 -0
  66. package/dist/commands/remember-cli.js +257 -0
  67. package/dist/commands/remember.js +15 -6
  68. package/dist/commands/schema-repair.js +88 -15
  69. package/dist/commands/search.js +99 -14
  70. package/dist/commands/secret.js +173 -0
  71. package/dist/commands/self-update.js +3 -0
  72. package/dist/commands/show.js +32 -13
  73. package/dist/commands/source-add.js +7 -35
  74. package/dist/commands/source-clone.js +3 -0
  75. package/dist/commands/source-manage.js +3 -0
  76. package/dist/commands/tasks.js +161 -95
  77. package/dist/commands/url-checker.js +3 -0
  78. package/dist/core/action-contributors.js +3 -0
  79. package/dist/core/asset-ref.js +13 -2
  80. package/dist/core/asset-registry.js +9 -2
  81. package/dist/core/asset-serialize.js +88 -0
  82. package/dist/core/asset-spec.js +61 -5
  83. package/dist/core/common.js +93 -5
  84. package/dist/core/concurrent.js +3 -0
  85. package/dist/core/config-io.js +347 -0
  86. package/dist/core/config-migration.js +622 -0
  87. package/dist/core/config-schema.js +558 -0
  88. package/dist/core/config-sources.js +108 -0
  89. package/dist/core/config-types.js +4 -0
  90. package/dist/core/config-walker.js +337 -0
  91. package/dist/core/config.js +366 -1077
  92. package/dist/core/errors.js +42 -20
  93. package/dist/core/events.js +31 -25
  94. package/dist/core/file-lock.js +104 -0
  95. package/dist/core/frontmatter.js +75 -10
  96. package/dist/core/lesson-lint.js +3 -0
  97. package/dist/core/markdown.js +3 -0
  98. package/dist/core/memory-belief.js +62 -0
  99. package/dist/core/memory-contradiction-detect.js +274 -0
  100. package/dist/core/memory-improve.js +142 -14
  101. package/dist/core/parse.js +3 -0
  102. package/dist/core/paths.js +218 -50
  103. package/dist/core/proposal-quality-validators.js +380 -0
  104. package/dist/core/proposal-validators.js +11 -3
  105. package/dist/core/proposals.js +464 -5
  106. package/dist/core/state-db.js +349 -56
  107. package/dist/core/text-truncation.js +107 -0
  108. package/dist/core/time.js +3 -0
  109. package/dist/core/tty.js +59 -0
  110. package/dist/core/warn.js +7 -2
  111. package/dist/core/write-source.js +12 -0
  112. package/dist/indexer/db-backup.js +391 -0
  113. package/dist/indexer/db-search.js +136 -28
  114. package/dist/indexer/db.js +661 -166
  115. package/dist/indexer/ensure-index.js +3 -0
  116. package/dist/indexer/file-context.js +3 -0
  117. package/dist/indexer/graph-boost.js +162 -40
  118. package/dist/indexer/graph-db.js +241 -51
  119. package/dist/indexer/graph-dedup.js +3 -7
  120. package/dist/indexer/graph-extraction.js +242 -149
  121. package/dist/indexer/index-context.js +3 -9
  122. package/dist/indexer/indexer.js +84 -14
  123. package/dist/indexer/llm-cache.js +24 -19
  124. package/dist/indexer/manifest.js +3 -0
  125. package/dist/indexer/matchers.js +184 -11
  126. package/dist/indexer/memory-inference.js +94 -50
  127. package/dist/indexer/metadata-contributors.js +3 -0
  128. package/dist/indexer/metadata.js +110 -50
  129. package/dist/indexer/path-resolver.js +3 -0
  130. package/dist/indexer/project-context.js +192 -0
  131. package/dist/indexer/ranking-contributors.js +134 -7
  132. package/dist/indexer/ranking.js +8 -1
  133. package/dist/indexer/search-fields.js +5 -9
  134. package/dist/indexer/search-hit-enrichers.js +91 -2
  135. package/dist/indexer/search-source.js +20 -1
  136. package/dist/indexer/semantic-status.js +4 -1
  137. package/dist/indexer/staleness-detect.js +447 -0
  138. package/dist/indexer/usage-events.js +12 -9
  139. package/dist/indexer/walker.js +3 -0
  140. package/dist/integrations/agent/builders.js +135 -0
  141. package/dist/integrations/agent/config.js +121 -401
  142. package/dist/integrations/agent/detect.js +3 -0
  143. package/dist/integrations/agent/index.js +6 -14
  144. package/dist/integrations/agent/model-aliases.js +55 -0
  145. package/dist/integrations/agent/profiles.js +3 -0
  146. package/dist/integrations/agent/prompts.js +137 -8
  147. package/dist/integrations/agent/runner.js +208 -0
  148. package/dist/integrations/agent/sdk-runner.js +8 -2
  149. package/dist/integrations/agent/spawn.js +54 -14
  150. package/dist/integrations/github.js +3 -0
  151. package/dist/integrations/lockfile.js +22 -51
  152. package/dist/integrations/session-logs/index.js +4 -0
  153. package/dist/integrations/session-logs/inline-refs.js +35 -0
  154. package/dist/integrations/session-logs/pre-filter.js +152 -0
  155. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  156. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  157. package/dist/integrations/session-logs/types.js +3 -0
  158. package/dist/llm/call-ai.js +14 -26
  159. package/dist/llm/client.js +16 -2
  160. package/dist/llm/embedder.js +20 -29
  161. package/dist/llm/embedders/cache.js +3 -7
  162. package/dist/llm/embedders/local.js +42 -1
  163. package/dist/llm/embedders/remote.js +20 -8
  164. package/dist/llm/embedders/types.js +3 -7
  165. package/dist/llm/feature-gate.js +92 -56
  166. package/dist/llm/graph-extract.js +401 -30
  167. package/dist/llm/index-passes.js +44 -29
  168. package/dist/llm/memory-infer.js +30 -2
  169. package/dist/llm/metadata-enhance.js +3 -7
  170. package/dist/llm/prompts/extract-session.md +80 -0
  171. package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
  172. package/dist/output/cli-hints-full.md +60 -32
  173. package/dist/output/cli-hints-short.md +10 -7
  174. package/dist/output/cli-hints.js +5 -2
  175. package/dist/output/context.js +60 -8
  176. package/dist/output/renderers.js +170 -194
  177. package/dist/output/shapes/curate.js +56 -0
  178. package/dist/output/shapes/distill.js +10 -0
  179. package/dist/output/shapes/env-list.js +19 -0
  180. package/dist/output/shapes/events.js +11 -0
  181. package/dist/output/shapes/helpers.js +424 -0
  182. package/dist/output/shapes/history.js +7 -0
  183. package/dist/output/shapes/passthrough.js +105 -0
  184. package/dist/output/shapes/proposal-accept.js +7 -0
  185. package/dist/output/shapes/proposal-diff.js +7 -0
  186. package/dist/output/shapes/proposal-list.js +7 -0
  187. package/dist/output/shapes/proposal-producer.js +11 -0
  188. package/dist/output/shapes/proposal-reject.js +7 -0
  189. package/dist/output/shapes/proposal-show.js +7 -0
  190. package/dist/output/shapes/registry-search.js +6 -0
  191. package/dist/output/shapes/registry.js +30 -0
  192. package/dist/output/shapes/search.js +6 -0
  193. package/dist/output/shapes/secret-list.js +19 -0
  194. package/dist/output/shapes/show.js +6 -0
  195. package/dist/output/shapes/vault-list.js +19 -0
  196. package/dist/output/shapes.js +51 -549
  197. package/dist/output/text/add.js +6 -0
  198. package/dist/output/text/clone.js +6 -0
  199. package/dist/output/text/config.js +6 -0
  200. package/dist/output/text/curate.js +6 -0
  201. package/dist/output/text/distill.js +7 -0
  202. package/dist/output/text/enable-disable.js +7 -0
  203. package/dist/output/text/events.js +10 -0
  204. package/dist/output/text/feedback.js +6 -0
  205. package/dist/output/text/helpers.js +1059 -0
  206. package/dist/output/text/history.js +7 -0
  207. package/dist/output/text/import.js +6 -0
  208. package/dist/output/text/index.js +6 -0
  209. package/dist/output/text/info.js +6 -0
  210. package/dist/output/text/init.js +6 -0
  211. package/dist/output/text/list.js +6 -0
  212. package/dist/output/text/proposal-producer.js +8 -0
  213. package/dist/output/text/proposal.js +12 -0
  214. package/dist/output/text/registry-commands.js +11 -0
  215. package/dist/output/text/registry.js +30 -0
  216. package/dist/output/text/remember.js +6 -0
  217. package/dist/output/text/remove.js +6 -0
  218. package/dist/output/text/save.js +6 -0
  219. package/dist/output/text/search.js +6 -0
  220. package/dist/output/text/show.js +6 -0
  221. package/dist/output/text/update.js +6 -0
  222. package/dist/output/text/upgrade.js +6 -0
  223. package/dist/output/text/vault.js +16 -0
  224. package/dist/output/text/wiki.js +15 -0
  225. package/dist/output/text/workflow.js +14 -0
  226. package/dist/output/text.js +44 -1329
  227. package/dist/registry/build-index.js +3 -0
  228. package/dist/registry/create-provider-registry.js +3 -0
  229. package/dist/registry/factory.js +4 -1
  230. package/dist/registry/origin-resolve.js +3 -0
  231. package/dist/registry/providers/index.js +3 -0
  232. package/dist/registry/providers/skills-sh.js +11 -2
  233. package/dist/registry/providers/static-index.js +10 -1
  234. package/dist/registry/providers/types.js +3 -24
  235. package/dist/registry/resolve.js +11 -16
  236. package/dist/registry/types.js +3 -0
  237. package/dist/scripts/migrate-storage.js +17767 -0
  238. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  239. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  240. package/dist/setup/detect.js +3 -0
  241. package/dist/setup/ripgrep-install.js +3 -0
  242. package/dist/setup/ripgrep-resolve.js +3 -0
  243. package/dist/setup/setup.js +306 -67
  244. package/dist/setup/steps.js +3 -15
  245. package/dist/sources/include.js +3 -0
  246. package/dist/sources/provider-factory.js +3 -11
  247. package/dist/sources/provider.js +3 -20
  248. package/dist/sources/providers/filesystem.js +19 -23
  249. package/dist/sources/providers/git.js +171 -21
  250. package/dist/sources/providers/index.js +3 -0
  251. package/dist/sources/providers/install-types.js +3 -13
  252. package/dist/sources/providers/npm.js +3 -4
  253. package/dist/sources/providers/provider-utils.js +3 -0
  254. package/dist/sources/providers/sync-from-ref.js +3 -11
  255. package/dist/sources/providers/tar-utils.js +3 -0
  256. package/dist/sources/providers/website.js +18 -22
  257. package/dist/sources/resolve.js +3 -0
  258. package/dist/sources/types.js +3 -0
  259. package/dist/sources/website-ingest.js +3 -0
  260. package/dist/tasks/backends/cron.js +3 -0
  261. package/dist/tasks/backends/exec-utils.js +3 -0
  262. package/dist/tasks/backends/index.js +3 -11
  263. package/dist/tasks/backends/launchd.js +3 -0
  264. package/dist/tasks/backends/schtasks.js +3 -0
  265. package/dist/tasks/parser.js +51 -38
  266. package/dist/tasks/resolveAkmBin.js +3 -0
  267. package/dist/tasks/runner.js +35 -9
  268. package/dist/tasks/schedule.js +20 -1
  269. package/dist/tasks/schema.js +5 -3
  270. package/dist/tasks/validator.js +6 -3
  271. package/dist/version.js +3 -0
  272. package/dist/wiki/wiki-templates.js +3 -0
  273. package/dist/wiki/wiki.js +3 -0
  274. package/dist/workflows/authoring.js +3 -0
  275. package/dist/workflows/cli.js +3 -0
  276. package/dist/workflows/db.js +140 -10
  277. package/dist/workflows/document-cache.js +3 -10
  278. package/dist/workflows/parser.js +3 -0
  279. package/dist/workflows/renderer.js +3 -0
  280. package/dist/workflows/runs.js +18 -1
  281. package/dist/workflows/schema.js +3 -0
  282. package/dist/workflows/scope-key.js +3 -0
  283. package/dist/workflows/validator.js +5 -9
  284. package/docs/README.md +7 -2
  285. package/docs/data-and-telemetry.md +225 -0
  286. package/docs/migration/release-notes/0.7.5.md +2 -2
  287. package/docs/migration/release-notes/0.8.0.md +57 -5
  288. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  289. package/package.json +28 -11
  290. package/.github/LICENSE +0 -374
  291. package/dist/commands/install-audit.js +0 -385
  292. package/dist/commands/vault.js +0 -310
  293. package/dist/indexer/match-contributors.js +0 -141
  294. package/dist/integrations/agent/pipeline.js +0 -39
  295. package/dist/integrations/agent/runners.js +0 -31
@@ -1,3 +1,6 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  /**
2
5
  * `akm search` — entry point.
3
6
  *
@@ -9,11 +12,13 @@
9
12
  * Provider `search()` methods do not exist.
10
13
  */
11
14
  import { loadConfig } from "../core/config";
12
- import { UsageError } from "../core/errors";
15
+ import { rethrowIfTestIsolationError, UsageError } from "../core/errors";
13
16
  import { appendEvent } from "../core/events";
17
+ import { isTransientStashPath } from "../core/paths";
14
18
  import { bumpUtilityScoresBatch, closeDatabase, openExistingDatabase } from "../indexer/db";
15
19
  import { searchLocal } from "../indexer/db-search";
16
20
  import { resolveSourceEntries } from "../indexer/search-source";
21
+ import { getCurrentWorkflowScopeKey } from "../workflows/scope-key";
17
22
  // Eagerly import source providers to trigger self-registration before the
18
23
  // indexer or path-resolution code runs.
19
24
  import "../sources/providers/index";
@@ -26,10 +31,42 @@ export async function akmSearch(input) {
26
31
  const normalizedQuery = query.toLowerCase();
27
32
  const searchType = input.type ?? "any";
28
33
  const limit = normalizeLimit(input.limit);
29
- const source = parseSearchSource(input.source ?? "stash");
34
+ const parsedSource = parseSearchSource(input.source ?? "stash");
30
35
  const config = loadConfig();
31
- const sources = resolveSourceEntries(undefined, config);
32
- if (sources.length === 0) {
36
+ // Named-source filter: when --source is not a standard enum value, treat it
37
+ // as a named source from config.sources[].name. Validate early (before
38
+ // resolveSourceEntries, which can throw STASH_DIR_NOT_FOUND) so that a bad
39
+ // --source name always produces INVALID_SOURCE_VALUE regardless of stash state.
40
+ let namedSourceName;
41
+ let source;
42
+ if (parsedSource !== "stash" && parsedSource !== "registry" && parsedSource !== "both") {
43
+ namedSourceName = parsedSource;
44
+ // Check that the named source exists in the config before touching the stash.
45
+ const configSources = config.sources ?? [];
46
+ const foundInConfig = configSources.some((s) => s.name === namedSourceName) || configSources.some((s) => s.path === namedSourceName);
47
+ if (!foundInConfig) {
48
+ const validNames = configSources.map((s) => s.name).filter((n) => Boolean(n));
49
+ const hint = validNames.length > 0
50
+ ? `Known source names: ${validNames.join(", ")}`
51
+ : "No named sources are configured. Run `akm list` to see installed stashes.";
52
+ throw new UsageError(`Unknown source name: "${namedSourceName}". ${hint}`, "INVALID_SOURCE_VALUE");
53
+ }
54
+ source = "stash";
55
+ }
56
+ else {
57
+ source = parsedSource;
58
+ }
59
+ let allSources = resolveSourceEntries(undefined, config);
60
+ // When a named source was requested, narrow the sources list to just that entry.
61
+ // `resolveSourceEntries` sets `registryId` to `entry.name` for each config source.
62
+ if (namedSourceName !== undefined) {
63
+ const ns = namedSourceName;
64
+ allSources = allSources.filter((s) => s.registryId === ns || s.path === ns);
65
+ // allSources may still be empty if the configured source dir doesn't exist on
66
+ // disk (resolveSourceEntries skips non-existent dirs). Fall through to the
67
+ // zero-sources guard below which emits a friendly warning.
68
+ }
69
+ if (allSources.length === 0) {
33
70
  // stashDir: "" is a safe sentinel here — the response carries zero hits
34
71
  // and a warning, so no downstream code will try to use the empty path.
35
72
  const response = {
@@ -40,12 +77,15 @@ export async function akmSearch(input) {
40
77
  warnings: ["No stashes configured. Run `akm init` to create your working stash."],
41
78
  timing: { totalMs: Date.now() - t0 },
42
79
  };
43
- logSearchEvent(query, response);
80
+ if (!input.skipLogging)
81
+ logSearchEvent(query, response, undefined, undefined, input.eventSource);
44
82
  return response;
45
83
  }
46
84
  // Primary stash directory — used for DB path lookups and as the default
47
85
  // stash root. Safe because the empty-sources case is handled above.
48
- const stashDir = sources[0].path;
86
+ const stashDir = allSources[0].path;
87
+ // Expose the filtered source list to downstream search calls.
88
+ const sources = allSources;
49
89
  const filters = normalizeScopeFilters(input.filters);
50
90
  const includeProposed = input.includeProposed === true;
51
91
  const belief = input.belief ?? "all";
@@ -61,6 +101,12 @@ export async function akmSearch(input) {
61
101
  filters,
62
102
  includeProposed,
63
103
  beliefFilter: belief,
104
+ // When `--source <name>` narrowed the source list above, propagate
105
+ // that intent down to the database layer so FTS/vector hits from
106
+ // sources outside the narrowed set are filtered out post-ranking.
107
+ // Without this, the index (which spans every configured source)
108
+ // would leak hits from sources the caller did not request.
109
+ restrictToSources: namedSourceName !== undefined,
64
110
  });
65
111
  const registryResult = source === "stash" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
66
112
  if (source === "stash") {
@@ -75,7 +121,8 @@ export async function akmSearch(input) {
75
121
  warnings: localResult?.warnings?.length ? localResult.warnings : undefined,
76
122
  timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
77
123
  };
78
- logSearchEvent(query, response, undefined, localResult?.mode ?? "keyword");
124
+ if (!input.skipLogging)
125
+ logSearchEvent(query, response, undefined, localResult?.mode ?? "keyword", input.eventSource);
79
126
  return response;
80
127
  }
81
128
  const registryHits = (registryResult?.hits ?? []).map((hit) => {
@@ -109,7 +156,8 @@ export async function akmSearch(input) {
109
156
  warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
110
157
  timing: { totalMs: Date.now() - t0 },
111
158
  };
112
- logSearchEvent(query, response);
159
+ if (!input.skipLogging)
160
+ logSearchEvent(query, response, undefined, undefined, input.eventSource);
113
161
  return response;
114
162
  }
115
163
  // source === "both"
@@ -126,7 +174,8 @@ export async function akmSearch(input) {
126
174
  warnings: warnings.length ? warnings : undefined,
127
175
  timing: { totalMs: Date.now() - t0 },
128
176
  };
129
- logSearchEvent(query, response, undefined, localResult?.mode ?? "keyword");
177
+ if (!input.skipLogging)
178
+ logSearchEvent(query, response, undefined, undefined, input.eventSource);
130
179
  return response;
131
180
  }
132
181
  /**
@@ -162,13 +211,16 @@ function resolveEntryIds(db, hits) {
162
211
  * Per-entry events are recorded only for stash hits because registry hits
163
212
  * have no local entry_id to reference.
164
213
  */
165
- function logSearchEvent(query, response, existingDb, mode = "keyword") {
214
+ function logSearchEvent(query, response, existingDb, mode = "keyword", eventSource = "user") {
166
215
  // Emit a structured event to events.jsonl so workflow-trace consumers
167
216
  // detect akm search invocations without relying on stdout scraping.
168
217
  const stashHits = response.hits.filter((h) => h.type !== "registry");
218
+ // D8: include registry hit refs so a show following a registry-only search generates a select event
219
+ const registryHitRefs = (response.registryHits ?? []).map((h) => `registry:${h.id}`);
220
+ const allResultRefs = [...stashHits.map((h) => h.ref), ...registryHitRefs];
169
221
  appendEvent({
170
222
  eventType: "search",
171
- metadata: { query, hitCount: stashHits.length, resultRefs: stashHits.map((h) => h.ref), mode },
223
+ metadata: { query, hitCount: stashHits.length, resultRefs: allResultRefs, mode },
172
224
  });
173
225
  try {
174
226
  const db = existingDb ?? openExistingDatabase();
@@ -180,13 +232,23 @@ function logSearchEvent(query, response, existingDb, mode = "keyword") {
180
232
  query,
181
233
  entry_id: entryId,
182
234
  entry_ref: ref,
235
+ source: eventSource,
183
236
  });
184
237
  }
185
238
  // Bump utility scores for all resolved entries (MemRL retrieval signal).
186
239
  // The indexer overwrites these at next reindex; bumps are temporary hints.
187
240
  const resolvedIds = resolved.map((r) => r.entryId).filter((id) => id !== undefined);
188
241
  if (resolvedIds.length > 0) {
189
- bumpUtilityScoresBatch(db, resolvedIds, 1.0);
242
+ let scopeKey;
243
+ try {
244
+ const stashPath = response.stashDir;
245
+ const disabled = process.env.AKM_DISABLE_SCOPED_UTILITY === "1" || (stashPath && isTransientStashPath(stashPath));
246
+ scopeKey = disabled ? undefined : getCurrentWorkflowScopeKey();
247
+ }
248
+ catch {
249
+ // Non-fatal — fall back to global-only bumps on any error.
250
+ }
251
+ bumpUtilityScoresBatch(db, resolvedIds, 1.0, 0.1, scopeKey);
190
252
  }
191
253
  // Count registry hits separately so registry-only searches record a
192
254
  // non-zero resultCount. response.hits is always [] when source="registry".
@@ -202,6 +264,7 @@ function logSearchEvent(query, response, existingDb, mode = "keyword") {
202
264
  resolvedCount: resolved.length,
203
265
  mode,
204
266
  }),
267
+ source: eventSource,
205
268
  });
206
269
  }
207
270
  finally {
@@ -209,7 +272,8 @@ function logSearchEvent(query, response, existingDb, mode = "keyword") {
209
272
  closeDatabase(db);
210
273
  }
211
274
  }
212
- catch {
275
+ catch (err) {
276
+ rethrowIfTestIsolationError(err);
213
277
  /* fire-and-forget */
214
278
  }
215
279
  }
@@ -220,6 +284,24 @@ function normalizeLimit(limit) {
220
284
  }
221
285
  return Math.min(Math.floor(limit), 200);
222
286
  }
287
+ /**
288
+ * Parse the `--source` flag value.
289
+ *
290
+ * Accepts:
291
+ * - `stash` (default) — search the local stash index only
292
+ * - `registry` — search remote registries only
293
+ * - `both` — search stash and registries
294
+ * - `local` — alias for `stash`
295
+ * - Any named source from `config.sources[].name` — filters stash results to
296
+ * that single source only. The named-source path is detected and resolved
297
+ * inside `akmSearch`; this function returns the raw name so the caller can
298
+ * pass it through to `akmSearch` which accepts `SearchSource | string`.
299
+ *
300
+ * Unknown values that are not a known enum AND not a named source will still
301
+ * produce an error inside `akmSearch` when the config lookup finds nothing.
302
+ * This allows the CLI to accept named sources without requiring config access
303
+ * at parse time.
304
+ */
223
305
  export function parseSearchSource(source) {
224
306
  if (source === "stash" || source === "registry" || source === "both")
225
307
  return source;
@@ -228,7 +310,10 @@ export function parseSearchSource(source) {
228
310
  return "stash";
229
311
  if (typeof source === "undefined")
230
312
  return "stash";
231
- throw new UsageError(`Invalid value for --source: ${String(source)}. Expected one of: stash|registry|both`, "INVALID_SOURCE_VALUE");
313
+ // Pass through unknown strings they may be valid named sources.
314
+ // `akmSearch` will validate against config.sources and throw a UsageError
315
+ // with a helpful message if the name isn't found.
316
+ return source;
232
317
  }
233
318
  export function parseBeliefFilterMode(value) {
234
319
  if (value === undefined || value === "all")
@@ -0,0 +1,173 @@
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
+ * Secret asset type — whole-file secret storage.
6
+ *
7
+ * A `secret` holds a single SENSITIVE value used on its own for authentication
8
+ * (a PEM private key, an API token, a TLS cert, a service-account JSON): the
9
+ * ENTIRE file is the secret. There is no safe region to parse, so only the
10
+ * filename is ever surfaced. Where an `env` file holds a GROUP of related
11
+ * configuration and exposes key NAMES as metadata, a secret is ONE value and
12
+ * exposes nothing but its name — reach for `secret` when one value *is* the
13
+ * credential, and for `env` when loading a service's related configuration.
14
+ *
15
+ * Invariant: a secret's bytes must never be written to stdout, returned
16
+ * through the indexer / `akm show` renderer, or any structured output channel.
17
+ * The supported value-use paths are:
18
+ *
19
+ * - `akm secret run <ref> <VAR> -- <cmd>` — value injected into the child
20
+ * process env as `VAR=<value>` (see `readValue`).
21
+ * - `akm secret path <ref>` — print the file path so a command can read it
22
+ * itself (Docker `/run/secrets` + `_FILE` convention).
23
+ *
24
+ * Values are stored as raw bytes (no quoting, multi-line allowed) so they
25
+ * round-trip byte-exact, unlike env values which forbid literal newlines.
26
+ */
27
+ import crypto from "node:crypto";
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock";
31
+ // ── Write-lock helper ─────────────────────────────────────────────────────────
32
+ /**
33
+ * Acquire an exclusive lock for the given secret path, run `fn`, then release.
34
+ * Mirrors the env write-lock: O_EXCL creation, 5s deadline, PID-based stale
35
+ * detection. A timeout is always a stale lock or a programming error, so we
36
+ * throw rather than silently proceeding.
37
+ */
38
+ export function withSecretLock(secretPath, fn) {
39
+ const lockPath = `${secretPath}.lock`;
40
+ const deadline = Date.now() + 5000;
41
+ while (!tryAcquireLockSync(lockPath, String(process.pid))) {
42
+ const probe = probeLock(lockPath);
43
+ if (probe.state === "stale") {
44
+ releaseLock(lockPath);
45
+ continue;
46
+ }
47
+ if (Date.now() > deadline) {
48
+ const holderHint = probe.state === "held"
49
+ ? ` Lock file ${lockPath} is held by live PID ${probe.holderPid}.`
50
+ : ` Lock file ${lockPath} could not be inspected.`;
51
+ throw new Error(`Could not acquire secret lock for ${secretPath} after 5s.${holderHint} Retry once any other akm secret operation finishes, or remove the stale lock file.`);
52
+ }
53
+ if (typeof globalThis.Bun?.sleepSync ===
54
+ "function") {
55
+ globalThis.Bun.sleepSync(10);
56
+ }
57
+ else {
58
+ let spin = 0;
59
+ while (spin++ < 100_000) {
60
+ /* yield */
61
+ }
62
+ }
63
+ }
64
+ try {
65
+ return fn();
66
+ }
67
+ finally {
68
+ releaseLock(lockPath);
69
+ }
70
+ }
71
+ // ── Atomic byte write ──────────────────────────────────────────────────────────
72
+ /**
73
+ * Atomically write `data` to `target` at mode 0600. Unlike `writeFileAtomic`
74
+ * in core/common (string content), this accepts a Buffer so secret bytes
75
+ * round-trip exactly — binary certs and CRLF/LF line endings are preserved.
76
+ */
77
+ function writeSecretAtomic(target, data) {
78
+ const tmp = `${target}.tmp.${process.pid}.${crypto.randomBytes(8).toString("hex")}`;
79
+ const fd = fs.openSync(tmp, "w", 0o600);
80
+ try {
81
+ fs.writeSync(fd, data);
82
+ try {
83
+ fs.fdatasyncSync(fd);
84
+ }
85
+ catch {
86
+ // Best-effort durability; some pseudo-filesystems lack fdatasync.
87
+ }
88
+ }
89
+ finally {
90
+ fs.closeSync(fd);
91
+ }
92
+ fs.renameSync(tmp, target);
93
+ try {
94
+ const dirFd = fs.openSync(path.dirname(target), "r");
95
+ try {
96
+ fs.fsyncSync(dirFd);
97
+ }
98
+ finally {
99
+ fs.closeSync(dirFd);
100
+ }
101
+ }
102
+ catch {
103
+ // Directory fsync is unsupported on FAT / some FUSE mounts / Windows.
104
+ }
105
+ }
106
+ function ensureParentDir(filePath) {
107
+ const dir = path.dirname(filePath);
108
+ if (!fs.existsSync(dir))
109
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
110
+ }
111
+ // ── Public API ──────────────────────────────────────────────────────────────
112
+ /**
113
+ * Walk a `secrets/` directory and return the POSIX-relative names of every
114
+ * secret file. Lock files (`*.lock`), sensitive markers (`*.sensitive`), and
115
+ * secrets with a sibling `<name>.sensitive` marker are excluded. The file
116
+ * bodies are NEVER read.
117
+ */
118
+ export function listNames(secretsRoot) {
119
+ if (!fs.existsSync(secretsRoot))
120
+ return [];
121
+ const names = [];
122
+ const walk = (dir) => {
123
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
124
+ const full = path.join(dir, entry.name);
125
+ if (entry.isDirectory()) {
126
+ walk(full);
127
+ continue;
128
+ }
129
+ if (!entry.isFile())
130
+ continue;
131
+ if (entry.name.endsWith(".lock") || entry.name.endsWith(".sensitive"))
132
+ continue;
133
+ // A sibling `<name>.sensitive` marker suppresses listing.
134
+ if (fs.existsSync(`${full}.sensitive`))
135
+ continue;
136
+ names.push(path.relative(secretsRoot, full).split(path.sep).join("/"));
137
+ }
138
+ };
139
+ walk(secretsRoot);
140
+ return names.sort();
141
+ }
142
+ /**
143
+ * Read a secret's raw bytes. Internal use only (for `secret run` / `secret
144
+ * path`). Callers MUST NOT write the returned value to stdout or any log.
145
+ */
146
+ export function readValue(secretPath) {
147
+ return fs.readFileSync(secretPath);
148
+ }
149
+ /**
150
+ * Write (create or overwrite) a secret with the given raw bytes, atomically at
151
+ * mode 0600 under a write-lock. No quoting; multi-line / binary allowed.
152
+ */
153
+ export function setSecret(secretPath, value) {
154
+ ensureParentDir(secretPath);
155
+ withSecretLock(secretPath, () => {
156
+ writeSecretAtomic(secretPath, value);
157
+ });
158
+ }
159
+ /**
160
+ * Remove a secret file (and its `.sensitive` marker, if present). Returns true
161
+ * if the secret existed.
162
+ */
163
+ export function removeSecret(secretPath) {
164
+ return withSecretLock(secretPath, () => {
165
+ if (!fs.existsSync(secretPath))
166
+ return false;
167
+ fs.rmSync(secretPath);
168
+ const marker = `${secretPath}.sensitive`;
169
+ if (fs.existsSync(marker))
170
+ fs.rmSync(marker);
171
+ return true;
172
+ });
173
+ }
@@ -1,3 +1,6 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  import * as childProcess from "node:child_process";
2
5
  import { createHash } from "node:crypto";
3
6
  import fs from "node:fs";
@@ -1,3 +1,6 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  /**
2
5
  * `akm show` — entry point.
3
6
  *
@@ -16,10 +19,11 @@
16
19
  import fs from "node:fs";
17
20
  import path from "node:path";
18
21
  import { parseAssetRef } from "../core/asset-ref";
22
+ import { asNonEmptyString } from "../core/common";
19
23
  import { loadConfig } from "../core/config";
20
- import { NotFoundError, UsageError } from "../core/errors";
24
+ import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../core/errors";
21
25
  import { appendEvent, readEvents } from "../core/events";
22
- import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
26
+ import { parseFrontmatter } from "../core/frontmatter";
23
27
  import { closeDatabase, findEntryIdByRef, openExistingDatabase } from "../indexer/db";
24
28
  import { ensureIndex } from "../indexer/ensure-index";
25
29
  import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../indexer/file-context";
@@ -141,7 +145,7 @@ export async function akmShowUnified(input) {
141
145
  }
142
146
  // Count prior shows of this ref before logging the current one.
143
147
  const priorShowCount = recentShowCount(ref);
144
- logShowEvent(ref);
148
+ logShowEvent(ref, undefined, input.eventSource);
145
149
  if (priorShowCount >= 2) {
146
150
  // Agent has shown this same asset 3+ times — inject a loop-break hint.
147
151
  result.showLoopWarning = priorShowCount + 1;
@@ -179,7 +183,7 @@ function enforceScopeOrThrow(filePath, ref, scope) {
179
183
  for (const [key, expectedValue] of expected) {
180
184
  if (expectedValue === undefined)
181
185
  continue;
182
- const actual = toStringOrUndefined(fm[`scope_${key}`]);
186
+ const actual = asNonEmptyString(fm[`scope_${key}`]);
183
187
  if (actual !== expectedValue) {
184
188
  throw new NotFoundError(`Asset "${ref}" exists but is out of scope (expected scope_${key}="${expectedValue}").`);
185
189
  }
@@ -191,21 +195,29 @@ function enforceScopeOrThrow(filePath, ref, scope) {
191
195
  */
192
196
  function recentShowCount(ref) {
193
197
  try {
194
- const { events } = readEvents({ type: "show", ref });
198
+ const { events } = readEvents({
199
+ type: "show",
200
+ ref,
201
+ since: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
202
+ });
195
203
  return events.length;
196
204
  }
197
205
  catch {
198
206
  return 0;
199
207
  }
200
208
  }
201
- function logShowEvent(ref, existingDb) {
209
+ function logShowEvent(ref, existingDb, eventSource = "user") {
202
210
  // Emit a structured event to events.jsonl so workflow-trace consumers
203
211
  // detect akm show invocations without relying on stdout scraping.
204
212
  const parsed = parseAssetRef(ref);
205
213
  appendEvent({ eventType: "show", ref, metadata: { type: parsed.type, name: parsed.name } });
206
214
  // Detect if this show is a selection from a recent search result.
207
215
  try {
208
- const { events: recentSearches } = readEvents({ type: "search" });
216
+ // D7: bound the query to the last 60 s so we never scan unbounded history
217
+ const { events: recentSearches } = readEvents({
218
+ type: "search",
219
+ since: new Date(Date.now() - 60_000).toISOString(),
220
+ });
209
221
  const cutoffMs = Date.now() - 60_000;
210
222
  const matchingSearch = [...recentSearches].reverse().find((e) => {
211
223
  if (!e.ts || new Date(e.ts).getTime() < cutoffMs)
@@ -235,6 +247,7 @@ function logShowEvent(ref, existingDb) {
235
247
  event_type: "show",
236
248
  entry_ref: ref,
237
249
  entry_id: findEntryIdByRef(db, ref),
250
+ source: eventSource,
238
251
  });
239
252
  }
240
253
  finally {
@@ -242,7 +255,8 @@ function logShowEvent(ref, existingDb) {
242
255
  closeDatabase(db);
243
256
  }
244
257
  }
245
- catch {
258
+ catch (err) {
259
+ rethrowIfTestIsolationError(err);
246
260
  /* fire-and-forget */
247
261
  }
248
262
  }
@@ -316,7 +330,8 @@ export async function showLocal(input) {
316
330
  const related = listRelatedPathsForFile(sourceStashDir, assetPath, 5, db);
317
331
  return { total: related.length, hits: related };
318
332
  }
319
- catch {
333
+ catch (err) {
334
+ rethrowIfTestIsolationError(err);
320
335
  return { total: 0, hits: [] };
321
336
  }
322
337
  finally {
@@ -385,7 +400,7 @@ function buildSummaryResponse(full, assetPath) {
385
400
  const textContent = full.content ?? full.template ?? full.prompt;
386
401
  if (textContent && !description) {
387
402
  const parsed = parseFrontmatter(textContent);
388
- description = toStringOrUndefined(parsed.data.description);
403
+ description = asNonEmptyString(parsed.data.description);
389
404
  }
390
405
  }
391
406
  const summary = {
@@ -432,15 +447,19 @@ export function normalizeShowArgv(argv) {
432
447
  const showArgs = [];
433
448
  for (let i = 0; i < rest.length; i++) {
434
449
  const arg = rest[i];
435
- if (arg === "--quiet" || arg === "-q" || arg === "--for-agent" || arg === "--for-agent=true") {
450
+ if (arg === "--quiet" ||
451
+ arg === "-q" ||
452
+ arg === "--verbose" ||
453
+ arg === "--for-agent" ||
454
+ arg === "--for-agent=true") {
436
455
  globalFlags.push(arg);
437
456
  continue;
438
457
  }
439
- if (arg.startsWith("--format=") || arg.startsWith("--detail=")) {
458
+ if (arg.startsWith("--format=") || arg.startsWith("--detail=") || arg.startsWith("--shape=")) {
440
459
  globalFlags.push(arg);
441
460
  continue;
442
461
  }
443
- if (arg === "--format" || arg === "--detail") {
462
+ if (arg === "--format" || arg === "--detail" || arg === "--shape") {
444
463
  globalFlags.push(arg);
445
464
  if (rest[i + 1] !== undefined) {
446
465
  globalFlags.push(rest[i + 1]);
@@ -1,9 +1,11 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  import fs from "node:fs";
2
5
  import path from "node:path";
3
6
  import { isHttpUrl, resolveStashDir } from "../core/common";
4
7
  import { getSources, loadConfig, loadUserConfig, saveConfig } from "../core/config";
5
8
  import { ConfigError, UsageError } from "../core/errors";
6
- import { warn } from "../core/warn";
7
9
  import { akmIndex } from "../indexer/indexer";
8
10
  import { upsertLockEntry } from "../integrations/lockfile";
9
11
  import { parseRegistryRef } from "../registry/resolve";
@@ -11,7 +13,6 @@ import { detectStashRoot } from "../sources/providers/provider-utils";
11
13
  import { syncFromRef } from "../sources/providers/sync-from-ref";
12
14
  import { ensureWebsiteMirror, validateWebsiteInputUrl } from "../sources/website-ingest";
13
15
  import { ensureWikiNameAvailable, validateWikiName } from "../wiki/wiki";
14
- import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
15
16
  const VALID_OVERRIDE_TYPES = new Set(["wiki"]);
16
17
  export async function akmAdd(input) {
17
18
  const ref = input.ref.trim();
@@ -38,16 +39,13 @@ export async function akmAdd(input) {
38
39
  try {
39
40
  const parsed = parseRegistryRef(ref);
40
41
  if (parsed.source === "local") {
41
- if (input.trustThisInstall) {
42
- warn("--trust has no effect on local directory sources; the install audit is not run for local paths.");
43
- }
44
42
  return addLocalSource(ref, parsed.sourcePath, stashDir, wikiName, input.name);
45
43
  }
46
44
  }
47
45
  catch {
48
46
  // Not a local ref — fall through to registry install
49
47
  }
50
- return addRegistryStash(ref, stashDir, input.trustThisInstall, input.writable, wikiName);
48
+ return addRegistryStash(ref, stashDir, input.writable, wikiName);
51
49
  }
52
50
  export async function registerWikiSource(input) {
53
51
  const stashDir = resolveStashDir();
@@ -59,7 +57,6 @@ export async function registerWikiSource(input) {
59
57
  name,
60
58
  overrideType: "wiki",
61
59
  options: input.options,
62
- trustThisInstall: input.trustThisInstall,
63
60
  writable: input.writable,
64
61
  });
65
62
  }
@@ -186,38 +183,14 @@ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
186
183
  }
187
184
  /**
188
185
  * Install a stash from a registry (npm, github, git) by dispatching to the
189
- * matching syncable provider, then running the post-sync install audit and
190
- * persisting the lock entry.
186
+ * matching syncable provider and persisting the lock entry.
191
187
  */
192
- async function addRegistryStash(ref, stashDir, trustThisInstall, writable, wikiName) {
188
+ async function addRegistryStash(ref, stashDir, writable, wikiName) {
193
189
  const parsedRef = parseRegistryRef(ref);
194
190
  if (writable === true && parsedRef.source !== "git") {
195
191
  throw new ConfigError("writable: true is only supported on filesystem and git sources", "INVALID_CONFIG_FILE");
196
192
  }
197
- // Pre-sync registry-policy enforcement uses just the parsed ref (no fetch needed),
198
- // so we keep parity with the historical behavior where `enforceRegistryInstallPolicy`
199
- // ran before `extractTarGzSecure` etc.
200
- const config = loadConfig();
201
- const synced = await syncFromRef(ref, { trustThisInstall, writable });
202
- const registryLabels = deriveRegistryLabels({
203
- source: synced.source,
204
- ref: synced.ref,
205
- artifactUrl: synced.artifactUrl,
206
- });
207
- enforceRegistryInstallPolicy(registryLabels, config, ref);
208
- // Post-sync hook: install audit. Throws when blocked unless `--trust` is set
209
- // (in which case the audit report still surfaces in the response).
210
- const audit = auditInstallCandidate({
211
- rootDir: synced.extractedDir,
212
- source: synced.source,
213
- ref: synced.ref,
214
- registryLabels,
215
- config,
216
- trustThisInstall,
217
- });
218
- if (audit.blocked) {
219
- throw new Error(formatInstallAuditFailure(synced.ref, audit));
220
- }
193
+ const synced = await syncFromRef(ref, { writable });
221
194
  const replaced = (loadConfig().installed ?? []).find((entry) => entry.id === synced.id);
222
195
  const updatedConfig = upsertInstalledRegistryEntry({
223
196
  id: synced.id,
@@ -265,7 +238,6 @@ async function addRegistryStash(ref, stashDir, trustThisInstall, writable, wikiN
265
238
  cacheDir: synced.cacheDir,
266
239
  extractedDir: synced.extractedDir,
267
240
  installedAt: synced.syncedAt,
268
- audit,
269
241
  },
270
242
  config: {
271
243
  sourceCount: getSources(updatedConfig).length,
@@ -1,3 +1,6 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  import fs from "node:fs";
2
5
  import path from "node:path";
3
6
  import { makeAssetRef, parseAssetRef } from "../core/asset-ref";
@@ -1,3 +1,6 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
1
4
  import path from "node:path";
2
5
  import { isRemoteUrl } from "../core/common";
3
6
  import { getSources, loadConfig, loadUserConfig, saveConfig } from "../core/config";