scai 0.1.117 → 0.1.119

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 (96) hide show
  1. package/README.md +88 -503
  2. package/dist/agents/MainAgent.js +255 -0
  3. package/dist/agents/contextReviewStep.js +104 -0
  4. package/dist/agents/finalPlanGenStep.js +123 -0
  5. package/dist/agents/infoPlanGenStep.js +126 -0
  6. package/dist/agents/planGeneratorStep.js +118 -0
  7. package/dist/agents/planResolverStep.js +95 -0
  8. package/dist/agents/planTargetFilesStep.js +48 -0
  9. package/dist/agents/preFileSearchCheckStep.js +95 -0
  10. package/dist/agents/selectRelevantSourcesStep.js +100 -0
  11. package/dist/agents/semanticAnalysisStep.js +144 -0
  12. package/dist/agents/structuralAnalysisStep.js +46 -0
  13. package/dist/agents/transformPlanGenStep.js +107 -0
  14. package/dist/agents/understandIntentStep.js +72 -0
  15. package/dist/agents/validationAnalysisStep.js +87 -0
  16. package/dist/commands/AskCmd.js +47 -116
  17. package/dist/commands/ChangeLogUpdateCmd.js +11 -5
  18. package/dist/commands/CommitSuggesterCmd.js +50 -75
  19. package/dist/commands/DaemonCmd.js +119 -29
  20. package/dist/commands/IndexCmd.js +41 -24
  21. package/dist/commands/InspectCmd.js +0 -1
  22. package/dist/commands/ReadlineSingleton.js +18 -0
  23. package/dist/commands/ResetDbCmd.js +20 -21
  24. package/dist/commands/ReviewCmd.js +89 -54
  25. package/dist/commands/SummaryCmd.js +12 -18
  26. package/dist/commands/WorkflowCmd.js +41 -0
  27. package/dist/commands/factory.js +254 -0
  28. package/dist/config.js +67 -15
  29. package/dist/constants.js +20 -4
  30. package/dist/context.js +10 -11
  31. package/dist/daemon/daemonQueues.js +63 -0
  32. package/dist/daemon/daemonWorker.js +40 -63
  33. package/dist/daemon/generateSummaries.js +58 -0
  34. package/dist/daemon/runFolderCapsuleBatch.js +247 -0
  35. package/dist/daemon/runIndexingBatch.js +147 -0
  36. package/dist/daemon/runKgBatch.js +104 -0
  37. package/dist/db/fileIndex.js +168 -63
  38. package/dist/db/functionExtractors/extractFromJava.js +210 -6
  39. package/dist/db/functionExtractors/extractFromJs.js +173 -214
  40. package/dist/db/functionExtractors/extractFromTs.js +159 -160
  41. package/dist/db/functionExtractors/index.js +7 -5
  42. package/dist/db/schema.js +55 -20
  43. package/dist/db/sqlTemplates.js +50 -19
  44. package/dist/fileRules/builtins.js +31 -14
  45. package/dist/fileRules/codeAllowedExtensions.js +4 -0
  46. package/dist/fileRules/fileExceptions.js +0 -13
  47. package/dist/fileRules/ignoredExtensions.js +10 -0
  48. package/dist/index.js +128 -325
  49. package/dist/lib/generate.js +37 -14
  50. package/dist/lib/generateFolderCapsules.js +109 -0
  51. package/dist/lib/spinner.js +12 -5
  52. package/dist/modelSetup.js +1 -11
  53. package/dist/pipeline/modules/changeLogModule.js +16 -19
  54. package/dist/pipeline/modules/chunkManagerModule.js +24 -0
  55. package/dist/pipeline/modules/cleanupModule.js +95 -91
  56. package/dist/pipeline/modules/codeTransformModule.js +208 -0
  57. package/dist/pipeline/modules/commentModule.js +20 -11
  58. package/dist/pipeline/modules/commitSuggesterModule.js +36 -14
  59. package/dist/pipeline/modules/contextReviewModule.js +52 -0
  60. package/dist/pipeline/modules/fileReaderModule.js +72 -0
  61. package/dist/pipeline/modules/fileSearchModule.js +136 -0
  62. package/dist/pipeline/modules/finalAnswerModule.js +53 -0
  63. package/dist/pipeline/modules/gatherInfoModule.js +176 -0
  64. package/dist/pipeline/modules/generateTestsModule.js +63 -54
  65. package/dist/pipeline/modules/kgModule.js +26 -11
  66. package/dist/pipeline/modules/preserveCodeModule.js +91 -49
  67. package/dist/pipeline/modules/refactorModule.js +19 -7
  68. package/dist/pipeline/modules/repairTestsModule.js +44 -36
  69. package/dist/pipeline/modules/reviewModule.js +23 -13
  70. package/dist/pipeline/modules/summaryModule.js +27 -35
  71. package/dist/pipeline/modules/writeFileModule.js +86 -0
  72. package/dist/pipeline/registry/moduleRegistry.js +38 -93
  73. package/dist/pipeline/runModulePipeline.js +22 -19
  74. package/dist/scripts/dbcheck.js +143 -228
  75. package/dist/utils/buildContextualPrompt.js +245 -172
  76. package/dist/utils/debugContext.js +24 -0
  77. package/dist/utils/fileTree.js +16 -6
  78. package/dist/utils/loadRelevantFolderCapsules.js +64 -0
  79. package/dist/utils/log.js +2 -0
  80. package/dist/utils/normalizeData.js +23 -0
  81. package/dist/utils/planActions.js +60 -0
  82. package/dist/utils/promptBuilderHelper.js +67 -0
  83. package/dist/utils/promptLogHelper.js +52 -0
  84. package/dist/utils/sanitizeQuery.js +20 -8
  85. package/dist/utils/sleep.js +3 -0
  86. package/dist/utils/splitCodeIntoChunk.js +65 -32
  87. package/dist/utils/vscode.js +49 -0
  88. package/dist/workflow/workflowResolver.js +14 -0
  89. package/dist/workflow/workflowRunner.js +103 -0
  90. package/package.json +6 -5
  91. package/dist/agent/agentManager.js +0 -39
  92. package/dist/agent/workflowManager.js +0 -95
  93. package/dist/commands/ModulePipelineCmd.js +0 -31
  94. package/dist/daemon/daemonBatch.js +0 -186
  95. package/dist/fileRules/scoreFiles.js +0 -71
  96. package/dist/lib/generateEmbedding.js +0 -22
