@bobsworkshop/cli 0.6.0 → 0.7.0

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-SADPOL7M.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."));
@@ -1641,12 +1618,17 @@ async function playWelcomeAnimation() {
1641
1618
  console.log(MUTED4(" \u2551") + pad2(" " + BRAND_SECONDARY4('\u25B8 bob push "msg"') + MUTED4(" \u2014 Git commit + push"), 41));
1642
1619
  console.log(MUTED4(" \u2551") + pad2(" " + BRAND_SECONDARY4("\u25B8 bob --help") + MUTED4(" \u2014 See all commands"), 40));
1643
1620
  console.log(MUTED4(" \u2551") + pad2("", 0));
1621
+ console.log(hRule2());
1622
+ console.log(MUTED4(" \u2551") + pad2("", 0));
1623
+ console.log(MUTED4(" \u2551") + pad2(" " + SUCCESS4("\u{1F331} Join 1,700+ builders in our community:"), 43));
1624
+ console.log(MUTED4(" \u2551") + pad2(" " + INFO4("https://discord.gg/wM9ZBXdd"), 21));
1625
+ console.log(MUTED4(" \u2551") + pad2("", 0));
1644
1626
  console.log(MUTED4(" \u255A" + "\u2550".repeat(BOX_WIDTH2) + "\u255D"));
1645
1627
  console.log("");
1646
1628
  await sleep3(800);
1647
1629
  }
1648
1630
  function sleep3(ms) {
1649
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1631
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1650
1632
  }
1651
1633
 
1652
1634
  // src/core/provider-detect.ts
@@ -1939,7 +1921,7 @@ ${content}
1939
1921
  }
1940
1922
  rl.pause();
1941
1923
  const confirmPromptText = ERROR4(` \u{1F5D1}\uFE0F Delete ${filePath}? (y/n): `);
