scai 0.1.32 โ†’ 0.1.34

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 CHANGED
@@ -7,10 +7,8 @@
7
7
  **scai** is a privacy-first, local AI assistant for developers. It brings semantic understanding to your codebase, directly from the terminal:
8
8
 
9
9
  - ๐Ÿ’ฌ Suggest intelligent Git commit messages
10
- - โœจ Comment and refactor code files
11
10
  - ๐Ÿง Summarize files in plain English
12
11
  - ๐Ÿ“œ Auto-update your changelog
13
- - ๐Ÿงช Generate Jest tests (ALPHA)
14
12
  - ๐Ÿ” Search and ask questions across your codebase (ALPHA)
15
13
  - ๐Ÿ” 100% local โ€” no API keys, no cloud, no telemetry
16
14
 
@@ -33,6 +31,7 @@ scai runs entirely on your machine and doesn't require cloud APIs or API keys. T
33
31
  - โœ… **Privacy-first**: no telemetry, no server round-trips
34
32
  - โœ… **EU & GDPR-friendly**: designed with compliance in mind
35
33
  - โœ… **Developer control**: full transparency and override options
34
+ - โœ… **Offline support**: works even without an internet connection
36
35
 
37
36
  ---
38
37
 
@@ -59,20 +58,48 @@ scai runs entirely on your machine and doesn't require cloud APIs or API keys. T
59
58
 
60
59
  ### ๐Ÿ”ง Git Commit Suggestions
61
60
 
61
+ Use AI to suggest a meaningful commit message based on your staged code:
62
+
62
63
  ```bash
63
64
  git add .
64
65
  scai git sugg
65
66
  ```
66
67
 
67
- Suggests a meaningful commit message based on your staged code.
68
- To auto-commit:
68
+ To automatically commit with the selected suggestion:
69
69
 
70
70
  ```bash
71
71
  scai git sugg --commit
72
72
  ```
73
73
 
74
+ You can also include a changelog entry along with the commit:
75
+
76
+ ```bash
77
+ scai git sugg --commit --changelog
78
+ ```
79
+
80
+ This will:
81
+ 1. Suggest a commit message based on your `git diff --cached`
82
+ 2. Propose a changelog entry (if relevant)
83
+ 3. Allow you to approve, regenerate, or skip the changelog
84
+ 4. Automatically stage and commit the changes
85
+
74
86
  ---
75
87
 
88
+ ### ๐Ÿ“ Generate a Standalone Changelog Entry
89
+
90
+ If you want to generate a changelog entry without committing:
91
+
92
+ ```bash
93
+ scai gen changelog
94
+ ```
95
+
96
+ This will:
97
+ - Analyze the current `git diff` (staged or unstaged)
98
+ - Propose a list of **user-facing changes** in clean markdown bullet points
99
+ - Let you accept, regenerate, or skip the update
100
+ - Append the entry to `CHANGELOG.md` and stage it if accepted
101
+
102
+
76
103
  ### ๐Ÿ› ๏ธ Code Generation Commands (`gen` group)
77
104
 
78
105
  ```bash
@@ -1,39 +1,87 @@
1
- // src/commands/handleChangelogUpdate.ts
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
- export async function handleChangelogUpdate() {
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
- let diff = execSync("git diff", { encoding: "utf-8" }).trim();
45
+ // If no diff is provided, fetch the current diff from git silently
10
46
  if (!diff) {
11
- diff = execSync("git diff --cached", { encoding: "utf-8" }).trim();
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
- console.log("โš ๏ธ No staged or unstaged changes to include in changelog.");
15
- return;
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
- if (!result.content.trim()) {
19
- console.log("โœ… No significant changes for changelog.");
20
- return;
21
- }
22
- const root = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
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
- catch {
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 update changelog:", err.message);
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('โš ๏ธ No commit suggestions generated.');
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 { handleChangelogUpdate } from './commands/ChangeLogUpdateCmd.js';
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
- // ๐Ÿš€ Init command
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
- cmd
37
- .command('sugg')
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
- // The sugg command under the 'git' group
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 handleChangelogUpdate();
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
- You're an experienced changelog writer. Based on this Git diff, write a markdown bullet-point entry suitable for CHANGELOG.md:
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
- 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,
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
- // Return the actual changelog summary and any suggestions
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
  };
@@ -1,7 +1,7 @@
1
1
  export async function runModulePipeline(modules, input) {
2
2
  let current = input;
3
3
  // Add flag or condition for logging (optional)
4
- const isDebug = true;
4
+ const isDebug = false;
5
5
  if (isDebug) {
6
6
  console.log('Input: ', input);
7
7
  }
@@ -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 './specificFileExceptions.js';
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 '../utils/specificFileExceptions.js';
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.32",
3
+ "version": "0.1.34",
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": {