scai 0.1.116 → 0.1.118
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/agents/MainAgent.js +255 -0
- package/dist/agents/contextReviewStep.js +104 -0
- package/dist/agents/finalPlanGenStep.js +123 -0
- package/dist/agents/infoPlanGenStep.js +126 -0
- package/dist/agents/planGeneratorStep.js +118 -0
- package/dist/agents/planResolverStep.js +95 -0
- package/dist/agents/planTargetFilesStep.js +48 -0
- package/dist/agents/preFileSearchCheckStep.js +95 -0
- package/dist/agents/selectRelevantSourcesStep.js +100 -0
- package/dist/agents/semanticAnalysisStep.js +144 -0
- package/dist/agents/structuralAnalysisStep.js +46 -0
- package/dist/agents/transformPlanGenStep.js +107 -0
- package/dist/agents/understandIntentStep.js +72 -0
- package/dist/agents/validationAnalysisStep.js +87 -0
- package/dist/commands/AskCmd.js +47 -116
- package/dist/commands/ChangeLogUpdateCmd.js +11 -5
- package/dist/commands/CommitSuggesterCmd.js +50 -75
- package/dist/commands/DaemonCmd.js +119 -29
- package/dist/commands/IndexCmd.js +41 -24
- package/dist/commands/InspectCmd.js +0 -1
- package/dist/commands/ReadlineSingleton.js +18 -0
- package/dist/commands/ResetDbCmd.js +20 -21
- package/dist/commands/ReviewCmd.js +89 -54
- package/dist/commands/SummaryCmd.js +12 -18
- package/dist/commands/WorkflowCmd.js +41 -0
- package/dist/commands/factory.js +254 -0
- package/dist/config.js +67 -15
- package/dist/constants.js +20 -4
- package/dist/context.js +10 -11
- package/dist/daemon/daemonQueues.js +63 -0
- package/dist/daemon/daemonWorker.js +40 -63
- package/dist/daemon/generateSummaries.js +58 -0
- package/dist/daemon/runFolderCapsuleBatch.js +247 -0
- package/dist/daemon/runIndexingBatch.js +147 -0
- package/dist/daemon/runKgBatch.js +104 -0
- package/dist/db/fileIndex.js +168 -63
- package/dist/db/functionExtractors/extractFromJava.js +210 -6
- package/dist/db/functionExtractors/extractFromJs.js +186 -198
- package/dist/db/functionExtractors/extractFromTs.js +181 -192
- package/dist/db/functionExtractors/index.js +7 -5
- package/dist/db/schema.js +55 -20
- package/dist/db/sqlTemplates.js +50 -19
- package/dist/fileRules/builtins.js +31 -0
- package/dist/fileRules/codeAllowedExtensions.js +4 -0
- package/dist/fileRules/fileExceptions.js +0 -13
- package/dist/fileRules/ignoredExtensions.js +10 -0
- package/dist/index.js +128 -325
- package/dist/lib/generate.js +37 -14
- package/dist/lib/generateFolderCapsules.js +109 -0
- package/dist/lib/spinner.js +12 -5
- package/dist/modelSetup.js +35 -6
- package/dist/pipeline/modules/changeLogModule.js +16 -19
- package/dist/pipeline/modules/chunkManagerModule.js +24 -0
- package/dist/pipeline/modules/cleanupModule.js +96 -91
- package/dist/pipeline/modules/codeTransformModule.js +208 -0
- package/dist/pipeline/modules/commentModule.js +20 -11
- package/dist/pipeline/modules/commitSuggesterModule.js +36 -14
- package/dist/pipeline/modules/contextReviewModule.js +52 -0
- package/dist/pipeline/modules/fileReaderModule.js +72 -0
- package/dist/pipeline/modules/fileSearchModule.js +136 -0
- package/dist/pipeline/modules/finalAnswerModule.js +53 -0
- package/dist/pipeline/modules/gatherInfoModule.js +176 -0
- package/dist/pipeline/modules/generateTestsModule.js +63 -54
- package/dist/pipeline/modules/kgModule.js +26 -11
- package/dist/pipeline/modules/preserveCodeModule.js +91 -49
- package/dist/pipeline/modules/refactorModule.js +19 -7
- package/dist/pipeline/modules/repairTestsModule.js +44 -36
- package/dist/pipeline/modules/reviewModule.js +23 -13
- package/dist/pipeline/modules/summaryModule.js +27 -35
- package/dist/pipeline/modules/writeFileModule.js +86 -0
- package/dist/pipeline/registry/moduleRegistry.js +38 -93
- package/dist/pipeline/runModulePipeline.js +22 -19
- package/dist/scripts/dbcheck.js +156 -91
- package/dist/utils/buildContextualPrompt.js +245 -164
- package/dist/utils/debugContext.js +24 -0
- package/dist/utils/fileTree.js +16 -6
- package/dist/utils/loadRelevantFolderCapsules.js +64 -0
- package/dist/utils/log.js +2 -0
- package/dist/utils/normalizeData.js +23 -0
- package/dist/utils/planActions.js +60 -0
- package/dist/utils/promptBuilderHelper.js +67 -0
- package/dist/utils/promptLogHelper.js +52 -0
- package/dist/utils/sanitizeQuery.js +20 -8
- package/dist/utils/sharedUtils.js +8 -0
- package/dist/utils/sleep.js +3 -0
- package/dist/utils/splitCodeIntoChunk.js +65 -32
- package/dist/utils/vscode.js +49 -0
- package/dist/workflow/workflowResolver.js +14 -0
- package/dist/workflow/workflowRunner.js +103 -0
- package/package.json +6 -5
- package/dist/agent/agentManager.js +0 -39
- package/dist/agent/workflowManager.js +0 -95
- package/dist/commands/ModulePipelineCmd.js +0 -31
- package/dist/daemon/daemonBatch.js +0 -186
- package/dist/fileRules/scoreFiles.js +0 -71
- package/dist/lib/generateEmbedding.js +0 -22
package/dist/config.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { CONFIG_PATH, SCAI_HOME, SCAI_REPOS } from './constants.js';
|
|
3
|
+
import { CONFIG_LOCK_PATH, CONFIG_PATH, DEFAULT_DAEMON_IDLE_SLEEP_MS, DEFAULT_DAEMON_SLEEP_MS, PID_PATH, SCAI_HOME, SCAI_REPOS } from './constants.js';
|
|
4
4
|
import { getDbForRepo } from './db/client.js';
|
|
5
5
|
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: '
|
|
10
|
-
contextLength:
|
|
9
|
+
model: 'qwen3-coder:30b',
|
|
10
|
+
contextLength: 32768,
|
|
11
11
|
language: 'ts',
|
|
12
12
|
indexDir: '',
|
|
13
13
|
githubToken: '',
|
|
14
14
|
repos: {},
|
|
15
15
|
activeRepo: null,
|
|
16
|
+
daemon: {
|
|
17
|
+
sleepMs: DEFAULT_DAEMON_SLEEP_MS,
|
|
18
|
+
idleSleepMs: DEFAULT_DAEMON_IDLE_SLEEP_MS,
|
|
19
|
+
},
|
|
16
20
|
};
|
|
17
21
|
function ensureConfigDir() {
|
|
18
22
|
if (!fs.existsSync(SCAI_HOME)) {
|
|
@@ -49,6 +53,23 @@ export function writeConfig(newCfg) {
|
|
|
49
53
|
}
|
|
50
54
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));
|
|
51
55
|
}
|
|
56
|
+
export function daemonIsRunning() {
|
|
57
|
+
if (!fs.existsSync(PID_PATH))
|
|
58
|
+
return false;
|
|
59
|
+
const pid = parseInt(fs.readFileSync(PID_PATH, 'utf8'), 10);
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function getDaemonLockedRepo() {
|
|
69
|
+
if (!fs.existsSync(CONFIG_LOCK_PATH))
|
|
70
|
+
return null;
|
|
71
|
+
return fs.readFileSync(CONFIG_LOCK_PATH, 'utf8');
|
|
72
|
+
}
|
|
52
73
|
export const Config = {
|
|
53
74
|
getModel() {
|
|
54
75
|
const cfg = readConfig();
|
|
@@ -67,7 +88,6 @@ export const Config = {
|
|
|
67
88
|
console.log(`📦 Model set for repo '${active}': ${model}`);
|
|
68
89
|
}
|
|
69
90
|
else {
|
|
70
|
-
// Set global default model
|
|
71
91
|
cfg.model = model;
|
|
72
92
|
console.log(`📦 Global default model set to: ${model}`);
|
|
73
93
|
}
|
|
@@ -99,18 +119,12 @@ export const Config = {
|
|
|
99
119
|
return cfg.repos[activeRepo]?.indexDir ?? '';
|
|
100
120
|
},
|
|
101
121
|
async setIndexDir(indexDir) {
|
|
102
|
-
// Normalize the provided index directory
|
|
103
122
|
const normalizedIndexDir = normalizePath(indexDir);
|
|
104
|
-
// Compute a stable repo key
|
|
105
123
|
const repoKey = getHashedRepoKey(normalizedIndexDir);
|
|
106
124
|
const scaiRepoRoot = path.join(SCAI_REPOS, repoKey);
|
|
107
|
-
// Ensure base folders exist
|
|
108
125
|
fs.mkdirSync(scaiRepoRoot, { recursive: true });
|
|
109
|
-
// Set the active repo using the precomputed repoKey
|
|
110
126
|
this.setActiveRepo(repoKey);
|
|
111
|
-
// Update the repo configuration with the normalized indexDir
|
|
112
127
|
await this.setRepoIndexDir(repoKey, normalizedIndexDir);
|
|
113
|
-
// Initialize DB if it does not exist
|
|
114
128
|
const dbPath = path.join(scaiRepoRoot, 'db.sqlite');
|
|
115
129
|
if (!fs.existsSync(dbPath)) {
|
|
116
130
|
console.log(`📦 Database not found. ${chalk.green('Initializing DB')} at ${normalizePath(dbPath)}`);
|
|
@@ -121,20 +135,28 @@ export const Config = {
|
|
|
121
135
|
const cfg = readConfig();
|
|
122
136
|
if (!cfg.repos[repoKey])
|
|
123
137
|
cfg.repos[repoKey] = {};
|
|
124
|
-
cfg.repos[repoKey] = {
|
|
125
|
-
|
|
126
|
-
indexDir, // Already normalized
|
|
127
|
-
};
|
|
128
|
-
await writeConfig(cfg);
|
|
138
|
+
cfg.repos[repoKey] = { ...cfg.repos[repoKey], indexDir };
|
|
139
|
+
writeConfig(cfg);
|
|
129
140
|
console.log(`✅ Repo index directory set for ${repoKey} : ${indexDir}`);
|
|
130
141
|
},
|
|
131
142
|
setActiveRepo(repoKey) {
|
|
132
143
|
const cfg = readConfig();
|
|
144
|
+
// 🔒 Check for running daemon lock
|
|
145
|
+
const lockedRepo = getDaemonLockedRepo();
|
|
146
|
+
if (daemonIsRunning() && lockedRepo && lockedRepo !== repoKey) {
|
|
147
|
+
const sps = lockedRepo.split('-')[0];
|
|
148
|
+
console.log(`❌ Cannot switch active repo while daemon is running.\n` +
|
|
149
|
+
` Daemon is currently locked to repo: ${sps}\n` +
|
|
150
|
+
` Go to repository: ${sps}, and stop the daemon first: scai daemon stop`);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
// ✅ If allowed, switch
|
|
133
154
|
cfg.activeRepo = repoKey;
|
|
134
155
|
if (!cfg.repos[repoKey])
|
|
135
156
|
cfg.repos[repoKey] = {};
|
|
136
157
|
writeConfig(cfg);
|
|
137
158
|
console.log(`✅ Active repo switched to: ${repoKey}`);
|
|
159
|
+
return true;
|
|
138
160
|
},
|
|
139
161
|
printAllRepos() {
|
|
140
162
|
const cfg = readConfig();
|
|
@@ -175,6 +197,25 @@ export const Config = {
|
|
|
175
197
|
writeConfig(cfg);
|
|
176
198
|
console.log('✅ GitHub token updated');
|
|
177
199
|
},
|
|
200
|
+
/* ------------------- 💤 Daemon Config ------------------- */
|
|
201
|
+
getDaemonConfig() {
|
|
202
|
+
const cfg = readConfig();
|
|
203
|
+
const daemonCfg = cfg.daemon ?? { sleepMs: DEFAULT_DAEMON_SLEEP_MS, idleSleepMs: DEFAULT_DAEMON_IDLE_SLEEP_MS };
|
|
204
|
+
const sleepMs = Number(process.env.SCAI_SLEEP_MS) || daemonCfg.sleepMs;
|
|
205
|
+
const idleSleepMs = Number(process.env.SCAI_IDLE_SLEEP_MS) || daemonCfg.idleSleepMs;
|
|
206
|
+
return { sleepMs, idleSleepMs };
|
|
207
|
+
},
|
|
208
|
+
setDaemonConfig(newCfg) {
|
|
209
|
+
const cfg = readConfig();
|
|
210
|
+
cfg.daemon = {
|
|
211
|
+
sleepMs: DEFAULT_DAEMON_SLEEP_MS,
|
|
212
|
+
idleSleepMs: DEFAULT_DAEMON_IDLE_SLEEP_MS,
|
|
213
|
+
...cfg.daemon,
|
|
214
|
+
...newCfg,
|
|
215
|
+
};
|
|
216
|
+
writeConfig(cfg);
|
|
217
|
+
console.log(`🕒 Daemon configuration updated: ${JSON.stringify(cfg.daemon, null, 2)}`);
|
|
218
|
+
},
|
|
178
219
|
show() {
|
|
179
220
|
const cfg = readConfig();
|
|
180
221
|
const active = cfg.activeRepo;
|
|
@@ -184,6 +225,17 @@ export const Config = {
|
|
|
184
225
|
console.log(` Model : ${repoCfg?.model || cfg.model}`);
|
|
185
226
|
console.log(` Language : ${repoCfg?.language || cfg.language}`);
|
|
186
227
|
console.log(` GitHub Token : ${cfg.githubToken ? '*****' : 'Not Set'}`);
|
|
228
|
+
const daemon = this.getDaemonConfig();
|
|
229
|
+
console.log(` Daemon sleepMs : ${daemon.sleepMs}ms`);
|
|
230
|
+
console.log(` Daemon idleMs : ${daemon.idleSleepMs}ms`);
|
|
231
|
+
// ✅ Show lock status
|
|
232
|
+
let lockStatus = 'Unlocked';
|
|
233
|
+
let lockedRepo = null;
|
|
234
|
+
if (fs.existsSync(CONFIG_LOCK_PATH)) {
|
|
235
|
+
lockedRepo = fs.readFileSync(CONFIG_LOCK_PATH, 'utf8');
|
|
236
|
+
lockStatus = `Locked to repo '${lockedRepo}'`;
|
|
237
|
+
}
|
|
238
|
+
console.log(` Daemon Lock : ${lockStatus}`);
|
|
187
239
|
},
|
|
188
240
|
getRaw() {
|
|
189
241
|
return readConfig();
|
package/dist/constants.js
CHANGED
|
@@ -20,6 +20,12 @@ export const PID_PATH = path.join(SCAI_HOME, 'daemon.pid');
|
|
|
20
20
|
* ~/.scai/config.json
|
|
21
21
|
*/
|
|
22
22
|
export const CONFIG_PATH = path.join(SCAI_HOME, 'config.json');
|
|
23
|
+
/**
|
|
24
|
+
* Path to the daemon lock file that prevents starting multiple daemons
|
|
25
|
+
* for different repos simultaneously:
|
|
26
|
+
* ~/.scai/daemon.lock
|
|
27
|
+
*/
|
|
28
|
+
export const CONFIG_LOCK_PATH = path.join(SCAI_HOME, 'daemon.lock');
|
|
23
29
|
/**
|
|
24
30
|
* Path to the daemon log file:
|
|
25
31
|
* ~/.scai/daemon.log
|
|
@@ -30,6 +36,16 @@ export const LOG_PATH = path.join(SCAI_HOME, 'daemon.log');
|
|
|
30
36
|
* ~/.scai/prompt.log
|
|
31
37
|
*/
|
|
32
38
|
export const PROMPT_LOG_PATH = path.join(SCAI_HOME, 'prompt.log');
|
|
39
|
+
/**
|
|
40
|
+
* Default sleep time for the daemon loop in milliseconds.
|
|
41
|
+
* This is used when no value is configured in the config file or via environment variables.
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_DAEMON_SLEEP_MS = 120000; // 1 minute
|
|
44
|
+
/**
|
|
45
|
+
* Default idle sleep time for the daemon loop in milliseconds.
|
|
46
|
+
* Used when the daemon has nothing to process and is idle.
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_DAEMON_IDLE_SLEEP_MS = 300000; // 5 minutes
|
|
33
49
|
/**
|
|
34
50
|
* Get the active index directory based on the active repo.
|
|
35
51
|
*
|
|
@@ -50,13 +66,13 @@ export function getIndexDir() {
|
|
|
50
66
|
}
|
|
51
67
|
}
|
|
52
68
|
/**
|
|
53
|
-
*
|
|
69
|
+
* Highest ranked results
|
|
54
70
|
*/
|
|
55
|
-
export const
|
|
71
|
+
export const NUM_TOPFILES = 5;
|
|
56
72
|
/**
|
|
57
|
-
* Limit for number of
|
|
73
|
+
* Limit for number of related files included in model prompt.
|
|
58
74
|
*/
|
|
59
|
-
export const
|
|
75
|
+
export const RELATED_FILES_LIMIT = 50;
|
|
60
76
|
/**
|
|
61
77
|
* Limit number of summary lines
|
|
62
78
|
*/
|
package/dist/context.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// context.ts
|
|
2
|
-
import { readConfig,
|
|
2
|
+
import { readConfig, Config } from "./config.js";
|
|
3
3
|
import { normalizePath } from "./utils/contentUtils.js";
|
|
4
4
|
import { getDbForRepo, getDbPathForRepo } from "./db/client.js";
|
|
5
5
|
import fs from "fs";
|
|
@@ -37,8 +37,13 @@ export async function updateContext() {
|
|
|
37
37
|
throw err;
|
|
38
38
|
}
|
|
39
39
|
const activeRepoChanged = cfg.activeRepo !== repoKey;
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
if (activeRepoChanged) {
|
|
41
|
+
const switched = Config.setActiveRepo(repoKey);
|
|
42
|
+
if (switched === false) {
|
|
43
|
+
// ❗ If switching is denied by lock, stop updateContext completely
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
42
47
|
const repoCfg = cfg.repos[repoKey];
|
|
43
48
|
let ok = true;
|
|
44
49
|
if (activeRepoChanged) {
|
|
@@ -84,14 +89,8 @@ export async function updateContext() {
|
|
|
84
89
|
chalk.bold(chalk.yellow("scai config set-model <model>")));
|
|
85
90
|
ok = false;
|
|
86
91
|
}
|
|
87
|
-
|
|
88
|
-
console.log(chalk.
|
|
89
|
-
}
|
|
90
|
-
if (ok) {
|
|
91
|
-
console.log(chalk.bold.green("\n✅ Context OK\n"));
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
console.log(chalk.bold.red("\n⚠️ Context incomplete\n"));
|
|
92
|
+
if (!ok) {
|
|
93
|
+
console.log(chalk.bold.red("\n⚠️ Repositoriy context not set correctly\n"));
|
|
95
94
|
}
|
|
96
95
|
return ok;
|
|
97
96
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getDbForRepo } from '../db/client.js';
|
|
2
|
+
import { log } from '../utils/log.js';
|
|
3
|
+
import { countUnprocessedFiles } from '../db/sqlTemplates.js';
|
|
4
|
+
/**
|
|
5
|
+
* LOOP 1 — Fast indexing
|
|
6
|
+
*/
|
|
7
|
+
export function hasUnindexedFiles() {
|
|
8
|
+
try {
|
|
9
|
+
const db = getDbForRepo();
|
|
10
|
+
const row = db.prepare(countUnprocessedFiles).get();
|
|
11
|
+
return row.count > 0;
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
log('❌ hasUnindexedFiles failed:', err);
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* LOOP 2 — Folder capsules
|
|
20
|
+
* Any folder that contains files but lacks a capsule
|
|
21
|
+
*/
|
|
22
|
+
export function hasUncapsuledFolders() {
|
|
23
|
+
try {
|
|
24
|
+
const db = getDbForRepo();
|
|
25
|
+
const row = db.prepare(`
|
|
26
|
+
SELECT COUNT(*) AS count
|
|
27
|
+
FROM (
|
|
28
|
+
SELECT DISTINCT
|
|
29
|
+
substr(path, 1, length(path) - length(filename) - 1) AS folder
|
|
30
|
+
FROM files
|
|
31
|
+
WHERE processing_status NOT IN ('skipped', 'failed')
|
|
32
|
+
) folders
|
|
33
|
+
LEFT JOIN folder_capsules fc
|
|
34
|
+
ON fc.path = folders.folder
|
|
35
|
+
WHERE fc.path IS NULL
|
|
36
|
+
`).get();
|
|
37
|
+
return row.count > 0;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
log('❌ hasUncapsuledFolders failed:', err);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* LOOP 3 — Knowledge graph
|
|
46
|
+
* Files indexed but KG not extracted yet
|
|
47
|
+
*/
|
|
48
|
+
export function hasPendingKgWork() {
|
|
49
|
+
try {
|
|
50
|
+
const db = getDbForRepo();
|
|
51
|
+
const row = db.prepare(`
|
|
52
|
+
SELECT COUNT(*) AS count
|
|
53
|
+
FROM files
|
|
54
|
+
WHERE functions_extracted_at IS NULL
|
|
55
|
+
AND processing_status NOT IN ('skipped', 'failed')
|
|
56
|
+
`).get();
|
|
57
|
+
return row.count > 0;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
log('❌ hasPendingKgWork failed:', err);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,80 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { runDaemonBatch } from './daemonBatch.js';
|
|
1
|
+
import { hasUnindexedFiles, hasUncapsuledFolders, hasPendingKgWork, } from './daemonQueues.js';
|
|
3
2
|
import { log } from '../utils/log.js';
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { Config } from '../config.js';
|
|
4
|
+
import { runIndexingBatch } from './runIndexingBatch.js';
|
|
5
|
+
import { runKgBatch } from './runKgBatch.js';
|
|
6
|
+
import { runFolderCapsuleBatch } from './runFolderCapsuleBatch.js';
|
|
7
|
+
import { sleep } from '../utils/sleep.js';
|
|
6
8
|
// 🚨 Immediate signal that the worker even starts
|
|
7
9
|
log('🛠️ daemonWorker.js loaded');
|
|
8
|
-
async function isQueueEmpty() {
|
|
9
|
-
try {
|
|
10
|
-
const db = getDbForRepo();
|
|
11
|
-
const row = db.prepare(`
|
|
12
|
-
SELECT COUNT(*) AS count
|
|
13
|
-
FROM files
|
|
14
|
-
WHERE processing_status IN ('unprocessed')
|
|
15
|
-
`).get();
|
|
16
|
-
const castRow = row;
|
|
17
|
-
if (typeof castRow.count !== 'number') {
|
|
18
|
-
log('⚠️ Invalid count value in DB query result:', row);
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
log(`📦 Queue size: ${castRow.count}`);
|
|
22
|
-
return castRow.count === 0;
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
log('❌ Error checking if queue is empty:', error);
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
10
|
export async function daemonWorker() {
|
|
30
|
-
log('🚀 Daemon worker starting
|
|
11
|
+
log('🚀 Daemon worker starting (3-loop mode)...');
|
|
12
|
+
const { sleepMs, idleSleepMs } = Config.getDaemonConfig();
|
|
31
13
|
while (true) {
|
|
32
14
|
try {
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
log('
|
|
38
|
-
didWork = await
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
catch (batchErr) {
|
|
42
|
-
log('🔥 Error inside runDaemonBatch():', batchErr);
|
|
15
|
+
// --------------------------------------------------
|
|
16
|
+
// LOOP 1: FAST FILE INDEXING (HIGHEST PRIORITY)
|
|
17
|
+
// --------------------------------------------------
|
|
18
|
+
if (await hasUnindexedFiles()) {
|
|
19
|
+
log('⚡ Indexing loop: processing files...');
|
|
20
|
+
const didWork = await runIndexingBatch();
|
|
21
|
+
if (didWork)
|
|
22
|
+
continue; // process next batch immediately
|
|
43
23
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
log('🔥 Error checking queue status:', queueErr);
|
|
53
|
-
}
|
|
54
|
-
if (queueEmpty) {
|
|
55
|
-
log('🕊️ No work found. Idling...');
|
|
56
|
-
await sleep(IDLE_SLEEP_MS * 3);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
log('🟡 Work queue not empty, but no batch executed.');
|
|
61
|
-
}
|
|
24
|
+
// --------------------------------------------------
|
|
25
|
+
// LOOP 2: FOLDER CAPSULE GENERATION (BLOCKING, FINITE)
|
|
26
|
+
// --------------------------------------------------
|
|
27
|
+
if (await hasUncapsuledFolders()) {
|
|
28
|
+
log('📦 Folder capsule loop: generating capsules...');
|
|
29
|
+
const didWork = await runFolderCapsuleBatch();
|
|
30
|
+
if (didWork)
|
|
31
|
+
continue; // process next folder batch immediately
|
|
62
32
|
}
|
|
63
|
-
|
|
64
|
-
|
|
33
|
+
// --------------------------------------------------
|
|
34
|
+
// LOOP 3: KNOWLEDGE GRAPH (BACKGROUND / BEST EFFORT)
|
|
35
|
+
// --------------------------------------------------
|
|
36
|
+
if (await hasPendingKgWork()) {
|
|
37
|
+
log('🧠 KG loop: background enrichment...');
|
|
38
|
+
await runKgBatch(); // intentionally ignore didWork
|
|
39
|
+
await sleep(idleSleepMs);
|
|
40
|
+
continue;
|
|
65
41
|
}
|
|
66
|
-
|
|
42
|
+
// --------------------------------------------------
|
|
43
|
+
// IDLE
|
|
44
|
+
// --------------------------------------------------
|
|
45
|
+
log('🕊️ All queues empty. Idling...');
|
|
46
|
+
await sleep(idleSleepMs * 6);
|
|
67
47
|
}
|
|
68
48
|
catch (err) {
|
|
69
|
-
log('🔥
|
|
70
|
-
await sleep(
|
|
49
|
+
log('🔥 Fatal error in daemon worker:', err);
|
|
50
|
+
await sleep(idleSleepMs);
|
|
71
51
|
}
|
|
72
52
|
}
|
|
73
53
|
}
|
|
74
|
-
//
|
|
54
|
+
// Boot
|
|
75
55
|
daemonWorker().catch(err => {
|
|
76
56
|
log('❌ daemonWorker failed to start:', err);
|
|
77
57
|
});
|
|
78
|
-
function sleep(ms) {
|
|
79
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
80
|
-
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getDbForRepo } from "../db/client.js";
|
|
2
|
+
import { generate } from "../lib/generate.js";
|
|
3
|
+
export async function generateSummaries() {
|
|
4
|
+
const db = getDbForRepo();
|
|
5
|
+
const files = db
|
|
6
|
+
.prepare(`SELECT path, summary FROM files WHERE summary IS NOT NULL`)
|
|
7
|
+
.all();
|
|
8
|
+
// --- Group by folder ---
|
|
9
|
+
const folders = new Map();
|
|
10
|
+
for (const { path, summary } of files) {
|
|
11
|
+
if (!summary)
|
|
12
|
+
continue;
|
|
13
|
+
const parts = path.split("/");
|
|
14
|
+
parts.pop(); // remove filename
|
|
15
|
+
const folderPath = parts.join("/") || "/";
|
|
16
|
+
if (!folders.has(folderPath))
|
|
17
|
+
folders.set(folderPath, []);
|
|
18
|
+
folders.get(folderPath).push(summary);
|
|
19
|
+
}
|
|
20
|
+
// --- Generate folder-level summaries ---
|
|
21
|
+
for (const [folderPath, summaries] of folders.entries()) {
|
|
22
|
+
const content = summaries.join("\n\n");
|
|
23
|
+
const input = {
|
|
24
|
+
query: `Summarize folder ${folderPath}`,
|
|
25
|
+
content: `Summarize the following files to describe what this folder (${folderPath}) does:\n\n${content}`,
|
|
26
|
+
};
|
|
27
|
+
const response = await generate(input);
|
|
28
|
+
const summaryText = String(response.content ?? "").trim();
|
|
29
|
+
db.prepare(`
|
|
30
|
+
INSERT INTO summaries (path, type, summary, last_generated)
|
|
31
|
+
VALUES (?, 'folder', ?, datetime('now'))
|
|
32
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
33
|
+
summary=excluded.summary,
|
|
34
|
+
last_generated=datetime('now')
|
|
35
|
+
`).run(folderPath, summaryText);
|
|
36
|
+
console.log(`🧠 Folder summary generated for ${folderPath}`);
|
|
37
|
+
}
|
|
38
|
+
// --- Project-level summary generation ---
|
|
39
|
+
const allFolderSummaries = db
|
|
40
|
+
.prepare(`SELECT summary FROM summaries WHERE type='folder'`)
|
|
41
|
+
.all()
|
|
42
|
+
.map((r) => r.summary)
|
|
43
|
+
.join("\n\n");
|
|
44
|
+
const projectInput = {
|
|
45
|
+
query: "Summarize project",
|
|
46
|
+
content: `Summarize the overall purpose and structure of the project based on these folder summaries:\n\n${allFolderSummaries}`,
|
|
47
|
+
};
|
|
48
|
+
const projectResponse = await generate(projectInput);
|
|
49
|
+
const projectSummary = String(projectResponse.content ?? "").trim();
|
|
50
|
+
db.prepare(`
|
|
51
|
+
INSERT INTO summaries (path, type, summary, last_generated)
|
|
52
|
+
VALUES ('/', 'project', ?, datetime('now'))
|
|
53
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
54
|
+
summary=excluded.summary,
|
|
55
|
+
last_generated=datetime('now')
|
|
56
|
+
`).run(projectSummary);
|
|
57
|
+
console.log("🏗️ Project-level summary generated");
|
|
58
|
+
}
|