1942
- const confirm = await new Promise((resolve2) => {
1924
+ const confirm = await new Promise((resolve3) => {
1943
1925
  process.stdout.write(confirmPromptText);
1944
1926
  process.stdin.resume();
1945
1927
  process.stdin.setEncoding("utf-8");
@@ -1950,7 +1932,7 @@ ${content}
1950
1932
  inputBuffer += chunk.slice(0, newlineIdx);
1951
1933
  process.stdin.removeListener("data", onData);
1952
1934
  process.stdin.pause();
1953
- resolve2(inputBuffer.replace(/\r/g, "").trim());
1935
+ resolve3(inputBuffer.replace(/\r/g, "").trim());
1954
1936
  } else {
1955
1937
  inputBuffer += chunk;
1956
1938
  }
@@ -2559,8 +2541,8 @@ function registerByokCommand(program2) {
2559
2541
  return;
2560
2542
  }
2561
2543
  const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
2562
- const answer = await new Promise((resolve2) => {
2563
- 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);
2564
2546
  });
2565
2547
  rl.close();
2566
2548
  if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
@@ -2740,8 +2722,8 @@ function registerConversationsCommand(program2) {
2740
2722
  }
2741
2723
  renderConversationList(conversations, config.conversationId, options.search, result, true);
2742
2724
  const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
2743
- const answer = await new Promise((resolve2) => {
2744
- 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);
2745
2727
  });
2746
2728
  rl.close();
2747
2729
  const selection = parseInt(answer.trim());
@@ -2978,7 +2960,7 @@ function truncate(text, max) {
2978
2960
  return text.length > max ? text.slice(0, max - 3) + "..." : text;
2979
2961
  }
2980
2962
  function sleep4(ms) {
2981
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2963
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
2982
2964
  }
2983
2965
 
2984
2966
  // src/commands/fork.ts
@@ -3022,7 +3004,7 @@ function registerForkCommand(program2) {
3022
3004
  try {
3023
3005
  const result = await forkPromise;
3024
3006
  animation.stop();
3025
- await new Promise((resolve2) => setTimeout(resolve2, 200));
3007
+ await new Promise((resolve3) => setTimeout(resolve3, 200));
3026
3008
  if (result?.conversationId) {
3027
3009
  setConfigValue("conversationId", result.conversationId);
3028
3010
  console.log("");
@@ -3056,7 +3038,7 @@ function registerForkCommand(program2) {
3056
3038
  }
3057
3039
  } catch (error) {
3058
3040
  animation.stop();
3059
- await new Promise((resolve2) => setTimeout(resolve2, 200));
3041
+ await new Promise((resolve3) => setTimeout(resolve3, 200));
3060
3042
  console.log("");
3061
3043
  console.log(ERROR10(` \u274C Fork failed: ${error.message}`));
3062
3044
  console.log("");
@@ -3126,7 +3108,7 @@ function registerAnalyseCommand(program2) {
3126
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) => {
3127
3109
  const config = getConfig();
3128
3110
  if (options.auto) {
3129
- const { runAutoFix } = await import("./analyse-auto-3JL5TO3G.js");
3111
+ const { runAutoFix } = await import("./analyse-auto-GCWXNMNZ.js");
3130
3112
  const category = options.bugs ? "bugs" : options.features ? "features" : options.improvements ? "improvements" : options.upgrades ? "upgrades" : void 0;
3131
3113
  await runAutoFix({
3132
3114
  category,
@@ -3136,7 +3118,7 @@ function registerAnalyseCommand(program2) {
3136
3118
  return;
3137
3119
  }
3138
3120
  if (options.bugs || options.features || options.improvements || options.upgrades) {
3139
- const { showInteractiveResults } = await import("./analyse-results-NXVQJCO6.js");
3121
+ const { showInteractiveResults } = await import("./analyse-results-TIVOJ5SC.js");
3140
3122
  const category = options.bugs ? "bugs" : options.features ? "features" : options.improvements ? "improvements" : "upgrades";
3141
3123
  await showInteractiveResults(config, category, options.sort, options.search);
3142
3124
  return;
@@ -3591,8 +3573,8 @@ async function runTier3Autonomy(config) {
3591
3573
  console.log(GREEN(" \u2705 All tasks complete!"));
3592
3574
  console.log(AMBER2(" \u{1F4E4} MiniBob wants to push to GitHub."));
3593
3575
  const rl = readline6.createInterface({ input: process.stdin, output: process.stdout });
3594
- const answer = await new Promise((resolve2) => {
3595
- rl.question(CYAN(" Approve push? (y/n): "), resolve2);
3576
+ const answer = await new Promise((resolve3) => {
3577
+ rl.question(CYAN(" Approve push? (y/n): "), resolve3);
3596
3578
  });
3597
3579
  rl.close();
3598
3580
  if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
@@ -3622,7 +3604,7 @@ async function runTier3Autonomy(config) {
3622
3604
  } catch (pollError) {
3623
3605
  }
3624
3606
  if (running) {
3625
- await new Promise((resolve2) => setTimeout(resolve2, 2500));
3607
+ await new Promise((resolve3) => setTimeout(resolve3, 2500));
3626
3608
  }
3627
3609
  }
3628
3610
  console.log("");
@@ -3940,48 +3922,99 @@ function renderLocalTodoList(queue) {
3940
3922
 
3941
3923
  // src/commands/serve.ts
3942
3924
  import chalk19 from "chalk";
3925
+ import * as fs10 from "fs";
3943
3926
  import * as os3 from "os";
3944
3927
  import * as path11 from "path";
3928
+ import * as crypto2 from "crypto";
3929
+ import axios from "axios";
3945
3930
  var GREEN2 = chalk19.hex("#66BB6A");
3946
3931
  var AMBER3 = chalk19.hex("#FFAB00");
3947
- var BLUE2 = chalk19.hex("#42A5F5");
3948
3932
  var RED2 = chalk19.hex("#EF5350");
3949
3933
  var GRAY2 = chalk19.gray;
3950
3934
  var CYAN2 = chalk19.cyan;
3951
3935
  var BORDER5 = chalk19.hex("#455A64");
3936
+ var BOB_DIR3 = path11.join(os3.homedir(), ".bob");
3937
+ var ALGORITHM = "aes-256-cbc";
3952
3938
  var TIER_CONFIGS = {
3953
3939
  "Power": {
3954
3940
  activeInterval: 2e3,
3955
- // 2 seconds
3956
3941
  sleepInterval: 12e4,
3957
- // 120 seconds (2 minutes) when sleeping
3958
3942
  idleThreshold: 5 * 6e4,
3959
- // 5 min → enter sleep
3960
3943
  extendedIdleTimeout: null
3961
- // Never auto-exit
3962
3944
  },
3963
3945
  "Pro": {
3964
3946
  activeInterval: 1e4,
3965
- // 10 seconds
3966
3947
  sleepInterval: 3e4,
3967
- // 30 seconds when sleeping
3968
3948
  idleThreshold: 5 * 6e4,
3969
- // 5 min → enter sleep
3970
3949
  extendedIdleTimeout: 60 * 6e4
3971
- // 1 hour → auto-exit
3972
3950
  },
3973
3951
  "Starter": {
3974
3952
  activeInterval: 15e3,
3975
- // 15 seconds
3976
3953
  sleepInterval: null,
3977
- // No sleep mode — goes straight to auto-exit
3978
3954
  idleThreshold: null,
3979
- // No idle detection
3980
3955
  extendedIdleTimeout: 15 * 6e4
3981
- // 15 minutes → auto-exit
3982
3956
  }
3983
3957
  };
3984
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
+ }
3985
4018
  function registerServeCommand(program2) {
3986
4019
  program2.command("serve").description("Start an Active Bob \u2014 receive and execute commands from the web app or another device").action(async () => {
3987
4020
  const config = getConfig();
@@ -4062,14 +4095,15 @@ async function startActiveBob(config, sessionId, machineId, projectName, tierCon
4062
4095
  if (tierConfig.sleepInterval) {
4063
4096
  console.log(BORDER5(" \u2551") + GRAY2(` Sleep: every ${tierConfig.sleepInterval / 1e3}s after ${tierConfig.idleThreshold / 6e4} min idle`));
4064
4097
  } else {
4065
- console.log(BORDER5(" \u2551") + GRAY2(` Sleep: disabled`));
4098
+ console.log(BORDER5(" \u2551") + GRAY2(" Sleep: disabled"));
4066
4099
  }
4067
4100
  if (tierConfig.extendedIdleTimeout) {
4068
4101
  console.log(BORDER5(" \u2551") + GRAY2(` Auto-exit: after ${tierConfig.extendedIdleTimeout / 6e4} min idle`));
4069
4102
  } else {
4070
- console.log(BORDER5(" \u2551") + GREEN2(` Auto-exit: never (Power tier)`));
4103
+ console.log(BORDER5(" \u2551") + GREEN2(" Auto-exit: never (Power tier)"));
4071
4104
  }
4072
4105
  console.log(BORDER5(" \u2551"));
4106
+ console.log(BORDER5(" \u2551") + GRAY2(" Capabilities: chat, consult, push, index, analyse, backup, restore"));
4073
4107
  console.log(BORDER5(" \u2551") + GRAY2(" Press Ctrl+C to stop."));
4074
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"));
4075
4109
  console.log("");
@@ -4089,7 +4123,9 @@ async function startActiveBob(config, sessionId, machineId, projectName, tierCon
4089
4123
  return;
4090
4124
  }
4091
4125
  let running = true;
4126
+ let sigintHandlerAdded = false;
4092
4127
  const cleanup = async () => {
4128
+ if (!running) return;
4093
4129
  running = false;
4094
4130
  console.log("");
4095
4131
  console.log(GRAY2(" \u{1F50C} Shutting down Active Bob..."));
@@ -4105,8 +4141,11 @@ async function startActiveBob(config, sessionId, machineId, projectName, tierCon
4105
4141
  console.log("");
4106
4142
  process.exit(0);
4107
4143
  };
4108
- process.on("SIGINT", cleanup);
4109
- process.on("SIGTERM", cleanup);
4144
+ if (!sigintHandlerAdded) {
4145
+ process.once("SIGINT", cleanup);
4146
+ process.once("SIGTERM", cleanup);
4147
+ sigintHandlerAdded = true;
4148
+ }
4110
4149
  let lastCommandTime = Date.now();
4111
4150
  let isSleeping = false;
4112
4151
  while (running) {
@@ -4149,7 +4188,7 @@ async function startActiveBob(config, sessionId, machineId, projectName, tierCon
4149
4188
  }
4150
4189
  lastCommandTime = Date.now();
4151
4190
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
4152
- 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 ? "..." : ""}"` : ""}`));
4153
4192
  const commandResult = await executeRemoteCommand(type, payload, config);
4154
4193
  await callCloudFunction("completeRemoteCommand", {
4155
4194
  conversationId: config.conversationId,
@@ -4172,7 +4211,7 @@ async function startActiveBob(config, sessionId, machineId, projectName, tierCon
4172
4211
  }
4173
4212
  }
4174
4213
  if (running) {
4175
- await new Promise((resolve2) => setTimeout(resolve2, currentInterval));
4214
+ await new Promise((resolve3) => setTimeout(resolve3, currentInterval));
4176
4215
  }
4177
4216
  }
4178
4217
  }
@@ -4185,9 +4224,13 @@ async function executeRemoteCommand(type, payload, config) {
4185
4224
  case "push":
4186
4225
  return await executePush(payload);
4187
4226
  case "index":
4188
- return { success: true, message: "Index command received. Feature in progress." };
4227
+ return await executeIndex(payload, config);
4189
4228
  case "analyse":
4190
- 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);
4191
4234
  case "autonomy":
4192
4235
  return { success: true, message: "Autonomy command received. Feature in progress." };
4193
4236
  default:
@@ -4219,9 +4262,7 @@ ${fullContext}` : "") },
4219
4262
  ];
4220
4263
  const response = await callLocalModel(config.localEndpoint, messages);
4221
4264
  const proposed = extractProposedFile(response);
4222
- if (proposed) {
4223
- await proposeAndWriteFile(proposed, true);
4224
- }
4265
+ if (proposed) await proposeAndWriteFile(proposed, true);
4225
4266
  return {
4226
4267
  success: true,
4227
4268
  text: response,
@@ -4256,11 +4297,7 @@ ${fullContext}` : "") },
4256
4297
  { role: "user", content: message }
4257
4298
  ];
4258
4299
  const response = await callLocalModel(config.localEndpoint, messages);
4259
- return {
4260
- success: true,
4261
- text: response,
4262
- referencedFiles: selectedFiles
4263
- };
4300
+ return { success: true, text: response, referencedFiles: selectedFiles };
4264
4301
  } catch (error) {
4265
4302
  return { success: false, error: error.message };
4266
4303
  }
@@ -4274,7 +4311,9 @@ async function executePush(payload) {
4274
4311
  const isRepo = await git.checkIsRepo();
4275
4312
  if (!isRepo) return { success: false, error: "Not a git repository." };
4276
4313
  const status = await git.status();
4277
- 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
+ }
4278
4317
  await git.add(".");
4279
4318
  const commitResult = await git.commit(message);
4280
4319
  const branch = (await git.branchLocal()).current;
@@ -4297,6 +4336,336 @@ async function executePush(payload) {
4297
4336
  return { success: false, error: error.message };
4298
4337
  }
4299
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
+ }
4300
4669
 
4301
4670
  // src/commands/remote.ts
4302
4671
  import chalk20 from "chalk";
@@ -4311,7 +4680,7 @@ var ERROR12 = chalk20.hex("#EF5350");
4311
4680
  var MUTED14 = chalk20.hex("#78909C");
4312
4681
  var BORDER6 = chalk20.hex("#455A64");
4313
4682
  function registerRemoteCommand(program2) {
4314
- 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) => {
4315
4684
  const config = getConfig();
4316
4685
  if (!config.loggedIn || !config.authToken) {
4317
4686
  console.log("");
@@ -4332,7 +4701,16 @@ function registerRemoteCommand(program2) {
4332
4701
  await showConnectionStatus(config);
4333
4702
  return;
4334
4703
  }
4335
- 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
+ ];
4336
4714
  if (!validTypes.includes(type)) {
4337
4715
  console.log("");
4338
4716
  console.log(ERROR12(` \u274C Invalid command type: "${type}"`));
@@ -4343,6 +4721,9 @@ function registerRemoteCommand(program2) {
4343
4721
  const payload = { conversationId: config.conversationId };
4344
4722
  if (message) payload.message = message;
4345
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;
4346
4727
  if ((type === "chat" || type === "consult" || type === "push") && !message) {
4347
4728
  console.log("");
4348
4729
  console.log(ERROR12(` \u274C ${type} requires a message.`));
@@ -4378,17 +4759,22 @@ async function runInteractiveRemote(config, targetSession) {
4378
4759
  }
4379
4760
  console.log("");
4380
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"));
4381
- 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})`));
4382
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"));
4383
4764
  console.log(BORDER6(" \u2551") + MUTED14(` Conversation: ${config.conversationId?.slice(0, 28)}...`));
4384
4765
  console.log(BORDER6(" \u2551") + MUTED14(" Commands dispatched to the remote machine."));
4385
4766
  console.log(BORDER6(" \u2551"));
4386
4767
  console.log(BORDER6(" \u2551") + chalk20.white(" Slash Commands:"));
4387
- console.log(BORDER6(" \u2551") + MUTED14(' \u25B8 /consult "msg" \u2014 Strategic advice'));
4388
- console.log(BORDER6(" \u2551") + MUTED14(' \u25B8 /push "msg" \u2014 Git commit & push'));
4389
- console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /index \u2014 Re-index project"));
4390
- console.log(BORDER6(" \u2551") + MUTED14(" \u25B8 /analyse \u2014 Run analysis"));
4391
- 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"));
4392
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"));
4393
4779
  console.log("");
4394
4780
  const rl = readline7.createInterface({
@@ -4412,7 +4798,7 @@ async function runInteractiveRemote(config, targetSession) {
4412
4798
  if (trimmed.startsWith("/consult ")) {
4413
4799
  const msg = trimmed.slice(9).trim().replace(/^["']|["']$/g, "");
4414
4800
  if (msg) {
4415
- await dispatchAndShow(config, "consult", { message: msg, conversationId: config.conversationId }, targetSession);
4801
+ await dispatchAndShow(config, "consult", { message: msg }, targetSession);
4416
4802
  } else {
4417
4803
  console.log(ERROR12(' \u274C Provide a message: /consult "your question"'));
4418
4804
  }
@@ -4422,7 +4808,7 @@ async function runInteractiveRemote(config, targetSession) {
4422
4808
  if (trimmed.startsWith("/push ")) {
4423
4809
  const msg = trimmed.slice(6).trim().replace(/^["']|["']$/g, "");
4424
4810
  if (msg) {
4425
- await dispatchAndShow(config, "push", { message: msg, conversationId: config.conversationId }, targetSession);
4811
+ await dispatchAndShow(config, "push", { message: msg }, targetSession);
4426
4812
  } else {
4427
4813
  console.log(ERROR12(' \u274C Provide a commit message: /push "your message"'));
4428
4814
  }
@@ -4430,28 +4816,53 @@ async function runInteractiveRemote(config, targetSession) {
4430
4816
  return;
4431
4817
  }
4432
4818
  if (trimmed === "/index") {
4433
- await dispatchAndShow(config, "index", { conversationId: config.conversationId }, targetSession);
4819
+ await dispatchAndShow(config, "index", {}, targetSession);
4434
4820
  prompt();
4435
4821
  return;
4436
4822
  }
4437
4823
  if (trimmed === "/analyse" || trimmed === "/analyze") {
4438
- 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);
4439
4845
  prompt();
4440
4846
  return;
4441
4847
  }
4442
- await dispatchAndShow(config, "chat", { message: trimmed, conversationId: config.conversationId }, targetSession);
4848
+ await dispatchAndShow(config, "chat", { message: trimmed }, targetSession);
4443
4849
  prompt();
4444
4850
  });
4445
4851
  };
4446
4852
  prompt();
4447
4853
  }
4448
4854
  async function dispatchAndShow(config, type, payload, targetSession) {
4449
- 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;
4450
4861
  try {
4451
4862
  const result = await callCloudFunction("sendRemoteCommand", {
4452
4863
  conversationId: config.conversationId,
4453
4864
  type,
4454
- payload,
4865
+ payload: { ...payload, conversationId: config.conversationId },
4455
4866
  targetSession: targetSession || null
4456
4867
  });
4457
4868
  if (!result?.success) {
@@ -4461,7 +4872,8 @@ async function dispatchAndShow(config, type, payload, targetSession) {
4461
4872
  return;
4462
4873
  }
4463
4874
  const commandId = result.commandId;
4464
- while (true) {
4875
+ while (pollCount < MAX_POLLS) {
4876
+ pollCount++;
4465
4877
  try {
4466
4878
  const pollResult = await callCloudFunction("getRemoteCommandResult", {
4467
4879
  conversationId: config.conversationId,
@@ -4486,6 +4898,14 @@ async function dispatchAndShow(config, type, payload, targetSession) {
4486
4898
  } else if (pollResult.result?.message) {
4487
4899
  console.log("");
4488
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
+ }
4489
4909
  } else if (pollResult.result?.error) {
4490
4910
  console.log("");
4491
4911
  console.log(ERROR12(` \u274C ${pollResult.result.error}`));
@@ -4502,8 +4922,13 @@ async function dispatchAndShow(config, type, payload, targetSession) {
4502
4922
  }
4503
4923
  } catch {
4504
4924
  }
4505
- await new Promise((resolve2) => setTimeout(resolve2, 2e3));
4925
+ await new Promise((resolve3) => setTimeout(resolve3, 2e3));
4506
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("");
4507
4932
  } catch (error) {
4508
4933
  spinner.stop();
4509
4934
  console.log(ERROR12(` \u274C ${error.message}`));
@@ -4546,12 +4971,17 @@ async function showConnectionStatus(config) {
4546
4971
  console.log("");
4547
4972
  if (activeSessions.length > 0) {
4548
4973
  console.log(MUTED14(" Commands:"));
4549
- console.log(MUTED14(" \u25B8 bob remote --interactive \u2014 Persistent session"));
4550
- console.log(MUTED14(' \u25B8 bob remote chat "message" \u2014 One-shot'));
4551
- console.log(MUTED14(' \u25B8 bob remote consult "msg" \u2014 Strategic advice'));
4552
- console.log(MUTED14(' \u25B8 bob remote push "msg" \u2014 Git push'));
4553
- console.log(MUTED14(" \u25B8 bob remote index \u2014 Re-index"));
4554
- 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"));
4555
4985
  console.log("");
4556
4986
  }
4557
4987
  } catch (error) {
@@ -4586,8 +5016,8 @@ async function discoverAndConnect(config) {
4586
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"));
4587
5017
  console.log("");
4588
5018
  const rl = readline7.createInterface({ input: process.stdin, output: process.stdout });
4589
- const answer = await new Promise((resolve2) => {
4590
- 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);
4591
5021
  });
4592
5022
  rl.close();
4593
5023
  const selection = parseInt(answer.trim());
@@ -5011,7 +5441,7 @@ async function runCloudProfiler(options) {
5011
5441
  }
5012
5442
  }
5013
5443
  function sleep5(ms) {
5014
- return new Promise((resolve2) => setTimeout(resolve2, ms));
5444
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
5015
5445
  }
5016
5446
 
5017
5447
  // src/ui/profile-dashboard.ts
@@ -5293,7 +5723,7 @@ async function renderProfileDashboard() {
5293
5723
  import * as path12 from "path";
5294
5724
  var AMBER5 = chalk23.hex("#FFAB00");
5295
5725
  var GREEN4 = chalk23.hex("#66BB6A");
5296
- var BLUE3 = chalk23.hex("#42A5F5");
5726
+ var BLUE2 = chalk23.hex("#42A5F5");
5297
5727
  var GRAY4 = chalk23.gray;
5298
5728
  var CYAN4 = chalk23.cyan;
5299
5729
  var RED4 = chalk23.hex("#EF5350");
@@ -5744,1187 +6174,720 @@ function getWeekNumber(date) {
5744
6174
  return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
5745
6175
  }
5746
6176
 
5747
- // src/commands/userbob.ts
6177
+ // src/commands/backup.ts
5748
6178
  import chalk24 from "chalk";
5749
- import * as readline8 from "readline";
5750
- import * as fs10 from "fs";
6179
+ import ora11 from "ora";
6180
+ import * as fs11 from "fs";
5751
6181
  import * as path13 from "path";
5752
6182
  import * as os4 from "os";
5753
- var PURPLE = chalk24.hex("#AB47BC");
5754
- 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;
5755
6188
  var GREEN5 = chalk24.hex("#66BB6A");
5756
- var CYAN5 = chalk24.hex("#26C6DA");
5757
6189
  var RED5 = chalk24.hex("#EF5350");
6190
+ var AMBER6 = chalk24.hex("#FFAB00");
5758
6191
  var GRAY5 = chalk24.gray;
5759
- var BLUE4 = chalk24.hex("#42A5F5");
5760
- var BOB_COLOR = chalk24.hex("#E66F24");
5761
6192
  var BORDER10 = chalk24.hex("#455A64");
5762
- var WHITE3 = chalk24.white;
5763
- var BOB_DIR3 = path13.join(os4.homedir(), ".bob");
5764
- function getSessionFilePath() {
5765
- const projectName = path13.basename(process.cwd());
5766
- return path13.join(BOB_DIR3, "projects", projectName, "userbob-session.json");
5767
- }
5768
- function writeSessionFile(data) {
5769
- const filePath = getSessionFilePath();
5770
- const dir = path13.dirname(filePath);
5771
- if (!fs10.existsSync(dir)) fs10.mkdirSync(dir, { recursive: true });
5772
- fs10.writeFileSync(filePath, JSON.stringify(data, null, 2));
5773
- }
5774
- function readSessionFile() {
5775
- const filePath = getSessionFilePath();
5776
- 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) {
5777
6231
  try {
5778
- return JSON.parse(fs10.readFileSync(filePath, "utf-8"));
6232
+ fs11.rmSync(tmpDir, { recursive: true, force: true });
5779
6233
  } catch {
5780
- return null;
5781
6234
  }
5782
6235
  }
5783
- function clearSessionFile() {
5784
- const filePath = getSessionFilePath();
5785
- 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());
5786
6248
  }
5787
- function renderHUD(sat, target, stag, stagTarget, div, divTarget, grading) {
5788
- const satBar = sat >= target ? GREEN5(`${sat}%`) : sat >= target * 0.7 ? AMBER6(`${sat}%`) : RED5(`${sat}%`);
5789
- console.log("");
5790
- 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"));
5791
- console.log(
5792
- ` SAT: ${satBar} \u2192 ${target}% \u2502 STAG: ${stag}/${stagTarget > 0 ? stagTarget : "\u221E"} \u2502 DIV: ${div}/${divTarget > 0 ? divTarget : "\u221E"} \u2502 GRADE: ${grading}`
5793
- );
5794
- 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"));
5795
- console.log("");
6249
+ function getProjectBackupDir(projectName) {
6250
+ return path13.join(BOB_DIR4, "projects", projectName);
5796
6251
  }
5797
- function stripMarkdown(text) {
5798
- 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, "/");
5799
6254
  }
5800
- function renderMessage(sender, message, audit) {
5801
- const cleanMsg = stripMarkdown(message);
5802
- const maxWidth = 70;
5803
- const lines = wrapText2(cleanMsg, maxWidth - 4);
5804
- if (sender === "userBob") {
5805
- const topBar = PURPLE(` \u250C\u2500 UserBob ${"\u2500".repeat(maxWidth - 13)}\u2510`);
5806
- const bottomBar = PURPLE(` \u2514${"\u2500".repeat(maxWidth - 2)}\u2518`);
6255
+ function handleBackupError(error) {
6256
+ if (error.message?.includes("BOB_BACKUP_LICENSE_REQUIRED")) {
5807
6257
  console.log("");
5808
- console.log(topBar);
5809
- for (const line of lines) {
5810
- const padded = line.padEnd(maxWidth - 4);
5811
- console.log(PURPLE(" \u2502") + ` ${padded}` + PURPLE(" \u2502"));
5812
- }
5813
- console.log(bottomBar);
5814
- if (audit) {
5815
- const chips = [];
5816
- if (audit.satisfactionScore !== void 0) chips.push(CYAN5(`[SAT: ${audit.satisfactionScore}%]`));
5817
- if (audit.resemblanceScore !== void 0) chips.push(BLUE4(`[RES: ${audit.resemblanceScore}%]`));
5818
- if (audit.reasoning) chips.push(GRAY5(`[${String(audit.reasoning).slice(0, 50)}...]`));
5819
- if (chips.length > 0) console.log(" " + chips.join(" "));
5820
- }
5821
- } else if (sender === "bob") {
5822
- const indent = " ";
5823
- const topBar = BOB_COLOR(`${indent}\u250C${"\u2500".repeat(maxWidth - 12)}\u2500 Bob \u2500\u2510`);
5824
- 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")) {
5825
6261
  console.log("");
5826
- console.log(topBar);
5827
- for (const line of lines) {
5828
- const padded = line.padEnd(maxWidth - 4);
5829
- console.log(BOB_COLOR(`${indent}\u2502`) + ` ${padded}` + BOB_COLOR(" \u2502"));
5830
- }
5831
- console.log(bottomBar);
5832
- } 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")) {
5833
6265
  console.log("");
5834
- 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"));
5835
- console.log(GRAY5(` ${cleanMsg}`));
5836
- 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"));
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")) {
6269
+ console.log("");
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"));
5837
6272
  } else {
5838
6273
  console.log("");
5839
- console.log(GRAY5(` [${sender.toUpperCase()}] ${cleanMsg}`));
6274
+ console.log(RED5(` \u274C ${error.message}`));
5840
6275
  }
6276
+ console.log("");
5841
6277
  }
5842
- function wrapText2(text, maxWidth) {
5843
- const lines = [];
5844
- const paragraphs = text.split("\n");
5845
- for (const paragraph of paragraphs) {
5846
- if (paragraph.trim() === "") {
5847
- lines.push("");
5848
- continue;
5849
- }
5850
- const words = paragraph.split(" ");
5851
- let currentLine = "";
5852
- for (const word of words) {
5853
- if ((currentLine + " " + word).trim().length > maxWidth) {
5854
- if (currentLine) lines.push(currentLine.trim());
5855
- currentLine = word;
5856
- } else {
5857
- currentLine = currentLine ? currentLine + " " + word : word;
5858
- }
5859
- }
5860
- if (currentLine.trim()) lines.push(currentLine.trim());
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;
5861
6302
  }
5862
- return lines;
5863
6303
  }
5864
- async function handleSlashCommand(input, config, conversationId) {
5865
- const trimmed = input.trim();
5866
- if (trimmed === "/status") {
5867
- try {
5868
- const response = await callCloudFunction("getCLIConversationMessages", { conversationId, since: null });
5869
- const state = response?.state || {};
5870
- console.log("");
5871
- console.log(AMBER6(" \u2500\u2500\u2500 Current Parameters \u2500\u2500\u2500"));
5872
- console.log(GRAY5(` Target Satisfaction : ${state.targetSatisfaction ?? "N/A"}`));
5873
- console.log(GRAY5(` Grading Standard : ${state.gradingStandard ?? "N/A"}`));
5874
- console.log(GRAY5(` Current Satisfaction: ${state.currentSatisfaction ?? "N/A"}`));
5875
- console.log(GRAY5(` Stalemate : ${state.stalemateState?.current ?? 0}/${state.stalemateState?.target ?? "\u221E"}`));
5876
- console.log(GRAY5(` Divergence : ${state.divergenceState?.current ?? 0}/${state.divergenceState?.target ?? "\u221E"}`));
5877
- console.log(GRAY5(` Status : ${state.simulationStatus ?? "UNKNOWN"}`));
5878
- console.log(GRAY5(` Active : ${state.userBobActive ?? "UNKNOWN"}`));
5879
- console.log("");
5880
- } catch {
5881
- console.log(RED5(" \u274C Could not fetch conversation state."));
5882
- }
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("");
5883
6332
  return;
5884
6333
  }
5885
- const setMatch = trimmed.match(/^\/set\s+(grading|target|stag|div)\s+(\d+)$/i);
5886
- if (setMatch) {
5887
- const param = setMatch[1].toLowerCase();
5888
- const value = parseInt(setMatch[2], 10);
5889
- const paramMap = {
5890
- grading: "gradingStandard",
5891
- target: "targetSatisfaction",
5892
- stag: "stalemateZone",
5893
- div: "divergenceThreshold"
5894
- };
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;
5895
6354
  try {
5896
- await callHTTPFunction("userSimManagerService", {
5897
- action: "updateParameters",
5898
- conversationId,
5899
- uid: config.uid,
5900
- email: config.email,
5901
- params: { [paramMap[param]]: value }
5902
- });
5903
- console.log(GREEN5(` \u2705 ${param} updated to ${value}`));
5904
- } catch (e) {
5905
- console.log(RED5(` \u274C Failed to update ${param}: ${e.message}`));
5906
- }
5907
- return;
5908
- }
5909
- const injectMatch = trimmed.match(/^\/inject\s+"(.+)"$/);
5910
- if (injectMatch) {
5911
- const note = injectMatch[1];
5912
- try {
5913
- await callHTTPFunction("userSimManagerService", {
5914
- action: "injectNote",
5915
- conversationId,
5916
- uid: config.uid,
5917
- email: config.email,
5918
- note
5919
- });
5920
- console.log(GREEN5(` \u2705 Director's note injected.`));
5921
- } catch (e) {
5922
- console.log(RED5(` \u274C Failed to inject note: ${e.message}`));
5923
- }
5924
- return;
5925
- }
5926
- console.log(GRAY5(' Commands: /set grading|target|stag|div <n> /inject "note" /status /abort'));
5927
- }
5928
- async function runPlatformSimulation(config, conversationId, mission, params) {
5929
- await callHTTPFunction("userSimManagerService", {
5930
- action: "updateParameters",
5931
- conversationId,
5932
- uid: config.uid,
5933
- email: config.email,
5934
- params: {
5935
- targetSatisfaction: params.target,
5936
- gradingStandard: params.grading,
5937
- stalemateZone: params.stag,
5938
- divergenceThreshold: params.div
5939
- }
5940
- });
5941
- await callHTTPFunction("userSimManagerService", {
5942
- action: "injectNote",
5943
- conversationId,
5944
- uid: config.uid,
5945
- email: config.email,
5946
- note: mission
5947
- });
5948
- console.log(GREEN5(" \u2705 Mission injected. Simulation is running."));
5949
- console.log("");
5950
- 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"));
5951
- console.log(GRAY5(" Messages will stream below as Bob and UserBob interact."));
5952
- console.log(GRAY5(" You can type commands at any time:"));
5953
- console.log("");
5954
- console.log(AMBER6(" /abort") + GRAY5(" \u2014 Stop the simulation immediately"));
5955
- console.log(AMBER6(" /set target 90") + GRAY5(" \u2014 Update satisfaction target"));
5956
- console.log(AMBER6(" /set grading 70") + GRAY5(" \u2014 Update Teacher's Curve"));
5957
- console.log(AMBER6(" /set stag 5") + GRAY5(" \u2014 Update stalemate threshold"));
5958
- console.log(AMBER6(" /set div 3") + GRAY5(" \u2014 Update divergence threshold"));
5959
- console.log(AMBER6(' /inject "note"') + GRAY5(" \u2014 Inject a director's note mid-session"));
5960
- console.log(AMBER6(" /status") + GRAY5(" \u2014 Show current simulation parameters"));
5961
- console.log("");
5962
- 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"));
5963
- console.log("");
5964
- let running = true;
5965
- let lastMessageTimestamp = 0;
5966
- let hudState = { sat: 0, target: params.target, stag: 0, stagTarget: params.stag, div: 0, divTarget: params.div, grading: params.grading };
5967
- const sigintHandler = async () => {
5968
- if (!running) return;
5969
- running = false;
5970
- console.log("\n");
5971
- console.log(AMBER6(" \u{1F6D1} Aborting simulation..."));
5972
- try {
5973
- await callHTTPFunction("userSimManagerService", {
5974
- action: "abortMission",
5975
- conversationId,
5976
- uid: config.uid,
5977
- email: config.email
5978
- });
5979
- console.log(GREEN5(" \u2705 Simulation aborted."));
5980
- } catch {
5981
- }
5982
- process.exit(0);
5983
- };
5984
- process.on("SIGINT", sigintHandler);
5985
- const rl = readline8.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
5986
- rl.setPrompt("");
5987
- rl.on("line", async (line) => {
5988
- const trimmed = line.trim();
5989
- if (!trimmed) return;
5990
- if (trimmed === "/abort" || trimmed === "abort") {
5991
- running = false;
5992
- console.log(AMBER6(" \u{1F6D1} Aborting simulation..."));
5993
- try {
5994
- await callHTTPFunction("userSimManagerService", {
5995
- action: "abortMission",
5996
- conversationId,
5997
- uid: config.uid,
5998
- email: config.email
6355
+ if (archiveName) {
6356
+ uploadResult = await callCloudFunction("cliBackupLicense", {
6357
+ action: "requestArchiveUpload",
6358
+ projectName,
6359
+ isGlobal,
6360
+ isSource: false,
6361
+ archiveName,
6362
+ estimatedSizeGB
6363
+ });
6364
+ } else {
6365
+ uploadResult = await callCloudFunction("cliBackupLicense", {
6366
+ action: "requestUpload",
6367
+ projectName,
6368
+ isGlobal,
6369
+ isSource: false,
6370
+ estimatedSizeGB
5999
6371
  });
6000
- console.log(GREEN5(" \u2705 Simulation aborted."));
6001
- } catch {
6002
6372
  }
6003
- rl.close();
6004
- process.exit(0);
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;
6005
6379
  }
6006
- await handleSlashCommand(trimmed, config, conversationId);
6007
- });
6008
- while (running) {
6009
- await new Promise((r) => setTimeout(r, 3e3));
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();
6010
6389
  try {
6011
- const response = await callCloudFunction("getCLIConversationMessages", {
6012
- conversationId,
6013
- since: lastMessageTimestamp || null
6014
- });
6015
- const messages = response?.messages || [];
6016
- const state = response?.state || {};
6017
- for (const msg of messages) {
6018
- renderMessage(msg.sender, msg.message, msg.simulationAudit);
6019
- if (msg.timestamp && msg.timestamp > lastMessageTimestamp) {
6020
- lastMessageTimestamp = msg.timestamp;
6021
- }
6022
- }
6023
- if (state.currentSatisfaction !== void 0) hudState.sat = state.currentSatisfaction;
6024
- if (state.targetSatisfaction !== void 0) hudState.target = state.targetSatisfaction;
6025
- if (state.gradingStandard !== void 0) hudState.grading = state.gradingStandard;
6026
- if (state.stalemateState) {
6027
- hudState.stag = state.stalemateState.current ?? hudState.stag;
6028
- hudState.stagTarget = state.stalemateState.target ?? hudState.stagTarget;
6029
- }
6030
- if (state.divergenceState) {
6031
- hudState.div = state.divergenceState.current ?? hudState.div;
6032
- hudState.divTarget = state.divergenceState.target ?? hudState.divTarget;
6033
- }
6034
- if (state.userBobActive === false || state.simulationStatus && state.simulationStatus !== "RUNNING") {
6035
- if (messages.length > 0) {
6036
- renderHUD(hudState.sat, hudState.target, hudState.stag, hudState.stagTarget, hudState.div, hudState.divTarget, hudState.grading);
6037
- }
6038
- console.log("");
6039
- console.log(AMBER6(` \u{1F3C1} Simulation ended: ${state.simulationStatus || "INACTIVE"}`));
6040
- console.log("");
6041
- running = false;
6042
- break;
6043
- }
6044
- if (messages.length > 0) {
6045
- renderHUD(hudState.sat, hudState.target, hudState.stag, hudState.stagTarget, hudState.div, hudState.divTarget, hudState.grading);
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
+ });
6046
6405
  }
6047
- } catch (e) {
6048
- console.log(RED5(` \u274C Poll error: ${e.message}`));
6406
+ recordSpinner.succeed(GREEN5(" Recording usage ..."));
6407
+ } catch {
6408
+ recordSpinner.warn(AMBER6(" Usage recording failed (non-fatal). Backup was saved."));
6049
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);
6050
6425
  }
6051
- rl.close();
6052
- process.removeListener("SIGINT", sigintHandler);
6053
6426
  }
6054
- async function runLocalSimulation(config, dnaString, mission, params) {
6055
- writeSessionFile({ active: true, turns: 0, mission });
6056
- let running = true;
6057
- let turns = 0;
6058
- let conversationHistory = [];
6059
- let sat = 0;
6060
- let stalemateCurrent = 0;
6061
- let divergenceCurrent = 0;
6062
- let lastStatus = "";
6063
- const sigintHandler = () => {
6064
- running = false;
6065
- writeSessionFile({ active: false });
6066
- clearSessionFile();
6067
- console.log("\n" + AMBER6(" \u{1F6D1} Simulation stopped."));
6068
- process.exit(0);
6069
- };
6070
- process.on("SIGINT", sigintHandler);
6071
- const rl = readline8.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
6072
- rl.setPrompt("");
6073
- rl.on("line", (line) => {
6074
- const t = line.trim();
6075
- if (t === "/abort" || t === "abort") {
6076
- running = false;
6077
- writeSessionFile({ active: false });
6078
- clearSessionFile();
6079
- console.log(AMBER6(" \u{1F6D1} Simulation stopped."));
6080
- rl.close();
6081
- process.exit(0);
6082
- }
6083
- if (t.startsWith("/set ")) {
6084
- const m = t.match(/^\/set\s+(grading|target|stag|div)\s+(\d+)$/i);
6085
- if (m) {
6086
- const val = parseInt(m[2], 10);
6087
- if (m[1] === "grading") params.grading = val;
6088
- if (m[1] === "target") params.target = val;
6089
- if (m[1] === "stag") params.stag = val;
6090
- if (m[1] === "div") params.div = val;
6091
- console.log(GREEN5(` \u2705 ${m[1]} updated to ${val} (local)`));
6092
- }
6093
- }
6094
- if (t === "/status") {
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"));
6095
6443
  console.log("");
6096
- console.log(AMBER6(" \u2500\u2500\u2500 Local Sim Parameters \u2500\u2500\u2500"));
6097
- console.log(GRAY5(` Target: ${params.target} \u2502 Grading: ${params.grading} \u2502 Stag Limit: ${params.stag} \u2502 Div Limit: ${params.div}`));
6098
- console.log(GRAY5(` Current SAT: ${sat} \u2502 Turns: ${turns} \u2502 Stag: ${stalemateCurrent} \u2502 Div: ${divergenceCurrent}`));
6444
+ console.log(RED5(` \u274C File not found: ${filePath}`));
6099
6445
  console.log("");
6446
+ return;
6100
6447
  }
6101
- });
6102
- 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}`;
6103
- 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.
6104
-
6105
- Mission: ${mission}
6106
-
6107
- ${dnaString}
6108
-
6109
- 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:
6110
- {"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.
6111
-
6112
- Mission: ${mission}
6113
-
6114
- 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:
6115
- {"satisfactionScore": <0-100>, "status": "CONVERGING|STAGNATING|DIVERGING"}`;
6116
- 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"));
6117
- console.log(GRAY5(" Bob and UserBob will converse autonomously below."));
6118
- console.log(GRAY5(" Commands:"));
6119
- console.log(AMBER6(" /abort") + GRAY5(" \u2014 Stop the simulation"));
6120
- console.log(AMBER6(" /set target 90") + GRAY5(" \u2014 Update satisfaction target"));
6121
- console.log(AMBER6(" /set grading 70") + GRAY5(" \u2014 Update Teacher's Curve"));
6122
- console.log(AMBER6(" /set stag 5") + GRAY5(" \u2014 Update stalemate threshold"));
6123
- console.log(AMBER6(" /set div 3") + GRAY5(" \u2014 Update divergence threshold"));
6124
- console.log(AMBER6(" /status") + GRAY5(" \u2014 Show current parameters"));
6125
- 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"));
6126
- console.log("");
6127
- const kickstart = `Mission received: "${mission}". Bob, what's your first move?`;
6128
- console.log(PURPLE(" UserBob > ") + WHITE3(kickstart));
6129
- conversationHistory.push({ role: "user", content: kickstart });
6130
- while (running) {
6131
- const session = readSessionFile();
6132
- if (!session?.active) {
6133
- running = false;
6134
- break;
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;
6135
6455
  }
6136
- turns++;
6456
+ }
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
6502
+ };
6137
6503
  try {
6138
- const bobMessages = [
6139
- { role: "system", content: bobSystem },
6140
- ...conversationHistory
6141
- ];
6142
- const bobResponse = await callLocalModel(config.localEndpoint, bobMessages);
6143
- console.log(BOB_COLOR(" Bob > ") + WHITE3(bobResponse));
6144
- conversationHistory.push({ role: "assistant", content: bobResponse });
6145
- const ubMessages = [
6146
- { role: "system", content: userBobSystem },
6147
- ...conversationHistory
6148
- ];
6149
- const ubResponse = await callLocalModel(config.localEndpoint, ubMessages);
6150
- const jsonMatch = ubResponse.match(/\{[^}]*"satisfactionScore"[^}]*\}/);
6151
- const cleanResponse = ubResponse.replace(/\{[^}]*"satisfactionScore"[^}]*\}/, "").trim();
6152
- console.log(PURPLE(" UserBob > ") + WHITE3(cleanResponse));
6153
- let auditChips = [];
6154
- if (jsonMatch) {
6155
- try {
6156
- const audit = JSON.parse(jsonMatch[0]);
6157
- const rawScore = audit.satisfactionScore || 0;
6158
- sat = Math.round(rawScore * (params.grading / 100));
6159
- lastStatus = audit.status || "";
6160
- auditChips = [CYAN5(`[SAT: ${sat}%]`), BLUE4(`[RAW: ${rawScore}]`), GRAY5(`[${lastStatus}]`)];
6161
- if (lastStatus === "STAGNATING") {
6162
- stalemateCurrent++;
6163
- if (params.stag > 0 && stalemateCurrent >= params.stag) {
6164
- console.log(" " + auditChips.join(" "));
6165
- renderHUD(sat, params.target, stalemateCurrent, params.stag, divergenceCurrent, params.div, params.grading);
6166
- console.log(AMBER6(` \u{1F3C1} Stalemate threshold reached (${stalemateCurrent}/${params.stag}). Simulation ended.`));
6167
- running = false;
6168
- break;
6169
- }
6170
- } else if (lastStatus === "DIVERGING") {
6171
- divergenceCurrent++;
6172
- stalemateCurrent = 0;
6173
- if (params.div > 0 && divergenceCurrent >= params.div) {
6174
- console.log(" " + auditChips.join(" "));
6175
- renderHUD(sat, params.target, stalemateCurrent, params.stag, divergenceCurrent, params.div, params.grading);
6176
- console.log(AMBER6(` \u{1F3C1} Divergence threshold reached (${divergenceCurrent}/${params.div}). Simulation ended.`));
6177
- running = false;
6178
- break;
6179
- }
6180
- } else if (lastStatus === "CONVERGING") {
6181
- stalemateCurrent = 0;
6182
- divergenceCurrent = 0;
6183
- }
6184
- } catch {
6185
- }
6186
- }
6187
- if (auditChips.length) console.log(" " + auditChips.join(" "));
6188
- conversationHistory.push({ role: "user", content: ubResponse });
6189
- writeSessionFile({ active: true, turns, mission, sat });
6190
- renderHUD(sat, params.target, stalemateCurrent, params.stag, divergenceCurrent, params.div, params.grading);
6191
- if (sat >= params.target) {
6192
- console.log(GREEN5(` \u{1F3AF} Target satisfaction ${params.target}% reached! Mission complete.`));
6193
- running = false;
6194
- break;
6195
- }
6196
- await new Promise((r) => setTimeout(r, 1e3));
6197
- } catch (e) {
6198
- console.log(RED5(` \u274C Local model error: ${e.message}`));
6199
- console.log(GRAY5(" Retrying in 3 seconds..."));
6200
- await new Promise((r) => setTimeout(r, 3e3));
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;
6201
6511
  }
6202
- }
6203
- clearSessionFile();
6204
- rl.close();
6205
- process.removeListener("SIGINT", sigintHandler);
6206
- console.log("");
6207
- console.log(GRAY5(` Session complete. ${turns} turns processed.`));
6208
- console.log("");
6209
- }
6210
- function registerUserBobCommand(program2) {
6211
- 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) => {
6212
- const config = getConfig();
6213
- const params = {
6214
- target: parseInt(options.target, 10),
6215
- grading: parseInt(options.grading, 10),
6216
- stag: parseInt(options.stag, 10),
6217
- div: parseInt(options.div, 10)
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
6218
6528
  };
6219
- const usePlatform = !options.local && isAuthenticated();
6220
- console.log("");
6221
- 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"));
6222
- console.log(BORDER10(" \u2551") + PURPLE(" \u{1F916} UserBob \u2014 Digital Twin Simulation"));
6223
- console.log(BORDER10(" \u2551") + GRAY5(` Mode: ${usePlatform ? "Platform (Tier 3)" : "Local Ollama (Tier 1)"}`));
6224
- 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"));
6225
- console.log("");
6226
- const dna = buildDNAString();
6227
- if (dna) {
6228
- console.log(GREEN5(" \u2705 Behavioral DNA loaded."));
6229
- } else {
6230
- console.log(AMBER6(" \u26A0\uFE0F No behavioral profile found."));
6231
- console.log(GRAY5(" UserBob performs significantly better with your DNA loaded."));
6232
- console.log("");
6233
- const rl = readline8.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
6234
- const answer = await new Promise((resolve2) => rl.question(AMBER6(" Run `bob profile --today` now? (y/n): "), resolve2));
6235
- rl.close();
6236
- if (answer.trim().toLowerCase() === "y") {
6237
- console.log("");
6238
- console.log(GRAY5(" Run `bob profile --today` in a separate terminal, then re-run `bob userbob`."));
6239
- process.exit(0);
6240
- } else {
6241
- console.log("");
6242
- console.log(RED5(" \u26A0\uFE0F Running in Generic Mode \u2014 no behavioral profile loaded."));
6243
- console.log(RED5(" UserBob will respond using project context only."));
6244
- console.log(RED5(" Responses won't reflect your personal communication style,"));
6245
- console.log(RED5(" decision patterns, or engineering philosophy."));
6246
- console.log(GRAY5(" Run `bob profile --today` anytime to unlock full personalization."));
6247
- console.log("");
6248
- }
6529
+ try {
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."));
6249
6534
  }
6250
- let mission = missionArgs.length > 0 ? missionArgs.join(" ") : "";
6251
- if (!mission && !options.resume) {
6252
- const mrl = readline8.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
6253
- mission = await new Promise((resolve2) => mrl.question(AMBER6(" What's the mission? > "), resolve2));
6254
- mrl.close();
6255
- if (!mission.trim()) {
6256
- console.log(RED5(" \u274C Mission cannot be empty. Exiting."));
6257
- process.exit(1);
6258
- }
6259
- mission = mission.trim();
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}`));
6260
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"));
6261
6545
  console.log("");
6262
- console.log(GRAY5(` Target: ${params.target}% \u2502 Grade: ${params.grading} \u2502 Stag: ${params.stag || "\u221E"} \u2502 Div: ${params.div || "\u221E"}`));
6263
- console.log("");
6264
- if (usePlatform) {
6265
- const conversationId = config.conversationId;
6266
- if (!conversationId) {
6267
- console.log(RED5(" \u274C No active conversation. Run `bob conversations join` first."));
6268
- process.exit(1);
6269
- }
6270
- if (options.resume) {
6271
- console.log(AMBER6(" \u{1F504} Resuming simulation (no new mission note)..."));
6272
- await callHTTPFunction("userSimManagerService", {
6273
- action: "resumeMission",
6274
- conversationId,
6275
- uid: config.uid,
6276
- email: config.email
6277
- });
6278
- console.log(GREEN5(" \u2705 Simulation resumed. Entering watch mode..."));
6279
- console.log("");
6280
- await runPlatformSimulation(config, conversationId, mission || "Resumed session", params);
6281
- } else {
6282
- await runPlatformSimulation(config, conversationId, mission, params);
6283
- }
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);
6551
+ }
6552
+ }
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
6567
+ });
6284
6568
  return;
6285
6569
  }
6286
- if (!config.localEndpoint) {
6287
- console.log(RED5(" \u274C No local model configured."));
6288
- console.log(GRAY5(" Run: bob config set localEndpoint http://127.0.0.1:11434/api/chat"));
6289
- process.exit(1);
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}/`;
6290
6579
  }
6291
- await runLocalSimulation(config, dna, mission, params);
6292
- });
6293
- }
6294
-
6295
- // src/commands/command-center.ts
6296
- import chalk25 from "chalk";
6297
- import inquirer from "inquirer";
6298
- var ORANGE2 = chalk25.hex("#E66F24");
6299
- var AMBER7 = chalk25.hex("#FFAB00");
6300
- var GREEN6 = chalk25.hex("#66BB6A");
6301
- var RED6 = chalk25.hex("#EF5350");
6302
- var BLUE5 = chalk25.hex("#42A5F5");
6303
- var PURPLE2 = chalk25.hex("#AB47BC");
6304
- var CYAN6 = chalk25.hex("#26C6DA");
6305
- var TEAL = chalk25.hex("#26A69A");
6306
- var GRAY6 = chalk25.gray;
6307
- var WHITE4 = chalk25.white;
6308
- var BORDER11 = chalk25.hex("#455A64");
6309
- function statusColor(status) {
6310
- switch (status) {
6311
- case "queued":
6312
- return GRAY6;
6313
- case "awaiting_approval":
6314
- return AMBER7;
6315
- case "in_progress":
6316
- return BLUE5;
6317
- case "completed":
6318
- return GREEN6;
6319
- case "failed":
6320
- return RED6;
6321
- case "denied":
6322
- return RED6;
6323
- default:
6324
- return GRAY6;
6325
- }
6326
- }
6327
- function statusLabel(status) {
6328
- switch (status) {
6329
- case "queued":
6330
- return "QUEUED";
6331
- case "awaiting_approval":
6332
- return "NEEDS APPROVAL";
6333
- case "in_progress":
6334
- return "IN PROGRESS";
6335
- case "completed":
6336
- return "COMPLETE";
6337
- case "failed":
6338
- return "FAILED";
6339
- case "denied":
6340
- return "DENIED";
6341
- default:
6342
- return status.toUpperCase();
6343
- }
6344
- }
6345
- function categoryColor(category) {
6346
- const map = {
6347
- security: RED6,
6348
- frontend: BLUE5,
6349
- backend: GREEN6,
6350
- cloud_functions: PURPLE2,
6351
- documentation: TEAL,
6352
- testing: AMBER7,
6353
- review: CYAN6,
6354
- fullstack: ORANGE2
6355
- };
6356
- return map[category] || GRAY6;
6357
- }
6358
- function confidenceColor(confidence) {
6359
- if (confidence >= 80) return GREEN6;
6360
- if (confidence >= 50) return AMBER7;
6361
- return RED6;
6362
- }
6363
- function formatTimestamp(ms) {
6364
- if (!ms) return "\u2014";
6365
- return new Date(ms).toLocaleString("en-US", {
6366
- month: "short",
6367
- day: "numeric",
6368
- hour: "numeric",
6369
- minute: "2-digit",
6370
- hour12: true
6371
- });
6372
- }
6373
- function stripMarkdown2(text) {
6374
- 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();
6375
- }
6376
- function renderStats(stats) {
6377
- console.log("");
6378
- 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"));
6379
- const parts = [];
6380
- if (stats.awaiting_approval > 0) parts.push(AMBER7(`${stats.awaiting_approval} PENDING`));
6381
- if (stats.in_progress > 0) parts.push(BLUE5(`${stats.in_progress} RUNNING`));
6382
- if (stats.completed > 0) parts.push(GREEN6(`${stats.completed} DONE`));
6383
- if (stats.failed > 0) parts.push(RED6(`${stats.failed} FAILED`));
6384
- if (stats.denied > 0) parts.push(GRAY6(`${stats.denied} DENIED`));
6385
- parts.push(GRAY6(`${stats.total} TOTAL`));
6386
- console.log(" " + parts.join(GRAY6(" \u2502 ")));
6387
- 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"));
6388
- console.log("");
6389
- }
6390
- function renderTaskDetail(task) {
6391
- const catColor = categoryColor(task.request.category);
6392
- const confColor = confidenceColor(task.request.confidence);
6393
- const sColor = statusColor(task.outcome.status);
6394
- console.log("");
6395
- 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"));
6396
- console.log(BORDER11(" \u2502"));
6397
- console.log(BORDER11(" \u2502 ") + WHITE4(task.request.description));
6398
- console.log(BORDER11(" \u2502"));
6399
- 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)}`));
6400
- if (task.request.targetFile) {
6401
- console.log(BORDER11(" \u2502 ") + GRAY6("File: ") + CYAN6(task.request.targetFile));
6402
- }
6403
- console.log(BORDER11(" \u2502"));
6404
- console.log(BORDER11(" \u2502 ") + GRAY6("Priority: ") + WHITE4(`P${task.priority}`) + GRAY6(" \u2502 Difficulty: ") + WHITE4(task.difficulty.toUpperCase()) + GRAY6(" \u2502 Created: ") + WHITE4(formatTimestamp(task.createdAt)));
6405
- console.log(BORDER11(" \u2502"));
6406
- console.log(BORDER11(" \u2502 ") + AMBER7("TRIGGER"));
6407
- console.log(BORDER11(" \u2502 ") + GRAY6(stripMarkdown2(task.trigger.reasoning).slice(0, 120) + "..."));
6408
- if (task.trigger.turnSatisfaction !== null) {
6409
- console.log(BORDER11(" \u2502 ") + GRAY6("SAT at dispatch: ") + CYAN6(`${task.trigger.turnSatisfaction}%`));
6410
- }
6411
- if (task.outcome.resultSummary) {
6412
- console.log(BORDER11(" \u2502"));
6413
- console.log(BORDER11(" \u2502 ") + GREEN6("RESULT"));
6414
- console.log(BORDER11(" \u2502 ") + GRAY6(stripMarkdown2(task.outcome.resultSummary).slice(0, 200) + "..."));
6415
- }
6416
- if (task.outcome.filesModified && task.outcome.filesModified.length > 0) {
6417
- console.log(BORDER11(" \u2502"));
6418
- console.log(BORDER11(" \u2502 ") + AMBER7("FILES MODIFIED"));
6419
- for (const file of task.outcome.filesModified) {
6420
- const icon = file.action === "created" ? GREEN6("+") : AMBER7("~");
6421
- console.log(BORDER11(" \u2502 ") + icon + " " + CYAN6(file.path || file));
6422
- }
6423
- }
6424
- if (task.outcome.error) {
6425
- console.log(BORDER11(" \u2502"));
6426
- console.log(BORDER11(" \u2502 ") + RED6("ERROR"));
6427
- console.log(BORDER11(" \u2502 ") + RED6(task.outcome.error.slice(0, 150)));
6428
- }
6429
- if (task.isDenied && task.denyReason) {
6430
- console.log(BORDER11(" \u2502"));
6431
- console.log(BORDER11(" \u2502 ") + RED6("DENIED BY: ") + GRAY6(task.deniedBy || "Unknown"));
6432
- console.log(BORDER11(" \u2502 ") + RED6("REASON: ") + GRAY6(task.denyReason));
6433
- }
6434
- if (task.outcome.turnsUsed || task.outcome.tokensConsumed) {
6435
- console.log(BORDER11(" \u2502"));
6436
- if (task.outcome.turnsUsed) {
6437
- console.log(BORDER11(" \u2502 ") + GRAY6(`Turns: ${task.outcome.turnsUsed} \u2502 Tokens: ${task.outcome.tokensConsumed || 0} \u2502 Provider: ${task.outcome.provider || "unknown"}`));
6438
- }
6439
- }
6440
- console.log(BORDER11(" \u2502"));
6441
- 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"));
6442
- console.log("");
6443
- }
6444
- async function runDecisionStream(conversationId) {
6445
- console.log("");
6446
- 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"));
6447
- console.log(GRAY6(" Live feed of all autonomous decisions. Ctrl+C to exit.\n"));
6448
- let lastCount = 0;
6449
- let running = true;
6450
- process.on("SIGINT", () => {
6451
- running = false;
6452
- console.log("\n" + AMBER7(" Stream ended."));
6453
- process.exit(0);
6580
+ await runBackup({
6581
+ config,
6582
+ projectName,
6583
+ isGlobal,
6584
+ archiveName: options.archive,
6585
+ sourceDir,
6586
+ displayName
6587
+ });
6454
6588
  });
6455
- while (running) {
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();
6456
6597
  try {
6457
- const response = await callCloudFunction("getCLIAutonomousTasks", { conversationId });
6458
- const tasks = response?.tasks || [];
6459
- if (tasks.length > lastCount) {
6460
- const newTasks = tasks.slice(0, tasks.length - lastCount);
6461
- for (const task of newTasks.reverse()) {
6462
- const sColor = statusColor(task.outcome.status);
6463
- const catColor = categoryColor(task.request.category);
6464
- console.log(
6465
- 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)}`)
6466
- );
6598
+ const result = await callCloudFunction("getCLIBackupStatus", {
6599
+ projectName,
6600
+ isGlobal,
6601
+ isSource,
6602
+ filePath
6603
+ });
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)}`);
6626
+ }
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}`));
6467
6640
  }
