scai 0.1.32 ā 0.1.33
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 +30 -2
- package/dist/commands/ChangeLogUpdateCmd.js +71 -23
- package/dist/commands/CommitSuggesterCmd.js +32 -12
- package/dist/index.js +15 -14
- package/dist/pipeline/modules/changeLogModule.js +26 -14
- package/dist/pipeline/runModulePipeline.js +1 -1
- package/dist/utils/changeLogPrompt.js +30 -0
- package/dist/utils/removeIgnoredFiles.js +1 -1
- package/dist/utils/shouldIgnoreFiles.js +1 -1
- package/package.json +2 -2
- /package/dist/{utils ā config}/specificFileExceptions.js +0 -0
package/README.md
CHANGED
|
@@ -59,20 +59,48 @@ scai runs entirely on your machine and doesn't require cloud APIs or API keys. T
|
|
|
59
59
|
|
|
60
60
|
### š§ Git Commit Suggestions
|
|
61
61
|
|
|
62
|
+
Use AI to suggest a meaningful commit message based on your staged code:
|
|
63
|
+
|
|
62
64
|
```bash
|
|
63
65
|
git add .
|
|
64
66
|
scai git sugg
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
To auto-commit:
|
|
69
|
+
To automatically commit with the selected suggestion:
|
|
69
70
|
|
|
70
71
|
```bash
|
|
71
72
|
scai git sugg --commit
|
|
72
73
|
```
|
|
73
74
|
|
|
75
|
+
You can also include a changelog entry along with the commit:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
scai git sugg --commit --changelog
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This will:
|
|
82
|
+
1. Suggest a commit message based on your `git diff --cached`
|
|
83
|
+
2. Propose a changelog entry (if relevant)
|
|
84
|
+
3. Allow you to approve, regenerate, or skip the changelog
|
|
85
|
+
4. Automatically stage and commit the changes
|
|
86
|
+
|
|
74
87
|
---
|
|
75
88
|
|
|
89
|
+
### š Generate a Standalone Changelog Entry
|
|
90
|
+
|
|
91
|
+
If you want to generate a changelog entry without committing:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
scai gen changelog
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This will:
|
|
98
|
+
- Analyze the current `git diff` (staged or unstaged)
|
|
99
|
+
- Propose a list of **user-facing changes** in clean markdown bullet points
|
|
100
|
+
- Let you accept, regenerate, or skip the update
|
|
101
|
+
- Append the entry to `CHANGELOG.md` and stage it if accepted
|
|
102
|
+
|
|
103
|
+
|
|
76
104
|
### š ļø Code Generation Commands (`gen` group)
|
|
77
105
|
|
|
78
106
|
```bash
|
|
@@ -1,39 +1,87 @@
|
|
|
1
|
-
// src/commands/
|
|
1
|
+
// src/commands/ChangeLogUpdateCmd.ts
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { runModulePipeline } from '../pipeline/runModulePipeline.js';
|
|
6
6
|
import { changelogModule } from '../pipeline/modules/changeLogModule.js';
|
|
7
|
-
|
|
7
|
+
import { askChangelogApproval } from '../utils/changeLogPrompt.js';
|
|
8
|
+
export async function handleStandaloneChangelogUpdate() {
|
|
9
|
+
let diff = execSync("git diff", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
10
|
+
if (!diff) {
|
|
11
|
+
diff = execSync("git diff --cached", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
12
|
+
}
|
|
13
|
+
if (!diff) {
|
|
14
|
+
console.log('ā ļø No changes detected for changelog.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let entry = await generateChangelogEntry(diff);
|
|
18
|
+
if (!entry) {
|
|
19
|
+
console.log('ā ļø No significant changes found.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
while (true) {
|
|
23
|
+
const userChoice = await askChangelogApproval(entry);
|
|
24
|
+
if (userChoice === 'yes') {
|
|
25
|
+
const path = await updateChangelogFile(entry);
|
|
26
|
+
console.log(`ā
CHANGELOG.md updated: ${path}`);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
else if (userChoice === 'redo') {
|
|
30
|
+
console.log('š Regenerating changelog...');
|
|
31
|
+
entry = await generateChangelogEntry(diff);
|
|
32
|
+
if (!entry) {
|
|
33
|
+
console.log('ā ļø Could not regenerate entry. Exiting.');
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log('ā Skipped changelog update.');
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function generateChangelogEntry(diff) {
|
|
8
44
|
try {
|
|
9
|
-
|
|
45
|
+
// If no diff is provided, fetch the current diff from git silently
|
|
10
46
|
if (!diff) {
|
|
11
|
-
diff = execSync("git diff
|
|
47
|
+
diff = execSync("git diff", { encoding: "utf-8", stdio: 'pipe' }).trim();
|
|
48
|
+
// If no unstaged changes, fall back to staged changes
|
|
49
|
+
if (!diff) {
|
|
50
|
+
diff = execSync("git diff --cached", { encoding: "utf-8", stdio: 'pipe' }).trim();
|
|
51
|
+
}
|
|
12
52
|
}
|
|
13
53
|
if (!diff) {
|
|
14
|
-
|
|
15
|
-
|
|
54
|
+
// No changes found, auto cancel by returning null
|
|
55
|
+
console.log('ā ļø No changes detected for the changelog.');
|
|
56
|
+
return null;
|
|
16
57
|
}
|
|
58
|
+
// Generate the changelog entry using the diff silently
|
|
17
59
|
const result = await runModulePipeline([changelogModule], { content: diff });
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const changelogPath = path.join(root, "CHANGELOG.md");
|
|
24
|
-
let existing = "";
|
|
25
|
-
try {
|
|
26
|
-
existing = await fs.readFile(changelogPath, "utf-8");
|
|
60
|
+
const output = result?.summary?.trim();
|
|
61
|
+
if (!output || output === 'NO UPDATE') {
|
|
62
|
+
// Auto-cancel if no update
|
|
63
|
+
console.log('ā ļø No significant changes detected for changelog.');
|
|
64
|
+
return null;
|
|
27
65
|
}
|
|
28
|
-
|
|
29
|
-
console.log("š Creating new CHANGELOG.md");
|
|
30
|
-
}
|
|
31
|
-
const today = new Date().toISOString().split("T")[0];
|
|
32
|
-
const newEntry = `\n\n## ${today}\n\n${result.content}`;
|
|
33
|
-
await fs.writeFile(changelogPath, existing + newEntry, "utf-8");
|
|
34
|
-
console.log("š CHANGELOG.md updated.");
|
|
66
|
+
return output;
|
|
35
67
|
}
|
|
36
68
|
catch (err) {
|
|
37
|
-
console.error("ā Failed to
|
|
69
|
+
console.error("ā Failed to generate changelog entry:", err.message);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function updateChangelogFile(entry) {
|
|
74
|
+
const root = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
75
|
+
const changelogPath = path.join(root, "CHANGELOG.md");
|
|
76
|
+
let existing = '';
|
|
77
|
+
try {
|
|
78
|
+
existing = await fs.readFile(changelogPath, 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
console.log("š Creating new CHANGELOG.md");
|
|
38
82
|
}
|
|
83
|
+
const today = new Date().toISOString().split("T")[0];
|
|
84
|
+
const newEntry = `\n\n## ${today}\n\n${entry.trim()}`;
|
|
85
|
+
await fs.writeFile(changelogPath, existing + newEntry, 'utf-8');
|
|
86
|
+
return changelogPath;
|
|
39
87
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
3
|
import readline from 'readline';
|
|
4
4
|
import { commitSuggesterModule } from '../pipeline/modules/commitSuggesterModule.js';
|
|
5
|
+
import { generateChangelogEntry, updateChangelogFile } from './ChangeLogUpdateCmd.js';
|
|
6
|
+
import { askChangelogApproval } from '../utils/changeLogPrompt.js';
|
|
5
7
|
function askUserToChoose(suggestions) {
|
|
6
8
|
return new Promise((resolve) => {
|
|
7
9
|
console.log('\nš” AI-suggested commit messages:\n');
|
|
@@ -51,30 +53,48 @@ function promptCustomMessage() {
|
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
export async function suggestCommitMessage(options) {
|
|
56
|
+
// Ensure git diff does not print to console
|
|
54
57
|
try {
|
|
55
|
-
let diff = execSync("git diff", { encoding: "utf-8" }).trim();
|
|
58
|
+
let diff = execSync("git diff", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
56
59
|
if (!diff) {
|
|
57
|
-
diff = execSync("git diff --cached", { encoding: "utf-8" }).trim();
|
|
60
|
+
diff = execSync("git diff --cached", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
58
61
|
}
|
|
59
62
|
if (!diff) {
|
|
60
63
|
console.log('ā ļø No staged changes to suggest a message for.');
|
|
61
64
|
return;
|
|
62
65
|
}
|
|
66
|
+
if (options.changelog) {
|
|
67
|
+
let entryFinalized = false;
|
|
68
|
+
while (!entryFinalized) {
|
|
69
|
+
const changelogEntry = await generateChangelogEntry(diff);
|
|
70
|
+
if (!changelogEntry) {
|
|
71
|
+
console.log("ā¹ļø No changelog entry generated.");
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const userChoice = await askChangelogApproval(changelogEntry);
|
|
75
|
+
if (userChoice === 'yes') {
|
|
76
|
+
const changelogPath = await updateChangelogFile(changelogEntry);
|
|
77
|
+
execSync(`git add "${changelogPath}"`);
|
|
78
|
+
console.log("ā
CHANGELOG.md staged.");
|
|
79
|
+
entryFinalized = true;
|
|
80
|
+
}
|
|
81
|
+
else if (userChoice === 'redo') {
|
|
82
|
+
console.log("š Regenerating changelog entry...");
|
|
83
|
+
continue; // Loop again and regenerate
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.log("ā Skipped changelog update.");
|
|
87
|
+
entryFinalized = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Continue with commit suggestions
|
|
63
92
|
const response = await commitSuggesterModule.run({ content: diff });
|
|
64
93
|
const suggestions = response.suggestions || [];
|
|
65
94
|
if (!suggestions.length) {
|
|
66
|
-
console.log('ā ļø
|
|
95
|
+
console.log('ā ļø No commit suggestions generated.');
|
|
67
96
|
return;
|
|
68
97
|
}
|
|
69
|
-
// Show-only mode
|
|
70
|
-
if (!options.commit) {
|
|
71
|
-
console.log('\nš” AI-suggested commit messages:\n');
|
|
72
|
-
suggestions.slice(0, 3).forEach((msg, i) => {
|
|
73
|
-
console.log(`${i + 1}) ${msg}`);
|
|
74
|
-
});
|
|
75
|
-
process.exit(0); // ensure clean exit
|
|
76
|
-
}
|
|
77
|
-
// Commit mode with selection
|
|
78
98
|
let message = null;
|
|
79
99
|
while (message === null) {
|
|
80
100
|
const choice = await askUserToChoose(suggestions);
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { handleRefactor } from "./commands/RefactorCmd.js";
|
|
|
11
11
|
import { generateTests } from "./commands/TestGenCmd.js";
|
|
12
12
|
import { bootstrap } from './modelSetup.js';
|
|
13
13
|
import { summarizeFile } from "./commands/SummaryCmd.js";
|
|
14
|
-
import {
|
|
14
|
+
import { handleStandaloneChangelogUpdate } from './commands/ChangeLogUpdateCmd.js';
|
|
15
15
|
import { runModulePipelineFromCLI } from './commands/ModulePipelineCmd.js';
|
|
16
16
|
import { runIndexCommand } from './commands/IndexCmd.js';
|
|
17
17
|
import { resetDatabase } from './commands/ResetDbCmd.js';
|
|
@@ -25,7 +25,15 @@ const cmd = new Command('scai')
|
|
|
25
25
|
.version(version)
|
|
26
26
|
.option('--model <model>', 'Set the model to use (e.g., codellama:34b)')
|
|
27
27
|
.option('--lang <lang>', 'Set the target language (ts, java, rust)');
|
|
28
|
-
|
|
28
|
+
function defineSuggCommand(cmd) {
|
|
29
|
+
cmd
|
|
30
|
+
.command('sugg')
|
|
31
|
+
.description('Suggest a commit message from staged changes')
|
|
32
|
+
.option('-c, --commit', 'Automatically commit with suggested message')
|
|
33
|
+
.option('-l, --changelog', 'Generate and optionally stage a changelog entry')
|
|
34
|
+
.action((options) => suggestCommitMessage(options));
|
|
35
|
+
}
|
|
36
|
+
// š§ Main command group
|
|
29
37
|
cmd
|
|
30
38
|
.command('init')
|
|
31
39
|
.description('Initialize the model and download required models')
|
|
@@ -33,19 +41,12 @@ cmd
|
|
|
33
41
|
await bootstrap();
|
|
34
42
|
console.log('ā
Model initialization completed!');
|
|
35
43
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.description('Suggest a commit message from staged changes')
|
|
39
|
-
.option('-c, --commit', 'Automatically commit with suggested message')
|
|
40
|
-
.action(suggestCommitMessage);
|
|
44
|
+
// Register top-level `sugg` command
|
|
45
|
+
defineSuggCommand(cmd);
|
|
41
46
|
// š§ Group: Git-related commands
|
|
42
47
|
const git = cmd.command('git').description('Git utilities');
|
|
43
|
-
//
|
|
44
|
-
git
|
|
45
|
-
.command('sugg')
|
|
46
|
-
.description('Suggest a commit message from staged changes')
|
|
47
|
-
.option('-c, --commit', 'Automatically commit with suggested message')
|
|
48
|
-
.action(suggestCommitMessage);
|
|
48
|
+
// Register `sugg` under `git` group
|
|
49
|
+
defineSuggCommand(git);
|
|
49
50
|
// š ļø Group: `gen` commands for content generation
|
|
50
51
|
const gen = cmd.command('gen').description('Generate code-related output');
|
|
51
52
|
gen
|
|
@@ -57,7 +58,7 @@ gen
|
|
|
57
58
|
.command('changelog')
|
|
58
59
|
.description('Update or create the CHANGELOG.md based on current Git diff')
|
|
59
60
|
.action(async () => {
|
|
60
|
-
await
|
|
61
|
+
await handleStandaloneChangelogUpdate();
|
|
61
62
|
});
|
|
62
63
|
gen
|
|
63
64
|
.command('summ [file]')
|
|
@@ -6,30 +6,42 @@ export const changelogModule = {
|
|
|
6
6
|
async run(input) {
|
|
7
7
|
const model = Config.getModel();
|
|
8
8
|
const prompt = `
|
|
9
|
-
|
|
9
|
+
Using the Git diff below, return **only meaningful user-facing changes** as clean markdown bullet points.
|
|
10
|
+
|
|
11
|
+
Analyze the intent of each change and summarize it in plain English ā do not copy code or include explanations.
|
|
12
|
+
|
|
13
|
+
ONLY return the bullet points!
|
|
14
|
+
|
|
15
|
+
If no meaningful changes are present, return the text: "NO UPDATE".
|
|
10
16
|
|
|
11
17
|
--- DIFF START ---
|
|
12
18
|
${input.content}
|
|
13
19
|
--- DIFF END ---
|
|
14
|
-
|
|
15
|
-
ā
If the changes are significant, return a changelog entry.
|
|
16
|
-
ā If not, return ONLY: "NO UPDATE".
|
|
17
|
-
`.trim();
|
|
20
|
+
`.trim();
|
|
18
21
|
const response = await generate({ content: prompt }, model);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Check if we received a meaningful result or "NO UPDATE"
|
|
23
|
+
const content = response?.content?.trim();
|
|
24
|
+
if (content === 'NO UPDATE') {
|
|
25
|
+
console.log("ā ļø No meaningful updates found. Returning 'NO UPDATE'.");
|
|
26
|
+
return {
|
|
27
|
+
content: response.content,
|
|
28
|
+
summary: 'NO UPDATE',
|
|
24
29
|
suggestions: response?.suggestions ?? [],
|
|
25
|
-
filepath: input.filepath
|
|
30
|
+
filepath: input.filepath,
|
|
31
|
+
};
|
|
26
32
|
}
|
|
27
|
-
//
|
|
33
|
+
// Split the content into lines
|
|
34
|
+
const lines = content?.split('\n').map(line => line.trim()) ?? [];
|
|
35
|
+
// Filter out non-bullet lines (now including the 'ā¢' symbol)
|
|
36
|
+
const bulletLines = lines.filter(line => /^([*-+ā¢]|\d+\.)\s/.test(line));
|
|
37
|
+
// Join the filtered lines into the final changelog entry
|
|
38
|
+
const filtered = bulletLines.join('\n');
|
|
39
|
+
// Return the processed content and filtered changelog
|
|
28
40
|
return {
|
|
29
41
|
content: response.content,
|
|
30
|
-
summary,
|
|
42
|
+
summary: filtered,
|
|
31
43
|
suggestions: response?.suggestions ?? [],
|
|
32
44
|
filepath: input.filepath,
|
|
33
45
|
};
|
|
34
|
-
}
|
|
46
|
+
}
|
|
35
47
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/utils/changelogPrompt.ts
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
export async function askChangelogApproval(entry) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
console.log('\nš Proposed changelog entry:\n');
|
|
6
|
+
console.log(entry);
|
|
7
|
+
console.log('\n---');
|
|
8
|
+
console.log('1) ā
Accept and stage');
|
|
9
|
+
console.log('2) š Regenerate');
|
|
10
|
+
console.log('3) ā Skip changelog');
|
|
11
|
+
const rl = readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout,
|
|
14
|
+
});
|
|
15
|
+
rl.question('\nš Choose an option [1-3]: ', (answer) => {
|
|
16
|
+
rl.close();
|
|
17
|
+
switch (answer.trim()) {
|
|
18
|
+
case '1':
|
|
19
|
+
resolve('yes');
|
|
20
|
+
break;
|
|
21
|
+
case '2':
|
|
22
|
+
resolve('redo');
|
|
23
|
+
break;
|
|
24
|
+
default:
|
|
25
|
+
resolve('no');
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -2,7 +2,7 @@ import Database from 'better-sqlite3';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { IGNORED_EXTENSIONS } from '../config/IgnoredExtensions.js';
|
|
5
|
-
import { specificFileExceptions } from '
|
|
5
|
+
import { specificFileExceptions } from '../config/specificFileExceptions.js';
|
|
6
6
|
// THIS FILE IS MEANT TO BE RUN AS A NODE JS SCRIPT. node dist/src/utilsremoveIgnoredFiles.js
|
|
7
7
|
// It removes wrongly indexed files that don't add value to the model.
|
|
8
8
|
const DB_PATH = path.join(os.homedir(), '.scai', 'db.sqlite');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { IGNORED_EXTENSIONS } from '../config/IgnoredExtensions.js';
|
|
3
|
-
import { specificFileExceptions } from '../
|
|
3
|
+
import { specificFileExceptions } from '../config/specificFileExceptions.js';
|
|
4
4
|
export function shouldIgnoreFile(filePath) {
|
|
5
5
|
// Get file extension
|
|
6
6
|
const ext = path.extname(filePath).toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"scai": "./dist/index.js"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"llm"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"build": "tsc",
|
|
24
|
+
"build": "rm -rfd dist && tsc && git add .",
|
|
25
25
|
"start": "node dist/index.js"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
File without changes
|