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 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
- Suggests a meaningful commit message based on your staged code.
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/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.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": {