@bobsworkshop/cli
Advanced tools
| 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
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2171399
5.26%66
4.76%46327
5.38%253
5.86%18
5.88%