@bahulam/code 2.6.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 (273) hide show
  1. package/README.md +80 -0
  2. package/package.json +49 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +222 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +306 -0
  136. package/src/commands/agent.mjs +220 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +200 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/settings-loader.mjs +45 -0
  143. package/src/config/settings.mjs +132 -0
  144. package/src/context/ast-parser.mjs +298 -0
  145. package/src/context/bm25.mjs +85 -0
  146. package/src/context/retriever.mjs +308 -0
  147. package/src/context/skeleton.mjs +134 -0
  148. package/src/context/symbol-indexer.mjs +375 -0
  149. package/src/core/agent-history.mjs +111 -0
  150. package/src/core/agent-loop.mjs +486 -0
  151. package/src/core/approval-log.mjs +104 -0
  152. package/src/core/approval.mjs +476 -0
  153. package/src/core/attachments.mjs +380 -0
  154. package/src/core/backend-url.mjs +55 -0
  155. package/src/core/cache-control.mjs +92 -0
  156. package/src/core/cache.mjs +105 -0
  157. package/src/core/callback-client.mjs +180 -0
  158. package/src/core/checkpoints.mjs +142 -0
  159. package/src/core/compact-history.mjs +127 -0
  160. package/src/core/context-envelope.mjs +54 -0
  161. package/src/core/context-manager.mjs +198 -0
  162. package/src/core/error-guidance.mjs +311 -0
  163. package/src/core/file-diff.mjs +217 -0
  164. package/src/core/headless.mjs +448 -0
  165. package/src/core/hooks-manager.mjs +87 -0
  166. package/src/core/jsonl-writer.mjs +449 -0
  167. package/src/core/local-agent.mjs +537 -0
  168. package/src/core/local-store.mjs +836 -0
  169. package/src/core/mode-selector.mjs +51 -0
  170. package/src/core/output-filter.mjs +177 -0
  171. package/src/core/paths.mjs +190 -0
  172. package/src/core/policy-resolver.mjs +156 -0
  173. package/src/core/pricing.mjs +336 -0
  174. package/src/core/project-artifacts.mjs +39 -0
  175. package/src/core/project-context-loader.mjs +139 -0
  176. package/src/core/providers.mjs +219 -0
  177. package/src/core/rate-limit-display.mjs +121 -0
  178. package/src/core/rate-limiter.mjs +119 -0
  179. package/src/core/resume-mode.mjs +192 -0
  180. package/src/core/risk-tier.mjs +337 -0
  181. package/src/core/safety.mjs +203 -0
  182. package/src/core/scheduler.mjs +173 -0
  183. package/src/core/session-manager.mjs +360 -0
  184. package/src/core/session.mjs +143 -0
  185. package/src/core/settings-sync.mjs +85 -0
  186. package/src/core/stagnation.mjs +57 -0
  187. package/src/core/stream-client.mjs +829 -0
  188. package/src/core/streaming.mjs +182 -0
  189. package/src/core/system-prompt.mjs +140 -0
  190. package/src/core/tasks.mjs +196 -0
  191. package/src/core/tool-executor.mjs +1950 -0
  192. package/src/core/trust.mjs +158 -0
  193. package/src/core/work-scope.mjs +248 -0
  194. package/src/hooks/engine.mjs +162 -0
  195. package/src/index.mjs +426 -0
  196. package/src/mcp/client.mjs +253 -0
  197. package/src/mcp/transport-shttp.mjs +130 -0
  198. package/src/mcp/transport-sse.mjs +131 -0
  199. package/src/mcp/transport-ws.mjs +134 -0
  200. package/src/onboarding/preflight.mjs +360 -0
  201. package/src/permissions/checker.mjs +57 -0
  202. package/src/permissions/command-classifier.mjs +652 -0
  203. package/src/permissions/injection-check.mjs +60 -0
  204. package/src/permissions/path-check.mjs +102 -0
  205. package/src/permissions/prompt.mjs +73 -0
  206. package/src/permissions/sandbox.mjs +112 -0
  207. package/src/plugins/loader.mjs +138 -0
  208. package/src/skills/installer.mjs +188 -0
  209. package/src/skills/loader.mjs +252 -0
  210. package/src/skills/runner.mjs +55 -0
  211. package/src/state/orbit.mjs +263 -0
  212. package/src/state/verbosity.mjs +99 -0
  213. package/src/telemetry/index.mjs +96 -0
  214. package/src/terminal/agents.mjs +177 -0
  215. package/src/terminal/analytics.mjs +292 -0
  216. package/src/terminal/ansi.mjs +695 -0
  217. package/src/terminal/init.mjs +145 -0
  218. package/src/terminal/main.mjs +269 -0
  219. package/src/terminal/repl-explore.mjs +35 -0
  220. package/src/terminal/repl-format.mjs +257 -0
  221. package/src/terminal/repl-render.mjs +561 -0
  222. package/src/terminal/repl-resume.mjs +625 -0
  223. package/src/terminal/repl-state.mjs +103 -0
  224. package/src/terminal/repl-utils.mjs +34 -0
  225. package/src/terminal/repl.mjs +3832 -0
  226. package/src/terminal/skills.mjs +54 -0
  227. package/src/terminal/tool-display.mjs +240 -0
  228. package/src/tools/agent.mjs +137 -0
  229. package/src/tools/ask-user.mjs +61 -0
  230. package/src/tools/bash.mjs +231 -0
  231. package/src/tools/cron-create.mjs +120 -0
  232. package/src/tools/cron-delete.mjs +49 -0
  233. package/src/tools/cron-list.mjs +37 -0
  234. package/src/tools/edit.mjs +82 -0
  235. package/src/tools/enter-worktree.mjs +69 -0
  236. package/src/tools/exit-worktree.mjs +57 -0
  237. package/src/tools/glob.mjs +117 -0
  238. package/src/tools/grep.mjs +129 -0
  239. package/src/tools/lint.mjs +71 -0
  240. package/src/tools/ls.mjs +58 -0
  241. package/src/tools/lsp.mjs +115 -0
  242. package/src/tools/multi-edit.mjs +94 -0
  243. package/src/tools/notebook-edit.mjs +96 -0
  244. package/src/tools/project-overview.mjs +641 -0
  245. package/src/tools/read-mcp-resource.mjs +57 -0
  246. package/src/tools/read.mjs +138 -0
  247. package/src/tools/registry.mjs +116 -0
  248. package/src/tools/remote-trigger.mjs +84 -0
  249. package/src/tools/send-message.mjs +64 -0
  250. package/src/tools/skill.mjs +52 -0
  251. package/src/tools/test-runner.mjs +49 -0
  252. package/src/tools/todo-write.mjs +68 -0
  253. package/src/tools/tool-search.mjs +77 -0
  254. package/src/tools/web-fetch.mjs +65 -0
  255. package/src/tools/web-search.mjs +89 -0
  256. package/src/tools/write.mjs +55 -0
  257. package/src/ui/approval.mjs +263 -0
  258. package/src/ui/banner.mjs +235 -0
  259. package/src/ui/commands.mjs +537 -0
  260. package/src/ui/formatter.mjs +409 -0
  261. package/src/ui/icons.mjs +164 -0
  262. package/src/ui/input-dock.mjs +444 -0
  263. package/src/ui/markdown.mjs +278 -0
  264. package/src/ui/mission-report.mjs +296 -0
  265. package/src/ui/palette.mjs +189 -0
  266. package/src/ui/slash-commands.mjs +245 -0
  267. package/src/ui/spinner.mjs +116 -0
  268. package/src/ui/sub-agent.mjs +152 -0
  269. package/src/ui/term.mjs +159 -0
  270. package/src/ui/text-layout.mjs +127 -0
  271. package/src/ui/tool-card.mjs +463 -0
  272. package/src/ui/tool-details.mjs +312 -0
  273. package/src/ui/transcript-block.mjs +21 -0
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Context Retriever — T20: Unified context retrieval with BM25.
3
+ * Indexes project files and retrieves relevant chunks for LLM context.
4
+ */
5
+
6
+ import { BM25Index } from './bm25.mjs';
7
+ import { SymbolIndexer } from './symbol-indexer.mjs';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import { indexDir as getIndexDir } from '../core/paths.mjs';
11
+
12
+ const IGNORED_DIRS = new Set(['.git', 'node_modules', '.bahulam', '.kepler', '__pycache__', '.venv', 'venv', 'dist', 'build', '.next']);
13
+ const CODE_EXTS = new Set(['.js', '.mjs', '.ts', '.tsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.c', '.cpp', '.h', '.css', '.html', '.json', '.yaml', '.yml', '.toml', '.md', '.sh']);
14
+ const SYMBOL_EXTS = new Set(['.py', '.js', '.mjs', '.ts', '.tsx', '.jsx', '.go', '.rs']);
15
+ const MAX_FILE_SIZE = 100_000; // 100KB
16
+ const CHUNK_LINES = 50;
17
+ const CHUNK_OVERLAP = 10;
18
+
19
+ export class ContextRetriever {
20
+ constructor(projectDir = process.cwd()) {
21
+ this.projectDir = projectDir;
22
+ this.indexDir = getIndexDir(projectDir);
23
+ this.index = null;
24
+ this.symbolIndexer = null;
25
+ this.chunkTexts = new Map(); // id → original text content
26
+ }
27
+
28
+ /** Build or rebuild the search index (BM25 chunks + symbol index). */
29
+ async buildIndex() {
30
+ const files = this._scanFiles(this.projectDir);
31
+ const documents = [];
32
+
33
+ // Symbol indexer for AST-based search
34
+ this.symbolIndexer = new SymbolIndexer();
35
+ await this.symbolIndexer.init();
36
+
37
+ for (const filePath of files) {
38
+ try {
39
+ const content = fs.readFileSync(filePath, 'utf-8');
40
+ const relPath = path.relative(this.projectDir, filePath);
41
+
42
+ // BM25 chunks (existing behavior)
43
+ const chunks = this._chunkFile(content, relPath);
44
+ documents.push(...chunks);
45
+
46
+ // Symbol extraction for code files
47
+ const ext = path.extname(filePath).toLowerCase();
48
+ if (SYMBOL_EXTS.has(ext)) {
49
+ await this.symbolIndexer.indexFile(relPath, content);
50
+ }
51
+ } catch { /* skip unreadable files */ }
52
+ }
53
+
54
+ this.index = new BM25Index();
55
+ this.index.buildIndex(documents);
56
+
57
+ // Store chunk texts for retrieval
58
+ this.chunkTexts = new Map();
59
+ for (const doc of documents) {
60
+ this.chunkTexts.set(doc.id, doc.text);
61
+ }
62
+
63
+ // Persist
64
+ if (!fs.existsSync(this.indexDir)) fs.mkdirSync(this.indexDir, { recursive: true });
65
+ fs.writeFileSync(path.join(this.indexDir, 'bm25.json'), JSON.stringify(this.index.toJSON()));
66
+ fs.writeFileSync(path.join(this.indexDir, 'chunks.json'), JSON.stringify(Object.fromEntries(this.chunkTexts)));
67
+ fs.writeFileSync(path.join(this.indexDir, 'symbols.json'), JSON.stringify(this.symbolIndexer.toJSON()));
68
+
69
+ return { fileCount: files.length, chunkCount: documents.length, symbolCount: this.symbolIndexer.symbolCount };
70
+ }
71
+
72
+ /**
73
+ * Incrementally update index for a single changed file.
74
+ * Re-chunks the file and replaces its entries in the BM25 index.
75
+ * ~5-50ms per file — safe to call after every edit/write.
76
+ */
77
+ updateFile(filePath) {
78
+ if (!this.index) {
79
+ if (!this.loadIndex()) return false;
80
+ }
81
+
82
+ const absPath = path.resolve(filePath);
83
+ const relPath = path.relative(this.projectDir, absPath);
84
+
85
+ // Remove old chunks for this file
86
+ const oldIds = new Set();
87
+ for (const doc of this.index.docs) {
88
+ if (doc.id === relPath || doc.id.startsWith(relPath + ':')) {
89
+ oldIds.add(doc.id);
90
+ }
91
+ }
92
+
93
+ // Collect remaining documents (excluding old chunks for this file)
94
+ const remainingDocs = [];
95
+ for (const doc of this.index.docs) {
96
+ if (!oldIds.has(doc.id)) {
97
+ // Reconstruct text from tf map for rebuild
98
+ remainingDocs.push({ id: doc.id, text: this.chunkTexts.get(doc.id) || '' });
99
+ }
100
+ }
101
+
102
+ // Remove old chunk texts
103
+ for (const id of oldIds) this.chunkTexts.delete(id);
104
+
105
+ // Re-chunk if file still exists (delete = just remove)
106
+ const newChunks = [];
107
+ if (fs.existsSync(absPath)) {
108
+ try {
109
+ const content = fs.readFileSync(absPath, 'utf-8');
110
+ newChunks.push(...this._chunkFile(content, relPath));
111
+ } catch { /* skip unreadable */ }
112
+ }
113
+
114
+ // Rebuild index from remaining + new chunks
115
+ // This is fast (~5-20ms) because BM25 build is O(n) on token count
116
+ const allDocs = [...remainingDocs, ...newChunks];
117
+ this.index = new BM25Index();
118
+ this.index.buildIndex(allDocs);
119
+
120
+ // Update chunk texts
121
+ for (const chunk of newChunks) {
122
+ this.chunkTexts.set(chunk.id, chunk.text);
123
+ }
124
+
125
+ // Persist updated index
126
+ try {
127
+ if (!fs.existsSync(this.indexDir)) fs.mkdirSync(this.indexDir, { recursive: true });
128
+ fs.writeFileSync(path.join(this.indexDir, 'bm25.json'), JSON.stringify(this.index.toJSON()));
129
+ fs.writeFileSync(path.join(this.indexDir, 'chunks.json'), JSON.stringify(Object.fromEntries(this.chunkTexts)));
130
+ } catch { /* best-effort persist */ }
131
+
132
+ return true;
133
+ }
134
+
135
+ /** Load persisted index. */
136
+ loadIndex() {
137
+ const indexPath = path.join(this.indexDir, 'bm25.json');
138
+ const chunksPath = path.join(this.indexDir, 'chunks.json');
139
+ const symbolsPath = path.join(this.indexDir, 'symbols.json');
140
+ if (!fs.existsSync(indexPath)) return false;
141
+ try {
142
+ const data = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
143
+ this.index = BM25Index.fromJSON(data);
144
+
145
+ if (fs.existsSync(chunksPath)) {
146
+ const chunks = JSON.parse(fs.readFileSync(chunksPath, 'utf-8'));
147
+ this.chunkTexts = new Map(Object.entries(chunks));
148
+ }
149
+
150
+ if (fs.existsSync(symbolsPath)) {
151
+ const symData = JSON.parse(fs.readFileSync(symbolsPath, 'utf-8'));
152
+ this.symbolIndexer = SymbolIndexer.fromJSON(symData);
153
+ }
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Search symbols (functions, classes, methods) by query.
162
+ * Returns structured results with file:line, signature, parent class.
163
+ */
164
+ searchSymbols(query, topK = 5) {
165
+ if (!this.symbolIndexer) return [];
166
+ return this.symbolIndexer.search(query, topK);
167
+ }
168
+
169
+ /**
170
+ * Format symbol search results for the agent.
171
+ */
172
+ formatSymbolResults(results) {
173
+ if (!this.symbolIndexer || !results.length) return '';
174
+ return this.symbolIndexer.formatResults(results);
175
+ }
176
+
177
+ /** Retrieve relevant context chunks for a query, with full text. */
178
+ retrieve(query, topK = 10) {
179
+ if (!this.index) {
180
+ if (!this.loadIndex()) return [];
181
+ }
182
+ const results = this.index.search(query, topK);
183
+ // Attach chunk text to results
184
+ return results.map(r => ({
185
+ ...r,
186
+ text: this.chunkTexts.get(r.id) || `[File: ${r.id}]`,
187
+ }));
188
+ }
189
+
190
+ /** Scan project files respecting .gitignore-like patterns. */
191
+ _scanFiles(dir, depth = 0) {
192
+ if (depth > 15) return [];
193
+ const results = [];
194
+ let entries;
195
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
196
+
197
+ for (const entry of entries) {
198
+ if (entry.name.startsWith('.') && IGNORED_DIRS.has(entry.name)) continue;
199
+ if (IGNORED_DIRS.has(entry.name)) continue;
200
+
201
+ const fullPath = path.join(dir, entry.name);
202
+ if (entry.isDirectory()) {
203
+ results.push(...this._scanFiles(fullPath, depth + 1));
204
+ } else if (entry.isFile()) {
205
+ const ext = path.extname(entry.name);
206
+ if (!CODE_EXTS.has(ext)) continue;
207
+ try {
208
+ const stat = fs.statSync(fullPath);
209
+ if (stat.size > MAX_FILE_SIZE) continue;
210
+ } catch { continue; }
211
+ results.push(fullPath);
212
+ }
213
+ }
214
+ return results;
215
+ }
216
+
217
+ /**
218
+ * Chunk a file by AST boundaries (functions/classes) with line-based fallback.
219
+ * AST-aware chunks contain complete functions/classes — not arbitrary 50-line blocks.
220
+ */
221
+ _chunkFile(content, relPath) {
222
+ const lines = content.split('\n');
223
+
224
+ // Small files: single chunk
225
+ if (lines.length <= CHUNK_LINES) {
226
+ return [{ id: relPath, text: `${relPath}\n${content}` }];
227
+ }
228
+
229
+ // Try AST-aware chunking: split at function/class boundaries
230
+ const boundaries = this._findASTBoundaries(lines);
231
+
232
+ if (boundaries.length > 1) {
233
+ // AST-aware: chunk at function/class boundaries
234
+ const chunks = [];
235
+ for (const { name, startLine, endLine } of boundaries) {
236
+ const chunk = lines.slice(startLine, endLine).join('\n');
237
+ const id = `${relPath}:${startLine + 1}:${name}`;
238
+ chunks.push({ id, text: `${relPath}:${startLine + 1} (${name})\n${chunk}` });
239
+ }
240
+ return chunks;
241
+ }
242
+
243
+ // Fallback: line-based chunking for non-code files
244
+ const chunks = [];
245
+ for (let i = 0; i < lines.length; i += (CHUNK_LINES - CHUNK_OVERLAP)) {
246
+ const chunk = lines.slice(i, i + CHUNK_LINES).join('\n');
247
+ chunks.push({ id: `${relPath}:${i + 1}`, text: `${relPath}:${i + 1}\n${chunk}` });
248
+ }
249
+ return chunks;
250
+ }
251
+
252
+ /**
253
+ * Find function/class boundaries using regex patterns.
254
+ * Returns array of { name, startLine, endLine }.
255
+ */
256
+ _findASTBoundaries(lines) {
257
+ const boundaries = [];
258
+ const patterns = [
259
+ // Python: def/class (indentation-based)
260
+ /^(?:async\s+)?def\s+(\w+)\s*\(/,
261
+ /^class\s+(\w+)/,
262
+ // JS/TS: function, class, export function
263
+ /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
264
+ /^(?:export\s+)?class\s+(\w+)/,
265
+ // JS/TS: const fn = arrow
266
+ /^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(/,
267
+ // Go: func
268
+ /^func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(/,
269
+ // Rust: fn, struct, impl
270
+ /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
271
+ /^(?:pub\s+)?struct\s+(\w+)/,
272
+ ];
273
+
274
+ let currentStart = 0;
275
+ let currentName = '_top_level';
276
+
277
+ for (let i = 0; i < lines.length; i++) {
278
+ const line = lines[i].trimStart();
279
+ for (const pattern of patterns) {
280
+ const match = line.match(pattern);
281
+ if (match) {
282
+ // Close previous boundary
283
+ if (i > currentStart) {
284
+ boundaries.push({
285
+ name: currentName,
286
+ startLine: currentStart,
287
+ endLine: i,
288
+ });
289
+ }
290
+ currentStart = i;
291
+ currentName = match[1] || match[2] || 'unknown';
292
+ break;
293
+ }
294
+ }
295
+ }
296
+
297
+ // Close last boundary
298
+ if (lines.length > currentStart) {
299
+ boundaries.push({
300
+ name: currentName,
301
+ startLine: currentStart,
302
+ endLine: lines.length,
303
+ });
304
+ }
305
+
306
+ return boundaries;
307
+ }
308
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Project Skeleton — lightweight codebase overview for LLM context.
3
+ *
4
+ * Generates a compact representation of the project:
5
+ * - File tree (directories + file names)
6
+ * - Function/class signatures extracted via regex (not full AST)
7
+ *
8
+ * Designed to be ~500-1000 tokens — gives the model a "map" so it knows
9
+ * where to look without reading every file.
10
+ */
11
+
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+
15
+ const IGNORED_DIRS = new Set(['.git', 'node_modules', '.bahulam', '__pycache__', '.venv', 'venv', 'dist', 'build', '.next', '.cache', 'coverage', '.tox']);
16
+ const CODE_EXTS = new Set(['.js', '.mjs', '.ts', '.tsx', '.py', '.go', '.rs', '.java', '.rb', '.c', '.cpp', '.h']);
17
+ const MAX_FILE_SIZE = 200_000;
18
+
19
+ // Regex patterns for function/class signatures (multi-language)
20
+ const SIGNATURE_PATTERNS = [
21
+ // Python: def/class/async def
22
+ /^(?:async\s+)?(?:def|class)\s+(\w+)\s*[\(:].*$/gm,
23
+ // JS/TS: function, class, export function, const fn =
24
+ /^(?:export\s+)?(?:async\s+)?(?:function\s+(\w+)|class\s+(\w+))/gm,
25
+ // Go: func
26
+ /^func\s+(?:\(.*?\)\s+)?(\w+)\s*\(/gm,
27
+ // Rust: fn, struct, impl
28
+ /^(?:pub\s+)?(?:fn|struct|impl)\s+(\w+)/gm,
29
+ ];
30
+
31
+ /**
32
+ * Build a project skeleton — file tree + key signatures.
33
+ * @param {string} projectDir
34
+ * @param {Object} [options]
35
+ * @param {number} [options.maxFiles=200] — max files to include
36
+ * @param {number} [options.maxChars=4000] — max total chars (~1000 tokens)
37
+ * @returns {string} — skeleton text for LLM context
38
+ */
39
+ export function buildProjectSkeleton(projectDir, { maxFiles = 200, maxChars = 4000 } = {}) {
40
+ const files = scanFiles(projectDir, 0, maxFiles);
41
+ if (files.length === 0) return '';
42
+
43
+ const parts = [];
44
+ parts.push(`Project: ${path.basename(projectDir)} (${files.length} source files)`);
45
+ parts.push('');
46
+
47
+ // Group by directory
48
+ const dirs = new Map();
49
+ for (const f of files) {
50
+ const rel = path.relative(projectDir, f);
51
+ const dir = path.dirname(rel);
52
+ if (!dirs.has(dir)) dirs.set(dir, []);
53
+ dirs.get(dir).push(rel);
54
+ }
55
+
56
+ // File tree
57
+ parts.push('## File Tree');
58
+ for (const [dir, dirFiles] of dirs) {
59
+ parts.push(`${dir}/`);
60
+ for (const f of dirFiles) {
61
+ parts.push(` ${path.basename(f)}`);
62
+ }
63
+ }
64
+
65
+ // Key signatures (top-level functions/classes)
66
+ parts.push('');
67
+ parts.push('## Key Signatures');
68
+
69
+ let sigCount = 0;
70
+ for (const f of files) {
71
+ if (sigCount > 50) break; // Cap signatures
72
+ try {
73
+ const content = fs.readFileSync(f, 'utf-8');
74
+ const rel = path.relative(projectDir, f);
75
+ const sigs = extractSignatures(content);
76
+ if (sigs.length > 0) {
77
+ parts.push(`${rel}: ${sigs.join(', ')}`);
78
+ sigCount += sigs.length;
79
+ }
80
+ } catch { /* skip */ }
81
+ }
82
+
83
+ let skeleton = parts.join('\n');
84
+ if (skeleton.length > maxChars) {
85
+ skeleton = skeleton.slice(0, maxChars) + '\n... (truncated)';
86
+ }
87
+ return skeleton;
88
+ }
89
+
90
+ /**
91
+ * Extract function/class names from source code via regex.
92
+ */
93
+ function extractSignatures(content) {
94
+ const names = new Set();
95
+ for (const pattern of SIGNATURE_PATTERNS) {
96
+ pattern.lastIndex = 0;
97
+ let match;
98
+ while ((match = pattern.exec(content)) !== null) {
99
+ // Take the first non-null capture group
100
+ const name = match[1] || match[2] || match[3];
101
+ if (name && !name.startsWith('_')) names.add(name);
102
+ }
103
+ }
104
+ return Array.from(names).slice(0, 10);
105
+ }
106
+
107
+ /**
108
+ * Scan project for source files.
109
+ */
110
+ function scanFiles(dir, depth = 0, maxFiles = 200) {
111
+ if (depth > 10) return [];
112
+ const results = [];
113
+ let entries;
114
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
115
+
116
+ for (const entry of entries) {
117
+ if (results.length >= maxFiles) break;
118
+ if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) continue;
119
+
120
+ const fullPath = path.join(dir, entry.name);
121
+ if (entry.isDirectory()) {
122
+ results.push(...scanFiles(fullPath, depth + 1, maxFiles - results.length));
123
+ } else if (entry.isFile()) {
124
+ const ext = path.extname(entry.name);
125
+ if (!CODE_EXTS.has(ext)) continue;
126
+ try {
127
+ const stat = fs.statSync(fullPath);
128
+ if (stat.size > MAX_FILE_SIZE) continue;
129
+ } catch { continue; }
130
+ results.push(fullPath);
131
+ }
132
+ }
133
+ return results;
134
+ }