scai 0.1.108 → 0.1.110

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.
@@ -1,15 +1,16 @@
1
- import { Project, SyntaxKind } from 'ts-morph';
1
+ import { Project, SyntaxKind, } from 'ts-morph';
2
2
  import path from 'path';
3
3
  import { generateEmbedding } from '../../lib/generateEmbedding.js';
4
4
  import { log } from '../../utils/log.js';
5
5
  import { getDbForRepo } from '../client.js';
6
- import { markFileAsSkippedTemplate, markFileAsExtractedTemplate, markFileAsFailedTemplate } from '../sqlTemplates.js';
6
+ import { markFileAsSkippedTemplate, markFileAsExtractedTemplate, markFileAsFailedTemplate, } from '../sqlTemplates.js';
7
7
  export async function extractFromTS(filePath, content, fileId) {
8
8
  const db = getDbForRepo();
9
9
  try {
10
10
  const project = new Project({ useInMemoryFileSystem: true });
11
11
  const sourceFile = project.createSourceFile(filePath, content);
12
12
  const functions = [];
13
+ const classes = [];
13
14
  const allFuncs = [
14
15
  ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration),
15
16
  ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionExpression),
@@ -22,45 +23,101 @@ export async function extractFromTS(filePath, content, fileId) {
22
23
  const code = fn.getText();
23
24
  functions.push({ name, start_line: start, end_line: end, content: code });
24
25
  }
25
- if (functions.length === 0) {
26
- log(`⚠️ No functions found in TS file: ${filePath}`);
26
+ const allClasses = [
27
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration),
28
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.ClassExpression),
29
+ ];
30
+ for (const cls of allClasses) {
31
+ const name = cls.getName() ?? `${path.basename(filePath)}:<anon-class>`;
32
+ const start = cls.getStartLineNumber();
33
+ const end = cls.getEndLineNumber();
34
+ const code = cls.getText();
35
+ const superClass = cls.getExtends()?.getText() ?? null;
36
+ classes.push({
37
+ name,
38
+ start_line: start,
39
+ end_line: end,
40
+ content: code,
41
+ superClass,
42
+ });
43
+ }
44
+ if (functions.length === 0 && classes.length === 0) {
45
+ log(`⚠️ No functions/classes found in TS file: ${filePath}`);
27
46
  db.prepare(markFileAsSkippedTemplate).run({ id: fileId });
28
47
  return false;
29
48
  }
