akm-cli 0.7.5 → 0.8.0-rc2

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 (152) hide show
  1. package/.github/CHANGELOG.md +1 -1
  2. package/dist/cli/parse-args.js +43 -0
  3. package/dist/cli.js +853 -479
  4. package/dist/commands/agent-dispatch.js +102 -0
  5. package/dist/commands/agent-support.js +62 -0
  6. package/dist/commands/config-cli.js +68 -84
  7. package/dist/commands/consolidate.js +823 -0
  8. package/dist/commands/distill-promotion-policy.js +658 -0
  9. package/dist/commands/distill.js +244 -52
  10. package/dist/commands/eval-cases.js +40 -0
  11. package/dist/commands/events.js +2 -23
  12. package/dist/commands/graph.js +222 -0
  13. package/dist/commands/health.js +376 -0
  14. package/dist/commands/help/help-accept.md +9 -0
  15. package/dist/commands/help/help-improve.md +53 -0
  16. package/dist/commands/help/help-proposals.md +15 -0
  17. package/dist/commands/help/help-propose.md +17 -0
  18. package/dist/commands/help/help-reject.md +8 -0
  19. package/dist/commands/history.js +3 -30
  20. package/dist/commands/improve.js +1170 -0
  21. package/dist/commands/info.js +2 -2
  22. package/dist/commands/init.js +2 -2
  23. package/dist/commands/install-audit.js +5 -1
  24. package/dist/commands/installed-stashes.js +118 -138
  25. package/dist/commands/knowledge.js +133 -0
  26. package/dist/commands/lint/agent-linter.js +46 -0
  27. package/dist/commands/lint/base-linter.js +285 -0
  28. package/dist/commands/lint/command-linter.js +46 -0
  29. package/dist/commands/lint/default-linter.js +13 -0
  30. package/dist/commands/lint/index.js +107 -0
  31. package/dist/commands/lint/knowledge-linter.js +13 -0
  32. package/dist/commands/lint/memory-linter.js +58 -0
  33. package/dist/commands/lint/registry.js +33 -0
  34. package/dist/commands/lint/skill-linter.js +42 -0
  35. package/dist/commands/lint/task-linter.js +47 -0
  36. package/dist/commands/lint/types.js +1 -0
  37. package/dist/commands/lint/workflow-linter.js +53 -0
  38. package/dist/commands/lint.js +1 -0
  39. package/dist/commands/proposal.js +8 -7
  40. package/dist/commands/propose.js +78 -28
  41. package/dist/commands/reflect.js +143 -35
  42. package/dist/commands/registry-search.js +2 -2
  43. package/dist/commands/remember.js +54 -0
  44. package/dist/commands/schema-repair.js +130 -0
  45. package/dist/commands/search.js +21 -5
  46. package/dist/commands/show.js +121 -17
  47. package/dist/commands/source-add.js +10 -10
  48. package/dist/commands/source-manage.js +11 -19
  49. package/dist/commands/tasks.js +385 -0
  50. package/dist/commands/url-checker.js +39 -0
  51. package/dist/commands/vault.js +8 -26
  52. package/dist/core/action-contributors.js +25 -0
  53. package/dist/core/asset-ref.js +4 -0
  54. package/dist/core/asset-registry.js +4 -16
  55. package/dist/core/asset-spec.js +10 -0
  56. package/dist/core/common.js +94 -0
  57. package/dist/core/concurrent.js +22 -0
  58. package/dist/core/config.js +222 -128
  59. package/dist/core/events.js +73 -126
  60. package/dist/core/frontmatter.js +3 -1
  61. package/dist/core/markdown.js +17 -0
  62. package/dist/core/memory-improve.js +678 -0
  63. package/dist/core/parse.js +155 -0
  64. package/dist/core/paths.js +101 -3
  65. package/dist/core/proposal-validators.js +61 -0
  66. package/dist/core/proposals.js +49 -38
  67. package/dist/core/state-db.js +775 -0
  68. package/dist/core/time.js +51 -0
  69. package/dist/core/warn.js +59 -1
  70. package/dist/indexer/db-search.js +52 -238
  71. package/dist/indexer/db.js +378 -1
  72. package/dist/indexer/ensure-index.js +61 -0
  73. package/dist/indexer/graph-boost.js +247 -94
  74. package/dist/indexer/graph-db.js +201 -0
  75. package/dist/indexer/graph-dedup.js +99 -0
  76. package/dist/indexer/graph-extraction.js +409 -76
  77. package/dist/indexer/index-context.js +10 -0
  78. package/dist/indexer/indexer.js +442 -290
  79. package/dist/indexer/llm-cache.js +47 -0
  80. package/dist/indexer/match-contributors.js +141 -0
  81. package/dist/indexer/matchers.js +24 -190
  82. package/dist/indexer/memory-inference.js +63 -29
  83. package/dist/indexer/metadata-contributors.js +26 -0
  84. package/dist/indexer/metadata.js +194 -175
  85. package/dist/indexer/path-resolver.js +89 -0
  86. package/dist/indexer/ranking-contributors.js +204 -0
  87. package/dist/indexer/ranking.js +74 -0
  88. package/dist/indexer/search-hit-enrichers.js +22 -0
  89. package/dist/indexer/search-source.js +24 -9
  90. package/dist/indexer/semantic-status.js +2 -16
  91. package/dist/indexer/walker.js +25 -0
  92. package/dist/integrations/agent/config.js +175 -3
  93. package/dist/integrations/agent/index.js +3 -1
  94. package/dist/integrations/agent/pipeline.js +39 -0
  95. package/dist/integrations/agent/profiles.js +67 -5
  96. package/dist/integrations/agent/prompts.js +77 -72
  97. package/dist/integrations/agent/runners.js +31 -0
  98. package/dist/integrations/agent/sdk-runner.js +120 -0
  99. package/dist/integrations/agent/spawn.js +71 -16
  100. package/dist/integrations/lockfile.js +10 -18
  101. package/dist/integrations/session-logs/index.js +65 -0
  102. package/dist/integrations/session-logs/providers/claude-code.js +56 -0
  103. package/dist/integrations/session-logs/providers/opencode.js +52 -0
  104. package/dist/integrations/session-logs/types.js +1 -0
  105. package/dist/llm/call-ai.js +74 -0
  106. package/dist/llm/client.js +61 -122
  107. package/dist/llm/feature-gate.js +27 -16
  108. package/dist/llm/graph-extract.js +297 -62
  109. package/dist/llm/memory-infer.js +49 -71
  110. package/dist/llm/metadata-enhance.js +39 -22
  111. package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
  112. package/dist/output/cli-hints-full.md +277 -0
  113. package/dist/output/cli-hints-short.md +65 -0
  114. package/dist/output/cli-hints.js +2 -318
  115. package/dist/output/renderers.js +190 -123
  116. package/dist/output/shapes.js +33 -0
  117. package/dist/output/text.js +239 -2
  118. package/dist/registry/providers/skills-sh.js +61 -49
  119. package/dist/registry/providers/static-index.js +44 -48
  120. package/dist/setup/setup.js +510 -11
  121. package/dist/sources/provider-factory.js +2 -1
  122. package/dist/sources/providers/git.js +2 -2
  123. package/dist/sources/website-ingest.js +4 -0
  124. package/dist/tasks/backends/cron.js +200 -0
  125. package/dist/tasks/backends/exec-utils.js +25 -0
  126. package/dist/tasks/backends/index.js +32 -0
  127. package/dist/tasks/backends/launchd-template.xml +19 -0
  128. package/dist/tasks/backends/launchd.js +184 -0
  129. package/dist/tasks/backends/schtasks-template.xml +29 -0
  130. package/dist/tasks/backends/schtasks.js +212 -0
  131. package/dist/tasks/parser.js +198 -0
  132. package/dist/tasks/resolveAkmBin.js +84 -0
  133. package/dist/tasks/runner.js +432 -0
  134. package/dist/tasks/schedule.js +208 -0
  135. package/dist/tasks/schema.js +13 -0
  136. package/dist/tasks/validator.js +59 -0
  137. package/dist/wiki/index-template.md +12 -0
  138. package/dist/wiki/ingest-workflow-template.md +54 -0
  139. package/dist/wiki/log-template.md +8 -0
  140. package/dist/wiki/schema-template.md +61 -0
  141. package/dist/wiki/wiki-templates.js +12 -0
  142. package/dist/wiki/wiki.js +10 -61
  143. package/dist/workflows/authoring.js +5 -25
  144. package/dist/workflows/renderer.js +8 -3
  145. package/dist/workflows/runs.js +59 -91
  146. package/dist/workflows/validator.js +1 -1
  147. package/dist/workflows/workflow-template.md +24 -0
  148. package/docs/README.md +3 -0
  149. package/docs/migration/release-notes/0.7.0.md +1 -1
  150. package/docs/migration/release-notes/0.8.0.md +43 -0
  151. package/package.json +3 -2
  152. package/dist/templates/wiki-templates.js +0 -100
