scai 0.1.22 โ 0.1.24
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.
- package/dist/commands/AskCmd.js +46 -49
- package/dist/commands/DaemonCmd.js +23 -75
- package/dist/commands/IndexCmd.js +48 -37
- package/dist/commands/ResetDbCmd.js +54 -9
- package/dist/config/StopWords.js +15 -0
- package/dist/constants.js +34 -4
- package/dist/daemon/daemonBatch.js +65 -0
- package/dist/daemon/daemonWorker.js +27 -0
- package/dist/db/client.js +4 -0
- package/dist/db/fileIndex.js +82 -73
- package/dist/db/schema.js +19 -3
- package/dist/db/sqlTemplates.js +30 -26
- package/dist/index.js +31 -29
- package/dist/utils/log.js +15 -0
- package/dist/utils/sanitizeQuery.js +16 -0
- package/package.json +4 -2
package/dist/commands/AskCmd.js
CHANGED
|
@@ -1,66 +1,63 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
1
2
|
import { searchFiles } from "../db/fileIndex.js";
|
|
2
3
|
import { generate } from "../lib/generate.js";
|
|
3
|
-
import { summaryModule } from "../pipeline/modules/summaryModule.js";
|
|
4
4
|
export async function runAskCommand(query) {
|
|
5
5
|
if (!query) {
|
|
6
|
-
|
|
6
|
+
query = await promptOnce('๐ง Ask your question:\n> ');
|
|
7
|
+
}
|
|
8
|
+
query = query.trim();
|
|
9
|
+
if (!query) {
|
|
10
|
+
console.error('โ No question provided.\n๐ Usage: scai ask "your question"');
|
|
7
11
|
return;
|
|
8
12
|
}
|
|
9
13
|
console.log(`๐ Searching for: "${query}"\n`);
|
|
10
|
-
|
|
11
|
-
const results = await searchFiles(query, 5);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
else {
|
|
14
|
+
const start = Date.now();
|
|
15
|
+
const results = await searchFiles(query, 5);
|
|
16
|
+
const duration = Date.now() - start;
|
|
17
|
+
console.log(`โฑ๏ธ searchFiles took ${duration}ms and returned ${results.length} result(s)`);
|
|
18
|
+
if (results.length > 0) {
|
|
16
19
|
console.log('๐ Closest files based on semantic similarity:');
|
|
17
|
-
results.forEach(file => {
|
|
18
|
-
console.log(
|
|
20
|
+
results.forEach((file, i) => {
|
|
21
|
+
console.log(` ${i + 1}. ๐ Path: ${file?.path} | Score: ${file?.score?.toFixed(3)}`);
|
|
19
22
|
});
|
|
20
23
|
}
|
|
24
|
+
else {
|
|
25
|
+
console.log('โ ๏ธ No similar embeddings found. Asking the model for context instead...');
|
|
26
|
+
}
|
|
27
|
+
// ๐ง Use stored summaries directly
|
|
21
28
|
let allSummaries = '';
|
|
22
29
|
for (const file of results) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
console.log(`๐ Using cached summary for file: ${file?.path}`);
|
|
29
|
-
const summaryResponse = await summaryModule.run({ content: file?.summary ? file.summary : '', filepath: file?.path });
|
|
30
|
-
if (summaryResponse.summary) {
|
|
31
|
-
allSummaries += `\n${summaryResponse.summary}`;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
console.error(`โ Error processing file: ${file?.path}`, err instanceof Error ? err.message : err);
|
|
30
|
+
if (!file?.summary) {
|
|
31
|
+
console.warn(`โ ๏ธ No summary available for file: ${file?.path}`);
|
|
32
|
+
continue;
|
|
36
33
|
}
|
|
34
|
+
console.log(`๐ Using stored summary for: ${file.path}`);
|
|
35
|
+
allSummaries += `\n${file.summary}`;
|
|
37
36
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
console.error('โ Model request failed:', err);
|
|
50
|
-
}
|
|
37
|
+
const input = {
|
|
38
|
+
content: allSummaries ? `${query}\n\n${allSummaries}` : query,
|
|
39
|
+
filepath: '',
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
console.log(allSummaries.trim()
|
|
43
|
+
? '๐ง Summaries found, sending them to the model for synthesis...'
|
|
44
|
+
: 'โ ๏ธ No summaries found. Asking the model for context only...');
|
|
45
|
+
const modelResponse = await generate(input, 'llama3');
|
|
46
|
+
console.log(`\n๐ Model response:\n${modelResponse.content}`);
|
|
51
47
|
}
|
|
52
|
-
|
|
53
|
-
console.
|
|
54
|
-
try {
|
|
55
|
-
const input = {
|
|
56
|
-
content: query,
|
|
57
|
-
filepath: '',
|
|
58
|
-
};
|
|
59
|
-
const modelResponse = await generate(input, 'llama3');
|
|
60
|
-
console.log(`\n๐ Model response:\n${modelResponse.content}`);
|
|
61
|
-
}
|
|
62
|
-
catch (err) {
|
|
63
|
-
console.error('โ Model request failed:', err);
|
|
64
|
-
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error('โ Model request failed:', err);
|
|
65
50
|
}
|
|
66
51
|
}
|
|
52
|
+
function promptOnce(promptText) {
|
|
53
|
+
return new Promise(resolve => {
|
|
54
|
+
const rl = readline.createInterface({
|
|
55
|
+
input: process.stdin,
|
|
56
|
+
output: process.stdout
|
|
57
|
+
});
|
|
58
|
+
rl.question(promptText, answer => {
|
|
59
|
+
rl.close();
|
|
60
|
+
resolve(answer.trim());
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -1,80 +1,28 @@
|
|
|
1
|
-
import { summaryModule } from '../pipeline/modules/summaryModule.js';
|
|
2
|
-
import { db } from '../db/client.js';
|
|
3
|
-
import fs from 'fs/promises';
|
|
4
1
|
import fsSync from 'fs';
|
|
5
|
-
import
|
|
2
|
+
import { LOG_PATH, PID_PATH } from '../constants.js';
|
|
3
|
+
import { log } from '../utils/log.js';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
6
|
import path from 'path';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const DAEMON_INTERVAL_MINUTES = 30;
|
|
12
|
-
const PID_PATH = path.join(os.homedir(), '.scai/daemon.pid');
|
|
13
|
-
// Helper function to check if a file should be ignored
|
|
14
|
-
const shouldIgnoreFile = (filePath) => {
|
|
15
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
16
|
-
return IGNORED_EXTENSIONS.includes(ext);
|
|
17
|
-
};
|
|
18
|
-
export async function runDaemonBatch() {
|
|
19
|
-
console.log('๐ฅ Daemon batch: scanning for files to summarize...');
|
|
20
|
-
const rows = db.prepare(`
|
|
21
|
-
SELECT path, type FROM files
|
|
22
|
-
WHERE summary IS NULL OR summary = ''
|
|
23
|
-
ORDER BY last_modified DESC
|
|
24
|
-
LIMIT ?
|
|
25
|
-
`).all(MAX_FILES);
|
|
26
|
-
if (rows.length === 0) {
|
|
27
|
-
console.log('โ
No files left to summarize.');
|
|
28
|
-
return;
|
|
7
|
+
// ๐ Ensure daemon starts in the background
|
|
8
|
+
export async function startDaemon() {
|
|
9
|
+
if (fsSync.existsSync(PID_PATH)) {
|
|
10
|
+
log(`โ ๏ธ Daemon already running (PID file found at ${PID_PATH}). Skipping launch.`);
|
|
29
11
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Using named parameters for better readability and flexibility
|
|
46
|
-
db.prepare(`
|
|
47
|
-
UPDATE files
|
|
48
|
-
SET summary = @summary, embedding = @embedding, indexed_at = datetime('now')
|
|
49
|
-
WHERE path = @path
|
|
50
|
-
`).run({ summary, embedding, path: row.path });
|
|
51
|
-
console.log(`๐ Summarized: ${row.path}`);
|
|
52
|
-
console.log(`๐ข Embedded: ${row.path}`);
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
console.warn(`โ ๏ธ Failed: ${row.path}`, err instanceof Error ? err.message : err);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
export async function runDaemonScheduler() {
|
|
60
|
-
// Write PID to file
|
|
61
|
-
fsSync.mkdirSync(path.dirname(PID_PATH), { recursive: true });
|
|
62
|
-
fsSync.writeFileSync(PID_PATH, process.pid.toString(), 'utf-8');
|
|
63
|
-
console.log('๐ง Daemon started. PID:', process.pid);
|
|
64
|
-
console.log('โฑ๏ธ Will run every 30 minutes for 10 minutes.');
|
|
65
|
-
console.log('๐ง Background summarizer started. Will run every 30 minutes for 10 minutes.');
|
|
66
|
-
const startDaemonCycle = async () => {
|
|
67
|
-
const startTime = Date.now();
|
|
68
|
-
const endTime = startTime + DAEMON_DURATION_MINUTES * 60 * 1000;
|
|
69
|
-
while (Date.now() < endTime) {
|
|
70
|
-
await runDaemonBatch();
|
|
71
|
-
await new Promise(res => setTimeout(res, 60 * 1000)); // 1 min pause between mini-batches
|
|
72
|
-
}
|
|
73
|
-
console.log(`โฑ๏ธ Daemon completed 10-minute cycle. Next in ${DAEMON_INTERVAL_MINUTES} min.`);
|
|
74
|
-
};
|
|
75
|
-
// Repeat every 30 minutes
|
|
76
|
-
while (true) {
|
|
77
|
-
await startDaemonCycle();
|
|
78
|
-
await new Promise(res => setTimeout(res, DAEMON_INTERVAL_MINUTES * 60 * 1000));
|
|
12
|
+
else {
|
|
13
|
+
log('๐ Starting summarizer daemon in background mode...');
|
|
14
|
+
log(`๐ Logs will be saved to: ${LOG_PATH}`);
|
|
15
|
+
// Before starting the background process, set the environment variable
|
|
16
|
+
process.env.BACKGROUND_MODE = 'true'; // Set the mode to background
|
|
17
|
+
// Compute absolute path to the background worker (adjust path if needed)
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
const daemonWorkerPath = path.join(__dirname, '../daemon/daemonWorker.js');
|
|
21
|
+
// Spawn the daemonWorker.js file in the background
|
|
22
|
+
const child = spawn(process.execPath, [daemonWorkerPath], {
|
|
23
|
+
detached: true, // Detach the process so it runs independently
|
|
24
|
+
stdio: ['ignore', 'ignore', 'ignore'], // Suppress the output
|
|
25
|
+
});
|
|
26
|
+
child.unref(); // Allow the parent process to exit without waiting for the child
|
|
79
27
|
}
|
|
80
28
|
}
|
|
@@ -4,37 +4,48 @@ import { initSchema } from '../db/schema.js';
|
|
|
4
4
|
import { indexFile } from '../db/fileIndex.js';
|
|
5
5
|
import { shouldIgnoreFile } from '../utils/shouldIgnoreFiles.js';
|
|
6
6
|
import { detectFileType } from '../utils/detectFileType.js';
|
|
7
|
-
import {
|
|
7
|
+
import { startDaemon } from './DaemonCmd.js';
|
|
8
8
|
import { IGNORED_FOLDER_GLOBS } from '../config/IgnoredPaths.js';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
This will add more files into the existing index and may reduce accuracy or performance.
|
|
31
|
-
|
|
32
|
-
Use --force to continue, or consider clearing the index:
|
|
33
|
-
scai reset-db
|
|
34
|
-
|
|
35
|
-
Aborting.`);
|
|
9
|
+
import { Config } from '../config.js';
|
|
10
|
+
import { DB_PATH } from '../constants.js';
|
|
11
|
+
import { log } from '../utils/log.js';
|
|
12
|
+
import lockfile from 'proper-lockfile';
|
|
13
|
+
// ๐ง Lock the database to prevent simultaneous access
|
|
14
|
+
async function lockDb() {
|
|
15
|
+
try {
|
|
16
|
+
const lock = await lockfile.lock(DB_PATH); // DB_PATH from constants.ts
|
|
17
|
+
return lock;
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
log('โ Failed to acquire DB lock: ' + err);
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function runIndexCommand(targetDir, options = {}) {
|
|
25
|
+
try {
|
|
26
|
+
initSchema();
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.error('โ Failed to initialize schema:', err);
|
|
36
30
|
process.exit(1);
|
|
37
31
|
}
|
|
32
|
+
let resolvedDir;
|
|
33
|
+
if (options.force) {
|
|
34
|
+
// Force: use passed dir or fallback to cwd, no config updates
|
|
35
|
+
resolvedDir = path.resolve(targetDir || process.cwd());
|
|
36
|
+
console.warn('โ ๏ธ Running in --force mode. Config will not be updated.');
|
|
37
|
+
}
|
|
38
|
+
else if (targetDir) {
|
|
39
|
+
// User provided a directory: resolve and persist to config
|
|
40
|
+
resolvedDir = path.resolve(targetDir);
|
|
41
|
+
Config.setIndexDir(resolvedDir);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Use configured indexDir or fallback to cwd
|
|
45
|
+
resolvedDir = Config.getIndexDir() || process.cwd();
|
|
46
|
+
Config.setIndexDir(resolvedDir); // persist if not yet saved
|
|
47
|
+
}
|
|
48
|
+
log(`๐ Indexing files in: ${resolvedDir}`);
|
|
38
49
|
const files = await fg('**/*.*', {
|
|
39
50
|
cwd: resolvedDir,
|
|
40
51
|
ignore: IGNORED_FOLDER_GLOBS,
|
|
@@ -42,25 +53,25 @@ Aborting.`);
|
|
|
42
53
|
});
|
|
43
54
|
const countByExt = {};
|
|
44
55
|
let count = 0;
|
|
56
|
+
const release = await lockDb(); // Lock the DB before starting
|
|
45
57
|
for (const file of files) {
|
|
46
58
|
if (shouldIgnoreFile(file))
|
|
47
59
|
continue;
|
|
48
60
|
try {
|
|
49
61
|
const type = detectFileType(file);
|
|
50
|
-
indexFile(file, null, type); //
|
|
62
|
+
indexFile(file, null, type); // Index file without summary
|
|
51
63
|
const ext = path.extname(file);
|
|
52
64
|
countByExt[ext] = (countByExt[ext] || 0) + 1;
|
|
53
|
-
|
|
65
|
+
log(`๐ Indexed: ${path.relative(resolvedDir, file)}`);
|
|
54
66
|
count++;
|
|
55
67
|
}
|
|
56
68
|
catch (err) {
|
|
57
|
-
|
|
69
|
+
log(`โ ๏ธ Skipped in indexCmd ${file}: ${err instanceof Error ? err.message : err}`);
|
|
58
70
|
}
|
|
59
71
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
72
|
+
log('๐ Indexed files by extension:', JSON.stringify(countByExt, null, 2));
|
|
73
|
+
log(`โ
Done. Indexed ${count} files.`);
|
|
74
|
+
await release(); // Release the DB lock after indexing is done
|
|
75
|
+
// Auto-start daemon if not already running
|
|
76
|
+
startDaemon();
|
|
66
77
|
}
|
|
@@ -1,19 +1,57 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import
|
|
2
|
+
import fsp from 'fs/promises';
|
|
3
3
|
import { db } from '../db/client.js';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { DB_PATH, SCAI_HOME } from '../constants.js';
|
|
5
|
+
import lockfile from 'proper-lockfile';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
function getBackupDir() {
|
|
8
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
9
|
+
return path.join(SCAI_HOME, `backup-${timestamp}`);
|
|
10
|
+
}
|
|
11
|
+
async function backupScaiFolder() {
|
|
12
|
+
const backupDir = getBackupDir();
|
|
13
|
+
try {
|
|
14
|
+
await fsp.mkdir(backupDir, { recursive: true });
|
|
15
|
+
const files = await fsp.readdir(SCAI_HOME);
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const srcPath = path.join(SCAI_HOME, file);
|
|
18
|
+
const destPath = path.join(backupDir, file);
|
|
19
|
+
const stat = await fsp.stat(srcPath);
|
|
20
|
+
if (stat.isFile()) {
|
|
21
|
+
await fsp.copyFile(srcPath, destPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
console.log(`๐ฆ Backed up .scai folder to: ${backupDir}`);
|
|
25
|
+
return backupDir;
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.warn('โ ๏ธ Failed to back up .scai folder:', err instanceof Error ? err.message : err);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function resetDatabase() {
|
|
33
|
+
console.log('๐ Backing up existing .scai folder...');
|
|
34
|
+
await backupScaiFolder();
|
|
6
35
|
try {
|
|
7
|
-
db.close();
|
|
36
|
+
db.close();
|
|
8
37
|
console.log('๐ Closed SQLite database connection.');
|
|
9
38
|
}
|
|
10
39
|
catch (err) {
|
|
11
|
-
console.warn('โ ๏ธ Could not close database:', err);
|
|
40
|
+
console.warn('โ ๏ธ Could not close database:', err instanceof Error ? err.message : err);
|
|
12
41
|
}
|
|
13
|
-
|
|
42
|
+
try {
|
|
43
|
+
const releaseLock = await lockfile.unlock(DB_PATH).catch(() => null);
|
|
44
|
+
if (releaseLock) {
|
|
45
|
+
console.log('๐ Released database lock.');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.warn('โ ๏ธ Failed to release database lock:', err instanceof Error ? err.message : err);
|
|
50
|
+
}
|
|
51
|
+
if (fs.existsSync(DB_PATH)) {
|
|
14
52
|
try {
|
|
15
|
-
fs.unlinkSync(
|
|
16
|
-
console.log(
|
|
53
|
+
fs.unlinkSync(DB_PATH);
|
|
54
|
+
console.log(`๐งน Deleted existing database at ${DB_PATH}`);
|
|
17
55
|
}
|
|
18
56
|
catch (err) {
|
|
19
57
|
console.error('โ Failed to delete DB file:', err instanceof Error ? err.message : err);
|
|
@@ -21,7 +59,14 @@ export function resetDatabase() {
|
|
|
21
59
|
}
|
|
22
60
|
}
|
|
23
61
|
else {
|
|
24
|
-
console.log('โน๏ธ No existing database found
|
|
62
|
+
console.log('โน๏ธ No existing database found at:', DB_PATH);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
66
|
+
console.log('๐ Ensured that the database directory exists.');
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.warn('โ ๏ธ Could not ensure DB directory exists:', err instanceof Error ? err.message : err);
|
|
25
70
|
}
|
|
26
71
|
console.log('โ
Database has been reset. You can now re-run: scai index');
|
|
27
72
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/config/StopWords.ts
|
|
2
|
+
/**
|
|
3
|
+
* These common words are ignored from search queries
|
|
4
|
+
* to reduce noise and improve FTS and embedding match quality.
|
|
5
|
+
*/
|
|
6
|
+
export const STOP_WORDS = new Set([
|
|
7
|
+
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by',
|
|
8
|
+
'for', 'if', 'in', 'into', 'is', 'it', 'no', 'not',
|
|
9
|
+
'of', 'on', 'or', 'such', 'that', 'the', 'their',
|
|
10
|
+
'then', 'there', 'these', 'they', 'this', 'to', 'was',
|
|
11
|
+
'will', 'with', 'what', 'which', 'who', 'whom', 'where',
|
|
12
|
+
'when', 'why', 'how', 'from', 'all', 'any', 'can',
|
|
13
|
+
'did', 'do', 'has', 'have', 'i', 'me', 'my', 'you',
|
|
14
|
+
'your', 'we', 'us', 'our'
|
|
15
|
+
]);
|
package/dist/constants.js
CHANGED
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
+
/**
|
|
5
|
+
* The base directory where internal SCAI config/state is stored:
|
|
6
|
+
* ~/.scai
|
|
7
|
+
*/
|
|
4
8
|
export const SCAI_HOME = path.join(os.homedir(), '.scai');
|
|
9
|
+
/**
|
|
10
|
+
* Full path to the SQLite database used by SCAI:
|
|
11
|
+
* ~/.scai/db.sqlite
|
|
12
|
+
*/
|
|
5
13
|
export const DB_PATH = path.join(SCAI_HOME, 'db.sqlite');
|
|
14
|
+
/**
|
|
15
|
+
* Path to the daemon process ID file (if running in background mode):
|
|
16
|
+
* ~/.scai/daemon.pid
|
|
17
|
+
*/
|
|
6
18
|
export const PID_PATH = path.join(SCAI_HOME, 'daemon.pid');
|
|
19
|
+
/**
|
|
20
|
+
* Path to the config file that stores user settings like model, language, indexDir, etc.:
|
|
21
|
+
* ~/.scai/config.json
|
|
22
|
+
*/
|
|
7
23
|
export const CONFIG_PATH = path.join(SCAI_HOME, 'config.json');
|
|
8
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Path to the daemon log file:
|
|
26
|
+
* ~/.scai/daemon.log
|
|
27
|
+
*/
|
|
28
|
+
export const LOG_PATH = path.join(SCAI_HOME, 'daemon.log');
|
|
29
|
+
/**
|
|
30
|
+
* Get the active index directory.
|
|
31
|
+
*
|
|
32
|
+
* - If the user has configured an `indexDir`, use it.
|
|
33
|
+
* - If not, default to the userโs home directory (`~`), not `.scai`.
|
|
34
|
+
*/
|
|
9
35
|
export function getIndexDir() {
|
|
10
36
|
try {
|
|
11
37
|
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
12
|
-
return config.indexDir ||
|
|
38
|
+
return config.indexDir || os.homedir(); // ๐ Default: ~
|
|
13
39
|
}
|
|
14
40
|
catch (e) {
|
|
15
|
-
return
|
|
41
|
+
return os.homedir(); // ๐ Fallback if config file is missing or invalid
|
|
16
42
|
}
|
|
17
43
|
}
|
|
18
|
-
|
|
44
|
+
/**
|
|
45
|
+
* On-demand index directory to scan for files.
|
|
46
|
+
*
|
|
47
|
+
* Used by indexing logic (`scai index`) to determine what folder to scan.
|
|
48
|
+
*/
|
|
19
49
|
export const INDEX_DIR = getIndexDir();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { summaryModule } from '../pipeline/modules/summaryModule.js';
|
|
2
|
+
import { db } from '../db/client.js';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import fsSync from 'fs';
|
|
5
|
+
import { generateEmbedding } from '../lib/generateEmbedding.js';
|
|
6
|
+
import { DB_PATH } from '../constants.js';
|
|
7
|
+
import { log } from '../utils/log.js';
|
|
8
|
+
import lockfile from 'proper-lockfile';
|
|
9
|
+
import { shouldIgnoreFile } from '../utils/shouldIgnoreFiles.js';
|
|
10
|
+
const MAX_FILES_PER_BATCH = 5;
|
|
11
|
+
async function lockDb() {
|
|
12
|
+
try {
|
|
13
|
+
return await lockfile.lock(DB_PATH);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
log('โ Failed to acquire DB lock: ' + err);
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function runDaemonBatch() {
|
|
21
|
+
const rows = db.prepare(`
|
|
22
|
+
SELECT path, type FROM files
|
|
23
|
+
WHERE summary IS NULL OR summary = ''
|
|
24
|
+
ORDER BY last_modified DESC
|
|
25
|
+
LIMIT ?
|
|
26
|
+
`).all(MAX_FILES_PER_BATCH);
|
|
27
|
+
if (rows.length === 0) {
|
|
28
|
+
log('โ
No files left to summarize.');
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const release = await lockDb();
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
if (!fsSync.existsSync(row.path)) {
|
|
34
|
+
log(`โ ๏ธ Skipped missing file: ${row.path}`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (shouldIgnoreFile(row.path)) {
|
|
38
|
+
log(`โ ๏ธ Skipped (extension): ${row.path}`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const content = await fs.readFile(row.path, 'utf-8');
|
|
43
|
+
const result = await summaryModule.run({ content, filepath: row.path });
|
|
44
|
+
const summary = result?.summary?.trim() || null;
|
|
45
|
+
let embedding = null;
|
|
46
|
+
if (summary) {
|
|
47
|
+
const vector = await generateEmbedding(summary);
|
|
48
|
+
if (vector)
|
|
49
|
+
embedding = JSON.stringify(vector);
|
|
50
|
+
}
|
|
51
|
+
db.prepare(`
|
|
52
|
+
UPDATE files
|
|
53
|
+
SET summary = @summary, embedding = @embedding, indexed_at = datetime('now')
|
|
54
|
+
WHERE path = @path
|
|
55
|
+
`).run({ summary, embedding, path: row.path });
|
|
56
|
+
log(`๐ Summarized: ${row.path}`);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
log(`โ Failed: ${row.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
}
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
62
|
+
}
|
|
63
|
+
await release();
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
import { LOG_PATH, PID_PATH, SCAI_HOME } from '../constants.js';
|
|
3
|
+
import { log } from '../utils/log.js';
|
|
4
|
+
import { runDaemonBatch } from '../daemon/daemonBatch.js'; // โ
now from utils
|
|
5
|
+
const SLEEP_MS = 30 * 1000;
|
|
6
|
+
const IDLE_SLEEP_MS = 4 * SLEEP_MS;
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
async function runDaemonScheduler() {
|
|
11
|
+
fsSync.mkdirSync(SCAI_HOME, { recursive: true });
|
|
12
|
+
fsSync.writeFileSync(PID_PATH, process.pid.toString(), 'utf-8');
|
|
13
|
+
fsSync.appendFileSync(LOG_PATH, `\n\n๐ง Daemon started at ${new Date().toISOString()} โ PID ${process.pid}\n`);
|
|
14
|
+
let cycles = 0;
|
|
15
|
+
while (true) {
|
|
16
|
+
const didWork = await runDaemonBatch();
|
|
17
|
+
cycles++;
|
|
18
|
+
if (cycles % 20 === 0) {
|
|
19
|
+
log(`๐ Still running. Cycles: ${cycles}`);
|
|
20
|
+
}
|
|
21
|
+
await sleep(didWork ? SLEEP_MS : IDLE_SLEEP_MS);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
runDaemonScheduler().catch(err => {
|
|
25
|
+
log(`โ Daemon crashed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
package/dist/db/client.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { DB_PATH, SCAI_HOME } from '../constants.js';
|
|
4
|
+
// Ensure the directory exists
|
|
4
5
|
fs.mkdirSync(SCAI_HOME, { recursive: true });
|
|
6
|
+
// Open the database connection
|
|
5
7
|
export const db = new Database(DB_PATH);
|
|
8
|
+
// Set journal_mode to WAL for better concurrency
|
|
9
|
+
db.pragma('journal_mode = WAL');
|
package/dist/db/fileIndex.js
CHANGED
|
@@ -1,114 +1,123 @@
|
|
|
1
|
-
// File: src/db/fileIndex.ts
|
|
2
1
|
import { db } from './client.js';
|
|
3
2
|
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
4
|
import { generateEmbedding } from '../lib/generateEmbedding.js';
|
|
5
|
-
import * as sqlTemplates from './sqlTemplates.js';
|
|
5
|
+
import * as sqlTemplates from './sqlTemplates.js';
|
|
6
|
+
import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
|
|
7
|
+
/**
|
|
8
|
+
* Index a file into the local SQLite database.
|
|
9
|
+
*
|
|
10
|
+
* - Normalizes the file path for cross-platform compatibility.
|
|
11
|
+
* - Extracts file metadata (last modified time).
|
|
12
|
+
* - Performs an UPSERT into the `files` table with the latest summary/type/timestamp.
|
|
13
|
+
*/
|
|
6
14
|
export function indexFile(filePath, summary, type) {
|
|
7
15
|
const stats = fs.statSync(filePath);
|
|
8
16
|
const lastModified = stats.mtime.toISOString();
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const indexedAt = new Date().toISOString();
|
|
18
|
+
const normalizedPath = path.normalize(filePath).replace(/\\/g, '/');
|
|
19
|
+
db.prepare(sqlTemplates.upsertFileTemplate).run({
|
|
20
|
+
path: normalizedPath,
|
|
21
|
+
summary,
|
|
22
|
+
type,
|
|
23
|
+
lastModified,
|
|
24
|
+
indexedAt,
|
|
25
|
+
embedding: null
|
|
26
|
+
});
|
|
27
|
+
console.log(`๐ Indexed: ${normalizedPath}`);
|
|
19
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Perform a raw keyword-based full-text search using the FTS5 index.
|
|
31
|
+
*/
|
|
20
32
|
export function queryFiles(query, limit = 10) {
|
|
21
|
-
// Sanitize the query by removing or escaping special characters
|
|
22
33
|
const safeQuery = query
|
|
23
34
|
.trim()
|
|
24
35
|
.split(/\s+/)
|
|
25
36
|
.map(token => {
|
|
26
|
-
token = token
|
|
27
|
-
|
|
28
|
-
.replace(/'/g, "''"); // Escape single quotes for SQL safety
|
|
29
|
-
// For multi-word queries, wrap the token in quotes for exact phrase matching
|
|
30
|
-
if (token.includes(' ')) {
|
|
31
|
-
return `"${token}"`; // Exact phrase match for multi-word tokens
|
|
32
|
-
}
|
|
33
|
-
return `${token}*`; // Prefix match for single tokens
|
|
37
|
+
token = token.replace(/[?*\\"]/g, '').replace(/'/g, "''");
|
|
38
|
+
return token.includes(' ') ? `"${token}"` : `${token}*`;
|
|
34
39
|
})
|
|
35
40
|
.join(' OR ');
|
|
36
|
-
// Log the constructed query for debugging purposes
|
|
37
41
|
console.log(`Executing search query: ${safeQuery}`);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
const results = db.prepare(`
|
|
43
|
+
SELECT f.id, f.path, f.summary, f.type, f.last_modified, f.indexed_at
|
|
44
|
+
FROM files f
|
|
45
|
+
JOIN files_fts fts ON f.id = fts.rowid
|
|
46
|
+
WHERE fts.files_fts MATCH ?
|
|
47
|
+
LIMIT ?
|
|
48
|
+
`).all(safeQuery, limit);
|
|
49
|
+
console.log(`Search returned ${results.length} results.`);
|
|
50
|
+
results.forEach(result => {
|
|
51
|
+
console.log(`๐ Found in FTS search: ${result.path}`);
|
|
52
|
+
});
|
|
49
53
|
return results;
|
|
50
54
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const magB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
|
|
55
|
-
return dot / (magA * magB);
|
|
56
|
-
}
|
|
55
|
+
/**
|
|
56
|
+
* Perform a hybrid semantic + keyword-based search.
|
|
57
|
+
*/
|
|
57
58
|
export async function searchFiles(query, topK = 5) {
|
|
58
|
-
|
|
59
|
+
console.log(`๐ง Searching for query: "${query}"`);
|
|
59
60
|
const embedding = await generateEmbedding(query);
|
|
60
|
-
if (!embedding)
|
|
61
|
+
if (!embedding) {
|
|
62
|
+
console.log('โ ๏ธ Failed to generate embedding for query');
|
|
61
63
|
return [];
|
|
62
|
-
|
|
63
|
-
const safeQuery = query
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
}
|
|
65
|
+
const safeQuery = sanitizeQueryForFts(query);
|
|
66
|
+
console.log(`Executing search query in FTS5: ${safeQuery}`);
|
|
67
|
+
const ftsResults = db.prepare(`
|
|
68
|
+
SELECT fts.rowid AS id, f.path, f.summary, f.type, bm25(files_fts) AS bm25Score
|
|
69
|
+
FROM files f
|
|
70
|
+
JOIN files_fts fts ON f.id = fts.rowid
|
|
71
|
+
WHERE fts.files_fts MATCH ?
|
|
72
|
+
AND f.embedding IS NOT NULL
|
|
73
|
+
ORDER BY bm25Score DESC
|
|
74
|
+
LIMIT ?
|
|
75
|
+
`).all(safeQuery, topK);
|
|
76
|
+
console.log(`FTS search returned ${ftsResults.length} results`);
|
|
77
|
+
if (ftsResults.length === 0) {
|
|
78
|
+
console.log('โ ๏ธ No results found from FTS search');
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
ftsResults.forEach(result => {
|
|
82
|
+
console.log(`๐ FTS found: ${result.path}`);
|
|
83
|
+
});
|
|
81
84
|
const bm25Min = Math.min(...ftsResults.map(r => r.bm25Score));
|
|
82
85
|
const bm25Max = Math.max(...ftsResults.map(r => r.bm25Score));
|
|
83
|
-
// Calculate final score combining BM25 and cosine similarity
|
|
84
86
|
const scored = ftsResults.map(result => {
|
|
85
87
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!embResult || typeof embResult.embedding !== 'string')
|
|
88
|
+
const embResult = db.prepare(sqlTemplates.fetchEmbeddingTemplate).get({
|
|
89
|
+
path: result.path,
|
|
90
|
+
});
|
|
91
|
+
if (!embResult || typeof embResult.embedding !== 'string') {
|
|
92
|
+
console.log(`โ ๏ธ No embedding for file: ${result.path}`);
|
|
90
93
|
return null;
|
|
91
|
-
|
|
94
|
+
}
|
|
92
95
|
const vector = JSON.parse(embResult.embedding);
|
|
93
96
|
const sim = cosineSimilarity(embedding, vector);
|
|
94
|
-
// Normalize BM25 scores
|
|
95
97
|
const normalizedBm25 = 1 - ((result.bm25Score - bm25Min) / (bm25Max - bm25Min + 1e-5));
|
|
96
|
-
const
|
|
97
|
-
const finalScore = 0.7 * normalizedSim + 0.3 * normalizedBm25;
|
|
98
|
+
const finalScore = 0.7 * sim + 0.3 * normalizedBm25;
|
|
98
99
|
return {
|
|
99
100
|
path: result.path,
|
|
100
101
|
summary: result.summary,
|
|
101
102
|
score: finalScore,
|
|
102
|
-
sim
|
|
103
|
-
bm25: normalizedBm25
|
|
103
|
+
sim,
|
|
104
|
+
bm25: normalizedBm25,
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
catch (err) {
|
|
107
|
-
console.error(
|
|
108
|
+
console.error(`โ Error processing embedding for file: ${result.path}`, err);
|
|
108
109
|
return null;
|
|
109
110
|
}
|
|
110
|
-
})
|
|
111
|
+
})
|
|
112
|
+
.filter((r) => r !== null)
|
|
111
113
|
.sort((a, b) => b.score - a.score)
|
|
112
114
|
.slice(0, topK);
|
|
115
|
+
console.log(`Returning top ${topK} results based on combined score`);
|
|
113
116
|
return scored;
|
|
114
117
|
}
|
|
118
|
+
function cosineSimilarity(a, b) {
|
|
119
|
+
const dot = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
|
|
120
|
+
const magA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
|
|
121
|
+
const magB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
|
|
122
|
+
return dot / (magA * magB);
|
|
123
|
+
}
|
package/dist/db/schema.js
CHANGED
|
@@ -11,9 +11,25 @@ export function initSchema() {
|
|
|
11
11
|
embedding TEXT
|
|
12
12
|
);
|
|
13
13
|
|
|
14
|
-
-- FTS5 table for fast fullโtext search of summaries and paths
|
|
15
14
|
CREATE VIRTUAL TABLE IF NOT EXISTS files_fts
|
|
16
|
-
USING fts5(path, summary, content='');
|
|
15
|
+
USING fts5(path, summary, content='files', content_rowid='id');
|
|
16
|
+
|
|
17
|
+
-- FTS Triggers to keep files_fts in sync
|
|
18
|
+
CREATE TRIGGER IF NOT EXISTS files_ai AFTER INSERT ON files BEGIN
|
|
19
|
+
INSERT INTO files_fts(rowid, path, summary)
|
|
20
|
+
VALUES (new.id, new.path, new.summary);
|
|
21
|
+
END;
|
|
22
|
+
|
|
23
|
+
CREATE TRIGGER IF NOT EXISTS files_au AFTER UPDATE ON files BEGIN
|
|
24
|
+
UPDATE files_fts SET
|
|
25
|
+
path = new.path,
|
|
26
|
+
summary = new.summary
|
|
27
|
+
WHERE rowid = new.id;
|
|
28
|
+
END;
|
|
29
|
+
|
|
30
|
+
CREATE TRIGGER IF NOT EXISTS files_ad AFTER DELETE ON files BEGIN
|
|
31
|
+
DELETE FROM files_fts WHERE rowid = old.id;
|
|
32
|
+
END;
|
|
17
33
|
`);
|
|
18
|
-
console.log('โ
SQLite schema initialized');
|
|
34
|
+
console.log('โ
SQLite schema initialized with FTS5 triggers');
|
|
19
35
|
}
|
package/dist/db/sqlTemplates.js
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
//
|
|
2
|
-
export const
|
|
3
|
-
INSERT
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// Upsert file metadata into `files`
|
|
2
|
+
export const upsertFileTemplate = `
|
|
3
|
+
INSERT INTO files (path, summary, type, last_modified, indexed_at, embedding)
|
|
4
|
+
VALUES (:path, :summary, :type, :lastModified, :indexedAt, :embedding)
|
|
5
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
6
|
+
summary = CASE
|
|
7
|
+
WHEN excluded.summary IS NOT NULL AND excluded.summary != files.summary
|
|
8
|
+
THEN excluded.summary
|
|
9
|
+
ELSE files.summary
|
|
10
|
+
END,
|
|
11
|
+
type = excluded.type,
|
|
12
|
+
last_modified = excluded.last_modified,
|
|
13
|
+
indexed_at = excluded.indexed_at,
|
|
14
|
+
embedding = CASE
|
|
15
|
+
WHEN excluded.embedding IS NOT NULL AND excluded.embedding != files.embedding
|
|
16
|
+
THEN excluded.embedding
|
|
17
|
+
ELSE files.embedding
|
|
18
|
+
END
|
|
6
19
|
`;
|
|
7
|
-
//
|
|
8
|
-
export const updateFileTemplate = `
|
|
9
|
-
UPDATE files
|
|
10
|
-
SET type = :type,
|
|
11
|
-
last_modified = :lastModified,
|
|
12
|
-
indexed_at = datetime('now')
|
|
13
|
-
WHERE path = :path
|
|
14
|
-
AND last_modified != :lastModified
|
|
15
|
-
`;
|
|
16
|
-
// Template for deleting a file from FTS
|
|
17
|
-
export const deleteFromFtsTemplate = `
|
|
18
|
-
DELETE FROM files_fts
|
|
19
|
-
WHERE rowid = (SELECT id FROM files WHERE path = :path)
|
|
20
|
-
`;
|
|
21
|
-
// Template for inserting a file into FTS with its ID
|
|
22
|
-
export const insertIntoFtsTemplate = `
|
|
23
|
-
INSERT INTO files_fts(rowid, path, summary)
|
|
24
|
-
VALUES((SELECT id FROM files WHERE path = :path), :path, :summary)
|
|
25
|
-
`;
|
|
26
|
-
// Template for fetching BM25 scores from FTS
|
|
20
|
+
// Fetch search results with BM25 ranking
|
|
27
21
|
export const fetchBm25ScoresTemplate = `
|
|
28
22
|
SELECT f.path, f.summary, f.type, bm25(files_fts) AS bm25Score
|
|
29
23
|
FROM files_fts
|
|
@@ -31,7 +25,17 @@ export const fetchBm25ScoresTemplate = `
|
|
|
31
25
|
WHERE files_fts MATCH :query
|
|
32
26
|
LIMIT 50
|
|
33
27
|
`;
|
|
34
|
-
//
|
|
28
|
+
// Fetch embedding vector for a file
|
|
35
29
|
export const fetchEmbeddingTemplate = `
|
|
36
30
|
SELECT embedding FROM files WHERE path = :path
|
|
37
31
|
`;
|
|
32
|
+
// Used for non-embedding query in `queryFiles()`
|
|
33
|
+
export const rawQueryTemplate = `
|
|
34
|
+
SELECT f.path, f.summary, f.type, f.last_modified, f.indexed_at,
|
|
35
|
+
bm25(files_fts) AS rank
|
|
36
|
+
FROM files_fts
|
|
37
|
+
JOIN files f ON files_fts.rowid = f.id
|
|
38
|
+
WHERE files_fts MATCH :query
|
|
39
|
+
ORDER BY rank
|
|
40
|
+
LIMIT :limit
|
|
41
|
+
`;
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,6 @@ import { Config } from './config.js';
|
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
6
|
const require = createRequire(import.meta.url);
|
|
7
7
|
const { version } = require('../package.json');
|
|
8
|
-
// ๐ง Commands
|
|
9
|
-
import { checkEnv } from "./commands/EnvCmd.js";
|
|
10
|
-
import { checkGit } from "./commands/GitCmd.js";
|
|
11
8
|
import { suggestCommitMessage } from "./commands/CommitSuggesterCmd.js";
|
|
12
9
|
import { handleRefactor } from "./commands/RefactorCmd.js";
|
|
13
10
|
import { generateTests } from "./commands/TestGenCmd.js";
|
|
@@ -18,7 +15,7 @@ import { runModulePipelineFromCLI } from './commands/ModulePipelineCmd.js';
|
|
|
18
15
|
import { runIndexCommand } from './commands/IndexCmd.js';
|
|
19
16
|
import { resetDatabase } from './commands/ResetDbCmd.js';
|
|
20
17
|
import { runQueryCommand } from './commands/QueryCmd.js';
|
|
21
|
-
import {
|
|
18
|
+
import { startDaemon } from './commands/DaemonCmd.js';
|
|
22
19
|
import { runStopDaemonCommand } from "./commands/StopDaemonCmd.js";
|
|
23
20
|
import { runAskCommand } from './commands/AskCmd.js';
|
|
24
21
|
// ๐๏ธ CLI Setup
|
|
@@ -34,12 +31,14 @@ cmd
|
|
|
34
31
|
await bootstrap();
|
|
35
32
|
console.log('โ
Model initialization completed!');
|
|
36
33
|
});
|
|
34
|
+
cmd
|
|
35
|
+
.command('sugg')
|
|
36
|
+
.description('Suggest a commit message from staged changes')
|
|
37
|
+
.option('-c, --commit', 'Automatically commit with suggested message')
|
|
38
|
+
.action(suggestCommitMessage);
|
|
37
39
|
// ๐ง Group: Git-related commands
|
|
38
40
|
const git = cmd.command('git').description('Git utilities');
|
|
39
|
-
git
|
|
40
|
-
.command('status')
|
|
41
|
-
.description('Check Git status')
|
|
42
|
-
.action(checkGit);
|
|
41
|
+
// The sugg command under the 'git' group
|
|
43
42
|
git
|
|
44
43
|
.command('sugg')
|
|
45
44
|
.description('Suggest a commit message from staged changes')
|
|
@@ -66,16 +65,6 @@ gen
|
|
|
66
65
|
.command('tests <file>')
|
|
67
66
|
.description('Generate a Jest test file for the specified JS/TS module')
|
|
68
67
|
.action((file) => generateTests(file));
|
|
69
|
-
// ๐ Indexing
|
|
70
|
-
cmd
|
|
71
|
-
.command('index [targetDir]')
|
|
72
|
-
.description('Index supported files in the given directory (or current folder if none)')
|
|
73
|
-
.option('-d, --detached', 'Run summarizer daemon after indexing')
|
|
74
|
-
.option('--force', 'Force indexing even if another folder has already been indexed')
|
|
75
|
-
.action((targetDir, options) => {
|
|
76
|
-
const resolvedDir = targetDir ? path.resolve(targetDir) : process.cwd();
|
|
77
|
-
runIndexCommand(resolvedDir, { detached: options.detached, force: options.force });
|
|
78
|
-
});
|
|
79
68
|
// โ๏ธ Group: Configuration settings
|
|
80
69
|
const set = cmd.command('set').description('Set configuration values');
|
|
81
70
|
set
|
|
@@ -100,34 +89,47 @@ set
|
|
|
100
89
|
Config.show();
|
|
101
90
|
});
|
|
102
91
|
// ๐งช Diagnostics and info
|
|
103
|
-
cmd
|
|
104
|
-
.command('env')
|
|
105
|
-
.description('Check environment variables')
|
|
106
|
-
.action(checkEnv);
|
|
107
92
|
cmd
|
|
108
93
|
.command('config')
|
|
109
94
|
.description('Show the currently active model and language settings')
|
|
110
95
|
.action(() => {
|
|
111
96
|
Config.show();
|
|
112
97
|
});
|
|
98
|
+
// Add explanation about alpha features directly in the help menu
|
|
99
|
+
cmd.addHelpText('after', `
|
|
100
|
+
๐จ Alpha Features:
|
|
101
|
+
- The "index", "daemon", "stop-daemon", "reset-db" commands are considered alpha features.
|
|
102
|
+
- These commands are in active development and may change in the future.
|
|
103
|
+
|
|
104
|
+
๐ก Use with caution and expect possible changes or instability.
|
|
105
|
+
`);
|
|
106
|
+
// ๐ Indexing
|
|
107
|
+
cmd
|
|
108
|
+
.command('index [targetDir]')
|
|
109
|
+
.description('Index supported files in the given directory (or current folder if none)')
|
|
110
|
+
.option('--force', 'Force indexing even if another folder has already been indexed')
|
|
111
|
+
.action((targetDir, options) => {
|
|
112
|
+
runIndexCommand(targetDir, { force: options.force });
|
|
113
|
+
});
|
|
113
114
|
// ๐ง Query and assistant
|
|
114
115
|
cmd
|
|
115
116
|
.command('query <query>')
|
|
116
117
|
.description('Search indexed files by keyword')
|
|
117
118
|
.action(runQueryCommand);
|
|
118
119
|
cmd
|
|
119
|
-
.command('ask')
|
|
120
|
-
.description('Ask a question
|
|
121
|
-
.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
runAskCommand(q);
|
|
120
|
+
.command('ask [question...]') // <- the ... makes it variadic
|
|
121
|
+
.description('Ask a question based on indexed files')
|
|
122
|
+
.action((questionParts) => {
|
|
123
|
+
const fullQuery = questionParts?.join(' ');
|
|
124
|
+
runAskCommand(fullQuery);
|
|
125
125
|
});
|
|
126
126
|
// ๐ ๏ธ Background tasks and maintenance
|
|
127
127
|
cmd
|
|
128
128
|
.command('daemon')
|
|
129
129
|
.description('Run background summarization of indexed files')
|
|
130
|
-
.action(
|
|
130
|
+
.action(async () => {
|
|
131
|
+
await startDaemon(); // ignore the return value
|
|
132
|
+
});
|
|
131
133
|
cmd
|
|
132
134
|
.command('stop-daemon')
|
|
133
135
|
.description('Stop the background summarizer daemon')
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { LOG_PATH } from '../constants.js';
|
|
3
|
+
export function log(...args) {
|
|
4
|
+
const timestamp = new Date().toISOString();
|
|
5
|
+
const message = args.map(arg => typeof arg === 'string' ? arg : JSON.stringify(arg, null, 2)).join(' ');
|
|
6
|
+
const isBackground = process.env.BACKGROUND_MODE === 'true';
|
|
7
|
+
if (isBackground) {
|
|
8
|
+
// If running in background, log to a file
|
|
9
|
+
fs.appendFileSync(LOG_PATH, `[${timestamp}] ${message}\n`);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
// Otherwise, log to the console
|
|
13
|
+
console.log(`[${timestamp}] ${message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/utils/sanitizeQuery.ts
|
|
2
|
+
import { STOP_WORDS } from '../config/StopWords.js';
|
|
3
|
+
export function sanitizeQueryForFts(input) {
|
|
4
|
+
const tokens = input
|
|
5
|
+
.trim()
|
|
6
|
+
.split(/\s+/)
|
|
7
|
+
.map(token => token.toLowerCase())
|
|
8
|
+
.filter(token => token.length > 2 &&
|
|
9
|
+
!STOP_WORDS.has(token) &&
|
|
10
|
+
/^[a-z0-9]+$/.test(token))
|
|
11
|
+
.map(token => token.replace(/[?*\\"]/g, '').replace(/'/g, "''") + '*');
|
|
12
|
+
// ๐ Prevent FTS syntax errors by returning a catch-all query
|
|
13
|
+
if (tokens.length === 0)
|
|
14
|
+
return '*';
|
|
15
|
+
return tokens.join(' OR ');
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"scai": "./dist/index.js"
|
|
@@ -27,12 +27,14 @@
|
|
|
27
27
|
"better-sqlite3": "^12.1.1",
|
|
28
28
|
"commander": "^11.0.0",
|
|
29
29
|
"fast-glob": "^3.3.3",
|
|
30
|
-
"ora": "^8.2.0"
|
|
30
|
+
"ora": "^8.2.0",
|
|
31
|
+
"proper-lockfile": "^4.1.2"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/better-sqlite3": "^7.6.13",
|
|
34
35
|
"@types/jest": "^30.0.0",
|
|
35
36
|
"@types/node": "^24.0.1",
|
|
37
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
36
38
|
"jest": "^30.0.2",
|
|
37
39
|
"ts-jest": "^29.4.0",
|
|
38
40
|
"typescript": "^5.8.3"
|