scai 0.1.117 → 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.
Files changed (95) hide show
  1. package/dist/agents/MainAgent.js +255 -0
  2. package/dist/agents/contextReviewStep.js +104 -0
  3. package/dist/agents/finalPlanGenStep.js +123 -0
  4. package/dist/agents/infoPlanGenStep.js +126 -0
  5. package/dist/agents/planGeneratorStep.js +118 -0
  6. package/dist/agents/planResolverStep.js +95 -0
  7. package/dist/agents/planTargetFilesStep.js +48 -0
  8. package/dist/agents/preFileSearchCheckStep.js +95 -0
  9. package/dist/agents/selectRelevantSourcesStep.js +100 -0
  10. package/dist/agents/semanticAnalysisStep.js +144 -0
  11. package/dist/agents/structuralAnalysisStep.js +46 -0
  12. package/dist/agents/transformPlanGenStep.js +107 -0
  13. package/dist/agents/understandIntentStep.js +72 -0
  14. package/dist/agents/validationAnalysisStep.js +87 -0
  15. package/dist/commands/AskCmd.js +47 -116
  16. package/dist/commands/ChangeLogUpdateCmd.js +11 -5
  17. package/dist/commands/CommitSuggesterCmd.js +50 -75
  18. package/dist/commands/DaemonCmd.js +119 -29
  19. package/dist/commands/IndexCmd.js +41 -24
  20. package/dist/commands/InspectCmd.js +0 -1
  21. package/dist/commands/ReadlineSingleton.js +18 -0
  22. package/dist/commands/ResetDbCmd.js +20 -21
  23. package/dist/commands/ReviewCmd.js +89 -54
  24. package/dist/commands/SummaryCmd.js +12 -18
  25. package/dist/commands/WorkflowCmd.js +41 -0
  26. package/dist/commands/factory.js +254 -0
  27. package/dist/config.js +67 -15
  28. package/dist/constants.js +20 -4
  29. package/dist/context.js +10 -11
  30. package/dist/daemon/daemonQueues.js +63 -0
  31. package/dist/daemon/daemonWorker.js +40 -63
  32. package/dist/daemon/generateSummaries.js +58 -0
  33. package/dist/daemon/runFolderCapsuleBatch.js +247 -0
  34. package/dist/daemon/runIndexingBatch.js +147 -0
  35. package/dist/daemon/runKgBatch.js +104 -0
  36. package/dist/db/fileIndex.js +168 -63
  37. package/dist/db/functionExtractors/extractFromJava.js +210 -6
  38. package/dist/db/functionExtractors/extractFromJs.js +173 -214
  39. package/dist/db/functionExtractors/extractFromTs.js +159 -160
  40. package/dist/db/functionExtractors/index.js +7 -5
  41. package/dist/db/schema.js +55 -20
  42. package/dist/db/sqlTemplates.js +50 -19
  43. package/dist/fileRules/builtins.js +31 -14
  44. package/dist/fileRules/codeAllowedExtensions.js +4 -0
  45. package/dist/fileRules/fileExceptions.js +0 -13
  46. package/dist/fileRules/ignoredExtensions.js +10 -0
  47. package/dist/index.js +128 -325
  48. package/dist/lib/generate.js +37 -14
  49. package/dist/lib/generateFolderCapsules.js +109 -0
  50. package/dist/lib/spinner.js +12 -5
  51. package/dist/modelSetup.js +0 -10
  52. package/dist/pipeline/modules/changeLogModule.js +16 -19
  53. package/dist/pipeline/modules/chunkManagerModule.js +24 -0
  54. package/dist/pipeline/modules/cleanupModule.js +96 -91
  55. package/dist/pipeline/modules/codeTransformModule.js +208 -0
  56. package/dist/pipeline/modules/commentModule.js +20 -11
  57. package/dist/pipeline/modules/commitSuggesterModule.js +36 -14
  58. package/dist/pipeline/modules/contextReviewModule.js +52 -0
  59. package/dist/pipeline/modules/fileReaderModule.js +72 -0
  60. package/dist/pipeline/modules/fileSearchModule.js +136 -0
  61. package/dist/pipeline/modules/finalAnswerModule.js +53 -0
  62. package/dist/pipeline/modules/gatherInfoModule.js +176 -0
  63. package/dist/pipeline/modules/generateTestsModule.js +63 -54
  64. package/dist/pipeline/modules/kgModule.js +26 -11
  65. package/dist/pipeline/modules/preserveCodeModule.js +91 -49
  66. package/dist/pipeline/modules/refactorModule.js +19 -7
  67. package/dist/pipeline/modules/repairTestsModule.js +44 -36
  68. package/dist/pipeline/modules/reviewModule.js +23 -13
  69. package/dist/pipeline/modules/summaryModule.js +27 -35
  70. package/dist/pipeline/modules/writeFileModule.js +86 -0
  71. package/dist/pipeline/registry/moduleRegistry.js +38 -93
  72. package/dist/pipeline/runModulePipeline.js +22 -19
  73. package/dist/scripts/dbcheck.js +143 -228
  74. package/dist/utils/buildContextualPrompt.js +245 -172
  75. package/dist/utils/debugContext.js +24 -0
  76. package/dist/utils/fileTree.js +16 -6
  77. package/dist/utils/loadRelevantFolderCapsules.js +64 -0
  78. package/dist/utils/log.js +2 -0
  79. package/dist/utils/normalizeData.js +23 -0
  80. package/dist/utils/planActions.js +60 -0
  81. package/dist/utils/promptBuilderHelper.js +67 -0
  82. package/dist/utils/promptLogHelper.js +52 -0
  83. package/dist/utils/sanitizeQuery.js +20 -8
  84. package/dist/utils/sleep.js +3 -0
  85. package/dist/utils/splitCodeIntoChunk.js +65 -32
  86. package/dist/utils/vscode.js +49 -0
  87. package/dist/workflow/workflowResolver.js +14 -0
  88. package/dist/workflow/workflowRunner.js +103 -0
  89. package/package.json +6 -5
  90. package/dist/agent/agentManager.js +0 -39
  91. package/dist/agent/workflowManager.js +0 -95
  92. package/dist/commands/ModulePipelineCmd.js +0 -31
  93. package/dist/daemon/daemonBatch.js +0 -186
  94. package/dist/fileRules/scoreFiles.js +0 -71
  95. 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: 'llama3:8b',
