scai 0.1.41 → 0.1.43

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.
@@ -1,9 +1,16 @@
1
- import readline from 'readline';
1
+ import fs from 'fs';
2
2
  import path from 'path';
3
- import { searchFiles, queryFiles } from "../db/fileIndex.js";
4
- import { sanitizeQueryForFts } from "../utils/sanitizeQuery.js";
5
- import { generate } from "../lib/generate.js";
3
+ import readline from 'readline';
4
+ import { searchFiles, queryFiles } from '../db/fileIndex.js';
5
+ import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
6
+ import { generate } from '../lib/generate.js';
7
+ import { buildContextualPrompt } from '../utils/buildContextualPrompt.js';
8
+ import { generateFileTree } from '../utils/fileTree.js';
9
+ import { log } from '../utils/log.js';
10
+ import { PROMPT_LOG_PATH, SCAI_HOME, INDEX_DIR } from '../constants.js';
11
+ const MAX_RELATED_FILES = 5;
6
12
  export async function runAskCommand(query) {
13
+ // 🧠 Prompt the user if no query is passed
7
14
  if (!query) {
8
15
  query = await promptOnce('🧠 Ask your question:\n> ');
9
16
  }
@@ -12,21 +19,32 @@ export async function runAskCommand(query) {
12
19
  console.error('āŒ No question provided.\nšŸ‘‰ Usage: scai ask "your question"');
13
20
  return;
14
21
  }
22
+ console.log(`šŸ“ Using index root: ${INDEX_DIR}`);
15
23
  console.log(`šŸ” Searching for: "${query}"\n`);
24
+ // 🧠 Step 1: Semantic + fallback search
16
25
  const start = Date.now();
17
- const semanticResults = await searchFiles(query, 5);
26
+ const semanticResults = await searchFiles(query, MAX_RELATED_FILES);
18
27
  const duration = Date.now() - start;
19
28
  console.log(`ā±ļø searchFiles took ${duration}ms and returned ${semanticResults.length} result(s)`);
20
- // Also run fallback keyword search
29
+ // šŸ” Log raw semantic results
30
+ console.log('šŸ” Raw semantic search results:');
31
+ semanticResults.forEach((file, i) => {
32
+ console.log(` ${i + 1}. šŸ“„ Path: ${file.path} | Score: ${file.score?.toFixed(3) ?? 'n/a'}`);
33
+ });
21
34
  const safeQuery = sanitizeQueryForFts(query);
22
35
  const fallbackResults = queryFiles(safeQuery, 10);
23
- // Merge semantic and fallback results
36
+ // šŸ” Log raw keyword fallback results
37
+ console.log('\nšŸ” Raw fallback keyword (FTS) search results:');
38
+ fallbackResults.forEach((file, i) => {
39
+ console.log(` ${i + 1}. šŸ“„ Path: ${file.path}`);
40
+ });
41
+ // 🧠 Step 2: Merge results
24
42
  const seen = new Set();
25
43
  const combinedResults = [];
26
44
  for (const file of semanticResults) {
27
45
  const resolved = path.resolve(file.path);
28
46
  seen.add(resolved);
29
- combinedResults.push(file); // Already scored
47
+ combinedResults.push(file);
30
48
  }
31
49
  for (const file of fallbackResults) {
32
50
  const resolved = path.resolve(file.path);
@@ -35,7 +53,7 @@ export async function runAskCommand(query) {
35
53
  combinedResults.push({
36
54
  path: file.path,
37
55
  summary: file.summary,
38
- score: 0.0, // fallback score
56
+ score: 0.0,
39
57
  });
40
58
  }
41
59
  }
@@ -46,26 +64,52 @@ export async function runAskCommand(query) {
46
64
  });
47
65
  }
48
66
  else {
49
- console.log('āš ļø No similar files found. Asking the model for context only...');
67
+ console.log('āš ļø No similar files found. Asking the model using question only...');
50
68
  }
