@bobsworkshop/cli 0.7.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +139 -5
  2. package/dist/agent-store-DUAYA6SK.js +34 -0
  3. package/dist/agent-store-PI4KOFBE.js +35 -0
  4. package/dist/agent-store-PTYRRKJ5.js +35 -0
  5. package/dist/analyse-auto-BY4BAC2U.js +529 -0
  6. package/dist/analyse-auto-F3O5DPS2.js +530 -0
  7. package/dist/analyse-auto-FOHKFESD.js +530 -0
  8. package/dist/analyse-auto-I7TIDS4E.js +530 -0
  9. package/dist/analyse-auto-MD6IA3VH.js +529 -0
  10. package/dist/analyse-auto-PJUTCRBW.js +530 -0
  11. package/dist/analyse-auto-RCY7F4TU.js +530 -0
  12. package/dist/analyse-auto-VFZGDTTQ.js +530 -0
  13. package/dist/analyse-results-42IGEWAQ.js +9 -0
  14. package/dist/analyse-results-5SAMUHHK.js +9 -0
  15. package/dist/analyse-results-CAXJXEXA.js +9 -0
  16. package/dist/analyse-results-FV5G6X3P.js +9 -0
  17. package/dist/analyse-results-L26WBPUH.js +8 -0
  18. package/dist/analyse-results-QSTTMRQE.js +9 -0
  19. package/dist/analyse-results-XAVKSCF4.js +9 -0
  20. package/dist/analyse-results-XMY3HWMA.js +8 -0
  21. package/dist/bob.js +5116 -54
  22. package/dist/chunk-3RG5ZIWI.js +10 -0
  23. package/dist/chunk-AQGIC65Q.js +922 -0
  24. package/dist/chunk-AYXEMVQS.js +925 -0
  25. package/dist/chunk-B23KYYX3.js +1196 -0
  26. package/dist/chunk-CAF7EJSC.js +213 -0
  27. package/dist/chunk-HU5PQOJI.js +1196 -0
  28. package/dist/chunk-JTMMSCF7.js +1212 -0
  29. package/dist/chunk-NZW7H2BY.js +1196 -0
  30. package/dist/chunk-PNKVD2UK.js +26 -0
  31. package/dist/chunk-WRMNJJA6.js +1259 -0
  32. package/dist/persona-loader-3I5Y6CRD.js +15 -0
  33. package/dist/persona-loader-EVL7ORNP.js +15 -0
  34. package/dist/persona-loader-SEDW2PPJ.js +14 -0
  35. package/package.json +60 -59
