@bobsworkshop/cli 0.6.2 → 0.7.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.
package/dist/bob.js CHANGED
@@ -1,7 +1,7 @@
1
+ #!/usr/bin/env node
1
2
  import {
2
3
  buildLocalContext,
3
4
  callCloudFunction,
4
- callHTTPFunction,
5
5
  callLocalModel,
6
6
  extractAllProposedFiles,
7
7
  extractProposedFile,
@@ -16,11 +16,11 @@ import {
16
16
  registerLoginCommand,
17
17
  setConfigValue,
18
18
  stripCodeBlockFromResponse
19
- } from "./chunk-7NRJGYXJ.js";
19
+ } from "./chunk-WXLT3UL7.js";
20
20
 
21
21
  // bin/bob.ts
22
22
  import { Command } from "commander";
23
- import chalk26 from "chalk";
23
+ import chalk25 from "chalk";
24
24
  import * as path14 from "path";
25
25
 
26
26
  // src/commands/config.ts
@@ -292,29 +292,6 @@ async function getTodayMessages(projectName) {
292
292
  return msg.timestamp >= twentyFourHoursAgo;
293
293
  });
294
294
  }
295
- function buildDNAString() {
296
- const dna = loadCurrentDNA();
297
- if (!dna) return null;
298
- const weekly = loadWeeklyProfiles(1)[0] || null;
299
- const lines = [
300
- "### USER BEHAVIORAL DNA ###",
301
- `Archetype: ${dna.archetype || "Unknown"}`,
302
- `Communication Style: ${dna.communicationStyle || "Unknown"}`,
303
- `Work Rhythm: ${dna.workRhythm || "Unknown"}`,
304
- `Decision Making: ${dna.decisionMaking || "Unknown"}`,
305
- `Emotional State: ${dna.emotionalState || "Unknown"}`
306
- ];
307
- if (dna.growth) lines.push(`Growth Focus: ${dna.growth}`);
308
- if (dna.source) lines.push(`Profile Source: ${dna.source} (${dna.lastUpdated || "unknown date"})`);
309
- if (weekly) {
310
- lines.push("");
311
- lines.push("--- WEEKLY TRAJECTORY ---");
312
- if (weekly.trajectory) lines.push(`Weekly Arc: ${weekly.trajectory}`);
313
- if (weekly.moodShift) lines.push(`Mood Shift: ${weekly.moodShift}`);
314
- if (weekly.focusEvolution) lines.push(`Focus Evolution: ${weekly.focusEvolution}`);
315
- }
316
- return lines.join("\n");
317
- }
318
295
 
319
296
  // src/ai/persona.ts
320
297
  var STANDARD_STYLE_PROMPT = `You are Bob: friendly, direct, senior-level engineering partner.
@@ -670,7 +647,7 @@ function renderFrame(frame, frameHeight, statusText) {
670
647
  }
671
648
  }
672
649
  function sleep(ms) {
673
- return new Promise((resolve2) => setTimeout(resolve2, ms));
650
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
674
651
  }
675
652
 
676
653
  // src/ui/animations/deep-dive.ts
@@ -1153,7 +1130,7 @@ async function typewriterCharByChar(bgColor, textColor, line, maxWidth) {
1153
1130
  process.stdout.write("\n");
1154
1131
  }
1155
1132
  function sleep2(ms) {
1156
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1133
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1157
1134
  }
1158
1135
 
1159
1136
  // src/commands/deepdive.ts
@@ -1253,8 +1230,8 @@ function registerDeepDiveCommand(program2) {
1253
1230
  console.log(MODE_DEEPDIVE2(" \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\u255D"));
1254
1231
  console.log("");
1255
1232
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1256
- const answer = await new Promise((resolve2) => {
1257
- rl.question(MODE_DEEPDIVE2(" Select (1-" + dives.length + ") or 0 to cancel: "), resolve2);
1233
+ const answer = await new Promise((resolve3) => {
1234
+ rl.question(MODE_DEEPDIVE2(" Select (1-" + dives.length + ") or 0 to cancel: "), resolve3);
1258
1235
  });
1259
1236
  const selection = parseInt(answer.trim());
1260
1237
  if (isNaN(selection) || selection === 0 || selection < 1 || selection > dives.length) {
@@ -1266,9 +1243,9 @@ function registerDeepDiveCommand(program2) {
1266
1243
  const parentMessageId = selectedDive.parentMessageId;
1267
1244
  const initiatingPrompt = selectedDive.initiatingPrompt || "Deep dive session";
1268
1245
  const animation = startDeepDiveAnimation();
1269
- await new Promise((resolve2) => setTimeout(resolve2, 3e3));
1246
+ await new Promise((resolve3) => setTimeout(resolve3, 3e3));
1270
1247
  animation.stop();
1271
- await new Promise((resolve2) => setTimeout(resolve2, 300));
1248
+ await new Promise((resolve3) => setTimeout(resolve3, 300));
1272
1249
  await runDeepDiveSession(config, config.conversationId, parentMessageId, initiatingPrompt, rl);
1273
1250
  rl.close();
1274
1251
  } catch (error) {
@@ -1329,8 +1306,8 @@ async function enterDeepDive(config, conversationId, rl) {
1329
1306
  }
1330
1307
  console.log(MODE_DEEPDIVE2(" \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\u255D"));
1331
1308
  console.log("");
1332
- const answer = await new Promise((resolve2) => {
1333
- rl.question(MODE_DEEPDIVE2(" Select (1-" + messages.length + ") or 0 to cancel: "), resolve2);
1309
+ const answer = await new Promise((resolve3) => {
1310
+ rl.question(MODE_DEEPDIVE2(" Select (1-" + messages.length + ") or 0 to cancel: "), resolve3);
1334
1311
  });
1335
1312
  const selection = parseInt(answer.trim());
1336
1313
  if (isNaN(selection) || selection === 0 || selection < 1 || selection > messages.length) {
@@ -1343,13 +1320,13 @@ async function enterDeepDive(config, conversationId, rl) {
1343
1320
  const animation = startDeepDiveAnimation();
1344
1321
  const divePromise = callCloudFunction("initiateCLIDeepDive", { conversationId, parentMessageId, initiatingPrompt });
1345
1322
  try {
1346
- await Promise.all([divePromise, new Promise((resolve2) => setTimeout(resolve2, 3e3))]);
1323
+ await Promise.all([divePromise, new Promise((resolve3) => setTimeout(resolve3, 3e3))]);
1347
1324
  animation.stop();
1348
- await new Promise((resolve2) => setTimeout(resolve2, 300));
1325
+ await new Promise((resolve3) => setTimeout(resolve3, 300));
1349
1326
  await runDeepDiveSession(config, conversationId, parentMessageId, initiatingPrompt, rl);
1350
1327
  } catch (error) {
1351
1328
  animation.stop();
1352
- await new Promise((resolve2) => setTimeout(resolve2, 200));
1329
+ await new Promise((resolve3) => setTimeout(resolve3, 200));
1353
1330
  console.log(ERROR2(` \u274C Could not initiate deep dive: ${error.message}`));
1354
1331
  }
1355
1332
  }
@@ -1369,7 +1346,7 @@ async function runDeepDiveSession(config, conversationId, parentMessageId, initi
1369
1346
  console.log("");
1370
1347
  let lastBobResponse = "";
1371
1348
  let lastConstraints3 = [];
1372
- return new Promise((resolve2) => {
1349
+ return new Promise((resolve3) => {
1373
1350
  const deepDivePrompt = () => {
1374
1351
  rl.question(MODE_DEEPDIVE2(" \u{1F93F} You: "), async (input) => {
1375
1352
  const trimmed = input.trim();
@@ -1384,7 +1361,7 @@ async function runDeepDiveSession(config, conversationId, parentMessageId, initi
1384
1361
  console.log(MODE_DEEPDIVE2(" \u2551") + MUTED2(` Back in: ${conversationId.slice(0, 24)}...`));
1385
1362
  console.log(MODE_DEEPDIVE2(" \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\u255D"));
1386
1363
  console.log("");
1387
- resolve2();
1364
+ resolve3();
1388
1365
  return;
1389
1366
  }
1390
1367
  if (trimmed === "/promote") {
@@ -1400,7 +1377,7 @@ async function runDeepDiveSession(config, conversationId, parentMessageId, initi
1400
1377
  console.log(ERROR2(` \u274C Promote failed: ${error.message}`));
1401
1378
  console.log("");
1402
1379
  }
1403
- resolve2();
1380
+ resolve3();
1404
1381
  return;
1405
1382
  }
1406
1383
  if (trimmed === "/clear") {
@@ -1431,14 +1408,14 @@ async function runDeepDiveSession(config, conversationId, parentMessageId, initi
1431
1408
  console.log(BORDER(" \u2502"));
1432
1409
  console.log(BORDER(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
1433
1410
  console.log("");
1434
- const answer = await new Promise((resolve3) => {
1435
- rl.question(MODE_DEEPDIVE2(" Leave deep dive for main conversation? (y/N): "), resolve3);
1411
+ const answer = await new Promise((resolve4) => {
1412
+ rl.question(MODE_DEEPDIVE2(" Leave deep dive for main conversation? (y/N): "), resolve4);
1436
1413
  });
1437
1414
  if (answer.trim().toLowerCase() === "y") {
1438
1415
  console.log("");
1439
1416
  console.log(SUCCESS2(" \u2705 Surfacing from deep dive. Personalization Mode will activate in main conversation."));
1440
1417
  console.log("");
1441
- resolve2();
1418
+ resolve3();
1442
1419
  return;
1443
1420
  }
1444
1421
  console.log(MUTED2(" Staying in deep dive."));
@@ -1651,7 +1628,7 @@ async function playWelcomeAnimation() {
1651
1628
  await sleep3(800);
1652
1629
  }
1653
1630
  function sleep3(ms) {
1654
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1631
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1655
1632
  }
1656
1633
 
1657
1634
  // src/core/provider-detect.ts
@@ -1944,7 +1921,7 @@ ${content}
1944
1921
  }
1945
1922
  rl.pause();
1946
1923
  const confirmPromptText = ERROR4(` \u{1F5D1}\uFE0F Delete ${filePath}? (y/n): `);
1947
- const confirm = await new Promise((resolve2) => {
1924
+ const confirm = await new Promise((resolve3) => {
1948
1925
  process.stdout.write(confirmPromptText);
1949
1926
  process.stdin.resume();
1950
1927
  process.stdin.setEncoding("utf-8");
@@ -1955,7 +1932,7 @@ ${content}
1955
1932
  inputBuffer += chunk.slice(0, newlineIdx);
1956
1933
  process.stdin.removeListener("data", onData);
1957
1934
  process.stdin.pause();
1958
- resolve2(inputBuffer.replace(/\r/g, "").trim());
1935
+ resolve3(inputBuffer.replace(/\r/g, "").trim());
1959
1936
  } else {
1960
1937
  inputBuffer += chunk;
1961
1938
  }
@@ -2564,8 +2541,8 @@ function registerByokCommand(program2) {
2564
2541
  return;
2565
2542
  }
2566
2543
  const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
2567
- const answer = await new Promise((resolve2) => {
2568
- rl.question(WARNING9(` Remove ${provider} key? (y/n): `), resolve2);
2544
+ const answer = await new Promise((resolve3) => {
2545
+ rl.question(WARNING9(` Remove ${provider} key? (y/n): `), resolve3);
2569
2546
  });
2570
2547
  rl.close();
2571
2548
  if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
@@ -2745,8 +2722,8 @@ function registerConversationsCommand(program2) {
2745
2722
  }
2746
2723
  renderConversationList(conversations, config.conversationId, options.search, result, true);
2747
2724
  const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
2748
- const answer = await new Promise((resolve2) => {
2749
- rl.question(INFO11(" Select (1-" + conversations.length + ") or 0 to cancel: "), resolve2);
2725
+ const answer = await new Promise((resolve3) => {
2726
+ rl.question(INFO11(" Select (1-" + conversations.length + ") or 0 to cancel: "), resolve3);
2750
2727
  });
2751
2728
  rl.close();
2752
2729
  const selection = parseInt(answer.trim());
@@ -2983,7 +2960,7 @@ function truncate(text, max) {
2983
2960
  return text.length > max ? text.slice(0, max - 3) + "..." : text;
2984
2961
  }
2985
2962
  function sleep4(ms) {
2986
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2963
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
2987
2964
  }
2988
2965
 
2989
2966
  // src/commands/fork.ts
@@ -3027,7 +3004,7 @@ function registerForkCommand(program2) {
3027
3004
  try {
3028
3005
  const result = await forkPromise;
3029
3006
  animation.stop();
3030
- await new Promise((resolve2) => setTimeout(resolve2, 200));
3007
+ await new Promise((resolve3) => setTimeout(resolve3, 200));
3031
3008
  if (result?.conversationId) {
3032
3009
  setConfigValue("conversationId", result.conversationId);
3033
3010
  console.log("");
@@ -3061,7 +3038,7 @@ function registerForkCommand(program2) {
3061
3038
  }
3062
3039
  } catch (error) {
3063
3040
  animation.stop();
3064
- await new Promise((resolve2) => setTimeout(resolve2, 200));
3041
+ await new Promise((resolve3) => setTimeout(resolve3, 200));
3065
3042
  console.log("");
3066
3043
  console.log(ERROR10(` \u274C Fork failed: ${error.message}`));
3067
3044
  console.log("");
@@ -3131,7 +3108,7 @@ function registerAnalyseCommand(program2) {
3131
3108
  program2.command("analyse").description("Analyse the current project for bugs, features, improvements, and upgrades").option("--results", "Show analysis dashboard or filtered list").option("--bugs", "Show bugs list (interactive)").option("--features", "Show features list (interactive)").option("--improvements", "Show improvements list (interactive)").option("--upgrades", "Show upgrades list (interactive)").option("--sort <method>", "Sort by: priority (default) or file").option("--search <query>", "Filter results by keyword").option("--status", "Show current analysis job status").option("--auto", "Auto-fix mode: Bob triages and MiniBob implements").option("--confidence <number>", "Confidence gate for auto-fix (default: 90)", "90").option("--priority <level>", "Priority gate for auto-fix: critical, high, medium, low (default: critical)", "critical").action(async (options) => {
3132
3109
  const config = getConfig();
3133
3110
  if (options.auto) {
3134
- const { runAutoFix } = await import("./analyse-auto-XTPO6UCM.js");
3111
+ const { runAutoFix } = await import("./analyse-auto-GCWXNMNZ.js");
3135
3112
  const category = options.bugs ? "bugs" : options.features ? "features" : options.improvements ? "improvements" : options.upgrades ? "upgrades" : void 0;
3136
3113
  await runAutoFix({
3137
3114
  category,
@@ -3141,7 +3118,7 @@ function registerAnalyseCommand(program2) {
3141
3118
  return;
3142
3119
  }
3143
3120
  if (options.bugs || options.features || options.improvements || options.upgrades) {
3144
- const { showInteractiveResults } = await import("./analyse-results-I2QBXA5F.js");
3121
+ const { showInteractiveResults } = await import("./analyse-results-TIVOJ5SC.js");
3145
3122
  const category = options.bugs ? "bugs" : options.features ? "features" : options.improvements ? "improvements" : "upgrades";
3146
3123
  await showInteractiveResults(config, category, options.sort, options.search);
3147
3124
  return;
@@ -3596,8 +3573,8 @@ async function runTier3Autonomy(config) {
3596
3573
  console.log(GREEN(" \u2705 All tasks complete!"));
3597
3574
  console.log(AMBER2(" \u{1F4E4} MiniBob wants to push to GitHub."));
3598
3575
  const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
3599
- const answer = await new Promise((resolve2) => {
3600
- rl.question(CYAN(" Approve push? (y/n): "), resolve2);
3576
+ const answer = await new Promise((resolve3) => {
3577
+ rl.question(CYAN(" Approve push? (y/n): "), resolve3);
3601
3578
  });
3602
3579
  rl.close();
3603
3580
  if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
@@ -3627,7 +3604,7 @@ async function runTier3Autonomy(config) {
3627
3604
  } catch (pollError) {
3628
3605
  }
3629
3606
  if (running) {
3630
- await new Promise((resolve2) => setTimeout(resolve2, 2500));
3607
+ await new Promise((resolve3) => setTimeout(resolve3, 2500));
3631
3608
  }
3632
3609
  }
3633
3610
  console.log("");
@@ -3945,48 +3922,99 @@ function renderLocalTodoList(queue) {
3945
3922
 
3946
3923
  // src/commands/serve.ts
3947
3924
  import chalk19 from "chalk";
3925
+ import * as fs10 from "fs";
3948
3926
  import * as os3 from "os";
3949
3927
  import * as path11 from "path";
3928
+ import * as crypto2 from "crypto";
3929
+ import axios from "axios";
3950
3930
  var GREEN2 = chalk19.hex("#66BB6A");
3951
3931
  var AMBER3 = chalk19.hex("#FFAB00");
3952
- var BLUE2 = chalk19.hex("#42A5F5");
3953
3932
  var RED2 = chalk19.hex("#EF5350");
3954
3933
  var GRAY2 = chalk19.gray;
3955
3934
  var CYAN2 = chalk19.cyan;
3956
3935
  var BORDER5 = chalk19.hex("#455A64");
3936
+ var BOB_DIR3 = path11.join(os3.homedir(), ".bob");
3937
+ var ALGORITHM = "aes-256-cbc";
3957
3938
  var TIER_CONFIGS = {
3958
3939
  "Power": {
3959
3940
  activeInterval: 2e3,
3960
- // 2 seconds
3961
3941
  sleepInterval: 12e4,
3962
- // 120 seconds (2 minutes) when sleeping
3963
3942
  idleThreshold: 5 * 6e4,
3964
- // 5 min → enter sleep
3965
3943
  extendedIdleTimeout: null
3966
- // Never auto-exit
3967
3944
  },
3968
3945
  "Pro": {
3969
3946
  activeInterval: 1e4,
3970
- // 10 seconds
3971
3947
  sleepInterval: 3e4,
3972
- // 30 seconds when sleeping
3973
3948
  idleThreshold: 5 * 6e4,
3974
- // 5 min → enter sleep
3975
3949
  extendedIdleTimeout: 60 * 6e4
3976
- // 1 hour → auto-exit
3977
3950
  },
3978
3951
  "Starter": {
3979
3952
  activeInterval: 15e3,
3980
- // 15 seconds
3981
3953
  sleepInterval: null,
3982
- // No sleep mode — goes straight to auto-exit
3983
3954
  idleThreshold: null,
3984
- // No idle detection
3985
3955
  extendedIdleTimeout: 15 * 6e4
3986
- // 15 minutes → auto-exit
3987
3956
  }
3988
3957
  };
3989
3958
  var BLOCKED_TIERS = ["Explore", "Free", "free", "explore"];
3959
+ function deriveKey(uid, salt) {
3960
+ return crypto2.pbkdf2Sync(uid, salt, 1e5, 32, "sha256");
3961
+ }
3962
+ function encrypt(inputPath, outputPath, uid) {
3963
+ const salt = crypto2.randomBytes(16).toString("hex");
3964
+ const key = deriveKey(uid, salt);
3965
+ const iv = crypto2.randomBytes(16);
3966
+ const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
3967
+ const input = fs10.readFileSync(inputPath);
3968
+ const encrypted = Buffer.concat([cipher.update(input), cipher.final()]);
3969
+ const header = Buffer.from(salt + iv.toString("hex"), "utf-8");
3970
+ fs10.writeFileSync(outputPath, Buffer.concat([header, encrypted]));
3971
+ }
3972
+ function decrypt(inputPath, outputPath, uid) {
3973
+ const data = fs10.readFileSync(inputPath);
3974
+ const header = data.slice(0, 64).toString("utf-8");
3975
+ const salt = header.slice(0, 32);
3976
+ const iv = Buffer.from(header.slice(32, 64), "hex");
3977
+ const encrypted = data.slice(64);
3978
+ const key = deriveKey(uid, salt);
3979
+ const decipher = crypto2.createDecipheriv(ALGORITHM, key, iv);
3980
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
3981
+ fs10.writeFileSync(outputPath, decrypted);
3982
+ }
3983
+ function getTempDir() {
3984
+ const tmpDir = path11.join(os3.tmpdir(), `bob-remote-${Date.now()}`);
3985
+ fs10.mkdirSync(tmpDir, { recursive: true });
3986
+ return tmpDir;
3987
+ }
3988
+ function cleanupTemp(tmpDir) {
3989
+ try {
3990
+ fs10.rmSync(tmpDir, { recursive: true, force: true });
3991
+ } catch {
3992
+ }
3993
+ }
3994
+ var IGNORE_DIRS2 = ["node_modules", ".git", "dist", "build", ".dart_tool", ".idea", ".gradle", ".pub-cache", ".bob"];
3995
+ var CODE_EXTENSIONS2 = /* @__PURE__ */ new Set([".dart", ".js", ".ts", ".html", ".css", ".json", ".yaml", ".yml", ".xml", ".sh", ".md"]);
3996
+ function scanProjectFiles2(rootDir, currentDir, depth = 0) {
3997
+ if (depth > 6) return [];
3998
+ const dir = currentDir || rootDir;
3999
+ const files = [];
4000
+ try {
4001
+ const entries = fs10.readdirSync(dir, { withFileTypes: true });
4002
+ for (const entry of entries) {
4003
+ if (IGNORE_DIRS2.includes(entry.name)) continue;
4004
+ if (entry.name.startsWith(".")) continue;
4005
+ const fullPath = path11.join(dir, entry.name);
4006
+ const relativePath = path11.relative(rootDir, fullPath).replace(/\\/g, "/");
4007
+ if (entry.isDirectory()) {
4008
+ files.push(...scanProjectFiles2(rootDir, fullPath, depth + 1));
4009
+ } else {
4010
+ const ext = path11.extname(entry.name).toLowerCase();
4011
+ if (CODE_EXTENSIONS2.has(ext)) files.push(relativePath);
4012
+ }
4013
+ }
4014
+ } catch {
4015
+ }
4016
+ return files;
4017
+ }
3990
4018
  function registerServeCommand(program2) {
3991
4019
  program2.command("serve").description("Start an Active Bob \u2014 receive and execute commands from the web app or another device").action(async () => {
3992
4020
  const config = getConfig();
@@ -4067,14 +4095,15 @@ async function startActiveBob(config, sessionId, machineId, projectName, tierCon
4067
4095
  if (tierConfig.sleepInterval) {
4068
4096
  console.log(BORDER5(" \u2551") + GRAY2(` Sleep: every ${tierConfig.sleepInterval / 1e3}s after ${tierConfig.idleThreshold / 6e4} min idle`));
4069
4097
  } else {
4070
- console.log(BORDER5(" \u2551") + GRAY2(` Sleep: disabled`));
4098
+ console.log(BORDER5(" \u2551") + GRAY2(" Sleep: disabled"));
4071
4099
  }
4072
4100
  if (tierConfig.extendedIdleTimeout) {
4073
4101
  console.log(BORDER5(" \u2551") + GRAY2(` Auto-exit: after ${tierConfig.extendedIdleTimeout / 6e4} min idle`));
4074
4102
  } else {
4075
- console.log(BORDER5(" \u2551") + GREEN2(` Auto-exit: never (Power tier)`));
4103
+ console.log(BORDER5(" \u2551") + GREEN2(" Auto-exit: never (Power tier)"));
4076
4104
  }
4077
4105
  console.log(BORDER5(" \u2551"));
4106
+ console.log(BORDER5(" \u2551") + GRAY2(" Capabilities: chat, consult, push, index, analyse, backup, restore"));
4078
4107
  console.log(BORDER5(" \u2551") + GRAY2(" Press Ctrl+C to stop."));
4079
4108
  console.log(BORDER5(" \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"));
4080
4109
  console.log("");
@@ -4094,7 +4123,9 @@ async function startActiveBob(config, sessionId, machineId, projectName, tierCon
4094
4123
  return;
4095
4124
  }
4096
4125
  let running = true;
4126
+ let sigintHandlerAdded = false;
4097
4127
  const cleanup = async () => {
4128
+ if (!running) return;
4098
4129
  running = false;
4099
4130
  console.log("");
4100
4131
  console.log(GRAY2(" \u{1F50C} Shutting down Active Bob..."));
@@ -4110,8 +4141,11 @@ async function startActiveBob(config, sessionId, machineId, projectName, tierCon
4110
4141
  console.log("");
4111
4142
  process.exit(0);
4112
4143
  };
4113
- process.on("SIGINT", cleanup);
4114
- process.on("SIGTERM", cleanup);
4144
+ if (!sigintHandlerAdded) {
4145
+ process.once("SIGINT", cleanup);
4146
+ process.once("SIGTERM", cleanup);
4147
+ sigintHandlerAdded = true;
4148
+ }
4115
4149
  let lastCommandTime = Date.now();
4116
4150
  let isSleeping = false;
4117
4151
  while (running) {
@@ -4154,7 +4188,7 @@ async function startActiveBob(config, sessionId, machineId, projectName, tierCon
4154
4188
  }
4155
4189
  lastCommandTime = Date.now();
4156
4190
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
4157
- console.log(AMBER3(` [${timestamp}] \u23F3 Received: ${type} ${payload.message ? '"' + payload.message.slice(0, 40) + (payload.message.length > 40 ? "..." : "") + '"' : ""}`));
4191
+ console.log(AMBER3(` [${timestamp}] \u23F3 Received: ${type}${payload.message ? ` "${payload.message.slice(0, 40)}${payload.message.length > 40 ? "..." : ""}"` : ""}`));
4158
4192
  const commandResult = await executeRemoteCommand(type, payload, config);
4159
4193
  await callCloudFunction("completeRemoteCommand", {
4160
4194
  conversationId: config.conversationId,
@@ -4177,7 +4211,7 @@ async function startActiveBob(config, sessionId, machineId, projectName, tierCon
4177
4211
  }
4178
4212
  }
4179
4213
  if (running) {
4180
- await new Promise((resolve2) => setTimeout(resolve2, currentInterval));
4214
+ await new Promise((resolve3) => setTimeout(resolve3, currentInterval));
4181
4215
  }
4182
4216
  }
4183
4217
  }
@@ -4190,9 +4224,13 @@ async function executeRemoteCommand(type, payload, config) {
4190
4224
  case "push":
4191
4225
  return await executePush(payload);
4192
4226
  case "index":
4193
- return { success: true, message: "Index command received. Feature in progress." };
4227
+ return await executeIndex(payload, config);
4194
4228
  case "analyse":
4195
- return { success: true, message: "Analyse command received. Feature in progress." };
4229
+ return await executeAnalyse(payload, config);
4230
+ case "backup":
4231
+ return await executeBackup(payload, config);
4232
+ case "restore":
4233
+ return await executeRestore(payload, config);
4196
4234
  case "autonomy":
4197
4235
  return { success: true, message: "Autonomy command received. Feature in progress." };
4198
4236
  default:
@@ -4224,9 +4262,7 @@ ${fullContext}` : "") },
4224
4262
  ];
