scai 0.1.110 → 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.
@@ -1,13 +1,12 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import readline from 'readline';
4
- import { searchFiles, queryFiles, getFunctionsForFiles } from '../db/fileIndex.js';
4
+ import { searchFiles, queryFiles } from '../db/fileIndex.js';
5
5
  import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
6
6
  import { generate } from '../lib/generate.js';
7
7
  import { buildContextualPrompt } from '../utils/buildContextualPrompt.js';
8
- import { generateFocusedFileTree } from '../utils/fileTree.js';
9
8
  import { log } from '../utils/log.js';
10
- import { PROMPT_LOG_PATH, SCAI_HOME, RELATED_FILES_LIMIT, MAX_SUMMARY_LINES, getIndexDir, MAX_FUNCTION_LINES } from '../constants.js';
9
+ import { PROMPT_LOG_PATH, SCAI_HOME, RELATED_FILES_LIMIT, getIndexDir } from '../constants.js';
11
10
  import chalk from 'chalk';
12
11
  export async function runAskCommand(query) {
13
12
  if (!query) {
@@ -19,22 +18,21 @@ export async function runAskCommand(query) {
19
18
  return;
20
19
  }
21
20
  console.log(`šŸ“ Using index root: ${getIndexDir()}`);
22
- console.log(`šŸ” Searching for: "${query}"\n`);
23
- // 🟩 STEP 1: Semantic Search
21
+ // Semantic Search
24
22
  const start = Date.now();
25
- const semanticResults = await searchFiles(query, RELATED_FILES_LIMIT); // RankedFile[]
23
+ const semanticResults = await searchFiles(query, RELATED_FILES_LIMIT);
26
24
  const duration = Date.now() - start;
27
25
  console.log(`ā±ļø searchFiles took ${duration}ms and returned ${semanticResults.length} result(s)`);
28
26
  semanticResults.forEach((file, i) => {
29
27
  console.log(` ${i + 1}. šŸ“„ Path: ${file.path} | Score: ${file.score?.toFixed(3) ?? 'n/a'}`);
30
28
  });
31
- // 🟩 STEP 1.5: Fallback FTS search
29
+ // Fallback FTS search
32
30
  const safeQuery = sanitizeQueryForFts(query);
33
- const fallbackResults = queryFiles(safeQuery, 10); // FileRow[]
31
+ const fallbackResults = queryFiles(safeQuery, 10);
34
32
  fallbackResults.forEach((file, i) => {
35
33
  console.log(` ${i + 1}. šŸ”Ž Fallback Match: ${file.path}`);
36
34
  });
37
- // 🟩 STEP 2: Merge results
35
+ // Merge results
38
36
  const seen = new Set();
39
37
  const combinedResults = [];
40
38
  for (const file of semanticResults) {
@@ -56,7 +54,20 @@ export async function runAskCommand(query) {
56
54
  });
57
55
  }
58
56
  }
