scai 0.1.117 → 0.1.119
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 +88 -503
- package/dist/agents/MainAgent.js +255 -0
- package/dist/agents/contextReviewStep.js +104 -0
- package/dist/agents/finalPlanGenStep.js +123 -0
- package/dist/agents/infoPlanGenStep.js +126 -0
- package/dist/agents/planGeneratorStep.js +118 -0
- package/dist/agents/planResolverStep.js +95 -0
- package/dist/agents/planTargetFilesStep.js +48 -0
- package/dist/agents/preFileSearchCheckStep.js +95 -0
- package/dist/agents/selectRelevantSourcesStep.js +100 -0
- package/dist/agents/semanticAnalysisStep.js +144 -0
- package/dist/agents/structuralAnalysisStep.js +46 -0
- package/dist/agents/transformPlanGenStep.js +107 -0
- package/dist/agents/understandIntentStep.js +72 -0
- package/dist/agents/validationAnalysisStep.js +87 -0
- package/dist/commands/AskCmd.js +47 -116
- package/dist/commands/ChangeLogUpdateCmd.js +11 -5
- package/dist/commands/CommitSuggesterCmd.js +50 -75
- package/dist/commands/DaemonCmd.js +119 -29
- package/dist/commands/IndexCmd.js +41 -24
- package/dist/commands/InspectCmd.js +0 -1
- package/dist/commands/ReadlineSingleton.js +18 -0
- package/dist/commands/ResetDbCmd.js +20 -21
- package/dist/commands/ReviewCmd.js +89 -54
- package/dist/commands/SummaryCmd.js +12 -18
- package/dist/commands/WorkflowCmd.js +41 -0
- package/dist/commands/factory.js +254 -0
- package/dist/config.js +67 -15
- package/dist/constants.js +20 -4
- package/dist/context.js +10 -11
- package/dist/daemon/daemonQueues.js +63 -0
- package/dist/daemon/daemonWorker.js +40 -63
- package/dist/daemon/generateSummaries.js +58 -0
- package/dist/daemon/runFolderCapsuleBatch.js +247 -0
- package/dist/daemon/runIndexingBatch.js +147 -0
- package/dist/daemon/runKgBatch.js +104 -0
- package/dist/db/fileIndex.js +168 -63
- package/dist/db/functionExtractors/extractFromJava.js +210 -6
- package/dist/db/functionExtractors/extractFromJs.js +173 -214
- package/dist/db/functionExtractors/extractFromTs.js +159 -160
- package/dist/db/functionExtractors/index.js +7 -5
- package/dist/db/schema.js +55 -20
- package/dist/db/sqlTemplates.js +50 -19
- package/dist/fileRules/builtins.js +31 -14
- package/dist/fileRules/codeAllowedExtensions.js +4 -0
- package/dist/fileRules/fileExceptions.js +0 -13
- package/dist/fileRules/ignoredExtensions.js +10 -0
- package/dist/index.js +128 -325
- package/dist/lib/generate.js +37 -14
- package/dist/lib/generateFolderCapsules.js +109 -0
- package/dist/lib/spinner.js +12 -5
- package/dist/modelSetup.js +1 -11
- package/dist/pipeline/modules/changeLogModule.js +16 -19
- package/dist/pipeline/modules/chunkManagerModule.js +24 -0
- package/dist/pipeline/modules/cleanupModule.js +95 -91
- package/dist/pipeline/modules/codeTransformModule.js +208 -0
- package/dist/pipeline/modules/commentModule.js +20 -11
- package/dist/pipeline/modules/commitSuggesterModule.js +36 -14
- package/dist/pipeline/modules/contextReviewModule.js +52 -0
- package/dist/pipeline/modules/fileReaderModule.js +72 -0
- package/dist/pipeline/modules/fileSearchModule.js +136 -0
- package/dist/pipeline/modules/finalAnswerModule.js +53 -0
- package/dist/pipeline/modules/gatherInfoModule.js +176 -0
- package/dist/pipeline/modules/generateTestsModule.js +63 -54
- package/dist/pipeline/modules/kgModule.js +26 -11
- package/dist/pipeline/modules/preserveCodeModule.js +91 -49
- package/dist/pipeline/modules/refactorModule.js +19 -7
- package/dist/pipeline/modules/repairTestsModule.js +44 -36
- package/dist/pipeline/modules/reviewModule.js +23 -13
- package/dist/pipeline/modules/summaryModule.js +27 -35
- package/dist/pipeline/modules/writeFileModule.js +86 -0
- package/dist/pipeline/registry/moduleRegistry.js +38 -93
- package/dist/pipeline/runModulePipeline.js +22 -19
- package/dist/scripts/dbcheck.js +143 -228
- package/dist/utils/buildContextualPrompt.js +245 -172
- package/dist/utils/debugContext.js +24 -0
- package/dist/utils/fileTree.js +16 -6
- package/dist/utils/loadRelevantFolderCapsules.js +64 -0
- package/dist/utils/log.js +2 -0
- package/dist/utils/normalizeData.js +23 -0
- package/dist/utils/planActions.js +60 -0
- package/dist/utils/promptBuilderHelper.js +67 -0
- package/dist/utils/promptLogHelper.js +52 -0
- package/dist/utils/sanitizeQuery.js +20 -8
- package/dist/utils/sleep.js +3 -0
- package/dist/utils/splitCodeIntoChunk.js +65 -32
- package/dist/utils/vscode.js +49 -0
- package/dist/workflow/workflowResolver.js +14 -0
- package/dist/workflow/workflowRunner.js +103 -0
- package/package.json +6 -5
- package/dist/agent/agentManager.js +0 -39
- package/dist/agent/workflowManager.js +0 -95
- package/dist/commands/ModulePipelineCmd.js +0 -31
- package/dist/daemon/daemonBatch.js +0 -186
- package/dist/fileRules/scoreFiles.js +0 -71
- package/dist/lib/generateEmbedding.js +0 -22
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// File: src/pipeline/modules/commitSuggesterModule.ts
|
|
2
2
|
import { Config } from '../../config.js';
|
|
3
|
+
import { generate } from '../../lib/generate.js';
|
|
3
4
|
export const commitSuggesterModule = {
|
|
4
5
|
name: 'commitSuggester',
|
|
5
6
|
description: 'Suggests conventional commit messages from Git diff',
|
|
6
|
-
async
|
|
7
|
+
run: async (input) => {
|
|
7
8
|
const model = Config.getModel();
|
|
9
|
+
const diffContent = typeof input.content === 'string' ? input.content : '';
|
|
8
10
|
const prompt = `
|
|
9
11
|
Suggest ALWAYS 3 concise, conventional Git commit messages based on the input code diff.
|
|
10
12
|
|
|
@@ -20,17 +22,37 @@ Format your response exactly as:
|
|
|
20
22
|
3. <type>: <message>
|
|
21
23
|
|
|
22
24
|
Here is the diff:
|
|
23
|
-
${
|
|
24
|
-
`.trim();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
${diffContent}
|
|
26
|
+
`.trim();
|
|
27
|
+
try {
|
|
28
|
+
// === INTERACTION #1: generate.ts (via ModuleIO) ===
|
|
29
|
+
const genInput = {
|
|
30
|
+
query: 'Suggest 3 conventional commit messages',
|
|
31
|
+
content: prompt
|
|
32
|
+
};
|
|
33
|
+
const genOutput = await generate(genInput);
|
|
34
|
+
const rawText = typeof genOutput.data === 'string' ? genOutput.data : '';
|
|
35
|
+
// === Parse lines ===
|
|
36
|
+
const lines = rawText
|
|
37
|
+
.split('\n')
|
|
38
|
+
.map(line => line.trim())
|
|
39
|
+
.filter(line => /^[1-3][.)]\s+/.test(line));
|
|
40
|
+
const suggestions = lines.map(line => line.replace(/^[1-3][.)]\s+/, '').replace(/^"(.*)"$/, '$1').trim());
|
|
41
|
+
return {
|
|
42
|
+
query: input.query,
|
|
43
|
+
content: diffContent,
|
|
44
|
+
data: genOutput.data,
|
|
45
|
+
suggestions
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.warn('⚠️ commitSuggesterModule failed:', err);
|
|
50
|
+
return {
|
|
51
|
+
query: input.query,
|
|
52
|
+
content: diffContent,
|
|
53
|
+
data: {},
|
|
54
|
+
suggestions: []
|
|
55
|
+
};
|
|
56
|
+
}
|
|
35
57
|
}
|
|
36
58
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { generate } from "../../lib/generate.js";
|
|
2
|
+
import { logInputOutput } from "../../utils/promptLogHelper.js";
|
|
3
|
+
export const contextReviewModule = {
|
|
4
|
+
name: "contextReview",
|
|
5
|
+
description: "Analyzes summaries and global context to decide if reading full files is needed before answering.",
|
|
6
|
+
groups: ["analysis"],
|
|
7
|
+
run: async (input) => {
|
|
8
|
+
const query = input.query;
|
|
9
|
+
// Convert content to a text blob for the model
|
|
10
|
+
const contextText = typeof input.content === "string"
|
|
11
|
+
? input.content
|
|
12
|
+
: JSON.stringify(input.content ?? "", null, 2);
|
|
13
|
+
const promptText = `
|
|
14
|
+
You are an AI reasoning agent reviewing file summaries and contextual notes for a software repository.
|
|
15
|
+
|
|
16
|
+
User query:
|
|
17
|
+
${query}
|
|
18
|
+
|
|
19
|
+
Context:
|
|
20
|
+
${contextText}
|
|
21
|
+
|
|
22
|
+
Your task:
|
|
23
|
+
1. Summarize what this context reveals about the codebase.
|
|
24
|
+
2. Identify what is still missing or unclear.
|
|
25
|
+
3. Decide if reading full file contents is required ("needFullRead") or if current summaries are enough ("enoughForAnswer").
|
|
26
|
+
Respond concisely but analytically.
|
|
27
|
+
`.trim();
|
|
28
|
+
// ✅ Call generate() with ModuleIO-compliant input
|
|
29
|
+
const aiResponse = await generate({
|
|
30
|
+
query,
|
|
31
|
+
content: promptText,
|
|
32
|
+
});
|
|
33
|
+
// ✅ Model output is in aiResponse.data
|
|
34
|
+
const modelText = typeof aiResponse.data === "string"
|
|
35
|
+
? aiResponse.data.trim()
|
|
36
|
+
: JSON.stringify(aiResponse.data);
|
|
37
|
+
const lower = modelText.toLowerCase();
|
|
38
|
+
const decision = lower.includes("needfullread") || lower.includes("read full")
|
|
39
|
+
? "needFullRead"
|
|
40
|
+
: "enoughForAnswer";
|
|
41
|
+
// ✅ Return fully ModuleIO-compliant output
|
|
42
|
+
const output = {
|
|
43
|
+
query,
|
|
44
|
+
data: {
|
|
45
|
+
decision,
|
|
46
|
+
understanding: modelText,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
logInputOutput("contextReview", "output", output.data);
|
|
50
|
+
return output;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { logInputOutput } from "../../utils/promptLogHelper.js";
|
|
4
|
+
import { getDbForRepo } from "../../db/client.js";
|
|
5
|
+
import { updateFileWithSummary, upsertFileFtsTemplate, } from "../../db/sqlTemplates.js";
|
|
6
|
+
export const fileReaderModule = {
|
|
7
|
+
name: "fileReader",
|
|
8
|
+
description: "Reads files from disk only if they are not available in the DB, updates DB and FTS.",
|
|
9
|
+
groups: ["analysis"],
|
|
10
|
+
run: async (input) => {
|
|
11
|
+
let filePaths = [];
|
|
12
|
+
logInputOutput('filereader', 'input', input.content);
|
|
13
|
+
if (Array.isArray(input.content)) {
|
|
14
|
+
filePaths = input.content;
|
|
15
|
+
}
|
|
16
|
+
else if (typeof input.content === "object" && input.content !== null) {
|
|
17
|
+
const obj = input.content;
|
|
18
|
+
if (Array.isArray(obj.filePaths))
|
|
19
|
+
filePaths = obj.filePaths;
|
|
20
|
+
}
|
|
21
|
+
if (filePaths.length === 0) {
|
|
22
|
+
const emptyOutput = {
|
|
23
|
+
query: input.query,
|
|
24
|
+
data: { files: [] },
|
|
25
|
+
};
|
|
26
|
+
logInputOutput("fileReader", "output", emptyOutput.data);
|
|
27
|
+
return emptyOutput;
|
|
28
|
+
}
|
|
29
|
+
const db = getDbForRepo();
|
|
30
|
+
const files = [];
|
|
31
|
+
for (const filePath of filePaths) {
|
|
32
|
+
const fullPath = path.resolve(filePath);
|
|
33
|
+
// 1️⃣ Check DB for existing summary/content
|
|
34
|
+
const row = db
|
|
35
|
+
.prepare("SELECT path, content_text, summary FROM files WHERE path = ?")
|
|
36
|
+
.get(fullPath);
|
|
37
|
+
if (row?.content_text) {
|
|
38
|
+
files.push({ filePath: fullPath, content: row.content_text });
|
|
39
|
+
continue; // already in DB, no need to read disk
|
|
40
|
+
}
|
|
41
|
+
// 2️⃣ Read file from disk
|
|
42
|
+
let content = "";
|
|
43
|
+
try {
|
|
44
|
+
content = await fs.readFile(fullPath, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
content = `ERROR: ${err.message}`;
|
|
48
|
+
}
|
|
49
|
+
files.push({ filePath: fullPath, content });
|
|
50
|
+
// 3️⃣ Update DB and FTS if content successfully read
|
|
51
|
+
if (!content.startsWith("ERROR")) {
|
|
52
|
+
db.prepare(updateFileWithSummary).run({
|
|
53
|
+
path: fullPath,
|
|
54
|
+
summary: null, // optionally leave null; summary can be generated later
|
|
55
|
+
contentText: content,
|
|
56
|
+
});
|
|
57
|
+
db.prepare(upsertFileFtsTemplate).run({
|
|
58
|
+
path: fullPath,
|
|
59
|
+
filename: path.basename(fullPath),
|
|
60
|
+
summary: null,
|
|
61
|
+
contentText: content,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const output = {
|
|
66
|
+
query: input.query,
|
|
67
|
+
data: { files },
|
|
68
|
+
};
|
|
69
|
+
logInputOutput("fileReader", "output", output.data);
|
|
70
|
+
return output;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// File: src/modules/fileSearchModule.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { searchFiles } from "../../db/fileIndex.js";
|
|
6
|
+
import { Config } from "../../config.js";
|
|
7
|
+
import { getDbForRepo } from "../../db/client.js";
|
|
8
|
+
import { IGNORED_EXTENSIONS } from "../../fileRules/ignoredExtensions.js";
|
|
9
|
+
import { logInputOutput } from "../../utils/promptLogHelper.js";
|
|
10
|
+
import { generate } from "../../lib/generate.js";
|
|
11
|
+
import { cleanupModule } from "./cleanupModule.js";
|
|
12
|
+
async function fetchSummariesForPaths(paths) {
|
|
13
|
+
if (paths.length === 0)
|
|
14
|
+
return {};
|
|
15
|
+
const db = getDbForRepo();
|
|
16
|
+
const placeholders = paths.map(() => "?").join(", ");
|
|
17
|
+
const rows = db.prepare(`SELECT path, summary FROM summaries
|
|
18
|
+
WHERE path IN (${placeholders}) AND summary IS NOT NULL;`).all(...paths);
|
|
19
|
+
const map = {};
|
|
20
|
+
for (const row of rows)
|
|
21
|
+
map[row.path] = row.summary;
|
|
22
|
+
return map;
|
|
23
|
+
}
|
|
24
|
+
export const fileSearchModule = {
|
|
25
|
+
name: "fileSearch",
|
|
26
|
+
description: "Searches the indexed repo for files matching a generated query.",
|
|
27
|
+
groups: ["analysis"],
|
|
28
|
+
run: async (input) => {
|
|
29
|
+
const query = input.query ?? "";
|
|
30
|
+
const ctx = input.context;
|
|
31
|
+
if (!ctx) {
|
|
32
|
+
throw new Error("[fileSearch] StructuredContext is required.");
|
|
33
|
+
}
|
|
34
|
+
// -------------------------------------------------
|
|
35
|
+
// STEP 0: Repo root resolution
|
|
36
|
+
// -------------------------------------------------
|
|
37
|
+
let repoRoot = Config.getIndexDir() ?? process.cwd();
|
|
38
|
+
repoRoot = path.resolve(repoRoot);
|
|
39
|
+
if (!fs.existsSync(repoRoot)) {
|
|
40
|
+
return { query, data: { files: [] } };
|
|
41
|
+
}
|
|
42
|
+
// -------------------------------------------------
|
|
43
|
+
// STEP 1: LLM query generation
|
|
44
|
+
// -------------------------------------------------
|
|
45
|
+
const llmPrompt = `
|
|
46
|
+
You are a search query generator. Given the following user input,
|
|
47
|
+
produce a concise, meaningful query suitable for searching code files.
|
|
48
|
+
|
|
49
|
+
Input:
|
|
50
|
+
${typeof query === "string" ? query : JSON.stringify(query, null, 2)}
|
|
51
|
+
|
|
52
|
+
Return only the improved query text.
|
|
53
|
+
`.trim();
|
|
54
|
+
const llmResponse = await generate({
|
|
55
|
+
content: llmPrompt,
|
|
56
|
+
query
|
|
57
|
+
});
|
|
58
|
+
const cleaned = await cleanupModule.run({
|
|
59
|
+
query,
|
|
60
|
+
content: llmResponse.data
|
|
61
|
+
});
|
|
62
|
+
const searchQuery = typeof cleaned.data === "string" ? cleaned.data.trim() : "";
|
|
63
|
+
if (!searchQuery) {
|
|
64
|
+
return { query, data: { files: [] } };
|
|
65
|
+
}
|
|
66
|
+
// -------------------------------------------------
|
|
67
|
+
// STEP 2: Semantic search
|
|
68
|
+
// -------------------------------------------------
|
|
69
|
+
let results = [];
|
|
70
|
+
try {
|
|
71
|
+
results = await searchFiles(searchQuery, 5);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.warn("❌ [fileSearch] Semantic search failed:", err);
|
|
75
|
+
}
|
|
76
|
+
// -------------------------------------------------
|
|
77
|
+
// STEP 3: Grep fallback
|
|
78
|
+
// -------------------------------------------------
|
|
79
|
+
if (results.length === 0) {
|
|
80
|
+
try {
|
|
81
|
+
const exclude = IGNORED_EXTENSIONS.map(ext => `--exclude=*${ext}`).join(" ");
|
|
82
|
+
const stdout = execSync(`grep -ril ${exclude} "${query}" "${repoRoot}"`, { encoding: "utf8" });
|
|
83
|
+
results = stdout
|
|
84
|
+
.split("\n")
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.map(f => ({ path: f }));
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (err?.status !== 1) {
|
|
90
|
+
console.warn("⚠️ [fileSearch] Grep fallback failed:", err);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// -------------------------------------------------
|
|
95
|
+
// STEP 4: DB Summary enrichment
|
|
96
|
+
// -------------------------------------------------
|
|
97
|
+
const missingPaths = results.filter(f => !f.summary).map(f => f.path);
|
|
98
|
+
if (missingPaths.length > 0) {
|
|
99
|
+
try {
|
|
100
|
+
const map = await fetchSummariesForPaths(missingPaths);
|
|
101
|
+
results.forEach(f => {
|
|
102
|
+
if (!f.summary && map[f.path]) {
|
|
103
|
+
f.summary = map[f.path];
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.warn("⚠️ [fileSearch] Summary enrichment failed:", err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// -------------------------------------------------
|
|
112
|
+
// STEP 5: Persist discovered files to initContext.relatedFiles
|
|
113
|
+
// -------------------------------------------------
|
|
114
|
+
if (!ctx.initContext) {
|
|
115
|
+
ctx.initContext = {
|
|
116
|
+
userQuery: query
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const existing = new Set(ctx.initContext.relatedFiles ?? []);
|
|
120
|
+
const discoveredPaths = results.map(r => r.path);
|
|
121
|
+
// Append + deduplicate
|
|
122
|
+
ctx.initContext.relatedFiles = [
|
|
123
|
+
...(ctx.initContext.relatedFiles ?? []),
|
|
124
|
+
...discoveredPaths.filter(p => !existing.has(p))
|
|
125
|
+
];
|
|
126
|
+
// -------------------------------------------------
|
|
127
|
+
// STEP 6: Log and return ModuleIO output
|
|
128
|
+
// -------------------------------------------------
|
|
129
|
+
const output = {
|
|
130
|
+
query,
|
|
131
|
+
data: { files: results }
|
|
132
|
+
};
|
|
133
|
+
logInputOutput("fileSearch", "output", output);
|
|
134
|
+
return output;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { logInputOutput } from "../../utils/promptLogHelper.js";
|
|
2
|
+
import { generate } from "../../lib/generate.js";
|
|
3
|
+
export const finalAnswerModule = {
|
|
4
|
+
name: "finalAnswer",
|
|
5
|
+
description: "Generates a final answer using structured context and relevant working files.",
|
|
6
|
+
groups: ["finalize"],
|
|
7
|
+
run: async (input) => {
|
|
8
|
+
const query = input.query;
|
|
9
|
+
const context = input.context;
|
|
10
|
+
if (!context)
|
|
11
|
+
throw new Error("[finalAnswerModule] No context provided");
|
|
12
|
+
// Only consider authoritative target files
|
|
13
|
+
const targetFiles = context.plan?.targetFiles ?? [];
|
|
14
|
+
const workingFiles = (context.workingFiles ?? []).filter(f => targetFiles.includes(f.path));
|
|
15
|
+
const filesSummary = workingFiles.map(f => ({
|
|
16
|
+
path: f.path,
|
|
17
|
+
hasCode: !!f.code,
|
|
18
|
+
summary: f.summary ?? "",
|
|
19
|
+
codeSnippet: f.code ? f.code.slice(0, 500) : undefined, // optional snippet, avoid huge content
|
|
20
|
+
}));
|
|
21
|
+
const analysisSummary = {
|
|
22
|
+
intent: context.analysis?.intent,
|
|
23
|
+
fileAnalysis: context.analysis?.fileAnalysis,
|
|
24
|
+
combinedAnalysis: context.analysis?.combinedAnalysis,
|
|
25
|
+
};
|
|
26
|
+
const promptText = `
|
|
27
|
+
You are an AI assistant answering a developer query.
|
|
28
|
+
|
|
29
|
+
Query:
|
|
30
|
+
${query}
|
|
31
|
+
|
|
32
|
+
Analysis summary:
|
|
33
|
+
${JSON.stringify(analysisSummary, null, 2)}
|
|
34
|
+
|
|
35
|
+
Target files and content:
|
|
36
|
+
${JSON.stringify(filesSummary, null, 2)}
|
|
37
|
+
|
|
38
|
+
Instructions:
|
|
39
|
+
- Provide a clear, concise answer based only on the available files and analysis.
|
|
40
|
+
- Highlight risks or important notes from fileAnalysis or combinedAnalysis.
|
|
41
|
+
- Do not ask for additional files; assume the provided code is authoritative.
|
|
42
|
+
`;
|
|
43
|
+
const aiResponse = await generate({ query, content: promptText });
|
|
44
|
+
const output = {
|
|
45
|
+
query,
|
|
46
|
+
content: '',
|
|
47
|
+
data: aiResponse.data,
|
|
48
|
+
};
|
|
49
|
+
logInputOutput("finalAnswer", "output", output.data);
|
|
50
|
+
console.log('\n--> Answer: ', output.data);
|
|
51
|
+
return output;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// src/pipeline/modules/gatherInfoModule.ts
|
|
2
|
+
import { getDbForRepo } from "../../db/client.js";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { sanitizeQueryForFts } from "../../utils/sanitizeQuery.js";
|
|
5
|
+
import { logInputOutput } from "../../utils/promptLogHelper.js"; // ✅ import logger
|
|
6
|
+
/** Escape % and _ for LIKE queries */
|
|
7
|
+
function sanitizeForLike(input) {
|
|
8
|
+
return input.replace(/[%_]/g, "\\$&");
|
|
9
|
+
}
|
|
10
|
+
/** ✅ Strip embeddings before logging */
|
|
11
|
+
function stripEmbeddings(output) {
|
|
12
|
+
if (output?.data?.files) {
|
|
13
|
+
output.data.files = output.data.files.map((f) => {
|
|
14
|
+
const { embedding, ...rest } = f;
|
|
15
|
+
return rest;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return output;
|
|
19
|
+
}
|
|
20
|
+
export const gatherInfoModule = {
|
|
21
|
+
name: "gatherInfo",
|
|
22
|
+
description: "Collects relevant code summaries, functions, classes, and related graph info for the current query.",
|
|
23
|
+
run: async function (input) {
|
|
24
|
+
const db = getDbForRepo();
|
|
25
|
+
const query = input.metadata?.query?.trim() ?? "";
|
|
26
|
+
const maxFiles = input.metadata?.maxFiles ?? 10;
|
|
27
|
+
if (!query) {
|
|
28
|
+
const emptyOutput = {
|
|
29
|
+
content: "⚠️ No query provided to gatherInfoModule.",
|
|
30
|
+
data: { files: [], functions: [], classes: [], summaries: [], tags: [] },
|
|
31
|
+
};
|
|
32
|
+
logInputOutput("gatherInfo", "output", stripEmbeddings(emptyOutput)); // ✅
|
|
33
|
+
return emptyOutput;
|
|
34
|
+
}
|
|
35
|
+
const sanitizedFts = sanitizeQueryForFts(query);
|
|
36
|
+
const likeQuery = `%${sanitizeForLike(query)}%`;
|
|
37
|
+
// 🩹 Handle legacy DBs that might not have `functions_extracted`
|
|
38
|
+
let files = [];
|
|
39
|
+
try {
|
|
40
|
+
files = db
|
|
41
|
+
.prepare(`
|
|
42
|
+
SELECT f.id, f.path, f.type, f.summary, f.embedding,
|
|
43
|
+
f.last_modified, f.indexed_at,
|
|
44
|
+
COALESCE(f.functions_extracted, 0) AS functions_extracted,
|
|
45
|
+
f.functions_extracted_at, f.processing_status
|
|
46
|
+
FROM files f
|
|
47
|
+
JOIN files_fts fts ON fts.rowid = f.id
|
|
48
|
+
WHERE files_fts MATCH '${sanitizedFts}'
|
|
49
|
+
ORDER BY f.path ASC
|
|
50
|
+
LIMIT ?;
|
|
51
|
+
`)
|
|
52
|
+
.all(maxFiles);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.warn(chalk.yellow("⚠️ 'functions_extracted' column missing in files table, running fallback query..."));
|
|
56
|
+
files = db
|
|
57
|
+
.prepare(`
|
|
58
|
+
SELECT f.id, f.path, f.type, f.summary, f.embedding,
|
|
59
|
+
f.last_modified, f.indexed_at, f.processing_status
|
|
60
|
+
FROM files f
|
|
61
|
+
JOIN files_fts fts ON fts.rowid = f.id
|
|
62
|
+
WHERE files_fts MATCH '${sanitizedFts}'
|
|
63
|
+
ORDER BY f.path ASC
|
|
64
|
+
LIMIT ?;
|
|
65
|
+
`)
|
|
66
|
+
.all(maxFiles);
|
|
67
|
+
}
|
|
68
|
+
const functions = db
|
|
69
|
+
.prepare(`
|
|
70
|
+
SELECT fn.id, fn.name, fn.start_line, fn.end_line,
|
|
71
|
+
substr(fn.content, 1, 400) AS content,
|
|
72
|
+
f.path AS file_path
|
|
73
|
+
FROM functions fn
|
|
74
|
+
JOIN files f ON f.id = fn.file_id
|
|
75
|
+
WHERE fn.name LIKE ? ESCAPE '\\'
|
|
76
|
+
OR fn.content LIKE ? ESCAPE '\\'
|
|
77
|
+
ORDER BY f.path ASC
|
|
78
|
+
LIMIT ?;
|
|
79
|
+
`)
|
|
80
|
+
.all(likeQuery, likeQuery, maxFiles);
|
|
81
|
+
const classes = db
|
|
82
|
+
.prepare(`
|
|
83
|
+
SELECT c.name, f.path AS file_path, substr(c.content, 1, 400) AS snippet
|
|
84
|
+
FROM graph_classes c
|
|
85
|
+
JOIN files f ON f.id = c.file_id
|
|
86
|
+
WHERE c.name LIKE ? ESCAPE '\\'
|
|
87
|
+
OR c.content LIKE ? ESCAPE '\\'
|
|
88
|
+
ORDER BY f.path ASC
|
|
89
|
+
LIMIT ?;
|
|
90
|
+
`)
|
|
91
|
+
.all(likeQuery, likeQuery, maxFiles);
|
|
92
|
+
const tagRows = db
|
|
93
|
+
.prepare(`
|
|
94
|
+
SELECT DISTINCT gtm.name
|
|
95
|
+
FROM graph_tags_master gtm
|
|
96
|
+
WHERE gtm.name LIKE ? ESCAPE '\\'
|
|
97
|
+
ORDER BY gtm.name ASC;
|
|
98
|
+
`)
|
|
99
|
+
.all(likeQuery);
|
|
100
|
+
const tags = tagRows.map((t) => t.name);
|
|
101
|
+
const summaries = db
|
|
102
|
+
.prepare(`
|
|
103
|
+
SELECT path, type, summary
|
|
104
|
+
FROM summaries
|
|
105
|
+
WHERE summary LIKE ? ESCAPE '\\'
|
|
106
|
+
ORDER BY CASE WHEN type='project' THEN 0 ELSE 1 END, path ASC
|
|
107
|
+
LIMIT 5;
|
|
108
|
+
`)
|
|
109
|
+
.all(likeQuery);
|
|
110
|
+
// --- Gather common project config files
|
|
111
|
+
let configFiles = [];
|
|
112
|
+
try {
|
|
113
|
+
configFiles = db
|
|
114
|
+
.prepare(`
|
|
115
|
+
SELECT f.id, f.path, f.type, f.summary, f.embedding,
|
|
116
|
+
f.last_modified, f.indexed_at,
|
|
117
|
+
COALESCE(f.functions_extracted, 0) AS functions_extracted,
|
|
118
|
+
f.functions_extracted_at, f.processing_status
|
|
119
|
+
FROM files f
|
|
120
|
+
WHERE path LIKE '%package.json%'
|
|
121
|
+
OR path LIKE '%tsconfig.json%'
|
|
122
|
+
OR path LIKE '%.eslintrc%'
|
|
123
|
+
OR path LIKE '%.env%'
|
|
124
|
+
OR path LIKE '%README.md%'
|
|
125
|
+
ORDER BY path ASC;
|
|
126
|
+
`)
|
|
127
|
+
.all();
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
configFiles = db
|
|
131
|
+
.prepare(`
|
|
132
|
+
SELECT f.id, f.path, f.type, f.summary, f.embedding,
|
|
133
|
+
f.last_modified, f.indexed_at, f.processing_status
|
|
134
|
+
FROM files f
|
|
135
|
+
WHERE path LIKE '%package.json%'
|
|
136
|
+
OR path LIKE '%tsconfig.json%'
|
|
137
|
+
OR path LIKE '%.eslintrc%'
|
|
138
|
+
OR path LIKE '%.env%'
|
|
139
|
+
OR path LIKE '%README.md%'
|
|
140
|
+
ORDER BY path ASC;
|
|
141
|
+
`)
|
|
142
|
+
.all();
|
|
143
|
+
}
|
|
144
|
+
// Merge & deduplicate
|
|
145
|
+
const uniqueFiles = [
|
|
146
|
+
...files,
|
|
147
|
+
...configFiles.filter((c) => !files.find((f) => f.path === c.path)),
|
|
148
|
+
];
|
|
149
|
+
if (!uniqueFiles.length &&
|
|
150
|
+
!functions.length &&
|
|
151
|
+
!classes.length &&
|
|
152
|
+
!summaries.length &&
|
|
153
|
+
!tags.length) {
|
|
154
|
+
const emptyOutput = {
|
|
155
|
+
content: `⚠️ No relevant information found for query: "${query}"`,
|
|
156
|
+
data: { files: [], functions: [], classes: [], summaries: [], tags: [] },
|
|
157
|
+
};
|
|
158
|
+
logInputOutput("gatherInfo", "output", stripEmbeddings(emptyOutput)); // ✅
|
|
159
|
+
return emptyOutput;
|
|
160
|
+
}
|
|
161
|
+
const output = {
|
|
162
|
+
content: `Gathered ${uniqueFiles.length} files, ${functions.length} functions, ${classes.length} classes, and ${summaries.length} summaries.`,
|
|
163
|
+
data: {
|
|
164
|
+
query, // ✅ ensure query passes forward
|
|
165
|
+
files: uniqueFiles,
|
|
166
|
+
functions,
|
|
167
|
+
classes,
|
|
168
|
+
summaries,
|
|
169
|
+
tags,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
// ✅ Log sanitized module output (no embeddings)
|
|
173
|
+
logInputOutput("gatherInfo", "output", stripEmbeddings(output));
|
|
174
|
+
return output;
|
|
175
|
+
},
|
|
176
|
+
};
|