6468
- lastCount = tasks.length;
6469
6641
  }
6470
- } catch (e) {
6471
- console.log(RED6(` \u274C Poll error: ${e.message}`));
6472
- }
6473
- await new Promise((r) => setTimeout(r, 3e3));
6474
- }
6475
- }
6476
- async function runSettings(conversationId) {
6477
- let response;
6478
- try {
6479
- response = await callCloudFunction("getCLIAutonomousTasks", { conversationId });
6480
- } catch (e) {
6481
- console.log(RED6(` \u274C Failed to fetch settings: ${e.message}`));
6482
- return;
6483
- }
6484
- const settings = response?.settings || {};
6485
- console.log("");
6486
- 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"));
6487
- console.log(GRAY6(` Current threshold: ${settings.autonomousConfidenceThreshold ?? 75}%`));
6488
- console.log(GRAY6(" Tasks below this confidence level require your approval."));
6489
- console.log("");
6490
- const { threshold } = await inquirer.prompt([
6491
- {
6492
- type: "number",
6493
- name: "threshold",
6494
- message: AMBER7(" Confidence threshold (0-100):"),
6495
- default: settings.autonomousConfidenceThreshold ?? 75,
6496
- validate: (val) => val >= 0 && val <= 100 ? true : "Must be 0-100"
6497
- }
6498
- ]);
6499
- const categories = ["security", "frontend", "backend", "cloud_functions", "documentation", "testing", "review", "fullstack"];
6500
- const overrides = { ...settings.autonomousCategoryOverrides || {} };
6501
- console.log("");
6502
- console.log(GRAY6(" Category overrides:"));
6503
- console.log("");
6504
- for (const cat of categories) {
6505
- const current = overrides[cat] || "threshold";
6506
- const { override } = await inquirer.prompt([
6507
- {
6508
- type: "select",
6509
- name: "override",
6510
- message: categoryColor(cat)(` ${cat.padEnd(20)}`),
6511
- default: current,
6512
- choices: [
6513
- { name: GRAY6("Use Threshold"), value: "threshold" },
6514
- { name: GREEN6("Always Auto-Execute"), value: "autonomous" },
6515
- { name: RED6("Always Require Approval"), value: "approval_required" }
6516
- ]
6517
- }
6518
- ]);
6519
- if (override === "threshold") {
6520
- delete overrides[cat];
6521
- } else {
6522
- overrides[cat] = override;
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"));
6643
+ console.log("");
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"));
6652
+ console.log("");
6653
+ } catch (error) {
6654
+ spinner.stop();
6655
+ handleBackupError(error);
6523
6656
  }
6524
- }
6525
- try {
6526
- await callCloudFunction("updateCLIAutonomySettings", {
6527
- conversationId,
6528
- autonomousConfidenceThreshold: threshold,
6529
- autonomousCategoryOverrides: overrides
6530
- });
6531
- console.log("");
6532
- console.log(GREEN6(" \u2705 Settings saved."));
6533
- } catch {
6534
- console.log("");
6535
- console.log(AMBER7(" \u26A0\uFE0F `updateCLIAutonomySettings` not deployed yet."));
6536
- console.log(GRAY6(" Update via web app for now, or deploy the CF first."));
6537
- }
6538
- console.log("");
6539
- }
6540
- async function runTaskBoard(conversationId) {
6541
- let continueLoop = true;
6542
- while (continueLoop) {
6543
- let response;
6657
+ });
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;
6544
6668
  try {
6545
- response = await callCloudFunction("getCLIAutonomousTasks", { conversationId });
6546
- } catch (e) {
6547
- console.log(RED6(` \u274C Failed to fetch tasks: ${e.message}`));
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);
6548
6679
  return;
