scai 0.1.18 ā 0.1.20
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 +66 -0
- package/dist/commands/ChangeLogUpdateCmd.js +3 -3
- package/dist/commands/CommitSuggesterCmd.js +4 -4
- package/dist/commands/DaemonCmd.js +80 -0
- package/dist/commands/IndexCmd.js +23 -24
- package/dist/commands/ModulePipelineCmd.js +1 -1
- package/dist/commands/QueryCmd.js +20 -0
- package/dist/commands/RefactorCmd.js +5 -5
- package/dist/commands/StopDaemonCmd.js +23 -0
- package/dist/commands/SummaryCmd.js +10 -6
- package/dist/commands/TestGenCmd.js +2 -2
- package/dist/config/IgnoredExtensions.js +46 -0
- package/dist/config/IgnoredPaths.js +45 -0
- package/dist/config/ModelConfig.js +1 -1
- package/dist/db/fileIndex.js +111 -15
- package/dist/db/schema.js +9 -3
- package/dist/db/sqlTemplates.js +37 -0
- package/dist/index.js +29 -2
- package/dist/lib/generate.js +9 -3
- package/dist/lib/generateEmbedding.js +22 -0
- package/dist/pipeline/modules/changeLogModule.js +17 -3
- package/dist/pipeline/modules/cleanupModule.js +3 -3
- package/dist/pipeline/modules/commentModule.js +3 -3
- package/dist/pipeline/modules/commitSuggesterModule.js +5 -5
- package/dist/pipeline/modules/generateTestsModule.js +6 -6
- package/dist/pipeline/modules/refactorModule.js +3 -3
- package/dist/pipeline/modules/summaryModule.js +25 -46
- package/dist/pipeline/runModulePipeline.js +4 -3
- package/dist/utils/detectFileType.js +52 -0
- package/dist/utils/shouldIgnoreFiles.js +6 -0
- package/package.json +1 -1
- package/dist/utils/getSummary.js +0 -31
- package/dist/utils/languageConfig.js +0 -7
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { searchFiles } from "../db/fileIndex.js";
|
|
2
|
+
import { generate } from "../lib/generate.js";
|
|
3
|
+
import { summaryModule } from "../pipeline/modules/summaryModule.js";
|
|
4
|
+
export async function runAskCommand(query) {
|
|
5
|
+
if (!query) {
|
|
6
|
+
console.error('ā Please provide a search query.\nš Usage: scai ask "keyword"');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
console.log(`š Searching for: "${query}"\n`);
|
|
10
|
+
// Use vector-based search
|
|
11
|
+
const results = await searchFiles(query, 5); // Or 3 if you want fewer
|
|
12
|
+
if (results.length === 0) {
|
|
13
|
+
console.log('ā ļø No similar embeddings found. Asking the model for context instead...');
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log('š Closest files based on semantic similarity:');
|
|
17
|
+
results.forEach(file => {
|
|
18
|
+
console.log(`š Path: ${file?.path}`);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
let allSummaries = '';
|
|
22
|
+
for (const file of results) {
|
|
23
|
+
try {
|
|
24
|
+
if (!file?.summary) {
|
|
25
|
+
console.warn(`ā ļø No summary available for file: ${file?.path}`);
|
|
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);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (allSummaries.trim()) {
|
|
39
|
+
console.log('š§ Summaries found, sending them to the model for synthesis...');
|
|
40
|
+
try {
|
|
41
|
+
const input = {
|
|
42
|
+
content: `${query}\n\n${allSummaries}`,
|
|
43
|
+
filepath: '',
|
|
44
|
+
};
|
|
45
|
+
const modelResponse = await generate(input, 'llama3');
|
|
46
|
+
console.log(`\nš Model response:\n${modelResponse.content}`);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error('ā Model request failed:', err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log('ā ļø No summaries found. Asking the model for context only...');
|
|
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
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -14,8 +14,8 @@ export async function handleChangelogUpdate() {
|
|
|
14
14
|
console.log("ā ļø No staged or unstaged changes to include in changelog.");
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
const result = await runModulePipeline([changelogModule], {
|
|
18
|
-
if (!result.
|
|
17
|
+
const result = await runModulePipeline([changelogModule], { content: diff });
|
|
18
|
+
if (!result.content.trim()) {
|
|
19
19
|
console.log("ā
No significant changes for changelog.");
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
@@ -29,7 +29,7 @@ export async function handleChangelogUpdate() {
|
|
|
29
29
|
console.log("š Creating new CHANGELOG.md");
|
|
30
30
|
}
|
|
31
31
|
const today = new Date().toISOString().split("T")[0];
|
|
32
|
-
const newEntry = `\n\n## ${today}\n\n${result.
|
|
32
|
+
const newEntry = `\n\n## ${today}\n\n${result.content}`;
|
|
33
33
|
await fs.writeFile(changelogPath, existing + newEntry, "utf-8");
|
|
34
34
|
console.log("š CHANGELOG.md updated.");
|
|
35
35
|
}
|
|
@@ -60,8 +60,8 @@ export async function suggestCommitMessage(options) {
|
|
|
60
60
|
console.log('ā ļø No staged changes to suggest a message for.');
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
|
-
const
|
|
64
|
-
const suggestions =
|
|
63
|
+
const response = await commitSuggesterModule.run({ content: diff });
|
|
64
|
+
const suggestions = response.suggestions || [];
|
|
65
65
|
if (!suggestions.length) {
|
|
66
66
|
console.log('ā ļø No commit suggestions generated.');
|
|
67
67
|
return;
|
|
@@ -80,8 +80,8 @@ export async function suggestCommitMessage(options) {
|
|
|
80
80
|
const choice = await askUserToChoose(suggestions);
|
|
81
81
|
if (choice === 'regenerate') {
|
|
82
82
|
console.log('\nš Regenerating suggestions...\n');
|
|
83
|
-
const
|
|
84
|
-
suggestions.splice(0, suggestions.length, ...(
|
|
83
|
+
const response = await commitSuggesterModule.run({ content: diff });
|
|
84
|
+
suggestions.splice(0, suggestions.length, ...(response.suggestions || []));
|
|
85
85
|
continue;
|
|
86
86
|
}
|
|
87
87
|
if (choice === 'custom') {
|
|
@@ -0,0 +1,80 @@
|
|
|
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 os from 'os';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { generateEmbedding } from '../lib/generateEmbedding.js';
|
|
8
|
+
import { IGNORED_EXTENSIONS } from '../config/IgnoredExtensions.js';
|
|
9
|
+
const MAX_FILES = 1000;
|
|
10
|
+
const DAEMON_DURATION_MINUTES = 25;
|
|
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;
|
|
29
|
+
}
|
|
30
|
+
for (const row of rows) {
|
|
31
|
+
if (shouldIgnoreFile(row.path)) {
|
|
32
|
+
console.log(`ā ļø Ignored file (unwanted extension): ${row.path}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const content = await fs.readFile(row.path, 'utf-8');
|
|
37
|
+
const result = await summaryModule.run({ content, filepath: row.path });
|
|
38
|
+
const summary = result?.summary?.trim() ? result.summary : null;
|
|
39
|
+
let embedding = null;
|
|
40
|
+
if (summary) {
|
|
41
|
+
const vector = await generateEmbedding(summary);
|
|
42
|
+
if (vector)
|
|
43
|
+
embedding = JSON.stringify(vector);
|
|
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));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -1,45 +1,44 @@
|
|
|
1
1
|
import fg from 'fast-glob';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { ModelConfig } from '../config/ModelConfig.js';
|
|
4
3
|
import { initSchema } from '../db/schema.js';
|
|
5
4
|
import { indexFile } from '../db/fileIndex.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { shouldIgnoreFile } from '../utils/shouldIgnoreFiles.js';
|
|
6
|
+
import { detectFileType } from '../utils/detectFileType.js';
|
|
7
|
+
import { runDaemonScheduler } from './DaemonCmd.js';
|
|
8
|
+
import { IGNORED_FOLDER_GLOBS } from '../config/IgnoredPaths.js';
|
|
8
9
|
const IGNORE = [
|
|
9
|
-
'**/node_modules/**',
|
|
10
|
-
'**/
|
|
11
|
-
'**/build/**',
|
|
12
|
-
'**/coverage/**',
|
|
13
|
-
'**/.git/**',
|
|
14
|
-
'**/*.test.*'
|
|
10
|
+
'**/node_modules/**', '**/dist/**', '**/build/**',
|
|
11
|
+
'**/coverage/**', '**/.git/**', '**/*.test.*'
|
|
15
12
|
];
|
|
16
|
-
export async function runIndexCommand(targetDir = process.cwd()) {
|
|
13
|
+
export async function runIndexCommand(targetDir = process.cwd(), options = {}) {
|
|
17
14
|
console.log(`š Indexing files in: ${targetDir}`);
|
|
18
15
|
initSchema();
|
|
19
|
-
const
|
|
20
|
-
const exts = EXTENSIONS_BY_LANG[lang] || ['.txt'];
|
|
21
|
-
const patterns = exts.map(ext => `**/*${ext}`);
|
|
22
|
-
const files = await fg(patterns, {
|
|
16
|
+
const files = await fg('**/*.*', {
|
|
23
17
|
cwd: targetDir,
|
|
24
|
-
ignore:
|
|
18
|
+
ignore: IGNORED_FOLDER_GLOBS,
|
|
25
19
|
absolute: true,
|
|
26
20
|
});
|
|
27
|
-
// Count file extensions (diagnostic)
|
|
28
21
|
const countByExt = {};
|
|
29
|
-
|
|
30
|
-
const ext = path.extname(file);
|
|
31
|
-
countByExt[ext] = (countByExt[ext] || 0) + 1;
|
|
32
|
-
});
|
|
33
|
-
console.log('š Indexed files by extension:', countByExt);
|
|
22
|
+
let count = 0;
|
|
34
23
|
for (const file of files) {
|
|
24
|
+
if (shouldIgnoreFile(file))
|
|
25
|
+
continue;
|
|
35
26
|
try {
|
|
36
|
-
const
|
|
37
|
-
indexFile(file,
|
|
27
|
+
const type = detectFileType(file);
|
|
28
|
+
indexFile(file, null, type); // empty summary for now
|
|
29
|
+
const ext = path.extname(file);
|
|
30
|
+
countByExt[ext] = (countByExt[ext] || 0) + 1;
|
|
38
31
|
console.log(`š Indexed: ${path.relative(targetDir, file)}`);
|
|
32
|
+
count++;
|
|
39
33
|
}
|
|
40
34
|
catch (err) {
|
|
41
35
|
console.warn(`ā ļø Skipped ${file}:`, err instanceof Error ? err.message : err);
|
|
42
36
|
}
|
|
43
37
|
}
|
|
44
|
-
console.log(
|
|
38
|
+
console.log('š Indexed files by extension:', countByExt);
|
|
39
|
+
console.log(`ā
Done. Indexed ${count} files.`);
|
|
40
|
+
if (options.detached) {
|
|
41
|
+
console.log('š Starting summarizer daemon in background mode...');
|
|
42
|
+
runDaemonScheduler(); // Infinite loop every 30 min
|
|
43
|
+
}
|
|
45
44
|
}
|
|
@@ -16,7 +16,7 @@ export async function runModulePipelineFromCLI(file, options) {
|
|
|
16
16
|
console.error(`ā Could not read file: ${file}`);
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
|
-
const input = {
|
|
19
|
+
const input = { content: fileContent, filepath: file };
|
|
20
20
|
// Retrieve modules from the registry
|
|
21
21
|
const resolvedModules = moduleNames.map((name) => {
|
|
22
22
|
const module = getModuleByName(name);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { queryFiles } from '../db/fileIndex.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export async function runQueryCommand(query) {
|
|
4
|
+
if (!query) {
|
|
5
|
+
console.error('ā Please provide a search query.\nš Usage: scai query "keyword"');
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
console.log(`š Searching for: "${query}"\n`);
|
|
9
|
+
const results = queryFiles(query);
|
|
10
|
+
if (results.length === 0) {
|
|
11
|
+
console.log('ā ļø No matching files found.');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
results.forEach((result, index) => {
|
|
15
|
+
console.log(`š [${index + 1}] ${path.relative(process.cwd(), result.path)}`);
|
|
16
|
+
console.log(` š ${result.summary}`);
|
|
17
|
+
console.log();
|
|
18
|
+
});
|
|
19
|
+
console.log(`ā
Found ${results.length} result(s).`);
|
|
20
|
+
}
|
|
@@ -25,17 +25,17 @@ export async function handleRefactor(filepath, options = {}) {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
// Read source code
|
|
28
|
-
const
|
|
28
|
+
const content = await fs.readFile(filepath, 'utf-8');
|
|
29
29
|
// Run through pipeline modules
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
30
|
+
const response = await runModulePipeline([addCommentsModule, cleanupModule], { content });
|
|
31
|
+
if (!response.content.trim())
|
|
32
32
|
throw new Error('ā ļø Model returned empty result');
|
|
33
33
|
// Save refactored output
|
|
34
|
-
await fs.writeFile(refactoredPath,
|
|
34
|
+
await fs.writeFile(refactoredPath, response.content, 'utf-8');
|
|
35
35
|
console.log(`ā
Refactored code saved to: ${refactoredPath}`);
|
|
36
36
|
console.log(`ā¹ļø Run again with '--apply' to overwrite the original.`);
|
|
37
37
|
}
|
|
38
38
|
catch (err) {
|
|
39
|
-
console.error('ā Error in refactor command:', err.message);
|
|
39
|
+
console.error('ā Error in refactor command:', err instanceof Error ? err.message : err);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
const PID_PATH = path.join(os.homedir(), '.scai/daemon.pid');
|
|
5
|
+
export async function runStopDaemonCommand() {
|
|
6
|
+
if (!fs.existsSync(PID_PATH)) {
|
|
7
|
+
console.log('š No daemon is currently running.');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const pid = parseInt(fs.readFileSync(PID_PATH, 'utf-8'), 10);
|
|
11
|
+
if (isNaN(pid)) {
|
|
12
|
+
console.error('ā ļø Invalid PID file.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid);
|
|
17
|
+
fs.unlinkSync(PID_PATH);
|
|
18
|
+
console.log(`ā
Daemon process ${pid} stopped.`);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
console.error(`ā Failed to stop process ${pid}:`, err instanceof Error ? err.message : err);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -3,10 +3,10 @@ import readline from 'readline';
|
|
|
3
3
|
import { summaryModule } from '../pipeline/modules/summaryModule.js'; // Import summaryModule
|
|
4
4
|
import { summarizeCode } from '../utils/summarizer.js'; // Import summarizeCode
|
|
5
5
|
export async function summarizeFile(filepath) {
|
|
6
|
-
let
|
|
6
|
+
let content = '';
|
|
7
7
|
if (filepath) {
|
|
8
8
|
try {
|
|
9
|
-
|
|
9
|
+
content = await fs.readFile(filepath, 'utf-8');
|
|
10
10
|
}
|
|
11
11
|
catch (err) {
|
|
12
12
|
console.error(`ā Could not read or summarize ${filepath}:`, err.message);
|
|
@@ -24,14 +24,18 @@ export async function summarizeFile(filepath) {
|
|
|
24
24
|
terminal: false,
|
|
25
25
|
});
|
|
26
26
|
for await (const line of rl) {
|
|
27
|
-
|
|
27
|
+
content += line + '\n';
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
if (
|
|
30
|
+
if (content.trim()) {
|
|
31
31
|
// Call the summary module to get the raw summary
|
|
32
|
-
const
|
|
32
|
+
const response = await summaryModule.run({ content, filepath });
|
|
33
33
|
// Pass the summary text to the utility function for formatting
|
|
34
|
-
|
|
34
|
+
if (!response.summary) {
|
|
35
|
+
console.warn("No summary available.");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const formattedSummary = summarizeCode(response.summary);
|
|
35
39
|
console.log(formattedSummary);
|
|
36
40
|
}
|
|
37
41
|
else {
|
|
@@ -4,8 +4,8 @@ import { cleanupModule } from '../pipeline/modules/cleanupModule.js';
|
|
|
4
4
|
import { runModulePipeline } from '../pipeline/runModulePipeline.js';
|
|
5
5
|
export async function generateTests(filepath) {
|
|
6
6
|
try {
|
|
7
|
-
const
|
|
8
|
-
const result = await runModulePipeline([generateTestsModule, cleanupModule], {
|
|
7
|
+
const content = await fs.readFile(filepath, 'utf-8');
|
|
8
|
+
const result = await runModulePipeline([generateTestsModule, cleanupModule], { content, filepath });
|
|
9
9
|
console.log('ā
Test generated and cleaned up.');
|
|
10
10
|
}
|
|
11
11
|
catch (err) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/config/IgnoredExtensions.ts
|
|
2
|
+
export const IGNORED_EXTENSIONS = [
|
|
3
|
+
// š¼ Media
|
|
4
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico',
|
|
5
|
+
'.mp4', '.mp3', '.mov', '.avi', '.mkv', '.flv', '.wav', '.flac',
|
|
6
|
+
'.aac', '.m4a', '.wma', '.3gp', '.webm', '.ogg', '.aiff', '.au',
|
|
7
|
+
// š¦ Archives & install packages
|
|
8
|
+
'.zip', '.tar', '.gz', '.bz2', '.xz', '.rar', '.7z',
|
|
9
|
+
'.jar', '.war', '.ear', '.deb', '.rpm', '.pkg', '.msi', '.dmg', '.cab', '.apk',
|
|
10
|
+
'.tar.gz', '.tar.bz2', '.tar.xz', '.tar.lzma', '.tar.zst',
|
|
11
|
+
// š§± Binaries & executables
|
|
12
|
+
'.exe', '.dll', '.bin', '.so', '.dylib', '.a', '.lib',
|
|
13
|
+
'.iso', '.img', '.elf', '.o', '.obj', '.msm', '.vbs', '.jscript',
|
|
14
|
+
'.cmd', '.bat', '.ps1', '.sh', '.bash', '.run',
|
|
15
|
+
// š§Ŗ Runtime / build / cache
|
|
16
|
+
'.log', '.tmp', '.map',
|
|
17
|
+
'.db', '.sqlite', '.pkl', '.sav', '.rdb', '.ldb',
|
|
18
|
+
'.pyc', '.class', '.tsbuildinfo', '.coverage', '.eslintcache',
|
|
19
|
+
'.yarn', '.webpack', '.babel', '.compilercache',
|
|
20
|
+
// š¤ Fonts & styles
|
|
21
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf', '.css.map',
|
|
22
|
+
'.scss', '.sass', '.less', '.styl',
|
|
23
|
+
// š Certs, keys, credentials
|
|
24
|
+
'.crt', '.key', '.pem', '.pub', '.asc', '.gpg', '.p12', '.csr', '.der', '.pfx',
|
|
25
|
+
// ā»ļø Backups / temp
|
|
26
|
+
'.bak', '.old', '.swp', '.swo', '.orig',
|
|
27
|
+
'.sublime-workspace', '.sublime-project', '.db-shm', '.db-wal',
|
|
28
|
+
// š System/config folders (still ignored by path, not extension)
|
|
29
|
+
'.DS_Store', '.bundle', '.npmrc',
|
|
30
|
+
// šŗļø GIS / Geospatial
|
|
31
|
+
'.shp', '.shx', '.dbf', '.prj', '.qix', '.sbn', '.sbx', '.shp.xml', '.cpg', '.gpkg', '.mif', '.mid',
|
|
32
|
+
// š Enterprise BI / Reporting
|
|
33
|
+
'.pbix', '.rdl', '.rpt', '.bqy', '.iqy',
|
|
34
|
+
// š§Ŗ ETL / DWH / Modeling
|
|
35
|
+
'.abf', '.dtsx', '.bim', '.xmi',
|
|
36
|
+
// šļø CAD / Engineering
|
|
37
|
+
'.dwg', '.dxf', '.step', '.stp', '.sldprt', '.sldasm',
|
|
38
|
+
'.iges', '.igs', '.3ds', '.fbx',
|
|
39
|
+
// š§¾ Forms / Print / Publishing
|
|
40
|
+
'.xps', '.afpub', '.pub', '.indd', '.qxd', '.frm', '.frx', '.frl',
|
|
41
|
+
// š° ERP / Finance / Legacy DB
|
|
42
|
+
'.mbd', '.fdb', '.nav', '.accdb', '.mdb', '.gdb',
|
|
43
|
+
'.sap', '.sappkg', '.qbw', '.qbb',
|
|
44
|
+
// š Lock files (but NOT lock *configs*)
|
|
45
|
+
'.lck', '.lockfile', '.db-lock', '.pid', '.socket',
|
|
46
|
+
];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// List of folders and file globs to ignore during indexing and scanning.
|
|
2
|
+
// These patterns are used by fast-glob in all commands.
|
|
3
|
+
export const IGNORED_FOLDER_GLOBS = [
|
|
4
|
+
'**/node_modules/**',
|
|
5
|
+
'**/dist/**',
|
|
6
|
+
'**/build/**',
|
|
7
|
+
'**/target/**',
|
|
8
|
+
'**/coverage/**',
|
|
9
|
+
'**/.git/**',
|
|
10
|
+
'**/.next/**',
|
|
11
|
+
'**/.vercel/**',
|
|
12
|
+
'**/.idea/**',
|
|
13
|
+
'**/.vscode/**',
|
|
14
|
+
'**/__pycache__/**',
|
|
15
|
+
'**/.venv/**',
|
|
16
|
+
'**/env/**',
|
|
17
|
+
'**/.gradle/**',
|
|
18
|
+
'**/.output/**',
|
|
19
|
+
'**/tmp/**',
|
|
20
|
+
'**/*.test.*',
|
|
21
|
+
'**/.m2/**',
|
|
22
|
+
'**/.gradle/**',
|
|
23
|
+
'**/.tox/**',
|
|
24
|
+
'**/.nox/**',
|
|
25
|
+
'**/.hypothesis/**',
|
|
26
|
+
'**/.npm/**',
|
|
27
|
+
'**/.yarn/**',
|
|
28
|
+
'**/*.o',
|
|
29
|
+
'**/*.out',
|
|
30
|
+
'**/*.exe',
|
|
31
|
+
'**/*.dll',
|
|
32
|
+
'**/.cache/**',
|
|
33
|
+
'**/.pylint.d/**',
|
|
34
|
+
'**/.eslintcache/**',
|
|
35
|
+
'**/.cache-loader/**',
|
|
36
|
+
'**/.serverless/**',
|
|
37
|
+
'**/.docker/**',
|
|
38
|
+
'**/.sublime-workspace',
|
|
39
|
+
'**/.sublime-project',
|
|
40
|
+
'**/*.log',
|
|
41
|
+
'**/npm-debug.log',
|
|
42
|
+
'**/yarn-error.log',
|
|
43
|
+
'**/debug.log',
|
|
44
|
+
'**/Dockerfile',
|
|
45
|
+
];
|
package/dist/db/fileIndex.js
CHANGED
|
@@ -1,18 +1,114 @@
|
|
|
1
|
+
// File: src/db/fileIndex.ts
|
|
1
2
|
import { db } from './client.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { generateEmbedding } from '../lib/generateEmbedding.js';
|
|
5
|
+
import * as sqlTemplates from './sqlTemplates.js'; // Import the SQL templates
|
|
6
|
+
export function indexFile(filePath, summary, type) {
|
|
7
|
+
const stats = fs.statSync(filePath);
|
|
8
|
+
const lastModified = stats.mtime.toISOString();
|
|
9
|
+
// 1) INSERT new rows (only when path not present)
|
|
10
|
+
const insertStmt = db.prepare(sqlTemplates.insertFileTemplate);
|
|
11
|
+
insertStmt.run({ path: filePath, summary, type, lastModified });
|
|
12
|
+
// 2) UPDATE metadata if file already existed and changed
|
|
13
|
+
const updateStmt = db.prepare(sqlTemplates.updateFileTemplate);
|
|
14
|
+
updateStmt.run({ path: filePath, type, lastModified });
|
|
15
|
+
// Step 1: Delete from FTS where the path matches
|
|
16
|
+
db.prepare(sqlTemplates.deleteFromFtsTemplate).run({ path: filePath });
|
|
17
|
+
// Step 2: Insert into FTS with the same id
|
|
18
|
+
db.prepare(sqlTemplates.insertIntoFtsTemplate).run({ path: filePath, summary });
|
|
8
19
|
}
|
|
9
|
-
export function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
export function queryFiles(query, limit = 3) {
|
|
21
|
+
// Sanitize the query by removing or escaping special characters
|
|
22
|
+
const safeQuery = query
|
|
23
|
+
.trim()
|
|
24
|
+
.split(/\s+/)
|
|
25
|
+
.map(token => {
|
|
26
|
+
token = token
|
|
27
|
+
.replace(/[?*\\"]/g, '') // Remove question marks, asterisks, backslashes, and double quotes
|
|
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
|
|
34
|
+
})
|
|
35
|
+
.join(' OR ');
|
|
36
|
+
// Log the constructed query for debugging purposes
|
|
37
|
+
console.log(`Executing search query: ${safeQuery}`);
|
|
38
|
+
// Execute the query with safeQuery and limit as parameters
|
|
39
|
+
const sql = `
|
|
40
|
+
SELECT f.path, f.summary, f.type, f.last_modified, f.indexed_at,
|
|
41
|
+
bm25(files_fts) AS rank
|
|
42
|
+
FROM files_fts
|
|
43
|
+
JOIN files f ON files_fts.rowid = f.id
|
|
44
|
+
WHERE files_fts MATCH :query
|
|
45
|
+
ORDER BY rank
|
|
46
|
+
LIMIT :limit
|
|
47
|
+
`;
|
|
48
|
+
const results = db.prepare(sql).all({ query: safeQuery, limit });
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
export function cosineSimilarity(a, b) {
|
|
52
|
+
const dot = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
|
|
53
|
+
const magA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
|
|
54
|
+
const magB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
|
|
55
|
+
return dot / (magA * magB);
|
|
56
|
+
}
|
|
57
|
+
export async function searchFiles(query, topK = 5) {
|
|
58
|
+
// Generate the query embedding
|
|
59
|
+
const embedding = await generateEmbedding(query);
|
|
60
|
+
if (!embedding)
|
|
61
|
+
return [];
|
|
62
|
+
// Sanitize the query by removing or escaping special characters
|
|
63
|
+
const safeQuery = query
|
|
64
|
+
.trim()
|
|
65
|
+
.split(/\s+/)
|
|
66
|
+
.map(token => {
|
|
67
|
+
token = token
|
|
68
|
+
.replace(/[?*\\"]/g, '') // Remove question marks, asterisks, backslashes, and double quotes
|
|
69
|
+
.replace(/'/g, "''"); // Escape single quotes for SQL safety
|
|
70
|
+
// For multi-word queries, wrap the token in quotes for exact phrase matching
|
|
71
|
+
if (token.includes(' ')) {
|
|
72
|
+
return `"${token}"`; // Exact phrase match for multi-word tokens
|
|
73
|
+
}
|
|
74
|
+
return `${token}*`; // Prefix match for single tokens
|
|
75
|
+
})
|
|
76
|
+
.join(' OR ');
|
|
77
|
+
// Log the constructed query for debugging purposes
|
|
78
|
+
console.log(`Executing search query: ${safeQuery}`);
|
|
79
|
+
// Fetch BM25 scores from the FTS using the safeQuery string directly
|
|
80
|
+
const ftsResults = db.prepare(sqlTemplates.fetchBm25ScoresTemplate).all({ query: safeQuery });
|
|
81
|
+
const bm25Min = Math.min(...ftsResults.map(r => r.bm25Score));
|
|
82
|
+
const bm25Max = Math.max(...ftsResults.map(r => r.bm25Score));
|
|
83
|
+
// Calculate final score combining BM25 and cosine similarity
|
|
84
|
+
const scored = ftsResults.map(result => {
|
|
85
|
+
try {
|
|
86
|
+
// Fetch embedding for each file from the `files` table
|
|
87
|
+
const embResult = db.prepare(sqlTemplates.fetchEmbeddingTemplate).get({ path: result.path });
|
|
88
|
+
// Check if embedding exists and has the correct structure
|
|
89
|
+
if (!embResult || typeof embResult.embedding !== 'string')
|
|
90
|
+
return null;
|
|
91
|
+
// Parse the embedding
|
|
92
|
+
const vector = JSON.parse(embResult.embedding);
|
|
93
|
+
const sim = cosineSimilarity(embedding, vector);
|
|
94
|
+
// Normalize BM25 scores
|
|
95
|
+
const normalizedBm25 = 1 - ((result.bm25Score - bm25Min) / (bm25Max - bm25Min + 1e-5));
|
|
96
|
+
const normalizedSim = sim; // cosineSimilarity is already 0ā1
|
|
97
|
+
const finalScore = 0.7 * normalizedSim + 0.3 * normalizedBm25;
|
|
98
|
+
return {
|
|
99
|
+
path: result.path,
|
|
100
|
+
summary: result.summary,
|
|
101
|
+
score: finalScore,
|
|
102
|
+
sim: normalizedSim,
|
|
103
|
+
bm25: normalizedBm25
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error(`Error processing embedding for file: ${result.path}`, err);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}).filter(Boolean)
|
|
111
|
+
.sort((a, b) => b.score - a.score)
|
|
112
|
+
.slice(0, topK);
|
|
113
|
+
return scored;
|
|
18
114
|
}
|
package/dist/db/schema.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { db } from
|
|
1
|
+
import { db } from "./client.js";
|
|
2
2
|
export function initSchema() {
|
|
3
3
|
db.exec(`
|
|
4
4
|
CREATE TABLE IF NOT EXISTS files (
|
|
5
5
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
6
|
path TEXT UNIQUE,
|
|
7
7
|
summary TEXT,
|
|
8
|
-
|
|
9
|
-
indexed_at TEXT
|
|
8
|
+
type TEXT,
|
|
9
|
+
indexed_at TEXT,
|
|
10
|
+
last_modified TEXT,
|
|
11
|
+
embedding TEXT
|
|
10
12
|
);
|
|
13
|
+
|
|
14
|
+
-- FTS5 table for fast fullātext search of summaries and paths
|
|
15
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS files_fts
|
|
16
|
+
USING fts5(path, summary, content='');
|
|
11
17
|
`);
|
|
12
18
|
console.log('ā
SQLite schema initialized');
|
|
13
19
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Template for inserting or ignoring new file entries
|
|
2
|
+
export const insertFileTemplate = `
|
|
3
|
+
INSERT OR IGNORE INTO files
|
|
4
|
+
(path, summary, type, indexed_at, last_modified)
|
|
5
|
+
VALUES (:path, :summary, :type, datetime('now'), :lastModified)
|
|
6
|
+
`;
|
|
7
|
+
// Template for updating file metadata if it has changed
|
|
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
|
|
27
|
+
export const fetchBm25ScoresTemplate = `
|
|
28
|
+
SELECT f.path, f.summary, f.type, bm25(files_fts) AS bm25Score
|
|
29
|
+
FROM files_fts
|
|
30
|
+
JOIN files f ON files_fts.rowid = f.id
|
|
31
|
+
WHERE files_fts MATCH :query
|
|
32
|
+
LIMIT 50
|
|
33
|
+
`;
|
|
34
|
+
// Template for fetching embedding for a specific file
|
|
35
|
+
export const fetchEmbeddingTemplate = `
|
|
36
|
+
SELECT embedding FROM files WHERE path = :path
|
|
37
|
+
`;
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,10 @@ import { handleChangelogUpdate } from './commands/ChangeLogUpdateCmd.js';
|
|
|
17
17
|
import { runModulePipelineFromCLI } from './commands/ModulePipelineCmd.js';
|
|
18
18
|
import { runIndexCommand } from './commands/IndexCmd.js';
|
|
19
19
|
import { resetDatabase } from './commands/ResetDbCmd.js';
|
|
20
|
+
import { runQueryCommand } from './commands/QueryCmd.js';
|
|
21
|
+
import { runDaemonBatch } from './commands/DaemonCmd.js';
|
|
22
|
+
import { runStopDaemonCommand } from "./commands/StopDaemonCmd.js";
|
|
23
|
+
import { runAskCommand } from './commands/AskCmd.js';
|
|
20
24
|
// Create the CLI instance
|
|
21
25
|
const cmd = new Command('scai')
|
|
22
26
|
.version(version)
|
|
@@ -68,16 +72,39 @@ cmd
|
|
|
68
72
|
.action(() => {
|
|
69
73
|
ModelConfig.logCurrentConfig();
|
|
70
74
|
});
|
|
75
|
+
cmd
|
|
76
|
+
.command('daemon')
|
|
77
|
+
.description('Run background summarization of indexed files')
|
|
78
|
+
.action(runDaemonBatch);
|
|
79
|
+
cmd
|
|
80
|
+
.command('stop-daemon')
|
|
81
|
+
.description('Stop the background summarizer daemon')
|
|
82
|
+
.action(runStopDaemonCommand);
|
|
71
83
|
cmd
|
|
72
84
|
.command('index [targetDir]')
|
|
73
85
|
.description('Index supported files in the given directory (or current folder if none)')
|
|
74
|
-
.
|
|
75
|
-
|
|
86
|
+
.option('-d, --detached', 'Run summarizer daemon after indexing')
|
|
87
|
+
.action((targetDir, options) => {
|
|
88
|
+
runIndexCommand(targetDir, { detached: options.detached });
|
|
89
|
+
});
|
|
90
|
+
cmd
|
|
91
|
+
.command('query <query>')
|
|
92
|
+
.description('Search indexed files by keyword')
|
|
93
|
+
.action(runQueryCommand);
|
|
94
|
+
// Command structure using Commander
|
|
95
|
+
cmd
|
|
96
|
+
.command('ask')
|
|
97
|
+
.description('Ask a question using file summaries and a local model')
|
|
98
|
+
.argument('<question...>', 'The question to ask')
|
|
99
|
+
.action((question) => {
|
|
100
|
+
const q = question.join(' ');
|
|
101
|
+
runAskCommand(q); // No model option, just pass the question
|
|
76
102
|
});
|
|
77
103
|
cmd
|
|
78
104
|
.command('reset-db')
|
|
79
105
|
.description('Delete and reset the SQLite database')
|
|
80
106
|
.action(() => resetDatabase());
|
|
107
|
+
// Default
|
|
81
108
|
cmd
|
|
82
109
|
.arguments('<file>')
|
|
83
110
|
.option('-m, --modules <modules>', 'Comma-separated list of modules to run (e.g., comments,cleanup,summary)')
|
package/dist/lib/generate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
// File: lib/generate.ts
|
|
1
2
|
import ora from 'ora';
|
|
2
|
-
export async function generate(
|
|
3
|
+
export async function generate(input, model) {
|
|
3
4
|
const spinner = ora(`š§ Thinking with ${model}...`).start();
|
|
4
5
|
try {
|
|
5
6
|
const res = await fetch('http://localhost:11434/api/generate', {
|
|
@@ -7,16 +8,21 @@ export async function generate(prompt, model) {
|
|
|
7
8
|
headers: { 'Content-Type': 'application/json' },
|
|
8
9
|
body: JSON.stringify({
|
|
9
10
|
model,
|
|
10
|
-
prompt,
|
|
11
|
+
prompt: input.content,
|
|
11
12
|
stream: false,
|
|
12
13
|
}),
|
|
13
14
|
});
|
|
14
15
|
const data = await res.json();
|
|
15
16
|
spinner.succeed('ā
Model response received.');
|
|
16
|
-
|
|
17
|
+
process.stdout.write('\n'); // ā
Prevents terminal suppression bug
|
|
18
|
+
return {
|
|
19
|
+
content: data.response?.trim() ?? '',
|
|
20
|
+
filepath: input.filepath,
|
|
21
|
+
};
|
|
17
22
|
}
|
|
18
23
|
catch (err) {
|
|
19
24
|
spinner.fail('ā Model request failed.');
|
|
25
|
+
process.stdout.write('\n'); // In case of error flush output too
|
|
20
26
|
throw err;
|
|
21
27
|
}
|
|
22
28
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function generateEmbedding(text) {
|
|
2
|
+
try {
|
|
3
|
+
const res = await fetch('http://localhost:11434/api/embeddings', {
|
|
4
|
+
method: 'POST',
|
|
5
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6
|
+
body: JSON.stringify({
|
|
7
|
+
model: 'mistral', // or 'llama3' ā whatever works best
|
|
8
|
+
prompt: text,
|
|
9
|
+
}),
|
|
10
|
+
});
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
console.error('ā Failed to generate embedding:', await res.text());
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const data = await res.json();
|
|
16
|
+
return data.embedding;
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.error('ā Embedding error:', err instanceof Error ? err.message : err);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -9,13 +9,27 @@ export const changelogModule = {
|
|
|
9
9
|
You're an experienced changelog writer. Based on this Git diff, write a markdown bullet-point entry suitable for CHANGELOG.md:
|
|
10
10
|
|
|
11
11
|
--- DIFF START ---
|
|
12
|
-
${input.
|
|
12
|
+
${input.content}
|
|
13
13
|
--- DIFF END ---
|
|
14
14
|
|
|
15
15
|
ā
If the changes are significant, return a changelog entry.
|
|
16
16
|
ā If not, return ONLY: "NO UPDATE".
|
|
17
17
|
`.trim();
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const response = await generate({ content: prompt }, model);
|
|
19
|
+
const summary = response?.summary?.trim();
|
|
20
|
+
if (!summary || summary === 'NO UPDATE') {
|
|
21
|
+
// Return an empty summary and empty suggestions if there is no update.
|
|
22
|
+
return { content: response.content,
|
|
23
|
+
summary,
|
|
24
|
+
suggestions: response?.suggestions ?? [],
|
|
25
|
+
filepath: input.filepath };
|
|
26
|
+
}
|
|
27
|
+
// Return the actual changelog summary and any suggestions
|
|
28
|
+
return {
|
|
29
|
+
content: response.content,
|
|
30
|
+
summary,
|
|
31
|
+
suggestions: response?.suggestions ?? [],
|
|
32
|
+
filepath: input.filepath,
|
|
33
|
+
};
|
|
20
34
|
},
|
|
21
35
|
};
|
|
@@ -15,8 +15,8 @@ function isNaturalLanguageNoise(line) {
|
|
|
15
15
|
export const cleanupModule = {
|
|
16
16
|
name: 'cleanup',
|
|
17
17
|
description: 'Remove markdown fences and natural language noise from top/bottom of code',
|
|
18
|
-
async run({
|
|
19
|
-
let lines =
|
|
18
|
+
async run({ content }) {
|
|
19
|
+
let lines = content.trim().split('\n');
|
|
20
20
|
// āāāāā Clean top āāāāā
|
|
21
21
|
while (lines.length) {
|
|
22
22
|
const line = lines[0].trim();
|
|
@@ -37,6 +37,6 @@ export const cleanupModule = {
|
|
|
37
37
|
break;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
return {
|
|
40
|
+
return { content: lines.join('\n').trim() };
|
|
41
41
|
}
|
|
42
42
|
};
|
|
@@ -19,10 +19,10 @@ Your task is to add clear and insightful single-line comments to the code.
|
|
|
19
19
|
- The code should be valid ${lang.toUpperCase()} after your changes.
|
|
20
20
|
|
|
21
21
|
--- CODE START ---
|
|
22
|
-
${input.
|
|
22
|
+
${input.content}
|
|
23
23
|
--- CODE END ---
|
|
24
24
|
`.trim();
|
|
25
|
-
const
|
|
26
|
-
return {
|
|
25
|
+
const response = await generate({ content: prompt }, model);
|
|
26
|
+
return { content: response.content === 'NO UPDATE' ? '' : response.content };
|
|
27
27
|
},
|
|
28
28
|
};
|
|
@@ -3,7 +3,7 @@ import { ModelConfig } from '../../config/ModelConfig.js';
|
|
|
3
3
|
export const commitSuggesterModule = {
|
|
4
4
|
name: 'commitSuggester',
|
|
5
5
|
description: 'Suggests conventional commit messages from Git diff',
|
|
6
|
-
async run({
|
|
6
|
+
async run({ content }) {
|
|
7
7
|
const model = ModelConfig.getModel();
|
|
8
8
|
const prompt = `
|
|
9
9
|
Suggest ALWAYS 3 concise, conventional Git commit messages based on the input code diff.
|
|
@@ -14,16 +14,16 @@ Use this format ONLY:
|
|
|
14
14
|
3. refactor: ...
|
|
15
15
|
|
|
16
16
|
Here is the diff:
|
|
17
|
-
${
|
|
17
|
+
${content}
|
|
18
18
|
`.trim();
|
|
19
|
-
const
|
|
20
|
-
const lines =
|
|
19
|
+
const response = await generate({ content: prompt }, model);
|
|
20
|
+
const lines = response.content
|
|
21
21
|
.split('\n')
|
|
22
22
|
.map(line => line.trim())
|
|
23
23
|
.filter(line => /^\d+\.\s+/.test(line));
|
|
24
24
|
const suggestions = lines.map(line => line.replace(/^\d+\.\s+/, '').replace(/^"(.*)"$/, '$1').trim());
|
|
25
25
|
return {
|
|
26
|
-
|
|
26
|
+
content,
|
|
27
27
|
suggestions
|
|
28
28
|
};
|
|
29
29
|
}
|
|
@@ -6,7 +6,7 @@ import { generate } from '../../lib/generate.js';
|
|
|
6
6
|
export const generateTestsModule = {
|
|
7
7
|
name: 'generateTests',
|
|
8
8
|
description: 'Generate a Jest test file for the class/module',
|
|
9
|
-
async run({
|
|
9
|
+
async run({ content, filepath }) {
|
|
10
10
|
const model = ModelConfig.getModel();
|
|
11
11
|
const lang = ModelConfig.getLanguage();
|
|
12
12
|
if (!filepath)
|
|
@@ -21,16 +21,16 @@ Guidelines:
|
|
|
21
21
|
- Only return valid TypeScript code
|
|
22
22
|
|
|
23
23
|
--- CODE START ---
|
|
24
|
-
${
|
|
24
|
+
${content}
|
|
25
25
|
--- CODE END ---
|
|
26
26
|
`.trim();
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
27
|
+
const response = await generate({ content: prompt }, model);
|
|
28
|
+
if (!response)
|
|
29
29
|
throw new Error('ā ļø No test code returned from model');
|
|
30
30
|
const { dir, name } = path.parse(filepath);
|
|
31
31
|
const testPath = path.join(dir, `${name}.test.ts`);
|
|
32
|
-
await fs.writeFile(testPath,
|
|
32
|
+
await fs.writeFile(testPath, response.content, 'utf-8');
|
|
33
33
|
console.log(`ā
Test file saved to: ${testPath}`);
|
|
34
|
-
return {
|
|
34
|
+
return { content, filepath }; // unchanged input
|
|
35
35
|
}
|
|
36
36
|
};
|
|
@@ -16,13 +16,13 @@ Refactor the following code:
|
|
|
16
16
|
- Output the full, valid ${lang.toUpperCase()} code
|
|
17
17
|
|
|
18
18
|
--- CODE START ---
|
|
19
|
-
${input.
|
|
19
|
+
${input.content}
|
|
20
20
|
--- CODE END ---
|
|
21
21
|
`.trim();
|
|
22
|
-
const response = await generate(prompt, model);
|
|
22
|
+
const response = await generate({ content: prompt }, model);
|
|
23
23
|
if (!response) {
|
|
24
24
|
throw new Error('ā Model returned empty response for refactoring.');
|
|
25
25
|
}
|
|
26
|
-
return {
|
|
26
|
+
return { content: response.content };
|
|
27
27
|
}
|
|
28
28
|
};
|
|
@@ -1,64 +1,43 @@
|
|
|
1
1
|
import { ModelConfig } from '../../config/ModelConfig.js';
|
|
2
2
|
import { generate } from '../../lib/generate.js';
|
|
3
|
-
import fs from 'fs/promises';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
export const summaryModule = {
|
|
6
5
|
name: 'summary',
|
|
7
|
-
description: '
|
|
8
|
-
async
|
|
6
|
+
description: 'Generates a general summary of any file content.',
|
|
7
|
+
run: async ({ content, filepath }) => {
|
|
9
8
|
const model = ModelConfig.getModel();
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
try {
|
|
14
|
-
const raw = await fs.readFile('./.scai/context.flat.json', 'utf-8');
|
|
15
|
-
const flatContext = JSON.parse(raw);
|
|
16
|
-
if (filepath) {
|
|
17
|
-
const dir = path.dirname(filepath).replace(/\\/g, '/'); // Normalize slashes
|
|
18
|
-
console.log("Dir: ", dir);
|
|
19
|
-
const contextSubset = Object.entries(flatContext)
|
|
20
|
-
.filter(([file]) => file.startsWith(dir))
|
|
21
|
-
.slice(0, 10); // limit if needed
|
|
22
|
-
if (contextSubset.length) {
|
|
23
|
-
contextString = 'š Local Context:\n' + contextSubset
|
|
24
|
-
.map(([file, summary]) => `- ${file}: ${summary}`)
|
|
25
|
-
.join('\n');
|
|
26
|
-
console.log("Context string input to prompt: ", contextString);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
console.warn('ā ļø Context file not found or failed to parse.');
|
|
32
|
-
}
|
|
9
|
+
const ext = filepath ? path.extname(filepath).toLowerCase() : '';
|
|
10
|
+
const filename = filepath ? path.basename(filepath) : '';
|
|
11
|
+
// More neutral prompt for general-purpose content
|
|
33
12
|
const prompt = `
|
|
34
|
-
You are
|
|
35
|
-
|
|
36
|
-
Project Overview:
|
|
37
|
-
${contextString ? contextString + '\n\n' : ''}
|
|
13
|
+
You are an assistant specialized in summarizing files.
|
|
38
14
|
|
|
39
|
-
|
|
15
|
+
Your task is to summarize the contents of the following file as clearly and concisely as possible.
|
|
40
16
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// - [Main features or components]
|
|
44
|
-
// - [Any interesting logic or patterns]
|
|
17
|
+
File: ${filename}
|
|
18
|
+
Extension: ${ext}
|
|
45
19
|
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
20
|
+
š Instructions:
|
|
21
|
+
- Identify the main topic and purpose of the file
|
|
22
|
+
- Summarize key content and sections
|
|
23
|
+
- Mention any technical, legal, or structural info if relevant
|
|
24
|
+
- Do NOT include the raw content or repeat lines from it
|
|
25
|
+
- Return a human-readable bullet-point summary
|
|
49
26
|
|
|
50
|
-
---
|
|
51
|
-
${
|
|
52
|
-
---
|
|
27
|
+
--- FILE CONTENT START ---
|
|
28
|
+
${content}
|
|
29
|
+
--- FILE CONTENT END ---
|
|
53
30
|
`.trim();
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
console.log(
|
|
31
|
+
const response = await generate({ content, filepath }, model);
|
|
32
|
+
if (response.content) {
|
|
33
|
+
response.summary = response.content;
|
|
34
|
+
console.log('\nš Summary:\n');
|
|
35
|
+
console.log(response.summary);
|
|
58
36
|
}
|
|
59
37
|
else {
|
|
60
38
|
console.warn('ā ļø No summary generated.');
|
|
39
|
+
response.summary = 'ā ļø No summary generated.';
|
|
61
40
|
}
|
|
62
|
-
return
|
|
41
|
+
return response;
|
|
63
42
|
}
|
|
64
43
|
};
|
|
@@ -5,12 +5,13 @@ export async function runModulePipeline(modules, input) {
|
|
|
5
5
|
if (isDebug) {
|
|
6
6
|
console.log('Input: ', input);
|
|
7
7
|
}
|
|
8
|
+
let response = { content: '' };
|
|
8
9
|
for (const mod of modules) {
|
|
9
10
|
try {
|
|
10
|
-
|
|
11
|
+
response = await mod.run(current);
|
|
11
12
|
if (isDebug) {
|
|
12
13
|
console.log(`āļø Running: ${mod.name}`);
|
|
13
|
-
console.log("Current: ",
|
|
14
|
+
console.log("Current: ", response.content);
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
catch (error) {
|
|
@@ -19,5 +20,5 @@ export async function runModulePipeline(modules, input) {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
// Return the output, assuming 'code' holds the relevant transformed content
|
|
22
|
-
return
|
|
23
|
+
return response; // Ensure the return type matches PromptOutput
|
|
23
24
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
export function detectFileType(filepath) {
|
|
3
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
4
|
+
const map = {
|
|
5
|
+
// Programming languages
|
|
6
|
+
'.ts': 'typescript',
|
|
7
|
+
'.tsx': 'typescript',
|
|
8
|
+
'.js': 'javascript',
|
|
9
|
+
'.jsx': 'javascript',
|
|
10
|
+
'.java': 'java',
|
|
11
|
+
'.py': 'python',
|
|
12
|
+
'.rb': 'ruby',
|
|
13
|
+
'.php': 'php',
|
|
14
|
+
'.go': 'go',
|
|
15
|
+
'.rs': 'rust',
|
|
16
|
+
'.c': 'c',
|
|
17
|
+
'.cpp': 'cpp',
|
|
18
|
+
'.cs': 'csharp',
|
|
19
|
+
'.swift': 'swift',
|
|
20
|
+
'.kt': 'kotlin',
|
|
21
|
+
'.scala': 'scala',
|
|
22
|
+
// Markup & docs
|
|
23
|
+
'.md': 'markdown',
|
|
24
|
+
'.html': 'html',
|
|
25
|
+
'.htm': 'html',
|
|
26
|
+
'.xml': 'xml',
|
|
27
|
+
'.json': 'json',
|
|
28
|
+
'.yaml': 'yaml',
|
|
29
|
+
'.yml': 'yaml',
|
|
30
|
+
// Configs
|
|
31
|
+
'.ini': 'config',
|
|
32
|
+
'.toml': 'config',
|
|
33
|
+
'.env': 'config',
|
|
34
|
+
// Data
|
|
35
|
+
'.sql': 'sql',
|
|
36
|
+
'.csv': 'csv',
|
|
37
|
+
'.tsv': 'tsv',
|
|
38
|
+
// Text & writing
|
|
39
|
+
'.txt': 'text',
|
|
40
|
+
'.log': 'log',
|
|
41
|
+
'.rst': 'text',
|
|
42
|
+
// Office
|
|
43
|
+
'.doc': 'word',
|
|
44
|
+
'.docx': 'word',
|
|
45
|
+
'.pdf': 'pdf',
|
|
46
|
+
'.ppt': 'powerpoint',
|
|
47
|
+
'.pptx': 'powerpoint',
|
|
48
|
+
'.xls': 'excel',
|
|
49
|
+
'.xlsx': 'excel',
|
|
50
|
+
};
|
|
51
|
+
return map[ext] || ext.replace('.', '') || 'unknown';
|
|
52
|
+
}
|
package/package.json
CHANGED
package/dist/utils/getSummary.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// src/utils/getSummary.ts
|
|
2
|
-
import path from 'path';
|
|
3
|
-
export function getSummary(filename, lang) {
|
|
4
|
-
const base = path.basename(filename).toLowerCase();
|
|
5
|
-
const ext = path.extname(base);
|
|
6
|
-
if (base === 'package.json')
|
|
7
|
-
return 'Defines project metadata and dependencies.';
|
|
8
|
-
if (base === 'tsconfig.json')
|
|
9
|
-
return 'TypeScript compiler settings.';
|
|
10
|
-
if (base === 'pyproject.toml')
|
|
11
|
-
return 'Python build and dependency configuration.';
|
|
12
|
-
if (base === 'Cargo.toml')
|
|
13
|
-
return 'Rust project configuration.';
|
|
14
|
-
if (base === 'pom.xml')
|
|
15
|
-
return 'Maven config for a Java project.';
|
|
16
|
-
if (base === 'README.md')
|
|
17
|
-
return 'Project documentation.';
|
|
18
|
-
if (base.startsWith('index'))
|
|
19
|
-
return 'Entry point module.';
|
|
20
|
-
if (lang === 'ts' || lang === 'js') {
|
|
21
|
-
if (base.includes('service'))
|
|
22
|
-
return 'Service logic module.';
|
|
23
|
-
if (base.includes('util'))
|
|
24
|
-
return 'Utility/helper module.';
|
|
25
|
-
if (base.includes('controller'))
|
|
26
|
-
return 'Handles request/response logic.';
|
|
27
|
-
if (base.includes('router'))
|
|
28
|
-
return 'Routing definitions.';
|
|
29
|
-
}
|
|
30
|
-
return `Generic ${ext.replace('.', '')} file.`;
|
|
31
|
-
}
|