scai 0.1.82 → 0.1.84

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.
@@ -0,0 +1,102 @@
1
+ ## 2025-06-23
2
+
3
+ The new markdown bullet-point entry for CHANGELOG.md includes the following updates:
4
+
5
+ * Added support for generating changelog entries based on Git diffs using the `changelog` command.
6
+ * Updated the summary of the code file directly to the terminal using the `summ` command.
7
+ * Refactored the code to use the new `handleChangelogUpdate()` function instead of the old `updateReadmeIfNeeded()` function for generating changelogs.
8
+
9
+ ## 2025-06-24
10
+
11
+ ### Changelog Update (Version 0.1.16)
12
+
13
+ * Update to version 0.1.16 with changes from `package.json`.
14
+ * Add support for running modules using the `module` command.
15
+ * Add support for generating tests using the `tests` module.
16
+ * Add support for suggesting a commit message using the `suggest` module.
17
+ * Add support for updating the changelog using the `changelog` module.
18
+
19
+
20
+ ## 2025-06-29
21
+ Type handling with the module pipeline
22
+ ┌────────────────────────────────────────────────────────────────┐
23
+ │ 1. Command Layer (CLI) │
24
+ │ - User interaction │
25
+ │ - Handles input/output │
26
+ │ │
27
+ │ 🡆 Uses: │
28
+ │ - Input: PromptInput │
29
+ │ - Output: PromptOutput (reads summary/suggestions) │
30
+ └────────────┬───────────────────────────────────────────────────┘
31
+
32
+
33
+ ┌────────────────────────────────────────────────────────────────┐
34
+ │ 2. PromptModule Layer │
35
+ │ - Domain-specific logic │
36
+ │ - Calls `generate()` │
37
+ │ - Parses model output into usable fields │
38
+ │ │
39
+ │ 🡆 Type: PromptModule │
40
+ │ - run(input: PromptInput) => Promise<PromptOutput> │
41
+ │ │
42
+ │ 🡆 Uses: │
43
+ │ - Receives: PromptInput │
44
+ │ - Calls: generate(PromptInput, model): Promise<PromptOutput>│
45
+ │ - Parses output.content → summary/suggestions fields │
46
+ └────────────┬───────────────────────────────────────────────────┘
47
+
48
+
49
+ ┌────────────────────────────────────────────────────────────────┐
50
+ │ 3. generate() Function │
51
+ │ - Sends prompt to model │
52
+ │ - Returns raw model text in `.content` │
53
+ │ │
54
+ │ 🡆 Signature: │
55
+ │ generate(input: PromptInput, model: string) │
56
+ │ => Promise<PromptOutput> │
57
+ │ │
58
+ │ 🡆 Output fields: │
59
+ │ - content: string (LLM output text) │
60
+ │ - filepath?: string (optional passthrough) │
61
+ └────────────────────────────────────────────────────────────────┘
62
+
63
+
64
+
65
+ ## 2025-06-30
66
+
67
+ * Added context generation via summaries in sqlite3 db with FTS5
68
+ * Improved the `dbstats.sh` script to include more accurate row counts and improved formatting for better readability. (#20)
69
+ * Added an `IGNORED_FOLDER_GLOBS` constant in `src/config/IgnoredPaths.ts` to allow for ignoring folders and file globs during indexing and scanning. (#17)
70
+ * Fixed a bug in the `searchFiles` function of `src/db/fileIndex.ts` that prevented search results from being returned correctly. (#25)
71
+ * Updated the `runModulePipeline` function in `src/pipeline/runModulePipeline.ts` to allow for returning more accurate output and ensure consistent return types. (#27)
72
+
73
+ ## 2025-07-01
74
+
75
+ ### Version 0.1.20 (2023-04-01)
76
+ * Added support for querying file summaries and a local model using the `scai ask` command. The command will first search for matching files and pass their summaries to the model for context-based synthesis, if any are found. If no summaries are found, it will instead query the model for context without them.
77
+
78
+
79
+ ## 2025-07-12
80
+
81
+ * • Fixes to improve code quality and maintainability.
82
+ * • Enhances model initialization for better performance.
83
+ * • Adds support for generating changelogs based on Git diff.
84
+ * • Improves the handling of ignored files to reduce noise in the model.
85
+ * • Updates the `changelog` command to generate a meaningful summary.
86
+ * • Enhances the `sugg` command to provide more accurate suggestions.
87
+ * • Fixes issues with refactoring code to improve readability.
88
+
89
+ ## 2025-07-24
90
+
91
+ * Added token and owner/repo parameters to `getPullRequestsForReview` function
92
+ * Updated `askUserToPickPR` function to accept a list of PR objects
93
+ * Removed unnecessary code from `reviewPullRequestCmd`
94
+ * Added `cancel` option to `askReviewApproval` function
95
+
96
+ ## 2025-08-10
97
+
98
+ • Removed versioned dist folder
99
+ • Update .gitignore to ignore Node and build artifacts, editor/IDE files, and project-specific ignores.
100
+ • Added scripts for building cli, server, and dashboard
101
+ • Restructure the workspace into 3 modules, cli, dashboard, server
102
+ • Improved `askChangelogApproval` to support editing and skipping of changelog entries
@@ -5,16 +5,10 @@ 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
+ import { openTextEditor } from '../utils/editor.js';
8
9
  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);
10
+ // Don't bother with diffs at all here
11
+ let entry = await generateChangelogEntry();
18
12
  if (!entry) {
19
13
  console.log('⚠️ No significant changes found.');
20
14
  return;
@@ -28,7 +22,7 @@ export async function handleStandaloneChangelogUpdate() {
28
22
  }
29
23
  else if (userChoice === 'redo') {
30
24
  console.log('🔁 Regenerating changelog...');
31
- entry = await generateChangelogEntry(diff);
25
+ entry = await generateChangelogEntry();
32
26
  if (!entry) {
33
27
  console.log('⚠️ Could not regenerate entry. Exiting.');
34
28
  break;
@@ -40,26 +34,26 @@ export async function handleStandaloneChangelogUpdate() {
40
34
  }
41
35
  }
42
36
  }
43
- export async function generateChangelogEntry(diff) {
37
+ export async function generateChangelogEntry(currentCommitMsg) {
44
38
  try {
45
- // If no diff is provided, fetch the current diff from git silently
46
- if (!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
- }
39
+ const lastTag = getLastGitTag();
40
+ if (!lastTag) {
41
+ console.log("⚠️ No previous git tag found. Using all commits.");
52
42
  }
53
- if (!diff) {
54
- // No changes found, auto cancel by returning null
55
- console.log('⚠️ No changes detected for the changelog.');
43
+ let commits = lastTag
44
+ ? getCommitsSinceTag(lastTag)
45
+ : execSync('git log --pretty=format:%s', { encoding: 'utf-8' }).trim();
46
+ if (currentCommitMsg) {
47
+ commits = commits ? `${commits}\n${currentCommitMsg}` : currentCommitMsg;
48
+ }
49
+ console.log('Number of commits:', commits.split('\n').length);
50
+ if (!commits) {
51
+ console.log("⚠️ No commits found since last release.");
56
52
  return null;
57
53
  }
58
- // Generate the changelog entry using the diff silently
59
- const result = await runModulePipeline([changelogModule], { content: diff });
54
+ const result = await runModulePipeline([changelogModule], { content: commits });
60
55
  const output = result?.summary?.trim();
61
56
  if (!output || output === 'NO UPDATE') {
62
- // Auto-cancel if no update
63
57
  console.log('⚠️ No significant changes detected for changelog.');
64
58
  return null;
65
59
  }
@@ -70,6 +64,18 @@ export async function generateChangelogEntry(diff) {
70
64
  return null;
71
65
  }
72
66
  }
67
+ function getLastGitTag() {
68
+ try {
69
+ return execSync('git describe --tags --abbrev=0', { encoding: 'utf-8' }).trim();
70
+ }
71
+ catch {
72
+ return null; // no tags found
73
+ }
74
+ }
75
+ function getCommitsSinceTag(tag) {
76
+ // Get commit messages in a simple format, e.g. only commit messages
77
+ return execSync(`git log ${tag}..HEAD --pretty=format:%s`, { encoding: 'utf-8' }).trim();
78
+ }
73
79
  export async function updateChangelogFile(entry) {
74
80
  const root = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
75
81
  const changelogPath = path.join(root, "CHANGELOG.md");
@@ -85,3 +91,38 @@ export async function updateChangelogFile(entry) {
85
91
  await fs.writeFile(changelogPath, existing + newEntry, 'utf-8');
86
92
  return changelogPath;
87
93
  }
94
+ export async function handleChangelogWithCommitMessage(commitMsg) {
95
+ let entryFinalized = false;
96
+ let changelogEntry = await generateChangelogEntry(commitMsg);
97
+ if (!changelogEntry) {
98
+ console.log("ℹ️ No changelog entry generated.");
99
+ return;
100
+ }
101
+ while (!entryFinalized) {
102
+ const userChoice = await askChangelogApproval(changelogEntry);
103
+ if (userChoice === 'yes') {
104
+ const changelogPath = await updateChangelogFile(changelogEntry);
105
+ execSync(`git add "${changelogPath}"`);
106
+ console.log("✅ CHANGELOG.md staged.");
107
+ entryFinalized = true;
108
+ }
109
+ else if (userChoice === 'redo') {
110
+ console.log("🔁 Regenerating changelog entry...");
111
+ changelogEntry = await generateChangelogEntry(commitMsg);
112
+ if (!changelogEntry) {
113
+ console.log('⚠️ Could not regenerate entry. Exiting.');
114
+ break;
115
+ }
116
+ }
117
+ else if (userChoice === 'edit') {
118
+ changelogEntry = await openTextEditor(changelogEntry, 'scai-changelog.txt');
119
+ if (!changelogEntry) {
120
+ console.log('⚠️ No changes made to changelog. Returning to prompt.');
121
+ }
122
+ }
123
+ else {
124
+ console.log("❌ Skipped changelog update.");
125
+ entryFinalized = true;
126
+ }
127
+ }
128
+ }
@@ -2,23 +2,28 @@
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
+ import { handleChangelogWithCommitMessage } from './ChangeLogUpdateCmd.js';
6
+ import os from 'os';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { spawnSync } from 'child_process';
10
+ import chalk from 'chalk';
7
11
  function askUserToChoose(suggestions) {
8
12
  return new Promise((resolve) => {
9
13
  console.log('\n💡 AI-suggested commit messages:\n');
10
14
  suggestions.forEach((msg, i) => {
11
- console.log(`${i + 1}) ${msg}`);
15
+ console.log(`${i + 1}) ${chalk.hex('#FFA500')(`\`${msg}\``)}`);
12
16
  });
13
17
  console.log('\n---');
14
18
  console.log(`${suggestions.length + 1}) 🔁 Regenerate suggestions`);
15
19
  console.log(`${suggestions.length + 2}) ✍️ Write your own commit message`);
16
- console.log(`${suggestions.length + 3}) Cancel`);
20
+ console.log(`${suggestions.length + 3}) 🖋️ Edit a suggested commit message`);
21
+ console.log(`${suggestions.length + 4}) ❌ Cancel`);
17
22
  const rl = readline.createInterface({
18
23
  input: process.stdin,
19
24
  output: process.stdout,
20
25
  });
21
- rl.question(`\n👉 Choose a commit message [1-${suggestions.length + 3}]: `, (answer) => {
26
+ rl.question(`\n👉 Choose a commit message [1-${suggestions.length + 4}]: `, (answer) => {
22
27
  rl.close();
23
28
  const choice = parseInt(answer, 10);
24
29
  if (choice === suggestions.length + 1) {
@@ -28,6 +33,9 @@ function askUserToChoose(suggestions) {
28
33
  resolve('custom');
29
34
  }
30
35
  else if (choice === suggestions.length + 3) {
36
+ resolve('edit');
37
+ }
38
+ else if (choice === suggestions.length + 4) {
31
39
  resolve('cancel');
32
40
  }
33
41
  else if (!isNaN(choice) && choice >= 1 && choice <= suggestions.length) {
@@ -40,6 +48,46 @@ function askUserToChoose(suggestions) {
40
48
  });
41
49
  });
42
50
  }
51
+ async function promptEditCommitMessage(suggestedMessage) {
52
+ const tmpFilePath = path.join(os.tmpdir(), 'scai-commit-msg.txt');
53
+ fs.writeFileSync(tmpFilePath, `# Edit your commit message below.\n# Lines starting with '#' will be ignored.\n\n${suggestedMessage}`);
54
+ const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'vi');
55
+ spawnSync(editor, [tmpFilePath], { stdio: 'inherit' });
56
+ const editedContent = fs.readFileSync(tmpFilePath, 'utf-8');
57
+ return editedContent
58
+ .split('\n')
59
+ .filter(line => !line.trim().startsWith('#'))
60
+ .join('\n')
61
+ .trim() || suggestedMessage;
62
+ }
63
+ function askWhichSuggestionToEdit(suggestions) {
64
+ return new Promise((resolve) => {
65
+ console.log('\n🖋️ Select a commit message to edit:\n');
66
+ suggestions.forEach((msg, i) => {
67
+ console.log(`${i + 1}) ${chalk.hex('#FFA500')(`\`${msg}\``)}`);
68
+ });
69
+ console.log(`${suggestions.length + 1}) ❌ Cancel`);
70
+ const rl = readline.createInterface({
71
+ input: process.stdin,
72
+ output: process.stdout,
73
+ });
74
+ const editPrompt = chalk.magenta(`\n👉 Choose a commit message to edit [1-${suggestions.length + 1}]: `);
75
+ rl.question(editPrompt, (answer) => {
76
+ rl.close();
77
+ const choice = parseInt(answer, 10);
78
+ if (!isNaN(choice) && choice >= 1 && choice <= suggestions.length) {
79
+ resolve(choice - 1);
80
+ }
81
+ else if (choice === suggestions.length + 1) {
82
+ resolve('cancel');
83
+ }
84
+ else {
85
+ console.log('⚠️ Invalid selection.');
86
+ resolve('cancel');
87
+ }
88
+ });
89
+ });
90
+ }
43
91
  function promptCustomMessage() {
44
92
  return new Promise((resolve) => {
45
93
  const rl = readline.createInterface({
@@ -53,42 +101,15 @@ function promptCustomMessage() {
53
101
  });
54
102
  }
55
103
  export async function suggestCommitMessage(options) {
56
- // Ensure git diff does not print to console
57
104
  try {
58
- let diff = execSync("git diff", { encoding: "utf-8", stdio: "pipe" }).trim();
105
+ let diff = execSync("git diff --cached", { encoding: "utf-8", stdio: "pipe" }).trim();
59
106
  if (!diff) {
60
- diff = execSync("git diff --cached", { encoding: "utf-8", stdio: "pipe" }).trim();
107
+ diff = execSync("git diff", { encoding: "utf-8", stdio: "pipe" }).trim();
61
108
  }
62
109
  if (!diff) {
63
- console.log('⚠️ No staged changes to suggest a message for.');
110
+ console.log('⚠️ No staged changes to suggest a message for.');
64
111
  return;
65
112
  }
66
- // Handle changelog generation if the flag is provided
67
- if (options.changelog) {
68
- let entryFinalized = false;
69
- while (!entryFinalized) {
70
- const changelogEntry = await generateChangelogEntry(diff);
71
- if (!changelogEntry) {
72
- console.log("ℹ️ No changelog entry generated.");
73
- break;
74
- }
75
- const userChoice = await askChangelogApproval(changelogEntry);
76
- if (userChoice === 'yes') {
77
- const changelogPath = await updateChangelogFile(changelogEntry);
78
- execSync(`git add "${changelogPath}"`);
79
- console.log("✅ CHANGELOG.md staged.");
80
- entryFinalized = true;
81
- }
82
- else if (userChoice === 'redo') {
83
- console.log("🔁 Regenerating changelog entry...");
84
- continue; // Loop again and regenerate
85
- }
86
- else {
87
- console.log("❌ Skipped changelog update.");
88
- entryFinalized = true;
89
- }
90
- }
91
- }
92
113
  // Continue with commit suggestions
93
114
  const response = await commitSuggesterModule.run({ content: diff });
94
115
  const suggestions = response.suggestions || [];
@@ -108,6 +129,17 @@ export async function suggestCommitMessage(options) {
108
129
  if (choice === 'custom') {
109
130
  message = await promptCustomMessage();
110
131
  }
132
+ else if (choice === 'edit') {
133
+ // Ask which suggestion to edit using a dedicated prompt
134
+ const editChoice = await askWhichSuggestionToEdit(suggestions);
135
+ if (typeof editChoice === 'number') {
136
+ message = await promptEditCommitMessage(suggestions[editChoice]);
137
+ }
138
+ else {
139
+ console.log('⚠️ Edit cancelled, returning to main menu.');
140
+ continue;
141
+ }
142
+ }
111
143
  else if (choice === 'cancel') {
112
144
  console.log('❌ Commit cancelled.');
113
145
  return;
@@ -117,6 +149,10 @@ export async function suggestCommitMessage(options) {
117
149
  }
118
150
  }
119
151
  console.log(`\n✅ Selected commit message:\n${message}\n`);
152
+ // If changelog option is enabled, generate changelog including the selected commit message
153
+ if (options.changelog) {
154
+ await handleChangelogWithCommitMessage(message);
155
+ }
120
156
  const staged = execSync("git diff --cached", { encoding: "utf-8" }).trim();
121
157
  if (!staged) {
122
158
  console.log("⚠️ No files are currently staged for commit.");
@@ -2,16 +2,13 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { runModulePipeline } from '../pipeline/runModulePipeline.js';
4
4
  import { addCommentsModule } from '../pipeline/modules/commentModule.js';
5
- import { cleanupModule } from '../pipeline/modules/cleanupModule.js';
5
+ import { normalizePath } from '../utils/normalizePath.js';
6
6
  export async function handleRefactor(filepath, options = {}) {
7
7
  try {
8
- // Normalize path: add ./ prefix if no directory specified
9
- if (!filepath.startsWith('./') && !filepath.startsWith('/') && !filepath.includes('\\')) {
10
- filepath = `./${filepath}`;
11
- }
8
+ // Normalize and resolve filepath (includes expanding ~ and consistent slashes)
9
+ filepath = normalizePath(filepath);
12
10
  const { dir, name, ext } = path.parse(filepath);
13
11
  const refactoredPath = path.join(dir, `${name}.refactored${ext}`);
14
- // --apply flag: use existing refactored file and overwrite original
15
12
  if (options.apply) {
16
13
  try {
17
14
  const refactoredCode = await fs.readFile(refactoredPath, 'utf-8');
@@ -24,13 +21,10 @@ export async function handleRefactor(filepath, options = {}) {
24
21
  }
25
22
  return;
26
23
  }
27
- // Read source code
28
24
  const content = await fs.readFile(filepath, 'utf-8');
29
- // Run through pipeline modules
30
- const response = await runModulePipeline([addCommentsModule, cleanupModule], { content });
25
+ const response = await runModulePipeline([addCommentsModule], { content });
31
26
  if (!response.content.trim())
32
27
  throw new Error('⚠️ Model returned empty result');
33
- // Save refactored output
34
28
  await fs.writeFile(refactoredPath, response.content, 'utf-8');
35
29
  console.log(`✅ Refactored code saved to: ${refactoredPath}`);
36
30
  console.log(`ℹ️ Run again with '--apply' to overwrite the original.`);
@@ -164,24 +164,6 @@ function askReviewApproval() {
164
164
  });
165
165
  });
166
166
  }
167
- function askFinalReviewApproval() {
168
- return new Promise((resolve) => {
169
- console.log('\n---');
170
- console.log('1) ✅ Approve');
171
- console.log('2) ❌ Request Changes');
172
- console.log('3) 🚫 Cancel');
173
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
174
- rl.question(`\n👉 Choose an option [1-3]: `, (answer) => {
175
- rl.close();
176
- if (answer === '1')
177
- resolve('approve');
178
- else if (answer === '2')
179
- resolve('request-changes');
180
- else
181
- resolve('cancel');
182
- });
183
- });
184
- }
185
167
  // Prompt for custom review
186
168
  function promptCustomReview() {
187
169
  return new Promise((resolve) => {
@@ -212,23 +194,20 @@ function chunkDiff(diff, review_id) {
212
194
  const fullChunk = 'diff --git ' + chunk;
213
195
  const filePathMatch = fullChunk.match(/^diff --git a\/(.+?) b\//);
214
196
  const filePath = filePathMatch ? filePathMatch[1] : 'unknown';
215
- // Now we extract hunks and lines as per the DiffHunk type
216
197
  const hunks = [];
217
198
  let currentHunk = null;
218
- // Split chunk into lines
199
+ // This counts diff lines for *this file only* (context/+/- lines after first @@)
200
+ let positionCounter = 0;
219
201
  const lines = fullChunk.split('\n');
220
202
  lines.forEach(line => {
221
- const hunkHeaderMatch = line.match(/^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/);
203
+ const hunkHeaderMatch = line.match(/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
222
204
  if (hunkHeaderMatch) {
223
- // When we encounter a new hunk, process the previous one (if it exists) and start a new one
224
- if (currentHunk) {
205
+ if (currentHunk)
225
206
  hunks.push(currentHunk);
226
- }
227
- // Parse the hunk header
228
207
  const oldStart = parseInt(hunkHeaderMatch[1], 10);
229
208
  const newStart = parseInt(hunkHeaderMatch[3], 10);
230
- const oldLines = parseInt(hunkHeaderMatch[2], 10);
231
- const newLines = parseInt(hunkHeaderMatch[4], 10);
209
+ const oldLines = parseInt(hunkHeaderMatch[2] || '0', 10);
210
+ const newLines = parseInt(hunkHeaderMatch[4] || '0', 10);
232
211
  currentHunk = {
233
212
  oldStart,
234
213
  newStart,
@@ -236,33 +215,33 @@ function chunkDiff(diff, review_id) {
236
215
  newLines,
237
216
  lines: [],
238
217
  };
218
+ return; // don’t count @@ header line in positionCounter
239
219
  }
240
- else if (currentHunk) {
241
- // Process the lines inside the hunk
220
+ if (currentHunk) {
221
+ // Each line after @@ counts towards the diff position
222
+ positionCounter++;
242
223
  let lineType = 'context';
243
224
  if (line.startsWith('+'))
244
225
  lineType = 'add';
245
226
  if (line.startsWith('-'))
246
227
  lineType = 'del';
247
- // Create the DiffLine object
248
228
  currentHunk.lines.push({
249
229
  line,
250
230
  type: lineType,
251
231
  lineNumberOld: lineType === 'del' ? currentHunk.oldStart++ : undefined,
252
232
  lineNumberNew: lineType === 'add' ? currentHunk.newStart++ : undefined,
253
- review_id, // Assign the review_id here
233
+ position: positionCounter, // <-- key for GitHub inline API
234
+ review_id,
254
235
  });
255
236
  }
256
237
  });
257
- // Push the last hunk (if any)
258
- if (currentHunk) {
238
+ if (currentHunk)
259
239
  hunks.push(currentHunk);
260
- }
261
240
  return {
262
241
  filePath,
263
242
  content: fullChunk,
264
- hunks, // Return hunks, which now contain DiffLine objects with line numbers and review_id
265
- review_id, // Assign the review_id here for each chunk
243
+ hunks,
244
+ review_id,
266
245
  };
267
246
  });
268
247
  }
@@ -419,48 +398,36 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
419
398
  for (let i = 0; i < chunks.length; i++) {
420
399
  const chunk = chunks[i];
421
400
  const { choice, summary } = await reviewChunk(chunk, i, chunks.length);
422
- if (choice === 'approve') {
423
- reviewComments.push({
424
- path: chunk.filePath,
425
- body: summary,
426
- line: chunk.hunks[0]?.newStart || 1,
427
- side: 'RIGHT',
428
- });
429
- console.log(`💬 Posted AI review for chunk ${i + 1}`);
430
- }
431
- else if (choice === 'reject') {
432
- allApproved = false;
433
- reviewComments.push({
434
- path: chunk.filePath,
435
- body: summary,
436
- line: chunk.hunks[0]?.newStart || 1,
437
- side: 'RIGHT',
438
- });
401
+ if (choice === 'cancel' || choice === 'skip') {
402
+ console.log(chalk.gray(`⏭️ Skipped chunk ${i + 1}`));
403
+ continue;
439
404
  }
440
- else if (choice === 'custom') {
441
- const customReview = await promptCustomReview();
442
- reviewComments.push({
443
- path: chunk.filePath,
444
- body: customReview,
445
- line: chunk.hunks[0]?.newStart || 1,
446
- side: 'RIGHT',
447
- });
405
+ // Find the first line in the chunk with a valid position (usually the first 'add' or 'context' line)
406
+ const firstLineWithPosition = chunk.hunks
407
+ .flatMap(hunk => hunk.lines)
408
+ .find(line => line.position !== undefined);
409
+ if (!firstLineWithPosition) {
410
+ console.warn(`⚠️ Could not find valid position for inline comment in chunk ${i + 1}. Skipping comment.`);
411
+ continue;
448
412
  }
449
- else if (choice === 'cancel' || choice === 'skip') {
450
- console.log(chalk.gray(`⏭️ Skipped chunk ${i + 1}`));
413
+ let commentBody = summary;
414
+ if (choice === 'custom') {
415
+ commentBody = await promptCustomReview();
451
416
  }
452
- else if (typeof choice === 'string') {
453
- reviewComments.push({
454
- path: chunk.filePath,
455
- body: choice,
456
- line: chunk.hunks[0]?.newStart || 1,
457
- side: 'RIGHT',
458
- });
417
+ else if (typeof choice === 'string' && !['approve', 'reject', 'custom'].includes(choice)) {
418
+ commentBody = choice;
459
419
  }
420
+ reviewComments.push({
421
+ path: chunk.filePath,
422
+ body: commentBody,
423
+ position: firstLineWithPosition.position,
424
+ });
425
+ if (choice === 'reject')
426
+ allApproved = false;
460
427
  }
461
428
  console.log(chalk.blueBright('\n📝 Review Comments Preview:'));
462
429
  reviewComments.forEach((comment, idx) => {
463
- console.log(`${idx + 1}. ${comment.path}:${comment.line} [${comment.side}] — ${comment.body}`);
430
+ console.log(`${idx + 1}. ${comment.path}:${comment.position} — ${comment.body}`);
464
431
  });
465
432
  const shouldApprove = allApproved;
466
433
  const hasInlineComments = reviewComments.length > 0;
@@ -81,30 +81,29 @@ export async function submitReview(prNumber, body, event, comments) {
81
81
  const token = await ensureGitHubAuth();
82
82
  const { owner, repo } = await getRepoDetails();
83
83
  const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`;
84
+ // Prepare payload
85
+ const payload = { body, event };
86
+ if (comments && comments.length > 0) {
87
+ payload.comments = comments;
88
+ }
84
89
  const res = await fetch(url, {
85
90
  method: 'POST',
86
91
  headers: {
87
92
  Authorization: `token ${token}`,
88
93
  Accept: 'application/vnd.github.v3+json',
89
94
  },
90
- body: JSON.stringify({
91
- body,
92
- event,
93
- comments,
94
- }),
95
+ body: JSON.stringify(payload),
95
96
  });
96
97
  if (!res.ok) {
97
98
  const errorText = await res.text();
98
- // Attempt to parse error body
99
99
  let parsed = {};
100
100
  try {
101
101
  parsed = JSON.parse(errorText);
102
102
  }
103
103
  catch (_) {
104
- // leave as raw text if parsing fails
104
+ // fallback to raw text
105
105
  }
106
- const knownErrors = Array.isArray(parsed.errors) ? parsed.errors.join('; ') : '';
107
- // Handle known error cases
106
+ const knownErrors = Array.isArray(parsed.errors) ? parsed.errors.map((e) => e.message || e).join('; ') : '';
108
107
  if (res.status === 422) {
109
108
  if (knownErrors.includes('Can not approve your own pull request')) {
110
109
  console.warn(`⚠️ Skipping approval: You cannot approve your own pull request.`);
@@ -114,8 +113,8 @@ export async function submitReview(prNumber, body, event, comments) {
114
113
  console.warn(`⚠️ Cannot post comments: PR has no diff.`);
115
114
  return;
116
115
  }
117
- if (knownErrors.includes('path is missing') || knownErrors.includes('line is missing')) {
118
- console.warn(`⚠️ Some inline comments are missing a path or line number. Skipping review.`);
116
+ if (knownErrors.includes('path is missing') || knownErrors.includes('line is missing') || knownErrors.includes('position is missing')) {
117
+ console.warn(`⚠️ Some inline comments are missing required fields. Skipping review.`);
119
118
  return;
120
119
  }
121
120
  if (knownErrors.includes('Position is invalid') || knownErrors.includes('line must be part of the diff')) {
@@ -123,7 +122,6 @@ export async function submitReview(prNumber, body, event, comments) {
123
122
  return;
124
123
  }
125
124
  }
126
- // Unknown error
127
125
  throw new Error(`Failed to submit review: ${res.status} ${res.statusText} - ${errorText}`);
128
126
  }
129
127
  console.log(`✅ Submitted ${event} review for PR #${prNumber}`);
@@ -17,7 +17,7 @@ function getRepoOwnerAndNameFromGit(indexDir) {
17
17
  try {
18
18
  const originUrl = runGitCommand('git config --get remote.origin.url', indexDir);
19
19
  console.log(`🔗 Git origin URL from '${indexDir}': ${originUrl}`);
20
- const match = originUrl.match(/github\.com[:/](.+?)(?:\.git)?$/);
20
+ const match = originUrl.match(/github[^:/]*[:/](.+?)(?:\.git)?$/);
21
21
  if (!match)
22
22
  throw new Error("❌ Could not parse GitHub repo from origin URL.");
23
23
  const [owner, repo] = match[1].split('/');
@@ -1,6 +1,14 @@
1
1
  // File: lib/generate.ts
2
2
  import { Spinner } from './spinner.js';
3
+ import { readConfig } from '../config.js';
3
4
  export async function generate(input, model) {
5
+ const contextLength = readConfig().contextLength ?? 8192;
6
+ let prompt = input.content;
7
+ if (prompt.length > contextLength) {
8
+ console.warn(`⚠️ Warning: Input prompt length (${prompt.length}) exceeds model context length (${contextLength}). ` +
9
+ `The model may truncate or not handle the entire prompt. Truncating input.`);
10
+ prompt = prompt.slice(0, contextLength);
11
+ }
4
12
  const spinner = new Spinner(`🧠 Thinking with ${model}...`);
5
13
  spinner.start();
6
14
  try {
@@ -9,7 +17,7 @@ export async function generate(input, model) {
9
17
  headers: { 'Content-Type': 'application/json' },
10
18
  body: JSON.stringify({
11
19
  model,
12
- prompt: input.content,
20
+ prompt, // use truncated prompt here
13
21
  stream: false,
14
22
  }),
15
23
  });
@@ -1,26 +1,57 @@
1
1
  import { Config } from '../../config.js';
2
2
  import { generate } from '../../lib/generate.js';
3
+ import { detectFileType } from '../../fileRules/detectFileType.js';
3
4
  export const addCommentsModule = {
4
5
  name: 'addComments',
5
- description: 'Adds meaningful // comments to each block of code',
6
+ description: 'Adds meaningful comments to any file type (code, config, or data)',
6
7
  async run(input) {
7
8
  const model = Config.getModel();
8
- const lang = Config.getLanguage();
9
+ const fileType = detectFileType(input.filepath || '');
10
+ // Map file types to valid comment syntax
11
+ const commentMap = {
12
+ javascript: '//',
13
+ typescript: '//',
14
+ java: '//',
15
+ rust: '//',
16
+ c: '//',
17
+ cpp: '//',
18
+ csharp: '//',
19
+ go: '//',
20
+ swift: '//',
21
+ kotlin: '//',
22
+ scala: '//',
23
+ python: '#',
24
+ ruby: '#',
25
+ php: '//',
26
+ shell: '#',
27
+ bash: '#',
28
+ config: '#',
29
+ yaml: '#',
30
+ markdown: '<!-- -->',
31
+ html: '<!-- -->',
32
+ xml: '<!-- -->',
33
+ json: '/* */',
34
+ sql: '--',
35
+ csv: '#',
36
+ tsv: '#',
37
+ text: '#',
38
+ };
39
+ const commentSyntax = commentMap[fileType] || '//';
9
40
  const prompt = `
10
- You are a senior ${lang.toUpperCase()} engineer reviewing source code.
41
+ You are a senior engineer reviewing a ${fileType} file.
11
42
 
12
- Your task is to add clear and insightful single-line comments to the code.
43
+ Your task is to add clear, helpful, and accurate comments using "${commentSyntax}" syntax.
44
+ This applies to *every* section where an explanation could help — even for simple code or configuration.
13
45
 
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 to complex or non-obvious parts of the code.
18
- - Do NOT wrap the code in markdown or code blocks.
19
- - The code should be valid ${lang.toUpperCase()} after your changes.
46
+ Rules:
47
+ - Always return the full original file with comments added.
48
+ - Preserve all formatting, whitespace, and code exactly.
49
+ - Use only "${commentSyntax}" comments that are valid for ${fileType}.
50
+ - Do not wrap the output in markdown or code fences.
20
51
 
21
- --- CODE START ---
52
+ --- FILE START ---
22
53
  ${input.content}
23
- --- CODE END ---
54
+ --- FILE END ---
24
55
  `.trim();
25
56
  const response = await generate({ content: prompt }, model);
26
57
  return { content: response.content === 'NO UPDATE' ? '' : response.content };
@@ -1,28 +1,32 @@
1
1
  // src/utils/changelogPrompt.ts
2
+ import chalk from 'chalk';
2
3
  import readline from 'readline';
3
- export async function askChangelogApproval(entry) {
4
+ export async function askChangelogApproval(changelogEntry) {
4
5
  return new Promise((resolve) => {
5
- console.log('\n📜 Proposed changelog entry:\n');
6
- console.log(entry);
7
6
  console.log('\n---');
8
- console.log('1) Accept and stage');
7
+ console.log(chalk.yellow('Suggested changelog entry:\n'));
8
+ console.log(changelogEntry);
9
+ console.log('\nOptions:');
10
+ console.log('1) ✅ Accept');
9
11
  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) => {
12
+ console.log('3) 🖋️ Edit');
13
+ console.log('4) Skip');
14
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
15
+ rl.question('\n👉 Choose an option [1-4]: ', (answer) => {
16
16
  rl.close();
17
- switch (answer.trim()) {
17
+ switch (answer) {
18
18
  case '1':
19
19
  resolve('yes');
20
20
  break;
21
21
  case '2':
22
22
  resolve('redo');
23
23
  break;
24
+ case '3':
25
+ resolve('edit');
26
+ break;
27
+ case '4':
24
28
  default:
25
- resolve('no');
29
+ resolve('skip');
26
30
  break;
27
31
  }
28
32
  });
@@ -0,0 +1,16 @@
1
+ import os from 'os';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { spawnSync } from 'child_process';
5
+ export async function openTextEditor(initialContent, filename) {
6
+ const tmpFilePath = path.join(os.tmpdir(), filename);
7
+ fs.writeFileSync(tmpFilePath, initialContent, 'utf-8');
8
+ const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'vi');
9
+ spawnSync(editor, [tmpFilePath], { stdio: 'inherit' });
10
+ const editedContent = fs.readFileSync(tmpFilePath, 'utf-8');
11
+ return editedContent
12
+ .split('\n')
13
+ .filter(line => !line.trim().startsWith('#'))
14
+ .join('\n')
15
+ .trim();
16
+ }
@@ -1,4 +1,5 @@
1
1
  // src/utils/normalizePath.ts
2
+ import os from 'os';
2
3
  import path from "path";
3
4
  /**
4
5
  * Normalizes a path string for loose, fuzzy matching:
@@ -11,6 +12,9 @@ export function normalizePathForLooseMatch(p) {
11
12
  }
12
13
  // Helper to normalize and resolve paths to a consistent format (forward slashes)
13
14
  export function normalizePath(p) {
15
+ if (p.startsWith('~')) {
16
+ p = path.join(os.homedir(), p.slice(1));
17
+ }
14
18
  return path.resolve(p).replace(/\\/g, '/');
15
19
  }
16
20
  export function getRepoKeyForPath(pathToMatch, config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.82",
3
+ "version": "0.1.84",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -52,7 +52,7 @@
52
52
  "@types/better-sqlite3": "^7.6.13",
53
53
  "@types/columnify": "^1.5.4",
54
54
  "@types/jest": "^30.0.0",
55
- "@types/node": "^24.1.0",
55
+ "@types/node": "^24.2.1",
56
56
  "@types/proper-lockfile": "^4.1.4",
57
57
  "jest": "^30.0.2",
58
58
  "ts-jest": "^29.4.0",
package/LICENSE DELETED
@@ -1,4 +0,0 @@
1
- Free for personal and internal company use only.
2
- Commercial use (resale, SaaS, inclusion in paid tools) is prohibited.
3
- Contact me for commercial licensing.
4
-
package/README.md DELETED
@@ -1,452 +0,0 @@
1
- # ⚙️ scai — Smart Commit AI ✨
2
-
3
- > AI-powered CLI tool for commit messages **and** pull request reviews — using local models.
4
-
5
- **scai** is your AI pair‑programmer in the terminal. Focus on coding while scai:
6
-
7
- - 🤖 **Reviews open pull requests** and provides AI‑driven feedback (BETA)
8
- - 💬 **Suggests intelligent Git commit messages** based on your staged diff
9
- - 📝 Summarizes files in plain English
10
- - 📜 Auto‑updates your changelog
11
- - 🔍 (ALPHA) Search & ask questions across your codebase
12
- - 🔐 100% local — no API keys, no cloud, no telemetry
13
-
14
- ---
15
-
16
- ## 🚀 Features
17
-
18
- - ⚡ Powered by open-source models (e.g. `llama3`, `codellama`)
19
- - 🔍 Full-text indexing & semantic search (ALPHA)
20
- - 🛠️ Built with Node.js and TypeScript
21
- - ✅ Easily configurable via CLI or global flags
22
-
23
- ---
24
-
25
- ## ❤️ Why Local AI?
26
-
27
- **Your code stays yours.**
28
- scai runs entirely on your machine and doesn't require cloud APIs or API keys. That means:
29
-
30
- - ✅ **Privacy-first**: no telemetry, no server round-trips
31
- - ✅ **EU & GDPR-friendly**: designed with compliance in mind
32
- - ✅ **Developer control**: full transparency and override options
33
- - ✅ **Offline support**: works even without an internet connection
34
-
35
- ---
36
-
37
- ## 📦 Installation
38
-
39
- 1. **Install Ollama (for local models)**
40
- - macOS: `brew install ollama`
41
- - Windows: [Download here](https://ollama.com/download)
42
- - Start Ollama after installing.
43
-
44
- 2. **Install scai globally:**
45
- ```bash
46
- npm install -g scai
47
- ```
48
-
49
- 3. **Initialize models:**
50
-
51
- ```bash
52
- scai init
53
- ```
54
-
55
- ## ✨ AI Code Review, Powered by Your Terminal
56
-
57
- No more struggling to write pull request descriptions by hand. `scai git review` automatically generates a rich summary of your changes, complete with context, suggestions, and rationale.
58
-
59
- > ⚠️ These features are in **beta** — feedback welcome!
60
- Ping [@ticcr](https://bsky.app/profile/ticcr.xyz) on Bluesky — I'd love to hear your thoughts!
61
-
62
- ---
63
-
64
- ### 🔑 Setting Up Authentication (Required)
65
-
66
- To interact with GitHub and create pull requests, `scai` needs a personal access token with **repo** permissions.
67
-
68
- 1. **Create your GitHub Access Token**
69
- Follow this link to generate a token: [https://github.com/settings/personal-access-tokens](https://github.com/settings/personal-access-tokens)
70
-
71
- Make sure you enable at least:
72
-
73
- * `repo` (Full control of private repositories)
74
- * `workflow` (If you want PRs to trigger CI)
75
-
76
- 2. **Set the token in scai:**
77
-
78
- ```bash
79
- scai auth set
80
- ```
81
-
82
- This stores your token locally in a secure config file. You can inspect the setup at any time:
83
-
84
- ```bash
85
- scai auth check
86
- ```
87
-
88
- 3. **Set the index dir:**
89
-
90
- ```bash
91
- scai index set /path/to/repo
92
- ```
93
-
94
- This is the repo from which scai will look up pull requests that can be reviewed.
95
-
96
- ---
97
- ## ⚒️ Usage Overview
98
- ### 🧠 How to Use `scai git review`
99
-
100
- ```bash
101
- scai git review
102
- ```
103
-
104
- This will show you pull requests assigned to you for review:
105
-
106
- * Understand the diffs using a local model
107
- * Generate a structured pull request:
108
-
109
- * ✅ Title
110
- * ✅ Summary of changes
111
- * ✅ Explanation of why the changes matter
112
- * ✅ Optional changelog bullets
113
-
114
-
115
- SCAI supports an integrated review flow for GitHub pull requests. To get started:
116
-
117
- 1. **Set your working index directory (once per repo):**
118
-
119
- ```sh
120
- scai index set /path/to/repo
121
- ```
122
-
123
- 2. **Authenticate with GitHub:**
124
- ```sh
125
- scai git review
126
- ```
127
-
128
- This command will query you for the Personal Access Token and set it for you.
129
- You may also do this with the auth commands below
130
-
131
- ```sh
132
- scai auth set
133
- scai auth check
134
- ```
135
-
136
- 3. **Fetch and review pull requests:**
137
-
138
- ```sh
139
- scai git review
140
- ```
141
-
142
- Use `-a` to list all PRs that require a review:
143
-
144
- ```sh
145
- scai git review -a
146
- ```
147
-
148
- #### Example Workflow
149
-
150
- ```sh
151
- $ scai git review -a
152
- 📦 Resolving GitHub repo info from indexDir: ./
153
- 🔗 Git origin URL: git@github.com:org/repo.git
154
- ✅ Parsed: owner='org', repo='repo'
155
- 👤 Authenticated user: dev-user123
156
-
157
- 🔍 Fetching pull requests and diffs...
158
- ✅ Fetched 5 PR(s) with diffs.
159
-
160
- 📦 Open Pull Requests with review requested:
161
- | # | ID | TITLE | AUTHOR | STATUS | CREATED | REVIEWERS | REVIEWS |
162
- | - | ---- | ----------------------------- | ---------- | ------ | ---------- | ----------------------------- | ------------------- |
163
- | 1 | #120 | fix/session-timeout | dev-alice | Open | 2025-08-08 | code-analyzer\[bot], dev-bob | ✅ Approved |
164
- | 2 | #118 | feature/1482-support-wfs2 | dev-carol | Open | 2025-08-07 | code-analyzer\[bot], dev-dave | ✅ Approved |
165
- | 3 | #117 | refactor/win-server-support | dev-erin | Open | 2025-08-06 | dev-frank, dev-alice | ❌ Changes Requested |
166
- | 4 | #114 | bump/vue-i18n-9.14.5 | dependabot | Open | 2025-08-04 | code-analyzer\[bot] | ✅ Approved |
167
- | 5 | #113 | bugfix/null-navigator-check | dev-bob | Open | 2025-08-03 | dev-alice, dev-carol | ✅ Approved |
168
-
169
- 👉 Choose a PR to review [1-2]: 1
170
- ✅ Model response received.
171
-
172
- 💡 AI-suggested review:
173
-
174
- Solid improvement — this patch improves session stability and handles async state more reliably.
175
- You might consider renaming `sessionManager` to better reflect its dual role in auth and persistence.
176
-
177
- ---
178
- 1) ✅ Approve
179
- 2) ❌ Reject
180
- 3) ✍️ Edit
181
- 4) Write your own review
182
- 5) 🚪 Cancel
183
- ```
184
-
185
-
186
-
187
- ### 🔧 How to Use `scai git commit`
188
-
189
- Use AI to suggest a meaningful commit message based on your staged code:
190
-
191
- ```bash
192
- git add .
193
- scai git commit
194
- ```
195
-
196
- You can also include a changelog entry along with the commit:
197
-
198
- ```bash
199
- scai git commit --changelog
200
- ```
201
-
202
- This will:
203
- 1. Suggest a commit message based on your `git diff --cached`
204
- 2. Propose a changelog entry (if relevant)
205
- 3. Allow you to approve, regenerate, or skip the changelog
206
- 4. Automatically stage and commit the changes
207
-
208
- ---
209
-
210
- ### 📝 Generate a Standalone Changelog Entry
211
-
212
- If you want to generate a changelog entry without committing:
213
-
214
- ```bash
215
- scai gen changelog
216
- ```
217
-
218
- This will:
219
- - Analyze the current `git diff` (staged or unstaged)
220
- - Propose a list of **user-facing changes** in clean markdown bullet points
221
- - Let you accept, regenerate, or skip the update
222
- - Append the entry to `CHANGELOG.md` and stage it if accepted
223
-
224
-
225
- ### 🛠️ Code Generation Commands (`gen` group)
226
-
227
- ```bash
228
- scai gen summ <file>
229
- scai gen comm <file>
230
- scai gen changelog
231
- scai gen tests <file>
232
- ```
233
-
234
- * `summ`: Summarize a file
235
- * `comm`: Add comments to a file
236
- * `changelog`: Update or create `CHANGELOG.md` from Git diff
237
- * `tests`: Create Jest test stubs (ALPHA)
238
-
239
- You can also pipe file content directly:
240
-
241
- ```bash
242
- cat src/utils/math.ts | scai gen summ
243
- ```
244
-
245
- ---
246
-
247
- ## ⚙️ Configuration
248
-
249
- scai stores settings in `~/.scai/config.json`. You can override or view them:
250
-
251
- * **Set model:**
252
-
253
- ```bash
254
- scai set model codellama:7b
255
- ```
256
- * **Set language:**
257
-
258
- ```bash
259
- scai set lang ts
260
- ```
261
- * **Show config:**
262
-
263
- ```bash
264
- scai config
265
- ```
266
-
267
- <br>
268
-
269
- ## 🔁 Background Daemon and Indexing ⚠️ ALPHA Notice
270
-
271
- These features are experimental and subject to change:
272
-
273
- * `index`, `find`, `ask`
274
- * `daemon`, `stop-daemon`
275
- * `gen tests` (test generation)
276
-
277
- <br>
278
-
279
- ## Commands
280
-
281
- ### `index`
282
- The `index` command is used to manage and perform operations on the indexed files and repositories.
283
-
284
- #### `scai index start`
285
-
286
- Index supported files in the configured index directory.
287
-
288
- ```bash
289
- scai index set <dir>
290
- ```
291
-
292
- Set and activate the index directory.
293
- ```bash
294
- scai index list
295
- ```
296
-
297
- List all indexed repositories.
298
- ```bash
299
- scai index switch
300
- ```
301
-
302
- Switch active repository (by key or indexDir). Run without input for an interactive list of repositories.
303
-
304
-
305
- ---
306
-
307
- > **Note:** Indexing very large repositories (millions of lines) may take **hours or days**. Please be patient, and only index huge codebases if you are ok with some extra processing taking place on your computer.
308
-
309
- The `scai index` command **automatically** starts a background daemon that continuously:
310
-
311
- * Scans your target directory
312
- * Summarizes new or changed files
313
- * Updates embeddings and the search index
314
-
315
- You won't gain much value from the index unless you scope it to one repository.
316
-
317
- ---
318
-
319
- ### 🔍 Codebase Search & Ask (ALPHA)
320
-
321
- > **Important:** You must `index` a **code repository** first or `find` and `ask` have no context to work with.
322
-
323
- 1. **Set index directory:**
324
-
325
- ```bash
326
- scai index set /path/to/repo
327
- ```
328
-
329
- 2. **Index your repo (once):**
330
-
331
- ```bash
332
- scai index start
333
- ```
334
-
335
- 3. The daemon is designed to **consume minimal resources** and run unobtrusively. You can control it with:
336
-
337
- ```bash
338
- scai daemon # Start or show daemon status
339
- scai stop-daemon # Stop the background indexer
340
- ```
341
-
342
-
343
- 4. **Keyword search:**
344
-
345
- ```bash
346
- scai find YourClassName
347
- ```
348
-
349
- 5. **Natural-language questions:**
350
-
351
- ### 🧠 Natural-language questions
352
-
353
- Ask questions about your codebase using `scai ask`.
354
-
355
- You can run it in two ways:
356
-
357
- 1. **Inline question**
358
-
359
- ```bash
360
- scai ask "How does the controller work?"
361
- ```
362
-
363
- 2. **Interactive prompt**
364
-
365
- ```bash
366
- scai ask
367
- ```
368
-
369
- **Press enter**
370
- , then type your question when prompted:
371
-
372
- ```
373
- > How does the controller work?
374
- ```
375
-
376
- </br>
377
-
378
- ### 🚨 **OBS** 🚨
379
-
380
- `find` and `ask` rely on that index—without it they will return no useful results.
381
-
382
- ---
383
-
384
- Note the **Migrate** command is for **internal use only**. Do **not** run it unless explicitly instructed, as it may delete existing data or corrupt your local database.
385
-
386
- If you're upgrading from an earlier version, please run the following commands to avoid indexing issues:
387
-
388
- ```bash
389
- scai reset-db
390
- scai index
391
- ```
392
- </br>
393
-
394
- ---
395
-
396
- ## 🛍️ Maintenance & Utilities
397
-
398
- * **Reset database (w/ backup):**
399
-
400
- ```bash
401
- scai reset-db
402
- ```
403
- * **Backup only of `~/.scai`:**
404
-
405
- ```bash
406
- scai backup
407
- ```
408
-
409
- ---
410
-
411
- ## 🧺 Module Pipeline Mode
412
-
413
- For custom pipelines on a single file:
414
-
415
- ```bash
416
- scai src/file.ts -m summary,comments
417
- ```
418
- ---
419
-
420
- ## 🔐 License & Fair Use
421
-
422
- **scai is free to use** for individuals, teams, and commercial projects.
423
-
424
- You may:
425
-
426
- * ✅ Use internally or commercially
427
- * ✅ Fork and improve
428
- * ✅ Recommend to others
429
-
430
- You may **not**:
431
-
432
- * ❌ Resell as a product or service
433
- * ❌ Claim ownership of the tool
434
-
435
- </br>
436
-
437
- ### 📄 License
438
-
439
- Free for personal and internal company use only.
440
- Commercial use (resale, SaaS, inclusion in paid tools) is prohibited.
441
- Contact me for commercial licensing.
442
-
443
- ---
444
-
445
- </br>
446
-
447
- ## 🙌 Feedback
448
-
449
- Questions, ideas, or bugs?
450
- Ping [@ticcr](https://bsky.app/profile/ticcr.xyz) on Bluesky — I'd love to hear your thoughts!
451
-
452
- ---