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
@@ -9,11 +9,13 @@ import os from 'os';
9
9
  import path from 'path';
10
10
  import { spawnSync } from 'child_process';
11
11
  import columnify from 'columnify';
12
- import { Spinner } from '../lib/spinner.js'; // adjust path as needed
12
+ import { Spinner } from '../lib/spinner.js';
13
+ import { openDiffInVSCode } from '../utils/vscode.js';
14
+ // --- Helper functions ---
13
15
  function truncate(str, length) {
14
16
  return str.length > length ? str.slice(0, length - 3) + '...' : str;
15
17
  }
16
- // Fetch open PRs with review requested
18
+ // --- Fetch PRs with review requests ---
17
19
  export async function getPullRequestsForReview(token, owner, repo, username, branch = 'main', filterForUser = true) {
18
20
  const spinner = new Spinner('Fetching pull requests and diffs...');
19
21
  spinner.start();
@@ -24,7 +26,7 @@ export async function getPullRequestsForReview(token, owner, repo, username, bra
24
26
  for (const pr of prs) {
25
27
  const shouldInclude = !pr.draft &&
26
28
  !pr.merged_at &&
27
- (!filterForUser || pr.requested_reviewers?.some(r => r.login === username));
29
+ (!filterForUser || pr.requested_reviewers?.some((r) => r.login === username));
28
30
  if (!shouldInclude)
29
31
  continue;
30
32
  try {
@@ -65,7 +67,7 @@ export async function getPullRequestsForReview(token, owner, repo, username, bra
65
67
  spinner.succeed(`Fetched ${filtered.length} PR(s) with diffs.`);
66
68
  }
67
69
  if (failedPRs.length > 0) {
68
- const failedList = failedPRs.map(pr => `#${pr.number}`).join(', ');
70
+ const failedList = failedPRs.map((pr) => `#${pr.number}`).join(', ');
69
71
  console.warn(`⚠️ Skipped ${failedPRs.length} PR(s): ${failedList}`);
70
72
  }
71
73
  return filtered;
@@ -75,11 +77,11 @@ export async function getPullRequestsForReview(token, owner, repo, username, bra
75
77
  return [];
76
78
  }
77
79
  }
78
- // Prompt user to select PR
80
+ // --- Prompt user to select PR ---
79
81
  function askUserToPickPR(prs) {
80
82
  return new Promise((resolve) => {
81
83
  if (prs.length === 0) {
82
- console.log("⚠️ No pull requests with review requested.");
84
+ console.log('⚠️ No pull requests with review requested.');
83
85
  return resolve(null);
84
86
  }
85
87
  const rows = prs.map((pr, i) => ({
@@ -100,7 +102,7 @@ function askUserToPickPR(prs) {
100
102
  ? chalk.red('Changes Requested')
101
103
  : chalk.gray('—'),
102
104
  }));
103
- console.log(chalk.blue("\n📦 Open Pull Requests:"));
105
+ console.log(chalk.blue('\n📦 Open Pull Requests:'));
104
106
  console.log(columnify(rows, {
105
107
  columnSplitter: ' ',
106
108
  headingTransform: (h) => chalk.cyan(h.toUpperCase()),
@@ -323,8 +325,14 @@ async function promptAIReviewSuggestions(aiOutput, chunkContent) {
323
325
  }
324
326
  else if (trimmed === 'r') {
325
327
  console.log(chalk.yellow('\nRegenerating suggestions...\n'));
326
- const newSuggestion = await reviewModule.run({ content: chunkContent });
327
- const newOutput = newSuggestion.content || aiOutput;
328
+ const ioInput = {
329
+ query: 'regenerate_chunk_review',
330
+ content: chunkContent
331
+ };
332
+ const newSuggestion = await reviewModule.run(ioInput);
333
+ const newOutput = typeof newSuggestion.data === 'string'
334
+ ? newSuggestion.data
335
+ : newSuggestion.content?.toString() || aiOutput;
328
336
  suggestions = parseAISuggestions(newOutput);
329
337
  }
330
338
  else if (trimmed === 'c') {
@@ -364,12 +372,18 @@ export async function reviewChunk(chunk, chunkIndex, totalChunks) {
364
372
  // Build colored diff
365
373
  const lines = chunk.content.split('\n');
366
374
  const coloredDiff = lines.map(colorDiffLine).join('\n');
367
- // 1️⃣ Run the AI review
368
- const suggestion = await reviewModule.run({
369
- content: chunk.content,
370
- filepath: chunk.filePath
371
- });
372
- const aiOutput = suggestion.content?.trim() || '1. AI review summary not available.';
375
+ // 1️⃣ Run the AI review using ModuleIO
376
+ const ioInput = {
377
+ query: 'review_chunk',
378
+ content: {
379
+ chunkContent: chunk.content,
380
+ filepath: chunk.filePath
381
+ }
382
+ };
383
+ const suggestion = await reviewModule.run(ioInput);
384
+ const aiOutput = typeof suggestion.data === 'string'
385
+ ? suggestion.data.trim()
386
+ : suggestion.content?.toString() || '1. AI review summary not available.';
373
387
  // 2️⃣ Show the diff
374
388
  console.log(`\n${chalk.bold('--- Diff ---')}\n`);
375
389
  console.log(coloredDiff);
@@ -451,8 +465,18 @@ export async function promptChunkReviewMenu() {
451
465
  process.stdin.once('data', onKeyPress);
452
466
  });
453
467
  }
454
- // Main command to review PR
455
- export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
468
+ // Helper to ask if the user wants to open the diff in VSCode
469
+ async function askOpenInVSCode() {
470
+ return new Promise((resolve) => {
471
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
472
+ rl.question(chalk.cyan('\n💡 Open this PR diff in VS Code? [Y/n]: '), (answer) => {
473
+ rl.close();
474
+ resolve(answer.trim().toLowerCase() !== 'n');
475
+ });
476
+ });
477
+ }
478
+ // Main review command
479
+ export async function reviewPullRequestCmd(branch = "main", showAll = false) {
456
480
  try {
457
481
  const { owner, repo } = await getRepoDetails();
458
482
  const token = await ensureGitHubAuth();
@@ -460,46 +484,58 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
460
484
  const prsWithReviewRequested = await getPullRequestsForReview(token, owner, repo, username, branch, !showAll);
461
485
  if (prsWithReviewRequested.length === 0)
462
486
  return;
463
- const selectedIndex = await askUserToPickPR(prsWithReviewRequested.map(p => p.pr));
487
+ const selectedIndex = await askUserToPickPR(prsWithReviewRequested.map((p) => p.pr));
464
488
  if (selectedIndex === null)
465
489
  return;
466
490
  const { pr, diff } = prsWithReviewRequested[selectedIndex];
467
491
  if (pr.body) {
468
- console.log(chalk.magentaBright('\n📝 PR Description:\n') + chalk.gray(pr.body));
492
+ console.log(chalk.magentaBright("\n📝 PR Description:\n") + chalk.white(pr.body));
493
+ }
494
+ // ✅ Ask user if they want to view diff in VS Code
495
+ const openInVSCode = await askOpenInVSCode();
496
+ if (openInVSCode) {
497
+ console.log(chalk.gray(`\n🧭 Opening PR #${pr.number} diff in VS Code...`));
498
+ await openDiffInVSCode(`${pr.title || "pull_request"}.diff`, diff);
469
499
  }
500
+ // === Continue AI Review ===
470
501
  const chunks = chunkDiff(diff, pr.number.toString());
471
- const reviewMethod = chunks.length > 1 ? await askReviewMethod() : 'chunk';
472
- let reviewComments = [];
473
- if (reviewMethod === 'whole') {
474
- const result = await reviewModule.run({ content: diff, filepath: 'Whole PR Diff' });
475
- console.log(chalk.yellowBright("Raw AI output:\n"), result.content);
476
- // Use the parsed array for selecting or displaying suggestions
477
- let suggestions = result.suggestions;
478
- if (suggestions && suggestions.length > 3 && /here (are|is) \d+ suggestions/i.test(suggestions[0])) {
502
+ const reviewMethod = chunks.length > 1 ? await askReviewMethod() : "chunk";
503
+ const reviewComments = [];
504
+ if (reviewMethod === "whole") {
505
+ const ioInput = {
506
+ query: 'review_whole_pr',
507
+ content: {
508
+ diff,
509
+ description: "Whole PR Diff"
510
+ }
511
+ };
512
+ const result = await reviewModule.run(ioInput);
513
+ const rawOutput = typeof result.data === 'string'
514
+ ? result.data
515
+ : result.content?.toString() || '1. AI review summary not available.';
516
+ console.log(chalk.yellowBright("Raw AI output:\n"), rawOutput);
517
+ let suggestions = rawOutput.split('\n').filter(Boolean); // or your old logic
518
+ if (suggestions.length > 3 &&
519
+ /here (are|is) \d+ suggestions/i.test(suggestions[0])) {
479
520
  suggestions = suggestions.slice(1);
480
521
  }
481
522
  const finalReviewChoice = await askReviewApproval();
482
- let reviewText = '';
483
- // Pick the first suggestion as default if any exist
484
- if (suggestions && suggestions.length > 0) {
485
- reviewText = suggestions[0];
486
- }
487
- if (finalReviewChoice === 'approve') {
488
- reviewText = 'PR approved';
489
- await submitReview(pr.number, reviewText, 'APPROVE');
523
+ let reviewText = suggestions[0] || '';
524
+ if (finalReviewChoice === "approve") {
525
+ reviewText = "PR approved";
526
+ await submitReview(pr.number, reviewText, "APPROVE");
490
527
  }
491
- else if (finalReviewChoice === 'reject') {
492
- reviewText = 'Changes requested';
493
- await submitReview(pr.number, reviewText, 'REQUEST_CHANGES');
528
+ else if (finalReviewChoice === "reject") {
529
+ reviewText = "Changes requested";
530
+ await submitReview(pr.number, reviewText, "REQUEST_CHANGES");
494
531
  }
495
- else if (finalReviewChoice === 'custom') {
532
+ else if (finalReviewChoice === "custom") {
496
533
  reviewText = await promptCustomReview();
497
- await submitReview(pr.number, reviewText, 'COMMENT');
534
+ await submitReview(pr.number, reviewText, "COMMENT");
498
535
  }
499
- else if (finalReviewChoice === 'edit') {
500
- // let user edit the AI suggestion
536
+ else if (finalReviewChoice === "edit") {
501
537
  reviewText = await promptEditReview(reviewText);
502
- await submitReview(pr.number, reviewText, 'COMMENT');
538
+ await submitReview(pr.number, reviewText, "COMMENT");
503
539
  }
504
540
  }
505
541
  else {
@@ -508,27 +544,27 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
508
544
  for (let i = 0; i < chunks.length; i++) {
509
545
  const chunk = chunks[i];
510
546
  const { choice, summary } = await reviewChunk(chunk, i, chunks.length);
511
- if (choice === 'cancel') {
547
+ if (choice === "cancel") {
512
548
  console.log(chalk.red(`🚫 Review cancelled at chunk ${i + 1}`));
513
- return; // exit reviewPullRequestCmd early
549
+ return;
514
550
  }
515
- if (choice === 'skip') {
551
+ if (choice === "skip") {
516
552
  console.log(chalk.gray(`⏭️ Skipped chunk ${i + 1}`));
517
553
  continue;
518
554
  }
519
- // Find the first line in the chunk with a valid position (usually the first 'add' or 'context' line)
520
555
  const firstLineWithPosition = chunk.hunks
521
- .flatMap(hunk => hunk.lines)
522
- .find(line => line.position !== undefined);
556
+ .flatMap((h) => h.lines)
557
+ .find((line) => line.position !== undefined);
523
558
  if (!firstLineWithPosition) {
524
559
  console.warn(`⚠️ Could not find valid position for inline comment in chunk ${i + 1}. Skipping comment.`);
525
560
  continue;
526
561
  }
527
562
  let commentBody = summary;
528
- if (choice === 'custom') {
563
+ if (choice === "custom") {
529
564
  commentBody = await promptCustomReview();
530
565
  }
531
- else if (typeof choice === 'string' && !['approve', 'reject', 'custom'].includes(choice)) {
566
+ else if (typeof choice === "string" &&
567
+ !["approve", "reject", "custom"].includes(choice)) {
532
568
  commentBody = choice;
533
569
  }
534
570
  reviewComments.push({
@@ -536,10 +572,10 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
536
572
  body: commentBody,
537
573
  position: firstLineWithPosition.position,
538
574
  });
539
- if (choice === 'reject')
575
+ if (choice === "reject")
540
576
  allApproved = false;
541
577
  }
542
- console.log(chalk.blueBright('\n📝 Review Comments Preview:'));
578
+ console.log(chalk.blueBright("\n📝 Review Comments Preview:"));
543
579
  reviewComments.forEach((comment, idx) => {
544
580
  console.log(`${idx + 1}. ${comment.path}:${comment.position} — ${comment.body}`);
545
581
  });
@@ -562,7 +598,6 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
562
598
  ? chalk.green("📝 Submitting inline comments with approval.")
563
599
  : chalk.green("✔️ All chunks approved. Submitting final PR approval.")
564
600
  : chalk.red("❌ Not all chunks were approved. Changes requested."));
565
- // ✅ Only one submission, inline comments are preserved
566
601
  await submitReview(pr.number, reviewBody, reviewState, reviewComments);
567
602
  }
568
603
  }
@@ -1,17 +1,16 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import readline from 'readline';
4
- import { queryFiles, indexFile } from '../db/fileIndex.js';
4
+ import { queryFiles } from '../db/fileIndex.js';
5
5
  import { summaryModule } from '../pipeline/modules/summaryModule.js';
6
6
  import { detectFileType } from '../fileRules/detectFileType.js';
7
- import { generateEmbedding } from '../lib/generateEmbedding.js';
8
7
  import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
9
- import { getDbForRepo } from '../db/client.js';
10
8
  import { styleText } from '../utils/outputFormatter.js';
9
+ import { indexFile } from '../daemon/runIndexingBatch.js';
11
10
  export async function summarizeFile(filepath) {
12
11
  let content = '';
13
12
  let filePathResolved;
14
- // 📁 Resolve path like `scai find`
13
+ // 📁 Resolve file path from index or local disk
15
14
  if (filepath) {
16
15
  const sanitizedQuery = sanitizeQueryForFts(filepath);
17
16
  const matches = queryFiles(sanitizedQuery);
@@ -21,7 +20,6 @@ export async function summarizeFile(filepath) {
21
20
  console.log(`🔗 Matched file from index: ${path.relative(process.cwd(), filePathResolved)}`);
22
21
  }
23
22
  else {
24
- // Fallback: check if it's a real file in CWD
25
23
  const localPath = path.resolve(process.cwd(), filepath);
26
24
  try {
27
25
  await fs.access(localPath);
@@ -34,7 +32,7 @@ export async function summarizeFile(filepath) {
34
32
  }
35
33
  }
36
34
  }
37
- // 📄 Load file content from resolved path
35
+ // 📄 Load file content (from path or stdin)
38
36
  if (filePathResolved) {
39
37
  const matches = queryFiles(`"${filePathResolved}"`);
40
38
  const match = matches.find(row => path.resolve(row.path) === filePathResolved);
@@ -68,24 +66,20 @@ export async function summarizeFile(filepath) {
68
66
  // 🧠 Generate summary and save
69
67
  if (content.trim()) {
70
68
  console.log('🧪 Generating summary...\n');
71
- const response = await summaryModule.run({ content, filepath });
72
- if (!response.summary) {
69
+ const response = await summaryModule.run({
70
+ query: `Summarize file: ${filepath ?? 'stdin'}`,
71
+ content,
72
+ });
73
+ const summary = response.data?.summary;
74
+ if (!summary) {
73
75
  console.warn('⚠️ No summary generated.');
74
76
  return;
75
77
  }
76
- console.log(styleText(response.summary));
78
+ console.log(styleText(summary));
77
79
  if (filePathResolved) {
78
80
  const fileType = detectFileType(filePathResolved);
79
- indexFile(filePathResolved, response.summary, fileType);
81
+ indexFile(filePathResolved, summary, fileType);
80
82
  console.log('💾 Summary saved to local database.');
81
- const embedding = await generateEmbedding(response.summary);
82
- if (embedding) {
83
- const db = getDbForRepo();
84
- db.prepare(`
85
- UPDATE files SET embedding = ? WHERE path = ?
86
- `).run(JSON.stringify(embedding), filePathResolved.replace(/\\/g, '/'));
87
- console.log('📐 Embedding saved to database.');
88
- }
89
83
  }
90
84
  }
91
85
  else {
@@ -0,0 +1,41 @@
1
+ // File: src/commands/WorkflowCmd.ts
2
+ import chalk from 'chalk';
3
+ import { resolveModules } from '../workflow/workflowResolver.js';
4
+ import { runWorkflow } from '../workflow/workflowRunner.js';
5
+ /**
6
+ * Runs a workflow pipeline with one or more goals.
7
+ * Handles both file input (-f) and stdin input (pipe).
8
+ */
9
+ export async function runWorkflowCommand(goals, options) {
10
+ try {
11
+ console.log(chalk.cyan('🔁 Pipeline goals:'), goals.join(' → '));
12
+ // Resolve module implementations for the given goals
13
+ const modules = resolveModules(goals);
14
+ console.log(chalk.green('📋 Modules to run:'), modules.map((m) => m.name).join(' → '));
15
+ // Determine input source
16
+ const input = options.file
17
+ ? { filepath: options.file }
18
+ : { inputContent: await readStdin() };
19
+ // Run the workflow pipeline
20
+ await runWorkflow({ goals, modules, ...input });
21
+ console.log(chalk.green('✅ Workflow completed successfully.'));
22
+ }
23
+ catch (err) {
24
+ console.error(chalk.red('❌ Pipeline failed:'), err?.message ?? err);
25
+ process.exit(1);
26
+ }
27
+ }
28
+ /**
29
+ * Reads from stdin if no file path is provided.
30
+ */
31
+ function readStdin() {
32
+ return new Promise((resolve) => {
33
+ if (process.stdin.isTTY)
34
+ return resolve('');
35
+ let data = '';
36
+ process.stdin.setEncoding('utf8');
37
+ process.stdin.on('data', (chunk) => (data += chunk));
38
+ process.stdin.on('end', () => resolve(data));
39
+ process.stdin.resume();
40
+ });
41
+ }
@@ -0,0 +1,254 @@
1
+ // commands/factory.ts
2
+ import { Command } from 'commander';
3
+ import { bootstrap } from '../modelSetup.js';
4
+ import { runAskCommand } from './AskCmd.js';
5
+ import { runIndexCommand } from './IndexCmd.js';
6
+ import { resetDatabase } from './ResetDbCmd.js';
7
+ import { suggestCommitMessage } from './CommitSuggesterCmd.js';
8
+ import { reviewPullRequestCmd } from './ReviewCmd.js';
9
+ import { checkGit } from './GitCmd.js';
10
+ import { promptForToken } from '../github/token.js';
11
+ import { validateGitHubTokenAgainstRepo } from '../github/githubAuthCheck.js';
12
+ import { runWorkflowCommand } from './WorkflowCmd.js';
13
+ import { handleStandaloneChangelogUpdate } from './ChangeLogUpdateCmd.js';
14
+ import { runInteractiveSwitch } from './SwitchCmd.js';
15
+ import { runInteractiveDelete } from './DeleteIndex.js';
16
+ import { startDaemon, stopDaemon, restartDaemon, statusDaemon, unlockConfig, showLogs } from './DaemonCmd.js';
17
+ import { Config } from '../config.js';
18
+ import { fileURLToPath } from 'url';
19
+ import { dirname, resolve } from 'path';
20
+ import { execSync } from 'child_process';
21
+ import { runInspectCommand } from './InspectCmd.js';
22
+ import { runFindCommand } from './FindCmd.js';
23
+ import { runBackupCommand } from './BackupCmd.js';
24
+ import { updateContext } from '../context.js';
25
+ import { createRequire } from 'module';
26
+ const require = createRequire(import.meta.url);
27
+ const { version } = require('../../package.json');
28
+ export async function withContext(action) {
29
+ const ok = await updateContext();
30
+ if (!ok)
31
+ return; // or process.exit(1)
32
+ await action();
33
+ }
34
+ export function createProgram() {
35
+ const cmd = new Command('scai');
36
+ cmd.version(version, '-v, --version', 'output the current version');
37
+ // ---------------- Init commands ----------------
38
+ cmd
39
+ .command('init')
40
+ .description('Initialize the model and download required models')
41
+ .action(async () => {
42
+ await bootstrap();
43
+ console.log('✅ Model initialization completed!');
44
+ });
45
+ // ---------------- Git commands ----------------
46
+ const git = cmd.command('git').description('Git utilities');
47
+ git
48
+ .command('review')
49
+ .description('Review an open pull request using AI')
50
+ .option('-a, --all', 'Show all PRs requiring a review', false)
51
+ .action(async (opts) => {
52
+ await withContext(async () => {
53
+ await reviewPullRequestCmd('main', opts.all);
54
+ });
55
+ });
56
+ git
57
+ .command('commit')
58
+ .description('Suggest a commit message from staged changes')
59
+ .option('-l, --changelog', 'Generate and optionally stage a changelog entry')
60
+ .action(async (opts) => {
61
+ await withContext(async () => {
62
+ await suggestCommitMessage(opts);
63
+ });
64
+ });
65
+ git
66
+ .command('check')
67
+ .description('Check Git working directory and branch status')
68
+ .action(async () => {
69
+ await withContext(async () => { checkGit(); });
70
+ });
71
+ // ---------------- Auth commands ----------------
72
+ const auth = cmd.command('auth').description('GitHub authentication commands');
73
+ auth
74
+ .command('check')
75
+ .description('Check if GitHub authentication is valid')
76
+ .action(async () => {
77
+ await withContext(async () => {
78
+ try {
79
+ const token = Config.getGitHubToken();
80
+ if (!token)
81
+ return console.log('❌ GitHub token not found.');
82
+ const result = await validateGitHubTokenAgainstRepo();
83
+ console.log(result);
84
+ }
85
+ catch (err) {
86
+ console.error(err instanceof Error ? err.message : err);
87
+ }
88
+ });
89
+ });
90
+ auth
91
+ .command('reset')
92
+ .description('Reset GitHub authentication credentials')
93
+ .action(async () => {
94
+ await withContext(async () => {
95
+ Config.setGitHubToken('');
96
+ console.log('✅ GitHub token reset.');
97
+ });
98
+ });
99
+ auth
100
+ .command('set')
101
+ .description('Set GitHub Personal Access Token')
102
+ .action(async () => {
103
+ await withContext(async () => {
104
+ const token = (await promptForToken()).trim();
105
+ Config.setGitHubToken(token);
106
+ console.log('🔑 GitHub token set.');
107
+ });
108
+ });
109
+ // ---------------- Workflow ----------------
110
+ const workflow = cmd.command('workflow').description('Run or manage module pipelines');
111
+ workflow
112
+ .command('run <goals...>')
113
+ .description('Run one or more modules as a pipeline')
114
+ .option('-f, --file <filepath>', 'File to process (omit to read from stdin)')
115
+ .action(async (goals, options) => {
116
+ await withContext(async () => {
117
+ await runWorkflowCommand(goals, options);
118
+ });
119
+ })
120
+ .on('--help', () => {
121
+ console.log('\nExamples:');
122
+ console.log(' $ scai workflow run comments tests -f path/to/myfile.ts');
123
+ console.log(' $ cat file.ts | scai workflow run comments tests > file.test.ts\n');
124
+ console.log('Available modules: summary, comments, tests');
125
+ });
126
+ // ---------------- Gen / Changelog ----------------
127
+ const gen = cmd.command('gen').description('Generate code-related output');
128
+ gen
129
+ .command('changelog')
130
+ .description('Update or create CHANGELOG.md')
131
+ .action(async () => {
132
+ await withContext(async () => { await handleStandaloneChangelogUpdate(); });
133
+ });
134
+ // ---------------- Config ----------------
135
+ const config = cmd.command('config').description('Manage SCAI configuration');
136
+ config
137
+ .command('set-model <model>')
138
+ .description('Set the model to use')
139
+ .option('-g, --global', 'Set globally instead of repo')
140
+ .action(async (model, opts) => {
141
+ await withContext(async () => {
142
+ Config.setModel(model, opts.global ? 'global' : 'repo');
143
+ Config.show();
144
+ });
145
+ });
146
+ config
147
+ .command('set-lang <lang>')
148
+ .description('Set programming language')
149
+ .action(async (lang) => {
150
+ await withContext(async () => { Config.setLanguage(lang); Config.show(); });
151
+ });
152
+ config
153
+ .command('show')
154
+ .option('--raw', 'Show raw config')
155
+ .description('Display current configuration')
156
+ .action(async (opts) => {
157
+ await withContext(async () => {
158
+ if (opts.raw)
159
+ console.log(JSON.stringify(Config.getRaw(), null, 2));
160
+ else
161
+ Config.show();
162
+ });
163
+ });
164
+ // 💤 Set daemon sleep intervals
165
+ config
166
+ .command('set-daemon')
167
+ .description('Set daemon sleep intervals (ms between runs)')
168
+ .option('--sleep <ms>', 'Sleep time in milliseconds between batches')
169
+ .option('--idle <ms>', 'Sleep time in milliseconds when idle')
170
+ .action(async (options) => {
171
+ await withContext(async () => {
172
+ const updates = {};
173
+ if (options.sleep)
174
+ updates.sleepMs = parseInt(options.sleep, 10);
175
+ if (options.idle)
176
+ updates.idleSleepMs = parseInt(options.idle, 10);
177
+ if (!Object.keys(updates).length) {
178
+ console.log('⚠️ No options provided. Use --sleep or --idle.');
179
+ return;
180
+ }
181
+ Config.setDaemonConfig(updates);
182
+ const current = Config.getDaemonConfig();
183
+ console.log(`🕒 Updated daemon settings:\n sleepMs=${current.sleepMs}ms\n idleSleepMs=${current.idleSleepMs}ms`);
184
+ });
185
+ });
186
+ // ---------------- Index ----------------
187
+ const index = cmd.command('index').description('Index operations');
188
+ index
189
+ .command('start')
190
+ .description('Index supported files')
191
+ .action(async () => await withContext(runIndexCommand));
192
+ index
193
+ .command('set [dir]')
194
+ .description('Set index directory')
195
+ .action(async (dir = process.cwd()) => {
196
+ await Config.setIndexDir(dir);
197
+ Config.show();
198
+ });
199
+ index
200
+ .command('list')
201
+ .description('List all indexed repositories')
202
+ .action(async () => await withContext(async () => { await Config.printAllRepos(); }));
203
+ index
204
+ .command('switch')
205
+ .description('Switch active repository interactively')
206
+ .action(async () => await withContext(runInteractiveSwitch));
207
+ index
208
+ .command('delete')
209
+ .description('Delete repository interactively')
210
+ .action(async () => await withContext(runInteractiveDelete));
211
+ // ---------------- DB ----------------
212
+ const db = cmd.command('db').description('Database operations');
213
+ db
214
+ .command('check')
215
+ .description('Run dbcheck script')
216
+ .action(async () => {
217
+ await withContext(async () => {
218
+ const __filename = fileURLToPath(import.meta.url);
219
+ const __dirname = dirname(__filename);
220
+ const scriptPath = resolve(__dirname, '../..', 'dist/scripts', 'dbcheck.js');
221
+ try {
222
+ execSync(`node "${scriptPath}"`, { stdio: 'inherit' });
223
+ }
224
+ catch (err) {
225
+ console.error('❌ DB check failed:', err instanceof Error ? err.message : err);
226
+ }
227
+ });
228
+ });
229
+ db
230
+ .command('reset')
231
+ .description('Reset SQLite database')
232
+ .action(async () => await withContext(resetDatabase));
233
+ db
234
+ .command('inspect <filepath>')
235
+ .description('Inspect a file in the index')
236
+ .action(async (filepath) => await withContext(() => runInspectCommand(filepath)));
237
+ // ---------------- Daemon ----------------
238
+ const daemon = cmd.command('daemon').description('Daemon management');
239
+ daemon.command('start').description('Start daemon').action(async () => withContext(startDaemon));
240
+ daemon.command('stop').description('Stop daemon').action(async () => withContext(stopDaemon));
241
+ daemon.command('restart').description('Restart daemon').action(async () => withContext(restartDaemon));
242
+ daemon.command('status').description('Daemon status').action(async () => withContext(statusDaemon));
243
+ daemon.command('unlock').description('Force unlock').action(async () => withContext(unlockConfig));
244
+ daemon.command('logs').option('-n, --lines <n>', 'Last N log lines', '20')
245
+ .description('Show daemon logs')
246
+ .action(async (opts) => withContext(() => showLogs(parseInt(opts.lines, 10))));
247
+ // ---------------- Backup / Find / Ask ----------------
248
+ cmd.command('backup').description('Backup .scai folder').action(async () => await withContext(runBackupCommand));
249
+ cmd.command('find <query>').description('Search indexed files').action(async (q) => await withContext(() => runFindCommand(q)));
250
+ cmd.command('ask [question...]').description('Ask a question').action(async (parts) => {
251
+ await withContext(async () => { await runAskCommand(parts?.join(' ')); });
252
+ });
253
+ return cmd;
254
+ }