ctxman 3.0.0 → 3.1.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.
- package/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +381 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -67
- package/bin/cm-audit.js +121 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +687 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +772 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +116 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +312 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +53 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/ask.js +122 -0
- package/lib/rag/sources/FileDataSource.js +12 -9
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +246 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +257 -0
- package/lib/wizards/project-detector.js +572 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
|
@@ -21,7 +21,7 @@ export class ChromaDBStore extends BaseVectorStore {
|
|
|
21
21
|
try {
|
|
22
22
|
const { ChromaClient } = await import('chromadb');
|
|
23
23
|
this.client = new ChromaClient({ path: this.url });
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
this.collection = await this.client.getOrCreateCollection({
|
|
26
26
|
name: this.collectionName,
|
|
27
27
|
});
|
|
@@ -37,22 +37,22 @@ export class ChromaDBStore extends BaseVectorStore {
|
|
|
37
37
|
if (!this.collection) await this.init();
|
|
38
38
|
if (docs.length === 0) return;
|
|
39
39
|
|
|
40
|
-
const ids = docs.map(d => d.id || crypto.randomUUID());
|
|
41
|
-
const metadatas = docs.map(d => d.metadata || {});
|
|
42
|
-
const documents = docs.map(d => d.text);
|
|
43
|
-
|
|
40
|
+
const ids = docs.map((d) => d.id || crypto.randomUUID());
|
|
41
|
+
const metadatas = docs.map((d) => d.metadata || {});
|
|
42
|
+
const documents = docs.map((d) => d.text);
|
|
43
|
+
|
|
44
44
|
// Chroma can handle embeddings, but we have a provider.
|
|
45
45
|
// If we want to use our provider:
|
|
46
46
|
const embeddings = [];
|
|
47
47
|
for (const doc of docs) {
|
|
48
|
-
|
|
48
|
+
embeddings.push(await this.embeddingProvider.embed(doc.text));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
await this.collection.add({
|
|
52
52
|
ids,
|
|
53
53
|
embeddings,
|
|
54
54
|
metadatas,
|
|
55
|
-
documents
|
|
55
|
+
documents,
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -63,7 +63,7 @@ export class ChromaDBStore extends BaseVectorStore {
|
|
|
63
63
|
|
|
64
64
|
const results = await this.collection.query({
|
|
65
65
|
queryEmbeddings: [queryEmbedding],
|
|
66
|
-
nResults: limit
|
|
66
|
+
nResults: limit,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
// Chroma returns arrays of arrays
|
|
@@ -71,12 +71,12 @@ export class ChromaDBStore extends BaseVectorStore {
|
|
|
71
71
|
|
|
72
72
|
const hits = [];
|
|
73
73
|
const count = results.ids[0].length;
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
for (let i = 0; i < count; i++) {
|
|
76
76
|
hits.push({
|
|
77
77
|
text: results.documents[0][i],
|
|
78
78
|
score: results.distances ? results.distances[0][i] : 0,
|
|
79
|
-
metadata: results.metadatas[0][i]
|
|
79
|
+
metadata: results.metadatas[0][i],
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as lancedb from '@lancedb/lancedb';
|
|
2
1
|
import fs from 'fs/promises';
|
|
3
2
|
import { BaseVectorStore } from './BaseVectorStore.js';
|
|
4
3
|
|
|
@@ -19,11 +18,24 @@ export class LanceDBStore extends BaseVectorStore {
|
|
|
19
18
|
async init() {
|
|
20
19
|
if (this.initialized) return;
|
|
21
20
|
|
|
21
|
+
// Optional dependency: only `ctxman ask` needs it, so load it on first use
|
|
22
|
+
let lancedb;
|
|
23
|
+
try {
|
|
24
|
+
lancedb = await import('@lancedb/lancedb');
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Please install @lancedb/lancedb to use LanceDBStore (ctxman ask): npm install @lancedb/lancedb'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
// Ensure directory exists
|
|
23
35
|
await fs.mkdir(this.storagePath, { recursive: true });
|
|
24
36
|
|
|
25
37
|
this.db = await lancedb.connect(this.storagePath);
|
|
26
|
-
|
|
38
|
+
|
|
27
39
|
const tableNames = await this.db.tableNames();
|
|
28
40
|
if (tableNames.includes(this.tableName)) {
|
|
29
41
|
this.table = await this.db.openTable(this.tableName);
|
|
@@ -34,8 +46,8 @@ export class LanceDBStore extends BaseVectorStore {
|
|
|
34
46
|
|
|
35
47
|
/**
|
|
36
48
|
* Adds a single document to the store.
|
|
37
|
-
* @param {string} text
|
|
38
|
-
* @param {Object} metadata
|
|
49
|
+
* @param {string} text
|
|
50
|
+
* @param {Object} metadata
|
|
39
51
|
*/
|
|
40
52
|
async addDocument(text, metadata = {}) {
|
|
41
53
|
return this.addDocuments([{ text, metadata }]);
|
|
@@ -43,7 +55,7 @@ export class LanceDBStore extends BaseVectorStore {
|
|
|
43
55
|
|
|
44
56
|
/**
|
|
45
57
|
* Adds multiple documents to the store.
|
|
46
|
-
* @param {Array<{text: string, metadata: Object}>} docs
|
|
58
|
+
* @param {Array<{text: string, metadata: Object}>} docs
|
|
47
59
|
*/
|
|
48
60
|
async addDocuments(docs) {
|
|
49
61
|
if (docs.length === 0) return;
|
|
@@ -57,7 +69,7 @@ export class LanceDBStore extends BaseVectorStore {
|
|
|
57
69
|
text: doc.text,
|
|
58
70
|
id: crypto.randomUUID(),
|
|
59
71
|
timestamp: Date.now(),
|
|
60
|
-
...doc.metadata
|
|
72
|
+
...doc.metadata,
|
|
61
73
|
});
|
|
62
74
|
}
|
|
63
75
|
|
|
@@ -71,8 +83,8 @@ export class LanceDBStore extends BaseVectorStore {
|
|
|
71
83
|
|
|
72
84
|
/**
|
|
73
85
|
* Semantic search for documents.
|
|
74
|
-
* @param {string} queryText
|
|
75
|
-
* @param {number} limit
|
|
86
|
+
* @param {string} queryText
|
|
87
|
+
* @param {number} limit
|
|
76
88
|
* @returns {Promise<Array<{text: string, score: number, metadata: Object}>>}
|
|
77
89
|
*/
|
|
78
90
|
async search(queryText, limit = 5) {
|
|
@@ -80,19 +92,17 @@ export class LanceDBStore extends BaseVectorStore {
|
|
|
80
92
|
if (!this.table) return [];
|
|
81
93
|
|
|
82
94
|
const queryVector = await this.embeddingProvider.embed(queryText);
|
|
83
|
-
|
|
95
|
+
|
|
84
96
|
// LanceDB returns _distance by default (L2 distance usually).
|
|
85
97
|
// Lower is better.
|
|
86
|
-
const results = await this.table.search(queryVector)
|
|
87
|
-
.limit(limit)
|
|
88
|
-
.toArray();
|
|
98
|
+
const results = await this.table.search(queryVector).limit(limit).toArray();
|
|
89
99
|
|
|
90
|
-
return results.map(row => {
|
|
91
|
-
const { vector, text, _distance, ...metadata } = row;
|
|
100
|
+
return results.map((row) => {
|
|
101
|
+
const { vector: _vector, text, _distance, ...metadata } = row;
|
|
92
102
|
return {
|
|
93
103
|
text,
|
|
94
104
|
score: _distance,
|
|
95
|
-
metadata
|
|
105
|
+
metadata,
|
|
96
106
|
};
|
|
97
107
|
});
|
|
98
108
|
}
|
|
@@ -100,10 +110,10 @@ export class LanceDBStore extends BaseVectorStore {
|
|
|
100
110
|
async deleteTable() {
|
|
101
111
|
if (!this.initialized) await this.init();
|
|
102
112
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
} catch (
|
|
106
|
-
|
|
113
|
+
await this.db.dropTable(this.tableName);
|
|
114
|
+
this.table = null;
|
|
115
|
+
} catch (_e) {
|
|
116
|
+
// Ignore if not exists
|
|
107
117
|
}
|
|
108
118
|
}
|
|
109
119
|
}
|
|
@@ -24,8 +24,10 @@ export class PineconeStore extends BaseVectorStore {
|
|
|
24
24
|
this.client = new Pinecone({ apiKey: this.apiKey });
|
|
25
25
|
this.index = this.client.index(this.indexName);
|
|
26
26
|
} catch (error) {
|
|
27
|
-
|
|
28
|
-
throw new Error(
|
|
27
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'Please install @pinecone-database/pinecone to use PineconeStore: npm install @pinecone-database/pinecone'
|
|
30
|
+
);
|
|
29
31
|
}
|
|
30
32
|
throw error;
|
|
31
33
|
}
|
|
@@ -42,16 +44,16 @@ export class PineconeStore extends BaseVectorStore {
|
|
|
42
44
|
id: doc.id || crypto.randomUUID(),
|
|
43
45
|
values,
|
|
44
46
|
metadata: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
47
|
+
...doc.metadata,
|
|
48
|
+
text: doc.text, // Pinecone metadata is limited, but storing text is useful
|
|
49
|
+
},
|
|
48
50
|
});
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
// Pinecone likes batches
|
|
52
54
|
const batchSize = 100;
|
|
53
55
|
for (let i = 0; i < vectors.length; i += batchSize) {
|
|
54
|
-
|
|
56
|
+
await this.index.upsert(vectors.slice(i, i + batchSize));
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -62,13 +64,13 @@ export class PineconeStore extends BaseVectorStore {
|
|
|
62
64
|
const results = await this.index.query({
|
|
63
65
|
vector,
|
|
64
66
|
topK: limit,
|
|
65
|
-
includeMetadata: true
|
|
67
|
+
includeMetadata: true,
|
|
66
68
|
});
|
|
67
69
|
|
|
68
|
-
return results.matches.map(match => ({
|
|
70
|
+
return results.matches.map((match) => ({
|
|
69
71
|
text: match.metadata.text || '', // Assuming text is stored in metadata
|
|
70
72
|
score: match.score,
|
|
71
|
-
metadata: match.metadata
|
|
73
|
+
metadata: match.metadata,
|
|
72
74
|
}));
|
|
73
75
|
}
|
|
74
76
|
}
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ranked code search: BM25 over code chunks.
|
|
3
|
+
*
|
|
4
|
+
* A chunk is a method/function found by the method analyzers or, for code outside them and
|
|
5
|
+
* for files without methods, a window of up to 40 lines. Identifiers are split into their
|
|
6
|
+
* words (camelCase, snake_case, kebab-case) and lowercased, so `getUserName`, `get_user_name`
|
|
7
|
+
* and "user name" find each other. Pure JavaScript, no dependencies.
|
|
8
|
+
*
|
|
9
|
+
* The per-project index lives in memory and is rebuilt only for files whose modification
|
|
10
|
+
* time or size changed since the last search.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { Scanner } from '../core/Scanner.js';
|
|
16
|
+
import TokenUtils from '../utils/token-utils.js';
|
|
17
|
+
import { redactSecrets } from '../utils/secret-redactor.js';
|
|
18
|
+
import { readFileChecked } from '../utils/safe-read.js';
|
|
19
|
+
import { extractCodeUnits } from './code-units.js';
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_LIMIT = 10;
|
|
22
|
+
export const DEFAULT_MAX_TOKENS = 4000;
|
|
23
|
+
export const WINDOW_LINES = 40;
|
|
24
|
+
/** Files above this size (minified bundles, generated data) are not indexed */
|
|
25
|
+
export const MAX_FILE_BYTES = 1024 * 1024;
|
|
26
|
+
const MAX_CACHED_PROJECTS = 8;
|
|
27
|
+
|
|
28
|
+
// A word: letters, digits, `_` and `$`, optionally joined by `-` (kebab-case)
|
|
29
|
+
const WORD = /[\p{L}\p{N}_$]+(?:-[\p{L}\p{N}_$]+)*/gu;
|
|
30
|
+
// Parts of a camelCase word: `HTTPServer2Go` -> HTTP, Server, 2, Go
|
|
31
|
+
const PART = /\p{Lu}+(?!\p{Ll})|\p{Lu}?[\p{Ll}\p{Lo}\p{Lm}]+|\p{N}+/gu;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fold an English plural to its singular, conservatively: `users` -> `user`,
|
|
35
|
+
* `classes` -> `class`; `status`, `class` and `analysis` stay as they are
|
|
36
|
+
* @param {string} term - Lower-case term
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeTerm(term) {
|
|
40
|
+
if (term.length <= 3 || !term.endsWith('s')) return term;
|
|
41
|
+
if (/(?:ss|us|is)$/.test(term)) return term;
|
|
42
|
+
if (/(?:sses|shes|ches|xes|zes)$/.test(term)) return term.slice(0, -2);
|
|
43
|
+
if (term.endsWith('ies') && term.length > 4) return `${term.slice(0, -3)}y`;
|
|
44
|
+
return term.slice(0, -1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Split text into search terms.
|
|
49
|
+
*
|
|
50
|
+
* Each identifier yields its lower-cased parts (`parseHTTPResponse` -> parse, http,
|
|
51
|
+
* response) plus, when it has several parts, the joined form (`parsehttpresponse`), so an
|
|
52
|
+
* exact identifier query ranks the identifier above code that merely uses the same words.
|
|
53
|
+
* Single characters are dropped.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} text
|
|
56
|
+
* @returns {string[]}
|
|
57
|
+
*/
|
|
58
|
+
export function tokenize(text) {
|
|
59
|
+
const terms = [];
|
|
60
|
+
for (const [word] of String(text).matchAll(WORD)) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
for (const piece of word.split(/[-_$]+/)) {
|
|
63
|
+
for (const [part] of piece.matchAll(PART)) parts.push(part.toLowerCase());
|
|
64
|
+
}
|
|
65
|
+
for (const part of parts) {
|
|
66
|
+
if (part.length > 1) terms.push(normalizeTerm(part));
|
|
67
|
+
}
|
|
68
|
+
if (parts.length > 1) {
|
|
69
|
+
terms.push(normalizeTerm(parts.join('')));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return terms;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Okapi BM25 index over documents given as term lists.
|
|
77
|
+
*
|
|
78
|
+
* IDF is the non-negative variant ln(1 + (N - n + 0.5) / (n + 0.5)). Results with equal
|
|
79
|
+
* scores keep the order in which their documents were added.
|
|
80
|
+
*/
|
|
81
|
+
export class BM25Index {
|
|
82
|
+
/**
|
|
83
|
+
* @param {Object} [options]
|
|
84
|
+
* @param {number} [options.k1=1.2] - Term frequency saturation
|
|
85
|
+
* @param {number} [options.b=0.75] - Length normalisation
|
|
86
|
+
*/
|
|
87
|
+
constructor({ k1 = 1.2, b = 0.75 } = {}) {
|
|
88
|
+
this.k1 = k1;
|
|
89
|
+
this.b = b;
|
|
90
|
+
this.docs = [];
|
|
91
|
+
this.lengths = [];
|
|
92
|
+
this.postings = new Map();
|
|
93
|
+
this.totalLength = 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Number of documents */
|
|
97
|
+
get size() {
|
|
98
|
+
return this.docs.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Add a document
|
|
103
|
+
* @param {*} doc - Returned with the search results
|
|
104
|
+
* @param {string[]} terms - Terms of the document (see tokenize())
|
|
105
|
+
* @returns {number} Document id
|
|
106
|
+
*/
|
|
107
|
+
add(doc, terms) {
|
|
108
|
+
const id = this.docs.length;
|
|
109
|
+
const frequencies = new Map();
|
|
110
|
+
for (const term of terms) frequencies.set(term, (frequencies.get(term) || 0) + 1);
|
|
111
|
+
for (const [term, tf] of frequencies) {
|
|
112
|
+
let list = this.postings.get(term);
|
|
113
|
+
if (!list) {
|
|
114
|
+
list = [];
|
|
115
|
+
this.postings.set(term, list);
|
|
116
|
+
}
|
|
117
|
+
list.push(id, tf);
|
|
118
|
+
}
|
|
119
|
+
this.docs.push(doc);
|
|
120
|
+
this.lengths.push(terms.length);
|
|
121
|
+
this.totalLength += terms.length;
|
|
122
|
+
return id;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Inverse document frequency of a term
|
|
127
|
+
* @param {string} term
|
|
128
|
+
* @returns {number}
|
|
129
|
+
*/
|
|
130
|
+
idf(term) {
|
|
131
|
+
const n = (this.postings.get(term)?.length ?? 0) / 2;
|
|
132
|
+
return Math.log(1 + (this.docs.length - n + 0.5) / (n + 0.5));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Rank documents for a query
|
|
137
|
+
* @param {string|string[]} query - Query text, or terms
|
|
138
|
+
* @param {Object} [options]
|
|
139
|
+
* @param {number} [options.limit=10] - Maximum number of results
|
|
140
|
+
* @returns {Array<{doc: *, id: number, score: number}>} Best first
|
|
141
|
+
*/
|
|
142
|
+
search(query, { limit = DEFAULT_LIMIT } = {}) {
|
|
143
|
+
const terms = [...new Set(Array.isArray(query) ? query : tokenize(query))];
|
|
144
|
+
if (this.docs.length === 0 || terms.length === 0) return [];
|
|
145
|
+
|
|
146
|
+
const avgLength = this.totalLength / this.docs.length || 1;
|
|
147
|
+
const scores = new Map();
|
|
148
|
+
for (const term of terms) {
|
|
149
|
+
const list = this.postings.get(term);
|
|
150
|
+
if (!list) continue;
|
|
151
|
+
const idf = this.idf(term);
|
|
152
|
+
for (let i = 0; i < list.length; i += 2) {
|
|
153
|
+
const id = list[i];
|
|
154
|
+
const tf = list[i + 1];
|
|
155
|
+
const norm = this.k1 * (1 - this.b + (this.b * this.lengths[id]) / avgLength);
|
|
156
|
+
scores.set(id, (scores.get(id) || 0) + (idf * tf * (this.k1 + 1)) / (tf + norm));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return [...scores]
|
|
161
|
+
.sort((a, b) => b[1] - a[1] || a[0] - b[0])
|
|
162
|
+
.slice(0, Math.max(0, limit))
|
|
163
|
+
.map(([id, score]) => ({ doc: this.docs[id], id, score }));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Split a file into search chunks: its methods/functions, plus windows of at most
|
|
169
|
+
* `windowLines` lines over the code outside them (the whole file when it has none).
|
|
170
|
+
* Windows without a single term are dropped.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} content - File content
|
|
173
|
+
* @param {string} filePath - Path used for the language (extension)
|
|
174
|
+
* @param {Object} [options]
|
|
175
|
+
* @param {number} [options.windowLines=40]
|
|
176
|
+
* @param {Array} [options.units] - Code units, when already extracted
|
|
177
|
+
* @returns {Array<{name: string|null, kind: string, parent?: string, startLine: number,
|
|
178
|
+
* endLine: number}>} Sorted by start line
|
|
179
|
+
*/
|
|
180
|
+
export function chunkFile(content, filePath, { windowLines = WINDOW_LINES, units } = {}) {
|
|
181
|
+
const lines = content.split('\n');
|
|
182
|
+
// A trailing newline does not start another line
|
|
183
|
+
const lineCount = lines.length > 1 && lines.at(-1) === '' ? lines.length - 1 : lines.length;
|
|
184
|
+
const methods = (units ?? extractCodeUnits(content, filePath)).filter((u) => !u.container);
|
|
185
|
+
|
|
186
|
+
const chunks = methods.map((unit) => ({
|
|
187
|
+
name: unit.name,
|
|
188
|
+
kind: unit.kind,
|
|
189
|
+
...(unit.parent ? { parent: unit.parent } : {}),
|
|
190
|
+
startLine: unit.startLine,
|
|
191
|
+
endLine: Math.min(unit.endLine, lineCount),
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
const covered = new Uint8Array(lineCount + 2);
|
|
195
|
+
for (const chunk of chunks) covered.fill(1, chunk.startLine, chunk.endLine + 1);
|
|
196
|
+
|
|
197
|
+
let line = 1;
|
|
198
|
+
while (line <= lineCount) {
|
|
199
|
+
// Windows start and end on a non-blank line
|
|
200
|
+
if (covered[line] || !lines[line - 1].trim()) {
|
|
201
|
+
line++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const start = line;
|
|
205
|
+
while (line <= lineCount && !covered[line] && line - start < windowLines) line++;
|
|
206
|
+
let end = line - 1;
|
|
207
|
+
while (end > start && !lines[end - 1].trim()) end--;
|
|
208
|
+
// Same test as tokenize() yielding a term: a word of two or more characters
|
|
209
|
+
if (/[\p{L}\p{N}]{2}/u.test(lines.slice(start - 1, end).join('\n'))) {
|
|
210
|
+
chunks.push({ name: null, kind: 'window', startLine: start, endLine: end });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return chunks.sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Source text of a line range (1-based, inclusive)
|
|
219
|
+
* @param {string} content
|
|
220
|
+
* @param {number} startLine
|
|
221
|
+
* @param {number} endLine
|
|
222
|
+
* @returns {string}
|
|
223
|
+
*/
|
|
224
|
+
export function sliceLines(content, startLine, endLine) {
|
|
225
|
+
return content
|
|
226
|
+
.split('\n')
|
|
227
|
+
.slice(startLine - 1, endLine)
|
|
228
|
+
.join('\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Code-point order, independent of the locale
|
|
233
|
+
* @param {string} a
|
|
234
|
+
* @param {string} b
|
|
235
|
+
* @returns {number}
|
|
236
|
+
*/
|
|
237
|
+
export function compareStrings(a, b) {
|
|
238
|
+
if (a < b) return -1;
|
|
239
|
+
return a > b ? 1 : 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// root + options -> { signature, files: Map<relativePath, entry>, index }
|
|
243
|
+
const projectCache = new Map();
|
|
244
|
+
|
|
245
|
+
/** Forget every cached project index (tests, long-running servers after big changes) */
|
|
246
|
+
export function clearSearchCache() {
|
|
247
|
+
projectCache.clear();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Load a file entry: content, code units and chunks, reused while mtime and size match
|
|
252
|
+
* @private
|
|
253
|
+
*/
|
|
254
|
+
function loadFile(file, previous, windowLines) {
|
|
255
|
+
let stat;
|
|
256
|
+
try {
|
|
257
|
+
stat = fs.statSync(file.path);
|
|
258
|
+
} catch (_error) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const signature = `${stat.mtimeMs}:${stat.size}`;
|
|
262
|
+
if (previous && previous.signature === signature) return previous;
|
|
263
|
+
|
|
264
|
+
const entry = {
|
|
265
|
+
path: file.path,
|
|
266
|
+
relativePath: file.relativePath,
|
|
267
|
+
signature,
|
|
268
|
+
content: '',
|
|
269
|
+
units: [],
|
|
270
|
+
chunks: [],
|
|
271
|
+
};
|
|
272
|
+
const read = readFileChecked(file.path, { maxBytes: MAX_FILE_BYTES });
|
|
273
|
+
if (read.status === 'too-large') return { ...entry, skipped: 'too large' };
|
|
274
|
+
if (read.status !== 'ok') return null;
|
|
275
|
+
entry.content = read.buffer.toString('utf-8');
|
|
276
|
+
entry.units = extractCodeUnits(entry.content, file.path);
|
|
277
|
+
entry.chunks = chunkFile(entry.content, file.path, { windowLines, units: entry.units });
|
|
278
|
+
return entry;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The in-memory index of a project, built on first use and refreshed when files are added,
|
|
283
|
+
* removed or changed (by modification time and size). Files are found with the Scanner, so
|
|
284
|
+
* .gitignore, .contextignore and .contextinclude apply.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} root - Project directory
|
|
287
|
+
* @param {Object} [options]
|
|
288
|
+
* @param {number} [options.windowLines=40]
|
|
289
|
+
* @returns {{root: string, files: Map<string, Object>, index: BM25Index, signature: string,
|
|
290
|
+
* reused: boolean}}
|
|
291
|
+
* @throws {Error} When root is not a directory
|
|
292
|
+
*/
|
|
293
|
+
export function getProjectIndex(root, { windowLines = WINDOW_LINES } = {}) {
|
|
294
|
+
const resolvedRoot = path.resolve(root);
|
|
295
|
+
if (!fs.statSync(resolvedRoot, { throwIfNoEntry: false })?.isDirectory()) {
|
|
296
|
+
throw new Error(`Not a directory: ${root}`);
|
|
297
|
+
}
|
|
298
|
+
const key = `${resolvedRoot}\0${windowLines}`;
|
|
299
|
+
const cached = projectCache.get(key);
|
|
300
|
+
|
|
301
|
+
const scanned = new Scanner(resolvedRoot)
|
|
302
|
+
.scan()
|
|
303
|
+
.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
304
|
+
|
|
305
|
+
const files = new Map();
|
|
306
|
+
const signatureParts = [];
|
|
307
|
+
for (const file of scanned) {
|
|
308
|
+
const entry = loadFile(file, cached?.files.get(file.relativePath), windowLines);
|
|
309
|
+
if (!entry) continue;
|
|
310
|
+
files.set(file.relativePath, entry);
|
|
311
|
+
signatureParts.push(`${file.relativePath}\0${entry.signature}`);
|
|
312
|
+
}
|
|
313
|
+
const signature = signatureParts.join('\n');
|
|
314
|
+
|
|
315
|
+
// Most recently used last, so the first key is the one to evict
|
|
316
|
+
projectCache.delete(key);
|
|
317
|
+
if (cached && cached.signature === signature) {
|
|
318
|
+
projectCache.set(key, cached);
|
|
319
|
+
return { ...cached, reused: true };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const index = new BM25Index();
|
|
323
|
+
for (const entry of files.values()) {
|
|
324
|
+
const pathTerms = tokenize(entry.relativePath);
|
|
325
|
+
const lines = entry.content.split('\n');
|
|
326
|
+
for (const chunk of entry.chunks) {
|
|
327
|
+
// Index the redacted text, so the ranking cannot be used to probe secret values
|
|
328
|
+
const text = redactSecrets(lines.slice(chunk.startLine - 1, chunk.endLine).join('\n')).text;
|
|
329
|
+
const nameTerms = chunk.name ? tokenize(chunk.name) : [];
|
|
330
|
+
// The name counts three times and the file path once on top of the text
|
|
331
|
+
index.add({ entry, chunk }, [
|
|
332
|
+
...tokenize(text),
|
|
333
|
+
...nameTerms,
|
|
334
|
+
...nameTerms,
|
|
335
|
+
...nameTerms,
|
|
336
|
+
...pathTerms,
|
|
337
|
+
]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const project = { root: resolvedRoot, files, index, signature };
|
|
342
|
+
projectCache.set(key, project);
|
|
343
|
+
while (projectCache.size > MAX_CACHED_PROJECTS) {
|
|
344
|
+
projectCache.delete(projectCache.keys().next().value);
|
|
345
|
+
}
|
|
346
|
+
return { ...project, reused: false };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Cut text to its first lines that fit in a token budget
|
|
351
|
+
* @param {string} text
|
|
352
|
+
* @param {number} budget - Tokens
|
|
353
|
+
* @param {string} filePath - For the token estimate when tiktoken is missing
|
|
354
|
+
* @returns {string} The fitting prefix ('' when not even one line fits)
|
|
355
|
+
*/
|
|
356
|
+
export function fitToTokenBudget(text, budget, filePath) {
|
|
357
|
+
const lines = text.split('\n');
|
|
358
|
+
let low = 0;
|
|
359
|
+
let high = lines.length;
|
|
360
|
+
// Largest line count whose text fits
|
|
361
|
+
while (low < high) {
|
|
362
|
+
const mid = (low + high + 1) >> 1;
|
|
363
|
+
if (TokenUtils.calculate(lines.slice(0, mid).join('\n'), filePath) <= budget) low = mid;
|
|
364
|
+
else high = mid - 1;
|
|
365
|
+
}
|
|
366
|
+
return lines.slice(0, low).join('\n');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Ranked search over a project.
|
|
371
|
+
*
|
|
372
|
+
* @param {string} root - Project directory
|
|
373
|
+
* @param {string} query - Words or identifiers to look for
|
|
374
|
+
* @param {Object} [options]
|
|
375
|
+
* @param {number} [options.limit=10] - Maximum number of chunks
|
|
376
|
+
* @param {number} [options.maxTokens=4000] - Token budget for the chunk texts; chunks past
|
|
377
|
+
* it are cut (`truncated: true`) or listed without text
|
|
378
|
+
* @param {boolean} [options.redact=true] - Redact secrets in the returned text
|
|
379
|
+
* @param {number} [options.windowLines=40] - Window size for code outside methods
|
|
380
|
+
* @returns {{query: string, root: string, files: number, chunks: number, tokens: number,
|
|
381
|
+
* results: Array<{file: string, name: string|null, kind: string, parent?: string,
|
|
382
|
+
* startLine: number, endLine: number, score: number, tokens: number, text: string|null,
|
|
383
|
+
* truncated?: boolean}>}}
|
|
384
|
+
*/
|
|
385
|
+
export function searchProject(
|
|
386
|
+
root,
|
|
387
|
+
query,
|
|
388
|
+
{
|
|
389
|
+
limit = DEFAULT_LIMIT,
|
|
390
|
+
maxTokens = DEFAULT_MAX_TOKENS,
|
|
391
|
+
redact = true,
|
|
392
|
+
windowLines = WINDOW_LINES,
|
|
393
|
+
} = {}
|
|
394
|
+
) {
|
|
395
|
+
if (typeof query !== 'string' || !query.trim()) {
|
|
396
|
+
throw new Error('The search query is empty');
|
|
397
|
+
}
|
|
398
|
+
const project = getProjectIndex(root, { windowLines });
|
|
399
|
+
const hits = project.index.search(query, { limit });
|
|
400
|
+
|
|
401
|
+
let used = 0;
|
|
402
|
+
const results = hits.map(({ doc: { entry, chunk }, score }) => {
|
|
403
|
+
const raw = sliceLines(entry.content, chunk.startLine, chunk.endLine);
|
|
404
|
+
let text = redact ? redactSecrets(raw).text : raw;
|
|
405
|
+
let tokens = TokenUtils.calculate(text, entry.path);
|
|
406
|
+
const result = {
|
|
407
|
+
file: entry.relativePath,
|
|
408
|
+
name: chunk.name,
|
|
409
|
+
kind: chunk.kind,
|
|
410
|
+
...(chunk.parent ? { parent: chunk.parent } : {}),
|
|
411
|
+
startLine: chunk.startLine,
|
|
412
|
+
endLine: chunk.endLine,
|
|
413
|
+
score: Math.round(score * 10000) / 10000,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
if (used + tokens > maxTokens) {
|
|
417
|
+
text = fitToTokenBudget(text, maxTokens - used, entry.path);
|
|
418
|
+
tokens = text ? TokenUtils.calculate(text, entry.path) : 0;
|
|
419
|
+
result.truncated = true;
|
|
420
|
+
}
|
|
421
|
+
used += tokens;
|
|
422
|
+
return { ...result, tokens, text: text || null };
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
query,
|
|
427
|
+
root: project.root,
|
|
428
|
+
files: project.files.size,
|
|
429
|
+
chunks: project.index.size,
|
|
430
|
+
tokens: used,
|
|
431
|
+
results,
|
|
432
|
+
};
|
|
433
|
+
}
|