6549
6680
  }
6550
- const tasks = response?.tasks || [];
6551
- const stats = response?.stats || {};
6552
- renderStats(stats);
6553
- if (tasks.length === 0) {
6554
- console.log(GRAY6(" No autonomous tasks found for this conversation."));
6555
- console.log(GRAY6(" Tasks appear here when UserBob dispatches work to Mini Bob."));
6681
+ const { versions, archives } = statusResult;
6682
+ if (versions.length === 0 && archives.length === 0) {
6683
+ console.log("");
6684
+ const scopeMsg = isSource ? filePath ? `file "${filePath}"` : `source of ${projectName}` : isGlobal ? "global" : projectName;
6685
+ console.log(AMBER6(` \u26A0\uFE0F No backups found for ${scopeMsg}.`));
6556
6686
  console.log("");
6557
6687
  return;
6558
6688
  }
6559
- const seen = /* @__PURE__ */ new Map();
6560
- for (const task of tasks) {
6561
- const key = task.request.description.slice(0, 80);
6562
- if (!seen.has(key) || (task.createdAt || 0) > (seen.get(key).createdAt || 0)) {
6563
- seen.set(key, task);
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
+ });
6564
6698
  }
6565
6699
  }
6566
- const dedupedTasks = Array.from(seen.values());
6567
- const taskChoices = dedupedTasks.map((task) => {
6568
- const sColor = statusColor(task.outcome.status);
6569
- const catColor = categoryColor(task.request.category);
6570
- const desc = task.request.description.slice(0, 55) + (task.request.description.length > 55 ? "..." : "");
6571
- return {
6572
- name: sColor(` \u25CF ${statusLabel(task.outcome.status).padEnd(16)}`) + " " + catColor(`[${task.request.category.padEnd(15)}]`) + " " + WHITE4(desc),
6573
- value: task.id,
6574
- short: task.request.description.slice(0, 40)
6575
- };
6576
- });
6577
- taskChoices.push({
6578
- 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"),
6579
- value: "__separator__",
6580
- short: "",
6581
- disabled: true
6582
- });
6583
- taskChoices.push({
6584
- name: GRAY6(" \u21A9 Exit Command Center"),
6585
- value: "__exit__",
6586
- short: "Exit"
6587
- });
6588
- const { selectedTaskId } = await inquirer.prompt([
6589
- {
6590
- type: "select",
6591
- name: "selectedTaskId",
6592
- message: ORANGE2(" Select a task to inspect:"),
6593
- choices: taskChoices,
6594
- pageSize: 14
6595
- }
6596
- ]);
6597
- if (selectedTaskId === "__exit__" || selectedTaskId === "__separator__") {
6598
- continueLoop = false;
6599
- break;
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 }
6707
+ });
6708
+ }
6600
6709
  }
