akm-cli 0.8.0-rc1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +2162 -1258
  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 +233 -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 +17 -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 +662 -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 +114 -48
  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 -307
  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
@@ -0,0 +1,192 @@
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
+ * Project-context resolution for search ranking.
6
+ *
7
+ * Extracts meaningful identifier tokens from the current working directory
8
+ * so the ranking pipeline can boost assets that are relevant to the active
9
+ * project. Token extraction tries, in order:
10
+ *
11
+ * 1. `.git/config` remote-origin URL basename (strips `.git` extension)
12
+ * 2. `package.json` `name` field (last `/`-separated segment, minus common
13
+ * framework suffixes)
14
+ * 3. Basename of the directory returned by `resolveWorkflowScopeAnchor`
15
+ * 4. `null` — no meaningful project context (home dir, /tmp, etc.)
16
+ *
17
+ * Tokens are lowercased, split on `[-_/]`, then filtered through a noise-word
18
+ * blocklist. A maximum of 5 tokens is kept.
19
+ */
20
+ import fs from "node:fs";
21
+ import os from "node:os";
22
+ import path from "node:path";
23
+ import { resolveWorkflowScopeAnchor } from "../workflows/scope-key.js";
24
+ // Words that appear in almost every project name and carry no discriminating
25
+ // signal for ranking. Filtered out after token splitting.
26
+ const TOKEN_BLOCKLIST = new Set([
27
+ "my",
28
+ "the",
29
+ "app",
30
+ "lib",
31
+ "sdk",
32
+ "api",
33
+ "cli",
34
+ "tool",
35
+ "kit",
36
+ "core",
37
+ "main",
38
+ "index",
39
+ "src",
40
+ "test",
41
+ ]);
42
+ // Common suffixes stripped from package.json `name` before tokenisation so
43
+ // that e.g. `akm-cli` contributes `akm` rather than `akm` + `cli` (which is
44
+ // in the blocklist anyway, but explicit stripping keeps the raw name shorter).
45
+ const STRIP_SUFFIXES = ["-cli", "-app", "-lib", "-sdk", "-plugin"];
46
+ const MAX_TOKENS = 5;
47
+ // Paths that are definitely NOT a meaningful project root (home dir, tmp).
48
+ // When `resolveWorkflowScopeAnchor` returns one of these we return `null`.
49
+ function isNoiseRoot(dir) {
50
+ const homedir = os.homedir();
51
+ const normalized = dir.replace(/\/+$/, "");
52
+ if (normalized === homedir || normalized === homedir.replace(/\/+$/, ""))
53
+ return true;
54
+ // /tmp or /tmp/… (any depth)
55
+ if (normalized === "/tmp" || normalized.startsWith("/tmp/"))
56
+ return true;
57
+ // Windows-style temp
58
+ const tmpdir = os.tmpdir();
59
+ if (normalized === tmpdir || normalized.startsWith(tmpdir + path.sep))
60
+ return true;
61
+ return false;
62
+ }
63
+ /**
64
+ * Resolve the project context for the given working directory.
65
+ *
66
+ * @param cwd - Directory to inspect. Defaults to `process.cwd()`.
67
+ * @param fsOverride - Optional FS override for unit testing.
68
+ * @returns `ProjectContext` with a non-empty `tokens` set, or `null` when no
69
+ * meaningful context can be derived (home dir, /tmp, extraction failed).
70
+ */
71
+ export function resolveProjectContext(cwd, fsOverride) {
72
+ const effectiveCwd = cwd ?? process.cwd();
73
+ const readFile = fsOverride?.readFileSync ?? ((p, enc) => fs.readFileSync(p, enc));
74
+ // Attempt 1 — git remote-origin URL
75
+ const gitConfigPath = path.join(effectiveCwd, ".git", "config");
76
+ const gitTokens = tryExtractGitTokens(gitConfigPath, readFile);
77
+ if (gitTokens !== null && gitTokens.size > 0) {
78
+ return { tokens: gitTokens };
79
+ }
80
+ // Attempt 2 — package.json name
81
+ const pkgJsonPath = path.join(effectiveCwd, "package.json");
82
+ const pkgTokens = tryExtractPackageJsonTokens(pkgJsonPath, readFile);
83
+ if (pkgTokens !== null && pkgTokens.size > 0) {
84
+ return { tokens: pkgTokens };
85
+ }
86
+ // Attempt 3 — workflow scope anchor basename
87
+ try {
88
+ const anchor = resolveWorkflowScopeAnchor(effectiveCwd);
89
+ if (isNoiseRoot(anchor))
90
+ return null;
91
+ const baseName = path.basename(anchor);
92
+ const tokens = tokenize(baseName);
93
+ if (tokens.size > 0) {
94
+ return { tokens };
95
+ }
96
+ }
97
+ catch {
98
+ // Ignore errors from scope anchor resolution (e.g. during testing).
99
+ }
100
+ return null;
101
+ }
102
+ // ── Private helpers ──────────────────────────────────────────────────────────
103
+ /**
104
+ * Parse `.git/config` for the `[remote "origin"]` section and extract the
105
+ * repo name from the `url =` line.
106
+ *
107
+ * url = git@github.com:itlackey/akm.git → "akm"
108
+ * url = https://github.com/itlackey/akm → "akm"
109
+ */
110
+ function tryExtractGitTokens(gitConfigPath, readFile) {
111
+ try {
112
+ const content = readFile(gitConfigPath, "utf-8");
113
+ const urlMatch = extractRemoteOriginUrl(content);
114
+ if (!urlMatch)
115
+ return null;
116
+ // Strip .git extension, then take the last path segment.
117
+ const withoutGit = urlMatch.replace(/\.git$/, "");
118
+ const segments = withoutGit.replace(/\/$/, "").split(/[/:]/).filter(Boolean);
119
+ const repoName = segments[segments.length - 1] ?? "";
120
+ const tokens = tokenize(repoName);
121
+ return tokens.size > 0 ? tokens : null;
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ /**
128
+ * Extracts the `url =` value from the `[remote "origin"]` section of a git
129
+ * config file. Returns `null` when the section or key is absent.
130
+ */
131
+ function extractRemoteOriginUrl(content) {
132
+ // Split into sections delimited by `[...]` headers.
133
+ const lines = content.split(/\r?\n/);
134
+ let inOrigin = false;
135
+ for (const line of lines) {
136
+ const trimmed = line.trim();
137
+ if (trimmed.startsWith("[")) {
138
+ // New section header
139
+ inOrigin = /^\[remote\s+"origin"\]$/i.test(trimmed);
140
+ continue;
141
+ }
142
+ if (inOrigin) {
143
+ const m = trimmed.match(/^url\s*=\s*(.+)$/i);
144
+ if (m)
145
+ return m[1].trim();
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ /**
151
+ * Read `package.json` and extract the `name` field as tokens.
152
+ */
153
+ function tryExtractPackageJsonTokens(pkgPath, readFile) {
154
+ try {
155
+ const content = readFile(pkgPath, "utf-8");
156
+ const parsed = JSON.parse(content);
157
+ if (typeof parsed.name !== "string" || !parsed.name)
158
+ return null;
159
+ // For scoped packages (@org/pkg-name) take the last `/`-separated segment.
160
+ const rawName = parsed.name.split("/").pop() ?? parsed.name;
161
+ // Strip common framework suffixes before tokenising.
162
+ let strippedName = rawName;
163
+ for (const suffix of STRIP_SUFFIXES) {
164
+ if (strippedName.endsWith(suffix)) {
165
+ strippedName = strippedName.slice(0, -suffix.length);
166
+ break; // only strip one suffix
167
+ }
168
+ }
169
+ const tokens = tokenize(strippedName);
170
+ return tokens.size > 0 ? tokens : null;
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+ /**
177
+ * Split a raw name string into lowercase tokens, then filter through the
178
+ * blocklist and cap at `MAX_TOKENS`.
179
+ *
180
+ * "akm-cli" → Set { "akm" }
181
+ * "my-app" → Set {} (all tokens blocked)
182
+ * "openpalm" → Set { "openpalm" }
183
+ */
184
+ function tokenize(raw) {
185
+ const parts = raw
186
+ .toLowerCase()
187
+ .split(/[-_/]+/)
188
+ .filter(Boolean)
189
+ .filter((t) => !TOKEN_BLOCKLIST.has(t))
190
+ .slice(0, MAX_TOKENS);
191
+ return new Set(parts);
192
+ }
@@ -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 { computeGraphBoost } from "./graph-boost";
2
5
  const TYPE_BOOST = {
3
6
  skill: 0.4,
@@ -11,17 +14,36 @@ const TYPE_BOOST = {
11
14
  const MAX_BOOST_SUM = 3.0;
12
15
  const UTILITY_WEIGHT = 0.5;
13
16
  const UTILITY_MAX_BOOST = 1.5;
14
- const RECENCY_DECAY_DAYS = 30;
17
+ /**
18
+ * Phase 2A / Rec 5: default recency half-life (days) used when no
19
+ * `utilityDecayConfig` is supplied to the ranking pipeline. Matches the
20
+ * pre-2A hardcoded `RECENCY_DECAY_DAYS = 30` constant — the formula is
21
+ * default-safe and collapses to `exp(-days / 30)` when no overrides apply.
22
+ */
23
+ const DEFAULT_RECENCY_HALF_LIFE_DAYS = 30;
24
+ /**
25
+ * Cap on the effective half-life after applying the feedback stability
26
+ * boost — prevents indefinite half-life inflation for memories with many
27
+ * positive feedback events. `effectiveHalfLife = min(halfLife * boost^count, halfLife * 4)`.
28
+ */
29
+ const FEEDBACK_HALF_LIFE_CAP_MULTIPLIER = 4;
15
30
  function beliefStateBoost(item) {
16
31
  const entry = item.entry;
17
32
  if (entry.type !== "memory")
18
33
  return 0;
34
+ // Phase 1A: `asserted` and `deprecated` are first-class states.
35
+ // `asserted` carries stronger user-explicit authority than `active`.
36
+ // `deprecated` is a frozen historical state — penalized but milder than `superseded`.
19
37
  if (entry.beliefState === "contradicted")
20
38
  return -0.45;
21
39
  if (entry.beliefState === "superseded")
22
40
  return -0.25;
23
41
  if (entry.beliefState === "archived")
24
42
  return -0.6;
43
+ if (entry.beliefState === "deprecated")
44
+ return -0.15;
45
+ if (entry.beliefState === "asserted")
46
+ return 0.08;
25
47
  if (entry.beliefState === "active")
26
48
  return 0.06;
27
49
  return 0;
@@ -151,29 +173,131 @@ const graphRankingContributor = {
151
173
  return ctx.graphContext ? computeGraphBoost(ctx.graphContext, item.filePath) : 0;
152
174
  },
153
175
  };
176
+ /**
177
+ * Capture-mode boost — Phase 1B / Rec 7.
178
+ *
179
+ * Memories captured via the hot path (`akm remember`) get a modest additive
180
+ * boost so they outrank otherwise-equal background-derived memories. Memories
181
+ * without `captureMode` (legacy) return 0 and rank exactly as before.
182
+ */
183
+ const captureModeRankingContributor = {
184
+ name: "capture-mode-ranking",
185
+ appliesTo(item) {
186
+ return item.entry.type === "memory" && item.entry.captureMode === "hot";
187
+ },
188
+ adjust() {
189
+ return 0.2;
190
+ },
191
+ };
192
+ /**
193
+ * Lesson strength boost — Phase 7A / Advantage D4b.
194
+ *
195
+ * Each ref that has credited a lesson via `akm feedback --applied-to` adds
196
+ * 0.06 to the boost (capped at 0.3 ≈ five credits). Lessons without a
197
+ * `lessonStrength` array (or a number) return 0.
198
+ */
199
+ const lessonStrengthContributor = {
200
+ name: "lesson-strength-ranking",
201
+ appliesTo(item) {
202
+ return (item.entry.type === "lesson" && typeof item.entry.lessonStrength === "number" && item.entry.lessonStrength > 0);
203
+ },
204
+ adjust(item) {
205
+ const strength = item.entry.lessonStrength ?? 0;
206
+ return Math.min(0.3, 0.06 * strength);
207
+ },
208
+ };
209
+ /**
210
+ * Blend ratio for scoped vs. global utility signals.
211
+ *
212
+ * When a scoped row exists: `effectiveUtility = scoped * 0.7 + global * 0.3`
213
+ * This ensures the in-project signal strongly dominates while the global
214
+ * cold-start signal still helps when scoped history is sparse.
215
+ */
216
+ const SCOPED_UTILITY_BLEND_SCOPED = 0.7;
217
+ const SCOPED_UTILITY_BLEND_GLOBAL = 1 - SCOPED_UTILITY_BLEND_SCOPED;
154
218
  const utilityRankingContributor = {
155
219
  name: "utility-ranking",
156
220
  appliesTo(item, ctx) {
157
221
  const utilScore = ctx.utilityScores.get(item.id);
158
- return Boolean(utilScore && utilScore.utility > 0);
222
+ const scopedScore = ctx.scopedUtilityScores?.get(item.id);
223
+ return Boolean((utilScore && utilScore.utility > 0) || (scopedScore && scopedScore.utility > 0));
159
224
  },
160
225
  apply(item, ctx) {
161
226
  const utilScore = ctx.utilityScores.get(item.id);
162
- if (!utilScore || utilScore.utility <= 0)
227
+ const scopedScore = ctx.scopedUtilityScores?.get(item.id);
228
+ // Determine effective utility: prefer scoped when present, blend with global.
229
+ const globalUtility = utilScore?.utility ?? 0;
230
+ const scopedUtility = scopedScore?.utility ?? 0;
231
+ const effectiveUtility = scopedUtility > 0
232
+ ? scopedUtility * SCOPED_UTILITY_BLEND_SCOPED + globalUtility * SCOPED_UTILITY_BLEND_GLOBAL
233
+ : globalUtility;
234
+ if (effectiveUtility <= 0)
163
235
  return;
236
+ // Recency decay: use the global lastUsedAt for the decay factor (it's an
237
+ // ISO string with full resolution), falling back to scoped lastUsedAt (ms).
164
238
  let recencyFactor = 1;
165
- if (utilScore.lastUsedAt) {
166
- const lastUsedMs = new Date(utilScore.lastUsedAt).getTime();
239
+ const lastUsedRaw = utilScore?.lastUsedAt ?? (scopedScore ? new Date(scopedScore.lastUsedAt).toISOString() : undefined);
240
+ if (lastUsedRaw) {
241
+ const lastUsedMs = new Date(lastUsedRaw).getTime();
167
242
  const daysSinceLastUse = Number.isNaN(lastUsedMs)
168
243
  ? Infinity
169
244
  : Math.max(0, (Date.now() - lastUsedMs) / (1000 * 60 * 60 * 24));
170
- recencyFactor = Math.exp(-daysSinceLastUse / RECENCY_DECAY_DAYS);
245
+ // Phase 2A / Rec 5: configurable forgetting curve with optional
246
+ // feedback-stability boost. Absent config + absent positive feedback
247
+ // collapses to `exp(-days / 30)` — pre-2A default-safe.
248
+ const halfLifeDays = ctx.utilityDecayConfig?.halfLifeDays ?? DEFAULT_RECENCY_HALF_LIFE_DAYS;
249
+ const stabilityBoost = ctx.utilityDecayConfig?.feedbackStabilityBoost ?? 1.5;
250
+ const positiveCount = ctx.positiveFeedbackCounts?.get(item.id) ?? 0;
251
+ // `boost^count` is 1 when count is 0 OR when boost is 1.0, so neither
252
+ // a missing feedback count nor a "no boost" config widens the half-life.
253
+ let stabilizedHalfLife = halfLifeDays * stabilityBoost ** positiveCount;
254
+ stabilizedHalfLife = Math.min(stabilizedHalfLife, halfLifeDays * FEEDBACK_HALF_LIFE_CAP_MULTIPLIER);
255
+ // Defensive: half-life must stay positive to avoid div-by-zero / Infinity.
256
+ const safeHalfLife = Math.max(0.0001, stabilizedHalfLife);
257
+ recencyFactor = Math.exp(-daysSinceLastUse / safeHalfLife);
171
258
  }
172
- const rawBoost = 1 + utilScore.utility * recencyFactor * UTILITY_WEIGHT;
259
+ const rawBoost = 1 + effectiveUtility * recencyFactor * UTILITY_WEIGHT;
173
260
  item.score *= Math.min(rawBoost, UTILITY_MAX_BOOST);
174
261
  item.utilityBoosted = true;
175
262
  },
176
263
  };
264
+ /**
265
+ * Project-context boost.
266
+ *
267
+ * Auto-boosts assets whose name, tags, aliases, or search hints contain tokens
268
+ * derived from the current working directory's project name. For example, when
269
+ * running `akm search` from the `akm` git repo, assets tagged `akm` or named
270
+ * `akm-*` receive an additive boost.
271
+ *
272
+ * The boost is capped at 0.5 so it can never overpower an exact-name match
273
+ * (which contributes 2.0). Each matching token adds 0.2 up to the cap.
274
+ *
275
+ * Skipped entirely when `projectContext` is absent or has no tokens (e.g.
276
+ * when running from home dir, /tmp, or when disabled via
277
+ * `--no-project-context` / `AKM_DISABLE_PROJECT_CONTEXT=1`).
278
+ */
279
+ const projectContextRankingContributor = {
280
+ name: "project-context-ranking",
281
+ appliesTo(_item, ctx) {
282
+ return ctx.projectContext != null && ctx.projectContext.tokens.size > 0;
283
+ },
284
+ adjust(item, ctx) {
285
+ if (!ctx.projectContext)
286
+ return 0;
287
+ const fields = [
288
+ item.entry.name ?? "",
289
+ ...(item.entry.tags ?? []),
290
+ ...(item.entry.aliases ?? []),
291
+ ...(item.entry.searchHints ?? []),
292
+ ].map((s) => s.toLowerCase());
293
+ let hits = 0;
294
+ for (const token of ctx.projectContext.tokens) {
295
+ if (fields.some((f) => f.includes(token)))
296
+ hits++;
297
+ }
298
+ return Math.min(0.5, hits * 0.2);
299
+ },
300
+ };
177
301
  export const defaultRankingContributors = [
178
302
  exactNameRankingContributor,
179
303
  typeRankingContributor,
@@ -184,6 +308,9 @@ export const defaultRankingContributors = [
184
308
  descriptionRankingContributor,
185
309
  metadataRankingContributor,
186
310
  graphRankingContributor,
311
+ captureModeRankingContributor,
312
+ lessonStrengthContributor,
313
+ projectContextRankingContributor,
187
314
  ];
188
315
  export const defaultUtilityRankingContributors = [utilityRankingContributor];
189
316
  export function applyScoreContributors(item, ctx, contributors = defaultRankingContributors) {
@@ -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 { getUtilityScoresByIds } from "./db";
2
5
  import { applyScoreContributors, applyUtilityContributors } from "./ranking-contributors";
3
6
  export function normalizeFtsScores(results) {
@@ -58,14 +61,18 @@ export function applyRankingRules(options) {
58
61
  queryLower,
59
62
  queryTokens,
60
63
  graphContext: options.graphContext,
64
+ projectContext: options.projectContext,
61
65
  };
62
66
  for (const item of options.items) {
63
67
  applyScoreContributors(item, rankingContext);
64
68
  }
65
- const utilScoresMap = getUtilityScoresByIds(options.db, options.items.map((item) => item.id));
69
+ const { global: utilScoresMap, scoped: scopedUtilScoresMap } = getUtilityScoresByIds(options.db, options.items.map((item) => item.id), options.scopeKey);
66
70
  const utilityContext = {
67
71
  ...rankingContext,
68
72
  utilityScores: utilScoresMap,
73
+ scopedUtilityScores: scopedUtilScoresMap,
74
+ utilityDecayConfig: options.utilityDecayConfig,
75
+ positiveFeedbackCounts: options.positiveFeedbackCounts,
69
76
  };
70
77
  for (const item of options.items) {
71
78
  applyUtilityContributors(item, utilityContext);
@@ -1,12 +1,6 @@
1
- /**
2
- * Per-field search text extraction for FTS5 indexing.
3
- *
4
- * Extracted from indexer.ts to break the circular dependency:
5
- * db.ts -> indexer.ts -> db.ts
6
- *
7
- * This module imports only from metadata.ts (for the StashEntry type),
8
- * so it can be safely imported by both db.ts and indexer.ts.
9
- */
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/.
10
4
  /**
11
5
  * Return per-field search text for multi-column FTS5 indexing.
12
6
  *
@@ -45,6 +39,8 @@ export function buildSearchFields(entry) {
45
39
  hintParts.push(entry.xrefs.join(" "));
46
40
  if (entry.pageKind)
47
41
  hintParts.push(entry.pageKind);
42
+ if (entry.whenToUse)
43
+ hintParts.push(entry.whenToUse);
48
44
  const hints = hintParts.join(" ").toLowerCase();
49
45
  const contentParts = [];
50
46
  if (entry.toc) {
@@ -1,3 +1,8 @@
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
+ import { makeAssetRef } from "../core/asset-ref";
5
+ import { getDerivedForParent } from "./db";
1
6
  import { getRenderer } from "./file-context";
2
7
  const rendererSearchHitEnricher = {
3
8
  name: "renderer-search-hit-enricher",
@@ -12,8 +17,92 @@ const rendererSearchHitEnricher = {
12
17
  renderer?.enrichSearchHit?.(hit, ctx.stashDir);
13
18
  },
14
19
  };
15
- export const defaultSearchHitEnrichers = [rendererSearchHitEnricher];
16
- export async function enrichSearchHit(hit, ctx, enrichers = defaultSearchHitEnrichers) {
20
+ /**
21
+ * Phase 5A / Advantage D5 derived-memory enricher.
22
+ *
23
+ * When a parent memory has a `.derived` child indexed (the LLM-distilled
24
+ * lesson surface), this enricher rewrites the parent hit to surface the
25
+ * derived child's description / searchHints / tags AND sets `expandTo` to
26
+ * the derived child's ref so callers can fetch it via `akm show <ref>`.
27
+ *
28
+ * The parent ref is preserved on the hit — only the surface text is
29
+ * swapped, so links and provenance still point at the canonical parent.
30
+ *
31
+ * Skipped for:
32
+ * - non-memory hits
33
+ * - memory hits that are themselves derived children (name ends with
34
+ * `.derived`) — we never recurse parent→child→grandchild
35
+ * - contexts without an open DB connection
36
+ */
37
+ export const derivedMemoryEnricher = {
38
+ name: "derived-memory-enricher",
39
+ appliesTo(ctx) {
40
+ return ctx.type === "memory" && ctx.db !== undefined;
41
+ },
42
+ enrich(hit, ctx) {
43
+ if (!ctx.db)
44
+ return;
45
+ // Never recurse: a `.derived` hit is itself the child surface; leaving
46
+ // it untouched also avoids `<parent>.derived.derived` chains.
47
+ if (hit.name.toLowerCase().endsWith(".derived"))
48
+ return;
49
+ // Parent ref shape: `memory:<name>`. Re-build from the entry's name
50
+ // so we don't depend on whatever wiki/registry prefix `hit.ref` carries.
51
+ const parentRef = makeAssetRef("memory", hit.name);
52
+ const derived = getDerivedForParent(ctx.db, parentRef);
53
+ if (!derived)
54
+ return;
55
+ // Swap description / searchHints / tags from the derived child.
56
+ // The parent ref itself is preserved — only the surface text is swapped.
57
+ if (typeof derived.entry.description === "string" && derived.entry.description.length > 0) {
58
+ hit.description = derived.entry.description;
59
+ }
60
+ if (Array.isArray(derived.entry.searchHints) && derived.entry.searchHints.length > 0) {
61
+ // We don't have a `searchHints` field on SourceSearchHit today — it's
62
+ // only used inside ranking. The plan says to swap when present; we
63
+ // record it onto the hit only if a future renderer surfaces it. For
64
+ // now, treat as advisory (no-op when SearchHit lacks the field).
65
+ }
66
+ if (Array.isArray(derived.entry.tags) && derived.entry.tags.length > 0) {
67
+ hit.tags = derived.entry.tags;
68
+ }
69
+ hit.expandTo = makeAssetRef("memory", derived.entry.name);
70
+ },
71
+ };
72
+ /**
73
+ * Registry of additional enrichers — populated by
74
+ * {@link registerSearchHitEnricher} and consumed in addition to
75
+ * {@link defaultSearchHitEnrichers} when `enrichSearchHit` is invoked
76
+ * without an explicit enricher list.
77
+ *
78
+ * Kept module-local so callers must use `registerSearchHitEnricher` rather
79
+ * than mutating the array directly.
80
+ */
81
+ const additionalEnrichers = [];
82
+ export const defaultSearchHitEnrichers = [rendererSearchHitEnricher, derivedMemoryEnricher];
83
+ /**
84
+ * Register an additional enricher to be applied alongside the defaults.
85
+ *
86
+ * Idempotent on `name`: subsequent calls with the same name replace the
87
+ * previously-registered enricher (so tests can re-register cleanly without
88
+ * stacking duplicates).
89
+ */
90
+ export function registerSearchHitEnricher(enricher) {
91
+ const existingIndex = additionalEnrichers.findIndex((e) => e.name === enricher.name);
92
+ if (existingIndex >= 0) {
93
+ additionalEnrichers[existingIndex] = enricher;
94
+ }
95
+ else {
96
+ additionalEnrichers.push(enricher);
97
+ }
98
+ }
99
+ /**
100
+ * Test-only: clear the registered-enrichers list. Not part of the public API.
101
+ */
102
+ export function _resetRegisteredSearchHitEnrichers() {
103
+ additionalEnrichers.length = 0;
104
+ }
105
+ export async function enrichSearchHit(hit, ctx, enrichers = [...defaultSearchHitEnrichers, ...additionalEnrichers]) {
17
106
  for (const enricher of enrichers) {
18
107
  if (!enricher.appliesTo(ctx))
19
108
  continue;
@@ -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 { resolveStashDir } from "../core/common";
@@ -37,8 +40,24 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
37
40
  const seen = new Set([path.resolve(stashDir)]);
38
41
  const addSource = (dir, registryId, wikiName, writable) => {
39
42
  const resolved = path.resolve(dir);
40
- if (seen.has(resolved))
43
+ if (seen.has(resolved)) {
44
+ // Already in the source list — typically the primary stash injected at
45
+ // sources[0] before this loop. Enrich that entry with whatever metadata
46
+ // the matching config source carries so `--source <config-name>` can
47
+ // find it via registryId. Without this, the primary stash entry stays
48
+ // identity-less and a user-named primary source ("name": "my-stash")
49
+ // would validate but match zero entries when filtering.
50
+ const existing = sources.find((s) => s.path === resolved);
51
+ if (existing) {
52
+ if (registryId && !existing.registryId)
53
+ existing.registryId = registryId;
54
+ if (wikiName && !existing.wikiName)
55
+ existing.wikiName = wikiName;
56
+ if (writable && !existing.writable)
57
+ existing.writable = true;
58
+ }
41
59
  return;
60
+ }
42
61
  seen.add(resolved);
43
62
  if (isSuspiciousStashRoot(dir)) {
44
63
  warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`);
@@ -1,7 +1,10 @@
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 { writeFileAtomic } from "../core/common";
3
6
  import { getCacheDir, getSemanticStatusPath } from "../core/paths";
4
- import { DEFAULT_LOCAL_MODEL } from "../llm/embedder";
7
+ import { DEFAULT_LOCAL_MODEL } from "../llm/embedders/local";
5
8
  export function deriveSemanticProviderFingerprint(embedding) {
6
9
  if (embedding?.endpoint) {
7
10
  return `remote:${embedding.endpoint}|${embedding.model}|${embedding.dimension ?? "default"}`;