scai 0.1.109 → 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/CHANGELOG.md +19 -1
- package/dist/commands/AskCmd.js +49 -79
- package/dist/commands/DaemonCmd.js +3 -1
- package/dist/config.js +13 -8
- package/dist/context.js +36 -10
- package/dist/daemon/daemonBatch.js +68 -14
- package/dist/daemon/daemonWorker.js +19 -2
- package/dist/db/fileIndex.js +2 -1
- package/dist/db/functionExtractors/extractFromJs.js +96 -16
- package/dist/db/functionExtractors/extractFromTs.js +73 -16
- package/dist/db/functionExtractors/index.js +34 -33
- package/dist/db/functionIndex.js +1 -1
- package/dist/db/schema.js +51 -5
- package/dist/index.js +5 -9
- package/dist/lib/generate.js +3 -2
- package/dist/modelSetup.js +17 -20
- package/dist/pipeline/modules/changeLogModule.js +1 -1
- package/dist/pipeline/modules/cleanupModule.js +32 -13
- 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 +55 -0
- 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/scripts/dbcheck.js +98 -0
- package/dist/utils/buildContextualPrompt.js +103 -65
- package/dist/utils/log.js +1 -1
- package/dist/utils/sanitizeQuery.js +14 -6
- package/package.json +2 -2
|
@@ -29,25 +29,21 @@ function isTopOrBottomNoise(line) {
|
|
|
29
29
|
}
|
|
30
30
|
export const cleanupModule = {
|
|
31
31
|
name: 'cleanup',
|
|
32
|
-
description: 'Remove markdown fences
|
|
32
|
+
description: 'Remove markdown fences, fluff, and non-JSON lines with colored logging',
|
|
33
33
|
async run(input) {
|
|
34
|
-
// Normalize line endings to \n
|
|
34
|
+
// Normalize line endings to \n
|
|
35
35
|
let content = input.content.replace(/\r\n/g, '\n');
|
|
36
36
|
let lines = content.split('\n');
|
|
37
37
|
// --- CLEAN TOP ---
|
|
38
|
-
// Remove noise lines before the first triple tick or end
|
|
39
38
|
while (lines.length && (lines[0].trim() === '' || isTopOrBottomNoise(lines[0]))) {
|
|
40
39
|
if (/^```(?:\w+)?$/.test(lines[0].trim()))
|
|
41
|
-
break;
|
|
40
|
+
break;
|
|
42
41
|
console.log(chalk.red(`[cleanupModule] Removing noise from top:`), chalk.yellow(`"${lines[0].trim()}"`));
|
|
43
42
|
lines.shift();
|
|
44
43
|
}
|
|
45
|
-
// If opening fence found at top, find matching closing fence
|
|
46
44
|
if (lines.length && /^```(?:\w+)?$/.test(lines[0].trim())) {
|
|
47
45
|
console.log(chalk.red(`[cleanupModule] Found opening fenced block at top.`));
|
|
48
|
-
// Remove opening fence line
|
|
49
46
|
lines.shift();
|
|
50
|
-
// Find closing fence index
|
|
51
47
|
let closingIndex = -1;
|
|
52
48
|
for (let i = 0; i < lines.length; i++) {
|
|
53
49
|
if (/^```(?:\w+)?$/.test(lines[i].trim())) {
|
|
@@ -57,26 +53,22 @@ export const cleanupModule = {
|
|
|
57
53
|
}
|
|
58
54
|
if (closingIndex !== -1) {
|
|
59
55
|
console.log(chalk.red(`[cleanupModule] Found closing fenced block at line ${closingIndex + 1}, removing fence lines.`));
|
|
60
|
-
// Remove closing fence line
|
|
61
56
|
lines.splice(closingIndex, 1);
|
|
62
57
|
}
|
|
63
58
|
else {
|
|
64
59
|
console.log(chalk.yellow(`[cleanupModule] No closing fenced block found, only removed opening fence.`));
|
|
65
60
|
}
|
|
66
|
-
// NO removal of noise lines after fenced block here (to keep new comments intact)
|
|
67
61
|
}
|
|
68
62
|
// --- CLEAN BOTTOM ---
|
|
69
|
-
// If closing fence found at bottom, remove only that triple tick line
|
|
70
63
|
if (lines.length && /^```(?:\w+)?$/.test(lines[lines.length - 1].trim())) {
|
|
71
64
|
console.log(chalk.red(`[cleanupModule] Removing closing fenced block line at bottom.`));
|
|
72
65
|
lines.pop();
|
|
73
66
|
}
|
|
74
|
-
// Remove noise lines after closing fence (now bottom)
|
|
75
67
|
while (lines.length && (lines[lines.length - 1].trim() === '' || isTopOrBottomNoise(lines[lines.length - 1]))) {
|
|
76
68
|
console.log(chalk.red(`[cleanupModule] Removing noise from bottom after fenced block:`), chalk.yellow(`"${lines[lines.length - 1].trim()}"`));
|
|
77
69
|
lines.pop();
|
|
78
70
|
}
|
|
79
|
-
// ---
|
|
71
|
+
// --- REMOVE ANY LINGERING TRIPLE TICK LINES ANYWHERE ---
|
|
80
72
|
lines = lines.filter(line => {
|
|
81
73
|
const trimmed = line.trim();
|
|
82
74
|
if (/^```(?:\w+)?$/.test(trimmed)) {
|
|
@@ -85,6 +77,33 @@ export const cleanupModule = {
|
|
|
85
77
|
}
|
|
86
78
|
return true;
|
|
87
79
|
});
|
|
88
|
-
|
|
80
|
+
// --- FINAL CLEANUP: KEEP ONLY JSON LINES INSIDE BRACES ---
|
|
81
|
+
let jsonLines = [];
|
|
82
|
+
let braceDepth = 0;
|
|
83
|
+
let insideBraces = false;
|
|
84
|
+
for (let line of lines) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
// Detect start of JSON object/array
|
|
87
|
+
if (!insideBraces && (trimmed.startsWith('{') || trimmed.startsWith('['))) {
|
|
88
|
+
insideBraces = true;
|
|
89
|
+
}
|
|
90
|
+
if (insideBraces) {
|
|
91
|
+
// Track nested braces/brackets
|
|
92
|
+
for (const char of trimmed) {
|
|
93
|
+
if (char === '{' || char === '[')
|
|
94
|
+
braceDepth++;
|
|
95
|
+
if (char === '}' || char === ']')
|
|
96
|
+
braceDepth--;
|
|
97
|
+
}
|
|
98
|
+
// Skip lines that are clearly non-JSON inside braces
|
|
99
|
+
if (!trimmed.startsWith('//') && !/^\/\*/.test(trimmed) && trimmed !== '') {
|
|
100
|
+
jsonLines.push(line);
|
|
101
|
+
}
|
|
102
|
+
// Stop collecting after outermost brace closed
|
|
103
|
+
if (braceDepth === 0)
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { content: jsonLines.join('\n').trim() };
|
|
89
108
|
}
|
|
90
109
|
};
|
|
@@ -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 {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Config } from '../../config.js';
|
|
2
|
+
import { generate } from '../../lib/generate.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { cleanupModule } from './cleanupModule.js';
|
|
5
|
+
export const kgModule = {
|
|
6
|
+
name: 'knowledge-graph',
|
|
7
|
+
description: 'Generates a knowledge graph of entities, tags, and relationships from file content.',
|
|
8
|
+
run: async (input, content) => {
|
|
9
|
+
const model = Config.getModel();
|
|
10
|
+
const ext = input.filepath ? path.extname(input.filepath).toLowerCase() : '';
|
|
11
|
+
const filename = input.filepath ? path.basename(input.filepath) : '';
|
|
12
|
+
const prompt = `
|
|
13
|
+
You are an assistant specialized in building knowledge graphs from code or text.
|
|
14
|
+
|
|
15
|
+
Your task is to extract structured information from the file content below.
|
|
16
|
+
|
|
17
|
+
File: ${filename}
|
|
18
|
+
Extension: ${ext}
|
|
19
|
+
|
|
20
|
+
📋 Instructions:
|
|
21
|
+
- Identify all entities (functions, classes, modules, or main concepts)
|
|
22
|
+
- For each entity, generate tags describing its characteristics, purpose, or category
|
|
23
|
+
- Identify relationships between entities (e.g., "uses", "extends", "calls")
|
|
24
|
+
- Return output in JSON format with the following structure:
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
"entities": [
|
|
28
|
+
{ "name": "EntityName", "type": "class|function|module|concept", "tags": ["tag1", "tag2"] }
|
|
29
|
+
],
|
|
30
|
+
"edges": [
|
|
31
|
+
{ "from": "EntityName1", "to": "EntityName2", "type": "relationship_type" }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Do NOT include raw content from the file. Only provide the structured JSON output.
|
|
36
|
+
|
|
37
|
+
--- FILE CONTENT START ---
|
|
38
|
+
${content}
|
|
39
|
+
--- FILE CONTENT END ---
|
|
40
|
+
`.trim();
|
|
41
|
+
const response = await generate({ content: prompt, filepath: input.filepath });
|
|
42
|
+
try {
|
|
43
|
+
// Clean the model output first
|
|
44
|
+
const cleaned = await cleanupModule.run({ content: response.content });
|
|
45
|
+
console.log("Cleaned knowledge graph data: ", cleaned);
|
|
46
|
+
const jsonString = cleaned.content;
|
|
47
|
+
const parsed = JSON.parse(jsonString);
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.warn('⚠️ Failed to parse KG JSON:', err);
|
|
52
|
+
return { entities: [], edges: [] }; // fallback
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -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');
|
package/dist/scripts/dbcheck.js
CHANGED
|
@@ -224,3 +224,101 @@ const functionRows = db.prepare(`
|
|
|
224
224
|
LIMIT 50
|
|
225
225
|
`).all();
|
|
226
226
|
console.table(functionRows);
|
|
227
|
+
// === Class Table Stats ===
|
|
228
|
+
console.log('\n📊 Stats for Table: classes');
|
|
229
|
+
console.log('-------------------------------------------');
|
|
230
|
+
try {
|
|
231
|
+
const classCount = db.prepare(`SELECT COUNT(*) AS count FROM classes`).get().count;
|
|
232
|
+
const distinctClassFiles = db.prepare(`SELECT COUNT(DISTINCT file_id) AS count FROM classes`).get().count;
|
|
233
|
+
console.log(`🏷 Total classes: ${classCount}`);
|
|
234
|
+
console.log(`📂 Distinct files: ${distinctClassFiles}`);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
console.error('❌ Error accessing classes table:', err.message);
|
|
238
|
+
}
|
|
239
|
+
// === Example Classes ===
|
|
240
|
+
console.log('\n🧪 Example extracted classes:');
|
|
241
|
+
try {
|
|
242
|
+
const sampleClasses = db.prepare(`
|
|
243
|
+
SELECT id, name, file_id, start_line, end_line, substr(content, 1, 100) || '...' AS short_body
|
|
244
|
+
FROM classes
|
|
245
|
+
ORDER BY id DESC
|
|
246
|
+
LIMIT 5
|
|
247
|
+
`).all();
|
|
248
|
+
sampleClasses.forEach(cls => {
|
|
249
|
+
console.log(`🏷 ID: ${cls.id}`);
|
|
250
|
+
console.log(` Name: ${cls.name}`);
|
|
251
|
+
console.log(` File: ${cls.file_id}`);
|
|
252
|
+
console.log(` Lines: ${cls.start_line}-${cls.end_line}`);
|
|
253
|
+
console.log(` Body: ${cls.short_body}\n`);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
console.error('❌ Error printing class examples:', err.message);
|
|
258
|
+
}
|
|
259
|
+
// === Edge Table Stats ===
|
|
260
|
+
console.log('\n📊 Stats for Table: edges');
|
|
261
|
+
console.log('-------------------------------------------');
|
|
262
|
+
try {
|
|
263
|
+
const edgeCount = db.prepare(`SELECT COUNT(*) AS count FROM edges`).get().count;
|
|
264
|
+
const distinctRelations = db.prepare(`SELECT COUNT(DISTINCT relation) AS count FROM edges`).get().count;
|
|
265
|
+
console.log(`🔗 Total edges: ${edgeCount}`);
|
|
266
|
+
console.log(`🧩 Distinct relations: ${distinctRelations}`);
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
console.error('❌ Error accessing edges table:', err.message);
|
|
270
|
+
}
|
|
271
|
+
// === Example Edges ===
|
|
272
|
+
console.log('\n🧪 Example edges:');
|
|
273
|
+
try {
|
|
274
|
+
const sampleEdges = db.prepare(`
|
|
275
|
+
SELECT id, source_id, target_id, relation
|
|
276
|
+
FROM edges
|
|
277
|
+
ORDER BY id DESC
|
|
278
|
+
LIMIT 10
|
|
279
|
+
`).all();
|
|
280
|
+
sampleEdges.forEach(e => {
|
|
281
|
+
console.log(`🔗 Edge ${e.id}: ${e.source_id} -[${e.relation}]-> ${e.target_id}`);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
console.error('❌ Error printing edge examples:', err.message);
|
|
286
|
+
}
|
|
287
|
+
// === Tags Master Stats ===
|
|
288
|
+
console.log('\n📊 Stats for Table: tags_master');
|
|
289
|
+
console.log('-------------------------------------------');
|
|
290
|
+
try {
|
|
291
|
+
const tagCount = db.prepare(`SELECT COUNT(*) AS count FROM tags_master`).get().count;
|
|
292
|
+
console.log(`🏷 Total tags: ${tagCount}`);
|
|
293
|
+
const sampleTags = db.prepare(`
|
|
294
|
+
SELECT id, name
|
|
295
|
+
FROM tags_master
|
|
296
|
+
ORDER BY id DESC
|
|
297
|
+
LIMIT 5
|
|
298
|
+
`).all();
|
|
299
|
+
sampleTags.forEach(tag => {
|
|
300
|
+
console.log(`🏷 Tag ${tag.id}: ${tag.name}`);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
console.error('❌ Error accessing tags_master table:', err.message);
|
|
305
|
+
}
|
|
306
|
+
// === Entity Tags Stats ===
|
|
307
|
+
console.log('\n📊 Stats for Table: entity_tags');
|
|
308
|
+
console.log('-------------------------------------------');
|
|
309
|
+
try {
|
|
310
|
+
const entityTagCount = db.prepare(`SELECT COUNT(*) AS count FROM entity_tags`).get().count;
|
|
311
|
+
console.log(`🔗 Total entity-tags: ${entityTagCount}`);
|
|
312
|
+
const sampleEntityTags = db.prepare(`
|
|
313
|
+
SELECT id, entity_type, entity_id, tag_id
|
|
314
|
+
FROM entity_tags
|
|
315
|
+
ORDER BY id DESC
|
|
316
|
+
LIMIT 10
|
|
317
|
+
`).all();
|
|
318
|
+
sampleEntityTags.forEach(et => {
|
|
319
|
+
console.log(`🔗 EntityTag ${et.id}: ${et.entity_type} ${et.entity_id} -> tag ${et.tag_id}`);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
console.error('❌ Error accessing entity_tags table:', err.message);
|
|
324
|
+
}
|
|
@@ -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
|
}
|
package/dist/utils/log.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { LOG_PATH } from "../constants.js";
|
|
1
2
|
import fs from 'fs';
|
|
2
|
-
import { LOG_PATH } from '../constants.js';
|
|
3
3
|
export function log(...args) {
|
|
4
4
|
const timestamp = new Date().toISOString();
|
|
5
5
|
const message = args.map(arg => typeof arg === 'string' ? arg : JSON.stringify(arg, null, 2)).join(' ');
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.111",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"scai": "./dist/index.js"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"workflow"
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
|
-
"build": "rm -rfd dist && tsc && git add .",
|
|
37
|
+
"build": "rm -rfd dist && tsc && chmod +x dist/index.js && git add .",
|
|
38
38
|
"start": "node dist/index.js"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|