🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@bobsworkshop/cli

Package Overview
Dependencies
Maintainers
1
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bobsworkshop/cli - npm Package Compare versions

Comparing version
0.5.4
to
0.6.0
+529
dist/analyse-auto-3JL5TO3G.js
import {
callLocalModel,
getConfig,
loadLocalSuggestions,
markSuggestionStatus,
readFileContent
} from "./chunk-SADPOL7M.js";
// src/commands/analyse-auto.ts
import chalk from "chalk";
import inquirer from "inquirer";
import * as fs from "fs";
import * as path from "path";
import * as readline from "readline";
var RED = chalk.hex("#EF5350");
var GREEN = chalk.hex("#66BB6A");
var AMBER = chalk.hex("#FFAB00");
var BLUE = chalk.hex("#42A5F5");
var GRAY = chalk.gray;
var BORDER = chalk.hex("#455A64");
async function runAutoFix(options) {
const config = getConfig();
if (config.provider !== "local" || !config.localEndpoint) {
console.log("");
console.log(chalk.red(" \u274C Auto-fix requires a local model."));
console.log(GRAY(" Run `bob config set provider local`"));
console.log(GRAY(" Run `bob config set localEndpoint http://127.0.0.1:11434/api/chat`"));
console.log("");
return;
}
const confidenceGate = options.confidence || 90;
const priorityGate = options.priority || "critical";
const categories = options.category ? [options.category] : ["bugs", "features", "improvements", "upgrades"];
const isAutoMode = config.autoMode || false;
console.log("");
console.log(chalk.bold.cyan(" \u26A1 MiniBob Auto-Fix Mode"));
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"));
console.log(GRAY(` Confidence gate: ${confidenceGate}%`));
console.log(GRAY(` Priority gate: ${priorityGate}+`));
console.log(GRAY(` Categories: ${categories.join(", ")}`));
console.log(GRAY(` Auto mode: ${isAutoMode ? "ON (no approval prompts)" : "OFF (approval required)"}`));
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"));
console.log("");
let allSuggestions = [];
for (const cat of categories) {
allSuggestions.push(...loadLocalSuggestions(cat));
}
const priorityOrder = ["critical", "high", "medium", "low"];
const gateIndex = priorityOrder.indexOf(priorityGate.toLowerCase());
if (gateIndex >= 0) {
allSuggestions = allSuggestions.filter((s) => {
const idx = priorityOrder.indexOf(s.priority?.toLowerCase());
return idx >= 0 && idx <= gateIndex;
});
}
if (allSuggestions.length === 0) {
console.log(chalk.green(" \u2705 No suggestions match your gates. Project is clean!"));
console.log("");
return;
}
console.log(GRAY(` Found ${allSuggestions.length} suggestions matching criteria.`));
console.log("");
console.log(AMBER(" \u{1F9E0} Phase 1: Triage \u2014 Bob is evaluating suggestions..."));
console.log("");
const triageResults = await performTriage(allSuggestions, confidenceGate, config.localEndpoint);
if (!triageResults) return;
const autoApprove = triageResults.filter((r) => r.action === "work" && r.confidence >= confidenceGate);
const needsReview = triageResults.filter((r) => r.action === "review" || r.action === "work" && r.confidence < confidenceGate && r.confidence >= confidenceGate - 15);
const dismissed = triageResults.filter((r) => r.action === "dismiss" || r.confidence < confidenceGate - 15);
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"));
console.log(BORDER(" \u2551") + AMBER(" \u25C6 TRIAGE COMPLETE"));
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"));
console.log(BORDER(" \u2551") + GREEN(` \u2705 Auto-approve: ${autoApprove.length} items (confidence \u2265 ${confidenceGate}%)`));
if (needsReview.length > 0) {
console.log(BORDER(" \u2551") + AMBER(` \u{1F914} Needs review: ${needsReview.length} items`));
}
console.log(BORDER(" \u2551") + GRAY(` \u23F8\uFE0F Dismissed: ${dismissed.length} items`));
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"));
console.log("");
if (autoApprove.length > 0) {
console.log(GREEN(" \u2705 APPROVE (auto-fix these):"));
for (let i = 0; i < autoApprove.length; i++) {
const item = autoApprove[i];
console.log(GRAY(` ${i + 1}. ${item.suggestion.filePath} \u2014 ${item.suggestion.title || item.suggestion.description?.slice(0, 40) || "No title"} (${item.confidence}%)`));
}
console.log("");
}
if (needsReview.length > 0) {
console.log(AMBER(" \u{1F914} REVIEW (Bob wants your input):"));
for (let i = 0; i < needsReview.length; i++) {
const item = needsReview[i];
console.log(GRAY(` ${i + 1}. ${item.suggestion.filePath} \u2014 ${item.reason} (${item.confidence}%)`));
}
console.log("");
}
for (const item of dismissed) {
const suggestionIndex = parseInt(item.suggestion.id?.split("_").pop() || "0");
const category = detectCategory(item.suggestion);
markSuggestionStatus(item.suggestion.filePath, suggestionIndex, category, "dismissed", {
confidence: item.confidence,
reason: item.reason,
implementedBy: "bob-triage"
});
}
let workQueue = [];
if (isAutoMode) {
workQueue = autoApprove.map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" }));
console.log(GRAY(" [Auto mode] Proceeding without approval prompt."));
} else {
const { choice } = await inquirer.prompt([{
type: "select",
name: "choice",
message: AMBER("How would you like to proceed?"),
choices: [
{ name: GREEN(` \u2705 Auto-fix approved items only (${autoApprove.length} items)`), value: "approved_only" },
{ name: GREEN(` \u2705 Auto-fix ALL including review items (${autoApprove.length + needsReview.length} items)`), value: "all" },
{ name: BLUE(" \u{1F5E3}\uFE0F Talk to Bob about these suggestions"), value: "talk" },
{ name: GRAY(" \u2190 Cancel"), value: "cancel" }
]
}]);
if (choice === "cancel") {
console.log(GRAY(" Cancelled."));
return;
}
if (choice === "talk") {
const updatedQueue = await talkToBobAboutSuggestions(autoApprove, needsReview, dismissed, config.localEndpoint);
if (updatedQueue.length === 0) {
console.log(GRAY(" No items to implement."));
return;
}
workQueue = updatedQueue;
} else if (choice === "approved_only") {
workQueue = autoApprove.map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" }));
} else {
workQueue = [...autoApprove, ...needsReview].map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" }));
}
}
if (workQueue.length === 0) {
console.log(chalk.yellow(" \u26A0\uFE0F Nothing to implement."));
return;
}
console.log("");
console.log(AMBER(" \u{1F527} Phase 3: MiniBob Implementing..."));
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"));
console.log(GRAY(" \u{1F4AC} /skip <file> to skip. /done to stop early."));
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"));
console.log("");
await executeWithChat(workQueue, config);
const fixed = workQueue.filter((t) => t.status === "done");
const failed = workQueue.filter((t) => t.status === "failed");
const skipped = workQueue.filter((t) => t.status === "skipped");
console.log("");
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"));
console.log(BORDER(" \u2551") + AMBER(" \u25C6 MINIBOB AUTO-FIX REPORT"));
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"));
console.log(BORDER(" \u2551") + GREEN(` \u2705 Fixed: ${fixed.length} items`));
console.log(BORDER(" \u2551") + GRAY(` \u23F8\uFE0F Held: ${dismissed.length + skipped.length} items`));
if (failed.length > 0) {
console.log(BORDER(" \u2551") + RED(` \u274C Failed: ${failed.length} items`));
}
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"));
if (fixed.length > 0) {
console.log(BORDER(" \u2551") + GRAY(" Fixed files:"));
for (const item of fixed) {
console.log(BORDER(" \u2551") + GREEN(` \u2705 ${item.suggestion.filePath}`));
}
}
if (failed.length > 0) {
console.log(BORDER(" \u2551") + GRAY(" Failed:"));
for (const item of failed) {
console.log(BORDER(" \u2551") + RED(` \u274C ${item.suggestion.filePath}`));
}
}
if (skipped.length > 0) {
console.log(BORDER(" \u2551") + GRAY(" Skipped:"));
for (const item of skipped) {
console.log(BORDER(" \u2551") + GRAY(` \u23F8\uFE0F ${item.suggestion.filePath}`));
}
}
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"));
console.log("");
console.log(GRAY(" \u{1F4E6} All original files backed up to .bob-backups/"));
console.log(GRAY(' Run `bob push "MiniBob auto-fix batch"` to commit changes.'));
console.log("");
}
async function performTriage(suggestions, confidenceGate, endpoint) {
const triagePrompt = `You are the Lead QA Engineer triaging code suggestions for auto-implementation by MiniBob (a junior engineer).
For each suggestion, decide: WORK, REVIEW, or DISMISS.
DECISION CRITERIA:
- WORK: The fix is clear, specific, well-defined, and you are CONFIDENT it will not break anything. MiniBob can implement it without supervision.
- REVIEW: The fix is good but has side effects, touches shared logic, or behavioral changes that need human approval first.
- DISMISS: The suggestion is vague, risky, poorly defined, or the effort/risk outweighs the benefit.
CONFIDENCE SCORING \u2014 Your confidence represents:
"How certain am I that this fix will NOT break anything AND will ACTUALLY contribute positively to the project?"
- 95-100%: Fix is 1-5 lines, explicit instructions, zero side effects, purely additive improvement
- 85-94%: Clear fix, well-scoped, touches isolated logic, minimal risk
- 75-84%: Good fix but touches shared modules or has minor behavioral implications
- <75%: Requires judgment, structural changes, or has unpredictable side effects
SUGGESTIONS:
${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")}
Respond with ONLY a JSON array:
[{"index": 0, "action": "work"|"review"|"dismiss", "confidence": 0-100, "reason": "brief reason for this confidence level"}]`;
try {
const messages = [
{ role: "system", content: "You are a senior engineering lead. Respond with ONLY a valid JSON array." },
{ role: "user", content: triagePrompt }
];
const response = await callLocalModel(endpoint, messages);
const jsonMatch = response.match(/\[[\s\S]*\]/);
if (!jsonMatch) {
console.log(chalk.red(" \u274C Triage failed: Could not parse."));
return null;
}
const parsed = JSON.parse(jsonMatch[0]);
return parsed.map((d) => ({
action: d.action === "work" ? "work" : d.action === "review" ? "review" : "dismiss",
confidence: d.confidence || 0,
reason: d.reason || "",
suggestion: suggestions[d.index]
})).filter((r) => r.suggestion);
} catch (error) {
console.log(chalk.red(` \u274C Triage failed: ${error.message}`));
return null;
}
}
async function talkToBobAboutSuggestions(approved, review, dismissed, endpoint) {
console.log("");
console.log(BLUE(" \u{1F5E3}\uFE0F Chat with Bob about the suggestions"));
console.log(GRAY(" Commands: skip <file>, add <file>, /done"));
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"));
console.log("");
const history = [
{ role: "system", content: `You are Bob, helping decide which suggestions to implement. Be concise.
APPROVED: ${approved.map((r) => `${r.suggestion.filePath}: ${r.suggestion.title || r.suggestion.description}`).join("\n")}
REVIEW: ${review.map((r) => `${r.suggestion.filePath}: ${r.reason}`).join("\n")}
DISMISSED: ${dismissed.map((r) => `${r.suggestion.filePath}: ${r.reason}`).join("\n")}` }
];
let finalApproved = [...approved];
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
const prompt = () => {
rl.question(chalk.green(" You: "), async (input) => {
const trimmed = input.trim();
if (!trimmed) {
prompt();
return;
}
if (trimmed === "/done") {
rl.close();
resolve(finalApproved.map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" })));
return;
}
if (trimmed.toLowerCase().startsWith("skip ") || trimmed.toLowerCase().startsWith("remove ")) {
const target = trimmed.slice(trimmed.indexOf(" ") + 1).trim().toLowerCase();
const before = finalApproved.length;
finalApproved = finalApproved.filter((r) => !r.suggestion.filePath.toLowerCase().includes(target));
console.log(before > finalApproved.length ? chalk.yellow(` \u23F8\uFE0F Removed ${before - finalApproved.length} item(s)`) : GRAY(` No match for "${target}"`));
prompt();
return;
}
if (trimmed.toLowerCase().startsWith("add ")) {
const target = trimmed.slice(4).trim().toLowerCase();
const toAdd = [...review, ...dismissed].filter((r) => r.suggestion.filePath.toLowerCase().includes(target));
if (toAdd.length > 0) {
finalApproved.push(...toAdd);
console.log(chalk.green(` \u2705 Added ${toAdd.length} item(s)`));
} else {
console.log(GRAY(` No match for "${target}"`));
}
prompt();
return;
}
history.push({ role: "user", content: trimmed });
try {
const response = await callLocalModel(endpoint, history);
history.push({ role: "assistant", content: response });
console.log(chalk.bold.cyan(" \u{1F916} Bob: ") + response.split("\n")[0]);
if (response.split("\n").length > 1) {
response.split("\n").slice(1).forEach((l) => console.log(` ${l}`));
}
console.log("");
} catch {
}
prompt();
});
};
prompt();
});
}
async function executeWithChat(workQueue, config) {
renderTodoList(workQueue);
let userMessages = [];
let chatActive = true;
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const inputPromise = new Promise((resolve) => {
const askForInput = () => {
if (!chatActive) {
resolve();
return;
}
rl.question(chalk.gray(" \u{1F4AC} "), (input) => {
const trimmed = input.trim();
if (trimmed === "/done") {
for (const task of workQueue) {
if (task.status === "pending") task.status = "skipped";
}
chatActive = false;
resolve();
return;
}
if (trimmed.startsWith("/skip ")) {
const target = trimmed.slice(6).trim().toLowerCase();
for (const task of workQueue) {
if (task.status === "pending" && task.suggestion.filePath.toLowerCase().includes(target)) {
task.status = "skipped";
console.log(chalk.yellow(` \u23F8\uFE0F Skipping: ${task.suggestion.filePath}`));
}
}
} else if (trimmed) {
userMessages.push(trimmed);
}
if (chatActive) askForInput();
else resolve();
});
};
askForInput();
});
for (let i = 0; i < workQueue.length; i++) {
const task = workQueue[i];
if (task.status === "skipped") continue;
task.status = "working";
renderTodoList(workQueue);
if (userMessages.length > 0) {
const userMsg = userMessages.shift();
try {
const bobResponse = await callLocalModel(config.localEndpoint, [
{ role: "system", content: `You are Bob supervising MiniBob. Respond in 1-2 sentences. Current task: ${task.suggestion.filePath}` },
{ role: "user", content: userMsg }
]);
console.log(chalk.bold.cyan(` \u{1F916} Bob: `) + bobResponse.split("\n")[0]);
console.log("");
} catch {
}
}
const success = await implementTask(task, config.localEndpoint);
task.status = success ? "done" : "failed";
if (success) {
const suggestionIndex = parseInt(task.suggestion.id?.split("_").pop() || "0");
const category = detectCategory(task.suggestion);
markSuggestionStatus(task.suggestion.filePath, suggestionIndex, category, "implemented", {
confidence: task.confidence,
reason: task.reason,
implementedBy: "minibob-auto"
});
}
renderTodoList(workQueue);
}
chatActive = false;
rl.close();
await Promise.race([inputPromise, new Promise((resolve) => setTimeout(resolve, 100))]);
}
async function implementTask(task, endpoint) {
const suggestion = task.suggestion;
const fileContent = readFileContent(suggestion.filePath);
if (!fileContent) return false;
const prompt = `You are MiniBob \u2014 a junior engineer making SURGICAL code fixes under strict supervision.
CURRENT FILE: ${suggestion.filePath}
${fileContent}
CHANGE TO IMPLEMENT:
Title: ${suggestion.title || "Fix"}
Description: ${suggestion.description}
Implementation Instructions: ${suggestion.implementation || "Apply the fix described above."}
RULES (CRITICAL \u2014 VIOLATION = REJECTED):
- Return ONLY valid source code. No markdown, no code fences, no \`\`\`, no explanation text.
- Start the FIRST line with: // File: ${suggestion.filePath}
- PRESERVE ALL existing imports exactly as they are. Do NOT add, remove, or reorder imports.
- PRESERVE ALL existing exports exactly as they are. Do NOT rename exported functions or classes.
- PRESERVE the existing code structure, indentation, patterns, and naming conventions.
- Make the MINIMUM change necessary to implement the fix. Touch NOTHING else.
- Do NOT refactor, reorganize, or "improve" unrelated code.
- Do NOT add comments explaining what you changed.
- Do NOT wrap the response in markdown code blocks.
- The output must be valid TypeScript/JavaScript that compiles without errors.
- If you are unsure about a change, return the file UNCHANGED rather than risk breaking it.
Return the complete file content now:`;
try {
const messages = [
{ 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." },
{ role: "user", content: prompt }
];
const response = await callLocalModel(endpoint, messages);
const lines = response.split("\n");
const firstLine = lines[0].trim();
let newContent;
if (firstLine.match(/^\/\/\s*(File:)?\s*/)) {
newContent = lines.slice(1).join("\n").trim();
} else {
newContent = response.trim();
}
if (newContent.includes("```") || newContent.includes("## ") || newContent.startsWith("Here") || newContent.startsWith("I have") || newContent.startsWith("Sure")) {
console.log(chalk.yellow(` \u26A0\uFE0F MiniBob returned explanation instead of code. Skipping ${suggestion.filePath}.`));
return false;
}
if (newContent.length < fileContent.length * 0.5) {
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.`));
return false;
}
const originalExports = fileContent.match(/export\s+(function|class|const|interface|type|async\s+function)\s+\w+/g) || [];
for (const exp of originalExports) {
const exportName = exp.split(/\s+/).pop();
if (!newContent.includes(exportName)) {
console.log(chalk.yellow(` \u26A0\uFE0F MiniBob removed export "${exportName}". Rejecting change to ${suggestion.filePath}.`));
return false;
}
}
const originalImportCount = (fileContent.match(/^import\s+/gm) || []).length;
const newImportCount = (newContent.match(/^import\s+/gm) || []).length;
if (Math.abs(originalImportCount - newImportCount) > 2) {
console.log(chalk.yellow(` \u26A0\uFE0F MiniBob changed import count from ${originalImportCount} to ${newImportCount}. Rejecting.`));
return false;
}
const absolutePath = path.join(process.cwd(), suggestion.filePath);
const backupDir = path.join(process.cwd(), ".bob-backups");
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
if (fs.existsSync(absolutePath)) {
const timestamp = Date.now();
const backupName = suggestion.filePath.replace(/[\/\\]/g, "_") + `.${timestamp}.bak`;
fs.copyFileSync(absolutePath, path.join(backupDir, backupName));
}
const dir = path.dirname(absolutePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(absolutePath, newContent, "utf-8");
return true;
} catch {
return false;
}
}
function detectCategory(suggestion) {
const cwd = process.cwd();
const projectName = path.basename(cwd);
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
const analysisPath = path.join(homeDir, ".bob", "projects", projectName, "analysis", "results", "analysis.json");
if (!fs.existsSync(analysisPath)) return "bugs";
const allResults = JSON.parse(fs.readFileSync(analysisPath, "utf-8"));
const fileResults = allResults[suggestion.filePath];
if (!fileResults) return "bugs";
for (const cat of ["bugs", "features", "improvements", "upgrades"]) {
const items = fileResults[cat] || [];
for (const item of items) {
if (item.title === suggestion.title && item.description === suggestion.description) return cat;
}
}
return "bugs";
}
var lastTodoLines = 0;
function renderTodoList(queue) {
const lines = [];
lines.push("");
lines.push(AMBER(" \u{1F4CB} MiniBob Work Queue"));
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"));
for (let i = 0; i < queue.length; i++) {
const task = queue[i];
const label = task.suggestion.title || task.suggestion.description?.slice(0, 40) || "No title";
let icon;
let color;
switch (task.status) {
case "done":
icon = "\u2611";
color = GREEN;
break;
case "working":
icon = "\u23F3";
color = AMBER;
break;
case "failed":
icon = "\u2717";
color = RED;
break;
case "skipped":
icon = "\u23F8\uFE0F";
color = GRAY;
break;
default:
icon = "\u2610";
color = GRAY;
}
lines.push(color(` ${icon} [${i + 1}/${queue.length}] ${task.suggestion.filePath}`));
lines.push(color(` ${label} (${task.confidence}%)`));
}
const completed = queue.filter((t) => t.status === "done" || t.status === "failed" || t.status === "skipped").length;
const total = queue.length;
const percent = total > 0 ? completed / total : 0;
const barLen = 30;
const filled = Math.round(percent * barLen);
let barColor;
if (percent < 0.25) barColor = chalk.red;
else if (percent < 0.5) barColor = chalk.hex("#FF8C00");
else if (percent < 0.75) barColor = chalk.yellow;
else barColor = chalk.green;
lines.push("");
lines.push(` [${barColor("\u2588".repeat(filled))}${GRAY("\u2591".repeat(barLen - filled))}] ${completed}/${total} ${barColor(Math.round(percent * 100) + "%")}`);
lines.push("");
if (lastTodoLines > 0) {
process.stdout.write(`\x1B[${lastTodoLines}A`);
for (let i = 0; i < lastTodoLines; i++) {
process.stdout.write("\x1B[2K\n");
}
process.stdout.write(`\x1B[${lastTodoLines}A`);
}
for (const line of lines) {
process.stdout.write(line + "\n");
}
lastTodoLines = lines.length;
}
export {
runAutoFix
};
import {
loadLocalSuggestions,
showInteractiveResults
} from "./chunk-SADPOL7M.js";
export {
loadLocalSuggestions,
showInteractiveResults
};
import {
callLocalModel,
getConfig,
loadLocalSuggestions,
markSuggestionStatus,
readFileContent
} from "./chunk-SADPOL7M.js";
// src/commands/analyse-auto.ts
import chalk from "chalk";
import inquirer from "inquirer";
import * as fs from "fs";
import * as path from "path";
import * as readline from "readline";
var RED = chalk.hex("#EF5350");
var GREEN = chalk.hex("#66BB6A");
var AMBER = chalk.hex("#FFAB00");
var BLUE = chalk.hex("#42A5F5");
var GRAY = chalk.gray;
var BORDER = chalk.hex("#455A64");
async function runAutoFix(options) {
const config = getConfig();
if (config.provider !== "local" || !config.localEndpoint) {
console.log("");
console.log(chalk.red(" \u274C Auto-fix requires a local model."));
console.log(GRAY(" Run `bob config set provider local`"));
console.log(GRAY(" Run `bob config set localEndpoint http://127.0.0.1:11434/api/chat`"));
console.log("");
return;
}
const confidenceGate = options.confidence || 90;
const priorityGate = options.priority || "critical";
const categories = options.category ? [options.category] : ["bugs", "features", "improvements", "upgrades"];
const isAutoMode = config.autoMode || false;
console.log("");
console.log(chalk.bold.cyan(" \u26A1 MiniBob Auto-Fix Mode"));
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"));
console.log(GRAY(` Confidence gate: ${confidenceGate}%`));
console.log(GRAY(` Priority gate: ${priorityGate}+`));
console.log(GRAY(` Categories: ${categories.join(", ")}`));
console.log(GRAY(` Auto mode: ${isAutoMode ? "ON (no approval prompts)" : "OFF (approval required)"}`));
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"));
console.log("");
let allSuggestions = [];
for (const cat of categories) {
allSuggestions.push(...loadLocalSuggestions(cat));
}
const priorityOrder = ["critical", "high", "medium", "low"];
const gateIndex = priorityOrder.indexOf(priorityGate.toLowerCase());
if (gateIndex >= 0) {
allSuggestions = allSuggestions.filter((s) => {
const idx = priorityOrder.indexOf(s.priority?.toLowerCase());
return idx >= 0 && idx <= gateIndex;
});
}
if (allSuggestions.length === 0) {
console.log(chalk.green(" \u2705 No suggestions match your gates. Project is clean!"));
console.log("");
return;
}
console.log(GRAY(` Found ${allSuggestions.length} suggestions matching criteria.`));
console.log("");
console.log(AMBER(" \u{1F9E0} Phase 1: Triage \u2014 Bob is evaluating suggestions..."));
console.log("");
const triageResults = await performTriage(allSuggestions, confidenceGate, config.localEndpoint);
if (!triageResults) return;
const autoApprove = triageResults.filter((r) => r.action === "work" && r.confidence >= confidenceGate);
const needsReview = triageResults.filter((r) => r.action === "review" || r.action === "work" && r.confidence < confidenceGate && r.confidence >= confidenceGate - 15);
const dismissed = triageResults.filter((r) => r.action === "dismiss" || r.confidence < confidenceGate - 15);
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"));
console.log(BORDER(" \u2551") + AMBER(" \u25C6 TRIAGE COMPLETE"));
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"));
console.log(BORDER(" \u2551") + GREEN(` \u2705 Auto-approve: ${autoApprove.length} items (confidence \u2265 ${confidenceGate}%)`));
if (needsReview.length > 0) {
console.log(BORDER(" \u2551") + AMBER(` \u{1F914} Needs review: ${needsReview.length} items`));
}
console.log(BORDER(" \u2551") + GRAY(` \u23F8\uFE0F Dismissed: ${dismissed.length} items`));
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"));
console.log("");
if (autoApprove.length > 0) {
console.log(GREEN(" \u2705 APPROVE (auto-fix these):"));
for (let i = 0; i < autoApprove.length; i++) {
const item = autoApprove[i];
console.log(GRAY(` ${i + 1}. ${item.suggestion.filePath} \u2014 ${item.suggestion.title || item.suggestion.description?.slice(0, 40) || "No title"} (${item.confidence}%)`));
}
console.log("");
}
if (needsReview.length > 0) {
console.log(AMBER(" \u{1F914} REVIEW (Bob wants your input):"));
for (let i = 0; i < needsReview.length; i++) {
const item = needsReview[i];
console.log(GRAY(` ${i + 1}. ${item.suggestion.filePath} \u2014 ${item.reason} (${item.confidence}%)`));
}
console.log("");
}
for (const item of dismissed) {
const suggestionIndex = parseInt(item.suggestion.id?.split("_").pop() || "0");
const category = detectCategory(item.suggestion);
markSuggestionStatus(item.suggestion.filePath, suggestionIndex, category, "dismissed", {
confidence: item.confidence,
reason: item.reason,
implementedBy: "bob-triage"
});
}
let workQueue = [];
if (isAutoMode) {
workQueue = autoApprove.map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" }));
console.log(GRAY(" [Auto mode] Proceeding without approval prompt."));
} else {
const { choice } = await inquirer.prompt([{
type: "select",
name: "choice",
message: AMBER("How would you like to proceed?"),
choices: [
{ name: GREEN(` \u2705 Auto-fix approved items only (${autoApprove.length} items)`), value: "approved_only" },
{ name: GREEN(` \u2705 Auto-fix ALL including review items (${autoApprove.length + needsReview.length} items)`), value: "all" },
{ name: BLUE(" \u{1F5E3}\uFE0F Talk to Bob about these suggestions"), value: "talk" },
{ name: GRAY(" \u2190 Cancel"), value: "cancel" }
]
}]);
if (choice === "cancel") {
console.log(GRAY(" Cancelled."));
return;
}
if (choice === "talk") {
const updatedQueue = await talkToBobAboutSuggestions(autoApprove, needsReview, dismissed, config.localEndpoint);
if (updatedQueue.length === 0) {
console.log(GRAY(" No items to implement."));
return;
}
workQueue = updatedQueue;
} else if (choice === "approved_only") {
workQueue = autoApprove.map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" }));
} else {
workQueue = [...autoApprove, ...needsReview].map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" }));
}
}
if (workQueue.length === 0) {
console.log(chalk.yellow(" \u26A0\uFE0F Nothing to implement."));
return;
}
console.log("");
console.log(AMBER(" \u{1F527} Phase 3: MiniBob Implementing..."));
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"));
console.log(GRAY(" \u{1F4AC} /skip <file> to skip. /done to stop early."));
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"));
console.log("");
await executeWithChat(workQueue, config);
const fixed = workQueue.filter((t) => t.status === "done");
const failed = workQueue.filter((t) => t.status === "failed");
const skipped = workQueue.filter((t) => t.status === "skipped");
console.log("");
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"));
console.log(BORDER(" \u2551") + AMBER(" \u25C6 MINIBOB AUTO-FIX REPORT"));
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"));
console.log(BORDER(" \u2551") + GREEN(` \u2705 Fixed: ${fixed.length} items`));
console.log(BORDER(" \u2551") + GRAY(` \u23F8\uFE0F Held: ${dismissed.length + skipped.length} items`));
if (failed.length > 0) {
console.log(BORDER(" \u2551") + RED(` \u274C Failed: ${failed.length} items`));
}
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"));
if (fixed.length > 0) {
console.log(BORDER(" \u2551") + GRAY(" Fixed files:"));
for (const item of fixed) {
console.log(BORDER(" \u2551") + GREEN(` \u2705 ${item.suggestion.filePath}`));
}
}
if (failed.length > 0) {
console.log(BORDER(" \u2551") + GRAY(" Failed:"));
for (const item of failed) {
console.log(BORDER(" \u2551") + RED(` \u274C ${item.suggestion.filePath}`));
}
}
if (skipped.length > 0) {
console.log(BORDER(" \u2551") + GRAY(" Skipped:"));
for (const item of skipped) {
console.log(BORDER(" \u2551") + GRAY(` \u23F8\uFE0F ${item.suggestion.filePath}`));
}
}
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"));
console.log("");
console.log(GRAY(" \u{1F4E6} All original files backed up to .bob-backups/"));
console.log(GRAY(' Run `bob push "MiniBob auto-fix batch"` to commit changes.'));
console.log("");
}
async function performTriage(suggestions, confidenceGate, endpoint) {
const triagePrompt = `You are the Lead QA Engineer triaging code suggestions for auto-implementation by MiniBob (a junior engineer).
For each suggestion, decide: WORK, REVIEW, or DISMISS.
DECISION CRITERIA:
- WORK: The fix is clear, specific, well-defined, and you are CONFIDENT it will not break anything. MiniBob can implement it without supervision.
- REVIEW: The fix is good but has side effects, touches shared logic, or behavioral changes that need human approval first.
- DISMISS: The suggestion is vague, risky, poorly defined, or the effort/risk outweighs the benefit.
CONFIDENCE SCORING \u2014 Your confidence represents:
"How certain am I that this fix will NOT break anything AND will ACTUALLY contribute positively to the project?"
- 95-100%: Fix is 1-5 lines, explicit instructions, zero side effects, purely additive improvement
- 85-94%: Clear fix, well-scoped, touches isolated logic, minimal risk
- 75-84%: Good fix but touches shared modules or has minor behavioral implications
- <75%: Requires judgment, structural changes, or has unpredictable side effects
SUGGESTIONS:
${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")}
Respond with ONLY a JSON array:
[{"index": 0, "action": "work"|"review"|"dismiss", "confidence": 0-100, "reason": "brief reason for this confidence level"}]`;
try {
const messages = [
{ role: "system", content: "You are a senior engineering lead. Respond with ONLY a valid JSON array." },
{ role: "user", content: triagePrompt }
];
const response = await callLocalModel(endpoint, messages);
const jsonMatch = response.match(/\[[\s\S]*\]/);
if (!jsonMatch) {
console.log(chalk.red(" \u274C Triage failed: Could not parse."));
return null;
}
const parsed = JSON.parse(jsonMatch[0]);
return parsed.map((d) => ({
action: d.action === "work" ? "work" : d.action === "review" ? "review" : "dismiss",
confidence: d.confidence || 0,
reason: d.reason || "",
suggestion: suggestions[d.index]
})).filter((r) => r.suggestion);
} catch (error) {
console.log(chalk.red(` \u274C Triage failed: ${error.message}`));
return null;
}
}
async function talkToBobAboutSuggestions(approved, review, dismissed, endpoint) {
console.log("");
console.log(BLUE(" \u{1F5E3}\uFE0F Chat with Bob about the suggestions"));
console.log(GRAY(" Commands: skip <file>, add <file>, /done"));
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"));
console.log("");
const history = [
{ role: "system", content: `You are Bob, helping decide which suggestions to implement. Be concise.
APPROVED: ${approved.map((r) => `${r.suggestion.filePath}: ${r.suggestion.title || r.suggestion.description}`).join("\n")}
REVIEW: ${review.map((r) => `${r.suggestion.filePath}: ${r.reason}`).join("\n")}
DISMISSED: ${dismissed.map((r) => `${r.suggestion.filePath}: ${r.reason}`).join("\n")}` }
];
let finalApproved = [...approved];
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
const prompt = () => {
rl.question(chalk.green(" You: "), async (input) => {
const trimmed = input.trim();
if (!trimmed) {
prompt();
return;
}
if (trimmed === "/done") {
rl.close();
resolve(finalApproved.map((r) => ({ suggestion: r.suggestion, confidence: r.confidence, reason: r.reason, status: "pending" })));
return;
}
if (trimmed.toLowerCase().startsWith("skip ") || trimmed.toLowerCase().startsWith("remove ")) {
const target = trimmed.slice(trimmed.indexOf(" ") + 1).trim().toLowerCase();
const before = finalApproved.length;
finalApproved = finalApproved.filter((r) => !r.suggestion.filePath.toLowerCase().includes(target));
console.log(before > finalApproved.length ? chalk.yellow(` \u23F8\uFE0F Removed ${before - finalApproved.length} item(s)`) : GRAY(` No match for "${target}"`));
prompt();
return;
}
if (trimmed.toLowerCase().startsWith("add ")) {
const target = trimmed.slice(4).trim().toLowerCase();
const toAdd = [...review, ...dismissed].filter((r) => r.suggestion.filePath.toLowerCase().includes(target));
if (toAdd.length > 0) {
finalApproved.push(...toAdd);
console.log(chalk.green(` \u2705 Added ${toAdd.length} item(s)`));
} else {
console.log(GRAY(` No match for "${target}"`));
}
prompt();
return;
}
history.push({ role: "user", content: trimmed });
try {
const response = await callLocalModel(endpoint, history);
history.push({ role: "assistant", content: response });
console.log(chalk.bold.cyan(" \u{1F916} Bob: ") + response.split("\n")[0]);
if (response.split("\n").length > 1) {
response.split("\n").slice(1).forEach((l) => console.log(` ${l}`));
}
console.log("");
} catch {
}
prompt();
});
};
prompt();
});
}
async function executeWithChat(workQueue, config) {
renderTodoList(workQueue);
let userMessages = [];
let chatActive = true;
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const inputPromise = new Promise((resolve) => {
const askForInput = () => {
if (!chatActive) {
resolve();
return;
}
rl.question(chalk.gray(" \u{1F4AC} "), (input) => {
const trimmed = input.trim();
if (trimmed === "/done") {
for (const task of workQueue) {
if (task.status === "pending") task.status = "skipped";
}
chatActive = false;
resolve();
return;
}
if (trimmed.startsWith("/skip ")) {
const target = trimmed.slice(6).trim().toLowerCase();
for (const task of workQueue) {
if (task.status === "pending" && task.suggestion.filePath.toLowerCase().includes(target)) {
task.status = "skipped";
console.log(chalk.yellow(` \u23F8\uFE0F Skipping: ${task.suggestion.filePath}`));
}
}
} else if (trimmed) {
userMessages.push(trimmed);
}
if (chatActive) askForInput();
else resolve();
});
};
askForInput();
});
for (let i = 0; i < workQueue.length; i++) {
const task = workQueue[i];
if (task.status === "skipped") continue;
task.status = "working";
renderTodoList(workQueue);
if (userMessages.length > 0) {
const userMsg = userMessages.shift();
try {
const bobResponse = await callLocalModel(config.localEndpoint, [
{ role: "system", content: `You are Bob supervising MiniBob. Respond in 1-2 sentences. Current task: ${task.suggestion.filePath}` },
{ role: "user", content: userMsg }
]);
console.log(chalk.bold.cyan(` \u{1F916} Bob: `) + bobResponse.split("\n")[0]);
console.log("");
} catch {
}
}
const success = await implementTask(task, config.localEndpoint);
task.status = success ? "done" : "failed";
if (success) {
const suggestionIndex = parseInt(task.suggestion.id?.split("_").pop() || "0");
const category = detectCategory(task.suggestion);
markSuggestionStatus(task.suggestion.filePath, suggestionIndex, category, "implemented", {
confidence: task.confidence,
reason: task.reason,
implementedBy: "minibob-auto"
});
}
renderTodoList(workQueue);
}
chatActive = false;
rl.close();
await Promise.race([inputPromise, new Promise((resolve) => setTimeout(resolve, 100))]);
}
async function implementTask(task, endpoint) {
const suggestion = task.suggestion;
const fileContent = readFileContent(suggestion.filePath);
if (!fileContent) return false;
const prompt = `You are MiniBob \u2014 a junior engineer making SURGICAL code fixes under strict supervision.
CURRENT FILE: ${suggestion.filePath}
${fileContent}
CHANGE TO IMPLEMENT:
Title: ${suggestion.title || "Fix"}
Description: ${suggestion.description}
Implementation Instructions: ${suggestion.implementation || "Apply the fix described above."}
RULES (CRITICAL \u2014 VIOLATION = REJECTED):
- Return ONLY valid source code. No markdown, no code fences, no \`\`\`, no explanation text.
- Start the FIRST line with: // File: ${suggestion.filePath}
- PRESERVE ALL existing imports exactly as they are. Do NOT add, remove, or reorder imports.
- PRESERVE ALL existing exports exactly as they are. Do NOT rename exported functions or classes.
- PRESERVE the existing code structure, indentation, patterns, and naming conventions.
- Make the MINIMUM change necessary to implement the fix. Touch NOTHING else.
- Do NOT refactor, reorganize, or "improve" unrelated code.
- Do NOT add comments explaining what you changed.
- Do NOT wrap the response in markdown code blocks.
- The output must be valid TypeScript/JavaScript that compiles without errors.
- If you are unsure about a change, return the file UNCHANGED rather than risk breaking it.
Return the complete file content now:`;
try {
const messages = [
{ 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." },
{ role: "user", content: prompt }
];
const response = await callLocalModel(endpoint, messages);
const lines = response.split("\n");
const firstLine = lines[0].trim();
let newContent;
if (firstLine.match(/^\/\/\s*(File:)?\s*/)) {
newContent = lines.slice(1).join("\n").trim();
} else {
newContent = response.trim();
}
if (newContent.includes("```") || newContent.includes("## ") || newContent.startsWith("Here") || newContent.startsWith("I have") || newContent.startsWith("Sure")) {
console.log(chalk.yellow(` \u26A0\uFE0F MiniBob returned explanation instead of code. Skipping ${suggestion.filePath}.`));
return false;
}
if (newContent.length < fileContent.length * 0.5) {
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.`));
return false;
}
const originalExports = fileContent.match(/export\s+(function|class|const|interface|type|async\s+function)\s+\w+/g) || [];
for (const exp of originalExports) {
const exportName = exp.split(/\s+/).pop();
if (!newContent.includes(exportName)) {
console.log(chalk.yellow(` \u26A0\uFE0F MiniBob removed export "${exportName}". Rejecting change to ${suggestion.filePath}.`));
return false;
}
}
const originalImportCount = (fileContent.match(/^import\s+/gm) || []).length;
const newImportCount = (newContent.match(/^import\s+/gm) || []).length;
if (Math.abs(originalImportCount - newImportCount) > 2) {
console.log(chalk.yellow(` \u26A0\uFE0F MiniBob changed import count from ${originalImportCount} to ${newImportCount}. Rejecting.`));
return false;
}
const absolutePath = path.join(process.cwd(), suggestion.filePath);
const backupDir = path.join(process.cwd(), ".bob-backups");
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
if (fs.existsSync(absolutePath)) {
const timestamp = Date.now();
const backupName = suggestion.filePath.replace(/[\/\\]/g, "_") + `.${timestamp}.bak`;
fs.copyFileSync(absolutePath, path.join(backupDir, backupName));
}
const dir = path.dirname(absolutePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(absolutePath, newContent, "utf-8");
return true;
} catch {
return false;
}
}
function detectCategory(suggestion) {
const cwd = process.cwd();
const projectName = path.basename(cwd);
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
const analysisPath = path.join(homeDir, ".bob", "projects", projectName, "analysis", "results", "analysis.json");
if (!fs.existsSync(analysisPath)) return "bugs";
const allResults = JSON.parse(fs.readFileSync(analysisPath, "utf-8"));
const fileResults = allResults[suggestion.filePath];
if (!fileResults) return "bugs";
for (const cat of ["bugs", "features", "improvements", "upgrades"]) {
const items = fileResults[cat] || [];
for (const item of items) {
if (item.title === suggestion.title && item.description === suggestion.description) return cat;
}
}
return "bugs";
}
var lastTodoLines = 0;
function renderTodoList(queue) {
const lines = [];
lines.push("");
lines.push(AMBER(" \u{1F4CB} MiniBob Work Queue"));
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"));
for (let i = 0; i < queue.length; i++) {
const task = queue[i];
const label = task.suggestion.title || task.suggestion.description?.slice(0, 40) || "No title";
let icon;
let color;
switch (task.status) {
case "done":
icon = "\u2611";
color = GREEN;
break;
case "working":
icon = "\u23F3";
color = AMBER;
break;
case "failed":
icon = "\u2717";
color = RED;
break;
case "skipped":
icon = "\u23F8\uFE0F";
color = GRAY;
break;
default:
icon = "\u2610";
color = GRAY;
}
lines.push(color(` ${icon} [${i + 1}/${queue.length}] ${task.suggestion.filePath}`));
lines.push(color(` ${label} (${task.confidence}%)`));
}
const completed = queue.filter((t) => t.status === "done" || t.status === "failed" || t.status === "skipped").length;
const total = queue.length;
const percent = total > 0 ? completed / total : 0;
const barLen = 30;
const filled = Math.round(percent * barLen);
let barColor;
if (percent < 0.25) barColor = chalk.red;
else if (percent < 0.5) barColor = chalk.hex("#FF8C00");
else if (percent < 0.75) barColor = chalk.yellow;
else barColor = chalk.green;
lines.push("");
lines.push(` [${barColor("\u2588".repeat(filled))}${GRAY("\u2591".repeat(barLen - filled))}] ${completed}/${total} ${barColor(Math.round(percent * 100) + "%")}`);
lines.push("");
if (lastTodoLines > 0) {
process.stdout.write(`\x1B[${lastTodoLines}A`);
for (let i = 0; i < lastTodoLines; i++) {
process.stdout.write("\x1B[2K\n");
}
process.stdout.write(`\x1B[${lastTodoLines}A`);
}
for (const line of lines) {
process.stdout.write(line + "\n");
}
lastTodoLines = lines.length;
}
export {
runAutoFix
};
import {
loadLocalSuggestions,
showInteractiveResults
} from "./chunk-SADPOL7M.js";
export {
loadLocalSuggestions,
showInteractiveResults
};
// src/commands/analyse-results.ts
import chalk3 from "chalk";
import inquirer from "inquirer";
import * as fs4 from "fs";
import * as path4 from "path";
// src/core/api-client.ts
import axios2 from "axios";
// src/core/config-store.ts
import Conf from "conf";
// src/types/config.ts
var DEFAULT_CONFIG = {
tier: "local",
loggedIn: false,
email: null,
uid: null,
authToken: null,
refreshToken: null,
provider: null,
providerKey: null,
localEndpoint: null,
personalizationMode: false,
consultantMode: false,
autoMode: false,
idrp: false,
idrpFilter: "free",
activeProject: null,
conversationId: null,
activePersona: null,
hasSeenWelcome: false
};
// src/core/config-store.ts
var store = new Conf({
projectName: "bob-cli",
defaults: DEFAULT_CONFIG
});
function getConfig() {
return {
tier: store.get("tier"),
loggedIn: store.get("loggedIn"),
email: store.get("email"),
uid: store.get("uid"),
authToken: store.get("authToken"),
refreshToken: store.get("refreshToken"),
provider: store.get("provider"),
providerKey: store.get("providerKey"),
localEndpoint: store.get("localEndpoint"),
personalizationMode: store.get("personalizationMode"),
consultantMode: store.get("consultantMode"),
idrp: store.get("idrp"),
idrpFilter: store.get("idrpFilter"),
activeProject: store.get("activeProject"),
conversationId: store.get("conversationId"),
activePersona: store.get("activePersona"),
hasSeenWelcome: store.get("hasSeenWelcome"),
autoMode: store.get("autoMode")
};
}
function setConfigValue(key, value) {
store.set(key, value);
}
function getConfigPath() {
return store.path;
}
// src/commands/login.ts
import chalk from "chalk";
import http from "http";
import open from "open";
import axios from "axios";
import { URL } from "url";
import * as readline from "readline";
var CLI_AUTH_URL = "https://bobs-workshop.web.app/cli-auth";
var CALLBACK_PORT = 9876;
var FIREBASE_API_KEY = "AIzaSyB-hUZEonRIzbExVDwuneJaDjJZBvHdIps";
function registerLoginCommand(program) {
program.command("login").description("Authenticate with Bob's Workshop via browser").action(async () => {
console.log("");
console.log(chalk.bold.cyan(" \u{1F510} Bob CLI \u2014 Login"));
console.log(chalk.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"));
console.log("");
console.log(chalk.yellow(" \u26A0\uFE0F Important:"));
console.log(chalk.gray(" \u2022 Local conversations (Tier 1) will NOT sync to the platform."));
console.log(chalk.gray(" \u2022 Only NEW conversations created after login will save to Firebase."));
console.log(chalk.gray(" \u2022 Your local history stays in ~/.bob/projects/ (backup via `bob backup`)."));
console.log(chalk.gray(" \u2022 Logging in upgrades you to Tier 3 (Platform) with full features."));
console.log("");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve3) => {
rl.question(chalk.cyan(" Continue with login? (y/n): "), resolve3);
});
rl.close();
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
console.log("");
console.log(chalk.gray(" Login cancelled."));
console.log("");
return;
}
console.log("");
console.log(chalk.gray(" Opening browser for authentication..."));
console.log("");
try {
const result = await startAuthFlow();
if (result) {
const exchangeResult = await exchangeCustomToken(result.token);
setConfigValue("authToken", exchangeResult.idToken);
setConfigValue("refreshToken", exchangeResult.refreshToken);
setConfigValue("email", result.email);
setConfigValue("uid", result.uid);
setConfigValue("loggedIn", true);
setConfigValue("tier", "platform");
console.log("");
console.log(chalk.green(` \u2705 Logged in as ${result.email}`));
console.log(chalk.gray(" Tier: Platform (Tier 3)"));
console.log(chalk.gray(" All platform features are now available."));
console.log("");
}
} catch (error) {
console.log(chalk.red(` \u274C Login failed: ${error.message}`));
console.log("");
}
});
program.command("logout").description("Sign out and clear stored credentials").action(() => {
setConfigValue("authToken", null);
setConfigValue("refreshToken", null);
setConfigValue("email", null);
setConfigValue("uid", null);
setConfigValue("loggedIn", false);
setConfigValue("tier", "local");
console.log("");
console.log(chalk.gray(" \u{1F44B} Logged out. Switched to Tier 1 (local-first)."));
console.log("");
});
}
async function exchangeCustomToken(customToken) {
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${FIREBASE_API_KEY}`;
const response = await axios.post(url, {
token: customToken,
returnSecureToken: true
});
if (!response.data?.idToken || !response.data?.refreshToken) {
throw new Error("Token exchange failed \u2014 no ID token returned.");
}
return {
idToken: response.data.idToken,
refreshToken: response.data.refreshToken
};
}
async function refreshAuthToken(refreshToken) {
const url = `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`;
const response = await axios.post(url, {
grant_type: "refresh_token",
refresh_token: refreshToken
});
if (!response.data?.id_token) {
throw new Error("Token refresh failed.");
}
setConfigValue("authToken", response.data.id_token);
return response.data.id_token;
}
function startAuthFlow() {
return new Promise((resolve3, reject) => {
const timeout = setTimeout(() => {
server.close();
reject(new Error("Login timed out after 120 seconds. Please try again."));
}, 12e4);
const server = http.createServer((req, res) => {
if (!req.url?.startsWith("/callback")) {
res.writeHead(404);
res.end("Not found");
return;
}
try {
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
const token = url.searchParams.get("token");
const email = url.searchParams.get("email");
const uid = url.searchParams.get("uid");
if (!token || !email || !uid) {
res.writeHead(400);
res.end("Missing parameters");
reject(new Error("Invalid callback \u2014 missing token, email, or uid."));
return;
}
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
<html>
<body style="background: #0a0a0a; color: white; font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
<div style="text-align: center;">
<h1>\u2705 Authenticated!</h1>
<p style="color: #888;">You can close this tab and return to your terminal.</p>
</div>
</body>
</html>
`);
clearTimeout(timeout);
server.close();
resolve3({ token, email, uid });
} catch (e) {
res.writeHead(500);
res.end("Error");
reject(e);
}
});
server.listen(CALLBACK_PORT, () => {
console.log(chalk.gray(` \u{1F310} Waiting for authentication (port ${CALLBACK_PORT})...`));
console.log(chalk.gray(" If your browser doesn't open, visit:"));
console.log(chalk.cyan(` ${CLI_AUTH_URL}`));
console.log("");
open(CLI_AUTH_URL).catch(() => {
});
});
server.on("error", (err) => {
clearTimeout(timeout);
if (err.code === "EADDRINUSE") {
reject(new Error("Port 9876 is already in use. Close other instances and try again."));
} else {
reject(err);
}
});
});
}
// src/core/api-client.ts
var FUNCTIONS_BASE = "https://us-central1-seedlingapp.cloudfunctions.net";
async function callCloudFunction(functionName, data) {
const config = getConfig();
if (!config.authToken) {
throw new Error("Not authenticated. Run `bob login` first.");
}
try {
const response = await axios2.post(
`${FUNCTIONS_BASE}/${functionName}`,
{ data },
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${config.authToken}`
},
timeout: 18e4
}
);
return response.data?.result || response.data;
} catch (error) {
const status = error.response?.status;
if (status === 401 && config.refreshToken) {
try {
const newToken = await refreshAuthToken(config.refreshToken);
const retryResponse = await axios2.post(
`${FUNCTIONS_BASE}/${functionName}`,
{ data },
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${newToken}`
},
timeout: 18e4
}
);
return retryResponse.data?.result || retryResponse.data;
} catch (refreshError) {
setConfigValue("loggedIn", false);
throw new Error("Session expired. Run `bob login` again.");
}
}
if (status === 404) {
throw new Error(`Function "${functionName}" not found. Is it deployed?`);
}
if (status === 403) {
throw new Error("Permission denied. You may not have access to this feature.");
}
if (status === 500) {
const serverMsg = error.response?.data?.error?.message || error.response?.data?.error || "Internal server error";
throw new Error(`Server error: ${serverMsg}`);
}
if (status === 429) {
throw new Error("Rate limited. Please wait a moment and try again.");
}
const errorMsg = error.response?.data?.error?.message || error.message || `Request failed with status ${status}`;
throw new Error(errorMsg);
}
}
async function callHTTPFunction(functionName, data) {
const config = getConfig();
if (!config.authToken) {
throw new Error("Not authenticated. Run `bob login` first.");
}
try {
const response = await axios2.post(
`${FUNCTIONS_BASE}/${functionName}`,
{ data },
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${config.authToken}`
},
timeout: 3e5
}
);
return response.data?.data || response.data;
} catch (error) {
const status = error.response?.status;
if (status === 401 && config.refreshToken) {
try {
const newToken = await refreshAuthToken(config.refreshToken);
const retryResponse = await axios2.post(
`${FUNCTIONS_BASE}/${functionName}`,
{ data },
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${newToken}`
},
timeout: 3e5
}
);
return retryResponse.data?.data || retryResponse.data;
} catch (refreshError) {
setConfigValue("loggedIn", false);
throw new Error("Session expired. Run `bob login` again.");
}
}
if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT") {
throw new Error("Connection was reset. The function may still be running \u2014 check the web app for the response.");
}
if (status === 404) {
throw new Error(`Function "${functionName}" not found. Is it deployed?`);
}
if (status === 403) {
throw new Error("Permission denied. You may not have access to this feature.");
}
if (status === 500) {
const serverMsg = error.response?.data?.error?.message || error.response?.data?.error || "Internal server error";
throw new Error(`Server error: ${serverMsg}`);
}
if (status === 429) {
throw new Error("Rate limited. Please wait a moment and try again.");
}
const errorMsg = error.response?.data?.error?.message || error.message || `Request failed with status ${status}`;
throw new Error(errorMsg);
}
}
function isAuthenticated() {
const config = getConfig();
return !!(config.loggedIn && config.authToken);
}
// src/ai/providers/local.ts
import axios3 from "axios";
async function callLocalModel(endpoint, messages) {
try {
const response = await axios3.post(
endpoint,
{
model: "bob-local-dna:latest",
messages,
stream: false
},
{
headers: { "Content-Type": "application/json" },
timeout: 18e4
}
);
if (response.data?.message?.content) {
return {
text: response.data.message.content,
evalCount: response.data.eval_count || void 0,
promptEvalCount: response.data.prompt_eval_count || void 0,
evalDurationMs: response.data.eval_duration ? Math.round(response.data.eval_duration / 1e6) : void 0,
totalDurationMs: response.data.total_duration ? Math.round(response.data.total_duration / 1e6) : void 0
};
}
const choice = response.data?.choices?.[0];
if (choice?.message?.content) {
return {
text: choice.message.content,
evalCount: response.data.usage?.completion_tokens || void 0,
promptEvalCount: response.data.usage?.prompt_tokens || void 0
};
}
if (typeof response.data?.response === "string") {
return { text: response.data.response };
}
return { text: "No response received from local model." };
} catch (error) {
if (error.code === "ECONNREFUSED") {
throw new Error("Cannot connect to local model. Is Ollama running? Check your endpoint: " + endpoint);
}
throw new Error("Local model error: " + (error.response?.status ? `Status ${error.response.status}` : error.message));
}
}
// src/core/context-builder.ts
import * as fs from "fs";
import * as path from "path";
var IGNORE_DIRS = ["node_modules", ".git", "dist", "build", ".dart_tool", ".idea", ".gradle", ".pub-cache", ".bob"];
var MAX_DEPTH = 3;
function buildLocalContext(rootDir) {
const tree = getDirectoryTree(rootDir, 0);
return `Working Directory: ${rootDir}
File Tree:
${tree}`;
}
function getDirectoryTree(dir, depth) {
if (depth >= MAX_DEPTH) return "";
let result = "";
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (IGNORE_DIRS.includes(entry.name)) continue;
if (entry.name.startsWith(".") && depth === 0) continue;
const indent = " ".repeat(depth);
if (entry.isDirectory()) {
result += `${indent}${entry.name}/
`;
result += getDirectoryTree(path.join(dir, entry.name), depth + 1);
} else {
result += `${indent}${entry.name}
`;
}
}
} catch (e) {
}
return result;
}
function readFileContent(filePath) {
try {
return fs.readFileSync(path.resolve(filePath), "utf-8");
} catch (e) {
return null;
}
}
// src/core/file-writer.ts
import * as fs2 from "fs";
import * as path2 from "path";
import * as readline2 from "readline";
import chalk2 from "chalk";
var SUCCESS = chalk2.hex("#66BB6A");
var INFO = chalk2.hex("#26C6DA");
var WARNING = chalk2.hex("#FFC107");
var ERROR = chalk2.hex("#EF5350");
var MUTED = chalk2.hex("#78909C");
var BRAND_SECONDARY = chalk2.hex("#FFAB00");
var BORDER = chalk2.hex("#455A64");
function extractAllProposedFiles(response) {
const proposals = [];
const codeBlockRegex = /```[\w]*\n([\s\S]*?)```/g;
let match;
while ((match = codeBlockRegex.exec(response)) !== null) {
const codeContent = match[1].trim();
const lines = codeContent.split("\n");
if (lines.length === 0) continue;
const firstLine = lines[0].trim();
let filePathMatch = firstLine.match(/^\/\/\s*File:\s*(.+)$/);
if (!filePathMatch) filePathMatch = firstLine.match(/^\/\/\s*([\w\-\.\/\\]+\.\w+)\s*$/);
if (!filePathMatch) filePathMatch = firstLine.match(/^#\s*File:\s*(.+)$/);
if (!filePathMatch) filePathMatch = firstLine.match(/^#\s*([\w\-\.\/\\]+\.\w+)\s*$/);
if (!filePathMatch) filePathMatch = firstLine.match(/^\*\s*\[FILE:\s*(.+?)\]/);
if (!filePathMatch) continue;
const filePath = filePathMatch[1].trim();
if (!filePath.includes("/") && !filePath.includes("\\")) continue;
if (!filePath.includes(".")) continue;
const fileContent = lines.slice(1).join("\n").trim();
const isLocal = isLocalProjectFile(filePath);
const absolutePath = path2.join(process.cwd(), filePath);
const isNew = !fs2.existsSync(absolutePath);
proposals.push({ filePath, content: fileContent, isNew, isLocal });
}
return proposals;
}
function extractProposedFile(response) {
const all = extractAllProposedFiles(response);
return all.length > 0 ? all[0] : null;
}
function stripCodeBlockFromResponse(response) {
let stripped = response.replace(/```[\w]*\n\s*(?:\/\/\s*(?:File:)?\s*[\w\-\.\/\\]+\.\w+|#\s*(?:File:)?\s*[\w\-\.\/\\]+\.\w+|\*\s*\[FILE:)[^\n]*\n[\s\S]*?```/g, "").trim();
stripped = stripped.replace(/```capability_invocation\s*[\s\S]*?```/g, "").trim();
return stripped;
}
function isLocalProjectFile(filePath) {
const cwd = process.cwd();
const externalPatterns = ["functions/", "lib/", "android/", "ios/", "macos/", "windows/", "web/"];
for (const pattern of externalPatterns) {
if (filePath.startsWith(pattern)) {
const localPath = path2.join(cwd, pattern.replace("/", ""));
if (!fs2.existsSync(localPath)) return false;
}
}
const resolved = path2.resolve(cwd, filePath);
if (!resolved.startsWith(cwd)) return false;
return true;
}
async function processAllProposedFiles(response, autoApprove = false, existingRl) {
const proposals = extractAllProposedFiles(response);
const idrpProposals = extractIDRPFileProposals(response);
const allProposals = [...proposals, ...idrpProposals];
if (allProposals.length === 0) return;
for (const proposed of allProposals) {
if (proposed.isLocal) {
await proposeAndWriteFile(proposed, autoApprove, existingRl);
} else {
displayExternalFile(proposed);
}
}
}
function displayExternalFile(proposed) {
const totalLines = proposed.content.split("\n").length;
console.log("");
console.log(WARNING(` \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`));
console.log(WARNING(` \u2551`) + BRAND_SECONDARY(` \u{1F4CB} EXTERNAL: ${proposed.filePath}`));
console.log(WARNING(` \u2551`) + MUTED(` This file belongs to another project.`));
console.log(WARNING(` \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`));
const previewLines = proposed.content.split("\n").slice(0, 6);
for (const line of previewLines) {
console.log(WARNING(` \u2551`) + MUTED(` ${line}`));
}
if (totalLines > 6) {
console.log(WARNING(` \u2551`) + MUTED(` ... (${totalLines - 6} more lines)`));
}
console.log(WARNING(` \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`));
console.log(MUTED(` Copy this file manually to your project at: ${proposed.filePath}`));
console.log("");
}
async function proposeAndWriteFile(proposed, autoApprove = false, existingRl) {
if (!proposed.isLocal) {
displayExternalFile(proposed);
return false;
}
const absolutePath = path2.join(process.cwd(), proposed.filePath);
const action = proposed.isNew ? "CREATE" : "UPDATE";
const icon = proposed.isNew ? "\u{1F4C4}" : "\u270F\uFE0F";
const accentColor = proposed.isNew ? SUCCESS : BRAND_SECONDARY;
const totalLines = proposed.content.split("\n").length;
if (!autoApprove) {
console.log("");
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`));
console.log(BORDER(` \u2551`) + accentColor(` ${icon} ${action}: `) + chalk2.white(`${proposed.filePath}`) + MUTED(` (${totalLines} lines)`));
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`));
const previewLines = proposed.content.split("\n").slice(0, 6);
for (const line of previewLines) {
console.log(BORDER(` \u2551`) + MUTED(` ${line}`));
}
if (totalLines > 6) {
console.log(BORDER(` \u2551`) + MUTED(` ... (${totalLines - 6} more lines)`));
}
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`));
console.log("");
const promptText = INFO(` \u{1F4BE} ${action === "CREATE" ? "Write this file" : "Apply changes"}? `) + MUTED(`(y/n/path): `);
let answer;
if (existingRl) {
existingRl.pause();
process.stdout.write(promptText);
const buf = Buffer.alloc(1024);
const bytesRead = fs2.readSync(0, buf, 0, 1024, null);
answer = buf.toString("utf-8", 0, bytesRead).replace(/\r?\n/, "").trim();
existingRl.resume();
} else {
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
answer = await new Promise((resolve3) => {
rl.question(promptText, (ans) => {
rl.close();
resolve3(ans);
});
});
}
const trimmed = answer.trim().toLowerCase();
if (trimmed === "n" || trimmed === "no") {
console.log(MUTED(" \u23ED\uFE0F Skipped."));
return false;
}
if (trimmed !== "y" && trimmed !== "yes" && trimmed.length > 0) {
const customPath = path2.join(process.cwd(), trimmed);
return writeFile(customPath, proposed.content, proposed.filePath, proposed.isNew);
}
}
return writeFile(absolutePath, proposed.content, proposed.filePath, proposed.isNew);
}
function writeFile(targetPath, content, originalFilePath, isNew) {
try {
const dir = path2.dirname(targetPath);
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
if (!isNew && fs2.existsSync(targetPath)) {
const backupDir = path2.join(process.cwd(), ".bob-backups");
if (!fs2.existsSync(backupDir)) fs2.mkdirSync(backupDir, { recursive: true });
const timestamp = Date.now();
const backupName = originalFilePath.replace(/[\/\\]/g, "_") + `.${timestamp}.bak`;
fs2.copyFileSync(targetPath, path2.join(backupDir, backupName));
}
fs2.writeFileSync(targetPath, content, "utf-8");
const relativePath = path2.relative(process.cwd(), targetPath);
console.log(SUCCESS(` \u2705 Written: ${relativePath}`));
if (!isNew) {
console.log(MUTED(` \u{1F4E6} Backup saved to .bob-backups/`));
}
console.log("");
return true;
} catch (error) {
console.log(ERROR(` \u274C Write failed: ${error.message}`));
return false;
}
}
function extractIDRPFileProposals(response) {
const proposals = [];
const invocationRegex = /```capability_invocation\s*([\s\S]*?)```/g;
let match;
while ((match = invocationRegex.exec(response)) !== null) {
try {
const invocation = JSON.parse(match[1].trim());
if (invocation.action !== "invoke") continue;
if (!["workspace_create_file", "workspace_update_file"].includes(invocation.capabilityId)) continue;
const filePath = invocation.params?.filePath;
const content = invocation.params?.content || invocation.params?.newContent;
if (!filePath || !content) continue;
const absolutePath = path2.join(process.cwd(), filePath);
const isNew = !fs2.existsSync(absolutePath);
const isLocal = isLocalProjectFile(filePath);
proposals.push({ filePath, content, isNew, isLocal });
} catch {
}
}
return proposals;
}
// src/core/analysis-tracker.ts
import * as fs3 from "fs";
import * as path3 from "path";
var BOB_DIR = path3.join(process.env.HOME || process.env.USERPROFILE || "", ".bob");
function getResultsDir() {
const projectName = path3.basename(process.cwd());
return path3.join(BOB_DIR, "projects", projectName, "analysis", "results");
}
function getAnalysisPath() {
return path3.join(getResultsDir(), "analysis.json");
}
function getStatusLogPath() {
return path3.join(getResultsDir(), "status-log.json");
}
function markSuggestionStatus(filePath, suggestionIndex, category, status, metadata) {
const analysisPath = getAnalysisPath();
const logPath = getStatusLogPath();
if (!fs3.existsSync(analysisPath)) return;
const allResults = JSON.parse(fs3.readFileSync(analysisPath, "utf-8"));
if (allResults[filePath] && allResults[filePath][category]) {
const items = allResults[filePath][category];
if (items[suggestionIndex]) {
items[suggestionIndex].status = status;
items[suggestionIndex].statusUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
}
}
fs3.writeFileSync(analysisPath, JSON.stringify(allResults, null, 2));
let log = [];
if (fs3.existsSync(logPath)) {
try {
log = JSON.parse(fs3.readFileSync(logPath, "utf-8"));
} catch {
log = [];
}
}
log.push({
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
filePath,
category,
suggestionIndex,
action: status,
confidence: metadata?.confidence || null,
reason: metadata?.reason || null,
implementedBy: metadata?.implementedBy || "minibob",
previousStatus: "pending"
});
fs3.writeFileSync(logPath, JSON.stringify(log, null, 2));
}
function markSuggestionById(id, category, status, metadata) {
const analysisPath = getAnalysisPath();
if (!fs3.existsSync(analysisPath)) return;
const allResults = JSON.parse(fs3.readFileSync(analysisPath, "utf-8"));
for (const [filePath, fileResults] of Object.entries(allResults)) {
const items = fileResults[category];
if (!items) continue;
for (let i = 0; i < items.length; i++) {
const itemId = `${filePath.replace(/[\/\\]/g, "_")}_${i}`;
if (itemId === id) {
markSuggestionStatus(filePath, i, category, status, metadata);
return;
}
}
}
}
// src/commands/analyse-results.ts
var BRAND_PRIMARY = chalk3.hex("#E66F24");
var BRAND_SECONDARY2 = chalk3.hex("#FFAB00");
var SUCCESS2 = chalk3.hex("#66BB6A");
var INFO2 = chalk3.hex("#26C6DA");
var WARNING2 = chalk3.hex("#FFC107");
var ERROR2 = chalk3.hex("#EF5350");
var MUTED2 = chalk3.hex("#78909C");
var BORDER2 = chalk3.hex("#455A64");
var MODE_CONSULTANT = chalk3.hex("#AB47BC");
var PRIORITY_COLORS = {
"critical": chalk3.bgHex("#B71C1C").white,
"high": chalk3.hex("#FF6D00"),
"medium": chalk3.hex("#FFA726"),
"low": chalk3.hex("#66BB6A")
};
var CATEGORY_COLORS = {
"bugs": ERROR2,
"features": MODE_CONSULTANT,
"improvements": INFO2,
"upgrades": SUCCESS2
};
var CATEGORY_ICONS = {
"bugs": "\u{1F534}",
"features": "\u{1F7E3}",
"improvements": "\u{1F535}",
"upgrades": "\u{1F7E2}"
};
async function showInteractiveResults(config, category, sort, search) {
let allSuggestions = [];
if (config.tier === "platform" && config.provider !== "local" && config.loggedIn && config.conversationId) {
try {
const result = await callCloudFunction("getCLIAnalysisResults", {
conversationId: config.conversationId,
category,
sort: sort || "priority",
search: search || null
});
allSuggestions = result?.suggestions || [];
} catch (error) {
console.log(ERROR2(` \u274C ${error.message}`));
return;
}
} else {
allSuggestions = loadLocalSuggestions(category);
}
if (search) {
const query = search.toLowerCase();
allSuggestions = allSuggestions.filter(
(s) => (s.description || "").toLowerCase().includes(query) || (s.title || "").toLowerCase().includes(query) || (s.filePath || "").toLowerCase().includes(query)
);
}
sortSuggestions(allSuggestions, sort || "priority");
if (allSuggestions.length === 0) {
console.log("");
console.log(SUCCESS2(" \u2705 No items found. Clean!"));
console.log("");
return;
}
const color = CATEGORY_COLORS[category] || MUTED2;
const icon = CATEGORY_ICONS[category] || "\u25C6";
let running = true;
let displaySuggestions = [...allSuggestions];
let currentSort = sort || "priority";
while (running) {
console.log("");
console.log(color(` ${icon} ${category.toUpperCase()} (${displaySuggestions.length} items) \u2502 Sort: ${currentSort}`));
console.log(MUTED2(" \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
console.log("");
const choices = [];
choices.push({
name: INFO2(" \u{1F500} Toggle sort"),
value: "__sort__",
short: "Sort"
});
choices.push(new inquirer.Separator(MUTED2(" \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\u2500")));
for (let idx = 0; idx < displaySuggestions.length; idx++) {
const item = displaySuggestions[idx];
const pColor = PRIORITY_COLORS[item.priority?.toLowerCase()] || MUTED2;
const priorityLabel = pColor((item.priority || "MEDIUM").toUpperCase().padEnd(9));
const fileName = (item.filePath || "unknown").split("/").pop() || "unknown";
const title = (item.title || item.description || "No description").slice(0, 40);
const displayName = ` ${priorityLabel} ${INFO2(fileName.padEnd(20))} ${chalk3.white(title)}`;
choices.push({
name: displayName,
value: idx,
short: item.title || item.description?.slice(0, 30) || "Item",
description: `${item.priority} ${item.filePath} ${item.title} ${item.description}`
});
}
choices.push(new inquirer.Separator(MUTED2(" \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\u2500")));
choices.push({
name: MUTED2(" \u2190 Quit"),
value: "__quit__",
short: "Quit"
});
const { selected } = await inquirer.prompt([
{
type: "search",
name: "selected",
message: color(`Search ${category} (type to filter):`),
source: (input) => {
if (!input) return choices;
const query = input.toLowerCase();
const filtered = choices.filter((c) => {
if (c.type === "separator") return true;
if (c.value === "__sort__" || c.value === "__quit__") return true;
const searchable = c.description?.toLowerCase() || "";
return searchable.includes(query);
});
return filtered;
},
pageSize: 12
}
]);
if (selected === "__quit__") {
running = false;
break;
}
if (selected === "__sort__") {
currentSort = currentSort === "priority" ? "file" : "priority";
sortSuggestions(displaySuggestions, currentSort);
console.log(INFO2(` Sort changed to: ${currentSort}`));
continue;
}
if (typeof selected === "number") {
const item = displaySuggestions[selected];
const action = await showExpandedView(item, category);
if (action === "implement") {
await handleImplement(item, config, category);
displaySuggestions.splice(selected, 1);
const originalIdx = allSuggestions.findIndex((s) => s.id === item.id);
if (originalIdx !== -1) allSuggestions.splice(originalIdx, 1);
} else if (action === "dismiss") {
if (item.id) {
markSuggestionById(item.id, category, "dismissed", {
reason: "User dismissed from CLI",
implementedBy: "user"
});
}
displaySuggestions.splice(selected, 1);
const originalIdx = allSuggestions.findIndex((s) => s.id === item.id);
if (originalIdx !== -1) allSuggestions.splice(originalIdx, 1);
console.log(MUTED2(" \u23ED\uFE0F Dismissed and logged."));
}
}
}
}
async function showExpandedView(item, category) {
const color = CATEGORY_COLORS[category] || MUTED2;
const pColor = PRIORITY_COLORS[item.priority?.toLowerCase()] || MUTED2;
const icon = CATEGORY_ICONS[category] || "\u25C6";
console.log("");
console.log(BORDER2(" \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"));
console.log(BORDER2(" \u2551") + ` ${icon} ` + pColor(`${(item.priority || "MEDIUM").toUpperCase()} ${category.toUpperCase().slice(0, -1)}`));
console.log(BORDER2(" \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"));
console.log(BORDER2(" \u2551") + MUTED2(" File: ") + INFO2(item.filePath || "unknown"));
console.log(BORDER2(" \u2551") + MUTED2(" Priority: ") + pColor((item.priority || "medium").toUpperCase()));
console.log(BORDER2(" \u2551"));
console.log(BORDER2(" \u2551") + MUTED2(" Title:"));
console.log(BORDER2(" \u2551") + chalk3.white.bold(` ${item.title || "No title"}`));
console.log(BORDER2(" \u2551"));
console.log(BORDER2(" \u2551") + MUTED2(" Description:"));
const descLines = wrapText(item.description || "No description", 54);
for (const line of descLines) {
console.log(BORDER2(" \u2551") + chalk3.white(` ${line}`));
}
if (item.implementation) {
console.log(BORDER2(" \u2551"));
console.log(BORDER2(" \u2551") + MUTED2(" Implementation:"));
const implLines = wrapText(item.implementation, 54);
for (const line of implLines) {
console.log(BORDER2(" \u2551") + chalk3.white(` ${line}`));
}
}
console.log(BORDER2(" \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"));
console.log("");
const { action } = await inquirer.prompt([
{
type: "select",
name: "action",
message: BRAND_SECONDARY2("What do you want to do?"),
choices: [
{ name: SUCCESS2(" \u{1F527} Implement this fix"), value: "implement" },
{ name: ERROR2(" \u{1F5D1}\uFE0F Dismiss"), value: "dismiss" },
{ name: MUTED2(" \u2190 Back to list"), value: "back" }
]
}
]);
return action;
}
async function handleImplement(item, config, category) {
console.log("");
console.log(INFO2(" \u{1F527} Implementing fix..."));
console.log("");
if (config.provider === "local" && config.localEndpoint) {
const fileContent = readFileContent(item.filePath);
if (!fileContent) {
console.log(ERROR2(` \u274C Could not read file: ${item.filePath}`));
return;
}
const prompt = `You are MiniBob \u2014 a junior engineer making SURGICAL code fixes under strict supervision.
CURRENT FILE: ${item.filePath}
${fileContent}
CHANGE TO IMPLEMENT:
Title: ${item.title}
Description: ${item.description}
Implementation Instructions: ${item.implementation || "Apply the fix described above."}
RULES (CRITICAL \u2014 VIOLATION = REJECTED):
- Return ONLY valid source code. No markdown, no code fences, no \`\`\`, no explanation text.
- Start the FIRST line with: // File: ${item.filePath}
- PRESERVE ALL existing imports exactly as they are. Do NOT add, remove, or reorder imports.
- PRESERVE ALL existing exports exactly as they are. Do NOT rename exported functions or classes.
- PRESERVE the existing code structure, indentation, patterns, and naming conventions.
- Make the MINIMUM change necessary to implement the fix. Touch NOTHING else.
- Do NOT refactor, reorganize, or "improve" unrelated code.
- Do NOT add comments explaining what you changed.
- Do NOT wrap the response in markdown code blocks.
- The output must be valid TypeScript/JavaScript that compiles without errors.
- If you are unsure about a change, return the file UNCHANGED rather than risk breaking it.
Return the complete file content now:`;
try {
const messages = [
{ 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." },
{ role: "user", content: prompt }
];
const localResult = await callLocalModel(config.localEndpoint, messages);
const response = typeof localResult === "object" && localResult.text ? localResult.text : localResult;
const lines = response.split("\n");
const firstLine = lines[0].trim();
let newContent;
if (firstLine.match(/^\/\/\s*(File:)?\s*/)) {
newContent = lines.slice(1).join("\n").trim();
} else {
newContent = response.trim();
}
if (newContent.includes("```") || newContent.includes("## ") || newContent.startsWith("Here") || newContent.startsWith("I have") || newContent.startsWith("Sure")) {
console.log(WARNING2(" \u26A0\uFE0F MiniBob returned explanation instead of code. Fix rejected."));
return;
}
if (newContent.length < fileContent.length * 0.5) {
console.log(WARNING2(` \u26A0\uFE0F MiniBob's output is ${Math.round(newContent.length / fileContent.length * 100)}% of original size. Rejecting.`));
return;
}
const originalExports = fileContent.match(/export\s+(function|class|const|interface|type|async\s+function)\s+\w+/g) || [];
for (const exp of originalExports) {
const exportName = exp.split(/\s+/).pop();
if (!newContent.includes(exportName)) {
console.log(WARNING2(` \u26A0\uFE0F MiniBob removed export "${exportName}". Rejecting.`));
return;
}
}
await proposeAndWriteFile({
filePath: item.filePath,
content: newContent,
isNew: false,
isLocal: true
});
if (item.id) {
markSuggestionById(item.id, category, "implemented", {
reason: "User approved implementation from CLI",
implementedBy: "minibob"
});
}
} catch (error) {
console.log(ERROR2(` \u274C Implementation failed: ${error.message}`));
}
} else if (config.loggedIn && config.conversationId) {
try {
const result = await callCloudFunction("implementSuggestion", {
conversationId: config.conversationId,
filePath: item.filePath,
suggestionId: item.id || "unknown",
category,
jobId: `cli_impl_${Date.now()}`
});
if (result?.success) {
console.log(SUCCESS2(` \u2705 ${result.message}`));
if (item.id) {
markSuggestionById(item.id, category, "implemented", {
reason: "Platform implementation",
implementedBy: "platform"
});
}
} else {
console.log(ERROR2(" \u274C Implementation failed on platform."));
}
} catch (error) {
console.log(ERROR2(` \u274C ${error.message}`));
}
} else {
console.log(ERROR2(" \u274C No provider configured for implementation."));
}
console.log("");
}
function sortSuggestions(suggestions, method) {
if (method === "file") {
suggestions.sort((a, b) => (a.filePath || "").localeCompare(b.filePath || ""));
} else {
const priorityMap = { "critical": 0, "high": 1, "medium": 2, "low": 3 };
suggestions.sort((a, b) => {
const pA = priorityMap[a.priority?.toLowerCase()] ?? 99;
const pB = priorityMap[b.priority?.toLowerCase()] ?? 99;
return pA - pB;
});
}
}
function loadLocalSuggestions(category) {
const cwd = process.cwd();
const projectName = path4.basename(cwd);
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
const analysisPath = path4.join(homeDir, ".bob", "projects", projectName, "analysis", "results", "analysis.json");
if (!fs4.existsSync(analysisPath)) return [];
const allResults = JSON.parse(fs4.readFileSync(analysisPath, "utf-8"));
const suggestions = [];
for (const [filePath, fileResults] of Object.entries(allResults)) {
const items = fileResults[category] || [];
items.forEach((item, idx) => {
if (!item.status || item.status === "pending") {
suggestions.push({
...item,
filePath,
id: `${filePath.replace(/[\/\\]/g, "_")}_${idx}`
});
}
});
}
return suggestions;
}
function wrapText(text, maxWidth) {
const words = text.split(" ");
const lines = [];
let currentLine = "";
for (const word of words) {
if (currentLine.length + word.length + 1 > maxWidth) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine += (currentLine ? " " : "") + word;
}
}
if (currentLine) lines.push(currentLine);
return lines;
}
export {
getConfig,
setConfigValue,
getConfigPath,
registerLoginCommand,
callCloudFunction,
callHTTPFunction,
isAuthenticated,
callLocalModel,
buildLocalContext,
readFileContent,
extractAllProposedFiles,
extractProposedFile,
stripCodeBlockFromResponse,
processAllProposedFiles,
proposeAndWriteFile,
markSuggestionStatus,
showInteractiveResults,
loadLocalSuggestions
};
// src/commands/analyse-results.ts
import chalk3 from "chalk";
import inquirer from "inquirer";
import * as fs4 from "fs";
import * as path4 from "path";
// src/core/api-client.ts
import axios2 from "axios";
// src/core/config-store.ts
import Conf from "conf";
// src/types/config.ts
var DEFAULT_CONFIG = {
tier: "local",
loggedIn: false,
email: null,
uid: null,
authToken: null,
refreshToken: null,
provider: null,
providerKey: null,
localEndpoint: null,
personalizationMode: false,
consultantMode: false,
autoMode: false,
idrp: false,
idrpFilter: "free",
activeProject: null,
conversationId: null,
activePersona: null,
hasSeenWelcome: false
};
// src/core/config-store.ts
var store = new Conf({
projectName: "bob-cli",
defaults: DEFAULT_CONFIG
});
function getConfig() {
return {
tier: store.get("tier"),
loggedIn: store.get("loggedIn"),
email: store.get("email"),
uid: store.get("uid"),
authToken: store.get("authToken"),
refreshToken: store.get("refreshToken"),
provider: store.get("provider"),
providerKey: store.get("providerKey"),
localEndpoint: store.get("localEndpoint"),
personalizationMode: store.get("personalizationMode"),
consultantMode: store.get("consultantMode"),
idrp: store.get("idrp"),
idrpFilter: store.get("idrpFilter"),
activeProject: store.get("activeProject"),
conversationId: store.get("conversationId"),
activePersona: store.get("activePersona"),
hasSeenWelcome: store.get("hasSeenWelcome"),
autoMode: store.get("autoMode")
};
}
function setConfigValue(key, value) {
store.set(key, value);
}
function getConfigPath() {
return store.path;
}
// src/commands/login.ts
import chalk from "chalk";
import http from "http";
import open from "open";
import axios from "axios";
import { URL } from "url";
import * as readline from "readline";
var CLI_AUTH_URL = "https://bobs-workshop.web.app/cli-auth";
var CALLBACK_PORT = 9876;
var FIREBASE_API_KEY = "AIzaSyB-hUZEonRIzbExVDwuneJaDjJZBvHdIps";
function registerLoginCommand(program) {
program.command("login").description("Authenticate with Bob's Workshop via browser").action(async () => {
console.log("");
console.log(chalk.bold.cyan(" \u{1F510} Bob CLI \u2014 Login"));
console.log(chalk.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"));
console.log("");
console.log(chalk.yellow(" \u26A0\uFE0F Important:"));
console.log(chalk.gray(" \u2022 Local conversations (Tier 1) will NOT sync to the platform."));
console.log(chalk.gray(" \u2022 Only NEW conversations created after login will save to Firebase."));
console.log(chalk.gray(" \u2022 Your local history stays in ~/.bob/projects/ (backup via `bob backup`)."));
console.log(chalk.gray(" \u2022 Logging in upgrades you to Tier 3 (Platform) with full features."));
console.log("");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve3) => {
rl.question(chalk.cyan(" Continue with login? (y/n): "), resolve3);
});
rl.close();
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
console.log("");
console.log(chalk.gray(" Login cancelled."));
console.log("");
return;
}
console.log("");
console.log(chalk.gray(" Opening browser for authentication..."));
console.log("");
try {
const result = await startAuthFlow();
if (result) {
const exchangeResult = await exchangeCustomToken(result.token);
setConfigValue("authToken", exchangeResult.idToken);
setConfigValue("refreshToken", exchangeResult.refreshToken);
setConfigValue("email", result.email);
setConfigValue("uid", result.uid);
setConfigValue("loggedIn", true);
setConfigValue("tier", "platform");
console.log("");
console.log(chalk.green(` \u2705 Logged in as ${result.email}`));
console.log(chalk.gray(" Tier: Platform (Tier 3)"));
console.log(chalk.gray(" All platform features are now available."));
console.log("");
}
} catch (error) {
console.log(chalk.red(` \u274C Login failed: ${error.message}`));
console.log("");
}
});
program.command("logout").description("Sign out and clear stored credentials").action(() => {
setConfigValue("authToken", null);
setConfigValue("refreshToken", null);
setConfigValue("email", null);
setConfigValue("uid", null);
setConfigValue("loggedIn", false);
setConfigValue("tier", "local");
console.log("");
console.log(chalk.gray(" \u{1F44B} Logged out. Switched to Tier 1 (local-first)."));
console.log("");
});
}
async function exchangeCustomToken(customToken) {
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${FIREBASE_API_KEY}`;
const response = await axios.post(url, {
token: customToken,
returnSecureToken: true
});
if (!response.data?.idToken || !response.data?.refreshToken) {
throw new Error("Token exchange failed \u2014 no ID token returned.");
}
return {
idToken: response.data.idToken,
refreshToken: response.data.refreshToken
};
}
async function refreshAuthToken(refreshToken) {
const url = `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`;
const response = await axios.post(url, {
grant_type: "refresh_token",
refresh_token: refreshToken
});
if (!response.data?.id_token) {
throw new Error("Token refresh failed.");
}
setConfigValue("authToken", response.data.id_token);
return response.data.id_token;
}
function startAuthFlow() {
return new Promise((resolve3, reject) => {
const timeout = setTimeout(() => {
server.close();
reject(new Error("Login timed out after 120 seconds. Please try again."));
}, 12e4);
const server = http.createServer((req, res) => {
if (!req.url?.startsWith("/callback")) {
res.writeHead(404);
res.end("Not found");
return;
}
try {
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
const token = url.searchParams.get("token");
const email = url.searchParams.get("email");
const uid = url.searchParams.get("uid");
if (!token || !email || !uid) {
res.writeHead(400);
res.end("Missing parameters");
reject(new Error("Invalid callback \u2014 missing token, email, or uid."));
return;
}
res.writeHead(200, { "Content-Type": "text/html" });
res.end(`
<html>
<body style="background: #0a0a0a; color: white; font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
<div style="text-align: center;">
<h1>\u2705 Authenticated!</h1>
<p style="color: #888;">You can close this tab and return to your terminal.</p>
</div>
</body>
</html>
`);
clearTimeout(timeout);
server.close();
resolve3({ token, email, uid });
} catch (e) {
res.writeHead(500);
res.end("Error");
reject(e);
}
});
server.listen(CALLBACK_PORT, () => {
console.log(chalk.gray(` \u{1F310} Waiting for authentication (port ${CALLBACK_PORT})...`));
console.log(chalk.gray(" If your browser doesn't open, visit:"));
console.log(chalk.cyan(` ${CLI_AUTH_URL}`));
console.log("");
open(CLI_AUTH_URL).catch(() => {
});
});
server.on("error", (err) => {
clearTimeout(timeout);
if (err.code === "EADDRINUSE") {
reject(new Error("Port 9876 is already in use. Close other instances and try again."));
} else {
reject(err);
}
});
});
}
// src/core/api-client.ts
var FUNCTIONS_BASE = "https://us-central1-seedlingapp.cloudfunctions.net";
async function callCloudFunction(functionName, data) {
const config = getConfig();
if (!config.authToken) {
throw new Error("Not authenticated. Run `bob login` first.");
}
try {
const response = await axios2.post(
`${FUNCTIONS_BASE}/${functionName}`,
{ data },
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${config.authToken}`
},
timeout: 18e4
}
);
return response.data?.result || response.data;
} catch (error) {
const status = error.response?.status;
if (status === 401 && config.refreshToken) {
try {
const newToken = await refreshAuthToken(config.refreshToken);
const retryResponse = await axios2.post(
`${FUNCTIONS_BASE}/${functionName}`,
{ data },
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${newToken}`
},
timeout: 18e4
}
);
return retryResponse.data?.result || retryResponse.data;
} catch (refreshError) {
setConfigValue("loggedIn", false);
throw new Error("Session expired. Run `bob login` again.");
}
}
if (status === 404) {
throw new Error(`Function "${functionName}" not found. Is it deployed?`);
}
if (status === 403) {
throw new Error("Permission denied. You may not have access to this feature.");
}
if (status === 500) {
const serverMsg = error.response?.data?.error?.message || error.response?.data?.error || "Internal server error";
throw new Error(`Server error: ${serverMsg}`);
}
if (status === 429) {
throw new Error("Rate limited. Please wait a moment and try again.");
}
const errorMsg = error.response?.data?.error?.message || error.message || `Request failed with status ${status}`;
throw new Error(errorMsg);
}
}
async function callHTTPFunction(functionName, data) {
const config = getConfig();
if (!config.authToken) {
throw new Error("Not authenticated. Run `bob login` first.");
}
try {
const response = await axios2.post(
`${FUNCTIONS_BASE}/${functionName}`,
{ data },
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${config.authToken}`
},
timeout: 3e5
}
);
return response.data?.data || response.data;
} catch (error) {
const status = error.response?.status;
if (status === 401 && config.refreshToken) {
try {
const newToken = await refreshAuthToken(config.refreshToken);
const retryResponse = await axios2.post(
`${FUNCTIONS_BASE}/${functionName}`,
{ data },
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${newToken}`
},
timeout: 3e5
}
);
return retryResponse.data?.data || retryResponse.data;
} catch (refreshError) {
setConfigValue("loggedIn", false);
throw new Error("Session expired. Run `bob login` again.");
}
}
if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT") {
throw new Error("Connection was reset. The function may still be running \u2014 check the web app for the response.");
}
if (status === 404) {
throw new Error(`Function "${functionName}" not found. Is it deployed?`);
}
if (status === 403) {
throw new Error("Permission denied. You may not have access to this feature.");
}
if (status === 500) {
const serverMsg = error.response?.data?.error?.message || error.response?.data?.error || "Internal server error";
throw new Error(`Server error: ${serverMsg}`);
}
if (status === 429) {
throw new Error("Rate limited. Please wait a moment and try again.");
}
const errorMsg = error.response?.data?.error?.message || error.message || `Request failed with status ${status}`;
throw new Error(errorMsg);
}
}
function isAuthenticated() {
const config = getConfig();
return !!(config.loggedIn && config.authToken);
}
// src/ai/providers/local.ts
import axios3 from "axios";
async function callLocalModel(endpoint, messages) {
try {
const response = await axios3.post(
endpoint,
{
model: "bob-local-dna:latest",
messages,
stream: false
},
{
headers: { "Content-Type": "application/json" },
timeout: 18e4
}
);
if (response.data?.message?.content) {
return {
text: response.data.message.content,
evalCount: response.data.eval_count || void 0,
promptEvalCount: response.data.prompt_eval_count || void 0,
evalDurationMs: response.data.eval_duration ? Math.round(response.data.eval_duration / 1e6) : void 0,
totalDurationMs: response.data.total_duration ? Math.round(response.data.total_duration / 1e6) : void 0
};
}
const choice = response.data?.choices?.[0];
if (choice?.message?.content) {
return {
text: choice.message.content,
evalCount: response.data.usage?.completion_tokens || void 0,
promptEvalCount: response.data.usage?.prompt_tokens || void 0
};
}
if (typeof response.data?.response === "string") {
return { text: response.data.response };
}
return { text: "No response received from local model." };
} catch (error) {
if (error.code === "ECONNREFUSED") {
throw new Error("Cannot connect to local model. Is Ollama running? Check your endpoint: " + endpoint);
}
throw new Error("Local model error: " + (error.response?.status ? `Status ${error.response.status}` : error.message));
}
}
// src/core/context-builder.ts
import * as fs from "fs";
import * as path from "path";
var IGNORE_DIRS = ["node_modules", ".git", "dist", "build", ".dart_tool", ".idea", ".gradle", ".pub-cache", ".bob"];
var MAX_DEPTH = 3;
function buildLocalContext(rootDir) {
const tree = getDirectoryTree(rootDir, 0);
return `Working Directory: ${rootDir}
File Tree:
${tree}`;
}
function getDirectoryTree(dir, depth) {
if (depth >= MAX_DEPTH) return "";
let result = "";
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (IGNORE_DIRS.includes(entry.name)) continue;
if (entry.name.startsWith(".") && depth === 0) continue;
const indent = " ".repeat(depth);
if (entry.isDirectory()) {
result += `${indent}${entry.name}/
`;
result += getDirectoryTree(path.join(dir, entry.name), depth + 1);
} else {
result += `${indent}${entry.name}
`;
}
}
} catch (e) {
}
return result;
}
function readFileContent(filePath) {
try {
return fs.readFileSync(path.resolve(filePath), "utf-8");
} catch (e) {
return null;
}
}
// src/core/file-writer.ts
import * as fs2 from "fs";
import * as path2 from "path";
import * as readline2 from "readline";
import chalk2 from "chalk";
var SUCCESS = chalk2.hex("#66BB6A");
var INFO = chalk2.hex("#26C6DA");
var WARNING = chalk2.hex("#FFC107");
var ERROR = chalk2.hex("#EF5350");
var MUTED = chalk2.hex("#78909C");
var BRAND_SECONDARY = chalk2.hex("#FFAB00");
var BORDER = chalk2.hex("#455A64");
function extractAllProposedFiles(response) {
const proposals = [];
const codeBlockRegex = /```[\w]*\n([\s\S]*?)```/g;
let match;
while ((match = codeBlockRegex.exec(response)) !== null) {
const codeContent = match[1].trim();
const lines = codeContent.split("\n");
if (lines.length === 0) continue;
const firstLine = lines[0].trim();
let filePathMatch = firstLine.match(/^\/\/\s*File:\s*(.+)$/);
if (!filePathMatch) filePathMatch = firstLine.match(/^\/\/\s*([\w\-\.\/\\]+\.\w+)\s*$/);
if (!filePathMatch) filePathMatch = firstLine.match(/^#\s*File:\s*(.+)$/);
if (!filePathMatch) filePathMatch = firstLine.match(/^#\s*([\w\-\.\/\\]+\.\w+)\s*$/);
if (!filePathMatch) filePathMatch = firstLine.match(/^\*\s*\[FILE:\s*(.+?)\]/);
if (!filePathMatch) continue;
const filePath = filePathMatch[1].trim();
if (!filePath.includes("/") && !filePath.includes("\\")) continue;
if (!filePath.includes(".")) continue;
const fileContent = lines.slice(1).join("\n").trim();
const isLocal = isLocalProjectFile(filePath);
const absolutePath = path2.join(process.cwd(), filePath);
const isNew = !fs2.existsSync(absolutePath);
proposals.push({ filePath, content: fileContent, isNew, isLocal });
}
return proposals;
}
function extractProposedFile(response) {
const all = extractAllProposedFiles(response);
return all.length > 0 ? all[0] : null;
}
function stripCodeBlockFromResponse(response) {
let stripped = response.replace(/```[\w]*\n\s*(?:\/\/\s*(?:File:)?\s*[\w\-\.\/\\]+\.\w+|#\s*(?:File:)?\s*[\w\-\.\/\\]+\.\w+|\*\s*\[FILE:)[^\n]*\n[\s\S]*?```/g, "").trim();
stripped = stripped.replace(/```capability_invocation\s*[\s\S]*?```/g, "").trim();
return stripped;
}
function isLocalProjectFile(filePath) {
const cwd = process.cwd();
const externalPatterns = ["functions/", "lib/", "android/", "ios/", "macos/", "windows/", "web/"];
for (const pattern of externalPatterns) {
if (filePath.startsWith(pattern)) {
const localPath = path2.join(cwd, pattern.replace("/", ""));
if (!fs2.existsSync(localPath)) return false;
}
}
const resolved = path2.resolve(cwd, filePath);
if (!resolved.startsWith(cwd)) return false;
return true;
}
async function processAllProposedFiles(response, autoApprove = false, existingRl) {
const proposals = extractAllProposedFiles(response);
const idrpProposals = extractIDRPFileProposals(response);
const allProposals = [...proposals, ...idrpProposals];
if (allProposals.length === 0) return;
for (const proposed of allProposals) {
if (proposed.isLocal) {
await proposeAndWriteFile(proposed, autoApprove, existingRl);
} else {
displayExternalFile(proposed);
}
}
}
function displayExternalFile(proposed) {
const totalLines = proposed.content.split("\n").length;
console.log("");
console.log(WARNING(` \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`));
console.log(WARNING(` \u2551`) + BRAND_SECONDARY(` \u{1F4CB} EXTERNAL: ${proposed.filePath}`));
console.log(WARNING(` \u2551`) + MUTED(` This file belongs to another project.`));
console.log(WARNING(` \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`));
const previewLines = proposed.content.split("\n").slice(0, 6);
for (const line of previewLines) {
console.log(WARNING(` \u2551`) + MUTED(` ${line}`));
}
if (totalLines > 6) {
console.log(WARNING(` \u2551`) + MUTED(` ... (${totalLines - 6} more lines)`));
}
console.log(WARNING(` \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`));
console.log(MUTED(` Copy this file manually to your project at: ${proposed.filePath}`));
console.log("");
}
async function proposeAndWriteFile(proposed, autoApprove = false, existingRl) {
if (!proposed.isLocal) {
displayExternalFile(proposed);
return false;
}
const absolutePath = path2.join(process.cwd(), proposed.filePath);
const action = proposed.isNew ? "CREATE" : "UPDATE";
const icon = proposed.isNew ? "\u{1F4C4}" : "\u270F\uFE0F";
const accentColor = proposed.isNew ? SUCCESS : BRAND_SECONDARY;
const totalLines = proposed.content.split("\n").length;
if (!autoApprove) {
console.log("");
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`));
console.log(BORDER(` \u2551`) + accentColor(` ${icon} ${action}: `) + chalk2.white(`${proposed.filePath}`) + MUTED(` (${totalLines} lines)`));
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`));
const previewLines = proposed.content.split("\n").slice(0, 6);
for (const line of previewLines) {
console.log(BORDER(` \u2551`) + MUTED(` ${line}`));
}
if (totalLines > 6) {
console.log(BORDER(` \u2551`) + MUTED(` ... (${totalLines - 6} more lines)`));
}
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`));
console.log("");
const promptText = INFO(` \u{1F4BE} ${action === "CREATE" ? "Write this file" : "Apply changes"}? `) + MUTED(`(y/n/path): `);
let answer;
if (existingRl) {
existingRl.pause();
process.stdout.write(promptText);
const buf = Buffer.alloc(1024);
const bytesRead = fs2.readSync(0, buf, 0, 1024, null);
answer = buf.toString("utf-8", 0, bytesRead).replace(/\r?\n/, "").trim();
existingRl.resume();
} else {
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
answer = await new Promise((resolve3) => {
rl.question(promptText, (ans) => {
rl.close();
resolve3(ans);
});
});
}
const trimmed = answer.trim().toLowerCase();
if (trimmed === "n" || trimmed === "no") {
console.log(MUTED(" \u23ED\uFE0F Skipped."));
return false;
}
if (trimmed !== "y" && trimmed !== "yes" && trimmed.length > 0) {
const customPath = path2.join(process.cwd(), trimmed);
return writeFile(customPath, proposed.content, proposed.filePath, proposed.isNew);
}
}
return writeFile(absolutePath, proposed.content, proposed.filePath, proposed.isNew);
}
function writeFile(targetPath, content, originalFilePath, isNew) {
try {
const dir = path2.dirname(targetPath);
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
if (!isNew && fs2.existsSync(targetPath)) {
const backupDir = path2.join(process.cwd(), ".bob-backups");
if (!fs2.existsSync(backupDir)) fs2.mkdirSync(backupDir, { recursive: true });
const timestamp = Date.now();
const backupName = originalFilePath.replace(/[\/\\]/g, "_") + `.${timestamp}.bak`;
fs2.copyFileSync(targetPath, path2.join(backupDir, backupName));
}
fs2.writeFileSync(targetPath, content, "utf-8");
const relativePath = path2.relative(process.cwd(), targetPath);
console.log(SUCCESS(` \u2705 Written: ${relativePath}`));
if (!isNew) {
console.log(MUTED(` \u{1F4E6} Backup saved to .bob-backups/`));
}
console.log("");
return true;
} catch (error) {
console.log(ERROR(` \u274C Write failed: ${error.message}`));
return false;
}
}
function extractIDRPFileProposals(response) {
const proposals = [];
const invocationRegex = /```capability_invocation\s*([\s\S]*?)```/g;
let match;
while ((match = invocationRegex.exec(response)) !== null) {
try {
const invocation = JSON.parse(match[1].trim());
if (invocation.action !== "invoke") continue;
if (!["workspace_create_file", "workspace_update_file"].includes(invocation.capabilityId)) continue;
const filePath = invocation.params?.filePath;
const content = invocation.params?.content || invocation.params?.newContent;
if (!filePath || !content) continue;
const absolutePath = path2.join(process.cwd(), filePath);
const isNew = !fs2.existsSync(absolutePath);
const isLocal = isLocalProjectFile(filePath);
proposals.push({ filePath, content, isNew, isLocal });
} catch {
}
}
return proposals;
}
// src/core/analysis-tracker.ts
import * as fs3 from "fs";
import * as path3 from "path";
var BOB_DIR = path3.join(process.env.HOME || process.env.USERPROFILE || "", ".bob");
function getResultsDir() {
const projectName = path3.basename(process.cwd());
return path3.join(BOB_DIR, "projects", projectName, "analysis", "results");
}
function getAnalysisPath() {
return path3.join(getResultsDir(), "analysis.json");
}
function getStatusLogPath() {
return path3.join(getResultsDir(), "status-log.json");
}
function markSuggestionStatus(filePath, suggestionIndex, category, status, metadata) {
const analysisPath = getAnalysisPath();
const logPath = getStatusLogPath();
if (!fs3.existsSync(analysisPath)) return;
const allResults = JSON.parse(fs3.readFileSync(analysisPath, "utf-8"));
if (allResults[filePath] && allResults[filePath][category]) {
const items = allResults[filePath][category];
if (items[suggestionIndex]) {
items[suggestionIndex].status = status;
items[suggestionIndex].statusUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
}
}
fs3.writeFileSync(analysisPath, JSON.stringify(allResults, null, 2));
let log = [];
if (fs3.existsSync(logPath)) {
try {
log = JSON.parse(fs3.readFileSync(logPath, "utf-8"));
} catch {
log = [];
}
}
log.push({
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
filePath,
category,
suggestionIndex,
action: status,
confidence: metadata?.confidence || null,
reason: metadata?.reason || null,
implementedBy: metadata?.implementedBy || "minibob",
previousStatus: "pending"
});
fs3.writeFileSync(logPath, JSON.stringify(log, null, 2));
}
function markSuggestionById(id, category, status, metadata) {
const analysisPath = getAnalysisPath();
if (!fs3.existsSync(analysisPath)) return;
const allResults = JSON.parse(fs3.readFileSync(analysisPath, "utf-8"));
for (const [filePath, fileResults] of Object.entries(allResults)) {
const items = fileResults[category];
if (!items) continue;
for (let i = 0; i < items.length; i++) {
const itemId = `${filePath.replace(/[\/\\]/g, "_")}_${i}`;
if (itemId === id) {
markSuggestionStatus(filePath, i, category, status, metadata);
return;
}
}
}
}
// src/commands/analyse-results.ts
var BRAND_PRIMARY = chalk3.hex("#E66F24");
var BRAND_SECONDARY2 = chalk3.hex("#FFAB00");
var SUCCESS2 = chalk3.hex("#66BB6A");
var INFO2 = chalk3.hex("#26C6DA");
var WARNING2 = chalk3.hex("#FFC107");
var ERROR2 = chalk3.hex("#EF5350");
var MUTED2 = chalk3.hex("#78909C");
var BORDER2 = chalk3.hex("#455A64");
var MODE_CONSULTANT = chalk3.hex("#AB47BC");
var PRIORITY_COLORS = {
"critical": chalk3.bgHex("#B71C1C").white,
"high": chalk3.hex("#FF6D00"),
"medium": chalk3.hex("#FFA726"),
"low": chalk3.hex("#66BB6A")
};
var CATEGORY_COLORS = {
"bugs": ERROR2,
"features": MODE_CONSULTANT,
"improvements": INFO2,
"upgrades": SUCCESS2
};
var CATEGORY_ICONS = {
"bugs": "\u{1F534}",
"features": "\u{1F7E3}",
"improvements": "\u{1F535}",
"upgrades": "\u{1F7E2}"
};
async function showInteractiveResults(config, category, sort, search) {
let allSuggestions = [];
if (config.tier === "platform" && config.provider !== "local" && config.loggedIn && config.conversationId) {
try {
const result = await callCloudFunction("getCLIAnalysisResults", {
conversationId: config.conversationId,
category,
sort: sort || "priority",
search: search || null
});
allSuggestions = result?.suggestions || [];
} catch (error) {
console.log(ERROR2(` \u274C ${error.message}`));
return;
}
} else {
allSuggestions = loadLocalSuggestions(category);
}
if (search) {
const query = search.toLowerCase();
allSuggestions = allSuggestions.filter(
(s) => (s.description || "").toLowerCase().includes(query) || (s.title || "").toLowerCase().includes(query) || (s.filePath || "").toLowerCase().includes(query)
);
}
sortSuggestions(allSuggestions, sort || "priority");
if (allSuggestions.length === 0) {
console.log("");
console.log(SUCCESS2(" \u2705 No items found. Clean!"));
console.log("");
return;
}
const color = CATEGORY_COLORS[category] || MUTED2;
const icon = CATEGORY_ICONS[category] || "\u25C6";
let running = true;
let displaySuggestions = [...allSuggestions];
let currentSort = sort || "priority";
while (running) {
console.log("");
console.log(color(` ${icon} ${category.toUpperCase()} (${displaySuggestions.length} items) \u2502 Sort: ${currentSort}`));
console.log(MUTED2(" \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
console.log("");
const choices = [];
choices.push({
name: INFO2(" \u{1F500} Toggle sort"),
value: "__sort__",
short: "Sort"
});
choices.push(new inquirer.Separator(MUTED2(" \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\u2500")));
for (let idx = 0; idx < displaySuggestions.length; idx++) {
const item = displaySuggestions[idx];
const pColor = PRIORITY_COLORS[item.priority?.toLowerCase()] || MUTED2;
const priorityLabel = pColor((item.priority || "MEDIUM").toUpperCase().padEnd(9));
const fileName = (item.filePath || "unknown").split("/").pop() || "unknown";
const title = (item.title || item.description || "No description").slice(0, 40);
const displayName = ` ${priorityLabel} ${INFO2(fileName.padEnd(20))} ${chalk3.white(title)}`;
choices.push({
name: displayName,
value: idx,
short: item.title || item.description?.slice(0, 30) || "Item",
description: `${item.priority} ${item.filePath} ${item.title} ${item.description}`
});
}
choices.push(new inquirer.Separator(MUTED2(" \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\u2500")));
choices.push({
name: MUTED2(" \u2190 Quit"),
value: "__quit__",
short: "Quit"
});
const { selected } = await inquirer.prompt([
{
type: "search",
name: "selected",
message: color(`Search ${category} (type to filter):`),
source: (input) => {
if (!input) return choices;
const query = input.toLowerCase();
const filtered = choices.filter((c) => {
if (c.type === "separator") return true;
if (c.value === "__sort__" || c.value === "__quit__") return true;
const searchable = c.description?.toLowerCase() || "";
return searchable.includes(query);
});
return filtered;
},
pageSize: 12
}
]);
if (selected === "__quit__") {
running = false;
break;
}
if (selected === "__sort__") {
currentSort = currentSort === "priority" ? "file" : "priority";
sortSuggestions(displaySuggestions, currentSort);
console.log(INFO2(` Sort changed to: ${currentSort}`));
continue;
}
if (typeof selected === "number") {
const item = displaySuggestions[selected];
const action = await showExpandedView(item, category);
if (action === "implement") {
await handleImplement(item, config, category);
displaySuggestions.splice(selected, 1);
const originalIdx = allSuggestions.findIndex((s) => s.id === item.id);
if (originalIdx !== -1) allSuggestions.splice(originalIdx, 1);
} else if (action === "dismiss") {
if (item.id) {
markSuggestionById(item.id, category, "dismissed", {
reason: "User dismissed from CLI",
implementedBy: "user"
});
}
displaySuggestions.splice(selected, 1);
const originalIdx = allSuggestions.findIndex((s) => s.id === item.id);
if (originalIdx !== -1) allSuggestions.splice(originalIdx, 1);
console.log(MUTED2(" \u23ED\uFE0F Dismissed and logged."));
}
}
}
}
async function showExpandedView(item, category) {
const color = CATEGORY_COLORS[category] || MUTED2;
const pColor = PRIORITY_COLORS[item.priority?.toLowerCase()] || MUTED2;
const icon = CATEGORY_ICONS[category] || "\u25C6";
console.log("");
console.log(BORDER2(" \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"));
console.log(BORDER2(" \u2551") + ` ${icon} ` + pColor(`${(item.priority || "MEDIUM").toUpperCase()} ${category.toUpperCase().slice(0, -1)}`));
console.log(BORDER2(" \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"));
console.log(BORDER2(" \u2551") + MUTED2(" File: ") + INFO2(item.filePath || "unknown"));
console.log(BORDER2(" \u2551") + MUTED2(" Priority: ") + pColor((item.priority || "medium").toUpperCase()));
console.log(BORDER2(" \u2551"));
console.log(BORDER2(" \u2551") + MUTED2(" Title:"));
console.log(BORDER2(" \u2551") + chalk3.white.bold(` ${item.title || "No title"}`));
console.log(BORDER2(" \u2551"));
console.log(BORDER2(" \u2551") + MUTED2(" Description:"));
const descLines = wrapText(item.description || "No description", 54);
for (const line of descLines) {
console.log(BORDER2(" \u2551") + chalk3.white(` ${line}`));
}
if (item.implementation) {
console.log(BORDER2(" \u2551"));
console.log(BORDER2(" \u2551") + MUTED2(" Implementation:"));
const implLines = wrapText(item.implementation, 54);
for (const line of implLines) {
console.log(BORDER2(" \u2551") + chalk3.white(` ${line}`));
}
}
console.log(BORDER2(" \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"));
console.log("");
const { action } = await inquirer.prompt([
{
type: "select",
name: "action",
message: BRAND_SECONDARY2("What do you want to do?"),
choices: [
{ name: SUCCESS2(" \u{1F527} Implement this fix"), value: "implement" },
{ name: ERROR2(" \u{1F5D1}\uFE0F Dismiss"), value: "dismiss" },
{ name: MUTED2(" \u2190 Back to list"), value: "back" }
]
}
]);
return action;
}
async function handleImplement(item, config, category) {
console.log("");
console.log(INFO2(" \u{1F527} Implementing fix..."));
console.log("");
if (config.provider === "local" && config.localEndpoint) {
const fileContent = readFileContent(item.filePath);
if (!fileContent) {
console.log(ERROR2(` \u274C Could not read file: ${item.filePath}`));
return;
}
const prompt = `You are MiniBob \u2014 a junior engineer making SURGICAL code fixes under strict supervision.
CURRENT FILE: ${item.filePath}
${fileContent}
CHANGE TO IMPLEMENT:
Title: ${item.title}
Description: ${item.description}
Implementation Instructions: ${item.implementation || "Apply the fix described above."}
RULES (CRITICAL \u2014 VIOLATION = REJECTED):
- Return ONLY valid source code. No markdown, no code fences, no \`\`\`, no explanation text.
- Start the FIRST line with: // File: ${item.filePath}
- PRESERVE ALL existing imports exactly as they are. Do NOT add, remove, or reorder imports.
- PRESERVE ALL existing exports exactly as they are. Do NOT rename exported functions or classes.
- PRESERVE the existing code structure, indentation, patterns, and naming conventions.
- Make the MINIMUM change necessary to implement the fix. Touch NOTHING else.
- Do NOT refactor, reorganize, or "improve" unrelated code.
- Do NOT add comments explaining what you changed.
- Do NOT wrap the response in markdown code blocks.
- The output must be valid TypeScript/JavaScript that compiles without errors.
- If you are unsure about a change, return the file UNCHANGED rather than risk breaking it.
Return the complete file content now:`;
try {
const messages = [
{ 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." },
{ role: "user", content: prompt }
];
const localResult = await callLocalModel(config.localEndpoint, messages);
const response = typeof localResult === "object" && localResult.text ? localResult.text : localResult;
const lines = response.split("\n");
const firstLine = lines[0].trim();
let newContent;
if (firstLine.match(/^\/\/\s*(File:)?\s*/)) {
newContent = lines.slice(1).join("\n").trim();
} else {
newContent = response.trim();
}
if (newContent.includes("```") || newContent.includes("## ") || newContent.startsWith("Here") || newContent.startsWith("I have") || newContent.startsWith("Sure")) {
console.log(WARNING2(" \u26A0\uFE0F MiniBob returned explanation instead of code. Fix rejected."));
return;
}
if (newContent.length < fileContent.length * 0.5) {
console.log(WARNING2(` \u26A0\uFE0F MiniBob's output is ${Math.round(newContent.length / fileContent.length * 100)}% of original size. Rejecting.`));
return;
}
const originalExports = fileContent.match(/export\s+(function|class|const|interface|type|async\s+function)\s+\w+/g) || [];
for (const exp of originalExports) {
const exportName = exp.split(/\s+/).pop();
if (!newContent.includes(exportName)) {
console.log(WARNING2(` \u26A0\uFE0F MiniBob removed export "${exportName}". Rejecting.`));
return;
}
}
await proposeAndWriteFile({
filePath: item.filePath,
content: newContent,
isNew: false,
isLocal: true
});
if (item.id) {
markSuggestionById(item.id, category, "implemented", {
reason: "User approved implementation from CLI",
implementedBy: "minibob"
});
}
} catch (error) {
console.log(ERROR2(` \u274C Implementation failed: ${error.message}`));
}
} else if (config.loggedIn && config.conversationId) {
try {
const result = await callCloudFunction("implementSuggestion", {
conversationId: config.conversationId,
filePath: item.filePath,
suggestionId: item.id || "unknown",
category,
jobId: `cli_impl_${Date.now()}`
});
if (result?.success) {
console.log(SUCCESS2(` \u2705 ${result.message}`));
if (item.id) {
markSuggestionById(item.id, category, "implemented", {
reason: "Platform implementation",
implementedBy: "platform"
});
}
} else {
console.log(ERROR2(" \u274C Implementation failed on platform."));
}
} catch (error) {
console.log(ERROR2(` \u274C ${error.message}`));
}
} else {
console.log(ERROR2(" \u274C No provider configured for implementation."));
}
console.log("");
}
function sortSuggestions(suggestions, method) {
if (method === "file") {
suggestions.sort((a, b) => (a.filePath || "").localeCompare(b.filePath || ""));
} else {
const priorityMap = { "critical": 0, "high": 1, "medium": 2, "low": 3 };
suggestions.sort((a, b) => {
const pA = priorityMap[a.priority?.toLowerCase()] ?? 99;
const pB = priorityMap[b.priority?.toLowerCase()] ?? 99;
return pA - pB;
});
}
}
function loadLocalSuggestions(category) {
const cwd = process.cwd();
const projectName = path4.basename(cwd);
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
const analysisPath = path4.join(homeDir, ".bob", "projects", projectName, "analysis", "results", "analysis.json");
if (!fs4.existsSync(analysisPath)) return [];
const allResults = JSON.parse(fs4.readFileSync(analysisPath, "utf-8"));
const suggestions = [];
for (const [filePath, fileResults] of Object.entries(allResults)) {
const items = fileResults[category] || [];
items.forEach((item, idx) => {
if (!item.status || item.status === "pending") {
suggestions.push({
...item,
filePath,
id: `${filePath.replace(/[\/\\]/g, "_")}_${idx}`
});
}
});
}
return suggestions;
}
function wrapText(text, maxWidth) {
const words = text.split(" ");
const lines = [];
let currentLine = "";
for (const word of words) {
if (currentLine.length + word.length + 1 > maxWidth) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine += (currentLine ? " " : "") + word;
}
}
if (currentLine) lines.push(currentLine);
return lines;
}
export {
getConfig,
setConfigValue,
getConfigPath,
registerLoginCommand,
callCloudFunction,
callHTTPFunction,
isAuthenticated,
callLocalModel,
buildLocalContext,
readFileContent,
extractAllProposedFiles,
extractProposedFile,
stripCodeBlockFromResponse,
processAllProposedFiles,
proposeAndWriteFile,
markSuggestionStatus,
showInteractiveResults,
loadLocalSuggestions
};
+1
-1
{
"name": "@bobsworkshop/cli",
"version": "0.5.4",
"version": "0.6.0",
"description": "Bob's CLI — AI coding assistant and Forge orchestrator",

@@ -5,0 +5,0 @@ "type": "module",

+145
-20

@@ -10,2 +10,3 @@ <div align="center">

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![version](https://img.shields.io/badge/version-0.6.0-orange)](https://www.npmjs.com/package/@bobsworkshop/cli)

@@ -16,3 +17,3 @@ **Bob's CLI** is a locally-installed developer tool that provides a senior-level AI engineering partner directly inside your native terminal. Stay in your development environment. Never switch to a browser. Ship faster.

[Installation](#installation) · [Quick Start](#quick-start) · [Features](#features) · [Docs](https://seedling-io.gitbook.io/bob-cli/)
[Installation](#installation) · [Quick Start](#quick-start) · [Features](#features) · [UserBob](#userbob--your-digital-twin) · [Command Center](#autonomous-command-center) · [Docs](https://seedling-io.gitbook.io/bob-cli/)

@@ -39,2 +40,4 @@ ---

| Personalization Mode | ✅ | ❌ | ❌ | ❌ |
| Digital twin simulation | ✅ | ❌ | ❌ | ❌ |
| Autonomous task dispatch | ✅ | ❌ | ❌ | ❌ |
| Conversation persistence | ✅ | ✅ | ❌ | Partial |

@@ -94,3 +97,3 @@ | Deep Dives & Forks | ✅ | ❌ | ❌ | ❌ |

Sync to web. Access Claude, Gemini, deep dives, forks, and personalization.
Sync to web. Access Claude, Gemini, deep dives, forks, personalization, and UserBob.

@@ -117,2 +120,4 @@ ---

| **Profile** | Behavioral DNA profiling + dashboard |
| **UserBob** | AI digital twin simulation — your autonomous proxy |
| **Command Center** | Inspect, approve, and manage autonomous task dispatch |
| **Deep Dive** | Sandboxed exploration on any message |

@@ -140,2 +145,97 @@ | **Fork** | Branch conversations into sub-projects |

## UserBob — Your Digital Twin
**v0.6.0 introduces UserBob** — the most advanced feature in Bob's CLI. UserBob creates an autonomous AI proxy of you, built from your behavioral DNA, engineering philosophy, and communication style. Your digital twin negotiates with Bob on your behalf to advance a mission you define. You watch. You tune. You approve the results.
```bash
bob userbob "Refactor the auth service error handling"
```
The simulation runs autonomously — no human input required. Bob and your digital twin negotiate until satisfaction reaches your target, then implementation tasks are dispatched to Mini Bob for execution.
```
┌─ UserBob ──────────────────────────────────────────────────┐
│ The error handling in AuthService is incomplete. Bob, │
│ show me the current implementation before we proceed. │
└────────────────────────────────────────────────────────────┘
[SAT: 42%] [RES: 78%] [CONVERGING]
┌──────────────────────────── Bob ─┐
│ Here's the current auth service │
│ — I can see three areas where │
│ error handling is missing... │
└──────────────────────────────────┘
─── MISSION CONTROL ──────────────────────────────────────────
SAT: 42% → 85% │ STAG: 0/3 │ DIV: 0/2 │ GRADE: 60
────────────────────────────────────────────────────────────────
```
**Options:**
```bash
bob userbob "mission" # Inline mission
bob userbob # Interactive mission prompt
bob userbob --target 70 --grading 60 # Custom parameters
bob userbob --stag 3 --div 2 # Set safety thresholds
bob userbob --resume # Resume stalled session
bob userbob --local "mission" # Tier 1 Ollama mode
```
**Mid-session slash commands:**
```
/set target 80 Update satisfaction target
/set grading 70 Update Teacher's Curve
/set stag 5 Set stalemate threshold
/set div 3 Set divergence threshold
/inject "note" Steer the simulation mid-session
/status Show current parameters
/abort Stop immediately
```
**Generate your behavioral DNA first for best results:**
```bash
bob profile --today # Local (requires Ollama)
bob profile --cloud # Cloud (Power tier)
```
---
## Autonomous Command Center
Every task UserBob dispatches to Mini Bob is visible, manageable, and auditable from the CLI:
```bash
bob command-center # Interactive task board
bob cc # Alias
bob command-center --stream # Live decision stream
bob command-center --settings # Configure autonomy thresholds
```
**What you see:**
```
─── COMMAND CENTER ──────────────────────────────────────────
2 PENDING │ 8 RUNNING │ 31 DONE │ 41 TOTAL
────────────────────────────────────────────────────────────────
● NEEDS APPROVAL [frontend ] Create the TabletHomePage layout...
● IN PROGRESS [backend ] Update auth service error handling...
● COMPLETE [cloud_functions] Deploy rate limiter utility...
```
Select any task to see the full chain of custody: Trigger → Request → Outcome. Approve or deny pending tasks directly from the terminal with live execution log streaming.
**Configure how much autonomy UserBob has:**
```bash
bob command-center --settings
# Set global confidence threshold (tasks below this % require approval)
# Set per-category overrides (always auto / always ask / use threshold)
```
---
## Commands

@@ -146,18 +246,33 @@

```
bob chat "question" # AI coding partner
bob consult "question" # Strategic advice
bob index # Index codebase
bob analyse # Code review
bob analyse --auto # Auto-fix
bob autonomy # Full autonomous repair
bob profile --cloud # Generate DNA profile
bob profile # View dashboard
bob deepdive # Sandboxed exploration
bob fork "topic" # Branch conversation
bob serve # Start SovereignLink
bob remote chat "msg" # Remote execution
bob push "message" # Git push
bob login # Authenticate
bob byok set google <key> # Add BYOK key
bob whoami # Status
Conversation
bob chat "question" # AI coding partner
bob consult "question" # Strategic advice
bob conversations # List conversations
bob fork "topic" # Branch conversation
bob deepdive # Sandboxed exploration
Project Tools
bob index # Index codebase
bob analyse # Code review
bob analyse --auto # Auto-fix
bob autonomy # Full autonomous repair
bob push "message" # Git push
Digital Twin
bob userbob "mission" # Launch digital twin simulation
bob command-center # Autonomous task board
bob cc --stream # Live decision stream
Profile & Identity
bob profile --cloud # Generate DNA profile
bob profile # View dashboard
bob byok set google <key> # Add BYOK key
Remote (SovereignLink)
bob serve # Start SovereignLink
bob remote chat "msg" # Remote execution
Configuration
bob login # Authenticate
bob whoami # Status
```

@@ -176,2 +291,3 @@

- Emotional state calibrated encouragement
- UserBob uses your DNA to act as your authentic digital twin

@@ -193,2 +309,3 @@ ```bash

▸ Local profiling ▸ Cloud profiling + Frank Engine
▸ Local UserBob simulation ▸ UserBob + autonomous dispatch
▸ Zero cost ▸ Deep dives, forks, remote exec

@@ -201,2 +318,12 @@ ```

## What's New in v0.6.0
- **`bob userbob`** — AI digital twin simulation. Declare a mission, watch your autonomous proxy negotiate with Bob to advance it. Supports Tier 1 (Ollama) and Tier 3 (platform) with live message streaming, HUD footer, and mid-session slash commands.
- **`bob command-center`** — Autonomous Command Center. Full visibility into every task UserBob dispatches. Approve, deny, and monitor execution with live streaming logs directly in your terminal.
- **BYOK fallback token gate** — Platform-wide security patch across 14 Cloud Functions. Prevents runaway billing when BYOK keys fail and fall to platform providers. Your wallet is protected.
- **`getCLIConversationMessages`** — Dedicated single-conversation poller. No more cross-conversation scanning for live simulation updates.
- **4 new Cloud Functions** — `getCLIAutonomousTasks`, `getCLITaskExecutionLog`, `updateCLIAutonomySettings`, `getCLIConversationMessages`.
---
## The Philosophy

@@ -236,3 +363,1 @@

</div>

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display