51
- // Aggregate summaries
52
- let allSummaries = '';
53
- for (const file of combinedResults) {
54
- if (!file?.summary) {
55
- console.warn(`āš ļø No summary available for file: ${file?.path}`);
56
- continue;
57
- }
58
- console.log(`šŸ“ Using stored summary for: ${file.path}`);
59
- allSummaries += `\n${file.summary}`;
69
+ // 🧠 Step 3: Build metadata for prompt
70
+ const relatedFiles = combinedResults.slice(0, MAX_RELATED_FILES).map((file, index) => ({
71
+ path: file.path,
72
+ summary: file.summary || '(No summary available)', // Ensure summary is included
73
+ }));
74
+ // Get the top-ranked file (the first one in the sorted results)
75
+ const topRankedFile = combinedResults[0]; // The most relevant file
76
+ let fileTree = '';
77
+ try {
78
+ fileTree = generateFileTree(INDEX_DIR, 2); // Limit depth
79
+ }
80
+ catch (e) {
81
+ console.warn('āš ļø Failed to generate file tree:', e);
82
+ }
83
+ // Now we can build the prompt with summaries included for each file
84
+ const promptContent = buildContextualPrompt({
85
+ baseInstruction: query,
86
+ code: '', // No specific code selected
87
+ relatedFiles, // This now includes both path and summary for each file
88
+ projectFileTree: fileTree || undefined,
89
+ });
90
+ // 🧠 Step 4: Log prompt to file
91
+ try {
92
+ if (!fs.existsSync(SCAI_HOME))
93
+ fs.mkdirSync(SCAI_HOME, { recursive: true });
94
+ fs.writeFileSync(PROMPT_LOG_PATH, promptContent, 'utf-8');
95
+ log(`šŸ“ Prompt saved to ${PROMPT_LOG_PATH}`);
96
+ }
97
+ catch (err) {
98
+ log('āŒ Failed to write prompt log:', err);
60
99
  }
61
- const input = {
62
- content: allSummaries ? `${query}\n\n${allSummaries}` : query,
63
- filepath: '',
64
- };
100
+ // 🧠 Step 5: Call the model
65
101
  try {
66
- console.log(allSummaries.trim()
67
- ? '🧠 Summaries found, sending them to the model for synthesis...'
68
- : 'āš ļø No summaries found. Asking the model for context only...');
102
+ console.log('šŸ¤– Asking the model...');
103
+ // Create a more structured PromptInput object
104
+ const input = {
105
+ content: query, // Main instruction (the query)
106
+ filepath: topRankedFile?.path || '', // Include the path of the top-ranked file
107
+ metadata: {
108
+ summary: topRankedFile?.summary || '', // Add summary of the top-ranked file
109
+ relatedFiles: relatedFiles, // Pass related files as part of metadata
110
+ },
111
+ projectFileTree: fileTree || '' // Include file structure in metadata
112
+ };
69
113
  const modelResponse = await generate(input, 'llama3');
70
114
  console.log(`\nšŸ“ Model response:\n${modelResponse.content}`);
71
115
  }
@@ -77,7 +121,7 @@ function promptOnce(promptText) {
77
121
  return new Promise(resolve => {
78
122
  const rl = readline.createInterface({
79
123
  input: process.stdin,
80
- output: process.stdout
124
+ output: process.stdout,
81
125
  });
82
126
  rl.question(promptText, answer => {
83
127
  rl.close();
package/dist/constants.js CHANGED
@@ -26,6 +26,11 @@ export const CONFIG_PATH = path.join(SCAI_HOME, 'config.json');
26
26
  * ~/.scai/daemon.log
27
27
  */
28
28
  export const LOG_PATH = path.join(SCAI_HOME, 'daemon.log');
29
+ /**
30
+ * Path to the last prompt sent to the model:
31
+ * ~/.scai/prompt.log
32
+ */
33
+ export const PROMPT_LOG_PATH = path.join(SCAI_HOME, 'prompt.log');
29
34
  /**
30
35
  * Get the active index directory.
31
36
  *
@@ -0,0 +1,20 @@
1
+ export function buildContextualPrompt({ baseInstruction, code, summary, functions, relatedFiles, projectFileTree }) {
2
+ const parts = [baseInstruction];
3
+ if (summary) {
4
+ parts.push(`šŸ“„ File Summary:\n${summary}`);
5
+ }
6
+ if (functions?.length) {
7
+ parts.push(`šŸ”§ Functions:\n${functions.join(', ')}`);
8
+ }
9
+ if (relatedFiles?.length) {
10
+ const formatted = relatedFiles
11
+ .map(f => `• ${f.path}: ${f.summary}`)
12
+ .join('\n');
13
+ parts.push(`šŸ“š Related Files:\n${formatted}`);
14
+ }
15
+ if (projectFileTree) {
16
+ parts.push(`šŸ“ Project File Structure:\n\`\`\`\n${projectFileTree.trim()}\n\`\`\``);
17
+ }
18
+ parts.push(`\n--- CODE START ---\n${code}\n--- CODE END ---`);
19
+ return parts.join('\n\n');
20
+ }
@@ -0,0 +1,30 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ const IGNORED_DIRS = new Set([
4
+ 'node_modules', 'dist', '.git', '.vscode', 'coverage', 'build', 'out', 'logs', 'tmp'
5
+ ]);
6
+ export function generateFileTree(dir, maxDepth = 3, currentDepth = 0, prefix = '') {
7
+ if (currentDepth > maxDepth)
8
+ return '';
9
+ let output = '';
10
+ const items = fs.readdirSync(dir, { withFileTypes: true })
11
+ .filter(item => !IGNORED_DIRS.has(item.name))
12
+ .sort((a, b) => {
13
+ // Directories first
14
+ if (a.isDirectory() && !b.isDirectory())
15
+ return -1;
16
+ if (!a.isDirectory() && b.isDirectory())
17
+ return 1;
18
+ return a.name.localeCompare(b.name);
19
+ });
20
+ for (const [i, item] of items.entries()) {
21
+ const isLast = i === items.length - 1;
22
+ const connector = isLast ? '└── ' : 'ā”œā”€ā”€ ';
23
+ const childPrefix = prefix + (isLast ? ' ' : '│ ');
24
+ output += `${prefix}${connector}${item.name}\n`;
25
+ if (item.isDirectory()) {
26
+ output += generateFileTree(path.join(dir, item.name), maxDepth, currentDepth + 1, childPrefix);
27
+ }
28
+ }
29
+ return output;
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"