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.
- 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 +173 -214
- package/dist/db/functionExtractors/extractFromTs.js +159 -160
- 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 -14
- 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 +0 -10
- 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 +143 -228
- package/dist/utils/buildContextualPrompt.js +245 -172
- 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/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
|
@@ -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';
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
327
|
-
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
//
|
|
455
|
-
|
|
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(
|
|
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() :
|
|
472
|
-
|
|
473
|
-
if (reviewMethod ===
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
reviewText
|
|
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 ===
|
|
492
|
-
reviewText =
|
|
493
|
-
await submitReview(pr.number, reviewText,
|
|
528
|
+
else if (finalReviewChoice === "reject") {
|
|
529
|
+
reviewText = "Changes requested";
|
|
530
|
+
await submitReview(pr.number, reviewText, "REQUEST_CHANGES");
|
|
494
531
|
}
|
|
495
|
-
else if (finalReviewChoice ===
|
|
532
|
+
else if (finalReviewChoice === "custom") {
|
|
496
533
|
reviewText = await promptCustomReview();
|
|
497
|
-
await submitReview(pr.number, reviewText,
|
|
534
|
+
await submitReview(pr.number, reviewText, "COMMENT");
|
|
498
535
|
}
|
|
499
|
-
else if (finalReviewChoice ===
|
|
500
|
-
// let user edit the AI suggestion
|
|
536
|
+
else if (finalReviewChoice === "edit") {
|
|
501
537
|
reviewText = await promptEditReview(reviewText);
|
|
502
|
-
await submitReview(pr.number, reviewText,
|
|
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 ===
|
|
547
|
+
if (choice === "cancel") {
|
|
512
548
|
console.log(chalk.red(`🚫 Review cancelled at chunk ${i + 1}`));
|
|
513
|
-
return;
|
|
549
|
+
return;
|
|
514
550
|
}
|
|
515
|
-
if (choice ===
|
|
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(
|
|
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 ===
|
|
563
|
+
if (choice === "custom") {
|
|
529
564
|
commentBody = await promptCustomReview();
|
|
530
565
|
}
|
|
531
|
-
else if (typeof 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 ===
|
|
575
|
+
if (choice === "reject")
|
|
540
576
|
allApproved = false;
|
|
541
577
|
}
|
|
542
|
-
console.log(chalk.blueBright(
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
72
|
-
|
|
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(
|
|
78
|
+
console.log(styleText(summary));
|
|
77
79
|
if (filePathResolved) {
|
|
78
80
|
const fileType = detectFileType(filePathResolved);
|
|
79
|
-
indexFile(filePathResolved,
|
|
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
|
+
}
|