59
- // 🟩 STEP 3: Log results
57
+ // Exact match prioritization
58
+ const queryFilenameRaw = path.basename(query).toLowerCase();
59
+ const queryFilenameNoExt = queryFilenameRaw.replace(/\.[^/.]+$/, '');
60
+ const exactMatchIndex = combinedResults.findIndex(f => {
61
+ const base = path.basename(f.path).toLowerCase();
62
+ const baseNoExt = base.replace(/\.[^/.]+$/, '');
63
+ return base === queryFilenameRaw || baseNoExt === queryFilenameNoExt;
64
+ });
65
+ if (exactMatchIndex !== -1) {
66
+ const [exactMatch] = combinedResults.splice(exactMatchIndex, 1);
67
+ combinedResults.unshift(exactMatch);
68
+ console.log(`šŸŽÆ Exact match prioritized: ${exactMatch.path}`);
69
+ }
70
+ // Log combined results
60
71
  if (combinedResults.length) {
61
72
  console.log('\nšŸ“Š Final Related Files:');
62
73
  combinedResults.forEach((f, i) => {
@@ -66,79 +77,38 @@ export async function runAskCommand(query) {
66
77
  else {
67
78
  console.log('āš ļø No similar files found. Using query only.');
68
79
  }
69
- // 🟩 STEP 4: Load top file code + metadata
70
- const topFile = combinedResults[0];
71
- const filepath = topFile?.path || '';
72
- let code = '';
73
- let topSummary = topFile.summary || '(No summary available)';
74
- let topFunctions = [];
75
- const fileFunctions = {};
76
- // Truncate summary
77
- topSummary = topSummary.split('\n').slice(0, MAX_SUMMARY_LINES).join('\n');
78
- const allFileIds = combinedResults
79
- .map(file => file.id)
80
- .filter((id) => typeof id === 'number');
81
- const allFunctionsMap = getFunctionsForFiles(allFileIds); // Record<number, Function[]>
82
- try {
83
- code = fs.readFileSync(filepath, 'utf-8');
84
- const topFileId = topFile.id;
85
- topFunctions = allFunctionsMap[topFileId]?.map(fn => {
86
- const content = fn.content
87
- ? fn.content.split('\n').slice(0, MAX_FUNCTION_LINES).join('\n')
88
- : '(No content available)';
89
- return {
90
- name: fn.name,
91
- content,
92
- };
93
- }) || [];
94
- }
95
- catch (err) {
96
- console.warn(`āš ļø Failed to read or analyze top file (${filepath}):`, err);
80
+ // STEP 4+: Build contextual prompt using topFile + combinedResults
81
+ if (combinedResults.length === 0) {
82
+ throw new Error('āŒ No search results found. Cannot build contextual prompt.');
97
83
  }
98
- // 🟩 STEP 5: Build relatedFiles with functions and fileFunctions
99
- const relatedFiles = combinedResults.slice(0, RELATED_FILES_LIMIT).map(file => {
100
- const fileId = file.id;
101
- let summary = file.summary || '(No summary available)';
102
- if (summary) {
103
- summary = summary.split('\n').slice(0, MAX_SUMMARY_LINES).join('\n');
104
- }
105
- const functions = allFunctionsMap[fileId]?.map(fn => {
106
- const content = fn.content
107
- ? fn.content.split('\n').slice(0, MAX_FUNCTION_LINES).join('\n')
108
- : '(No content available)';
109
- return {
110
- name: fn.name,
111
- content,
112
- };
113
- }) || [];
114
- return {
115
- path: file.path,
116
- summary,
117
- functions,
118
- };
119
- });
120
- // 🟩 STEP 6: Generate file tree
121
- let fileTree = '';
84
+ const file = combinedResults[0];
85
+ let code = "";
86
+ // STEP 4++: Add code to params
122
87
  try {
123
- fileTree = generateFocusedFileTree(filepath, 2);
88
+ code = fs.readFileSync(file.path, 'utf-8');
89
+ if (!code) {
90
+ console.warn(`āš ļø No code loaded for top file: ${file.path}`);
91
+ }
124
92
  }
125
93
  catch (e) {
126
- console.warn('āš ļø Could not generate file tree:', e);
94
+ console.log("Error reading code from selected file: ", e instanceof Error ? e.message : e);
127
95
  }
128
- // 🟩 STEP 7: Build prompt
129
- console.log(chalk.blueBright('\nšŸ“¦ Building contextual prompt...'));
130
- const promptContent = buildContextualPrompt({
131
- baseInstruction: query,
96
+ const topFile = {
97
+ id: file.id,
98
+ path: file.path,
99
+ summary: file.summary ?? "",
132
100
  code,
133
- summary: topSummary,
134
- functions: topFunctions,
135
- relatedFiles,
136
- projectFileTree: fileTree || undefined,
137
- fileFunctions,
138
- });
101
+ };
102
+ const promptArgs = {
103
+ topFile,
104
+ relatedFiles: combinedResults,
105
+ query,
106
+ };
107
+ console.log(chalk.blueBright('\nšŸ“¦ Building contextual prompt...'));
108
+ const promptContent = await buildContextualPrompt(promptArgs);
139
109
  console.log(chalk.greenBright('āœ… Prompt built successfully.'));
140
110
  console.log(chalk.cyan(`[runAskCommand] Prompt token estimate: ~${Math.round(promptContent.length / 4)} tokens`));
141
- // 🟩 STEP 8: Save prompt
111
+ // STEP 5: Save prompt
142
112
  try {
143
113
  if (!fs.existsSync(SCAI_HOME))
144
114
  fs.mkdirSync(SCAI_HOME, { recursive: true });
@@ -148,21 +118,21 @@ export async function runAskCommand(query) {
148
118
  catch (err) {
149
119
  log('āŒ Failed to write prompt log:', err);
150
120
  }
151
- // 🟩 STEP 9: Ask model
121
+ // STEP 6: Ask model
152
122
  try {
153
123
  console.log('\nšŸ¤– Asking the model...');
154
124
  const input = {
155
125
  content: promptContent,
156
- filepath,
126
+ filepath: topFile.path,
157
127
  };
158
- const modelResponse = await generate(input, 'llama3');
128
+ const modelResponse = await generate(input);
159
129
  console.log(`\n🧠 Model Response:\n${modelResponse.content}`);
160
130
  }
161
131
  catch (err) {
162
132
  console.error('āŒ Model request failed:', err);
163
133
  }
164
134
  }
165
- // 🟩 Helper: Prompt once
135
+ // Helper: Prompt once
166
136
  function promptOnce(promptText) {
167
137
  return new Promise(resolve => {
168
138
  console.log(promptText);
@@ -6,6 +6,7 @@ import * as sqlTemplates from './sqlTemplates.js';
6
6
  import { CANDIDATE_LIMIT } from '../constants.js';
7
7
  import { getDbForRepo } from './client.js';
8
8
  import { scoreFiles } from '../fileRules/scoreFiles.js'; // šŸ‘ˆ NEW
9
+ import chalk from 'chalk';
9
10
  export function indexFile(filePath, summary, type) {
10
11
  const stats = fs.statSync(filePath);
11
12
  const lastModified = stats.mtime.toISOString();
@@ -44,7 +45,7 @@ export function queryFiles(safeQuery, limit = 10) {
44
45
  `).all(safeQuery, limit);
45
46
  }
46
47
  export async function searchFiles(query, topK = 5) {
47
- console.log(`🧠 Searching for query: "${query}"`);
48
+ console.log(chalk.yellow(`🧠 Searching for query: "${query}"`));
48
49
  const embedding = await generateEmbedding(query);
49
50
  if (!embedding) {
50
51
  console.log('āš ļø Failed to generate embedding for query');
@@ -1,8 +1,9 @@
1
1
  // File: lib/generate.ts
2
2
  import { Spinner } from './spinner.js';
3
- import { readConfig } from '../config.js';
3
+ import { Config, readConfig } from '../config.js';
4
4
  import { startModelProcess } from '../utils/checkModel.js';
5
- export async function generate(input, model) {
5
+ export async function generate(input) {
6
+ const model = Config.getModel();
6
7
  const contextLength = readConfig().contextLength ?? 8192;
7
8
  let prompt = input.content;
8
9
  if (prompt.length > contextLength) {
@@ -18,7 +18,7 @@ If no meaningful changes are present, return the text: "NO UPDATE".
18
18
  ${input.content}
19
19
  --- DIFF END ---
20
20
  `.trim();
21
- const response = await generate({ content: prompt }, model);
21
+ const response = await generate({ content: prompt });
22
22
  // Check if we received a meaningful result or "NO UPDATE"
23
23
  const content = response?.content?.trim();
24
24
  if (content === 'NO UPDATE') {
@@ -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 {
@@ -38,7 +38,7 @@ Do NOT include raw content from the file. Only provide the structured JSON outpu
38
38
  ${content}
39
39
  --- FILE CONTENT END ---
40
40
  `.trim();
41
- const response = await generate({ content: prompt, filepath: input.filepath }, model);
41
+ const response = await generate({ content: prompt, filepath: input.filepath });
42
42
  try {
43
43
  // Clean the model output first
44
44
  const cleaned = await cleanupModule.run({ content: response.content });
@@ -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');
@@ -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
  }
@@ -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.110",
3
+ "version": "0.1.111",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"