vemora 0.1.0-alpha.15

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 (254) hide show
  1. package/README.md +851 -0
  2. package/dist/cli.d.ts +16 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +682 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/ask.d.ts +14 -0
  7. package/dist/commands/ask.d.ts.map +1 -0
  8. package/dist/commands/ask.js +137 -0
  9. package/dist/commands/ask.js.map +1 -0
  10. package/dist/commands/audit.d.ts +17 -0
  11. package/dist/commands/audit.d.ts.map +1 -0
  12. package/dist/commands/audit.js +398 -0
  13. package/dist/commands/audit.js.map +1 -0
  14. package/dist/commands/brief.d.ts +16 -0
  15. package/dist/commands/brief.d.ts.map +1 -0
  16. package/dist/commands/brief.js +84 -0
  17. package/dist/commands/brief.js.map +1 -0
  18. package/dist/commands/chat.d.ts +7 -0
  19. package/dist/commands/chat.d.ts.map +1 -0
  20. package/dist/commands/chat.js +155 -0
  21. package/dist/commands/chat.js.map +1 -0
  22. package/dist/commands/context.d.ts +63 -0
  23. package/dist/commands/context.d.ts.map +1 -0
  24. package/dist/commands/context.js +794 -0
  25. package/dist/commands/context.js.map +1 -0
  26. package/dist/commands/dead-code.d.ts +15 -0
  27. package/dist/commands/dead-code.d.ts.map +1 -0
  28. package/dist/commands/dead-code.js +206 -0
  29. package/dist/commands/dead-code.js.map +1 -0
  30. package/dist/commands/deps.d.ts +20 -0
  31. package/dist/commands/deps.d.ts.map +1 -0
  32. package/dist/commands/deps.js +138 -0
  33. package/dist/commands/deps.js.map +1 -0
  34. package/dist/commands/focus.d.ts +8 -0
  35. package/dist/commands/focus.d.ts.map +1 -0
  36. package/dist/commands/focus.js +310 -0
  37. package/dist/commands/focus.js.map +1 -0
  38. package/dist/commands/index.d.ts +10 -0
  39. package/dist/commands/index.d.ts.map +1 -0
  40. package/dist/commands/index.js +366 -0
  41. package/dist/commands/index.js.map +1 -0
  42. package/dist/commands/init-agent.d.ts +23 -0
  43. package/dist/commands/init-agent.d.ts.map +1 -0
  44. package/dist/commands/init-agent.js +384 -0
  45. package/dist/commands/init-agent.js.map +1 -0
  46. package/dist/commands/init.d.ts +2 -0
  47. package/dist/commands/init.d.ts.map +1 -0
  48. package/dist/commands/init.js +122 -0
  49. package/dist/commands/init.js.map +1 -0
  50. package/dist/commands/knowledge.d.ts +14 -0
  51. package/dist/commands/knowledge.d.ts.map +1 -0
  52. package/dist/commands/knowledge.js +115 -0
  53. package/dist/commands/knowledge.js.map +1 -0
  54. package/dist/commands/plan.d.ts +24 -0
  55. package/dist/commands/plan.d.ts.map +1 -0
  56. package/dist/commands/plan.js +867 -0
  57. package/dist/commands/plan.js.map +1 -0
  58. package/dist/commands/query.d.ts +39 -0
  59. package/dist/commands/query.d.ts.map +1 -0
  60. package/dist/commands/query.js +392 -0
  61. package/dist/commands/query.js.map +1 -0
  62. package/dist/commands/remember.d.ts +11 -0
  63. package/dist/commands/remember.d.ts.map +1 -0
  64. package/dist/commands/remember.js +267 -0
  65. package/dist/commands/remember.js.map +1 -0
  66. package/dist/commands/report.d.ts +10 -0
  67. package/dist/commands/report.d.ts.map +1 -0
  68. package/dist/commands/report.js +243 -0
  69. package/dist/commands/report.js.map +1 -0
  70. package/dist/commands/status.d.ts +2 -0
  71. package/dist/commands/status.d.ts.map +1 -0
  72. package/dist/commands/status.js +127 -0
  73. package/dist/commands/status.js.map +1 -0
  74. package/dist/commands/summarize.d.ts +14 -0
  75. package/dist/commands/summarize.d.ts.map +1 -0
  76. package/dist/commands/summarize.js +170 -0
  77. package/dist/commands/summarize.js.map +1 -0
  78. package/dist/commands/triage.d.ts +33 -0
  79. package/dist/commands/triage.d.ts.map +1 -0
  80. package/dist/commands/triage.js +419 -0
  81. package/dist/commands/triage.js.map +1 -0
  82. package/dist/commands/usages.d.ts +14 -0
  83. package/dist/commands/usages.d.ts.map +1 -0
  84. package/dist/commands/usages.js +236 -0
  85. package/dist/commands/usages.js.map +1 -0
  86. package/dist/core/config.d.ts +35 -0
  87. package/dist/core/config.d.ts.map +1 -0
  88. package/dist/core/config.js +159 -0
  89. package/dist/core/config.js.map +1 -0
  90. package/dist/core/types.d.ts +287 -0
  91. package/dist/core/types.d.ts.map +1 -0
  92. package/dist/core/types.js +4 -0
  93. package/dist/core/types.js.map +1 -0
  94. package/dist/embeddings/factory.d.ts +9 -0
  95. package/dist/embeddings/factory.d.ts.map +1 -0
  96. package/dist/embeddings/factory.js +26 -0
  97. package/dist/embeddings/factory.js.map +1 -0
  98. package/dist/embeddings/noop.d.ts +17 -0
  99. package/dist/embeddings/noop.d.ts.map +1 -0
  100. package/dist/embeddings/noop.js +22 -0
  101. package/dist/embeddings/noop.js.map +1 -0
  102. package/dist/embeddings/ollama.d.ts +11 -0
  103. package/dist/embeddings/ollama.d.ts.map +1 -0
  104. package/dist/embeddings/ollama.js +49 -0
  105. package/dist/embeddings/ollama.js.map +1 -0
  106. package/dist/embeddings/openai.d.ts +10 -0
  107. package/dist/embeddings/openai.d.ts.map +1 -0
  108. package/dist/embeddings/openai.js +67 -0
  109. package/dist/embeddings/openai.js.map +1 -0
  110. package/dist/embeddings/provider.d.ts +19 -0
  111. package/dist/embeddings/provider.d.ts.map +1 -0
  112. package/dist/embeddings/provider.js +3 -0
  113. package/dist/embeddings/provider.js.map +1 -0
  114. package/dist/indexer/callgraph.d.ts +16 -0
  115. package/dist/indexer/callgraph.d.ts.map +1 -0
  116. package/dist/indexer/callgraph.js +154 -0
  117. package/dist/indexer/callgraph.js.map +1 -0
  118. package/dist/indexer/chunkBySlidingWindow.d.ts +6 -0
  119. package/dist/indexer/chunkBySlidingWindow.d.ts.map +1 -0
  120. package/dist/indexer/chunkBySlidingWindow.js +30 -0
  121. package/dist/indexer/chunkBySlidingWindow.js.map +1 -0
  122. package/dist/indexer/chunkBySymbols.d.ts +7 -0
  123. package/dist/indexer/chunkBySymbols.d.ts.map +1 -0
  124. package/dist/indexer/chunkBySymbols.js +57 -0
  125. package/dist/indexer/chunkBySymbols.js.map +1 -0
  126. package/dist/indexer/chunker.d.ts +15 -0
  127. package/dist/indexer/chunker.d.ts.map +1 -0
  128. package/dist/indexer/chunker.js +26 -0
  129. package/dist/indexer/chunker.js.map +1 -0
  130. package/dist/indexer/classHeader.d.ts +7 -0
  131. package/dist/indexer/classHeader.d.ts.map +1 -0
  132. package/dist/indexer/classHeader.js +37 -0
  133. package/dist/indexer/classHeader.js.map +1 -0
  134. package/dist/indexer/deps.d.ts +66 -0
  135. package/dist/indexer/deps.d.ts.map +1 -0
  136. package/dist/indexer/deps.js +412 -0
  137. package/dist/indexer/deps.js.map +1 -0
  138. package/dist/indexer/hasher.d.ts +17 -0
  139. package/dist/indexer/hasher.d.ts.map +1 -0
  140. package/dist/indexer/hasher.js +38 -0
  141. package/dist/indexer/hasher.js.map +1 -0
  142. package/dist/indexer/parser.d.ts +18 -0
  143. package/dist/indexer/parser.d.ts.map +1 -0
  144. package/dist/indexer/parser.js +355 -0
  145. package/dist/indexer/parser.js.map +1 -0
  146. package/dist/indexer/scanner.d.ts +18 -0
  147. package/dist/indexer/scanner.d.ts.map +1 -0
  148. package/dist/indexer/scanner.js +37 -0
  149. package/dist/indexer/scanner.js.map +1 -0
  150. package/dist/indexer/strategy.d.ts +11 -0
  151. package/dist/indexer/strategy.d.ts.map +1 -0
  152. package/dist/indexer/strategy.js +15 -0
  153. package/dist/indexer/strategy.js.map +1 -0
  154. package/dist/indexer/tests.d.ts +15 -0
  155. package/dist/indexer/tests.d.ts.map +1 -0
  156. package/dist/indexer/tests.js +68 -0
  157. package/dist/indexer/tests.js.map +1 -0
  158. package/dist/indexer/todos.d.ts +9 -0
  159. package/dist/indexer/todos.d.ts.map +1 -0
  160. package/dist/indexer/todos.js +29 -0
  161. package/dist/indexer/todos.js.map +1 -0
  162. package/dist/llm/anthropic.d.ts +8 -0
  163. package/dist/llm/anthropic.d.ts.map +1 -0
  164. package/dist/llm/anthropic.js +76 -0
  165. package/dist/llm/anthropic.js.map +1 -0
  166. package/dist/llm/claude-code.d.ts +37 -0
  167. package/dist/llm/claude-code.d.ts.map +1 -0
  168. package/dist/llm/claude-code.js +97 -0
  169. package/dist/llm/claude-code.js.map +1 -0
  170. package/dist/llm/factory.d.ts +7 -0
  171. package/dist/llm/factory.d.ts.map +1 -0
  172. package/dist/llm/factory.js +47 -0
  173. package/dist/llm/factory.js.map +1 -0
  174. package/dist/llm/ollama.d.ts +8 -0
  175. package/dist/llm/ollama.d.ts.map +1 -0
  176. package/dist/llm/ollama.js +83 -0
  177. package/dist/llm/ollama.js.map +1 -0
  178. package/dist/llm/openai.d.ts +8 -0
  179. package/dist/llm/openai.d.ts.map +1 -0
  180. package/dist/llm/openai.js +68 -0
  181. package/dist/llm/openai.js.map +1 -0
  182. package/dist/llm/provider.d.ts +35 -0
  183. package/dist/llm/provider.d.ts.map +1 -0
  184. package/dist/llm/provider.js +3 -0
  185. package/dist/llm/provider.js.map +1 -0
  186. package/dist/search/bm25.d.ts +3 -0
  187. package/dist/search/bm25.d.ts.map +1 -0
  188. package/dist/search/bm25.js +104 -0
  189. package/dist/search/bm25.js.map +1 -0
  190. package/dist/search/formatter.d.ts +43 -0
  191. package/dist/search/formatter.d.ts.map +1 -0
  192. package/dist/search/formatter.js +208 -0
  193. package/dist/search/formatter.js.map +1 -0
  194. package/dist/search/hybrid.d.ts +10 -0
  195. package/dist/search/hybrid.d.ts.map +1 -0
  196. package/dist/search/hybrid.js +53 -0
  197. package/dist/search/hybrid.js.map +1 -0
  198. package/dist/search/merge.d.ts +33 -0
  199. package/dist/search/merge.d.ts.map +1 -0
  200. package/dist/search/merge.js +158 -0
  201. package/dist/search/merge.js.map +1 -0
  202. package/dist/search/mmr.d.ts +23 -0
  203. package/dist/search/mmr.d.ts.map +1 -0
  204. package/dist/search/mmr.js +95 -0
  205. package/dist/search/mmr.js.map +1 -0
  206. package/dist/search/rerank.d.ts +12 -0
  207. package/dist/search/rerank.d.ts.map +1 -0
  208. package/dist/search/rerank.js +113 -0
  209. package/dist/search/rerank.js.map +1 -0
  210. package/dist/search/signature.d.ts +42 -0
  211. package/dist/search/signature.d.ts.map +1 -0
  212. package/dist/search/signature.js +112 -0
  213. package/dist/search/signature.js.map +1 -0
  214. package/dist/search/vector.d.ts +41 -0
  215. package/dist/search/vector.d.ts.map +1 -0
  216. package/dist/search/vector.js +185 -0
  217. package/dist/search/vector.js.map +1 -0
  218. package/dist/storage/cache.d.ts +30 -0
  219. package/dist/storage/cache.d.ts.map +1 -0
  220. package/dist/storage/cache.js +160 -0
  221. package/dist/storage/cache.js.map +1 -0
  222. package/dist/storage/knowledge.d.ts +23 -0
  223. package/dist/storage/knowledge.d.ts.map +1 -0
  224. package/dist/storage/knowledge.js +81 -0
  225. package/dist/storage/knowledge.js.map +1 -0
  226. package/dist/storage/planSession.d.ts +39 -0
  227. package/dist/storage/planSession.d.ts.map +1 -0
  228. package/dist/storage/planSession.js +78 -0
  229. package/dist/storage/planSession.js.map +1 -0
  230. package/dist/storage/repository.d.ts +27 -0
  231. package/dist/storage/repository.d.ts.map +1 -0
  232. package/dist/storage/repository.js +95 -0
  233. package/dist/storage/repository.js.map +1 -0
  234. package/dist/storage/session.d.ts +38 -0
  235. package/dist/storage/session.d.ts.map +1 -0
  236. package/dist/storage/session.js +100 -0
  237. package/dist/storage/session.js.map +1 -0
  238. package/dist/storage/summaries.d.ts +19 -0
  239. package/dist/storage/summaries.d.ts.map +1 -0
  240. package/dist/storage/summaries.js +66 -0
  241. package/dist/storage/summaries.js.map +1 -0
  242. package/dist/storage/usage.d.ts +39 -0
  243. package/dist/storage/usage.d.ts.map +1 -0
  244. package/dist/storage/usage.js +55 -0
  245. package/dist/storage/usage.js.map +1 -0
  246. package/dist/utils/git.d.ts +20 -0
  247. package/dist/utils/git.d.ts.map +1 -0
  248. package/dist/utils/git.js +49 -0
  249. package/dist/utils/git.js.map +1 -0
  250. package/dist/utils/tokenizer.d.ts +32 -0
  251. package/dist/utils/tokenizer.d.ts.map +1 -0
  252. package/dist/utils/tokenizer.js +66 -0
  253. package/dist/utils/tokenizer.js.map +1 -0
  254. package/package.json +71 -0
