scai 0.1.108 → 0.1.110
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 +24 -1
- package/dist/__tests__/example.test.js +10 -0
- package/dist/agent/agentManager.js +14 -2
- package/dist/agent/workflowManager.js +5 -0
- package/dist/commands/DaemonCmd.js +3 -1
- package/dist/config.js +13 -8
- package/dist/context.js +36 -10
- package/dist/daemon/daemonBatch.js +68 -14
- package/dist/daemon/daemonWorker.js +19 -2
- package/dist/db/functionExtractors/extractFromJs.js +96 -16
- package/dist/db/functionExtractors/extractFromTs.js +73 -16
- package/dist/db/functionExtractors/index.js +34 -33
- package/dist/db/functionIndex.js +1 -1
- package/dist/db/schema.js +51 -5
- package/dist/index.js +5 -9
- package/dist/modelSetup.js +17 -20
- package/dist/pipeline/modules/cleanGeneratedTestsModule.js +17 -6
- package/dist/pipeline/modules/cleanupModule.js +32 -13
- package/dist/pipeline/modules/kgModule.js +55 -0
- package/dist/pipeline/modules/repairTestsModule.js +40 -0
- package/dist/pipeline/modules/runTestsModule.js +37 -0
- package/dist/pipeline/registry/moduleRegistry.js +17 -0
- package/dist/scripts/dbcheck.js +98 -0
- package/dist/utils/log.js +1 -1
- package/package.json +2 -2
- package/dist/jest.config.js +0 -11
- package/dist/jest.setup.js +0 -14
- package/dist/utils/runTests.js +0 -11
package/dist/CHANGELOG.md
CHANGED
|
@@ -166,4 +166,27 @@ Type handling with the module pipeline
|
|
|
166
166
|
|
|
167
167
|
## 2025-09-01
|
|
168
168
|
|
|
169
|
-
* Improve handling of GitHub repository URLs and paths by extracting the owner and name separately
|
|
169
|
+
* Improve handling of GitHub repository URLs and paths by extracting the owner and name separately
|
|
170
|
+
|
|
171
|
+
## 2025-09-02
|
|
172
|
+
|
|
173
|
+
• Added test configuration for project and generated tests
|
|
174
|
+
• Add runTestsModule and repairTestsModule for testing pipeline
|
|
175
|
+
|
|
176
|
+
## 2025-09-05
|
|
177
|
+
|
|
178
|
+
• Enable execution of files as executable files in the scripts
|
|
179
|
+
• Remove context failure if models not installed
|
|
180
|
+
• Add ability to set global model
|
|
181
|
+
|
|
182
|
+
## 2025-09-08
|
|
183
|
+
|
|
184
|
+
### Requires DB reset ('scai db reset' followed by 'scai index start')
|
|
185
|
+
|
|
186
|
+
1. Improved daemon batch processing by skipping missing files, classifying unknown file types, and persisting entities/tags in the database.
|
|
187
|
+
2. Invoke kgModule in daemonBatch to build knowledge graphs after indexing.
|
|
188
|
+
3. Improved data modeling and extraction logic for functions and classes in TypeScript files.
|
|
189
|
+
4. Updated Edge/Table schema for better query performance.
|
|
190
|
+
5. Update package-lock.json to caniuse-lite@1.0.30001741.
|
|
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.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SCAI_HOME } from "../constants"; // example constant
|
|
2
|
+
// cli/src/__tests__/example.test.ts
|
|
3
|
+
describe("CLI src basic test", () => {
|
|
4
|
+
it("should pass a simple truthy test", () => {
|
|
5
|
+
expect(true).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
it("should import a constant from cli/src/constants.ts", () => {
|
|
8
|
+
expect(typeof SCAI_HOME).not.toBe("undefined");
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -18,8 +18,20 @@ export class Agent {
|
|
|
18
18
|
// Resolve modules (with before/after dependencies)
|
|
19
19
|
const modules = this.resolveModules(this.goals);
|
|
20
20
|
console.log(chalk.green("📋 Modules to run:"), modules.map((m) => m.name).join(" → "));
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
try {
|
|
22
|
+
// Check that the file exists before trying to read it
|
|
23
|
+
await fs.access(filepath);
|
|
24
|
+
// Read file content (optional, could be used by modules in workflow)
|
|
25
|
+
await fs.readFile(filepath, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err.code === "ENOENT") {
|
|
29
|
+
console.error(chalk.redBright("❌ Error:"), `File not found: ${chalk.yellow(filepath)}`);
|
|
30
|
+
console.error(`Make sure the path is correct. (cwd: ${chalk.gray(process.cwd())})`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
throw err; // rethrow for unexpected errors
|
|
34
|
+
}
|
|
23
35
|
// Delegate everything to handleAgentRun (like CLI commands do)
|
|
24
36
|
await handleAgentRun(filepath, modules);
|
|
25
37
|
console.log(chalk.green("✅ Agent finished!"));
|
|
@@ -76,6 +76,11 @@ export async function handleAgentRun(filepath, modules) {
|
|
|
76
76
|
baseChunks.length = 0;
|
|
77
77
|
baseChunks.push(...reset);
|
|
78
78
|
break;
|
|
79
|
+
case 'skip':
|
|
80
|
+
console.log(chalk.gray(`⏭️ Skipped writing for module ${mod.name}`));
|
|
81
|
+
// don’t touch files, but keep chunks flowing
|
|
82
|
+
workingChunks = processed;
|
|
83
|
+
break;
|
|
79
84
|
default:
|
|
80
85
|
console.log(chalk.yellow(`⚠️ Unknown mode; skipping write`));
|
|
81
86
|
// still move pipeline forward with processed
|
|
@@ -23,9 +23,11 @@ export async function startDaemon() {
|
|
|
23
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
24
|
const __dirname = path.dirname(__filename);
|
|
25
25
|
const daemonWorkerPath = path.join(__dirname, '../daemon/daemonWorker.js');
|
|
26
|
+
const out = fsSync.openSync(LOG_PATH, 'a');
|
|
27
|
+
const err = fsSync.openSync(LOG_PATH, 'a');
|
|
26
28
|
const child = spawn(process.execPath, [daemonWorkerPath], {
|
|
27
29
|
detached: true,
|
|
28
|
-
stdio: ['ignore',
|
|
30
|
+
stdio: ['ignore', out, err], // stdout/stderr -> log file
|
|
29
31
|
env: {
|
|
30
32
|
...process.env,
|
|
31
33
|
BACKGROUND_MODE: 'true',
|
package/dist/config.js
CHANGED
|
@@ -6,7 +6,7 @@ import { normalizePath } from './utils/contentUtils.js';
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { getHashedRepoKey } from './utils/repoKey.js';
|
|
8
8
|
const defaultConfig = {
|
|
9
|
-
model: '
|
|
9
|
+
model: 'llama3:8b',
|
|
10
10
|
contextLength: 4096,
|
|
11
11
|
language: 'ts',
|
|
12
12
|
indexDir: '',
|
|
@@ -55,18 +55,23 @@ export const Config = {
|
|
|
55
55
|
const repoCfg = cfg.repos?.[cfg.activeRepo ?? ''];
|
|
56
56
|
return repoCfg?.model || cfg.model;
|
|
57
57
|
},
|
|
58
|
-
setModel(model) {
|
|
58
|
+
setModel(model, scope = 'repo') {
|
|
59
59
|
const cfg = readConfig();
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
if (scope === 'repo') {
|
|
61
|
+
const active = cfg.activeRepo;
|
|
62
|
+
if (!active) {
|
|
63
|
+
console.error("❌ No active repo to set model for.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
62
66
|
cfg.repos[active] = { ...cfg.repos[active], model };
|
|
63
|
-
|
|
64
|
-
console.log(`📦 Model set to: ${model}`);
|
|
67
|
+
console.log(`📦 Model set for repo '${active}': ${model}`);
|
|
65
68
|
}
|
|
66
69
|
else {
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
// Set global default model
|
|
71
|
+
cfg.model = model;
|
|
72
|
+
console.log(`📦 Global default model set to: ${model}`);
|
|
69
73
|
}
|
|
74
|
+
writeConfig(cfg);
|
|
70
75
|
},
|
|
71
76
|
getLanguage() {
|
|
72
77
|
const cfg = readConfig();
|
package/dist/context.js
CHANGED
|
@@ -5,12 +5,25 @@ import { getHashedRepoKey } from "./utils/repoKey.js";
|
|
|
5
5
|
import { getDbForRepo, getDbPathForRepo } from "./db/client.js";
|
|
6
6
|
import fs from "fs";
|
|
7
7
|
import chalk from "chalk";
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
function modelExists(model) {
|
|
10
|
+
try {
|
|
11
|
+
const output = execSync("ollama list", { encoding: "utf-8" });
|
|
12
|
+
return output
|
|
13
|
+
.split("\n")
|
|
14
|
+
.map(line => line.trim())
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.some(line => line.toLowerCase().startsWith(model.toLowerCase() + " ") || line.toLowerCase() === model.toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.error(chalk.red("❌ Failed to check models with `ollama list`"));
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
8
23
|
export async function updateContext() {
|
|
9
24
|
const cwd = normalizePath(process.cwd());
|
|
10
25
|
const cfg = readConfig();
|
|
11
|
-
// 🔑 Find repoKey by matching indexDir to cwd
|
|
12
26
|
let repoKey = Object.keys(cfg.repos || {}).find((key) => normalizePath(cfg.repos[key]?.indexDir || "") === cwd);
|
|
13
|
-
// Initialize new repo config if not found
|
|
14
27
|
let isNewRepo = false;
|
|
15
28
|
if (!repoKey) {
|
|
16
29
|
repoKey = getHashedRepoKey(cwd);
|
|
@@ -19,28 +32,23 @@ export async function updateContext() {
|
|
|
19
32
|
cfg.repos[repoKey].indexDir = cwd;
|
|
20
33
|
isNewRepo = true;
|
|
21
34
|
}
|
|
22
|
-
// Check if active repo has changed
|
|
23
35
|
const activeRepoChanged = cfg.activeRepo !== repoKey;
|
|
24
|
-
// Always set this as active repo
|
|
25
36
|
cfg.activeRepo = repoKey;
|
|
26
37
|
writeConfig(cfg);
|
|
27
38
|
const repoCfg = cfg.repos[repoKey];
|
|
28
39
|
let ok = true;
|
|
29
|
-
// Only log detailed info if new repo or active repo changed
|
|
30
40
|
if (isNewRepo || activeRepoChanged) {
|
|
31
41
|
console.log(chalk.yellow("\n🔁 Updating context...\n"));
|
|
32
42
|
console.log(`✅ Active repo: ${chalk.green(repoKey)}`);
|
|
33
43
|
console.log(`✅ Index dir: ${chalk.cyan(repoCfg.indexDir || cwd)}`);
|
|
34
44
|
}
|
|
35
|
-
// GitHub token is optional
|
|
36
45
|
const token = repoCfg.githubToken || cfg.githubToken;
|
|
37
46
|
if (!token) {
|
|
38
|
-
console.log(`ℹ️ No GitHub token found. You can set one with
|
|
47
|
+
console.log(`ℹ️ No GitHub token found. You can set one with: ${chalk.bold(chalk.bgGreen("scai auth set"))}`);
|
|
39
48
|
}
|
|
40
49
|
else if (isNewRepo || activeRepoChanged) {
|
|
41
50
|
console.log(`✅ GitHub token present`);
|
|
42
51
|
}
|
|
43
|
-
// Ensure DB exists
|
|
44
52
|
const dbPath = getDbPathForRepo();
|
|
45
53
|
if (!fs.existsSync(dbPath)) {
|
|
46
54
|
console.log(chalk.yellow(`📦 Initializing DB at ${dbPath}`));
|
|
@@ -48,13 +56,31 @@ export async function updateContext() {
|
|
|
48
56
|
getDbForRepo();
|
|
49
57
|
}
|
|
50
58
|
catch {
|
|
51
|
-
ok = false;
|
|
59
|
+
ok = false;
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
else if (isNewRepo || activeRepoChanged) {
|
|
55
63
|
console.log(chalk.green("✅ Database present"));
|
|
56
64
|
}
|
|
57
|
-
//
|
|
65
|
+
// 🧠 Model check
|
|
66
|
+
const model = cfg.model;
|
|
67
|
+
if (!model) {
|
|
68
|
+
console.log(chalk.red("❌ No model configured.") +
|
|
69
|
+
"\n➡️ Set one with: " +
|
|
70
|
+
chalk.bold(chalk.bgGreen("scai config set-model <model>")));
|
|
71
|
+
ok = false;
|
|
72
|
+
}
|
|
73
|
+
else if (!modelExists(model)) {
|
|
74
|
+
console.log(chalk.red(`❌ Model '${model}' not installed in Ollama.`) +
|
|
75
|
+
"\n➡️ Install with: " +
|
|
76
|
+
chalk.bold(chalk.yellow(`ollama pull ${model}`)) +
|
|
77
|
+
" or choose another with: " +
|
|
78
|
+
chalk.bold(chalk.yellow("scai config set-model <model>")));
|
|
79
|
+
ok = false;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(chalk.green(`✅ Model '${model}' available`));
|
|
83
|
+
}
|
|
58
84
|
if (ok) {
|
|
59
85
|
console.log(chalk.bold.green("\n✅ Context OK\n"));
|
|
60
86
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { indexCodeForFile } from '../db/functionIndex.js';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import fsSync from 'fs';
|
|
4
4
|
import { generateEmbedding } from '../lib/generateEmbedding.js';
|
|
@@ -8,6 +8,7 @@ import { summaryModule } from '../pipeline/modules/summaryModule.js';
|
|
|
8
8
|
import { classifyFile } from '../fileRules/classifyFile.js';
|
|
9
9
|
import { getDbForRepo, getDbPathForRepo } from '../db/client.js';
|
|
10
10
|
import { markFileAsSkippedByPath, selectUnprocessedFiles, updateFileWithSummaryAndEmbedding, } from '../db/sqlTemplates.js';
|
|
11
|
+
import { kgModule } from '../pipeline/modules/kgModule.js';
|
|
11
12
|
const MAX_FILES_PER_BATCH = 5;
|
|
12
13
|
/**
|
|
13
14
|
* Acquires a lock on the database to ensure that only one daemon batch
|
|
@@ -32,7 +33,6 @@ async function lockDb() {
|
|
|
32
33
|
*/
|
|
33
34
|
export async function runDaemonBatch() {
|
|
34
35
|
log('🟡 Starting daemon batch...');
|
|
35
|
-
// Selects up to MAX_FILES_PER_BATCH files that haven't been processed yet
|
|
36
36
|
const db = getDbForRepo();
|
|
37
37
|
const rows = db.prepare(selectUnprocessedFiles).all(MAX_FILES_PER_BATCH);
|
|
38
38
|
if (rows.length === 0) {
|
|
@@ -42,13 +42,11 @@ export async function runDaemonBatch() {
|
|
|
42
42
|
const release = await lockDb();
|
|
43
43
|
for (const row of rows) {
|
|
44
44
|
log(`📂 Processing file: ${row.path}`);
|
|
45
|
-
// Skip if file is missing from the file system
|
|
46
45
|
if (!fsSync.existsSync(row.path)) {
|
|
47
46
|
log(`⚠️ Skipped missing file: ${row.path}`);
|
|
48
47
|
db.prepare(markFileAsSkippedByPath).run({ path: row.path });
|
|
49
48
|
continue;
|
|
50
49
|
}
|
|
51
|
-
// Skip if file is classified as something we don't process
|
|
52
50
|
const classification = classifyFile(row.path);
|
|
53
51
|
if (classification !== 'valid') {
|
|
54
52
|
log(`⏭️ Skipping (${classification}): ${row.path}`);
|
|
@@ -57,24 +55,20 @@ export async function runDaemonBatch() {
|
|
|
57
55
|
}
|
|
58
56
|
try {
|
|
59
57
|
const content = await fs.readFile(row.path, 'utf-8');
|
|
60
|
-
// Determine whether the file needs to be re-summarized
|
|
61
58
|
const needsResummary = !row.summary ||
|
|
62
59
|
!row.indexed_at ||
|
|
63
60
|
(row.last_modified && new Date(row.last_modified) > new Date(row.indexed_at));
|
|
64
61
|
if (needsResummary) {
|
|
65
62
|
log(`📝 Generating summary for ${row.path}...`);
|
|
66
|
-
// Generate a summary using the summary pipeline
|
|
67
63
|
const summaryResult = await summaryModule.run({ content, filepath: row.path });
|
|
68
64
|
const summary = summaryResult?.summary?.trim() || null;
|
|
69
65
|
let embedding = null;
|
|
70
|
-
// Generate an embedding from the summary (if present)
|
|
71
66
|
if (summary) {
|
|
72
67
|
const vector = await generateEmbedding(summary);
|
|
73
68
|
if (vector) {
|
|
74
69
|
embedding = JSON.stringify(vector);
|
|
75
70
|
}
|
|
76
71
|
}
|
|
77
|
-
// Update the file record with the new summary and embedding
|
|
78
72
|
db.prepare(updateFileWithSummaryAndEmbedding).run({
|
|
79
73
|
summary,
|
|
80
74
|
embedding,
|
|
@@ -85,19 +79,79 @@ export async function runDaemonBatch() {
|
|
|
85
79
|
else {
|
|
86
80
|
log(`⚡ Skipped summary (up-to-date) for ${row.path}`);
|
|
87
81
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
const success = await indexCodeForFile(row.path, row.id);
|
|
83
|
+
if (success) {
|
|
84
|
+
log(`✅ Indexed code for ${row.path}`);
|
|
85
|
+
try {
|
|
86
|
+
log(`🔗 Building Knowledge Graph for ${row.path}...`);
|
|
87
|
+
const kgInput = {
|
|
88
|
+
fileId: row.id,
|
|
89
|
+
filepath: row.path,
|
|
90
|
+
summary: row.summary || undefined,
|
|
91
|
+
};
|
|
92
|
+
const kgResult = await kgModule.run(kgInput, content);
|
|
93
|
+
log(`✅ Knowledge Graph built for ${row.path}`);
|
|
94
|
+
log(`Entities: ${kgResult.entities.length}, Edges: ${kgResult.edges.length}`);
|
|
95
|
+
// Persist KG entities + tags only if there are any
|
|
96
|
+
if (kgResult.entities.length > 0) {
|
|
97
|
+
const insertTag = db.prepare(`
|
|
98
|
+
INSERT OR IGNORE INTO tags_master (name) VALUES (:name)
|
|
99
|
+
`);
|
|
100
|
+
const getTagId = db.prepare(`
|
|
101
|
+
SELECT id FROM tags_master WHERE name = :name
|
|
102
|
+
`);
|
|
103
|
+
const insertEntityTag = db.prepare(`
|
|
104
|
+
INSERT OR IGNORE INTO entity_tags (entity_type, entity_id, tag_id)
|
|
105
|
+
VALUES (:entity_type, :entity_id, :tag_id)
|
|
106
|
+
`);
|
|
107
|
+
for (const entity of kgResult.entities) {
|
|
108
|
+
// Skip entity if type or tags are missing
|
|
109
|
+
if (!entity.type || !Array.isArray(entity.tags) || entity.tags.length === 0) {
|
|
110
|
+
console.warn(`⚠ Skipping entity due to missing type or tags:`, entity);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
for (const tag of entity.tags) {
|
|
114
|
+
// Skip empty or invalid tags
|
|
115
|
+
if (!tag || typeof tag !== 'string') {
|
|
116
|
+
console.warn(`⚠ Skipping invalid tag for entity ${entity.type}:`, tag);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
// ✅ Use :name in SQL and plain key in object
|
|
121
|
+
insertTag.run({ name: tag });
|
|
122
|
+
const tagRow = getTagId.get({ name: tag });
|
|
123
|
+
if (!tagRow) {
|
|
124
|
+
console.warn(`⚠ Could not find tag ID for: ${tag}`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
insertEntityTag.run({
|
|
128
|
+
entity_type: entity.type,
|
|
129
|
+
entity_id: row.id,
|
|
130
|
+
tag_id: tagRow.id,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.error(`❌ Failed to persist entity/tag:`, { entity, tag, error: err });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
log(`✅ Persisted entities + tags for ${row.path}`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
log(`⚠️ No entities found for ${row.path}, skipping DB inserts`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (kgErr) {
|
|
145
|
+
log(`❌ KG build failed for ${row.path}: ${kgErr instanceof Error ? kgErr.message : String(kgErr)}`);
|
|
146
|
+
}
|
|
92
147
|
}
|
|
93
148
|
else {
|
|
94
|
-
log(`ℹ️ No
|
|
149
|
+
log(`ℹ️ No code elements extracted for ${row.path}`);
|
|
95
150
|
}
|
|
96
151
|
}
|
|
97
152
|
catch (err) {
|
|
98
153
|
log(`❌ Failed: ${row.path}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
99
154
|
}
|
|
100
|
-
// Add a small delay to throttle processing
|
|
101
155
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
102
156
|
}
|
|
103
157
|
await release();
|
|
@@ -31,9 +31,26 @@ export async function daemonWorker() {
|
|
|
31
31
|
while (true) {
|
|
32
32
|
try {
|
|
33
33
|
log('🔄 Running daemon batch...');
|
|
34
|
-
|
|
34
|
+
// Wrap the batch in debug
|
|
35
|
+
let didWork = false;
|
|
36
|
+
try {
|
|
37
|
+
log('🔹 Running runDaemonBatch()...');
|
|
38
|
+
didWork = await runDaemonBatch();
|
|
39
|
+
log('✅ runDaemonBatch() completed successfully');
|
|
40
|
+
}
|
|
41
|
+
catch (batchErr) {
|
|
42
|
+
log('🔥 Error inside runDaemonBatch():', batchErr);
|
|
43
|
+
}
|
|
35
44
|
if (!didWork) {
|
|
36
|
-
|
|
45
|
+
let queueEmpty = false;
|
|
46
|
+
try {
|
|
47
|
+
log('🔹 Checking if queue is empty...');
|
|
48
|
+
queueEmpty = await isQueueEmpty();
|
|
49
|
+
log(`🔹 Queue empty status: ${queueEmpty}`);
|
|
50
|
+
}
|
|
51
|
+
catch (queueErr) {
|
|
52
|
+
log('🔥 Error checking queue status:', queueErr);
|
|
53
|
+
}
|
|
37
54
|
if (queueEmpty) {
|
|
38
55
|
log('🕊️ No work found. Idling...');
|
|
39
56
|
await sleep(IDLE_SLEEP_MS * 3);
|
|
@@ -29,6 +29,7 @@ export async function extractFromJS(filePath, content, fileId) {
|
|
|
29
29
|
locations: true,
|
|
30
30
|
});
|
|
31
31
|
const functions = [];
|
|
32
|
+
const classes = [];
|
|
32
33
|
walkAncestor(ast, {
|
|
33
34
|
FunctionDeclaration(node, ancestors) {
|
|
34
35
|
const parent = ancestors[ancestors.length - 2];
|
|
@@ -60,31 +61,63 @@ export async function extractFromJS(filePath, content, fileId) {
|
|
|
60
61
|
content: content.slice(node.start, node.end),
|
|
61
62
|
});
|
|
62
63
|
},
|
|
64
|
+
ClassDeclaration(node) {
|
|
65
|
+
const className = node.id?.name || `${path.basename(filePath)}:<anon-class>`;
|
|
66
|
+
classes.push({
|
|
67
|
+
name: className,
|
|
68
|
+
start_line: node.loc?.start.line ?? -1,
|
|
69
|
+
end_line: node.loc?.end.line ?? -1,
|
|
70
|
+
content: content.slice(node.start, node.end),
|
|
71
|
+
superClass: node.superClass?.name ?? null,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
ClassExpression(node) {
|
|
75
|
+
const className = node.id?.name || `${path.basename(filePath)}:<anon-class>`;
|
|
76
|
+
classes.push({
|
|
77
|
+
name: className,
|
|
78
|
+
start_line: node.loc?.start.line ?? -1,
|
|
79
|
+
end_line: node.loc?.end.line ?? -1,
|
|
80
|
+
content: content.slice(node.start, node.end),
|
|
81
|
+
superClass: node.superClass?.name ?? null,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
63
84
|
});
|
|
64
|
-
if (functions.length === 0) {
|
|
65
|
-
log(`⚠️ No functions found in: ${filePath}`);
|
|
85
|
+
if (functions.length === 0 && classes.length === 0) {
|
|
86
|
+
log(`⚠️ No functions/classes found in: ${filePath}`);
|
|
66
87
|
db.prepare(markFileAsSkippedTemplate).run({ id: fileId });
|
|
67
88
|
return false;
|
|
68
89
|
}
|
|
69
|
-
log(`🔍 Found ${functions.length} functions in ${filePath}`);
|
|
90
|
+
log(`🔍 Found ${functions.length} functions and ${classes.length} classes in ${filePath}`);
|
|
91
|
+
// Insert functions
|
|
70
92
|
for (const fn of functions) {
|
|
71
93
|
const embedding = await generateEmbedding(fn.content);
|
|
72
|
-
const result = db
|
|
94
|
+
const result = db
|
|
95
|
+
.prepare(`
|
|
73
96
|
INSERT INTO functions (
|
|
74
97
|
file_id, name, start_line, end_line, content, embedding, lang
|
|
75
98
|
) VALUES (
|
|
76
99
|
@file_id, @name, @start_line, @end_line, @content, @embedding, @lang
|
|
77
100
|
)
|
|
78
|
-
`)
|
|
101
|
+
`)
|
|
102
|
+
.run({
|
|
79
103
|
file_id: fileId,
|
|
80
104
|
name: fn.name,
|
|
81
105
|
start_line: fn.start_line,
|
|
82
106
|
end_line: fn.end_line,
|
|
83
107
|
content: fn.content,
|
|
84
108
|
embedding: JSON.stringify(embedding),
|
|
85
|
-
lang: 'js'
|
|
109
|
+
lang: 'js',
|
|
110
|
+
});
|
|
111
|
+
const functionId = result.lastInsertRowid;
|
|
112
|
+
// file → function edge
|
|
113
|
+
db.prepare(`INSERT INTO edges (source_type, source_id, target_type, target_id, relation)
|
|
114
|
+
VALUES (@source_type, @source_id, @target_type, @target_id, 'contains')`).run({
|
|
115
|
+
source_type: 'file',
|
|
116
|
+
source_id: fileId,
|
|
117
|
+
target_type: 'function',
|
|
118
|
+
target_id: functionId,
|
|
86
119
|
});
|
|
87
|
-
|
|
120
|
+
// Walk inside function to find calls
|
|
88
121
|
const fnAst = parse(fn.content, {
|
|
89
122
|
ecmaVersion: 'latest',
|
|
90
123
|
sourceType: 'module',
|
|
@@ -96,26 +129,73 @@ export async function extractFromJS(filePath, content, fileId) {
|
|
|
96
129
|
if (node.callee?.type === 'Identifier' && node.callee.name) {
|
|
97
130
|
calls.push({ calleeName: node.callee.name });
|
|
98
131
|
}
|
|
99
|
-
}
|
|
132
|
+
},
|
|
100
133
|
});
|
|
101
134
|
for (const call of calls) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
135
|
+
// Store name for later resolution
|
|
136
|
+
db.prepare(`INSERT INTO function_calls (caller_id, callee_name) VALUES (@caller_id, @callee_name)`).run({ caller_id: functionId, callee_name: call.calleeName });
|
|
137
|
+
// Optional unresolved edge
|
|
138
|
+
db.prepare(`INSERT INTO edges (source_type, source_id, target_type, target_id, relation)
|
|
139
|
+
VALUES (@source_type, @source_id, @target_type, @target_id, 'calls')`).run({
|
|
140
|
+
source_type: 'function',
|
|
141
|
+
source_id: functionId,
|
|
142
|
+
target_type: 'function',
|
|
143
|
+
target_id: 0, // unresolved callee
|
|
108
144
|
});
|
|
109
145
|
}
|
|
110
146
|
log(`📌 Indexed function: ${fn.name} with ${calls.length} calls`);
|
|
111
147
|
}
|
|
148
|
+
// Insert classes
|
|
149
|
+
for (const cls of classes) {
|
|
150
|
+
const embedding = await generateEmbedding(cls.content);
|
|
151
|
+
const result = db
|
|
152
|
+
.prepare(`
|
|
153
|
+
INSERT INTO classes (
|
|
154
|
+
file_id, name, start_line, end_line, content, embedding, lang
|
|
155
|
+
) VALUES (
|
|
156
|
+
@file_id, @name, @start_line, @end_line, @content, @embedding, @lang
|
|
157
|
+
)
|
|
158
|
+
`)
|
|
159
|
+
.run({
|
|
160
|
+
file_id: fileId,
|
|
161
|
+
name: cls.name,
|
|
162
|
+
start_line: cls.start_line,
|
|
163
|
+
end_line: cls.end_line,
|
|
164
|
+
content: cls.content,
|
|
165
|
+
embedding: JSON.stringify(embedding),
|
|
166
|
+
lang: 'js',
|
|
167
|
+
});
|
|
168
|
+
const classId = result.lastInsertRowid;
|
|
169
|
+
// file → class edge
|
|
170
|
+
db.prepare(`INSERT INTO edges (source_type, source_id, target_type, target_id, relation)
|
|
171
|
+
VALUES (@source_type, @source_id, @target_type, @target_id, 'contains')`).run({
|
|
172
|
+
source_type: 'file',
|
|
173
|
+
source_id: fileId,
|
|
174
|
+
target_type: 'class',
|
|
175
|
+
target_id: classId,
|
|
176
|
+
});
|
|
177
|
+
// superclass → store unresolved reference
|
|
178
|
+
if (cls.superClass) {
|
|
179
|
+
db.prepare(`INSERT INTO edges (source_type, source_id, target_type, target_id, relation)
|
|
180
|
+
VALUES (@source_type, @source_id, @target_type, @target_id, 'inherits')`).run({
|
|
181
|
+
source_type: 'class',
|
|
182
|
+
source_id: classId,
|
|
183
|
+
target_type: 'class',
|
|
184
|
+
target_id: 0, // unresolved superclass
|
|
185
|
+
});
|
|
186
|
+
console.log(`🔗 Class ${cls.name} inherits ${cls.superClass} (edge stored for later resolution)`);
|
|
187
|
+
}
|
|
188
|
+
console.log(`🏷 Indexed class: ${cls.name} (id=${classId})`);
|
|
189
|
+
}
|
|
190
|
+
// Optional summary after extraction
|
|
191
|
+
console.log(`📊 Extraction summary for ${filePath}: ${functions.length} functions, ${classes.length} classes`);
|
|
112
192
|
db.prepare(markFileAsExtractedTemplate).run({ id: fileId });
|
|
113
|
-
log(`✅ Marked functions as extracted for ${filePath}`);
|
|
193
|
+
log(`✅ Marked functions/classes as extracted for ${filePath}`);
|
|
114
194
|
return true;
|
|
115
195
|
}
|
|
116
196
|
catch (err) {
|
|
117
197
|
log(`❌ Failed to extract from: ${filePath}`);
|
|
118
|
-
log(` ↳ ${
|
|
198
|
+
log(` ↳ ${err.message}`);
|
|
119
199
|
db.prepare(markFileAsFailedTemplate).run({ id: fileId });
|
|
120
200
|
return false;
|
|
121
201
|
}
|