@@ -0,0 +1,1196 @@
1
+ // src/commands/analyse-results.ts
2
+ import chalk3 from "chalk";
3
+ import inquirer from "inquirer";
4
+ import * as fs5 from "fs";
5
+ import * as path5 from "path";
6
+
7
+ // src/core/api-client.ts
8
+ import axios2 from "axios";
9
+
10
+ // src/core/config-store.ts
11
+ import Conf from "conf";
12
+
13
+ // src/types/config.ts
14
+ var DEFAULT_CONFIG = {
15
+ tier: "local",
16
+ loggedIn: false,
17
+ email: null,
18
+ uid: null,
19
+ authToken: null,
20
+ refreshToken: null,
21
+ provider: null,
22
+ providerKey: null,
23
+ localEndpoint: null,
24
+ personalizationMode: false,
25
+ consultantMode: false,
26
+ autoMode: false,
27
+ idrp: false,
28
+ idrpFilter: "free",
29
+ activeProject: null,
30
+ conversationId: null,
31
+ activePersona: null,
32
+ hasSeenWelcome: false
33
+ };
34
+
35
+ // src/core/config-store.ts
36
+ var store = new Conf({
37
+ projectName: "bob-cli",
38
+ defaults: DEFAULT_CONFIG
39
+ });
40
+ function getConfig() {
41
+ return {
42
+ tier: store.get("tier"),
43
+ loggedIn: store.get("loggedIn"),
44
+ email: store.get("email"),
45
+ uid: store.get("uid"),
46
+ authToken: store.get("authToken"),
47
+ refreshToken: store.get("refreshToken"),
48
+ provider: store.get("provider"),
49
+ providerKey: store.get("providerKey"),
50
+ localEndpoint: store.get("localEndpoint"),
51
+ personalizationMode: store.get("personalizationMode"),
52
+ consultantMode: store.get("consultantMode"),
53
+ idrp: store.get("idrp"),
54
+ idrpFilter: store.get("idrpFilter"),
55
+ activeProject: store.get("activeProject"),
56
+ conversationId: store.get("conversationId"),
57
+ activePersona: store.get("activePersona"),
58
+ hasSeenWelcome: store.get("hasSeenWelcome"),
59
+ autoMode: store.get("autoMode")
60
+ };
61
+ }
62
+ function setConfigValue(key, value) {
63
+ store.set(key, value);
64
+ }
65
+ function getConfigPath() {
66
+ return store.path;
67
+ }
68
+
69
+ // src/commands/login.ts
70
+ import chalk from "chalk";
71
+ import http from "http";
72
+ import open from "open";
73
+ import axios from "axios";
74
+ import { URL } from "url";
75
+ import * as readline from "readline";
76
+
77
+ // src/core/project-map.ts
78
+ import * as fs from "fs";
79
+ import * as path from "path";
80
+ import * as os from "os";
81
+ var BOB_DIR = path.join(os.homedir(), ".bob");
82
+ var PROJECTS_DIR = path.join(BOB_DIR, "projects");
83
+ function getProjectName(workingDir) {
84
+ return path.basename(workingDir);
85
+ }
86
+ function getProjectDir(workingDir) {
87
+ const name = getProjectName(workingDir);
88
+ return path.join(PROJECTS_DIR, name);
89
+ }
90
+ function ensureProjectStructure(workingDir) {
91
+ const projectDir = getProjectDir(workingDir);
92
+ const conversationsDir = path.join(projectDir, "conversations");
93
+ const analysisDir = path.join(projectDir, "analysis");
94
+ const runsDir = path.join(analysisDir, "runs");
95
+ for (const dir of [BOB_DIR, PROJECTS_DIR, projectDir, conversationsDir, analysisDir, runsDir]) {
96
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
97
+ }
98
+ const metaPath = path.join(projectDir, "project.json");
99
+ if (!fs.existsSync(metaPath)) {
100
+ const meta = {
101
+ name: getProjectName(workingDir),
102
+ path: workingDir,
103
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
104
+ lastIndexed: null,
105
+ activeConversationId: null
106
+ };
107
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
108
+ }
109
+ return { projectDir, conversationsDir, analysisDir, runsDir };
110
+ }
111
+ function getActiveConversationId(workingDir) {
112
+ const cwd = workingDir || process.cwd();
113
+ const projectDir = getProjectDir(cwd);
114
+ const metaPath = path.join(projectDir, "project.json");
115
+ if (!fs.existsSync(metaPath)) return null;
116
+ try {
117
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
118
+ return meta.activeConversationId || null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+ function setActiveConversationId(conversationId, workingDir) {
124
+ const cwd = workingDir || process.cwd();
125
+ ensureProjectStructure(cwd);
126
+ const projectDir = getProjectDir(cwd);
127
+ const metaPath = path.join(projectDir, "project.json");
128
+ try {
129
+ let meta;
130
+ if (fs.existsSync(metaPath)) {
131
+ meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
132
+ } else {
133
+ meta = {
134
+ name: getProjectName(cwd),
135
+ path: cwd,
136
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
137
+ lastIndexed: null,
138
+ activeConversationId: null
139
+ };
140
+ }
141
+ meta.activeConversationId = conversationId;
142
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
143
+ } catch (e) {
144
+ }
145
+ }
146
+ function createAnalysisRun(workingDir, files) {
147
+ const { runsDir } = ensureProjectStructure(workingDir);
148
+ const runId = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
149
+ const runDir = path.join(runsDir, runId);
150
+ const tasksDir = path.join(runDir, "tasks");
151
+ fs.mkdirSync(runDir, { recursive: true });
152
+ fs.mkdirSync(tasksDir, { recursive: true });
153
+ const manifest = {
154
+ runId,
155
+ status: "in_progress",
156
+ totalFiles: files.length,
157
+ completedFiles: 0,
158
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
159
+ projectPath: workingDir
160
+ };
161
+ fs.writeFileSync(path.join(runDir, "manifest.json"), JSON.stringify(manifest, null, 2));
162
+ for (const filePath of files) {
163
+ const taskId = filePath.replace(/[\/\\]/g, "_");
164
+ const task = {
165
+ filePath,
166
+ status: false,
167
+ summary: null,
168
+ dependencies: [],
169
+ error: null
170
+ };
171
+ fs.writeFileSync(path.join(tasksDir, `${taskId}.json`), JSON.stringify(task, null, 2));
172
+ }
173
+ return { runId, runDir, tasksDir };
174
+ }
175
+ function completeTask(tasksDir, filePath, summary) {
176
+ const taskId = filePath.replace(/[\/\\]/g, "_");
177
+ const taskPath = path.join(tasksDir, `${taskId}.json`);
178
+ if (fs.existsSync(taskPath)) {
179
+ const task = JSON.parse(fs.readFileSync(taskPath, "utf-8"));
180
+ task.status = true;
181
+ task.summary = summary;
182
+ fs.writeFileSync(taskPath, JSON.stringify(task, null, 2));
183
+ }
184
+ }
185
+ function updateManifestProgress(runDir, completedFiles, status) {
186
+ const manifestPath = path.join(runDir, "manifest.json");
187
+ if (fs.existsSync(manifestPath)) {
188
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
189
+ manifest.completedFiles = completedFiles;
190
+ if (status) manifest.status = status;
191
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
192
+ }
193
+ }
194
+ function saveSummaries(workingDir, summaries) {
195
+ const { analysisDir } = ensureProjectStructure(workingDir);
196
+ fs.writeFileSync(path.join(analysisDir, "summaries.json"), JSON.stringify(summaries, null, 2));
197
+ const projectDir = getProjectDir(workingDir);
198
+ const metaPath = path.join(projectDir, "project.json");
199
+ if (fs.existsSync(metaPath)) {
200
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
201
+ meta.lastIndexed = (/* @__PURE__ */ new Date()).toISOString();
202
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
203
+ }
204
+ }
205
+ function saveDependencies(workingDir, dependencies) {
206
+ const { analysisDir } = ensureProjectStructure(workingDir);
207
+ fs.writeFileSync(path.join(analysisDir, "dependencies.json"), JSON.stringify(dependencies, null, 2));
208
+ }
209
+ function loadSummaries(workingDir) {
210
+ const { analysisDir } = ensureProjectStructure(workingDir);
211
+ const summariesPath = path.join(analysisDir, "summaries.json");
212
+ if (!fs.existsSync(summariesPath)) return null;
213
+ try {
214
+ return JSON.parse(fs.readFileSync(summariesPath, "utf-8"));
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+ function loadDependencies(workingDir) {
220
+ const { analysisDir } = ensureProjectStructure(workingDir);
221
+ const depsPath = path.join(analysisDir, "dependencies.json");
222
+ if (!fs.existsSync(depsPath)) return null;
223
+ try {
224
+ return JSON.parse(fs.readFileSync(depsPath, "utf-8"));
225
+ } catch {
226
+ return null;
227
+ }
228
+ }
229
+
230
+ // src/commands/login.ts
231
+ var CLI_AUTH_URL = "https://bobs-workshop.web.app/cli-auth";
232
+ var CALLBACK_PORT = 9876;
233
+ var FIREBASE_API_KEY = "AIzaSyB-hUZEonRIzbExVDwuneJaDjJZBvHdIps";
234
+ function registerLoginCommand(program) {
235
+ program.command("login").description("Authenticate with Bob's Workshop via browser").action(async () => {
236
+ console.log("");
237
+ console.log(chalk.bold.cyan(" \u{1F510} Bob CLI \u2014 Login"));
238
+ 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"));
239
+ console.log("");
240
+ console.log(chalk.yellow(" \u26A0\uFE0F Important:"));
241
+ console.log(chalk.gray(" \u2022 Local conversations (Tier 1) will NOT sync to the platform."));
242
+ console.log(chalk.gray(" \u2022 Only NEW conversations created after login will save to Firebase."));
243
+ console.log(chalk.gray(" \u2022 Your local history stays in ~/.bob/projects/ (backup via `bob backup`)."));
244
+ console.log(chalk.gray(" \u2022 Logging in upgrades you to Tier 3 (Platform) with full features."));
245
+ console.log("");
246
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
247
+ const answer = await new Promise((resolve3) => {
248
+ rl.question(chalk.cyan(" Continue with login? (y/n): "), resolve3);
249
+ });
250
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
251
+ rl.close();
252
+ console.log("");
253
+ console.log(chalk.gray(" Login cancelled."));
254
+ console.log("");
255
+ return;
256
+ }
257
+ console.log("");
258
+ console.log(chalk.hex("#455A64")(" \u2554" + "\u2550".repeat(54) + "\u2557"));
259
+ console.log(chalk.hex("#455A64")(" \u2551") + chalk.hex("#FFAB00")(" \u{1F4CB} Before connecting to Bob's Workshop: ") + chalk.hex("#455A64")(" \u2551"));
260
+ console.log(chalk.hex("#455A64")(" \u2560" + "\u2550".repeat(54) + "\u2563"));
261
+ console.log(chalk.hex("#455A64")(" \u2551") + " " + chalk.hex("#455A64")("\u2551"));
262
+ console.log(chalk.hex("#455A64")(" \u2551") + chalk.hex("#66BB6A")(" \u2705 What syncs: ") + chalk.white("Conversation context + behavioral profile") + " " + chalk.hex("#455A64")("\u2551"));
263
+ 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"));
264
+ console.log(chalk.hex("#455A64")(" \u2551") + chalk.hex("#EF5350")(" \u274C No telemetry, no silent uploads, no gray areas. ") + chalk.hex("#455A64")(" \u2551"));
265
+ console.log(chalk.hex("#455A64")(" \u2551") + " " + chalk.hex("#455A64")("\u2551"));
266
+ console.log(chalk.hex("#455A64")(" \u2551") + chalk.gray(" Return to local-only anytime with `bob logout`. ") + chalk.hex("#455A64")(" \u2551"));
267
+ console.log(chalk.hex("#455A64")(" \u2551") + " " + chalk.hex("#455A64")("\u2551"));
268
+ console.log(chalk.hex("#455A64")(" \u255A" + "\u2550".repeat(54) + "\u255D"));
269
+ console.log("");
270
+ const consentAnswer = await new Promise((resolve3) => {
271
+ rl.question(chalk.cyan(" Confirm sync consent? (y/n): "), resolve3);
272
+ });
273
+ rl.close();
274
+ if (consentAnswer.toLowerCase() !== "y" && consentAnswer.toLowerCase() !== "yes") {
275
+ console.log("");
276
+ console.log(chalk.gray(" Login cancelled. You remain on Tier 1 (local-first)."));
277
+ console.log("");
278
+ return;
279
+ }
280
+ console.log("");
281
+ console.log(chalk.gray(" Opening browser for authentication..."));
282
+ console.log("");
283
+ try {
284
+ const result = await startAuthFlow();
285
+ if (result) {
286
+ const exchangeResult = await exchangeCustomToken(result.token);
287
+ setConfigValue("authToken", exchangeResult.idToken);
288
+ setConfigValue("refreshToken", exchangeResult.refreshToken);
289
+ setConfigValue("email", result.email);
290
+ setConfigValue("uid", result.uid);
291
+ setConfigValue("loggedIn", true);
292
+ setConfigValue("tier", "platform");
293
+ console.log("");
294
+ console.log(chalk.green(` \u2705 Logged in as ${result.email}`));
295
+ console.log(chalk.gray(" Tier: Platform (Tier 3)"));
296
+ console.log(chalk.gray(" All platform features are now available."));
297
+ console.log("");
298
+ }
299
+ } catch (error) {
300
+ console.log(chalk.red(` \u274C Login failed: ${error.message}`));
301
+ console.log("");
302
+ }
303
+ });
304
+ program.command("logout").description("Sign out and clear stored credentials").action(() => {
305
+ setConfigValue("authToken", null);
306
+ setConfigValue("refreshToken", null);
307
+ setConfigValue("email", null);
308
+ setConfigValue("uid", null);
309
+ setConfigValue("loggedIn", false);
310
+ setConfigValue("tier", "local");
311
+ setConfigValue("conversationId", null);
312
+ setActiveConversationId("", process.cwd());
313
+ console.log("");
314
+ console.log(chalk.gray(" \u{1F44B} Logged out. Switched to Tier 1 (local-first)."));
315
+ console.log("");
316
+ });
317
+ }
318
+ async function exchangeCustomToken(customToken) {
319
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${FIREBASE_API_KEY}`;
320
+ const response = await axios.post(url, {
321
+ token: customToken,
322
+ returnSecureToken: true
323
+ });
324
+ if (!response.data?.idToken || !response.data?.refreshToken) {
325
+ throw new Error("Token exchange failed \u2014 no ID token returned.");
326
+ }
327
+ return {
328
+ idToken: response.data.idToken,
329
+ refreshToken: response.data.refreshToken
330
+ };
331
+ }
332
+ async function refreshAuthToken(refreshToken) {
333
+ const url = `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`;
334
+ const response = await axios.post(url, {
335
+ grant_type: "refresh_token",
336
+ refresh_token: refreshToken
337
+ });
338
+ if (!response.data?.id_token) {
339
+ throw new Error("Token refresh failed.");
340
+ }
341
+ setConfigValue("authToken", response.data.id_token);
342
+ return response.data.id_token;
343
+ }
344
+ function startAuthFlow() {
345
+ return new Promise((resolve3, reject) => {
346
+ const timeout = setTimeout(() => {
347
+ server.close();
348
+ reject(new Error("Login timed out after 120 seconds. Please try again."));
349
+ }, 12e4);
350
+ const server = http.createServer((req, res) => {
351
+ if (!req.url?.startsWith("/callback")) {
352
+ res.writeHead(404);
353
+ res.end("Not found");
354
+ return;
355
+ }
356
+ try {
357
+ const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
358
+ const token = url.searchParams.get("token");
359
+ const email = url.searchParams.get("email");
360
+ const uid = url.searchParams.get("uid");
361
+ if (!token || !email || !uid) {
362
+ res.writeHead(400);
363
+ res.end("Missing parameters");
364
+ reject(new Error("Invalid callback \u2014 missing token, email, or uid."));
365
+ return;
366
+ }
367
+ res.writeHead(200, { "Content-Type": "text/html" });
368
+ res.end(`
369
+ <html>
370
+ <body style="background: #0a0a0a; color: white; font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
371
+ <div style="text-align: center;">
372
+ <h1>\u2705 Authenticated!</h1>
373
+ <p style="color: #888;">You can close this tab and return to your terminal.</p>
374
+ </div>
375
+ </body>
376
+ </html>
377
+ `);
378
+ clearTimeout(timeout);
379
+ server.close();
380
+ resolve3({ token, email, uid });
381
+ } catch (e) {
382
+ res.writeHead(500);
383
+ res.end("Error");
384
+ reject(e);
385
+ }
386
+ });
387
+ server.listen(CALLBACK_PORT, () => {
388
+ console.log(chalk.gray(` \u{1F310} Waiting for authentication (port ${CALLBACK_PORT})...`));
389
+ console.log(chalk.gray(" If your browser doesn't open, visit:"));
390
+ console.log(chalk.cyan(` ${CLI_AUTH_URL}`));
391
+ console.log("");
392
+ open(CLI_AUTH_URL).catch(() => {
393
+ });
394
+ });
395
+ server.on("error", (err) => {
396
+ clearTimeout(timeout);
397
+ if (err.code === "EADDRINUSE") {
398
+ reject(new Error("Port 9876 is already in use. Close other instances and try again."));
399
+ } else {
400
+ reject(err);
401
+ }
402
+ });
403
+ });
404
+ }
405
+
406
+ // src/core/api-client.ts
407
+ var FUNCTIONS_BASE = "https://us-central1-seedlingapp.cloudfunctions.net";
408
+ async function callCloudFunction(functionName, data) {
409
+ const config = getConfig();
410
+ if (!config.authToken) {
411
+ throw new Error("Not authenticated. Run `bob login` first.");
412
+ }
413
+ try {
414
+ const response = await axios2.post(
415
+ `${FUNCTIONS_BASE}/${functionName}`,
416
+ { data },
417
+ {
418
+ headers: {
419
+ "Content-Type": "application/json",
420
+ "Authorization": `Bearer ${config.authToken}`
421
+ },
422
+ timeout: 18e4
423
+ }
424
+ );
425
+ return response.data?.result || response.data;
426
+ } catch (error) {
427
+ const status = error.response?.status;
428
+ const serverMsg = error.response?.data?.error?.message || error.response?.data?.error || error.message || `Request failed with status ${status}`;
429
+ if (status === 401 && config.refreshToken) {
430
+ let newToken;
431
+ try {
432
+ newToken = await refreshAuthToken(config.refreshToken);
433
+ } catch {
434
+ setConfigValue("loggedIn", false);
435
+ throw new Error("Session expired. Run `bob login` again.");
436
+ }
437
+ try {
438
+ const retry = await axios2.post(
439
+ `${FUNCTIONS_BASE}/${functionName}`,
440
+ { data },
441
+ {
442
+ headers: {
443
+ "Content-Type": "application/json",
444
+ "Authorization": `Bearer ${newToken}`
445
+ },
446
+ timeout: 18e4
447
+ }
448
+ );
449
+ return retry.data?.result || retry.data;
450
+ } catch (retryError) {
451
+ const retryStatus = retryError.response?.status;
452
+ const retryMsg = retryError.response?.data?.error?.message || retryError.message;
453
+ if (retryStatus === 401) {
454
+ setConfigValue("loggedIn", false);
455
+ throw new Error("Session expired. Run `bob login` again.");
456
+ }
457
+ throw new Error(retryMsg);
458
+ }
459
+ }
460
+ if (status === 403) throw new Error(serverMsg);
461
+ if (status === 404) throw new Error(`Function "${functionName}" not found. Is it deployed?`);
462
+ if (status === 500) throw new Error(`Server error: ${serverMsg}`);
463
+ if (status === 429) throw new Error("Rate limited. Please wait a moment and try again.");
464
+ if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT") {
465
+ throw new Error("Connection was reset. The function may still be running.");
466
+ }
467
+ throw new Error(serverMsg);
468
+ }
469
+ }
470
+ function isAuthenticated() {
471
+ const config = getConfig();
472
+ return !!(config.loggedIn && config.authToken);
473
+ }
474
+
475
+ // src/ai/providers/local.ts
476
+ import axios3 from "axios";
477
+ async function callLocalModel(endpoint, messages) {
478
+ try {
479
+ const response = await axios3.post(
480
+ endpoint,
481
+ {
482
+ model: "bob-local-dna:latest",
483
+ messages,
484
+ stream: false
485
+ },
486
+ {
487
+ headers: { "Content-Type": "application/json" },
488
+ timeout: 18e4
489
+ }
490
+ );
491
+ if (response.data?.message?.content) {
492
+ return {
493
+ text: response.data.message.content,
494
+ evalCount: response.data.eval_count || void 0,
495
+ promptEvalCount: response.data.prompt_eval_count || void 0,
496
+ evalDurationMs: response.data.eval_duration ? Math.round(response.data.eval_duration / 1e6) : void 0,
497
+ totalDurationMs: response.data.total_duration ? Math.round(response.data.total_duration / 1e6) : void 0
498
+ };
499
+ }
500
+ const choice = response.data?.choices?.[0];
501
+ if (choice?.message?.content) {
502
+ return {
503
+ text: choice.message.content,
504
+ evalCount: response.data.usage?.completion_tokens || void 0,
505
+ promptEvalCount: response.data.usage?.prompt_tokens || void 0
506
+ };
507
+ }
508
+ if (typeof response.data?.response === "string") {
509
+ return { text: response.data.response };
510
+ }
511
+ return { text: "No response received from local model." };
512
+ } catch (error) {
513
+ if (error.code === "ECONNREFUSED") {
514
+ throw new Error("Cannot connect to local model. Is Ollama running? Check your endpoint: " + endpoint);
515
+ }
516
+ throw new Error("Local model error: " + (error.response?.status ? `Status ${error.response.status}` : error.message));
517
+ }
518
+ }
519
+
520
+ // src/core/context-builder.ts
521
+ import * as fs2 from "fs";
522
+ import * as path2 from "path";
523
+ var IGNORE_DIRS = ["node_modules", ".git", "dist", "build", ".dart_tool", ".idea", ".gradle", ".pub-cache", ".bob"];
524
+ var MAX_DEPTH = 3;
525
+ function buildLocalContext(rootDir) {
526
+ const tree = getDirectoryTree(rootDir, 0);
527
+ return `Working Directory: ${rootDir}
528
+
529
+ File Tree:
530
+ ${tree}`;
531
+ }
532
+ function getDirectoryTree(dir, depth) {
533
+ if (depth >= MAX_DEPTH) return "";
534
+ let result = "";
535
+ try {
536
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
537
+ for (const entry of entries) {
538
+ if (IGNORE_DIRS.includes(entry.name)) continue;
539
+ if (entry.name.startsWith(".") && depth === 0) continue;
540
+ const indent = " ".repeat(depth);
541
+ if (entry.isDirectory()) {
542
+ result += `${indent}${entry.name}/
543
+ `;
544
+ result += getDirectoryTree(path2.join(dir, entry.name), depth + 1);
545
+ } else {
546
+ result += `${indent}${entry.name}
547
+ `;
548
+ }
549
+ }
550
+ } catch (e) {
551
+ }
552
+ return result;
553
+ }
554
+ function readFileContent(filePath) {
555
+ try {
556
+ return fs2.readFileSync(path2.resolve(filePath), "utf-8");
557
+ } catch (e) {
558
+ return null;
559
+ }
560
+ }
561
+
562
+ // src/core/file-writer.ts
563
+ import * as fs3 from "fs";
564
+ import * as path3 from "path";
565
+ import * as readline2 from "readline";
566
+ import chalk2 from "chalk";
567
+ var SUCCESS = chalk2.hex("#66BB6A");
568
+ var INFO = chalk2.hex("#26C6DA");
569
+ var WARNING = chalk2.hex("#FFC107");
570
+ var ERROR = chalk2.hex("#EF5350");
571
+ var MUTED = chalk2.hex("#78909C");
572
+ var BRAND_SECONDARY = chalk2.hex("#FFAB00");
573
+ var BORDER = chalk2.hex("#455A64");
574
+ function extractAllProposedFiles(response) {
575
+ const proposals = [];
576
+ const codeBlockRegex = /```[\w]*\n([\s\S]*?)```/g;
577
+ let match;
578
+ while ((match = codeBlockRegex.exec(response)) !== null) {
579
+ const codeContent = match[1].trim();
580
+ const lines = codeContent.split("\n");
581
+ if (lines.length === 0) continue;
582
+ const firstLine = lines[0].trim();
583
+ let filePathMatch = firstLine.match(/^\/\/\s*File:\s*(.+)$/);
584
+ if (!filePathMatch) filePathMatch = firstLine.match(/^\/\/\s*([\w\-\.\/\\]+\.\w+)\s*$/);
585
+ if (!filePathMatch) filePathMatch = firstLine.match(/^#\s*File:\s*(.+)$/);
586
+ if (!filePathMatch) filePathMatch = firstLine.match(/^#\s*([\w\-\.\/\\]+\.\w+)\s*$/);
587
+ if (!filePathMatch) filePathMatch = firstLine.match(/^\*\s*\[FILE:\s*(.+?)\]/);
588
+ if (!filePathMatch) continue;
589
+ const filePath = filePathMatch[1].trim();
590
+ if (!filePath.includes("/") && !filePath.includes("\\")) continue;
591
+ if (!filePath.includes(".")) continue;
592
+ const fileContent = lines.slice(1).join("\n").trim();
593
+ const isLocal = isLocalProjectFile(filePath);
594
+ const absolutePath = path3.join(process.cwd(), filePath);
595
+ const isNew = !fs3.existsSync(absolutePath);
596
+ proposals.push({ filePath, content: fileContent, isNew, isLocal });
597
+ }
598
+ return proposals;
599
+ }
600
+ function extractProposedFile(response) {
601
+ const all = extractAllProposedFiles(response);
602
+ return all.length > 0 ? all[0] : null;
603
+ }
604
+ function stripCodeBlockFromResponse(response) {
605
+ 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();
606
+ stripped = stripped.replace(/```capability_invocation\s*[\s\S]*?```/g, "").trim();
607
+ return stripped;
608
+ }
609
+ function isLocalProjectFile(filePath) {
610
+ const cwd = process.cwd();
611
+ const externalPatterns = ["functions/", "lib/", "android/", "ios/", "macos/", "windows/", "web/"];
612
+ for (const pattern of externalPatterns) {
613
+ if (filePath.startsWith(pattern)) {
614
+ const localPath = path3.join(cwd, pattern.replace("/", ""));
615
+ if (!fs3.existsSync(localPath)) return false;
616
+ }
617
+ }
618
+ const resolved = path3.resolve(cwd, filePath);
619
+ if (!resolved.startsWith(cwd)) return false;
620
+ return true;
621
+ }
622
+ async function processAllProposedFiles(response, autoApprove = false, existingRl) {
623
+ const proposals = extractAllProposedFiles(response);
624
+ const idrpProposals = extractIDRPFileProposals(response);
625
+ const allProposals = [...proposals, ...idrpProposals];
626
+ if (allProposals.length === 0) return;
627
+ for (const proposed of allProposals) {
628
+ if (proposed.isLocal) {
629
+ await proposeAndWriteFile(proposed, autoApprove, existingRl);
630
+ } else {
631
+ displayExternalFile(proposed);
632
+ }
633
+ }
634
+ }
635
+ function displayExternalFile(proposed) {
636
+ const totalLines = proposed.content.split("\n").length;
637
+ console.log("");
638
+ 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`));
639
+ console.log(WARNING(` \u2551`) + BRAND_SECONDARY(` \u{1F4CB} EXTERNAL: ${proposed.filePath}`));
640
+ console.log(WARNING(` \u2551`) + MUTED(` This file belongs to another project.`));
641
+ 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`));
642
+ const previewLines = proposed.content.split("\n").slice(0, 6);
643
+ for (const line of previewLines) {
644
+ console.log(WARNING(` \u2551`) + MUTED(` ${line}`));
645
+ }
646
+ if (totalLines > 6) {
647
+ console.log(WARNING(` \u2551`) + MUTED(` ... (${totalLines - 6} more lines)`));
648
+ }
649
+ 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`));
650
+ console.log(MUTED(` Copy this file manually to your project at: ${proposed.filePath}`));
651
+ console.log("");
652
+ }
653
+ async function proposeAndWriteFile(proposed, autoApprove = false, existingRl) {
654
+ if (!proposed.isLocal) {
655
+ displayExternalFile(proposed);
656
+ return false;
657
+ }
658
+ const absolutePath = path3.join(process.cwd(), proposed.filePath);
659
+ const action = proposed.isNew ? "CREATE" : "UPDATE";
660
+ const icon = proposed.isNew ? "\u{1F4C4}" : "\u270F\uFE0F";
661
+ const accentColor = proposed.isNew ? SUCCESS : BRAND_SECONDARY;
662
+ const totalLines = proposed.content.split("\n").length;
663
+ if (!autoApprove) {
664
+ console.log("");
665
+ 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`));
666
+ console.log(BORDER(` \u2551`) + accentColor(` ${icon} ${action}: `) + chalk2.white(`${proposed.filePath}`) + MUTED(` (${totalLines} lines)`));
667
+ 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`));
668
+ const previewLines = proposed.content.split("\n").slice(0, 6);
669
+ for (const line of previewLines) {
670
+ console.log(BORDER(` \u2551`) + MUTED(` ${line}`));
671
+ }
672
+ if (totalLines > 6) {
673
+ console.log(BORDER(` \u2551`) + MUTED(` ... (${totalLines - 6} more lines)`));
674
+ }
675
+ 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`));
676
+ console.log("");
677
+ const promptText = INFO(` \u{1F4BE} ${action === "CREATE" ? "Write this file" : "Apply changes"}? `) + MUTED(`(y/n/path): `);
678
+ let answer;
679
+ if (existingRl) {
680
+ existingRl.pause();
681
+ process.stdout.write(promptText);
682
+ const buf = Buffer.alloc(1024);
683
+ const bytesRead = fs3.readSync(0, buf, 0, 1024, null);
684
+ answer = buf.toString("utf-8", 0, bytesRead).replace(/\r?\n/, "").trim();
685
+ existingRl.resume();
686
+ } else {
687
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
688
+ answer = await new Promise((resolve3) => {
689
+ rl.question(promptText, (ans) => {
690
+ rl.close();
691
+ resolve3(ans);
692
+ });
693
+ });
694
+ }
695
+ const trimmed = answer.trim().toLowerCase();
696
+ if (trimmed === "n" || trimmed === "no") {
697
+ console.log(MUTED(" \u23ED\uFE0F Skipped."));
698
+ return false;
699
+ }
700
+ if (trimmed !== "y" && trimmed !== "yes" && trimmed.length > 0) {
701
+ const customPath = path3.join(process.cwd(), trimmed);
702
+ return writeFile(customPath, proposed.content, proposed.filePath, proposed.isNew);
703
+ }
704
+ }
705
+ return writeFile(absolutePath, proposed.content, proposed.filePath, proposed.isNew);
706
+ }
707
+ function writeFile(targetPath, content, originalFilePath, isNew) {
708
+ try {
709
+ const dir = path3.dirname(targetPath);
710
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
711
+ if (!isNew && fs3.existsSync(targetPath)) {
712
+ const backupDir = path3.join(process.cwd(), ".bob-backups");
713
+ if (!fs3.existsSync(backupDir)) fs3.mkdirSync(backupDir, { recursive: true });
714
+ const timestamp = Date.now();
715
+ const backupName = originalFilePath.replace(/[\/\\]/g, "_") + `.${timestamp}.bak`;
716
+ fs3.copyFileSync(targetPath, path3.join(backupDir, backupName));
717
+ }
718
+ fs3.writeFileSync(targetPath, content, "utf-8");
719
+ const relativePath = path3.relative(process.cwd(), targetPath);
720
+ console.log(SUCCESS(` \u2705 Written: ${relativePath}`));
721
+ if (!isNew) {
722
+ console.log(MUTED(` \u{1F4E6} Backup saved to .bob-backups/`));
723
+ }
724
+ console.log("");
725
+ return true;
726
+ } catch (error) {
727
+ console.log(ERROR(` \u274C Write failed: ${error.message}`));
728
+ return false;
729
+ }
730
+ }
731
+ function extractIDRPFileProposals(response) {
732
+ const proposals = [];
733
+ const invocationRegex = /```capability_invocation\s*([\s\S]*?)```/g;
734
+ let match;
735
+ while ((match = invocationRegex.exec(response)) !== null) {
736
+ try {
737
+ const invocation = JSON.parse(match[1].trim());
738
+ if (invocation.action !== "invoke") continue;
739
+ if (!["workspace_create_file", "workspace_update_file"].includes(invocation.capabilityId)) continue;
740
+ const filePath = invocation.params?.filePath;
741
+ const content = invocation.params?.content || invocation.params?.newContent;
742
+ if (!filePath || !content) continue;
743
+ const absolutePath = path3.join(process.cwd(), filePath);
744
+ const isNew = !fs3.existsSync(absolutePath);
745
+ const isLocal = isLocalProjectFile(filePath);
746
+ proposals.push({ filePath, content, isNew, isLocal });
747
+ } catch {
748
+ }
749
+ }
750
+ return proposals;
751
+ }
752
+
753
+ // src/core/analysis-tracker.ts
754
+ import * as fs4 from "fs";
755
+ import * as path4 from "path";
756
+ var BOB_DIR2 = path4.join(process.env.HOME || process.env.USERPROFILE || "", ".bob");
757
+ function getResultsDir() {
758
+ const projectName = path4.basename(process.cwd());
759
+ return path4.join(BOB_DIR2, "projects", projectName, "analysis", "results");
760
+ }
761
+ function getAnalysisPath() {
762
+ return path4.join(getResultsDir(), "analysis.json");
763
+ }
764
+ function getStatusLogPath() {
765
+ return path4.join(getResultsDir(), "status-log.json");
766
+ }
767
+ function markSuggestionStatus(filePath, suggestionIndex, category, status, metadata) {
768
+ const analysisPath = getAnalysisPath();
769
+ const logPath = getStatusLogPath();
770
+ if (!fs4.existsSync(analysisPath)) return;
771
+ const allResults = JSON.parse(fs4.readFileSync(analysisPath, "utf-8"));
772
+ if (allResults[filePath] && allResults[filePath][category]) {
773
+ const items = allResults[filePath][category];
774
+ if (items[suggestionIndex]) {
775
+ items[suggestionIndex].status = status;
776
+ items[suggestionIndex].statusUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
777
+ }
778
+ }
779
+ fs4.writeFileSync(analysisPath, JSON.stringify(allResults, null, 2));
780
+ let log = [];
781
+ if (fs4.existsSync(logPath)) {
782
+ try {
783
+ log = JSON.parse(fs4.readFileSync(logPath, "utf-8"));
784
+ } catch {
785
+ log = [];
786
+ }
787
+ }
788
+ log.push({
789
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
790
+ filePath,
791
+ category,
792
+ suggestionIndex,
793
+ action: status,
794
+ confidence: metadata?.confidence || null,
795
+ reason: metadata?.reason || null,
796
+ implementedBy: metadata?.implementedBy || "minibob",
797
+ previousStatus: "pending"
798
+ });
799
+ fs4.writeFileSync(logPath, JSON.stringify(log, null, 2));
800
+ }
801
+ function markSuggestionById(id, category, status, metadata) {
802
+ const analysisPath = getAnalysisPath();
803
+ if (!fs4.existsSync(analysisPath)) return;
804
+ const allResults = JSON.parse(fs4.readFileSync(analysisPath, "utf-8"));
805
+ for (const [filePath, fileResults] of Object.entries(allResults)) {
806
+ const items = fileResults[category];
807
+ if (!items) continue;
808
+ for (let i = 0; i < items.length; i++) {
809
+ const itemId = `${filePath.replace(/[\/\\]/g, "_")}_${i}`;
810
+ if (itemId === id) {
811
+ markSuggestionStatus(filePath, i, category, status, metadata);
812
+ return;
813
+ }
814
+ }
815
+ }
816
+ }
817
+
818
+ // src/commands/analyse-results.ts
819
+ var BRAND_PRIMARY = chalk3.hex("#E66F24");
820
+ var BRAND_SECONDARY2 = chalk3.hex("#FFAB00");
821
+ var SUCCESS2 = chalk3.hex("#66BB6A");
822
+ var INFO2 = chalk3.hex("#26C6DA");
823
+ var WARNING2 = chalk3.hex("#FFC107");
824
+ var ERROR2 = chalk3.hex("#EF5350");
825
+ var MUTED2 = chalk3.hex("#78909C");
826
+ var BORDER2 = chalk3.hex("#455A64");
827
+ var MODE_CONSULTANT = chalk3.hex("#AB47BC");
828
+ var PRIORITY_COLORS = {
829
+ "critical": chalk3.bgHex("#B71C1C").white,
830
+ "high": chalk3.hex("#FF6D00"),
831
+ "medium": chalk3.hex("#FFA726"),
832
+ "low": chalk3.hex("#66BB6A")
833
+ };
834
+ var CATEGORY_COLORS = {
835
+ "bugs": ERROR2,
836
+ "features": MODE_CONSULTANT,
837
+ "improvements": INFO2,
838
+ "upgrades": SUCCESS2
839
+ };
840
+ var CATEGORY_ICONS = {
841
+ "bugs": "\u{1F534}",
842
+ "features": "\u{1F7E3}",
843
+ "improvements": "\u{1F535}",
844
+ "upgrades": "\u{1F7E2}"
845
+ };
846
+ async function showInteractiveResults(config, category, sort, search) {
847
+ const conversationId = getActiveConversationId(process.cwd()) || config.conversationId;
848
+ let allSuggestions = [];
849
+ if (config.tier === "platform" && config.provider !== "local" && config.loggedIn && conversationId) {
850
+ try {
851
+ const result = await callCloudFunction("getCLIAnalysisResults", {
852
+ conversationId,
853
+ category,
854
+ sort: sort || "priority",
855
+ search: search || null
856
+ });
857
+ allSuggestions = result?.suggestions || [];
858
+ } catch (error) {
859
+ console.log(ERROR2(` \u274C ${error.message}`));
860
+ return;
861
+ }
862
+ } else {
863
+ allSuggestions = loadLocalSuggestions(category);
864
+ }
865
+ if (search) {
866
+ const query = search.toLowerCase();
867
+ allSuggestions = allSuggestions.filter(
868
+ (s) => (s.description || "").toLowerCase().includes(query) || (s.title || "").toLowerCase().includes(query) || (s.filePath || "").toLowerCase().includes(query)
869
+ );
870
+ }
871
+ sortSuggestions(allSuggestions, sort || "priority");
872
+ if (allSuggestions.length === 0) {
873
+ console.log("");
874
+ console.log(SUCCESS2(" \u2705 No items found. Clean!"));
875
+ console.log("");
876
+ return;
877
+ }
878
+ const color = CATEGORY_COLORS[category] || MUTED2;
879
+ const icon = CATEGORY_ICONS[category] || "\u25C6";
880
+ let running = true;
881
+ let displaySuggestions = [...allSuggestions];
882
+ let currentSort = sort || "priority";
883
+ while (running) {
884
+ console.log("");
885
+ console.log(color(` ${icon} ${category.toUpperCase()} (${displaySuggestions.length} items) \u2502 Sort: ${currentSort}`));
886
+ 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"));
887
+ console.log("");
888
+ const choices = [];
889
+ choices.push({
890
+ name: INFO2(" \u{1F500} Toggle sort"),
891
+ value: "__sort__",
892
+ short: "Sort"
893
+ });
894
+ 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")));
895
+ for (let idx = 0; idx < displaySuggestions.length; idx++) {
896
+ const item = displaySuggestions[idx];
897
+ const pColor = PRIORITY_COLORS[item.priority?.toLowerCase()] || MUTED2;
898
+ const priorityLabel = pColor((item.priority || "MEDIUM").toUpperCase().padEnd(9));
899
+ const fileName = (item.filePath || "unknown").split("/").pop() || "unknown";
900
+ const title = (item.title || item.description || "No description").slice(0, 40);
901
+ const displayName = ` ${priorityLabel} ${INFO2(fileName.padEnd(20))} ${chalk3.white(title)}`;
902
+ choices.push({
903
+ name: displayName,
904
+ value: idx,
905
+ short: item.title || item.description?.slice(0, 30) || "Item",
906
+ description: `${item.priority} ${item.filePath} ${item.title} ${item.description}`
907
+ });
908
+ }
909
+ 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")));
910
+ choices.push({
911
+ name: MUTED2(" \u2190 Quit"),
912
+ value: "__quit__",
913
+ short: "Quit"
914
+ });
915
+ const { selected } = await inquirer.prompt([
916
+ {
917
+ type: "search",
918
+ name: "selected",
919
+ message: color(`Search ${category} (type to filter):`),
920
+ source: (input) => {
921
+ if (!input) return choices;
922
+ const query = input.toLowerCase();
923
+ const filtered = choices.filter((c) => {
924
+ if (c.type === "separator") return true;
925
+ if (c.value === "__sort__" || c.value === "__quit__") return true;
926
+ const searchable = c.description?.toLowerCase() || "";
927
+ return searchable.includes(query);
928
+ });
929
+ return filtered;
930
+ },
931
+ pageSize: 12
932
+ }
933
+ ]);
934
+ if (selected === "__quit__") {
935
+ running = false;
936
+ break;
937
+ }
938
+ if (selected === "__sort__") {
939
+ currentSort = currentSort === "priority" ? "file" : "priority";
940
+ sortSuggestions(displaySuggestions, currentSort);
941
+ console.log(INFO2(` Sort changed to: ${currentSort}`));
942
+ continue;
943
+ }
944
+ if (typeof selected === "number") {
945
+ const item = displaySuggestions[selected];
946
+ const action = await showExpandedView(item, category);
947
+ if (action === "implement") {
948
+ await handleImplement(item, config, category, conversationId);
949
+ displaySuggestions.splice(selected, 1);
950
+ const originalIdx = allSuggestions.findIndex((s) => s.id === item.id);
951
+ if (originalIdx !== -1) allSuggestions.splice(originalIdx, 1);
952
+ } else if (action === "dismiss") {
953
+ if (item.id) {
954
+ markSuggestionById(item.id, category, "dismissed", {
955
+ reason: "User dismissed from CLI",
956
+ implementedBy: "user"
957
+ });
958
+ }
959
+ displaySuggestions.splice(selected, 1);
960
+ const originalIdx = allSuggestions.findIndex((s) => s.id === item.id);
961
+ if (originalIdx !== -1) allSuggestions.splice(originalIdx, 1);
962
+ console.log(MUTED2(" \u23ED\uFE0F Dismissed and logged."));
963
+ }
964
+ }
965
+ }
966
+ }
967
+ async function showExpandedView(item, category) {
968
+ const color = CATEGORY_COLORS[category] || MUTED2;
969
+ const pColor = PRIORITY_COLORS[item.priority?.toLowerCase()] || MUTED2;
970
+ const icon = CATEGORY_ICONS[category] || "\u25C6";
971
+ console.log("");
972
+ 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"));
973
+ console.log(BORDER2(" \u2551") + ` ${icon} ` + pColor(`${(item.priority || "MEDIUM").toUpperCase()} ${category.toUpperCase().slice(0, -1)}`));
974
+ 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"));
975
+ console.log(BORDER2(" \u2551") + MUTED2(" File: ") + INFO2(item.filePath || "unknown"));
976
+ console.log(BORDER2(" \u2551") + MUTED2(" Priority: ") + pColor((item.priority || "medium").toUpperCase()));
977
+ console.log(BORDER2(" \u2551"));
978
+ console.log(BORDER2(" \u2551") + MUTED2(" Title:"));
979
+ console.log(BORDER2(" \u2551") + chalk3.white.bold(` ${item.title || "No title"}`));
980
+ console.log(BORDER2(" \u2551"));
981
+ console.log(BORDER2(" \u2551") + MUTED2(" Description:"));
982
+ const descLines = wrapText(item.description || "No description", 54);
983
+ for (const line of descLines) {
984
+ console.log(BORDER2(" \u2551") + chalk3.white(` ${line}`));
985
+ }
986
+ if (item.implementation) {
987
+ console.log(BORDER2(" \u2551"));
988
+ console.log(BORDER2(" \u2551") + MUTED2(" Implementation:"));
989
+ const implLines = wrapText(item.implementation, 54);
990
+ for (const line of implLines) {
991
+ console.log(BORDER2(" \u2551") + chalk3.white(` ${line}`));
992
+ }
993
+ }
994
+ 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"));
995
+ console.log("");
996
+ const { action } = await inquirer.prompt([
997
+ {
998
+ type: "select",
999
+ name: "action",
1000
+ message: BRAND_SECONDARY2("What do you want to do?"),
1001
+ choices: [
1002
+ { name: SUCCESS2(" \u{1F527} Implement this fix"), value: "implement" },
1003
+ { name: ERROR2(" \u{1F5D1}\uFE0F Dismiss"), value: "dismiss" },
1004
+ { name: MUTED2(" \u2190 Back to list"), value: "back" }
1005
+ ]
1006
+ }
1007
+ ]);
1008
+ return action;
1009
+ }
1010
+ async function handleImplement(item, config, category, conversationId) {
1011
+ console.log("");
1012
+ console.log(INFO2(" \u{1F527} Implementing fix..."));
1013
+ console.log("");
1014
+ if (config.provider === "local" && config.localEndpoint) {
1015
+ const fileContent = readFileContent(item.filePath);
1016
+ if (!fileContent) {
1017
+ console.log(ERROR2(` \u274C Could not read file: ${item.filePath}`));
1018
+ return;
1019
+ }
1020
+ const prompt = `You are MiniBob \u2014 a junior engineer making SURGICAL code fixes under strict supervision.
1021
+
1022
+ CURRENT FILE: ${item.filePath}
1023
+ ${fileContent}
1024
+
1025
+ CHANGE TO IMPLEMENT:
1026
+ Title: ${item.title}
1027
+ Description: ${item.description}
1028
+ Implementation Instructions: ${item.implementation || "Apply the fix described above."}
1029
+
1030
+ RULES (CRITICAL \u2014 VIOLATION = REJECTED):
1031
+ - Return ONLY valid source code. No markdown, no code fences, no \`\`\`, no explanation text.
1032
+ - Start the FIRST line with: // File: ${item.filePath}
1033
+ - PRESERVE ALL existing imports exactly as they are.
1034
+ - PRESERVE ALL existing exports exactly as they are.
1035
+ - PRESERVE the existing code structure, indentation, patterns, and naming conventions.
1036
+ - Make the MINIMUM change necessary to implement the fix. Touch NOTHING else.
1037
+ - Do NOT refactor, reorganize, or "improve" unrelated code.
1038
+ - Do NOT add comments explaining what you changed.
1039
+ - Do NOT wrap the response in markdown code blocks.
1040
+ - If you are unsure about a change, return the file UNCHANGED rather than risk breaking it.
1041
+
1042
+ Return the complete file content now:`;
1043
+ try {
1044
+ const messages = [
1045
+ { 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." },
1046
+ { role: "user", content: prompt }
1047
+ ];
1048
+ const localResult = await callLocalModel(config.localEndpoint, messages);
1049
+ const response = typeof localResult === "object" && localResult.text ? localResult.text : localResult;
1050
+ const lines = response.split("\n");
1051
+ const firstLine = lines[0].trim();
1052
+ let newContent;
1053
+ if (firstLine.match(/^\/\/\s*(File:)?\s*/)) {
1054
+ newContent = lines.slice(1).join("\n").trim();
1055
+ } else {
1056
+ newContent = response.trim();
1057
+ }
1058
+ if (newContent.includes("```") || newContent.includes("## ") || newContent.startsWith("Here") || newContent.startsWith("I have") || newContent.startsWith("Sure")) {
1059
+ console.log(WARNING2(" \u26A0\uFE0F MiniBob returned explanation instead of code. Fix rejected."));
1060
+ return;
1061
+ }
1062
+ if (newContent.length < fileContent.length * 0.5) {
1063
+ console.log(WARNING2(` \u26A0\uFE0F MiniBob's output is ${Math.round(newContent.length / fileContent.length * 100)}% of original size. Rejecting.`));
1064
+ return;
1065
+ }
1066
+ const originalExports = fileContent.match(/export\s+(function|class|const|interface|type|async\s+function)\s+\w+/g) || [];
1067
+ for (const exp of originalExports) {
1068
+ const exportName = exp.split(/\s+/).pop();
1069
+ if (!newContent.includes(exportName)) {
1070
+ console.log(WARNING2(` \u26A0\uFE0F MiniBob removed export "${exportName}". Rejecting.`));
1071
+ return;
1072
+ }
1073
+ }
1074
+ await proposeAndWriteFile({
1075
+ filePath: item.filePath,
1076
+ content: newContent,
1077
+ isNew: false,
1078
+ isLocal: true
1079
+ });
1080
+ if (item.id) {
1081
+ markSuggestionById(item.id, category, "implemented", {
1082
+ reason: "User approved implementation from CLI",
1083
+ implementedBy: "minibob"
1084
+ });
1085
+ }
1086
+ } catch (error) {
1087
+ console.log(ERROR2(` \u274C Implementation failed: ${error.message}`));
1088
+ }
1089
+ } else if (config.loggedIn && conversationId) {
1090
+ try {
1091
+ const result = await callCloudFunction("implementSuggestion", {
1092
+ conversationId,
1093
+ filePath: item.filePath,
1094
+ suggestionId: item.id || "unknown",
1095
+ category,
1096
+ jobId: `cli_impl_${Date.now()}`
1097
+ });
1098
+ if (result?.success) {
1099
+ console.log(SUCCESS2(` \u2705 ${result.message}`));
1100
+ if (item.id) {
1101
+ markSuggestionById(item.id, category, "implemented", {
1102
+ reason: "Platform implementation",
1103
+ implementedBy: "platform"
1104
+ });
1105
+ }
1106
+ } else {
1107
+ console.log(ERROR2(" \u274C Implementation failed on platform."));
1108
+ }
1109
+ } catch (error) {
1110
+ console.log(ERROR2(` \u274C ${error.message}`));
1111
+ }
1112
+ } else {
1113
+ console.log(ERROR2(" \u274C No provider configured for implementation."));
1114
+ }
1115
+ console.log("");
1116
+ }
1117
+ function sortSuggestions(suggestions, method) {
1118
+ if (method === "file") {
1119
+ suggestions.sort((a, b) => (a.filePath || "").localeCompare(b.filePath || ""));
1120
+ } else {
1121
+ const priorityMap = { "critical": 0, "high": 1, "medium": 2, "low": 3 };
1122
+ suggestions.sort((a, b) => {
1123
+ const pA = priorityMap[a.priority?.toLowerCase()] ?? 99;
1124
+ const pB = priorityMap[b.priority?.toLowerCase()] ?? 99;
1125
+ return pA - pB;
1126
+ });
1127
+ }
1128
+ }
1129
+ function loadLocalSuggestions(category) {
1130
+ const cwd = process.cwd();
1131
+ const projectName = path5.basename(cwd);
1132
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
1133
+ const analysisPath = path5.join(homeDir, ".bob", "projects", projectName, "analysis", "results", "analysis.json");
1134
+ if (!fs5.existsSync(analysisPath)) return [];
1135
+ const allResults = JSON.parse(fs5.readFileSync(analysisPath, "utf-8"));
1136
+ const suggestions = [];
1137
+ for (const [filePath, fileResults] of Object.entries(allResults)) {
1138
+ const items = fileResults[category] || [];
1139
+ items.forEach((item, idx) => {
1140
+ if (!item.status || item.status === "pending") {
1141
+ suggestions.push({
1142
+ ...item,
1143
+ filePath,
1144
+ id: `${filePath.replace(/[\/\\]/g, "_")}_${idx}`
1145
+ });
1146
+ }
1147
+ });
1148
+ }
1149
+ return suggestions;
1150
+ }
1151
+ function wrapText(text, maxWidth) {
1152
+ const words = text.split(" ");
1153
+ const lines = [];
1154
+ let currentLine = "";
1155
+ for (const word of words) {
1156
+ if (currentLine.length + word.length + 1 > maxWidth) {
1157
+ lines.push(currentLine);
1158
+ currentLine = word;
1159
+ } else {
1160
+ currentLine += (currentLine ? " " : "") + word;
1161
+ }
1162
+ }
1163
+ if (currentLine) lines.push(currentLine);
1164
+ return lines;
1165
+ }
1166
+
1167
+ export {
1168
+ getConfig,
1169
+ setConfigValue,
1170
+ getConfigPath,
1171
+ getProjectName,
1172
+ ensureProjectStructure,
1173
+ getActiveConversationId,
1174
+ setActiveConversationId,
1175
+ createAnalysisRun,
1176
+ completeTask,
1177
+ updateManifestProgress,
1178
+ saveSummaries,
1179
+ saveDependencies,
1180
+ loadSummaries,
1181
+ loadDependencies,
1182
+ registerLoginCommand,
1183
+ callCloudFunction,
1184
+ isAuthenticated,
1185
+ callLocalModel,
1186
+ buildLocalContext,
1187
+ readFileContent,
1188
+ extractAllProposedFiles,
1189
+ extractProposedFile,
1190
+ stripCodeBlockFromResponse,
1191
+ processAllProposedFiles,
1192
+ proposeAndWriteFile,
1193
+ markSuggestionStatus,
1194
+ showInteractiveResults,
1195
+ loadLocalSuggestions
1196
+ };