scai 0.1.31 → 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
@@ -1,6 +1,8 @@
1
- # ⚙️ scai — Semantic CLI Assistant
1
+ # ⚙️ scai — Smart Commit AI
2
+
3
+
4
+ > AI-powered CLI tool for commits and summaries — using local models.
2
5
 
3
- > AI-powered CLI tool for commits, summaries, search, questions, and more — using local models.
4
6
 
5
7
  **scai** is a privacy-first, local AI assistant for developers. It brings semantic understanding to your codebase, directly from the terminal:
6
8
 
@@ -9,7 +11,7 @@
9
11
  - 🧐 Summarize files in plain English
10
12
  - 📜 Auto-update your changelog
11
13
  - 🧪 Generate Jest tests (ALPHA)
12
- - 🔍 Search and ask questions across your codebase (BETA)
14
+ - 🔍 Search and ask questions across your codebase (ALPHA)
13
15
  - 🔐 100% local — no API keys, no cloud, no telemetry
14
16
 
15
17
  ---
@@ -17,7 +19,7 @@
17
19
  ## 🚀 Features
18
20
 
19
21
  - ⚡ Powered by open-source models (e.g. `llama3`, `codellama`)
20
- - 🔍 Full-text indexing & semantic search (BETA)
22
+ - 🔍 Full-text indexing & semantic search (ALPHA)
21
23
  - 🛠️ Built with Node.js and TypeScript
22
24
  - ✅ Easily configurable via CLI or global flags
23
25
 
@@ -57,20 +59,48 @@ scai runs entirely on your machine and doesn't require cloud APIs or API keys. T
57
59
 
58
60
  ### 🔧 Git Commit Suggestions
59
61
 
62
+ Use AI to suggest a meaningful commit message based on your staged code:
63
+
60
64
  ```bash
61
65
  git add .
62
66
  scai git sugg
63
67
  ```
64
68
 
65
- Suggests a meaningful commit message based on your staged code.
66
- To auto-commit:
69
+ To automatically commit with the selected suggestion:
67
70
 
68
71
  ```bash
69
72
  scai git sugg --commit
70
73
  ```
71
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
+
72
87
  ---
73
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
+
74
104
  ### 🛠️ Code Generation Commands (`gen` group)
75
105
 
76
106
  ```bash
@@ -115,7 +145,7 @@ scai stores settings in `~/.scai/config.json`. You can override or view them:
115
145
 
116
146
  <br>
117
147
 
118
- ## 🔁 Background Daemon and Indexing ⚠️ Beta Notice
148
+ ## 🔁 Background Daemon and Indexing ⚠️ ALPHA Notice
119
149
 
120
150
  These features are experimental and subject to change:
121
151
 
@@ -137,7 +167,7 @@ You won't gain much value from the index unless you scope it to one repository.
137
167
 
138
168
  ---
139
169
 
140
- ### 🔍 Codebase Search & Ask (BETA)
170
+ ### 🔍 Codebase Search & Ask (ALPHA)
141
171
 
142
172
  > **Important:** You must `index` a **code repository** first or `find` and `ask` have no context to work with.
143
173
 
@@ -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.31",
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": {