6601
- const selectedTask = dedupedTasks.find((t) => t.id === selectedTaskId);
6602
- if (!selectedTask) continue;
6603
- renderTaskDetail(selectedTask);
6604
- const actions = [];
6605
- if (selectedTask.outcome.status === "awaiting_approval") {
6606
- actions.push({ name: GREEN6(" \u2705 Approve \u2014 execute this task now"), value: "approve", short: "Approve" });
6607
- actions.push({ name: RED6(" \u274C Deny \u2014 reject this task"), value: "deny", short: "Deny" });
6608
- }
6609
- actions.push({ name: AMBER7(" \u21A9 Back to task list"), value: "back", short: "Back" });
6610
- actions.push({ name: GRAY6(" \u2715 Exit Command Center"), value: "exit", short: "Exit" });
6611
- const { action } = await inquirer.prompt([
6612
- {
6613
- type: "select",
6614
- name: "action",
6615
- message: ORANGE2(" What would you like to do?"),
6616
- choices: actions
6617
- }
6618
- ]);
6619
- if (action === "approve") {
6620
- const { confirmed } = await inquirer.prompt([
6621
- {
6622
- type: "confirm",
6623
- name: "confirmed",
6624
- message: AMBER7(" Approve task and execute autonomously?"),
6625
- default: true
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;
6725
+ }
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."));
6734
+ } else {
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."));
6737
+ }
6738
+ console.log("");
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."));
6747
+ console.log("");
6748
+ return;
6749
+ }
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);
6626
6795
  }