package/dist/index.js CHANGED
@@ -1,341 +1,144 @@
1
1
  #!/usr/bin/env node
2
+ import readline from 'readline';
3
+ import { spawn } from 'child_process';
2
4
  import { createRequire } from 'module';
3
5
  const require = createRequire(import.meta.url);
4
- const { version } = require('../package.json');
5
- import { Command } from "commander";
6
- import { Config } from './config.js';
7
- import { suggestCommitMessage } from "./commands/CommitSuggesterCmd.js";
8
- import { bootstrap } from './modelSetup.js';
9
- import { summarizeFile } from "./commands/SummaryCmd.js";
10
- import { handleStandaloneChangelogUpdate } from './commands/ChangeLogUpdateCmd.js';
11
- import { runIndexCommand } from './commands/IndexCmd.js';
12
- import { resetDatabase } from './commands/ResetDbCmd.js';
13
- import { runFindCommand } from './commands/FindCmd.js';
14
- import { startDaemon } from './commands/DaemonCmd.js';
15
- import { runStopDaemonCommand } from "./commands/StopDaemonCmd.js";
6
+ const shellQuote = require('shell-quote');
7
+ import { createProgram as cmdFactory, withContext } from './commands/factory.js';
16
8
  import { runAskCommand } from './commands/AskCmd.js';