10
- contextLength: 4096,
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
- ...cfg.repos[repoKey],
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
- * Limit for number of related files included in model prompt.
69
+ * Highest ranked results
54
70
  */
55
- export const RELATED_FILES_LIMIT = 5;
71
+ export const NUM_TOPFILES = 5;
56
72
  /**
57
- * Limit for number of candidate files to score.
73
+ * Limit for number of related files included in model prompt.
58
74
  */
59
- export const CANDIDATE_LIMIT = 100;
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, writeConfig } from "./config.js";
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
- cfg.activeRepo = repoKey;
41
- writeConfig(cfg);
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
- else {
88
- console.log(chalk.green(`✅ Model '${model}' available`));
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 { getDbForRepo } from '../db/client.js';
2
- import { runDaemonBatch } from './daemonBatch.js';
1
+ import { hasUnindexedFiles, hasUncapsuledFolders, hasPendingKgWork, } from './daemonQueues.js';
3
2
  import { log } from '../utils/log.js';
4
- const SLEEP_MS = 2000;
5
- const IDLE_SLEEP_MS = 5000;
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 up...');
11
+ log('🚀 Daemon worker starting (3-loop mode)...');
12
+ const { sleepMs, idleSleepMs } = Config.getDaemonConfig();
31
13
  while (true) {
32
14
  try {
33
- log('🔄 Running daemon batch...');
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);
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
- if (!didWork) {
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
- }
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
- else {
64
- log('✅ Batch executed, sleeping briefly...');
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
- await sleep(didWork ? SLEEP_MS : IDLE_SLEEP_MS);
42
+ // --------------------------------------------------
43
+ // IDLE
44
+ // --------------------------------------------------
45
+ log('🕊️ All queues empty. Idling...');
46
+ await sleep(idleSleepMs * 6);
67
47
  }
68
48
  catch (err) {
69
- log('🔥 Error in daemonWorker loop:', err);
70
- await sleep(IDLE_SLEEP_MS * 2); // prevent tight error loop
49
+ log('🔥 Fatal error in daemon worker:', err);
50
+ await sleep(idleSleepMs);
71
51
  }
72
52
  }
73
53
  }
74
- // Run the daemon
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
+ }