6627
- ]);
6628
- if (confirmed) {
6629
- console.log("");
6630
- 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"));
6631
- console.log(GRAY6(" Mini Bob is executing. Streaming live logs...\n"));
6632
- callCloudFunction("approveAutonomousTask", {
6633
- conversationId,
6634
- taskId: selectedTask.id,
6635
- action: "approve"
6636
- }).then(() => {
6637
- }).catch((e) => {
6638
- console.log(RED6(`
6639
- \u274C Execution error: ${e.message}`));
6640
- });
6641
- let running = true;
6642
- let seenLogIds = /* @__PURE__ */ new Set();
6643
- let pollErrors = 0;
6644
- const maxPollErrors = 5;
6645
- const sigintHandler = () => {
6646
- running = false;
6647
- console.log("\n" + AMBER7(" Stream ended. Task continues in background."));
6648
- process.exit(0);
6649
- };
6650
- process.on("SIGINT", sigintHandler);
6651
- while (running) {
6652
- await new Promise((r) => setTimeout(r, 2e3));
6653
- try {
6654
- const taskResponse = await callCloudFunction("getCLIAutonomousTasks", {
6655
- conversationId,
6656
- statusFilter: null
6657
- });
6658
- const updatedTask = taskResponse?.tasks?.find((t) => t.id === selectedTask.id);
6659
- const logResponse = await callCloudFunction("getCLITaskExecutionLog", {
6660
- conversationId,
6661
- taskId: selectedTask.id
6662
- });
6663
- const logEntries = logResponse?.entries || [];
6664
- for (const entry of logEntries) {
6665
- if (seenLogIds.has(entry.id)) continue;
6666
- seenLogIds.add(entry.id);
6667
- const stage = entry.stage || "EXECUTION";
6668
- let prefix;
6669
- switch (stage) {
6670
- case "INIT":
6671
- prefix = CYAN6(" [INIT] ");
6672
- break;
6673
- case "TOOL_CALL":
6674
- prefix = AMBER7(" [TOOL] ");
6675
- break;
6676
- case "FALLBACK":
6677
- prefix = AMBER7(" [FALLBACK] ");
6678
- break;
6679
- case "COMPLETE":
6680
- prefix = GREEN6(" [DONE] ");
6681
- break;
6682
- case "ERROR":
6683
- prefix = RED6(" [ERROR] ");
6684
- break;
6685
- case "APPROVED":
6686
- prefix = GREEN6(" [APPROVED] ");
6687
- break;
6688
- case "DENIED":
6689
- prefix = RED6(" [DENIED] ");
6690
- break;
6691
- case "AWAITING_APPROVAL":
6692
- prefix = AMBER7(" [PENDING] ");
6693
- break;
6694
- default:
6695
- prefix = GRAY6(" [LOG] ");
6696
- break;
6697
- }
6698
- console.log(prefix + WHITE4(entry.text));
6699
- }
6700
- if (updatedTask) {
6701
- const status = updatedTask.outcome.status;
6702
- if (status === "completed") {
6703
- console.log("");
6704
- console.log(GREEN6(" \u2705 Task complete!"));
6705
- if (updatedTask.outcome.filesModified?.length > 0) {
6706
- console.log("");
6707
- console.log(AMBER7(" Files modified:"));
6708
- for (const file of updatedTask.outcome.filesModified) {
6709
- const icon = file.action === "created" ? GREEN6("+") : AMBER7("~");
6710
- console.log(` ${icon} ${CYAN6(file.path || file)}`);
6711
- }
6712
- }
6713
- if (updatedTask.outcome.resultSummary) {
6714
- console.log("");
6715
- console.log(AMBER7(" Summary:"));
6716
- const summary = stripMarkdown2(updatedTask.outcome.resultSummary);
6717
- const lines = summary.split("\n").slice(0, 8);
6718
- for (const line of lines) {
6719
- console.log(GRAY6(` ${line}`));
6720
- }
6721
- }
6722
- if (updatedTask.outcome.turnsUsed) {
6723
- console.log("");
6724
- console.log(GRAY6(` Turns: ${updatedTask.outcome.turnsUsed} \u2502 Tokens: ${updatedTask.outcome.tokensConsumed || 0} \u2502 Provider: ${updatedTask.outcome.provider || "unknown"}`));
6725
- }
6726
- running = false;
6727
- } else if (status === "failed") {
6728
- console.log("");
6729
- console.log(RED6(` \u274C Task failed: ${updatedTask.outcome.error || "Unknown error"}`));
6730
- running = false;
6731
- } else if (status === "denied") {
6732
- console.log("");
6733
- console.log(RED6(" \u274C Task was denied."));
6734
- running = false;
6735
- }
6736
- }
6737
- pollErrors = 0;
6738
- } catch (e) {
6739
- pollErrors++;
6740
- if (pollErrors >= maxPollErrors) {
6741
- console.log(RED6(` \u274C Lost connection after ${maxPollErrors} errors. Task continues in background.`));
6742
- running = false;
6743
- }
6744
- }
6796
+ } else if (isSource) {
6797
+ preRestoreBackup = `${projectDir}-pre-restore-${Date.now()}`;
6798
+ if (fs11.existsSync(projectDir)) {
6799
+ fs11.cpSync(projectDir, preRestoreBackup, { recursive: true });
6745
6800
  }
6746
- process.removeListener("SIGINT", sigintHandler);
6747
- console.log("");
6748
- }
6749
- } else if (action === "deny") {
6750
- const { reason } = await inquirer.prompt([
6751
- {
6752
- type: "input",
6753
- name: "reason",
6754
- message: AMBER7(" Denial reason (optional):"),
6755
- 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 });
6756
6806
  }
6757
- ]);
6758
- try {
6759
- console.log(GRAY6(" Denying task..."));
6760
- await callCloudFunction("approveAutonomousTask", {
6761
- conversationId,
6762
- taskId: selectedTask.id,
6763
- action: "deny",
6764
- reason: reason.trim() || null
6765
- });
6766
- console.log("");
6767
- console.log(RED6(" \u274C Task denied."));
6768
- console.log("");
6769
- } catch (e) {
6770
- console.log(RED6(` \u274C Failed to deny: ${e.message}`));
6771
6807
  }