17
- import { runBackupCommand } from './commands/BackupCmd.js';
18
- import { runInspectCommand } from "./commands/InspectCmd.js";
19
- import { reviewPullRequestCmd } from "./commands/ReviewCmd.js";
20
- import { promptForToken } from "./github/token.js";
21
- import { validateGitHubTokenAgainstRepo } from "./github/githubAuthCheck.js";
22
- import { checkGit } from "./commands/GitCmd.js";
23
- import { runInteractiveSwitch } from "./commands/SwitchCmd.js";
24
- import { execSync } from "child_process";
25
- import { fileURLToPath } from "url";
26
- import { dirname, resolve } from "path";
27
- import { runInteractiveDelete } from './commands/DeleteIndex.js';
28
- import { resolveTargetsToFiles } from './utils/resolveTargetsToFiles.js';
29
- import { updateContext } from './context.js';
30
- import { Agent } from './agent/agentManager.js';
31
- // 🎛️ CLI Setup
32
- const cmd = new Command('scai')
33
- .version(version, '-v, --version', 'output the current version');
34
- // 🔧 Main command group
35
- cmd
36
- .command('init')
37
- .description('Initialize the model and download required models')
38
- .action(async () => {
39
- await bootstrap();
40
- console.log(' Model initialization completed!');
41
- });
42
- // 🔧 Group: Agent-related commands
43
- const agent = cmd
44
- .command('agent')
45
- .description(`Run an agent workflow. Example available tools:\n` +
46
- ` - summary\n` +
47
- ` - comments\n` +
48
- ` - tests\n\n` +
49
- `Example usage:\n` +
50
- ` $ scai agent run comments tests -f path/to/myfile\n` +
51
- ` This will run the agent with the goals: comments → tests\n`);
52
- // Run workflow subcommand
53
- const runCmd = agent
54
- .command('run <goals...>')
55
- .description('Run an agent workflow with a list of goals')
56
- .option('-f, --file <filepath>', 'File to process', 'example.txt')
57
- .action(async (cmdGoals, cmd) => {
58
- await withContext(async () => {
59
- const goals = cmdGoals;
60
- const file = cmd.file;
61
- console.log('Agent will execute:', goals.join(' → '));
62
- console.log('On file:', file);
63
- const agentInstance = new Agent(goals);
64
- await agentInstance.execute(file);
65
- });
66
- });
67
- // Optional: show example modules on --help
68
- runCmd.on('--help', () => {
69
- console.log('\nExample tools:');
70
- console.log(' - summary');
71
- console.log(' - comments');
72
- console.log(' - tests');
73
- });
74
- // 🔧 Group: Git-related commands
75
- const git = cmd.command('git').description('Git utilities');
76
- git
77
- .command('review')
78
- .description('Review an open pull request using AI')
79
- .option('-a, --all', 'Show all PRs requiring a review (not just for the current user)', false)
80
- .action(async (cmd) => {
81
- await withContext(async () => {
82
- const showAll = cmd.all;
83
- await reviewPullRequestCmd('main', showAll);
84
- });
85
- });
86
- git
87
- .command('commit')
88
- .description('Suggest a commit message from staged changes and optionally commit')
89
- .option('-l, --changelog', 'Generate and optionally stage a changelog entry')
90
- .action(async (options) => {
91
- await withContext(async () => {
92
- suggestCommitMessage(options);
93
- });
94
- });
95
- git
96
- .command('check')
97
- .description('Check Git working directory and branch status')
98
- .action(async () => {
99
- await withContext(async () => {
100
- checkGit();
101
- });
102
- });
103
- // Add auth-related commands
104
- const auth = cmd.command('auth').description('GitHub authentication commands');
105
- // ⚡ Auth commands
106
- auth
107
- .command('check')
108
- .description('Check if GitHub authentication is set up and valid')
109
- .action(async () => {
110
- await withContext(async () => {
9
+ import { setRl } from './commands/ReadlineSingleton.js';
10
+ const program = cmdFactory(); // single Commander instance
11
+ let inShell = false;
12
+ const customCommands = {};
13
+ // =====================================================
14
+ // TEST QUERIES
15
+ // =====================================================
16
+ const testQueries = [
17
+ 'please write me comprehensive comments for spatialmap.js and typescript.ts files',
18
+ 'refactor spatialmap.js to improve readability and reduce nesting',
19
+ 'explain the intent and architecture of the semantic analysis module',
20
+ 'add validation and error handling to the context review step',
21
+ 'summarize this repo architecture and identify weak coupling points',
22
+ 'Where are all the database queries defined?',
23
+ 'Which functions have no tests written for them yet?',
24
+ 'What could cause memory leaks in this codebase?',
25
+ 'How does authentication work here?',
26
+ 'Is there a clear separation between frontend and backend code?',
27
+ 'please refactor the buildContextualPrompt into smaller functions',
28
+ 'Is error handling consistent across the codebase?',
29
+ 'What’s missing from the README?',
30
+ 'How do I run the test suite?',
31
+ 'Are there any flaky tests in this repo?',
32
+ 'Are there any security vulnerabilities in our dependencies?',
33
+ 'Is there any dead code we can safely remove?'
34
+ ];
35
+ // =====================================================
36
+ // HELPERS
37
+ // =====================================================
38
+ function pickRandom(items) {
39
+ return items[Math.floor(Math.random() * items.length)];
40
+ }
41
+ async function runQuery(query) {
42
+ await withContext(() => runAskCommand(query));
43
+ }
44
+ // =====================================================
45
+ // BUILT-IN COMMANDS
46
+ // =====================================================
47
+ customCommands.test = async () => {
48
+ await runQuery(testQueries[0]);
49
+ };
50
+ customCommands['test-random'] = async () => {
51
+ const query = pickRandom(testQueries);
52
+ console.log(`\n🎲 [test-random] Selected query:\n→ ${query}\n`);
53
+ await runQuery(query);
54
+ };
55
+ customCommands.exit = async () => {
56
+ console.log('Exiting...');
57
+ process.exit(0);
58
+ };
59
+ customCommands.q = customCommands.exit;
60
+ customCommands.quit = customCommands.exit;
61
+ // =====================================================
62
+ // EXTENSION API
63
+ // =====================================================
64
+ export function registerCommand(name, fn) {
65
+ customCommands[name] = fn;
66
+ }
67
+ async function startShell() {
68
+ if (inShell)
69
+ return;
70
+ inShell = true;
71
+ const rl = readline.createInterface({
72
+ input: process.stdin,
73
+ output: process.stdout,
74
+ prompt: 'scai> ',
75
+ historySize: 200,
76
+ });
77
+ setRl(rl);
78
+ rl.prompt();
79
+ rl.on('line', async (line) => {
111
80
  try {
112
- const token = Config.getGitHubToken();
113
- if (!token) {
114
- console.log('❌ GitHub authentication not found. Please set your token.');
81
+ const trimmed = line.trim();
82
+ if (!trimmed) {
83
+ rl.prompt();
115
84
  return;
116
85
  }
117
- const result = await validateGitHubTokenAgainstRepo();
118
- console.log(result);
86
+ // --- Shell command
87
+ if (trimmed.startsWith('!')) {
88
+ const child = spawn(trimmed.slice(1).trim(), { shell: true, stdio: 'inherit' });
89
+ child.on('exit', () => rl.prompt());
90
+ child.on('error', (err) => { console.error('Shell command error:', err); rl.prompt(); });
91
+ return;
92
+ }
93
+ // --- Slash commands
94
+ if (trimmed.startsWith('/')) {
95
+ const argvParts = shellQuote.parse(trimmed.slice(1))
96
+ .map((tok) => typeof tok === 'object' ? tok.op ?? tok.pattern ?? '' : String(tok))
97
+ .filter(Boolean);
98
+ const cmdName = argvParts[0];
99
+ if (customCommands[cmdName]) {
100
+ await customCommands[cmdName]();
101
+ }
102
+ else {
103
+ try {
104
+ await program.parseAsync(argvParts, { from: 'user' });
105
+ }
106
+ catch (err) {
107
+ console.error('Command error:', err instanceof Error ? err.message : err);
108
+ }
109
+ }
110
+ rl.prompt();
111
+ return;
112
+ }
113
+ // --- Bare input → LLM
114
+ await withContext(() => runAskCommand(trimmed));
115
+ rl.prompt();
119
116
  }
120
117
  catch (err) {
121
- console.error(typeof err === 'string' ? err : err.message);
122
- }
123
- });
124
- });
125
- auth
126
- .command('reset')
127
- .description('Reset GitHub authentication credentials')
128
- .action(async () => {
129
- await withContext(async () => {
130
- Config.setGitHubToken('');
131
- console.log('🔄 GitHub authentication has been reset.');
132
- const token = Config.getGitHubToken();
133
- console.log(token ? '❌ Token still exists in the configuration.' : '✅ Token successfully removed.');
134
- });
135
- });
136
- auth
137
- .command('set')
138
- .description('Set your GitHub Personal Access Token')
139
- .action(async () => {
140
- await withContext(async () => {
141
- const token = await promptForToken();
142
- Config.setGitHubToken(token.trim());
143
- console.log('🔑 GitHub token set successfully.');
144
- });
145
- });
146
- // 🛠️ Group: `gen` commands for content generation
147
- const gen = cmd.command('gen').description('Generate code-related output');
148
- gen
149
- .command("comm <targets...>")
150
- .description("Write comments for the given file(s) or folder(s)")
151
- .action(async (targets) => {
152
- await withContext(async () => {
153
- const files = await resolveTargetsToFiles(targets);
154
- for (const file of files) {
155
- const agent = new Agent(["comments"]); // goals: "comments"
156
- await agent.execute(file); // run on the file
157
- }
158
- });
159
- });
160
- gen
161
- .command("test <targets...>")
162
- .description("Generate tests for the given file(s) or folder(s)")
163
- .action(async (targets) => {
164
- await withContext(async () => {
165
- const files = await resolveTargetsToFiles(targets);
166
- for (const file of files) {
167
- const agent = new Agent(["tests"]); // goals: "tests"
168
- await agent.execute(file);
169
- }
170
- });
171
- });
172
- gen
173
- .command('changelog')
174
- .description('Update or create the CHANGELOG.md based on current Git diff')
175
- .action(async () => {
176
- await withContext(async () => {
177
- await handleStandaloneChangelogUpdate();
178
- });
179
- });
180
- gen
181
- .command('summ [file]')
182
- .description('Print a summary of the given file to the terminal')
183
- .action(async (file) => {
184
- await withContext(async () => {
185
- summarizeFile(file);
186
- });
187
- });
188
- // ⚙️ Group: Configuration settings
189
- const config = cmd.command('config').description('Manage SCAI configuration');
190
- config
191
- .command('set-model <model>')
192
- .description('Set the model to use')
193
- .option('-g, --global', 'Set the global default model instead of the active repo')
194
- .action(async (model, options) => {
195
- await withContext(async () => {
196
- const scope = options.global ? 'global' : 'repo';
197
- Config.setModel(model, scope);
198
- Config.show();
199
- });
200
- });
201
- config
202
- .command('set-lang <lang>')
203
- .description('Set the programming language')
204
- .action(async (lang) => {
205
- await withContext(async () => {
206
- Config.setLanguage(lang);
207
- Config.show();
208
- });
209
- });
210
- config
211
- .command("show")
212
- .option("--raw", "Show full raw config")
213
- .description("Display current configuration")
214
- .action(async (options) => {
215
- await withContext(async () => {
216
- if (options.raw) {
217
- console.log(JSON.stringify(Config.getRaw(), null, 2));
218
- }
219
- else {
220
- Config.show();
118
+ console.error('REPL error:', err instanceof Error ? err.stack : err);
119
+ rl.prompt();
221
120
  }
222
121
  });
223
- });
224
- const index = cmd.command('index').description('index operations');
225
- index
226
- .command('start')
227
- .description('Index supported files in the configured index directory')
228
- .action(async () => await withContext(async () => {
229
- await runIndexCommand();
230
- }));
231
- index
232
- .command('set [dir]')
233
- .description('Set and activate index directory')
234
- .action(async (dir = process.cwd()) => {
235
- await Config.setIndexDir(dir);
236
- Config.show();
237
- });
238
- index
239
- .command('list')
240
- .description('List all indexed repositories')
241
- .action(async () => await withContext(async () => {
242
- await Config.printAllRepos();
243
- }));
244
- index
245
- .command('switch')
246
- .description('Switch active repository (by interactive list only)')
247
- .action(async () => await withContext(async () => {
248
- await runInteractiveSwitch();
249
- }));
250
- index
251
- .command('delete')
252
- .description('Delete a repository from the index (interactive)')
253
- .action(async () => await withContext(async () => {
254
- await runInteractiveDelete();
255
- }));
256
- const db = cmd.command('db').description('Database operations');
257
- db
258
- .command('check')
259
- .description('Run the dbcheck script to check the database status')
260
- .action(async () => await withContext(async () => {
261
- const __filename = fileURLToPath(import.meta.url);
262
- const __dirname = dirname(__filename);
263
- const scriptPath = resolve(__dirname, '..', 'dist/scripts', 'dbcheck.js');
264
- console.log(`🚀 Running database check script: ${scriptPath}`);
122
+ rl.on('close', () => { console.log('Bye!'); process.exit(0); });
123
+ process.on('SIGINT', () => { console.log('\nExiting REPL...'); rl.close(); });
124
+ }
125
+ // ---------------- Main -----------------
126
+ async function main() {
127
+ process.on('unhandledRejection', (reason) => console.error('Unhandled Rejection:', reason));
128
+ process.on('uncaughtException', (err) => { console.error('Uncaught Exception:', err.stack ?? err); process.exit(1); });
129
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
130
+ const args = process.argv.slice(2);
131
+ // Run REPL if no args OR user typed `scai shell`
132
+ if (isInteractive && (args.length === 0 || (args.length === 1 && args[0] === 'shell'))) {
133
+ await startShell();
134
+ return;
135
+ }
265
136
  try {
266
- execSync(`node "${scriptPath}"`, { stdio: 'inherit' });
137
+ await program.parseAsync(process.argv);
267
138
  }
268
139
  catch (err) {
269
- console.error(' Error running dbcheck script:', err instanceof Error ? err.message : err);
140
+ console.error('CLI Error:', err instanceof Error ? err.message : err);
141
+ process.exit(1);
270
142
  }
271
- }));
272
- db
273
- .command('reset')
274
- .description('Delete and reset the SQLite database')
275
- .action(async () => await withContext(async () => {
276
- await resetDatabase();
277
- }));
278
- db
279
- .command('inspect')
280
- .argument('<filepath>', 'Path to the file to inspect')
281
- .description('Inspect a specific file and print its indexed summary and functions')
282
- .action(async (filepath) => await withContext(async () => {
283
- await runInspectCommand(filepath);
284
- }));
285
- const daemon = cmd
286
- .command('daemon')
287
- .description('Background summarizer operations');
288
- // Start the daemon
289
- daemon
290
- .command('start')
291
- .description('Run background summarization of indexed files')
292
- .action(async () => {
293
- await withContext(async () => {
294
- await startDaemon();
295
- });
296
- });
297
- // Stop the daemon
298
- daemon
299
- .command('stop')
300
- .description('Stop the background summarizer daemon')
301
- .action(async () => {
302
- await withContext(async () => {
303
- await runStopDaemonCommand();
304
- });
305
- });
306
- cmd
307
- .command('backup')
308
- .description('Backup the current .scai folder')
309
- .action(async () => await withContext(async () => {
310
- await runBackupCommand();
311
- }));
312
- cmd
313
- .command('find <query>')
314
- .description('Search indexed files by keyword')
315
- .action(async (query) => await withContext(async () => {
316
- await runFindCommand(query);
317
- }));
318
- cmd
319
- .command('ask [question...]')
320
- .description('Ask a question based on indexed files')
321
- .action(async (questionParts) => await withContext(async () => {
322
- const fullQuery = questionParts?.join(' ');
323
- await runAskCommand(fullQuery);
324
- }));
325
- cmd.addHelpText('after', `
326
- 🚨 Alpha Features:
327
- - The "index", "daemon", "stop-daemon", "reset-db" commands are considered alpha features.
328
- - These commands are in active development and may change in the future.
329
-
330
- 💡 Use with caution and expect possible changes or instability.
331
- `);
332
- async function withContext(action) {
333
- const ok = await updateContext();
334
- //if (!ok) process.exit(1);
335
- await action();
336
143
  }
337
- // 👇 this should be the very last line
338
- cmd.parseAsync(process.argv).catch((err) => {
339
- console.error(err.message); // only show our styled message
340
- process.exit(1);
341
- });
144
+ main();
@@ -2,24 +2,36 @@
2
2
  import { Spinner } from './spinner.js';
3
3
  import { Config, readConfig } from '../config.js';
4
4
  import { startModelProcess } from '../utils/checkModel.js';
5
+ /**
6
+ * The generate module uses the local model API to produce output
7
+ * based on the `content` of the input. It returns the response
8
+ * on the `data` property of ModuleIO.
9
+ */
5
10
  export async function generate(input) {
6
11
  const model = Config.getModel();
7
- const contextLength = readConfig().contextLength ?? 8192;
8
- let prompt = input.content;
9
- const spinner = new Spinner(`🧠 Thinking with ${model}...`);
12
+ const { contextLength } = readConfig();
13
+ // Safely build prompt
14
+ const queryPart = input.query ? `User query:\n${input.query}\n\n` : "";
15
+ const contentPart = input.content
16
+ ? (typeof input.content === "string"
17
+ ? input.content
18
+ : JSON.stringify(input.content, null, 2))
19
+ : "";
20
+ const prompt = `${queryPart}${contentPart}`.trim();
21
+ const spinner = new Spinner();
10
22
  spinner.start();
11
23
  try {
12
- return await doGenerate(prompt, model, spinner, false);
24
+ const data = await doGenerate(prompt, model, contextLength, spinner, false);
25
+ return { query: input.query, data };
13
26
  }
14
- catch (err) {
15
- spinner.fail('Model request failed. Attempting to start model...');
16
- process.stdout.write('\n');
27
+ catch {
28
+ spinner.fail('Model request failed. Attempting restart...');
17
29
  await startModelProcess();
18
- spinner.update(`🧠 Retrying with ${model}...`);
19
- return await doGenerate(prompt, model, spinner, true); // retry once
30
+ const data = await doGenerate(prompt, model, contextLength, spinner, true);
31
+ return { query: input.query, data };
20
32
  }
21
33
  }
22
- async function doGenerate(prompt, model, spinner, retrying) {
34
+ async function doGenerate(prompt, model, contextLength, spinner, retrying) {
23
35
  const res = await fetch('http://localhost:11434/api/generate', {
24
36
  method: 'POST',
25
37
  headers: { 'Content-Type': 'application/json' },
@@ -27,12 +39,23 @@ async function doGenerate(prompt, model, spinner, retrying) {
27
39
  model,
28
40
  prompt,
29
41
  stream: false,
42
+ options: {
43
+ num_ctx: contextLength,
44
+ },
30
45
  }),
31
46
  });
47
+ if (!res.ok) {
48
+ throw new Error(`Model request failed with status ${res.status}`);
49
+ }
32
50
  const data = await res.json();
33
- spinner.succeed(retrying ? 'Model response received after restart.' : 'Model response received.');
51
+ if (retrying) {
52
+ spinner.succeed('Model response received after restart.');
53
+ }
54
+ else {
55
+ spinner.succeed();
56
+ }
57
+ process.stdout.write('\n');
58
+ return data.response?.trim() ?? '';
34
59
  process.stdout.write('\n');
35
- return {
36
- content: data.response?.trim() ?? '',
37
- };
60
+ return data.response?.trim() ?? '';
38
61
  }
@@ -0,0 +1,109 @@
1
+ import { getDbForRepo } from "../db/client.js";
2
+ import { generate } from "../lib/generate.js";
3
+ import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
4
+ import path from "path";
5
+ export async function generateFolderCapsules() {
6
+ const db = getDbForRepo();
7
+ // --- Load files (paths only) ---
8
+ const files = db
9
+ .prepare(`SELECT path FROM files`)
10
+ .all();
11
+ // --- Group files by folder ---
12
+ const folders = new Map();
13
+ for (const { path: filePath } of files) {
14
+ const dir = path.dirname(filePath) || "/";
15
+ const depth = dir === "/" ? 0 : dir.split("/").length;
16
+ if (!folders.has(dir)) {
17
+ folders.set(dir, {
18
+ path: dir,
19
+ depth,
20
+ files: [],
21
+ });
22
+ }
23
+ folders.get(dir).files.push({
24
+ name: path.basename(filePath),
25
+ ext: path.extname(filePath).replace(".", "") || "unknown",
26
+ });
27
+ }
28
+ // --- Generate one capsule per folder ---
29
+ for (const folder of folders.values()) {
30
+ const byType = {};
31
+ for (const f of folder.files) {
32
+ byType[f.ext] = (byType[f.ext] ?? 0) + 1;
33
+ }
34
+ const inputPayload = {
35
+ path: folder.path,
36
+ depth: folder.depth,
37
+ fileCount: folder.files.length,
38
+ byType,
39
+ files: folder.files.map((f) => f.name),
40
+ };
41
+ const input = {
42
+ query: `Generate a FolderCapsule for ${folder.path}`,
43
+ content: `
44
+ You are generating a structured FolderCapsule describing the architectural role of a folder.
45
+
46
+ Return ONLY valid JSON matching this TypeScript interface:
47
+
48
+ interface FolderCapsule {
49
+ path: string;
50
+ depth: number;
51
+ stats: {
52
+ fileCount: number;
53
+ byType: Record<string, number>;
54
+ };
55
+ roles: string[];
56
+ concerns: string[];
57
+ keyFiles: { path: string; reason: string }[];
58
+ dependencies: {
59
+ importsFrom: string[];
60
+ usedBy: string[];
61
+ };
62
+ confidence: number;
63
+ }
64
+
65
+ Folder facts:
66
+ ${JSON.stringify(inputPayload, null, 2)}
67
+ `.trim(),
68
+ };
69
+ const response = await generate(input);
70
+ // --- Cleanup model output ---
71
+ const cleaned = await cleanupModule.run({
72
+ query: input.query,
73
+ content: response.content,
74
+ });
75
+ let capsule = null;
76
+ try {
77
+ capsule = JSON.parse(String(cleaned.content));
78
+ }
79
+ catch {
80
+ console.warn(`⚠️ Failed to parse FolderCapsule for ${folder.path}`);
81
+ continue;
82
+ }
83
+ db.prepare(`
84
+ INSERT INTO folder_capsules
85
+ (path, depth, capsule_json, confidence, last_generated, source_file_count)
86
+ VALUES
87
+ (
88
+ @path,
89
+ @depth,
90
+ @capsule_json,
91
+ @confidence,
92
+ datetime('now'),
93
+ @source_file_count
94
+ )
95
+ ON CONFLICT(path) DO UPDATE SET
96
+ capsule_json = excluded.capsule_json,
97
+ confidence = excluded.confidence,
98
+ last_generated = datetime('now'),
99
+ source_file_count = excluded.source_file_count
100
+ `).run({
101
+ path: folder.path,
102
+ depth: folder.depth,
103
+ capsule_json: JSON.stringify(capsule),
104
+ confidence: capsule?.confidence ?? null,
105
+ source_file_count: folder.files.length,
106
+ });
107
+ console.log(`📦 Folder capsule generated for ${folder.path}`);
108
+ }
109
+ }