scai 0.1.117 → 0.1.118

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.
Files changed (95) hide show
  1. package/dist/agents/MainAgent.js +255 -0
  2. package/dist/agents/contextReviewStep.js +104 -0
  3. package/dist/agents/finalPlanGenStep.js +123 -0
  4. package/dist/agents/infoPlanGenStep.js +126 -0
  5. package/dist/agents/planGeneratorStep.js +118 -0
  6. package/dist/agents/planResolverStep.js +95 -0
  7. package/dist/agents/planTargetFilesStep.js +48 -0
  8. package/dist/agents/preFileSearchCheckStep.js +95 -0
  9. package/dist/agents/selectRelevantSourcesStep.js +100 -0
  10. package/dist/agents/semanticAnalysisStep.js +144 -0
  11. package/dist/agents/structuralAnalysisStep.js +46 -0
  12. package/dist/agents/transformPlanGenStep.js +107 -0
  13. package/dist/agents/understandIntentStep.js +72 -0
  14. package/dist/agents/validationAnalysisStep.js +87 -0
  15. package/dist/commands/AskCmd.js +47 -116
  16. package/dist/commands/ChangeLogUpdateCmd.js +11 -5
  17. package/dist/commands/CommitSuggesterCmd.js +50 -75
  18. package/dist/commands/DaemonCmd.js +119 -29
  19. package/dist/commands/IndexCmd.js +41 -24
  20. package/dist/commands/InspectCmd.js +0 -1
  21. package/dist/commands/ReadlineSingleton.js +18 -0
  22. package/dist/commands/ResetDbCmd.js +20 -21
  23. package/dist/commands/ReviewCmd.js +89 -54
  24. package/dist/commands/SummaryCmd.js +12 -18
  25. package/dist/commands/WorkflowCmd.js +41 -0
  26. package/dist/commands/factory.js +254 -0
  27. package/dist/config.js +67 -15
  28. package/dist/constants.js +20 -4
  29. package/dist/context.js +10 -11
  30. package/dist/daemon/daemonQueues.js +63 -0
  31. package/dist/daemon/daemonWorker.js +40 -63
  32. package/dist/daemon/generateSummaries.js +58 -0
  33. package/dist/daemon/runFolderCapsuleBatch.js +247 -0
  34. package/dist/daemon/runIndexingBatch.js +147 -0
  35. package/dist/daemon/runKgBatch.js +104 -0
  36. package/dist/db/fileIndex.js +168 -63
  37. package/dist/db/functionExtractors/extractFromJava.js +210 -6
  38. package/dist/db/functionExtractors/extractFromJs.js +173 -214
  39. package/dist/db/functionExtractors/extractFromTs.js +159 -160
  40. package/dist/db/functionExtractors/index.js +7 -5
  41. package/dist/db/schema.js +55 -20
  42. package/dist/db/sqlTemplates.js +50 -19
  43. package/dist/fileRules/builtins.js +31 -14
  44. package/dist/fileRules/codeAllowedExtensions.js +4 -0
  45. package/dist/fileRules/fileExceptions.js +0 -13
  46. package/dist/fileRules/ignoredExtensions.js +10 -0
  47. package/dist/index.js +128 -325
  48. package/dist/lib/generate.js +37 -14
  49. package/dist/lib/generateFolderCapsules.js +109 -0
  50. package/dist/lib/spinner.js +12 -5
  51. package/dist/modelSetup.js +0 -10
  52. package/dist/pipeline/modules/changeLogModule.js +16 -19
  53. package/dist/pipeline/modules/chunkManagerModule.js +24 -0
  54. package/dist/pipeline/modules/cleanupModule.js +96 -91
  55. package/dist/pipeline/modules/codeTransformModule.js +208 -0
  56. package/dist/pipeline/modules/commentModule.js +20 -11
  57. package/dist/pipeline/modules/commitSuggesterModule.js +36 -14
  58. package/dist/pipeline/modules/contextReviewModule.js +52 -0
  59. package/dist/pipeline/modules/fileReaderModule.js +72 -0
  60. package/dist/pipeline/modules/fileSearchModule.js +136 -0
  61. package/dist/pipeline/modules/finalAnswerModule.js +53 -0
  62. package/dist/pipeline/modules/gatherInfoModule.js +176 -0
  63. package/dist/pipeline/modules/generateTestsModule.js +63 -54
  64. package/dist/pipeline/modules/kgModule.js +26 -11
  65. package/dist/pipeline/modules/preserveCodeModule.js +91 -49
  66. package/dist/pipeline/modules/refactorModule.js +19 -7
  67. package/dist/pipeline/modules/repairTestsModule.js +44 -36
  68. package/dist/pipeline/modules/reviewModule.js +23 -13
  69. package/dist/pipeline/modules/summaryModule.js +27 -35
  70. package/dist/pipeline/modules/writeFileModule.js +86 -0
  71. package/dist/pipeline/registry/moduleRegistry.js +38 -93
  72. package/dist/pipeline/runModulePipeline.js +22 -19
  73. package/dist/scripts/dbcheck.js +143 -228
  74. package/dist/utils/buildContextualPrompt.js +245 -172
  75. package/dist/utils/debugContext.js +24 -0
  76. package/dist/utils/fileTree.js +16 -6
  77. package/dist/utils/loadRelevantFolderCapsules.js +64 -0
  78. package/dist/utils/log.js +2 -0
  79. package/dist/utils/normalizeData.js +23 -0
  80. package/dist/utils/planActions.js +60 -0
  81. package/dist/utils/promptBuilderHelper.js +67 -0
  82. package/dist/utils/promptLogHelper.js +52 -0
  83. package/dist/utils/sanitizeQuery.js +20 -8
  84. package/dist/utils/sleep.js +3 -0
  85. package/dist/utils/splitCodeIntoChunk.js +65 -32
  86. package/dist/utils/vscode.js +49 -0
  87. package/dist/workflow/workflowResolver.js +14 -0
  88. package/dist/workflow/workflowRunner.js +103 -0
  89. package/package.json +6 -5
  90. package/dist/agent/agentManager.js +0 -39
  91. package/dist/agent/workflowManager.js +0 -95
  92. package/dist/commands/ModulePipelineCmd.js +0 -31
  93. package/dist/daemon/daemonBatch.js +0 -186
  94. package/dist/fileRules/scoreFiles.js +0 -71
  95. package/dist/lib/generateEmbedding.js +0 -22
@@ -1,10 +1,12 @@
1
- import { generate } from '../../lib/generate.js';
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 run({ content }) {
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
- ${content}
24
- `.trim();
25
- const response = await generate({ content: prompt });
26
- const lines = response.content
27
- .split('\n')
28
- .map(line => line.trim())
29
- .filter(line => /^\d+\.\s+/.test(line));
30
- const suggestions = lines.map(line => line.replace(/^\d+\.\s+/, '').replace(/^"(.*)"$/, '$1').trim());
31
- return {
32
- content,
33
- suggestions
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
+ };