@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.
- package/README.md +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- 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
|
+
}
|