6772
- } else if (action === "exit") {
6773
- continueLoop = false;
6774
- break;
6775
- }
6776
- }
6777
- }
6778
- function registerCommandCenterCommand(program2) {
6779
- 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) => {
6780
- 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 ..."));
6781
6822
  console.log("");
6782
- console.log(RED6(" \u274C Authentication required."));
6783
- 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"));
6784
6837
  console.log("");
6785
- process.exit(1);
6786
- }
6787
- const config = getConfig();
6788
- const conversationId = config.conversationId;
6789
- if (!conversationId) {
6838
+ } catch (error) {
6790
6839
  console.log("");
6791
- console.log(RED6(" \u274C No active conversation."));
6792
- 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."));
6793
6842
  console.log("");
6794
- process.exit(1);
6843
+ } finally {
6844
+ cleanupTemp2(tmpDir);
6795
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/"));
6796
6868
  console.log("");
6797
- 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"));
6798
- console.log(BORDER11(" \u2551") + ORANGE2(" \u25C9 AUTONOMOUS COMMAND CENTER"));
6799
- console.log(BORDER11(" \u2551") + GRAY6(` Conversation: ${conversationId.slice(0, 24)}...`));
6800
- 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"));
6801
- if (options.stream) {
6802
- await runDecisionStream(conversationId);
6803
- return;
6804
- }
6805
- if (options.settings) {
6806
- await runSettings(conversationId);
6807
- return;
6808
- }
6809
- await runTaskBoard(conversationId);
6810
6869
  });
