@bike4mind/cli 0.2.30 → 0.2.31-b4m-cli-undo-command.19493

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/index.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  getEffectiveApiKey,
6
6
  getOpenWeatherKey,
7
7
  getSerperKey
8
- } from "./chunk-WIB75EEX.js";
9
- import "./chunk-RUI6HNLO.js";
8
+ } from "./chunk-T67NGQW6.js";
9
+ import "./chunk-GQGOWACU.js";
10
10
  import {
11
11
  ConfigStore,
12
12
  logger
@@ -15,8 +15,8 @@ import {
15
15
  selectActiveBackgroundAgents,
16
16
  useCliStore
17
17
  } from "./chunk-BYXFQJYT.js";
18
- import "./chunk-LLA44SW7.js";
19
- import "./chunk-U63VANTQ.js";
18
+ import "./chunk-2LLA4MTN.js";
19
+ import "./chunk-ZOWCX4MQ.js";
20
20
  import {
21
21
  BFLImageService,
22
22
  BaseStorage,
@@ -28,7 +28,7 @@ import {
28
28
  OpenAIBackend,
29
29
  OpenAIImageService,
30
30
  XAIImageService
31
- } from "./chunk-BQAPZP5Y.js";
31
+ } from "./chunk-RI45VJW3.js";
32
32
  import {
33
33
  AiEvents,
34
34
  ApiKeyEvents,
@@ -83,8 +83,10 @@ import {
83
83
  VideoModels,
84
84
  XAI_IMAGE_MODELS,
85
85
  b4mLLMTools,
86
- getMcpProviderMetadata
87
- } from "./chunk-T4VPCTBM.js";
86
+ getMcpProviderMetadata,
87
+ getViewById,
88
+ resolveNavigationIntents
89
+ } from "./chunk-GE7Q64MS.js";
88
90
  import {
89
91
  Logger
90
92
  } from "./chunk-OCYRD7D6.js";
@@ -94,7 +96,7 @@ import React21, { useState as useState10, useEffect as useEffect7, useCallback a
94
96
  import { render, Box as Box20, Text as Text20, useApp, useInput as useInput9 } from "ink";
95
97
  import { execSync } from "child_process";
96
98
  import { randomBytes as randomBytes5 } from "crypto";
97
- import { v4 as uuidv411 } from "uuid";
99
+ import { v4 as uuidv413 } from "uuid";
98
100
 
99
101
  // src/components/App.tsx
100
102
  import React15, { useState as useState6, useEffect as useEffect5 } from "react";
@@ -350,9 +352,23 @@ function CommandAutocomplete({ commands, selectedIndex }) {
350
352
  if (commands.length === 0) {
351
353
  return /* @__PURE__ */ React3.createElement(Box2, { marginLeft: 2, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "No matching commands"));
352
354
  }
353
- return /* @__PURE__ */ React3.createElement(Box2, { flexDirection: "column", marginLeft: 2, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, commands.length === 1 ? "1 command" : `${commands.length} commands`, " \u2022 Use \u2191\u2193 to navigate, Enter to select")), commands.map((cmd, index) => {
355
+ const VIEWPORT_SIZE = 6;
356
+ const totalCommands = commands.length;
357
+ let startIndex = 0;
358
+ let endIndex = Math.min(VIEWPORT_SIZE, totalCommands);
359
+ if (totalCommands > VIEWPORT_SIZE) {
360
+ const halfViewport = Math.floor(VIEWPORT_SIZE / 2);
361
+ startIndex = Math.max(0, selectedIndex - halfViewport);
362
+ endIndex = Math.min(totalCommands, startIndex + VIEWPORT_SIZE);
363
+ if (endIndex === totalCommands) {
364
+ startIndex = Math.max(0, totalCommands - VIEWPORT_SIZE);
365
+ }
366
+ }
367
+ const visibleCommands = commands.slice(startIndex, endIndex);
368
+ return /* @__PURE__ */ React3.createElement(Box2, { flexDirection: "column", marginLeft: 2, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, totalCommands === 1 ? "1 command" : `${totalCommands} commands`, totalCommands > VIEWPORT_SIZE && ` (${selectedIndex + 1}/${totalCommands})`, " \u2022 Use \u2191\u2193 to navigate, Enter to select")), visibleCommands.map((cmd, viewportIndex) => {
369
+ const actualIndex = startIndex + viewportIndex;
354
370
  const args = cmd.args ? ` ${cmd.args}` : "";
355
- const isSelected = index === selectedIndex;
371
+ const isSelected = actualIndex === selectedIndex;
356
372
  const sourceIcon = cmd.source === "global" ? "\u{1F3E0} " : cmd.source === "project" ? "\u{1F4C1} " : cmd.source === "built-in" ? "\u{1F527} " : "";
357
373
  return /* @__PURE__ */ React3.createElement(Box2, { key: cmd.name, marginLeft: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: isSelected ? "cyan" : void 0, bold: isSelected }, isSelected ? "\u25B8 " : " ", sourceIcon, "/", cmd.name, args, " - ", cmd.description));
358
374
  }));
@@ -713,8 +729,25 @@ var COMMANDS = [
713
729
  },
714
730
  {
715
731
  name: "rewind",
716
- description: "Rewind conversation to a previous point",
717
- aliases: ["undo"]
732
+ description: "Rewind conversation to a previous point"
733
+ },
734
+ {
735
+ name: "undo",
736
+ description: "Undo the last file change"
737
+ },
738
+ {
739
+ name: "checkpoints",
740
+ description: "List available file restore points"
741
+ },
742
+ {
743
+ name: "restore",
744
+ description: "Restore files to a specific checkpoint",
745
+ args: "<number>"
746
+ },
747
+ {
748
+ name: "diff",
749
+ description: "Show diff between current state and a checkpoint",
750
+ args: "[number]"
718
751
  },
719
752
  {
720
753
  name: "project-config",
@@ -1096,8 +1129,8 @@ function InputPrompt({
1096
1129
  useEffect3(() => {
1097
1130
  onBashModeChange?.(isBashMode);
1098
1131
  }, [isBashMode, onBashModeChange]);
1099
- const shouldShowCommandAutocomplete = value.startsWith("/") && !disabled && !fileAutocomplete?.active && !looksLikeFilePath(value) && !pastedContent;
1100
- const commandQuery = shouldShowCommandAutocomplete ? value.slice(1) : "";
1132
+ const commandQuery = value.startsWith("/") ? value.slice(1) : "";
1133
+ const shouldShowCommandAutocomplete = value.startsWith("/") && !disabled && !fileAutocomplete?.active && !looksLikeFilePath(value) && !pastedContent && !commandQuery.includes(" ");
1101
1134
  const filteredCommands = useMemo(() => {
1102
1135
  if (!shouldShowCommandAutocomplete) return [];
1103
1136
  return searchCommands(commandQuery, commands);
@@ -2818,9 +2851,327 @@ var CommandHistoryStore = class {
2818
2851
  }
2819
2852
  };
2820
2853
 
2821
- // src/storage/CustomCommandStore.ts
2822
- import fs5 from "fs/promises";
2854
+ // src/storage/CheckpointStore.ts
2855
+ import { promises as fs5 } from "fs";
2856
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync, unlinkSync } from "fs";
2857
+ import { execFileSync } from "child_process";
2823
2858
  import path6 from "path";
2859
+ var MAX_FILE_SIZE2 = 10 * 1024 * 1024;
2860
+ var DEFAULT_KEEP_COUNT = 50;
2861
+ var ABSENT_MARKER = ".b4m-absent";
2862
+ var CheckpointStore = class {
2863
+ constructor(projectDir) {
2864
+ this.metadata = null;
2865
+ this.sessionId = null;
2866
+ this.initialized = false;
2867
+ this.projectDir = projectDir;
2868
+ this.shadowRepoDir = path6.join(projectDir, ".b4m", "shadow-repo");
2869
+ this.metadataPath = path6.join(projectDir, ".b4m", "checkpoints.json");
2870
+ }
2871
+ /**
2872
+ * Initialize the shadow git repository and load metadata
2873
+ */
2874
+ async init(sessionId) {
2875
+ this.sessionId = sessionId;
2876
+ await fs5.mkdir(path6.join(this.projectDir, ".b4m"), { recursive: true });
2877
+ if (!existsSync4(path6.join(this.shadowRepoDir, ".git"))) {
2878
+ await fs5.mkdir(this.shadowRepoDir, { recursive: true });
2879
+ this.git("init");
2880
+ this.git("config", "user.email", "checkpoint@b4m.local");
2881
+ this.git("config", "user.name", "B4M Checkpoint");
2882
+ this.git("commit", "--allow-empty", "-m", "checkpoint-init");
2883
+ }
2884
+ await this.loadMetadata();
2885
+ await this.ensureGitignore();
2886
+ await this.pruneCheckpoints(DEFAULT_KEEP_COUNT);
2887
+ this.initialized = true;
2888
+ }
2889
+ /**
2890
+ * Update session ID (e.g., on /clear or /resume)
2891
+ */
2892
+ setSessionId(sessionId) {
2893
+ this.sessionId = sessionId;
2894
+ }
2895
+ /**
2896
+ * Create a checkpoint by snapshotting the current state of target files
2897
+ * before they are modified by a tool.
2898
+ *
2899
+ * @param toolName - The tool about to modify files
2900
+ * @param filePaths - Relative paths of files about to be modified
2901
+ * @param name - Optional human-readable checkpoint name
2902
+ */
2903
+ async createCheckpoint(toolName, filePaths, name) {
2904
+ if (!this.initialized || !this.sessionId) {
2905
+ return null;
2906
+ }
2907
+ const checkpointName = name || `before-${toolName}-${path6.basename(filePaths[0] || "unknown")}`;
2908
+ try {
2909
+ let hasChanges = false;
2910
+ for (const filePath of filePaths) {
2911
+ const absolutePath = path6.resolve(this.projectDir, filePath);
2912
+ const shadowPath = path6.join(this.shadowRepoDir, filePath);
2913
+ const shadowDir = path6.dirname(shadowPath);
2914
+ const absentMarkerPath = path6.join(shadowDir, `${path6.basename(filePath)}${ABSENT_MARKER}`);
2915
+ await fs5.mkdir(shadowDir, { recursive: true });
2916
+ if (existsSync4(absolutePath)) {
2917
+ const stats = await fs5.stat(absolutePath);
2918
+ if (stats.size > MAX_FILE_SIZE2) {
2919
+ continue;
2920
+ }
2921
+ if (existsSync4(absentMarkerPath)) {
2922
+ await fs5.unlink(absentMarkerPath);
2923
+ }
2924
+ await fs5.copyFile(absolutePath, shadowPath);
2925
+ hasChanges = true;
2926
+ } else {
2927
+ if (existsSync4(shadowPath)) {
2928
+ await fs5.unlink(shadowPath);
2929
+ }
2930
+ await fs5.writeFile(absentMarkerPath, "", "utf-8");
2931
+ hasChanges = true;
2932
+ }
2933
+ }
2934
+ if (!hasChanges) {
2935
+ return null;
2936
+ }
2937
+ this.git("add", "-A");
2938
+ try {
2939
+ this.git("diff", "--cached", "--quiet");
2940
+ return null;
2941
+ } catch {
2942
+ }
2943
+ this.git("commit", "-m", checkpointName);
2944
+ const sha = this.git("rev-parse", "--short", "HEAD").trim();
2945
+ const checkpoint = {
2946
+ id: sha,
2947
+ name: checkpointName,
2948
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2949
+ toolName,
2950
+ filePaths: [...filePaths],
2951
+ sessionId: this.sessionId
2952
+ };
2953
+ if (!this.metadata) {
2954
+ this.metadata = { checkpoints: [], createdAt: (/* @__PURE__ */ new Date()).toISOString() };
2955
+ }
2956
+ this.metadata.checkpoints.push(checkpoint);
2957
+ await this.saveMetadata();
2958
+ return checkpoint;
2959
+ } catch {
2960
+ return null;
2961
+ }
2962
+ }
2963
+ /**
2964
+ * List checkpoints for the current session (most recent first)
2965
+ */
2966
+ listCheckpoints() {
2967
+ if (!this.metadata || !this.sessionId) {
2968
+ return [];
2969
+ }
2970
+ return this.metadata.checkpoints.filter((cp) => cp.sessionId === this.sessionId).reverse();
2971
+ }
2972
+ /**
2973
+ * Get a specific checkpoint by 1-based index (1 = most recent)
2974
+ */
2975
+ getCheckpoint(index) {
2976
+ const checkpoints = this.listCheckpoints();
2977
+ if (index < 1 || index > checkpoints.length) {
2978
+ return null;
2979
+ }
2980
+ return checkpoints[index - 1];
2981
+ }
2982
+ /**
2983
+ * Restore files to the state captured in a specific checkpoint
2984
+ *
2985
+ * @param index - 1-based index (1 = most recent)
2986
+ * @returns The checkpoint that was restored to
2987
+ */
2988
+ async restoreCheckpoint(index) {
2989
+ const checkpoint = this.getCheckpoint(index);
2990
+ if (!checkpoint) {
2991
+ throw new Error(`Checkpoint #${index} not found. Use /checkpoints to see available restore points.`);
2992
+ }
2993
+ for (const filePath of checkpoint.filePaths) {
2994
+ const absolutePath = path6.resolve(this.projectDir, filePath);
2995
+ try {
2996
+ const content = this.git("show", `${checkpoint.id}:${filePath}`);
2997
+ await fs5.mkdir(path6.dirname(absolutePath), { recursive: true });
2998
+ await fs5.writeFile(absolutePath, content, "utf-8");
2999
+ } catch {
3000
+ try {
3001
+ this.git("show", `${checkpoint.id}:${path6.dirname(filePath)}/${path6.basename(filePath)}${ABSENT_MARKER}`);
3002
+ if (existsSync4(absolutePath)) {
3003
+ await fs5.unlink(absolutePath);
3004
+ }
3005
+ } catch {
3006
+ }
3007
+ }
3008
+ }
3009
+ return checkpoint;
3010
+ }
3011
+ /**
3012
+ * Undo the last file change (restore to most recent checkpoint)
3013
+ */
3014
+ async undoLast() {
3015
+ return this.restoreCheckpoint(1);
3016
+ }
3017
+ /**
3018
+ * Get diff between current file state and a checkpoint
3019
+ *
3020
+ * @param index - 1-based index (1 = most recent, default)
3021
+ * @returns Unified diff string
3022
+ */
3023
+ getCheckpointDiff(index = 1) {
3024
+ const checkpoint = this.getCheckpoint(index);
3025
+ if (!checkpoint) {
3026
+ throw new Error(`Checkpoint #${index} not found. Use /checkpoints to see available restore points.`);
3027
+ }
3028
+ const diffParts = [];
3029
+ for (const filePath of checkpoint.filePaths) {
3030
+ const absolutePath = path6.resolve(this.projectDir, filePath);
3031
+ try {
3032
+ let checkpointContent;
3033
+ try {
3034
+ checkpointContent = this.git("show", `${checkpoint.id}:${filePath}`);
3035
+ } catch {
3036
+ checkpointContent = "";
3037
+ }
3038
+ let currentContent = "";
3039
+ if (existsSync4(absolutePath)) {
3040
+ currentContent = readFileSync4(absolutePath, "utf-8");
3041
+ }
3042
+ if (checkpointContent === currentContent) {
3043
+ continue;
3044
+ }
3045
+ const tmpCheckpoint = path6.join(this.shadowRepoDir, ".diff-a");
3046
+ const tmpCurrent = path6.join(this.shadowRepoDir, ".diff-b");
3047
+ writeFileSync(tmpCheckpoint, checkpointContent, "utf-8");
3048
+ writeFileSync(tmpCurrent, currentContent, "utf-8");
3049
+ try {
3050
+ this.git(
3051
+ "diff",
3052
+ "--no-index",
3053
+ "--color",
3054
+ `--src-prefix=checkpoint:`,
3055
+ `--dst-prefix=current:`,
3056
+ tmpCheckpoint,
3057
+ tmpCurrent
3058
+ );
3059
+ } catch (diffError) {
3060
+ if (diffError && typeof diffError === "object" && "stdout" in diffError) {
3061
+ const output = diffError.stdout?.toString() || "";
3062
+ if (output) {
3063
+ diffParts.push(`--- ${filePath} (checkpoint #${index})
3064
+ +++ ${filePath} (current)
3065
+ ${output}`);
3066
+ }
3067
+ }
3068
+ }
3069
+ try {
3070
+ unlinkSync(tmpCheckpoint);
3071
+ unlinkSync(tmpCurrent);
3072
+ } catch {
3073
+ }
3074
+ } catch {
3075
+ }
3076
+ }
3077
+ return diffParts.join("\n");
3078
+ }
3079
+ /**
3080
+ * Prune old checkpoints beyond the keep count
3081
+ */
3082
+ async pruneCheckpoints(keepCount = DEFAULT_KEEP_COUNT) {
3083
+ if (!this.metadata) return;
3084
+ const total = this.metadata.checkpoints.length;
3085
+ if (total <= keepCount) return;
3086
+ this.metadata.checkpoints = this.metadata.checkpoints.slice(-keepCount);
3087
+ await this.saveMetadata();
3088
+ try {
3089
+ this.git("gc", "--auto", "--quiet");
3090
+ } catch {
3091
+ }
3092
+ }
3093
+ /**
3094
+ * Clean up the shadow repository entirely
3095
+ */
3096
+ async cleanup() {
3097
+ try {
3098
+ await fs5.rm(this.shadowRepoDir, { recursive: true, force: true });
3099
+ if (existsSync4(this.metadataPath)) {
3100
+ await fs5.unlink(this.metadataPath);
3101
+ }
3102
+ this.metadata = null;
3103
+ this.initialized = false;
3104
+ } catch {
3105
+ }
3106
+ }
3107
+ // --- Private helpers ---
3108
+ /**
3109
+ * Execute a git command in the shadow repo
3110
+ */
3111
+ git(...args) {
3112
+ return execFileSync("git", args, {
3113
+ cwd: this.shadowRepoDir,
3114
+ encoding: "utf-8",
3115
+ stdio: ["pipe", "pipe", "pipe"],
3116
+ timeout: 1e4
3117
+ });
3118
+ }
3119
+ /**
3120
+ * Load checkpoint metadata from disk
3121
+ */
3122
+ async loadMetadata() {
3123
+ try {
3124
+ if (existsSync4(this.metadataPath)) {
3125
+ const data = await fs5.readFile(this.metadataPath, "utf-8");
3126
+ this.metadata = JSON.parse(data);
3127
+ } else {
3128
+ this.metadata = {
3129
+ checkpoints: [],
3130
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3131
+ };
3132
+ }
3133
+ } catch {
3134
+ this.metadata = {
3135
+ checkpoints: [],
3136
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3137
+ };
3138
+ }
3139
+ }
3140
+ /**
3141
+ * Save checkpoint metadata to disk
3142
+ */
3143
+ async saveMetadata() {
3144
+ if (!this.metadata) return;
3145
+ await fs5.writeFile(this.metadataPath, JSON.stringify(this.metadata, null, 2), "utf-8");
3146
+ }
3147
+ /**
3148
+ * Ensure .b4m/ is in .gitignore
3149
+ */
3150
+ async ensureGitignore() {
3151
+ const gitignorePath = path6.join(this.projectDir, ".gitignore");
3152
+ const entryToAdd = ".b4m/";
3153
+ try {
3154
+ let content = "";
3155
+ try {
3156
+ content = await fs5.readFile(gitignorePath, "utf-8");
3157
+ } catch {
3158
+ }
3159
+ if (content.includes(entryToAdd) || content.includes(".b4m")) {
3160
+ return;
3161
+ }
3162
+ const newContent = content.trim() + (content ? "\n" : "") + `
3163
+ # B4M checkpoint data
3164
+ ${entryToAdd}
3165
+ `;
3166
+ await fs5.writeFile(gitignorePath, newContent, "utf-8");
3167
+ } catch {
3168
+ }
3169
+ }
3170
+ };
3171
+
3172
+ // src/storage/CustomCommandStore.ts
3173
+ import fs6 from "fs/promises";
3174
+ import path7 from "path";
2824
3175
  import os from "os";
2825
3176
 
2826
3177
  // src/utils/commandParser.ts
@@ -2983,14 +3334,14 @@ var CustomCommandStore = class {
2983
3334
  const home = os.homedir();
2984
3335
  const root = projectRoot || process.cwd();
2985
3336
  this.globalCommandsDirs = [
2986
- path6.join(home, ".bike4mind", "commands"),
2987
- path6.join(home, ".claude", "commands"),
2988
- path6.join(home, ".claude", "skills")
3337
+ path7.join(home, ".bike4mind", "commands"),
3338
+ path7.join(home, ".claude", "commands"),
3339
+ path7.join(home, ".claude", "skills")
2989
3340
  ];
2990
3341
  this.projectCommandsDirs = [
2991
- path6.join(root, ".bike4mind", "commands"),
2992
- path6.join(root, ".claude", "commands"),
2993
- path6.join(root, ".claude", "skills")
3342
+ path7.join(root, ".bike4mind", "commands"),
3343
+ path7.join(root, ".claude", "commands"),
3344
+ path7.join(root, ".claude", "skills")
2994
3345
  ];
2995
3346
  }
2996
3347
  /**
@@ -3015,7 +3366,7 @@ var CustomCommandStore = class {
3015
3366
  */
3016
3367
  async loadCommandsFromDirectory(directory, source) {
3017
3368
  try {
3018
- const stats = await fs5.stat(directory);
3369
+ const stats = await fs6.stat(directory);
3019
3370
  if (!stats.isDirectory()) {
3020
3371
  return;
3021
3372
  }
@@ -3048,9 +3399,9 @@ var CustomCommandStore = class {
3048
3399
  async findCommandFiles(directory) {
3049
3400
  const files = [];
3050
3401
  try {
3051
- const entries = await fs5.readdir(directory, { withFileTypes: true });
3402
+ const entries = await fs6.readdir(directory, { withFileTypes: true });
3052
3403
  for (const entry of entries) {
3053
- const fullPath = path6.join(directory, entry.name);
3404
+ const fullPath = path7.join(directory, entry.name);
3054
3405
  if (entry.isDirectory()) {
3055
3406
  const subFiles = await this.findCommandFiles(fullPath);
3056
3407
  files.push(...subFiles);
@@ -3070,14 +3421,14 @@ var CustomCommandStore = class {
3070
3421
  * @param source - Source identifier ('global' or 'project')
3071
3422
  */
3072
3423
  async loadCommandFile(filePath, source) {
3073
- const filename = path6.basename(filePath);
3424
+ const filename = path7.basename(filePath);
3074
3425
  const isSkillFile = filename.toLowerCase() === "skill.md";
3075
3426
  const commandName = isSkillFile ? this.extractSkillName(filePath) : extractCommandName(filename);
3076
3427
  if (!commandName) {
3077
3428
  console.warn(`Invalid command filename: ${filename} (must end with .md and have valid name)`);
3078
3429
  return;
3079
3430
  }
3080
- const fileContent = await fs5.readFile(filePath, "utf-8");
3431
+ const fileContent = await fs6.readFile(filePath, "utf-8");
3081
3432
  const command = parseCommandFile(fileContent, filePath, commandName, source);
3082
3433
  this.commands.set(commandName, command);
3083
3434
  }
@@ -3089,7 +3440,7 @@ var CustomCommandStore = class {
3089
3440
  * @returns Skill name or null if invalid
3090
3441
  */
3091
3442
  extractSkillName(filePath) {
3092
- const parentDir = path6.basename(path6.dirname(filePath));
3443
+ const parentDir = path7.basename(path7.dirname(filePath));
3093
3444
  return parentDir && parentDir !== "skills" ? parentDir : null;
3094
3445
  }
3095
3446
  /**
@@ -3151,15 +3502,15 @@ var CustomCommandStore = class {
3151
3502
  */
3152
3503
  async createCommandFile(name, isGlobal = false) {
3153
3504
  const targetDir = isGlobal ? this.globalCommandsDirs[0] : this.projectCommandsDirs[0];
3154
- const filePath = path6.join(targetDir, `${name}.md`);
3155
- const fileExists = await fs5.access(filePath).then(
3505
+ const filePath = path7.join(targetDir, `${name}.md`);
3506
+ const fileExists = await fs6.access(filePath).then(
3156
3507
  () => true,
3157
3508
  () => false
3158
3509
  );
3159
3510
  if (fileExists) {
3160
3511
  throw new Error(`Command file already exists: ${filePath}`);
3161
3512
  }
3162
- await fs5.mkdir(targetDir, { recursive: true });
3513
+ await fs6.mkdir(targetDir, { recursive: true });
3163
3514
  const template = `---
3164
3515
  description: ${name} command
3165
3516
  argument-hint: [args]
@@ -3174,7 +3525,7 @@ You can use:
3174
3525
  - $1, $2, etc. for positional arguments
3175
3526
  - @filename for file references
3176
3527
  `;
3177
- await fs5.writeFile(filePath, template, "utf-8");
3528
+ await fs6.writeFile(filePath, template, "utf-8");
3178
3529
  return filePath;
3179
3530
  }
3180
3531
  };
@@ -3388,7 +3739,8 @@ ${options.context}` : this.getSystemPrompt()
3388
3739
  maxTokens,
3389
3740
  temperature,
3390
3741
  abortSignal: options.signal,
3391
- tool_choice: this.context.toolChoice
3742
+ tool_choice: this.context.toolChoice,
3743
+ executeTools: false
3392
3744
  },
3393
3745
  async (texts, completionInfo) => {
3394
3746
  for (const text of texts) {
@@ -4405,7 +4757,7 @@ import { GoogleGenerativeAI } from "@google/generative-ai";
4405
4757
  import OpenAI2 from "openai";
4406
4758
 
4407
4759
  // ../../b4m-core/packages/services/dist/src/importHistoryService/index.js
4408
- import fs6, { unlinkSync } from "fs";
4760
+ import fs7, { unlinkSync as unlinkSync2 } from "fs";
4409
4761
  import yauzl from "yauzl";
4410
4762
  import axios3 from "axios";
4411
4763
 
@@ -6096,8 +6448,8 @@ async function processAndStoreImages(images, context) {
6096
6448
  const buffer = await downloadImage(image);
6097
6449
  const fileType = await fileTypeFromBuffer2(buffer);
6098
6450
  const filename = `${uuidv45()}.${fileType?.ext}`;
6099
- const path19 = await context.imageGenerateStorage.upload(buffer, filename, {});
6100
- return path19;
6451
+ const path21 = await context.imageGenerateStorage.upload(buffer, filename, {});
6452
+ return path21;
6101
6453
  }));
6102
6454
  }
6103
6455
  async function updateQuestAndReturnMarkdown(storedImageUrls, context) {
@@ -7423,8 +7775,8 @@ async function processAndStoreImage(imageUrl, context) {
7423
7775
  const buffer = await downloadImage2(imageUrl);
7424
7776
  const fileType = await fileTypeFromBuffer3(buffer);
7425
7777
  const filename = `${uuidv46()}.${fileType?.ext}`;
7426
- const path19 = await context.imageGenerateStorage.upload(buffer, filename, {});
7427
- return path19;
7778
+ const path21 = await context.imageGenerateStorage.upload(buffer, filename, {});
7779
+ return path21;
7428
7780
  }
7429
7781
  async function generateFullMask(imageBuffer) {
7430
7782
  const sharp = (await import("sharp")).default;
@@ -9057,8 +9409,8 @@ var getHeliocentricCoords = (planet, T) => {
9057
9409
  const sinNode = Math.sin(longNode);
9058
9410
  const x = r * (cosNode * cosOmega - sinNode * sinOmega * cosI);
9059
9411
  const y = r * (sinNode * cosOmega + cosNode * sinOmega * cosI);
9060
- const z147 = r * sinOmega * sinI;
9061
- return { x, y, z: z147, r };
9412
+ const z148 = r * sinOmega * sinI;
9413
+ return { x, y, z: z148, r };
9062
9414
  };
9063
9415
  var getGeocentricEcliptic = (planet, earth) => {
9064
9416
  const dx = planet.x - earth.x;
@@ -9365,33 +9717,33 @@ var planetVisibilityTool = {
9365
9717
  };
9366
9718
 
9367
9719
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/fileRead/index.js
9368
- import { promises as fs7 } from "fs";
9369
- import { existsSync as existsSync4, statSync as statSync4 } from "fs";
9370
- import path7 from "path";
9371
- var MAX_FILE_SIZE2 = 10 * 1024 * 1024;
9720
+ import { promises as fs8 } from "fs";
9721
+ import { existsSync as existsSync5, statSync as statSync4 } from "fs";
9722
+ import path8 from "path";
9723
+ var MAX_FILE_SIZE3 = 10 * 1024 * 1024;
9372
9724
  async function readFileContent(params) {
9373
9725
  const { path: filePath, encoding = "utf-8", offset = 0, limit } = params;
9374
- const normalizedPath = path7.normalize(filePath);
9375
- const resolvedPath = path7.resolve(process.cwd(), normalizedPath);
9376
- const cwd = path7.resolve(process.cwd());
9726
+ const normalizedPath = path8.normalize(filePath);
9727
+ const resolvedPath = path8.resolve(process.cwd(), normalizedPath);
9728
+ const cwd = path8.resolve(process.cwd());
9377
9729
  if (!resolvedPath.startsWith(cwd)) {
9378
9730
  throw new Error(`Access denied: Cannot read files outside of current working directory`);
9379
9731
  }
9380
- if (!existsSync4(resolvedPath)) {
9732
+ if (!existsSync5(resolvedPath)) {
9381
9733
  throw new Error(`File not found: ${filePath}`);
9382
9734
  }
9383
9735
  const stats = statSync4(resolvedPath);
9384
9736
  if (stats.isDirectory()) {
9385
9737
  throw new Error(`Path is a directory, not a file: ${filePath}`);
9386
9738
  }
9387
- if (stats.size > MAX_FILE_SIZE2) {
9388
- throw new Error(`File too large: ${(stats.size / 1024 / 1024).toFixed(2)}MB (max ${MAX_FILE_SIZE2 / 1024 / 1024}MB)`);
9739
+ if (stats.size > MAX_FILE_SIZE3) {
9740
+ throw new Error(`File too large: ${(stats.size / 1024 / 1024).toFixed(2)}MB (max ${MAX_FILE_SIZE3 / 1024 / 1024}MB)`);
9389
9741
  }
9390
9742
  const isBinary = await checkIfBinary(resolvedPath);
9391
9743
  if (isBinary && encoding === "utf-8") {
9392
9744
  throw new Error(`File appears to be binary. Use encoding 'base64' to read binary files, or specify a different encoding.`);
9393
9745
  }
9394
- const content = await fs7.readFile(resolvedPath, encoding);
9746
+ const content = await fs8.readFile(resolvedPath, encoding);
9395
9747
  if (typeof content === "string") {
9396
9748
  const lines = content.split("\n");
9397
9749
  const totalLines = lines.length;
@@ -9429,7 +9781,7 @@ ${content}`;
9429
9781
  }
9430
9782
  async function checkIfBinary(filePath) {
9431
9783
  const buffer = Buffer.alloc(8192);
9432
- const fd = await fs7.open(filePath, "r");
9784
+ const fd = await fs8.open(filePath, "r");
9433
9785
  try {
9434
9786
  const { bytesRead } = await fd.read(buffer, 0, 8192, 0);
9435
9787
  const chunk = buffer.slice(0, bytesRead);
@@ -9446,7 +9798,7 @@ var fileReadTool = {
9446
9798
  context.logger.info("\u{1F4C4} FileRead: Reading file", { path: params.path });
9447
9799
  try {
9448
9800
  const content = await readFileContent(params);
9449
- const stats = statSync4(path7.resolve(process.cwd(), path7.normalize(params.path)));
9801
+ const stats = statSync4(path8.resolve(process.cwd(), path8.normalize(params.path)));
9450
9802
  context.logger.info("\u2705 FileRead: Success", {
9451
9803
  path: params.path,
9452
9804
  size: stats.size,
@@ -9490,25 +9842,25 @@ var fileReadTool = {
9490
9842
  };
9491
9843
 
9492
9844
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/createFile/index.js
9493
- import { promises as fs8 } from "fs";
9494
- import { existsSync as existsSync5 } from "fs";
9495
- import path8 from "path";
9845
+ import { promises as fs9 } from "fs";
9846
+ import { existsSync as existsSync6 } from "fs";
9847
+ import path9 from "path";
9496
9848
  async function createFile(params) {
9497
9849
  const { path: filePath, content, createDirectories = true } = params;
9498
- const normalizedPath = path8.normalize(filePath);
9499
- const resolvedPath = path8.resolve(process.cwd(), normalizedPath);
9500
- const cwd = path8.resolve(process.cwd());
9850
+ const normalizedPath = path9.normalize(filePath);
9851
+ const resolvedPath = path9.resolve(process.cwd(), normalizedPath);
9852
+ const cwd = path9.resolve(process.cwd());
9501
9853
  if (!resolvedPath.startsWith(cwd)) {
9502
9854
  throw new Error(`Access denied: Cannot create files outside of current working directory`);
9503
9855
  }
9504
- const fileExists = existsSync5(resolvedPath);
9856
+ const fileExists = existsSync6(resolvedPath);
9505
9857
  const action = fileExists ? "overwritten" : "created";
9506
9858
  if (createDirectories) {
9507
- const dir = path8.dirname(resolvedPath);
9508
- await fs8.mkdir(dir, { recursive: true });
9859
+ const dir = path9.dirname(resolvedPath);
9860
+ await fs9.mkdir(dir, { recursive: true });
9509
9861
  }
9510
- await fs8.writeFile(resolvedPath, content, "utf-8");
9511
- const stats = await fs8.stat(resolvedPath);
9862
+ await fs9.writeFile(resolvedPath, content, "utf-8");
9863
+ const stats = await fs9.stat(resolvedPath);
9512
9864
  const lines = content.split("\n").length;
9513
9865
  return `File ${action} successfully: ${filePath}
9514
9866
  Size: ${stats.size} bytes
@@ -9519,7 +9871,7 @@ var createFileTool = {
9519
9871
  implementation: (context) => ({
9520
9872
  toolFn: async (value) => {
9521
9873
  const params = value;
9522
- const fileExists = existsSync5(path8.resolve(process.cwd(), path8.normalize(params.path)));
9874
+ const fileExists = existsSync6(path9.resolve(process.cwd(), path9.normalize(params.path)));
9523
9875
  context.logger.info(`\u{1F4DD} CreateFile: ${fileExists ? "Overwriting" : "Creating"} file`, {
9524
9876
  path: params.path,
9525
9877
  size: params.content.length
@@ -9561,7 +9913,7 @@ var createFileTool = {
9561
9913
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/globFiles/index.js
9562
9914
  import { glob } from "glob";
9563
9915
  import { stat } from "fs/promises";
9564
- import path9 from "path";
9916
+ import path10 from "path";
9565
9917
  var DEFAULT_IGNORE_PATTERNS = [
9566
9918
  "**/node_modules/**",
9567
9919
  "**/.git/**",
@@ -9577,7 +9929,7 @@ var DEFAULT_IGNORE_PATTERNS = [
9577
9929
  async function findFiles(params) {
9578
9930
  const { pattern, dir_path, case_sensitive = true, respect_git_ignore = true } = params;
9579
9931
  const baseCwd = process.cwd();
9580
- const targetDir = dir_path ? path9.resolve(baseCwd, path9.normalize(dir_path)) : baseCwd;
9932
+ const targetDir = dir_path ? path10.resolve(baseCwd, path10.normalize(dir_path)) : baseCwd;
9581
9933
  if (!targetDir.startsWith(baseCwd)) {
9582
9934
  throw new Error(`Access denied: Cannot search outside of current working directory`);
9583
9935
  }
@@ -9617,7 +9969,7 @@ async function findFiles(params) {
9617
9969
  const summary = `Found ${filesWithStats.length} file(s)${truncated ? ` (showing first ${MAX_RESULTS})` : ""} matching: ${pattern}`;
9618
9970
  const dirInfo = dir_path ? `
9619
9971
  Directory: ${dir_path}` : "";
9620
- const filesList = results.map((file) => path9.relative(baseCwd, file.path)).join("\n");
9972
+ const filesList = results.map((file) => path10.relative(baseCwd, file.path)).join("\n");
9621
9973
  return `${summary}${dirInfo}
9622
9974
 
9623
9975
  ${filesList}`;
@@ -9671,27 +10023,48 @@ var globFilesTool = {
9671
10023
 
9672
10024
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/grepSearch/index.js
9673
10025
  import { stat as stat2 } from "fs/promises";
9674
- import path10 from "path";
9675
- import { execFile } from "child_process";
10026
+ import { existsSync as existsSync7 } from "fs";
10027
+ import path11 from "path";
10028
+ import { execFile, execFileSync as execFileSync2 } from "child_process";
9676
10029
  import { promisify } from "util";
9677
10030
  import { createRequire } from "module";
9678
10031
  var execFileAsync = promisify(execFile);
9679
10032
  var require2 = createRequire(import.meta.url);
10033
+ var cachedRgPath = null;
9680
10034
  function getRipgrepPath() {
10035
+ if (cachedRgPath)
10036
+ return cachedRgPath;
9681
10037
  try {
9682
10038
  const ripgrepPath = require2.resolve("@vscode/ripgrep");
9683
- const ripgrepDir = path10.dirname(ripgrepPath);
9684
- const rgBinary = path10.join(ripgrepDir, "..", "bin", "rg" + (process.platform === "win32" ? ".exe" : ""));
10039
+ const ripgrepDir = path11.dirname(ripgrepPath);
10040
+ const rgBinary = path11.join(ripgrepDir, "..", "bin", "rg" + (process.platform === "win32" ? ".exe" : ""));
10041
+ if (!existsSync7(rgBinary)) {
10042
+ const postinstallScript = path11.join(ripgrepDir, "..", "lib", "postinstall.js");
10043
+ if (existsSync7(postinstallScript)) {
10044
+ execFileSync2(process.execPath, [postinstallScript], {
10045
+ cwd: path11.join(ripgrepDir, ".."),
10046
+ stdio: "pipe",
10047
+ timeout: 3e4
10048
+ });
10049
+ }
10050
+ if (!existsSync7(rgBinary)) {
10051
+ throw new Error(`ripgrep binary not found at ${rgBinary}. The postinstall download may have failed. Try running: cd ${path11.join(ripgrepDir, "..")} && node lib/postinstall.js`);
10052
+ }
10053
+ }
10054
+ cachedRgPath = rgBinary;
9685
10055
  return rgBinary;
9686
10056
  } catch (error) {
10057
+ if (error instanceof Error && error.message.includes("ripgrep binary not found")) {
10058
+ throw error;
10059
+ }
9687
10060
  throw new Error("ripgrep is not available. Please install @vscode/ripgrep: pnpm add @vscode/ripgrep --filter @bike4mind/services");
9688
10061
  }
9689
10062
  }
9690
10063
  function isPathWithinWorkspace(targetPath, baseCwd) {
9691
- const resolvedTarget = path10.resolve(targetPath);
9692
- const resolvedBase = path10.resolve(baseCwd);
9693
- const relativePath = path10.relative(resolvedBase, resolvedTarget);
9694
- return !relativePath.startsWith("..") && !path10.isAbsolute(relativePath);
10064
+ const resolvedTarget = path11.resolve(targetPath);
10065
+ const resolvedBase = path11.resolve(baseCwd);
10066
+ const relativePath = path11.relative(resolvedBase, resolvedTarget);
10067
+ return !relativePath.startsWith("..") && !path11.isAbsolute(relativePath);
9695
10068
  }
9696
10069
  function convertGlobToRipgrepGlobs(globPattern) {
9697
10070
  if (!globPattern || globPattern === "**/*") {
@@ -9707,7 +10080,7 @@ function convertGlobToRipgrepGlobs(globPattern) {
9707
10080
  async function searchFiles2(params) {
9708
10081
  const { pattern, dir_path, include } = params;
9709
10082
  const baseCwd = process.cwd();
9710
- const targetDir = dir_path ? path10.resolve(baseCwd, dir_path) : baseCwd;
10083
+ const targetDir = dir_path ? path11.resolve(baseCwd, dir_path) : baseCwd;
9711
10084
  if (!isPathWithinWorkspace(targetDir, baseCwd)) {
9712
10085
  throw new Error(`Path validation failed: "${dir_path}" resolves outside the allowed workspace directory`);
9713
10086
  }
@@ -9769,7 +10142,7 @@ async function searchFiles2(params) {
9769
10142
  if (item.type === "match") {
9770
10143
  const match = item;
9771
10144
  allMatches.push({
9772
- filePath: path10.relative(targetDir, match.data.path.text) || path10.basename(match.data.path.text),
10145
+ filePath: path11.relative(targetDir, match.data.path.text) || path11.basename(match.data.path.text),
9773
10146
  lineNumber: match.data.line_number,
9774
10147
  line: match.data.lines.text.trimEnd()
9775
10148
  // Remove trailing newline
@@ -9862,18 +10235,18 @@ var grepSearchTool = {
9862
10235
  };
9863
10236
 
9864
10237
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/deleteFile/index.js
9865
- import { promises as fs9 } from "fs";
9866
- import { existsSync as existsSync6, statSync as statSync5 } from "fs";
9867
- import path11 from "path";
10238
+ import { promises as fs10 } from "fs";
10239
+ import { existsSync as existsSync8, statSync as statSync5 } from "fs";
10240
+ import path12 from "path";
9868
10241
  async function deleteFile(params) {
9869
10242
  const { path: filePath, recursive = false } = params;
9870
- const normalizedPath = path11.normalize(filePath);
9871
- const resolvedPath = path11.resolve(process.cwd(), normalizedPath);
9872
- const cwd = path11.resolve(process.cwd());
10243
+ const normalizedPath = path12.normalize(filePath);
10244
+ const resolvedPath = path12.resolve(process.cwd(), normalizedPath);
10245
+ const cwd = path12.resolve(process.cwd());
9873
10246
  if (!resolvedPath.startsWith(cwd)) {
9874
10247
  throw new Error(`Access denied: Cannot delete files outside of current working directory`);
9875
10248
  }
9876
- if (!existsSync6(resolvedPath)) {
10249
+ if (!existsSync8(resolvedPath)) {
9877
10250
  throw new Error(`File or directory not found: ${filePath}`);
9878
10251
  }
9879
10252
  const stats = statSync5(resolvedPath);
@@ -9883,10 +10256,10 @@ async function deleteFile(params) {
9883
10256
  throw new Error(`Path is a directory: ${filePath}. Use recursive=true to delete directories and their contents.`);
9884
10257
  }
9885
10258
  if (isDirectory) {
9886
- await fs9.rm(resolvedPath, { recursive: true, force: true });
10259
+ await fs10.rm(resolvedPath, { recursive: true, force: true });
9887
10260
  return `Directory deleted successfully: ${filePath}`;
9888
10261
  } else {
9889
- await fs9.unlink(resolvedPath);
10262
+ await fs10.unlink(resolvedPath);
9890
10263
  return `File deleted successfully: ${filePath}
9891
10264
  Size: ${size} bytes`;
9892
10265
  }
@@ -9896,8 +10269,8 @@ var deleteFileTool = {
9896
10269
  implementation: (context) => ({
9897
10270
  toolFn: async (value) => {
9898
10271
  const params = value;
9899
- const resolvedPath = path11.resolve(process.cwd(), path11.normalize(params.path));
9900
- const isDirectory = existsSync6(resolvedPath) && statSync5(resolvedPath).isDirectory();
10272
+ const resolvedPath = path12.resolve(process.cwd(), path12.normalize(params.path));
10273
+ const isDirectory = existsSync8(resolvedPath) && statSync5(resolvedPath).isDirectory();
9901
10274
  context.logger.info(`\u{1F5D1}\uFE0F DeleteFile: Deleting ${isDirectory ? "directory" : "file"}`, {
9902
10275
  path: params.path,
9903
10276
  recursive: params.recursive
@@ -9942,13 +10315,13 @@ function formatSearchResults(files) {
9942
10315
  const notes = file.notes ? `
9943
10316
  Notes: ${file.notes}` : "";
9944
10317
  const fileType = file.type || "FILE";
9945
- return `${index + 1}. **${file.fileName}**
10318
+ return `${index + 1}. **${file.fileName}** (ID: ${file.id})
9946
10319
  Type: ${fileType} | MIME: ${file.mimeType}
9947
10320
  Tags: ${tags}${notes}`;
9948
10321
  });
9949
10322
  return `Found ${files.length} document(s) in your knowledge base:
9950
10323
 
9951
- ` + formattedFiles.join("\n\n") + "\n\n*To use these documents in your conversation, you can ask me to reference specific files by name.*";
10324
+ ` + formattedFiles.join("\n\n") + "\n\n*Use retrieve_knowledge_content with a file ID or tags to read the actual document content.*";
9952
10325
  }
9953
10326
  var knowledgeBaseSearchTool = {
9954
10327
  name: "search_knowledge_base",
@@ -9975,6 +10348,8 @@ var knowledgeBaseSearchTool = {
9975
10348
  by: "fileName",
9976
10349
  direction: "asc"
9977
10350
  }, {
10351
+ textSearch: true,
10352
+ // Search across fileName + tags + notes for better recall
9978
10353
  includeShared: true,
9979
10354
  // Include owned + explicitly shared + org-shared files
9980
10355
  userGroups: context.user.groups || []
@@ -9989,18 +10364,18 @@ var knowledgeBaseSearchTool = {
9989
10364
  },
9990
10365
  toolSchema: {
9991
10366
  name: "search_knowledge_base",
9992
- description: "Search the user's uploaded knowledge base (fab files). Returns relevant documents from the user's own files, organization-shared files, and files explicitly shared with them. Use this tool when the user asks about their own documents, uploaded files, or organization knowledge.",
10367
+ description: "Search the user's uploaded knowledge base (fab files). Searches across file names, tags, and notes for broad recall. Returns relevant documents from the user's own files, organization-shared files, and files explicitly shared with them. Use this tool when the user asks about their own documents, uploaded files, or organization knowledge.",
9993
10368
  parameters: {
9994
10369
  type: "object",
9995
10370
  properties: {
9996
10371
  query: {
9997
10372
  type: "string",
9998
- description: "The search query to find relevant documents by filename or content"
10373
+ description: "The search query to find relevant documents. Matches against file names, tags, and notes."
9999
10374
  },
10000
10375
  tags: {
10001
10376
  type: "array",
10002
10377
  items: { type: "string" },
10003
- description: "Optional: filter results to only include files with these tags"
10378
+ description: 'Optional: filter results by tag names. Supports partial matching. For optimization docs, use tags like "opti:family:scheduling", "opti:QUBO", "opti:solver:highs", etc. Any matching tag qualifies the file.'
10004
10379
  },
10005
10380
  file_type: {
10006
10381
  type: "string",
@@ -10020,85 +10395,1707 @@ var knowledgeBaseSearchTool = {
10020
10395
  })
10021
10396
  };
10022
10397
 
10023
- // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/bashExecute/index.js
10024
- import { spawn } from "child_process";
10025
- import path12 from "path";
10026
- var DEFAULT_TIMEOUT_MS = 6e4;
10027
- var MAX_OUTPUT_SIZE = 100 * 1024;
10028
- var DANGEROUS_PATTERNS = [
10029
- // Destructive file operations
10030
- {
10031
- pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|.*\s+-[a-zA-Z]*r).*\//i,
10032
- reason: "Recursive delete with path",
10033
- block: true
10034
- },
10035
- {
10036
- pattern: /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|.*\s+-[a-zA-Z]*f).*--no-preserve-root/i,
10037
- reason: "Force delete without preserve root",
10038
- block: true
10039
- },
10040
- {
10041
- pattern: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+\//i,
10042
- reason: "Recursive force delete on root paths",
10043
- block: true
10044
- },
10045
- {
10046
- pattern: /\brm\s+-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*\s+\//i,
10047
- reason: "Force recursive delete on root paths",
10048
- block: true
10049
- },
10050
- // System-level dangerous commands
10051
- { pattern: /\bsudo\b/i, reason: "Elevated privileges (sudo)", block: true },
10052
- { pattern: /\bsu\s+(-|root)/i, reason: "Switch to root user", block: true },
10053
- { pattern: /\bchmod\s+777\b/i, reason: "Overly permissive chmod", block: false },
10054
- { pattern: /\bchown\s+-R\s+root/i, reason: "Recursive chown to root", block: true },
10055
- // Disk/partition operations
10056
- { pattern: /\bmkfs\b/i, reason: "Filesystem creation", block: true },
10057
- { pattern: /\bfdisk\b/i, reason: "Disk partitioning", block: true },
10058
- { pattern: /\bdd\s+.*of=\/dev\//i, reason: "Direct disk write", block: true },
10059
- // Network attacks
10060
- { pattern: /\b(nc|netcat)\s+.*-e\s+\/bin\/(ba)?sh/i, reason: "Reverse shell attempt", block: true },
10061
- { pattern: /\bcurl\s+.*\|\s*(ba)?sh/i, reason: "Piping remote script to shell", block: true },
10062
- { pattern: /\bwget\s+.*\|\s*(ba)?sh/i, reason: "Piping remote script to shell", block: true },
10063
- // Fork bombs and resource exhaustion
10064
- { pattern: /:\(\)\s*\{\s*:\|:&\s*\}\s*;/i, reason: "Fork bomb", block: true },
10065
- { pattern: /\bwhile\s+true.*do.*done.*&/i, reason: "Infinite loop in background", block: false },
10066
- // Credential/sensitive data access
10067
- { pattern: /\/etc\/shadow/i, reason: "Access to shadow file", block: true },
10068
- { pattern: /\/etc\/passwd.*>/i, reason: "Modifying passwd file", block: true },
10069
- { pattern: /\baws\s+.*--profile\s+/i, reason: "AWS profile access", block: false },
10070
- // History/log manipulation
10071
- { pattern: /\bhistory\s+-c\b/i, reason: "Clearing shell history", block: false },
10072
- { pattern: />\s*\/var\/log\//i, reason: "Overwriting system logs", block: true },
10073
- // Dangerous redirects
10074
- { pattern: />\s*\/dev\/sda/i, reason: "Writing to block device", block: true },
10075
- { pattern: />\s*\/dev\/null.*2>&1.*</i, reason: "Potentially hiding output", block: false },
10076
- // Environment manipulation
10077
- { pattern: /\bexport\s+PATH\s*=\s*[^$]/i, reason: "Overwriting PATH", block: false },
10078
- { pattern: /\bexport\s+LD_PRELOAD/i, reason: "LD_PRELOAD manipulation", block: true },
10079
- // Process/system control
10080
- { pattern: /\bkill\s+-9\s+(-1|1)\b/i, reason: "Killing all processes", block: true },
10081
- { pattern: /\bkillall\s+-9\b/i, reason: "Force killing processes", block: false },
10082
- { pattern: /\bshutdown\b/i, reason: "System shutdown", block: true },
10083
- { pattern: /\breboot\b/i, reason: "System reboot", block: true },
10084
- { pattern: /\binit\s+[06]\b/i, reason: "System runlevel change", block: true }
10085
- ];
10086
- var SAFE_COMMAND_PREFIXES = [
10087
- "ls",
10088
- "cat",
10089
- "head",
10090
- "tail",
10091
- "grep",
10092
- "find",
10093
- "echo",
10094
- "pwd",
10095
- "whoami",
10096
- "date",
10097
- "cal",
10098
- "wc",
10099
- "sort",
10100
- "uniq",
10101
- "cut",
10398
+ // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/knowledgeBaseRetrieve/index.js
10399
+ var DEFAULT_MAX_CHARS = 8e3;
10400
+ var ABSOLUTE_MAX_CHARS = 16e3;
10401
+ var knowledgeBaseRetrieveTool = {
10402
+ name: "retrieve_knowledge_content",
10403
+ implementation: (context) => ({
10404
+ toolFn: async (value) => {
10405
+ const params = value;
10406
+ const { file_id, tags, query, max_chars } = params;
10407
+ const charBudget = Math.min(max_chars ?? DEFAULT_MAX_CHARS, ABSOLUTE_MAX_CHARS);
10408
+ context.logger.log("\u{1F4D6} Knowledge Retrieve: params", { file_id, tags, query, max_chars: charBudget });
10409
+ if (!file_id && !tags?.length && !query) {
10410
+ return "Error: You must provide at least one of file_id, tags, or query.";
10411
+ }
10412
+ if (!context.db.fabfiles) {
10413
+ context.logger.error("\u274C Knowledge Retrieve: fabfiles repository not available");
10414
+ return "Knowledge base retrieval is not available at this time.";
10415
+ }
10416
+ if (!context.db.fabfilechunks) {
10417
+ context.logger.error("\u274C Knowledge Retrieve: fabfilechunks repository not available");
10418
+ return "Knowledge base retrieval is not available at this time (chunk reader unavailable).";
10419
+ }
10420
+ try {
10421
+ let files = [];
10422
+ if (file_id) {
10423
+ const file = await context.db.fabfiles.findByIdAndUserId(file_id, context.userId);
10424
+ if (file) {
10425
+ files = [file];
10426
+ } else {
10427
+ const searchResults = await context.db.fabfiles.search(context.userId, file_id, { tags: [], shared: false }, { page: 1, limit: 1 }, { by: "fileName", direction: "asc" }, { textSearch: true, includeShared: true, userGroups: context.user.groups || [] });
10428
+ files = searchResults.data;
10429
+ }
10430
+ if (files.length === 0) {
10431
+ return `No document found with ID "${file_id}". The file may not exist or you may not have access to it. Try using search_knowledge_base to find the correct file ID.`;
10432
+ }
10433
+ }
10434
+ if (files.length === 0 && (tags?.length || query)) {
10435
+ const searchResults = await context.db.fabfiles.search(context.userId, query || "", { tags: tags || [], shared: false }, { page: 1, limit: 5 }, { by: "fileName", direction: "asc" }, { textSearch: true, includeShared: true, userGroups: context.user.groups || [] });
10436
+ files = searchResults.data;
10437
+ if (files.length === 0) {
10438
+ const searchDesc = [query && `query "${query}"`, tags?.length && `tags [${tags.join(", ")}]`].filter(Boolean).join(" and ");
10439
+ return `No documents found matching ${searchDesc}. Try broadening your search with search_knowledge_base.`;
10440
+ }
10441
+ }
10442
+ let totalCharsUsed = 0;
10443
+ const sections = [];
10444
+ let filesRetrieved = 0;
10445
+ for (const file of files) {
10446
+ if (totalCharsUsed >= charBudget)
10447
+ break;
10448
+ const chunks = await context.db.fabfilechunks.findByFabFileId(file.id);
10449
+ if (chunks.length === 0) {
10450
+ context.logger.log(`\u{1F4D6} Knowledge Retrieve: No chunks for file ${file.fileName} (${file.id})`);
10451
+ continue;
10452
+ }
10453
+ const fullText = chunks.map((c) => c.text).join("\n");
10454
+ const remainingBudget = charBudget - totalCharsUsed;
10455
+ const truncated = fullText.length > remainingBudget;
10456
+ const content = truncated ? fullText.slice(0, remainingBudget) : fullText;
10457
+ const fileTags = file.tags?.map((t) => t.name).join(", ") || "none";
10458
+ const charLabel = truncated ? `${content.length} (truncated from ${fullText.length})` : `${content.length}`;
10459
+ sections.push(`### ${file.fileName} (ID: ${file.id})
10460
+ Tags: ${fileTags}
10461
+ Chunks: ${chunks.length} | Characters: ${charLabel}
10462
+ ---
10463
+ ` + content);
10464
+ totalCharsUsed += content.length;
10465
+ filesRetrieved++;
10466
+ }
10467
+ if (filesRetrieved === 0) {
10468
+ return "Found matching documents but they have no indexed content. The files may not have been processed yet.";
10469
+ }
10470
+ const header = `Retrieved content from ${filesRetrieved} of ${files.length} document(s):
10471
+ `;
10472
+ return header + "\n" + sections.join("\n\n---\n\n");
10473
+ } catch (error) {
10474
+ context.logger.error("\u274C Knowledge Retrieve: Error during retrieval:", error);
10475
+ return "An error occurred while retrieving document content. Please try again.";
10476
+ }
10477
+ },
10478
+ toolSchema: {
10479
+ name: "retrieve_knowledge_content",
10480
+ description: "Read the actual text content of knowledge base documents. Use this after search_knowledge_base to read documents by file ID, or provide tags/query to find and read documents in one step. Returns the full text content (up to the character budget) for grounding your responses in the user's curated knowledge.",
10481
+ parameters: {
10482
+ type: "object",
10483
+ properties: {
10484
+ file_id: {
10485
+ type: "string",
10486
+ description: "The file ID to retrieve (from search_knowledge_base results). Most efficient for single-document retrieval."
10487
+ },
10488
+ tags: {
10489
+ type: "array",
10490
+ items: { type: "string" },
10491
+ description: 'Filter documents by tags. For optimization docs, use tags like "opti:family:scheduling", "opti:solver:highs", etc.'
10492
+ },
10493
+ query: {
10494
+ type: "string",
10495
+ description: "Search query to find documents. Can be combined with tags for more targeted retrieval."
10496
+ },
10497
+ max_chars: {
10498
+ type: "number",
10499
+ description: "Maximum characters of content to return (default: 8000, max: 16000). Lower values for quick lookups, higher for detailed reading.",
10500
+ minimum: 500,
10501
+ maximum: 16e3
10502
+ }
10503
+ }
10504
+ }
10505
+ }
10506
+ })
10507
+ };
10508
+
10509
+ // ../../b4m-core/packages/quantum/dist/src/types.js
10510
+ import { z as z139 } from "zod";
10511
+ var OperationSchema = z139.object({
10512
+ jobId: z139.number(),
10513
+ machineId: z139.number(),
10514
+ duration: z139.number().positive()
10515
+ });
10516
+ var JobSchema = z139.object({
10517
+ id: z139.number(),
10518
+ name: z139.string(),
10519
+ operations: z139.array(OperationSchema)
10520
+ });
10521
+ var MachineSchema = z139.object({
10522
+ id: z139.number(),
10523
+ name: z139.string()
10524
+ });
10525
+ var SchedulingProblemSchema = z139.object({
10526
+ name: z139.string(),
10527
+ description: z139.string().optional(),
10528
+ jobs: z139.array(JobSchema),
10529
+ machines: z139.array(MachineSchema)
10530
+ });
10531
+ var SolverIdSchema = z139.enum([
10532
+ "greedy",
10533
+ "naive",
10534
+ "random-restart",
10535
+ "simulated-annealing",
10536
+ "simulated-annealing-medium",
10537
+ "simulated-annealing-large",
10538
+ "tabu",
10539
+ "genetic-algorithm",
10540
+ "ant-colony",
10541
+ "highs",
10542
+ "simulated-qaoa",
10543
+ "ionq-qaoa"
10544
+ ]);
10545
+
10546
+ // ../../b4m-core/packages/quantum/dist/src/solvers/greedy-solver.js
10547
+ var greedySolver = {
10548
+ id: "greedy",
10549
+ name: "Greedy (Earliest Start)",
10550
+ description: "Schedules each operation at the earliest available time on its machine, respecting job ordering.",
10551
+ async solve(problem, options) {
10552
+ const startTime = performance.now();
10553
+ const machineAvailableAt = /* @__PURE__ */ new Map();
10554
+ for (const machine of problem.machines) {
10555
+ machineAvailableAt.set(machine.id, 0);
10556
+ }
10557
+ const jobCompletedAt = /* @__PURE__ */ new Map();
10558
+ for (const job of problem.jobs) {
10559
+ jobCompletedAt.set(job.id, 0);
10560
+ }
10561
+ const schedule = [];
10562
+ const totalOps = problem.jobs.reduce((sum2, job) => sum2 + job.operations.length, 0);
10563
+ let opsScheduled = 0;
10564
+ for (const job of problem.jobs) {
10565
+ for (let opIndex = 0; opIndex < job.operations.length; opIndex++) {
10566
+ const op = job.operations[opIndex];
10567
+ const machineReady = machineAvailableAt.get(op.machineId) ?? 0;
10568
+ const jobReady = jobCompletedAt.get(job.id) ?? 0;
10569
+ const opStart = Math.max(machineReady, jobReady);
10570
+ const opEnd = opStart + op.duration;
10571
+ schedule.push({
10572
+ jobId: job.id,
10573
+ machineId: op.machineId,
10574
+ operationIndex: opIndex,
10575
+ startTime: opStart,
10576
+ endTime: opEnd
10577
+ });
10578
+ machineAvailableAt.set(op.machineId, opEnd);
10579
+ jobCompletedAt.set(job.id, opEnd);
10580
+ opsScheduled++;
10581
+ if (options?.onProgress) {
10582
+ options.onProgress({
10583
+ solverId: "greedy",
10584
+ percentage: Math.round(opsScheduled / totalOps * 100),
10585
+ bestMakespan: opEnd,
10586
+ currentIteration: opsScheduled,
10587
+ totalIterations: totalOps
10588
+ });
10589
+ }
10590
+ }
10591
+ }
10592
+ const makespan = Math.max(...schedule.map((op) => op.endTime));
10593
+ const elapsedMs = performance.now() - startTime;
10594
+ return {
10595
+ solverId: "greedy",
10596
+ solverName: "Greedy (Earliest Start)",
10597
+ makespan,
10598
+ schedule,
10599
+ elapsedMs,
10600
+ iterations: totalOps
10601
+ };
10602
+ }
10603
+ };
10604
+
10605
+ // ../../b4m-core/packages/quantum/dist/src/solvers/schedule-utils.js
10606
+ function buildOperationList(problem) {
10607
+ const ops = [];
10608
+ for (const job of problem.jobs) {
10609
+ for (let i = 0; i < job.operations.length; i++) {
10610
+ ops.push({ jobId: job.id, opIndex: i });
10611
+ }
10612
+ }
10613
+ return ops;
10614
+ }
10615
+ function decodePermutation(permutation, problem) {
10616
+ const schedule = [];
10617
+ const machineAvailableAt = /* @__PURE__ */ new Map();
10618
+ const jobCompletedAt = /* @__PURE__ */ new Map();
10619
+ const jobNextOp = /* @__PURE__ */ new Map();
10620
+ for (const machine of problem.machines) {
10621
+ machineAvailableAt.set(machine.id, 0);
10622
+ }
10623
+ for (const job of problem.jobs) {
10624
+ jobCompletedAt.set(job.id, 0);
10625
+ jobNextOp.set(job.id, 0);
10626
+ }
10627
+ const remaining = [...permutation];
10628
+ const maxPasses = remaining.length * remaining.length;
10629
+ let passes = 0;
10630
+ while (remaining.length > 0 && passes < maxPasses) {
10631
+ let scheduled = false;
10632
+ for (let i = 0; i < remaining.length; i++) {
10633
+ const ref = remaining[i];
10634
+ const nextOp = jobNextOp.get(ref.jobId) ?? 0;
10635
+ if (ref.opIndex !== nextOp)
10636
+ continue;
10637
+ const job = problem.jobs.find((j) => j.id === ref.jobId);
10638
+ const op = job.operations[ref.opIndex];
10639
+ const machineReady = machineAvailableAt.get(op.machineId) ?? 0;
10640
+ const jobReady = jobCompletedAt.get(ref.jobId) ?? 0;
10641
+ const start = Math.max(machineReady, jobReady);
10642
+ const end = start + op.duration;
10643
+ schedule.push({
10644
+ jobId: ref.jobId,
10645
+ machineId: op.machineId,
10646
+ operationIndex: ref.opIndex,
10647
+ startTime: start,
10648
+ endTime: end
10649
+ });
10650
+ machineAvailableAt.set(op.machineId, end);
10651
+ jobCompletedAt.set(ref.jobId, end);
10652
+ jobNextOp.set(ref.jobId, nextOp + 1);
10653
+ remaining.splice(i, 1);
10654
+ scheduled = true;
10655
+ break;
10656
+ }
10657
+ if (!scheduled) {
10658
+ remaining.push(remaining.shift());
10659
+ }
10660
+ passes++;
10661
+ }
10662
+ return schedule;
10663
+ }
10664
+ function computeMakespan(schedule) {
10665
+ if (schedule.length === 0)
10666
+ return 0;
10667
+ return Math.max(...schedule.map((op) => op.endTime));
10668
+ }
10669
+ function randomPermutation(problem, rng = Math.random) {
10670
+ const ops = buildOperationList(problem);
10671
+ for (let i = ops.length - 1; i > 0; i--) {
10672
+ const j = Math.floor(rng() * (i + 1));
10673
+ [ops[i], ops[j]] = [ops[j], ops[i]];
10674
+ }
10675
+ return ops;
10676
+ }
10677
+ function swapNeighbor(perm, rng = Math.random) {
10678
+ const newPerm = [...perm];
10679
+ const i = Math.floor(rng() * newPerm.length);
10680
+ let j = Math.floor(rng() * (newPerm.length - 1));
10681
+ if (j >= i)
10682
+ j++;
10683
+ [newPerm[i], newPerm[j]] = [newPerm[j], newPerm[i]];
10684
+ return newPerm;
10685
+ }
10686
+ function createRng(seed) {
10687
+ let s = seed | 0;
10688
+ return () => {
10689
+ s = s + 1831565813 | 0;
10690
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
10691
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
10692
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
10693
+ };
10694
+ }
10695
+
10696
+ // ../../b4m-core/packages/quantum/dist/src/solvers/naive-solver.js
10697
+ var MAX_PERMUTATIONS = 40320;
10698
+ function* permutations(arr) {
10699
+ if (arr.length <= 1) {
10700
+ yield [...arr];
10701
+ return;
10702
+ }
10703
+ for (let i = 0; i < arr.length; i++) {
10704
+ const rest = [...arr.slice(0, i), ...arr.slice(i + 1)];
10705
+ for (const perm of permutations(rest)) {
10706
+ yield [arr[i], ...perm];
10707
+ }
10708
+ }
10709
+ }
10710
+ var naiveSolver = {
10711
+ id: "naive",
10712
+ name: "Naive (Brute Force)",
10713
+ description: "Evaluates all permutations to find the optimal schedule. Only feasible for small problems.",
10714
+ async solve(problem, options) {
10715
+ const startTime = performance.now();
10716
+ const ops = buildOperationList(problem);
10717
+ const totalPerms = factorial(ops.length);
10718
+ const cappedTotal = Math.min(totalPerms, MAX_PERMUTATIONS);
10719
+ let bestMakespan = Infinity;
10720
+ let bestSchedule = decodePermutation(ops, problem);
10721
+ let count = 0;
10722
+ for (const perm of permutations(ops)) {
10723
+ if (count >= MAX_PERMUTATIONS)
10724
+ break;
10725
+ const schedule = decodePermutation(perm, problem);
10726
+ const makespan = computeMakespan(schedule);
10727
+ if (makespan < bestMakespan) {
10728
+ bestMakespan = makespan;
10729
+ bestSchedule = schedule;
10730
+ }
10731
+ count++;
10732
+ if (options?.onProgress && count % 100 === 0) {
10733
+ options.onProgress({
10734
+ solverId: "naive",
10735
+ percentage: Math.min(99, Math.round(count / cappedTotal * 100)),
10736
+ bestMakespan,
10737
+ currentIteration: count,
10738
+ totalIterations: cappedTotal
10739
+ });
10740
+ }
10741
+ }
10742
+ options?.onProgress?.({
10743
+ solverId: "naive",
10744
+ percentage: 100,
10745
+ bestMakespan,
10746
+ currentIteration: count,
10747
+ totalIterations: cappedTotal
10748
+ });
10749
+ return {
10750
+ solverId: "naive",
10751
+ solverName: "Naive (Brute Force)",
10752
+ makespan: bestMakespan,
10753
+ schedule: bestSchedule,
10754
+ elapsedMs: performance.now() - startTime,
10755
+ iterations: count
10756
+ };
10757
+ }
10758
+ };
10759
+ function factorial(n) {
10760
+ if (n <= 1)
10761
+ return 1;
10762
+ let result = 1;
10763
+ for (let i = 2; i <= n; i++)
10764
+ result *= i;
10765
+ return result;
10766
+ }
10767
+
10768
+ // ../../b4m-core/packages/quantum/dist/src/solvers/random-restart-solver.js
10769
+ var RESTARTS = 50;
10770
+ var HILL_CLIMB_ITERATIONS = 500;
10771
+ var randomRestartSolver = {
10772
+ id: "random-restart",
10773
+ name: "Random Restart Hill Climbing",
10774
+ description: "Multiple random starts with local search improvement via operation swaps.",
10775
+ async solve(problem, options) {
10776
+ const startTime = performance.now();
10777
+ const timeoutMs = options?.timeoutMs ?? 1e4;
10778
+ const rng = createRng(42);
10779
+ let globalBestMakespan = Infinity;
10780
+ let globalBestSchedule = decodePermutation(randomPermutation(problem, rng), problem);
10781
+ let totalIterations = 0;
10782
+ for (let restart = 0; restart < RESTARTS; restart++) {
10783
+ if (performance.now() - startTime > timeoutMs)
10784
+ break;
10785
+ let currentPerm = randomPermutation(problem, rng);
10786
+ let currentSchedule = decodePermutation(currentPerm, problem);
10787
+ let currentMakespan = computeMakespan(currentSchedule);
10788
+ for (let i = 0; i < HILL_CLIMB_ITERATIONS; i++) {
10789
+ if (performance.now() - startTime > timeoutMs)
10790
+ break;
10791
+ const neighborPerm = swapNeighbor(currentPerm, rng);
10792
+ const neighborSchedule = decodePermutation(neighborPerm, problem);
10793
+ const neighborMakespan = computeMakespan(neighborSchedule);
10794
+ if (neighborMakespan < currentMakespan) {
10795
+ currentPerm = neighborPerm;
10796
+ currentSchedule = neighborSchedule;
10797
+ currentMakespan = neighborMakespan;
10798
+ }
10799
+ totalIterations++;
10800
+ }
10801
+ if (currentMakespan < globalBestMakespan) {
10802
+ globalBestMakespan = currentMakespan;
10803
+ globalBestSchedule = currentSchedule;
10804
+ }
10805
+ options?.onProgress?.({
10806
+ solverId: "random-restart",
10807
+ percentage: Math.round((restart + 1) / RESTARTS * 100),
10808
+ bestMakespan: globalBestMakespan,
10809
+ currentIteration: restart + 1,
10810
+ totalIterations: RESTARTS
10811
+ });
10812
+ }
10813
+ return {
10814
+ solverId: "random-restart",
10815
+ solverName: "Random Restart Hill Climbing",
10816
+ makespan: globalBestMakespan,
10817
+ schedule: globalBestSchedule,
10818
+ elapsedMs: performance.now() - startTime,
10819
+ iterations: totalIterations
10820
+ };
10821
+ }
10822
+ };
10823
+
10824
+ // ../../b4m-core/packages/quantum/dist/src/solvers/simulated-annealing-solver.js
10825
+ var SA_CONFIGS = [
10826
+ {
10827
+ id: "simulated-annealing",
10828
+ name: "Simulated Annealing (S)",
10829
+ description: "Fast simulated annealing with 5,000 iterations. Good for quick estimates.",
10830
+ initialTemp: 100,
10831
+ coolingRate: 0.995,
10832
+ iterations: 5e3
10833
+ },
10834
+ {
10835
+ id: "simulated-annealing-medium",
10836
+ name: "Simulated Annealing (M)",
10837
+ description: "Medium simulated annealing with 25,000 iterations. Balanced speed/quality.",
10838
+ initialTemp: 200,
10839
+ coolingRate: 0.9985,
10840
+ iterations: 25e3
10841
+ },
10842
+ {
10843
+ id: "simulated-annealing-large",
10844
+ name: "Simulated Annealing (L)",
10845
+ description: "Long simulated annealing with 100,000 iterations. Best solution quality.",
10846
+ initialTemp: 500,
10847
+ coolingRate: 0.99995,
10848
+ iterations: 1e5
10849
+ }
10850
+ ];
10851
+ function createSASolver(config) {
10852
+ return {
10853
+ id: config.id,
10854
+ name: config.name,
10855
+ description: config.description,
10856
+ async solve(problem, options) {
10857
+ const startTime = performance.now();
10858
+ const timeoutMs = options?.timeoutMs ?? 3e4;
10859
+ const rng = createRng(42);
10860
+ let currentPerm = randomPermutation(problem, rng);
10861
+ let currentSchedule = decodePermutation(currentPerm, problem);
10862
+ let currentMakespan = computeMakespan(currentSchedule);
10863
+ let bestSchedule = currentSchedule;
10864
+ let bestMakespan = currentMakespan;
10865
+ let temperature = config.initialTemp;
10866
+ const progressInterval = Math.max(1, Math.floor(config.iterations / 100));
10867
+ for (let i = 0; i < config.iterations; i++) {
10868
+ if (performance.now() - startTime > timeoutMs)
10869
+ break;
10870
+ const neighborPerm = swapNeighbor(currentPerm, rng);
10871
+ const neighborSchedule = decodePermutation(neighborPerm, problem);
10872
+ const neighborMakespan = computeMakespan(neighborSchedule);
10873
+ const delta = neighborMakespan - currentMakespan;
10874
+ if (delta < 0 || rng() < Math.exp(-delta / temperature)) {
10875
+ currentPerm = neighborPerm;
10876
+ currentSchedule = neighborSchedule;
10877
+ currentMakespan = neighborMakespan;
10878
+ if (currentMakespan < bestMakespan) {
10879
+ bestSchedule = currentSchedule;
10880
+ bestMakespan = currentMakespan;
10881
+ }
10882
+ }
10883
+ temperature *= config.coolingRate;
10884
+ if (options?.onProgress && i % progressInterval === 0) {
10885
+ options.onProgress({
10886
+ solverId: config.id,
10887
+ percentage: Math.round(i / config.iterations * 100),
10888
+ bestMakespan,
10889
+ currentIteration: i,
10890
+ totalIterations: config.iterations
10891
+ });
10892
+ }
10893
+ }
10894
+ options?.onProgress?.({
10895
+ solverId: config.id,
10896
+ percentage: 100,
10897
+ bestMakespan,
10898
+ currentIteration: config.iterations,
10899
+ totalIterations: config.iterations
10900
+ });
10901
+ return {
10902
+ solverId: config.id,
10903
+ solverName: config.name,
10904
+ makespan: bestMakespan,
10905
+ schedule: bestSchedule,
10906
+ elapsedMs: performance.now() - startTime,
10907
+ iterations: config.iterations
10908
+ };
10909
+ }
10910
+ };
10911
+ }
10912
+ var simulatedAnnealingSolver = createSASolver(SA_CONFIGS[0]);
10913
+ var simulatedAnnealingMediumSolver = createSASolver(SA_CONFIGS[1]);
10914
+ var simulatedAnnealingLargeSolver = createSASolver(SA_CONFIGS[2]);
10915
+
10916
+ // ../../b4m-core/packages/quantum/dist/src/solvers/tabu-solver.js
10917
+ var ITERATIONS = 1e4;
10918
+ var TABU_TENURE = 15;
10919
+ var NEIGHBORHOOD_SIZE = 20;
10920
+ var tabuSolver = {
10921
+ id: "tabu",
10922
+ name: "Tabu Search",
10923
+ description: "Hill climbing with short-term memory to avoid revisiting recent solutions.",
10924
+ async solve(problem, options) {
10925
+ const startTime = performance.now();
10926
+ const timeoutMs = options?.timeoutMs ?? 15e3;
10927
+ const rng = createRng(42);
10928
+ let currentPerm = randomPermutation(problem, rng);
10929
+ let currentSchedule = decodePermutation(currentPerm, problem);
10930
+ let currentMakespan = computeMakespan(currentSchedule);
10931
+ let bestSchedule = currentSchedule;
10932
+ let bestMakespan = currentMakespan;
10933
+ const tabuList = /* @__PURE__ */ new Map();
10934
+ const progressInterval = Math.max(1, Math.floor(ITERATIONS / 100));
10935
+ for (let iter = 0; iter < ITERATIONS; iter++) {
10936
+ if (performance.now() - startTime > timeoutMs)
10937
+ break;
10938
+ let bestNeighborPerm = null;
10939
+ let bestNeighborMakespan = Infinity;
10940
+ let bestSwapKey = "";
10941
+ for (let n = 0; n < NEIGHBORHOOD_SIZE; n++) {
10942
+ const i = Math.floor(rng() * currentPerm.length);
10943
+ let j = Math.floor(rng() * (currentPerm.length - 1));
10944
+ if (j >= i)
10945
+ j++;
10946
+ const swapKey = `${Math.min(i, j)},${Math.max(i, j)}`;
10947
+ const candidatePerm = [...currentPerm];
10948
+ [candidatePerm[i], candidatePerm[j]] = [candidatePerm[j], candidatePerm[i]];
10949
+ const candidateSchedule = decodePermutation(candidatePerm, problem);
10950
+ const candidateMakespan = computeMakespan(candidateSchedule);
10951
+ const isTabu = (tabuList.get(swapKey) ?? 0) > iter;
10952
+ const aspirationMet = candidateMakespan < bestMakespan;
10953
+ if ((!isTabu || aspirationMet) && candidateMakespan < bestNeighborMakespan) {
10954
+ bestNeighborPerm = candidatePerm;
10955
+ bestNeighborMakespan = candidateMakespan;
10956
+ bestSwapKey = swapKey;
10957
+ }
10958
+ }
10959
+ if (bestNeighborPerm) {
10960
+ currentPerm = bestNeighborPerm;
10961
+ currentSchedule = decodePermutation(currentPerm, problem);
10962
+ currentMakespan = bestNeighborMakespan;
10963
+ tabuList.set(bestSwapKey, iter + TABU_TENURE);
10964
+ if (currentMakespan < bestMakespan) {
10965
+ bestMakespan = currentMakespan;
10966
+ bestSchedule = currentSchedule;
10967
+ }
10968
+ }
10969
+ if (iter % 100 === 0) {
10970
+ for (const [key, expiry] of tabuList) {
10971
+ if (expiry <= iter)
10972
+ tabuList.delete(key);
10973
+ }
10974
+ }
10975
+ if (options?.onProgress && iter % progressInterval === 0) {
10976
+ options.onProgress({
10977
+ solverId: "tabu",
10978
+ percentage: Math.round(iter / ITERATIONS * 100),
10979
+ bestMakespan,
10980
+ currentIteration: iter,
10981
+ totalIterations: ITERATIONS
10982
+ });
10983
+ }
10984
+ }
10985
+ options?.onProgress?.({
10986
+ solverId: "tabu",
10987
+ percentage: 100,
10988
+ bestMakespan,
10989
+ currentIteration: ITERATIONS,
10990
+ totalIterations: ITERATIONS
10991
+ });
10992
+ return {
10993
+ solverId: "tabu",
10994
+ solverName: "Tabu Search",
10995
+ makespan: bestMakespan,
10996
+ schedule: bestSchedule,
10997
+ elapsedMs: performance.now() - startTime,
10998
+ iterations: ITERATIONS
10999
+ };
11000
+ }
11001
+ };
11002
+
11003
+ // ../../b4m-core/packages/quantum/dist/src/solvers/genetic-algorithm-solver.js
11004
+ var POPULATION_SIZE = 50;
11005
+ var GENERATIONS = 200;
11006
+ var TOURNAMENT_SIZE = 5;
11007
+ var MUTATION_RATE = 0.15;
11008
+ var geneticAlgorithmSolver = {
11009
+ id: "genetic-algorithm",
11010
+ name: "Genetic Algorithm",
11011
+ description: "Population-based search with tournament selection, crossover, and mutation.",
11012
+ async solve(problem, options) {
11013
+ const startTime = performance.now();
11014
+ const timeoutMs = options?.timeoutMs ?? 2e4;
11015
+ const rng = createRng(42);
11016
+ let population = [];
11017
+ for (let i = 0; i < POPULATION_SIZE; i++) {
11018
+ const perm = randomPermutation(problem, rng);
11019
+ const schedule = decodePermutation(perm, problem);
11020
+ const makespan = computeMakespan(schedule);
11021
+ population.push({ perm, makespan });
11022
+ }
11023
+ let bestIndividual = population.reduce((best, ind) => ind.makespan < best.makespan ? ind : best);
11024
+ for (let gen = 0; gen < GENERATIONS; gen++) {
11025
+ if (performance.now() - startTime > timeoutMs)
11026
+ break;
11027
+ const newPopulation = [];
11028
+ newPopulation.push({ ...bestIndividual });
11029
+ while (newPopulation.length < POPULATION_SIZE) {
11030
+ const parent1 = tournamentSelect(population, TOURNAMENT_SIZE, rng);
11031
+ const parent2 = tournamentSelect(population, TOURNAMENT_SIZE, rng);
11032
+ let childPerm = crossover(parent1.perm, parent2.perm, rng);
11033
+ if (rng() < MUTATION_RATE) {
11034
+ childPerm = swapNeighbor(childPerm, rng);
11035
+ }
11036
+ const childSchedule = decodePermutation(childPerm, problem);
11037
+ const childMakespan = computeMakespan(childSchedule);
11038
+ newPopulation.push({ perm: childPerm, makespan: childMakespan });
11039
+ }
11040
+ population = newPopulation;
11041
+ const genBest = population.reduce((best, ind) => ind.makespan < best.makespan ? ind : best);
11042
+ if (genBest.makespan < bestIndividual.makespan) {
11043
+ bestIndividual = genBest;
11044
+ }
11045
+ options?.onProgress?.({
11046
+ solverId: "genetic-algorithm",
11047
+ percentage: Math.round((gen + 1) / GENERATIONS * 100),
11048
+ bestMakespan: bestIndividual.makespan,
11049
+ currentIteration: gen + 1,
11050
+ totalIterations: GENERATIONS
11051
+ });
11052
+ }
11053
+ const bestSchedule = decodePermutation(bestIndividual.perm, problem);
11054
+ return {
11055
+ solverId: "genetic-algorithm",
11056
+ solverName: "Genetic Algorithm",
11057
+ makespan: bestIndividual.makespan,
11058
+ schedule: bestSchedule,
11059
+ elapsedMs: performance.now() - startTime,
11060
+ iterations: GENERATIONS * POPULATION_SIZE
11061
+ };
11062
+ }
11063
+ };
11064
+ function tournamentSelect(population, size, rng) {
11065
+ let best = population[Math.floor(rng() * population.length)];
11066
+ for (let i = 1; i < size; i++) {
11067
+ const candidate = population[Math.floor(rng() * population.length)];
11068
+ if (candidate.makespan < best.makespan) {
11069
+ best = candidate;
11070
+ }
11071
+ }
11072
+ return best;
11073
+ }
11074
+ function crossover(parent1, parent2, rng) {
11075
+ const len = parent1.length;
11076
+ const start = Math.floor(rng() * len);
11077
+ const end = start + Math.floor(rng() * (len - start));
11078
+ const child = new Array(len).fill(null);
11079
+ const used = /* @__PURE__ */ new Set();
11080
+ for (let i = start; i <= end && i < len; i++) {
11081
+ child[i] = parent1[i];
11082
+ used.add(`${parent1[i].jobId}-${parent1[i].opIndex}`);
11083
+ }
11084
+ let pos = 0;
11085
+ for (const op of parent2) {
11086
+ const key = `${op.jobId}-${op.opIndex}`;
11087
+ if (!used.has(key)) {
11088
+ while (child[pos] !== null)
11089
+ pos++;
11090
+ child[pos] = op;
11091
+ used.add(key);
11092
+ }
11093
+ }
11094
+ return child;
11095
+ }
11096
+
11097
+ // ../../b4m-core/packages/quantum/dist/src/solvers/ant-colony-solver.js
11098
+ var NUM_ANTS = 20;
11099
+ var ITERATIONS2 = 100;
11100
+ var ALPHA = 1;
11101
+ var BETA = 2;
11102
+ var EVAPORATION = 0.1;
11103
+ var Q = 100;
11104
+ var antColonySolver = {
11105
+ id: "ant-colony",
11106
+ name: "Ant Colony Optimization",
11107
+ description: "Swarm intelligence using pheromone trails to guide solution construction.",
11108
+ async solve(problem, options) {
11109
+ const startTime = performance.now();
11110
+ const timeoutMs = options?.timeoutMs ?? 2e4;
11111
+ const rng = createRng(42);
11112
+ const allOps = buildOperationList(problem);
11113
+ const numOps = allOps.length;
11114
+ const pheromone = Array.from({ length: numOps }, () => new Array(numOps).fill(1));
11115
+ const heuristic = allOps.map((ref) => {
11116
+ const job = problem.jobs.find((j) => j.id === ref.jobId);
11117
+ const op = job.operations[ref.opIndex];
11118
+ return 1 / op.duration;
11119
+ });
11120
+ let bestMakespan = Infinity;
11121
+ let bestSchedule = decodePermutation(allOps, problem);
11122
+ for (let iter = 0; iter < ITERATIONS2; iter++) {
11123
+ if (performance.now() - startTime > timeoutMs)
11124
+ break;
11125
+ const antSolutions = [];
11126
+ for (let ant = 0; ant < NUM_ANTS; ant++) {
11127
+ const perm = constructSolution(allOps, pheromone, heuristic, rng);
11128
+ const schedule = decodePermutation(perm, problem);
11129
+ const makespan = computeMakespan(schedule);
11130
+ antSolutions.push({ perm, makespan });
11131
+ if (makespan < bestMakespan) {
11132
+ bestMakespan = makespan;
11133
+ bestSchedule = schedule;
11134
+ }
11135
+ }
11136
+ for (let i = 0; i < numOps; i++) {
11137
+ for (let j = 0; j < numOps; j++) {
11138
+ pheromone[i][j] *= 1 - EVAPORATION;
11139
+ }
11140
+ }
11141
+ for (const solution of antSolutions) {
11142
+ const deposit = Q / solution.makespan;
11143
+ for (let pos = 0; pos < solution.perm.length; pos++) {
11144
+ const opIdx = allOps.findIndex((o) => o.jobId === solution.perm[pos].jobId && o.opIndex === solution.perm[pos].opIndex);
11145
+ if (opIdx >= 0) {
11146
+ pheromone[pos][opIdx] += deposit;
11147
+ }
11148
+ }
11149
+ }
11150
+ options?.onProgress?.({
11151
+ solverId: "ant-colony",
11152
+ percentage: Math.round((iter + 1) / ITERATIONS2 * 100),
11153
+ bestMakespan,
11154
+ currentIteration: iter + 1,
11155
+ totalIterations: ITERATIONS2
11156
+ });
11157
+ }
11158
+ return {
11159
+ solverId: "ant-colony",
11160
+ solverName: "Ant Colony Optimization",
11161
+ makespan: bestMakespan,
11162
+ schedule: bestSchedule,
11163
+ elapsedMs: performance.now() - startTime,
11164
+ iterations: ITERATIONS2 * NUM_ANTS
11165
+ };
11166
+ }
11167
+ };
11168
+ function constructSolution(allOps, pheromone, heuristic, rng) {
11169
+ const remaining = new Set(allOps.map((_, i) => i));
11170
+ const result = [];
11171
+ for (let pos = 0; pos < allOps.length; pos++) {
11172
+ const candidates = Array.from(remaining);
11173
+ const weights = candidates.map((opIdx) => {
11174
+ const tau = Math.pow(pheromone[pos][opIdx], ALPHA);
11175
+ const eta = Math.pow(heuristic[opIdx], BETA);
11176
+ return tau * eta;
11177
+ });
11178
+ const totalWeight = weights.reduce((sum2, w) => sum2 + w, 0);
11179
+ let r = rng() * totalWeight;
11180
+ let selectedIdx = candidates[0];
11181
+ for (let i = 0; i < candidates.length; i++) {
11182
+ r -= weights[i];
11183
+ if (r <= 0) {
11184
+ selectedIdx = candidates[i];
11185
+ break;
11186
+ }
11187
+ }
11188
+ result.push(allOps[selectedIdx]);
11189
+ remaining.delete(selectedIdx);
11190
+ }
11191
+ return result;
11192
+ }
11193
+
11194
+ // ../../b4m-core/packages/quantum/dist/src/solvers/metadata.js
11195
+ var solverMetadata = {
11196
+ naive: {
11197
+ icon: "\u{1F40C}",
11198
+ // 🐌
11199
+ color: "neutral",
11200
+ complexity: "Exponential - O(n!)",
11201
+ requires: "Browser compute",
11202
+ tagline: "Try every possible solution",
11203
+ fullDescription: "The simplest possible approach: systematically enumerate and evaluate every valid permutation of operations. Guaranteed to find the optimal solution, but becomes computationally infeasible for larger problems due to factorial growth.",
11204
+ howItWorks: [
11205
+ "Generate all valid operation orderings (permutations)",
11206
+ "Evaluate each ordering to compute makespan",
11207
+ "Track the best solution found",
11208
+ "Return the globally optimal solution"
11209
+ ],
11210
+ keyParameters: [{ name: "Search Space", value: "n! permutations for n operations (with pruning for precedence)" }],
11211
+ strengths: ["Guarantees optimal solution", "Simple to understand and implement", "No parameter tuning needed"],
11212
+ weaknesses: [
11213
+ "O(n!) time complexity",
11214
+ "Infeasible for problems with >10 operations",
11215
+ "No early termination heuristics"
11216
+ ],
11217
+ bestFor: "Tiny problems (\u226410 ops) where you need a guaranteed optimal baseline",
11218
+ wikipedia: "https://en.wikipedia.org/wiki/Brute-force_search"
11219
+ },
11220
+ greedy: {
11221
+ icon: "\u26A1",
11222
+ // ⚡
11223
+ color: "success",
11224
+ complexity: "O(n\xB2)",
11225
+ requires: "Browser compute",
11226
+ tagline: "Always pick the locally best choice",
11227
+ fullDescription: 'A fast heuristic that builds a solution by always selecting the "best" available operation at each step. Uses the Shortest Processing Time (SPT) rule: schedule the shortest available operation first. Fast but may miss global optimum.',
11228
+ howItWorks: [
11229
+ "Start with an empty schedule",
11230
+ "Find all operations whose predecessors are complete",
11231
+ "Pick the one with shortest duration (SPT rule)",
11232
+ "Schedule it at the earliest possible time",
11233
+ "Repeat until all operations scheduled"
11234
+ ],
11235
+ keyParameters: [{ name: "Priority Rule", value: "SPT (Shortest Processing Time) \u2014 favors quick operations" }],
11236
+ strengths: ["Extremely fast (O(n\xB2))", "Produces valid solutions instantly", "Good baseline for comparison"],
11237
+ weaknesses: ["No backtracking", "Often far from optimal", "Can make locally optimal but globally poor choices"],
11238
+ bestFor: "Quick baseline, real-time scheduling, or as initial solution for metaheuristics",
11239
+ wikipedia: "https://en.wikipedia.org/wiki/Greedy_algorithm"
11240
+ },
11241
+ "random-restart": {
11242
+ icon: "\u{1F3B2}",
11243
+ // 🎲
11244
+ color: "primary",
11245
+ complexity: "~5 seconds",
11246
+ requires: "Browser compute",
11247
+ tagline: "Climb hills, restart when stuck",
11248
+ fullDescription: "Combines local search with random restarts. From each starting point, repeatedly move to better neighboring solutions until no improvement is possible (local optimum). Then restart from a new random solution. The best solution across all restarts is returned.",
11249
+ howItWorks: [
11250
+ "Generate a random valid schedule",
11251
+ "Try swapping pairs of operations",
11252
+ "Accept swaps that improve makespan",
11253
+ "When stuck (no improving swaps), restart from new random solution",
11254
+ "Track best solution across all restarts"
11255
+ ],
11256
+ keyParameters: [
11257
+ { name: "Max Restarts", value: "Number of times to restart from random (100)" },
11258
+ { name: "Stagnation Limit", value: "Iterations without improvement before restart (1000)" }
11259
+ ],
11260
+ strengths: ["Escapes local optima via restarts", "Simple and parallelizable", "Better than pure hill climbing"],
11261
+ weaknesses: [
11262
+ "No information sharing between restarts",
11263
+ "May revisit similar solutions",
11264
+ "Random restarts are uninformed"
11265
+ ],
11266
+ bestFor: "Medium problems where pure hill climbing gets stuck",
11267
+ wikipedia: "https://en.wikipedia.org/wiki/Hill_climbing#Random-restart_hill_climbing"
11268
+ },
11269
+ "simulated-annealing": {
11270
+ icon: "\u{1F525}",
11271
+ // 🔥
11272
+ color: "warning",
11273
+ complexity: "~7 seconds",
11274
+ requires: "Browser compute",
11275
+ tagline: "Cool down from chaos to order",
11276
+ fullDescription: 'Inspired by metallurgy: heating metal and slowly cooling it produces stronger crystal structures. SA starts "hot" (accepting bad moves) and "cools" (becoming pickier). This allows escaping local optima early while converging to good solutions later.',
11277
+ howItWorks: [
11278
+ "Start with high temperature (T=100)",
11279
+ "Generate neighbor by swapping two operations",
11280
+ "If better, always accept",
11281
+ "If worse, accept with probability e^(-\u0394/T)",
11282
+ "Gradually reduce temperature (cooling)",
11283
+ "As T\u21920, behaves like pure hill climbing"
11284
+ ],
11285
+ keyParameters: [
11286
+ { name: "Initial Temperature", value: 'Starting "heat" level (100)' },
11287
+ { name: "Cooling Rate", value: "How fast temperature decreases (~0.9999...)" },
11288
+ { name: "Iterations", value: "5 million moves evaluated" }
11289
+ ],
11290
+ strengths: [
11291
+ "Escapes local optima probabilistically",
11292
+ "Well-studied theoretical properties",
11293
+ "Single parameter to tune (cooling schedule)"
11294
+ ],
11295
+ weaknesses: ["Cooling schedule is problem-dependent", "Can be slow to converge", "Single-solution method"],
11296
+ bestFor: "General optimization when you have moderate compute budget",
11297
+ wikipedia: "https://en.wikipedia.org/wiki/Simulated_annealing"
11298
+ },
11299
+ "simulated-annealing-medium": {
11300
+ icon: "\u{1F525}",
11301
+ // 🔥
11302
+ color: "warning",
11303
+ complexity: "~70 seconds",
11304
+ requires: "Browser compute",
11305
+ tagline: "Extended cooling for deeper exploration",
11306
+ fullDescription: "Same algorithm as SA but with 10\xD7 more iterations (50M). The slower cooling schedule allows more thorough exploration of the solution space before settling into a final basin.",
11307
+ howItWorks: ["Same as SA standard", "But with 50 million iterations", "Slower cooling = more exploration time"],
11308
+ keyParameters: [
11309
+ { name: "Iterations", value: "50 million moves (10\xD7 standard)" },
11310
+ { name: "Timeout", value: "5 minutes max" }
11311
+ ],
11312
+ strengths: ["More thorough than standard SA", "Better for complex landscapes", "Higher chance of global optimum"],
11313
+ weaknesses: [
11314
+ "10\xD7 slower than standard SA",
11315
+ "Diminishing returns on easy problems",
11316
+ "May be overkill for small instances"
11317
+ ],
11318
+ bestFor: "When standard SA finds good but not great solutions",
11319
+ wikipedia: "https://en.wikipedia.org/wiki/Simulated_annealing"
11320
+ },
11321
+ "simulated-annealing-large": {
11322
+ icon: "\u{1F525}",
11323
+ // 🔥
11324
+ color: "danger",
11325
+ complexity: "~12 minutes",
11326
+ requires: "Browser compute",
11327
+ tagline: "Maximum exploration budget",
11328
+ fullDescription: "The heavy artillery: 500 million iterations with up to 30 minutes of compute. For when you absolutely need the best possible solution and have time to wait.",
11329
+ howItWorks: [
11330
+ "Same as SA standard/medium",
11331
+ "But with 500 million iterations",
11332
+ "Very slow cooling for maximum exploration"
11333
+ ],
11334
+ keyParameters: [
11335
+ { name: "Iterations", value: "500 million moves (100\xD7 standard)" },
11336
+ { name: "Timeout", value: "30 minutes max" }
11337
+ ],
11338
+ strengths: ["Maximum exploration", "Best chance at global optimum", "Leaves no stone unturned"],
11339
+ weaknesses: ["Very slow (up to 30 min)", "Massive overkill for small problems", "Diminishing returns"],
11340
+ bestFor: "Large problems where quality matters more than time",
11341
+ wikipedia: "https://en.wikipedia.org/wiki/Simulated_annealing"
11342
+ },
11343
+ tabu: {
11344
+ icon: "\u{1F50D}",
11345
+ // 🔍
11346
+ color: "primary",
11347
+ complexity: "~20\u201340 seconds",
11348
+ requires: "Browser compute",
11349
+ tagline: "Remember mistakes to avoid repeating them",
11350
+ fullDescription: 'This is a classical (simple) Tabu Search following Glover (1986) with aspiration by objective. It maintains a short-term recency-based memory of recent swap moves that are temporarily forbidden ("tabu"). The neighborhood is generated by random pairwise swaps of operations in the permutation representation. Unlike reactive or robust variants, this implementation uses a fixed tabu tenure and does not employ long-term memory, intensification, or diversification phases.',
11351
+ howItWorks: [
11352
+ "Start from a random permutation of operations (seed: 42 for reproducibility)",
11353
+ "Generate 20 random neighbor solutions by swapping two operation positions",
11354
+ "Select the best non-tabu neighbor (even if worse than current \u2014 forced exploration)",
11355
+ "Record the swap as tabu for 15 iterations (fixed tenure)",
11356
+ "Aspiration criterion: override tabu if move produces a new global best",
11357
+ "Repeat for 10,000 iterations or until timeout (15s)",
11358
+ "Periodically clean expired entries from the tabu list"
11359
+ ],
11360
+ keyParameters: [
11361
+ { name: "Variant", value: "Simple Tabu Search (Glover 1986) with aspiration by objective" },
11362
+ { name: "Tabu Structure", value: "Swap-based \u2014 records position pairs (i,j) that were swapped" },
11363
+ { name: "Tabu Tenure", value: "Fixed at 15 iterations (no reactive adjustment)" },
11364
+ { name: "Neighborhood Size", value: "20 random swap candidates evaluated per iteration" },
11365
+ { name: "Total Iterations", value: "10,000" },
11366
+ { name: "Aspiration", value: "Accept tabu move if it improves the global best" },
11367
+ { name: "Memory Type", value: "Short-term recency only (no frequency-based long-term memory)" }
11368
+ ],
11369
+ strengths: [
11370
+ "Memory prevents cycling back to recently visited solutions",
11371
+ "Forced acceptance of worse moves enables escaping local optima",
11372
+ "Aspiration criterion preserves exploitation of exceptional solutions",
11373
+ "Deterministic with seed (reproducible results)"
11374
+ ],
11375
+ weaknesses: [
11376
+ "Fixed tenure \u2014 no adaptive/reactive adjustment to search dynamics",
11377
+ "No long-term memory (frequency-based diversification)",
11378
+ "No intensification phase to deep-search promising regions",
11379
+ "Small neighborhood (20) may miss good moves in large search spaces"
11380
+ ],
11381
+ bestFor: "Medium-complexity problems with many local optima and plateau regions",
11382
+ wikipedia: "https://en.wikipedia.org/wiki/Tabu_search",
11383
+ otherResources: [
11384
+ { label: "Glover (1986) \u2014 Original paper", url: "https://doi.org/10.1016/0305-0548(86)90048-1" },
11385
+ {
11386
+ label: "Glover & Laguna \u2014 Tabu Search (book)",
11387
+ url: "https://en.wikipedia.org/wiki/Tabu_search#References"
11388
+ }
11389
+ ]
11390
+ },
11391
+ "genetic-algorithm": {
11392
+ icon: "\u{1F9EC}",
11393
+ // 🧬
11394
+ color: "success",
11395
+ complexity: "~30 seconds",
11396
+ requires: "Browser compute",
11397
+ tagline: "Evolve solutions through natural selection",
11398
+ fullDescription: 'Inspired by biological evolution: maintain a population of solutions that reproduce, mutate, and compete. Better solutions are more likely to survive and pass on their "genes" (good partial solutions). Over generations, the population evolves toward optimality.',
11399
+ howItWorks: [
11400
+ "Initialize population of 100 random schedules",
11401
+ "Evaluate fitness (1/makespan) for each",
11402
+ "Selection: pick parents via tournament (best of 5 random)",
11403
+ "Crossover: combine two parents to create child",
11404
+ "Mutation: randomly swap operations (20% chance)",
11405
+ "Elitism: top 5 solutions survive unchanged",
11406
+ "Repeat for 15,000 generations"
11407
+ ],
11408
+ keyParameters: [
11409
+ { name: "Population Size", value: "Number of solutions maintained (100)" },
11410
+ { name: "Generations", value: "Evolution cycles (15,000)" },
11411
+ { name: "Crossover Rate", value: "Probability of combining parents (80%)" },
11412
+ { name: "Mutation Rate", value: "Probability of random change (20%)" },
11413
+ { name: "Elite Count", value: "Best solutions preserved unchanged (5)" }
11414
+ ],
11415
+ strengths: ["Population maintains diversity", "Crossover combines good building blocks", "Naturally parallel"],
11416
+ weaknesses: ["Many parameters to tune", "Can converge prematurely", "Crossover design is problem-specific"],
11417
+ bestFor: "Complex problems where good solutions share common substructures",
11418
+ wikipedia: "https://en.wikipedia.org/wiki/Genetic_algorithm",
11419
+ otherResources: [
11420
+ {
11421
+ label: "Order Crossover (OX)",
11422
+ url: "https://en.wikipedia.org/wiki/Crossover_(genetic_algorithm)#Order_crossover_(OX)"
11423
+ }
11424
+ ]
11425
+ },
11426
+ "ant-colony": {
11427
+ icon: "\u{1F41C}",
11428
+ // 🐜
11429
+ color: "success",
11430
+ complexity: "~30 seconds",
11431
+ requires: "Browser compute",
11432
+ tagline: "Follow the pheromone trails",
11433
+ fullDescription: "Inspired by how ants find shortest paths to food: they deposit pheromones, and other ants preferentially follow stronger trails. Over time, shorter paths accumulate more pheromone (ants traverse them faster), creating positive feedback toward good solutions.",
11434
+ howItWorks: [
11435
+ "Initialize pheromone trails uniformly",
11436
+ "Each ant builds a complete schedule probabilistically",
11437
+ "Operations with more pheromone are more likely to be chosen",
11438
+ "After all ants finish, evaluate their solutions",
11439
+ "Deposit pheromone on edges used by good solutions",
11440
+ "Evaporate some pheromone (forget old information)",
11441
+ "Best ant's solution gets extra pheromone (elitism)"
11442
+ ],
11443
+ keyParameters: [
11444
+ { name: "Number of Ants", value: "Solutions built per iteration (50)" },
11445
+ { name: "Iterations", value: "Pheromone update cycles (100,000)" },
11446
+ { name: "Alpha (\u03B1)", value: "Pheromone importance (1.0)" },
11447
+ { name: "Beta (\u03B2)", value: "Heuristic importance (2.0)" },
11448
+ { name: "Evaporation Rate", value: "Pheromone decay per iteration (10%)" }
11449
+ ],
11450
+ strengths: ["Implicit parallelism", "Positive feedback accelerates convergence", "Robust to problem changes"],
11451
+ weaknesses: ["Many parameters", "Can converge prematurely", "Pheromone model must match problem"],
11452
+ bestFor: 'Routing and sequencing problems with clear "edge" structure',
11453
+ wikipedia: "https://en.wikipedia.org/wiki/Ant_colony_optimization_algorithms",
11454
+ otherResources: [{ label: "Marco Dorigo (inventor)", url: "https://en.wikipedia.org/wiki/Marco_Dorigo" }]
11455
+ },
11456
+ highs: {
11457
+ icon: "\u{1F3AF}",
11458
+ // 🎯
11459
+ color: "success",
11460
+ complexity: "Seconds to minutes",
11461
+ requires: "Browser compute (WASM)",
11462
+ available: false,
11463
+ tagline: "Open-source solver rivaling commercial giants",
11464
+ fullDescription: "HiGHS (High-performance Interior-point, Gradient-descent, Simplex) is an MIT-licensed open-source solver that achieves 90%+ of commercial solver performance. Developed at the University of Edinburgh, it has rapidly become the leading open-source alternative to Gurobi and COPT for linear and mixed-integer programming.",
11465
+ howItWorks: [
11466
+ "Formulate problem as Integer Linear Program (ILP/MIP)",
11467
+ "Presolve: simplify model, tighten bounds, detect infeasibility",
11468
+ "LP relaxation using dual simplex or interior-point method",
11469
+ "Branch-and-bound with sophisticated node selection",
11470
+ "Cutting planes: Gomory cuts, MIR cuts, cover cuts",
11471
+ "Return optimal (or near-optimal) solution with gap certificate"
11472
+ ],
11473
+ keyParameters: [
11474
+ { name: "Time Limit", value: "Maximum solve time before returning best found" },
11475
+ { name: "MIP Gap", value: "Stop when proven within X% of optimal (e.g., 0.01 = 1%)" },
11476
+ { name: "Threads", value: "Number of parallel threads (default: all cores)" }
11477
+ ],
11478
+ strengths: [
11479
+ "MIT licensed \u2014 fully open source, embed anywhere",
11480
+ "90%+ of Gurobi/COPT performance on most problems",
11481
+ "Active development with rapid improvements",
11482
+ "Can deploy in customer infrastructure (no license servers)"
11483
+ ],
11484
+ weaknesses: [
11485
+ "Slightly slower than Gurobi on very large instances",
11486
+ "Fewer specialized cuts than commercial solvers",
11487
+ "Less mature ecosystem (but growing fast)"
11488
+ ],
11489
+ bestFor: "Production scheduling when you need commercial-grade results without commercial licensing",
11490
+ wikipedia: "https://en.wikipedia.org/wiki/HiGHS_optimization_solver",
11491
+ otherResources: [
11492
+ { label: "HiGHS GitHub", url: "https://github.com/ERGO-Code/HiGHS" },
11493
+ { label: "HiGHS Documentation", url: "https://highs.dev/" },
11494
+ { label: "Mittelmann MIP Benchmarks", url: "http://plato.asu.edu/ftp/milp.html" }
11495
+ ]
11496
+ },
11497
+ "simulated-qaoa": {
11498
+ icon: "\u{1F52E}",
11499
+ // 🔮
11500
+ color: "primary",
11501
+ complexity: "~30\u201360 seconds",
11502
+ requires: "Backend compute",
11503
+ available: false,
11504
+ tagline: "Quantum-inspired classical simulation",
11505
+ fullDescription: "A classical simulator of the Quantum Approximate Optimization Algorithm (QAOA). Encodes the job-shop scheduling problem as a QUBO (Quadratic Unconstrained Binary Optimization), then simulates the quantum variational circuit classically. Provides a preview of quantum advantage without requiring real quantum hardware.",
11506
+ howItWorks: [
11507
+ "Encode scheduling problem as QUBO matrix",
11508
+ "Initialize simulated quantum state |+\u27E9\u2297\u207F",
11509
+ "Apply p layers of QAOA circuit (problem + mixer unitaries)",
11510
+ "Classically optimize variational parameters (\u03B3, \u03B2)",
11511
+ "Measure (sample) the final state to extract candidate schedules",
11512
+ "Decode best bitstring back to a valid schedule"
11513
+ ],
11514
+ keyParameters: [
11515
+ { name: "QAOA Depth (p)", value: "Number of alternating layers (default: 3)" },
11516
+ { name: "Optimizer", value: "COBYLA for variational parameter optimization" },
11517
+ { name: "Shots", value: "Number of measurement samples (1024)" },
11518
+ { name: "Qubits", value: "One per possible (job, timeslot) assignment" }
11519
+ ],
11520
+ strengths: [
11521
+ "Preview quantum algorithms without quantum hardware",
11522
+ "Explores solution space via quantum superposition (simulated)",
11523
+ "Useful for benchmarking against real quantum results",
11524
+ "Runs entirely in-browser or on backend \u2014 no API keys needed"
11525
+ ],
11526
+ weaknesses: [
11527
+ "Exponential classical overhead \u2014 limited to ~20 qubits",
11528
+ "Cannot capture true quantum speedup",
11529
+ "QUBO encoding may not be optimal for all problem structures",
11530
+ "Variational optimization can get stuck in local minima"
11531
+ ],
11532
+ bestFor: "Small problems where you want to preview quantum approaches before investing in real hardware",
11533
+ wikipedia: "https://en.wikipedia.org/wiki/Quantum_approximate_optimization_algorithm",
11534
+ otherResources: [
11535
+ { label: "Farhi et al. (2014) \u2014 Original QAOA Paper", url: "https://arxiv.org/abs/1411.4028" },
11536
+ { label: "QUBO Formulation Guide", url: "https://en.wikipedia.org/wiki/Quadratic_unconstrained_binary_optimization" }
11537
+ ]
11538
+ },
11539
+ "ionq-qaoa": {
11540
+ icon: "\u269B\uFE0F",
11541
+ // ⚛️
11542
+ color: "danger",
11543
+ complexity: "Minutes (queue + execution)",
11544
+ requires: "IonQ API key + credits",
11545
+ available: false,
11546
+ tagline: "Real quantum hardware optimization",
11547
+ fullDescription: "Runs the Quantum Approximate Optimization Algorithm on IonQ\u2019s trapped-ion quantum computers. This is real quantum computation \u2014 the problem is encoded as a QUBO, compiled to native quantum gates, and executed on physical qubits. Results include genuine quantum effects like superposition and entanglement.",
11548
+ howItWorks: [
11549
+ "Encode scheduling problem as QUBO matrix",
11550
+ "Compile QAOA circuit to IonQ\u2019s native gate set (MS, GPi, GPi2)",
11551
+ "Submit job to IonQ cloud via API",
11552
+ "Queue for hardware execution (trapped-ion processor)",
11553
+ "Execute quantum circuit with real qubits",
11554
+ "Retrieve measurement results and decode to schedule"
11555
+ ],
11556
+ keyParameters: [
11557
+ { name: "QAOA Depth (p)", value: "Number of alternating layers (default: 1\u20132 for NISQ)" },
11558
+ { name: "Backend", value: "IonQ Harmony (11 qubits) or Aria (25 qubits)" },
11559
+ { name: "Shots", value: "Number of circuit executions (1024)" },
11560
+ { name: "Error Mitigation", value: "IonQ debiasing enabled by default" }
11561
+ ],
11562
+ strengths: [
11563
+ "Real quantum computation with genuine quantum effects",
11564
+ "Trapped-ion qubits have high gate fidelity (~99.5%)",
11565
+ "All-to-all connectivity \u2014 no SWAP overhead",
11566
+ "Potential for quantum advantage on certain problem classes"
11567
+ ],
11568
+ weaknesses: [
11569
+ "Requires IonQ API key and credits (not free)",
11570
+ "Queue wait times can be minutes to hours",
11571
+ "Limited to ~25 qubits (current hardware)",
11572
+ "NISQ noise limits circuit depth and solution quality"
11573
+ ],
11574
+ bestFor: "Exploring real quantum optimization on small problem instances when you have IonQ access",
11575
+ wikipedia: "https://en.wikipedia.org/wiki/Trapped-ion_quantum_computer",
11576
+ otherResources: [
11577
+ { label: "IonQ Documentation", url: "https://docs.ionq.com/" },
11578
+ { label: "IonQ Aria Specs", url: "https://ionq.com/quantum-systems/aria" },
11579
+ { label: "QAOA on NISQ Devices", url: "https://arxiv.org/abs/1812.01041" }
11580
+ ]
11581
+ }
11582
+ };
11583
+
11584
+ // ../../b4m-core/packages/quantum/dist/src/solvers/index.js
11585
+ var allSolvers = [
11586
+ greedySolver,
11587
+ naiveSolver,
11588
+ randomRestartSolver,
11589
+ simulatedAnnealingSolver,
11590
+ simulatedAnnealingMediumSolver,
11591
+ simulatedAnnealingLargeSolver,
11592
+ tabuSolver,
11593
+ geneticAlgorithmSolver,
11594
+ antColonySolver
11595
+ ];
11596
+ var solverRegistry = new Map(allSolvers.map((s) => [s.id, s]));
11597
+ function getSolver(id) {
11598
+ return solverRegistry.get(id);
11599
+ }
11600
+ function getAvailableSolverIds() {
11601
+ return Array.from(solverRegistry.keys());
11602
+ }
11603
+ var displaySolvers = [
11604
+ ...allSolvers.map((s) => ({ id: s.id, name: s.name, description: s.description })),
11605
+ { id: "highs", name: "HiGHS (WASM)", description: solverMetadata.highs.tagline },
11606
+ { id: "simulated-qaoa", name: "Simulated QAOA", description: solverMetadata["simulated-qaoa"].tagline },
11607
+ { id: "ionq-qaoa", name: "IonQ QAOA", description: solverMetadata["ionq-qaoa"].tagline }
11608
+ ];
11609
+
11610
+ // ../../b4m-core/packages/quantum/dist/src/prompts/system-prompt.js
11611
+ var QUANTUM_CANVASSER_SYSTEM_PROMPT = `You are OptiHashi, an AI agent specializing in combinatorial optimization and job-shop scheduling. You help users formulate scheduling problems, run solver algorithms, and interpret optimization results. You are solver-agnostic \u2014 you route problems to the best solver whether classical (greedy, simulated annealing, genetic algorithms, HiGHS MIP) or quantum (QAOA), building credibility through honest recommendations.
11612
+
11613
+ ## Your Capabilities
11614
+
11615
+ You have two specialized tools:
11616
+
11617
+ ### quantum_formulate
11618
+ Converts natural language descriptions into structured SchedulingProblem definitions. When a user describes a scheduling scenario in plain English, use this tool to extract:
11619
+ - Jobs (ordered sequences of operations)
11620
+ - Machines (resources that process operations)
11621
+ - Operation durations and machine assignments
11622
+ - Precedence constraints (operations within a job must execute in order)
11623
+
11624
+ ### quantum_schedule
11625
+ Runs optimization solvers on a structured SchedulingProblem. Available solvers:
11626
+ ${allSolvers.map((s) => `- **${s.name}** (\`${s.id}\`): ${s.description}`).join("\n")}
11627
+
11628
+ ## How to Help Users
11629
+
11630
+ ### Problem Formulation
11631
+ When a user describes a scheduling scenario:
11632
+ 1. Use \`quantum_formulate\` to convert their description into a structured problem
11633
+ 2. Present the formulated problem clearly, showing jobs, machines, and operation sequences
11634
+ 3. Ask if adjustments are needed before solving
11635
+
11636
+ ### Running Solvers
11637
+ When solving a problem:
11638
+ 1. Start with the \`greedy\` solver for a quick baseline result
11639
+ 2. For better solutions, suggest running multiple solvers: \`greedy\`, \`simulated-annealing\`, \`tabu\`, \`genetic-algorithm\`
11640
+ 3. Explain that metaheuristic solvers explore larger solution spaces but take longer
11641
+ 4. Compare results across solvers when multiple are run
11642
+
11643
+ ### Interpreting Results
11644
+ When presenting solver results:
11645
+ - Explain the **makespan** (total schedule length) and why lower is better
11646
+ - Walk through the schedule showing which jobs run on which machines at what times
11647
+ - Identify bottleneck machines (those with the most continuous utilization)
11648
+ - Suggest improvements if the problem structure allows (e.g., rebalancing operations)
11649
+
11650
+ ### QUBO Encoding
11651
+ If asked about quantum computing aspects:
11652
+ - Explain how scheduling problems are encoded as QUBO (Quadratic Unconstrained Binary Optimization) matrices
11653
+ - Time-indexed binary variables: x_{o,t} = 1 if operation o starts at time t
11654
+ - Three constraint families: exactly-once (each operation starts exactly once), no-overlap (no two operations on same machine at same time), precedence (operations within a job maintain order)
11655
+ - QUBO matrices can be solved by quantum annealers (D-Wave) or quantum-inspired classical solvers
11656
+
11657
+ ## Communication Style
11658
+ - Be precise with numbers and scheduling terminology
11659
+ - Use clear formatting: tables for schedules, bullet points for comparisons
11660
+ - When explaining optimization concepts, ground them in the user's specific problem
11661
+ - Proactively suggest next steps (e.g., "Would you like to try more solvers?" or "Should I adjust the problem?")
11662
+
11663
+ ## Important Notes
11664
+ - All solvers run client-side in the browser via Web Workers \u2014 no server compute needed
11665
+ - The greedy solver is deterministic and instant; metaheuristic solvers involve randomness
11666
+ - For very large problems (50+ operations), recommend the greedy and simulated-annealing solvers
11667
+ - The HiGHS MIP solver uses WASM and provides optimal solutions for small problems but may be slow on large ones`;
11668
+ var PROBLEM_FORMULATION_PROMPT = `You are a job-shop scheduling problem formulator. Convert the user's natural language description into a structured SchedulingProblem JSON object.
11669
+
11670
+ RULES:
11671
+ 1. Each job has an id (number starting from 0), a name, and an ordered array of operations
11672
+ 2. Each machine has an id (number starting from 0) and a name
11673
+ 3. Each operation has jobId (matching its parent job), machineId (which machine it runs on), and duration (positive integer)
11674
+ 4. Operations within a job execute in order (first operation must finish before second starts)
11675
+ 5. Each machine can only process one operation at a time
11676
+ 6. Assign reasonable durations if not specified (use whole numbers)
11677
+ 7. Create meaningful names for jobs and machines based on context
11678
+
11679
+ RESPOND WITH ONLY A JSON OBJECT matching this schema:
11680
+ {
11681
+ "name": "string - descriptive problem name",
11682
+ "description": "string - brief description",
11683
+ "jobs": [
11684
+ {
11685
+ "id": 0,
11686
+ "name": "Job Name",
11687
+ "operations": [
11688
+ { "jobId": 0, "machineId": 0, "duration": 3 }
11689
+ ]
11690
+ }
11691
+ ],
11692
+ "machines": [
11693
+ { "id": 0, "name": "Machine Name" }
11694
+ ]
11695
+ }
11696
+
11697
+ No markdown, no explanation, no code blocks \u2014 just the raw JSON object.`;
11698
+
11699
+ // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/quantumSchedule/index.js
11700
+ function formatResult(result) {
11701
+ const lines = [
11702
+ `Solver: ${result.solverName} (${result.solverId})`,
11703
+ `Makespan: ${result.makespan}`,
11704
+ `Elapsed: ${result.elapsedMs.toFixed(1)}ms`
11705
+ ];
11706
+ if (result.iterations !== void 0) {
11707
+ lines.push(`Iterations: ${result.iterations}`);
11708
+ }
11709
+ lines.push("Schedule:");
11710
+ const byMachine = /* @__PURE__ */ new Map();
11711
+ for (const op of result.schedule) {
11712
+ if (!byMachine.has(op.machineId))
11713
+ byMachine.set(op.machineId, []);
11714
+ byMachine.get(op.machineId).push(op);
11715
+ }
11716
+ for (const [machineId, ops] of byMachine) {
11717
+ const sorted = ops.sort((a, b) => a.startTime - b.startTime);
11718
+ const timeline = sorted.map((op) => `Job${op.jobId}[${op.startTime}-${op.endTime}]`).join(" -> ");
11719
+ lines.push(` Machine ${machineId}: ${timeline}`);
11720
+ }
11721
+ return lines.join("\n");
11722
+ }
11723
+ async function runQuantumSchedule(params) {
11724
+ const parseResult = SchedulingProblemSchema.safeParse(params.problem);
11725
+ if (!parseResult.success) {
11726
+ return `Error: Invalid scheduling problem format.
11727
+ ${parseResult.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}
11728
+
11729
+ Expected format: { name, jobs: [{ id, name, operations: [{ jobId, machineId, duration }] }], machines: [{ id, name }] }`;
11730
+ }
11731
+ const problem = parseResult.data;
11732
+ let solverIds;
11733
+ if (params.solverIds && params.solverIds.length > 0) {
11734
+ const invalid = params.solverIds.filter((id) => !SolverIdSchema.safeParse(id).success);
11735
+ if (invalid.length > 0) {
11736
+ return `Error: Unknown solver IDs: ${invalid.join(", ")}.
11737
+ Available solvers: ${getAvailableSolverIds().join(", ")}`;
11738
+ }
11739
+ solverIds = params.solverIds;
11740
+ } else {
11741
+ solverIds = ["greedy"];
11742
+ }
11743
+ const timeoutMs = params.timeoutMs ?? 1e4;
11744
+ const results = [];
11745
+ const errors = [];
11746
+ for (const solverId of solverIds) {
11747
+ const solver = getSolver(solverId);
11748
+ if (!solver) {
11749
+ errors.push(`Solver "${solverId}" not found`);
11750
+ continue;
11751
+ }
11752
+ try {
11753
+ const result = await solver.solve(problem, { timeoutMs });
11754
+ results.push(result);
11755
+ } catch (err) {
11756
+ errors.push(`${solver.name}: ${err instanceof Error ? err.message : String(err)}`);
11757
+ }
11758
+ }
11759
+ if (results.length === 0) {
11760
+ return `Error: No solvers completed successfully.
11761
+ ${errors.join("\n")}`;
11762
+ }
11763
+ results.sort((a, b) => a.makespan - b.makespan);
11764
+ const lines = [];
11765
+ lines.push(`## Scheduling Results for "${problem.name}"`);
11766
+ lines.push(`Problem: ${problem.jobs.length} jobs, ${problem.machines.length} machines, ${problem.jobs.reduce((s, j) => s + j.operations.length, 0)} operations`);
11767
+ lines.push("");
11768
+ if (results.length > 1) {
11769
+ lines.push(`### Winner: ${results[0].solverName} (makespan: ${results[0].makespan})`);
11770
+ lines.push("");
11771
+ }
11772
+ for (const result of results) {
11773
+ lines.push(`### ${result.solverName}`);
11774
+ lines.push(formatResult(result));
11775
+ lines.push("");
11776
+ }
11777
+ if (errors.length > 0) {
11778
+ lines.push("### Errors");
11779
+ errors.forEach((e) => lines.push(`- ${e}`));
11780
+ }
11781
+ return lines.join("\n");
11782
+ }
11783
+ var quantumScheduleTool = {
11784
+ name: "quantum_schedule",
11785
+ implementation: () => ({
11786
+ toolFn: (value) => runQuantumSchedule(value),
11787
+ toolSchema: {
11788
+ name: "quantum_schedule",
11789
+ description: `Run quantum-inspired optimization solvers on a job-shop scheduling problem. Finds optimal or near-optimal schedules that minimize makespan (total completion time).
11790
+
11791
+ Available solvers: ${allSolvers.map((s) => `${s.id} (${s.name})`).join(", ")}.
11792
+
11793
+ The "greedy" solver runs instantly. Metaheuristic solvers (simulated-annealing, tabu, genetic-algorithm, ant-colony) explore larger solution spaces but take longer. Use multiple solvers to race them against each other.
11794
+
11795
+ The problem must define jobs (each with ordered operations) and machines. Each operation runs on a specific machine for a specific duration. Operations within a job must run in order.`,
11796
+ parameters: {
11797
+ type: "object",
11798
+ properties: {
11799
+ problem: {
11800
+ type: "object",
11801
+ description: "The scheduling problem to solve. Must include name, jobs array, and machines array.",
11802
+ properties: {
11803
+ name: { type: "string", description: "Name of the scheduling problem" },
11804
+ description: { type: "string", description: "Optional description" },
11805
+ jobs: {
11806
+ type: "array",
11807
+ description: "Array of jobs, each with id, name, and operations",
11808
+ items: {
11809
+ type: "object",
11810
+ properties: {
11811
+ id: { type: "number" },
11812
+ name: { type: "string" },
11813
+ operations: {
11814
+ type: "array",
11815
+ items: {
11816
+ type: "object",
11817
+ properties: {
11818
+ jobId: { type: "number", description: "Must match the parent job id" },
11819
+ machineId: { type: "number", description: "Which machine this operation runs on" },
11820
+ duration: { type: "number", description: "Processing time" }
11821
+ },
11822
+ required: ["jobId", "machineId", "duration"]
11823
+ }
11824
+ }
11825
+ },
11826
+ required: ["id", "name", "operations"]
11827
+ }
11828
+ },
11829
+ machines: {
11830
+ type: "array",
11831
+ description: "Array of machines with id and name",
11832
+ items: {
11833
+ type: "object",
11834
+ properties: {
11835
+ id: { type: "number" },
11836
+ name: { type: "string" }
11837
+ },
11838
+ required: ["id", "name"]
11839
+ }
11840
+ }
11841
+ },
11842
+ required: ["name", "jobs", "machines"]
11843
+ },
11844
+ solverIds: {
11845
+ type: "array",
11846
+ items: { type: "string" },
11847
+ description: `Which solvers to run. Defaults to ["greedy"]. Available: ${getAvailableSolverIds().join(", ")}. Use multiple to race solvers.`
11848
+ },
11849
+ timeoutMs: {
11850
+ type: "number",
11851
+ description: "Maximum time per solver in milliseconds (default: 10000)"
11852
+ }
11853
+ },
11854
+ required: ["problem"]
11855
+ }
11856
+ }
11857
+ })
11858
+ };
11859
+
11860
+ // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/quantumFormulate/index.js
11861
+ var quantumFormulateTool = {
11862
+ name: "quantum_formulate",
11863
+ implementation: (context) => ({
11864
+ toolFn: async (value) => {
11865
+ const params = value;
11866
+ const userDescription = params.description?.trim();
11867
+ if (!userDescription) {
11868
+ return "Error: Please provide a description of the scheduling problem to formulate.";
11869
+ }
11870
+ try {
11871
+ let rawContent = "";
11872
+ await context.llm.complete(context.model ?? "gpt-4", [
11873
+ { role: "system", content: PROBLEM_FORMULATION_PROMPT },
11874
+ { role: "user", content: userDescription }
11875
+ ], { temperature: 0.2, stream: false }, async (texts) => {
11876
+ rawContent = texts.filter((t) => t !== null && t !== void 0).join("");
11877
+ });
11878
+ let jsonStr = rawContent.trim();
11879
+ const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
11880
+ if (codeBlockMatch) {
11881
+ jsonStr = codeBlockMatch[1].trim();
11882
+ }
11883
+ let parsed;
11884
+ try {
11885
+ parsed = JSON.parse(jsonStr);
11886
+ } catch {
11887
+ return `Error: Could not parse LLM response as JSON. Raw response:
11888
+ ${rawContent}
11889
+
11890
+ Please try rephrasing your description with more specific details about jobs, machines, and processing times.`;
11891
+ }
11892
+ const validated = SchedulingProblemSchema.safeParse(parsed);
11893
+ if (!validated.success) {
11894
+ return `Error: LLM generated an invalid problem structure.
11895
+ ${validated.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}
11896
+
11897
+ Raw output:
11898
+ ${jsonStr}
11899
+
11900
+ Please try rephrasing your description.`;
11901
+ }
11902
+ const problem = validated.data;
11903
+ const totalOps = problem.jobs.reduce((s, j) => s + j.operations.length, 0);
11904
+ const lines = [
11905
+ `## Formulated: "${problem.name}"`,
11906
+ problem.description ? `
11907
+ ${problem.description}` : "",
11908
+ "",
11909
+ `**${problem.jobs.length} jobs, ${problem.machines.length} machines, ${totalOps} operations**`,
11910
+ "",
11911
+ "### Jobs:",
11912
+ ...problem.jobs.map((j) => {
11913
+ const ops = j.operations.map((op, i) => {
11914
+ const machine = problem.machines.find((m) => m.id === op.machineId);
11915
+ return `Step ${i + 1}: ${machine?.name ?? `Machine ${op.machineId}`} (${op.duration}t)`;
11916
+ });
11917
+ return `- **${j.name}**: ${ops.join(" -> ")}`;
11918
+ }),
11919
+ "",
11920
+ "### Machines:",
11921
+ ...problem.machines.map((m) => `- ${m.name} (id: ${m.id})`),
11922
+ "",
11923
+ "### Structured Problem (JSON):",
11924
+ "```json",
11925
+ JSON.stringify(problem, null, 2),
11926
+ "```",
11927
+ "",
11928
+ "You can now use the `quantum_schedule` tool to solve this problem with various optimization algorithms."
11929
+ ];
11930
+ return lines.filter((l) => l !== void 0).join("\n");
11931
+ } catch (err) {
11932
+ return `Error formulating problem: ${err instanceof Error ? err.message : String(err)}`;
11933
+ }
11934
+ },
11935
+ toolSchema: {
11936
+ name: "quantum_formulate",
11937
+ description: `Convert a natural language description of a scheduling/optimization problem into a structured SchedulingProblem that can be solved by the quantum_schedule tool.
11938
+
11939
+ Describe the problem in plain English \u2014 what jobs need to be done, what machines or resources are available, how long each step takes, and any ordering constraints. The tool will produce a structured problem definition.
11940
+
11941
+ Examples:
11942
+ - "I have 3 orders to manufacture. Each needs cutting, welding, and painting. Cutting takes 2h, welding 3h, painting 1h."
11943
+ - "Schedule 4 software builds across 2 CI runners. Build A needs compile (5min) then test (3min)..."
11944
+ - "A bakery needs to make 5 cakes. Each cake goes through mixing, baking, and decorating on separate stations."`,
11945
+ parameters: {
11946
+ type: "object",
11947
+ properties: {
11948
+ description: {
11949
+ type: "string",
11950
+ description: "Natural language description of the scheduling problem to formulate"
11951
+ }
11952
+ },
11953
+ required: ["description"]
11954
+ }
11955
+ }
11956
+ })
11957
+ };
11958
+
11959
+ // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/navigateView/index.js
11960
+ var navigateViewTool = {
11961
+ name: "navigate_view",
11962
+ implementation: (context) => ({
11963
+ toolFn: async (value) => {
11964
+ const params = value;
11965
+ const { suggestions } = params;
11966
+ context.logger.log("\u{1F9ED} navigate_view: Received suggestions:", JSON.stringify(suggestions));
11967
+ if (!suggestions || !Array.isArray(suggestions) || suggestions.length === 0) {
11968
+ return "No navigation suggestions provided.";
11969
+ }
11970
+ const capped = suggestions.slice(0, 3);
11971
+ const invalid = capped.filter((s) => !getViewById(s.viewId));
11972
+ if (invalid.length > 0) {
11973
+ context.logger.log("\u{1F9ED} navigate_view: Unknown viewIds:", invalid.map((s) => s.viewId));
11974
+ }
11975
+ const isAdmin = context.user?.isAdmin === true;
11976
+ const intents = resolveNavigationIntents(capped, isAdmin);
11977
+ if (intents.length === 0) {
11978
+ return "No valid navigation views matched the provided viewIds.";
11979
+ }
11980
+ context.logger.log("\u{1F9ED} navigate_view: Resolved intents:", intents.map((i) => i.viewId));
11981
+ return JSON.stringify({
11982
+ __navigationIntents: true,
11983
+ intents,
11984
+ message: `Suggested ${intents.length} navigation option(s): ${intents.map((i) => i.label).join(", ")}`
11985
+ });
11986
+ },
11987
+ toolSchema: {
11988
+ name: "navigate_view",
11989
+ description: "Suggest navigation to relevant app views. Returns inline action buttons the user can click. ALWAYS use this tool when your response discusses a topic that has a matching view (e.g., scheduling \u2192 opti.scheduling, user management \u2192 admin.users). Call this tool alongside your text answer \u2014 answer the question AND suggest where to go.",
11990
+ parameters: {
11991
+ type: "object",
11992
+ properties: {
11993
+ suggestions: {
11994
+ type: "array",
11995
+ description: "Navigation suggestions (1-3 items). Each suggests a view the user might want to visit.",
11996
+ items: {
11997
+ type: "object",
11998
+ properties: {
11999
+ viewId: {
12000
+ type: "string",
12001
+ description: 'The view ID from the available views list (e.g., "opti.scheduling", "admin.users")'
12002
+ },
12003
+ reason: {
12004
+ type: "string",
12005
+ description: "Brief reason why this view is relevant (max 80 chars, shown as tooltip)"
12006
+ }
12007
+ },
12008
+ required: ["viewId", "reason"]
12009
+ },
12010
+ maxItems: 3,
12011
+ minItems: 1
12012
+ }
12013
+ },
12014
+ required: ["suggestions"]
12015
+ }
12016
+ }
12017
+ })
12018
+ };
12019
+
12020
+ // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/bashExecute/index.js
12021
+ import { spawn } from "child_process";
12022
+ import path13 from "path";
12023
+ var DEFAULT_TIMEOUT_MS = 6e4;
12024
+ var MAX_OUTPUT_SIZE = 100 * 1024;
12025
+ var DANGEROUS_PATTERNS = [
12026
+ // Destructive file operations
12027
+ {
12028
+ pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|.*\s+-[a-zA-Z]*r).*\//i,
12029
+ reason: "Recursive delete with path",
12030
+ block: true
12031
+ },
12032
+ {
12033
+ pattern: /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|.*\s+-[a-zA-Z]*f).*--no-preserve-root/i,
12034
+ reason: "Force delete without preserve root",
12035
+ block: true
12036
+ },
12037
+ {
12038
+ pattern: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+\//i,
12039
+ reason: "Recursive force delete on root paths",
12040
+ block: true
12041
+ },
12042
+ {
12043
+ pattern: /\brm\s+-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*\s+\//i,
12044
+ reason: "Force recursive delete on root paths",
12045
+ block: true
12046
+ },
12047
+ // System-level dangerous commands
12048
+ { pattern: /\bsudo\b/i, reason: "Elevated privileges (sudo)", block: true },
12049
+ { pattern: /\bsu\s+(-|root)/i, reason: "Switch to root user", block: true },
12050
+ { pattern: /\bchmod\s+777\b/i, reason: "Overly permissive chmod", block: false },
12051
+ { pattern: /\bchown\s+-R\s+root/i, reason: "Recursive chown to root", block: true },
12052
+ // Disk/partition operations
12053
+ { pattern: /\bmkfs\b/i, reason: "Filesystem creation", block: true },
12054
+ { pattern: /\bfdisk\b/i, reason: "Disk partitioning", block: true },
12055
+ { pattern: /\bdd\s+.*of=\/dev\//i, reason: "Direct disk write", block: true },
12056
+ // Network attacks
12057
+ { pattern: /\b(nc|netcat)\s+.*-e\s+\/bin\/(ba)?sh/i, reason: "Reverse shell attempt", block: true },
12058
+ { pattern: /\bcurl\s+.*\|\s*(ba)?sh/i, reason: "Piping remote script to shell", block: true },
12059
+ { pattern: /\bwget\s+.*\|\s*(ba)?sh/i, reason: "Piping remote script to shell", block: true },
12060
+ // Fork bombs and resource exhaustion
12061
+ { pattern: /:\(\)\s*\{\s*:\|:&\s*\}\s*;/i, reason: "Fork bomb", block: true },
12062
+ { pattern: /\bwhile\s+true.*do.*done.*&/i, reason: "Infinite loop in background", block: false },
12063
+ // Credential/sensitive data access
12064
+ { pattern: /\/etc\/shadow/i, reason: "Access to shadow file", block: true },
12065
+ { pattern: /\/etc\/passwd.*>/i, reason: "Modifying passwd file", block: true },
12066
+ { pattern: /\baws\s+.*--profile\s+/i, reason: "AWS profile access", block: false },
12067
+ // History/log manipulation
12068
+ { pattern: /\bhistory\s+-c\b/i, reason: "Clearing shell history", block: false },
12069
+ { pattern: />\s*\/var\/log\//i, reason: "Overwriting system logs", block: true },
12070
+ // Dangerous redirects
12071
+ { pattern: />\s*\/dev\/sda/i, reason: "Writing to block device", block: true },
12072
+ { pattern: />\s*\/dev\/null.*2>&1.*</i, reason: "Potentially hiding output", block: false },
12073
+ // Environment manipulation
12074
+ { pattern: /\bexport\s+PATH\s*=\s*[^$]/i, reason: "Overwriting PATH", block: false },
12075
+ { pattern: /\bexport\s+LD_PRELOAD/i, reason: "LD_PRELOAD manipulation", block: true },
12076
+ // Process/system control
12077
+ { pattern: /\bkill\s+-9\s+(-1|1)\b/i, reason: "Killing all processes", block: true },
12078
+ { pattern: /\bkillall\s+-9\b/i, reason: "Force killing processes", block: false },
12079
+ { pattern: /\bshutdown\b/i, reason: "System shutdown", block: true },
12080
+ { pattern: /\breboot\b/i, reason: "System reboot", block: true },
12081
+ { pattern: /\binit\s+[06]\b/i, reason: "System runlevel change", block: true }
12082
+ ];
12083
+ var SAFE_COMMAND_PREFIXES = [
12084
+ "ls",
12085
+ "cat",
12086
+ "head",
12087
+ "tail",
12088
+ "grep",
12089
+ "find",
12090
+ "echo",
12091
+ "pwd",
12092
+ "whoami",
12093
+ "date",
12094
+ "cal",
12095
+ "wc",
12096
+ "sort",
12097
+ "uniq",
12098
+ "cut",
10102
12099
  "tr",
10103
12100
  "sed",
10104
12101
  "awk",
@@ -10206,7 +12203,7 @@ async function executeBashCommand(params) {
10206
12203
  };
10207
12204
  }
10208
12205
  const baseCwd = process.cwd();
10209
- const targetCwd = relativeCwd ? path12.resolve(baseCwd, relativeCwd) : baseCwd;
12206
+ const targetCwd = relativeCwd ? path13.resolve(baseCwd, relativeCwd) : baseCwd;
10210
12207
  const effectiveTimeout = Math.min(timeout, 5 * 60 * 1e3);
10211
12208
  return new Promise((resolve3) => {
10212
12209
  let stdout = "";
@@ -10269,7 +12266,7 @@ async function executeBashCommand(params) {
10269
12266
  });
10270
12267
  });
10271
12268
  }
10272
- function formatResult(result, command) {
12269
+ function formatResult2(result, command) {
10273
12270
  const parts = [];
10274
12271
  parts.push(`$ ${command}`);
10275
12272
  parts.push("");
@@ -10323,7 +12320,7 @@ var bashExecuteTool = {
10323
12320
  }
10324
12321
  try {
10325
12322
  const result = await executeBashCommand(params);
10326
- const formattedResult = formatResult(result, params.command);
12323
+ const formattedResult = formatResult2(result, params.command);
10327
12324
  context.logger.info("Bash: Command completed", {
10328
12325
  exitCode: result.exitCode,
10329
12326
  timedOut: result.timedOut,
@@ -11209,9 +13206,9 @@ function parseQuery(query) {
11209
13206
  }
11210
13207
 
11211
13208
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/editLocalFile/index.js
11212
- import { promises as fs10 } from "fs";
11213
- import { existsSync as existsSync7 } from "fs";
11214
- import path13 from "path";
13209
+ import { promises as fs11 } from "fs";
13210
+ import { existsSync as existsSync9 } from "fs";
13211
+ import path14 from "path";
11215
13212
  import { diffLines as diffLines3 } from "diff";
11216
13213
  function generateDiff(original, modified) {
11217
13214
  const differences = diffLines3(original, modified);
@@ -11235,16 +13232,16 @@ function generateDiff(original, modified) {
11235
13232
  }
11236
13233
  async function editLocalFile(params) {
11237
13234
  const { path: filePath, old_string, new_string } = params;
11238
- const normalizedPath = path13.normalize(filePath);
11239
- const resolvedPath = path13.resolve(process.cwd(), normalizedPath);
11240
- const cwd = path13.resolve(process.cwd());
13235
+ const normalizedPath = path14.normalize(filePath);
13236
+ const resolvedPath = path14.resolve(process.cwd(), normalizedPath);
13237
+ const cwd = path14.resolve(process.cwd());
11241
13238
  if (!resolvedPath.startsWith(cwd)) {
11242
13239
  throw new Error(`Access denied: Cannot edit files outside of current working directory`);
11243
13240
  }
11244
- if (!existsSync7(resolvedPath)) {
13241
+ if (!existsSync9(resolvedPath)) {
11245
13242
  throw new Error(`File not found: ${filePath}`);
11246
13243
  }
11247
- const currentContent = await fs10.readFile(resolvedPath, "utf-8");
13244
+ const currentContent = await fs11.readFile(resolvedPath, "utf-8");
11248
13245
  if (!currentContent.includes(old_string)) {
11249
13246
  const preview = old_string.length > 100 ? old_string.substring(0, 100) + "..." : old_string;
11250
13247
  throw new Error(`String to replace not found in file. Make sure the old_string matches exactly (including whitespace and line endings). Searched for: "${preview}"`);
@@ -11254,7 +13251,7 @@ async function editLocalFile(params) {
11254
13251
  throw new Error(`Found ${occurrences} occurrences of the string to replace. Please provide a more specific old_string that matches exactly one location.`);
11255
13252
  }
11256
13253
  const newContent = currentContent.replace(old_string, new_string);
11257
- await fs10.writeFile(resolvedPath, newContent, "utf-8");
13254
+ await fs11.writeFile(resolvedPath, newContent, "utf-8");
11258
13255
  const diffResult = generateDiff(old_string, new_string);
11259
13256
  return `File edited successfully: ${filePath}
11260
13257
  Changes: +${diffResult.additions} lines, -${diffResult.deletions} lines
@@ -11492,7 +13489,7 @@ async function getFileStats(files, since, filterPath) {
11492
13489
  });
11493
13490
  });
11494
13491
  }
11495
- function formatResult2(result) {
13492
+ function formatResult3(result) {
11496
13493
  const parts = [];
11497
13494
  if (result.error) {
11498
13495
  parts.push(`Error: ${result.error}`);
@@ -11567,7 +13564,7 @@ var recentChangesTool = {
11567
13564
  }
11568
13565
  try {
11569
13566
  const result = await getRecentChanges(params);
11570
- const formattedResult = formatResult2(result);
13567
+ const formattedResult = formatResult3(result);
11571
13568
  context.logger.info("RecentChanges: Retrieved file changes", {
11572
13569
  totalFiles: result.totalFiles,
11573
13570
  displayedFiles: result.files.length,
@@ -11665,8 +13662,14 @@ var b4mTools = {
11665
13662
  sunrise_sunset: sunriseSunsetTool,
11666
13663
  iss_tracker: issTrackerTool,
11667
13664
  planet_visibility: planetVisibilityTool,
11668
- // Knowledge base search
11669
- search_knowledge_base: knowledgeBaseSearchTool
13665
+ // Knowledge base tools
13666
+ search_knowledge_base: knowledgeBaseSearchTool,
13667
+ retrieve_knowledge_content: knowledgeBaseRetrieveTool,
13668
+ // Quantum optimization tools
13669
+ quantum_schedule: quantumScheduleTool,
13670
+ quantum_formulate: quantumFormulateTool,
13671
+ // Navigation tool
13672
+ navigate_view: navigateViewTool
11670
13673
  };
11671
13674
  var cliOnlyTools = {
11672
13675
  // File operation tools
@@ -11801,54 +13804,258 @@ var generateMcpTools = async (mcpData) => {
11801
13804
  return result;
11802
13805
  };
11803
13806
 
13807
+ // ../../b4m-core/packages/services/dist/src/llm/agents/ServerSubagentOrchestrator.js
13808
+ var SUBAGENT_TIMEOUT_MS = 5 * 60 * 1e3;
13809
+
13810
+ // ../../b4m-core/packages/services/dist/src/llm/agents/CodeReviewAgent.js
13811
+ var CodeReviewAgent = (config) => ({
13812
+ name: "code_review",
13813
+ description: "Code review specialist for analyzing code quality, bugs, and improvements",
13814
+ model: config?.model ?? "claude-sonnet-4-5-20250929",
13815
+ defaultThoroughness: config?.defaultThoroughness ?? "medium",
13816
+ maxIterations: { quick: 3, medium: 8, very_thorough: 15 },
13817
+ deniedTools: ["image_generation", "edit_image", "delegate_to_agent", ...config?.extraDeniedTools ?? []],
13818
+ allowedTools: config?.extraAllowedTools,
13819
+ systemPrompt: `You are a code review specialist. Your job is to analyze code for quality, correctness, security, and maintainability.
13820
+
13821
+ ## Focus Areas
13822
+ - Bugs, logic errors, and edge cases
13823
+ - Security vulnerabilities (injection, auth issues, data exposure)
13824
+ - Code quality and readability
13825
+ - Performance concerns
13826
+ - Adherence to project patterns and conventions
13827
+
13828
+ ## Review Process
13829
+ 1. Understand the context and intent of the code changes
13830
+ 2. Check for bugs, logic errors, and unhandled edge cases
13831
+ 3. Identify security vulnerabilities and data handling issues
13832
+ 4. Evaluate code clarity, naming, and structure
13833
+ 5. Look for performance problems or unnecessary complexity
13834
+
13835
+ ## Output Format
13836
+ Provide actionable feedback:
13837
+
13838
+ ### Critical Issues
13839
+ - Bugs, security vulnerabilities, or correctness problems that must be fixed
13840
+
13841
+ ### Suggestions
13842
+ - Code quality improvements with rationale
13843
+
13844
+ ### Positive Observations
13845
+ - Well-implemented patterns worth noting (optional)
13846
+
13847
+ Focus on actionable, specific feedback referencing exact code locations. Your review will be used by the main agent.`
13848
+ });
13849
+
13850
+ // ../../b4m-core/packages/services/dist/src/llm/agents/ProjectManagerAgent.js
13851
+ var ProjectManagerAgent = (config) => ({
13852
+ name: "project_manager",
13853
+ description: "Project management via Jira and Confluence (create issues, search, update status, write docs). ALWAYS delegate Jira/Confluence requests to this agent \u2014 you do not have direct access to these tools",
13854
+ model: config?.model ?? "claude-sonnet-4-5-20250929",
13855
+ defaultThoroughness: config?.defaultThoroughness ?? "medium",
13856
+ maxIterations: { quick: 3, medium: 8, very_thorough: 15 },
13857
+ allowedTools: [...config?.extraAllowedTools ?? []],
13858
+ deniedTools: [...config?.extraDeniedTools ?? []],
13859
+ exclusiveMcpServers: ["atlassian"],
13860
+ systemPrompt: `You are a project management specialist with access to Jira and Confluence. Your job is to help manage projects, issues, documentation, and team workflows.
13861
+
13862
+ ## Capabilities
13863
+
13864
+ ### Jira
13865
+ - Search for issues using JQL
13866
+ - Create, update, and transition issues
13867
+ - Add comments and manage watchers
13868
+ - List projects and issue types
13869
+
13870
+ ### Confluence
13871
+ - Search for documentation
13872
+ - Create and update pages
13873
+ - Browse spaces and page hierarchies
13874
+
13875
+ ## Best Practices
13876
+ 1. When searching Jira, use precise JQL queries (e.g., \`project = PROJ AND status = "In Progress"\`)
13877
+ 2. When creating issues, always check available issue types first with \`atlassian__jira_list_issue_types\`
13878
+ 3. When updating issue status, use \`atlassian__jira_update_issue_transition\` with the target status name
13879
+ 4. When creating Confluence pages, use the user's personal space if no space is specified
13880
+ 5. Always confirm destructive operations (delete) with the user before proceeding
13881
+
13882
+ ## Output Format
13883
+ Provide a clear summary of actions taken:
13884
+ 1. What was done (created, updated, searched, etc.)
13885
+ 2. Links or keys to relevant items (e.g., PROJ-123)
13886
+ 3. Any issues or warnings encountered
13887
+
13888
+ Be precise with issue keys and project names. Your results will be used by the main agent.`
13889
+ });
13890
+
13891
+ // ../../b4m-core/packages/services/dist/src/llm/agents/GithubManagerAgent.js
13892
+ var GithubManagerAgent = (config) => ({
13893
+ name: "github_manager",
13894
+ description: "GitHub operations (issues, pull requests, code search, branches, workflows, reviews). ALWAYS delegate GitHub requests to this agent \u2014 you do not have direct access to these tools",
13895
+ model: config?.model ?? "claude-sonnet-4-5-20250929",
13896
+ defaultThoroughness: config?.defaultThoroughness ?? "medium",
13897
+ maxIterations: { quick: 3, medium: 8, very_thorough: 15 },
13898
+ allowedTools: [...config?.extraAllowedTools ?? []],
13899
+ deniedTools: [...config?.extraDeniedTools ?? []],
13900
+ exclusiveMcpServers: ["github"],
13901
+ systemPrompt: `You are a GitHub specialist with access to GitHub's API. Your job is to help manage repositories, issues, pull requests, code search, branches, and CI/CD workflows.
13902
+
13903
+ ## First Step: ALWAYS Resolve Repository Context Automatically
13904
+ Before doing ANYTHING else, call the \`github__current_user\` tool. The response includes:
13905
+ - **selected_repositories**: The list of repositories the user has enabled for AI access (in \`owner/repo\` format)
13906
+ - **user**: The authenticated user's profile (login, name, etc.)
13907
+
13908
+ ### Repository Resolution Rules (CRITICAL)
13909
+ You MUST use the selected_repositories list to automatically resolve the owner and repo. NEVER ask the user for the org, owner, or full repository path. Instead:
13910
+
13911
+ 1. **Exact match**: If the user says a repo name that exactly matches a repo name in selected_repositories, use it immediately.
13912
+ 2. **Partial/fuzzy match**: If the user provides a partial name, match it against any repo in selected_repositories whose name contains that term (e.g., if the user says "lumina" and selected_repositories contains \`SomeOrg/lumina5\`, use that).
13913
+ 3. **Single repo shortcut**: If only one repository is selected, ALWAYS default to it \u2014 no questions asked.
13914
+ 4. **Multiple matches**: Only if multiple selected repos match the user's term AND you truly cannot disambiguate, list the matching repos and ask which one. Do NOT list repos that don't match.
13915
+ 5. **No match**: If nothing in selected_repositories matches, tell the user which repos are available and ask them to clarify.
13916
+
13917
+ **NEVER ask the user to "paste the GitHub URL" or provide the org/owner name.** You already have this information from \`github__current_user\`. The owner and repo are embedded in the \`owner/repo\` format of each selected repository entry.
13918
+
13919
+ ### Defaults for Ambiguous Requests
13920
+ - **Timezone**: Default to UTC unless the user specifies otherwise.
13921
+ - **PR/Issue state**: Default to \`open\` unless the user specifies otherwise.
13922
+ - **Scope**: All matching repos unless the user narrows it down.
13923
+ - **When in doubt, act**: Prefer making reasonable assumptions and proceeding over asking clarifying questions. You can always note your assumptions in the response.
13924
+
13925
+ ## Capabilities
13926
+
13927
+ ### Issues
13928
+ - Create, update, and search issues
13929
+ - Add comments and manage labels
13930
+ - List and filter issues by state, assignee, labels
13931
+
13932
+ ### Pull Requests
13933
+ - Create, update, and list pull requests
13934
+ - Get PR diffs, files changed, and review status
13935
+ - Merge pull requests and manage reviews
13936
+ - Request reviews from team members
13937
+
13938
+ ### Code & Repository
13939
+ - Search code across repositories
13940
+ - Get file contents and commit history
13941
+ - Create and list branches and tags
13942
+ - Fork repositories
13943
+
13944
+ ### CI/CD & Workflows
13945
+ - List and monitor workflow runs
13946
+ - Get job logs for debugging failures
13947
+ - Re-run failed jobs or entire workflows
13948
+ - Download workflow artifacts
13949
+
13950
+ ### Notifications
13951
+ - List and manage GitHub notifications
13952
+ - Mark notifications as read/done
13953
+
13954
+ ## Best Practices
13955
+ 1. When searching issues or PRs, use GitHub search syntax (e.g., \`is:open label:bug assignee:username\`)
13956
+ 2. When creating PRs, always include a clear title and description
13957
+ 3. When reviewing PR changes, use \`get_pull_request_diff\` or \`get_pull_request_files\` for context
13958
+ 4. For CI/CD debugging, use \`get_job_logs\` with \`failed_only=true\` to focus on failures
13959
+ 5. Always include links to issues/PRs in your responses (e.g., owner/repo#123)
13960
+
13961
+ ## Output Format
13962
+ Provide a clear summary of actions taken:
13963
+ 1. What was done (created, searched, merged, etc.)
13964
+ 2. Always provide links to relevant items (e.g., owner/repo#123, PR URLs)
13965
+ 3. Any issues or warnings encountered
13966
+
13967
+ Be precise with repository names, issue numbers, and PR numbers. Your results will be used by the main agent.`
13968
+ });
13969
+
13970
+ // ../../b4m-core/packages/services/dist/src/llm/agents/ServerAgentStore.js
13971
+ var ServerAgentStore = class {
13972
+ constructor() {
13973
+ const builtInAgents = [
13974
+ // For now disable explore and plan agents as it is much faster if the parent llm handles this.
13975
+ // ExploreAgent(),
13976
+ // PlanAgent(),
13977
+ CodeReviewAgent(),
13978
+ ProjectManagerAgent(),
13979
+ GithubManagerAgent()
13980
+ ];
13981
+ this.agents = new Map(builtInAgents.map((a) => [a.name, a]));
13982
+ }
13983
+ getAgent(name) {
13984
+ return this.agents.get(name);
13985
+ }
13986
+ getAllAgents() {
13987
+ return Array.from(this.agents.values());
13988
+ }
13989
+ getAgentNames() {
13990
+ return Array.from(this.agents.keys());
13991
+ }
13992
+ hasAgent(name) {
13993
+ return this.agents.has(name);
13994
+ }
13995
+ /**
13996
+ * Get the deduplicated list of MCP server names that are exclusive to agents.
13997
+ * Derived from each agent's `exclusiveMcpServers` field.
13998
+ */
13999
+ getExclusiveMcpServers() {
14000
+ const servers = /* @__PURE__ */ new Set();
14001
+ for (const agent of this.agents.values()) {
14002
+ if (agent.exclusiveMcpServers) {
14003
+ for (const s of agent.exclusiveMcpServers) {
14004
+ servers.add(s);
14005
+ }
14006
+ }
14007
+ }
14008
+ return Array.from(servers);
14009
+ }
14010
+ };
14011
+ var serverAgentStore = new ServerAgentStore();
14012
+
11804
14013
  // ../../b4m-core/packages/services/dist/src/llm/ChatCompletionProcess.js
11805
14014
  import throttle2 from "lodash/throttle.js";
11806
14015
 
11807
14016
  // ../../b4m-core/packages/services/dist/src/llm/ChatCompletionFeatures.js
11808
- import { z as z139 } from "zod";
14017
+ import { z as z140 } from "zod";
11809
14018
  import uniq4 from "lodash/uniq.js";
11810
- var QuestStartBodySchema = z139.object({
11811
- userId: z139.string(),
11812
- sessionId: z139.string(),
11813
- questId: z139.string(),
11814
- message: z139.string(),
11815
- messageFileIds: z139.array(z139.string()),
11816
- historyCount: z139.number(),
11817
- fabFileIds: z139.array(z139.string()),
14019
+ var QuestStartBodySchema = z140.object({
14020
+ userId: z140.string(),
14021
+ sessionId: z140.string(),
14022
+ questId: z140.string(),
14023
+ message: z140.string().min(1, "Message cannot be empty"),
14024
+ messageFileIds: z140.array(z140.string()),
14025
+ historyCount: z140.number(),
14026
+ fabFileIds: z140.array(z140.string()),
11818
14027
  params: ChatCompletionCreateInputSchema,
11819
14028
  dashboardParams: DashboardParamsSchema.optional(),
11820
- enableQuestMaster: z139.boolean().optional(),
11821
- enableMementos: z139.boolean().optional(),
11822
- enableArtifacts: z139.boolean().optional(),
11823
- enableAgents: z139.boolean().optional(),
11824
- enableLattice: z139.boolean().optional(),
14029
+ enableQuestMaster: z140.boolean().optional(),
14030
+ enableMementos: z140.boolean().optional(),
14031
+ enableArtifacts: z140.boolean().optional(),
14032
+ enableAgents: z140.boolean().optional(),
14033
+ enableLattice: z140.boolean().optional(),
11825
14034
  promptMeta: PromptMetaZodSchema,
11826
- tools: z139.array(z139.union([b4mLLMTools, z139.string()])).optional(),
11827
- mcpServers: z139.array(z139.string()).optional(),
11828
- projectId: z139.string().optional(),
11829
- organizationId: z139.string().nullable().optional(),
14035
+ tools: z140.array(z140.union([b4mLLMTools, z140.string()])).optional(),
14036
+ mcpServers: z140.array(z140.string()).optional(),
14037
+ projectId: z140.string().optional(),
14038
+ organizationId: z140.string().nullable().optional(),
11830
14039
  questMaster: QuestMasterParamsSchema.optional(),
11831
- toolPromptId: z139.string().optional(),
14040
+ toolPromptId: z140.string().optional(),
11832
14041
  researchMode: ResearchModeParamsSchema.optional(),
11833
- fallbackModel: z139.string().optional(),
11834
- embeddingModel: z139.string().optional(),
11835
- queryComplexity: z139.string(),
14042
+ fallbackModel: z140.string().optional(),
14043
+ embeddingModel: z140.string().optional(),
14044
+ queryComplexity: z140.string(),
11836
14045
  imageConfig: GenerateImageToolCallSchema.optional(),
11837
- deepResearchConfig: z139.object({
11838
- maxDepth: z139.number().optional(),
11839
- duration: z139.number().optional(),
14046
+ deepResearchConfig: z140.object({
14047
+ maxDepth: z140.number().optional(),
14048
+ duration: z140.number().optional(),
11840
14049
  // Note: searchers are passed via ToolContext and not through this API schema
11841
- searchers: z139.array(z139.any()).optional()
14050
+ searchers: z140.array(z140.any()).optional()
11842
14051
  }).optional(),
11843
- extraContextMessages: z139.array(z139.object({
11844
- role: z139.enum(["user", "assistant", "system", "function", "tool"]),
11845
- content: z139.union([z139.string(), z139.array(z139.any())]),
11846
- fabFileIds: z139.array(z139.string()).optional()
14052
+ extraContextMessages: z140.array(z140.object({
14053
+ role: z140.enum(["user", "assistant", "system", "function", "tool"]),
14054
+ content: z140.union([z140.string(), z140.array(z140.any())]),
14055
+ fabFileIds: z140.array(z140.string()).optional()
11847
14056
  })).optional(),
11848
14057
  /** User's timezone (IANA format, e.g., "America/New_York") */
11849
- timezone: z139.string().optional(),
11850
- /** Pre-fetched API key table from invoke phase — avoids redundant DB call in process (#6616 P1-a) */
11851
- apiKeyTable: z139.record(z139.string(), z139.string().nullable()).optional()
14058
+ timezone: z140.string().optional()
11852
14059
  });
11853
14060
 
11854
14061
  // ../../b4m-core/packages/services/dist/src/llm/StatusManager.js
@@ -12348,89 +14555,89 @@ var BUILT_IN_TOOL_SET = new Set(b4mLLMTools.options);
12348
14555
  import axios8 from "axios";
12349
14556
  import { fileTypeFromBuffer as fileTypeFromBuffer4 } from "file-type";
12350
14557
  import { v4 as uuidv47 } from "uuid";
12351
- import { z as z140 } from "zod";
14558
+ import { z as z141 } from "zod";
12352
14559
  import { fromZodError as fromZodError2 } from "zod-validation-error";
12353
14560
  var ImageGenerationBodySchema = OpenAIImageGenerationInput.extend({
12354
- sessionId: z140.string(),
12355
- questId: z140.string(),
12356
- userId: z140.string(),
12357
- prompt: z140.string(),
12358
- organizationId: z140.string().nullable().optional(),
12359
- safety_tolerance: z140.number().min(BFL_SAFETY_TOLERANCE.MIN).max(BFL_SAFETY_TOLERANCE.MAX).optional().default(BFL_SAFETY_TOLERANCE.DEFAULT),
12360
- prompt_upsampling: z140.boolean().optional().default(false),
12361
- seed: z140.number().nullable().optional(),
12362
- output_format: z140.enum(["jpeg", "png"]).nullable().optional().default("png"),
12363
- width: z140.number().optional(),
12364
- height: z140.number().optional(),
12365
- aspect_ratio: z140.string().optional(),
12366
- fabFileIds: z140.array(z140.string()).optional()
14561
+ sessionId: z141.string(),
14562
+ questId: z141.string(),
14563
+ userId: z141.string(),
14564
+ prompt: z141.string(),
14565
+ organizationId: z141.string().nullable().optional(),
14566
+ safety_tolerance: z141.number().min(BFL_SAFETY_TOLERANCE.MIN).max(BFL_SAFETY_TOLERANCE.MAX).optional().default(BFL_SAFETY_TOLERANCE.DEFAULT),
14567
+ prompt_upsampling: z141.boolean().optional().default(false),
14568
+ seed: z141.number().nullable().optional(),
14569
+ output_format: z141.enum(["jpeg", "png"]).nullable().optional().default("png"),
14570
+ width: z141.number().optional(),
14571
+ height: z141.number().optional(),
14572
+ aspect_ratio: z141.string().optional(),
14573
+ fabFileIds: z141.array(z141.string()).optional()
12367
14574
  });
12368
14575
 
12369
14576
  // ../../b4m-core/packages/services/dist/src/llm/VideoGeneration.js
12370
14577
  import axios9 from "axios";
12371
14578
  import { v4 as uuidv48 } from "uuid";
12372
- import { z as z141 } from "zod";
14579
+ import { z as z142 } from "zod";
12373
14580
  import { fromZodError as fromZodError3 } from "zod-validation-error";
12374
- var VideoGenerationBodySchema = z141.object({
12375
- sessionId: z141.string(),
12376
- questId: z141.string(),
12377
- userId: z141.string(),
12378
- prompt: z141.string(),
12379
- model: z141.nativeEnum(VideoModels).default(VideoModels.SORA_2),
12380
- seconds: z141.union([z141.literal(4), z141.literal(8), z141.literal(12)]).default(4),
12381
- size: z141.enum(["720x1280", "1280x720", "1024x1792", "1792x1024"]).default(VIDEO_SIZE_CONSTRAINTS.SORA.defaultSize),
12382
- organizationId: z141.string().nullable().optional()
14581
+ var VideoGenerationBodySchema = z142.object({
14582
+ sessionId: z142.string(),
14583
+ questId: z142.string(),
14584
+ userId: z142.string(),
14585
+ prompt: z142.string(),
14586
+ model: z142.nativeEnum(VideoModels).default(VideoModels.SORA_2),
14587
+ seconds: z142.union([z142.literal(4), z142.literal(8), z142.literal(12)]).default(4),
14588
+ size: z142.enum(["720x1280", "1280x720", "1024x1792", "1792x1024"]).default(VIDEO_SIZE_CONSTRAINTS.SORA.defaultSize),
14589
+ organizationId: z142.string().nullable().optional()
12383
14590
  });
12384
14591
 
12385
14592
  // ../../b4m-core/packages/services/dist/src/llm/ImageEdit.js
12386
14593
  import axios10 from "axios";
12387
14594
  import { fileTypeFromBuffer as fileTypeFromBuffer5 } from "file-type";
12388
14595
  import { v4 as uuidv49 } from "uuid";
12389
- import { z as z142 } from "zod";
14596
+ import { z as z143 } from "zod";
12390
14597
  import { fromZodError as fromZodError4 } from "zod-validation-error";
12391
- var ImageEditBodySchema = OpenAIImageGenerationInput.extend({
12392
- sessionId: z142.string(),
12393
- questId: z142.string(),
12394
- userId: z142.string(),
12395
- prompt: z142.string(),
12396
- organizationId: z142.string().nullable().optional(),
12397
- safety_tolerance: z142.number().min(BFL_SAFETY_TOLERANCE.MIN).max(BFL_SAFETY_TOLERANCE.MAX).optional().default(BFL_SAFETY_TOLERANCE.DEFAULT),
12398
- prompt_upsampling: z142.boolean().optional().default(false),
12399
- seed: z142.number().nullable().optional(),
12400
- output_format: z142.enum(["jpeg", "png"]).optional().default("png"),
12401
- width: z142.number().optional(),
12402
- height: z142.number().optional(),
12403
- aspect_ratio: z142.string().optional(),
12404
- size: z142.string().optional(),
12405
- fabFileIds: z142.array(z142.string()).optional(),
12406
- image: z142.string()
14598
+ var ImageEditBodySchema = OpenAIImageGenerationInput.extend({
14599
+ sessionId: z143.string(),
14600
+ questId: z143.string(),
14601
+ userId: z143.string(),
14602
+ prompt: z143.string(),
14603
+ organizationId: z143.string().nullable().optional(),
14604
+ safety_tolerance: z143.number().min(BFL_SAFETY_TOLERANCE.MIN).max(BFL_SAFETY_TOLERANCE.MAX).optional().default(BFL_SAFETY_TOLERANCE.DEFAULT),
14605
+ prompt_upsampling: z143.boolean().optional().default(false),
14606
+ seed: z143.number().nullable().optional(),
14607
+ output_format: z143.enum(["jpeg", "png"]).optional().default("png"),
14608
+ width: z143.number().optional(),
14609
+ height: z143.number().optional(),
14610
+ aspect_ratio: z143.string().optional(),
14611
+ size: z143.string().optional(),
14612
+ fabFileIds: z143.array(z143.string()).optional(),
14613
+ image: z143.string()
12407
14614
  });
12408
14615
 
12409
14616
  // ../../b4m-core/packages/services/dist/src/llm/refineText.js
12410
- import { z as z143 } from "zod";
12411
- var refineTextLLMSchema = z143.object({
12412
- text: z143.string(),
12413
- context: z143.string().optional()
14617
+ import { z as z144 } from "zod";
14618
+ var refineTextLLMSchema = z144.object({
14619
+ text: z144.string(),
14620
+ context: z144.string().optional()
12414
14621
  // tone: z.enum(['formal', 'informal', 'neutral']).optional(),
12415
14622
  // style: z.enum(['technical', 'creative', 'academic', 'business', 'narrative']).optional(),
12416
14623
  });
12417
14624
 
12418
14625
  // ../../b4m-core/packages/services/dist/src/llm/MementoEvaluationService.js
12419
- import { z as z144 } from "zod";
12420
- var SingleMementoEvalSchema = z144.object({
12421
- importance: z144.number().min(1).max(10),
14626
+ import { z as z145 } from "zod";
14627
+ var SingleMementoEvalSchema = z145.object({
14628
+ importance: z145.number().min(1).max(10),
12422
14629
  // 1-10 scale for personal info importance
12423
- summary: z144.string(),
12424
- tags: z144.array(z144.string()).optional()
14630
+ summary: z145.string(),
14631
+ tags: z145.array(z145.string()).optional()
12425
14632
  });
12426
- var MementoEvalResponseSchema = z144.object({
12427
- isPersonal: z144.boolean(),
12428
- mementos: z144.array(SingleMementoEvalSchema).optional()
14633
+ var MementoEvalResponseSchema = z145.object({
14634
+ isPersonal: z145.boolean(),
14635
+ mementos: z145.array(SingleMementoEvalSchema).optional()
12429
14636
  // Array of distinct personal information
12430
14637
  });
12431
14638
 
12432
14639
  // ../../b4m-core/packages/services/dist/src/llm/SmallLLMService.js
12433
- import { z as z145 } from "zod";
14640
+ import { z as z146 } from "zod";
12434
14641
 
12435
14642
  // ../../b4m-core/packages/services/dist/src/auth/AccessTokenGeneratorService.js
12436
14643
  import jwt2 from "jsonwebtoken";
@@ -12449,10 +14656,10 @@ var ToolErrorType;
12449
14656
  // src/utils/diffPreview.ts
12450
14657
  import * as Diff from "diff";
12451
14658
  import { readFile } from "fs/promises";
12452
- import { existsSync as existsSync8 } from "fs";
14659
+ import { existsSync as existsSync10 } from "fs";
12453
14660
  async function generateFileDiffPreview(args) {
12454
14661
  try {
12455
- if (!existsSync8(args.path)) {
14662
+ if (!existsSync10(args.path)) {
12456
14663
  const lines2 = args.content.split("\n");
12457
14664
  const preview = lines2.slice(0, 20).join("\n");
12458
14665
  const hasMore = lines2.length > 20;
@@ -12490,10 +14697,10 @@ ${diffLines4.join("\n")}`;
12490
14697
  }
12491
14698
  async function generateFileDeletePreview(args) {
12492
14699
  try {
12493
- if (!existsSync8(args.path)) {
14700
+ if (!existsSync10(args.path)) {
12494
14701
  return `[File does not exist: ${args.path}]`;
12495
14702
  }
12496
- const stats = await import("fs/promises").then((fs14) => fs14.stat(args.path));
14703
+ const stats = await import("fs/promises").then((fs15) => fs15.stat(args.path));
12497
14704
  return `[File will be deleted]
12498
14705
 
12499
14706
  Path: ${args.path}
@@ -12546,6 +14753,10 @@ var ServerToolExecutor = class {
12546
14753
  };
12547
14754
 
12548
14755
  // src/llm/ToolRouter.ts
14756
+ var wsToolExecutor = null;
14757
+ function setWebSocketToolExecutor(executor) {
14758
+ wsToolExecutor = executor;
14759
+ }
12549
14760
  var SERVER_TOOLS = ["weather_info", "web_search", "web_fetch"];
12550
14761
  var LOCAL_TOOLS = [
12551
14762
  "file_read",
@@ -12568,7 +14779,15 @@ function isLocalTool(toolName) {
12568
14779
  }
12569
14780
  async function executeTool(toolName, input, apiClient, localToolFn) {
12570
14781
  if (isServerTool(toolName)) {
12571
- logger.debug(`[ToolRouter] Routing ${toolName} to server`);
14782
+ if (wsToolExecutor) {
14783
+ logger.debug(`[ToolRouter] Routing ${toolName} to server via WebSocket`);
14784
+ const result = await wsToolExecutor.execute(toolName, input);
14785
+ if (!result.success) {
14786
+ return `Error executing ${toolName}: ${result.error || "Tool execution failed"}`;
14787
+ }
14788
+ return typeof result.content === "string" ? result.content : JSON.stringify(result.content ?? "");
14789
+ }
14790
+ logger.debug(`[ToolRouter] Routing ${toolName} to server via HTTP`);
12572
14791
  const executor = new ServerToolExecutor(apiClient);
12573
14792
  return await executor.executeTool(toolName, input);
12574
14793
  } else if (isLocalTool(toolName)) {
@@ -12666,7 +14885,7 @@ async function executeCommandHook(hook, context) {
12666
14885
  }
12667
14886
  }
12668
14887
  var MAX_PATTERN_LENGTH = 200;
12669
- function matchesToolPattern(toolName, pattern) {
14888
+ function matchesToolPattern2(toolName, pattern) {
12670
14889
  if (pattern.length > MAX_PATTERN_LENGTH) {
12671
14890
  console.warn(`Hook pattern exceeds max length (${MAX_PATTERN_LENGTH}), skipping: ${pattern.slice(0, 50)}...`);
12672
14891
  return false;
@@ -12684,7 +14903,7 @@ async function executeHooks(hooks, context) {
12684
14903
  }
12685
14904
  const matchingHooks = [];
12686
14905
  for (const matcher of hooks) {
12687
- const shouldMatch = !matcher.matcher || !context.tool_name || matchesToolPattern(context.tool_name, matcher.matcher);
14906
+ const shouldMatch = !matcher.matcher || !context.tool_name || matchesToolPattern2(context.tool_name, matcher.matcher);
12688
14907
  if (shouldMatch) {
12689
14908
  matchingHooks.push(...matcher.hooks);
12690
14909
  }
@@ -12723,7 +14942,7 @@ function buildHookContext(params) {
12723
14942
  }
12724
14943
 
12725
14944
  // src/agents/types.ts
12726
- import { z as z146 } from "zod";
14945
+ import { z as z147 } from "zod";
12727
14946
  var HookBlockedError = class extends Error {
12728
14947
  constructor(toolName, reason) {
12729
14948
  super(`Hook blocked execution of ${toolName}: ${reason || "No reason provided"}`);
@@ -12735,40 +14954,40 @@ var ALWAYS_DENIED_FOR_AGENTS = [
12735
14954
  "agent_delegate"
12736
14955
  // No agent chaining
12737
14956
  ];
12738
- var CommandHookSchema = z146.object({
12739
- type: z146.literal("command"),
12740
- command: z146.string().min(1, "Command is required for command hooks"),
12741
- timeout: z146.number().optional()
14957
+ var CommandHookSchema = z147.object({
14958
+ type: z147.literal("command"),
14959
+ command: z147.string().min(1, "Command is required for command hooks"),
14960
+ timeout: z147.number().optional()
12742
14961
  });
12743
- var PromptHookSchema = z146.object({
12744
- type: z146.literal("prompt"),
12745
- prompt: z146.string().min(1, "Prompt is required for prompt hooks"),
12746
- timeout: z146.number().optional()
14962
+ var PromptHookSchema = z147.object({
14963
+ type: z147.literal("prompt"),
14964
+ prompt: z147.string().min(1, "Prompt is required for prompt hooks"),
14965
+ timeout: z147.number().optional()
12747
14966
  });
12748
- var HookDefinitionSchema = z146.discriminatedUnion("type", [CommandHookSchema, PromptHookSchema]);
12749
- var HookMatcherSchema = z146.object({
12750
- matcher: z146.string().optional(),
12751
- hooks: z146.array(HookDefinitionSchema)
14967
+ var HookDefinitionSchema = z147.discriminatedUnion("type", [CommandHookSchema, PromptHookSchema]);
14968
+ var HookMatcherSchema = z147.object({
14969
+ matcher: z147.string().optional(),
14970
+ hooks: z147.array(HookDefinitionSchema)
12752
14971
  });
12753
- var AgentHooksSchema = z146.object({
12754
- PreToolUse: z146.array(HookMatcherSchema).optional(),
12755
- PostToolUse: z146.array(HookMatcherSchema).optional(),
12756
- PostToolUseFailure: z146.array(HookMatcherSchema).optional(),
12757
- Stop: z146.array(HookMatcherSchema).optional()
14972
+ var AgentHooksSchema = z147.object({
14973
+ PreToolUse: z147.array(HookMatcherSchema).optional(),
14974
+ PostToolUse: z147.array(HookMatcherSchema).optional(),
14975
+ PostToolUseFailure: z147.array(HookMatcherSchema).optional(),
14976
+ Stop: z147.array(HookMatcherSchema).optional()
12758
14977
  }).optional();
12759
- var AgentFrontmatterSchema = z146.object({
12760
- description: z146.string().min(1, "Agent description is required"),
12761
- model: z146.string().optional(),
12762
- "allowed-tools": z146.array(z146.string()).optional(),
12763
- "denied-tools": z146.array(z146.string()).optional(),
12764
- skills: z146.array(z146.string()).optional(),
12765
- "max-iterations": z146.object({
12766
- quick: z146.number().int().positive().optional(),
12767
- medium: z146.number().int().positive().optional(),
12768
- very_thorough: z146.number().int().positive().optional()
14978
+ var AgentFrontmatterSchema = z147.object({
14979
+ description: z147.string().min(1, "Agent description is required"),
14980
+ model: z147.string().optional(),
14981
+ "allowed-tools": z147.array(z147.string()).optional(),
14982
+ "denied-tools": z147.array(z147.string()).optional(),
14983
+ skills: z147.array(z147.string()).optional(),
14984
+ "max-iterations": z147.object({
14985
+ quick: z147.number().int().positive().optional(),
14986
+ medium: z147.number().int().positive().optional(),
14987
+ very_thorough: z147.number().int().positive().optional()
12769
14988
  }).optional(),
12770
- "default-thoroughness": z146.enum(["quick", "medium", "very_thorough"]).optional(),
12771
- variables: z146.record(z146.string()).optional(),
14989
+ "default-thoroughness": z147.enum(["quick", "medium", "very_thorough"]).optional(),
14990
+ variables: z147.record(z147.string()).optional(),
12772
14991
  hooks: AgentHooksSchema
12773
14992
  });
12774
14993
  var DEFAULT_MAX_ITERATIONS = {
@@ -12780,25 +14999,26 @@ var DEFAULT_AGENT_MODEL = "claude-3-5-haiku-20241022";
12780
14999
  var DEFAULT_THOROUGHNESS = "medium";
12781
15000
 
12782
15001
  // src/utils/toolsAdapter.ts
15002
+ import path15 from "path";
12783
15003
  var NoOpStorage = class extends BaseStorage {
12784
15004
  async upload(input, destination, options) {
12785
15005
  return `/tmp/${destination}`;
12786
15006
  }
12787
- async download(path19) {
15007
+ async download(path21) {
12788
15008
  throw new Error("Download not supported in CLI");
12789
15009
  }
12790
- async delete(path19) {
15010
+ async delete(path21) {
12791
15011
  }
12792
- async getSignedUrl(path19) {
12793
- return `/tmp/${path19}`;
15012
+ async getSignedUrl(path21) {
15013
+ return `/tmp/${path21}`;
12794
15014
  }
12795
- getPublicUrl(path19) {
12796
- return `/tmp/${path19}`;
15015
+ getPublicUrl(path21) {
15016
+ return `/tmp/${path21}`;
12797
15017
  }
12798
- async getPreview(path19) {
12799
- return `/tmp/${path19}`;
15018
+ async getPreview(path21) {
15019
+ return `/tmp/${path21}`;
12800
15020
  }
12801
- async getMetadata(path19) {
15021
+ async getMetadata(path21) {
12802
15022
  return { size: 0, contentType: "application/octet-stream" };
12803
15023
  }
12804
15024
  };
@@ -12971,6 +15191,27 @@ function wrapToolWithHooks(tool, hooks, hookContext) {
12971
15191
  }
12972
15192
  };
12973
15193
  }
15194
+ var CHECKPOINT_TOOLS = /* @__PURE__ */ new Set(["create_file", "edit_local_file", "delete_file"]);
15195
+ function wrapToolWithCheckpointing(tool, checkpointStore) {
15196
+ if (!checkpointStore || !CHECKPOINT_TOOLS.has(tool.toolSchema.name)) {
15197
+ return tool;
15198
+ }
15199
+ const originalFn = tool.toolFn;
15200
+ const toolName = tool.toolSchema.name;
15201
+ return {
15202
+ ...tool,
15203
+ toolFn: async (args) => {
15204
+ const filePath = args?.path;
15205
+ if (filePath) {
15206
+ try {
15207
+ await checkpointStore.createCheckpoint(toolName, [filePath], `before-${toolName}-${path15.basename(filePath)}`);
15208
+ } catch {
15209
+ }
15210
+ }
15211
+ return originalFn(args);
15212
+ }
15213
+ };
15214
+ }
12974
15215
  var TOOL_NAME_MAPPING = {
12975
15216
  // Claude Code -> B4M
12976
15217
  read: "file_read",
@@ -12995,7 +15236,7 @@ function normalizeToolName(toolName) {
12995
15236
  }
12996
15237
  return TOOL_NAME_MAPPING[toolName] || toolName;
12997
15238
  }
12998
- function generateCliTools(userId, llm, model, permissionManager, showPermissionPrompt, agentContext, configStore, apiClient, toolFilter) {
15239
+ function generateCliTools(userId, llm, model, permissionManager, showPermissionPrompt, agentContext, configStore, apiClient, toolFilter, checkpointStore) {
12999
15240
  const logger2 = new CliLogger();
13000
15241
  const storage = new NoOpStorage();
13001
15242
  const user = {
@@ -13054,9 +15295,17 @@ function generateCliTools(userId, llm, model, permissionManager, showPermissionP
13054
15295
  // imageProcessorLambdaName (not needed for CLI)
13055
15296
  tools_to_generate
13056
15297
  );
13057
- let tools = Object.entries(toolsMap).map(
13058
- ([_, tool]) => wrapToolWithPermission(tool, permissionManager, showPermissionPrompt, agentContext, configStore, apiClient)
13059
- );
15298
+ let tools = Object.entries(toolsMap).map(([_, tool]) => {
15299
+ const permissionWrapped = wrapToolWithPermission(
15300
+ tool,
15301
+ permissionManager,
15302
+ showPermissionPrompt,
15303
+ agentContext,
15304
+ configStore,
15305
+ apiClient
15306
+ );
15307
+ return wrapToolWithCheckpointing(permissionWrapped, checkpointStore ?? null);
15308
+ });
13060
15309
  if (toolFilter) {
13061
15310
  const { allowedTools, deniedTools } = toolFilter;
13062
15311
  const normalizedAllowed = allowedTools?.map(normalizeToolName);
@@ -13234,8 +15483,8 @@ function getEnvironmentName(configApiConfig) {
13234
15483
  }
13235
15484
 
13236
15485
  // src/utils/contextLoader.ts
13237
- import * as fs11 from "fs";
13238
- import * as path14 from "path";
15486
+ import * as fs12 from "fs";
15487
+ import * as path16 from "path";
13239
15488
  import { homedir as homedir3 } from "os";
13240
15489
  var CONTEXT_FILE_SIZE_LIMIT = 100 * 1024;
13241
15490
  var PROJECT_CONTEXT_FILES = [
@@ -13256,9 +15505,9 @@ function formatFileSize2(bytes) {
13256
15505
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
13257
15506
  }
13258
15507
  function tryReadContextFile(dir, filename, source) {
13259
- const filePath = path14.join(dir, filename);
15508
+ const filePath = path16.join(dir, filename);
13260
15509
  try {
13261
- const stats = fs11.lstatSync(filePath);
15510
+ const stats = fs12.lstatSync(filePath);
13262
15511
  if (stats.isDirectory()) {
13263
15512
  return null;
13264
15513
  }
@@ -13272,7 +15521,7 @@ function tryReadContextFile(dir, filename, source) {
13272
15521
  error: `${source === "global" ? "Global" : "Project"} ${filename} exceeds 100KB limit (${formatFileSize2(stats.size)})`
13273
15522
  };
13274
15523
  }
13275
- const content = fs11.readFileSync(filePath, "utf-8");
15524
+ const content = fs12.readFileSync(filePath, "utf-8");
13276
15525
  return {
13277
15526
  filename,
13278
15527
  content,
@@ -13324,7 +15573,7 @@ ${project.content}`;
13324
15573
  }
13325
15574
  async function loadContextFiles(projectDir) {
13326
15575
  const errors = [];
13327
- const globalDir = path14.join(homedir3(), ".bike4mind");
15576
+ const globalDir = path16.join(homedir3(), ".bike4mind");
13328
15577
  const projectDirectory = projectDir || process.cwd();
13329
15578
  const [globalResult, projectResult] = await Promise.all([
13330
15579
  Promise.resolve(findContextFile(globalDir, GLOBAL_CONTEXT_FILES, "global")),
@@ -13595,8 +15844,8 @@ function substituteArguments(template, args) {
13595
15844
  // ../../b4m-core/packages/mcp/dist/src/client.js
13596
15845
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
13597
15846
  import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
13598
- import path15 from "path";
13599
- import { existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
15847
+ import path17 from "path";
15848
+ import { existsSync as existsSync11, readdirSync as readdirSync3 } from "fs";
13600
15849
  var MCPClient = class {
13601
15850
  // Note: This class handles MCP server communication with repository filtering
13602
15851
  mcp;
@@ -13637,18 +15886,18 @@ var MCPClient = class {
13637
15886
  const root = process.env.INIT_CWD || process.cwd();
13638
15887
  const candidatePaths = [
13639
15888
  // When running from SST Lambda with node_modules structure (copyFiles)
13640
- path15.join(root, `node_modules/@bike4mind/mcp/dist/src/${this.serverName}/index.js`),
15889
+ path17.join(root, `node_modules/@bike4mind/mcp/dist/src/${this.serverName}/index.js`),
13641
15890
  // When running from SST Lambda deployed environment (/var/task)
13642
- path15.join(root, `b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
15891
+ path17.join(root, `b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
13643
15892
  // When running from SST Lambda (.sst/artifacts/mcpHandler-dev), navigate to monorepo root (3 levels up)
13644
- path15.join(root, `../../../b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
15893
+ path17.join(root, `../../../b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
13645
15894
  // When running from packages/client (Next.js app), navigate to monorepo root (2 levels up)
13646
- path15.join(root, `../../b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
15895
+ path17.join(root, `../../b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
13647
15896
  // Original paths (backward compatibility)
13648
- path15.join(root, `/b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
13649
- path15.join(root, "core", "mcp", "servers", this.serverName, "dist", "index.js")
15897
+ path17.join(root, `/b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
15898
+ path17.join(root, "core", "mcp", "servers", this.serverName, "dist", "index.js")
13650
15899
  ];
13651
- const serverScriptPath = candidatePaths.find((p) => existsSync9(p));
15900
+ const serverScriptPath = candidatePaths.find((p) => existsSync11(p));
13652
15901
  if (!serverScriptPath) {
13653
15902
  const getDirectories = (source) => readdirSync3(source, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
13654
15903
  console.error(`[MCP] Server script not found. Tried paths:`, candidatePaths);
@@ -14530,6 +16779,173 @@ var ServerLlmBackend = class {
14530
16779
  }
14531
16780
  };
14532
16781
 
16782
+ // src/llm/WebSocketLlmBackend.ts
16783
+ import { v4 as uuidv411 } from "uuid";
16784
+ function stripThinkingBlocks2(text) {
16785
+ return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
16786
+ }
16787
+ var WebSocketLlmBackend = class {
16788
+ constructor(options) {
16789
+ this.wsManager = options.wsManager;
16790
+ this.apiClient = options.apiClient;
16791
+ this.currentModel = options.model;
16792
+ this.tokenGetter = options.tokenGetter;
16793
+ this.wsCompletionUrl = options.wsCompletionUrl;
16794
+ }
16795
+ /**
16796
+ * Send completion request via HTTP POST, receive streaming response via WebSocket.
16797
+ * Collects all streamed chunks, then calls callback once at completion
16798
+ * with the full accumulated content.
16799
+ */
16800
+ async complete(model, messages, options, callback) {
16801
+ logger.debug(`[WebSocketLlmBackend] Starting complete() with model: ${model}`);
16802
+ if (options.abortSignal?.aborted) {
16803
+ logger.debug("[WebSocketLlmBackend] Request aborted before start");
16804
+ return;
16805
+ }
16806
+ if (!this.wsManager.isConnected) {
16807
+ throw new Error("WebSocket is not connected");
16808
+ }
16809
+ const requestId = uuidv411();
16810
+ return new Promise((resolve3, reject) => {
16811
+ const isVerbose = process.env.B4M_VERBOSE === "1";
16812
+ const isUltraVerbose = process.env.B4M_DEBUG_STREAM === "1";
16813
+ const streamLogger = new StreamLogger(logger, "WebSocketLlmBackend", isVerbose, isUltraVerbose);
16814
+ streamLogger.streamStart();
16815
+ let eventCount = 0;
16816
+ let accumulatedText = "";
16817
+ let lastUsageInfo = {};
16818
+ let toolsUsed = [];
16819
+ let thinkingBlocks = [];
16820
+ let settled = false;
16821
+ const settle = (action) => {
16822
+ if (settled) return;
16823
+ settled = true;
16824
+ this.wsManager.offRequest(requestId);
16825
+ this.wsManager.offDisconnect(onDisconnect);
16826
+ action();
16827
+ };
16828
+ const settleResolve = () => settle(() => resolve3());
16829
+ const settleReject = (err) => settle(() => reject(err));
16830
+ const onDisconnect = () => {
16831
+ logger.debug("[WebSocketLlmBackend] Connection dropped during completion");
16832
+ settleReject(new Error("WebSocket connection lost during completion"));
16833
+ };
16834
+ this.wsManager.onDisconnect(onDisconnect);
16835
+ if (options.abortSignal) {
16836
+ if (options.abortSignal.aborted) {
16837
+ settleResolve();
16838
+ return;
16839
+ }
16840
+ options.abortSignal.addEventListener(
16841
+ "abort",
16842
+ () => {
16843
+ logger.debug("[WebSocketLlmBackend] Abort signal received");
16844
+ settleResolve();
16845
+ },
16846
+ { once: true }
16847
+ );
16848
+ }
16849
+ const updateUsage = (usage) => {
16850
+ if (usage) {
16851
+ lastUsageInfo = { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens };
16852
+ }
16853
+ };
16854
+ this.wsManager.onRequest(requestId, (message) => {
16855
+ if (options.abortSignal?.aborted) return;
16856
+ const action = message.action;
16857
+ if (action === "cli_completion_chunk") {
16858
+ eventCount++;
16859
+ const chunk = message.chunk;
16860
+ streamLogger.onEvent(eventCount, JSON.stringify(chunk));
16861
+ const textChunk = chunk.text || "";
16862
+ if (textChunk) accumulatedText += textChunk;
16863
+ updateUsage(chunk.usage);
16864
+ if (chunk.type === "content") {
16865
+ streamLogger.onContent(eventCount, textChunk, accumulatedText);
16866
+ } else if (chunk.type === "tool_use") {
16867
+ streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${chunk.tools?.length}`);
16868
+ if (chunk.tools && chunk.tools.length > 0) toolsUsed = chunk.tools;
16869
+ if (chunk.thinking && chunk.thinking.length > 0) thinkingBlocks = chunk.thinking;
16870
+ }
16871
+ } else if (action === "cli_completion_done") {
16872
+ streamLogger.streamComplete(accumulatedText);
16873
+ const cleanedText = stripThinkingBlocks2(accumulatedText);
16874
+ if (!cleanedText && toolsUsed.length === 0) {
16875
+ settleResolve();
16876
+ return;
16877
+ }
16878
+ const info = {
16879
+ ...lastUsageInfo,
16880
+ ...toolsUsed.length > 0 && { toolsUsed },
16881
+ ...thinkingBlocks.length > 0 && { thinking: thinkingBlocks }
16882
+ };
16883
+ callback([cleanedText], info).then(() => settleResolve()).catch((err) => settleReject(err));
16884
+ } else if (action === "cli_completion_error") {
16885
+ const errorMsg = message.error || "Server error";
16886
+ streamLogger.onCriticalEvent(eventCount, "ERROR", errorMsg);
16887
+ settleReject(new Error(errorMsg));
16888
+ }
16889
+ });
16890
+ const axiosInstance = this.apiClient.getAxiosInstance();
16891
+ axiosInstance.post(
16892
+ this.wsCompletionUrl,
16893
+ {
16894
+ requestId,
16895
+ model,
16896
+ messages,
16897
+ options: {
16898
+ temperature: options.temperature,
16899
+ maxTokens: options.maxTokens,
16900
+ stream: true,
16901
+ tools: options.tools || []
16902
+ }
16903
+ },
16904
+ { signal: options.abortSignal }
16905
+ ).catch((err) => {
16906
+ const msg = err instanceof Error ? err.message : String(err);
16907
+ settleReject(new Error(`HTTP request failed: ${msg}`));
16908
+ });
16909
+ });
16910
+ }
16911
+ /**
16912
+ * Get available models from server (REST call, not streaming).
16913
+ * Delegates to ApiClient -- same as ServerLlmBackend.
16914
+ */
16915
+ async getModelInfo() {
16916
+ try {
16917
+ logger.debug("[WebSocketLlmBackend] Fetching models from /api/models");
16918
+ const response = await this.apiClient.get("/api/models");
16919
+ if (!response || typeof response !== "object" || !Array.isArray(response.models)) {
16920
+ logger.warn("[WebSocketLlmBackend] Invalid API response format, using fallback models");
16921
+ return this.getFallbackModels();
16922
+ }
16923
+ const filteredModels = response.models.filter(
16924
+ (model) => model.type === "text" && model.supportsTools === true
16925
+ );
16926
+ if (filteredModels.length === 0) {
16927
+ logger.warn("[WebSocketLlmBackend] No CLI-compatible models found, using fallback");
16928
+ return this.getFallbackModels();
16929
+ }
16930
+ logger.debug(`[WebSocketLlmBackend] Loaded ${filteredModels.length} models`);
16931
+ return filteredModels;
16932
+ } catch (error) {
16933
+ logger.warn(
16934
+ `[WebSocketLlmBackend] Failed to fetch models: ${error instanceof Error ? error.message : String(error)}`
16935
+ );
16936
+ return this.getFallbackModels();
16937
+ }
16938
+ }
16939
+ getFallbackModels() {
16940
+ return [
16941
+ { id: "claude-sonnet-4-5-20250929", name: "Claude 4.5 Sonnet" },
16942
+ { id: "claude-3-5-haiku-20241022", name: "Claude 3.5 Haiku" },
16943
+ { id: "gpt-4o", name: "GPT-4o" },
16944
+ { id: "gpt-4o-mini", name: "GPT-4o Mini" }
16945
+ ];
16946
+ }
16947
+ };
16948
+
14533
16949
  // src/llm/NotifyingLlmBackend.ts
14534
16950
  var NotifyingLlmBackend = class {
14535
16951
  constructor(inner, backgroundManager) {
@@ -14564,6 +16980,253 @@ Please acknowledge these background agent results and incorporate them into your
14564
16980
  }
14565
16981
  };
14566
16982
 
16983
+ // src/ws/WebSocketConnectionManager.ts
16984
+ var WebSocketConnectionManager = class {
16985
+ constructor(wsUrl, getToken) {
16986
+ this.ws = null;
16987
+ this.heartbeatInterval = null;
16988
+ this.reconnectAttempts = 0;
16989
+ this.maxReconnectDelay = 3e4;
16990
+ this.handlers = /* @__PURE__ */ new Map();
16991
+ this.disconnectHandlers = /* @__PURE__ */ new Set();
16992
+ this.reconnectTimer = null;
16993
+ this.connected = false;
16994
+ this.connecting = false;
16995
+ this.closed = false;
16996
+ this.wsUrl = wsUrl;
16997
+ this.getToken = getToken;
16998
+ }
16999
+ /**
17000
+ * Connect to the WebSocket server.
17001
+ * Resolves when connection is established, rejects on failure.
17002
+ */
17003
+ async connect() {
17004
+ if (this.connected || this.connecting) return;
17005
+ this.connecting = true;
17006
+ const token = await this.getToken();
17007
+ if (!token) {
17008
+ this.connecting = false;
17009
+ throw new Error("No access token available for WebSocket connection");
17010
+ }
17011
+ return new Promise((resolve3, reject) => {
17012
+ logger.debug(`[WS] Connecting to ${this.wsUrl}...`);
17013
+ this.ws = new WebSocket(this.wsUrl, [`access_token.${token}`]);
17014
+ this.ws.onopen = () => {
17015
+ logger.debug("[WS] Connected");
17016
+ this.connected = true;
17017
+ this.connecting = false;
17018
+ this.reconnectAttempts = 0;
17019
+ this.startHeartbeat();
17020
+ resolve3();
17021
+ };
17022
+ this.ws.onmessage = (event) => {
17023
+ try {
17024
+ const data = typeof event.data === "string" ? event.data : event.data.toString();
17025
+ const message = JSON.parse(data);
17026
+ const requestId = message.requestId;
17027
+ if (requestId && this.handlers.has(requestId)) {
17028
+ this.handlers.get(requestId)(message);
17029
+ } else {
17030
+ logger.debug(`[WS] Unhandled message: ${message.action || "unknown"}`);
17031
+ }
17032
+ } catch (err) {
17033
+ logger.debug(`[WS] Failed to parse message: ${err}`);
17034
+ }
17035
+ };
17036
+ this.ws.onclose = () => {
17037
+ logger.debug("[WS] Connection closed");
17038
+ this.cleanup();
17039
+ this.notifyDisconnect();
17040
+ if (!this.closed) {
17041
+ this.scheduleReconnect();
17042
+ }
17043
+ };
17044
+ this.ws.onerror = (err) => {
17045
+ logger.debug(`[WS] Error: ${err}`);
17046
+ if (this.connecting) {
17047
+ this.connecting = false;
17048
+ this.connected = false;
17049
+ reject(new Error("WebSocket connection failed"));
17050
+ }
17051
+ };
17052
+ });
17053
+ }
17054
+ /** Whether the connection is currently established */
17055
+ get isConnected() {
17056
+ return this.connected;
17057
+ }
17058
+ /**
17059
+ * Send a JSON message over the WebSocket connection.
17060
+ */
17061
+ send(data) {
17062
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
17063
+ throw new Error("WebSocket is not connected");
17064
+ }
17065
+ const payload = JSON.stringify(data);
17066
+ const sizeKB = (payload.length / 1024).toFixed(1);
17067
+ logger.debug(`[WS] Sending ${sizeKB} KB (action: ${data.action})`);
17068
+ if (payload.length > 32e3) {
17069
+ logger.warn(`[WS] Payload ${sizeKB} KB exceeds API Gateway 32 KB frame limit \u2014 connection will be closed`);
17070
+ }
17071
+ this.ws.send(payload);
17072
+ }
17073
+ /**
17074
+ * Register a handler for messages matching a specific requestId.
17075
+ */
17076
+ onRequest(requestId, handler) {
17077
+ this.handlers.set(requestId, handler);
17078
+ }
17079
+ /**
17080
+ * Remove a handler for a specific requestId.
17081
+ */
17082
+ offRequest(requestId) {
17083
+ this.handlers.delete(requestId);
17084
+ }
17085
+ /**
17086
+ * Register a handler that fires when the connection drops.
17087
+ */
17088
+ onDisconnect(handler) {
17089
+ this.disconnectHandlers.add(handler);
17090
+ }
17091
+ /**
17092
+ * Remove a disconnect handler.
17093
+ */
17094
+ offDisconnect(handler) {
17095
+ this.disconnectHandlers.delete(handler);
17096
+ }
17097
+ /**
17098
+ * Close the connection and stop all heartbeat/reconnect logic.
17099
+ */
17100
+ disconnect() {
17101
+ this.closed = true;
17102
+ this.cleanup();
17103
+ if (this.ws) {
17104
+ this.ws.close();
17105
+ this.ws = null;
17106
+ }
17107
+ this.handlers.clear();
17108
+ this.disconnectHandlers.clear();
17109
+ }
17110
+ startHeartbeat() {
17111
+ this.stopHeartbeat();
17112
+ this.heartbeatInterval = setInterval(
17113
+ () => {
17114
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
17115
+ this.ws.send(JSON.stringify({ action: "heartbeat" }));
17116
+ logger.debug("[WS] Heartbeat sent");
17117
+ }
17118
+ },
17119
+ 5 * 60 * 1e3
17120
+ );
17121
+ }
17122
+ stopHeartbeat() {
17123
+ if (this.heartbeatInterval) {
17124
+ clearInterval(this.heartbeatInterval);
17125
+ this.heartbeatInterval = null;
17126
+ }
17127
+ }
17128
+ cleanup() {
17129
+ this.connected = false;
17130
+ this.connecting = false;
17131
+ this.stopHeartbeat();
17132
+ if (this.reconnectTimer) {
17133
+ clearTimeout(this.reconnectTimer);
17134
+ this.reconnectTimer = null;
17135
+ }
17136
+ }
17137
+ notifyDisconnect() {
17138
+ for (const handler of this.disconnectHandlers) {
17139
+ try {
17140
+ handler();
17141
+ } catch {
17142
+ }
17143
+ }
17144
+ }
17145
+ scheduleReconnect() {
17146
+ if (this.closed) return;
17147
+ this.reconnectAttempts++;
17148
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
17149
+ logger.debug(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
17150
+ this.reconnectTimer = setTimeout(async () => {
17151
+ this.reconnectTimer = null;
17152
+ if (this.closed) return;
17153
+ try {
17154
+ await this.connect();
17155
+ } catch {
17156
+ logger.debug("[WS] Reconnection failed");
17157
+ }
17158
+ }, delay);
17159
+ }
17160
+ };
17161
+
17162
+ // src/ws/WebSocketToolExecutor.ts
17163
+ import { v4 as uuidv412 } from "uuid";
17164
+ var WebSocketToolExecutor = class {
17165
+ constructor(wsManager, tokenGetter) {
17166
+ this.wsManager = wsManager;
17167
+ this.tokenGetter = tokenGetter;
17168
+ }
17169
+ /**
17170
+ * Execute a server-side tool via WebSocket.
17171
+ * Returns the tool result or throws on error.
17172
+ */
17173
+ async execute(toolName, input, abortSignal) {
17174
+ if (!this.wsManager.isConnected) {
17175
+ throw new Error("WebSocket is not connected");
17176
+ }
17177
+ const token = await this.tokenGetter();
17178
+ if (!token) {
17179
+ throw new Error("No access token available");
17180
+ }
17181
+ const requestId = uuidv412();
17182
+ return new Promise((resolve3, reject) => {
17183
+ let settled = false;
17184
+ const settle = (action) => {
17185
+ if (settled) return;
17186
+ settled = true;
17187
+ this.wsManager.offRequest(requestId);
17188
+ this.wsManager.offDisconnect(onDisconnect);
17189
+ action();
17190
+ };
17191
+ const settleResolve = (result) => settle(() => resolve3(result));
17192
+ const settleReject = (err) => settle(() => reject(err));
17193
+ const onDisconnect = () => {
17194
+ settleReject(new Error("WebSocket connection lost during tool execution"));
17195
+ };
17196
+ this.wsManager.onDisconnect(onDisconnect);
17197
+ if (abortSignal) {
17198
+ if (abortSignal.aborted) {
17199
+ settleReject(new Error("Tool execution aborted"));
17200
+ return;
17201
+ }
17202
+ abortSignal.addEventListener("abort", () => settleReject(new Error("Tool execution aborted")), {
17203
+ once: true
17204
+ });
17205
+ }
17206
+ this.wsManager.onRequest(requestId, (message) => {
17207
+ if (message.action === "cli_tool_response") {
17208
+ settleResolve({
17209
+ success: message.success,
17210
+ content: message.content,
17211
+ error: message.error
17212
+ });
17213
+ }
17214
+ });
17215
+ try {
17216
+ this.wsManager.send({
17217
+ action: "cli_tool_request",
17218
+ accessToken: token,
17219
+ requestId,
17220
+ toolName,
17221
+ input
17222
+ });
17223
+ } catch (err) {
17224
+ settleReject(err instanceof Error ? err : new Error(String(err)));
17225
+ }
17226
+ });
17227
+ }
17228
+ };
17229
+
14567
17230
  // src/auth/ApiClient.ts
14568
17231
  import axios11 from "axios";
14569
17232
  var ApiClient = class {
@@ -14701,7 +17364,7 @@ import { isAxiosError as isAxiosError2 } from "axios";
14701
17364
  // package.json
14702
17365
  var package_default = {
14703
17366
  name: "@bike4mind/cli",
14704
- version: "0.2.30",
17367
+ version: "0.2.31-b4m-cli-undo-command.19493+44c80c9bc",
14705
17368
  type: "module",
14706
17369
  description: "Interactive CLI tool for Bike4Mind with ReAct agents",
14707
17370
  license: "UNLICENSED",
@@ -14812,10 +17475,10 @@ var package_default = {
14812
17475
  },
14813
17476
  devDependencies: {
14814
17477
  "@bike4mind/agents": "0.1.0",
14815
- "@bike4mind/common": "2.52.0",
14816
- "@bike4mind/mcp": "1.31.0",
14817
- "@bike4mind/services": "2.50.0",
14818
- "@bike4mind/utils": "2.7.0",
17478
+ "@bike4mind/common": "2.52.1-b4m-cli-undo-command.19493+44c80c9bc",
17479
+ "@bike4mind/mcp": "1.31.1-b4m-cli-undo-command.19493+44c80c9bc",
17480
+ "@bike4mind/services": "2.50.1-b4m-cli-undo-command.19493+44c80c9bc",
17481
+ "@bike4mind/utils": "2.7.1-b4m-cli-undo-command.19493+44c80c9bc",
14819
17482
  "@types/better-sqlite3": "^7.6.13",
14820
17483
  "@types/diff": "^5.0.9",
14821
17484
  "@types/jsonwebtoken": "^9.0.4",
@@ -14836,29 +17499,29 @@ var package_default = {
14836
17499
  optionalDependencies: {
14837
17500
  "@vscode/ripgrep": "^1.17.0"
14838
17501
  },
14839
- gitHead: "e18c60eb7133cb85a538c4d87ccfafc6c95fc211"
17502
+ gitHead: "44c80c9bcf8bf132a8b75c3635426a901adce99e"
14840
17503
  };
14841
17504
 
14842
17505
  // src/agents/toolFilter.ts
14843
- function matchesToolPattern2(toolName, pattern) {
17506
+ function matchesToolPattern3(toolName, pattern) {
14844
17507
  const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
14845
17508
  return new RegExp(`^${regexPattern}$`).test(toolName);
14846
17509
  }
14847
- function matchesAnyPattern(toolName, patterns) {
14848
- return patterns.some((pattern) => matchesToolPattern2(toolName, pattern));
17510
+ function matchesAnyPattern2(toolName, patterns) {
17511
+ return patterns.some((pattern) => matchesToolPattern3(toolName, pattern));
14849
17512
  }
14850
- function filterToolsByPatterns(allTools, allowedPatterns, deniedPatterns) {
17513
+ function filterToolsByPatterns2(allTools, allowedPatterns, deniedPatterns) {
14851
17514
  return allTools.filter((tool) => {
14852
17515
  const toolName = tool.toolSchema.name;
14853
17516
  if (deniedPatterns && deniedPatterns.length > 0) {
14854
- if (matchesAnyPattern(toolName, deniedPatterns)) {
17517
+ if (matchesAnyPattern2(toolName, deniedPatterns)) {
14855
17518
  return false;
14856
17519
  }
14857
17520
  }
14858
17521
  if (!allowedPatterns || allowedPatterns.length === 0) {
14859
17522
  return true;
14860
17523
  }
14861
- return matchesAnyPattern(toolName, allowedPatterns);
17524
+ return matchesAnyPattern2(toolName, allowedPatterns);
14862
17525
  });
14863
17526
  }
14864
17527
 
@@ -15127,9 +17790,12 @@ var SubagentOrchestrator = class {
15127
17790
  this.deps.showPermissionPrompt,
15128
17791
  agentContext,
15129
17792
  this.deps.configStore,
15130
- this.deps.apiClient
17793
+ this.deps.apiClient,
17794
+ void 0,
17795
+ // toolFilter (applied separately below)
17796
+ this.deps.checkpointStore
15131
17797
  );
15132
- const filteredTools = filterToolsByPatterns(allTools, toolFilter.allowedTools, toolFilter.deniedTools);
17798
+ const filteredTools = filterToolsByPatterns2(allTools, toolFilter.allowedTools, toolFilter.deniedTools);
15133
17799
  if (this.deps.customCommandStore) {
15134
17800
  const skillTool = createSkillTool({
15135
17801
  customCommandStore: this.deps.customCommandStore,
@@ -15280,8 +17946,8 @@ var SubagentOrchestrator = class {
15280
17946
  };
15281
17947
 
15282
17948
  // src/agents/AgentStore.ts
15283
- import fs12 from "fs/promises";
15284
- import path16 from "path";
17949
+ import fs13 from "fs/promises";
17950
+ import path18 from "path";
15285
17951
  import os2 from "os";
15286
17952
  import matter2 from "gray-matter";
15287
17953
  var FULL_MODEL_ID_PREFIXES = [
@@ -15430,10 +18096,10 @@ var AgentStore = class {
15430
18096
  const root = projectRoot || process.cwd();
15431
18097
  const home = os2.homedir();
15432
18098
  this.builtinAgentsDir = builtinDir;
15433
- this.globalB4MAgentsDir = path16.join(home, ".bike4mind", "agents");
15434
- this.globalClaudeAgentsDir = path16.join(home, ".claude", "agents");
15435
- this.projectB4MAgentsDir = path16.join(root, ".bike4mind", "agents");
15436
- this.projectClaudeAgentsDir = path16.join(root, ".claude", "agents");
18099
+ this.globalB4MAgentsDir = path18.join(home, ".bike4mind", "agents");
18100
+ this.globalClaudeAgentsDir = path18.join(home, ".claude", "agents");
18101
+ this.projectB4MAgentsDir = path18.join(root, ".bike4mind", "agents");
18102
+ this.projectClaudeAgentsDir = path18.join(root, ".claude", "agents");
15437
18103
  }
15438
18104
  /**
15439
18105
  * Load all agents from all directories
@@ -15457,7 +18123,7 @@ var AgentStore = class {
15457
18123
  */
15458
18124
  async loadAgentsFromDirectory(directory, source) {
15459
18125
  try {
15460
- const stats = await fs12.stat(directory);
18126
+ const stats = await fs13.stat(directory);
15461
18127
  if (!stats.isDirectory()) {
15462
18128
  return;
15463
18129
  }
@@ -15485,9 +18151,9 @@ var AgentStore = class {
15485
18151
  async findAgentFiles(directory) {
15486
18152
  const files = [];
15487
18153
  try {
15488
- const entries = await fs12.readdir(directory, { withFileTypes: true });
18154
+ const entries = await fs13.readdir(directory, { withFileTypes: true });
15489
18155
  for (const entry of entries) {
15490
- const fullPath = path16.join(directory, entry.name);
18156
+ const fullPath = path18.join(directory, entry.name);
15491
18157
  if (entry.isDirectory()) {
15492
18158
  const subFiles = await this.findAgentFiles(fullPath);
15493
18159
  files.push(...subFiles);
@@ -15504,10 +18170,10 @@ var AgentStore = class {
15504
18170
  * Parse a single agent markdown file
15505
18171
  */
15506
18172
  async parseAgentFile(filePath, source) {
15507
- const content = await fs12.readFile(filePath, "utf-8");
18173
+ const content = await fs13.readFile(filePath, "utf-8");
15508
18174
  const { data: frontmatter, content: body } = matter2(content);
15509
18175
  const parsed = AgentFrontmatterSchema.parse(frontmatter);
15510
- const name = path16.basename(filePath, ".md");
18176
+ const name = path18.basename(filePath, ".md");
15511
18177
  const modelInput = parsed.model || DEFAULT_AGENT_MODEL;
15512
18178
  const resolution = resolveModelAlias(modelInput, name, filePath);
15513
18179
  if (!resolution.resolved && resolution.warning) {
@@ -15587,16 +18253,16 @@ var AgentStore = class {
15587
18253
  */
15588
18254
  async createAgentFile(name, isGlobal = false, useClaude = true) {
15589
18255
  const targetDir = isGlobal ? useClaude ? this.globalClaudeAgentsDir : this.globalB4MAgentsDir : useClaude ? this.projectClaudeAgentsDir : this.projectB4MAgentsDir;
15590
- const filePath = path16.join(targetDir, `${name}.md`);
18256
+ const filePath = path18.join(targetDir, `${name}.md`);
15591
18257
  try {
15592
- await fs12.access(filePath);
18258
+ await fs13.access(filePath);
15593
18259
  throw new Error(`Agent file already exists: ${filePath}`);
15594
18260
  } catch (error) {
15595
18261
  if (error.code !== "ENOENT") {
15596
18262
  throw error;
15597
18263
  }
15598
18264
  }
15599
- await fs12.mkdir(targetDir, { recursive: true });
18265
+ await fs13.mkdir(targetDir, { recursive: true });
15600
18266
  const template = `---
15601
18267
  description: ${name} agent description
15602
18268
  model: claude-3-5-haiku-20241022
@@ -15631,7 +18297,7 @@ You are a ${name} specialist. Your job is to [describe primary task].
15631
18297
  ## Output Format
15632
18298
  Describe the expected output format here.
15633
18299
  `;
15634
- await fs12.writeFile(filePath, template, "utf-8");
18300
+ await fs13.writeFile(filePath, template, "utf-8");
15635
18301
  return filePath;
15636
18302
  }
15637
18303
  /**
@@ -16284,7 +18950,7 @@ function createTodoStore(onUpdate) {
16284
18950
 
16285
18951
  // src/tools/findDefinitionTool.ts
16286
18952
  import { stat as stat3 } from "fs/promises";
16287
- import path17 from "path";
18953
+ import path19 from "path";
16288
18954
  import { execFile as execFile2 } from "child_process";
16289
18955
  import { promisify as promisify2 } from "util";
16290
18956
  import { createRequire as createRequire2 } from "module";
@@ -16305,8 +18971,8 @@ var ALL_KEYWORDS = Object.values(KIND_KEYWORDS).flat();
16305
18971
  function getRipgrepPath2() {
16306
18972
  try {
16307
18973
  const ripgrepPath = require3.resolve("@vscode/ripgrep");
16308
- const ripgrepDir = path17.dirname(ripgrepPath);
16309
- return path17.join(ripgrepDir, "..", "bin", "rg" + (process.platform === "win32" ? ".exe" : ""));
18974
+ const ripgrepDir = path19.dirname(ripgrepPath);
18975
+ return path19.join(ripgrepDir, "..", "bin", "rg" + (process.platform === "win32" ? ".exe" : ""));
16310
18976
  } catch {
16311
18977
  throw new Error(
16312
18978
  "ripgrep is not available. Please install @vscode/ripgrep: pnpm add @vscode/ripgrep --filter @bike4mind/services"
@@ -16314,10 +18980,10 @@ function getRipgrepPath2() {
16314
18980
  }
16315
18981
  }
16316
18982
  function isPathWithinWorkspace2(targetPath, baseCwd) {
16317
- const resolvedTarget = path17.resolve(targetPath);
16318
- const resolvedBase = path17.resolve(baseCwd);
16319
- const relativePath = path17.relative(resolvedBase, resolvedTarget);
16320
- return !relativePath.startsWith("..") && !path17.isAbsolute(relativePath);
18983
+ const resolvedTarget = path19.resolve(targetPath);
18984
+ const resolvedBase = path19.resolve(baseCwd);
18985
+ const relativePath = path19.relative(resolvedBase, resolvedTarget);
18986
+ return !relativePath.startsWith("..") && !path19.isAbsolute(relativePath);
16321
18987
  }
16322
18988
  function escapeRegex(str) {
16323
18989
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -16350,7 +19016,7 @@ async function findDefinitions(params) {
16350
19016
  throw new Error("symbol_name is required");
16351
19017
  }
16352
19018
  const baseCwd = process.cwd();
16353
- const targetDir = search_path ? path17.resolve(baseCwd, search_path) : baseCwd;
19019
+ const targetDir = search_path ? path19.resolve(baseCwd, search_path) : baseCwd;
16354
19020
  if (!isPathWithinWorkspace2(targetDir, baseCwd)) {
16355
19021
  throw new Error(`Path validation failed: "${search_path}" resolves outside the allowed workspace directory`);
16356
19022
  }
@@ -16405,7 +19071,7 @@ async function findDefinitions(params) {
16405
19071
  const lineText = match.data.lines.text.trimEnd();
16406
19072
  if (isLikelyDefinition(lineText)) {
16407
19073
  allMatches.push({
16408
- filePath: path17.relative(targetDir, match.data.path.text) || path17.basename(match.data.path.text),
19074
+ filePath: path19.relative(targetDir, match.data.path.text) || path19.basename(match.data.path.text),
16409
19075
  lineNumber: match.data.line_number,
16410
19076
  line: lineText
16411
19077
  });
@@ -16483,8 +19149,8 @@ function createFindDefinitionTool() {
16483
19149
  }
16484
19150
 
16485
19151
  // src/tools/getFileStructure/index.ts
16486
- import { existsSync as existsSync10, promises as fs13, statSync as statSync6 } from "fs";
16487
- import path18 from "path";
19152
+ import { existsSync as existsSync12, promises as fs14, statSync as statSync6 } from "fs";
19153
+ import path20 from "path";
16488
19154
 
16489
19155
  // src/tools/getFileStructure/formatter.ts
16490
19156
  var SECTIONS = [
@@ -16521,35 +19187,35 @@ function formatSection(lines, title, items, format) {
16521
19187
  }
16522
19188
 
16523
19189
  // src/tools/getFileStructure/index.ts
16524
- var MAX_FILE_SIZE3 = 10 * 1024 * 1024;
19190
+ var MAX_FILE_SIZE4 = 10 * 1024 * 1024;
16525
19191
  function createGetFileStructureTool() {
16526
19192
  return {
16527
19193
  toolFn: async (value) => {
16528
19194
  const params = value;
16529
19195
  try {
16530
19196
  const cwd = process.cwd();
16531
- const resolvedPath = path18.resolve(cwd, params.path);
19197
+ const resolvedPath = path20.resolve(cwd, params.path);
16532
19198
  if (!resolvedPath.startsWith(cwd)) {
16533
19199
  return "Error: Access denied - cannot read files outside of current working directory";
16534
19200
  }
16535
- if (!existsSync10(resolvedPath)) {
19201
+ if (!existsSync12(resolvedPath)) {
16536
19202
  return `Error: File not found: ${params.path}`;
16537
19203
  }
16538
19204
  const stats = statSync6(resolvedPath);
16539
19205
  if (stats.isDirectory()) {
16540
19206
  return `Error: Path is a directory, not a file: ${params.path}`;
16541
19207
  }
16542
- if (stats.size > MAX_FILE_SIZE3) {
16543
- return `Error: File too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Max: ${MAX_FILE_SIZE3 / 1024 / 1024}MB`;
19208
+ if (stats.size > MAX_FILE_SIZE4) {
19209
+ return `Error: File too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Max: ${MAX_FILE_SIZE4 / 1024 / 1024}MB`;
16544
19210
  }
16545
- const ext = path18.extname(resolvedPath).toLowerCase();
19211
+ const ext = path20.extname(resolvedPath).toLowerCase();
16546
19212
  const { getLanguageForExtension, parseFileStructure, getSupportedLanguages } = await import("./treeSitterEngine-4SGFQDY3.js");
16547
19213
  const languageId = getLanguageForExtension(ext);
16548
19214
  if (!languageId) {
16549
19215
  const supported = getSupportedLanguages();
16550
19216
  return `Error: Unsupported file type "${ext}". Supported languages: ${supported.join(", ")}`;
16551
19217
  }
16552
- const sourceCode = await fs13.readFile(resolvedPath, "utf-8");
19218
+ const sourceCode = await fs14.readFile(resolvedPath, "utf-8");
16553
19219
  const lineCount = sourceCode.split("\n").length;
16554
19220
  const items = await parseFileStructure(sourceCode, languageId);
16555
19221
  return formatStructureOutput(params.path, items, stats.size, lineCount);
@@ -16610,7 +19276,9 @@ function CliApp() {
16610
19276
  agentStore: null,
16611
19277
  abortController: null,
16612
19278
  contextContent: "",
16613
- backgroundManager: null
19279
+ backgroundManager: null,
19280
+ wsManager: null,
19281
+ checkpointStore: null
16614
19282
  });
16615
19283
  const [isInitialized, setIsInitialized] = useState10(false);
16616
19284
  const [initError, setInitError] = useState10(null);
@@ -16638,6 +19306,10 @@ function CliApp() {
16638
19306
  })
16639
19307
  );
16640
19308
  }
19309
+ if (state.wsManager) {
19310
+ state.wsManager.disconnect();
19311
+ setWebSocketToolExecutor(null);
19312
+ }
16641
19313
  if (state.agent) {
16642
19314
  state.agent.removeAllListeners();
16643
19315
  }
@@ -16656,7 +19328,7 @@ function CliApp() {
16656
19328
  setTimeout(() => {
16657
19329
  process.exit(0);
16658
19330
  }, 100);
16659
- }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore]);
19331
+ }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore, state.wsManager]);
16660
19332
  useInput9((input, key) => {
16661
19333
  if (key.escape) {
16662
19334
  const store = useCliStore.getState();
@@ -16731,7 +19403,7 @@ function CliApp() {
16731
19403
  if (!isAuthenticated) {
16732
19404
  console.log("\u2139\uFE0F AI features disabled. Available commands: /login, /help, /config\n");
16733
19405
  const minimalSession = {
16734
- id: uuidv411(),
19406
+ id: uuidv413(),
16735
19407
  name: `Session ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
16736
19408
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
16737
19409
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -16759,10 +19431,45 @@ function CliApp() {
16759
19431
  console.log(`\u{1F30D} API Environment: ${envName} (${apiBaseURL})`);
16760
19432
  }
16761
19433
  const apiClient = new ApiClient(apiBaseURL, state.configStore);
16762
- const llm = new ServerLlmBackend({
16763
- apiClient,
16764
- model: config.defaultModel
16765
- });
19434
+ const tokenGetter = async () => {
19435
+ const tokens = await state.configStore.getAuthTokens();
19436
+ return tokens?.accessToken ?? null;
19437
+ };
19438
+ let wsManager = null;
19439
+ let llm;
19440
+ try {
19441
+ const serverConfig = await apiClient.get(
19442
+ "/api/settings/serverConfig"
19443
+ );
19444
+ const wsUrl = serverConfig?.websocketUrl;
19445
+ const wsCompletionUrl = serverConfig?.wsCompletionUrl;
19446
+ if (wsUrl && wsCompletionUrl) {
19447
+ wsManager = new WebSocketConnectionManager(wsUrl, tokenGetter);
19448
+ await wsManager.connect();
19449
+ const wsToolExecutor2 = new WebSocketToolExecutor(wsManager, tokenGetter);
19450
+ setWebSocketToolExecutor(wsToolExecutor2);
19451
+ llm = new WebSocketLlmBackend({
19452
+ wsManager,
19453
+ apiClient,
19454
+ model: config.defaultModel,
19455
+ tokenGetter,
19456
+ wsCompletionUrl
19457
+ });
19458
+ logger.debug("\u{1F50C} Using WebSocket transport (bypasses CloudFront timeout)");
19459
+ } else {
19460
+ throw new Error("No websocketUrl or wsCompletionUrl in server config");
19461
+ }
19462
+ } catch (wsError) {
19463
+ logger.debug(
19464
+ `[WS] WebSocket unavailable, using SSE fallback: ${wsError instanceof Error ? wsError.message : String(wsError)}`
19465
+ );
19466
+ wsManager = null;
19467
+ setWebSocketToolExecutor(null);
19468
+ llm = new ServerLlmBackend({
19469
+ apiClient,
19470
+ model: config.defaultModel
19471
+ });
19472
+ }
16766
19473
  const models = await llm.getModelInfo();
16767
19474
  if (models.length === 0) {
16768
19475
  throw new Error("No models available from server.");
@@ -16775,7 +19482,7 @@ function CliApp() {
16775
19482
  }
16776
19483
  llm.currentModel = modelInfo.id;
16777
19484
  const newSession = {
16778
- id: uuidv411(),
19485
+ id: uuidv413(),
16779
19486
  name: `Session ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
16780
19487
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
16781
19488
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -16825,6 +19532,12 @@ function CliApp() {
16825
19532
  enqueuePermissionPrompt(prompt);
16826
19533
  });
16827
19534
  };
19535
+ const checkpointProjectDir = state.configStore.getProjectConfigDir() || process.cwd();
19536
+ const checkpointStore = new CheckpointStore(checkpointProjectDir);
19537
+ try {
19538
+ await checkpointStore.init(newSession.id);
19539
+ } catch {
19540
+ }
16828
19541
  const agentContext = {
16829
19542
  currentAgent: null,
16830
19543
  observationQueue: []
@@ -16837,7 +19550,10 @@ function CliApp() {
16837
19550
  promptFn,
16838
19551
  agentContext,
16839
19552
  state.configStore,
16840
- apiClient
19553
+ apiClient,
19554
+ void 0,
19555
+ // toolFilter
19556
+ checkpointStore
16841
19557
  );
16842
19558
  startupLog.push(`\u{1F6E0}\uFE0F Loaded ${b4mTools2.length} B4M tool(s)`);
16843
19559
  const mcpManager = new McpManager(config);
@@ -16868,7 +19584,8 @@ function CliApp() {
16868
19584
  apiClient,
16869
19585
  agentStore,
16870
19586
  customCommandStore: state.customCommandStore,
16871
- enableParallelToolExecution: config.preferences.enableParallelToolExecution === true
19587
+ enableParallelToolExecution: config.preferences.enableParallelToolExecution === true,
19588
+ checkpointStore
16872
19589
  });
16873
19590
  const backgroundManager = new BackgroundAgentManager(orchestrator);
16874
19591
  backgroundManager.setOnStatusChange((job) => {
@@ -16989,8 +19706,12 @@ function CliApp() {
16989
19706
  // Store agent store for agent management commands
16990
19707
  contextContent: contextResult.mergedContent,
16991
19708
  // Store raw context for compact instructions
16992
- backgroundManager
19709
+ backgroundManager,
16993
19710
  // Store for grouped notification turn tracking
19711
+ wsManager,
19712
+ // WebSocket connection manager (null if using SSE fallback)
19713
+ checkpointStore
19714
+ // File change checkpointing for undo/restore
16994
19715
  }));
16995
19716
  setStoreSession(newSession);
16996
19717
  const bannerLines = [
@@ -17085,13 +19806,13 @@ function CliApp() {
17085
19806
  messageContent = multimodalMessage.content;
17086
19807
  }
17087
19808
  const userMessage = {
17088
- id: uuidv411(),
19809
+ id: uuidv413(),
17089
19810
  role: "user",
17090
19811
  content: userMessageContent,
17091
19812
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
17092
19813
  };
17093
19814
  const pendingAssistantMessage = {
17094
- id: uuidv411(),
19815
+ id: uuidv413(),
17095
19816
  role: "assistant",
17096
19817
  content: "...",
17097
19818
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17304,13 +20025,13 @@ function CliApp() {
17304
20025
  userMessageContent = message;
17305
20026
  }
17306
20027
  const userMessage = {
17307
- id: uuidv411(),
20028
+ id: uuidv413(),
17308
20029
  role: "user",
17309
20030
  content: userMessageContent,
17310
20031
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
17311
20032
  };
17312
20033
  const pendingAssistantMessage = {
17313
- id: uuidv411(),
20034
+ id: uuidv413(),
17314
20035
  role: "assistant",
17315
20036
  content: "...",
17316
20037
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17390,7 +20111,7 @@ function CliApp() {
17390
20111
  const currentSession = useCliStore.getState().session;
17391
20112
  if (currentSession) {
17392
20113
  const cancelMessage = {
17393
- id: uuidv411(),
20114
+ id: uuidv413(),
17394
20115
  role: "assistant",
17395
20116
  content: "\u26A0\uFE0F Operation cancelled by user",
17396
20117
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17435,7 +20156,7 @@ function CliApp() {
17435
20156
  setState((prev) => ({ ...prev, abortController }));
17436
20157
  try {
17437
20158
  const pendingAssistantMessage = {
17438
- id: uuidv411(),
20159
+ id: uuidv413(),
17439
20160
  role: "assistant",
17440
20161
  content: "...",
17441
20162
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17463,7 +20184,7 @@ function CliApp() {
17463
20184
  const currentSession = useCliStore.getState().session;
17464
20185
  if (!currentSession) return;
17465
20186
  const continuationMessage = {
17466
- id: uuidv411(),
20187
+ id: uuidv413(),
17467
20188
  role: "assistant",
17468
20189
  content: "---\n\n**Background Agent Results:**\n\n" + result.finalAnswer,
17469
20190
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17524,13 +20245,13 @@ function CliApp() {
17524
20245
  isError = true;
17525
20246
  }
17526
20247
  const userMessage = {
17527
- id: uuidv411(),
20248
+ id: uuidv413(),
17528
20249
  role: "user",
17529
20250
  content: `$ ${command}`,
17530
20251
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
17531
20252
  };
17532
20253
  const assistantMessage = {
17533
- id: uuidv411(),
20254
+ id: uuidv413(),
17534
20255
  role: "assistant",
17535
20256
  content: isError ? `\u274C Error:
17536
20257
  ${output}` : output.trim() || "(no output)",
@@ -17683,6 +20404,10 @@ Available commands:
17683
20404
  /exit - Exit the CLI
17684
20405
  /clear - Start a new session
17685
20406
  /rewind - Rewind conversation to a previous point
20407
+ /undo - Undo the last file change
20408
+ /checkpoints - List available file restore points
20409
+ /restore <n> - Restore files to a specific checkpoint
20410
+ /diff [n] - Show diff between current state and a checkpoint
17686
20411
  /login - Authenticate with your B4M account
17687
20412
  /logout - Clear authentication and sign out
17688
20413
  /whoami - Show current authenticated user
@@ -17775,6 +20500,9 @@ Keyboard Shortcuts:
17775
20500
  }
17776
20501
  await logger.initialize(loadedSession.id);
17777
20502
  logger.debug("=== Session Resumed ===");
20503
+ if (state.checkpointStore) {
20504
+ state.checkpointStore.setSessionId(loadedSession.id);
20505
+ }
17778
20506
  setState((prev) => ({ ...prev, session: loadedSession }));
17779
20507
  setStoreSession(loadedSession);
17780
20508
  useCliStore.getState().clearPendingMessages();
@@ -17999,7 +20727,7 @@ Keyboard Shortcuts:
17999
20727
  console.clear();
18000
20728
  const model = state.session?.model || state.config?.defaultModel || "claude-sonnet";
18001
20729
  const newSession = {
18002
- id: uuidv411(),
20730
+ id: uuidv413(),
18003
20731
  name: `Session ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
18004
20732
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
18005
20733
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -18013,6 +20741,9 @@ Keyboard Shortcuts:
18013
20741
  };
18014
20742
  await logger.initialize(newSession.id);
18015
20743
  logger.debug("=== New Session Started via /clear ===");
20744
+ if (state.checkpointStore) {
20745
+ state.checkpointStore.setSessionId(newSession.id);
20746
+ }
18016
20747
  setState((prev) => ({ ...prev, session: newSession }));
18017
20748
  setStoreSession(newSession);
18018
20749
  useCliStore.getState().clearPendingMessages();
@@ -18023,8 +20754,7 @@ Keyboard Shortcuts:
18023
20754
  `);
18024
20755
  break;
18025
20756
  }
18026
- case "rewind":
18027
- case "undo": {
20757
+ case "rewind": {
18028
20758
  if (!state.session) {
18029
20759
  console.log("No active session to rewind");
18030
20760
  return;
@@ -18083,6 +20813,103 @@ Keyboard Shortcuts:
18083
20813
  }));
18084
20814
  break;
18085
20815
  }
20816
+ case "undo": {
20817
+ if (!state.checkpointStore) {
20818
+ console.log("Checkpointing not available.");
20819
+ return;
20820
+ }
20821
+ const undoCheckpoints = state.checkpointStore.listCheckpoints();
20822
+ if (undoCheckpoints.length === 0) {
20823
+ console.log("No checkpoints available. No file changes have been made yet.");
20824
+ return;
20825
+ }
20826
+ try {
20827
+ const restored = await state.checkpointStore.undoLast();
20828
+ console.log(`
20829
+ \u2705 Restored ${restored.filePaths.length} file(s) to state before: ${restored.name}`);
20830
+ for (const f of restored.filePaths) {
20831
+ console.log(` - ${f}`);
20832
+ }
20833
+ console.log("");
20834
+ } catch (err) {
20835
+ console.log(`Failed to undo: ${err instanceof Error ? err.message : String(err)}`);
20836
+ }
20837
+ break;
20838
+ }
20839
+ case "checkpoints": {
20840
+ if (!state.checkpointStore) {
20841
+ console.log("Checkpointing not available.");
20842
+ return;
20843
+ }
20844
+ const cpList = state.checkpointStore.listCheckpoints();
20845
+ if (cpList.length === 0) {
20846
+ console.log("No checkpoints yet. Checkpoints are created automatically before file changes.");
20847
+ return;
20848
+ }
20849
+ console.log("\nCheckpoints (most recent first):\n");
20850
+ cpList.forEach((cp, idx) => {
20851
+ const ageMs = Date.now() - new Date(cp.timestamp).getTime();
20852
+ const ageSec = Math.floor(ageMs / 1e3);
20853
+ let age;
20854
+ if (ageSec < 60) age = `${ageSec}s ago`;
20855
+ else if (ageSec < 3600) age = `${Math.floor(ageSec / 60)}m ago`;
20856
+ else age = `${Math.floor(ageSec / 3600)}h ago`;
20857
+ console.log(` ${idx + 1}. ${cp.name} (${age}) - ${cp.filePaths.length} file(s)`);
20858
+ });
20859
+ console.log("\nUse /restore <number> to restore, /diff <number> to see changes.\n");
20860
+ break;
20861
+ }
20862
+ case "restore": {
20863
+ if (!state.checkpointStore) {
20864
+ console.log("Checkpointing not available.");
20865
+ return;
20866
+ }
20867
+ const restoreTarget = args[0];
20868
+ if (!restoreTarget) {
20869
+ console.log("Usage: /restore <checkpoint-number>");
20870
+ console.log("Use /checkpoints to list available restore points.");
20871
+ return;
20872
+ }
20873
+ const restoreNum = parseInt(restoreTarget, 10);
20874
+ if (isNaN(restoreNum) || restoreNum < 1) {
20875
+ console.log("Please provide a valid checkpoint number. Use /checkpoints to list.");
20876
+ return;
20877
+ }
20878
+ try {
20879
+ const restoredCp = await state.checkpointStore.restoreCheckpoint(restoreNum);
20880
+ console.log(`
20881
+ \u2705 Restored to checkpoint: ${restoredCp.name}`);
20882
+ for (const f of restoredCp.filePaths) {
20883
+ console.log(` - ${f}`);
20884
+ }
20885
+ console.log("");
20886
+ } catch (err) {
20887
+ console.log(`Failed to restore: ${err instanceof Error ? err.message : String(err)}`);
20888
+ }
20889
+ break;
20890
+ }
20891
+ case "diff": {
20892
+ if (!state.checkpointStore) {
20893
+ console.log("Checkpointing not available.");
20894
+ return;
20895
+ }
20896
+ const diffTarget = args[0] ? parseInt(args[0], 10) : 1;
20897
+ if (isNaN(diffTarget) || diffTarget < 1) {
20898
+ console.log("Usage: /diff [checkpoint-number] (defaults to 1, most recent)");
20899
+ return;
20900
+ }
20901
+ try {
20902
+ const diffOutput = state.checkpointStore.getCheckpointDiff(diffTarget);
20903
+ if (!diffOutput.trim()) {
20904
+ console.log("No differences found between checkpoint and current state.");
20905
+ } else {
20906
+ console.log(diffOutput);
20907
+ }
20908
+ } catch (err) {
20909
+ console.log(`Failed to generate diff: ${err instanceof Error ? err.message : String(err)}`);
20910
+ }
20911
+ break;
20912
+ }
18086
20913
  case "usage": {
18087
20914
  const usageAuthTokens = await state.configStore.getAuthTokens();
18088
20915
  if (!usageAuthTokens || new Date(usageAuthTokens.expiresAt) <= /* @__PURE__ */ new Date()) {
@@ -18504,9 +21331,9 @@ No usage data available for the last ${USAGE_DAYS} days.`);
18504
21331
  return { ...prev, config: updatedConfig };
18505
21332
  });
18506
21333
  if (modelChanged && state.agent) {
18507
- const llm = state.agent.context.llm;
18508
- if (llm) {
18509
- llm.currentModel = updatedConfig.defaultModel;
21334
+ const backend = state.agent.context.llm;
21335
+ if (backend) {
21336
+ backend.currentModel = updatedConfig.defaultModel;
18510
21337
  }
18511
21338
  }
18512
21339
  };