@axplusb/kepler 0.0.1 → 1.0.1

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 (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +101 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. package/index.js +0 -1
@@ -0,0 +1,298 @@
1
+ /**
2
+ * AST Parser — Extract structured code knowledge from source files.
3
+ *
4
+ * Multi-language regex-based extraction (no tree-sitter dependency).
5
+ * Returns function signatures, class definitions, imports, exports.
6
+ *
7
+ * This is Phase 3 of the Investigative Funnel:
8
+ * Raw code (2000 tokens) → Structured summary (200 tokens)
9
+ */
10
+
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+
14
+ /**
15
+ * Analyze a source file and return structured knowledge.
16
+ * @param {string} filePath - absolute path
17
+ * @param {Object} [options]
18
+ * @param {number} [options.startLine] - optional line range
19
+ * @param {number} [options.endLine]
20
+ * @returns {{ success: boolean, summary: string, structure: Object }}
21
+ */
22
+ export function analyzeCode(filePath, { startLine, endLine } = {}) {
23
+ try {
24
+ let content = fs.readFileSync(filePath, 'utf-8');
25
+ const totalLines = content.split('\n').length;
26
+
27
+ if (startLine || endLine) {
28
+ const lines = content.split('\n');
29
+ const start = (startLine || 1) - 1;
30
+ const end = endLine || lines.length;
31
+ content = lines.slice(start, end).join('\n');
32
+ }
33
+
34
+ const ext = path.extname(filePath).toLowerCase();
35
+ const relPath = filePath; // caller should make relative if needed
36
+
37
+ let structure;
38
+ if (['.ts', '.tsx', '.js', '.jsx', '.mjs'].includes(ext)) {
39
+ structure = parseJavaScriptTypeScript(content);
40
+ } else if (ext === '.py') {
41
+ structure = parsePython(content);
42
+ } else if (ext === '.go') {
43
+ structure = parseGo(content);
44
+ } else if (['.rs'].includes(ext)) {
45
+ structure = parseRust(content);
46
+ } else {
47
+ structure = parseGeneric(content);
48
+ }
49
+
50
+ structure.file = path.basename(filePath);
51
+ structure.lines = totalLines;
52
+ structure.language = ext.replace('.', '');
53
+
54
+ // Build concise summary
55
+ const summary = buildSummary(structure);
56
+
57
+ return { success: true, summary, structure };
58
+ } catch (err) {
59
+ return { success: false, summary: `Error analyzing ${filePath}: ${err.message}`, structure: {} };
60
+ }
61
+ }
62
+
63
+ // ── JavaScript / TypeScript ────────────────────────────────
64
+
65
+ function parseJavaScriptTypeScript(content) {
66
+ const imports = [];
67
+ const exports = [];
68
+ const functions = [];
69
+ const classes = [];
70
+ const interfaces = [];
71
+ const types = [];
72
+ const lines = content.split('\n');
73
+
74
+ for (let i = 0; i < lines.length; i++) {
75
+ const trimmed = lines[i].trim();
76
+ const lineNum = i + 1;
77
+
78
+ // Imports
79
+ const importMatch = trimmed.match(/^import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/);
80
+ if (importMatch) {
81
+ imports.push({ names: (importMatch[1] || importMatch[2] || '').trim(), from: importMatch[3] });
82
+ continue;
83
+ }
84
+
85
+ // Exports
86
+ if (trimmed.startsWith('export default')) {
87
+ exports.push('default');
88
+ } else if (trimmed.match(/^export\s+(const|let|var|function|class|interface|type|async)/)) {
89
+ const nameMatch = trimmed.match(/^export\s+(?:async\s+)?(?:const|let|var|function|class|interface|type)\s+(\w+)/);
90
+ if (nameMatch) exports.push(nameMatch[1]);
91
+ }
92
+
93
+ // Functions
94
+ const fnMatch = trimmed.match(/^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/);
95
+ if (fnMatch) {
96
+ functions.push({ name: fnMatch[1], params: fnMatch[2].trim(), line: lineNum });
97
+ continue;
98
+ }
99
+
100
+ // Arrow functions
101
+ const arrowMatch = trimmed.match(/^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*\w+)?\s*=>/);
102
+ if (arrowMatch) {
103
+ functions.push({ name: arrowMatch[1], params: arrowMatch[2].trim(), line: lineNum });
104
+ continue;
105
+ }
106
+
107
+ // Classes
108
+ const classMatch = trimmed.match(/^(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?/);
109
+ if (classMatch) {
110
+ classes.push({ name: classMatch[1], extends: classMatch[2] || null, line: lineNum });
111
+ continue;
112
+ }
113
+
114
+ // Methods
115
+ const methodMatch = trimmed.match(/^(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*\w+)?\s*\{/);
116
+ if (methodMatch && !['if', 'for', 'while', 'switch', 'catch'].includes(methodMatch[1])) {
117
+ functions.push({ name: methodMatch[1], params: methodMatch[2].trim(), line: lineNum, isMethod: true });
118
+ }
119
+
120
+ // Interfaces
121
+ const ifaceMatch = trimmed.match(/^(?:export\s+)?interface\s+(\w+)/);
122
+ if (ifaceMatch) interfaces.push({ name: ifaceMatch[1], line: lineNum });
123
+
124
+ // Types
125
+ const typeMatch = trimmed.match(/^(?:export\s+)?type\s+(\w+)\s*=/);
126
+ if (typeMatch) types.push({ name: typeMatch[1], line: lineNum });
127
+ }
128
+
129
+ return { imports, exports, functions, classes, interfaces, types };
130
+ }
131
+
132
+ // ── Python ─────────────────────────────────────────────────
133
+
134
+ function parsePython(content) {
135
+ const imports = [];
136
+ const functions = [];
137
+ const classes = [];
138
+ const decorators = [];
139
+ const lines = content.split('\n');
140
+
141
+ for (let i = 0; i < lines.length; i++) {
142
+ const trimmed = lines[i].trim();
143
+ const lineNum = i + 1;
144
+
145
+ const importMatch = trimmed.match(/^(?:from\s+(\S+)\s+)?import\s+(.+)/);
146
+ if (importMatch) {
147
+ imports.push({ from: importMatch[1] || '', names: importMatch[2].trim() });
148
+ continue;
149
+ }
150
+
151
+ if (trimmed.startsWith('@')) {
152
+ decorators.push(trimmed);
153
+ continue;
154
+ }
155
+
156
+ const fnMatch = trimmed.match(/^(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/);
157
+ if (fnMatch) {
158
+ functions.push({ name: fnMatch[1], params: fnMatch[2].trim(), line: lineNum });
159
+ continue;
160
+ }
161
+
162
+ const classMatch = trimmed.match(/^class\s+(\w+)(?:\(([^)]*)\))?/);
163
+ if (classMatch) {
164
+ classes.push({ name: classMatch[1], bases: classMatch[2] || '', line: lineNum });
165
+ }
166
+ }
167
+
168
+ return { imports, functions, classes, decorators };
169
+ }
170
+
171
+ // ── Go ─────────────────────────────────────────────────────
172
+
173
+ function parseGo(content) {
174
+ const imports = [];
175
+ const functions = [];
176
+ const structs = [];
177
+
178
+ for (const line of content.split('\n')) {
179
+ const trimmed = line.trim();
180
+
181
+ const importMatch = trimmed.match(/^import\s+"([^"]+)"/);
182
+ if (importMatch) imports.push(importMatch[1]);
183
+
184
+ const fnMatch = trimmed.match(/^func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?(\w+)\s*\(([^)]*)\)/);
185
+ if (fnMatch) {
186
+ functions.push({
187
+ name: fnMatch[3],
188
+ params: fnMatch[4]?.trim() || '',
189
+ receiver: fnMatch[2] || null,
190
+ });
191
+ }
192
+
193
+ const structMatch = trimmed.match(/^type\s+(\w+)\s+struct/);
194
+ if (structMatch) structs.push(structMatch[1]);
195
+ }
196
+
197
+ return { imports, functions, structs };
198
+ }
199
+
200
+ // ── Rust ───────────────────────────────────────────────────
201
+
202
+ function parseRust(content) {
203
+ const functions = [];
204
+ const structs = [];
205
+ const impls = [];
206
+
207
+ for (const line of content.split('\n')) {
208
+ const trimmed = line.trim();
209
+
210
+ const fnMatch = trimmed.match(/^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)\s*\(([^)]*)\)/);
211
+ if (fnMatch) functions.push({ name: fnMatch[1], params: fnMatch[2].trim() });
212
+
213
+ const structMatch = trimmed.match(/^(?:pub\s+)?struct\s+(\w+)/);
214
+ if (structMatch) structs.push(structMatch[1]);
215
+
216
+ const implMatch = trimmed.match(/^impl\s+(?:<[^>]+>\s+)?(\w+)/);
217
+ if (implMatch) impls.push(implMatch[1]);
218
+ }
219
+
220
+ return { functions, structs, impls };
221
+ }
222
+
223
+ // ── Generic fallback ───────────────────────────────────────
224
+
225
+ function parseGeneric(content) {
226
+ const functions = [];
227
+ const lines = content.split('\n');
228
+
229
+ for (const line of lines) {
230
+ const trimmed = line.trim();
231
+ // Try common patterns
232
+ const fnMatch = trimmed.match(/^(?:(?:pub|public|private|protected|static|async|export)\s+)*(?:function|def|fn|func)\s+(\w+)/);
233
+ if (fnMatch) functions.push({ name: fnMatch[1] });
234
+ }
235
+
236
+ return { functions };
237
+ }
238
+
239
+ // ── Summary Builder ────────────────────────────────────────
240
+
241
+ function buildSummary(structure) {
242
+ const parts = [];
243
+ parts.push(`${structure.file} (${structure.lines} lines, ${structure.language})`);
244
+ parts.push(`Use read_file with start_line/end_line to read specific sections.`);
245
+ parts.push('');
246
+
247
+ if (structure.classes?.length) {
248
+ for (const cls of structure.classes) {
249
+ const ext = cls.extends ? ` extends ${cls.extends}` : (cls.bases ? `(${cls.bases})` : '');
250
+ const ln = cls.line ? ` [line ${cls.line}]` : '';
251
+ parts.push(` class ${cls.name}${ext}${ln}`);
252
+ }
253
+ }
254
+
255
+ if (structure.interfaces?.length) {
256
+ const ifaceList = structure.interfaces.map(i =>
257
+ typeof i === 'string' ? i : `${i.name} [line ${i.line}]`
258
+ ).join(', ');
259
+ parts.push(` interfaces: ${ifaceList}`);
260
+ }
261
+
262
+ if (structure.types?.length) {
263
+ const typeList = structure.types.map(t =>
264
+ typeof t === 'string' ? t : `${t.name} [line ${t.line}]`
265
+ ).join(', ');
266
+ parts.push(` types: ${typeList}`);
267
+ }
268
+
269
+ if (structure.functions?.length) {
270
+ for (const fn of structure.functions.slice(0, 20)) {
271
+ const receiver = fn.receiver ? `(${fn.receiver}) ` : '';
272
+ const method = fn.isMethod ? ' ' : '';
273
+ const ln = fn.line ? ` [line ${fn.line}]` : '';
274
+ parts.push(`${method} ${receiver}${fn.name}(${fn.params || ''})${ln}`);
275
+ }
276
+ if (structure.functions.length > 20) {
277
+ parts.push(` ... and ${structure.functions.length - 20} more functions`);
278
+ }
279
+ }
280
+
281
+ if (structure.structs?.length) {
282
+ parts.push(` structs: ${structure.structs.join(', ')}`);
283
+ }
284
+
285
+ if (structure.imports?.length) {
286
+ const importSummary = structure.imports.slice(0, 5).map(i =>
287
+ i.from ? `${i.from}` : (typeof i === 'string' ? i : i.names)
288
+ ).join(', ');
289
+ const more = structure.imports.length > 5 ? ` +${structure.imports.length - 5} more` : '';
290
+ parts.push(` imports: ${importSummary}${more}`);
291
+ }
292
+
293
+ if (structure.exports?.length) {
294
+ parts.push(` exports: ${structure.exports.join(', ')}`);
295
+ }
296
+
297
+ return parts.join('\n');
298
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * BM25 Search — T20: Lightweight text search index (~100 LOC).
3
+ * Parameters: k1=1.2, b=0.75 (standard BM25 tuning).
4
+ */
5
+
6
+ export class BM25Index {
7
+ constructor(k1 = 1.2, b = 0.75) {
8
+ this.k1 = k1;
9
+ this.b = b;
10
+ this.docs = []; // [{ id, tokens, length }]
11
+ this.df = new Map(); // term → doc frequency
12
+ this.avgDl = 0;
13
+ this.N = 0;
14
+ }
15
+
16
+ /** Tokenize text into lowercase terms. */
17
+ static tokenize(text) {
18
+ return text.toLowerCase().match(/[a-z_][a-z0-9_]*/g) || [];
19
+ }
20
+
21
+ /** Add a document to the index. */
22
+ addDocument(id, text) {
23
+ const tokens = BM25Index.tokenize(text);
24
+ const tf = new Map();
25
+ for (const t of tokens) tf.set(t, (tf.get(t) || 0) + 1);
26
+ this.docs.push({ id, tf, length: tokens.length });
27
+
28
+ for (const term of tf.keys()) {
29
+ this.df.set(term, (this.df.get(term) || 0) + 1);
30
+ }
31
+ this.N = this.docs.length;
32
+ this.avgDl = this.docs.reduce((s, d) => s + d.length, 0) / this.N;
33
+ }
34
+
35
+ /** Build index from array of { id, text } objects. */
36
+ buildIndex(documents) {
37
+ this.docs = [];
38
+ this.df = new Map();
39
+ for (const doc of documents) {
40
+ this.addDocument(doc.id, doc.text);
41
+ }
42
+ }
43
+
44
+ /** Search for query, return top-K results sorted by BM25 score. */
45
+ search(query, topK = 10) {
46
+ const queryTokens = BM25Index.tokenize(query);
47
+ if (queryTokens.length === 0) return [];
48
+
49
+ const scores = [];
50
+
51
+ for (const doc of this.docs) {
52
+ let score = 0;
53
+ for (const qt of queryTokens) {
54
+ const tf = doc.tf.get(qt) || 0;
55
+ if (tf === 0) continue;
56
+ const dfVal = this.df.get(qt) || 0;
57
+ const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
58
+ const tfNorm = (tf * (this.k1 + 1)) / (tf + this.k1 * (1 - this.b + this.b * doc.length / this.avgDl));
59
+ score += idf * tfNorm;
60
+ }
61
+ if (score > 0) scores.push({ id: doc.id, score });
62
+ }
63
+
64
+ return scores.sort((a, b) => b.score - a.score).slice(0, topK);
65
+ }
66
+
67
+ /** Serialize index to JSON. */
68
+ toJSON() {
69
+ return {
70
+ k1: this.k1, b: this.b, N: this.N, avgDl: this.avgDl,
71
+ docs: this.docs.map(d => ({ id: d.id, tf: Object.fromEntries(d.tf), length: d.length })),
72
+ df: Object.fromEntries(this.df),
73
+ };
74
+ }
75
+
76
+ /** Load index from JSON. */
77
+ static fromJSON(data) {
78
+ const idx = new BM25Index(data.k1, data.b);
79
+ idx.N = data.N;
80
+ idx.avgDl = data.avgDl;
81
+ idx.df = new Map(Object.entries(data.df));
82
+ idx.docs = data.docs.map(d => ({ id: d.id, tf: new Map(Object.entries(d.tf)), length: d.length }));
83
+ return idx;
84
+ }
85
+ }
@@ -0,0 +1,270 @@
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 * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import { indexDir as getIndexDir } from '../core/paths.mjs';
10
+
11
+ const IGNORED_DIRS = new Set(['.git', 'node_modules', '.orca', '__pycache__', '.venv', 'venv', 'dist', 'build', '.next']);
12
+ 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']);
13
+ const MAX_FILE_SIZE = 100_000; // 100KB
14
+ const CHUNK_LINES = 50;
15
+ const CHUNK_OVERLAP = 10;
16
+
17
+ export class ContextRetriever {
18
+ constructor(projectDir = process.cwd()) {
19
+ this.projectDir = projectDir;
20
+ this.indexDir = getIndexDir(projectDir);
21
+ this.index = null;
22
+ this.chunkTexts = new Map(); // id → original text content
23
+ }
24
+
25
+ /** Build or rebuild the search index. */
26
+ async buildIndex() {
27
+ const files = this._scanFiles(this.projectDir);
28
+ const documents = [];
29
+
30
+ for (const filePath of files) {
31
+ try {
32
+ const content = fs.readFileSync(filePath, 'utf-8');
33
+ const relPath = path.relative(this.projectDir, filePath);
34
+ const chunks = this._chunkFile(content, relPath);
35
+ documents.push(...chunks);
36
+ } catch { /* skip unreadable files */ }
37
+ }
38
+
39
+ this.index = new BM25Index();
40
+ this.index.buildIndex(documents);
41
+
42
+ // Store chunk texts for retrieval
43
+ this.chunkTexts = new Map();
44
+ for (const doc of documents) {
45
+ this.chunkTexts.set(doc.id, doc.text);
46
+ }
47
+
48
+ // Persist index + chunk texts
49
+ if (!fs.existsSync(this.indexDir)) fs.mkdirSync(this.indexDir, { recursive: true });
50
+ fs.writeFileSync(path.join(this.indexDir, 'bm25.json'), JSON.stringify(this.index.toJSON()));
51
+ fs.writeFileSync(path.join(this.indexDir, 'chunks.json'), JSON.stringify(Object.fromEntries(this.chunkTexts)));
52
+
53
+ return { fileCount: files.length, chunkCount: documents.length };
54
+ }
55
+
56
+ /**
57
+ * Incrementally update index for a single changed file.
58
+ * Re-chunks the file and replaces its entries in the BM25 index.
59
+ * ~5-50ms per file — safe to call after every edit/write.
60
+ */
61
+ updateFile(filePath) {
62
+ if (!this.index) {
63
+ if (!this.loadIndex()) return false;
64
+ }
65
+
66
+ const absPath = path.resolve(filePath);
67
+ const relPath = path.relative(this.projectDir, absPath);
68
+
69
+ // Remove old chunks for this file
70
+ const oldIds = new Set();
71
+ for (const doc of this.index.docs) {
72
+ if (doc.id === relPath || doc.id.startsWith(relPath + ':')) {
73
+ oldIds.add(doc.id);
74
+ }
75
+ }
76
+
77
+ // Collect remaining documents (excluding old chunks for this file)
78
+ const remainingDocs = [];
79
+ for (const doc of this.index.docs) {
80
+ if (!oldIds.has(doc.id)) {
81
+ // Reconstruct text from tf map for rebuild
82
+ remainingDocs.push({ id: doc.id, text: this.chunkTexts.get(doc.id) || '' });
83
+ }
84
+ }
85
+
86
+ // Remove old chunk texts
87
+ for (const id of oldIds) this.chunkTexts.delete(id);
88
+
89
+ // Re-chunk if file still exists (delete = just remove)
90
+ const newChunks = [];
91
+ if (fs.existsSync(absPath)) {
92
+ try {
93
+ const content = fs.readFileSync(absPath, 'utf-8');
94
+ newChunks.push(...this._chunkFile(content, relPath));
95
+ } catch { /* skip unreadable */ }
96
+ }
97
+
98
+ // Rebuild index from remaining + new chunks
99
+ // This is fast (~5-20ms) because BM25 build is O(n) on token count
100
+ const allDocs = [...remainingDocs, ...newChunks];
101
+ this.index = new BM25Index();
102
+ this.index.buildIndex(allDocs);
103
+
104
+ // Update chunk texts
105
+ for (const chunk of newChunks) {
106
+ this.chunkTexts.set(chunk.id, chunk.text);
107
+ }
108
+
109
+ // Persist updated index
110
+ try {
111
+ if (!fs.existsSync(this.indexDir)) fs.mkdirSync(this.indexDir, { recursive: true });
112
+ fs.writeFileSync(path.join(this.indexDir, 'bm25.json'), JSON.stringify(this.index.toJSON()));
113
+ fs.writeFileSync(path.join(this.indexDir, 'chunks.json'), JSON.stringify(Object.fromEntries(this.chunkTexts)));
114
+ } catch { /* best-effort persist */ }
115
+
116
+ return true;
117
+ }
118
+
119
+ /** Load persisted index. */
120
+ loadIndex() {
121
+ const indexPath = path.join(this.indexDir, 'bm25.json');
122
+ const chunksPath = path.join(this.indexDir, 'chunks.json');
123
+ if (!fs.existsSync(indexPath)) return false;
124
+ try {
125
+ const data = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
126
+ this.index = BM25Index.fromJSON(data);
127
+
128
+ // Load chunk texts if available
129
+ if (fs.existsSync(chunksPath)) {
130
+ const chunks = JSON.parse(fs.readFileSync(chunksPath, 'utf-8'));
131
+ this.chunkTexts = new Map(Object.entries(chunks));
132
+ }
133
+ return true;
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ /** Retrieve relevant context chunks for a query, with full text. */
140
+ retrieve(query, topK = 10) {
141
+ if (!this.index) {
142
+ if (!this.loadIndex()) return [];
143
+ }
144
+ const results = this.index.search(query, topK);
145
+ // Attach chunk text to results
146
+ return results.map(r => ({
147
+ ...r,
148
+ text: this.chunkTexts.get(r.id) || `[File: ${r.id}]`,
149
+ }));
150
+ }
151
+
152
+ /** Scan project files respecting .gitignore-like patterns. */
153
+ _scanFiles(dir, depth = 0) {
154
+ if (depth > 15) return [];
155
+ const results = [];
156
+ let entries;
157
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
158
+
159
+ for (const entry of entries) {
160
+ if (entry.name.startsWith('.') && IGNORED_DIRS.has(entry.name)) continue;
161
+ if (IGNORED_DIRS.has(entry.name)) continue;
162
+
163
+ const fullPath = path.join(dir, entry.name);
164
+ if (entry.isDirectory()) {
165
+ results.push(...this._scanFiles(fullPath, depth + 1));
166
+ } else if (entry.isFile()) {
167
+ const ext = path.extname(entry.name);
168
+ if (!CODE_EXTS.has(ext)) continue;
169
+ try {
170
+ const stat = fs.statSync(fullPath);
171
+ if (stat.size > MAX_FILE_SIZE) continue;
172
+ } catch { continue; }
173
+ results.push(fullPath);
174
+ }
175
+ }
176
+ return results;
177
+ }
178
+
179
+ /**
180
+ * Chunk a file by AST boundaries (functions/classes) with line-based fallback.
181
+ * AST-aware chunks contain complete functions/classes — not arbitrary 50-line blocks.
182
+ */
183
+ _chunkFile(content, relPath) {
184
+ const lines = content.split('\n');
185
+
186
+ // Small files: single chunk
187
+ if (lines.length <= CHUNK_LINES) {
188
+ return [{ id: relPath, text: `${relPath}\n${content}` }];
189
+ }
190
+
191
+ // Try AST-aware chunking: split at function/class boundaries
192
+ const boundaries = this._findASTBoundaries(lines);
193
+
194
+ if (boundaries.length > 1) {
195
+ // AST-aware: chunk at function/class boundaries
196
+ const chunks = [];
197
+ for (const { name, startLine, endLine } of boundaries) {
198
+ const chunk = lines.slice(startLine, endLine).join('\n');
199
+ const id = `${relPath}:${startLine + 1}:${name}`;
200
+ chunks.push({ id, text: `${relPath}:${startLine + 1} (${name})\n${chunk}` });
201
+ }
202
+ return chunks;
203
+ }
204
+
205
+ // Fallback: line-based chunking for non-code files
206
+ const chunks = [];
207
+ for (let i = 0; i < lines.length; i += (CHUNK_LINES - CHUNK_OVERLAP)) {
208
+ const chunk = lines.slice(i, i + CHUNK_LINES).join('\n');
209
+ chunks.push({ id: `${relPath}:${i + 1}`, text: `${relPath}:${i + 1}\n${chunk}` });
210
+ }
211
+ return chunks;
212
+ }
213
+
214
+ /**
215
+ * Find function/class boundaries using regex patterns.
216
+ * Returns array of { name, startLine, endLine }.
217
+ */
218
+ _findASTBoundaries(lines) {
219
+ const boundaries = [];
220
+ const patterns = [
221
+ // Python: def/class (indentation-based)
222
+ /^(?:async\s+)?def\s+(\w+)\s*\(/,
223
+ /^class\s+(\w+)/,
224
+ // JS/TS: function, class, export function
225
+ /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
226
+ /^(?:export\s+)?class\s+(\w+)/,
227
+ // JS/TS: const fn = arrow
228
+ /^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(/,
229
+ // Go: func
230
+ /^func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(/,
231
+ // Rust: fn, struct, impl
232
+ /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
233
+ /^(?:pub\s+)?struct\s+(\w+)/,
234
+ ];
235
+
236
+ let currentStart = 0;
237
+ let currentName = '_top_level';
238
+
239
+ for (let i = 0; i < lines.length; i++) {
240
+ const line = lines[i].trimStart();
241
+ for (const pattern of patterns) {
242
+ const match = line.match(pattern);
243
+ if (match) {
244
+ // Close previous boundary
245
+ if (i > currentStart) {
246
+ boundaries.push({
247
+ name: currentName,
248
+ startLine: currentStart,
249
+ endLine: i,
250
+ });
251
+ }
252
+ currentStart = i;
253
+ currentName = match[1] || match[2] || 'unknown';
254
+ break;
255
+ }
256
+ }
257
+ }
258
+
259
+ // Close last boundary
260
+ if (lines.length > currentStart) {
261
+ boundaries.push({
262
+ name: currentName,
263
+ startLine: currentStart,
264
+ endLine: lines.length,
265
+ });
266
+ }
267
+
268
+ return boundaries;
269
+ }
270
+ }