@@ -0,0 +1,204 @@
1
+ import { computeGraphBoost } from "./graph-boost";
2
+ const TYPE_BOOST = {
3
+ skill: 0.4,
4
+ command: 0.35,
5
+ workflow: 0.35,
6
+ agent: 0.3,
7
+ script: 0.2,
8
+ knowledge: 0.22,
9
+ memory: -0.02,
10
+ };
11
+ const MAX_BOOST_SUM = 3.0;
12
+ const UTILITY_WEIGHT = 0.5;
13
+ const UTILITY_MAX_BOOST = 1.5;
14
+ const RECENCY_DECAY_DAYS = 30;
15
+ function beliefStateBoost(item) {
16
+ const entry = item.entry;
17
+ if (entry.type !== "memory")
18
+ return 0;
19
+ if (entry.beliefState === "contradicted")
20
+ return -0.45;
21
+ if (entry.beliefState === "superseded")
22
+ return -0.25;
23
+ if (entry.beliefState === "archived")
24
+ return -0.6;
25
+ if (entry.beliefState === "active")
26
+ return 0.06;
27
+ return 0;
28
+ }
29
+ const exactNameRankingContributor = {
30
+ name: "exact-name-ranking",
31
+ appliesTo: () => true,
32
+ adjust(item, ctx) {
33
+ const entry = item.entry;
34
+ const nameLower = entry.name.toLowerCase();
35
+ const rawNameBase = nameLower.split("/").pop() ?? nameLower;
36
+ const nameBase = entry.type === "memory" && rawNameBase.endsWith(".derived")
37
+ ? rawNameBase.slice(0, -".derived".length)
38
+ : rawNameBase;
39
+ if (nameBase === ctx.queryLower || nameLower === ctx.queryLower) {
40
+ return 2.0;
41
+ }
42
+ if (nameBase.includes(ctx.queryLower) || ctx.queryLower.includes(nameBase)) {
43
+ return 1.0;
44
+ }
45
+ const nameTokens = nameBase.split(/[-_\s]+/).filter(Boolean);
46
+ const matchCount = ctx.queryTokens.filter((qt) => nameTokens.some((nt) => nt === qt || nt.includes(qt))).length;
47
+ return matchCount > 0 ? Math.min(0.9, matchCount * 0.3) : 0;
48
+ },
49
+ };
50
+ const typeRankingContributor = {
51
+ name: "type-ranking",
52
+ appliesTo: () => true,
53
+ adjust(item) {
54
+ return TYPE_BOOST[item.entry.type] ?? 0;
55
+ },
56
+ };
57
+ const memoryRankingContributor = {
58
+ name: "memory-ranking",
59
+ appliesTo(item) {
60
+ return item.entry.type === "memory";
61
+ },
62
+ adjust(item) {
63
+ const derivedBoost = item.entry.name.toLowerCase().endsWith(".derived") ? 0.12 : -0.08;
64
+ return derivedBoost + beliefStateBoost(item);
65
+ },
66
+ };
67
+ const tagRankingContributor = {
68
+ name: "tag-ranking",
69
+ appliesTo(item) {
70
+ return Array.isArray(item.entry.tags) && item.entry.tags.length > 0;
71
+ },
72
+ adjust(item, ctx) {
73
+ let tagBoost = 0;
74
+ for (const tag of item.entry.tags ?? []) {
75
+ if (ctx.queryTokens.some((token) => tag.toLowerCase() === token))
76
+ tagBoost += 0.15;
77
+ }
78
+ return Math.min(0.3, tagBoost);
79
+ },
80
+ };
81
+ const searchHintRankingContributor = {
82
+ name: "search-hint-ranking",
83
+ appliesTo(item) {
84
+ return Array.isArray(item.entry.searchHints) && item.entry.searchHints.length > 0;
85
+ },
86
+ adjust(item, ctx) {
87
+ let hintBoost = 0;
88
+ for (const hint of item.entry.searchHints ?? []) {
89
+ const hintLower = hint.toLowerCase();
90
+ for (const token of ctx.queryTokens) {
91
+ if (hintLower.includes(token)) {
92
+ hintBoost += 0.12;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ return Math.min(0.24, hintBoost);
98
+ },
99
+ };
100
+ const aliasRankingContributor = {
101
+ name: "alias-ranking",
102
+ appliesTo(item) {
103
+ return Array.isArray(item.entry.aliases) && item.entry.aliases.length > 0;
104
+ },
105
+ adjust(item, ctx) {
106
+ let boost = 0;
107
+ for (const alias of item.entry.aliases ?? []) {
108
+ const aliasLower = alias.toLowerCase();
109
+ if (aliasLower === ctx.queryLower) {
110
+ boost += 1.5;
111
+ break;
112
+ }
113
+ if (ctx.queryTokens.some((token) => aliasLower.includes(token)))
114
+ boost += 0.3;
115
+ }
116
+ return boost;
117
+ },
118
+ };
119
+ const descriptionRankingContributor = {
120
+ name: "description-ranking",
121
+ appliesTo(item) {
122
+ return typeof item.entry.description === "string" && item.entry.description.length > 0;
123
+ },
124
+ adjust(item, ctx) {
125
+ const descLower = item.entry.description?.toLowerCase() ?? "";
126
+ const descMatchCount = ctx.queryTokens.filter((token) => descLower.includes(token)).length;
127
+ if (descMatchCount === ctx.queryTokens.length && ctx.queryTokens.length > 1)
128
+ return 0.25;
129
+ if (descMatchCount > 0)
130
+ return 0.1;
131
+ return 0;
132
+ },
133
+ };
134
+ const metadataRankingContributor = {
135
+ name: "metadata-ranking",
136
+ appliesTo: () => true,
137
+ adjust(item) {
138
+ let boost = item.entry.quality === "curated" ? 0.05 : 0;
139
+ if (typeof item.entry.confidence === "number") {
140
+ boost += Math.min(0.05, Math.max(0, item.entry.confidence) * 0.05);
141
+ }
142
+ return boost;
143
+ },
144
+ };
145
+ const graphRankingContributor = {
146
+ name: "graph-ranking",
147
+ appliesTo(_item, ctx) {
148
+ return ctx.graphContext !== null;
149
+ },
150
+ adjust(item, ctx) {
151
+ return ctx.graphContext ? computeGraphBoost(ctx.graphContext, item.filePath) : 0;
152
+ },
153
+ };
154
+ const utilityRankingContributor = {
155
+ name: "utility-ranking",
156
+ appliesTo(item, ctx) {
157
+ const utilScore = ctx.utilityScores.get(item.id);
158
+ return Boolean(utilScore && utilScore.utility > 0);
159
+ },
160
+ apply(item, ctx) {
161
+ const utilScore = ctx.utilityScores.get(item.id);
162
+ if (!utilScore || utilScore.utility <= 0)
163
+ return;
164
+ let recencyFactor = 1;
165
+ if (utilScore.lastUsedAt) {
166
+ const lastUsedMs = new Date(utilScore.lastUsedAt).getTime();
167
+ const daysSinceLastUse = Number.isNaN(lastUsedMs)
168
+ ? Infinity
169
+ : Math.max(0, (Date.now() - lastUsedMs) / (1000 * 60 * 60 * 24));
170
+ recencyFactor = Math.exp(-daysSinceLastUse / RECENCY_DECAY_DAYS);
171
+ }
172
+ const rawBoost = 1 + utilScore.utility * recencyFactor * UTILITY_WEIGHT;
173
+ item.score *= Math.min(rawBoost, UTILITY_MAX_BOOST);
174
+ item.utilityBoosted = true;
175
+ },
176
+ };
177
+ export const defaultRankingContributors = [
178
+ exactNameRankingContributor,
179
+ typeRankingContributor,
180
+ memoryRankingContributor,
181
+ tagRankingContributor,
182
+ searchHintRankingContributor,
183
+ aliasRankingContributor,
184
+ descriptionRankingContributor,
185
+ metadataRankingContributor,
186
+ graphRankingContributor,
187
+ ];
188
+ export const defaultUtilityRankingContributors = [utilityRankingContributor];
189
+ export function applyScoreContributors(item, ctx, contributors = defaultRankingContributors) {
190
+ let boostSum = 0;
191
+ for (const contributor of contributors) {
192
+ if (!contributor.appliesTo(item, ctx))
193
+ continue;
194
+ boostSum += contributor.adjust(item, ctx);
195
+ }
196
+ item.score *= 1 + Math.min(boostSum, MAX_BOOST_SUM);
197
+ }
198
+ export function applyUtilityContributors(item, ctx, contributors = defaultUtilityRankingContributors) {
199
+ for (const contributor of contributors) {
200
+ if (!contributor.appliesTo(item, ctx))
201
+ continue;
202
+ contributor.apply(item, ctx);
203
+ }
204
+ }
@@ -0,0 +1,74 @@
1
+ import { getUtilityScoresByIds } from "./db";
2
+ import { applyScoreContributors, applyUtilityContributors } from "./ranking-contributors";
3
+ export function normalizeFtsScores(results) {
4
+ const ftsScoreMap = new Map();
5
+ if (results.length === 0)
6
+ return ftsScoreMap;
7
+ const bestBm25 = results[0].bm25Score;
8
+ const worstBm25 = results[results.length - 1].bm25Score;
9
+ const range = bestBm25 - worstBm25;
10
+ for (const result of results) {
11
+ const normalized = range !== 0 ? (result.bm25Score - worstBm25) / range : 1.0;
12
+ const ftsScore = 0.3 + normalized * 0.7;
13
+ ftsScoreMap.set(result.id, { score: ftsScore, result });
14
+ }
15
+ return ftsScoreMap;
16
+ }
17
+ export function combineSearchScores(options) {
18
+ const FTS_WEIGHT = 0.7;
19
+ const VEC_WEIGHT = 0.3;
20
+ const scored = [];
21
+ const seenIds = new Set();
22
+ for (const [id, { score: ftsScore, result }] of options.ftsScoreMap) {
23
+ seenIds.add(id);
24
+ const embedScore = options.embedScoreMap.get(id);
25
+ const combinedScore = embedScore !== undefined ? ftsScore * FTS_WEIGHT + embedScore * VEC_WEIGHT : ftsScore;
26
+ scored.push({
27
+ id,
28
+ entry: result.entry,
29
+ filePath: result.filePath,
30
+ score: combinedScore,
31
+ rankingMode: embedScore !== undefined ? "hybrid" : "fts",
32
+ });
33
+ }
34
+ for (const [id, cosine] of options.embedScoreMap) {
35
+ if (seenIds.has(id))
36
+ continue;
37
+ const found = options.getEntryById(id);
38
+ if (!found)
39
+ continue;
40
+ if (options.typeFilter && found.entry.type !== options.typeFilter)
41
+ continue;
42
+ scored.push({
43
+ id,
44
+ entry: found.entry,
45
+ filePath: found.filePath,
46
+ score: cosine * VEC_WEIGHT,
47
+ rankingMode: "semantic",
48
+ });
49
+ }
50
+ return scored;
51
+ }
52
+ export function applyRankingRules(options) {
53
+ const queryTokens = options.query.toLowerCase().split(/\s+/).filter(Boolean);
54
+ const queryLower = options.query.toLowerCase().trim();
55
+ const rankingContext = {
56
+ db: options.db,
57
+ query: options.query,
58
+ queryLower,
59
+ queryTokens,
60
+ graphContext: options.graphContext,
61
+ };
62
+ for (const item of options.items) {
63
+ applyScoreContributors(item, rankingContext);
64
+ }
65
+ const utilScoresMap = getUtilityScoresByIds(options.db, options.items.map((item) => item.id));
66
+ const utilityContext = {
67
+ ...rankingContext,
68
+ utilityScores: utilScoresMap,
69
+ };
70
+ for (const item of options.items) {
71
+ applyUtilityContributors(item, utilityContext);
72
+ }
73
+ return options.items;
74
+ }
@@ -0,0 +1,22 @@
1
+ import { getRenderer } from "./file-context";
2
+ const rendererSearchHitEnricher = {
3
+ name: "renderer-search-hit-enricher",
4
+ appliesTo(ctx) {
5
+ return ctx.rendererRegistry.rendererNameFor(ctx.type) !== undefined;
6
+ },
7
+ async enrich(hit, ctx) {
8
+ const rendererName = ctx.rendererRegistry.rendererNameFor(ctx.type);
9
+ if (!rendererName)
10
+ return;
11
+ const renderer = await getRenderer(rendererName);
12
+ renderer?.enrichSearchHit?.(hit, ctx.stashDir);
13
+ },
14
+ };
15
+ export const defaultSearchHitEnrichers = [rendererSearchHitEnricher];
16
+ export async function enrichSearchHit(hit, ctx, enrichers = defaultSearchHitEnrichers) {
17
+ for (const enricher of enrichers) {
18
+ if (!enricher.appliesTo(ctx))
19
+ continue;
20
+ await enricher.enrich(hit, ctx);
21
+ }
22
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { resolveStashDir } from "../core/common";
4
- import { loadConfig } from "../core/config";
4
+ import { getSources, loadConfig } from "../core/config";
5
5
  import { resolveSourceProviderFactory } from "../sources/provider-factory";
6
6
  // Eager side-effect imports so all built-in source providers self-register
7
7
  // before resolveEntryContentDir() runs.
@@ -21,7 +21,7 @@ const GIT_STASH_TYPES = new Set(["git"]);
21
21
  * 1. The primary stash directory (the entry marked `primary: true`, or the
22
22
  * legacy top-level `stashDir`). Always emitted, even when the directory
23
23
  * does not yet exist on disk, so callers can use it as the clone target.
24
- * 2. Each entry in `config.sources ?? config.stashes[]` (in declared order), excluding the
24
+ * 2. Each entry in `config.sources[]` (in declared order), excluding the
25
25
  * one already emitted as the primary.
26
26
  * 3. Each entry in `config.installed[]` (registry-managed stashes).
27
27
  *
@@ -32,9 +32,10 @@ const GIT_STASH_TYPES = new Set(["git"]);
32
32
  export function resolveSourceEntries(overrideStashDir, existingConfig) {
33
33
  const stashDir = overrideStashDir ?? resolveStashDir();
34
34
  const config = existingConfig ?? loadConfig();
35
- const sources = [{ path: stashDir }];
35
+ // Primary stash is always writable.
36
+ const sources = [{ path: stashDir, writable: true }];
36
37
  const seen = new Set([path.resolve(stashDir)]);
37
- const addSource = (dir, registryId, wikiName) => {
38
+ const addSource = (dir, registryId, wikiName, writable) => {
38
39
  const resolved = path.resolve(dir);
39
40
  if (seen.has(resolved))
40
41
  return;
@@ -47,13 +48,14 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
47
48
  path: resolved,
48
49
  ...(registryId ? { registryId } : {}),
49
50
  ...(wikiName ? { wikiName } : {}),
51
+ ...(writable ? { writable: true } : {}),
50
52
  });
51
53
  }
52
54
  };
53
55
  // (1) + (2) Single pass over declared stashes — primary first if present,
54
56
  // then the rest in declared order. The primary's directory is already
55
57
  // injected as `sources[0]` above, so we only need to dedupe the source set.
56
- const stashes = config.sources ?? config.stashes ?? [];
58
+ const stashes = getSources(config);
57
59
  const primaryIdx = stashes.findIndex((entry) => entry.primary === true);
58
60
  const ordered = [];
59
61
  if (primaryIdx >= 0) {
@@ -72,11 +74,12 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
72
74
  const dir = resolveEntryContentDir(entry);
73
75
  if (dir == null)
74
76
  continue;
75
- addSource(dir, entry.name, entry.wikiName);
77
+ addSource(dir, entry.name, entry.wikiName, entry.writable === true);
76
78
  }
77
79
  // (3) Installed stashes (registry-managed). Always last.
80
+ // Only installed entries explicitly marked writable: true are considered writable.
78
81
  for (const entry of config.installed ?? []) {
79
- addSource(entry.stashRoot, entry.id, entry.wikiName);
82
+ addSource(entry.stashRoot, entry.id, entry.wikiName, entry.writable === true);
80
83
  }
81
84
  return sources;
82
85
  }
@@ -132,6 +135,19 @@ function resolveEntryContentDir(entry) {
132
135
  export function resolveAllStashDirs(overrideStashDir) {
133
136
  return resolveSourceEntries(overrideStashDir).map((s) => s.path);
134
137
  }
138
+ /**
139
+ * Return the resolved absolute paths of all writable stash sources.
140
+ *
141
+ * The primary stash is always writable. Filesystem/git sources that have
142
+ * `writable: true` in config are also included. Registry-cached sources
143
+ * (installed without `writable: true`) are excluded because they are
144
+ * overwritten on `akm update` and must never be mutated.
145
+ */
146
+ export function getWritableStashDirs(overrideStashDir, existingConfig) {
147
+ return resolveSourceEntries(overrideStashDir, existingConfig)
148
+ .filter((s) => s.writable === true)
149
+ .map((s) => s.path);
150
+ }
135
151
  /**
136
152
  * Find which source a file path belongs to.
137
153
  */
@@ -226,8 +242,7 @@ function isValidDirectory(dir) {
226
242
  export async function ensureSourceCaches(config, options) {
227
243
  const cfg = config ?? loadConfig();
228
244
  const force = options?.force === true;
229
- // Use sources[] (current key) with fallback to stashes[] (deprecated, one-release compat).
230
- const entries = cfg.sources ?? cfg.stashes ?? [];
245
+ const entries = getSources(cfg);
231
246
  for (const entry of entries) {
232
247
  if (!GIT_STASH_TYPES.has(entry.type) || !entry.url || entry.enabled === false)
233
248
  continue;
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
2
+ import { writeFileAtomic } from "../core/common";
3
3
  import { getCacheDir, getSemanticStatusPath } from "../core/paths";
4
4
  import { DEFAULT_LOCAL_MODEL } from "../llm/embedder";
5
5
  export function deriveSemanticProviderFingerprint(embedding) {
@@ -41,21 +41,7 @@ export function readSemanticStatus() {
41
41
  export function writeSemanticStatus(status) {
42
42
  const dir = getCacheDir();
43
43
  fs.mkdirSync(dir, { recursive: true });
44
- const filePath = getSemanticStatusPath();
45
- const tmpPath = path.join(dir, `semantic-status.json.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`);
46
- fs.writeFileSync(tmpPath, `${JSON.stringify(status, null, 2)}\n`, "utf8");
47
- try {
48
- fs.renameSync(tmpPath, filePath);
49
- }
50
- catch (err) {
51
- try {
52
- fs.unlinkSync(tmpPath);
53
- }
54
- catch {
55
- /* ignore cleanup failure */
56
- }
57
- throw err;
58
- }
44
+ writeFileAtomic(getSemanticStatusPath(), `${JSON.stringify(status, null, 2)}\n`);
59
45
  }
60
46
  export function clearSemanticStatus() {
61
47
  try {
@@ -136,6 +136,31 @@ function isInsideGitRepo(dir) {
136
136
  }
137
137
  return false;
138
138
  }
139
+ /**
140
+ * Recursively yield every `.md` file under `root`.
141
+ *
142
+ * Shared by graph-extraction and memory-inference so the generator logic
143
+ * lives in exactly one place. Silently skips directories that cannot be
144
+ * read (e.g. permission errors).
145
+ */
146
+ export function* walkMarkdownFiles(root) {
147
+ let entries;
148
+ try {
149
+ entries = fs.readdirSync(root, { withFileTypes: true });
150
+ }
151
+ catch {
152
+ return;
153
+ }
154
+ for (const entry of entries) {
155
+ const full = path.join(root, entry.name);
156
+ if (entry.isDirectory()) {
157
+ yield* walkMarkdownFiles(full);
158
+ }
159
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
160
+ yield full;
161
+ }
162
+ }
163
+ }
139
164
  /** Manual walk for non-git directories. */
140
165
  function walkStashManual(stashRoot) {
141
166
  const results = [];
@@ -27,14 +27,28 @@ import { ConfigError } from "../../core/errors";
27
27
  import { warn } from "../../core/warn";
28
28
  import { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles";
29
29
  /** Keys recognised at the top level of an `agent` config block. */
30
- const KNOWN_AGENT_KEYS = new Set(["default", "timeoutMs", "profiles"]);
30
+ const KNOWN_AGENT_KEYS = new Set(["default", "timeoutMs", "profiles", "processes"]);
31
31
  /** Keys recognised on a profile entry. */
32
- const KNOWN_PROFILE_KEYS = new Set(["bin", "args", "stdio", "env", "envPassthrough", "timeoutMs", "parseOutput"]);
32
+ const KNOWN_PROFILE_KEYS = new Set([
33
+ "bin",
34
+ "args",
35
+ "stdio",
36
+ "env",
37
+ "envPassthrough",
38
+ "timeoutMs",
39
+ "parseOutput",
40
+ "sdkMode",
41
+ "model",
42
+ "endpoint",
43
+ "apiKey",
44
+ ]);
33
45
  /**
34
46
  * Default hard timeout for an agent CLI. Spec §12.2 calls for a hard
35
47
  * timeout; 60s matches the example value in `docs/configuration.md`.
36
48
  */
37
49
  export const DEFAULT_AGENT_TIMEOUT_MS = 60_000;
50
+ /** Keys recognised on a `processes[<name>]` object entry. */
51
+ const KNOWN_PROCESS_ENTRY_KEYS = new Set(["profile", "timeoutMs"]);
38
52
  /**
39
53
  * Parse a raw value (typically `rawConfig.agent` from `JSON.parse`) into a
40
54
  * normalised {@link AgentConfig}. Returns `undefined` when the value is not
@@ -83,6 +97,11 @@ export function parseAgentConfig(value) {
83
97
  if (profiles)
84
98
  out.profiles = profiles;
85
99
  }
100
+ if ("processes" in raw) {
101
+ const processes = parseProcessesMap(raw.processes);
102
+ if (processes)
103
+ out.processes = processes;
104
+ }
86
105
  return out;
87
106
  }
88
107
  function parseAgentProfilesMap(value) {
@@ -98,6 +117,124 @@ function parseAgentProfilesMap(value) {
98
117
  }
99
118
  return Object.keys(out).length > 0 ? out : undefined;
100
119
  }
120
+ /**
121
+ * Parse one entry in `agent.processes`. Accepts a string (profile name) or an
122
+ * object with optional `profile` and `timeoutMs` fields. Returns `undefined`
123
+ * and emits a warning for entries that are neither valid strings nor valid
124
+ * objects (warn-and-ignore).
125
+ */
126
+ export function parseProcessEntry(value, name) {
127
+ if (typeof value === "string") {
128
+ if (!value.trim()) {
129
+ warn(`[akm] Ignoring agent.processes."${name}": string value must be non-empty (a profile name).`);
130
+ return undefined;
131
+ }
132
+ return value.trim();
133
+ }
134
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
135
+ warn(`[akm] Ignoring agent.processes."${name}": expected a string (profile name) or an object with optional "profile" and "timeoutMs".`);
136
+ return undefined;
137
+ }
138
+ const raw = value;
139
+ // Warn on unknown keys (warn-and-ignore contract).
140
+ for (const key of Object.keys(raw)) {
141
+ if (!KNOWN_PROCESS_ENTRY_KEYS.has(key)) {
142
+ warn(`[akm] Ignoring unknown agent.processes."${name}" key: "${key}"`);
143
+ }
144
+ }
145
+ const out = {};
146
+ if ("profile" in raw) {
147
+ if (typeof raw.profile === "string" && raw.profile.trim()) {
148
+ out.profile = raw.profile.trim();
149
+ }
150
+ else if (raw.profile !== undefined) {
151
+ warn(`[akm] Ignoring agent.processes."${name}".profile: expected a non-empty string.`);
152
+ }
153
+ }
154
+ if ("timeoutMs" in raw) {
155
+ if (raw.timeoutMs === null) {
156
+ // null = unlimited — explicit, valid.
157
+ out.timeoutMs = null;
158
+ }
159
+ else if (typeof raw.timeoutMs === "number" &&
160
+ Number.isFinite(raw.timeoutMs) &&
161
+ Number.isInteger(raw.timeoutMs) &&
162
+ raw.timeoutMs > 0) {
163
+ out.timeoutMs = raw.timeoutMs;
164
+ }
165
+ else {
166
+ warn(`[akm] Ignoring agent.processes."${name}".timeoutMs: expected a positive integer (milliseconds) or null (unlimited).`);
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+ /**
172
+ * Parse the `agent.processes` map. Returns `undefined` when the value is not
173
+ * a valid object; per-entry validation errors are warn-and-ignored (per spec §9.2).
174
+ */
175
+ export function parseProcessesMap(value) {
176
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
177
+ warn("[akm] Ignoring agent.processes: expected an object.");
178
+ return undefined;
179
+ }
180
+ const out = {};
181
+ for (const [name, raw] of Object.entries(value)) {
182
+ const parsed = parseProcessEntry(raw, name);
183
+ if (parsed !== undefined)
184
+ out[name] = parsed;
185
+ }
186
+ return Object.keys(out).length > 0 ? out : undefined;
187
+ }
188
+ /**
189
+ * Resolve the agent profile and effective timeout for a named process.
190
+ *
191
+ * Resolution order:
192
+ * 1. `config.processes[processName]` — if a string, that is the profile name;
193
+ * if an object, extract `profile` (and optionally `timeoutMs`).
194
+ * 2. Profile name falls back to `config.default` when not specified in the
195
+ * process entry.
196
+ * 3. `timeoutMs` falls back: `process.timeoutMs` (null = unlimited) →
197
+ * profile.timeoutMs → agent.timeoutMs → DEFAULT_AGENT_TIMEOUT_MS.
198
+ *
199
+ * Returns `{ profile, timeoutMs }` where `timeoutMs` is `undefined` when the
200
+ * resolved timeout is `null` (unlimited) or when no timeout is set at any
201
+ * layer (callers treat `undefined` as the DEFAULT_AGENT_TIMEOUT_MS default).
202
+ *
203
+ * Throws {@link ConfigError} (via {@link requireAgentProfile}) when the agent
204
+ * block is missing or the resolved profile cannot be used.
205
+ */
206
+ export function resolveProcessAgentProfile(processName, agentConfig) {
207
+ let profileName;
208
+ let processTimeoutMs; // null = unlimited from config
209
+ const processEntry = agentConfig?.processes?.[processName];
210
+ if (processEntry !== undefined) {
211
+ if (typeof processEntry === "string") {
212
+ profileName = processEntry;
213
+ }
214
+ else {
215
+ profileName = processEntry.profile;
216
+ processTimeoutMs = processEntry.timeoutMs;
217
+ }
218
+ }
219
+ // Profile name falls back to agent.default when not set in the process entry.
220
+ const resolvedProfile = requireAgentProfile(agentConfig, profileName);
221
+ // Timeout resolution: process entry → profile → agent-level → undefined (caller applies DEFAULT).
222
+ let resolvedTimeoutMs;
223
+ if (processTimeoutMs === null) {
224
+ // null = explicit "unlimited" — surface as undefined so callers omit the timer.
225
+ resolvedTimeoutMs = undefined;
226
+ }
227
+ else if (processTimeoutMs !== undefined) {
228
+ resolvedTimeoutMs = processTimeoutMs;
229
+ }
230
+ else if (resolvedProfile.timeoutMs !== undefined) {
231
+ resolvedTimeoutMs = resolvedProfile.timeoutMs;
232
+ }
233
+ else if (agentConfig?.timeoutMs !== undefined) {
234
+ resolvedTimeoutMs = agentConfig.timeoutMs;
235
+ }
236
+ return { profile: resolvedProfile, timeoutMs: resolvedTimeoutMs };
237
+ }
101
238
  function parseAgentProfileConfig(name, value) {
102
239
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
103
240
  warn(`[akm] Ignoring agent.profiles."${name}": expected an object.`);
@@ -171,6 +308,30 @@ function parseAgentProfileConfig(name, value) {
171
308
  else if (raw.parseOutput !== undefined) {
172
309
  warn(`[akm] Ignoring agent.profiles."${name}".parseOutput: expected "text" or "json".`);
173
310
  }
311
+ if (raw.sdkMode === true || raw.sdkMode === false) {
312
+ out.sdkMode = raw.sdkMode;
313
+ }
314
+ else if (raw.sdkMode !== undefined) {
315
+ warn(`[akm] Ignoring agent.profiles."${name}".sdkMode: expected a boolean.`);
316
+ }
317
+ if (typeof raw.model === "string" && raw.model.trim()) {
318
+ out.model = raw.model.trim();
319
+ }
320
+ else if (raw.model !== undefined) {
321
+ warn(`[akm] Ignoring agent.profiles."${name}".model: expected a non-empty string.`);
322
+ }
323
+ if (typeof raw.endpoint === "string" && raw.endpoint.trim()) {
324
+ out.endpoint = raw.endpoint.trim();
325
+ }
326
+ else if (raw.endpoint !== undefined) {
327
+ warn(`[akm] Ignoring agent.profiles."${name}".endpoint: expected a non-empty string.`);
328
+ }
329
+ if (typeof raw.apiKey === "string" && raw.apiKey.trim()) {
330
+ out.apiKey = raw.apiKey.trim();
331
+ }
332
+ else if (raw.apiKey !== undefined) {
333
+ warn(`[akm] Ignoring agent.profiles."${name}".apiKey: expected a non-empty string.`);
334
+ }
174
335
  return out;
175
336
  }
176
337
  /**
@@ -184,7 +345,7 @@ function parseAgentProfileConfig(name, value) {
184
345
  */
185
346
  export function resolveAgentProfile(name, overrides) {
186
347
  const builtin = getBuiltinAgentProfile(name);
187
- if (!builtin && !overrides?.bin)
348
+ if (!builtin && !overrides?.bin && overrides?.sdkMode !== true)
188
349
  return undefined;
189
350
  const base = builtin ??
190
351
  {
@@ -208,6 +369,10 @@ export function resolveAgentProfile(name, overrides) {
208
369
  : base.envPassthrough,
209
370
  timeoutMs: overrides.timeoutMs ?? base.timeoutMs,
210
371
  parseOutput: overrides.parseOutput ?? base.parseOutput,
372
+ sdkMode: overrides.sdkMode ?? base.sdkMode,
373
+ model: overrides.model ?? base.model,
374
+ endpoint: overrides.endpoint ?? base.endpoint,
375
+ apiKey: overrides.apiKey ?? base.apiKey,
211
376
  };
212
377
  return merged;
213
378
  }
@@ -274,6 +439,13 @@ export function requireAgentProfile(agent, requested) {
274
439
  if (!profile) {
275
440
  throw new ConfigError(`agent profile "${name}" is not built-in and has no \`bin\` override.`, "INVALID_CONFIG_FILE", `Define agent.profiles."${name}".bin in config.json, or pick one of: ${listAgentProfileNames(agent).join(", ")}.`);
276
441
  }
442
+ // Apply the top-level agent.timeoutMs as the effective default for this
443
+ // profile when the profile itself has no timeout override. This makes
444
+ // `agent.timeoutMs` the universal fallback without requiring every
445
+ // profile definition in config.json to repeat it.
446
+ if (profile.timeoutMs === undefined && agent?.timeoutMs !== undefined) {
447
+ return { ...profile, timeoutMs: agent.timeoutMs };
448
+ }
277
449
  return profile;
278
450
  }
279
451
  /**