4225
4263
  const response = await callLocalModel(config.localEndpoint, messages);
4226
4264
  const proposed = extractProposedFile(response);
4227
- if (proposed) {
4228
- await proposeAndWriteFile(proposed, true);
4229
- }
4265
+ if (proposed) await proposeAndWriteFile(proposed, true);
4230
4266
  return {
4231
4267
  success: true,
4232
4268
  text: response,
@@ -4261,11 +4297,7 @@ ${fullContext}` : "") },
4261
4297
  { role: "user", content: message }
4262
4298
  ];
4263
4299
  const response = await callLocalModel(config.localEndpoint, messages);
4264
- return {
4265
- success: true,
4266
- text: response,
4267
- referencedFiles: selectedFiles
4268
- };
4300
+ return { success: true, text: response, referencedFiles: selectedFiles };
4269
4301
  } catch (error) {
4270
4302
  return { success: false, error: error.message };
4271
4303
  }
@@ -4279,7 +4311,9 @@ async function executePush(payload) {
4279
4311
  const isRepo = await git.checkIsRepo();
4280
4312
  if (!isRepo) return { success: false, error: "Not a git repository." };
4281
4313
  const status = await git.status();
4282
- if (status.files.length === 0) return { success: true, message: "Nothing to commit. Working tree clean." };
4314
+ if (status.files.length === 0) {
4315
+ return { success: true, message: "Nothing to commit. Working tree clean." };
4316
+ }
4283
4317
  await git.add(".");
4284
4318
  const commitResult = await git.commit(message);
4285
4319
  const branch = (await git.branchLocal()).current;
@@ -4302,6 +4336,336 @@ async function executePush(payload) {
4302
4336
  return { success: false, error: error.message };
4303
4337
  }
4304
4338
  }
4339
+ async function executeIndex(payload, config) {
4340
+ if (!config.localEndpoint) {
4341
+ return { success: false, error: "No local endpoint configured. Index requires a local model." };
4342
+ }
4343
+ const cwd = process.cwd();
4344
+ const projectName = getProjectName(cwd);
4345
+ try {
4346
+ const files = scanProjectFiles2(cwd);
4347
+ if (files.length === 0) {
4348
+ return { success: false, error: "No code files found to index." };
4349
+ }
4350
+ const { runId, runDir, tasksDir } = createAnalysisRun(cwd, files);
4351
+ const summaries = {};
4352
+ let completed = 0;
4353
+ for (const filePath of files) {
4354
+ const absolutePath = path11.join(cwd, filePath);
4355
+ let content;
4356
+ try {
4357
+ content = fs10.readFileSync(absolutePath, "utf-8");
4358
+ } catch {
4359
+ completed++;
4360
+ continue;
4361
+ }
4362
+ if (content.length > 5e4) {
4363
+ summaries[filePath] = `Large file (${Math.round(content.length / 1e3)}KB). Skipped detailed analysis.`;
4364
+ completeTask(tasksDir, filePath, summaries[filePath]);
4365
+ completed++;
4366
+ updateManifestProgress(runDir, completed);
4367
+ continue;
4368
+ }
4369
+ try {
4370
+ const messages = [
4371
+ {
4372
+ role: "system",
4373
+ content: "You are a code analyst. Respond with ONLY a 2-3 sentence summary. No formatting, no headers, no bullets. Just plain sentences."
4374
+ },
4375
+ {
4376
+ role: "user",
4377
+ content: `Summarize this file. What does it do, what does it export, and what does it depend on?
4378
+
4379
+ File: ${filePath}
4380
+
4381
+ ${content}`
4382
+ }
4383
+ ];
4384
+ const summary = await callLocalModel(config.localEndpoint, messages);
4385
+ summaries[filePath] = summary.trim();
4386
+ completeTask(tasksDir, filePath, summary.trim());
4387
+ } catch {
4388
+ summaries[filePath] = "Could not summarize.";
4389
+ }
4390
+ completed++;
4391
+ updateManifestProgress(runDir, completed);
4392
+ }
4393
+ let dependencies = {};
4394
+ try {
4395
+ const summaryContext = Object.entries(summaries).map(([fp, summary]) => `[${fp}]: ${summary}`).join("\n\n");
4396
+ const messages = [
4397
+ {
4398
+ role: "system",
4399
+ content: "You are a senior software architect. Respond with ONLY a valid JSON object. No explanation, no markdown."
4400
+ },
4401
+ {
4402
+ role: "user",
4403
+ content: `Based on these file summaries, generate a JSON dependency map. Each key is a file path, each value is an array of file paths it depends on.
4404
+
4405
+ FILE SUMMARIES:
4406
+ ${summaryContext}
4407
+
4408
+ Respond with ONLY the JSON object:`
4409
+ }
4410
+ ];
4411
+ const depResponse = await callLocalModel(config.localEndpoint, messages);
4412
+ const jsonMatch = depResponse.match(/\{[\s\S]*\}/);
4413
+ if (jsonMatch) {
4414
+ dependencies = JSON.parse(jsonMatch[0]);
4415
+ }
4416
+ } catch {
4417
+ dependencies = {};
4418
+ }
4419
+ saveSummaries(cwd, summaries);
4420
+ saveDependencies(cwd, dependencies);
4421
+ updateManifestProgress(runDir, completed, "completed");
4422
+ return {
4423
+ success: true,
4424
+ message: `Indexed ${Object.keys(summaries).length} files in ${projectName}. Dependency map generated.`,
4425
+ fileCount: Object.keys(summaries).length,
4426
+ projectName
4427
+ };
4428
+ } catch (error) {
4429
+ return { success: false, error: error.message };
4430
+ }
4431
+ }
4432
+ async function executeAnalyse(payload, config) {
4433
+ if (!config.localEndpoint) {
4434
+ return { success: false, error: "No local endpoint configured. Analysis requires a local model." };
4435
+ }
4436
+ const cwd = process.cwd();
4437
+ const projectName = getProjectName(cwd);
4438
+ try {
4439
+ const summaries = loadSummaries(cwd);
4440
+ if (!summaries || Object.keys(summaries).length === 0) {
4441
+ return {
4442
+ success: false,
4443
+ error: `Project not indexed. Run \`bob remote index\` first, or \`bob index\` locally.`
4444
+ };
4445
+ }
4446
+ const dependencies = loadDependencies(cwd) || {};
4447
+ const files = Object.keys(summaries);
4448
+ const { analysisDir } = ensureProjectStructure(cwd);
4449
+ const resultsDir = path11.join(analysisDir, "results");
4450
+ if (!fs10.existsSync(resultsDir)) fs10.mkdirSync(resultsDir, { recursive: true });
4451
+ const allResults = {};
4452
+ let totalBugs = 0;
4453
+ let totalFeatures = 0;
4454
+ let totalImprovements = 0;
4455
+ let totalUpgrades = 0;
4456
+ for (const filePath of files) {
4457
+ const absolutePath = path11.join(cwd, filePath);
4458
+ let content;
4459
+ try {
4460
+ content = fs10.readFileSync(absolutePath, "utf-8");
4461
+ } catch {
4462
+ continue;
4463
+ }
4464
+ if (content.length > 3e4) continue;
4465
+ const fileDeps = dependencies[filePath] || [];
4466
+ let depContext = "";
4467
+ if (fileDeps.length > 0) {
4468
+ depContext = `
4469
+ RELATED FILES:
4470
+ ${fileDeps.map((d) => `- ${d}: ${summaries[d] || "unknown"}`).join("\n")}
4471
+ `;
4472
+ }
4473
+ const analysisPrompt = `You are the Lead QA Engineer on this project. Perform a thorough code review.
4474
+
4475
+ For each issue provide: title, description (why it's a problem), priority (critical/high/medium/low), implementation (exact fix steps).
4476
+
4477
+ Respond with ONLY a JSON object:
4478
+ {
4479
+ "bugs": [{"title": "...", "description": "...", "priority": "...", "implementation": "..."}],
4480
+ "features": [{"title": "...", "description": "...", "priority": "...", "implementation": "..."}],
4481
+ "improvements": [{"title": "...", "description": "...", "priority": "...", "implementation": "..."}],
4482
+ "upgrades": [{"title": "...", "description": "...", "priority": "...", "implementation": "..."}]
4483
+ }
4484
+ ${depContext}
4485
+ FILE: ${filePath}
4486
+ ${content}`;
4487
+ try {
4488
+ const messages = [
4489
+ { role: "system", content: "You are the Lead QA Engineer. Respond with ONLY valid JSON. Quality over quantity." },
4490
+ { role: "user", content: analysisPrompt }
4491
+ ];
4492
+ const responseText = await callLocalModel(config.localEndpoint, messages);
4493
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
4494
+ if (jsonMatch) {
4495
+ const parsed = JSON.parse(jsonMatch[0]);
4496
+ for (const cat of ["bugs", "features", "improvements", "upgrades"]) {
4497
+ if (parsed[cat]) {
4498
+ parsed[cat] = parsed[cat].map((item) => ({ ...item, filePath }));
4499
+ }
4500
+ }
4501
+ allResults[filePath] = parsed;
4502
+ totalBugs += parsed.bugs?.length || 0;
4503
+ totalFeatures += parsed.features?.length || 0;
4504
+ totalImprovements += parsed.improvements?.length || 0;
4505
+ totalUpgrades += parsed.upgrades?.length || 0;
4506
+ }
4507
+ } catch {
4508
+ continue;
4509
+ }
4510
+ }
4511
+ fs10.writeFileSync(
4512
+ path11.join(resultsDir, "analysis.json"),
4513
+ JSON.stringify(allResults, null, 2)
4514
+ );
4515
+ fs10.writeFileSync(
4516
+ path11.join(resultsDir, "counts.json"),
4517
+ JSON.stringify({ bugs: totalBugs, features: totalFeatures, improvements: totalImprovements, upgrades: totalUpgrades }, null, 2)
4518
+ );
4519
+ return {
4520
+ success: true,
4521
+ message: `Analysis complete for ${projectName}. Found: ${totalBugs} bugs, ${totalFeatures} features, ${totalImprovements} improvements, ${totalUpgrades} upgrades.`,
4522
+ counts: { bugs: totalBugs, features: totalFeatures, improvements: totalImprovements, upgrades: totalUpgrades },
4523
+ projectName
4524
+ };
4525
+ } catch (error) {
4526
+ return { success: false, error: error.message };
4527
+ }
4528
+ }
4529
+ async function executeBackup(payload, config) {
4530
+ const { isSource = false, isGlobal = false, archiveName } = payload;
4531
+ if (!config.uid) {
4532
+ return { success: false, error: "User UID not available. Re-login required." };
4533
+ }
4534
+ const cwd = process.cwd();
4535
+ const projectName = path11.basename(cwd);
4536
+ let sourceDir;
4537
+ let displayLabel;
4538
+ if (isGlobal) {
4539
+ sourceDir = BOB_DIR3;
4540
+ displayLabel = "global (~/.bob/)";
4541
+ } else if (isSource) {
4542
+ sourceDir = cwd;
4543
+ displayLabel = `source: ${projectName}`;
4544
+ } else {
4545
+ sourceDir = path11.join(BOB_DIR3, "projects", projectName);
4546
+ displayLabel = `context: ${projectName}`;
4547
+ }
4548
+ if (!fs10.existsSync(sourceDir)) {
4549
+ return { success: false, error: `Source directory not found: ${sourceDir}` };
4550
+ }
4551
+ const tmpDir = getTempDir();
4552
+ const archivePath = path11.join(tmpDir, "bob-backup.tar.gz");
4553
+ const encryptedPath = path11.join(tmpDir, "bob-backup.bob.enc");
4554
+ try {
4555
+ const tar = await import("tar");
4556
+ const relativeSource = path11.relative(os3.homedir(), sourceDir);
4557
+ await tar.create(
4558
+ { gzip: true, file: archivePath, cwd: os3.homedir() },
4559
+ [relativeSource]
4560
+ );
4561
+ const archiveStats = fs10.statSync(archivePath);
4562
+ const estimatedSizeGB = archiveStats.size / (1024 * 1024 * 1024);
4563
+ const sizeLabel = archiveStats.size < 1024 * 1024 ? `${(archiveStats.size / 1024).toFixed(1)} KB` : `${(archiveStats.size / (1024 * 1024)).toFixed(1)} MB`;
4564
+ encrypt(archivePath, encryptedPath, config.uid);
4565
+ let uploadResult;
4566
+ const action = archiveName ? isSource ? "requestSourceArchiveUpload" : "requestArchiveUpload" : isSource ? "requestSourceUpload" : "requestUpload";
4567
+ uploadResult = await callCloudFunction("cliBackupLicense", {
4568
+ action,
4569
+ projectName,
4570
+ isGlobal,
4571
+ isSource,
4572
+ archiveName: archiveName || null,
4573
+ estimatedSizeGB
4574
+ });
4575
+ const encryptedData = fs10.readFileSync(encryptedPath);
4576
+ await axios.put(uploadResult.uploadUrl, encryptedData, {
4577
+ headers: {
4578
+ "Content-Type": "application/octet-stream",
4579
+ "Content-Length": encryptedData.length
4580
+ },
4581
+ maxBodyLength: Infinity,
4582
+ maxContentLength: Infinity
4583
+ });
4584
+ const recordAction = archiveName ? isSource ? "recordSourceArchiveUsage" : "recordArchiveUsage" : isSource ? "recordSourceUsage" : "recordUsage";
4585
+ try {
4586
+ await callCloudFunction("cliBackupLicense", {
4587
+ action: recordAction,
4588
+ projectName,
4589
+ isGlobal,
4590
+ isSource,
4591
+ archiveId: uploadResult.archiveId || null
4592
+ });
4593
+ } catch {
4594
+ }
4595
+ const label = archiveName ? `archive "${archiveName}"` : "backup";
4596
+ return {
4597
+ success: true,
4598
+ message: `Remote ${label} complete for ${displayLabel}. Size: ${sizeLabel}.`,
4599
+ sizeBytes: archiveStats.size,
4600
+ projectName,
4601
+ isSource,
4602
+ isGlobal
4603
+ };
4604
+ } catch (error) {
4605
+ return { success: false, error: `Backup failed: ${error.message}` };
4606
+ } finally {
4607
+ cleanupTemp(tmpDir);
4608
+ }
4609
+ }
4610
+ async function executeRestore(payload, config) {
4611
+ const { isSource = false, isGlobal = false } = payload;
4612
+ if (!config.uid) {
4613
+ return { success: false, error: "User UID not available. Re-login required." };
4614
+ }
4615
+ const cwd = process.cwd();
4616
+ const projectName = path11.basename(cwd);
4617
+ const tmpDir = getTempDir();
4618
+ const downloadPath = path11.join(tmpDir, "bob-backup.bob.enc");
4619
+ const decryptedPath = path11.join(tmpDir, "bob-backup.tar.gz");
4620
+ try {
4621
+ const downloadResult = await callCloudFunction("cliBackupLicense", {
4622
+ action: isSource ? "requestSourceDownload" : "requestDownload",
4623
+ projectName,
4624
+ isGlobal,
4625
+ isSource,
4626
+ s3VersionId: null
4627
+ // always latest in headless mode
4628
+ });
4629
+ const response = await axios.get(downloadResult.downloadUrl, {
4630
+ responseType: "arraybuffer",
4631
+ maxContentLength: Infinity
4632
+ });
4633
+ fs10.writeFileSync(downloadPath, Buffer.from(response.data));
4634
+ decrypt(downloadPath, decryptedPath, config.uid);
4635
+ let restoreTarget;
4636
+ if (isGlobal) {
4637
+ restoreTarget = BOB_DIR3;
4638
+ } else if (isSource) {
4639
+ restoreTarget = cwd;
4640
+ } else {
4641
+ restoreTarget = path11.join(BOB_DIR3, "projects", projectName);
4642
+ }
4643
+ const preRestoreBackup = `${restoreTarget}-pre-restore-${Date.now()}`;
4644
+ if (fs10.existsSync(restoreTarget)) {
4645
+ fs10.cpSync(restoreTarget, preRestoreBackup, { recursive: true });
4646
+ }
4647
+ const tar = await import("tar");
4648
+ if (isSource) {
4649
+ const parentDir = path11.dirname(cwd);
4650
+ await tar.extract({ file: decryptedPath, cwd: parentDir });
4651
+ } else {
4652
+ await tar.extract({ file: decryptedPath, cwd: os3.homedir() });
4653
+ }
4654
+ const scopeLabel = isGlobal ? "global (~/.bob/)" : isSource ? `source: ${projectName}` : `context: ${projectName}`;
4655
+ return {
4656
+ success: true,
4657
+ message: `Remote restore complete for ${scopeLabel}. Pre-restore backup saved locally.`,
4658
+ projectName,
4659
+ isSource,
4660
+ isGlobal,
4661
+ preRestoreBackup: path11.basename(preRestoreBackup)
4662
+ };
4663
+ } catch (error) {
4664
+ return { success: false, error: `Restore failed: ${error.message}` };
4665
+ } finally {
4666
+ cleanupTemp(tmpDir);
4667
+ }
4668
+ }
4305
4669
 
