🚀 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
1.0.0
to
1.0.1
+530
dist/analyse-auto-VFZGDTTQ.js
import {
callLocalModel,
getConfig,
loadLocalSuggestions,
markSuggestionStatus,
readFileContent
} from "./chunk-WRMNJJA6.js";
import "./chunk-PNKVD2UK.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-WRMNJJA6.js";
import "./chunk-PNKVD2UK.js";
export {
loadLocalSuggestions,
showInteractiveResults
};
// src/commands/analyse-results.ts
import chalk3 from "chalk";
import inquirer from "inquirer";
import * as fs5 from "fs";
import * as path5 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";
// src/core/project-map.ts
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
var BOB_DIR = path.join(os.homedir(), ".bob");
var PROJECTS_DIR = path.join(BOB_DIR, "projects");
function getProjectName(workingDir) {
return path.basename(workingDir);
}
function getProjectDir(workingDir) {
const name = getProjectName(workingDir);
return path.join(PROJECTS_DIR, name);
}
function ensureProjectStructure(workingDir) {
const projectDir = getProjectDir(workingDir);
const conversationsDir = path.join(projectDir, "conversations");
const analysisDir = path.join(projectDir, "analysis");
const runsDir = path.join(analysisDir, "runs");
for (const dir of [BOB_DIR, PROJECTS_DIR, projectDir, conversationsDir, analysisDir, runsDir]) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
const metaPath = path.join(projectDir, "project.json");
if (!fs.existsSync(metaPath)) {
const meta = {
name: getProjectName(workingDir),
path: workingDir,
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
lastIndexed: null,
activeConversationId: null
};
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
}
return { projectDir, conversationsDir, analysisDir, runsDir };
}
function getActiveConversationId(workingDir) {
const cwd = workingDir || process.cwd();
const projectDir = getProjectDir(cwd);
const metaPath = path.join(projectDir, "project.json");
if (!fs.existsSync(metaPath)) return null;
try {
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
return meta.activeConversationId || null;
} catch {
return null;
}
}
function setActiveConversationId(conversationId, workingDir) {
const cwd = workingDir || process.cwd();
ensureProjectStructure(cwd);
const projectDir = getProjectDir(cwd);
const metaPath = path.join(projectDir, "project.json");
try {
let meta;
if (fs.existsSync(metaPath)) {
meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
} else {
meta = {
name: getProjectName(cwd),
path: cwd,
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
lastIndexed: null,
activeConversationId: null
};
}
meta.activeConversationId = conversationId;
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
} catch (e) {
}
}
function createAnalysisRun(workingDir, files) {
const { runsDir } = ensureProjectStructure(workingDir);
const runId = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
const runDir = path.join(runsDir, runId);
const tasksDir = path.join(runDir, "tasks");
fs.mkdirSync(runDir, { recursive: true });
fs.mkdirSync(tasksDir, { recursive: true });
const manifest = {
runId,
status: "in_progress",
totalFiles: files.length,
completedFiles: 0,
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
projectPath: workingDir
};
fs.writeFileSync(path.join(runDir, "manifest.json"), JSON.stringify(manifest, null, 2));
for (const filePath of files) {
const taskId = filePath.replace(/[\/\\]/g, "_");
const task = {
filePath,
status: false,
summary: null,
dependencies: [],
error: null
};
fs.writeFileSync(path.join(tasksDir, `${taskId}.json`), JSON.stringify(task, null, 2));
}
return { runId, runDir, tasksDir };
}
function completeTask(tasksDir, filePath, summary) {
const taskId = filePath.replace(/[\/\\]/g, "_");
const taskPath = path.join(tasksDir, `${taskId}.json`);
if (fs.existsSync(taskPath)) {
const task = JSON.parse(fs.readFileSync(taskPath, "utf-8"));
task.status = true;
task.summary = summary;
fs.writeFileSync(taskPath, JSON.stringify(task, null, 2));
}
}
function updateManifestProgress(runDir, completedFiles, status) {
const manifestPath = path.join(runDir, "manifest.json");
if (fs.existsSync(manifestPath)) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
manifest.completedFiles = completedFiles;
if (status) manifest.status = status;
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
}
}
function saveSummaries(workingDir, summaries) {
const { analysisDir } = ensureProjectStructure(workingDir);
fs.writeFileSync(path.join(analysisDir, "summaries.json"), JSON.stringify(summaries, null, 2));
const projectDir = getProjectDir(workingDir);
const metaPath = path.join(projectDir, "project.json");
if (fs.existsSync(metaPath)) {
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
meta.lastIndexed = (/* @__PURE__ */ new Date()).toISOString();
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
}
}
function saveDependencies(workingDir, dependencies) {
const { analysisDir } = ensureProjectStructure(workingDir);
fs.writeFileSync(path.join(analysisDir, "dependencies.json"), JSON.stringify(dependencies, null, 2));
}
function loadSummaries(workingDir) {
const { analysisDir } = ensureProjectStructure(workingDir);
const summariesPath = path.join(analysisDir, "summaries.json");
if (!fs.existsSync(summariesPath)) return null;
try {
return JSON.parse(fs.readFileSync(summariesPath, "utf-8"));
} catch {
return null;
}
}
function loadDependencies(workingDir) {
const { analysisDir } = ensureProjectStructure(workingDir);
const depsPath = path.join(analysisDir, "dependencies.json");
if (!fs.existsSync(depsPath)) return null;
try {
return JSON.parse(fs.readFileSync(depsPath, "utf-8"));
} catch {
return null;
}
}
// src/commands/login.ts
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);
});
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
rl.close();
console.log("");
console.log(chalk.gray(" Login cancelled."));
console.log("");
return;
}
console.log("");
console.log(chalk.hex("#455A64")(" \u2554" + "\u2550".repeat(54) + "\u2557"));
console.log(chalk.hex("#455A64")(" \u2551") + chalk.hex("#FFAB00")(" \u{1F4CB} Before connecting to Bob's Workshop: ") + chalk.hex("#455A64")(" \u2551"));
console.log(chalk.hex("#455A64")(" \u2560" + "\u2550".repeat(54) + "\u2563"));
console.log(chalk.hex("#455A64")(" \u2551") + " " + chalk.hex("#455A64")("\u2551"));
console.log(chalk.hex("#455A64")(" \u2551") + chalk.hex("#66BB6A")(" \u2705 What syncs: ") + chalk.white("Conversation context + behavioral profile") + " " + chalk.hex("#455A64")("\u2551"));
console.log(chalk.hex("#455A64")(" \u2551") + chalk.hex("#EF5350")(" \u274C Never syncs: ") + chalk.white("Your source code (stays on your machine) ") + " " + chalk.hex("#455A64")("\u2551"));
console.log(chalk.hex("#455A64")(" \u2551") + chalk.hex("#EF5350")(" \u274C No telemetry, no silent uploads, no gray areas. ") + chalk.hex("#455A64")(" \u2551"));
console.log(chalk.hex("#455A64")(" \u2551") + " " + chalk.hex("#455A64")("\u2551"));
console.log(chalk.hex("#455A64")(" \u2551") + chalk.gray(" Return to local-only anytime with `bob logout`. ") + chalk.hex("#455A64")(" \u2551"));
console.log(chalk.hex("#455A64")(" \u2551") + " " + chalk.hex("#455A64")("\u2551"));
console.log(chalk.hex("#455A64")(" \u255A" + "\u2550".repeat(54) + "\u255D"));
console.log("");
const consentAnswer = await new Promise((resolve3) => {
rl.question(chalk.cyan(" Confirm sync consent? (y/n): "), resolve3);
});
rl.close();
if (consentAnswer.toLowerCase() !== "y" && consentAnswer.toLowerCase() !== "yes") {
console.log("");
console.log(chalk.gray(" Login cancelled. You remain on Tier 1 (local-first)."));
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");
setConfigValue("conversationId", null);
setActiveConversationId("", process.cwd());
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;
const serverMsg = error.response?.data?.error?.message || error.response?.data?.error || error.message || `Request failed with status ${status}`;
if (status === 401 && config.refreshToken) {
let newToken;
try {
newToken = await refreshAuthToken(config.refreshToken);
} catch {
setConfigValue("loggedIn", false);
throw new Error("Session expired. Run `bob login` again.");
}
try {
const retry = await axios2.post(
`${FUNCTIONS_BASE}/${functionName}`,
{ data },
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${newToken}`
},
timeout: 18e4
}
);
return retry.data?.result || retry.data;
} catch (retryError) {
const retryStatus = retryError.response?.status;
const retryMsg = retryError.response?.data?.error?.message || retryError.message;
if (retryStatus === 401) {
setConfigValue("loggedIn", false);
throw new Error("Session expired. Run `bob login` again.");
}
throw new Error(retryMsg);
}
}
if (status === 403) throw new Error(serverMsg);
if (status === 404) throw new Error(`Function "${functionName}" not found. Is it deployed?`);
if (status === 500) throw new Error(`Server error: ${serverMsg}`);
if (status === 429) throw new Error("Rate limited. Please wait a moment and try again.");
if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT") {
throw new Error("Connection was reset. The function may still be running.");
}
throw new Error(serverMsg);
}
}
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;
const serverMsg = error.response?.data?.error?.message || error.response?.data?.error || error.message || `Request failed with status ${status}`;
if (status === 401 && config.refreshToken) {
let newToken;
try {
newToken = await refreshAuthToken(config.refreshToken);
} catch {
setConfigValue("loggedIn", false);
throw new Error("Session expired. Run `bob login` again.");
}
try {
const retry = await axios2.post(
`${FUNCTIONS_BASE}/${functionName}`,
{ data },
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${newToken}`
},
timeout: 3e5
}
);
return retry.data?.data || retry.data;
} catch (retryError) {
const retryStatus = retryError.response?.status;
const retryMsg = retryError.response?.data?.error?.message || retryError.message;
if (retryStatus === 401) {
setConfigValue("loggedIn", false);
throw new Error("Session expired. Run `bob login` again.");
}
throw new Error(retryMsg);
}
}
if (status === 403) throw new Error(serverMsg);
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 === 500) throw new Error(`Server error: ${serverMsg}`);
if (status === 429) throw new Error("Rate limited. Please wait a moment and try again.");
throw new Error(serverMsg);
}
}
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 fs2 from "fs";
import * as path2 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 = fs2.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(path2.join(dir, entry.name), depth + 1);
} else {
result += `${indent}${entry.name}
`;
}
}
} catch (e) {
}
return result;
}
function readFileContent(filePath) {
try {
return fs2.readFileSync(path2.resolve(filePath), "utf-8");
} catch (e) {
return null;
}
}
// src/core/file-writer.ts
import * as fs3 from "fs";
import * as path3 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 = path3.join(process.cwd(), filePath);
const isNew = !fs3.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 = path3.join(cwd, pattern.replace("/", ""));
if (!fs3.existsSync(localPath)) return false;
}
}
const resolved = path3.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 = path3.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 = fs3.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 = path3.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 = path3.dirname(targetPath);
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
if (!isNew && fs3.existsSync(targetPath)) {
const backupDir = path3.join(process.cwd(), ".bob-backups");
if (!fs3.existsSync(backupDir)) fs3.mkdirSync(backupDir, { recursive: true });
const timestamp = Date.now();
const backupName = originalFilePath.replace(/[\/\\]/g, "_") + `.${timestamp}.bak`;
fs3.copyFileSync(targetPath, path3.join(backupDir, backupName));
}
fs3.writeFileSync(targetPath, content, "utf-8");
const relativePath = path3.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 = path3.join(process.cwd(), filePath);
const isNew = !fs3.existsSync(absolutePath);
const isLocal = isLocalProjectFile(filePath);
proposals.push({ filePath, content, isNew, isLocal });
} catch {
}
}
return proposals;
}
// src/core/analysis-tracker.ts
import * as fs4 from "fs";
import * as path4 from "path";
var BOB_DIR2 = path4.join(process.env.HOME || process.env.USERPROFILE || "", ".bob");
function getResultsDir() {
const projectName = path4.basename(process.cwd());
return path4.join(BOB_DIR2, "projects", projectName, "analysis", "results");
}
function getAnalysisPath() {
return path4.join(getResultsDir(), "analysis.json");
}
function getStatusLogPath() {
return path4.join(getResultsDir(), "status-log.json");
}
function markSuggestionStatus(filePath, suggestionIndex, category, status, metadata) {
const analysisPath = getAnalysisPath();
const logPath = getStatusLogPath();
if (!fs4.existsSync(analysisPath)) return;
const allResults = JSON.parse(fs4.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();
}
}
fs4.writeFileSync(analysisPath, JSON.stringify(allResults, null, 2));
let log = [];
if (fs4.existsSync(logPath)) {
try {
log = JSON.parse(fs4.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"
});
fs4.writeFileSync(logPath, JSON.stringify(log, null, 2));
}
function markSuggestionById(id, category, status, metadata) {
const analysisPath = getAnalysisPath();
if (!fs4.existsSync(analysisPath)) return;
const allResults = JSON.parse(fs4.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) {
const conversationId = getActiveConversationId(process.cwd()) || config.conversationId;
let allSuggestions = [];
if (config.tier === "platform" && config.provider !== "local" && config.loggedIn && conversationId) {
try {
const result = await callCloudFunction("getCLIAnalysisResults", {
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, conversationId);
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, conversationId) {
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.
- PRESERVE ALL existing exports exactly as they are.
- 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.
- 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. 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 && conversationId) {
try {
const result = await callCloudFunction("implementSuggestion", {
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 = path5.basename(cwd);
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
const analysisPath = path5.join(homeDir, ".bob", "projects", projectName, "analysis", "results", "analysis.json");
if (!fs5.existsSync(analysisPath)) return [];
const allResults = JSON.parse(fs5.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,
getProjectName,
ensureProjectStructure,
getActiveConversationId,
setActiveConversationId,
createAnalysisRun,
completeTask,
updateManifestProgress,
saveSummaries,
saveDependencies,
loadSummaries,
loadDependencies,
registerLoginCommand,
callCloudFunction,
callHTTPFunction,
isAuthenticated,
callLocalModel,
buildLocalContext,
readFileContent,
extractAllProposedFiles,
extractProposedFile,
stripCodeBlockFromResponse,
processAllProposedFiles,
proposeAndWriteFile,
markSuggestionStatus,
showInteractiveResults,
loadLocalSuggestions
};
+1
-1
{
"name": "@bobsworkshop/cli",
"version": "1.0.0",
"version": "1.0.1",
"description": "Bob's CLI — AI coding assistant and Forge orchestrator",

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

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