scai 0.1.19 → 0.1.21
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/README.md +108 -83
- package/dist/commands/AskCmd.js +66 -0
- package/dist/commands/DaemonCmd.js +27 -5
- package/dist/commands/IndexCmd.js +26 -4
- package/dist/commands/QueryCmd.js +2 -2
- package/dist/config/IgnoredExtensions.js +30 -9
- package/dist/config/IgnoredPaths.js +24 -0
- package/dist/config.js +68 -0
- package/dist/constants.js +19 -0
- package/dist/db/client.js +2 -4
- package/dist/db/fileIndex.js +96 -33
- package/dist/db/schema.js +2 -1
- package/dist/db/sqlTemplates.js +37 -0
- package/dist/index.js +75 -33
- package/dist/lib/generateEmbedding.js +22 -0
- package/dist/pipeline/modules/changeLogModule.js +2 -2
- package/dist/pipeline/modules/commentModule.js +3 -3
- package/dist/pipeline/modules/commitSuggesterModule.js +2 -2
- package/dist/pipeline/modules/generateTestsModule.js +3 -3
- package/dist/pipeline/modules/refactorModule.js +3 -3
- package/dist/pipeline/modules/summaryModule.js +2 -3
- package/package.json +1 -1
- package/dist/config/ModelConfig.js +0 -23
- /package/dist/{pipeline/types.js → types.js} +0 -0
package/README.md
CHANGED
|
@@ -1,150 +1,175 @@
|
|
|
1
1
|
# ⚙️ scai — Smart Commit AI ✨
|
|
2
2
|
|
|
3
|
-
> AI-powered CLI
|
|
3
|
+
> AI-powered CLI tool for smart commit messages, code comments, summaries, test generation, and changelogs — all powered by local models.
|
|
4
4
|
|
|
5
|
-
**scai** (Smart Commit AI) is a lightweight, privacy-focused CLI tool that
|
|
5
|
+
**scai** (Smart Commit AI) is a lightweight, privacy-focused CLI tool that helps developers work faster and cleaner:
|
|
6
6
|
|
|
7
7
|
- 💬 Suggest high-quality Git commit messages
|
|
8
8
|
- ✨ Automatically comment your code
|
|
9
9
|
- 🧠 Summarize code files instantly in the terminal
|
|
10
|
+
- 🧪 Generate test files for your code (JavaScript/TypeScript)
|
|
10
11
|
- 📝 Generate changelog entries based on Git diffs
|
|
11
|
-
- 🔒 100% local — no API keys, no cloud, no telemetry
|
|
12
|
+
- 🔒 100% local — no API keys, no cloud, no telemetry
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
15
16
|
## 🚀 Features
|
|
16
17
|
|
|
17
|
-
- ⚡️ Powered by local
|
|
18
|
-
- 🛠️
|
|
19
|
-
- 🔒 No external services
|
|
20
|
-
- ✅ Global options
|
|
18
|
+
- ⚡️ Powered by local AI models (e.g., `llama3`, `codellama`)
|
|
19
|
+
- 🛠️ Built with Node.js and TypeScript
|
|
20
|
+
- 🔒 No external services or APIs required
|
|
21
|
+
- ✅ Global options to select model and programming language
|
|
21
22
|
|
|
22
23
|
---
|
|
23
24
|
|
|
24
25
|
## ❤️ Why Local AI?
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
At scai, we believe your code — and your workflow — should stay **yours**. That's why we prioritize **local models** and **privacy-first** development:
|
|
27
28
|
|
|
28
|
-
✅
|
|
29
|
-
✅
|
|
30
|
-
✅
|
|
31
|
-
|
|
29
|
+
- ✅ **Local models**: scai runs entirely on your machine, with no need for cloud-based processing. We use open-source models like `llama3` and `codellama`, ensuring that your code stays private and secure.
|
|
30
|
+
- ✅ **Privacy by design**: We do not send your code to the cloud, store your data, or require API keys. Your data remains 100% local, ensuring full control over your project.
|
|
31
|
+
- ✅ **EU & Danish friendly**: As a Danish company, we are deeply committed to respecting European privacy regulations (GDPR). This means no data collection, no tracking, and full compliance with the strictest data protection laws.
|
|
32
|
+
|
|
33
|
+
By running everything locally, you can be sure that your sensitive code and workflows stay safe, private, and within your control.
|
|
34
|
+
|
|
35
|
+
---
|
|
32
36
|
|
|
33
37
|
## 📦 Installation
|
|
34
38
|
|
|
35
|
-
1. **Install
|
|
39
|
+
1. **Install Ollama:**
|
|
36
40
|
- On **Windows**: [Download Ollama](https://ollama.com/download)
|
|
37
|
-
- On **macOS**:
|
|
38
|
-
- Via Homebrew:
|
|
39
|
-
```bash
|
|
40
|
-
brew install ollama
|
|
41
|
-
```
|
|
42
|
-
- Or download directly from the website
|
|
41
|
+
- On **macOS**: via Homebrew (`brew install ollama`) or [download from Ollama](https://ollama.com)
|
|
43
42
|
|
|
44
|
-
2. **Install scai globally:**
|
|
43
|
+
2. **Install scai globally:**
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g scai
|
|
46
|
+
```
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
3. **Initialize the tool and models:**
|
|
49
|
+
```bash
|
|
50
|
+
scai init
|
|
51
|
+
```
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
This will download required models and set up scai.
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
scai init
|
|
54
|
-
```
|
|
55
|
+
---
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
- Start the Ollama background server
|
|
58
|
-
- Download required models (`llama3`, etc.)
|
|
57
|
+
## ⚒️ Usage Examples
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
### 🔧 Git Commands
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
- **Check Git status (better than `git status`):**
|
|
62
|
+
```bash
|
|
63
|
+
scai git status
|
|
64
|
+
```
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
git add .
|
|
66
|
-
scai sugg
|
|
67
|
-
```
|
|
66
|
+
This is an enhanced version of `git status`. It does more than just show the status of your working directory:
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
```
|
|
68
|
+
- **Check if your working directory is clean**: It tells you if there are any uncommitted changes.
|
|
69
|
+
- **Check your current branch**: It displays your current branch name.
|
|
70
|
+
- **Check if you're up to date with `origin/main`**: Unlike `git status`, which only shows local status, scai ensures you know if your branch is behind or ahead of the `origin/main` branch. It compares your local commit hash with the remote `origin/main` hash, giving you more comprehensive information.
|
|
73
71
|
|
|
74
|
-
|
|
72
|
+
Example output:
|
|
73
|
+
```
|
|
74
|
+
✅ Git working directory is clean
|
|
75
|
+
✅ Up to date with origin/main
|
|
76
|
+
```
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
If you're behind `origin/main`, it will show:
|
|
79
|
+
```
|
|
80
|
+
🔄 Branch <your-branch-name> is not up to date with origin/main
|
|
81
|
+
```
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
- **Suggest a commit message:**
|
|
84
|
+
```bash
|
|
85
|
+
git add .
|
|
86
|
+
scai git sugg
|
|
87
|
+
```
|
|
81
88
|
|
|
82
|
-
|
|
89
|
+
*Example output:*
|
|
90
|
+
```
|
|
91
|
+
feat(api): add error handling to user service
|
|
92
|
+
```
|
|
83
93
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
To commit automatically with the suggestion:
|
|
95
|
+
```bash
|
|
96
|
+
scai git sugg --commit
|
|
97
|
+
```
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
- `-a, --apply` — Overwrite the original file with the commented version
|
|
99
|
+
### 🛠️ Generate Code-Related Output (`gen` commands)
|
|
90
100
|
|
|
91
|
-
|
|
101
|
+
- **Comment a code file:**
|
|
102
|
+
```bash
|
|
103
|
+
scai gen comm <file>
|
|
104
|
+
```
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
Adds clear, helpful comments to the code. Optional flag:
|
|
107
|
+
- `-a, --apply` — Overwrite the original file with the commented version.
|
|
94
108
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
- **Summarize a code file:**
|
|
110
|
+
```bash
|
|
111
|
+
scai gen summ <file>
|
|
112
|
+
```
|
|
98
113
|
|
|
99
|
-
Prints a summary of what the code does directly
|
|
114
|
+
Prints a summary of what the code does directly in the terminal. You can also pipe file content:
|
|
115
|
+
```bash
|
|
116
|
+
cat <file> | scai gen summ
|
|
117
|
+
```
|
|
100
118
|
|
|
101
|
-
|
|
119
|
+
- **Generate tests for a code file:**
|
|
120
|
+
```bash
|
|
121
|
+
scai gen tests <file>
|
|
122
|
+
```
|
|
102
123
|
|
|
103
|
-
|
|
104
|
-
cat <file> | scai summ
|
|
105
|
-
```
|
|
124
|
+
Creates a Jest test file for the specified JavaScript/TypeScript module.
|
|
106
125
|
|
|
107
|
-
|
|
126
|
+
- **Update the changelog:**
|
|
127
|
+
```bash
|
|
128
|
+
scai gen changelog
|
|
129
|
+
```
|
|
108
130
|
|
|
109
|
-
|
|
131
|
+
Analyzes the current Git diff and updates (or creates) a `CHANGELOG.md` file with relevant changes.
|
|
110
132
|
|
|
111
|
-
|
|
112
|
-
scai changelog
|
|
113
|
-
```
|
|
133
|
+
## ⚙️ Configuration
|
|
114
134
|
|
|
115
|
-
|
|
135
|
+
scai stores your configuration locally in the `~/.scai/config.json` file. You can configure the model and programming language settings as follows:
|
|
116
136
|
|
|
117
|
-
|
|
137
|
+
- **Set the model:**
|
|
138
|
+
```bash
|
|
139
|
+
scai set model <model>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
e.g., `scai set model codellama:7b`
|
|
143
|
+
|
|
144
|
+
- **Set the programming language:**
|
|
145
|
+
```bash
|
|
146
|
+
scai set lang <lang>
|
|
147
|
+
```
|
|
118
148
|
|
|
119
|
-
|
|
149
|
+
e.g., `scai set lang rust`
|
|
120
150
|
|
|
121
|
-
|
|
151
|
+
- **Show current configuration:**
|
|
152
|
+
```bash
|
|
153
|
+
scai config
|
|
154
|
+
```
|
|
122
155
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
156
|
+
You can also use global options `--model` and `--lang` with any command to override the settings:
|
|
157
|
+
```bash
|
|
158
|
+
scai --model codellama:34b --lang ts git sugg
|
|
159
|
+
```
|
|
127
160
|
|
|
128
|
-
|
|
129
|
-
scai <file> --modules comment,summary > test.txt
|
|
130
|
-
```
|
|
131
|
-
Or to both stdout and a file with tee fx.
|
|
132
|
-
```bash
|
|
133
|
-
scai <file> --modules comment,summary | tee test.txt
|
|
134
|
-
```
|
|
161
|
+
---
|
|
135
162
|
|
|
136
163
|
## 🔐 License & Fair Use
|
|
137
164
|
|
|
138
165
|
**scai is free to use** for individuals, teams, and companies — including in commercial work.
|
|
139
166
|
|
|
140
167
|
You may:
|
|
141
|
-
|
|
142
168
|
- ✅ Use it internally in your projects
|
|
143
169
|
- ✅ Use it at work or in commercial development
|
|
144
170
|
- ✅ Share and recommend it
|
|
145
171
|
|
|
146
172
|
But you may not:
|
|
147
|
-
|
|
148
173
|
- ❌ Repackage or resell **scai** as a product or SaaS
|
|
149
174
|
- ❌ Claim ownership of the tool
|
|
150
175
|
|
|
@@ -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
|
+
}
|
|
@@ -4,10 +4,17 @@ import fs from 'fs/promises';
|
|
|
4
4
|
import fsSync from 'fs';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import path from 'path';
|
|
7
|
-
|
|
8
|
-
|
|
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;
|
|
9
11
|
const DAEMON_INTERVAL_MINUTES = 30;
|
|
10
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
|
+
};
|
|
11
18
|
export async function runDaemonBatch() {
|
|
12
19
|
console.log('📥 Daemon batch: scanning for files to summarize...');
|
|
13
20
|
const rows = db.prepare(`
|
|
@@ -15,19 +22,34 @@ export async function runDaemonBatch() {
|
|
|
15
22
|
WHERE summary IS NULL OR summary = ''
|
|
16
23
|
ORDER BY last_modified DESC
|
|
17
24
|
LIMIT ?
|
|
18
|
-
`).all(MAX_FILES);
|
|
25
|
+
`).all(MAX_FILES);
|
|
19
26
|
if (rows.length === 0) {
|
|
20
27
|
console.log('✅ No files left to summarize.');
|
|
21
28
|
return;
|
|
22
29
|
}
|
|
23
30
|
for (const row of rows) {
|
|
31
|
+
if (shouldIgnoreFile(row.path)) {
|
|
32
|
+
console.log(`⚠️ Ignored file (unwanted extension): ${row.path}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
24
35
|
try {
|
|
25
36
|
const content = await fs.readFile(row.path, 'utf-8');
|
|
26
37
|
const result = await summaryModule.run({ content, filepath: row.path });
|
|
27
38
|
const summary = result?.summary?.trim() ? result.summary : null;
|
|
28
|
-
|
|
29
|
-
|
|
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 });
|
|
30
51
|
console.log(`📝 Summarized: ${row.path}`);
|
|
52
|
+
console.log(`🔢 Embedded: ${row.path}`);
|
|
31
53
|
}
|
|
32
54
|
catch (err) {
|
|
33
55
|
console.warn(`⚠️ Failed: ${row.path}`, err instanceof Error ? err.message : err);
|
|
@@ -6,15 +6,37 @@ import { shouldIgnoreFile } from '../utils/shouldIgnoreFiles.js';
|
|
|
6
6
|
import { detectFileType } from '../utils/detectFileType.js';
|
|
7
7
|
import { runDaemonScheduler } from './DaemonCmd.js';
|
|
8
8
|
import { IGNORED_FOLDER_GLOBS } from '../config/IgnoredPaths.js';
|
|
9
|
+
import { db } from '../db/client.js';
|
|
9
10
|
const IGNORE = [
|
|
10
11
|
'**/node_modules/**', '**/dist/**', '**/build/**',
|
|
11
12
|
'**/coverage/**', '**/.git/**', '**/*.test.*'
|
|
12
13
|
];
|
|
13
14
|
export async function runIndexCommand(targetDir = process.cwd(), options = {}) {
|
|
14
|
-
|
|
15
|
+
const resolvedDir = path.resolve(targetDir);
|
|
16
|
+
console.log(`📂 Indexing files in: ${resolvedDir}`);
|
|
15
17
|
initSchema();
|
|
18
|
+
// 🧠 Check if another directory has already been indexed
|
|
19
|
+
const indexedPaths = db.prepare(`
|
|
20
|
+
SELECT DISTINCT path FROM files LIMIT 100
|
|
21
|
+
`).all();
|
|
22
|
+
const knownRoot = indexedPaths.length > 0
|
|
23
|
+
? path.dirname(indexedPaths[0].path)
|
|
24
|
+
: null;
|
|
25
|
+
if (knownRoot && !resolvedDir.startsWith(knownRoot) && !options.force) {
|
|
26
|
+
console.warn(`⚠️ You're indexing a different folder than before:
|
|
27
|
+
- Previously: ${knownRoot}
|
|
28
|
+
- Now: ${resolvedDir}
|
|
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.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
16
38
|
const files = await fg('**/*.*', {
|
|
17
|
-
cwd:
|
|
39
|
+
cwd: resolvedDir,
|
|
18
40
|
ignore: IGNORED_FOLDER_GLOBS,
|
|
19
41
|
absolute: true,
|
|
20
42
|
});
|
|
@@ -28,7 +50,7 @@ export async function runIndexCommand(targetDir = process.cwd(), options = {}) {
|
|
|
28
50
|
indexFile(file, null, type); // empty summary for now
|
|
29
51
|
const ext = path.extname(file);
|
|
30
52
|
countByExt[ext] = (countByExt[ext] || 0) + 1;
|
|
31
|
-
console.log(`📄 Indexed: ${path.relative(
|
|
53
|
+
console.log(`📄 Indexed: ${path.relative(resolvedDir, file)}`);
|
|
32
54
|
count++;
|
|
33
55
|
}
|
|
34
56
|
catch (err) {
|
|
@@ -39,6 +61,6 @@ export async function runIndexCommand(targetDir = process.cwd(), options = {}) {
|
|
|
39
61
|
console.log(`✅ Done. Indexed ${count} files.`);
|
|
40
62
|
if (options.detached) {
|
|
41
63
|
console.log('🚀 Starting summarizer daemon in background mode...');
|
|
42
|
-
runDaemonScheduler();
|
|
64
|
+
runDaemonScheduler();
|
|
43
65
|
}
|
|
44
66
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { queryFiles } from '../db/fileIndex.js';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
export async function runQueryCommand(query) {
|
|
4
4
|
if (!query) {
|
|
@@ -6,7 +6,7 @@ export async function runQueryCommand(query) {
|
|
|
6
6
|
return;
|
|
7
7
|
}
|
|
8
8
|
console.log(`🔍 Searching for: "${query}"\n`);
|
|
9
|
-
const results =
|
|
9
|
+
const results = queryFiles(query);
|
|
10
10
|
if (results.length === 0) {
|
|
11
11
|
console.log('⚠️ No matching files found.');
|
|
12
12
|
return;
|
|
@@ -3,23 +3,44 @@ export const IGNORED_EXTENSIONS = [
|
|
|
3
3
|
// 🖼 Media
|
|
4
4
|
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico',
|
|
5
5
|
'.mp4', '.mp3', '.mov', '.avi', '.mkv', '.flv', '.wav', '.flac',
|
|
6
|
+
'.aac', '.m4a', '.wma', '.3gp', '.webm', '.ogg', '.aiff', '.au',
|
|
6
7
|
// 📦 Archives & install packages
|
|
7
8
|
'.zip', '.tar', '.gz', '.bz2', '.xz', '.rar', '.7z',
|
|
8
|
-
'.jar', '.war', '.ear',
|
|
9
|
-
'.
|
|
9
|
+
'.jar', '.war', '.ear', '.deb', '.rpm', '.pkg', '.msi', '.dmg', '.cab', '.apk',
|
|
10
|
+
'.tar.gz', '.tar.bz2', '.tar.xz', '.tar.lzma', '.tar.zst',
|
|
10
11
|
// 🧱 Binaries & executables
|
|
11
12
|
'.exe', '.dll', '.bin', '.so', '.dylib', '.a', '.lib',
|
|
12
|
-
'.iso', '.img', '.elf', '.o', '.obj',
|
|
13
|
+
'.iso', '.img', '.elf', '.o', '.obj', '.msm', '.vbs', '.jscript',
|
|
14
|
+
'.cmd', '.bat', '.ps1', '.sh', '.bash', '.run',
|
|
13
15
|
// 🧪 Runtime / build / cache
|
|
14
|
-
'.log', '.
|
|
16
|
+
'.log', '.tmp', '.map',
|
|
15
17
|
'.db', '.sqlite', '.pkl', '.sav', '.rdb', '.ldb',
|
|
16
|
-
'.pyc', '.class', '.tsbuildinfo', '.coverage',
|
|
18
|
+
'.pyc', '.class', '.tsbuildinfo', '.coverage', '.eslintcache',
|
|
19
|
+
'.yarn', '.webpack', '.babel', '.compilercache',
|
|
17
20
|
// 🔤 Fonts & styles
|
|
18
21
|
'.woff', '.woff2', '.ttf', '.eot', '.otf', '.css.map',
|
|
22
|
+
'.scss', '.sass', '.less', '.styl',
|
|
19
23
|
// 🔐 Certs, keys, credentials
|
|
20
|
-
'.crt', '.key', '.pem', '.pub', '.asc', '.gpg',
|
|
24
|
+
'.crt', '.key', '.pem', '.pub', '.asc', '.gpg', '.p12', '.csr', '.der', '.pfx',
|
|
21
25
|
// ♻️ Backups / temp
|
|
22
|
-
'.bak', '.old', '.swp', '.swo', '.
|
|
23
|
-
|
|
24
|
-
|
|
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',
|
|
25
46
|
];
|
|
@@ -18,4 +18,28 @@ export const IGNORED_FOLDER_GLOBS = [
|
|
|
18
18
|
'**/.output/**',
|
|
19
19
|
'**/tmp/**',
|
|
20
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',
|
|
21
45
|
];
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { CONFIG_PATH, SCAI_HOME, INDEX_DIR } from './constants.js'; // Correctly import INDEX_DIR from constants
|
|
3
|
+
// Default configuration values
|
|
4
|
+
const defaultConfig = {
|
|
5
|
+
model: 'llama3',
|
|
6
|
+
language: 'ts',
|
|
7
|
+
indexDir: INDEX_DIR, // Default index directory from constants
|
|
8
|
+
};
|
|
9
|
+
// Function to ensure the configuration directory exists
|
|
10
|
+
function ensureConfigDir() {
|
|
11
|
+
if (!fs.existsSync(SCAI_HOME)) {
|
|
12
|
+
fs.mkdirSync(SCAI_HOME, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Function to read the configuration file
|
|
16
|
+
function readConfig() {
|
|
17
|
+
try {
|
|
18
|
+
const content = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
19
|
+
return { ...defaultConfig, ...JSON.parse(content) };
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return defaultConfig; // Return default config if read fails
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Function to write the configuration to the config file
|
|
26
|
+
function writeConfig(newConfig) {
|
|
27
|
+
ensureConfigDir();
|
|
28
|
+
const current = readConfig();
|
|
29
|
+
const merged = { ...current, ...newConfig };
|
|
30
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));
|
|
31
|
+
}
|
|
32
|
+
export const Config = {
|
|
33
|
+
// Get the current model from the config
|
|
34
|
+
getModel() {
|
|
35
|
+
return readConfig().model;
|
|
36
|
+
},
|
|
37
|
+
// Set a new model in the config
|
|
38
|
+
setModel(model) {
|
|
39
|
+
writeConfig({ model });
|
|
40
|
+
console.log(`📦 Model set to: ${model}`);
|
|
41
|
+
},
|
|
42
|
+
// Get the current language from the config
|
|
43
|
+
getLanguage() {
|
|
44
|
+
return readConfig().language;
|
|
45
|
+
},
|
|
46
|
+
// Set a new language in the config
|
|
47
|
+
setLanguage(language) {
|
|
48
|
+
writeConfig({ language });
|
|
49
|
+
console.log(`🗣️ Language set to: ${language}`);
|
|
50
|
+
},
|
|
51
|
+
// Get the index directory from the config
|
|
52
|
+
getIndexDir() {
|
|
53
|
+
return readConfig().indexDir;
|
|
54
|
+
},
|
|
55
|
+
// Set a new index directory in the config
|
|
56
|
+
setIndexDir(indexDir) {
|
|
57
|
+
writeConfig({ indexDir });
|
|
58
|
+
console.log(`📁 Index directory set to: ${indexDir}`);
|
|
59
|
+
},
|
|
60
|
+
// Show the current configuration
|
|
61
|
+
show() {
|
|
62
|
+
const cfg = readConfig();
|
|
63
|
+
console.log(`🔧 Current configuration:`);
|
|
64
|
+
console.log(` Model : ${cfg.model}`);
|
|
65
|
+
console.log(` Language : ${cfg.language}`);
|
|
66
|
+
console.log(` Index dir : ${cfg.indexDir}`);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
export const SCAI_HOME = path.join(os.homedir(), '.scai');
|
|
5
|
+
export const DB_PATH = path.join(SCAI_HOME, 'db.sqlite');
|
|
6
|
+
export const PID_PATH = path.join(SCAI_HOME, 'daemon.pid');
|
|
7
|
+
export const CONFIG_PATH = path.join(SCAI_HOME, 'config.json');
|
|
8
|
+
// Function to read config and get the indexDir on-demand
|
|
9
|
+
export function getIndexDir() {
|
|
10
|
+
try {
|
|
11
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
12
|
+
return config.indexDir || path.join(os.homedir(), 'defaultIndex'); // Default if not set
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
return path.join(os.homedir(), 'defaultIndex'); // Fallback if no config file
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// On-demand index directory retrieval
|
|
19
|
+
export const INDEX_DIR = getIndexDir();
|
package/dist/db/client.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import path from 'path';
|
|
3
2
|
import fs from 'fs';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
3
|
+
import { DB_PATH, SCAI_HOME } from '../constants.js';
|
|
4
|
+
fs.mkdirSync(SCAI_HOME, { recursive: true });
|
|
7
5
|
export const db = new Database(DB_PATH);
|
package/dist/db/fileIndex.js
CHANGED
|
@@ -1,51 +1,114 @@
|
|
|
1
|
+
// File: src/db/fileIndex.ts
|
|
1
2
|
import { db } from './client.js';
|
|
2
3
|
import fs from 'fs';
|
|
4
|
+
import { generateEmbedding } from '../lib/generateEmbedding.js';
|
|
5
|
+
import * as sqlTemplates from './sqlTemplates.js'; // Import the SQL templates
|
|
3
6
|
export function indexFile(filePath, summary, type) {
|
|
4
7
|
const stats = fs.statSync(filePath);
|
|
5
8
|
const lastModified = stats.mtime.toISOString();
|
|
6
9
|
// 1) INSERT new rows (only when path not present)
|
|
7
|
-
const insertStmt = db.prepare(
|
|
8
|
-
|
|
9
|
-
(path, summary, type, indexed_at, last_modified)
|
|
10
|
-
VALUES (?, ?, ?, datetime('now'), ?)
|
|
11
|
-
`);
|
|
12
|
-
insertStmt.run(filePath, summary, type, lastModified);
|
|
10
|
+
const insertStmt = db.prepare(sqlTemplates.insertFileTemplate);
|
|
11
|
+
insertStmt.run({ path: filePath, summary, type, lastModified });
|
|
13
12
|
// 2) UPDATE metadata if file already existed and changed
|
|
14
|
-
const updateStmt = db.prepare(
|
|
15
|
-
|
|
16
|
-
SET type = ?,
|
|
17
|
-
last_modified = ?,
|
|
18
|
-
indexed_at = datetime('now')
|
|
19
|
-
WHERE path = ?
|
|
20
|
-
AND last_modified != ?
|
|
21
|
-
`);
|
|
22
|
-
updateStmt.run(type, lastModified, filePath, lastModified);
|
|
13
|
+
const updateStmt = db.prepare(sqlTemplates.updateFileTemplate);
|
|
14
|
+
updateStmt.run({ path: filePath, type, lastModified });
|
|
23
15
|
// Step 1: Delete from FTS where the path matches
|
|
24
|
-
db.prepare(
|
|
25
|
-
DELETE FROM files_fts
|
|
26
|
-
WHERE rowid = (SELECT id FROM files WHERE path = ?)
|
|
27
|
-
`).run(filePath);
|
|
16
|
+
db.prepare(sqlTemplates.deleteFromFtsTemplate).run({ path: filePath });
|
|
28
17
|
// Step 2: Insert into FTS with the same id
|
|
29
|
-
db.prepare(
|
|
30
|
-
INSERT INTO files_fts(rowid, path, summary)
|
|
31
|
-
VALUES((SELECT id FROM files WHERE path = ?), ?, ?)
|
|
32
|
-
`).run(filePath, filePath, summary);
|
|
18
|
+
db.prepare(sqlTemplates.insertIntoFtsTemplate).run({ path: filePath, summary });
|
|
33
19
|
}
|
|
34
|
-
export function
|
|
35
|
-
//
|
|
36
|
-
const
|
|
20
|
+
export function queryFiles(query, limit = 10) {
|
|
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 = `
|
|
37
40
|
SELECT f.path, f.summary, f.type, f.last_modified, f.indexed_at,
|
|
38
41
|
bm25(files_fts) AS rank
|
|
39
42
|
FROM files_fts
|
|
40
43
|
JOIN files f ON files_fts.rowid = f.id
|
|
41
|
-
WHERE files_fts MATCH
|
|
44
|
+
WHERE files_fts MATCH :query
|
|
42
45
|
ORDER BY rank
|
|
43
|
-
LIMIT
|
|
44
|
-
|
|
45
|
-
const
|
|
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
|
|
46
64
|
.trim()
|
|
47
65
|
.split(/\s+/)
|
|
48
|
-
.map(token =>
|
|
49
|
-
|
|
50
|
-
|
|
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;
|
|
51
114
|
}
|
package/dist/db/schema.js
CHANGED
|
@@ -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
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { Config } from './config.js';
|
|
4
5
|
import { createRequire } from 'module';
|
|
5
6
|
const require = createRequire(import.meta.url);
|
|
6
7
|
const { version } = require('../package.json');
|
|
7
|
-
//
|
|
8
|
+
// 🧠 Commands
|
|
8
9
|
import { checkEnv } from "./commands/EnvCmd.js";
|
|
9
10
|
import { checkGit } from "./commands/GitCmd.js";
|
|
10
11
|
import { suggestCommitMessage } from "./commands/CommitSuggesterCmd.js";
|
|
11
12
|
import { handleRefactor } from "./commands/RefactorCmd.js";
|
|
12
13
|
import { generateTests } from "./commands/TestGenCmd.js";
|
|
13
14
|
import { bootstrap } from './modelSetup.js';
|
|
14
|
-
import { ModelConfig } from './config/ModelConfig.js';
|
|
15
15
|
import { summarizeFile } from "./commands/SummaryCmd.js";
|
|
16
16
|
import { handleChangelogUpdate } from './commands/ChangeLogUpdateCmd.js';
|
|
17
17
|
import { runModulePipelineFromCLI } from './commands/ModulePipelineCmd.js';
|
|
@@ -20,12 +20,13 @@ import { resetDatabase } from './commands/ResetDbCmd.js';
|
|
|
20
20
|
import { runQueryCommand } from './commands/QueryCmd.js';
|
|
21
21
|
import { runDaemonBatch } from './commands/DaemonCmd.js';
|
|
22
22
|
import { runStopDaemonCommand } from "./commands/StopDaemonCmd.js";
|
|
23
|
-
|
|
23
|
+
import { runAskCommand } from './commands/AskCmd.js';
|
|
24
|
+
// 🎛️ CLI Setup
|
|
24
25
|
const cmd = new Command('scai')
|
|
25
26
|
.version(version)
|
|
26
27
|
.option('--model <model>', 'Set the model to use (e.g., codellama:34b)')
|
|
27
28
|
.option('--lang <lang>', 'Set the target language (ts, java, rust)');
|
|
28
|
-
//
|
|
29
|
+
// 🚀 Init command
|
|
29
30
|
cmd
|
|
30
31
|
.command('init')
|
|
31
32
|
.description('Initialize the model and download required models')
|
|
@@ -33,44 +34,96 @@ cmd
|
|
|
33
34
|
await bootstrap();
|
|
34
35
|
console.log('✅ Model initialization completed!');
|
|
35
36
|
});
|
|
36
|
-
|
|
37
|
+
// 🔧 Group: Git-related commands
|
|
38
|
+
const git = cmd.command('git').description('Git utilities');
|
|
39
|
+
git
|
|
40
|
+
.command('status')
|
|
41
|
+
.description('Check Git status')
|
|
42
|
+
.action(checkGit);
|
|
43
|
+
git
|
|
37
44
|
.command('sugg')
|
|
38
45
|
.description('Suggest a commit message from staged changes')
|
|
39
46
|
.option('-c, --commit', 'Automatically commit with suggested message')
|
|
40
47
|
.action(suggestCommitMessage);
|
|
41
|
-
|
|
48
|
+
// 🛠️ Group: `gen` commands for content generation
|
|
49
|
+
const gen = cmd.command('gen').description('Generate code-related output');
|
|
50
|
+
gen
|
|
42
51
|
.command('comm <file>')
|
|
43
52
|
.description('Write comments for the given file')
|
|
44
53
|
.option('-a, --apply', 'Apply the refactored version to the original file')
|
|
45
54
|
.action((file, options) => handleRefactor(file, options));
|
|
46
|
-
|
|
55
|
+
gen
|
|
47
56
|
.command('changelog')
|
|
48
57
|
.description('Update or create the CHANGELOG.md based on current Git diff')
|
|
49
58
|
.action(async () => {
|
|
50
59
|
await handleChangelogUpdate();
|
|
51
60
|
});
|
|
52
|
-
|
|
61
|
+
gen
|
|
53
62
|
.command('summ [file]')
|
|
54
63
|
.description('Print a summary of the given file to the terminal')
|
|
55
64
|
.action((file) => summarizeFile(file));
|
|
65
|
+
gen
|
|
66
|
+
.command('tests <file>')
|
|
67
|
+
.description('Generate a Jest test file for the specified JS/TS module')
|
|
68
|
+
.action((file) => generateTests(file));
|
|
69
|
+
// 🔍 Indexing
|
|
56
70
|
cmd
|
|
57
|
-
.command('
|
|
58
|
-
.description('
|
|
59
|
-
.
|
|
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
|
+
// ⚙️ Group: Configuration settings
|
|
80
|
+
const set = cmd.command('set').description('Set configuration values');
|
|
81
|
+
set
|
|
82
|
+
.command('model <model>')
|
|
83
|
+
.description('Set the model to use')
|
|
84
|
+
.action((model) => {
|
|
85
|
+
Config.setModel(model);
|
|
86
|
+
Config.show();
|
|
87
|
+
});
|
|
88
|
+
set
|
|
89
|
+
.command('lang <lang>')
|
|
90
|
+
.description('Set the programming language')
|
|
91
|
+
.action((lang) => {
|
|
92
|
+
Config.setLanguage(lang);
|
|
93
|
+
Config.show();
|
|
94
|
+
});
|
|
95
|
+
set
|
|
96
|
+
.command('index-dir <dir>')
|
|
97
|
+
.description('Set the path to the indexed directory')
|
|
98
|
+
.action((dir) => {
|
|
99
|
+
Config.setIndexDir(path.resolve(dir));
|
|
100
|
+
Config.show();
|
|
101
|
+
});
|
|
102
|
+
// 🧪 Diagnostics and info
|
|
60
103
|
cmd
|
|
61
104
|
.command('env')
|
|
62
105
|
.description('Check environment variables')
|
|
63
106
|
.action(checkEnv);
|
|
64
|
-
cmd
|
|
65
|
-
.command('gen-tests <file>')
|
|
66
|
-
.description('Generate a Jest test file for the specified JS/TS module')
|
|
67
|
-
.action((file) => generateTests(file));
|
|
68
107
|
cmd
|
|
69
108
|
.command('config')
|
|
70
109
|
.description('Show the currently active model and language settings')
|
|
71
110
|
.action(() => {
|
|
72
|
-
|
|
111
|
+
Config.show();
|
|
73
112
|
});
|
|
113
|
+
// 🧠 Query and assistant
|
|
114
|
+
cmd
|
|
115
|
+
.command('query <query>')
|
|
116
|
+
.description('Search indexed files by keyword')
|
|
117
|
+
.action(runQueryCommand);
|
|
118
|
+
cmd
|
|
119
|
+
.command('ask')
|
|
120
|
+
.description('Ask a question using file summaries and a local model')
|
|
121
|
+
.argument('<question...>', 'The question to ask')
|
|
122
|
+
.action((question) => {
|
|
123
|
+
const q = question.join(' ');
|
|
124
|
+
runAskCommand(q);
|
|
125
|
+
});
|
|
126
|
+
// 🛠️ Background tasks and maintenance
|
|
74
127
|
cmd
|
|
75
128
|
.command('daemon')
|
|
76
129
|
.description('Run background summarization of indexed files')
|
|
@@ -79,22 +132,11 @@ cmd
|
|
|
79
132
|
.command('stop-daemon')
|
|
80
133
|
.description('Stop the background summarizer daemon')
|
|
81
134
|
.action(runStopDaemonCommand);
|
|
82
|
-
cmd
|
|
83
|
-
.command('index [targetDir]')
|
|
84
|
-
.description('Index supported files in the given directory (or current folder if none)')
|
|
85
|
-
.option('-d, --detached', 'Run summarizer daemon after indexing')
|
|
86
|
-
.action((targetDir, options) => {
|
|
87
|
-
runIndexCommand(targetDir, { detached: options.detached });
|
|
88
|
-
});
|
|
89
|
-
cmd
|
|
90
|
-
.command('query <query>')
|
|
91
|
-
.description('Search indexed files by keyword')
|
|
92
|
-
.action(runQueryCommand);
|
|
93
135
|
cmd
|
|
94
136
|
.command('reset-db')
|
|
95
137
|
.description('Delete and reset the SQLite database')
|
|
96
138
|
.action(() => resetDatabase());
|
|
97
|
-
//
|
|
139
|
+
// 🧬 Fallback: Pipeline mode
|
|
98
140
|
cmd
|
|
99
141
|
.arguments('<file>')
|
|
100
142
|
.option('-m, --modules <modules>', 'Comma-separated list of modules to run (e.g., comments,cleanup,summary)')
|
|
@@ -105,11 +147,11 @@ cmd
|
|
|
105
147
|
}
|
|
106
148
|
runModulePipelineFromCLI(file, options);
|
|
107
149
|
});
|
|
108
|
-
// ✅
|
|
150
|
+
// ✅ Parse CLI args
|
|
109
151
|
cmd.parse(process.argv);
|
|
110
|
-
//
|
|
152
|
+
// 🔁 Apply global options post-parse
|
|
111
153
|
const opts = cmd.opts();
|
|
112
154
|
if (opts.model)
|
|
113
|
-
|
|
155
|
+
Config.setModel(opts.model);
|
|
114
156
|
if (opts.lang)
|
|
115
|
-
|
|
157
|
+
Config.setLanguage(opts.lang);
|
|
@@ -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
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Config } from '../../config.js';
|
|
2
2
|
import { generate } from '../../lib/generate.js';
|
|
3
3
|
export const changelogModule = {
|
|
4
4
|
name: 'changelogModule',
|
|
5
5
|
description: 'Generates changelog entry based on Git diff',
|
|
6
6
|
async run(input) {
|
|
7
|
-
const model =
|
|
7
|
+
const model = Config.getModel();
|
|
8
8
|
const prompt = `
|
|
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
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Config } from '../../config.js';
|
|
2
2
|
import { generate } from '../../lib/generate.js';
|
|
3
3
|
export const addCommentsModule = {
|
|
4
4
|
name: 'addComments',
|
|
5
5
|
description: 'Adds meaningful // comments to each block of code',
|
|
6
6
|
async run(input) {
|
|
7
|
-
const model =
|
|
8
|
-
const lang =
|
|
7
|
+
const model = Config.getModel();
|
|
8
|
+
const lang = Config.getLanguage();
|
|
9
9
|
const prompt = `
|
|
10
10
|
You are a senior ${lang.toUpperCase()} engineer reviewing source code.
|
|
11
11
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { generate } from '../../lib/generate.js';
|
|
2
|
-
import {
|
|
2
|
+
import { Config } from '../../config.js';
|
|
3
3
|
export const commitSuggesterModule = {
|
|
4
4
|
name: 'commitSuggester',
|
|
5
5
|
description: 'Suggests conventional commit messages from Git diff',
|
|
6
6
|
async run({ content }) {
|
|
7
|
-
const model =
|
|
7
|
+
const model = Config.getModel();
|
|
8
8
|
const prompt = `
|
|
9
9
|
Suggest ALWAYS 3 concise, conventional Git commit messages based on the input code diff.
|
|
10
10
|
Use this format ONLY:
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// src/pipeline/modules/generateTestsModule.ts
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { Config } from '../../config.js';
|
|
5
5
|
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
9
|
async run({ content, filepath }) {
|
|
10
|
-
const model =
|
|
11
|
-
const lang =
|
|
10
|
+
const model = Config.getModel();
|
|
11
|
+
const lang = Config.getLanguage();
|
|
12
12
|
if (!filepath)
|
|
13
13
|
throw new Error('Missing filepath in pipeline context');
|
|
14
14
|
const prompt = `
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Config } from '../../config.js';
|
|
2
2
|
import { generate } from '../../lib/generate.js';
|
|
3
3
|
export const refactorModule = {
|
|
4
4
|
name: 'refactor',
|
|
5
5
|
description: 'Break code into small, clean functions',
|
|
6
6
|
async run(input) {
|
|
7
|
-
const model =
|
|
8
|
-
const lang =
|
|
7
|
+
const model = Config.getModel();
|
|
8
|
+
const lang = Config.getLanguage();
|
|
9
9
|
const prompt = `
|
|
10
10
|
You are a senior ${lang.toUpperCase()} engineer.
|
|
11
11
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Config } from '../../config.js';
|
|
2
2
|
import { generate } from '../../lib/generate.js';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
export const summaryModule = {
|
|
5
5
|
name: 'summary',
|
|
6
6
|
description: 'Generates a general summary of any file content.',
|
|
7
7
|
run: async ({ content, filepath }) => {
|
|
8
|
-
const model =
|
|
8
|
+
const model = Config.getModel();
|
|
9
9
|
const ext = filepath ? path.extname(filepath).toLowerCase() : '';
|
|
10
10
|
const filename = filepath ? path.basename(filepath) : '';
|
|
11
11
|
// More neutral prompt for general-purpose content
|
|
@@ -36,7 +36,6 @@ ${content}
|
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
38
38
|
console.warn('⚠️ No summary generated.');
|
|
39
|
-
response.summary = '⚠️ No summary generated.';
|
|
40
39
|
}
|
|
41
40
|
return response;
|
|
42
41
|
}
|
package/package.json
CHANGED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export class ModelConfig {
|
|
2
|
-
static setModel(model) {
|
|
3
|
-
this.model = model;
|
|
4
|
-
console.log(`📦 Model set to: ${model}`);
|
|
5
|
-
}
|
|
6
|
-
static getModel() {
|
|
7
|
-
return this.model;
|
|
8
|
-
}
|
|
9
|
-
static setLanguage(lang) {
|
|
10
|
-
this.language = lang;
|
|
11
|
-
console.log(`🗣️ Language set to: ${lang}`);
|
|
12
|
-
}
|
|
13
|
-
static getLanguage() {
|
|
14
|
-
return this.language;
|
|
15
|
-
}
|
|
16
|
-
static logCurrentConfig() {
|
|
17
|
-
console.log(`🔧 Current configuration:`);
|
|
18
|
-
console.log(` Model : ${this.model}`);
|
|
19
|
-
console.log(` Language: ${this.language}`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
ModelConfig.model = 'codellama:7b';
|
|
23
|
-
ModelConfig.language = 'ts';
|
|
File without changes
|