30
- log(`🔍 Found ${functions.length} TypeScript functions in ${filePath}`);
49
+ log(`🔍 Found ${functions.length} functions and ${classes.length} classes in ${filePath}`);
50
+ // Insert functions
31
51
  for (const fn of functions) {
32
52
  const embedding = await generateEmbedding(fn.content);
33
- const result = db.prepare(`
53
+ const result = db
54
+ .prepare(`
34
55
  INSERT INTO functions (
35
56
  file_id, name, start_line, end_line, content, embedding, lang
36
57
  ) VALUES (
37
58
  @file_id, @name, @start_line, @end_line, @content, @embedding, @lang
38
59
  )
39
- `).run({
60
+ `)
61
+ .run({
40
62
  file_id: fileId,
41
63
  name: fn.name,
42
64
  start_line: fn.start_line,
43
65
  end_line: fn.end_line,
44
66
  content: fn.content,
45
67
  embedding: JSON.stringify(embedding),
46
- lang: 'ts'
68
+ lang: 'ts',
47
69
  });
48
- const callerId = result.lastInsertRowid;
49
- // Simplified call detection (no walking for now)
70
+ const functionId = result.lastInsertRowid;
71
+ // file function edge
72
+ db.prepare(`INSERT INTO edges (source_type, source_id, target_type, target_id, relation)
73
+ VALUES ('file', @source_id, 'function', @target_id, 'contains')`).run({ source_id: fileId, target_id: functionId });
74
+ // Simplified call detection (regex)
50
75
  const callMatches = fn.content.matchAll(/(\w+)\s*\(/g);
51
76
  for (const match of callMatches) {
52
- db.prepare(`
53
- INSERT INTO function_calls (caller_id, callee_name)
54
- VALUES (@caller_id, @callee_name)
55
- `).run({
56
- caller_id: callerId,
77
+ // Store call by name (resolution happens later)
78
+ db.prepare(`INSERT INTO function_calls (caller_id, callee_name)
79
+ VALUES (@caller_id, @callee_name)`).run({
80
+ caller_id: functionId,
57
81
  callee_name: match[1],
58
82
  });
59
83
  }
60
84
  log(`📌 Indexed TS function: ${fn.name}`);
61
85
  }
86
+ // Insert classes
87
+ for (const cls of classes) {
88
+ const embedding = await generateEmbedding(cls.content);
89
+ const result = db
90
+ .prepare(`
91
+ INSERT INTO classes (
92
+ file_id, name, start_line, end_line, content, embedding, lang
93
+ ) VALUES (
94
+ @file_id, @name, @start_line, @end_line, @content, @embedding, @lang
95
+ )
96
+ `)
97
+ .run({
98
+ file_id: fileId,
99
+ name: cls.name,
100
+ start_line: cls.start_line,
101
+ end_line: cls.end_line,
102
+ content: cls.content,
103
+ embedding: JSON.stringify(embedding),
104
+ lang: 'ts',
105
+ });
106
+ const classId = result.lastInsertRowid;
107
+ // file → class edge
108
+ db.prepare(`INSERT INTO edges (source_type, source_id, target_type, target_id, relation)
109
+ VALUES ('file', @source_id, 'class', @target_id, 'contains')`).run({ source_id: fileId, target_id: classId });
110
+ // superclass reference → store in helper table for later resolution
111
+ if (cls.superClass) {
112
+ db.prepare(`INSERT INTO class_inheritance (class_id, super_name)
113
+ VALUES (@class_id, @super_name)`).run({ class_id: classId, super_name: cls.superClass });
114
+ log(`🔗 Class ${cls.name} extends ${cls.superClass} (edge stored for later resolution)`);
115
+ }
116
+ log(`🏷 Indexed TS class: ${cls.name} (id=${classId})`);
117
+ }
118
+ log(`📊 Extraction summary for ${filePath}: ${functions.length} functions, ${classes.length} classes`);
62
119
  db.prepare(markFileAsExtractedTemplate).run({ id: fileId });
63
- log(`✅ Marked TS functions as extracted for ${filePath}`);
120
+ log(`✅ Marked TS functions/classes as extracted for ${filePath}`);
64
121
  return true;
65
122
  }
66
123
  catch (err) {
@@ -1,43 +1,44 @@
1
- import { log } from '../../utils/log.js';
2
- import { detectFileType } from '../../fileRules/detectFileType.js';
3
- import { extractFromJava } from './extractFromJava.js';
4
- import { extractFromJS } from './extractFromJs.js';
5
- import { extractFromXML } from './extractFromXML.js';
6
- import { getDbForRepo } from '../client.js';
7
- import { markFileAsFailedTemplate, markFileAsSkippedByPath } from '../sqlTemplates.js';
8
- import { extractFromTS } from './extractFromTs.js';
9
- /**
10
- * Detects file type and delegates to the appropriate extractor.
11
- */
1
+ import { getDbForRepo } from "../client.js";
2
+ import { markFileAsSkippedByPath, markFileAsFailedTemplate } from "../sqlTemplates.js";
3
+ import { extractFromJava } from "./extractFromJava.js";
4
+ import { extractFromJS } from "./extractFromJs.js";
5
+ import { extractFromTS } from "./extractFromTs.js";
6
+ import { extractFromXML } from "./extractFromXML.js";
7
+ import { detectFileType } from "../../fileRules/detectFileType.js";
8
+ import { log } from "../../utils/log.js";
12
9
  export async function extractFunctionsFromFile(filePath, content, fileId) {
13
10
  const type = detectFileType(filePath).trim().toLowerCase();
14
11
  const db = getDbForRepo();
15
12
  try {
16
- if (type === 'js' || type === 'javascript') {
17
- log(`✅ Attempting to extract JS functions from ${filePath}`);
18
- return await extractFromJS(filePath, content, fileId);
13
+ let success = false;
14
+ switch (type) {
15
+ case 'js':
16
+ case 'javascript':
17
+ log(`📄 Extracting JS code from ${filePath}`);
18
+ success = await extractFromJS(filePath, content, fileId);
19
+ break;
20
+ case 'ts':
21
+ case 'typescript':
22
+ log(`📘 Extracting TS code from ${filePath}`);
23
+ success = await extractFromTS(filePath, content, fileId);
24
+ break;
25
+ case 'java':
26
+ log(`⚠️ Java extraction not implemented for ${filePath}`);
27
+ await extractFromJava(filePath, content, fileId);
28
+ return false;
29
+ case 'xml':
30
+ log(`⚠️ XML extraction not implemented for ${filePath}`);
31
+ await extractFromXML(filePath, content, fileId);
32
+ return false;
33
+ default:
34
+ log(`⚠️ Unsupported file type: ${type}. Skipping ${filePath}`);
35
+ db.prepare(markFileAsSkippedByPath).run({ path: filePath });
36
+ return false;
19
37
  }
20
- if (type === 'ts' || type === 'typescript') {
21
- log(`📘 Extracting TS functions from ${filePath}`);
22
- return await extractFromTS(filePath, content, fileId);
23
- }
24
- if (type === 'java') {
25
- log(`❌ Nothing extracted for ${filePath} due to missing implementation`);
26
- await extractFromJava(filePath, content, fileId);
27
- return false;
28
- }
29
- if (type === 'xml') {
30
- log(`❌ Nothing extracted for ${filePath} due to missing implementation`);
31
- await extractFromXML(filePath, content, fileId);
32
- return false;
33
- }
34
- log(`⚠️ Unsupported file type: ${type} for function extraction. Skipping ${filePath}`);
35
- db.prepare(markFileAsSkippedByPath).run({ path: filePath });
36
- return false;
38
+ return success;
37
39
  }
38
40
  catch (error) {
39
- log(`❌ Failed to extract functions from ${filePath}: ${error instanceof Error ? error.message : error}`);
40
- // Use the sqlTemplate to mark the file as 'failed'
41
+ log(`❌ Failed to extract from ${filePath}: ${error instanceof Error ? error.message : error}`);
41
42
  db.prepare(markFileAsFailedTemplate).run({ id: fileId });
42
43
  return false;
43
44
  }
@@ -5,7 +5,7 @@ import { extractFunctionsFromFile } from './functionExtractors/index.js';
5
5
  * Extracts functions from file if language is supported.
6
6
  * Returns true if functions were extracted, false otherwise.
7
7
  */
8
- export async function indexFunctionsForFile(filePath, fileId) {
8
+ export async function indexCodeForFile(filePath, fileId) {
9
9
  const normalizedPath = path.normalize(filePath).replace(/\\/g, '/');
10
10
  const content = fs.readFileSync(filePath, 'utf-8');
11
11
  return await extractFunctionsFromFile(normalizedPath, content, fileId);
package/dist/db/schema.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { getDbForRepo } from "./client.js";
2
2
  export function initSchema() {
3
3
  const db = getDbForRepo();
4
+ // --- Existing tables ---
4
5
  db.exec(`
5
- -- Create the files table
6
6
  CREATE TABLE IF NOT EXISTS files (
7
7
  id INTEGER PRIMARY KEY AUTOINCREMENT,
8
8
  path TEXT UNIQUE,
@@ -16,12 +16,9 @@ export function initSchema() {
16
16
  functions_extracted_at TEXT
17
17
  );
18
18
 
19
- -- Create the full-text search table, auto-updated via content=files
20
19
  CREATE VIRTUAL TABLE IF NOT EXISTS files_fts
21
20
  USING fts5(filename, summary, path, content='files', content_rowid='id');
22
21
  `);
23
- console.log('✅ SQLite schema initialized with FTS5 auto-sync');
24
- // Create additional tables for functions and function_calls
25
22
  db.exec(`
26
23
  CREATE TABLE IF NOT EXISTS functions (
27
24
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -41,5 +38,54 @@ export function initSchema() {
41
38
  callee_name TEXT
42
39
  );
43
40
  `);
44
- console.log('✅ Schema for functions and function_calls initialized');
41
+ // --- KG-specific additions ---
42
+ // Classes table
43
+ db.exec(`
44
+ CREATE TABLE IF NOT EXISTS classes (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ file_id INTEGER REFERENCES files(id),
47
+ name TEXT,
48
+ start_line INTEGER,
49
+ end_line INTEGER,
50
+ content TEXT,
51
+ embedding TEXT,
52
+ lang TEXT
53
+ );
54
+
55
+ CREATE INDEX IF NOT EXISTS idx_class_file_id ON classes(file_id);
56
+ `);
57
+ // Edges table (function/class/file relations)
58
+ db.exec(`
59
+ CREATE TABLE IF NOT EXISTS edges (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ source_type TEXT NOT NULL, -- 'function' | 'class' | 'file'
62
+ source_id INTEGER NOT NULL,
63
+ target_type TEXT NOT NULL,
64
+ target_id INTEGER NOT NULL,
65
+ relation TEXT NOT NULL -- e.g., 'calls', 'inherits', 'contains'
66
+ );
67
+
68
+ CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_type, source_id);
69
+ CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_type, target_id);
70
+ `);
71
+ // --- Improved tags setup ---
72
+ // Master tag table
73
+ db.exec(`
74
+ CREATE TABLE IF NOT EXISTS tags_master (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ name TEXT UNIQUE NOT NULL
77
+ );
78
+
79
+ CREATE TABLE IF NOT EXISTS entity_tags (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ entity_type TEXT NOT NULL, -- 'function' | 'class' | 'file'
82
+ entity_id INTEGER NOT NULL,
83
+ tag_id INTEGER NOT NULL REFERENCES tags_master(id),
84
+ UNIQUE(entity_type, entity_id, tag_id)
85
+ );
86
+
87
+ CREATE INDEX IF NOT EXISTS idx_entity_tags_entity ON entity_tags(entity_type, entity_id);
88
+ CREATE INDEX IF NOT EXISTS idx_entity_tags_tag ON entity_tags(tag_id);
89
+ `);
90
+ console.log('✅ KG schema initialized (files, functions, classes, edges, tags)');
45
91
  }
package/dist/index.js CHANGED
@@ -193,9 +193,11 @@ const config = cmd.command('config').description('Manage SCAI configuration');
193
193
  config
194
194
  .command('set-model <model>')
195
195
  .description('Set the model to use')
196
- .action(async (model) => {
196
+ .option('-g, --global', 'Set the global default model instead of the active repo')
197
+ .action(async (model, options) => {
197
198
  await withContext(async () => {
198
- Config.setModel(model);
199
+ const scope = options.global ? 'global' : 'repo';
200
+ Config.setModel(model, scope);
199
201
  Config.show();
200
202
  });
201
203
  });
@@ -337,14 +339,8 @@ cmd.addHelpText('after', `
337
339
  💡 Use with caution and expect possible changes or instability.
338
340
  `);
339
341
  cmd.parse(process.argv);
340
- const opts = cmd.opts();
341
- if (opts.model)
342
- Config.setModel(opts.model);
343
- if (opts.lang)
344
- Config.setLanguage(opts.lang);
345
342
  async function withContext(action) {
346
343
  const ok = await updateContext();
347
- if (!ok)
348
- process.exit(1);
344
+ //if (!ok) process.exit(1);
349
345
  await action();
350
346
  }
@@ -9,7 +9,7 @@ import { readConfig, writeConfig } from './config.js';
9
9
  import { CONFIG_PATH } from './constants.js';
10
10
  // Constants
11
11
  const MODEL_PORT = 11434;
12
- const REQUIRED_MODELS = ['llama3', 'mistral'];
12
+ const REQUIRED_MODELS = ['llama3:8b'];
13
13
  const OLLAMA_URL = 'https://ollama.com/download';
14
14
  const isYesMode = process.argv.includes('--yes') || process.env.SCAI_YES === '1';
15
15
  let ollamaChecked = false;
@@ -30,16 +30,16 @@ export async function autoInitIfNeeded() {
30
30
  }
31
31
  }
32
32
  }
33
- // 🗨 Prompt user with 10-second timeout
34
- function promptUser(question) {
33
+ // 🗨 Prompt user with configurable timeout
34
+ function promptUser(question, timeout = 20000) {
35
35
  if (isYesMode)
36
36
  return Promise.resolve('y');
37
37
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
38
38
  return new Promise((resolve) => {
39
39
  const timer = setTimeout(() => {
40
40
  rl.close();
41
- resolve('');
42
- }, 10000); // 10 second timeout
41
+ resolve(''); // treat empty as "continue"
42
+ }, timeout);
43
43
  rl.question(question, (answer) => {
44
44
  clearTimeout(timer);
45
45
  rl.close();
@@ -89,7 +89,7 @@ async function ensureOllamaRunning() {
89
89
  windowsHide: true,
90
90
  });
91
91
  child.unref();
92
- await new Promise((res) => setTimeout(res, 10000));
92
+ await new Promise((res) => setTimeout(res, 10000)); // give more time
93
93
  if (await isOllamaRunning()) {
94
94
  console.log(chalk.green('✅ Ollama started successfully.'));
95
95
  ollamaAvailable = true;
@@ -102,23 +102,21 @@ async function ensureOllamaRunning() {
102
102
  process.exit(1);
103
103
  }
104
104
  }
105
- // If we get here, Ollama likely isn't installed
105
+ // Ollama not detected; prompt user but allow continuing
106
106
  console.log(chalk.red('❌ Ollama is not installed or not in PATH.'));
107
107
  console.log(chalk.yellow(`📦 Ollama is required to run local AI models.`));
108
- const answer = await promptUser('🌐 Would you like to open the download page in your browser? (y/N): ');
108
+ const answer = await promptUser(`🌐 Recommended model: ${REQUIRED_MODELS.join(', ')}\nOpen download page in browser? (y/N): `);
109
109
  if (answer.toLowerCase() === 'y') {
110
110
  openBrowser(OLLAMA_URL);
111
111
  }
112
- console.log(chalk.yellow('⏳ Waiting for you to install Ollama and press Enter to continue...'));
113
- await promptUser('👉 Press Enter once Ollama is installed and ready: ');
114
- // Retry once
112
+ await promptUser('⏳ Press Enter once Ollama is installed or to continue without it: ');
115
113
  if (await isOllamaRunning()) {
116
114
  console.log(chalk.green('✅ Ollama detected. Continuing...'));
117
115
  ollamaAvailable = true;
118
116
  }
119
117
  else {
120
- console.log(chalk.red('Ollama still not detected. Please check your installation.'));
121
- process.exit(1);
118
+ console.log(chalk.yellow('⚠️ Ollama not running. Models will not be available until installed.'));
119
+ ollamaAvailable = false; // continue anyway
122
120
  }
123
121
  }
124
122
  // 🧰 List installed models
@@ -134,7 +132,7 @@ async function getInstalledModels() {
134
132
  return [];
135
133
  }
136
134
  }
137
- // 📥 Download missing models
135
+ // 📥 Suggest required models but don’t block
138
136
  async function ensureModelsDownloaded() {
139
137
  if (!ollamaAvailable)
140
138
  return;
@@ -144,11 +142,11 @@ async function ensureModelsDownloaded() {
144
142
  console.log(chalk.green('✅ All required models are installed.'));
145
143
  return;
146
144
  }
147
- console.log(chalk.yellow(`📦 Missing models: ${missing.join(', ')}`));
148
- const answer = await promptUser('⬇️ Do you want to download them now? (y/N): ');
145
+ console.log(chalk.yellow(`📦 Suggested models: ${missing.join(', ')}`));
146
+ const answer = await promptUser('⬇️ Download them now? (y/N, continue anyway): ');
149
147
  if (answer.toLowerCase() !== 'y') {
150
- console.log(chalk.red('🚫 Aborting due to missing models.'));
151
- process.exit(1);
148
+ console.log(chalk.yellow('⚠️ Continuing without installing models. You can install later via config.'));
149
+ return;
152
150
  }
153
151
  for (const model of missing) {
154
152
  try {
@@ -157,8 +155,7 @@ async function ensureModelsDownloaded() {
157
155
  console.log(chalk.green(`✅ Pulled ${model}`));
158
156
  }
159
157
  catch {
160
- console.log(chalk.red(`❌ Failed to pull ${model}.`));
161
- process.exit(1);
158
+ console.log(chalk.red(`❌ Failed to pull ${model}, continuing...`));
162
159
  }
163
160
  }
164
161
  }
@@ -7,16 +7,27 @@ export const cleanGeneratedTestsModule = {
7
7
  // normalize + strip markdown
8
8
  const normalized = normalizeText(content);
9
9
  const stripped = stripMarkdownFences(normalized);
10
- // filter non-code lines
11
- const lines = stripped.split("\n");
12
10
  // filter non-code lines, but keep blank ones
11
+ const lines = stripped.split("\n");
13
12
  const codeLines = lines.filter(line => line.trim() === "" || isCodeLike(line));
14
- const cleanedCode = codeLines.join("\n");
13
+ // remove duplicate imports (normalize spacing/semicolon)
14
+ const seenImports = new Set();
15
+ const dedupedLines = codeLines.filter(line => {
16
+ if (line.trim().startsWith("import")) {
17
+ const key = line.trim().replace(/;$/, "");
18
+ if (seenImports.has(key)) {
19
+ return false;
20
+ }
21
+ seenImports.add(key);
22
+ }
23
+ return true;
24
+ });
25
+ const cleanedCode = dedupedLines.join("\n").trimEnd();
15
26
  return {
16
27
  originalContent: content,
17
- content: cleanedCode, // cleaned code for pipeline
18
- filepath, // original file path
19
- mode: "overwrite", // indicates overwrite existing file
28
+ content: cleanedCode,
29
+ filepath,
30
+ mode: "overwrite",
20
31
  };
21
32
  }
22
33
  };
@@ -29,25 +29,21 @@ function isTopOrBottomNoise(line) {
29
29
  }
30
30
  export const cleanupModule = {
31
31
  name: 'cleanup',
32
- description: 'Remove markdown fences and fluff from top/bottom of each chunk with colored logging',
32
+ description: 'Remove markdown fences, fluff, and non-JSON lines with colored logging',
33
33
  async run(input) {
34
- // Normalize line endings to \n to avoid issues with \r\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; // Stop if opening fence found
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
- // --- FINAL CLEANUP: REMOVE ANY LINGERING TRIPLE TICK LINES ANYWHERE ---
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
- return { content: lines.join('\n').trim() };
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
  };
@@ -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 }, model);
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
+ };
@@ -0,0 +1,40 @@
1
+ import { generate } from "../../lib/generate.js";
2
+ import { Config } from "../../config.js";
3
+ export const repairTestsModule = {
4
+ name: "repairTestsModule",
5
+ description: "Fix failing Jest tests using AI",
6
+ async run({ content, filepath, summary }) {
7
+ const model = Config.getModel();
8
+ const prompt = `
9
+ You are a senior engineer tasked with repairing Jest tests.
10
+
11
+ The following test file failed:
12
+
13
+ --- BEGIN TEST FILE ---
14
+ ${content}
15
+ --- END TEST FILE ---
16
+
17
+ Failure summary:
18
+ ${summary || "No summary provided."}
19
+
20
+ Instructions:
21
+ - Keep the overall structure, imports, and test cases.
22
+ - Only fix syntax errors, invalid Jest matchers, or broken references.
23
+ - Do NOT remove or replace entire test suites unless strictly necessary.
24
+ - Do NOT generate trivial placeholder tests (like add(2,3) examples).
25
+ - Only return valid Jest code, no explanations, no markdown fences.
26
+
27
+ Output the repaired test file:
28
+ `.trim();
29
+ const response = await generate({ content: prompt }, model);
30
+ if (!response)
31
+ throw new Error("⚠️ No repaired test code returned from model");
32
+ return {
33
+ originalContent: content,
34
+ content: response.content, // repaired test code
35
+ filepath, // repair in-place
36
+ summary: `Repaired tests based on failure: ${summary || "unknown error"}`,
37
+ mode: "overwrite" // signal to overwrite existing test file
38
+ };
39
+ }
40
+ };