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.
- package/dist/commands/AskCmd.js +71 -27
- package/dist/constants.js +5 -0
- package/dist/utils/buildContextualPrompt.js +20 -0
- package/dist/utils/fileTree.js +30 -0
- package/package.json +1 -1
package/dist/commands/AskCmd.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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,
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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);
|
|
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,
|
|
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
|
|
67
|
+
console.log('ā ļø No similar files found. Asking the model using question only...');
|
|
50
68
|
}
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
content: allSummaries ? `${query}\n\n${allSummaries}` : query,
|
|
63
|
-
filepath: '',
|
|
64
|
-
};
|
|
100
|
+
// š§ Step 5: Call the model
|
|
65
101
|
try {
|
|
66
|
-
console.log(
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|