scai 0.1.110 ā 0.1.111
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/dist/commands/AskCmd.js +49 -79
- package/dist/db/fileIndex.js +2 -1
- package/dist/lib/generate.js +3 -2
- package/dist/pipeline/modules/changeLogModule.js +1 -1
- package/dist/pipeline/modules/commentModule.js +1 -1
- package/dist/pipeline/modules/commitSuggesterModule.js +1 -1
- package/dist/pipeline/modules/generateTestsModule.js +1 -1
- package/dist/pipeline/modules/kgModule.js +1 -1
- package/dist/pipeline/modules/refactorModule.js +1 -1
- package/dist/pipeline/modules/repairTestsModule.js +1 -1
- package/dist/pipeline/modules/reviewModule.js +1 -1
- package/dist/pipeline/modules/summaryModule.js +1 -1
- package/dist/utils/buildContextualPrompt.js +103 -65
- package/dist/utils/sanitizeQuery.js +14 -6
- package/package.json +1 -1
package/dist/commands/AskCmd.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import readline from 'readline';
|
|
4
|
-
import { searchFiles, queryFiles
|
|
4
|
+
import { searchFiles, queryFiles } from '../db/fileIndex.js';
|
|
5
5
|
import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
|
|
6
6
|
import { generate } from '../lib/generate.js';
|
|
7
7
|
import { buildContextualPrompt } from '../utils/buildContextualPrompt.js';
|
|
8
|
-
import { generateFocusedFileTree } from '../utils/fileTree.js';
|
|
9
8
|
import { log } from '../utils/log.js';
|
|
10
|
-
import { PROMPT_LOG_PATH, SCAI_HOME, RELATED_FILES_LIMIT,
|
|
9
|
+
import { PROMPT_LOG_PATH, SCAI_HOME, RELATED_FILES_LIMIT, getIndexDir } from '../constants.js';
|
|
11
10
|
import chalk from 'chalk';
|
|
12
11
|
export async function runAskCommand(query) {
|
|
13
12
|
if (!query) {
|
|
@@ -19,22 +18,21 @@ export async function runAskCommand(query) {
|
|
|
19
18
|
return;
|
|
20
19
|
}
|
|
21
20
|
console.log(`š Using index root: ${getIndexDir()}`);
|
|
22
|
-
|
|
23
|
-
// š© STEP 1: Semantic Search
|
|
21
|
+
// Semantic Search
|
|
24
22
|
const start = Date.now();
|
|
25
|
-
const semanticResults = await searchFiles(query, RELATED_FILES_LIMIT);
|
|
23
|
+
const semanticResults = await searchFiles(query, RELATED_FILES_LIMIT);
|
|
26
24
|
const duration = Date.now() - start;
|
|
27
25
|
console.log(`ā±ļø searchFiles took ${duration}ms and returned ${semanticResults.length} result(s)`);
|
|
28
26
|
semanticResults.forEach((file, i) => {
|
|
29
27
|
console.log(` ${i + 1}. š Path: ${file.path} | Score: ${file.score?.toFixed(3) ?? 'n/a'}`);
|
|
30
28
|
});
|
|
31
|
-
//
|
|
29
|
+
// Fallback FTS search
|
|
32
30
|
const safeQuery = sanitizeQueryForFts(query);
|
|
33
|
-
const fallbackResults = queryFiles(safeQuery, 10);
|
|
31
|
+
const fallbackResults = queryFiles(safeQuery, 10);
|
|
34
32
|
fallbackResults.forEach((file, i) => {
|
|
35
33
|
console.log(` ${i + 1}. š Fallback Match: ${file.path}`);
|
|
36
34
|
});
|
|
37
|
-
//
|
|
35
|
+
// Merge results
|
|
38
36
|
const seen = new Set();
|
|
39
37
|
const combinedResults = [];
|
|
40
38
|
for (const file of semanticResults) {
|
|
@@ -56,7 +54,20 @@ export async function runAskCommand(query) {
|
|
|
56
54
|
});
|
|
57
55
|
}
|
|
58
56
|
}
|
|
59
|
-
//
|
|
57
|
+
// Exact match prioritization
|
|
58
|
+
const queryFilenameRaw = path.basename(query).toLowerCase();
|
|
59
|
+
const queryFilenameNoExt = queryFilenameRaw.replace(/\.[^/.]+$/, '');
|
|
60
|
+
const exactMatchIndex = combinedResults.findIndex(f => {
|
|
61
|
+
const base = path.basename(f.path).toLowerCase();
|
|
62
|
+
const baseNoExt = base.replace(/\.[^/.]+$/, '');
|
|
63
|
+
return base === queryFilenameRaw || baseNoExt === queryFilenameNoExt;
|
|
64
|
+
});
|
|
65
|
+
if (exactMatchIndex !== -1) {
|
|
66
|
+
const [exactMatch] = combinedResults.splice(exactMatchIndex, 1);
|
|
67
|
+
combinedResults.unshift(exactMatch);
|
|
68
|
+
console.log(`šÆ Exact match prioritized: ${exactMatch.path}`);
|
|
69
|
+
}
|
|
70
|
+
// Log combined results
|
|
60
71
|
if (combinedResults.length) {
|
|
61
72
|
console.log('\nš Final Related Files:');
|
|
62
73
|
combinedResults.forEach((f, i) => {
|
|
@@ -66,79 +77,38 @@ export async function runAskCommand(query) {
|
|
|
66
77
|
else {
|
|
67
78
|
console.log('ā ļø No similar files found. Using query only.');
|
|
68
79
|
}
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let code = '';
|
|
73
|
-
let topSummary = topFile.summary || '(No summary available)';
|
|
74
|
-
let topFunctions = [];
|
|
75
|
-
const fileFunctions = {};
|
|
76
|
-
// Truncate summary
|
|
77
|
-
topSummary = topSummary.split('\n').slice(0, MAX_SUMMARY_LINES).join('\n');
|
|
78
|
-
const allFileIds = combinedResults
|
|
79
|
-
.map(file => file.id)
|
|
80
|
-
.filter((id) => typeof id === 'number');
|
|
81
|
-
const allFunctionsMap = getFunctionsForFiles(allFileIds); // Record<number, Function[]>
|
|
82
|
-
try {
|
|
83
|
-
code = fs.readFileSync(filepath, 'utf-8');
|
|
84
|
-
const topFileId = topFile.id;
|
|
85
|
-
topFunctions = allFunctionsMap[topFileId]?.map(fn => {
|
|
86
|
-
const content = fn.content
|
|
87
|
-
? fn.content.split('\n').slice(0, MAX_FUNCTION_LINES).join('\n')
|
|
88
|
-
: '(No content available)';
|
|
89
|
-
return {
|
|
90
|
-
name: fn.name,
|
|
91
|
-
content,
|
|
92
|
-
};
|
|
93
|
-
}) || [];
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
console.warn(`ā ļø Failed to read or analyze top file (${filepath}):`, err);
|
|
80
|
+
// STEP 4+: Build contextual prompt using topFile + combinedResults
|
|
81
|
+
if (combinedResults.length === 0) {
|
|
82
|
+
throw new Error('ā No search results found. Cannot build contextual prompt.');
|
|
97
83
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
let summary = file.summary || '(No summary available)';
|
|
102
|
-
if (summary) {
|
|
103
|
-
summary = summary.split('\n').slice(0, MAX_SUMMARY_LINES).join('\n');
|
|
104
|
-
}
|
|
105
|
-
const functions = allFunctionsMap[fileId]?.map(fn => {
|
|
106
|
-
const content = fn.content
|
|
107
|
-
? fn.content.split('\n').slice(0, MAX_FUNCTION_LINES).join('\n')
|
|
108
|
-
: '(No content available)';
|
|
109
|
-
return {
|
|
110
|
-
name: fn.name,
|
|
111
|
-
content,
|
|
112
|
-
};
|
|
113
|
-
}) || [];
|
|
114
|
-
return {
|
|
115
|
-
path: file.path,
|
|
116
|
-
summary,
|
|
117
|
-
functions,
|
|
118
|
-
};
|
|
119
|
-
});
|
|
120
|
-
// š© STEP 6: Generate file tree
|
|
121
|
-
let fileTree = '';
|
|
84
|
+
const file = combinedResults[0];
|
|
85
|
+
let code = "";
|
|
86
|
+
// STEP 4++: Add code to params
|
|
122
87
|
try {
|
|
123
|
-
|
|
88
|
+
code = fs.readFileSync(file.path, 'utf-8');
|
|
89
|
+
if (!code) {
|
|
90
|
+
console.warn(`ā ļø No code loaded for top file: ${file.path}`);
|
|
91
|
+
}
|
|
124
92
|
}
|
|
125
93
|
catch (e) {
|
|
126
|
-
console.
|
|
94
|
+
console.log("Error reading code from selected file: ", e instanceof Error ? e.message : e);
|
|
127
95
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
96
|
+
const topFile = {
|
|
97
|
+
id: file.id,
|
|
98
|
+
path: file.path,
|
|
99
|
+
summary: file.summary ?? "",
|
|
132
100
|
code,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
101
|
+
};
|
|
102
|
+
const promptArgs = {
|
|
103
|
+
topFile,
|
|
104
|
+
relatedFiles: combinedResults,
|
|
105
|
+
query,
|
|
106
|
+
};
|
|
107
|
+
console.log(chalk.blueBright('\nš¦ Building contextual prompt...'));
|
|
108
|
+
const promptContent = await buildContextualPrompt(promptArgs);
|
|
139
109
|
console.log(chalk.greenBright('ā
Prompt built successfully.'));
|
|
140
110
|
console.log(chalk.cyan(`[runAskCommand] Prompt token estimate: ~${Math.round(promptContent.length / 4)} tokens`));
|
|
141
|
-
//
|
|
111
|
+
// STEP 5: Save prompt
|
|
142
112
|
try {
|
|
143
113
|
if (!fs.existsSync(SCAI_HOME))
|
|
144
114
|
fs.mkdirSync(SCAI_HOME, { recursive: true });
|
|
@@ -148,21 +118,21 @@ export async function runAskCommand(query) {
|
|
|
148
118
|
catch (err) {
|
|
149
119
|
log('ā Failed to write prompt log:', err);
|
|
150
120
|
}
|
|
151
|
-
//
|
|
121
|
+
// STEP 6: Ask model
|
|
152
122
|
try {
|
|
153
123
|
console.log('\nš¤ Asking the model...');
|
|
154
124
|
const input = {
|
|
155
125
|
content: promptContent,
|
|
156
|
-
filepath,
|
|
126
|
+
filepath: topFile.path,
|
|
157
127
|
};
|
|
158
|
-
const modelResponse = await generate(input
|
|
128
|
+
const modelResponse = await generate(input);
|
|
159
129
|
console.log(`\nš§ Model Response:\n${modelResponse.content}`);
|
|
160
130
|
}
|
|
161
131
|
catch (err) {
|
|
162
132
|
console.error('ā Model request failed:', err);
|
|
163
133
|
}
|
|
164
134
|
}
|
|
165
|
-
//
|
|
135
|
+
// Helper: Prompt once
|
|
166
136
|
function promptOnce(promptText) {
|
|
167
137
|
return new Promise(resolve => {
|
|
168
138
|
console.log(promptText);
|
package/dist/db/fileIndex.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as sqlTemplates from './sqlTemplates.js';
|
|
|
6
6
|
import { CANDIDATE_LIMIT } from '../constants.js';
|
|
7
7
|
import { getDbForRepo } from './client.js';
|
|
8
8
|
import { scoreFiles } from '../fileRules/scoreFiles.js'; // š NEW
|
|
9
|
+
import chalk from 'chalk';
|
|
9
10
|
export function indexFile(filePath, summary, type) {
|
|
10
11
|
const stats = fs.statSync(filePath);
|
|
11
12
|
const lastModified = stats.mtime.toISOString();
|
|
@@ -44,7 +45,7 @@ export function queryFiles(safeQuery, limit = 10) {
|
|
|
44
45
|
`).all(safeQuery, limit);
|
|
45
46
|
}
|
|
46
47
|
export async function searchFiles(query, topK = 5) {
|
|
47
|
-
console.log(`š§ Searching for query: "${query}"`);
|
|
48
|
+
console.log(chalk.yellow(`š§ Searching for query: "${query}"`));
|
|
48
49
|
const embedding = await generateEmbedding(query);
|
|
49
50
|
if (!embedding) {
|
|
50
51
|
console.log('ā ļø Failed to generate embedding for query');
|
package/dist/lib/generate.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// File: lib/generate.ts
|
|
2
2
|
import { Spinner } from './spinner.js';
|
|
3
|
-
import { readConfig } from '../config.js';
|
|
3
|
+
import { Config, readConfig } from '../config.js';
|
|
4
4
|
import { startModelProcess } from '../utils/checkModel.js';
|
|
5
|
-
export async function generate(input
|
|
5
|
+
export async function generate(input) {
|
|
6
|
+
const model = Config.getModel();
|
|
6
7
|
const contextLength = readConfig().contextLength ?? 8192;
|
|
7
8
|
let prompt = input.content;
|
|
8
9
|
if (prompt.length > contextLength) {
|
|
@@ -18,7 +18,7 @@ If no meaningful changes are present, return the text: "NO UPDATE".
|
|
|
18
18
|
${input.content}
|
|
19
19
|
--- DIFF END ---
|
|
20
20
|
`.trim();
|
|
21
|
-
const response = await generate({ content: prompt }
|
|
21
|
+
const response = await generate({ content: prompt });
|
|
22
22
|
// Check if we received a meaningful result or "NO UPDATE"
|
|
23
23
|
const content = response?.content?.trim();
|
|
24
24
|
if (content === 'NO UPDATE') {
|
|
@@ -53,7 +53,7 @@ Rules:
|
|
|
53
53
|
${input.content}
|
|
54
54
|
|
|
55
55
|
`.trim();
|
|
56
|
-
const response = await generate({ content: prompt }
|
|
56
|
+
const response = await generate({ content: prompt });
|
|
57
57
|
const contentToReturn = (response.content && response.content !== 'NO UPDATE') ? response.content : input.content;
|
|
58
58
|
return {
|
|
59
59
|
content: contentToReturn,
|
|
@@ -22,7 +22,7 @@ Format your response exactly as:
|
|
|
22
22
|
Here is the diff:
|
|
23
23
|
${content}
|
|
24
24
|
`.trim();
|
|
25
|
-
const response = await generate({ content: prompt }
|
|
25
|
+
const response = await generate({ content: prompt });
|
|
26
26
|
const lines = response.content
|
|
27
27
|
.split('\n')
|
|
28
28
|
.map(line => line.trim())
|
|
@@ -45,7 +45,7 @@ describe('moduleUnderTest', () => {
|
|
|
45
45
|
${content}
|
|
46
46
|
--- END MODULE CODE ---
|
|
47
47
|
`.trim();
|
|
48
|
-
const response = await generate({ content: prompt }
|
|
48
|
+
const response = await generate({ content: prompt });
|
|
49
49
|
if (!response)
|
|
50
50
|
throw new Error('ā ļø No test code returned from model');
|
|
51
51
|
return {
|
|
@@ -38,7 +38,7 @@ Do NOT include raw content from the file. Only provide the structured JSON outpu
|
|
|
38
38
|
${content}
|
|
39
39
|
--- FILE CONTENT END ---
|
|
40
40
|
`.trim();
|
|
41
|
-
const response = await generate({ content: prompt, filepath: input.filepath }
|
|
41
|
+
const response = await generate({ content: prompt, filepath: input.filepath });
|
|
42
42
|
try {
|
|
43
43
|
// Clean the model output first
|
|
44
44
|
const cleaned = await cleanupModule.run({ content: response.content });
|
|
@@ -19,7 +19,7 @@ Refactor the following code:
|
|
|
19
19
|
${input.content}
|
|
20
20
|
--- CODE END ---
|
|
21
21
|
`.trim();
|
|
22
|
-
const response = await generate({ content: prompt }
|
|
22
|
+
const response = await generate({ content: prompt });
|
|
23
23
|
if (!response) {
|
|
24
24
|
throw new Error('ā Model returned empty response for refactoring.');
|
|
25
25
|
}
|
|
@@ -26,7 +26,7 @@ Instructions:
|
|
|
26
26
|
|
|
27
27
|
Output the repaired test file:
|
|
28
28
|
`.trim();
|
|
29
|
-
const response = await generate({ content: prompt }
|
|
29
|
+
const response = await generate({ content: prompt });
|
|
30
30
|
if (!response)
|
|
31
31
|
throw new Error("ā ļø No repaired test code returned from model");
|
|
32
32
|
return {
|
|
@@ -20,7 +20,7 @@ Format your response exactly as:
|
|
|
20
20
|
Changes:
|
|
21
21
|
${content}
|
|
22
22
|
`.trim();
|
|
23
|
-
const response = await generate({ content: prompt, filepath }
|
|
23
|
+
const response = await generate({ content: prompt, filepath });
|
|
24
24
|
// Parse response: only keep numbered lines
|
|
25
25
|
const lines = response.content
|
|
26
26
|
.split('\n')
|
|
@@ -27,7 +27,7 @@ Extension: ${ext}
|
|
|
27
27
|
${content}
|
|
28
28
|
--- FILE CONTENT END ---
|
|
29
29
|
`.trim();
|
|
30
|
-
const response = await generate({ content: prompt, filepath }
|
|
30
|
+
const response = await generate({ content: prompt, filepath });
|
|
31
31
|
if (response.content) {
|
|
32
32
|
response.summary = response.content;
|
|
33
33
|
console.log('\nš Summary:\n');
|
|
@@ -1,74 +1,112 @@
|
|
|
1
|
-
//
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
parts.push(`š§ Functions:\n${formattedFunctions}`);
|
|
1
|
+
// src/utils/buildContextualPrompt.ts
|
|
2
|
+
import { getDbForRepo } from "../db/client.js";
|
|
3
|
+
import { generateFocusedFileTree } from "./fileTree.js";
|
|
4
|
+
export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
|
|
5
|
+
const db = getDbForRepo();
|
|
6
|
+
const log = (...args) => console.log("[buildContextualPrompt]", ...args);
|
|
7
|
+
const promptSections = [];
|
|
8
|
+
const seenPaths = new Set();
|
|
9
|
+
function summarizeForPrompt(summary, maxLines = 5) {
|
|
10
|
+
if (!summary)
|
|
11
|
+
return undefined;
|
|
12
|
+
const lines = summary.split("\n").map(l => l.trim()).filter(Boolean);
|
|
13
|
+
if (lines.length <= maxLines)
|
|
14
|
+
return lines.join(" ");
|
|
15
|
+
return lines.slice(0, maxLines).join(" ") + " ā¦";
|
|
17
16
|
}
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// --- Step 1: Top file summary ---
|
|
18
|
+
if (topFile.summary) {
|
|
19
|
+
promptSections.push(`**Top file:** ${topFile.path}\n${topFile.summary}`);
|
|
20
|
+
seenPaths.add(topFile.path);
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
// --- Step 2: KG entities/tags for top file ---
|
|
23
|
+
const topEntitiesStmt = db.prepare(`
|
|
24
|
+
SELECT et.entity_type, et.entity_id, tm.name AS tag
|
|
25
|
+
FROM entity_tags et
|
|
26
|
+
JOIN tags_master tm ON et.tag_id = tm.id
|
|
27
|
+
WHERE et.entity_id = ?
|
|
28
|
+
`);
|
|
29
|
+
const topEntitiesRows = topEntitiesStmt.all(topFile.id);
|
|
30
|
+
if (topEntitiesRows.length > 0) {
|
|
31
|
+
const tags = topEntitiesRows.map(r => `- **${r.entity_type}**: ${r.tag}`);
|
|
32
|
+
promptSections.push(`**Knowledge Graph context for ${topFile.path}:**\n${tags.join("\n")}`);
|
|
31
33
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
// --- Step 3: Recursive KG traversal ---
|
|
35
|
+
const kgRelatedStmt = db.prepare(`
|
|
36
|
+
SELECT DISTINCT f.id, f.path, f.summary
|
|
37
|
+
FROM edges e
|
|
38
|
+
JOIN files f ON e.target_id = f.id
|
|
39
|
+
WHERE e.source_type = 'file'
|
|
40
|
+
AND e.target_type = 'file'
|
|
41
|
+
AND e.source_id = ?
|
|
42
|
+
`);
|
|
43
|
+
function getRelatedKGFiles(fileId, visited = new Set()) {
|
|
44
|
+
if (visited.has(fileId)) {
|
|
45
|
+
log(`š¹ Already visited fileId ${fileId}, skipping`);
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
visited.add(fileId);
|
|
49
|
+
const rows = kgRelatedStmt.all(fileId);
|
|
50
|
+
if (rows.length === 0) {
|
|
51
|
+
log(`ā ļø No edges found for fileId ${fileId}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
log(`š¹ Found ${rows.length} related files for fileId ${fileId}:`, rows.map(r => r.path));
|
|
55
|
+
}
|
|
56
|
+
let results = [];
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
results.push(row);
|
|
59
|
+
results.push(...getRelatedKGFiles(row.id, visited));
|
|
60
|
+
}
|
|
61
|
+
return results;
|
|
34
62
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
function buildFileTree(file, depth, visited = new Set()) {
|
|
64
|
+
log(`buildFileTree - file=${file.path}, depth=${depth}`);
|
|
65
|
+
if (depth === 0 || visited.has(file.id)) {
|
|
66
|
+
return { id: file.id.toString(), path: file.path, summary: summarizeForPrompt(file.summary) };
|
|
67
|
+
}
|
|
68
|
+
visited.add(file.id);
|
|
69
|
+
const relatedFiles = getRelatedKGFiles(file.id, visited)
|
|
70
|
+
.map(f => ({ id: f.id, path: f.path, summary: f.summary }))
|
|
71
|
+
.slice(0, 5); // limit max 5 children per node
|
|
72
|
+
log(`File ${file.path} has ${relatedFiles.length} related files`);
|
|
73
|
+
const relatedNodes = relatedFiles.map(f => buildFileTree(f, depth - 1, visited));
|
|
74
|
+
return {
|
|
75
|
+
id: file.id.toString(),
|
|
76
|
+
path: file.path,
|
|
77
|
+
summary: summarizeForPrompt(file.summary),
|
|
78
|
+
related: relatedNodes.length ? relatedNodes : undefined,
|
|
79
|
+
};
|
|
48
80
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
81
|
+
const kgTree = buildFileTree({ id: topFile.id, path: topFile.path, summary: topFile.summary }, kgDepth);
|
|
82
|
+
const kgJson = JSON.stringify(kgTree, null, 2);
|
|
83
|
+
promptSections.push(`**KG-Related Files (JSON tree, depth ${kgDepth}):**\n\`\`\`json\n${kgJson}\n\`\`\``);
|
|
84
|
+
// --- Step 4: File tree (shallow, depth 2) ---
|
|
85
|
+
let fileTree = "";
|
|
86
|
+
try {
|
|
87
|
+
fileTree = generateFocusedFileTree(topFile.path, 2);
|
|
88
|
+
if (fileTree) {
|
|
89
|
+
promptSections.push(`**Focused File Tree (depth 2):**\n\`\`\`\n${fileTree}\n\`\`\``);
|
|
90
|
+
}
|
|
54
91
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const relCount = relatedFiles.length;
|
|
58
|
-
const relTokens = relatedFiles.reduce((sum, f) => sum + estimateTokenCount(f.summary), 0);
|
|
59
|
-
console.log(labelColor(`š Related Files (${relCount}):`), contentColor(`${relTokens.toLocaleString()} tokens`));
|
|
60
|
-
// Optional: Show top 3 file names
|
|
61
|
-
const fileList = relatedFiles.slice(0, 3).map(f => `- ${path.basename(f.path)}`).join('\n');
|
|
62
|
-
if (fileList)
|
|
63
|
-
console.log(contentColor(fileList + (relCount > 3 ? `\n ...+${relCount - 3} more` : '')));
|
|
92
|
+
catch (e) {
|
|
93
|
+
console.warn("ā ļø Could not generate file tree:", e);
|
|
64
94
|
}
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
95
|
+
// --- Step 5: Code snippet ---
|
|
96
|
+
const MAX_LINES = 50;
|
|
97
|
+
if (topFile.code) {
|
|
98
|
+
const lines = topFile.code.split("\n").slice(0, MAX_LINES);
|
|
99
|
+
let snippet = lines.join("\n");
|
|
100
|
+
if (topFile.code.split("\n").length > MAX_LINES) {
|
|
101
|
+
snippet += "\n... [truncated]";
|
|
102
|
+
}
|
|
103
|
+
promptSections.push(`**Code Context (first ${MAX_LINES} lines):**\n\`\`\`\n${snippet}\n\`\`\``);
|
|
68
104
|
}
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
105
|
+
// --- Step 6: User query ---
|
|
106
|
+
promptSections.push(`**Query:** ${query}`);
|
|
107
|
+
// --- Step 7: Combine prompt ---
|
|
108
|
+
const promptText = promptSections.join("\n\n---\n\n");
|
|
109
|
+
log("ā
Contextual prompt built for:", topFile.path);
|
|
110
|
+
log("š Prompt preview:\n", promptText + "\n");
|
|
111
|
+
return promptText;
|
|
74
112
|
}
|
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
import { STOP_WORDS } from '../fileRules/stopWords.js';
|
|
3
3
|
export function sanitizeQueryForFts(input) {
|
|
4
4
|
input = input.trim().toLowerCase();
|
|
5
|
-
// If
|
|
5
|
+
// If the whole input looks like a filename/path, quote it
|
|
6
6
|
if (/^[\w\-./]+$/.test(input) && !/\s/.test(input)) {
|
|
7
|
-
// Escape quotes and wrap with double-quotes for FTS safety
|
|
8
7
|
return `"${input.replace(/"/g, '""')}"*`;
|
|
9
8
|
}
|
|
10
|
-
// Otherwise, treat it as a natural language prompt
|
|
11
9
|
const tokens = input
|
|
12
10
|
.split(/\s+/)
|
|
13
11
|
.map(token => token.toLowerCase())
|
|
12
|
+
.map(token => {
|
|
13
|
+
// If the token looks like a filename/path, keep it quoted
|
|
14
|
+
if (/[\w]+\.[a-z0-9]+$/.test(token)) {
|
|
15
|
+
return `"${token.replace(/"/g, '""')}"`;
|
|
16
|
+
}
|
|
17
|
+
// Otherwise, clean it like normal
|
|
18
|
+
return token
|
|
19
|
+
.replace(/[^a-z0-9_*"]/gi, '') // remove all invalid FTS5 chars
|
|
20
|
+
.replace(/'/g, "''");
|
|
21
|
+
})
|
|
14
22
|
.filter(token => token.length > 2 &&
|
|
15
|
-
!STOP_WORDS.has(token)
|
|
16
|
-
|
|
17
|
-
.map(token => token.
|
|
23
|
+
!STOP_WORDS.has(token.replace(/[*"]/g, '')) // check unquoted
|
|
24
|
+
)
|
|
25
|
+
.map(token => (token.startsWith('"') ? token : token + '*'));
|
|
18
26
|
return tokens.length > 0 ? tokens.join(' OR ') : '*';
|
|
19
27
|
}
|