scai 0.1.113 → 0.1.115
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/CHANGELOG.md +5 -1
- package/dist/config.js +1 -1
- package/dist/context.js +16 -10
- package/dist/index.js +8 -4
- package/dist/lib/generate.js +0 -3
- package/dist/utils/buildContextualPrompt.js +35 -34
- package/package.json +1 -1
package/dist/CHANGELOG.md
CHANGED
|
@@ -189,4 +189,8 @@ Type handling with the module pipeline
|
|
|
189
189
|
4. Updated Edge/Table schema for better query performance.
|
|
190
190
|
5. Update package-lock.json to caniuse-lite@1.0.30001741.
|
|
191
191
|
6. Enable execution of as an executable file in the scripts.
|
|
192
|
-
7. Remove context failure if models not installed. Add ability to set global model.
|
|
192
|
+
7. Remove context failure if models not installed. Add ability to set global model.
|
|
193
|
+
|
|
194
|
+
## 2025-09-13
|
|
195
|
+
|
|
196
|
+
• Improve robustness of context update logic
|
package/dist/config.js
CHANGED
|
@@ -151,7 +151,7 @@ export const Config = {
|
|
|
151
151
|
? chalk.green(`✅ ${key} (active)`)
|
|
152
152
|
: chalk.white(` ${key}`);
|
|
153
153
|
console.log(`- ${label}`);
|
|
154
|
-
console.log(` ↳ indexDir: ${r.indexDir}`);
|
|
154
|
+
console.log(` ↳ indexDir: ${isActive ? chalk.yellow(r.indexDir) : r.indexDir}`);
|
|
155
155
|
}
|
|
156
156
|
},
|
|
157
157
|
getGitHubToken() {
|
package/dist/context.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// context.ts
|
|
2
2
|
import { readConfig, writeConfig } from "./config.js";
|
|
3
3
|
import { normalizePath } from "./utils/contentUtils.js";
|
|
4
|
-
import { getHashedRepoKey } from "./utils/repoKey.js";
|
|
5
4
|
import { getDbForRepo, getDbPathForRepo } from "./db/client.js";
|
|
6
5
|
import fs from "fs";
|
|
7
6
|
import chalk from "chalk";
|
|
@@ -23,32 +22,39 @@ function modelExists(model) {
|
|
|
23
22
|
export async function updateContext() {
|
|
24
23
|
const cwd = normalizePath(process.cwd());
|
|
25
24
|
const cfg = readConfig();
|
|
25
|
+
// 🔎 Look up existing repo by indexDir
|
|
26
26
|
let repoKey = Object.keys(cfg.repos || {}).find((key) => normalizePath(cfg.repos[key]?.indexDir || "") === cwd);
|
|
27
|
-
let isNewRepo = false;
|
|
28
27
|
if (!repoKey) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const err = new Error();
|
|
29
|
+
err.message =
|
|
30
|
+
chalk.red("❌ Current directory is not in your list of indexed repos. See the 'scai index list' command.") +
|
|
31
|
+
"\n" +
|
|
32
|
+
chalk.blueBright(" ↳ To get the full benefit of an indexed repo, navigate to your repository root folder.") +
|
|
33
|
+
"\n\n" +
|
|
34
|
+
chalk.greenBright(" Run ") +
|
|
35
|
+
chalk.cyan("scai index set") +
|
|
36
|
+
chalk.greenBright(" command. This enables Scai to index the repo, find git credentials etc.\n");
|
|
37
|
+
throw err;
|
|
34
38
|
}
|
|
35
39
|
const activeRepoChanged = cfg.activeRepo !== repoKey;
|
|
36
40
|
cfg.activeRepo = repoKey;
|
|
37
41
|
writeConfig(cfg);
|
|
38
42
|
const repoCfg = cfg.repos[repoKey];
|
|
39
43
|
let ok = true;
|
|
40
|
-
if (
|
|
44
|
+
if (activeRepoChanged) {
|
|
41
45
|
console.log(chalk.yellow("\n🔁 Updating context...\n"));
|
|
42
46
|
console.log(`✅ Active repo: ${chalk.green(repoKey)}`);
|
|
43
47
|
console.log(`✅ Index dir: ${chalk.cyan(repoCfg.indexDir || cwd)}`);
|
|
44
48
|
}
|
|
49
|
+
// 🔑 Token check
|
|
45
50
|
const token = repoCfg.githubToken || cfg.githubToken;
|
|
46
51
|
if (!token) {
|
|
47
52
|
console.log(`ℹ️ No GitHub token found. You can set one with: ${chalk.bold(chalk.bgGreen("scai auth set"))}`);
|
|
48
53
|
}
|
|
49
|
-
else if (
|
|
54
|
+
else if (activeRepoChanged) {
|
|
50
55
|
console.log(`✅ GitHub token present`);
|
|
51
56
|
}
|
|
57
|
+
// 💾 DB check
|
|
52
58
|
const dbPath = getDbPathForRepo();
|
|
53
59
|
if (!fs.existsSync(dbPath)) {
|
|
54
60
|
console.log(chalk.yellow(`📦 Initializing DB at ${dbPath}`));
|
|
@@ -59,7 +65,7 @@ export async function updateContext() {
|
|
|
59
65
|
ok = false;
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
|
-
else if (
|
|
68
|
+
else if (activeRepoChanged) {
|
|
63
69
|
console.log(chalk.green("✅ Database present"));
|
|
64
70
|
}
|
|
65
71
|
// 🧠 Model check
|
package/dist/index.js
CHANGED
|
@@ -232,10 +232,10 @@ index
|
|
|
232
232
|
index
|
|
233
233
|
.command('set [dir]')
|
|
234
234
|
.description('Set and activate index directory')
|
|
235
|
-
.action(async (dir = process.cwd()) =>
|
|
236
|
-
Config.setIndexDir(dir);
|
|
235
|
+
.action(async (dir = process.cwd()) => {
|
|
236
|
+
await Config.setIndexDir(dir);
|
|
237
237
|
Config.show();
|
|
238
|
-
})
|
|
238
|
+
});
|
|
239
239
|
index
|
|
240
240
|
.command('list')
|
|
241
241
|
.description('List all indexed repositories')
|
|
@@ -336,9 +336,13 @@ cmd.addHelpText('after', `
|
|
|
336
336
|
|
|
337
337
|
💡 Use with caution and expect possible changes or instability.
|
|
338
338
|
`);
|
|
339
|
-
cmd.parse(process.argv);
|
|
340
339
|
async function withContext(action) {
|
|
341
340
|
const ok = await updateContext();
|
|
342
341
|
//if (!ok) process.exit(1);
|
|
343
342
|
await action();
|
|
344
343
|
}
|
|
344
|
+
// 👇 this should be the very last line
|
|
345
|
+
cmd.parseAsync(process.argv).catch((err) => {
|
|
346
|
+
console.error(err.message); // only show our styled message
|
|
347
|
+
process.exit(1);
|
|
348
|
+
});
|
package/dist/lib/generate.js
CHANGED
|
@@ -6,9 +6,6 @@ export async function generate(input) {
|
|
|
6
6
|
const model = Config.getModel();
|
|
7
7
|
const contextLength = readConfig().contextLength ?? 8192;
|
|
8
8
|
let prompt = input.content;
|
|
9
|
-
if (prompt.length > contextLength) {
|
|
10
|
-
console.warn(`⚠️ Warning: Input prompt length (${prompt.length}) exceeds model context length (${contextLength}).`);
|
|
11
|
-
}
|
|
12
9
|
const spinner = new Spinner(`🧠 Thinking with ${model}...`);
|
|
13
10
|
spinner.start();
|
|
14
11
|
try {
|
|
@@ -6,33 +6,51 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
|
|
|
6
6
|
const log = (...args) => console.log("[buildContextualPrompt]", ...args);
|
|
7
7
|
const promptSections = [];
|
|
8
8
|
const seenPaths = new Set();
|
|
9
|
-
//
|
|
9
|
+
// --- Utility: Summarize text to a few words ---
|
|
10
10
|
function summarizeForPrompt(summary, maxWords = 30) {
|
|
11
11
|
if (!summary)
|
|
12
12
|
return undefined;
|
|
13
13
|
const words = summary.split(/\s+/);
|
|
14
|
-
|
|
15
|
-
return summary.trim();
|
|
16
|
-
return words.slice(0, maxWords).join(" ") + " …";
|
|
14
|
+
return words.length <= maxWords ? summary.trim() : words.slice(0, maxWords).join(" ") + " …";
|
|
17
15
|
}
|
|
18
16
|
// --- Step 1: Top file summary ---
|
|
19
17
|
if (topFile.summary) {
|
|
20
18
|
promptSections.push(`**Top file:** ${topFile.path}\n${topFile.summary}`);
|
|
21
19
|
seenPaths.add(topFile.path);
|
|
22
20
|
}
|
|
23
|
-
//
|
|
24
|
-
|
|
21
|
+
// ===========================
|
|
22
|
+
// SECTION A: Database queries
|
|
23
|
+
// ===========================
|
|
24
|
+
// --- Step 2a: KG entities/tags ---
|
|
25
|
+
const topEntitiesRows = db.prepare(`
|
|
25
26
|
SELECT et.entity_type, et.entity_id, tm.name AS tag
|
|
26
27
|
FROM entity_tags et
|
|
27
28
|
JOIN tags_master tm ON et.tag_id = tm.id
|
|
28
29
|
WHERE et.entity_id = ?
|
|
29
|
-
`);
|
|
30
|
-
const topEntitiesRows = topEntitiesStmt.all(topFile.id);
|
|
30
|
+
`).all(topFile.id);
|
|
31
31
|
if (topEntitiesRows.length > 0) {
|
|
32
32
|
const tags = topEntitiesRows.map(r => `- **${r.entity_type}**: ${r.tag}`);
|
|
33
33
|
promptSections.push(`**Knowledge Graph context for ${topFile.path}:**\n${tags.join("\n")}`);
|
|
34
34
|
}
|
|
35
|
-
// --- Step
|
|
35
|
+
// --- Step 2b: Functions in this file ---
|
|
36
|
+
const functionRows = db
|
|
37
|
+
.prepare(`SELECT name, start_line, end_line, content FROM functions WHERE file_id = ? ORDER BY start_line`)
|
|
38
|
+
.all(topFile.id);
|
|
39
|
+
const FUNCTION_LIMIT = 15;
|
|
40
|
+
const hasMoreFunctions = functionRows.length > FUNCTION_LIMIT;
|
|
41
|
+
const functionsSummary = functionRows.slice(0, FUNCTION_LIMIT).map(f => {
|
|
42
|
+
const lines = f.content?.split("\n").map(l => l.trim()).filter(Boolean) || ["[no content]"];
|
|
43
|
+
const preview = lines.slice(0, 3) // first 3 lines
|
|
44
|
+
.map(l => l.slice(0, 200) + (l.length > 200 ? "…" : ""))
|
|
45
|
+
.join(" | ");
|
|
46
|
+
return `- ${f.name || "[anonymous]"} (lines ${f.start_line}-${f.end_line}) — ${preview}`;
|
|
47
|
+
});
|
|
48
|
+
if (functionsSummary.length) {
|
|
49
|
+
promptSections.push(`**Functions in ${topFile.path} (showing ${functionsSummary.length}${hasMoreFunctions ? ` of ${functionRows.length}` : ""}):**\n${functionsSummary.join("\n")}`);
|
|
50
|
+
}
|
|
51
|
+
// ===============================
|
|
52
|
+
// SECTION B: Graph / KG traversal
|
|
53
|
+
// ===============================
|
|
36
54
|
const kgRelatedStmt = db.prepare(`
|
|
37
55
|
SELECT DISTINCT f.id, f.path, f.summary
|
|
38
56
|
FROM edges e
|
|
@@ -42,18 +60,10 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
|
|
|
42
60
|
AND e.source_id = ?
|
|
43
61
|
`);
|
|
44
62
|
function getRelatedKGFiles(fileId, visited = new Set()) {
|
|
45
|
-
if (visited.has(fileId))
|
|
46
|
-
log(`🔹 Already visited fileId ${fileId}, skipping`);
|
|
63
|
+
if (visited.has(fileId))
|
|
47
64
|
return [];
|
|
48
|
-
}
|
|
49
65
|
visited.add(fileId);
|
|
50
66
|
const rows = kgRelatedStmt.all(fileId);
|
|
51
|
-
if (rows.length === 0) {
|
|
52
|
-
log(`⚠️ No edges found for fileId ${fileId}`);
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
log(`🔹 Found ${rows.length} related files for fileId ${fileId}:`, rows.map(r => r.path));
|
|
56
|
-
}
|
|
57
67
|
let results = [];
|
|
58
68
|
for (const row of rows) {
|
|
59
69
|
results.push(row);
|
|
@@ -62,12 +72,9 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
|
|
|
62
72
|
return results;
|
|
63
73
|
}
|
|
64
74
|
function buildFileTree(file, depth, visited = new Set()) {
|
|
65
|
-
|
|
66
|
-
if (visited.has(file.id)) {
|
|
75
|
+
if (visited.has(file.id))
|
|
67
76
|
return { id: file.id.toString(), path: file.path };
|
|
68
|
-
}
|
|
69
77
|
visited.add(file.id);
|
|
70
|
-
// progressively shorten summaries, drop at depth <= 1
|
|
71
78
|
const maxWordsByDepth = depth >= 3 ? 30 : depth === 2 ? 15 : 0;
|
|
72
79
|
const node = {
|
|
73
80
|
id: file.id.toString(),
|
|
@@ -78,7 +85,6 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
|
|
|
78
85
|
const relatedFiles = getRelatedKGFiles(file.id, visited)
|
|
79
86
|
.map(f => ({ id: f.id, path: f.path, summary: f.summary }))
|
|
80
87
|
.slice(0, 5); // cap children
|
|
81
|
-
log(`File ${file.path} has ${relatedFiles.length} related files`);
|
|
82
88
|
const relatedNodes = relatedFiles.map(f => buildFileTree(f, depth - 1, visited));
|
|
83
89
|
if (relatedNodes.length)
|
|
84
90
|
node.related = relatedNodes;
|
|
@@ -86,12 +92,10 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
|
|
|
86
92
|
return node;
|
|
87
93
|
}
|
|
88
94
|
const kgTree = buildFileTree({ id: topFile.id, path: topFile.path, summary: topFile.summary }, kgDepth);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// --- Step 4: File tree (shallow, depth 2) ---
|
|
92
|
-
let fileTree = "";
|
|
95
|
+
promptSections.push(`**KG-Related Files (JSON tree, depth ${kgDepth}):**\n\`\`\`json\n${JSON.stringify(kgTree, null, 2)}\n\`\`\``);
|
|
96
|
+
// --- Step 3: File tree (shallow) ---
|
|
93
97
|
try {
|
|
94
|
-
fileTree = generateFocusedFileTree(topFile.path, 2);
|
|
98
|
+
const fileTree = generateFocusedFileTree(topFile.path, 2);
|
|
95
99
|
if (fileTree) {
|
|
96
100
|
promptSections.push(`**Focused File Tree (depth 2):**\n\`\`\`\n${fileTree}\n\`\`\``);
|
|
97
101
|
}
|
|
@@ -99,21 +103,18 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
|
|
|
99
103
|
catch (e) {
|
|
100
104
|
console.warn("⚠️ Could not generate file tree:", e);
|
|
101
105
|
}
|
|
102
|
-
// --- Step
|
|
103
|
-
// Only include raw code if no summary exists, or if the query explicitly asks for it
|
|
106
|
+
// --- Step 4: Optional code snippet ---
|
|
104
107
|
const MAX_LINES = 50;
|
|
105
108
|
const queryNeedsCode = /\b(code|implementation|function|snippet)\b/i.test(query);
|
|
106
109
|
if ((!topFile.summary || queryNeedsCode) && topFile.code) {
|
|
107
110
|
const lines = topFile.code.split("\n").slice(0, MAX_LINES);
|
|
108
111
|
let snippet = lines.join("\n");
|
|
109
|
-
if (topFile.code.split("\n").length > MAX_LINES)
|
|
112
|
+
if (topFile.code.split("\n").length > MAX_LINES)
|
|
110
113
|
snippet += "\n... [truncated]";
|
|
111
|
-
}
|
|
112
114
|
promptSections.push(`**Code Context (first ${MAX_LINES} lines):**\n\`\`\`\n${snippet}\n\`\`\``);
|
|
113
115
|
}
|
|
114
|
-
// --- Step
|
|
116
|
+
// --- Step 5: User query ---
|
|
115
117
|
promptSections.push(`**Query:** ${query}`);
|
|
116
|
-
// --- Step 7: Combine prompt ---
|
|
117
118
|
const promptText = promptSections.join("\n\n---\n\n");
|
|
118
119
|
log("✅ Contextual prompt built for:", topFile.path);
|
|
119
120
|
log("📄 Prompt preview:\n", promptText + "\n");
|