scai 0.1.67 → 0.1.69

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.
@@ -127,7 +127,6 @@ export async function runAskCommand(query) {
127
127
  }
128
128
  // 🟩 STEP 7: Build prompt
129
129
  console.log(chalk.blueBright('\nšŸ“¦ Building contextual prompt...'));
130
- console.log(chalk.gray(`[runAskCommand] Calling buildContextualPrompt()`));
131
130
  const promptContent = buildContextualPrompt({
132
131
  baseInstruction: query,
133
132
  code,
@@ -3,26 +3,26 @@ import * as readline from 'readline';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import chalk from 'chalk';
6
- import { getDbForRepo } from './db/client.js'; // Ensure this function works correctly
6
+ import { getDbForRepo } from './db/client.js';
7
7
  import { readConfig, writeConfig } from './config.js';
8
8
  import { CONFIG_PATH } from './constants.js';
9
9
  // Constants
10
10
  const MODEL_PORT = 11434;
11
- const REQUIRED_MODELS = ['llama3', 'mistral']; // Expand as needed
11
+ const REQUIRED_MODELS = ['llama3', 'mistral'];
12
12
  const isYesMode = process.argv.includes('--yes') || process.env.SCAI_YES === '1';
13
13
  // 🧠 Auto init config/db if missing
14
14
  export async function autoInitIfNeeded() {
15
15
  const cfg = readConfig();
16
16
  if (!fs.existsSync(CONFIG_PATH)) {
17
17
  console.log(chalk.green('šŸ› ļø Config not found. Initializing...'));
18
- writeConfig({}); // This will create config.json with defaults
18
+ writeConfig({});
19
19
  }
20
20
  const activeRepo = cfg.activeRepo && cfg.repos[cfg.activeRepo];
21
21
  if (activeRepo) {
22
22
  const dbPath = path.join(activeRepo.indexDir, 'scai.db');
23
23
  if (!fs.existsSync(dbPath)) {
24
24
  console.log(chalk.green('šŸ“¦ DB not found. Initializing...'));
25
- getDbForRepo(); // This creates the DB
25
+ getDbForRepo();
26
26
  }
27
27
  }
28
28
  }
@@ -31,14 +31,15 @@ async function ensureOllamaRunning() {
31
31
  try {
32
32
  const res = await fetch(`http://localhost:${MODEL_PORT}`);
33
33
  if (res.ok) {
34
- console.log('āœ… Ollama is already running.');
34
+ console.log(chalk.green('āœ… Result:') + ` Ollama is already running on port ${MODEL_PORT}.`);
35
35
  return;
36
36
  }
37
37
  }
38
- catch {
39
- // Continue to spawn below
38
+ catch (err) {
39
+ console.log(chalk.yellow('āš™ļø Challenge:') + ' Ollama is not running. Attempting to start it...');
40
40
  }
41
- console.log(chalk.yellow('āš™ļø Ollama is not running. Starting it in the background...'));
41
+ console.log(chalk.yellow('āš™ļø Challenge:') + ` Ollama does not appear to be running on port ${MODEL_PORT}.\n` +
42
+ chalk.yellow('šŸš€ Action:') + ' Attempting to start Ollama in the background...');
42
43
  try {
43
44
  const child = spawn('ollama', ['serve'], {
44
45
  detached: true,
@@ -47,10 +48,10 @@ async function ensureOllamaRunning() {
47
48
  });
48
49
  child.unref();
49
50
  await new Promise((res) => setTimeout(res, 3000));
50
- console.log('āœ… Ollama started.');
51
+ console.log(chalk.green('āœ… Result:') + ' Ollama started successfully.');
51
52
  }
52
53
  catch (err) {
53
- console.error('āŒ Failed to start Ollama:', err);
54
+ console.error(chalk.red('āŒ Failed:') + ' Could not start Ollama process.', err);
54
55
  process.exit(1);
55
56
  }
56
57
  }
@@ -64,7 +65,7 @@ async function getInstalledModels() {
64
65
  .filter((model) => REQUIRED_MODELS.includes(model));
65
66
  }
66
67
  catch (err) {
67
- console.error('āŒ Could not fetch installed models:', err);
68
+ console.error(chalk.red('āŒ Could not fetch installed models:'), err);
68
69
  return [];
69
70
  }
70
71
  }
@@ -83,13 +84,13 @@ async function ensureModelsDownloaded() {
83
84
  const installed = await getInstalledModels();
84
85
  const missing = REQUIRED_MODELS.filter((m) => !installed.includes(m));
85
86
  if (!missing.length) {
86
- console.log('āœ… All required models are installed.');
87
+ console.log(chalk.green('āœ… All required models are installed.'));
87
88
  return;
88
89
  }
89
90
  console.log(chalk.yellow(`šŸ“¦ Missing models: ${missing.join(', ')}`));
90
91
  const answer = await promptUser('ā¬‡ļø Do you want to download them now? (y/N): ');
91
92
  if (answer.toLowerCase() !== 'y') {
92
- console.log('🚫 Aborting due to missing models.');
93
+ console.log(chalk.red('🚫 Aborting due to missing models.'));
93
94
  process.exit(1);
94
95
  }
95
96
  for (const model of missing) {
@@ -99,7 +100,7 @@ async function ensureModelsDownloaded() {
99
100
  console.log(chalk.green(`āœ… Pulled ${model}`));
100
101
  }
101
102
  catch (err) {
102
- console.error(`āŒ Failed to pull ${model}:`, err);
103
+ console.error(chalk.red(`āŒ Failed to pull ${model}:`), err);
103
104
  process.exit(1);
104
105
  }
105
106
  }
@@ -1,5 +1,6 @@
1
1
  // File: src/utils/buildContextualPrompt.ts
2
2
  import chalk from 'chalk';
3
+ import path from 'path';
3
4
  function estimateTokenCount(text) {
4
5
  return Math.round(text.length / 4); // simple heuristic approximation
5
6
  }
@@ -15,8 +16,7 @@ export function buildContextualPrompt({ baseInstruction, code, summary, function
15
16
  parts.push(`šŸ”§ Functions:\n${formattedFunctions}`);
16
17
  }
17
18
  else {
18
- console.log(chalk.gray(`[buildContextualPrompt]`) +
19
- chalk.yellow(` āš ļø No functions found in top file.`));
19
+ console.log(chalk.yellow(` āš ļø No functions found in top rated file.`));
20
20
  }
21
21
  if (relatedFiles?.length) {
22
22
  const formattedRelatedFiles = relatedFiles
@@ -36,21 +36,39 @@ export function buildContextualPrompt({ baseInstruction, code, summary, function
36
36
  const prompt = parts.join('\n\n');
37
37
  const tokenEstimate = estimateTokenCount(prompt);
38
38
  // šŸ”µ Colorized diagnostic output
39
+ // šŸ”µ Colorized diagnostic output
39
40
  const header = chalk.bgBlue.white.bold(' [SCAI] Prompt Overview ');
40
41
  const labelColor = chalk.cyan;
41
42
  const contentColor = chalk.gray;
42
43
  console.log('\n' + header);
43
44
  console.log(labelColor('šŸ”¢ Token Estimate:'), contentColor(`${tokenEstimate.toLocaleString()} tokens`));
44
- console.log(labelColor('🧩 Prompt Sections:'));
45
- console.log(contentColor([
46
- summary ? 'šŸ“„ Summary' : null,
47
- functions?.length ? 'šŸ”§ Functions' : null,
48
- relatedFiles?.length ? 'šŸ“š Related Files' : null,
49
- projectFileTree ? 'šŸ“ File Tree' : null,
50
- 'šŸ“¦ Code',
51
- ]
52
- .filter(Boolean)
53
- .join(', ')));
45
+ // šŸ“„ Summary
46
+ if (summary) {
47
+ console.log(labelColor('šŸ“„ Summary:'), contentColor(`${estimateTokenCount(summary).toLocaleString()} tokens`));
48
+ }
49
+ // šŸ”§ Functions
50
+ if (functions?.length) {
51
+ const fnCount = functions.length;
52
+ const fnTokens = functions.reduce((sum, f) => sum + estimateTokenCount(f.content), 0);
53
+ console.log(labelColor(`šŸ”§ Functions (${fnCount}):`), contentColor(`${fnTokens.toLocaleString()} tokens`));
54
+ }
55
+ // šŸ“š Related Files
56
+ if (relatedFiles?.length) {
57
+ const relCount = relatedFiles.length;
58
+ const relTokens = relatedFiles.reduce((sum, f) => sum + estimateTokenCount(f.summary), 0);
59
+ console.log(labelColor(`šŸ“š Related Files (${relCount}):`), contentColor(`${relTokens.toLocaleString()} tokens`));
60
+ // Optional: Show top 3 file names
61
+ const fileList = relatedFiles.slice(0, 3).map(f => `- ${path.basename(f.path)}`).join('\n');
62
+ if (fileList)
63
+ console.log(contentColor(fileList + (relCount > 3 ? `\n ...+${relCount - 3} more` : '')));
64
+ }
65
+ // šŸ“ File Tree
66
+ if (projectFileTree) {
67
+ console.log(labelColor('šŸ“ File Tree:'), contentColor(`${estimateTokenCount(projectFileTree).toLocaleString()} tokens`));
68
+ }
69
+ // šŸ“¦ Code Section
70
+ console.log(labelColor('šŸ“¦ Code:'), contentColor(`${estimateTokenCount(prompt).toLocaleString()} tokens`));
71
+ // šŸ“Œ Key
54
72
  console.log(labelColor('šŸ” Key:'), contentColor('[buildContextualPrompt]\n'));
55
73
  return prompt;
56
74
  }
@@ -1,17 +1,38 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { getIndexDir } from '../constants.js';
4
- export function generateFocusedFileTree(focusPath, maxDepth = 2) {
4
+ /**
5
+ * Generate a reduced file tree centered around the focus path, including nearby sibling folders.
6
+ */
7
+ export function generateFocusedFileTree(focusPath, maxDepth = 2, siblingWindow = 2) {
5
8
  const absoluteFocus = path.resolve(focusPath);
6
- const parentDir = path.dirname(absoluteFocus);
9
+ const fileOrDir = fs.statSync(absoluteFocus);
10
+ const targetDir = fileOrDir.isDirectory()
11
+ ? absoluteFocus
12
+ : path.dirname(absoluteFocus);
13
+ const parentDir = path.dirname(targetDir);
7
14
  const indexDir = getIndexDir();
8
- const relativeTitle = path.relative(indexDir, parentDir).replace(/\\/g, '/');
9
- const tree = generateFileTree(parentDir, maxDepth, absoluteFocus);
10
- return `šŸ“‚ ${relativeTitle || '.'}\n${tree}`;
15
+ const siblings = fs
16
+ .readdirSync(parentDir, { withFileTypes: true })
17
+ .filter(entry => entry.isDirectory())
18
+ .sort((a, b) => a.name.localeCompare(b.name));
19
+ const focusIndex = siblings.findIndex(entry => path.resolve(path.join(parentDir, entry.name)) === path.resolve(targetDir));
20
+ const start = Math.max(0, focusIndex - siblingWindow);
21
+ const end = Math.min(siblings.length, focusIndex + siblingWindow + 1);
22
+ const nearbySiblings = siblings.slice(start, end);
23
+ const relativeTitle = path.relative(indexDir, parentDir).replace(/\\/g, '/') || '.';
24
+ let output = `šŸ“‚ ${relativeTitle}\n`;
25
+ nearbySiblings.forEach(entry => {
26
+ const siblingPath = path.join(parentDir, entry.name);
27
+ const isFocusDir = path.resolve(siblingPath) === path.resolve(targetDir);
28
+ const tree = generateFileTree(siblingPath, maxDepth - 1, isFocusDir ? absoluteFocus : undefined, '│ ');
29
+ output += `${isFocusDir ? 'āž”ļø ' : ''}${entry.name}/\n${tree}`;
30
+ });
31
+ return output;
11
32
  }
12
33
  function generateFileTree(dir, depth, highlightPath, prefix = '') {
13
34
  if (depth < 0)
14
- return ''; // Stop at maxDepth
35
+ return '';
15
36
  let output = '';
16
37
  const entries = fs.readdirSync(dir, { withFileTypes: true });
17
38
  const sorted = entries.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()));
@@ -20,13 +41,11 @@ function generateFileTree(dir, depth, highlightPath, prefix = '') {
20
41
  const connector = isLast ? '└── ' : 'ā”œā”€ā”€ ';
21
42
  const fullPath = path.join(dir, entry.name);
22
43
  const isHighlighted = highlightPath && path.resolve(fullPath) === path.resolve(highlightPath);
23
- // If it is a directory, recurse and reduce depth
24
44
  if (entry.isDirectory()) {
25
45
  output += `${prefix}${connector}${entry.name}/\n`;
26
46
  output += generateFileTree(fullPath, depth - 1, highlightPath, prefix + (isLast ? ' ' : '│ '));
27
47
  }
28
48
  else {
29
- // Highlight the file if it matches the focusPath
30
49
  const name = isHighlighted ? `āž”ļø ${entry.name}` : entry.name;
31
50
  output += `${prefix}${connector}${name}\n`;
32
51
  }
@@ -1,10 +1,16 @@
1
- import { wrapText } from "./textWrapper.js";
2
- // src/utils/summarizer.ts
1
+ import columnify from 'columnify';
3
2
  export function summarizeCode(summaryText) {
4
- // Get the terminal width (default to 80 if not available)
5
3
  const terminalWidth = process.stdout.columns || 80;
6
- // Use two-thirds of the terminal width for the wrapping
7
- const twoThirdsTerminalWidth = Math.floor((terminalWidth * 2) / 3);
8
- // Wrap the summary output to fit within two-thirds of the terminal width
9
- return wrapText(summaryText, twoThirdsTerminalWidth);
4
+ // You can control wrapping here
5
+ const formatted = columnify([{ Summary: summaryText }], {
6
+ columnSplitter: ' ',
7
+ maxLineWidth: terminalWidth,
8
+ config: {
9
+ Summary: {
10
+ maxWidth: Math.floor((terminalWidth * 2) / 3), // Use 2/3 width like before
11
+ align: "left",
12
+ },
13
+ },
14
+ });
15
+ return formatted;
10
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -41,6 +41,7 @@
41
41
  "acorn-walk": "^8.3.2",
42
42
  "better-sqlite3": "^12.1.1",
43
43
  "chalk": "^5.4.1",
44
+ "columnify": "^1.6.0",
44
45
  "commander": "^11.0.0",
45
46
  "fast-glob": "^3.3.3",
46
47
  "proper-lockfile": "^4.1.2",
@@ -49,6 +50,7 @@
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/better-sqlite3": "^7.6.13",
53
+ "@types/columnify": "^1.5.4",
52
54
  "@types/jest": "^30.0.0",
53
55
  "@types/node": "^24.1.0",
54
56
  "@types/proper-lockfile": "^4.1.4",
@@ -1,26 +0,0 @@
1
- export function wrapText(text, maxWidth) {
2
- const words = text.split(' ');
3
- let wrappedText = '';
4
- let currentLine = '';
5
- words.forEach(word => {
6
- // If the word is longer than the maxWidth, break it up into multiple lines
7
- if (word.length > maxWidth) {
8
- // Break the word into smaller chunks
9
- while (word.length > maxWidth) {
10
- wrappedText += word.slice(0, maxWidth) + '\n';
11
- word = word.slice(maxWidth);
12
- }
13
- }
14
- // Check if adding the word would exceed the max width
15
- if ((currentLine + word).length > maxWidth) {
16
- wrappedText += currentLine + '\n'; // Add the current line and start a new one
17
- currentLine = word + ' '; // Start the new line with the current word
18
- }
19
- else {
20
- currentLine += word + ' '; // Add the word to the current line
21
- }
22
- });
23
- // Append the last line if any
24
- wrappedText += currentLine.trim(); // trim() to remove the extra space at the end
25
- return wrappedText;
26
- }