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.
@@ -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
  };
@@ -53,7 +53,7 @@ Rules:
53
53
  ${input.content}
54
54
 
55
55
  `.trim();
56
- const response = await generate({ content: prompt }, model);
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 }, model);
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 }, model);
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 }, model);
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 }, model);
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 }, model);
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 }, model);
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');
@@ -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
- // File: src/utils/buildContextualPrompt.ts
2
- import chalk from 'chalk';
3
- import path from 'path';
4
- function estimateTokenCount(text) {
5
- return Math.round(text.length / 4); // simple heuristic approximation
6
- }
7
- export function buildContextualPrompt({ baseInstruction, code, summary, functions, relatedFiles, projectFileTree, }) {
8
- const parts = [baseInstruction];
9
- if (summary) {
10
- parts.push(`📄 File Summary:\n${summary}`);
11
- }
12
- if (functions?.length) {
13
- const formattedFunctions = functions
14
- .map(fn => `• ${fn.name}:\n${fn.content}`)
15
- .join('\n\n');
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
- else {
19
- console.log(chalk.yellow(` ⚠️ No functions found in top rated file.`));
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
- if (relatedFiles?.length) {
22
- const formattedRelatedFiles = relatedFiles
23
- .map(f => {
24
- const relatedFunctions = f.functions
25
- .map(fn => ` • ${fn.name}:\n ${fn.content}`)
26
- .join('\n\n');
27
- return `• ${f.path}: ${f.summary}\n${relatedFunctions}`;
28
- })
29
- .join('\n\n');
30
- parts.push(`📚 Related Files:\n${formattedRelatedFiles}`);
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
- if (projectFileTree) {
33
- parts.push(`📁 Project File Structure:\n\`\`\`\n${projectFileTree.trim()}\n\`\`\``);
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
- parts.push(`\n--- CODE START ---\n${code}\n--- CODE END ---`);
36
- const prompt = parts.join('\n\n');
37
- const tokenEstimate = estimateTokenCount(prompt);
38
- // 🔵 Colorized diagnostic output
39
- // 🔵 Colorized diagnostic output
40
- const header = chalk.bgBlue.white.bold(' [SCAI] Prompt Overview ');
41
- const labelColor = chalk.cyan;
42
- const contentColor = chalk.gray;
43
- console.log('\n' + header);
44
- console.log(labelColor('🔢 Token Estimate:'), contentColor(`${tokenEstimate.toLocaleString()} tokens`));
45
- // 📄 Summary
46
- if (summary) {
47
- console.log(labelColor('📄 Summary:'), contentColor(`${estimateTokenCount(summary).toLocaleString()} tokens`));
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
- // 🔧 Functions
50
- if (functions?.length) {
51
- const fnCount = functions.length;
52
- const fnTokens = functions.reduce((sum, f) => sum + estimateTokenCount(f.content), 0);
53
- console.log(labelColor(`🔧 Functions (${fnCount}):`), contentColor(`${fnTokens.toLocaleString()} tokens`));
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
- // 📚 Related Files
56
- if (relatedFiles?.length) {
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
- // 📁 File Tree
66
- if (projectFileTree) {
67
- console.log(labelColor('📁 File Tree:'), contentColor(`${estimateTokenCount(projectFileTree).toLocaleString()} tokens`));
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
- // 📦 Code Section
70
- console.log(labelColor('📦 Code:'), contentColor(`${estimateTokenCount(prompt).toLocaleString()} tokens`));
71
- // 📌 Key
72
- console.log(labelColor('🔍 Key:'), contentColor('[buildContextualPrompt]\n'));
73
- return prompt;
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 it's a single filename-like string (includes dots or slashes), quote it
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
- /^[a-z0-9]+$/.test(token))
17
- .map(token => token.replace(/[?*\\"]/g, '').replace(/'/g, "''") + '*');
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.109",
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": {