6811
6870
  }
6812
6871
 
6813
6872
  // bin/bob.ts
6814
- var BRAND_PRIMARY11 = chalk26.hex("#E66F24");
6815
- var BRAND_SECONDARY15 = chalk26.hex("#FFAB00");
6816
- var SUCCESS16 = chalk26.hex("#66BB6A");
6817
- var INFO16 = chalk26.hex("#26C6DA");
6818
- var MUTED16 = chalk26.hex("#78909C");
6819
- var MODE_CONSULTANT9 = chalk26.hex("#AB47BC");
6820
6873
  var program = new Command();
6821
- program.name("bob").description("Bob's CLI \u2014 AI coding assistant and Forge orchestrator").version("0.2.0").helpOption(false).addHelpCommand(false);
6822
- program.option("-h, --help", "Print this usage information").on("option:help", () => {
6823
- printCustomHelp();
6824
- process.exit(0);
6825
- });
6826
- program.command("help").description("Display help for Bob's CLI").action(() => {
6827
- printCustomHelp();
6828
- });
6829
- function printCustomHelp() {
6830
- console.log("");
6831
- console.log(BRAND_PRIMARY11(" \u25C9 Bob's CLI") + MUTED16(" \u2014 Your AI Engineering Partner, In Your Terminal."));
6832
- console.log("");
6833
- console.log(chalk26.white(" Common commands:"));
6834
- console.log("");
6835
- console.log(BRAND_SECONDARY15(' bob chat "message"'));
6836
- console.log(MUTED16(" Chat with Bob \u2014 code-friendly engineering partner with file awareness."));
6837
- console.log("");
6838
- console.log(BRAND_SECONDARY15(' bob consult "message"'));
6839
- console.log(MUTED16(" Strategic advice only \u2014 no code, just architectural guidance."));
6840
- console.log("");
6841
- console.log(BRAND_SECONDARY15(" bob index"));
6842
- console.log(MUTED16(" Index your project \u2014 generates summaries and dependency map for context."));
6843
- console.log("");
6844
- console.log(chalk26.white(" Usage: ") + INFO16("bob <command> [arguments]"));
6845
- console.log("");
6846
- console.log(chalk26.white(" Global options:"));
6847
- console.log(MUTED16(" -h, --help Print this usage information."));
6848
- console.log(MUTED16(" -V, --version Output the version number."));
6849
- console.log("");
6850
- console.log(chalk26.white(" Available commands:"));
6851
- console.log("");
6852
- console.log(INFO16(" Conversation"));
6853
- printCmd("chat [message]", "Chat with Bob \u2014 code-friendly engineering partner");
6854
- printCmd("consult [message]", "Strategic advice only, no code output");
6855
- printCmd("conversations", "List, search, and join existing conversations");
6856
- printCmd("fork <title>", "Branch conversation into a focused sub-project");
6857
- printCmd("forks", "List all forks of the current conversation");
6858
- printCmd("deepdive", "Sandboxed exploration on a specific Bob message");
6859
- printCmd("deepdives", "List all deep dives in the current conversation");
6860
- printCmd("deepdives-join", "Re-enter an existing deep dive");
6861
- console.log("");
6862
- console.log(SUCCESS16(" Project Tools"));
6863
- printCmd("index", "Index your project \u2014 AI-powered summaries + dependency map");
6864
- printCmd("analyse", "Full QA code review \u2014 bugs, features, improvements, upgrades");
6865
- printCmd("analyse --auto", "Auto-fix mode \u2014 Bob triages, MiniBob implements");
6866
- printCmd("autonomy", "Full autonomous repair across entire codebase");
6867
- printCmd("push <message>", "Git stage + commit + push in one command");
6868
- console.log("");
6869
- console.log(BRAND_PRIMARY11(" Remote (SovereignLink)"));
6870
- printCmd("serve", "Start Active Bob \u2014 receive commands from any device");
6871
- printCmd("remote [type] [msg]", "Send commands to a remote Active Bob");
6872
- console.log("");
6873
- console.log(MODE_CONSULTANT9(" Profile & Identity"));
6874
- printCmd("profile", "View your behavioral DNA dashboard");
6875
- printCmd("profile --cloud", "Generate cloud-powered daily profile");
6876
- printCmd("userbob", "Launch your UserBob digital twin");
6877
- printCmd("byok", "Manage Bring Your Own Key configuration");
6878
- printCmd("command-center", "Autonomous Command Center \u2014 inspect and approve tasks");
6879
- printCmd("cc", "Alias for command-center");
6880
- console.log("");
6881
- console.log(MUTED16(" Configuration"));
6882
- printCmd("config show", "Display current configuration");
6883
- printCmd("config set <key> <val>", "Update a configuration value");
6884
- printCmd("login", "Authenticate with Bob's Workshop via browser");
6885
- printCmd("logout", "Sign out and clear stored credentials");
6886
- printCmd("whoami", "Show current auth status and project info");
6887
- console.log("");
6888
- console.log(chalk26.white(" Interactive slash commands ") + MUTED16("(inside a chat/consult session):"));
6889
- console.log("");
6890
- printCmd("/exit", "End the session");
6891
- printCmd("/new", "Start a fresh conversation");
6892
- printCmd("/clear", "Clear the terminal");
6893
- printCmd("/include <path>", "Load a file into active context");
6894
- printCmd("/delete <path>", "Delete a file (with backup)");
6895
- printCmd("/deepdive", "Deep dive on the last Bob message");
6896
- printCmd("/constraints", "View active negative constraints");
6897
- console.log("");
6898
- 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"));
6899
- console.log("");
6900
- console.log(MUTED16(" Run ") + INFO16("bob <command> --help") + MUTED16(" for details on a specific command."));
6901
- console.log("");
6902
- console.log(MUTED16(" \u{1F4D6} Full docs:"));
6903
- console.log(INFO16(" https://seedling-io.gitbook.io/bob-cli/bobs-cli-product-wiki-and-user-guide/command-reference"));
6904
- console.log("");
6905
- console.log(MUTED16(" Built by ") + BRAND_PRIMARY11("Bob's Workshop") + MUTED16(" \u2014 A Seedling Company."));
6906
- console.log("");
6907
- }
6908
- function printCmd(cmd, desc) {
6909
- const padded = cmd.padEnd(24);
6910
- console.log(BRAND_SECONDARY15(` ${padded}`) + MUTED16(desc));
6911
- }
6874
+ program.name("bob").description("Bob's CLI \u2014 AI coding assistant and Forge orchestrator").version("0.1.3");
6912
6875
  program.command("whoami").description("Show current authentication status and configuration").action(() => {
6913
6876
  const config = getConfig();
6914
6877
  const projectName = path14.basename(process.cwd());
6915
6878
  console.log("");
6916
- console.log(chalk26.bold(" \u{1F916} Bob's CLI"));
6917
- 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"));
6918
- console.log(` ${chalk26.cyan("Status:")} ${config.loggedIn ? chalk26.green("Logged in as " + config.email) : "Not logged in"}`);
6919
- console.log(` ${chalk26.cyan("Tier:")} ${config.tier === "platform" ? "Platform (Tier 3)" : "Local-first (Tier 1)"}`);
6920
- console.log(` ${chalk26.cyan("Provider:")} ${config.provider || "Not configured"}`);
6921
- console.log(` ${chalk26.cyan("Mode:")} ${config.personalizationMode ? "Personalized" : config.consultantMode ? "Consultant" : "Standard"}`);
6922
- console.log(` ${chalk26.cyan("IDRP:")} ${config.idrp ? "Enabled" : "Disabled"}`);
6923
- console.log(` ${chalk26.cyan("Project:")} ${projectName} (${process.cwd()})`);
6924
- 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"}`);
6925
6888
  console.log("");
6926
6889
  if (!config.loggedIn) {
6927
- console.log(chalk26.gray(" Run `bob login` to authenticate."));
6890
+ console.log(chalk25.gray(" Run `bob login` to authenticate."));
6928
6891
  console.log("");
6929
6892
  }
6930
6893
  });
@@ -6943,26 +6906,5 @@ registerAutonomyCommand(program);
6943
6906
  registerServeCommand(program);
6944
6907
  registerRemoteCommand(program);
6945
6908
  registerProfileCommand(program);
6946
- registerUserBobCommand(program);
6947
- registerCommandCenterCommand(program);
6948
- process.on("uncaughtException", (error) => {
6949
- console.error("");
6950
- console.error(chalk26.hex("#EF5350")(" \u274C An unexpected error occurred."));
6951
- console.error(chalk26.hex("#78909C")(` ${error.message || "Unknown error"}`));
6952
- console.error("");
6953
- console.error(chalk26.hex("#78909C")(" If this persists, please report it:"));
6954
- console.error(chalk26.hex("#26C6DA")(" https://github.com/bobsworkshop/bob-cli/issues"));
6955
- console.error("");
6956
- process.exit(1);
6957
- });
6958
- process.on("unhandledRejection", (reason) => {
6959
- console.error("");
6960
- console.error(chalk26.hex("#EF5350")(" \u274C An unexpected error occurred."));
6961
- console.error(chalk26.hex("#78909C")(` ${reason?.message || reason || "Unknown error"}`));
6962
- console.error("");
6963
- console.error(chalk26.hex("#78909C")(" If this persists, please report it:"));
6964
- console.error(chalk26.hex("#26C6DA")(" https://github.com/bobsworkshop/bob-cli/issues"));
6965
- console.error("");
6966
- process.exit(1);
6967
- });
6909
+ registerBackupCommand(program);
6968
6910
  program.parse();