scai 0.1.12 → 0.1.14

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,41 +1,34 @@
1
1
  # ⚙️ scai — Smart Commit AI ✨
2
2
 
3
- > AI-powered CLI tools for smart commit messages, auto generated comments, and readme files — all powered by local models.
3
+ > AI-powered CLI tools for smart commit messages, code comments, summaries, and changelogs — all powered by local models.
4
4
 
5
5
  **scai** (Smart Commit AI) is a lightweight, privacy-focused CLI tool that uses local AI models (via [Ollama](https://ollama.com)) to help developers work faster and cleaner:
6
6
 
7
- - 🤖 Suggest high-quality Git commit messages
8
- - ✨ Comments your code automatically
9
- - 🧠 Get a summary of any code file directly to the terminal
7
+ - 💬 Suggest high-quality Git commit messages
8
+ - ✨ Automatically comment your code
9
+ - 🧠 Summarize code files instantly in the terminal
10
+ - 📝 Generate changelog entries based on Git diffs
10
11
  - 🔒 100% local — no API keys, no cloud, no telemetry
11
12
 
12
13
  ---
13
14
 
14
15
  ## 🚀 Features
15
16
 
16
- - ⚡️ Powered by open local Ollama models like `mistral` and new `commentModule`
17
+ - ⚡️ Powered by local Ollama models like `llama3` and `codellama`
17
18
  - 🛠️ CLI built with Node.js + TypeScript
18
19
  - 🔒 No external services, full privacy by design
19
- - ✅ Now supports global options for model and language settings
20
-
21
- ### 📦 Recent Additions
22
-
23
- * **Summary Command**: Print a summary of the given file to the terminal (e.g., `scai summ <file>`)
20
+ - ✅ Global options for model and language selection
24
21
 
25
22
  ---
26
23
 
27
- ## ❤️ Philosophy: Why Local AI?
24
+ ## ❤️ Why Local AI?
28
25
 
29
26
  We believe your code — and your workflow — should stay **yours**. scai runs entirely on your machine using open-source models and tools.
30
27
 
31
- No internet connection. No vendor lock-in. Just local, private, AI-enhanced developer experience.
32
-
33
28
  ✅ Works entirely offline
34
- ✅ No API keys, cloud accounts, or telemetry
29
+ ✅ No API keys or cloud dependencies
35
30
  ✅ Backed by open-source models
36
- ✅ Designed for CLI-first devs and scriptable workflows
37
-
38
- **scai** follows the Unix philosophy — small, composable tools with powerful output.
31
+ ✅ Designed for CLI-first developers
39
32
 
40
33
  ---
41
34
 
@@ -44,28 +37,27 @@ No internet connection. No vendor lock-in. Just local, private, AI-enhanced deve
44
37
  1. **Install [Ollama](https://ollama.com)**
45
38
  - On **Windows**: [Download Ollama](https://ollama.com/download)
46
39
  - On **macOS**:
47
- - Install via Homebrew:
40
+ - Via Homebrew:
48
41
  ```bash
49
42
  brew install ollama
50
43
  ```
51
- - Or download directly from the [Ollama website](https://ollama.com/download)
52
- - Ensure it’s added to your system `PATH`
44
+ - Or download directly from the website
53
45
 
54
- 2. **Install scai globally via npm:**
46
+ 2. **Install scai globally:**
55
47
 
56
48
  ```bash
57
49
  npm install -g scai
58
50
  ```
59
51
 
60
- 3. **Run the initialization step to start Ollama and install models:**
52
+ 3. **Initialize the tool and models:**
61
53
 
62
54
  ```bash
63
55
  scai init
64
56
  ```
65
57
 
66
58
  This will:
67
- - Launch the Ollama background server (if not running)
68
- - Pull the required models (`llama3`, `mistral`) if they aren’t present
59
+ - Start the Ollama background server
60
+ - Download required models (`llama3`, etc.)
69
61
 
70
62
  ---
71
63
 
@@ -74,10 +66,7 @@ This will:
74
66
  ### 💬 Suggest a commit message
75
67
 
76
68
  ```bash
77
- # Stage your changes
78
69
  git add .
79
-
80
- # Let scai suggest a commit message
81
70
  scai sugg
82
71
  ```
83
72
 
@@ -86,7 +75,7 @@ scai sugg
86
75
  feat(api): add error handling to user service
87
76
  ```
88
77
 
89
- To automatically commit with the suggested message:
78
+ To commit automatically with the suggestion:
90
79
 
91
80
  ```bash
92
81
  scai sugg --commit
@@ -94,41 +83,59 @@ scai sugg --commit
94
83
 
95
84
  ---
96
85
 
97
- ### 🛠 Comment a code file
86
+ ### Comment a code file
98
87
 
99
88
  ```bash
100
- scai comment <file>
89
+ scai comm <file>
101
90
  ```
102
91
 
103
- Write comments for the given file. Optional flags:
104
- - `-a, --apply` - Apply the refactored version to the original file
92
+ Adds clear, helpful comments to the code. Optional flag:
93
+ - `-a, --apply` Overwrite the original file with the commented version
105
94
 
106
95
  ---
107
96
 
108
- ### 🔍 Code summary
97
+ ### 🔍 Summarize a code file
109
98
 
110
99
  ```bash
111
100
  scai summ <file>
112
101
  ```
113
102
 
114
- Create a quick summary of the file your working on to gain fast insights.
103
+ Prints a summary of what the code does directly to your terminal.
104
+
105
+ To pipe input for summarization:
106
+
107
+ ```bash
108
+ cat <file> | scai summ
109
+ ```
110
+
111
+
112
+ ---
113
+
114
+ ### 📝 Update the changelog
115
+
116
+ ```bash
117
+ scai changelog
118
+ ```
119
+
120
+ Analyzes the current Git diff and updates (or creates) a `CHANGELOG.md` file with relevant public-facing changes.
115
121
 
116
122
  ---
117
123
 
118
124
  ## 🔐 License & Fair Use
119
125
 
120
- **scai is free to use** for individuals, teams, and companies — including in commercial work.
126
+ **scai is free to use** for individuals, teams, and companies — including in commercial work.
127
+
121
128
  You may:
122
129
 
123
130
  - ✅ Use it internally in your projects
124
- - ✅ Use it at work or in commercial software development
125
- - ✅ Share and recommend it to colleagues
131
+ - ✅ Use it at work or in commercial development
132
+ - ✅ Share and recommend it
126
133
 
127
- However:
134
+ But you may not:
128
135
 
129
- - ❌ You may not **resell**, repackage, or redistribute **scai** as a commercial product or SaaS offering
130
- - ❌ You may not claim ownership or original authorship
136
+ - ❌ Repackage or resell **scai** as a product or SaaS
137
+ - ❌ Claim ownership of the tool
131
138
 
132
- For full terms, see the [LICENSE](./LICENSE) file.
139
+ See [LICENSE](./LICENSE) for full terms.
133
140
 
134
- ---
141
+ ---
@@ -0,0 +1,39 @@
1
+ // src/commands/handleChangelogUpdate.ts
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { runPromptPipeline } from '../pipeline/runPipeline.js';
6
+ import { changelogModule } from '../pipeline/modules/changeLogModule.js';
7
+ export async function handleChangelogUpdate() {
8
+ try {
9
+ let diff = execSync("git diff", { encoding: "utf-8" }).trim();
10
+ if (!diff) {
11
+ diff = execSync("git diff --cached", { encoding: "utf-8" }).trim();
12
+ }
13
+ if (!diff) {
14
+ console.log("⚠️ No staged or unstaged changes to include in changelog.");
15
+ return;
16
+ }
17
+ const result = await runPromptPipeline([changelogModule], { code: diff });
18
+ if (!result.code.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");
27
+ }
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.code}`;
33
+ await fs.writeFile(changelogPath, existing + newEntry, "utf-8");
34
+ console.log("📝 CHANGELOG.md updated.");
35
+ }
36
+ catch (err) {
37
+ console.error("❌ Failed to update changelog:", err.message);
38
+ }
39
+ }
@@ -1,11 +1,14 @@
1
+ // File: src/commands/CommitSuggesterCmd.ts
1
2
  import { execSync } from 'child_process';
2
3
  import readline from 'readline';
4
+ import { commitSuggesterModule } from '../pipeline/modules/commitSuggesterModule.js';
3
5
  function askUserToChoose(suggestions) {
4
6
  return new Promise((resolve) => {
5
7
  console.log('\n💡 AI-suggested commit messages:\n');
6
8
  suggestions.forEach((msg, i) => {
7
9
  console.log(`${i + 1}) ${msg}`);
8
10
  });
11
+ console.log('\n---');
9
12
  console.log(`${suggestions.length + 1}) 🔁 Regenerate suggestions`);
10
13
  console.log(`${suggestions.length + 2}) ✍️ Write your own commit message`);
11
14
  const rl = readline.createInterface({
@@ -23,32 +26,11 @@ function askUserToChoose(suggestions) {
23
26
  resolve('custom');
24
27
  }
25
28
  else {
26
- resolve(choice - 1); // Return 0-based index (0 to 3)
29
+ resolve(choice - 1);
27
30
  }
28
31
  });
29
32
  });
30
33
  }
31
- async function generateSuggestions(prompt) {
32
- const res = await fetch("http://localhost:11434/api/generate", {
33
- method: "POST",
34
- headers: { "Content-Type": "application/json" },
35
- body: JSON.stringify({
36
- model: "llama3",
37
- prompt,
38
- stream: false,
39
- }),
40
- });
41
- const { response } = await res.json();
42
- if (!response || typeof response !== 'string') {
43
- throw new Error('Invalid LLM response');
44
- }
45
- const lines = response.trim().split('\n').filter(line => /^\d+\.\s+/.test(line));
46
- const messages = lines.map(line => line.replace(/^\d+\.\s+/, '').replace(/^"(.*)"$/, '$1').trim());
47
- if (messages.length === 0) {
48
- throw new Error('No valid commit messages found in LLM response.');
49
- }
50
- return messages;
51
- }
52
34
  function promptCustomMessage() {
53
35
  return new Promise((resolve) => {
54
36
  const rl = readline.createInterface({
@@ -71,19 +53,12 @@ export async function suggestCommitMessage(options) {
71
53
  console.log('⚠️ No staged changes to suggest a message for.');
72
54
  return;
73
55
  }
74
- const prompt = `Suggest 3 concise, conventional Git commit message options for this diff. Return ONLY the commit messages, numbered 1 to 3, like so:
75
- 1. ...
76
- 2. ...
77
- 3. ...
78
-
79
- Here is the diff:
80
- ${diff}`;
56
+ let suggestions = [];
81
57
  let message = null;
82
58
  while (message === null) {
83
- const suggestions = await generateSuggestions(prompt);
59
+ suggestions = await commitSuggesterModule.run({ code: diff }).then(out => out.suggestions || []);
84
60
  const choice = await askUserToChoose(suggestions);
85
61
  if (choice === suggestions.length) {
86
- // User chose "Regenerate"
87
62
  console.log('\n🔄 Regenerating suggestions...\n');
88
63
  continue;
89
64
  }
@@ -1,12 +1,36 @@
1
- // File: src/commands/SummaryCmd.ts
2
1
  import fs from 'fs/promises';
3
2
  import { summaryModule } from '../pipeline/modules/summaryModule.js';
3
+ import readline from 'readline';
4
4
  export async function summarizeFile(filepath) {
5
- try {
6
- const code = await fs.readFile(filepath, 'utf-8');
5
+ let code = '';
6
+ if (filepath) {
7
+ try {
8
+ code = await fs.readFile(filepath, 'utf-8');
9
+ }
10
+ catch (err) {
11
+ console.error(`❌ Could not read or summarize ${filepath}:`, err.message);
12
+ return;
13
+ }
14
+ }
15
+ // ⬇️ Add this check right here
16
+ else if (process.stdin.isTTY) {
17
+ console.error('❌ No file provided and no piped input.\n👉 Usage: scai summ <file> or cat file | scai summ');
18
+ return;
19
+ }
20
+ else {
21
+ const rl = readline.createInterface({
22
+ input: process.stdin,
23
+ output: process.stdout,
24
+ terminal: false,
25
+ });
26
+ for await (const line of rl) {
27
+ code += line + '\n';
28
+ }
29
+ }
30
+ if (code.trim()) {
7
31
  await summaryModule.run({ code });
8
32
  }
9
- catch (err) {
10
- console.error(`❌ Could not read or summarize ${filepath}:`, err.message);
33
+ else {
34
+ console.error('❌ No code provided to summarize.');
11
35
  }
12
36
  }
@@ -1,16 +1,23 @@
1
1
  export class ModelConfig {
2
2
  static setModel(model) {
3
3
  this.model = model;
4
+ console.log(`📦 Model set to: ${model}`);
4
5
  }
5
6
  static getModel() {
6
7
  return this.model;
7
8
  }
8
9
  static setLanguage(lang) {
9
10
  this.language = lang;
11
+ console.log(`🗣️ Language set to: ${lang}`);
10
12
  }
11
13
  static getLanguage() {
12
14
  return this.language;
13
15
  }
16
+ static logCurrentConfig() {
17
+ console.log(`🔧 Current configuration:`);
18
+ console.log(` Model : ${this.model}`);
19
+ console.log(` Language: ${this.language}`);
20
+ }
14
21
  }
15
22
  ModelConfig.model = 'codellama:7b';
16
23
  ModelConfig.language = 'ts';
package/dist/index.js CHANGED
@@ -7,11 +7,11 @@ import { checkEnv } from "./commands/EnvCmd.js";
7
7
  import { checkGit } from "./commands/GitCmd.js";
8
8
  import { suggestCommitMessage } from "./commands/CommitSuggesterCmd.js";
9
9
  import { handleRefactor } from "./commands/RefactorCmd.js";
10
- import { updateReadmeIfNeeded } from "./commands/ReadmeCmd.js";
11
10
  import { generateTests } from "./commands/TestGenCmd.js";
12
11
  import { bootstrap } from './modelSetup.js';
13
12
  import { ModelConfig } from './config/ModelConfig.js';
14
13
  import { summarizeFile } from "./commands/SummaryCmd.js";
14
+ import { handleChangelogUpdate } from './commands/ChangeLogUpdateCmd.js';
15
15
  // Create the CLI instance
16
16
  const cmd = new Command('scai')
17
17
  .version(version)
@@ -36,11 +36,13 @@ cmd
36
36
  .option('-a, --apply', 'Apply the refactored version to the original file')
37
37
  .action((file, options) => handleRefactor(file, options));
38
38
  cmd
39
- .command('readme')
40
- .description('Update README.md if relevant changes were made')
41
- .action(updateReadmeIfNeeded);
39
+ .command('changelog')
40
+ .description('Update or create the CHANGELOG.md based on current Git diff')
41
+ .action(async () => {
42
+ await handleChangelogUpdate();
43
+ });
42
44
  cmd
43
- .command('summ <file>')
45
+ .command('summ [file]')
44
46
  .description('Print a summary of the given file to the terminal')
45
47
  .action((file) => summarizeFile(file));
46
48
  cmd
@@ -55,6 +57,12 @@ cmd
55
57
  .command('gen-tests <file>')
56
58
  .description('Generate a Jest test file for the specified JS/TS module')
57
59
  .action((file) => generateTests(file));
60
+ cmd
61
+ .command('config')
62
+ .description('Show the currently active model and language settings')
63
+ .action(() => {
64
+ ModelConfig.logCurrentConfig();
65
+ });
58
66
  // ✅ Now that commands are defined, parse args
59
67
  cmd.parse(process.argv);
60
68
  // ✅ Apply global options after parsing
@@ -0,0 +1,22 @@
1
+ import ora from 'ora';
2
+ export async function generate(prompt, model) {
3
+ const spinner = ora(`🧠 Thinking with ${model}...`).start();
4
+ try {
5
+ const res = await fetch('http://localhost:11434/api/generate', {
6
+ method: 'POST',
7
+ headers: { 'Content-Type': 'application/json' },
8
+ body: JSON.stringify({
9
+ model,
10
+ prompt,
11
+ stream: false,
12
+ }),
13
+ });
14
+ const data = await res.json();
15
+ spinner.succeed('✅ Model response received.');
16
+ return data.response?.trim() ?? '';
17
+ }
18
+ catch (err) {
19
+ spinner.fail('❌ Model request failed.');
20
+ throw err;
21
+ }
22
+ }
@@ -0,0 +1,21 @@
1
+ import { ModelConfig } from '../../config/ModelConfig.js';
2
+ import { generate } from '../../lib/generate.js';
3
+ export const changelogModule = {
4
+ name: 'changelogModule',
5
+ description: 'Generates changelog entry based on Git diff',
6
+ async run(input) {
7
+ const model = ModelConfig.getModel();
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:
10
+
11
+ --- DIFF START ---
12
+ ${input.code}
13
+ --- DIFF END ---
14
+
15
+ ✅ If the changes are significant, return a changelog entry.
16
+ ❌ If not, return ONLY: "NO UPDATE".
17
+ `.trim();
18
+ const output = await generate(prompt, model);
19
+ return { code: output === 'NO UPDATE' ? '' : output };
20
+ },
21
+ };
@@ -1,4 +1,5 @@
1
1
  import { ModelConfig } from '../../config/ModelConfig.js';
2
+ import { generate } from '../../lib/generate.js';
2
3
  export const addCommentsModule = {
3
4
  name: 'addComments',
4
5
  description: 'Adds meaningful // comments to each block of code',
@@ -6,26 +7,22 @@ export const addCommentsModule = {
6
7
  const model = ModelConfig.getModel();
7
8
  const lang = ModelConfig.getLanguage();
8
9
  const prompt = `
9
- You are a senior ${lang.toUpperCase()} engineer.
10
+ You are a senior ${lang.toUpperCase()} engineer reviewing source code.
10
11
 
11
- Add clear and helpful single-line // comments to complex parts of the code.
12
- - Do NOT remove or change in ANY way any parts of the code!
13
- - Output ALL of the original code with your comments included
12
+ Your task is to add clear and helpful single-line // comments to complex or non-obvious parts of the code.
13
+
14
+ ⚠️ VERY IMPORTANT RULES:
15
+ - You MUST return the ENTIRE original code.
16
+ - You MUST NOT remove, replace, reformat, or alter ANY code.
17
+ - Only add single-line // comments in appropriate places.
18
+ - Do NOT wrap the code in markdown or code blocks.
19
+ - The code should be valid ${lang.toUpperCase()} after your changes.
14
20
 
15
21
  --- CODE START ---
16
22
  ${input.code}
17
23
  --- CODE END ---
18
24
  `.trim();
19
- const res = await fetch('http://localhost:11434/api/generate', {
20
- method: 'POST',
21
- headers: { 'Content-Type': 'application/json' },
22
- body: JSON.stringify({
23
- model,
24
- prompt,
25
- stream: false,
26
- }),
27
- });
28
- const data = await res.json();
29
- return { code: data.response?.trim() ?? '' };
25
+ const output = await generate(prompt, model);
26
+ return { code: output === 'NO UPDATE' ? '' : output };
30
27
  },
31
28
  };
@@ -0,0 +1,30 @@
1
+ import { generate } from '../../lib/generate.js';
2
+ import { ModelConfig } from '../../config/ModelConfig.js';
3
+ export const commitSuggesterModule = {
4
+ name: 'commitSuggester',
5
+ description: 'Suggests conventional commit messages from Git diff',
6
+ async run({ code }) {
7
+ const model = ModelConfig.getModel();
8
+ const prompt = `
9
+ Suggest 3 concise, conventional Git commit messages based on this diff.
10
+ Use this format ONLY:
11
+
12
+ 1. feat: ...
13
+ 2. fix: ...
14
+ 3. refactor: ...
15
+
16
+ Here is the diff:
17
+ ${code}
18
+ `.trim();
19
+ const raw = await generate(prompt, model);
20
+ const lines = raw
21
+ .split('\n')
22
+ .map(line => line.trim())
23
+ .filter(line => /^\d+\.\s+/.test(line));
24
+ const suggestions = lines.map(line => line.replace(/^\d+\.\s+/, '').replace(/^"(.*)"$/, '$1').trim());
25
+ return {
26
+ code,
27
+ suggestions
28
+ };
29
+ }
30
+ };
@@ -1,12 +1,16 @@
1
+ // src/pipeline/modules/generateTestsModule.ts
1
2
  import fs from 'fs/promises';
2
3
  import path from 'path';
3
4
  import { ModelConfig } from '../../config/ModelConfig.js';
5
+ import { generate } from '../../lib/generate.js';
4
6
  export const generateTestsModule = {
5
7
  name: 'generateTests',
6
8
  description: 'Generate a Jest test file for the class/module',
7
9
  async run({ code, filepath }) {
8
10
  const model = ModelConfig.getModel();
9
11
  const lang = ModelConfig.getLanguage();
12
+ if (!filepath)
13
+ throw new Error('Missing filepath in pipeline context');
10
14
  const prompt = `
11
15
  You are a senior ${lang.toUpperCase()} engineer. Given the following class or module, generate a Jest test file.
12
16
 
@@ -20,22 +24,9 @@ Guidelines:
20
24
  ${code}
21
25
  --- CODE END ---
22
26
  `.trim();
23
- const res = await fetch('http://localhost:11434/api/generate', {
24
- method: 'POST',
25
- headers: { 'Content-Type': 'application/json' },
26
- body: JSON.stringify({
27
- model,
28
- prompt,
29
- stream: false
30
- })
31
- });
32
- const data = await res.json();
33
- const testCode = data.response?.trim();
27
+ const testCode = await generate(prompt, model);
34
28
  if (!testCode)
35
29
  throw new Error('⚠️ No test code returned from model');
36
- // Determine test file path next to refactored file
37
- if (!filepath)
38
- throw new Error('Missing filepath in pipeline context');
39
30
  const { dir, name } = path.parse(filepath);
40
31
  const testPath = path.join(dir, `${name}.test.ts`);
41
32
  await fs.writeFile(testPath, testCode, 'utf-8');
@@ -1,4 +1,5 @@
1
1
  import { ModelConfig } from '../../config/ModelConfig.js';
2
+ import { generate } from '../../lib/generate.js';
2
3
  export const refactorModule = {
3
4
  name: 'refactor',
4
5
  description: 'Break code into small, clean functions',
@@ -9,37 +10,19 @@ export const refactorModule = {
9
10
  You are a senior ${lang.toUpperCase()} engineer.
10
11
 
11
12
  Refactor the following code:
12
- - Refactor only long and complex functions
13
- - Keep original names and semantics.
13
+ - Only split up long and complex functions
14
+ - Preserve original names and semantics
14
15
  - Do NOT insert comments
16
+ - Output the full, valid ${lang.toUpperCase()} code
15
17
 
16
18
  --- CODE START ---
17
19
  ${input.code}
18
20
  --- CODE END ---
19
- `.trim();
20
- try {
21
- const res = await fetch('http://localhost:11434/api/generate', {
22
- method: 'POST',
23
- headers: { 'Content-Type': 'application/json' },
24
- body: JSON.stringify({
25
- model,
26
- prompt,
27
- stream: false
28
- })
29
- });
30
- if (!res.ok) {
31
- const errBody = await res.text();
32
- throw new Error(`❌ Refactor failed: ${res.status} ${res.statusText} - ${errBody}`);
33
- }
34
- const data = await res.json();
35
- if (!data.response) {
36
- throw new Error('❌ No response field returned from model.');
37
- }
38
- return { code: data.response.trim() };
39
- }
40
- catch (err) {
41
- console.error('🔥 Error in refactorModule:', err);
42
- throw new Error('❌ Error in refactor module: ' + err.message);
21
+ `.trim();
22
+ const response = await generate(prompt, model);
23
+ if (!response) {
24
+ throw new Error('❌ Model returned empty response for refactoring.');
43
25
  }
26
+ return { code: response };
44
27
  }
45
28
  };
@@ -1,4 +1,5 @@
1
1
  import { ModelConfig } from '../../config/ModelConfig.js';
2
+ import { generate } from '../../lib/generate.js';
2
3
  export const summaryModule = {
3
4
  name: 'summary',
4
5
  description: 'Prints a summary of changes to the terminal',
@@ -8,27 +9,26 @@ export const summaryModule = {
8
9
  const prompt = `
9
10
  You are a senior ${lang.toUpperCase()} engineer.
10
11
 
11
- Analyze the code below and return a concise summary of its functionality or structure.
12
- Only return the summary in this format:
12
+ Take the following source code and do NOT modify it in any way. Your task is:
13
+
14
+ 1. Output the original code exactly as it is.
15
+ 2. After the code, append a short summary in this format:
13
16
 
14
17
  // Summary of code:
15
- // - [Bullet 1]
16
- // - [Bullet 2]
17
- // - etc.
18
+ // - [What the code does at a high level]
19
+ // - [Main features or components]
20
+ // - [Any interesting logic or patterns]
18
21
 
19
- Return the original code after the summary.
22
+ ⚠️ IMPORTANT:
23
+ - Do NOT omit or alter any part of the original code.
24
+ - Do NOT insert comments inside the code.
25
+ - Only append the summary AFTER the full code.
20
26
 
21
27
  --- CODE START ---
22
28
  ${code}
23
29
  --- CODE END ---
24
- `.trim();
25
- const res = await fetch('http://localhost:11434/api/generate', {
26
- method: 'POST',
27
- headers: { 'Content-Type': 'application/json' },
28
- body: JSON.stringify({ model, prompt, stream: false })
29
- });
30
- const data = await res.json();
31
- const summary = data.response?.trim();
30
+ `.trim();
31
+ const summary = await generate(prompt, model);
32
32
  if (summary) {
33
33
  console.log('\n📝 Code Summary:\n');
34
34
  console.log(summary);
@@ -36,7 +36,7 @@ ${code}
36
36
  else {
37
37
  console.warn('⚠️ No summary generated.');
38
38
  }
39
- // Return unchanged input so it’s pipe-safe, if reused in pipelines
39
+ // Return unchanged input for composability
40
40
  return { code };
41
41
  }
42
42
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -24,7 +24,8 @@
24
24
  "start": "node dist/index.js"
25
25
  },
26
26
  "dependencies": {
27
- "commander": "^11.0.0"
27
+ "commander": "^11.0.0",
28
+ "ora": "^8.2.0"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@types/jest": "^30.0.0",
@@ -1,106 +0,0 @@
1
- import { execSync } from 'child_process';
2
- import readline from 'readline';
3
- async function askUserToChoose(suggestions) {
4
- return new Promise((resolve) => {
5
- console.log('\n💡 AI-suggested commit messages:\n'); // Display all suggestions to the user
6
- suggestions.forEach((msg, i) => {
7
- console.log(`${i + 1}) ${msg}`);
8
- });
9
- console.log(`${suggestions.length + 1}) 🔁 Regenerate suggestions`); // Allow the user to regenerate suggestions if they want
10
- console.log(`${suggestions.length + 2}) ✍️ Write your own commit message`); // Allow the user to write their own commit message
11
- const rl = readline.createInterface({
12
- input: process.stdin,
13
- output: process.stdout,
14
- });
15
- rl.question(`\n👉 Choose a commit message [1-${suggestions.length + 2}]: `, // Ask the user to choose from the suggestions or write their own
16
- (answer) => {
17
- rl.close();
18
- const choice = parseInt(answer, 10); // Parse the user's input as a number
19
- if (isNaN(choice) || choice < 1 || choice > suggestions.length + 2) { // Check that the user has entered a valid number
20
- console.log('⚠️ Invalid selection. Using the first suggestion by default.'); // Default to the first suggestion if the input is invalid
21
- resolve(0); // Resolve the promise with the 0-based index (0 to 3)
22
- }
23
- else if (choice === suggestions.length + 2) { // If the user has chosen "Write your own commit message"
24
- resolve('custom'); // Return 'custom' to indicate that the user wants to write their own commit message
25
- }
26
- else {
27
- resolve(choice - 1); // Return the 0-based index (0 to 3) of the selected suggestion
28
- }
29
- });
30
- });
31
- }
32
- async function generateSuggestions(prompt) {
33
- const res = await fetch("http://localhost:11434/api/generate", {
34
- method: "POST",
35
- headers: { "Content-Type": "application/json" },
36
- body: JSON.stringify({
37
- model: "llama3",
38
- prompt,
39
- stream: false,
40
- }),
41
- });
42
- const { response } = await res.json(); // Parse the response from the API as JSON
43
- if (!response || typeof response !== 'string') { // Check that the response is valid
44
- throw new Error('Invalid LLM response'); // Throw an error if the response is invalid
45
- }
46
- const lines = response.trim().split('\n').filter(line => /^\d+\.\s+/.test(line)); // Split the response into individual lines and filter out any lines that don't start with a number followed by a period
47
- const messages = lines.map(line => // Map each line to its commit message suggestion
48
- line.replace(/^\d+\.\s+/, '').replace(/^"(.*)"$/, '$1').trim());
49
- if (messages.length === 0) { // Check that there are any commit message suggestions
50
- throw new Error('No valid commit messages found in LLM response.'); // Throw an error if there are no valid commit message suggestions
51
- }
52
- return messages; // Return the array of commit message suggestions
53
- }
54
- function promptCustomMessage() {
55
- return new Promise((resolve) => {
56
- const rl = readline.createInterface({
57
- input: process.stdin,
58
- output: process.stdout,
59
- });
60
- rl.question('\n📝 Enter your custom commit message:\n> ', (input) => {
61
- rl.close();
62
- resolve(input.trim()); // Resolve the promise with the trimmed input string
63
- });
64
- });
65
- }
66
- export async function suggestCommitMessage(options) {
67
- try {
68
- let diff = execSync("git diff", { encoding: "utf-8" }).trim(); // Get the current Git diff
69
- if (!diff) { // Check that there are any changes to stage
70
- diff = execSync("git diff --cached", { encoding: "utf-8" }).trim(); // If there are no staged changes, check for unstaged changes
71
- }
72
- if (!diff) { // If there are still no changes, display a message and return early
73
- console.log('⚠️ No staged changes to suggest a message for.');
74
- return;
75
- }
76
- const prompt = `Suggest 3 concise, conventional Git commit message options for this diff. Return ONLY the commit messages, numbered 1 to 3, like so:
77
- 1. ...
78
- 2. ...
79
- 3. ...
80
-
81
- Here is the diff:
82
- ${diff}`; // Create a prompt string for the user to enter their own commit message suggestions
83
- let message = null;
84
- while (message === null) { // Loop until the user has entered a valid commit message or chosen to write their own
85
- const suggestions = await generateSuggestions(prompt); // Generate commit message suggestions based on the diff output from Git
86
- const choice = await askUserToChoose(suggestions); // Ask the user to choose from the suggestions or write their own
87
- if (choice === 'custom') { // If the user has chosen "Write your own commit message"
88
- message = await promptCustomMessage(); // Prompt the user for their custom commit message
89
- break; // Break out of the loop and use the custom commit message
90
- }
91
- message = suggestions[choice]; // Use the selected suggestion as the commit message
92
- }
93
- console.log(`\n✅ Selected commit message:\n${message}\n`); // Display the selected commit message to the user
94
- if (options.commit) { // If the user has specified that they want to commit the changes
95
- const commitDiff = execSync("git diff", { encoding: "utf-8" }).trim(); // Get the current Git diff again, in case anything has changed since the last call to `generateSuggestions`
96
- if (commitDiff) { // Check that there are any staged changes
97
- execSync("git add .", { encoding: "utf-8" }); // Stage all changes before committing
98
- }
99
- execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' }); // Commit the changes with the selected commit message
100
- console.log('✅ Committed with selected message.'); // Display a success message to the user
101
- }
102
- }
103
- catch (err) { // If there is an error in the code
104
- console.error('❌ Error in commit message suggestion:', err); // Log the error to the console
105
- }
106
- }
@@ -1,146 +0,0 @@
1
- "use strict";
2
- Here;
3
- 's a Jest test file for the `checkEnv` function: `` `typescript
4
- import { checkEnv } from "../src/utils";
5
-
6
- describe("checkEnv", () => {
7
- it("should warn when missing required env vars", () => {
8
- const processEnv = Object.assign({}, process.env);
9
- delete processEnv.DB_HOST;
10
- delete processEnv.API_KEY;
11
- const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
12
- checkEnv();
13
- expect(consoleWarnSpy).toHaveBeenCalledWith("❌ Missing env vars: DB_HOST, API_KEY");
14
- consoleWarnSpy.mockRestore();
15
- });
16
-
17
- it("should log a message when all required env vars are set", () => {
18
- const processEnv = Object.assign({}, process.env);
19
- checkEnv();
20
- expect(console.log).toHaveBeenCalledWith("✅ All env vars are set");
21
- });
22
- });
23
- ` ``;
24
- In;
25
- this;
26
- test;
27
- file, we;
28
- first;
29
- var the = ;
30
- `checkEnv`;
31
- function from() { }
32
- the;
33
- contains;
34
- it.We;
35
- then;
36
- define;
37
- two;
38
- test;
39
- cases: one;
40
- for (when; required; environment)
41
- variables;
42
- are;
43
- missing;
44
- and;
45
- another;
46
- for (when; all; required)
47
- environment;
48
- variables;
49
- are;
50
- set.
51
- ;
52
- In;
53
- the;
54
- first;
55
- test;
56
- we;
57
- use `jest.spyOn()`;
58
- to;
59
- mock;
60
- the `console.warn()`;
61
- method;
62
- and;
63
- replace;
64
- it;
65
- with (a)
66
- spy;
67
- function () { }
68
- We;
69
- then;
70
- delete the `DB_HOST`;
71
- and `API_KEY`;
72
- environment;
73
- variables;
74
- and;
75
- call;
76
- the `checkEnv()`;
77
- function () { }
78
- Finally, we;
79
- check;
80
- that;
81
- the `console.warn()`;
82
- method;
83
- was;
84
- called;
85
- with (the)
86
- expected;
87
- message.
88
- ;
89
- In;
90
- the;
91
- second;
92
- test;
93
- we;
94
- simply;
95
- call;
96
- the `checkEnv()`;
97
- function without() { }
98
- deleting;
99
- any;
100
- environment;
101
- variables;
102
- and;
103
- check;
104
- that;
105
- the `console.log()`;
106
- method;
107
- was;
108
- called;
109
- with (the)
110
- expected;
111
- message.
112
- ;
113
- Note;
114
- that;
115
- we;
116
- use;
117
- the `mockImplementation()`;
118
- method;
119
- to;
120
- mock;
121
- the `console.warn()`;
122
- method;
123
- and;
124
- the `mockRestore()`;
125
- method;
126
- to;
127
- restore;
128
- it;
129
- after;
130
- the;
131
- test;
132
- is;
133
- done.This;
134
- is;
135
- necessary;
136
- because;
137
- Jest;
138
- does;
139
- not;
140
- allow;
141
- us;
142
- to;
143
- modify;
144
- the;
145
- original `console.warn()`;
146
- method.;
@@ -1,61 +0,0 @@
1
- import { execSync } from 'child_process';
2
- import fs from 'fs/promises';
3
- import path from 'path';
4
- export async function updateReadmeIfNeeded() {
5
- try {
6
- let diff = execSync("git diff", { encoding: "utf-8" }).trim();
7
- if (!diff) {
8
- diff = execSync("git diff --cached", { encoding: "utf-8" }).trim();
9
- }
10
- if (!diff) {
11
- console.log('⚠️ No staged changes to suggest a message for.');
12
- return;
13
- }
14
- const readmePath = path.resolve("README.md");
15
- let readme = "";
16
- try {
17
- readme = await fs.readFile(readmePath, "utf-8");
18
- }
19
- catch {
20
- console.log("📄 No existing README.md found, skipping update.");
21
- return;
22
- }
23
- const prompt = `
24
- You're an experienced documentation writer. Here's the current README:
25
-
26
- --- README START ---
27
- ${readme}
28
- --- README END ---
29
-
30
- Here is a Git diff of recent code changes:
31
-
32
- --- DIFF START ---
33
- ${diff}
34
- --- DIFF END ---
35
-
36
- ✅ If the changes are significant and relevant to the public-facing documentation, return an updated README.
37
- ❌ If they are not, return ONLY: "NO UPDATE".
38
- `;
39
- const res = await fetch("http://localhost:11434/api/generate", {
40
- method: "POST",
41
- headers: { "Content-Type": "application/json" },
42
- body: JSON.stringify({
43
- model: "llama3",
44
- prompt,
45
- stream: false,
46
- }),
47
- });
48
- const { response } = await res.json();
49
- const result = response.trim();
50
- if (result === "NO UPDATE") {
51
- console.log("✅ No significant changes for README.");
52
- }
53
- else {
54
- await fs.writeFile(readmePath, result, "utf-8");
55
- console.log("📝 README.md updated based on significant changes.");
56
- }
57
- }
58
- catch (err) {
59
- console.error("❌ Failed to update README:", err.message);
60
- }
61
- }