@bobsworkshop/cli 0.1.3 → 0.1.5
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/README.md +160 -0
- package/dist/bin/analyse-auto-6T42LV3G.js +529 -0
- package/dist/bin/analyse-auto-BHLEMIH5.js +529 -0
- package/dist/bin/analyse-auto-CM7XEPKT.js +529 -0
- package/dist/bin/analyse-auto-KZNPVVCR.js +529 -0
- package/dist/bin/analyse-auto-OBCDWYWX.js +529 -0
- package/dist/bin/analyse-auto-QUGN5LES.js +529 -0
- package/dist/bin/analyse-auto-RDYNFTCD.js +529 -0
- package/dist/bin/analyse-auto-Y5QUQU4G.js +529 -0
- package/dist/bin/analyse-results-5XEQNL5W.js +8 -0
- package/dist/bin/analyse-results-F7TH2YMO.js +8 -0
- package/dist/bin/analyse-results-KM5C7XL7.js +8 -0
- package/dist/bin/analyse-results-KU3KEG3E.js +8 -0
- package/dist/bin/analyse-results-OU6F6TRX.js +8 -0
- package/dist/bin/analyse-results-QSOD3KVC.js +8 -0
- package/dist/bin/analyse-results-UHU4DPO3.js +8 -0
- package/dist/bin/analyse-results-ZM5ABNEL.js +8 -0
- package/dist/bin/bob.js +5144 -0
- package/dist/bin/chunk-IYYF7MYV.js +964 -0
- package/dist/bin/chunk-J4RRWEHU.js +939 -0
- package/dist/bin/chunk-KDCVG7F2.js +964 -0
- package/dist/bin/chunk-KSHHT2WT.js +939 -0
- package/dist/bin/chunk-L554PTBY.js +888 -0
- package/dist/bin/chunk-LHWBSCJ4.js +878 -0
- package/dist/bin/chunk-RSOPJT6F.js +883 -0
- package/dist/bin/chunk-TXCQFX4W.js +946 -0
- package/dist/bob.js +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callLocalModel,
|
|
3
|
+
getConfig,
|
|
4
|
+
loadLocalSuggestions,
|
|
5
|
+
markSuggestionStatus,
|
|
6
|
+
readFileContent
|
|
7
|
+
} from "./chunk-L554PTBY.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/analyse-auto.ts
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import inquirer from "inquirer";
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import * as readline from "readline";
|
|
15
|
+
var RED = chalk.hex("#EF5350");
|
|
16
|
+
var GREEN = chalk.hex("#66BB6A");
|
|
17
|
+
var AMBER = chalk.hex("#FFAB00");
|
|
18
|
+
var BLUE = chalk.hex("#42A5F5");
|
|
19
|
+
var GRAY = chalk.gray;
|
|
20
|
+
var BORDER = chalk.hex("#455A64");
|
|
21
|
+
async function runAutoFix(options) {
|
|
22
|
+
const config = getConfig();
|
|
23
|
+
if (config.provider !== "local" || !config.localEndpoint) {
|
|
24
|
+
console.log("");
|
|
25
|
+
console.log(chalk.red(" \u274C Auto-fix requires a local model."));
|
|
26
|
+
console.log(GRAY(" Run `bob config set provider local`"));
|
|
27
|
+
console.log(GRAY(" Run `bob config set localEndpoint http://127.0.0.1:11434/api/chat`"));
|
|
28
|
+
console.log("");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const confidenceGate = options.confidence || 90;
|
|
32
|
+
const priorityGate = options.priority || "critical";
|
|
33
|
+
const categories = options.category ? [options.category] : ["bugs", "features", "improvements", "upgrades"];
|
|
34
|
+
const isAutoMode = config.autoMode || false;
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log(chalk.bold.cyan(" \u26A1 MiniBob Auto-Fix Mode"));
|
|
37
|
+
console.log(GRAY(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
38
|
+
console.log(GRAY(` Confidence gate: ${confidenceGate}%`));
|
|
39
|
+
console.log(GRAY(` Priority gate: ${priorityGate}+`));
|
|
40
|
+
console.log(GRAY(` Categories: ${categories.join(", ")}`));
|
|
41
|
+
console.log(GRAY(` Auto mode: ${isAutoMode ? "ON (no approval prompts)" : "OFF (approval required)"}`));
|
|
42
|
+
console.log(GRAY(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
43
|
+
console.log("");
|
|
44
|
+
let allSuggestions = [];
|
|
45
|
+
for (const cat of categories) {
|
|
46
|
+
allSuggestions.push(...loadLocalSuggestions(cat));
|
|
47
|
+
}
|
|
48
|
+
const priorityOrder = ["critical", "high", "medium", "low"];
|
|
49
|
+
const gateIndex = priorityOrder.indexOf(priorityGate.toLowerCase());
|
|
50
|
+
if (gateIndex >= 0) {
|
|
51
|
+
allSuggestions = allSuggestions.filter((s) => {
|
|
52
|
+
const idx = priorityOrder.indexOf(s.priority?.toLowerCase());
|
|
53
|
+
return idx >= 0 && idx <= gateIndex;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (allSuggestions.length === 0) {
|
|
57
|
+
console.log(chalk.green(" \u2705 No suggestions match your gates. Project is clean!"));
|
|
58
|
+
console.log("");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.log(GRAY(` Found ${allSuggestions.length} suggestions matching criteria.`));
|
|
62
|
+
console.log("");
|
|
63
|
+
console.log(AMBER(" \u{1F9E0} Phase 1: Triage \u2014 Bob is evaluating suggestions..."));
|
|
64
|
+
console.log("");
|
|
65
|
+
const triageResults = await performTriage(allSuggestions, confidenceGate, config.localEndpoint);
|
|
66
|
+
if (!triageResults) return;
|
|
67
|
+
const autoApprove = triageResults.filter((r) => r.action === "work" && r.confidence >= confidenceGate);
|
|
68
|
+
const needsReview = triageResults.filter((r) => r.action === "review" || r.action === "work" && r.confidence < confidenceGate && r.confidence >= confidenceGate - 15);
|
|
69
|
+
const dismissed = triageResults.filter((r) => r.action === "dismiss" || r.confidence < confidenceGate - 15);
|
|
70
|
+
console.log(BORDER(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
71
|
+
console.log(BORDER(" \u2551") + AMBER(" \u25C6 TRIAGE COMPLETE"));
|
|
72
|
+
console.log(BORDER(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
|
|
73
|
+
console.log(BORDER(" \u2551") + GREEN(` \u2705 Auto-approve: ${autoApprove.length} items (confidence \u2265 ${confidenceGate}%)`));
|
|
74
|
+
if (needsReview.length > 0) {
|
|
75
|
+
console.log(BORDER(" \u2551") + AMBER(` \u{1F914} Needs review: ${needsReview.length} items`));
|
|
76
|
+
}
|
|
77
|
+
console.log(BORDER(" \u2551") + GRAY(` \u23F8\uFE0F Dismissed: ${dismissed.length} items`));
|
|
78
|
+
console.log(BORDER(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
79
|
+
console.log("");
|
|
80
|
+
if (autoApprove.length > 0) {
|
|
81
|
+
console.log(GREEN(" \u2705 APPROVE (auto-fix these):"));
|
|
82
|
+
for (let i = 0; i < autoApprove.length; i++) {
|
|
83
|
+
const item = autoApprove[i];
|
|
84
|
+
console.log(GRAY(` ${i + 1}. ${item.suggestion.filePath} \u2014 ${item.suggestion.title || item.suggestion.description?.slice(0, 40) || "No title"} (${item.confidence}%)`));
|
|
85
|
+
}
|
|
86
|
+
console.log("");
|
|
87
|
+
}
|
|
88
|
+
if (needsReview.length > 0) {
|
|
89
|
+
console.log(AMBER(" \u{1F914} REVIEW (Bob wants your input):"));
|
|
90
|
+
for (let i = 0; i < needsReview.length; i++) {
|
|
91
|
+
const item = needsReview[i];
|
|
92
|
+
console.log(GRAY(` ${i + 1}. ${item.suggestion.filePath} \u2014 ${item.reason} (${item.confidence}%)`));
|
|
93
|
+
}
|
|
94
|
+
console.log("");
|
|
95
|
+
}
|
|
96
|
+
for (const item of dismissed) {
|
|
97
|
+
const suggestionIndex = parseInt(item.suggestion.id?.split("_").pop() || "0");
|
|
98
|
+
const category = detectCategory(item.suggestion);
|
|
99
|
+
markSuggestionStatus(item.suggestion.filePath, suggestionIndex, category, "dismissed", {
|
|
100
|
+
confidence: item.confidence,
|
|
101
|
+
reason: item.reason,
|
|
102
|
+
implementedBy: "bob-triage"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
let workQueue = [];
|
|
106
|
+
if (isAutoMode) {
|
|
107
|
+
workQueue = autoApprove.map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" }));
|
|
108
|
+
console.log(GRAY(" [Auto mode] Proceeding without approval prompt."));
|
|
109
|
+
} else {
|
|
110
|
+
const { choice } = await inquirer.prompt([{
|
|
111
|
+
type: "select",
|
|
112
|
+
name: "choice",
|
|
113
|
+
message: AMBER("How would you like to proceed?"),
|
|
114
|
+
choices: [
|
|
115
|
+
{ name: GREEN(` \u2705 Auto-fix approved items only (${autoApprove.length} items)`), value: "approved_only" },
|
|
116
|
+
{ name: GREEN(` \u2705 Auto-fix ALL including review items (${autoApprove.length + needsReview.length} items)`), value: "all" },
|
|
117
|
+
{ name: BLUE(" \u{1F5E3}\uFE0F Talk to Bob about these suggestions"), value: "talk" },
|
|
118
|
+
{ name: GRAY(" \u2190 Cancel"), value: "cancel" }
|
|
119
|
+
]
|
|
120
|
+
}]);
|
|
121
|
+
if (choice === "cancel") {
|
|
122
|
+
console.log(GRAY(" Cancelled."));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (choice === "talk") {
|
|
126
|
+
const updatedQueue = await talkToBobAboutSuggestions(autoApprove, needsReview, dismissed, config.localEndpoint);
|
|
127
|
+
if (updatedQueue.length === 0) {
|
|
128
|
+
console.log(GRAY(" No items to implement."));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
workQueue = updatedQueue;
|
|
132
|
+
} else if (choice === "approved_only") {
|
|
133
|
+
workQueue = autoApprove.map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" }));
|
|
134
|
+
} else {
|
|
135
|
+
workQueue = [...autoApprove, ...needsReview].map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" }));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (workQueue.length === 0) {
|
|
139
|
+
console.log(chalk.yellow(" \u26A0\uFE0F Nothing to implement."));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.log("");
|
|
143
|
+
console.log(AMBER(" \u{1F527} Phase 3: MiniBob Implementing..."));
|
|
144
|
+
console.log(GRAY(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
145
|
+
console.log(GRAY(" \u{1F4AC} /skip <file> to skip. /done to stop early."));
|
|
146
|
+
console.log(GRAY(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
147
|
+
console.log("");
|
|
148
|
+
await executeWithChat(workQueue, config);
|
|
149
|
+
const fixed = workQueue.filter((t) => t.status === "done");
|
|
150
|
+
const failed = workQueue.filter((t) => t.status === "failed");
|
|
151
|
+
const skipped = workQueue.filter((t) => t.status === "skipped");
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(BORDER(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
154
|
+
console.log(BORDER(" \u2551") + AMBER(" \u25C6 MINIBOB AUTO-FIX REPORT"));
|
|
155
|
+
console.log(BORDER(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
|
|
156
|
+
console.log(BORDER(" \u2551") + GREEN(` \u2705 Fixed: ${fixed.length} items`));
|
|
157
|
+
console.log(BORDER(" \u2551") + GRAY(` \u23F8\uFE0F Held: ${dismissed.length + skipped.length} items`));
|
|
158
|
+
if (failed.length > 0) {
|
|
159
|
+
console.log(BORDER(" \u2551") + RED(` \u274C Failed: ${failed.length} items`));
|
|
160
|
+
}
|
|
161
|
+
console.log(BORDER(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563"));
|
|
162
|
+
if (fixed.length > 0) {
|
|
163
|
+
console.log(BORDER(" \u2551") + GRAY(" Fixed files:"));
|
|
164
|
+
for (const item of fixed) {
|
|
165
|
+
console.log(BORDER(" \u2551") + GREEN(` \u2705 ${item.suggestion.filePath}`));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (failed.length > 0) {
|
|
169
|
+
console.log(BORDER(" \u2551") + GRAY(" Failed:"));
|
|
170
|
+
for (const item of failed) {
|
|
171
|
+
console.log(BORDER(" \u2551") + RED(` \u274C ${item.suggestion.filePath}`));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (skipped.length > 0) {
|
|
175
|
+
console.log(BORDER(" \u2551") + GRAY(" Skipped:"));
|
|
176
|
+
for (const item of skipped) {
|
|
177
|
+
console.log(BORDER(" \u2551") + GRAY(` \u23F8\uFE0F ${item.suggestion.filePath}`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log(BORDER(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
181
|
+
console.log("");
|
|
182
|
+
console.log(GRAY(" \u{1F4E6} All original files backed up to .bob-backups/"));
|
|
183
|
+
console.log(GRAY(' Run `bob push "MiniBob auto-fix batch"` to commit changes.'));
|
|
184
|
+
console.log("");
|
|
185
|
+
}
|
|
186
|
+
async function performTriage(suggestions, confidenceGate, endpoint) {
|
|
187
|
+
const triagePrompt = `You are the Lead QA Engineer triaging code suggestions for auto-implementation by MiniBob (a junior engineer).
|
|
188
|
+
|
|
189
|
+
For each suggestion, decide: WORK, REVIEW, or DISMISS.
|
|
190
|
+
|
|
191
|
+
DECISION CRITERIA:
|
|
192
|
+
- WORK: The fix is clear, specific, well-defined, and you are CONFIDENT it will not break anything. MiniBob can implement it without supervision.
|
|
193
|
+
- REVIEW: The fix is good but has side effects, touches shared logic, or behavioral changes that need human approval first.
|
|
194
|
+
- DISMISS: The suggestion is vague, risky, poorly defined, or the effort/risk outweighs the benefit.
|
|
195
|
+
|
|
196
|
+
CONFIDENCE SCORING \u2014 Your confidence represents:
|
|
197
|
+
"How certain am I that this fix will NOT break anything AND will ACTUALLY contribute positively to the project?"
|
|
198
|
+
- 95-100%: Fix is 1-5 lines, explicit instructions, zero side effects, purely additive improvement
|
|
199
|
+
- 85-94%: Clear fix, well-scoped, touches isolated logic, minimal risk
|
|
200
|
+
- 75-84%: Good fix but touches shared modules or has minor behavioral implications
|
|
201
|
+
- <75%: Requires judgment, structural changes, or has unpredictable side effects
|
|
202
|
+
|
|
203
|
+
SUGGESTIONS:
|
|
204
|
+
${suggestions.map((s, i) => `[${i}] ${s.priority?.toUpperCase()} | ${s.filePath} | ${s.title || "No title"} | ${s.description || "No description"} | Implementation: ${s.implementation || "None provided"}`).join("\n")}
|
|
205
|
+
|
|
206
|
+
Respond with ONLY a JSON array:
|
|
207
|
+
[{"index": 0, "action": "work"|"review"|"dismiss", "confidence": 0-100, "reason": "brief reason for this confidence level"}]`;
|
|
208
|
+
try {
|
|
209
|
+
const messages = [
|
|
210
|
+
{ role: "system", content: "You are a senior engineering lead. Respond with ONLY a valid JSON array." },
|
|
211
|
+
{ role: "user", content: triagePrompt }
|
|
212
|
+
];
|
|
213
|
+
const response = await callLocalModel(endpoint, messages);
|
|
214
|
+
const jsonMatch = response.match(/\[[\s\S]*\]/);
|
|
215
|
+
if (!jsonMatch) {
|
|
216
|
+
console.log(chalk.red(" \u274C Triage failed: Could not parse."));
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
220
|
+
return parsed.map((d) => ({
|
|
221
|
+
action: d.action === "work" ? "work" : d.action === "review" ? "review" : "dismiss",
|
|
222
|
+
confidence: d.confidence || 0,
|
|
223
|
+
reason: d.reason || "",
|
|
224
|
+
suggestion: suggestions[d.index]
|
|
225
|
+
})).filter((r) => r.suggestion);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.log(chalk.red(` \u274C Triage failed: ${error.message}`));
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function talkToBobAboutSuggestions(approved, review, dismissed, endpoint) {
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log(BLUE(" \u{1F5E3}\uFE0F Chat with Bob about the suggestions"));
|
|
234
|
+
console.log(GRAY(" Commands: skip <file>, add <file>, /done"));
|
|
235
|
+
console.log(GRAY(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
236
|
+
console.log("");
|
|
237
|
+
const history = [
|
|
238
|
+
{ role: "system", content: `You are Bob, helping decide which suggestions to implement. Be concise.
|
|
239
|
+
|
|
240
|
+
APPROVED: ${approved.map((r) => `${r.suggestion.filePath}: ${r.suggestion.title || r.suggestion.description}`).join("\n")}
|
|
241
|
+
|
|
242
|
+
REVIEW: ${review.map((r) => `${r.suggestion.filePath}: ${r.reason}`).join("\n")}
|
|
243
|
+
|
|
244
|
+
DISMISSED: ${dismissed.map((r) => `${r.suggestion.filePath}: ${r.reason}`).join("\n")}` }
|
|
245
|
+
];
|
|
246
|
+
let finalApproved = [...approved];
|
|
247
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
248
|
+
return new Promise((resolve) => {
|
|
249
|
+
const prompt = () => {
|
|
250
|
+
rl.question(chalk.green(" You: "), async (input) => {
|
|
251
|
+
const trimmed = input.trim();
|
|
252
|
+
if (!trimmed) {
|
|
253
|
+
prompt();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (trimmed === "/done") {
|
|
257
|
+
rl.close();
|
|
258
|
+
resolve(finalApproved.map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" })));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (trimmed.toLowerCase().startsWith("skip ") || trimmed.toLowerCase().startsWith("remove ")) {
|
|
262
|
+
const target = trimmed.slice(trimmed.indexOf(" ") + 1).trim().toLowerCase();
|
|
263
|
+
const before = finalApproved.length;
|
|
264
|
+
finalApproved = finalApproved.filter((r) => !r.suggestion.filePath.toLowerCase().includes(target));
|
|
265
|
+
console.log(before > finalApproved.length ? chalk.yellow(` \u23F8\uFE0F Removed ${before - finalApproved.length} item(s)`) : GRAY(` No match for "${target}"`));
|
|
266
|
+
prompt();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (trimmed.toLowerCase().startsWith("add ")) {
|
|
270
|
+
const target = trimmed.slice(4).trim().toLowerCase();
|
|
271
|
+
const toAdd = [...review, ...dismissed].filter((r) => r.suggestion.filePath.toLowerCase().includes(target));
|
|
272
|
+
if (toAdd.length > 0) {
|
|
273
|
+
finalApproved.push(...toAdd);
|
|
274
|
+
console.log(chalk.green(` \u2705 Added ${toAdd.length} item(s)`));
|
|
275
|
+
} else {
|
|
276
|
+
console.log(GRAY(` No match for "${target}"`));
|
|
277
|
+
}
|
|
278
|
+
prompt();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
history.push({ role: "user", content: trimmed });
|
|
282
|
+
try {
|
|
283
|
+
const response = await callLocalModel(endpoint, history);
|
|
284
|
+
history.push({ role: "assistant", content: response });
|
|
285
|
+
console.log(chalk.bold.cyan(" \u{1F916} Bob: ") + response.split("\n")[0]);
|
|
286
|
+
if (response.split("\n").length > 1) {
|
|
287
|
+
response.split("\n").slice(1).forEach((l) => console.log(` ${l}`));
|
|
288
|
+
}
|
|
289
|
+
console.log("");
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
prompt();
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
prompt();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async function executeWithChat(workQueue, config) {
|
|
299
|
+
renderTodoList(workQueue);
|
|
300
|
+
let userMessages = [];
|
|
301
|
+
let chatActive = true;
|
|
302
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
303
|
+
const inputPromise = new Promise((resolve) => {
|
|
304
|
+
const askForInput = () => {
|
|
305
|
+
if (!chatActive) {
|
|
306
|
+
resolve();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
rl.question(chalk.gray(" \u{1F4AC} "), (input) => {
|
|
310
|
+
const trimmed = input.trim();
|
|
311
|
+
if (trimmed === "/done") {
|
|
312
|
+
for (const task of workQueue) {
|
|
313
|
+
if (task.status === "pending") task.status = "skipped";
|
|
314
|
+
}
|
|
315
|
+
chatActive = false;
|
|
316
|
+
resolve();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (trimmed.startsWith("/skip ")) {
|
|
320
|
+
const target = trimmed.slice(6).trim().toLowerCase();
|
|
321
|
+
for (const task of workQueue) {
|
|
322
|
+
if (task.status === "pending" && task.suggestion.filePath.toLowerCase().includes(target)) {
|
|
323
|
+
task.status = "skipped";
|
|
324
|
+
console.log(chalk.yellow(` \u23F8\uFE0F Skipping: ${task.suggestion.filePath}`));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} else if (trimmed) {
|
|
328
|
+
userMessages.push(trimmed);
|
|
329
|
+
}
|
|
330
|
+
if (chatActive) askForInput();
|
|
331
|
+
else resolve();
|
|
332
|
+
});
|
|
333
|
+
};
|
|
334
|
+
askForInput();
|
|
335
|
+
});
|
|
336
|
+
for (let i = 0; i < workQueue.length; i++) {
|
|
337
|
+
const task = workQueue[i];
|
|
338
|
+
if (task.status === "skipped") continue;
|
|
339
|
+
task.status = "working";
|
|
340
|
+
renderTodoList(workQueue);
|
|
341
|
+
if (userMessages.length > 0) {
|
|
342
|
+
const userMsg = userMessages.shift();
|
|
343
|
+
try {
|
|
344
|
+
const bobResponse = await callLocalModel(config.localEndpoint, [
|
|
345
|
+
{ role: "system", content: `You are Bob supervising MiniBob. Respond in 1-2 sentences. Current task: ${task.suggestion.filePath}` },
|
|
346
|
+
{ role: "user", content: userMsg }
|
|
347
|
+
]);
|
|
348
|
+
console.log(chalk.bold.cyan(` \u{1F916} Bob: `) + bobResponse.split("\n")[0]);
|
|
349
|
+
console.log("");
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const success = await implementTask(task, config.localEndpoint);
|
|
354
|
+
task.status = success ? "done" : "failed";
|
|
355
|
+
if (success) {
|
|
356
|
+
const suggestionIndex = parseInt(task.suggestion.id?.split("_").pop() || "0");
|
|
357
|
+
const category = detectCategory(task.suggestion);
|
|
358
|
+
markSuggestionStatus(task.suggestion.filePath, suggestionIndex, category, "implemented", {
|
|
359
|
+
confidence: task.confidence,
|
|
360
|
+
reason: task.reason,
|
|
361
|
+
implementedBy: "minibob-auto"
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
renderTodoList(workQueue);
|
|
365
|
+
}
|
|
366
|
+
chatActive = false;
|
|
367
|
+
rl.close();
|
|
368
|
+
await Promise.race([inputPromise, new Promise((resolve) => setTimeout(resolve, 100))]);
|
|
369
|
+
}
|
|
370
|
+
async function implementTask(task, endpoint) {
|
|
371
|
+
const suggestion = task.suggestion;
|
|
372
|
+
const fileContent = readFileContent(suggestion.filePath);
|
|
373
|
+
if (!fileContent) return false;
|
|
374
|
+
const prompt = `You are MiniBob \u2014 a junior engineer making SURGICAL code fixes under strict supervision.
|
|
375
|
+
|
|
376
|
+
CURRENT FILE: ${suggestion.filePath}
|
|
377
|
+
${fileContent}
|
|
378
|
+
|
|
379
|
+
CHANGE TO IMPLEMENT:
|
|
380
|
+
Title: ${suggestion.title || "Fix"}
|
|
381
|
+
Description: ${suggestion.description}
|
|
382
|
+
Implementation Instructions: ${suggestion.implementation || "Apply the fix described above."}
|
|
383
|
+
|
|
384
|
+
RULES (CRITICAL \u2014 VIOLATION = REJECTED):
|
|
385
|
+
- Return ONLY valid source code. No markdown, no code fences, no \`\`\`, no explanation text.
|
|
386
|
+
- Start the FIRST line with: // File: ${suggestion.filePath}
|
|
387
|
+
- PRESERVE ALL existing imports exactly as they are. Do NOT add, remove, or reorder imports.
|
|
388
|
+
- PRESERVE ALL existing exports exactly as they are. Do NOT rename exported functions or classes.
|
|
389
|
+
- PRESERVE the existing code structure, indentation, patterns, and naming conventions.
|
|
390
|
+
- Make the MINIMUM change necessary to implement the fix. Touch NOTHING else.
|
|
391
|
+
- Do NOT refactor, reorganize, or "improve" unrelated code.
|
|
392
|
+
- Do NOT add comments explaining what you changed.
|
|
393
|
+
- Do NOT wrap the response in markdown code blocks.
|
|
394
|
+
- The output must be valid TypeScript/JavaScript that compiles without errors.
|
|
395
|
+
- If you are unsure about a change, return the file UNCHANGED rather than risk breaking it.
|
|
396
|
+
|
|
397
|
+
Return the complete file content now:`;
|
|
398
|
+
try {
|
|
399
|
+
const messages = [
|
|
400
|
+
{ role: "system", content: "You are MiniBob, a junior engineer making SURGICAL fixes. Return ONLY valid source code. NO markdown. NO code fences. NO explanation. Start with // File: comment. Make the ABSOLUTE MINIMUM change needed. Do NOT restructure, refactor, or touch ANYTHING beyond the specific fix. If unsure, return the file unchanged." },
|
|
401
|
+
{ role: "user", content: prompt }
|
|
402
|
+
];
|
|
403
|
+
const response = await callLocalModel(endpoint, messages);
|
|
404
|
+
const lines = response.split("\n");
|
|
405
|
+
const firstLine = lines[0].trim();
|
|
406
|
+
let newContent;
|
|
407
|
+
if (firstLine.match(/^\/\/\s*(File:)?\s*/)) {
|
|
408
|
+
newContent = lines.slice(1).join("\n").trim();
|
|
409
|
+
} else {
|
|
410
|
+
newContent = response.trim();
|
|
411
|
+
}
|
|
412
|
+
if (newContent.includes("```") || newContent.includes("## ") || newContent.startsWith("Here") || newContent.startsWith("I have") || newContent.startsWith("Sure")) {
|
|
413
|
+
console.log(chalk.yellow(` \u26A0\uFE0F MiniBob returned explanation instead of code. Skipping ${suggestion.filePath}.`));
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
if (newContent.length < fileContent.length * 0.5) {
|
|
417
|
+
console.log(chalk.yellow(` \u26A0\uFE0F MiniBob's output is ${Math.round(newContent.length / fileContent.length * 100)}% of original size. Rejecting to prevent data loss.`));
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
const originalExports = fileContent.match(/export\s+(function|class|const|interface|type|async\s+function)\s+\w+/g) || [];
|
|
421
|
+
for (const exp of originalExports) {
|
|
422
|
+
const exportName = exp.split(/\s+/).pop();
|
|
423
|
+
if (!newContent.includes(exportName)) {
|
|
424
|
+
console.log(chalk.yellow(` \u26A0\uFE0F MiniBob removed export "${exportName}". Rejecting change to ${suggestion.filePath}.`));
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const originalImportCount = (fileContent.match(/^import\s+/gm) || []).length;
|
|
429
|
+
const newImportCount = (newContent.match(/^import\s+/gm) || []).length;
|
|
430
|
+
if (Math.abs(originalImportCount - newImportCount) > 2) {
|
|
431
|
+
console.log(chalk.yellow(` \u26A0\uFE0F MiniBob changed import count from ${originalImportCount} to ${newImportCount}. Rejecting.`));
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
const absolutePath = path.join(process.cwd(), suggestion.filePath);
|
|
435
|
+
const backupDir = path.join(process.cwd(), ".bob-backups");
|
|
436
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
437
|
+
if (fs.existsSync(absolutePath)) {
|
|
438
|
+
const timestamp = Date.now();
|
|
439
|
+
const backupName = suggestion.filePath.replace(/[\/\\]/g, "_") + `.${timestamp}.bak`;
|
|
440
|
+
fs.copyFileSync(absolutePath, path.join(backupDir, backupName));
|
|
441
|
+
}
|
|
442
|
+
const dir = path.dirname(absolutePath);
|
|
443
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
444
|
+
fs.writeFileSync(absolutePath, newContent, "utf-8");
|
|
445
|
+
return true;
|
|
446
|
+
} catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function detectCategory(suggestion) {
|
|
451
|
+
const cwd = process.cwd();
|
|
452
|
+
const projectName = path.basename(cwd);
|
|
453
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
454
|
+
const analysisPath = path.join(homeDir, ".bob", "projects", projectName, "analysis", "results", "analysis.json");
|
|
455
|
+
if (!fs.existsSync(analysisPath)) return "bugs";
|
|
456
|
+
const allResults = JSON.parse(fs.readFileSync(analysisPath, "utf-8"));
|
|
457
|
+
const fileResults = allResults[suggestion.filePath];
|
|
458
|
+
if (!fileResults) return "bugs";
|
|
459
|
+
for (const cat of ["bugs", "features", "improvements", "upgrades"]) {
|
|
460
|
+
const items = fileResults[cat] || [];
|
|
461
|
+
for (const item of items) {
|
|
462
|
+
if (item.title === suggestion.title && item.description === suggestion.description) return cat;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return "bugs";
|
|
466
|
+
}
|
|
467
|
+
var lastTodoLines = 0;
|
|
468
|
+
function renderTodoList(queue) {
|
|
469
|
+
const lines = [];
|
|
470
|
+
lines.push("");
|
|
471
|
+
lines.push(AMBER(" \u{1F4CB} MiniBob Work Queue"));
|
|
472
|
+
lines.push(GRAY(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
473
|
+
for (let i = 0; i < queue.length; i++) {
|
|
474
|
+
const task = queue[i];
|
|
475
|
+
const label = task.suggestion.title || task.suggestion.description?.slice(0, 40) || "No title";
|
|
476
|
+
let icon;
|
|
477
|
+
let color;
|
|
478
|
+
switch (task.status) {
|
|
479
|
+
case "done":
|
|
480
|
+
icon = "\u2611";
|
|
481
|
+
color = GREEN;
|
|
482
|
+
break;
|
|
483
|
+
case "working":
|
|
484
|
+
icon = "\u23F3";
|
|
485
|
+
color = AMBER;
|
|
486
|
+
break;
|
|
487
|
+
case "failed":
|
|
488
|
+
icon = "\u2717";
|
|
489
|
+
color = RED;
|
|
490
|
+
break;
|
|
491
|
+
case "skipped":
|
|
492
|
+
icon = "\u23F8\uFE0F";
|
|
493
|
+
color = GRAY;
|
|
494
|
+
break;
|
|
495
|
+
default:
|
|
496
|
+
icon = "\u2610";
|
|
497
|
+
color = GRAY;
|
|
498
|
+
}
|
|
499
|
+
lines.push(color(` ${icon} [${i + 1}/${queue.length}] ${task.suggestion.filePath}`));
|
|
500
|
+
lines.push(color(` ${label} (${task.confidence}%)`));
|
|
501
|
+
}
|
|
502
|
+
const completed = queue.filter((t) => t.status === "done" || t.status === "failed" || t.status === "skipped").length;
|
|
503
|
+
const total = queue.length;
|
|
504
|
+
const percent = total > 0 ? completed / total : 0;
|
|
505
|
+
const barLen = 30;
|
|
506
|
+
const filled = Math.round(percent * barLen);
|
|
507
|
+
let barColor;
|
|
508
|
+
if (percent < 0.25) barColor = chalk.red;
|
|
509
|
+
else if (percent < 0.5) barColor = chalk.hex("#FF8C00");
|
|
510
|
+
else if (percent < 0.75) barColor = chalk.yellow;
|
|
511
|
+
else barColor = chalk.green;
|
|
512
|
+
lines.push("");
|
|
513
|
+
lines.push(` [${barColor("\u2588".repeat(filled))}${GRAY("\u2591".repeat(barLen - filled))}] ${completed}/${total} ${barColor(Math.round(percent * 100) + "%")}`);
|
|
514
|
+
lines.push("");
|
|
515
|
+
if (lastTodoLines > 0) {
|
|
516
|
+
process.stdout.write(`\x1B[${lastTodoLines}A`);
|
|
517
|
+
for (let i = 0; i < lastTodoLines; i++) {
|
|
518
|
+
process.stdout.write("\x1B[2K\n");
|
|
519
|
+
}
|
|
520
|
+
process.stdout.write(`\x1B[${lastTodoLines}A`);
|
|
521
|
+
}
|
|
522
|
+
for (const line of lines) {
|
|
523
|
+
process.stdout.write(line + "\n");
|
|
524
|
+
}
|
|
525
|
+
lastTodoLines = lines.length;
|
|
526
|
+
}
|
|
527
|
+
export {
|
|
528
|
+
runAutoFix
|
|
529
|
+
};
|