scai 0.1.122 → 0.1.123
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 +74 -52
- package/dist/pipeline/modules/finalAnswerModule.js +80 -25
- package/dist/utils/splitCodeIntoChunk.js +45 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
# ⚙️ SCAI — Source Code AI 🌿
|
|
2
2
|
|
|
3
|
-
> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.**
|
|
3
|
+
> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.**
|
|
4
|
+
> **100% local • No token cost • Private by design • GDPR-friendly** — made in Denmark/EU with ❤️.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
🔗 **Website:** [https://scai.dk](https://scai.dk)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
SCAI is your AI coding companion in the terminal. Stay focused on coding while SCAI helps you understand, analyze, and reason about your codebase using local language models.
|
|
9
|
+
|
|
10
|
+
**Local Model Note:** SCAI runs entirely on local LLMs. This means **no API keys, no token cost, and full privacy**, but also **more limited capabilities** compared to cloud-hosted AI.
|
|
11
|
+
|
|
12
|
+
> ⚠️ **Alpha Version Notice**
|
|
13
|
+
> If you have previously installed SCAI, please run:
|
|
14
|
+
>
|
|
15
|
+
> ```bash
|
|
16
|
+
> scai db reset && scai index start
|
|
17
|
+
> ```
|
|
18
|
+
>
|
|
19
|
+
> before using this version.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🗣️ Language Support (Important)
|
|
24
|
+
|
|
25
|
+
SCAI is currently **tested and validated only on the following languages**:
|
|
26
|
+
|
|
27
|
+
* **JavaScript (JS)**
|
|
28
|
+
* **TypeScript (TS)**
|
|
29
|
+
* **Java**
|
|
30
|
+
|
|
31
|
+
Other languages may work partially, but analysis quality, indexing accuracy, and agent behavior are **not guaranteed** outside these languages. Broader language support is planned, but for now SCAI should be considered **JS/TS/Java-first**.
|
|
8
32
|
|
|
9
33
|
---
|
|
10
34
|
|
|
@@ -18,7 +42,9 @@ scai init
|
|
|
18
42
|
scai index start
|
|
19
43
|
```
|
|
20
44
|
|
|
21
|
-
This
|
|
45
|
+
This initializes local models (recommended: `qwen3-coder:30b`) and starts indexing your code repository.
|
|
46
|
+
|
|
47
|
+
> ⏳ **Note:** Initial indexing and analysis can take **minutes to hours** depending on repository size and enabled analysis tools.
|
|
22
48
|
|
|
23
49
|
### 2️⃣ Check Available Commands
|
|
24
50
|
|
|
@@ -38,7 +64,7 @@ scai shell
|
|
|
38
64
|
|
|
39
65
|
Once in the REPL, you can:
|
|
40
66
|
|
|
41
|
-
|
|
67
|
+
### Ask questions about your codebase
|
|
42
68
|
|
|
43
69
|
```text
|
|
44
70
|
scai> How many functions in config.js are missing tests?
|
|
@@ -47,7 +73,7 @@ scai> Where are all the database queries defined?
|
|
|
47
73
|
scai> List files involved in authentication
|
|
48
74
|
```
|
|
49
75
|
|
|
50
|
-
|
|
76
|
+
### Run CLI commands from inside the REPL
|
|
51
77
|
|
|
52
78
|
```text
|
|
53
79
|
scai> /git commit
|
|
@@ -57,64 +83,72 @@ scai> /index switch
|
|
|
57
83
|
scai> /index delete
|
|
58
84
|
```
|
|
59
85
|
|
|
60
|
-
|
|
86
|
+
### Execute shell commands
|
|
61
87
|
|
|
62
88
|
```text
|
|
63
89
|
scai> !ls -la
|
|
64
90
|
scai> !git status
|
|
65
91
|
```
|
|
66
92
|
|
|
67
|
-
> ✅
|
|
93
|
+
> ✅ REPL queries are free, offline, GDPR-friendly, and **no token cost**.
|
|
68
94
|
|
|
69
95
|
---
|
|
70
96
|
|
|
71
|
-
## 📦 Indexing
|
|
97
|
+
## 📦 Repository Indexing
|
|
72
98
|
|
|
73
|
-
Before
|
|
99
|
+
Before SCAI can answer questions, your repository must be indexed.
|
|
74
100
|
|
|
75
|
-
|
|
101
|
+
### Common Index Commands
|
|
76
102
|
|
|
77
103
|
```bash
|
|
78
104
|
scai index set /path/to/repo
|
|
105
|
+
scai index start
|
|
106
|
+
scai index list
|
|
107
|
+
scai index switch
|
|
108
|
+
scai index delete
|
|
79
109
|
```
|
|
80
110
|
|
|
81
|
-
|
|
111
|
+
Only indexed repositories can be queried.
|
|
82
112
|
|
|
83
|
-
|
|
84
|
-
scai index start
|
|
85
|
-
```
|
|
113
|
+
---
|
|
86
114
|
|
|
87
|
-
|
|
115
|
+
## 🧠 Background Indexing & Analysis (Daemon)
|
|
88
116
|
|
|
89
|
-
|
|
90
|
-
scai index list
|
|
91
|
-
```
|
|
117
|
+
SCAI performs **deep repository indexing and static analysis** using background workers. This includes:
|
|
92
118
|
|
|
93
|
-
*
|
|
119
|
+
* File structure discovery
|
|
120
|
+
* Language-aware parsing (JS / TS / Java)
|
|
121
|
+
* Symbol and dependency mapping
|
|
122
|
+
* Heuristic analysis for tests, architecture, and patterns
|
|
94
123
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
124
|
+
⚠️ **Important:** On first install or on large repositories, this process can take **several hours**.
|
|
125
|
+
|
|
126
|
+
All background work is handled by the **SCAI daemon**, which can be fully controlled from the CLI.
|
|
98
127
|
|
|
99
|
-
|
|
128
|
+
### Daemon Commands
|
|
100
129
|
|
|
101
130
|
```bash
|
|
102
|
-
scai
|
|
131
|
+
scai daemon start
|
|
132
|
+
scai daemon stop
|
|
133
|
+
scai daemon restart
|
|
134
|
+
scai daemon status
|
|
135
|
+
scai daemon unlock
|
|
136
|
+
scai daemon logs
|
|
103
137
|
```
|
|
104
138
|
|
|
105
|
-
|
|
139
|
+
You can safely stop the daemon at any time. Indexing and analysis will resume when restarted.
|
|
106
140
|
|
|
107
141
|
---
|
|
108
142
|
|
|
109
143
|
## ⚙️ Configuration
|
|
110
144
|
|
|
111
|
-
Set the AI model
|
|
145
|
+
Set the local AI model (recommended):
|
|
112
146
|
|
|
113
147
|
```bash
|
|
114
148
|
scai config set-model qwen3-coder:30b
|
|
115
149
|
```
|
|
116
150
|
|
|
117
|
-
|
|
151
|
+
View current configuration:
|
|
118
152
|
|
|
119
153
|
```bash
|
|
120
154
|
scai config show --raw
|
|
@@ -124,42 +158,30 @@ scai config show --raw
|
|
|
124
158
|
|
|
125
159
|
## 🔧 Git Commit Assistant
|
|
126
160
|
|
|
127
|
-
|
|
161
|
+
Generate meaningful commit messages based on staged changes:
|
|
128
162
|
|
|
129
163
|
```bash
|
|
130
164
|
git add .
|
|
131
165
|
scai git commit
|
|
132
166
|
```
|
|
133
167
|
|
|
134
|
-
|
|
168
|
+
All analysis is performed locally — **no token usage, no cloud calls**.
|
|
135
169
|
|
|
136
170
|
---
|
|
137
171
|
|
|
138
172
|
## 🔑 GitHub Authentication
|
|
139
173
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
* **Set your token:**
|
|
174
|
+
For GitHub-related features, SCAI requires a Personal Access Token.
|
|
143
175
|
|
|
144
176
|
```bash
|
|
145
177
|
scai auth set
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
* **Check your token:**
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
178
|
scai auth check
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
* **Reset your token:**
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
179
|
scai auth reset
|
|
158
180
|
```
|
|
159
181
|
|
|
160
182
|
---
|
|
161
183
|
|
|
162
|
-
## 🧠 Example Queries
|
|
184
|
+
## 🧠 Example Queries
|
|
163
185
|
|
|
164
186
|
* `Summarize codeTransform.js`
|
|
165
187
|
* `Explain utils/helpers.ts architecture`
|
|
@@ -167,26 +189,26 @@ scai auth reset
|
|
|
167
189
|
* `Show where database queries are defined`
|
|
168
190
|
* `Highlight potential memory leaks`
|
|
169
191
|
* `Describe how authentication works`
|
|
170
|
-
* `Which files handle error handling in contextReview.ts`
|
|
171
192
|
* `Summarize repo architecture`
|
|
172
193
|
|
|
173
194
|
---
|
|
174
195
|
|
|
175
196
|
## 🔐 Privacy & GDPR
|
|
176
197
|
|
|
177
|
-
* Fully local — no
|
|
178
|
-
*
|
|
179
|
-
*
|
|
198
|
+
* Fully local — no cloud calls
|
|
199
|
+
* No API keys
|
|
200
|
+
* **No token cost**
|
|
201
|
+
* GDPR-friendly, built in Denmark/EU 🇩🇰
|
|
180
202
|
|
|
181
203
|
---
|
|
182
204
|
|
|
183
205
|
## 🙌 Feedback & Support
|
|
184
206
|
|
|
185
|
-
|
|
207
|
+
Feedback, bugs, and ideas are very welcome:
|
|
186
208
|
|
|
187
|
-
*
|
|
188
|
-
*
|
|
209
|
+
* 🌍 Website: [https://scai.dk](https://scai.dk)
|
|
210
|
+
* 🧵 Threads: [@scai.dk](https://threads.net/@scai.dk)
|
|
189
211
|
|
|
190
212
|
---
|
|
191
213
|
|
|
192
|
-
Enjoy your
|
|
214
|
+
Enjoy your **fully local, private, and developer-focused AI coding companion** ✨
|
|
@@ -3,58 +3,113 @@ import { generate } from "../../lib/generate.js";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
export const finalAnswerModule = {
|
|
5
5
|
name: "finalAnswer",
|
|
6
|
-
description: "
|
|
6
|
+
description: "Produces the final, post-execution explanation for the user.",
|
|
7
7
|
groups: ["finalize"],
|
|
8
8
|
run: async (input) => {
|
|
9
9
|
const query = input.query;
|
|
10
10
|
const context = input.context;
|
|
11
|
-
if (!context)
|
|
11
|
+
if (!context) {
|
|
12
12
|
throw new Error("[finalAnswerModule] No context provided");
|
|
13
|
+
}
|
|
14
|
+
// ---------------------------------------------------------------------
|
|
15
|
+
// Detect execution state
|
|
16
|
+
// ---------------------------------------------------------------------
|
|
13
17
|
const transformedFiles = context.execution?.codeTransformArtifacts?.files ?? [];
|
|
14
|
-
const
|
|
15
|
-
|
|
18
|
+
const hasTransforms = transformedFiles.length > 0;
|
|
19
|
+
// ---------------------------------------------------------------------
|
|
20
|
+
// Select authoritative files
|
|
21
|
+
// ---------------------------------------------------------------------
|
|
22
|
+
const filesToConsider = hasTransforms
|
|
16
23
|
? transformedFiles.map(f => ({
|
|
17
24
|
path: f.filePath,
|
|
18
|
-
codeSnippet: f.content.slice(0, 500),
|
|
19
25
|
notes: f.notes,
|
|
26
|
+
codeSnippet: f.content.slice(0, 500),
|
|
20
27
|
}))
|
|
21
|
-
: workingFiles.map(f => ({
|
|
28
|
+
: (context.workingFiles ?? []).map(f => ({
|
|
22
29
|
path: f.path,
|
|
23
|
-
codeSnippet: f.code ? f.code.slice(0, 500) : undefined,
|
|
24
30
|
summary: f.summary ?? "",
|
|
31
|
+
codeSnippet: f.code ? f.code.slice(0, 500) : undefined,
|
|
25
32
|
}));
|
|
26
|
-
|
|
33
|
+
// ---------------------------------------------------------------------
|
|
34
|
+
// Analysis framing (before / after)
|
|
35
|
+
// ---------------------------------------------------------------------
|
|
36
|
+
const analysisPayload = {
|
|
27
37
|
intent: context.analysis?.intent,
|
|
28
|
-
|
|
38
|
+
// BEFORE: what problem was being solved
|
|
39
|
+
understanding: context.analysis?.understanding,
|
|
40
|
+
// AFTER: what is now true across the system
|
|
29
41
|
combinedAnalysis: context.analysis?.combinedAnalysis,
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
// Supporting detail (optional but useful)
|
|
43
|
+
fileAnalysis: context.analysis?.fileAnalysis,
|
|
32
44
|
};
|
|
45
|
+
// ---------------------------------------------------------------------
|
|
46
|
+
// Phase-aware prompt construction
|
|
47
|
+
// ---------------------------------------------------------------------
|
|
48
|
+
const phasePreamble = hasTransforms
|
|
49
|
+
? `
|
|
50
|
+
SYSTEM CONTEXT:
|
|
51
|
+
This is the FINAL STEP in a long, multi-stage execution pipeline.
|
|
52
|
+
|
|
53
|
+
All planned code transformations have already been executed.
|
|
54
|
+
The files shown represent the FINAL and AUTHORITATIVE state of the codebase.
|
|
55
|
+
|
|
56
|
+
Your role is to explain what was done and why — not to suggest changes.
|
|
57
|
+
`
|
|
58
|
+
: `
|
|
59
|
+
SYSTEM CONTEXT:
|
|
60
|
+
This request does not include executed code changes.
|
|
61
|
+
Provide a direct explanation based on the available context.
|
|
62
|
+
`;
|
|
63
|
+
const instructions = hasTransforms
|
|
64
|
+
? `
|
|
65
|
+
INSTRUCTIONS (POST-EXECUTION EXPLANATION MODE):
|
|
66
|
+
|
|
67
|
+
- Explain the completed changes using past tense.
|
|
68
|
+
- Anchor the explanation in the original problem (understanding).
|
|
69
|
+
- Describe system-wide effects using combinedAnalysis.
|
|
70
|
+
- Refer to transformed files as evidence, not proposals.
|
|
71
|
+
- Mention relevant risks or trade-offs, if any.
|
|
72
|
+
- DO NOT explain how to implement the change.
|
|
73
|
+
- DO NOT suggest additional work.
|
|
74
|
+
- DO NOT ask questions.
|
|
75
|
+
`
|
|
76
|
+
: `
|
|
77
|
+
INSTRUCTIONS:
|
|
78
|
+
|
|
79
|
+
- Answer the developer’s question clearly and concisely.
|
|
80
|
+
- Base your response strictly on the provided context.
|
|
81
|
+
- Do not invent changes or assume missing information.
|
|
82
|
+
`;
|
|
33
83
|
const promptText = `
|
|
34
|
-
|
|
84
|
+
${phasePreamble}
|
|
35
85
|
|
|
36
|
-
|
|
86
|
+
You are producing the FINAL ANSWER for this run.
|
|
87
|
+
|
|
88
|
+
User query:
|
|
37
89
|
${query}
|
|
38
90
|
|
|
39
|
-
Analysis
|
|
40
|
-
${JSON.stringify(
|
|
91
|
+
Analysis:
|
|
92
|
+
${JSON.stringify(analysisPayload, null, 2)}
|
|
41
93
|
|
|
42
|
-
|
|
94
|
+
Relevant files (final state):
|
|
43
95
|
${JSON.stringify(filesToConsider, null, 2)}
|
|
44
96
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const aiResponse = await generate({
|
|
97
|
+
${instructions}
|
|
98
|
+
`.trim();
|
|
99
|
+
// ---------------------------------------------------------------------
|
|
100
|
+
// Generate final answer
|
|
101
|
+
// ---------------------------------------------------------------------
|
|
102
|
+
const aiResponse = await generate({
|
|
103
|
+
query,
|
|
104
|
+
content: promptText,
|
|
105
|
+
});
|
|
51
106
|
const output = {
|
|
52
107
|
query,
|
|
53
|
-
content:
|
|
108
|
+
content: "",
|
|
54
109
|
data: aiResponse.data,
|
|
55
110
|
};
|
|
56
111
|
logInputOutput("finalAnswer", "output", output.data);
|
|
57
|
-
console.log(
|
|
112
|
+
console.log("\n\n\n\n--> Answer:\n", chalk.blue(output.data));
|
|
58
113
|
return output;
|
|
59
|
-
}
|
|
114
|
+
},
|
|
60
115
|
};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { encode } from 'gpt-3-encoder';
|
|
2
|
-
export function splitCodeIntoChunks(text, softLimit = 1500,
|
|
3
|
-
hardLimitMultiplier = 2 // hard limit will be softLimit * multiplier
|
|
4
|
-
) {
|
|
2
|
+
export function splitCodeIntoChunks(text, softLimit = 1500, hardLimitMultiplier = 2) {
|
|
5
3
|
const hardLimit = softLimit * hardLimitMultiplier;
|
|
6
4
|
const lines = text.split('\n');
|
|
7
5
|
const chunks = [];
|
|
@@ -10,12 +8,19 @@ hardLimitMultiplier = 2 // hard limit will be softLimit * multiplier
|
|
|
10
8
|
let inMultiComment = false;
|
|
11
9
|
let inFunction = false;
|
|
12
10
|
let inTryBlock = false;
|
|
11
|
+
let inTryChain = false;
|
|
12
|
+
let tryChainHasHandler = false;
|
|
13
13
|
let globalBraceDepth = 0;
|
|
14
14
|
let functionBraceDepth = 0;
|
|
15
|
+
let tryBraceDepth = 0;
|
|
15
16
|
let parenDepth = 0;
|
|
16
17
|
let bracketDepth = 0;
|
|
18
|
+
let justClosedFunction = false;
|
|
19
|
+
let justClosedTryBlock = false;
|
|
17
20
|
for (const line of lines) {
|
|
18
21
|
const trimmed = line.trim();
|
|
22
|
+
justClosedFunction = false;
|
|
23
|
+
justClosedTryBlock = false;
|
|
19
24
|
// ---------- comments ----------
|
|
20
25
|
if (trimmed.includes('/*') && !trimmed.includes('*/'))
|
|
21
26
|
inMultiComment = true;
|
|
@@ -30,22 +35,40 @@ hardLimitMultiplier = 2 // hard limit will be softLimit * multiplier
|
|
|
30
35
|
inFunction = true;
|
|
31
36
|
functionBraceDepth = 0;
|
|
32
37
|
}
|
|
33
|
-
// ---------- try/catch ----------
|
|
34
|
-
if (trimmed.startsWith('try
|
|
38
|
+
// ---------- try / catch / finally start ----------
|
|
39
|
+
if (!inTryChain && trimmed.startsWith('try')) {
|
|
40
|
+
inTryChain = true;
|
|
35
41
|
inTryBlock = true;
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
tryBraceDepth = 0;
|
|
43
|
+
}
|
|
44
|
+
if (inTryChain &&
|
|
45
|
+
(trimmed.startsWith('catch') || trimmed.startsWith('finally'))) {
|
|
46
|
+
inTryBlock = true;
|
|
47
|
+
tryChainHasHandler = true;
|
|
48
|
+
}
|
|
38
49
|
// ---------- depth tracking ----------
|
|
39
50
|
for (const char of line) {
|
|
40
51
|
if (char === '{') {
|
|
41
52
|
globalBraceDepth++;
|
|
42
53
|
if (inFunction)
|
|
43
54
|
functionBraceDepth++;
|
|
55
|
+
if (inTryChain)
|
|
56
|
+
tryBraceDepth++;
|
|
44
57
|
}
|
|
45
58
|
else if (char === '}') {
|
|
46
59
|
globalBraceDepth = Math.max(0, globalBraceDepth - 1);
|
|
47
|
-
if (inFunction)
|
|
60
|
+
if (inFunction) {
|
|
48
61
|
functionBraceDepth = Math.max(0, functionBraceDepth - 1);
|
|
62
|
+
if (functionBraceDepth === 0) {
|
|
63
|
+
justClosedFunction = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (inTryChain) {
|
|
67
|
+
tryBraceDepth = Math.max(0, tryBraceDepth - 1);
|
|
68
|
+
if (tryBraceDepth === 0) {
|
|
69
|
+
justClosedTryBlock = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
49
72
|
}
|
|
50
73
|
else if (char === '(') {
|
|
51
74
|
parenDepth++;
|
|
@@ -60,6 +83,16 @@ hardLimitMultiplier = 2 // hard limit will be softLimit * multiplier
|
|
|
60
83
|
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
61
84
|
}
|
|
62
85
|
}
|
|
86
|
+
if (justClosedFunction)
|
|
87
|
+
inFunction = false;
|
|
88
|
+
if (justClosedTryBlock) {
|
|
89
|
+
inTryBlock = false;
|
|
90
|
+
// Only close the chain if we've already seen a handler
|
|
91
|
+
if (tryChainHasHandler) {
|
|
92
|
+
inTryChain = false;
|
|
93
|
+
tryChainHasHandler = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
63
96
|
// ---------- add line ----------
|
|
64
97
|
currentChunkLines.push(line);
|
|
65
98
|
currentTokens += encode(line + '\n').length;
|
|
@@ -67,20 +100,18 @@ hardLimitMultiplier = 2 // hard limit will be softLimit * multiplier
|
|
|
67
100
|
const softLimitReached = currentTokens >= softLimit;
|
|
68
101
|
const hardLimitReached = currentTokens >= hardLimit;
|
|
69
102
|
const safeToSplit = !inMultiComment &&
|
|
70
|
-
!
|
|
103
|
+
!inFunction &&
|
|
104
|
+
!inTryChain &&
|
|
105
|
+
!justClosedFunction &&
|
|
106
|
+
!justClosedTryBlock &&
|
|
71
107
|
functionBraceDepth === 0 &&
|
|
72
108
|
parenDepth === 0 &&
|
|
73
109
|
bracketDepth === 0;
|
|
74
|
-
// try to split after soft limit if safe, or force at hard limit
|
|
75
110
|
if ((softLimitReached && safeToSplit) || hardLimitReached) {
|
|
76
111
|
chunks.push(currentChunkLines.join('\n'));
|
|
77
112
|
currentChunkLines = [];
|
|
78
113
|
currentTokens = 0;
|
|
79
114
|
}
|
|
80
|
-
// ---------- function end ----------
|
|
81
|
-
if (inFunction && functionBraceDepth === 0) {
|
|
82
|
-
inFunction = false;
|
|
83
|
-
}
|
|
84
115
|
}
|
|
85
116
|
if (currentChunkLines.length > 0) {
|
|
86
117
|
chunks.push(currentChunkLines.join('\n'));
|