@@ -0,0 +1,794 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runContext = runContext;
7
+ exports.generateContextString = generateContextString;
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const ora_1 = __importDefault(require("ora"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const config_1 = require("../core/config");
13
+ const git_1 = require("../utils/git");
14
+ const factory_1 = require("../embeddings/factory");
15
+ const deps_1 = require("../indexer/deps");
16
+ const tests_1 = require("../indexer/tests");
17
+ const bm25_1 = require("../search/bm25");
18
+ const hybrid_1 = require("../search/hybrid");
19
+ const merge_1 = require("../search/merge");
20
+ const mmr_1 = require("../search/mmr");
21
+ const rerank_1 = require("../search/rerank");
22
+ const formatter_1 = require("../search/formatter");
23
+ const signature_1 = require("../search/signature");
24
+ const vector_1 = require("../search/vector");
25
+ const cache_1 = require("../storage/cache");
26
+ const knowledge_1 = require("../storage/knowledge");
27
+ const repository_1 = require("../storage/repository");
28
+ const session_1 = require("../storage/session");
29
+ const usage_1 = require("../storage/usage");
30
+ const summaries_1 = require("../storage/summaries");
31
+ const tokenizer_1 = require("../utils/tokenizer");
32
+ // ─── Main ─────────────────────────────────────────────────────────────────────
33
+ async function runContext(rootDir, options = {}) {
34
+ const config = (0, config_1.loadConfig)(rootDir);
35
+ const repo = new repository_1.RepositoryStorage(rootDir);
36
+ const cacheStorage = new cache_1.EmbeddingCacheStorage(config.projectId);
37
+ const summaryStorage = new summaries_1.SummaryStorage(rootDir);
38
+ const topK = options.topK ?? 5;
39
+ // Apply config.display.format as default if no explicit format was passed.
40
+ const resolvedOptions = {
41
+ ...options,
42
+ format: options.format ?? config.display?.format ?? "markdown",
43
+ };
44
+ let chunks = repo.loadChunks();
45
+ const symbols = repo.loadSymbols();
46
+ const depGraph = repo.loadDeps();
47
+ const callGraph = repo.loadCallGraph();
48
+ const fileSummaries = summaryStorage.hasFileSummaries()
49
+ ? summaryStorage.loadFileSummaries()
50
+ : {};
51
+ const projectSummary = summaryStorage.loadProjectSummary();
52
+ const knowledgeEntries = (0, knowledge_1.filterValidAt)(new knowledge_1.KnowledgeStorage(rootDir).load());
53
+ if (options.since) {
54
+ const changedFiles = new Set((0, git_1.getChangedFiles)(options.since, rootDir));
55
+ if (changedFiles.size === 0) {
56
+ console.log(chalk_1.default.yellow(`No changed files since ${options.since}.`));
57
+ return;
58
+ }
59
+ chunks = chunks.filter((c) => changedFiles.has(c.file));
60
+ if (chunks.length === 0) {
61
+ console.log(chalk_1.default.yellow(`No indexed chunks in the diff since ${options.since}.`));
62
+ return;
63
+ }
64
+ console.log(chalk_1.default.gray(` Scope: ${changedFiles.size} changed file(s) since ${options.since}\n`));
65
+ }
66
+ if (chunks.length === 0) {
67
+ console.error(chalk_1.default.red("No index found. Run `vemora index` first."));
68
+ process.exit(1);
69
+ }
70
+ if (!options.query && !options.file) {
71
+ console.error(chalk_1.default.red("Provide --query <text> and/or --file <path>."));
72
+ process.exit(1);
73
+ }
74
+ let results = [];
75
+ let searchType = "none";
76
+ let tokensSavedSession = 0;
77
+ let tokensSavedDedup = 0;
78
+ let tokensSavedBudget = 0;
79
+ const usageStart = Date.now();
80
+ if (typeof options.query === "string" && options.query.length > 0) {
81
+ const useKeyword = options.keyword || config.embedding.provider === "none";
82
+ const useHybrid = options.hybrid;
83
+ // Symbol-aware routing: direct lookup for precise identifier queries.
84
+ const symbolHits = !useHybrid && !useKeyword
85
+ ? (0, vector_1.symbolLookup)(options.query, chunks, symbols)
86
+ : [];
87
+ if (symbolHits.length > 0) {
88
+ results = symbolHits;
89
+ searchType = "symbol";
90
+ }
91
+ else if (useKeyword) {
92
+ const spinner = (0, ora_1.default)("Performing hybrid search...").start();
93
+ try {
94
+ const cache = cacheStorage.load();
95
+ if (!cache ||
96
+ (cache.chunkIds?.length === 0 &&
97
+ Object.keys(cache.embeddings ?? {}).length === 0)) {
98
+ spinner.warn("No embeddings — falling back to BM25.");
99
+ results = (0, bm25_1.computeBM25Scores)(options.query, chunks, symbols, topK);
100
+ searchType = "bm25";
101
+ }
102
+ else {
103
+ const provider = (0, factory_1.createEmbeddingProvider)(config.embedding);
104
+ const [queryEmbedding] = await provider.embed([options.query]);
105
+ spinner.succeed("Embedded");
106
+ results = await (0, hybrid_1.hybridSearch)(options.query, queryEmbedding, chunks, cache, symbols, {
107
+ alpha: options.alpha,
108
+ topK,
109
+ });
110
+ searchType = "hybrid";
111
+ }
112
+ }
113
+ catch (err) {
114
+ spinner.fail(`Hybrid search failed: ${err.message}`);
115
+ results = (0, bm25_1.computeBM25Scores)(options.query, chunks, symbols, topK);
116
+ searchType = "bm25";
117
+ }
118
+ }
119
+ else {
120
+ const spinner = (0, ora_1.default)("Generating query embedding...").start();
121
+ try {
122
+ const cache = cacheStorage.load();
123
+ const cachedCount = cache
124
+ ? cache.chunkIds
125
+ ? cache.chunkIds.length
126
+ : Object.keys(cache.embeddings ?? {}).length
127
+ : 0;
128
+ if (!cache || cachedCount === 0) {
129
+ spinner.warn("No embeddings — falling back to BM25.");
130
+ results = (0, bm25_1.computeBM25Scores)(options.query, chunks, symbols, topK);
131
+ searchType = "bm25";
132
+ }
133
+ else {
134
+ const provider = (0, factory_1.createEmbeddingProvider)(config.embedding);
135
+ const [queryEmbedding] = await provider.embed([options.query]);
136
+ spinner.succeed("Embedded");
137
+ results = (0, vector_1.vectorSearch)(queryEmbedding, chunks, cache, symbols, topK);
138
+ searchType = "vector";
139
+ if (results.length === 0) {
140
+ results = (0, bm25_1.computeBM25Scores)(options.query, chunks, symbols, topK);
141
+ searchType = "bm25";
142
+ }
143
+ }
144
+ }
145
+ catch (err) {
146
+ spinner.fail(`Embedding failed: ${err.message}`);
147
+ results = (0, bm25_1.computeBM25Scores)(options.query, chunks, symbols, topK);
148
+ searchType = "bm25";
149
+ }
150
+ }
151
+ // ── Reranking (optional) ──────────────────────────────────────────────
152
+ if (options.rerank && results.length > 0) {
153
+ results = await (0, rerank_1.rerankResults)(options.query, results, topK, config.reranker, config.summarization?.model);
154
+ }
155
+ // ── MMR deduplication (optional) ─────────────────────────────────────
156
+ if (options.mmr && results.length > 1) {
157
+ const cache = cacheStorage.load();
158
+ results = (0, mmr_1.applyMMR)(results, cache, topK, options.lambda ?? 0.5);
159
+ }
160
+ // ── Adjacent chunk merge (optional) ──────────────────────────────────
161
+ if (options.merge && results.length > 1) {
162
+ results = (0, merge_1.mergeAdjacentChunks)(results, options.mergeGap);
163
+ }
164
+ // ── Session filter (optional) ─────────────────────────────────────────
165
+ const sessionStorage = new session_1.SessionStorage(config.projectId);
166
+ if (options.fresh)
167
+ sessionStorage.reset();
168
+ if ((options.session || options.fresh) && results.length > 0) {
169
+ const seenIds = sessionStorage.getSeenIds();
170
+ if (seenIds.size > 0) {
171
+ const tBefore = (0, tokenizer_1.sumResultTokens)(results);
172
+ const unseen = results.filter((r) => !seenIds.has(r.chunk.id));
173
+ results = unseen.length > 0 ? unseen : results.slice(0, 1);
174
+ tokensSavedSession = tBefore - (0, tokenizer_1.sumResultTokens)(results);
175
+ }
176
+ }
177
+ // ── Semantic deduplication (always-on) ───────────────────────────────
178
+ if (results.length > 1) {
179
+ const tBefore = (0, tokenizer_1.sumResultTokens)(results);
180
+ const cache = cacheStorage.load();
181
+ results = (0, merge_1.deduplicateBySimilarity)(results, cache);
182
+ tokensSavedDedup = tBefore - (0, tokenizer_1.sumResultTokens)(results);
183
+ }
184
+ // ── Token budget (optional) ───────────────────────────────────────────
185
+ if (options.budget && options.budget > 0) {
186
+ const tBefore = (0, tokenizer_1.sumResultTokens)(results);
187
+ results = (0, tokenizer_1.applyTokenBudget)(results, options.budget);
188
+ tokensSavedBudget = tBefore - (0, tokenizer_1.sumResultTokens)(results);
189
+ }
190
+ // ── Persist session ───────────────────────────────────────────────────
191
+ if ((options.session || options.fresh) && results.length > 0) {
192
+ sessionStorage.markSeen(results.map((r) => r.chunk.id));
193
+ }
194
+ // ── Record usage event ──────────────────────────────────────────────
195
+ try {
196
+ new usage_1.UsageStorage(config.projectId).append({
197
+ ts: new Date().toISOString(),
198
+ command: "context",
199
+ query: options.query?.slice(0, 120),
200
+ searchType,
201
+ format: options.format,
202
+ topK,
203
+ resultsReturned: results.length,
204
+ tokensReturned: (0, tokenizer_1.sumResultTokens)(results),
205
+ tokensSavedDedup,
206
+ tokensSavedSession,
207
+ tokensSavedBudget,
208
+ durationMs: Date.now() - usageStart,
209
+ topFiles: [...new Set(results.map((r) => r.chunk.file))].slice(0, 3),
210
+ });
211
+ }
212
+ catch {
213
+ // usage tracking is best-effort — never block the main output
214
+ }
215
+ }
216
+ const contextStr = generateContextString(config, results, depGraph, callGraph, fileSummaries, projectSummary ? projectSummary.overview : null, resolvedOptions, rootDir, chunks, knowledgeEntries);
217
+ console.log(contextStr);
218
+ }
219
+ /**
220
+ * Procedural function to build the actual context string.
221
+ * This is separated from runContext to allow for benchmarking and testing.
222
+ *
223
+ * When `options.structured` is true and results are available, delegates to
224
+ * `generateStructuredContextString` which organises output by semantic role
225
+ * (Entry Point → Direct Dependencies → Called By → Types & Interfaces → Related Patterns)
226
+ * instead of a flat ranked list.
227
+ *
228
+ * @param allChunks - Full chunk corpus, required for structured mode dep resolution.
229
+ * Optional for backward compatibility with bench.ts.
230
+ */
231
+ function generateContextString(config, results, depGraph, callGraph, fileSummaries, projectOverview, options, rootDir, allChunks = [], knowledgeEntries = []) {
232
+ if (options.structured && results.length > 0) {
233
+ return generateStructuredContextString(config, results, depGraph, callGraph, fileSummaries, projectOverview, options, rootDir, allChunks, knowledgeEntries);
234
+ }
235
+ if (options.format === "terse") {
236
+ const lines = [];
237
+ lines.push(`# ${config.projectName}`);
238
+ lines.push("");
239
+ const relevant = rankKnowledgeEntries(options.query, results, knowledgeEntries);
240
+ for (const entry of relevant) {
241
+ lines.push(`[${entry.category}] ${entry.title}: ${entry.body}`);
242
+ }
243
+ if (relevant.length > 0)
244
+ lines.push("");
245
+ lines.push((0, formatter_1.formatTerse)(results, {}));
246
+ return lines.join("\n");
247
+ }
248
+ const fmt = options.format ?? "markdown";
249
+ const lines = [];
250
+ const hr = fmt === "markdown" ? "---" : "=".repeat(60);
251
+ const importedByMap = (0, deps_1.computeImportedBy)(depGraph);
252
+ // ── Section: Project overview ──────────────────────────────────────────────
253
+ lines.push(fmt === "markdown"
254
+ ? `# AI Context — ${config.projectName}`
255
+ : `=== AI CONTEXT BLOCK — ${config.projectName.toUpperCase()} ===`);
256
+ lines.push("");
257
+ if (projectOverview) {
258
+ lines.push(fmt === "markdown" ? "## Project Overview" : "[Project Overview]");
259
+ lines.push("");
260
+ lines.push(projectOverview);
261
+ lines.push("");
262
+ lines.push(hr);
263
+ lines.push("");
264
+ }
265
+ // ── Section: Knowledge entries ────────────────────────────────────────────
266
+ const relevantKnowledge = rankKnowledgeEntries(options.query, results, knowledgeEntries);
267
+ if (relevantKnowledge.length > 0) {
268
+ lines.push(fmt === "markdown" ? "## Knowledge" : "[Knowledge]");
269
+ lines.push("");
270
+ for (const entry of relevantKnowledge) {
271
+ const badge = fmt === "markdown" ? `\`${entry.category}\`` : entry.category;
272
+ lines.push(fmt === "markdown"
273
+ ? `**${entry.title}** ${badge}`
274
+ : `${entry.title} [${entry.category}]`);
275
+ lines.push(entry.body);
276
+ if (entry.relatedFiles?.length) {
277
+ lines.push(fmt === "markdown"
278
+ ? `_Files: ${entry.relatedFiles.map((f) => `\`${f}\``).join(", ")}_`
279
+ : `Files: ${entry.relatedFiles.join(", ")}`);
280
+ }
281
+ lines.push("");
282
+ }
283
+ lines.push(hr);
284
+ lines.push("");
285
+ }
286
+ // ── Section: Specific file context ────────────────────────────────────────
287
+ if (options.file) {
288
+ const relFile = resolveRelPath(rootDir, options.file);
289
+ lines.push(fmt === "markdown" ? "## File Context" : "[File Context]");
290
+ lines.push("");
291
+ lines.push(fmt === "markdown" ? `**File:** \`${relFile}\`` : `File: ${relFile}`);
292
+ lines.push("");
293
+ // Full file content
294
+ const absPath = path_1.default.join(rootDir, relFile);
295
+ if (fs_1.default.existsSync(absPath)) {
296
+ // Resolve symlinks before reading to prevent traversal via symlinks
297
+ const realRoot = fs_1.default.realpathSync(path_1.default.resolve(rootDir));
298
+ const realPath = fs_1.default.realpathSync(absPath);
299
+ if (!realPath.startsWith(realRoot + path_1.default.sep) && realPath !== realRoot) {
300
+ throw new Error(`File path escapes project root via symlink: ${options.file}`);
301
+ }
302
+ const content = fs_1.default.readFileSync(realPath, "utf-8");
303
+ const ext = relFile.split(".").pop() ?? "";
304
+ if (fmt === "markdown") {
305
+ lines.push(`\`\`\`${ext}`);
306
+ lines.push(content);
307
+ lines.push("```");
308
+ }
309
+ else {
310
+ lines.push(content);
311
+ }
312
+ lines.push("");
313
+ }
314
+ else {
315
+ lines.push(fmt === "markdown" ? "_File not found._" : "(file not found)");
316
+ lines.push("");
317
+ }
318
+ // Imports and used-by for the file
319
+ const fileDeps = depGraph[relFile];
320
+ const usedBy = importedByMap.get(relFile) ?? [];
321
+ if (fileDeps?.imports.length) {
322
+ lines.push(fmt === "markdown"
323
+ ? "**Imports from project:**"
324
+ : "Imports from project:");
325
+ for (const imp of fileDeps.imports) {
326
+ const syms = imp.symbols.length > 0 ? ` (${imp.symbols.join(", ")})` : "";
327
+ lines.push(fmt === "markdown"
328
+ ? `- \`${imp.file}\`${syms}`
329
+ : ` - ${imp.file}${syms}`);
330
+ }
331
+ lines.push("");
332
+ }
333
+ if (usedBy.length > 0) {
334
+ lines.push(fmt === "markdown" ? "**Used by:**" : "Used by:");
335
+ for (const caller of usedBy) {
336
+ lines.push(fmt === "markdown" ? `- \`${caller}\`` : ` - ${caller}`);
337
+ }
338
+ lines.push("");
339
+ }
340
+ // Git commit history for this file
341
+ const commits = (0, git_1.getFileGitHistory)(rootDir, relFile);
342
+ if (commits.length > 0) {
343
+ lines.push(fmt === "markdown" ? "**Recent commits:**" : "Recent commits:");
344
+ for (const c of commits) {
345
+ lines.push(fmt === "markdown"
346
+ ? `- \`${c.sha}\` ${c.message} _(${c.author}, ${c.date})_`
347
+ : ` - ${c.sha} ${c.message} (${c.author}, ${c.date})`);
348
+ }
349
+ lines.push("");
350
+ }
351
+ // TODOs / FIXMEs in this file
352
+ const fileTodos = new repository_1.RepositoryStorage(rootDir).loadTodos().filter((t) => t.file === relFile);
353
+ if (fileTodos.length > 0) {
354
+ lines.push(fmt === "markdown" ? "**TODOs / FIXMEs:**" : "TODOs / FIXMEs:");
355
+ for (const t of fileTodos) {
356
+ lines.push(fmt === "markdown"
357
+ ? `- **${t.type}** (line ${t.line}): ${t.text}`
358
+ : ` - ${t.type} (line ${t.line}): ${t.text}`);
359
+ }
360
+ lines.push("");
361
+ }
362
+ // Test files linked to this source file
363
+ const allFileKeys = [...new Set(allChunks.map((c) => c.file))];
364
+ const testFiles = (0, tests_1.findTestFiles)(relFile, allFileKeys, importedByMap);
365
+ if (testFiles.length > 0) {
366
+ lines.push(fmt === "markdown" ? "**Test files:**" : "Test files:");
367
+ for (const tf of testFiles) {
368
+ lines.push(fmt === "markdown" ? `- \`${tf}\`` : ` - ${tf}`);
369
+ }
370
+ lines.push("");
371
+ }
372
+ // Caller context: symbols exported by this file and who calls them
373
+ const fileSymbols = [
374
+ ...new Set(allChunks
375
+ .filter((c) => c.file === relFile && c.symbol)
376
+ .map((c) => c.symbol)),
377
+ ];
378
+ const callerRows = [];
379
+ for (const sym of fileSymbols) {
380
+ const callInfo = callGraph[`${relFile}:${sym}`];
381
+ if (callInfo?.calledBy.length) {
382
+ const callers = callInfo.calledBy.slice(0, 5);
383
+ const rest = callInfo.calledBy.length - callers.length;
384
+ const callerList = fmt === "markdown"
385
+ ? callers.map((c) => `\`${c}\``).join(", ") +
386
+ (rest > 0 ? ` _+${rest} more_` : "")
387
+ : callers.join(", ") + (rest > 0 ? ` +${rest} more` : "");
388
+ callerRows.push(fmt === "markdown"
389
+ ? `- \`${sym}\` ← ${callerList}`
390
+ : ` - ${sym} <- ${callerList}`);
391
+ }
392
+ }
393
+ if (callerRows.length > 0) {
394
+ lines.push(fmt === "markdown" ? "**Symbol callers:**" : "Symbol callers:");
395
+ lines.push(...callerRows);
396
+ lines.push("");
397
+ }
398
+ lines.push(hr);
399
+ lines.push("");
400
+ }
401
+ // ── Section: Relevant code via query ──────────────────────────────────────
402
+ if (options.query) {
403
+ lines.push(fmt === "markdown" ? "## Relevant Code" : "[Relevant Code]");
404
+ lines.push("");
405
+ if (results.length === 0) {
406
+ lines.push("_No relevant results found._");
407
+ }
408
+ else {
409
+ const seen = new Set();
410
+ for (const { chunk, score, symbol } of results) {
411
+ if (seen.has(chunk.id))
412
+ continue;
413
+ seen.add(chunk.id);
414
+ const ext = chunk.file.split(".").pop() ?? "";
415
+ const symLabel = chunk.symbol
416
+ ? ` — ${symbol?.type ?? "symbol"} \`${chunk.symbol}\``
417
+ : "";
418
+ if (fmt === "markdown") {
419
+ lines.push(`### \`${chunk.file}\`${symLabel}`);
420
+ lines.push(`Lines ${chunk.start}–${chunk.end} · Score: ${score.toFixed(4)}`);
421
+ }
422
+ else {
423
+ lines.push(`[${chunk.file}${symLabel}]`);
424
+ lines.push(`lines ${chunk.start}–${chunk.end} score: ${score.toFixed(4)}`);
425
+ }
426
+ lines.push("");
427
+ // Deps context (only if graph available)
428
+ const fileDeps = depGraph[chunk.file];
429
+ const usedBy = importedByMap.get(chunk.file) ?? [];
430
+ if (fileDeps?.imports.length) {
431
+ lines.push(fmt === "markdown" ? "**Imports:**" : "Imports:");
432
+ for (const imp of fileDeps.imports.slice(0, 5)) {
433
+ const syms = imp.symbols.length > 0
434
+ ? ` (${imp.symbols.slice(0, 4).join(", ")})`
435
+ : "";
436
+ lines.push(fmt === "markdown"
437
+ ? `- \`${imp.file}\`${syms}`
438
+ : ` - ${imp.file}${syms}`);
439
+ }
440
+ lines.push("");
441
+ }
442
+ if (usedBy.length > 0) {
443
+ lines.push(fmt === "markdown" ? "**Used by:**" : "Used by:");
444
+ for (const caller of usedBy.slice(0, 3)) {
445
+ lines.push(fmt === "markdown" ? `- \`${caller}\`` : ` - ${caller}`);
446
+ }
447
+ lines.push("");
448
+ }
449
+ // Call Graph context
450
+ const symbolId = chunk.symbol ? `${chunk.file}:${chunk.symbol}` : null;
451
+ const callInfo = symbolId ? callGraph[symbolId] : null;
452
+ if (callInfo) {
453
+ if (callInfo.calls.length > 0) {
454
+ lines.push(fmt === "markdown" ? "**Calls:**" : "Calls:");
455
+ for (const call of callInfo.calls.slice(0, 5)) {
456
+ const loc = call.file ? ` (in \`${call.file}\`)` : "";
457
+ lines.push(fmt === "markdown"
458
+ ? `- \`${call.name}\`${loc}`
459
+ : ` - ${call.name}${loc}`);
460
+ }
461
+ lines.push("");
462
+ }
463
+ if (callInfo.calledBy.length > 0) {
464
+ lines.push(fmt === "markdown" ? "**Called by:**" : "Called by:");
465
+ for (const callerId of callInfo.calledBy.slice(0, 5)) {
466
+ lines.push(fmt === "markdown" ? `- \`${callerId}\`` : ` - ${callerId}`);
467
+ }
468
+ lines.push("");
469
+ }
470
+ }
471
+ // Code
472
+ const codeLines = chunk.content.split("\n");
473
+ const limit = options.showCode ? codeLines.length : signature_1.HIGH_CODE_LINES;
474
+ const preview = codeLines.slice(0, limit).join("\n");
475
+ const truncated = codeLines.length > limit;
476
+ if (fmt === "markdown") {
477
+ lines.push(`\`\`\`${ext}`);
478
+ lines.push(preview);
479
+ if (truncated)
480
+ lines.push(`// … (${codeLines.length - limit} more lines)`);
481
+ lines.push("```");
482
+ }
483
+ else {
484
+ lines.push(preview);
485
+ if (truncated)
486
+ lines.push(`... (${codeLines.length - limit} more lines)`);
487
+ }
488
+ lines.push("");
489
+ lines.push(hr);
490
+ lines.push("");
491
+ }
492
+ }
493
+ }
494
+ // ── Footer ────────────────────────────────────────────────────────────────
495
+ lines.push(fmt === "markdown"
496
+ ? `_Generated by \`vemora context\` — ${new Date().toISOString()}_`
497
+ : `=== END AI CONTEXT BLOCK — ${new Date().toISOString()} ===`);
498
+ return lines.join("\n");
499
+ }
500
+ // ─── Knowledge ranking ────────────────────────────────────────────────────────
501
+ /**
502
+ * Ranks knowledge entries by relevance to the current query and result set.
503
+ *
504
+ * Scoring (higher = more relevant):
505
+ * +10 relatedFiles overlaps with result files
506
+ * +8 relatedSymbols overlaps with result symbols
507
+ * +2 per query term found in title+body
508
+ * +4/3/2/1 category weight: gotcha > pattern > decision > glossary
509
+ * +1 confidence === 'high'
510
+ *
511
+ * Only entries with score > 0 are returned (max 5, sorted descending).
512
+ */
513
+ function rankKnowledgeEntries(query, results, entries, maxEntries = 5) {
514
+ if (entries.length === 0)
515
+ return [];
516
+ const resultFiles = new Set(results.map((r) => r.chunk.file));
517
+ const resultSymbols = new Set(results.flatMap((r) => (r.chunk.symbol ? [r.chunk.symbol] : [])));
518
+ const categoryWeight = {
519
+ gotcha: 4,
520
+ pattern: 3,
521
+ decision: 2,
522
+ glossary: 1,
523
+ };
524
+ const queryTerms = query
525
+ ? new Set(query
526
+ .toLowerCase()
527
+ .split(/[\s\W]+/)
528
+ .filter((t) => t.length >= 2))
529
+ : new Set();
530
+ const scored = entries.map((entry) => {
531
+ let score = 0;
532
+ if (entry.relatedFiles?.some((f) => resultFiles.has(f)))
533
+ score += 10;
534
+ if (entry.relatedSymbols?.some((s) => resultSymbols.has(s)))
535
+ score += 8;
536
+ if (queryTerms.size > 0) {
537
+ const text = `${entry.title} ${entry.body}`.toLowerCase();
538
+ for (const term of queryTerms) {
539
+ if (text.includes(term))
540
+ score += 2;
541
+ }
542
+ }
543
+ score += categoryWeight[entry.category] ?? 1;
544
+ if (entry.confidence === "high")
545
+ score += 1;
546
+ return { entry, score };
547
+ });
548
+ return scored
549
+ .filter((s) => s.score > (categoryWeight[s.entry.category] ?? 1)) // exclude pure category-weight matches
550
+ .sort((a, b) => b.score - a.score)
551
+ .slice(0, maxEntries)
552
+ .map((s) => s.entry);
553
+ }
554
+ // ─── Structured context renderer ─────────────────────────────────────────────
555
+ /**
556
+ * Renders a structured context block organised by semantic role:
557
+ *
558
+ * ## Entry Point — top-ranked chunk, full code
559
+ * ## Direct Dependencies — signatures of functions called by the entry point (call graph depth-1)
560
+ * ## Called By — callers of the entry point (contract context)
561
+ * ## Types & Interfaces — interface/type chunks from the result set
562
+ * ## Related Patterns — remaining result chunks (signatures to save tokens)
563
+ *
564
+ * This layout reduces token usage by ~20-35% compared to the flat list and
565
+ * helps LLMs navigate the codebase more effectively because the relationships
566
+ * are explicit rather than implied by rank.
567
+ */
568
+ function generateStructuredContextString(config, results, depGraph, callGraph, fileSummaries, projectOverview, options, _rootDir, allChunks, knowledgeEntries = []) {
569
+ const fmt = options.format ?? "markdown";
570
+ const hr = fmt === "markdown" ? "---" : "=".repeat(60);
571
+ const lines = [];
572
+ // ── Header ────────────────────────────────────────────────────────────────
573
+ lines.push(fmt === "markdown"
574
+ ? `# AI Context — ${config.projectName}`
575
+ : `=== AI CONTEXT BLOCK — ${config.projectName.toUpperCase()} ===`);
576
+ if (options.query) {
577
+ lines.push("");
578
+ lines.push(fmt === "markdown"
579
+ ? `> **Query:** ${options.query}`
580
+ : `Query: ${options.query}`);
581
+ }
582
+ lines.push("");
583
+ if (projectOverview) {
584
+ lines.push(fmt === "markdown" ? "## Project Overview" : "[Project Overview]");
585
+ lines.push("");
586
+ lines.push(projectOverview);
587
+ lines.push("");
588
+ lines.push(hr);
589
+ lines.push("");
590
+ }
591
+ // ── Section: Knowledge entries ────────────────────────────────────────────
592
+ const relevantKnowledge = rankKnowledgeEntries(options.query, results, knowledgeEntries);
593
+ if (relevantKnowledge.length > 0) {
594
+ lines.push(fmt === "markdown" ? "## Knowledge" : "[Knowledge]");
595
+ lines.push("");
596
+ for (const entry of relevantKnowledge) {
597
+ const badge = fmt === "markdown" ? `\`${entry.category}\`` : entry.category;
598
+ lines.push(fmt === "markdown"
599
+ ? `**${entry.title}** ${badge}`
600
+ : `${entry.title} [${entry.category}]`);
601
+ lines.push(entry.body);
602
+ if (entry.relatedFiles?.length) {
603
+ lines.push(fmt === "markdown"
604
+ ? `_Files: ${entry.relatedFiles.map((f) => `\`${f}\``).join(", ")}_`
605
+ : `Files: ${entry.relatedFiles.join(", ")}`);
606
+ }
607
+ lines.push("");
608
+ }
609
+ lines.push(hr);
610
+ lines.push("");
611
+ }
612
+ // ── Partition results ─────────────────────────────────────────────────────
613
+ const seen = new Set();
614
+ const dedupedResults = results.filter((r) => {
615
+ if (seen.has(r.chunk.id))
616
+ return false;
617
+ seen.add(r.chunk.id);
618
+ return true;
619
+ });
620
+ const topResult = dedupedResults[0];
621
+ const typeResults = dedupedResults
622
+ .slice(1)
623
+ .filter((r) => r.symbol?.type === "interface" || r.symbol?.type === "type");
624
+ const relatedResults = dedupedResults
625
+ .slice(1)
626
+ .filter((r) => r.symbol?.type !== "interface" && r.symbol?.type !== "type");
627
+ // ── Section 1: Entry Point ────────────────────────────────────────────────
628
+ lines.push(fmt === "markdown" ? "## Entry Point" : "[Entry Point]");
629
+ lines.push("");
630
+ {
631
+ const { chunk, score, symbol } = topResult;
632
+ const ext = chunk.file.split(".").pop() ?? "";
633
+ const symLabel = chunk.symbol
634
+ ? ` — ${symbol?.type ?? "symbol"} \`${chunk.symbol}\``
635
+ : "";
636
+ lines.push(fmt === "markdown"
637
+ ? `**\`${chunk.file}\`**${symLabel}`
638
+ : `${chunk.file}${symLabel}`);
639
+ lines.push(`Lines ${chunk.start}–${chunk.end} · Score: ${score.toFixed(4)}`);
640
+ lines.push("");
641
+ const codeLines = chunk.content.split("\n");
642
+ const limit = options.showCode ? codeLines.length : signature_1.HIGH_CODE_LINES;
643
+ const preview = codeLines.slice(0, limit).join("\n");
644
+ lines.push(fmt === "markdown" ? `\`\`\`${ext}` : "");
645
+ lines.push(preview);
646
+ if (codeLines.length > limit) {
647
+ lines.push(`// … (${codeLines.length - limit} more lines — use --show-code to expand)`);
648
+ }
649
+ if (fmt === "markdown")
650
+ lines.push("```");
651
+ lines.push("");
652
+ }
653
+ lines.push(hr);
654
+ lines.push("");
655
+ // ── Section 2: Direct Dependencies (call graph depth-1) ───────────────────
656
+ const topSymbolId = topResult.chunk.symbol
657
+ ? `${topResult.chunk.file}:${topResult.chunk.symbol}`
658
+ : null;
659
+ const callInfo = topSymbolId ? callGraph[topSymbolId] : null;
660
+ if (callInfo?.calls.length) {
661
+ lines.push(fmt === "markdown" ? "## Direct Dependencies" : "[Direct Dependencies]");
662
+ lines.push("");
663
+ let depCount = 0;
664
+ for (const call of callInfo.calls.slice(0, 8)) {
665
+ // Look up a matching chunk (prefer same file if provided)
666
+ const matching = allChunks.find((c) => c.symbol === call.name && (!call.file || c.file === call.file)) ?? allChunks.find((c) => c.symbol === call.name);
667
+ const chunkFile = matching?.file ?? call.file ?? "(external)";
668
+ const ext = chunkFile.split(".").pop() ?? "";
669
+ const sig = matching
670
+ ? (0, signature_1.extractSignature)(matching.content)
671
+ : `// ${call.name} — not found in index`;
672
+ lines.push(fmt === "markdown"
673
+ ? `**\`${chunkFile}\`** — \`${call.name}\``
674
+ : `${chunkFile} — ${call.name}`);
675
+ lines.push("");
676
+ lines.push(fmt === "markdown" ? `\`\`\`${ext}` : "");
677
+ lines.push(sig);
678
+ if (fmt === "markdown")
679
+ lines.push("```");
680
+ lines.push("");
681
+ depCount++;
682
+ }
683
+ const hiddenDeps = callInfo.calls.length - Math.min(callInfo.calls.length, 8);
684
+ if (hiddenDeps > 0) {
685
+ lines.push(fmt === "markdown"
686
+ ? `_…and ${hiddenDeps} more calls (see call graph)_`
687
+ : `...and ${hiddenDeps} more calls`);
688
+ lines.push("");
689
+ }
690
+ if (depCount > 0) {
691
+ lines.push(hr);
692
+ lines.push("");
693
+ }
694
+ }
695
+ // ── Section 3: Called By ──────────────────────────────────────────────────
696
+ if (callInfo?.calledBy.length) {
697
+ lines.push(fmt === "markdown" ? "## Called By" : "[Called By]");
698
+ lines.push("");
699
+ for (const callerId of callInfo.calledBy.slice(0, 6)) {
700
+ lines.push(fmt === "markdown" ? `- \`${callerId}\`` : ` - ${callerId}`);
701
+ }
702
+ if (callInfo.calledBy.length > 6) {
703
+ lines.push(fmt === "markdown"
704
+ ? `- _…and ${callInfo.calledBy.length - 6} more_`
705
+ : ` ...and ${callInfo.calledBy.length - 6} more`);
706
+ }
707
+ lines.push("");
708
+ lines.push(hr);
709
+ lines.push("");
710
+ }
711
+ // ── Section 4: Types & Interfaces ────────────────────────────────────────
712
+ if (typeResults.length > 0) {
713
+ lines.push(fmt === "markdown" ? "## Types & Interfaces" : "[Types & Interfaces]");
714
+ lines.push("");
715
+ for (const { chunk } of typeResults) {
716
+ const ext = chunk.file.split(".").pop() ?? "";
717
+ lines.push(fmt === "markdown"
718
+ ? `**\`${chunk.file}\`** — \`${chunk.symbol}\``
719
+ : `${chunk.file} — ${chunk.symbol}`);
720
+ lines.push("");
721
+ lines.push(fmt === "markdown" ? `\`\`\`${ext}` : "");
722
+ lines.push((0, signature_1.extractSignature)(chunk.content));
723
+ if (fmt === "markdown")
724
+ lines.push("```");
725
+ lines.push("");
726
+ }
727
+ lines.push(hr);
728
+ lines.push("");
729
+ }
730
+ // ── Section 5: Related Patterns ───────────────────────────────────────────
731
+ if (relatedResults.length > 0) {
732
+ lines.push(fmt === "markdown" ? "## Related Patterns" : "[Related Patterns]");
733
+ lines.push("");
734
+ for (const { chunk, score, symbol } of relatedResults) {
735
+ const ext = chunk.file.split(".").pop() ?? "";
736
+ const symLabel = chunk.symbol
737
+ ? ` — ${symbol?.type ?? "symbol"} \`${chunk.symbol}\``
738
+ : "";
739
+ lines.push(fmt === "markdown"
740
+ ? `### \`${chunk.file}\`${symLabel}`
741
+ : `${chunk.file}${symLabel}`);
742
+ lines.push(`Score: ${score.toFixed(4)}`);
743
+ lines.push("");
744
+ // Use signature to keep tokens low for secondary results
745
+ const display = chunk.symbol
746
+ ? (0, signature_1.extractSignature)(chunk.content)
747
+ : chunk.content.split("\n").slice(0, 10).join("\n") +
748
+ (chunk.content.split("\n").length > 10 ? "\n// …" : "");
749
+ lines.push(fmt === "markdown" ? `\`\`\`${ext}` : "");
750
+ lines.push(display);
751
+ if (fmt === "markdown")
752
+ lines.push("```");
753
+ lines.push("");
754
+ }
755
+ lines.push(hr);
756
+ lines.push("");
757
+ }
758
+ // ── Footer ────────────────────────────────────────────────────────────────
759
+ lines.push(fmt === "markdown"
760
+ ? `_Generated by \`vemora context --structured\` — ${new Date().toISOString()}_`
761
+ : `=== END AI CONTEXT BLOCK — ${new Date().toISOString()} ===`);
762
+ return lines.join("\n");
763
+ }
764
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
765
+ /**
766
+ * Resolve a file argument to a project-relative path.
767
+ *
768
+ * Accepts:
769
+ * - Absolute paths → relativised against rootDir
770
+ * - Paths already relative to rootDir → used as-is if the file exists
771
+ * - Paths relative to cwd → resolved to absolute, then relativised
772
+ */
773
+ function resolveRelPath(rootDir, filePath) {
774
+ const resolvedRoot = path_1.default.resolve(rootDir);
775
+ let absPath;
776
+ if (path_1.default.isAbsolute(filePath)) {
777
+ absPath = filePath;
778
+ }
779
+ else {
780
+ // Try relative to rootDir first, then fall back to cwd
781
+ const fromRoot = path_1.default.join(resolvedRoot, filePath);
782
+ absPath = fs_1.default.existsSync(fromRoot)
783
+ ? fromRoot
784
+ : path_1.default.resolve(process.cwd(), filePath);
785
+ }
786
+ // Guard: reject paths that escape the project root (path traversal)
787
+ const normalised = path_1.default.resolve(absPath);
788
+ if (!normalised.startsWith(resolvedRoot + path_1.default.sep) &&
789
+ normalised !== resolvedRoot) {
790
+ throw new Error(`File path escapes project root: ${filePath}`);
791
+ }
792
+ return path_1.default.relative(resolvedRoot, normalised);
793
+ }
794
+ //# sourceMappingURL=context.js.map