4306
4670
  // src/commands/remote.ts
4307
4671
  import chalk20 from "chalk";
@@ -4316,7 +4680,7 @@ var ERROR12 = chalk20.hex("#EF5350");
4316
4680
  var MUTED14 = chalk20.hex("#78909C");
4317
4681
  var BORDER6 = chalk20.hex("#455A64");
4318
4682
  function registerRemoteCommand(program2) {
4319
- program2.command("remote [type] [message]").description("Send commands to an Active Bob on a remote machine").option("--new", "Discover and connect to a different Active Bob").option("--auto", "For analyse: run auto-fix mode").option("-i, --interactive", "Enter interactive remote session").option("--session <id>", "Target a specific Active Bob session").action(async (type, message, options) => {
4683
+ program2.command("remote [type] [message]").description("Send commands to an Active Bob on a remote machine").option("--new", "Discover and connect to a different Active Bob").option("--auto", "For analyse: run auto-fix mode").option("-i, --interactive", "Enter interactive remote session").option("--session <id>", "Target a specific Active Bob session").option("--source", "For backup/restore: use source code mode").option("--global", "For backup/restore: use global mode (Grid only)").option("--archive <name>", "For backup: save as named archive").action(async (type, message, options) => {
4320
4684
  const config = getConfig();
4321
4685
  if (!config.loggedIn || !config.authToken) {
4322
4686
  console.log("");
@@ -4337,7 +4701,16 @@ function registerRemoteCommand(program2) {
4337
4701
  await showConnectionStatus(config);
4338
4702
  return;
4339
4703
  }
4340
- const validTypes = ["chat", "consult", "index", "analyse", "push", "autonomy"];
4704
+ const validTypes = [
4705
+ "chat",
4706
+ "consult",
4707
+ "index",
4708
+ "analyse",
4709
+ "push",
4710
+ "autonomy",
4711
+ "backup",
4712
+ "restore"
4713
+ ];
4341
4714
  if (!validTypes.includes(type)) {
4342
4715
  console.log("");
4343
4716
  console.log(ERROR12(` \u274C Invalid command type: "${type}"`));
@@ -4348,6 +4721,9 @@ function registerRemoteCommand(program2) {
4348
4721
  const payload = { conversationId: config.conversationId };
4349
4722
  if (message) payload.message = message;
4350
4723
  if (options.auto) payload.auto = true;
4724
+ if (options.source) payload.isSource = true;
4725
+ if (options.global) payload.isGlobal = true;
4726
+ if (options.archive) payload.archiveName = options.archive;
4351
4727
  if ((type === "chat" || type === "consult" || type === "push") && !message) {
4352
4728
  console.log("");
4353
4729
  console.log(ERROR12(` \u274C ${type} requires a message.`));
@@ -4383,17 +4759,22 @@ async function runInteractiveRemote(config, targetSession) {
4383
4759
  }
4384
4760
  console.log("");
4385
4761
  console.log(BORDER6(" \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"));
4386
- console.log(BORDER6(" \u2551") + INFO14(` \u{1F310} Active Bob \u2014 Remote Session`) + MUTED14(` (${activeBobName})`));
4762
+ console.log(BORDER6(" \u2551") + INFO14(" \u{1F310} Active Bob \u2014 Remote Session") + MUTED14(` (${activeBobName})`));
4387
4763
  console.log(BORDER6(" \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"));
4388
4764
  console.log(BORDER6(" \u2551") + MUTED14(` Conversation: ${config.conversationId?.slice(0, 28)}...`));
4389
4765
  console.log(BORDER6(" \u2551") + MUTED14(" Commands dispatched to the remote machine."));
4390
4766
  console.log(BORDER6(" \u2551"));
4391
4767
  console.log(BORDER6(" \u2551") + chalk20.white(" Slash Commands:"));
4392
- console.log(BORDER6(" \u2551") + MUTED14(' \u25B8 /consult "msg" \u2014 Strategic advice'));
4393
- console.log(BORDER6(" \u2551") + MUTED14(' \u25B8 /push "msg" \u2014 Git commit & push'));
4394
- console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /index \u2014 Re-index project"));
4395
- console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /analyse \u2014 Run analysis"));
4396
- console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /exit \u2014 Disconnect"));
4768
+ console.log(BORDER6(" \u2551") + MUTED14(' \u25B8 /consult "msg" \u2014 Strategic advice'));
4769
+ console.log(BORDER6(" \u2551") + MUTED14(' \u25B8 /push "msg" \u2014 Git commit & push'));
4770
+ console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /index \u2014 Re-index project"));
4771
+ console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /analyse \u2014 Run QA analysis"));
4772
+ console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /backup \u2014 Backup context"));
4773
+ console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /backup source \u2014 Backup source code"));
4774
+ console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /backup global \u2014 Backup ~/.bob/ (Grid)"));
4775
+ console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /restore \u2014 Restore latest context"));
4776
+ console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /restore source \u2014 Restore latest source"));
4777
+ console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /exit \u2014 Disconnect"));
4397
4778
  console.log(BORDER6(" \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"));
4398
4779
  console.log("");
4399
4780
  const rl = readline7.createInterface({
@@ -4417,7 +4798,7 @@ async function runInteractiveRemote(config, targetSession) {
4417
4798
  if (trimmed.startsWith("/consult ")) {
4418
4799
  const msg = trimmed.slice(9).trim().replace(/^["']|["']$/g, "");
4419
4800
  if (msg) {
4420
- await dispatchAndShow(config, "consult", { message: msg, conversationId: config.conversationId }, targetSession);
4801
+ await dispatchAndShow(config, "consult", { message: msg }, targetSession);
4421
4802
  } else {
4422
4803
  console.log(ERROR12(' \u274C Provide a message: /consult "your question"'));
4423
4804
  }
@@ -4427,7 +4808,7 @@ async function runInteractiveRemote(config, targetSession) {
4427
4808
  if (trimmed.startsWith("/push ")) {
4428
4809
  const msg = trimmed.slice(6).trim().replace(/^["']|["']$/g, "");
4429
4810
  if (msg) {
4430
- await dispatchAndShow(config, "push", { message: msg, conversationId: config.conversationId }, targetSession);
4811
+ await dispatchAndShow(config, "push", { message: msg }, targetSession);
4431
4812
  } else {
4432
4813
  console.log(ERROR12(' \u274C Provide a commit message: /push "your message"'));
4433
4814
  }
@@ -4435,28 +4816,53 @@ async function runInteractiveRemote(config, targetSession) {
4435
4816
  return;
4436
4817
  }
4437
4818
  if (trimmed === "/index") {
4438
- await dispatchAndShow(config, "index", { conversationId: config.conversationId }, targetSession);
4819
+ await dispatchAndShow(config, "index", {}, targetSession);
4439
4820
  prompt();
4440
4821
  return;
4441
4822
  }
4442
4823
  if (trimmed === "/analyse" || trimmed === "/analyze") {
4443
- await dispatchAndShow(config, "analyse", { conversationId: config.conversationId }, targetSession);
4824
+ await dispatchAndShow(config, "analyse", {}, targetSession);
4825
+ prompt();
4826
+ return;
4827
+ }
4828
+ if (trimmed.startsWith("/backup")) {
4829
+ const parts = trimmed.split(" ");
4830
+ const flag = parts[1]?.toLowerCase();
4831
+ const backupPayload = {};
4832
+ if (flag === "source") backupPayload.isSource = true;
4833
+ if (flag === "global") backupPayload.isGlobal = true;
4834
+ await dispatchAndShow(config, "backup", backupPayload, targetSession);
4835
+ prompt();
4836
+ return;
4837
+ }
4838
+ if (trimmed.startsWith("/restore")) {
4839
+ const parts = trimmed.split(" ");
4840
+ const flag = parts[1]?.toLowerCase();
4841
+ const restorePayload = {};
4842
+ if (flag === "source") restorePayload.isSource = true;
4843
+ if (flag === "global") restorePayload.isGlobal = true;
4844
+ await dispatchAndShow(config, "restore", restorePayload, targetSession);
4444
4845
  prompt();
4445
4846
  return;
4446
4847
  }
4447
- await dispatchAndShow(config, "chat", { message: trimmed, conversationId: config.conversationId }, targetSession);
4848
+ await dispatchAndShow(config, "chat", { message: trimmed }, targetSession);
4448
4849
  prompt();
4449
4850
  });
4450
4851
  };
4451
4852
  prompt();
4452
4853
  }
4453
4854
  async function dispatchAndShow(config, type, payload, targetSession) {
4454
- const spinner = ora7({ text: BRAND_SECONDARY13(` \u{1F4E1} Active Bob executing: ${type}...`), spinner: "dots" }).start();
4855
+ const spinner = ora7({
4856
+ text: BRAND_SECONDARY13(` \u{1F4E1} Active Bob executing: ${type}...`),
4857
+ spinner: "dots"
4858
+ }).start();
4859
+ let pollCount = 0;
4860
+ const MAX_POLLS = 300;
4455
4861
  try {
4456
4862
  const result = await callCloudFunction("sendRemoteCommand", {
4457
4863
  conversationId: config.conversationId,
4458
4864
  type,
4459
- payload,
4865
+ payload: { ...payload, conversationId: config.conversationId },
4460
4866
  targetSession: targetSession || null
4461
4867
  });
4462
4868
  if (!result?.success) {
@@ -4466,7 +4872,8 @@ async function dispatchAndShow(config, type, payload, targetSession) {
4466
4872
  return;
4467
4873
  }
4468
4874
  const commandId = result.commandId;
4469
- while (true) {
4875
+ while (pollCount < MAX_POLLS) {
4876
+ pollCount++;
4470
4877
  try {
4471
4878
  const pollResult = await callCloudFunction("getRemoteCommandResult", {
4472
4879
  conversationId: config.conversationId,
@@ -4491,6 +4898,14 @@ async function dispatchAndShow(config, type, payload, targetSession) {
4491
4898
  } else if (pollResult.result?.message) {
4492
4899
  console.log("");
4493
4900
  console.log(SUCCESS14(` \u2705 ${pollResult.result.message}`));
4901
+ if (pollResult.result?.counts) {
4902
+ const c = pollResult.result.counts;
4903
+ console.log(MUTED14(` \u{1F41B} ${c.bugs} bugs \u2B50 ${c.features} features \u{1F527} ${c.improvements} improvements \u2B06\uFE0F ${c.upgrades} upgrades`));
4904
+ }
4905
+ if (pollResult.result?.sizeBytes) {
4906
+ const sizeLabel = pollResult.result.sizeBytes < 1024 * 1024 ? `${(pollResult.result.sizeBytes / 1024).toFixed(1)} KB` : `${(pollResult.result.sizeBytes / (1024 * 1024)).toFixed(1)} MB`;
4907
+ console.log(MUTED14(` \u{1F4E6} Size: ${sizeLabel}`));
4908
+ }
4494
4909
  } else if (pollResult.result?.error) {
4495
4910
  console.log("");
4496
4911
  console.log(ERROR12(` \u274C ${pollResult.result.error}`));
@@ -4507,8 +4922,13 @@ async function dispatchAndShow(config, type, payload, targetSession) {
4507
4922
  }
4508
4923
  } catch {
4509
4924
  }
4510
- await new Promise((resolve2) => setTimeout(resolve2, 2e3));
4925
+ await new Promise((resolve3) => setTimeout(resolve3, 2e3));
4511
4926
  }
4927
+ spinner.stop();
4928
+ console.log("");
4929
+ console.log(WARNING13(" \u26A0\uFE0F Command timed out after 10 minutes."));
4930
+ console.log(MUTED14(" The command may still be running on the remote machine."));
4931
+ console.log("");
4512
4932
  } catch (error) {
4513
4933
  spinner.stop();
4514
4934
  console.log(ERROR12(` \u274C ${error.message}`));
@@ -4551,12 +4971,17 @@ async function showConnectionStatus(config) {
4551
4971
  console.log("");
4552
4972
  if (activeSessions.length > 0) {
4553
4973
  console.log(MUTED14(" Commands:"));
4554
- console.log(MUTED14(" \u25B8 bob remote --interactive \u2014 Persistent session"));
4555
- console.log(MUTED14(' \u25B8 bob remote chat "message" \u2014 One-shot'));
4556
- console.log(MUTED14(' \u25B8 bob remote consult "msg" \u2014 Strategic advice'));
4557
- console.log(MUTED14(' \u25B8 bob remote push "msg" \u2014 Git push'));
4558
- console.log(MUTED14(" \u25B8 bob remote index \u2014 Re-index"));
4559
- console.log(MUTED14(" \u25B8 bob remote analyse \u2014 Run analysis"));
4974
+ console.log(MUTED14(" \u25B8 bob remote --interactive \u2014 Persistent session"));
4975
+ console.log(MUTED14(' \u25B8 bob remote chat "message" \u2014 One-shot chat'));
4976
+ console.log(MUTED14(' \u25B8 bob remote consult "msg" \u2014 Strategic advice'));
4977
+ console.log(MUTED14(' \u25B8 bob remote push "msg" \u2014 Git push'));
4978
+ console.log(MUTED14(" \u25B8 bob remote index \u2014 Re-index project"));
4979
+ console.log(MUTED14(" \u25B8 bob remote analyse \u2014 Run QA analysis"));
4980
+ console.log(MUTED14(" \u25B8 bob remote backup \u2014 Backup context"));
4981
+ console.log(MUTED14(" \u25B8 bob remote backup --source \u2014 Backup source code"));
4982
+ console.log(MUTED14(" \u25B8 bob remote backup --global \u2014 Backup ~/.bob/ (Grid)"));
4983
+ console.log(MUTED14(" \u25B8 bob remote restore \u2014 Restore latest"));
4984
+ console.log(MUTED14(" \u25B8 bob remote restore --source \u2014 Restore latest source"));
4560
4985
  console.log("");
4561
4986
  }
4562
4987
  } catch (error) {
@@ -4591,8 +5016,8 @@ async function discoverAndConnect(config) {
4591
5016
  console.log(BORDER6(" \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"));
4592
5017
  console.log("");
4593
5018
  const rl = readline7.createInterface({ input: process.stdin, output: process.stdout });
4594
- const answer = await new Promise((resolve2) => {
4595
- rl.question(INFO14(" Select (1-" + bobs.length + ") or 0 to cancel: "), resolve2);
5019
+ const answer = await new Promise((resolve3) => {
5020
+ rl.question(INFO14(" Select (1-" + bobs.length + ") or 0 to cancel: "), resolve3);
4596
5021
  });
4597
5022
  rl.close();
4598
5023
  const selection = parseInt(answer.trim());
@@ -5016,7 +5441,7 @@ async function runCloudProfiler(options) {
5016
5441
  }
5017
5442
  }
5018
5443
  function sleep5(ms) {
5019
- return new Promise((resolve2) => setTimeout(resolve2, ms));
5444
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
5020
5445
  }
5021
5446
 
5022
5447
  // src/ui/profile-dashboard.ts
@@ -5298,7 +5723,7 @@ async function renderProfileDashboard() {
5298
5723
  import * as path12 from "path";
5299
5724
  var AMBER5 = chalk23.hex("#FFAB00");
5300
5725
  var GREEN4 = chalk23.hex("#66BB6A");
5301
- var BLUE3 = chalk23.hex("#42A5F5");
5726
+ var BLUE2 = chalk23.hex("#42A5F5");
5302
5727
  var GRAY4 = chalk23.gray;
5303
5728
  var CYAN4 = chalk23.cyan;
5304
5729
  var RED4 = chalk23.hex("#EF5350");
@@ -5749,1187 +6174,720 @@ function getWeekNumber(date) {
5749
6174
  return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
5750
6175
  }
5751
6176
 
5752
- // src/commands/userbob.ts
6177
+ // src/commands/backup.ts
5753
6178
  import chalk24 from "chalk";
5754
- import * as readline8 from "readline";
5755
- import * as fs10 from "fs";
6179
+ import ora11 from "ora";
6180
+ import * as fs11 from "fs";
5756
6181
  import * as path13 from "path";
5757
6182
  import * as os4 from "os";
5758
- var PURPLE = chalk24.hex("#AB47BC");
5759
- var AMBER6 = chalk24.hex("#FFAB00");
6183
+ import * as crypto3 from "crypto";
6184
+ import axios2 from "axios";
6185
+ import inquirer from "inquirer";
6186
+ var BRAND = chalk24.hex("#E66F24");
6187
+ var CYAN5 = chalk24.cyan;
5760
6188
  var GREEN5 = chalk24.hex("#66BB6A");
5761
- var CYAN5 = chalk24.hex("#26C6DA");
5762
6189
  var RED5 = chalk24.hex("#EF5350");
6190
+ var AMBER6 = chalk24.hex("#FFAB00");
5763
6191
  var GRAY5 = chalk24.gray;
5764
- var BLUE4 = chalk24.hex("#42A5F5");
5765
- var BOB_COLOR = chalk24.hex("#E66F24");
5766
6192
  var BORDER10 = chalk24.hex("#455A64");
5767
- var WHITE3 = chalk24.white;
5768
- var BOB_DIR3 = path13.join(os4.homedir(), ".bob");
5769
- function getSessionFilePath() {
5770
- const projectName = path13.basename(process.cwd());
5771
- return path13.join(BOB_DIR3, "projects", projectName, "userbob-session.json");
5772
- }
5773
- function writeSessionFile(data) {
5774
- const filePath = getSessionFilePath();
5775
- const dir = path13.dirname(filePath);
5776
- if (!fs10.existsSync(dir)) fs10.mkdirSync(dir, { recursive: true });
5777
- fs10.writeFileSync(filePath, JSON.stringify(data, null, 2));
5778
- }
5779
- function readSessionFile() {
5780
- const filePath = getSessionFilePath();
5781
- if (!fs10.existsSync(filePath)) return null;
6193
+ var BOB_DIR4 = path13.join(os4.homedir(), ".bob");
6194
+ var ALGORITHM2 = "aes-256-cbc";
6195
+ function deriveKey2(uid, salt) {
6196
+ return crypto3.pbkdf2Sync(uid, salt, 1e5, 32, "sha256");
6197
+ }
6198
+ function encrypt2(inputPath, outputPath, uid) {
6199
+ const salt = crypto3.randomBytes(16).toString("hex");
6200
+ const key = deriveKey2(uid, salt);
6201
+ const iv = crypto3.randomBytes(16);
6202
+ const cipher = crypto3.createCipheriv(ALGORITHM2, key, iv);
6203
+ const input = fs11.readFileSync(inputPath);
6204
+ const encrypted = Buffer.concat([cipher.update(input), cipher.final()]);
6205
+ const header = Buffer.from(salt + iv.toString("hex"), "utf-8");
6206
+ fs11.writeFileSync(outputPath, Buffer.concat([header, encrypted]));
6207
+ }
6208
+ function decrypt2(inputPath, outputPath, uid) {
6209
+ const data = fs11.readFileSync(inputPath);
6210
+ const header = data.slice(0, 64).toString("utf-8");
6211
+ const salt = header.slice(0, 32);
6212
+ const iv = Buffer.from(header.slice(32, 64), "hex");
6213
+ const encrypted = data.slice(64);
6214
+ const key = deriveKey2(uid, salt);
6215
+ const decipher = crypto3.createDecipheriv(ALGORITHM2, key, iv);
6216
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
6217
+ fs11.writeFileSync(outputPath, decrypted);
6218
+ }
6219
+ function formatBytes(bytes) {
6220
+ if (bytes < 1024) return `${bytes} B`;
6221
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
6222
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
6223
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
6224
+ }
6225
+ function getTempDir2() {
6226
+ const tmpDir = path13.join(os4.tmpdir(), `bob-backup-${Date.now()}`);
6227
+ fs11.mkdirSync(tmpDir, { recursive: true });
6228
+ return tmpDir;
6229
+ }
6230
+ function cleanupTemp2(tmpDir) {
5782
6231
  try {
5783
- return JSON.parse(fs10.readFileSync(filePath, "utf-8"));
6232
+ fs11.rmSync(tmpDir, { recursive: true, force: true });
5784
6233
  } catch {
5785
- return null;
5786
6234
  }
5787
6235
  }
5788
- function clearSessionFile() {
5789
- const filePath = getSessionFilePath();
5790
- if (fs10.existsSync(filePath)) fs10.unlinkSync(filePath);
6236
+ function requireAuth(config) {
6237
+ if (!config.loggedIn || !config.authToken) {
6238
+ console.log("");
6239
+ console.log(RED5(" \u274C Backup requires authentication."));
6240
+ console.log(GRAY5(" Run `bob login` to authenticate."));
6241
+ console.log("");
6242
+ return false;
6243
+ }
6244
+ return true;
6245
+ }
6246
+ function getCurrentProjectName() {
6247
+ return path13.basename(process.cwd());
5791
6248
  }
5792
- function renderHUD(sat, target, stag, stagTarget, div, divTarget, grading) {
5793
- const satBar = sat >= target ? GREEN5(`${sat}%`) : sat >= target * 0.7 ? AMBER6(`${sat}%`) : RED5(`${sat}%`);
5794
- console.log("");
5795
- console.log(BORDER10(" \u2500\u2500\u2500 MISSION CONTROL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5796
- console.log(
5797
- ` SAT: ${satBar} \u2192 ${target}% \u2502 STAG: ${stag}/${stagTarget > 0 ? stagTarget : "\u221E"} \u2502 DIV: ${div}/${divTarget > 0 ? divTarget : "\u221E"} \u2502 GRADE: ${grading}`
5798
- );
5799
- console.log(BORDER10(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5800
- console.log("");
6249
+ function getProjectBackupDir(projectName) {
6250
+ return path13.join(BOB_DIR4, "projects", projectName);
5801
6251
  }
5802
- function stripMarkdown(text) {
5803
- return text.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^---+$/gm, "").replace(/^>\s?/gm, "").replace(/`([^`]+)`/g, "$1").replace(/^\s*[-*+]\s+/gm, " \u2022 ").replace(/^\s*\d+\.\s+/gm, " ").replace(/\n{3,}/g, "\n\n").trim();
6252
+ function normalizeFilePath(filePath) {
6253
+ return filePath.replace(/\\/g, "/");
5804
6254
  }
5805
- function renderMessage(sender, message, audit) {
5806
- const cleanMsg = stripMarkdown(message);
5807
- const maxWidth = 70;
5808
- const lines = wrapText2(cleanMsg, maxWidth - 4);
5809
- if (sender === "userBob") {
5810
- const topBar = PURPLE(` \u250C\u2500 UserBob ${"\u2500".repeat(maxWidth - 13)}\u2510`);
5811
- const bottomBar = PURPLE(` \u2514${"\u2500".repeat(maxWidth - 2)}\u2518`);
6255
+ function handleBackupError(error) {
6256
+ if (error.message?.includes("BOB_BACKUP_LICENSE_REQUIRED")) {
5812
6257
  console.log("");
5813
- console.log(topBar);
5814
- for (const line of lines) {
5815
- const padded = line.padEnd(maxWidth - 4);
5816
- console.log(PURPLE(" \u2502") + ` ${padded}` + PURPLE(" \u2502"));
5817
- }
5818
- console.log(bottomBar);
5819
- if (audit) {
5820
- const chips = [];
5821
- if (audit.satisfactionScore !== void 0) chips.push(CYAN5(`[SAT: ${audit.satisfactionScore}%]`));
5822
- if (audit.resemblanceScore !== void 0) chips.push(BLUE4(`[RES: ${audit.resemblanceScore}%]`));
5823
- if (audit.reasoning) chips.push(GRAY5(`[${String(audit.reasoning).slice(0, 50)}...]`));
5824
- if (chips.length > 0) console.log(" " + chips.join(" "));
5825
- }
5826
- } else if (sender === "bob") {
5827
- const indent = " ";
5828
- const topBar = BOB_COLOR(`${indent}\u250C${"\u2500".repeat(maxWidth - 12)}\u2500 Bob \u2500\u2510`);
5829
- const bottomBar = BOB_COLOR(`${indent}\u2514${"\u2500".repeat(maxWidth - 2)}\u2518`);
6258
+ console.log(AMBER6(" \u26A0\uFE0F No active backup license found."));
6259
+ console.log(GRAY5(" Purchase one at: app.bobsworkshop.com/iap \u2192 Bob Backup"));
6260
+ } else if (error.message?.includes("STORAGE_QUOTA_EXCEEDED")) {
5830
6261
  console.log("");
5831
- console.log(topBar);
5832
- for (const line of lines) {
5833
- const padded = line.padEnd(maxWidth - 4);
5834
- console.log(BOB_COLOR(`${indent}\u2502`) + ` ${padded}` + BOB_COLOR(" \u2502"));
5835
- }
5836
- console.log(bottomBar);
5837
- } else if (sender === "system") {
6262
+ console.log(AMBER6(" \u26A0\uFE0F Storage quota exceeded."));
6263
+ console.log(GRAY5(" Purchase a storage pack: app.bobsworkshop.com/iap \u2192 Storage Packs"));
6264
+ } else if (error.message?.includes("ARCHIVE_SLOTS_EXHAUSTED")) {
6265
+ console.log("");
6266
+ console.log(AMBER6(" \u26A0\uFE0F All archive slots are used."));
6267
+ console.log(GRAY5(" Upgrade your Workshop SKU for more archive slots."));
6268
+ } else if (error.message?.includes("GRID_REQUIRED")) {
5838
6269
  console.log("");
5839
- console.log(CYAN5(" \u2500\u2500 SYSTEM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5840
- console.log(GRAY5(` ${cleanMsg}`));
5841
- console.log(CYAN5(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6270
+ console.log(AMBER6(" \u26A0\uFE0F Global backup requires the Grid Workshop plan."));
6271
+ console.log(GRAY5(" Upgrade at: app.bobsworkshop.com/iap \u2192 Workshop"));
5842
6272
  } else {
5843
6273
  console.log("");
5844
- console.log(GRAY5(` [${sender.toUpperCase()}] ${cleanMsg}`));
6274
+ console.log(RED5(` \u274C ${error.message}`));
5845
6275
  }
6276
+ console.log("");
5846
6277
  }
5847
- function wrapText2(text, maxWidth) {
5848
- const lines = [];
5849
- const paragraphs = text.split("\n");
5850
- for (const paragraph of paragraphs) {
5851
- if (paragraph.trim() === "") {
5852
- lines.push("");
5853
- continue;
5854
- }
5855
- const words = paragraph.split(" ");
5856
- let currentLine = "";
5857
- for (const word of words) {
5858
- if ((currentLine + " " + word).trim().length > maxWidth) {
5859
- if (currentLine) lines.push(currentLine.trim());
5860
- currentLine = word;
6278
+ function loadGitignorePatterns(projectDir) {
6279
+ const gitignorePath = path13.join(projectDir, ".gitignore");
6280
+ const defaultIgnore = [
6281
+ "node_modules",
6282
+ ".git",
6283
+ "dist",
6284
+ "build",
6285
+ ".dart_tool",
6286
+ ".idea",
6287
+ ".gradle",
6288
+ ".pub-cache",
6289
+ "*.log",
6290
+ ".env",
6291
+ ".env.local",
6292
+ ".env.*",
6293
+ "coverage",
6294
+ ".nyc_output"
6295
+ ];
6296
+ if (!fs11.existsSync(gitignorePath)) return defaultIgnore;
6297
+ try {
6298
+ const lines = fs11.readFileSync(gitignorePath, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
6299
+ return [...defaultIgnore, ...lines];
6300
+ } catch {
6301
+ return defaultIgnore;
6302
+ }
6303
+ }
6304
+ function shouldIgnore(filePath, patterns) {
6305
+ const parts = filePath.split(/[/\\]/);
6306
+ return patterns.some((pattern) => {
6307
+ const clean = pattern.replace(/^\//, "").replace(/\/$/, "");
6308
+ return parts.some((part) => {
6309
+ if (clean.includes("*")) {
6310
+ const regex = new RegExp("^" + clean.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
6311
+ return regex.test(part);
6312
+ }
6313
+ return part === clean;
6314
+ });
6315
+ });
6316
+ }
6317
+ async function runBackup(options) {
6318
+ const { config, projectName, isGlobal, archiveName, sourceDir, displayName } = options;
6319
+ console.log("");
6320
+ console.log(BORDER10(" \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"));
6321
+ console.log(BORDER10(" \u2551") + BRAND(" \u2601\uFE0F Bob Backup ") + BORDER10("\u2551"));
6322
+ console.log(BORDER10(" \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"));
6323
+ console.log(BORDER10(" \u2551") + GRAY5(` Scope: ${isGlobal ? "\u{1F310} Global (~/.bob/)" : `\u{1F4C1} Project: ${projectName}`}`));
6324
+ if (archiveName) console.log(BORDER10(" \u2551") + GRAY5(` Archive: "${archiveName}"`));
6325
+ console.log(BORDER10(" \u2551"));
6326
+ if (!fs11.existsSync(sourceDir)) {
6327
+ console.log(BORDER10(" \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"));
6328
+ console.log("");
6329
+ console.log(RED5(` \u274C Source directory not found: ${sourceDir}`));
6330
+ console.log(GRAY5(` Run \`bob index\` first to initialize this project.`));
6331
+ console.log("");
6332
+ return;
6333
+ }
6334
+ const tmpDir = getTempDir2();
6335
+ const archivePath = path13.join(tmpDir, "bob-backup.tar.gz");
6336
+ const encryptedPath = path13.join(tmpDir, "bob-backup.bob.enc");
6337
+ try {
6338
+ const compressSpinner = ora11({ text: GRAY5(" Compressing ..."), spinner: "dots" }).start();
6339
+ const tar = await import("tar");
6340
+ const relativeSource = path13.relative(os4.homedir(), sourceDir);
6341
+ await tar.create(
6342
+ { gzip: true, file: archivePath, cwd: os4.homedir() },
6343
+ [relativeSource]
6344
+ );
6345
+ const archiveStats = fs11.statSync(archivePath);
6346
+ const estimatedSizeGB = archiveStats.size / (1024 * 1024 * 1024);
6347
+ compressSpinner.succeed(GREEN5(` Compressing ${displayName} ...`) + GRAY5(` ${formatBytes(archiveStats.size)}`));
6348
+ const encryptSpinner = ora11({ text: GRAY5(" Encrypting archive ..."), spinner: "dots" }).start();
6349
+ encrypt2(archivePath, encryptedPath, config.uid);
6350
+ const encryptedStats = fs11.statSync(encryptedPath);
6351
+ encryptSpinner.succeed(GREEN5(" Encrypting archive ...") + GRAY5(` ${formatBytes(encryptedStats.size)}`));
6352
+ const urlSpinner = ora11({ text: GRAY5(" Requesting upload authorization ..."), spinner: "dots" }).start();
6353
+ let uploadResult;
6354
+ try {
6355
+ if (archiveName) {
6356
+ uploadResult = await callCloudFunction("cliBackupLicense", {
6357
+ action: "requestArchiveUpload",
6358
+ projectName,
6359
+ isGlobal,
6360
+ isSource: false,
6361
+ archiveName,
6362
+ estimatedSizeGB
6363
+ });
5861
6364
  } else {
5862
- currentLine = currentLine ? currentLine + " " + word : word;
6365
+ uploadResult = await callCloudFunction("cliBackupLicense", {
6366
+ action: "requestUpload",
6367
+ projectName,
6368
+ isGlobal,
6369
+ isSource: false,
6370
+ estimatedSizeGB
6371
+ });
5863
6372
  }
6373
+ urlSpinner.succeed(GREEN5(" Requesting upload authorization ..."));
6374
+ } catch (error) {
6375
+ urlSpinner.fail(RED5(" \u274C Authorization failed."));
6376
+ console.log(BORDER10(" \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"));
6377
+ handleBackupError(error);
6378
+ return;
5864
6379
  }
5865
- if (currentLine.trim()) lines.push(currentLine.trim());
5866
- }
5867
- return lines;
5868
- }
5869
- async function handleSlashCommand(input, config, conversationId) {
5870
- const trimmed = input.trim();
5871
- if (trimmed === "/status") {
6380
+ const uploadSpinner = ora11({ text: GRAY5(" Uploading to S3 ..."), spinner: "dots" }).start();
6381
+ const encryptedData = fs11.readFileSync(encryptedPath);
6382
+ await axios2.put(uploadResult.uploadUrl, encryptedData, {
6383
+ headers: { "Content-Type": "application/octet-stream", "Content-Length": encryptedData.length },
6384
+ maxBodyLength: Infinity,
6385
+ maxContentLength: Infinity
6386
+ });
6387
+ uploadSpinner.succeed(GREEN5(" Uploading to S3 ..."));
6388
+ const recordSpinner = ora11({ text: GRAY5(" Recording usage ..."), spinner: "dots" }).start();
5872
6389
  try {
5873
- const response = await callCloudFunction("getCLIConversationMessages", { conversationId, since: null });
5874
- const state = response?.state || {};
5875
- console.log("");
5876
- console.log(AMBER6(" \u2500\u2500\u2500 Current Parameters \u2500\u2500\u2500"));
5877
- console.log(GRAY5(` Target Satisfaction : ${state.targetSatisfaction ?? "N/A"}`));
5878
- console.log(GRAY5(` Grading Standard : ${state.gradingStandard ?? "N/A"}`));
5879
- console.log(GRAY5(` Current Satisfaction: ${state.currentSatisfaction ?? "N/A"}`));
5880
- console.log(GRAY5(` Stalemate : ${state.stalemateState?.current ?? 0}/${state.stalemateState?.target ?? "\u221E"}`));
5881
- console.log(GRAY5(` Divergence : ${state.divergenceState?.current ?? 0}/${state.divergenceState?.target ?? "\u221E"}`));
5882
- console.log(GRAY5(` Status : ${state.simulationStatus ?? "UNKNOWN"}`));
5883
- console.log(GRAY5(` Active : ${state.userBobActive ?? "UNKNOWN"}`));
5884
- console.log("");
6390
+ if (archiveName && uploadResult.archiveId) {
6391
+ await callCloudFunction("cliBackupLicense", {
6392
+ action: "recordArchiveUsage",
6393
+ projectName,
6394
+ isGlobal,
6395
+ isSource: false,
6396
+ archiveId: uploadResult.archiveId
6397
+ });
6398
+ } else {
6399
+ await callCloudFunction("cliBackupLicense", {
6400
+ action: "recordUsage",
6401
+ projectName,
6402
+ isGlobal,
6403
+ isSource: false
6404
+ });
6405
+ }
6406
+ recordSpinner.succeed(GREEN5(" Recording usage ..."));
5885
6407
  } catch {
5886
- console.log(RED5(" \u274C Could not fetch conversation state."));
6408
+ recordSpinner.warn(AMBER6(" Usage recording failed (non-fatal). Backup was saved."));
6409
+ }
6410
+ const now = (/* @__PURE__ */ new Date()).toLocaleString();
6411
+ console.log(BORDER10(" \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"));
6412
+ if (archiveName) {
6413
+ console.log(BORDER10(" \u2551") + GREEN5(` \u2705 Archive saved: "${archiveName}" \u2014 ${now}`));
6414
+ } else {
6415
+ console.log(BORDER10(" \u2551") + GREEN5(` \u2705 Backup complete \u2014 ${now}`));
6416
+ }
6417
+ console.log(BORDER10(" \u2551") + GRAY5(" Run `bob backup list` to see all revisions."));
6418
+ console.log(BORDER10(" \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"));
6419
+ console.log("");
6420
+ } catch (error) {
6421
+ console.log(BORDER10(" \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"));
6422
+ handleBackupError(error);
6423
+ } finally {
6424
+ cleanupTemp2(tmpDir);
6425
+ }
6426
+ }
6427
+ async function runSourceBackup(options) {
6428
+ const { config, projectName, projectDir, archiveName } = options;
6429
+ const filePath = options.filePath ? normalizeFilePath(options.filePath) : void 0;
6430
+ const isFileMode = !!filePath;
6431
+ const scopeLabel = isFileMode ? `\u{1F4C4} File: ${filePath}` : `\u{1F4BE} Source: ${projectName}`;
6432
+ console.log("");
6433
+ console.log(BORDER10(" \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"));
6434
+ console.log(BORDER10(" \u2551") + BRAND(" \u2601\uFE0F Bob Source Backup ") + BORDER10("\u2551"));
6435
+ console.log(BORDER10(" \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"));
6436
+ console.log(BORDER10(" \u2551") + GRAY5(` Scope: ${scopeLabel}`));
6437
+ if (archiveName) console.log(BORDER10(" \u2551") + GRAY5(` Archive: "${archiveName}"`));
6438
+ console.log(BORDER10(" \u2551"));
6439
+ if (isFileMode) {
6440
+ const absoluteFilePath = path13.resolve(projectDir, filePath);
6441
+ if (!fs11.existsSync(absoluteFilePath)) {
6442
+ console.log(BORDER10(" \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"));
6443
+ console.log("");
6444
+ console.log(RED5(` \u274C File not found: ${filePath}`));
6445
+ console.log("");
6446
+ return;
6447
+ }
6448
+ } else {
6449
+ if (!fs11.existsSync(projectDir)) {
6450
+ console.log(BORDER10(" \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"));
6451
+ console.log("");
6452
+ console.log(RED5(` \u274C Project directory not found: ${projectDir}`));
6453
+ console.log("");
6454
+ return;
5887
6455
  }
5888
- return;
5889
6456
  }
5890
- const setMatch = trimmed.match(/^\/set\s+(grading|target|stag|div)\s+(\d+)$/i);
5891
- if (setMatch) {
5892
- const param = setMatch[1].toLowerCase();
5893
- const value = parseInt(setMatch[2], 10);
5894
- const paramMap = {
5895
- grading: "gradingStandard",
5896
- target: "targetSatisfaction",
5897
- stag: "stalemateZone",
5898
- div: "divergenceThreshold"
6457
+ const tmpDir = getTempDir2();
6458
+ const archivePath = path13.join(tmpDir, "bob-source.tar.gz");
6459
+ const encryptedPath = path13.join(tmpDir, "bob-source.bob.enc");
6460
+ try {
6461
+ const compressSpinner = ora11({ text: GRAY5(" Compressing source ..."), spinner: "dots" }).start();
6462
+ const tar = await import("tar");
6463
+ if (isFileMode) {
6464
+ const relativeFilePath = path13.normalize(filePath).replace(/\\/g, "/");
6465
+ await tar.create(
6466
+ { gzip: true, file: archivePath, cwd: projectDir },
6467
+ [relativeFilePath]
6468
+ );
6469
+ } else {
6470
+ const ignorePatterns = loadGitignorePatterns(projectDir);
6471
+ const parentDir = path13.dirname(projectDir);
6472
+ const projectDirName = path13.basename(projectDir);
6473
+ await tar.create(
6474
+ {
6475
+ gzip: true,
6476
+ file: archivePath,
6477
+ cwd: parentDir,
6478
+ filter: (fp) => !shouldIgnore(fp, ignorePatterns)
6479
+ },
6480
+ [projectDirName]
6481
+ );
6482
+ }
6483
+ const archiveStats = fs11.statSync(archivePath);
6484
+ const estimatedSizeGB = archiveStats.size / (1024 * 1024 * 1024);
6485
+ compressSpinner.succeed(
6486
+ GREEN5(` Compressing ${isFileMode ? filePath : projectName} ...`) + GRAY5(` ${formatBytes(archiveStats.size)}`)
6487
+ );
6488
+ const encryptSpinner = ora11({ text: GRAY5(" Encrypting ..."), spinner: "dots" }).start();
6489
+ encrypt2(archivePath, encryptedPath, config.uid);
6490
+ const encryptedStats = fs11.statSync(encryptedPath);
6491
+ encryptSpinner.succeed(GREEN5(" Encrypting ...") + GRAY5(` ${formatBytes(encryptedStats.size)}`));
6492
+ const urlSpinner = ora11({ text: GRAY5(" Requesting upload authorization ..."), spinner: "dots" }).start();
6493
+ let uploadResult;
6494
+ const uploadPayload = {
6495
+ action: archiveName ? "requestSourceArchiveUpload" : "requestSourceUpload",
6496
+ projectName,
6497
+ isGlobal: false,
6498
+ isSource: true,
6499
+ filePath: filePath || null,
6500
+ archiveName: archiveName || null,
6501
+ estimatedSizeGB
5899
6502
  };
5900
6503
  try {
5901
- await callHTTPFunction("userSimManagerService", {
5902
- action: "updateParameters",
5903
- conversationId,
5904
- uid: config.uid,
5905
- email: config.email,
5906
- params: { [paramMap[param]]: value }
5907
- });
5908
- console.log(GREEN5(` \u2705 ${param} updated to ${value}`));
5909
- } catch (e) {
5910
- console.log(RED5(` \u274C Failed to update ${param}: ${e.message}`));
6504
+ uploadResult = await callCloudFunction("cliBackupLicense", uploadPayload);
6505
+ urlSpinner.succeed(GREEN5(" Requesting upload authorization ..."));
6506
+ } catch (error) {
6507
+ urlSpinner.fail(RED5(" \u274C Authorization failed."));
6508
+ console.log(BORDER10(" \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"));
6509
+ handleBackupError(error);
6510
+ return;
5911
6511
  }
5912
- return;
5913
- }
5914
- const injectMatch = trimmed.match(/^\/inject\s+"(.+)"$/);
5915
- if (injectMatch) {
5916
- const note = injectMatch[1];
6512
+ const uploadSpinner = ora11({ text: GRAY5(" Uploading to S3 ..."), spinner: "dots" }).start();
6513
+ const encryptedData = fs11.readFileSync(encryptedPath);
6514
+ await axios2.put(uploadResult.uploadUrl, encryptedData, {
6515
+ headers: { "Content-Type": "application/octet-stream", "Content-Length": encryptedData.length },
6516
+ maxBodyLength: Infinity,
6517
+ maxContentLength: Infinity
6518
+ });
6519
+ uploadSpinner.succeed(GREEN5(" Uploading to S3 ..."));
6520
+ const recordSpinner = ora11({ text: GRAY5(" Recording usage ..."), spinner: "dots" }).start();
6521
+ const recordPayload = {
6522
+ action: archiveName ? "recordSourceArchiveUsage" : "recordSourceUsage",
6523
+ projectName,
6524
+ isGlobal: false,
6525
+ isSource: true,
6526
+ filePath: filePath || null,
6527
+ archiveId: uploadResult.archiveId || null
6528
+ };
5917
6529
  try {
5918
- await callHTTPFunction("userSimManagerService", {
5919
- action: "injectNote",
5920
- conversationId,
5921
- uid: config.uid,
5922
- email: config.email,
5923
- note
5924
- });
5925
- console.log(GREEN5(` \u2705 Director's note injected.`));
5926
- } catch (e) {
5927
- console.log(RED5(` \u274C Failed to inject note: ${e.message}`));
6530
+ await callCloudFunction("cliBackupLicense", recordPayload);
6531
+ recordSpinner.succeed(GREEN5(" Recording usage ..."));
6532
+ } catch {
6533
+ recordSpinner.warn(AMBER6(" Usage recording failed (non-fatal). Backup was saved."));
5928
6534
  }
5929
- return;
6535
+ const now = (/* @__PURE__ */ new Date()).toLocaleString();
6536
+ console.log(BORDER10(" \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"));
6537
+ if (archiveName) {
6538
+ console.log(BORDER10(" \u2551") + GREEN5(` \u2705 Source archive saved: "${archiveName}" \u2014 ${now}`));
6539
+ } else {
6540
+ console.log(BORDER10(" \u2551") + GREEN5(` \u2705 Source backup complete \u2014 ${now}`));
6541
+ }
6542
+ console.log(BORDER10(" \u2551") + GRAY5(` ${isFileMode ? `File: ${filePath}` : `Project: ${projectName} (gitignore respected)`}`));
6543
+ console.log(BORDER10(" \u2551") + GRAY5(" Run `bob backup list --source` to see all source revisions."));
6544
+ console.log(BORDER10(" \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"));
6545
+ console.log("");
6546
+ } catch (error) {
6547
+ console.log(BORDER10(" \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"));
6548
+ handleBackupError(error);
6549
+ } finally {
6550
+ cleanupTemp2(tmpDir);
5930
6551
  }
5931
- console.log(GRAY5(' Commands: /set grading|target|stag|div <n> /inject "note" /status /abort'));
5932
6552
  }
5933
- async function runPlatformSimulation(config, conversationId, mission, params) {
5934
- await callHTTPFunction("userSimManagerService", {
5935
- action: "updateParameters",
5936
- conversationId,
5937
- uid: config.uid,
5938
- email: config.email,
5939
- params: {
5940
- targetSatisfaction: params.target,
5941
- gradingStandard: params.grading,
5942
- stalemateZone: params.stag,
5943
- divergenceThreshold: params.div
5944
- }
5945
- });
5946
- await callHTTPFunction("userSimManagerService", {
5947
- action: "injectNote",
5948
- conversationId,
5949
- uid: config.uid,
5950
- email: config.email,
5951
- note: mission
5952
- });
5953
- console.log(GREEN5(" \u2705 Mission injected. Simulation is running."));
5954
- console.log("");
5955
- console.log(BORDER10(" \u2500\u2500\u2500 LIVE SIMULATION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5956
- console.log(GRAY5(" Messages will stream below as Bob and UserBob interact."));
5957
- console.log(GRAY5(" You can type commands at any time:"));
5958
- console.log("");
5959
- console.log(AMBER6(" /abort") + GRAY5(" \u2014 Stop the simulation immediately"));
5960
- console.log(AMBER6(" /set target 90") + GRAY5(" \u2014 Update satisfaction target"));
5961
- console.log(AMBER6(" /set grading 70") + GRAY5(" \u2014 Update Teacher's Curve"));
5962
- console.log(AMBER6(" /set stag 5") + GRAY5(" \u2014 Update stalemate threshold"));
5963
- console.log(AMBER6(" /set div 3") + GRAY5(" \u2014 Update divergence threshold"));
5964
- console.log(AMBER6(' /inject "note"') + GRAY5(" \u2014 Inject a director's note mid-session"));
5965
- console.log(AMBER6(" /status") + GRAY5(" \u2014 Show current simulation parameters"));
5966
- console.log("");
5967
- console.log(BORDER10(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5968
- console.log("");
5969
- let running = true;
5970
- let lastMessageTimestamp = 0;
5971
- let hudState = { sat: 0, target: params.target, stag: 0, stagTarget: params.stag, div: 0, divTarget: params.div, grading: params.grading };
5972
- const sigintHandler = async () => {
5973
- if (!running) return;
5974
- running = false;
5975
- console.log("\n");
5976
- console.log(AMBER6(" \u{1F6D1} Aborting simulation..."));
5977
- try {
5978
- await callHTTPFunction("userSimManagerService", {
5979
- action: "abortMission",
5980
- conversationId,
5981
- uid: config.uid,
5982
- email: config.email
6553
+ function registerBackupCommand(program2) {
6554
+ const backupCmd = program2.command("backup").description("Encrypt and upload your Bob CLI data to secure cloud storage");
6555
+ backupCmd.command("create").description("Create a new encrypted backup").option("--archive <name>", "Save as a named archive snapshot").option("--global", "Back up entire ~/.bob/ (Grid Workshop SKU required)").option("--source", "Back up actual project source code (gitignore respected)").option("--file <path>", "Back up a single file (use with --source)").action(async (options) => {
6556
+ const config = getConfig();
6557
+ if (!requireAuth(config)) return;
6558
+ const projectName = getCurrentProjectName();
6559
+ const projectDir = process.cwd();
6560
+ if (options.source) {
6561
+ await runSourceBackup({
6562
+ config,
6563
+ projectName,
6564
+ projectDir,
6565
+ filePath: options.file,
6566
+ archiveName: options.archive
5983
6567
  });
5984
- console.log(GREEN5(" \u2705 Simulation aborted."));
5985
- } catch {
6568
+ return;
5986
6569
  }
5987
- process.exit(0);
5988
- };
5989
- process.on("SIGINT", sigintHandler);
5990
- const rl = readline8.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
5991
- rl.setPrompt("");
5992
- rl.on("line", async (line) => {
5993
- const trimmed = line.trim();
5994
- if (!trimmed) return;
5995
- if (trimmed === "/abort" || trimmed === "abort") {
5996
- running = false;
5997
- console.log(AMBER6(" \u{1F6D1} Aborting simulation..."));
5998
- try {
5999
- await callHTTPFunction("userSimManagerService", {
6000
- action: "abortMission",
6001
- conversationId,
6002
- uid: config.uid,
6003
- email: config.email
6004
- });
6005
- console.log(GREEN5(" \u2705 Simulation aborted."));
6006
- } catch {
6007
- }
6008
- rl.close();
6009
- process.exit(0);
6570
+ const isGlobal = options.global || false;
6571
+ let sourceDir;
6572
+ let displayName;
6573
+ if (isGlobal) {
6574
+ sourceDir = BOB_DIR4;
6575
+ displayName = "~/.bob/ (global)";
6576
+ } else {
6577
+ sourceDir = getProjectBackupDir(projectName);
6578
+ displayName = `~/.bob/projects/${projectName}/`;
6010
6579
  }
6011
- await handleSlashCommand(trimmed, config, conversationId);
6580
+ await runBackup({
6581
+ config,
6582
+ projectName,
6583
+ isGlobal,
6584
+ archiveName: options.archive,
6585
+ sourceDir,
6586
+ displayName
6587
+ });
6012
6588
  });
6013
- while (running) {
6014
- await new Promise((r) => setTimeout(r, 3e3));
6589
+ backupCmd.command("list").description("List all backup revisions and named archives").option("--global", "Show global backup revisions").option("--source", "Show source code backup revisions").option("--file <path>", "Show revisions for a specific file (use with --source)").action(async (options) => {
6590
+ const config = getConfig();
6591
+ if (!requireAuth(config)) return;
6592
+ const isGlobal = options.global || false;
6593
+ const isSource = options.source || false;
6594
+ const projectName = getCurrentProjectName();
6595
+ const filePath = options.file ? normalizeFilePath(options.file) : null;
6596
+ const spinner = ora11({ text: CYAN5(" Loading backup history..."), spinner: "dots" }).start();
6015
6597
  try {
6016
- const response = await callCloudFunction("getCLIConversationMessages", {
6017
- conversationId,
6018
- since: lastMessageTimestamp || null
6598
+ const result = await callCloudFunction("getCLIBackupStatus", {
6599
+ projectName,
6600
+ isGlobal,
6601
+ isSource,
6602
+ filePath
6019
6603
  });
6020
- const messages = response?.messages || [];
6021
- const state = response?.state || {};
6022
- for (const msg of messages) {
6023
- renderMessage(msg.sender, msg.message, msg.simulationAudit);
6024
- if (msg.timestamp && msg.timestamp > lastMessageTimestamp) {
6025
- lastMessageTimestamp = msg.timestamp;
6604
+ spinner.stop();
6605
+ const { storage, retention, lastBackedUpAt, versions, archives } = result;
6606
+ const scopeLabel = isGlobal ? "\u{1F310} Global" : isSource ? filePath ? `\u{1F4C4} ${filePath}` : `\u{1F4BE} Source: ${projectName}` : `\u{1F4C1} ${projectName}`;
6607
+ console.log("");
6608
+ console.log(BORDER10(" \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"));
6609
+ console.log(BORDER10(" \u2551") + BRAND(" \u2601\uFE0F Bob Backup Status ") + BORDER10("\u2551"));
6610
+ console.log(BORDER10(" \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"));
6611
+ console.log(BORDER10(" \u2551") + GRAY5(` Scope: ${scopeLabel}`));
6612
+ console.log(BORDER10(" \u2551") + GRAY5(` Storage: ${storage.usedGB.toFixed(2)} GB / ${storage.totalGB} GB (${storage.usedPercent}% used)`));
6613
+ console.log(BORDER10(" \u2551") + GRAY5(` Retention: ${retention.months} months`));
6614
+ console.log(BORDER10(" \u2551") + GRAY5(` Archives: ${retention.usedArchiveSlots}/${retention.archiveSlots} slots used`));
6615
+ if (lastBackedUpAt) {
6616
+ console.log(BORDER10(" \u2551") + GRAY5(` Last backup: ${new Date(lastBackedUpAt).toLocaleString()}`));
6617
+ }
6618
+ if (versions.length > 0) {
6619
+ console.log(BORDER10(" \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"));
6620
+ console.log(BORDER10(" \u2551") + CYAN5(" Revisions"));
6621
+ console.log(BORDER10(" \u2551") + GRAY5(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6622
+ for (const v of versions) {
6623
+ const date = new Date(v.lastModified).toLocaleString();
6624
+ const label = v.isLatest ? GREEN5(` ${v.label.padEnd(10)}`) : GRAY5(` ${v.label.padEnd(10)}`);
6625
+ console.log(BORDER10(" \u2551") + `${label} ${GRAY5(formatBytes(v.sizeBytes).padEnd(10))} ${GRAY5(date)}`);
6026
6626
  }
6027
- }
6028
- if (state.currentSatisfaction !== void 0) hudState.sat = state.currentSatisfaction;
6029
- if (state.targetSatisfaction !== void 0) hudState.target = state.targetSatisfaction;
6030
- if (state.gradingStandard !== void 0) hudState.grading = state.gradingStandard;
6031
- if (state.stalemateState) {
6032
- hudState.stag = state.stalemateState.current ?? hudState.stag;
6033
- hudState.stagTarget = state.stalemateState.target ?? hudState.stagTarget;
6034
- }
6035
- if (state.divergenceState) {
6036
- hudState.div = state.divergenceState.current ?? hudState.div;
6037
- hudState.divTarget = state.divergenceState.target ?? hudState.divTarget;
6038
- }
6039
- if (state.userBobActive === false || state.simulationStatus && state.simulationStatus !== "RUNNING") {
6040
- if (messages.length > 0) {
6041
- renderHUD(hudState.sat, hudState.target, hudState.stag, hudState.stagTarget, hudState.div, hudState.divTarget, hudState.grading);
6627
+ } else {
6628
+ console.log(BORDER10(" \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"));
6629
+ console.log(BORDER10(" \u2551") + GRAY5(` No revisions found.`));
6630
+ }
6631
+ if (archives.length > 0) {
6632
+ console.log(BORDER10(" \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"));
6633
+ console.log(BORDER10(" \u2551") + AMBER6(" Named Archives"));
6634
+ console.log(BORDER10(" \u2551") + GRAY5(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6635
+ for (const a of archives) {
6636
+ const date = new Date(a.createdAt).toLocaleString();
6637
+ const expires = new Date(a.expiresAt).toLocaleString();
6638
+ console.log(BORDER10(" \u2551") + ` \u{1F4CC} ${AMBER6(a.name.padEnd(24))} ${GRAY5(formatBytes(a.sizeGB * 1024 * 1024 * 1024))}`);
6639
+ console.log(BORDER10(" \u2551") + GRAY5(` Created: ${date} | Expires: ${expires}`));
6042
6640
  }
6043
- console.log("");
6044
- console.log(AMBER6(` \u{1F3C1} Simulation ended: ${state.simulationStatus || "INACTIVE"}`));
6045
- console.log("");
6046
- running = false;
6047
- break;
6048
6641
  }
6049
- if (messages.length > 0) {
6050
- renderHUD(hudState.sat, hudState.target, hudState.stag, hudState.stagTarget, hudState.div, hudState.divTarget, hudState.grading);
6051
- }
6052
- } catch (e) {
6053
- console.log(RED5(` \u274C Poll error: ${e.message}`));
6054
- }
6055
- }
6056
- rl.close();
6057
- process.removeListener("SIGINT", sigintHandler);
6058
- }
6059
- async function runLocalSimulation(config, dnaString, mission, params) {
6060
- writeSessionFile({ active: true, turns: 0, mission });
6061
- let running = true;
6062
- let turns = 0;
6063
- let conversationHistory = [];
6064
- let sat = 0;
6065
- let stalemateCurrent = 0;
6066
- let divergenceCurrent = 0;
6067
- let lastStatus = "";
6068
- const sigintHandler = () => {
6069
- running = false;
6070
- writeSessionFile({ active: false });
6071
- clearSessionFile();
6072
- console.log("\n" + AMBER6(" \u{1F6D1} Simulation stopped."));
6073
- process.exit(0);
6074
- };
6075
- process.on("SIGINT", sigintHandler);
6076
- const rl = readline8.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
6077
- rl.setPrompt("");
6078
- rl.on("line", (line) => {
6079
- const t = line.trim();
6080
- if (t === "/abort" || t === "abort") {
6081
- running = false;
6082
- writeSessionFile({ active: false });
6083
- clearSessionFile();
6084
- console.log(AMBER6(" \u{1F6D1} Simulation stopped."));
6085
- rl.close();
6086
- process.exit(0);
6087
- }
6088
- if (t.startsWith("/set ")) {
6089
- const m = t.match(/^\/set\s+(grading|target|stag|div)\s+(\d+)$/i);
6090
- if (m) {
6091
- const val = parseInt(m[2], 10);
6092
- if (m[1] === "grading") params.grading = val;
6093
- if (m[1] === "target") params.target = val;
6094
- if (m[1] === "stag") params.stag = val;
6095
- if (m[1] === "div") params.div = val;
6096
- console.log(GREEN5(` \u2705 ${m[1]} updated to ${val} (local)`));
6097
- }
6098
- }
6099
- if (t === "/status") {
6642
+ console.log(BORDER10(" \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"));
6100
6643
  console.log("");
6101
- console.log(AMBER6(" \u2500\u2500\u2500 Local Sim Parameters \u2500\u2500\u2500"));
6102
- console.log(GRAY5(` Target: ${params.target} \u2502 Grading: ${params.grading} \u2502 Stag Limit: ${params.stag} \u2502 Div Limit: ${params.div}`));
6103
- console.log(GRAY5(` Current SAT: ${sat} \u2502 Turns: ${turns} \u2502 Stag: ${stalemateCurrent} \u2502 Div: ${divergenceCurrent}`));
6644
+ console.log(GRAY5(" Commands:"));
6645
+ console.log(GRAY5(" bob backup create \u2014 Context backup"));
6646
+ console.log(GRAY5(" bob backup create --source \u2014 Full source backup"));
6647
+ console.log(GRAY5(" bob backup create --source --file <path> \u2014 Single file backup"));
6648
+ console.log(GRAY5(" bob backup create --global \u2014 Full ~/.bob/ (Grid)"));
6649
+ console.log(GRAY5(" bob backup restore \u2014 Restore context"));
6650
+ console.log(GRAY5(" bob backup restore --source \u2014 Restore source"));
6651
+ console.log(GRAY5(" bob backup restore --source --file <path> \u2014 Restore single file"));
6104
6652
  console.log("");
6653
+ } catch (error) {
6654
+ spinner.stop();
6655
+ handleBackupError(error);
6105
6656
  }
6106
6657
  });
6107
- const bobSystem = `You are Bob \u2014 a senior AI engineering consultant. A developer's digital twin (UserBob) is evaluating your work. Respond helpfully and directly to advance the mission. Mission context: ${mission}`;
6108
- const userBobSystem = dnaString ? `You are a digital twin of a software engineer. You ARE this developer. Your personality, communication style, and engineering philosophy are defined below.
6109
-
6110
- Mission: ${mission}
6111
-
6112
- ${dnaString}
6113
-
6114
- After each Bob response, evaluate it 0-100 on how well it advances YOUR mission. Reply with your natural reaction, then append exactly one JSON footer on its own line:
6115
- {"satisfactionScore": <0-100>, "status": "CONVERGING|STAGNATING|DIVERGING"}` : `You are a digital twin of a software engineer. You have no personal profile loaded \u2014 respond based on the mission context only.
6116
-
6117
- Mission: ${mission}
6118
-
6119
- After each Bob response, evaluate it 0-100 on mission alignment. Reply with your reaction, then append exactly one JSON footer on its own line:
6120
- {"satisfactionScore": <0-100>, "status": "CONVERGING|STAGNATING|DIVERGING"}`;
6121
- console.log(BORDER10(" \u2500\u2500\u2500 LIVE LOCAL SIMULATION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6122
- console.log(GRAY5(" Bob and UserBob will converse autonomously below."));
6123
- console.log(GRAY5(" Commands:"));
6124
- console.log(AMBER6(" /abort") + GRAY5(" \u2014 Stop the simulation"));
6125
- console.log(AMBER6(" /set target 90") + GRAY5(" \u2014 Update satisfaction target"));
6126
- console.log(AMBER6(" /set grading 70") + GRAY5(" \u2014 Update Teacher's Curve"));
6127
- console.log(AMBER6(" /set stag 5") + GRAY5(" \u2014 Update stalemate threshold"));
6128
- console.log(AMBER6(" /set div 3") + GRAY5(" \u2014 Update divergence threshold"));
6129
- console.log(AMBER6(" /status") + GRAY5(" \u2014 Show current parameters"));
6130
- console.log(BORDER10(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6131
- console.log("");
6132
- const kickstart = `Mission received: "${mission}". Bob, what's your first move?`;
6133
- console.log(PURPLE(" UserBob > ") + WHITE3(kickstart));
6134
- conversationHistory.push({ role: "user", content: kickstart });
6135
- while (running) {
6136
- const session = readSessionFile();
6137
- if (!session?.active) {
6138
- running = false;
6139
- break;
6140
- }
6141
- turns++;
6658
+ backupCmd.command("restore").description("Restore from a backup revision or named archive").option("--global", "Restore from global backup").option("--source", "Restore source code backup").option("--file <path>", "Restore a single file (use with --source)").action(async (options) => {
6659
+ const config = getConfig();
6660
+ if (!requireAuth(config)) return;
6661
+ const isGlobal = options.global || false;
6662
+ const isSource = options.source || false;
6663
+ const projectName = getCurrentProjectName();
6664
+ const projectDir = process.cwd();
6665
+ const filePath = options.file ? normalizeFilePath(options.file) : null;
6666
+ const spinner = ora11({ text: CYAN5(" Loading available backups..."), spinner: "dots" }).start();
6667
+ let statusResult;
6142
6668
  try {
6143
- const bobMessages = [
6144
- { role: "system", content: bobSystem },
6145
- ...conversationHistory
6146
- ];
6147
- const bobResponse = await callLocalModel(config.localEndpoint, bobMessages);
6148
- console.log(BOB_COLOR(" Bob > ") + WHITE3(bobResponse));
6149
- conversationHistory.push({ role: "assistant", content: bobResponse });
6150
- const ubMessages = [
6151
- { role: "system", content: userBobSystem },
6152
- ...conversationHistory
6153
- ];
6154
- const ubResponse = await callLocalModel(config.localEndpoint, ubMessages);
6155
- const jsonMatch = ubResponse.match(/\{[^}]*"satisfactionScore"[^}]*\}/);
6156
- const cleanResponse = ubResponse.replace(/\{[^}]*"satisfactionScore"[^}]*\}/, "").trim();
6157
- console.log(PURPLE(" UserBob > ") + WHITE3(cleanResponse));
6158
- let auditChips = [];
6159
- if (jsonMatch) {
6160
- try {
6161
- const audit = JSON.parse(jsonMatch[0]);
6162
- const rawScore = audit.satisfactionScore || 0;
6163
- sat = Math.round(rawScore * (params.grading / 100));
6164
- lastStatus = audit.status || "";
6165
- auditChips = [CYAN5(`[SAT: ${sat}%]`), BLUE4(`[RAW: ${rawScore}]`), GRAY5(`[${lastStatus}]`)];
6166
- if (lastStatus === "STAGNATING") {
6167
- stalemateCurrent++;
6168
- if (params.stag > 0 && stalemateCurrent >= params.stag) {
6169
- console.log(" " + auditChips.join(" "));
6170
- renderHUD(sat, params.target, stalemateCurrent, params.stag, divergenceCurrent, params.div, params.grading);
6171
- console.log(AMBER6(` \u{1F3C1} Stalemate threshold reached (${stalemateCurrent}/${params.stag}). Simulation ended.`));
6172
- running = false;
6173
- break;
6174
- }
6175
- } else if (lastStatus === "DIVERGING") {
6176
- divergenceCurrent++;
6177
- stalemateCurrent = 0;
6178
- if (params.div > 0 && divergenceCurrent >= params.div) {
6179
- console.log(" " + auditChips.join(" "));
6180
- renderHUD(sat, params.target, stalemateCurrent, params.stag, divergenceCurrent, params.div, params.grading);
6181
- console.log(AMBER6(` \u{1F3C1} Divergence threshold reached (${divergenceCurrent}/${params.div}). Simulation ended.`));
6182
- running = false;
6183
- break;
6184
- }
6185
- } else if (lastStatus === "CONVERGING") {
6186
- stalemateCurrent = 0;
6187
- divergenceCurrent = 0;
6188
- }
6189
- } catch {
6190
- }
6191
- }
6192
- if (auditChips.length) console.log(" " + auditChips.join(" "));
6193
- conversationHistory.push({ role: "user", content: ubResponse });
6194
- writeSessionFile({ active: true, turns, mission, sat });
6195
- renderHUD(sat, params.target, stalemateCurrent, params.stag, divergenceCurrent, params.div, params.grading);
6196
- if (sat >= params.target) {
6197
- console.log(GREEN5(` \u{1F3AF} Target satisfaction ${params.target}% reached! Mission complete.`));
6198
- running = false;
6199
- break;
6200
- }
6201
- await new Promise((r) => setTimeout(r, 1e3));
6202
- } catch (e) {
6203
- console.log(RED5(` \u274C Local model error: ${e.message}`));
6204
- console.log(GRAY5(" Retrying in 3 seconds..."));
6205
- await new Promise((r) => setTimeout(r, 3e3));
6669
+ statusResult = await callCloudFunction("getCLIBackupStatus", {
6670
+ projectName,
6671
+ isGlobal,
6672
+ isSource,
6673
+ filePath
6674
+ });
6675
+ spinner.stop();
6676
+ } catch (error) {
6677
+ spinner.stop();
6678
+ handleBackupError(error);
6679
+ return;
6206
6680
  }
6207
- }
6208
- clearSessionFile();
6209
- rl.close();
6210
- process.removeListener("SIGINT", sigintHandler);
6211
- console.log("");
6212
- console.log(GRAY5(` Session complete. ${turns} turns processed.`));
6213
- console.log("");
6214
- }
6215
- function registerUserBobCommand(program2) {
6216
- program2.command("userbob [mission...]").description("Launch your UserBob digital twin simulation").option("--local", "Force local Ollama mode (Tier 1)").option("--target <number>", "Satisfaction target (default: 85)", "85").option("--grading <number>", "Teacher's curve grading standard (default: 50)", "50").option("--stag <number>", "Stalemate threshold \u2014 0 = infinite (default: 0)", "0").option("--div <number>", "Divergence threshold \u2014 0 = infinite (default: 0)", "0").option("--resume", "Resume without injecting a new mission note").action(async (missionArgs, options) => {
6217
- const config = getConfig();
6218
- const params = {
6219
- target: parseInt(options.target, 10),
6220
- grading: parseInt(options.grading, 10),
6221
- stag: parseInt(options.stag, 10),
6222
- div: parseInt(options.div, 10)
6223
- };
6224
- const usePlatform = !options.local && isAuthenticated();
6225
- console.log("");
6226
- console.log(BORDER10(" \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"));
6227
- console.log(BORDER10(" \u2551") + PURPLE(" \u{1F916} UserBob \u2014 Digital Twin Simulation"));
6228
- console.log(BORDER10(" \u2551") + GRAY5(` Mode: ${usePlatform ? "Platform (Tier 3)" : "Local Ollama (Tier 1)"}`));
6229
- console.log(BORDER10(" \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"));
6230
- console.log("");
6231
- const dna = buildDNAString();
6232
- if (dna) {
6233
- console.log(GREEN5(" \u2705 Behavioral DNA loaded."));
6234
- } else {
6235
- console.log(AMBER6(" \u26A0\uFE0F No behavioral profile found."));
6236
- console.log(GRAY5(" UserBob performs significantly better with your DNA loaded."));
6681
+ const { versions, archives } = statusResult;
6682
+ if (versions.length === 0 && archives.length === 0) {
6237
6683
  console.log("");
6238
- const rl = readline8.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
6239
- const answer = await new Promise((resolve2) => rl.question(AMBER6(" Run `bob profile --today` now? (y/n): "), resolve2));
6240
- rl.close();
6241
- if (answer.trim().toLowerCase() === "y") {
6242
- console.log("");
6243
- console.log(GRAY5(" Run `bob profile --today` in a separate terminal, then re-run `bob userbob`."));
6244
- process.exit(0);
6245
- } else {
6246
- console.log("");
6247
- console.log(RED5(" \u26A0\uFE0F Running in Generic Mode \u2014 no behavioral profile loaded."));
6248
- console.log(RED5(" UserBob will respond using project context only."));
6249
- console.log(RED5(" Responses won't reflect your personal communication style,"));
6250
- console.log(RED5(" decision patterns, or engineering philosophy."));
6251
- console.log(GRAY5(" Run `bob profile --today` anytime to unlock full personalization."));
6252
- console.log("");
6253
- }
6684
+ const scopeMsg = isSource ? filePath ? `file "${filePath}"` : `source of ${projectName}` : isGlobal ? "global" : projectName;
6685
+ console.log(AMBER6(` \u26A0\uFE0F No backups found for ${scopeMsg}.`));
6686
+ console.log("");
6687
+ return;
6254
6688
  }
6255
- let mission = missionArgs.length > 0 ? missionArgs.join(" ") : "";
6256
- if (!mission && !options.resume) {
6257
- const mrl = readline8.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
6258
- mission = await new Promise((resolve2) => mrl.question(AMBER6(" What's the mission? > "), resolve2));
6259
- mrl.close();
6260
- if (!mission.trim()) {
6261
- console.log(RED5(" \u274C Mission cannot be empty. Exiting."));
6262
- process.exit(1);
6689
+ const choices = [];
6690
+ if (versions.length > 0) {
6691
+ choices.push(new inquirer.Separator(CYAN5(" \u2500\u2500 Revisions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")));
6692
+ for (const v of versions) {
6693
+ const date = new Date(v.lastModified).toLocaleString();
6694
+ choices.push({
6695
+ name: ` ${v.isLatest ? GREEN5("\u25CF ") : " "}${v.label.padEnd(12)} ${GRAY5(formatBytes(v.sizeBytes).padEnd(10))} ${GRAY5(date)}`,
6696
+ value: { type: "revision", versionId: v.versionId, label: v.label }
6697
+ });
6263
6698
  }
6264
- mission = mission.trim();
6265
6699
  }
6266
- console.log("");
6267
- console.log(GRAY5(` Target: ${params.target}% \u2502 Grade: ${params.grading} \u2502 Stag: ${params.stag || "\u221E"} \u2502 Div: ${params.div || "\u221E"}`));
6268
- console.log("");
6269
- if (usePlatform) {
6270
- const conversationId = config.conversationId;
6271
- if (!conversationId) {
6272
- console.log(RED5(" \u274C No active conversation. Run `bob conversations join` first."));
6273
- process.exit(1);
6274
- }
6275
- if (options.resume) {
6276
- console.log(AMBER6(" \u{1F504} Resuming simulation (no new mission note)..."));
6277
- await callHTTPFunction("userSimManagerService", {
6278
- action: "resumeMission",
6279
- conversationId,
6280
- uid: config.uid,
6281
- email: config.email
6700
+ if (archives.length > 0) {
6701
+ choices.push(new inquirer.Separator(AMBER6(" \u2500\u2500 Named Archives \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")));
6702
+ for (const a of archives) {
6703
+ const date = new Date(a.createdAt).toLocaleString();
6704
+ choices.push({
6705
+ name: ` \u{1F4CC} ${AMBER6(a.name.padEnd(24))} ${GRAY5(date)}`,
6706
+ value: { type: "archive", archiveId: a.archiveId, name: a.name }
6282
6707
  });
6283
- console.log(GREEN5(" \u2705 Simulation resumed. Entering watch mode..."));
6284
- console.log("");
6285
- await runPlatformSimulation(config, conversationId, mission || "Resumed session", params);
6286
- } else {
6287
- await runPlatformSimulation(config, conversationId, mission, params);
6288
6708
  }
6289
- return;
6290
6709
  }
6291
- if (!config.localEndpoint) {
6292
- console.log(RED5(" \u274C No local model configured."));
6293
- console.log(GRAY5(" Run: bob config set localEndpoint http://127.0.0.1:11434/api/chat"));
6294
- process.exit(1);
6295
- }
6296
- await runLocalSimulation(config, dna, mission, params);
6297
- });
6298
- }
6299
-
6300
- // src/commands/command-center.ts
6301
- import chalk25 from "chalk";
6302
- import inquirer from "inquirer";
6303
- var ORANGE2 = chalk25.hex("#E66F24");
6304
- var AMBER7 = chalk25.hex("#FFAB00");
6305
- var GREEN6 = chalk25.hex("#66BB6A");
6306
- var RED6 = chalk25.hex("#EF5350");
6307
- var BLUE5 = chalk25.hex("#42A5F5");
6308
- var PURPLE2 = chalk25.hex("#AB47BC");
6309
- var CYAN6 = chalk25.hex("#26C6DA");
6310
- var TEAL = chalk25.hex("#26A69A");
6311
- var GRAY6 = chalk25.gray;
6312
- var WHITE4 = chalk25.white;
6313
- var BORDER11 = chalk25.hex("#455A64");
6314
- function statusColor(status) {
6315
- switch (status) {
6316
- case "queued":
6317
- return GRAY6;
6318
- case "awaiting_approval":
6319
- return AMBER7;
6320
- case "in_progress":
6321
- return BLUE5;
6322
- case "completed":
6323
- return GREEN6;
6324
- case "failed":
6325
- return RED6;
6326
- case "denied":
6327
- return RED6;
6328
- default:
6329
- return GRAY6;
6330
- }
6331
- }
6332
- function statusLabel(status) {
6333
- switch (status) {
6334
- case "queued":
6335
- return "QUEUED";
6336
- case "awaiting_approval":
6337
- return "NEEDS APPROVAL";
6338
- case "in_progress":
6339
- return "IN PROGRESS";
6340
- case "completed":
6341
- return "COMPLETE";
6342
- case "failed":
6343
- return "FAILED";
6344
- case "denied":
6345
- return "DENIED";
6346
- default:
6347
- return status.toUpperCase();
6348
- }
6349
- }
6350
- function categoryColor(category) {
6351
- const map = {
6352
- security: RED6,
6353
- frontend: BLUE5,
6354
- backend: GREEN6,
6355
- cloud_functions: PURPLE2,
6356
- documentation: TEAL,
6357
- testing: AMBER7,
6358
- review: CYAN6,
6359
- fullstack: ORANGE2
6360
- };
6361
- return map[category] || GRAY6;
6362
- }
6363
- function confidenceColor(confidence) {
6364
- if (confidence >= 80) return GREEN6;
6365
- if (confidence >= 50) return AMBER7;
6366
- return RED6;
6367
- }
6368
- function formatTimestamp(ms) {
6369
- if (!ms) return "\u2014";
6370
- return new Date(ms).toLocaleString("en-US", {
6371
- month: "short",
6372
- day: "numeric",
6373
- hour: "numeric",
6374
- minute: "2-digit",
6375
- hour12: true
6376
- });
6377
- }
6378
- function stripMarkdown2(text) {
6379
- return text.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^---+$/gm, "").replace(/`([^`]+)`/g, "$1").replace(/^\s*[-*+]\s+/gm, " \u2022 ").replace(/\n{3,}/g, "\n\n").trim();
6380
- }
6381
- function renderStats(stats) {
6382
- console.log("");
6383
- console.log(BORDER11(" \u2500\u2500\u2500 COMMAND CENTER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6384
- const parts = [];
6385
- if (stats.awaiting_approval > 0) parts.push(AMBER7(`${stats.awaiting_approval} PENDING`));
6386
- if (stats.in_progress > 0) parts.push(BLUE5(`${stats.in_progress} RUNNING`));
6387
- if (stats.completed > 0) parts.push(GREEN6(`${stats.completed} DONE`));
6388
- if (stats.failed > 0) parts.push(RED6(`${stats.failed} FAILED`));
6389
- if (stats.denied > 0) parts.push(GRAY6(`${stats.denied} DENIED`));
6390
- parts.push(GRAY6(`${stats.total} TOTAL`));
6391
- console.log(" " + parts.join(GRAY6(" \u2502 ")));
6392
- console.log(BORDER11(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6393
- console.log("");
6394
- }
6395
- function renderTaskDetail(task) {
6396
- const catColor = categoryColor(task.request.category);
6397
- const confColor = confidenceColor(task.request.confidence);
6398
- const sColor = statusColor(task.outcome.status);
6399
- console.log("");
6400
- console.log(BORDER11(" \u250C\u2500 TASK DETAIL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
6401
- console.log(BORDER11(" \u2502"));
6402
- console.log(BORDER11(" \u2502 ") + WHITE4(task.request.description));
6403
- console.log(BORDER11(" \u2502"));
6404
- console.log(BORDER11(" \u2502 ") + catColor(`[${task.request.category.toUpperCase()}]`) + " " + WHITE4(`${task.request.taskType.toUpperCase()}`) + " " + confColor(`${task.request.confidence}% confidence`) + " " + sColor(`\u25CF ${statusLabel(task.outcome.status)}`));
6405
- if (task.request.targetFile) {
6406
- console.log(BORDER11(" \u2502 ") + GRAY6("File: ") + CYAN6(task.request.targetFile));
6407
- }
6408
- console.log(BORDER11(" \u2502"));
6409
- console.log(BORDER11(" \u2502 ") + GRAY6("Priority: ") + WHITE4(`P${task.priority}`) + GRAY6(" \u2502 Difficulty: ") + WHITE4(task.difficulty.toUpperCase()) + GRAY6(" \u2502 Created: ") + WHITE4(formatTimestamp(task.createdAt)));
6410
- console.log(BORDER11(" \u2502"));
6411
- console.log(BORDER11(" \u2502 ") + AMBER7("TRIGGER"));
6412
- console.log(BORDER11(" \u2502 ") + GRAY6(stripMarkdown2(task.trigger.reasoning).slice(0, 120) + "..."));
6413
- if (task.trigger.turnSatisfaction !== null) {
6414
- console.log(BORDER11(" \u2502 ") + GRAY6("SAT at dispatch: ") + CYAN6(`${task.trigger.turnSatisfaction}%`));
6415
- }
6416
- if (task.outcome.resultSummary) {
6417
- console.log(BORDER11(" \u2502"));
6418
- console.log(BORDER11(" \u2502 ") + GREEN6("RESULT"));
6419
- console.log(BORDER11(" \u2502 ") + GRAY6(stripMarkdown2(task.outcome.resultSummary).slice(0, 200) + "..."));
6420
- }
6421
- if (task.outcome.filesModified && task.outcome.filesModified.length > 0) {
6422
- console.log(BORDER11(" \u2502"));
6423
- console.log(BORDER11(" \u2502 ") + AMBER7("FILES MODIFIED"));
6424
- for (const file of task.outcome.filesModified) {
6425
- const icon = file.action === "created" ? GREEN6("+") : AMBER7("~");
6426
- console.log(BORDER11(" \u2502 ") + icon + " " + CYAN6(file.path || file));
6427
- }
6428
- }
6429
- if (task.outcome.error) {
6430
- console.log(BORDER11(" \u2502"));
6431
- console.log(BORDER11(" \u2502 ") + RED6("ERROR"));
6432
- console.log(BORDER11(" \u2502 ") + RED6(task.outcome.error.slice(0, 150)));
6433
- }
6434
- if (task.isDenied && task.denyReason) {
6435
- console.log(BORDER11(" \u2502"));
6436
- console.log(BORDER11(" \u2502 ") + RED6("DENIED BY: ") + GRAY6(task.deniedBy || "Unknown"));
6437
- console.log(BORDER11(" \u2502 ") + RED6("REASON: ") + GRAY6(task.denyReason));
6438
- }
6439
- if (task.outcome.turnsUsed || task.outcome.tokensConsumed) {
6440
- console.log(BORDER11(" \u2502"));
6441
- if (task.outcome.turnsUsed) {
6442
- console.log(BORDER11(" \u2502 ") + GRAY6(`Turns: ${task.outcome.turnsUsed} \u2502 Tokens: ${task.outcome.tokensConsumed || 0} \u2502 Provider: ${task.outcome.provider || "unknown"}`));
6443
- }
6444
- }
6445
- console.log(BORDER11(" \u2502"));
6446
- console.log(BORDER11(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
6447
- console.log("");
6448
- }
6449
- async function runDecisionStream(conversationId) {
6450
- console.log("");
6451
- console.log(ORANGE2(" \u2500\u2500\u2500 DECISION STREAM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6452
- console.log(GRAY6(" Live feed of all autonomous decisions. Ctrl+C to exit.\n"));
6453
- let lastCount = 0;
6454
- let running = true;
6455
- process.on("SIGINT", () => {
6456
- running = false;
6457
- console.log("\n" + AMBER7(" Stream ended."));
6458
- process.exit(0);
6459
- });
6460
- while (running) {
6461
- try {
6462
- const response = await callCloudFunction("getCLIAutonomousTasks", { conversationId });
6463
- const tasks = response?.tasks || [];
6464
- if (tasks.length > lastCount) {
6465
- const newTasks = tasks.slice(0, tasks.length - lastCount);
6466
- for (const task of newTasks.reverse()) {
6467
- const sColor = statusColor(task.outcome.status);
6468
- const catColor = categoryColor(task.request.category);
6469
- console.log(
6470
- GRAY6(` ${formatTimestamp(task.createdAt)} `) + catColor(`[${task.request.category.toUpperCase()}]`) + " " + WHITE4(task.request.description.slice(0, 60) + (task.request.description.length > 60 ? "..." : "")) + " " + sColor(`\u25CF ${statusLabel(task.outcome.status)}`)
6471
- );
6472
- }
6473
- lastCount = tasks.length;
6474
- }
6475
- } catch (e) {
6476
- console.log(RED6(` \u274C Poll error: ${e.message}`));
6710
+ choices.push(new inquirer.Separator());
6711
+ choices.push({ name: GRAY5(" \u2190 Cancel"), value: null });
6712
+ console.log("");
6713
+ const scopeLabel = isSource ? filePath ? `file "${filePath}"` : `source: ${projectName}` : isGlobal ? "global" : projectName;
6714
+ const { selected } = await inquirer.prompt([{
6715
+ type: "select",
6716
+ name: "selected",
6717
+ message: CYAN5(` Select a backup to restore (${scopeLabel}):`),
6718
+ choices,
6719
+ pageSize: 12
6720
+ }]);
6721
+ if (!selected) {
6722
+ console.log(GRAY5(" Cancelled."));
6723
+ console.log("");
6724
+ return;
6477
6725
  }
6478
- await new Promise((r) => setTimeout(r, 3e3));
6479
- }
6480
- }
6481
- async function runSettings(conversationId) {
6482
- let response;
6483
- try {
6484
- response = await callCloudFunction("getCLIAutonomousTasks", { conversationId });
6485
- } catch (e) {
6486
- console.log(RED6(` \u274C Failed to fetch settings: ${e.message}`));
6487
- return;
6488
- }
6489
- const settings = response?.settings || {};
6490
- console.log("");
6491
- console.log(ORANGE2(" \u2500\u2500\u2500 AUTONOMY SETTINGS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6492
- console.log(GRAY6(` Current threshold: ${settings.autonomousConfidenceThreshold ?? 75}%`));
6493
- console.log(GRAY6(" Tasks below this confidence level require your approval."));
6494
- console.log("");
6495
- const { threshold } = await inquirer.prompt([
6496
- {
6497
- type: "number",
6498
- name: "threshold",
6499
- message: AMBER7(" Confidence threshold (0-100):"),
6500
- default: settings.autonomousConfidenceThreshold ?? 75,
6501
- validate: (val) => val >= 0 && val <= 100 ? true : "Must be 0-100"
6502
- }
6503
- ]);
6504
- const categories = ["security", "frontend", "backend", "cloud_functions", "documentation", "testing", "review", "fullstack"];
6505
- const overrides = { ...settings.autonomousCategoryOverrides || {} };
6506
- console.log("");
6507
- console.log(GRAY6(" Category overrides:"));
6508
- console.log("");
6509
- for (const cat of categories) {
6510
- const current = overrides[cat] || "threshold";
6511
- const { override } = await inquirer.prompt([
6512
- {
6513
- type: "select",
6514
- name: "override",
6515
- message: categoryColor(cat)(` ${cat.padEnd(20)}`),
6516
- default: current,
6517
- choices: [
6518
- { name: GRAY6("Use Threshold"), value: "threshold" },
6519
- { name: GREEN6("Always Auto-Execute"), value: "autonomous" },
6520
- { name: RED6("Always Require Approval"), value: "approval_required" }
6521
- ]
6522
- }
6523
- ]);
6524
- if (override === "threshold") {
6525
- delete overrides[cat];
6726
+ const label = selected.type === "archive" ? `archive "${selected.name}"` : selected.label;
6727
+ console.log("");
6728
+ if (isSource && filePath) {
6729
+ console.log(AMBER6(` \u26A0\uFE0F This will restore ${filePath} from ${label}.`));
6730
+ console.log(GRAY5(" Current file will be backed up to .bob-backups/ first."));
6731
+ } else if (isSource) {
6732
+ console.log(AMBER6(` \u26A0\uFE0F This will restore source code of ${projectName} from ${label}.`));
6733
+ console.log(GRAY5(" Current project will be backed up locally first."));
6526
6734
  } else {
6527
- overrides[cat] = override;
6735
+ console.log(AMBER6(` \u26A0\uFE0F This will restore ${isGlobal ? "~/.bob/" : `~/.bob/projects/${projectName}/`} from ${label}.`));
6736
+ console.log(GRAY5(" Your current data will be backed up locally first."));
6528
6737
  }
6529
- }
6530
- try {
6531
- await callCloudFunction("updateCLIAutonomySettings", {
6532
- conversationId,
6533
- autonomousConfidenceThreshold: threshold,
6534
- autonomousCategoryOverrides: overrides
6535
- });
6536
6738
  console.log("");
6537
- console.log(GREEN6(" \u2705 Settings saved."));
6538
- } catch {
6539
- console.log("");
6540
- console.log(AMBER7(" \u26A0\uFE0F `updateCLIAutonomySettings` not deployed yet."));
6541
- console.log(GRAY6(" Update via web app for now, or deploy the CF first."));
6542
- }
6543
- console.log("");
6544
- }
6545
- async function runTaskBoard(conversationId) {
6546
- let continueLoop = true;
6547
- while (continueLoop) {
6548
- let response;
6549
- try {
6550
- response = await callCloudFunction("getCLIAutonomousTasks", { conversationId });
6551
- } catch (e) {
6552
- console.log(RED6(` \u274C Failed to fetch tasks: ${e.message}`));
6553
- return;
6554
- }
6555
- const tasks = response?.tasks || [];
6556
- const stats = response?.stats || {};
6557
- renderStats(stats);
6558
- if (tasks.length === 0) {
6559
- console.log(GRAY6(" No autonomous tasks found for this conversation."));
6560
- console.log(GRAY6(" Tasks appear here when UserBob dispatches work to Mini Bob."));
6739
+ const { confirmed } = await inquirer.prompt([{
6740
+ type: "confirm",
6741
+ name: "confirmed",
6742
+ message: AMBER6(" Continue with restore?"),
6743
+ default: false
6744
+ }]);
6745
+ if (!confirmed) {
6746
+ console.log(GRAY5(" Cancelled."));
6561
6747
  console.log("");
6562
6748
  return;
6563
6749
  }
6564
- const seen = /* @__PURE__ */ new Map();
6565
- for (const task of tasks) {
6566
- const key = task.request.description.slice(0, 80);
6567
- if (!seen.has(key) || (task.createdAt || 0) > (seen.get(key).createdAt || 0)) {
6568
- seen.set(key, task);
6569
- }
6570
- }
6571
- const dedupedTasks = Array.from(seen.values());
6572
- const taskChoices = dedupedTasks.map((task) => {
6573
- const sColor = statusColor(task.outcome.status);
6574
- const catColor = categoryColor(task.request.category);
6575
- const desc = task.request.description.slice(0, 55) + (task.request.description.length > 55 ? "..." : "");
6576
- return {
6577
- name: sColor(` \u25CF ${statusLabel(task.outcome.status).padEnd(16)}`) + " " + catColor(`[${task.request.category.padEnd(15)}]`) + " " + WHITE4(desc),
6578
- value: task.id,
6579
- short: task.request.description.slice(0, 40)
6580
- };
6581
- });
6582
- taskChoices.push({
6583
- name: BORDER11(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),
6584
- value: "__separator__",
6585
- short: "",
6586
- disabled: true
6587
- });
6588
- taskChoices.push({
6589
- name: GRAY6(" \u21A9 Exit Command Center"),
6590
- value: "__exit__",
6591
- short: "Exit"
6592
- });
6593
- const { selectedTaskId } = await inquirer.prompt([
6594
- {
6595
- type: "select",
6596
- name: "selectedTaskId",
6597
- message: ORANGE2(" Select a task to inspect:"),
6598
- choices: taskChoices,
6599
- pageSize: 14
6600
- }
6601
- ]);
6602
- if (selectedTaskId === "__exit__" || selectedTaskId === "__separator__") {
6603
- continueLoop = false;
6604
- break;
6605
- }
6606
- const selectedTask = dedupedTasks.find((t) => t.id === selectedTaskId);
6607
- if (!selectedTask) continue;
6608
- renderTaskDetail(selectedTask);
6609
- const actions = [];
6610
- if (selectedTask.outcome.status === "awaiting_approval") {
6611
- actions.push({ name: GREEN6(" \u2705 Approve \u2014 execute this task now"), value: "approve", short: "Approve" });
6612
- actions.push({ name: RED6(" \u274C Deny \u2014 reject this task"), value: "deny", short: "Deny" });
6613
- }
6614
- actions.push({ name: AMBER7(" \u21A9 Back to task list"), value: "back", short: "Back" });
6615
- actions.push({ name: GRAY6(" \u2715 Exit Command Center"), value: "exit", short: "Exit" });
6616
- const { action } = await inquirer.prompt([
6617
- {
6618
- type: "select",
6619
- name: "action",
6620
- message: ORANGE2(" What would you like to do?"),
6621
- choices: actions
6622
- }
6623
- ]);
6624
- if (action === "approve") {
6625
- const { confirmed } = await inquirer.prompt([
6626
- {
6627
- type: "confirm",
6628
- name: "confirmed",
6629
- message: AMBER7(" Approve task and execute autonomously?"),
6630
- default: true
6750
+ const tmpDir = getTempDir2();
6751
+ const downloadPath = path13.join(tmpDir, "bob-backup.bob.enc");
6752
+ const decryptedPath = path13.join(tmpDir, "bob-backup.tar.gz");
6753
+ try {
6754
+ const dlSpinner = ora11({
6755
+ text: GRAY5(` Downloading ${label} ...`),
6756
+ spinner: "dots"
6757
+ }).start();
6758
+ let downloadAction;
6759
+ if (isSource) {
6760
+ downloadAction = selected.type === "archive" ? "requestSourceArchiveDownload" : "requestSourceDownload";
6761
+ } else {
6762
+ downloadAction = selected.type === "archive" ? "requestArchiveDownload" : "requestDownload";
6763
+ }
6764
+ const downloadResult = await callCloudFunction("cliBackupLicense", {
6765
+ action: downloadAction,
6766
+ projectName,
6767
+ isGlobal,
6768
+ isSource,
6769
+ filePath,
6770
+ s3VersionId: selected.type === "revision" ? selected.versionId : null,
6771
+ archiveId: selected.type === "archive" ? selected.archiveId : null
6772
+ });
6773
+ const response = await axios2.get(downloadResult.downloadUrl, {
6774
+ responseType: "arraybuffer",
6775
+ maxContentLength: Infinity
6776
+ });
6777
+ fs11.writeFileSync(downloadPath, Buffer.from(response.data));
6778
+ dlSpinner.succeed(GREEN5(` Downloading ${label} ...`));
6779
+ const decryptSpinner = ora11({ text: GRAY5(" Decrypting ..."), spinner: "dots" }).start();
6780
+ decrypt2(downloadPath, decryptedPath, config.uid);
6781
+ decryptSpinner.succeed(GREEN5(" Decrypting ..."));
6782
+ const backupSpinner = ora11({
6783
+ text: GRAY5(" Backing up current state ..."),
6784
+ spinner: "dots"
6785
+ }).start();
6786
+ let preRestoreBackup;
6787
+ if (isSource && filePath) {
6788
+ const absoluteFilePath = path13.resolve(projectDir, filePath);
6789
+ const backupDir = path13.join(projectDir, ".bob-backups");
6790
+ if (!fs11.existsSync(backupDir)) fs11.mkdirSync(backupDir, { recursive: true });
6791
+ const backupFileName = filePath.replace(/[\/\\]/g, "_") + `.${Date.now()}.bak`;
6792
+ preRestoreBackup = path13.join(backupDir, backupFileName);
6793
+ if (fs11.existsSync(absoluteFilePath)) {
6794
+ fs11.copyFileSync(absoluteFilePath, preRestoreBackup);
6631
6795
  }
6632
- ]);
6633
- if (confirmed) {
6634
- console.log("");
6635
- console.log(ORANGE2(" \u2500\u2500\u2500 EXECUTION STREAM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6636
- console.log(GRAY6(" Mini Bob is executing. Streaming live logs...\n"));
6637
- callCloudFunction("approveAutonomousTask", {
6638
- conversationId,
6639
- taskId: selectedTask.id,
6640
- action: "approve"
6641
- }).then(() => {
6642
- }).catch((e) => {
6643
- console.log(RED6(`
6644
- \u274C Execution error: ${e.message}`));
6645
- });
6646
- let running = true;
6647
- let seenLogIds = /* @__PURE__ */ new Set();
6648
- let pollErrors = 0;
6649
- const maxPollErrors = 5;
6650
- const sigintHandler = () => {
6651
- running = false;
6652
- console.log("\n" + AMBER7(" Stream ended. Task continues in background."));
6653
- process.exit(0);
6654
- };
6655
- process.on("SIGINT", sigintHandler);
6656
- while (running) {
6657
- await new Promise((r) => setTimeout(r, 2e3));
6658
- try {
6659
- const taskResponse = await callCloudFunction("getCLIAutonomousTasks", {
6660
- conversationId,
6661
- statusFilter: null
6662
- });
6663
- const updatedTask = taskResponse?.tasks?.find((t) => t.id === selectedTask.id);
6664
- const logResponse = await callCloudFunction("getCLITaskExecutionLog", {
6665
- conversationId,
6666
- taskId: selectedTask.id
6667
- });
6668
- const logEntries = logResponse?.entries || [];
6669
- for (const entry of logEntries) {
6670
- if (seenLogIds.has(entry.id)) continue;
6671
- seenLogIds.add(entry.id);
6672
- const stage = entry.stage || "EXECUTION";
6673
- let prefix;
6674
- switch (stage) {
6675
- case "INIT":
6676
- prefix = CYAN6(" [INIT] ");
6677
- break;
6678
- case "TOOL_CALL":
6679
- prefix = AMBER7(" [TOOL] ");
6680
- break;
6681
- case "FALLBACK":
6682
- prefix = AMBER7(" [FALLBACK] ");
6683
- break;
6684
- case "COMPLETE":
6685
- prefix = GREEN6(" [DONE] ");
6686
- break;
6687
- case "ERROR":
6688
- prefix = RED6(" [ERROR] ");
6689
- break;
6690
- case "APPROVED":
6691
- prefix = GREEN6(" [APPROVED] ");
6692
- break;
6693
- case "DENIED":
6694
- prefix = RED6(" [DENIED] ");
6695
- break;
6696
- case "AWAITING_APPROVAL":
6697
- prefix = AMBER7(" [PENDING] ");
6698
- break;
6699
- default:
6700
- prefix = GRAY6(" [LOG] ");
6701
- break;
6702
- }
6703
- console.log(prefix + WHITE4(entry.text));
6704
- }
6705
- if (updatedTask) {
6706
- const status = updatedTask.outcome.status;
6707
- if (status === "completed") {
6708
- console.log("");
6709
- console.log(GREEN6(" \u2705 Task complete!"));
6710
- if (updatedTask.outcome.filesModified?.length > 0) {
6711
- console.log("");
6712
- console.log(AMBER7(" Files modified:"));
6713
- for (const file of updatedTask.outcome.filesModified) {
6714
- const icon = file.action === "created" ? GREEN6("+") : AMBER7("~");
6715
- console.log(` ${icon} ${CYAN6(file.path || file)}`);
6716
- }
6717
- }
6718
- if (updatedTask.outcome.resultSummary) {
6719
- console.log("");
6720
- console.log(AMBER7(" Summary:"));
6721
- const summary = stripMarkdown2(updatedTask.outcome.resultSummary);
6722
- const lines = summary.split("\n").slice(0, 8);
6723
- for (const line of lines) {
6724
- console.log(GRAY6(` ${line}`));
6725
- }
6726
- }
6727
- if (updatedTask.outcome.turnsUsed) {
6728
- console.log("");
6729
- console.log(GRAY6(` Turns: ${updatedTask.outcome.turnsUsed} \u2502 Tokens: ${updatedTask.outcome.tokensConsumed || 0} \u2502 Provider: ${updatedTask.outcome.provider || "unknown"}`));
6730
- }
6731
- running = false;
6732
- } else if (status === "failed") {
6733
- console.log("");
6734
- console.log(RED6(` \u274C Task failed: ${updatedTask.outcome.error || "Unknown error"}`));
6735
- running = false;
6736
- } else if (status === "denied") {
6737
- console.log("");
6738
- console.log(RED6(" \u274C Task was denied."));
6739
- running = false;
6740
- }
6741
- }
6742
- pollErrors = 0;
6743
- } catch (e) {
6744
- pollErrors++;
6745
- if (pollErrors >= maxPollErrors) {
6746
- console.log(RED6(` \u274C Lost connection after ${maxPollErrors} errors. Task continues in background.`));
6747
- running = false;
6748
- }
6749
- }
6796
+ } else if (isSource) {
6797
+ preRestoreBackup = `${projectDir}-pre-restore-${Date.now()}`;
6798
+ if (fs11.existsSync(projectDir)) {
6799
+ fs11.cpSync(projectDir, preRestoreBackup, { recursive: true });
6750
6800
  }
6751
- process.removeListener("SIGINT", sigintHandler);
6752
- console.log("");
6753
- }
6754
- } else if (action === "deny") {
6755
- const { reason } = await inquirer.prompt([
6756
- {
6757
- type: "input",
6758
- name: "reason",
6759
- message: AMBER7(" Denial reason (optional):"),
6760
- default: ""
6801
+ } else {
6802
+ const restoreTarget = isGlobal ? BOB_DIR4 : getProjectBackupDir(projectName);
6803
+ preRestoreBackup = `${restoreTarget}-pre-restore-${Date.now()}`;
6804
+ if (fs11.existsSync(restoreTarget)) {
6805
+ fs11.cpSync(restoreTarget, preRestoreBackup, { recursive: true });
6761
6806
  }
6762
- ]);
6763
- try {
6764
- console.log(GRAY6(" Denying task..."));
6765
- await callCloudFunction("approveAutonomousTask", {
6766
- conversationId,
6767
- taskId: selectedTask.id,
6768
- action: "deny",
6769
- reason: reason.trim() || null
6770
- });
6771
- console.log("");
6772
- console.log(RED6(" \u274C Task denied."));
6773
- console.log("");
6774
- } catch (e) {
6775
- console.log(RED6(` \u274C Failed to deny: ${e.message}`));
6776
6807
  }
6777
- } else if (action === "exit") {
6778
- continueLoop = false;
6779
- break;
6780
- }
6781
- }
6782
- }
6783
- function registerCommandCenterCommand(program2) {
6784
- program2.command("command-center").alias("cc").description("Autonomous Command Center \u2014 inspect, approve, and manage UserBob dispatch tasks").option("--stream", "Live decision stream feed").option("--settings", "Configure autonomy threshold and category overrides").action(async (options) => {
6785
- if (!isAuthenticated()) {
6808
+ backupSpinner.succeed(
6809
+ GREEN5(" Backing up current state ...") + GRAY5(` \u2192 ${path13.basename(preRestoreBackup)}`)
6810
+ );
6811
+ const extractSpinner = ora11({ text: GRAY5(" Extracting ..."), spinner: "dots" }).start();
6812
+ const tar = await import("tar");
6813
+ if (isSource && filePath) {
6814
+ await tar.extract({ file: decryptedPath, cwd: projectDir });
6815
+ } else if (isSource) {
6816
+ const parentDir = path13.dirname(projectDir);
6817
+ await tar.extract({ file: decryptedPath, cwd: parentDir });
6818
+ } else {
6819
+ await tar.extract({ file: decryptedPath, cwd: os4.homedir() });
6820
+ }
6821
+ extractSpinner.succeed(GREEN5(" Extracting ..."));
6786
6822
  console.log("");
6787
- console.log(RED6(" \u274C Authentication required."));
6788
- console.log(GRAY6(" Run `bob login` first."));
6823
+ console.log(BORDER10(" \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"));
6824
+ console.log(BORDER10(" \u2551") + GREEN5(" \u2705 Restore complete. ") + BORDER10("\u2551"));
6825
+ if (isSource && filePath) {
6826
+ console.log(BORDER10(" \u2551") + GRAY5(` Restored: ${filePath}`));
6827
+ console.log(BORDER10(" \u2551") + GRAY5(` Backup: .bob-backups/${path13.basename(preRestoreBackup)}`));
6828
+ } else if (isSource) {
6829
+ console.log(BORDER10(" \u2551") + GRAY5(` Restored: ${projectName}/ source`));
6830
+ console.log(BORDER10(" \u2551") + GRAY5(` Backup: ${path13.basename(preRestoreBackup)}/`));
6831
+ } else {
6832
+ console.log(BORDER10(" \u2551") + GRAY5(` Restored: ${isGlobal ? "~/.bob/" : `~/.bob/projects/${projectName}/`}`));
6833
+ console.log(BORDER10(" \u2551") + GRAY5(` Backup: ${path13.basename(preRestoreBackup)}`));
6834
+ }
6835
+ console.log(BORDER10(" \u2551") + GRAY5(` From: ${label}`));
6836
+ console.log(BORDER10(" \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"));
6789
6837
  console.log("");
6790
- process.exit(1);
6791
- }
6792
- const config = getConfig();
6793
- const conversationId = config.conversationId;
6794
- if (!conversationId) {
6838
+ } catch (error) {
6795
6839
  console.log("");
6796
- console.log(RED6(" \u274C No active conversation."));
6797
- console.log(GRAY6(" Run `bob conversations join` first."));
6840
+ console.log(RED5(` \u274C Restore failed: ${error.message}`));
6841
+ console.log(GRAY5(" Your original data was not modified."));
6798
6842
  console.log("");
6799
- process.exit(1);
6843
+ } finally {
6844
+ cleanupTemp2(tmpDir);
6800
6845
  }
6846
+ });
6847
+ backupCmd.action(async () => {
6848
+ const config = getConfig();
6849
+ if (!requireAuth(config)) return;
6850
+ const projectName = getCurrentProjectName();
6851
+ console.log("");
6852
+ console.log(GRAY5(` Current project: ${projectName}`));
6853
+ console.log("");
6854
+ console.log(GRAY5(" Bob Backup commands:"));
6855
+ console.log("");
6856
+ console.log(GRAY5(" bob backup create \u2014 Context backup (Bob data)"));
6857
+ console.log(GRAY5(" bob backup create --source \u2014 Source code backup"));
6858
+ console.log(GRAY5(" bob backup create --source --file <path> \u2014 Single file backup"));
6859
+ console.log(GRAY5(' bob backup create --archive "name" \u2014 Named archive'));
6860
+ console.log(GRAY5(" bob backup create --global \u2014 Full ~/.bob/ (Grid only)"));
6861
+ console.log(GRAY5(" bob backup list \u2014 List context revisions"));
6862
+ console.log(GRAY5(" bob backup list --source \u2014 List source revisions"));
6863
+ console.log(GRAY5(" bob backup list --source --file <path> \u2014 List file revisions"));
6864
+ console.log(GRAY5(" bob backup restore \u2014 Restore context"));
6865
+ console.log(GRAY5(" bob backup restore --source \u2014 Restore source code"));
6866
+ console.log(GRAY5(" bob backup restore --source --file <path> \u2014 Restore single file"));
6867
+ console.log(GRAY5(" bob backup restore --global \u2014 Restore full ~/.bob/"));
6801
6868
  console.log("");
6802
- console.log(BORDER11(" \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"));
6803
- console.log(BORDER11(" \u2551") + ORANGE2(" \u25C9 AUTONOMOUS COMMAND CENTER"));
6804
- console.log(BORDER11(" \u2551") + GRAY6(` Conversation: ${conversationId.slice(0, 24)}...`));
6805
- console.log(BORDER11(" \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"));
6806
- if (options.stream) {
6807
- await runDecisionStream(conversationId);
6808
- return;
6809
- }
6810
- if (options.settings) {
6811
- await runSettings(conversationId);
6812
- return;
6813
- }
6814
- await runTaskBoard(conversationId);
6815
6869
  });
6816
6870
  }
6817
6871
 
6818
6872
  // bin/bob.ts
6819
- var BRAND_PRIMARY11 = chalk26.hex("#E66F24");
6820
- var BRAND_SECONDARY15 = chalk26.hex("#FFAB00");
6821
- var SUCCESS16 = chalk26.hex("#66BB6A");
6822
- var INFO16 = chalk26.hex("#26C6DA");
6823
- var MUTED16 = chalk26.hex("#78909C");
6824
- var MODE_CONSULTANT9 = chalk26.hex("#AB47BC");
6825
6873
  var program = new Command();
6826
- program.name("bob").description("Bob's CLI \u2014 AI coding assistant and Forge orchestrator").version("0.2.0").helpOption(false).addHelpCommand(false);
6827
- program.option("-h, --help", "Print this usage information").on("option:help", () => {
6828
- printCustomHelp();
6829
- process.exit(0);
6830
- });
6831
- program.command("help").description("Display help for Bob's CLI").action(() => {
6832
- printCustomHelp();
6833
- });
6834
- function printCustomHelp() {
6835
- console.log("");
6836
- console.log(BRAND_PRIMARY11(" \u25C9 Bob's CLI") + MUTED16(" \u2014 Your AI Engineering Partner, In Your Terminal."));
6837
- console.log("");
6838
- console.log(chalk26.white(" Common commands:"));
6839
- console.log("");
6840
- console.log(BRAND_SECONDARY15(' bob chat "message"'));
6841
- console.log(MUTED16(" Chat with Bob \u2014 code-friendly engineering partner with file awareness."));
6842
- console.log("");
6843
- console.log(BRAND_SECONDARY15(' bob consult "message"'));
6844
- console.log(MUTED16(" Strategic advice only \u2014 no code, just architectural guidance."));
6845
- console.log("");
6846
- console.log(BRAND_SECONDARY15(" bob index"));
6847
- console.log(MUTED16(" Index your project \u2014 generates summaries and dependency map for context."));
6848
- console.log("");
6849
- console.log(chalk26.white(" Usage: ") + INFO16("bob <command> [arguments]"));
6850
- console.log("");
6851
- console.log(chalk26.white(" Global options:"));
6852
- console.log(MUTED16(" -h, --help Print this usage information."));
6853
- console.log(MUTED16(" -V, --version Output the version number."));
6854
- console.log("");
6855
- console.log(chalk26.white(" Available commands:"));
6856
- console.log("");
6857
- console.log(INFO16(" Conversation"));
6858
- printCmd("chat [message]", "Chat with Bob \u2014 code-friendly engineering partner");
6859
- printCmd("consult [message]", "Strategic advice only, no code output");
6860
- printCmd("conversations", "List, search, and join existing conversations");
6861
- printCmd("fork <title>", "Branch conversation into a focused sub-project");
6862
- printCmd("forks", "List all forks of the current conversation");
6863
- printCmd("deepdive", "Sandboxed exploration on a specific Bob message");
6864
- printCmd("deepdives", "List all deep dives in the current conversation");
6865
- printCmd("deepdives-join", "Re-enter an existing deep dive");
6866
- console.log("");
6867
- console.log(SUCCESS16(" Project Tools"));
6868
- printCmd("index", "Index your project \u2014 AI-powered summaries + dependency map");
6869
- printCmd("analyse", "Full QA code review \u2014 bugs, features, improvements, upgrades");
6870
- printCmd("analyse --auto", "Auto-fix mode \u2014 Bob triages, MiniBob implements");
6871
- printCmd("autonomy", "Full autonomous repair across entire codebase");
6872
- printCmd("push <message>", "Git stage + commit + push in one command");
6873
- console.log("");
6874
- console.log(BRAND_PRIMARY11(" Remote (SovereignLink)"));
6875
- printCmd("serve", "Start Active Bob \u2014 receive commands from any device");
6876
- printCmd("remote [type] [msg]", "Send commands to a remote Active Bob");
6877
- console.log("");
6878
- console.log(MODE_CONSULTANT9(" Profile & Identity"));
6879
- printCmd("profile", "View your behavioral DNA dashboard");
6880
- printCmd("profile --cloud", "Generate cloud-powered daily profile");
6881
- printCmd("userbob", "Launch your UserBob digital twin");
6882
- printCmd("byok", "Manage Bring Your Own Key configuration");
6883
- printCmd("command-center", "Autonomous Command Center \u2014 inspect and approve tasks");
6884
- printCmd("cc", "Alias for command-center");
6885
- console.log("");
6886
- console.log(MUTED16(" Configuration"));
6887
- printCmd("config show", "Display current configuration");
6888
- printCmd("config set <key> <val>", "Update a configuration value");
6889
- printCmd("login", "Authenticate with Bob's Workshop via browser");
6890
- printCmd("logout", "Sign out and clear stored credentials");
6891
- printCmd("whoami", "Show current auth status and project info");
6892
- console.log("");
6893
- console.log(chalk26.white(" Interactive slash commands ") + MUTED16("(inside a chat/consult session):"));
6894
- console.log("");
6895
- printCmd("/exit", "End the session");
6896
- printCmd("/new", "Start a fresh conversation");
6897
- printCmd("/clear", "Clear the terminal");
6898
- printCmd("/include <path>", "Load a file into active context");
6899
- printCmd("/delete <path>", "Delete a file (with backup)");
6900
- printCmd("/deepdive", "Deep dive on the last Bob message");
6901
- printCmd("/constraints", "View active negative constraints");
6902
- console.log("");
6903
- console.log(MUTED16(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6904
- console.log("");
6905
- console.log(MUTED16(" Run ") + INFO16("bob <command> --help") + MUTED16(" for details on a specific command."));
6906
- console.log("");
6907
- console.log(MUTED16(" \u{1F4D6} Full docs:"));
6908
- console.log(INFO16(" https://seedling-io.gitbook.io/bob-cli/bobs-cli-product-wiki-and-user-guide/command-reference"));
6909
- console.log("");
6910
- console.log(MUTED16(" Built by ") + BRAND_PRIMARY11("Bob's Workshop") + MUTED16(" \u2014 A Seedling Company."));
6911
- console.log("");
6912
- }
6913
- function printCmd(cmd, desc) {
6914
- const padded = cmd.padEnd(24);
6915
- console.log(BRAND_SECONDARY15(` ${padded}`) + MUTED16(desc));
6916
- }
6874
+ program.name("bob").description("Bob's CLI \u2014 AI coding assistant and Forge orchestrator").version("0.1.3");
6917
6875
  program.command("whoami").description("Show current authentication status and configuration").action(() => {
6918
6876
  const config = getConfig();
6919
6877
  const projectName = path14.basename(process.cwd());
6920
6878
  console.log("");
6921
- console.log(chalk26.bold(" \u{1F916} Bob's CLI"));
6922
- console.log(chalk26.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"));
6923
- console.log(` ${chalk26.cyan("Status:")} ${config.loggedIn ? chalk26.green("Logged in as " + config.email) : "Not logged in"}`);
6924
- console.log(` ${chalk26.cyan("Tier:")} ${config.tier === "platform" ? "Platform (Tier 3)" : "Local-first (Tier 1)"}`);
6925
- console.log(` ${chalk26.cyan("Provider:")} ${config.provider || "Not configured"}`);
6926
- console.log(` ${chalk26.cyan("Mode:")} ${config.personalizationMode ? "Personalized" : config.consultantMode ? "Consultant" : "Standard"}`);
6927
- console.log(` ${chalk26.cyan("IDRP:")} ${config.idrp ? "Enabled" : "Disabled"}`);
6928
- console.log(` ${chalk26.cyan("Project:")} ${projectName} (${process.cwd()})`);
6929
- console.log(` ${chalk26.cyan("Session:")} ${config.conversationId ? config.conversationId.slice(0, 20) + "..." : "None"}`);
6879
+ console.log(chalk25.bold(" \u{1F916} Bob's CLI"));
6880
+ console.log(chalk25.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"));
6881
+ console.log(` ${chalk25.cyan("Status:")} ${config.loggedIn ? chalk25.green("Logged in as " + config.email) : "Not logged in"}`);
6882
+ console.log(` ${chalk25.cyan("Tier:")} ${config.tier === "platform" ? "Platform (Tier 3)" : "Local-first (Tier 1)"}`);
6883
+ console.log(` ${chalk25.cyan("Provider:")} ${config.provider || "Not configured"}`);
6884
+ console.log(` ${chalk25.cyan("Mode:")} ${config.personalizationMode ? "Personalized" : config.consultantMode ? "Consultant" : "Standard"}`);
6885
+ console.log(` ${chalk25.cyan("IDRP:")} ${config.idrp ? "Enabled" : "Disabled"}`);
6886
+ console.log(` ${chalk25.cyan("Project:")} ${projectName} (${process.cwd()})`);
6887
+ console.log(` ${chalk25.cyan("Session:")} ${config.conversationId ? config.conversationId.slice(0, 20) + "..." : "None"}`);
6930
6888
  console.log("");
6931
6889
  if (!config.loggedIn) {
6932
- console.log(chalk26.gray(" Run `bob login` to authenticate."));
6890
+ console.log(chalk25.gray(" Run `bob login` to authenticate."));
6933
6891
  console.log("");
6934
6892
  }
6935
6893
  });
@@ -6948,26 +6906,5 @@ registerAutonomyCommand(program);
6948
6906
  registerServeCommand(program);
6949
6907
  registerRemoteCommand(program);
6950
6908
  registerProfileCommand(program);
6951
- registerUserBobCommand(program);
6952
- registerCommandCenterCommand(program);
6953
- process.on("uncaughtException", (error) => {
6954
- console.error("");
6955
- console.error(chalk26.hex("#EF5350")(" \u274C An unexpected error occurred."));
6956
- console.error(chalk26.hex("#78909C")(` ${error.message || "Unknown error"}`));
6957
- console.error("");
6958
- console.error(chalk26.hex("#78909C")(" If this persists, please report it:"));
6959
- console.error(chalk26.hex("#26C6DA")(" https://github.com/bobsworkshop/bob-cli/issues"));
6960
- console.error("");
6961
- process.exit(1);
6962
- });
6963
- process.on("unhandledRejection", (reason) => {
6964
- console.error("");
6965
- console.error(chalk26.hex("#EF5350")(" \u274C An unexpected error occurred."));
6966
- console.error(chalk26.hex("#78909C")(` ${reason?.message || reason || "Unknown error"}`));
6967
- console.error("");
6968
- console.error(chalk26.hex("#78909C")(" If this persists, please report it:"));
6969
- console.error(chalk26.hex("#26C6DA")(" https://github.com/bobsworkshop/bob-cli/issues"));
6970
- console.error("");
6971
- process.exit(1);
6972
- });
6909
+ registerBackupCommand(program);
6973
6910
  program.parse();