scai 0.1.27 โ†’ 0.1.28

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,54 +1,53 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import readline from 'readline';
4
- import { queryFiles } from '../db/fileIndex.js';
4
+ import { queryFiles, indexFile } from '../db/fileIndex.js';
5
5
  import { summaryModule } from '../pipeline/modules/summaryModule.js';
6
6
  import { summarizeCode } from '../utils/summarizer.js';
7
+ import { detectFileType } from '../utils/detectFileType.js';
8
+ import { generateEmbedding } from '../lib/generateEmbedding.js';
9
+ import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
10
+ import { db } from '../db/client.js';
7
11
  export async function summarizeFile(filepath) {
8
12
  let content = '';
9
- let summary;
10
- let filePathResolved = filepath ? path.resolve(process.cwd(), filepath) : undefined;
11
- // Handle case where user provides only a filename (without extension)
12
- if (filepath && !path.extname(filepath)) {
13
- // Search for matching files using the provided base name
14
- const matches = queryFiles(`"${filepath}"`);
13
+ let filePathResolved;
14
+ // ๐Ÿ“ Resolve path like `scai find`
15
+ if (filepath) {
16
+ const sanitizedQuery = sanitizeQueryForFts(filepath);
17
+ const matches = queryFiles(sanitizedQuery);
15
18
  if (matches.length > 0) {
16
- const match = matches[0]; // Get the first match (adjust based on your preference)
17
- filePathResolved = path.resolve(process.cwd(), match.path);
19
+ const topMatch = matches[0];
20
+ filePathResolved = path.resolve(process.cwd(), topMatch.path);
21
+ console.log(`๐Ÿ”— Matched file: ${path.relative(process.cwd(), filePathResolved)}`);
22
+ }
23
+ else {
24
+ console.error(`โŒ Could not resolve file from query: "${filepath}"`);
25
+ return;
18
26
  }
19
27
  }
20
- else if (filepath && path.extname(filepath)) {
21
- // Handle case where full filename with extension is provided
22
- filePathResolved = path.resolve(process.cwd(), filepath);
23
- }
24
- // Now, let's search the database for a summary
28
+ // ๐Ÿ“„ Load file content from resolved path
25
29
  if (filePathResolved) {
30
+ const matches = queryFiles(`"${filePathResolved}"`);
31
+ const match = matches.find(row => path.resolve(row.path) === filePathResolved);
32
+ if (match?.summary) {
33
+ console.log(`๐Ÿง  Cached summary for ${filepath}:\n`);
34
+ console.log(summarizeCode(match.summary));
35
+ return;
36
+ }
26
37
  try {
27
- // Try to find an existing summary from the database using the resolved path
28
- const matches = queryFiles(`"${filePathResolved}"`);
29
- const match = matches.find(row => path.resolve(row.path) === filePathResolved);
30
- if (match?.summary) {
31
- // If a summary exists in the database, use it
32
- console.log(`๐Ÿง  Cached summary for ${filepath}:\n`);
33
- console.log(summarizeCode(match.summary));
34
- return;
35
- }
36
- // If no cached summary, read the file content
37
38
  content = await fs.readFile(filePathResolved, 'utf-8');
38
39
  }
39
40
  catch (err) {
40
- console.error(`โŒ Could not process ${filepath}:`, err.message);
41
+ console.error(`โŒ Could not read file: ${filePathResolved}\n${err.message}`);
41
42
  return;
42
43
  }
43
44
  }
44
45
  else if (!process.stdin.isTTY) {
45
- // If no file path and input comes from stdin (piped content)
46
46
  const rl = readline.createInterface({
47
47
  input: process.stdin,
48
48
  output: process.stdout,
49
49
  terminal: false,
50
50
  });
51
- // Collect all piped input into the `content` string
52
51
  for await (const line of rl) {
53
52
  content += line + '\n';
54
53
  }
@@ -57,17 +56,27 @@ export async function summarizeFile(filepath) {
57
56
  console.error('โŒ No file provided and no piped input.\n๐Ÿ‘‰ Usage: scai summ <file> or cat file | scai summ');
58
57
  return;
59
58
  }
60
- // If content is available (from file or stdin)
59
+ // ๐Ÿง  Generate summary and save
61
60
  if (content.trim()) {
62
61
  console.log('๐Ÿงช Generating summary...\n');
63
- // Generate a summary using your summarization pipeline
64
62
  const response = await summaryModule.run({ content, filepath });
65
63
  if (!response.summary) {
66
64
  console.warn('โš ๏ธ No summary generated.');
67
65
  return;
68
66
  }
69
- // Print the formatted summary
70
67
  console.log(summarizeCode(response.summary));
68
+ if (filePathResolved) {
69
+ const fileType = detectFileType(filePathResolved);
70
+ indexFile(filePathResolved, response.summary, fileType);
71
+ console.log('๐Ÿ’พ Summary saved to local database.');
72
+ const embedding = await generateEmbedding(response.summary);
73
+ if (embedding) {
74
+ db.prepare(`
75
+ UPDATE files SET embedding = ? WHERE path = ?
76
+ `).run(JSON.stringify(embedding), filePathResolved.replace(/\\/g, '/'));
77
+ console.log('๐Ÿ“ Embedding saved to database.');
78
+ }
79
+ }
71
80
  }
72
81
  else {
73
82
  console.error('โŒ No content provided to summarize.');
@@ -53,17 +53,15 @@ export async function searchFiles(query, topK = 5) {
53
53
  const safeQuery = sanitizeQueryForFts(query);
54
54
  console.log(`Executing search query in FTS5: ${safeQuery}`);
55
55
  const ftsResults = db.prepare(`
56
- SELECT fts.rowid AS id, f.path, f.summary, f.type, bm25(files_fts) AS bm25Score
57
- FROM files f
58
- JOIN files_fts fts ON f.id = fts.rowid
59
- WHERE fts.files_fts MATCH ?
60
- AND f.embedding IS NOT NULL
61
- ORDER BY bm25Score DESC
62
- LIMIT ?
63
- `).all(safeQuery, topK);
56
+ SELECT fts.rowid AS id, f.path, f.summary, f.type, bm25(files_fts) AS bm25Score, f.embedding
57
+ FROM files f
58
+ JOIN files_fts fts ON f.id = fts.rowid
59
+ WHERE fts.files_fts MATCH ?
60
+ ORDER BY bm25Score ASC
61
+ LIMIT ?
62
+ `).all(safeQuery, 20);
64
63
  console.log(`FTS search returned ${ftsResults.length} results`);
65
64
  if (ftsResults.length === 0) {
66
- console.log('โš ๏ธ No results found from FTS search');
67
65
  return [];
68
66
  }
69
67
  ftsResults.forEach(result => {
@@ -72,36 +70,37 @@ export async function searchFiles(query, topK = 5) {
72
70
  const bm25Min = Math.min(...ftsResults.map(r => r.bm25Score));
73
71
  const bm25Max = Math.max(...ftsResults.map(r => r.bm25Score));
74
72
  const scored = ftsResults.map(result => {
75
- try {
76
- const embResult = db.prepare(sqlTemplates.fetchEmbeddingTemplate).get({
77
- path: result.path,
78
- });
79
- if (!embResult || typeof embResult.embedding !== 'string') {
80
- console.log(`โš ๏ธ No embedding for file: ${result.path}`);
81
- return null;
73
+ let finalScore = 0;
74
+ let sim = 0;
75
+ if (result.embedding) {
76
+ try {
77
+ const vector = JSON.parse(result.embedding);
78
+ sim = cosineSimilarity(embedding, vector);
79
+ const normalizedBm25 = 1 - ((result.bm25Score - bm25Min) / (bm25Max - bm25Min + 1e-5));
80
+ finalScore = 0.7 * sim + 0.3 * normalizedBm25;
81
+ }
82
+ catch (err) {
83
+ console.error(`โŒ Failed to parse embedding for ${result.path}:`, err);
84
+ finalScore = 0; // fallback
82
85
  }
83
- const vector = JSON.parse(embResult.embedding);
84
- const sim = cosineSimilarity(embedding, vector);
85
- const normalizedBm25 = 1 - ((result.bm25Score - bm25Min) / (bm25Max - bm25Min + 1e-5));
86
- const finalScore = 0.7 * sim + 0.3 * normalizedBm25;
87
- return {
88
- path: result.path,
89
- summary: result.summary,
90
- score: finalScore,
91
- sim,
92
- bm25: normalizedBm25,
93
- };
94
86
  }
95
- catch (err) {
96
- console.error(`โŒ Error processing embedding for file: ${result.path}`, err);
97
- return null;
87
+ else {
88
+ // No embedding: fallback to inverse bm25-only
89
+ finalScore = 1 - ((result.bm25Score - bm25Min) / (bm25Max - bm25Min + 1e-5));
98
90
  }
99
- })
100
- .filter((r) => r !== null)
91
+ return {
92
+ path: result.path,
93
+ summary: result.summary,
94
+ score: finalScore,
95
+ sim,
96
+ bm25: result.bm25Score,
97
+ };
98
+ });
99
+ const sorted = scored
101
100
  .sort((a, b) => b.score - a.score)
102
101
  .slice(0, topK);
103
102
  console.log(`Returning top ${topK} results based on combined score`);
104
- return scored;
103
+ return sorted;
105
104
  }
106
105
  function cosineSimilarity(a, b) {
107
106
  const dot = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
@@ -0,0 +1,10 @@
1
+ // src/utils/normalizePath.ts
2
+ /**
3
+ * Normalizes a path string for loose, fuzzy matching:
4
+ * - Lowercases
5
+ * - Removes slashes and backslashes
6
+ * - Removes whitespace
7
+ */
8
+ export function normalizePathForLooseMatch(p) {
9
+ return p.toLowerCase().replace(/[\\/]/g, '').replace(/\s+/g, '');
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -17,7 +17,8 @@
17
17
  "refactor",
18
18
  "devtools",
19
19
  "local",
20
- "typescript"
20
+ "typescript",
21
+ "llm"
21
22
  ],
22
23
  "scripts": {
23
24
  "build": "tsc",