@bike4mind/cli 0.2.30-subagent-delegation.19188 → 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,7 +5,7 @@ import {
5
5
  getEffectiveApiKey,
6
6
  getOpenWeatherKey,
7
7
  getSerperKey
8
- } from "./chunk-YNQB2RM7.js";
8
+ } from "./chunk-T67NGQW6.js";
9
9
  import "./chunk-GQGOWACU.js";
10
10
  import {
11
11
  ConfigStore,
@@ -14,9 +14,9 @@ import {
14
14
  import {
15
15
  selectActiveBackgroundAgents,
16
16
  useCliStore
17
- } from "./chunk-TVW4ZESU.js";
18
- import "./chunk-44RZA4RG.js";
19
- import "./chunk-PP5WVA27.js";
17
+ } from "./chunk-BYXFQJYT.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-FLA4PWXB.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-ICLEQOH6.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";
@@ -120,10 +122,22 @@ import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
120
122
  // src/components/CustomTextInput.tsx
121
123
  import React2, { useEffect, useRef, useState } from "react";
122
124
  import { Text as Text2, useInput } from "ink";
125
+
126
+ // src/config/constants.ts
127
+ var USAGE_DAYS = 30;
128
+ var MODEL_NAME_COLUMN_WIDTH = 18;
129
+ var USAGE_CACHE_TTL = 5 * 60 * 1e3;
130
+ var PASTE_LINE_THRESHOLD = 5;
131
+ var MAX_PASTE_SIZE = 5e5;
132
+ var IMAGE_DETECTION_MAX_LENGTH = 500;
133
+
134
+ // src/components/CustomTextInput.tsx
123
135
  function CustomTextInput({
124
136
  value,
125
137
  onChange,
126
138
  onSubmit,
139
+ onPaste,
140
+ pasteIndicator,
127
141
  placeholder = "",
128
142
  showCursor = true,
129
143
  disabled = false
@@ -275,6 +289,11 @@ function CustomTextInput({
275
289
  }
276
290
  }
277
291
  if (key.backspace || key.delete) {
292
+ if (pasteIndicator) {
293
+ onChange("");
294
+ setCursorOffset(0);
295
+ return;
296
+ }
278
297
  if (cursorOffset > 0) {
279
298
  const newValue = value.slice(0, cursorOffset - 1) + value.slice(cursorOffset);
280
299
  onChange(newValue);
@@ -291,6 +310,20 @@ function CustomTextInput({
291
310
  return;
292
311
  }
293
312
  if (!key.ctrl && !key.meta && input.length > 0) {
313
+ if (input.length > 1 && onPaste) {
314
+ const normalized = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
315
+ const lineCount = normalized.split("\n").length;
316
+ if (lineCount >= PASTE_LINE_THRESHOLD) {
317
+ onPaste(normalized);
318
+ return;
319
+ }
320
+ }
321
+ if (pasteIndicator) {
322
+ const sanitizedInput2 = input === "\r" ? "\n" : input;
323
+ onChange(sanitizedInput2);
324
+ setCursorOffset(sanitizedInput2.length);
325
+ return;
326
+ }
294
327
  const sanitizedInput = input === "\r" ? "\n" : input;
295
328
  const newValue = value.slice(0, cursorOffset) + sanitizedInput + value.slice(cursorOffset);
296
329
  onChange(newValue);
@@ -299,6 +332,9 @@ function CustomTextInput({
299
332
  },
300
333
  { isActive: !disabled }
301
334
  );
335
+ if (pasteIndicator) {
336
+ return /* @__PURE__ */ React2.createElement(Text2, null, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, pasteIndicator), showCursor && /* @__PURE__ */ React2.createElement(Text2, { inverse: true }, " "));
337
+ }
302
338
  const hasValue = value.length > 0;
303
339
  if (!hasValue) {
304
340
  return /* @__PURE__ */ React2.createElement(Text2, null, showCursor ? /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Text2, { inverse: true }, " "), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, placeholder)) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, placeholder));
@@ -316,9 +352,23 @@ function CommandAutocomplete({ commands, selectedIndex }) {
316
352
  if (commands.length === 0) {
317
353
  return /* @__PURE__ */ React3.createElement(Box2, { marginLeft: 2, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "No matching commands"));
318
354
  }
319
- 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;
320
370
  const args = cmd.args ? ` ${cmd.args}` : "";
321
- const isSelected = index === selectedIndex;
371
+ const isSelected = actualIndex === selectedIndex;
322
372
  const sourceIcon = cmd.source === "global" ? "\u{1F3E0} " : cmd.source === "project" ? "\u{1F4C1} " : cmd.source === "built-in" ? "\u{1F527} " : "";
323
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));
324
374
  }));
@@ -679,8 +729,25 @@ var COMMANDS = [
679
729
  },
680
730
  {
681
731
  name: "rewind",
682
- description: "Rewind conversation to a previous point",
683
- 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]"
684
751
  },
685
752
  {
686
753
  name: "project-config",
@@ -1041,8 +1108,11 @@ function InputPrompt({
1041
1108
  onBashModeChange
1042
1109
  }) {
1043
1110
  const value = useCliStore((state) => state.inputValue);
1044
- const setInputValue = useCliStore((state) => state.setInputValue);
1045
- const setValue = setInputValue;
1111
+ const setValue = useCliStore((state) => state.setInputValue);
1112
+ const pastedContent = useCliStore((state) => state.pastedContent);
1113
+ const pastedLineCount = useCliStore((state) => state.pastedLineCount);
1114
+ const setPastedContent = useCliStore((state) => state.setPastedContent);
1115
+ const clearPaste = useCliStore((state) => state.clearPaste);
1046
1116
  const [selectedIndex, setSelectedIndex] = useState3(0);
1047
1117
  const [historyIndex, setHistoryIndex] = useState3(-1);
1048
1118
  const [tempInput, setTempInput] = useState3("");
@@ -1059,8 +1129,8 @@ function InputPrompt({
1059
1129
  useEffect3(() => {
1060
1130
  onBashModeChange?.(isBashMode);
1061
1131
  }, [isBashMode, onBashModeChange]);
1062
- const shouldShowCommandAutocomplete = value.startsWith("/") && !disabled && !fileAutocomplete?.active && !looksLikeFilePath(value);
1063
- 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(" ");
1064
1134
  const filteredCommands = useMemo(() => {
1065
1135
  if (!shouldShowCommandAutocomplete) return [];
1066
1136
  return searchCommands(commandQuery, commands);
@@ -1166,7 +1236,14 @@ function InputPrompt({
1166
1236
  }
1167
1237
  };
1168
1238
  const handleSubmit = (input) => {
1169
- if (disabled || !input.trim()) return;
1239
+ if (disabled) return;
1240
+ if (pastedContent) {
1241
+ const fullContent = pastedContent;
1242
+ clearPaste();
1243
+ onSubmit(fullContent);
1244
+ return;
1245
+ }
1246
+ if (!input.trim()) return;
1170
1247
  if (fileAutocomplete?.active && filteredFiles.length > 0) {
1171
1248
  const selectedFile = filteredFiles[fileSelectedIndex];
1172
1249
  if (selectedFile) {
@@ -1202,13 +1279,23 @@ function InputPrompt({
1202
1279
  setHistoryIndex(-1);
1203
1280
  setFileAutocomplete(null);
1204
1281
  };
1282
+ const handlePaste = (content) => {
1283
+ const truncated = content.length > MAX_PASTE_SIZE ? content.slice(0, MAX_PASTE_SIZE) : content;
1284
+ const lineCount = truncated.split("\n").length;
1285
+ setPastedContent(truncated, lineCount);
1286
+ };
1205
1287
  const handleChange = async (newValue) => {
1206
- if (ImageInputDetector.containsImageData(newValue)) {
1207
- const imageEvent = ImageInputDetector.extractImageData(newValue);
1208
- if (imageEvent && onImageDetected) {
1209
- const placeholder = await onImageDetected(imageEvent.data);
1210
- setValue(`${placeholder} `);
1211
- return;
1288
+ if (pastedContent) {
1289
+ clearPaste();
1290
+ }
1291
+ if (newValue.length <= IMAGE_DETECTION_MAX_LENGTH) {
1292
+ if (ImageInputDetector.containsImageData(newValue)) {
1293
+ const imageEvent = ImageInputDetector.extractImageData(newValue);
1294
+ if (imageEvent && onImageDetected) {
1295
+ const placeholder = await onImageDetected(imageEvent.data);
1296
+ setValue(`${placeholder} `);
1297
+ return;
1298
+ }
1212
1299
  }
1213
1300
  }
1214
1301
  setValue(newValue);
@@ -1239,6 +1326,8 @@ function InputPrompt({
1239
1326
  value,
1240
1327
  onChange: handleChange,
1241
1328
  onSubmit: handleSubmit,
1329
+ onPaste: handlePaste,
1330
+ pasteIndicator: pastedContent ? `[pasted +${pastedLineCount} lines]` : null,
1242
1331
  placeholder: getPlaceholder(),
1243
1332
  showCursor: !disabled,
1244
1333
  disabled
@@ -2762,9 +2851,327 @@ var CommandHistoryStore = class {
2762
2851
  }
2763
2852
  };
2764
2853
 
2765
- // src/storage/CustomCommandStore.ts
2766
- 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";
2767
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";
2768
3175
  import os from "os";
2769
3176
 
2770
3177
  // src/utils/commandParser.ts
@@ -2927,14 +3334,14 @@ var CustomCommandStore = class {
2927
3334
  const home = os.homedir();
2928
3335
  const root = projectRoot || process.cwd();
2929
3336
  this.globalCommandsDirs = [
2930
- path6.join(home, ".bike4mind", "commands"),
2931
- path6.join(home, ".claude", "commands"),
2932
- path6.join(home, ".claude", "skills")
3337
+ path7.join(home, ".bike4mind", "commands"),
3338
+ path7.join(home, ".claude", "commands"),
3339
+ path7.join(home, ".claude", "skills")
2933
3340
  ];
2934
3341
  this.projectCommandsDirs = [
2935
- path6.join(root, ".bike4mind", "commands"),
2936
- path6.join(root, ".claude", "commands"),
2937
- path6.join(root, ".claude", "skills")
3342
+ path7.join(root, ".bike4mind", "commands"),
3343
+ path7.join(root, ".claude", "commands"),
3344
+ path7.join(root, ".claude", "skills")
2938
3345
  ];
2939
3346
  }
2940
3347
  /**
@@ -2959,7 +3366,7 @@ var CustomCommandStore = class {
2959
3366
  */
2960
3367
  async loadCommandsFromDirectory(directory, source) {
2961
3368
  try {
2962
- const stats = await fs5.stat(directory);
3369
+ const stats = await fs6.stat(directory);
2963
3370
  if (!stats.isDirectory()) {
2964
3371
  return;
2965
3372
  }
@@ -2992,9 +3399,9 @@ var CustomCommandStore = class {
2992
3399
  async findCommandFiles(directory) {
2993
3400
  const files = [];
2994
3401
  try {
2995
- const entries = await fs5.readdir(directory, { withFileTypes: true });
3402
+ const entries = await fs6.readdir(directory, { withFileTypes: true });
2996
3403
  for (const entry of entries) {
2997
- const fullPath = path6.join(directory, entry.name);
3404
+ const fullPath = path7.join(directory, entry.name);
2998
3405
  if (entry.isDirectory()) {
2999
3406
  const subFiles = await this.findCommandFiles(fullPath);
3000
3407
  files.push(...subFiles);
@@ -3014,14 +3421,14 @@ var CustomCommandStore = class {
3014
3421
  * @param source - Source identifier ('global' or 'project')
3015
3422
  */
3016
3423
  async loadCommandFile(filePath, source) {
3017
- const filename = path6.basename(filePath);
3424
+ const filename = path7.basename(filePath);
3018
3425
  const isSkillFile = filename.toLowerCase() === "skill.md";
3019
3426
  const commandName = isSkillFile ? this.extractSkillName(filePath) : extractCommandName(filename);
3020
3427
  if (!commandName) {
3021
3428
  console.warn(`Invalid command filename: ${filename} (must end with .md and have valid name)`);
3022
3429
  return;
3023
3430
  }
3024
- const fileContent = await fs5.readFile(filePath, "utf-8");
3431
+ const fileContent = await fs6.readFile(filePath, "utf-8");
3025
3432
  const command = parseCommandFile(fileContent, filePath, commandName, source);
3026
3433
  this.commands.set(commandName, command);
3027
3434
  }
@@ -3033,7 +3440,7 @@ var CustomCommandStore = class {
3033
3440
  * @returns Skill name or null if invalid
3034
3441
  */
3035
3442
  extractSkillName(filePath) {
3036
- const parentDir = path6.basename(path6.dirname(filePath));
3443
+ const parentDir = path7.basename(path7.dirname(filePath));
3037
3444
  return parentDir && parentDir !== "skills" ? parentDir : null;
3038
3445
  }
3039
3446
  /**
@@ -3095,15 +3502,15 @@ var CustomCommandStore = class {
3095
3502
  */
3096
3503
  async createCommandFile(name, isGlobal = false) {
3097
3504
  const targetDir = isGlobal ? this.globalCommandsDirs[0] : this.projectCommandsDirs[0];
3098
- const filePath = path6.join(targetDir, `${name}.md`);
3099
- const fileExists = await fs5.access(filePath).then(
3505
+ const filePath = path7.join(targetDir, `${name}.md`);
3506
+ const fileExists = await fs6.access(filePath).then(
3100
3507
  () => true,
3101
3508
  () => false
3102
3509
  );
3103
3510
  if (fileExists) {
3104
3511
  throw new Error(`Command file already exists: ${filePath}`);
3105
3512
  }
3106
- await fs5.mkdir(targetDir, { recursive: true });
3513
+ await fs6.mkdir(targetDir, { recursive: true });
3107
3514
  const template = `---
3108
3515
  description: ${name} command
3109
3516
  argument-hint: [args]
@@ -3118,7 +3525,7 @@ You can use:
3118
3525
  - $1, $2, etc. for positional arguments
3119
3526
  - @filename for file references
3120
3527
  `;
3121
- await fs5.writeFile(filePath, template, "utf-8");
3528
+ await fs6.writeFile(filePath, template, "utf-8");
3122
3529
  return filePath;
3123
3530
  }
3124
3531
  };
@@ -4350,7 +4757,7 @@ import { GoogleGenerativeAI } from "@google/generative-ai";
4350
4757
  import OpenAI2 from "openai";
4351
4758
 
4352
4759
  // ../../b4m-core/packages/services/dist/src/importHistoryService/index.js
4353
- import fs6, { unlinkSync } from "fs";
4760
+ import fs7, { unlinkSync as unlinkSync2 } from "fs";
4354
4761
  import yauzl from "yauzl";
4355
4762
  import axios3 from "axios";
4356
4763
 
@@ -6041,8 +6448,8 @@ async function processAndStoreImages(images, context) {
6041
6448
  const buffer = await downloadImage(image);
6042
6449
  const fileType = await fileTypeFromBuffer2(buffer);
6043
6450
  const filename = `${uuidv45()}.${fileType?.ext}`;
6044
- const path19 = await context.imageGenerateStorage.upload(buffer, filename, {});
6045
- return path19;
6451
+ const path21 = await context.imageGenerateStorage.upload(buffer, filename, {});
6452
+ return path21;
6046
6453
  }));
6047
6454
  }
6048
6455
  async function updateQuestAndReturnMarkdown(storedImageUrls, context) {
@@ -7368,8 +7775,8 @@ async function processAndStoreImage(imageUrl, context) {
7368
7775
  const buffer = await downloadImage2(imageUrl);
7369
7776
  const fileType = await fileTypeFromBuffer3(buffer);
7370
7777
  const filename = `${uuidv46()}.${fileType?.ext}`;
7371
- const path19 = await context.imageGenerateStorage.upload(buffer, filename, {});
7372
- return path19;
7778
+ const path21 = await context.imageGenerateStorage.upload(buffer, filename, {});
7779
+ return path21;
7373
7780
  }
7374
7781
  async function generateFullMask(imageBuffer) {
7375
7782
  const sharp = (await import("sharp")).default;
@@ -9002,8 +9409,8 @@ var getHeliocentricCoords = (planet, T) => {
9002
9409
  const sinNode = Math.sin(longNode);
9003
9410
  const x = r * (cosNode * cosOmega - sinNode * sinOmega * cosI);
9004
9411
  const y = r * (sinNode * cosOmega + cosNode * sinOmega * cosI);
9005
- const z147 = r * sinOmega * sinI;
9006
- return { x, y, z: z147, r };
9412
+ const z148 = r * sinOmega * sinI;
9413
+ return { x, y, z: z148, r };
9007
9414
  };
9008
9415
  var getGeocentricEcliptic = (planet, earth) => {
9009
9416
  const dx = planet.x - earth.x;
@@ -9310,33 +9717,33 @@ var planetVisibilityTool = {
9310
9717
  };
9311
9718
 
9312
9719
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/fileRead/index.js
9313
- import { promises as fs7 } from "fs";
9314
- import { existsSync as existsSync4, statSync as statSync4 } from "fs";
9315
- import path7 from "path";
9316
- 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;
9317
9724
  async function readFileContent(params) {
9318
9725
  const { path: filePath, encoding = "utf-8", offset = 0, limit } = params;
9319
- const normalizedPath = path7.normalize(filePath);
9320
- const resolvedPath = path7.resolve(process.cwd(), normalizedPath);
9321
- 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());
9322
9729
  if (!resolvedPath.startsWith(cwd)) {
9323
9730
  throw new Error(`Access denied: Cannot read files outside of current working directory`);
9324
9731
  }
9325
- if (!existsSync4(resolvedPath)) {
9732
+ if (!existsSync5(resolvedPath)) {
9326
9733
  throw new Error(`File not found: ${filePath}`);
9327
9734
  }
9328
9735
  const stats = statSync4(resolvedPath);
9329
9736
  if (stats.isDirectory()) {
9330
9737
  throw new Error(`Path is a directory, not a file: ${filePath}`);
9331
9738
  }
9332
- if (stats.size > MAX_FILE_SIZE2) {
9333
- 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)`);
9334
9741
  }
9335
9742
  const isBinary = await checkIfBinary(resolvedPath);
9336
9743
  if (isBinary && encoding === "utf-8") {
9337
9744
  throw new Error(`File appears to be binary. Use encoding 'base64' to read binary files, or specify a different encoding.`);
9338
9745
  }
9339
- const content = await fs7.readFile(resolvedPath, encoding);
9746
+ const content = await fs8.readFile(resolvedPath, encoding);
9340
9747
  if (typeof content === "string") {
9341
9748
  const lines = content.split("\n");
9342
9749
  const totalLines = lines.length;
@@ -9374,7 +9781,7 @@ ${content}`;
9374
9781
  }
9375
9782
  async function checkIfBinary(filePath) {
9376
9783
  const buffer = Buffer.alloc(8192);
9377
- const fd = await fs7.open(filePath, "r");
9784
+ const fd = await fs8.open(filePath, "r");
9378
9785
  try {
9379
9786
  const { bytesRead } = await fd.read(buffer, 0, 8192, 0);
9380
9787
  const chunk = buffer.slice(0, bytesRead);
@@ -9391,7 +9798,7 @@ var fileReadTool = {
9391
9798
  context.logger.info("\u{1F4C4} FileRead: Reading file", { path: params.path });
9392
9799
  try {
9393
9800
  const content = await readFileContent(params);
9394
- const stats = statSync4(path7.resolve(process.cwd(), path7.normalize(params.path)));
9801
+ const stats = statSync4(path8.resolve(process.cwd(), path8.normalize(params.path)));
9395
9802
  context.logger.info("\u2705 FileRead: Success", {
9396
9803
  path: params.path,
9397
9804
  size: stats.size,
@@ -9435,25 +9842,25 @@ var fileReadTool = {
9435
9842
  };
9436
9843
 
9437
9844
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/createFile/index.js
9438
- import { promises as fs8 } from "fs";
9439
- import { existsSync as existsSync5 } from "fs";
9440
- import path8 from "path";
9845
+ import { promises as fs9 } from "fs";
9846
+ import { existsSync as existsSync6 } from "fs";
9847
+ import path9 from "path";
9441
9848
  async function createFile(params) {
9442
9849
  const { path: filePath, content, createDirectories = true } = params;
9443
- const normalizedPath = path8.normalize(filePath);
9444
- const resolvedPath = path8.resolve(process.cwd(), normalizedPath);
9445
- 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());
9446
9853
  if (!resolvedPath.startsWith(cwd)) {
9447
9854
  throw new Error(`Access denied: Cannot create files outside of current working directory`);
9448
9855
  }
9449
- const fileExists = existsSync5(resolvedPath);
9856
+ const fileExists = existsSync6(resolvedPath);
9450
9857
  const action = fileExists ? "overwritten" : "created";
9451
9858
  if (createDirectories) {
9452
- const dir = path8.dirname(resolvedPath);
9453
- await fs8.mkdir(dir, { recursive: true });
9859
+ const dir = path9.dirname(resolvedPath);
9860
+ await fs9.mkdir(dir, { recursive: true });
9454
9861
  }
9455
- await fs8.writeFile(resolvedPath, content, "utf-8");
9456
- const stats = await fs8.stat(resolvedPath);
9862
+ await fs9.writeFile(resolvedPath, content, "utf-8");
9863
+ const stats = await fs9.stat(resolvedPath);
9457
9864
  const lines = content.split("\n").length;
9458
9865
  return `File ${action} successfully: ${filePath}
9459
9866
  Size: ${stats.size} bytes
@@ -9464,7 +9871,7 @@ var createFileTool = {
9464
9871
  implementation: (context) => ({
9465
9872
  toolFn: async (value) => {
9466
9873
  const params = value;
9467
- const fileExists = existsSync5(path8.resolve(process.cwd(), path8.normalize(params.path)));
9874
+ const fileExists = existsSync6(path9.resolve(process.cwd(), path9.normalize(params.path)));
9468
9875
  context.logger.info(`\u{1F4DD} CreateFile: ${fileExists ? "Overwriting" : "Creating"} file`, {
9469
9876
  path: params.path,
9470
9877
  size: params.content.length
@@ -9506,7 +9913,7 @@ var createFileTool = {
9506
9913
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/globFiles/index.js
9507
9914
  import { glob } from "glob";
9508
9915
  import { stat } from "fs/promises";
9509
- import path9 from "path";
9916
+ import path10 from "path";
9510
9917
  var DEFAULT_IGNORE_PATTERNS = [
9511
9918
  "**/node_modules/**",
9512
9919
  "**/.git/**",
@@ -9522,7 +9929,7 @@ var DEFAULT_IGNORE_PATTERNS = [
9522
9929
  async function findFiles(params) {
9523
9930
  const { pattern, dir_path, case_sensitive = true, respect_git_ignore = true } = params;
9524
9931
  const baseCwd = process.cwd();
9525
- 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;
9526
9933
  if (!targetDir.startsWith(baseCwd)) {
9527
9934
  throw new Error(`Access denied: Cannot search outside of current working directory`);
9528
9935
  }
@@ -9562,7 +9969,7 @@ async function findFiles(params) {
9562
9969
  const summary = `Found ${filesWithStats.length} file(s)${truncated ? ` (showing first ${MAX_RESULTS})` : ""} matching: ${pattern}`;
9563
9970
  const dirInfo = dir_path ? `
9564
9971
  Directory: ${dir_path}` : "";
9565
- 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");
9566
9973
  return `${summary}${dirInfo}
9567
9974
 
9568
9975
  ${filesList}`;
@@ -9616,27 +10023,48 @@ var globFilesTool = {
9616
10023
 
9617
10024
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/grepSearch/index.js
9618
10025
  import { stat as stat2 } from "fs/promises";
9619
- import path10 from "path";
9620
- 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";
9621
10029
  import { promisify } from "util";
9622
10030
  import { createRequire } from "module";
9623
10031
  var execFileAsync = promisify(execFile);
9624
10032
  var require2 = createRequire(import.meta.url);
10033
+ var cachedRgPath = null;
9625
10034
  function getRipgrepPath() {
10035
+ if (cachedRgPath)
10036
+ return cachedRgPath;
9626
10037
  try {
9627
10038
  const ripgrepPath = require2.resolve("@vscode/ripgrep");
9628
- const ripgrepDir = path10.dirname(ripgrepPath);
9629
- 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;
9630
10055
  return rgBinary;
9631
10056
  } catch (error) {
10057
+ if (error instanceof Error && error.message.includes("ripgrep binary not found")) {
10058
+ throw error;
10059
+ }
9632
10060
  throw new Error("ripgrep is not available. Please install @vscode/ripgrep: pnpm add @vscode/ripgrep --filter @bike4mind/services");
9633
10061
  }
9634
10062
  }
9635
10063
  function isPathWithinWorkspace(targetPath, baseCwd) {
9636
- const resolvedTarget = path10.resolve(targetPath);
9637
- const resolvedBase = path10.resolve(baseCwd);
9638
- const relativePath = path10.relative(resolvedBase, resolvedTarget);
9639
- 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);
9640
10068
  }
9641
10069
  function convertGlobToRipgrepGlobs(globPattern) {
9642
10070
  if (!globPattern || globPattern === "**/*") {
@@ -9652,7 +10080,7 @@ function convertGlobToRipgrepGlobs(globPattern) {
9652
10080
  async function searchFiles2(params) {
9653
10081
  const { pattern, dir_path, include } = params;
9654
10082
  const baseCwd = process.cwd();
9655
- const targetDir = dir_path ? path10.resolve(baseCwd, dir_path) : baseCwd;
10083
+ const targetDir = dir_path ? path11.resolve(baseCwd, dir_path) : baseCwd;
9656
10084
  if (!isPathWithinWorkspace(targetDir, baseCwd)) {
9657
10085
  throw new Error(`Path validation failed: "${dir_path}" resolves outside the allowed workspace directory`);
9658
10086
  }
@@ -9714,7 +10142,7 @@ async function searchFiles2(params) {
9714
10142
  if (item.type === "match") {
9715
10143
  const match = item;
9716
10144
  allMatches.push({
9717
- 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),
9718
10146
  lineNumber: match.data.line_number,
9719
10147
  line: match.data.lines.text.trimEnd()
9720
10148
  // Remove trailing newline
@@ -9807,18 +10235,18 @@ var grepSearchTool = {
9807
10235
  };
9808
10236
 
9809
10237
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/deleteFile/index.js
9810
- import { promises as fs9 } from "fs";
9811
- import { existsSync as existsSync6, statSync as statSync5 } from "fs";
9812
- 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";
9813
10241
  async function deleteFile(params) {
9814
10242
  const { path: filePath, recursive = false } = params;
9815
- const normalizedPath = path11.normalize(filePath);
9816
- const resolvedPath = path11.resolve(process.cwd(), normalizedPath);
9817
- 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());
9818
10246
  if (!resolvedPath.startsWith(cwd)) {
9819
10247
  throw new Error(`Access denied: Cannot delete files outside of current working directory`);
9820
10248
  }
9821
- if (!existsSync6(resolvedPath)) {
10249
+ if (!existsSync8(resolvedPath)) {
9822
10250
  throw new Error(`File or directory not found: ${filePath}`);
9823
10251
  }
9824
10252
  const stats = statSync5(resolvedPath);
@@ -9828,10 +10256,10 @@ async function deleteFile(params) {
9828
10256
  throw new Error(`Path is a directory: ${filePath}. Use recursive=true to delete directories and their contents.`);
9829
10257
  }
9830
10258
  if (isDirectory) {
9831
- await fs9.rm(resolvedPath, { recursive: true, force: true });
10259
+ await fs10.rm(resolvedPath, { recursive: true, force: true });
9832
10260
  return `Directory deleted successfully: ${filePath}`;
9833
10261
  } else {
9834
- await fs9.unlink(resolvedPath);
10262
+ await fs10.unlink(resolvedPath);
9835
10263
  return `File deleted successfully: ${filePath}
9836
10264
  Size: ${size} bytes`;
9837
10265
  }
@@ -9841,8 +10269,8 @@ var deleteFileTool = {
9841
10269
  implementation: (context) => ({
9842
10270
  toolFn: async (value) => {
9843
10271
  const params = value;
9844
- const resolvedPath = path11.resolve(process.cwd(), path11.normalize(params.path));
9845
- 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();
9846
10274
  context.logger.info(`\u{1F5D1}\uFE0F DeleteFile: Deleting ${isDirectory ? "directory" : "file"}`, {
9847
10275
  path: params.path,
9848
10276
  recursive: params.recursive
@@ -9887,13 +10315,13 @@ function formatSearchResults(files) {
9887
10315
  const notes = file.notes ? `
9888
10316
  Notes: ${file.notes}` : "";
9889
10317
  const fileType = file.type || "FILE";
9890
- return `${index + 1}. **${file.fileName}**
10318
+ return `${index + 1}. **${file.fileName}** (ID: ${file.id})
9891
10319
  Type: ${fileType} | MIME: ${file.mimeType}
9892
10320
  Tags: ${tags}${notes}`;
9893
10321
  });
9894
10322
  return `Found ${files.length} document(s) in your knowledge base:
9895
10323
 
9896
- ` + 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.*";
9897
10325
  }
9898
10326
  var knowledgeBaseSearchTool = {
9899
10327
  name: "search_knowledge_base",
@@ -9920,6 +10348,8 @@ var knowledgeBaseSearchTool = {
9920
10348
  by: "fileName",
9921
10349
  direction: "asc"
9922
10350
  }, {
10351
+ textSearch: true,
10352
+ // Search across fileName + tags + notes for better recall
9923
10353
  includeShared: true,
9924
10354
  // Include owned + explicitly shared + org-shared files
9925
10355
  userGroups: context.user.groups || []
@@ -9934,18 +10364,18 @@ var knowledgeBaseSearchTool = {
9934
10364
  },
9935
10365
  toolSchema: {
9936
10366
  name: "search_knowledge_base",
9937
- 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.",
9938
10368
  parameters: {
9939
10369
  type: "object",
9940
10370
  properties: {
9941
10371
  query: {
9942
10372
  type: "string",
9943
- 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."
9944
10374
  },
9945
10375
  tags: {
9946
10376
  type: "array",
9947
10377
  items: { type: "string" },
9948
- 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.'
9949
10379
  },
9950
10380
  file_type: {
9951
10381
  type: "string",
@@ -9965,64 +10395,1686 @@ var knowledgeBaseSearchTool = {
9965
10395
  })
9966
10396
  };
9967
10397
 
9968
- // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/bashExecute/index.js
9969
- import { spawn } from "child_process";
9970
- import path12 from "path";
9971
- var DEFAULT_TIMEOUT_MS = 6e4;
9972
- var MAX_OUTPUT_SIZE = 100 * 1024;
9973
- var DANGEROUS_PATTERNS = [
9974
- // Destructive file operations
9975
- {
9976
- pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|.*\s+-[a-zA-Z]*r).*\//i,
9977
- reason: "Recursive delete with path",
9978
- block: true
9979
- },
9980
- {
9981
- pattern: /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|.*\s+-[a-zA-Z]*f).*--no-preserve-root/i,
9982
- reason: "Force delete without preserve root",
9983
- block: true
9984
- },
9985
- {
9986
- pattern: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+\//i,
9987
- reason: "Recursive force delete on root paths",
9988
- block: true
9989
- },
9990
- {
9991
- pattern: /\brm\s+-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*\s+\//i,
9992
- reason: "Force recursive delete on root paths",
9993
- block: true
9994
- },
9995
- // System-level dangerous commands
9996
- { pattern: /\bsudo\b/i, reason: "Elevated privileges (sudo)", block: true },
9997
- { pattern: /\bsu\s+(-|root)/i, reason: "Switch to root user", block: true },
9998
- { pattern: /\bchmod\s+777\b/i, reason: "Overly permissive chmod", block: false },
9999
- { pattern: /\bchown\s+-R\s+root/i, reason: "Recursive chown to root", block: true },
10000
- // Disk/partition operations
10001
- { pattern: /\bmkfs\b/i, reason: "Filesystem creation", block: true },
10002
- { pattern: /\bfdisk\b/i, reason: "Disk partitioning", block: true },
10003
- { pattern: /\bdd\s+.*of=\/dev\//i, reason: "Direct disk write", block: true },
10004
- // Network attacks
10005
- { pattern: /\b(nc|netcat)\s+.*-e\s+\/bin\/(ba)?sh/i, reason: "Reverse shell attempt", block: true },
10006
- { pattern: /\bcurl\s+.*\|\s*(ba)?sh/i, reason: "Piping remote script to shell", block: true },
10007
- { pattern: /\bwget\s+.*\|\s*(ba)?sh/i, reason: "Piping remote script to shell", block: true },
10008
- // Fork bombs and resource exhaustion
10009
- { pattern: /:\(\)\s*\{\s*:\|:&\s*\}\s*;/i, reason: "Fork bomb", block: true },
10010
- { pattern: /\bwhile\s+true.*do.*done.*&/i, reason: "Infinite loop in background", block: false },
10011
- // Credential/sensitive data access
10012
- { pattern: /\/etc\/shadow/i, reason: "Access to shadow file", block: true },
10013
- { pattern: /\/etc\/passwd.*>/i, reason: "Modifying passwd file", block: true },
10014
- { pattern: /\baws\s+.*--profile\s+/i, reason: "AWS profile access", block: false },
10015
- // History/log manipulation
10016
- { pattern: /\bhistory\s+-c\b/i, reason: "Clearing shell history", block: false },
10017
- { pattern: />\s*\/var\/log\//i, reason: "Overwriting system logs", block: true },
10018
- // Dangerous redirects
10019
- { pattern: />\s*\/dev\/sda/i, reason: "Writing to block device", block: true },
10020
- { pattern: />\s*\/dev\/null.*2>&1.*</i, reason: "Potentially hiding output", block: false },
10021
- // Environment manipulation
10022
- { pattern: /\bexport\s+PATH\s*=\s*[^$]/i, reason: "Overwriting PATH", block: false },
10023
- { pattern: /\bexport\s+LD_PRELOAD/i, reason: "LD_PRELOAD manipulation", block: true },
10024
- // Process/system control
10025
- { pattern: /\bkill\s+-9\s+(-1|1)\b/i, reason: "Killing all processes", block: true },
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 },
10026
12078
  { pattern: /\bkillall\s+-9\b/i, reason: "Force killing processes", block: false },
10027
12079
  { pattern: /\bshutdown\b/i, reason: "System shutdown", block: true },
10028
12080
  { pattern: /\breboot\b/i, reason: "System reboot", block: true },
@@ -10151,7 +12203,7 @@ async function executeBashCommand(params) {
10151
12203
  };
10152
12204
  }
10153
12205
  const baseCwd = process.cwd();
10154
- const targetCwd = relativeCwd ? path12.resolve(baseCwd, relativeCwd) : baseCwd;
12206
+ const targetCwd = relativeCwd ? path13.resolve(baseCwd, relativeCwd) : baseCwd;
10155
12207
  const effectiveTimeout = Math.min(timeout, 5 * 60 * 1e3);
10156
12208
  return new Promise((resolve3) => {
10157
12209
  let stdout = "";
@@ -10214,7 +12266,7 @@ async function executeBashCommand(params) {
10214
12266
  });
10215
12267
  });
10216
12268
  }
10217
- function formatResult(result, command) {
12269
+ function formatResult2(result, command) {
10218
12270
  const parts = [];
10219
12271
  parts.push(`$ ${command}`);
10220
12272
  parts.push("");
@@ -10268,7 +12320,7 @@ var bashExecuteTool = {
10268
12320
  }
10269
12321
  try {
10270
12322
  const result = await executeBashCommand(params);
10271
- const formattedResult = formatResult(result, params.command);
12323
+ const formattedResult = formatResult2(result, params.command);
10272
12324
  context.logger.info("Bash: Command completed", {
10273
12325
  exitCode: result.exitCode,
10274
12326
  timedOut: result.timedOut,
@@ -11154,9 +13206,9 @@ function parseQuery(query) {
11154
13206
  }
11155
13207
 
11156
13208
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/editLocalFile/index.js
11157
- import { promises as fs10 } from "fs";
11158
- import { existsSync as existsSync7 } from "fs";
11159
- import path13 from "path";
13209
+ import { promises as fs11 } from "fs";
13210
+ import { existsSync as existsSync9 } from "fs";
13211
+ import path14 from "path";
11160
13212
  import { diffLines as diffLines3 } from "diff";
11161
13213
  function generateDiff(original, modified) {
11162
13214
  const differences = diffLines3(original, modified);
@@ -11180,16 +13232,16 @@ function generateDiff(original, modified) {
11180
13232
  }
11181
13233
  async function editLocalFile(params) {
11182
13234
  const { path: filePath, old_string, new_string } = params;
11183
- const normalizedPath = path13.normalize(filePath);
11184
- const resolvedPath = path13.resolve(process.cwd(), normalizedPath);
11185
- 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());
11186
13238
  if (!resolvedPath.startsWith(cwd)) {
11187
13239
  throw new Error(`Access denied: Cannot edit files outside of current working directory`);
11188
13240
  }
11189
- if (!existsSync7(resolvedPath)) {
13241
+ if (!existsSync9(resolvedPath)) {
11190
13242
  throw new Error(`File not found: ${filePath}`);
11191
13243
  }
11192
- const currentContent = await fs10.readFile(resolvedPath, "utf-8");
13244
+ const currentContent = await fs11.readFile(resolvedPath, "utf-8");
11193
13245
  if (!currentContent.includes(old_string)) {
11194
13246
  const preview = old_string.length > 100 ? old_string.substring(0, 100) + "..." : old_string;
11195
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}"`);
@@ -11199,7 +13251,7 @@ async function editLocalFile(params) {
11199
13251
  throw new Error(`Found ${occurrences} occurrences of the string to replace. Please provide a more specific old_string that matches exactly one location.`);
11200
13252
  }
11201
13253
  const newContent = currentContent.replace(old_string, new_string);
11202
- await fs10.writeFile(resolvedPath, newContent, "utf-8");
13254
+ await fs11.writeFile(resolvedPath, newContent, "utf-8");
11203
13255
  const diffResult = generateDiff(old_string, new_string);
11204
13256
  return `File edited successfully: ${filePath}
11205
13257
  Changes: +${diffResult.additions} lines, -${diffResult.deletions} lines
@@ -11437,7 +13489,7 @@ async function getFileStats(files, since, filterPath) {
11437
13489
  });
11438
13490
  });
11439
13491
  }
11440
- function formatResult2(result) {
13492
+ function formatResult3(result) {
11441
13493
  const parts = [];
11442
13494
  if (result.error) {
11443
13495
  parts.push(`Error: ${result.error}`);
@@ -11512,7 +13564,7 @@ var recentChangesTool = {
11512
13564
  }
11513
13565
  try {
11514
13566
  const result = await getRecentChanges(params);
11515
- const formattedResult = formatResult2(result);
13567
+ const formattedResult = formatResult3(result);
11516
13568
  context.logger.info("RecentChanges: Retrieved file changes", {
11517
13569
  totalFiles: result.totalFiles,
11518
13570
  displayedFiles: result.files.length,
@@ -11610,8 +13662,14 @@ var b4mTools = {
11610
13662
  sunrise_sunset: sunriseSunsetTool,
11611
13663
  iss_tracker: issTrackerTool,
11612
13664
  planet_visibility: planetVisibilityTool,
11613
- // Knowledge base search
11614
- 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
11615
13673
  };
11616
13674
  var cliOnlyTools = {
11617
13675
  // File operation tools
@@ -11653,6 +13711,20 @@ var generateTools = (userId, user, logger2, { db }, storage, imageGenerateStorag
11653
13711
  [key]: tool.implementation(context, config[key])
11654
13712
  }), {});
11655
13713
  };
13714
+ function normalizeToolParameters(rest) {
13715
+ const rawParameters = rest?.input_schema ?? rest?.inputSchema ?? rest?.parameters;
13716
+ if (rawParameters && typeof rawParameters === "object") {
13717
+ return {
13718
+ ...rawParameters,
13719
+ properties: rawParameters.properties ?? {}
13720
+ };
13721
+ }
13722
+ return {
13723
+ type: "object",
13724
+ properties: {},
13725
+ additionalProperties: true
13726
+ };
13727
+ }
11656
13728
  var ATLASSIAN_RECONNECT_MESSAGE = "\u26A0\uFE0F Your Atlassian connection has expired.\n\nPlease reconnect your Atlassian account in Settings > Connected Apps to continue using Confluence and Jira tools.";
11657
13729
  var generateMcpTools = async (mcpData) => {
11658
13730
  let tools;
@@ -11679,12 +13751,7 @@ var generateMcpTools = async (mcpData) => {
11679
13751
  const namespacedToolName = `${serverName}__${originalToolName}`;
11680
13752
  const providerMetadata = getMcpProviderMetadata(serverName);
11681
13753
  const fallbackDescription = providerMetadata?.defaultToolDescriptions?.[originalToolName] ?? "";
11682
- const rawParameters = rest?.input_schema ?? rest?.inputSchema ?? rest?.parameters;
11683
- const parameters = rawParameters && typeof rawParameters === "object" ? rawParameters : {
11684
- type: "object",
11685
- properties: {},
11686
- additionalProperties: true
11687
- };
13754
+ const parameters = normalizeToolParameters(rest);
11688
13755
  const optionTools = {
11689
13756
  toolFn: async (args) => {
11690
13757
  Logger.debug(`Calling ${originalToolName} tool via ${mcpData.serverName}`, args);
@@ -11737,94 +13804,8 @@ var generateMcpTools = async (mcpData) => {
11737
13804
  return result;
11738
13805
  };
11739
13806
 
11740
- // ../../b4m-core/packages/services/dist/src/llm/agents/ExploreAgent.js
11741
- var ExploreAgent = (config) => ({
11742
- name: "explore",
11743
- description: "Fast research, exploration, and information search",
11744
- model: config?.model ?? "claude-3-5-haiku-20241022",
11745
- defaultThoroughness: config?.defaultThoroughness ?? "medium",
11746
- maxIterations: { quick: 2, medium: 5, very_thorough: 10 },
11747
- deniedTools: [
11748
- "image_generation",
11749
- "edit_image",
11750
- "deep_research",
11751
- "delegate_to_agent",
11752
- ...config?.extraDeniedTools ?? []
11753
- ],
11754
- allowedTools: config?.extraAllowedTools,
11755
- systemPrompt: `You are a research and exploration specialist. Your job is to search and analyze information efficiently.
11756
-
11757
- ## Focus Areas
11758
- - Finding relevant information across knowledge bases and the web
11759
- - Understanding context and patterns
11760
- - Providing clear, concise summaries
11761
-
11762
- ## Tool Usage
11763
- Use these tools strategically:
11764
- - \`web_search\` - Search the web for information
11765
- - \`web_fetch\` - Fetch and read full webpage content
11766
- - \`search_knowledge_base\` - Search the user's knowledge bases
11767
- - \`current_datetime\` - Get the current date and time
11768
-
11769
- ## Search Strategy
11770
- 1. Start with targeted searches using specific keywords
11771
- 2. Use web_fetch to read full content from promising results
11772
- 3. Cross-reference findings from multiple sources
11773
- 4. Check knowledge bases for relevant internal information
11774
-
11775
- ## Output Format
11776
- Provide a clear summary including:
11777
- 1. What you found (key facts, sources, patterns)
11778
- 2. Key insights or observations
11779
- 3. Relevant source references
11780
-
11781
- Be thorough but concise. Your summary will be used by the main agent.`
11782
- });
11783
-
11784
- // ../../b4m-core/packages/services/dist/src/llm/agents/PlanAgent.js
11785
- var PlanAgent = (config) => ({
11786
- name: "plan",
11787
- description: "Task breakdown and implementation planning",
11788
- model: config?.model ?? "claude-3-5-haiku-20241022",
11789
- defaultThoroughness: config?.defaultThoroughness ?? "medium",
11790
- maxIterations: { quick: 3, medium: 7, very_thorough: 12 },
11791
- deniedTools: [
11792
- "image_generation",
11793
- "edit_image",
11794
- "deep_research",
11795
- "delegate_to_agent",
11796
- ...config?.extraDeniedTools ?? []
11797
- ],
11798
- allowedTools: config?.extraAllowedTools,
11799
- systemPrompt: `You are a task planning specialist. Your job is to break down complex tasks into clear, actionable steps.
11800
-
11801
- ## Focus Areas
11802
- - Identifying dependencies and blockers
11803
- - Creating logical sequence of steps
11804
- - Researching context before planning
11805
-
11806
- ## Process
11807
- 1. First, research the topic to understand the current landscape
11808
- 2. Identify what already exists vs. what needs to be done
11809
- 3. Break down work into discrete, actionable steps
11810
- 4. Order steps by dependencies
11811
-
11812
- ## Output Format
11813
- Provide a structured plan:
11814
-
11815
- ### Prerequisites
11816
- - What must exist before starting
11817
-
11818
- ### Steps
11819
- 1. Step with clear deliverable
11820
- 2. Next step (depends on: Step 1)
11821
- ...
11822
-
11823
- ### Risks & Considerations
11824
- - Potential issues to watch for
11825
-
11826
- Be specific and actionable. Your plan will be used by the main agent.`
11827
- });
13807
+ // ../../b4m-core/packages/services/dist/src/llm/agents/ServerSubagentOrchestrator.js
13808
+ var SUBAGENT_TIMEOUT_MS = 5 * 60 * 1e3;
11828
13809
 
11829
13810
  // ../../b4m-core/packages/services/dist/src/llm/agents/CodeReviewAgent.js
11830
13811
  var CodeReviewAgent = (config) => ({
@@ -11919,12 +13900,27 @@ var GithubManagerAgent = (config) => ({
11919
13900
  exclusiveMcpServers: ["github"],
11920
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.
11921
13902
 
11922
- ## First Step: Resolve Repository Context
11923
- ALWAYS start by calling the \`github__current_user\` tool. The response includes:
13903
+ ## First Step: ALWAYS Resolve Repository Context Automatically
13904
+ Before doing ANYTHING else, call the \`github__current_user\` tool. The response includes:
11924
13905
  - **selected_repositories**: The list of repositories the user has enabled for AI access (in \`owner/repo\` format)
11925
13906
  - **user**: The authenticated user's profile (login, name, etc.)
11926
13907
 
11927
- Use this to resolve the owner and repo name when the user does not specify them. If only one repository is selected, default to that repository. If multiple are selected and the user's request is ambiguous, mention the available repositories and ask for clarification via your response.
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.
11928
13924
 
11929
13925
  ## Capabilities
11930
13926
 
@@ -11965,7 +13961,7 @@ Use this to resolve the owner and repo name when the user does not specify them.
11965
13961
  ## Output Format
11966
13962
  Provide a clear summary of actions taken:
11967
13963
  1. What was done (created, searched, merged, etc.)
11968
- 2. Links to relevant items (e.g., owner/repo#123, PR URLs)
13964
+ 2. Always provide links to relevant items (e.g., owner/repo#123, PR URLs)
11969
13965
  3. Any issues or warnings encountered
11970
13966
 
11971
13967
  Be precise with repository names, issue numbers, and PR numbers. Your results will be used by the main agent.`
@@ -11975,8 +13971,9 @@ Be precise with repository names, issue numbers, and PR numbers. Your results wi
11975
13971
  var ServerAgentStore = class {
11976
13972
  constructor() {
11977
13973
  const builtInAgents = [
11978
- ExploreAgent(),
11979
- PlanAgent(),
13974
+ // For now disable explore and plan agents as it is much faster if the parent llm handles this.
13975
+ // ExploreAgent(),
13976
+ // PlanAgent(),
11980
13977
  CodeReviewAgent(),
11981
13978
  ProjectManagerAgent(),
11982
13979
  GithubManagerAgent()
@@ -12017,50 +14014,48 @@ var serverAgentStore = new ServerAgentStore();
12017
14014
  import throttle2 from "lodash/throttle.js";
12018
14015
 
12019
14016
  // ../../b4m-core/packages/services/dist/src/llm/ChatCompletionFeatures.js
12020
- import { z as z139 } from "zod";
14017
+ import { z as z140 } from "zod";
12021
14018
  import uniq4 from "lodash/uniq.js";
12022
- var QuestStartBodySchema = z139.object({
12023
- userId: z139.string(),
12024
- sessionId: z139.string(),
12025
- questId: z139.string(),
12026
- message: z139.string(),
12027
- messageFileIds: z139.array(z139.string()),
12028
- historyCount: z139.number(),
12029
- 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()),
12030
14027
  params: ChatCompletionCreateInputSchema,
12031
14028
  dashboardParams: DashboardParamsSchema.optional(),
12032
- enableQuestMaster: z139.boolean().optional(),
12033
- enableMementos: z139.boolean().optional(),
12034
- enableArtifacts: z139.boolean().optional(),
12035
- enableAgents: z139.boolean().optional(),
12036
- 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(),
12037
14034
  promptMeta: PromptMetaZodSchema,
12038
- tools: z139.array(z139.union([b4mLLMTools, z139.string()])).optional(),
12039
- mcpServers: z139.array(z139.string()).optional(),
12040
- projectId: z139.string().optional(),
12041
- 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(),
12042
14039
  questMaster: QuestMasterParamsSchema.optional(),
12043
- toolPromptId: z139.string().optional(),
14040
+ toolPromptId: z140.string().optional(),
12044
14041
  researchMode: ResearchModeParamsSchema.optional(),
12045
- fallbackModel: z139.string().optional(),
12046
- embeddingModel: z139.string().optional(),
12047
- queryComplexity: z139.string(),
14042
+ fallbackModel: z140.string().optional(),
14043
+ embeddingModel: z140.string().optional(),
14044
+ queryComplexity: z140.string(),
12048
14045
  imageConfig: GenerateImageToolCallSchema.optional(),
12049
- deepResearchConfig: z139.object({
12050
- maxDepth: z139.number().optional(),
12051
- duration: z139.number().optional(),
14046
+ deepResearchConfig: z140.object({
14047
+ maxDepth: z140.number().optional(),
14048
+ duration: z140.number().optional(),
12052
14049
  // Note: searchers are passed via ToolContext and not through this API schema
12053
- searchers: z139.array(z139.any()).optional()
14050
+ searchers: z140.array(z140.any()).optional()
12054
14051
  }).optional(),
12055
- extraContextMessages: z139.array(z139.object({
12056
- role: z139.enum(["user", "assistant", "system", "function", "tool"]),
12057
- content: z139.union([z139.string(), z139.array(z139.any())]),
12058
- 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()
12059
14056
  })).optional(),
12060
14057
  /** User's timezone (IANA format, e.g., "America/New_York") */
12061
- timezone: z139.string().optional(),
12062
- /** Pre-fetched API key table from invoke phase — avoids redundant DB call in process (#6616 P1-a) */
12063
- apiKeyTable: z139.record(z139.string(), z139.string().nullable()).optional()
14058
+ timezone: z140.string().optional()
12064
14059
  });
12065
14060
 
12066
14061
  // ../../b4m-core/packages/services/dist/src/llm/StatusManager.js
@@ -12560,89 +14555,89 @@ var BUILT_IN_TOOL_SET = new Set(b4mLLMTools.options);
12560
14555
  import axios8 from "axios";
12561
14556
  import { fileTypeFromBuffer as fileTypeFromBuffer4 } from "file-type";
12562
14557
  import { v4 as uuidv47 } from "uuid";
12563
- import { z as z140 } from "zod";
14558
+ import { z as z141 } from "zod";
12564
14559
  import { fromZodError as fromZodError2 } from "zod-validation-error";
12565
14560
  var ImageGenerationBodySchema = OpenAIImageGenerationInput.extend({
12566
- sessionId: z140.string(),
12567
- questId: z140.string(),
12568
- userId: z140.string(),
12569
- prompt: z140.string(),
12570
- organizationId: z140.string().nullable().optional(),
12571
- safety_tolerance: z140.number().min(BFL_SAFETY_TOLERANCE.MIN).max(BFL_SAFETY_TOLERANCE.MAX).optional().default(BFL_SAFETY_TOLERANCE.DEFAULT),
12572
- prompt_upsampling: z140.boolean().optional().default(false),
12573
- seed: z140.number().nullable().optional(),
12574
- output_format: z140.enum(["jpeg", "png"]).nullable().optional().default("png"),
12575
- width: z140.number().optional(),
12576
- height: z140.number().optional(),
12577
- aspect_ratio: z140.string().optional(),
12578
- 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()
12579
14574
  });
12580
14575
 
12581
14576
  // ../../b4m-core/packages/services/dist/src/llm/VideoGeneration.js
12582
14577
  import axios9 from "axios";
12583
14578
  import { v4 as uuidv48 } from "uuid";
12584
- import { z as z141 } from "zod";
14579
+ import { z as z142 } from "zod";
12585
14580
  import { fromZodError as fromZodError3 } from "zod-validation-error";
12586
- var VideoGenerationBodySchema = z141.object({
12587
- sessionId: z141.string(),
12588
- questId: z141.string(),
12589
- userId: z141.string(),
12590
- prompt: z141.string(),
12591
- model: z141.nativeEnum(VideoModels).default(VideoModels.SORA_2),
12592
- seconds: z141.union([z141.literal(4), z141.literal(8), z141.literal(12)]).default(4),
12593
- size: z141.enum(["720x1280", "1280x720", "1024x1792", "1792x1024"]).default(VIDEO_SIZE_CONSTRAINTS.SORA.defaultSize),
12594
- 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()
12595
14590
  });
12596
14591
 
12597
14592
  // ../../b4m-core/packages/services/dist/src/llm/ImageEdit.js
12598
14593
  import axios10 from "axios";
12599
14594
  import { fileTypeFromBuffer as fileTypeFromBuffer5 } from "file-type";
12600
14595
  import { v4 as uuidv49 } from "uuid";
12601
- import { z as z142 } from "zod";
14596
+ import { z as z143 } from "zod";
12602
14597
  import { fromZodError as fromZodError4 } from "zod-validation-error";
12603
14598
  var ImageEditBodySchema = OpenAIImageGenerationInput.extend({
12604
- sessionId: z142.string(),
12605
- questId: z142.string(),
12606
- userId: z142.string(),
12607
- prompt: z142.string(),
12608
- organizationId: z142.string().nullable().optional(),
12609
- safety_tolerance: z142.number().min(BFL_SAFETY_TOLERANCE.MIN).max(BFL_SAFETY_TOLERANCE.MAX).optional().default(BFL_SAFETY_TOLERANCE.DEFAULT),
12610
- prompt_upsampling: z142.boolean().optional().default(false),
12611
- seed: z142.number().nullable().optional(),
12612
- output_format: z142.enum(["jpeg", "png"]).optional().default("png"),
12613
- width: z142.number().optional(),
12614
- height: z142.number().optional(),
12615
- aspect_ratio: z142.string().optional(),
12616
- size: z142.string().optional(),
12617
- fabFileIds: z142.array(z142.string()).optional(),
12618
- image: z142.string()
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()
12619
14614
  });
12620
14615
 
12621
14616
  // ../../b4m-core/packages/services/dist/src/llm/refineText.js
12622
- import { z as z143 } from "zod";
12623
- var refineTextLLMSchema = z143.object({
12624
- text: z143.string(),
12625
- 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()
12626
14621
  // tone: z.enum(['formal', 'informal', 'neutral']).optional(),
12627
14622
  // style: z.enum(['technical', 'creative', 'academic', 'business', 'narrative']).optional(),
12628
14623
  });
12629
14624
 
12630
14625
  // ../../b4m-core/packages/services/dist/src/llm/MementoEvaluationService.js
12631
- import { z as z144 } from "zod";
12632
- var SingleMementoEvalSchema = z144.object({
12633
- 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),
12634
14629
  // 1-10 scale for personal info importance
12635
- summary: z144.string(),
12636
- tags: z144.array(z144.string()).optional()
14630
+ summary: z145.string(),
14631
+ tags: z145.array(z145.string()).optional()
12637
14632
  });
12638
- var MementoEvalResponseSchema = z144.object({
12639
- isPersonal: z144.boolean(),
12640
- mementos: z144.array(SingleMementoEvalSchema).optional()
14633
+ var MementoEvalResponseSchema = z145.object({
14634
+ isPersonal: z145.boolean(),
14635
+ mementos: z145.array(SingleMementoEvalSchema).optional()
12641
14636
  // Array of distinct personal information
12642
14637
  });
12643
14638
 
12644
14639
  // ../../b4m-core/packages/services/dist/src/llm/SmallLLMService.js
12645
- import { z as z145 } from "zod";
14640
+ import { z as z146 } from "zod";
12646
14641
 
12647
14642
  // ../../b4m-core/packages/services/dist/src/auth/AccessTokenGeneratorService.js
12648
14643
  import jwt2 from "jsonwebtoken";
@@ -12661,10 +14656,10 @@ var ToolErrorType;
12661
14656
  // src/utils/diffPreview.ts
12662
14657
  import * as Diff from "diff";
12663
14658
  import { readFile } from "fs/promises";
12664
- import { existsSync as existsSync8 } from "fs";
14659
+ import { existsSync as existsSync10 } from "fs";
12665
14660
  async function generateFileDiffPreview(args) {
12666
14661
  try {
12667
- if (!existsSync8(args.path)) {
14662
+ if (!existsSync10(args.path)) {
12668
14663
  const lines2 = args.content.split("\n");
12669
14664
  const preview = lines2.slice(0, 20).join("\n");
12670
14665
  const hasMore = lines2.length > 20;
@@ -12702,10 +14697,10 @@ ${diffLines4.join("\n")}`;
12702
14697
  }
12703
14698
  async function generateFileDeletePreview(args) {
12704
14699
  try {
12705
- if (!existsSync8(args.path)) {
14700
+ if (!existsSync10(args.path)) {
12706
14701
  return `[File does not exist: ${args.path}]`;
12707
14702
  }
12708
- 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));
12709
14704
  return `[File will be deleted]
12710
14705
 
12711
14706
  Path: ${args.path}
@@ -12758,6 +14753,10 @@ var ServerToolExecutor = class {
12758
14753
  };
12759
14754
 
12760
14755
  // src/llm/ToolRouter.ts
14756
+ var wsToolExecutor = null;
14757
+ function setWebSocketToolExecutor(executor) {
14758
+ wsToolExecutor = executor;
14759
+ }
12761
14760
  var SERVER_TOOLS = ["weather_info", "web_search", "web_fetch"];
12762
14761
  var LOCAL_TOOLS = [
12763
14762
  "file_read",
@@ -12780,7 +14779,15 @@ function isLocalTool(toolName) {
12780
14779
  }
12781
14780
  async function executeTool(toolName, input, apiClient, localToolFn) {
12782
14781
  if (isServerTool(toolName)) {
12783
- 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`);
12784
14791
  const executor = new ServerToolExecutor(apiClient);
12785
14792
  return await executor.executeTool(toolName, input);
12786
14793
  } else if (isLocalTool(toolName)) {
@@ -12935,7 +14942,7 @@ function buildHookContext(params) {
12935
14942
  }
12936
14943
 
12937
14944
  // src/agents/types.ts
12938
- import { z as z146 } from "zod";
14945
+ import { z as z147 } from "zod";
12939
14946
  var HookBlockedError = class extends Error {
12940
14947
  constructor(toolName, reason) {
12941
14948
  super(`Hook blocked execution of ${toolName}: ${reason || "No reason provided"}`);
@@ -12947,40 +14954,40 @@ var ALWAYS_DENIED_FOR_AGENTS = [
12947
14954
  "agent_delegate"
12948
14955
  // No agent chaining
12949
14956
  ];
12950
- var CommandHookSchema = z146.object({
12951
- type: z146.literal("command"),
12952
- command: z146.string().min(1, "Command is required for command hooks"),
12953
- 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()
12954
14961
  });
12955
- var PromptHookSchema = z146.object({
12956
- type: z146.literal("prompt"),
12957
- prompt: z146.string().min(1, "Prompt is required for prompt hooks"),
12958
- 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()
12959
14966
  });
12960
- var HookDefinitionSchema = z146.discriminatedUnion("type", [CommandHookSchema, PromptHookSchema]);
12961
- var HookMatcherSchema = z146.object({
12962
- matcher: z146.string().optional(),
12963
- 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)
12964
14971
  });
12965
- var AgentHooksSchema = z146.object({
12966
- PreToolUse: z146.array(HookMatcherSchema).optional(),
12967
- PostToolUse: z146.array(HookMatcherSchema).optional(),
12968
- PostToolUseFailure: z146.array(HookMatcherSchema).optional(),
12969
- 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()
12970
14977
  }).optional();
12971
- var AgentFrontmatterSchema = z146.object({
12972
- description: z146.string().min(1, "Agent description is required"),
12973
- model: z146.string().optional(),
12974
- "allowed-tools": z146.array(z146.string()).optional(),
12975
- "denied-tools": z146.array(z146.string()).optional(),
12976
- skills: z146.array(z146.string()).optional(),
12977
- "max-iterations": z146.object({
12978
- quick: z146.number().int().positive().optional(),
12979
- medium: z146.number().int().positive().optional(),
12980
- 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()
12981
14988
  }).optional(),
12982
- "default-thoroughness": z146.enum(["quick", "medium", "very_thorough"]).optional(),
12983
- variables: z146.record(z146.string()).optional(),
14989
+ "default-thoroughness": z147.enum(["quick", "medium", "very_thorough"]).optional(),
14990
+ variables: z147.record(z147.string()).optional(),
12984
14991
  hooks: AgentHooksSchema
12985
14992
  });
12986
14993
  var DEFAULT_MAX_ITERATIONS = {
@@ -12992,25 +14999,26 @@ var DEFAULT_AGENT_MODEL = "claude-3-5-haiku-20241022";
12992
14999
  var DEFAULT_THOROUGHNESS = "medium";
12993
15000
 
12994
15001
  // src/utils/toolsAdapter.ts
15002
+ import path15 from "path";
12995
15003
  var NoOpStorage = class extends BaseStorage {
12996
15004
  async upload(input, destination, options) {
12997
15005
  return `/tmp/${destination}`;
12998
15006
  }
12999
- async download(path19) {
15007
+ async download(path21) {
13000
15008
  throw new Error("Download not supported in CLI");
13001
15009
  }
13002
- async delete(path19) {
15010
+ async delete(path21) {
13003
15011
  }
13004
- async getSignedUrl(path19) {
13005
- return `/tmp/${path19}`;
15012
+ async getSignedUrl(path21) {
15013
+ return `/tmp/${path21}`;
13006
15014
  }
13007
- getPublicUrl(path19) {
13008
- return `/tmp/${path19}`;
15015
+ getPublicUrl(path21) {
15016
+ return `/tmp/${path21}`;
13009
15017
  }
13010
- async getPreview(path19) {
13011
- return `/tmp/${path19}`;
15018
+ async getPreview(path21) {
15019
+ return `/tmp/${path21}`;
13012
15020
  }
13013
- async getMetadata(path19) {
15021
+ async getMetadata(path21) {
13014
15022
  return { size: 0, contentType: "application/octet-stream" };
13015
15023
  }
13016
15024
  };
@@ -13041,7 +15049,7 @@ function wrapToolWithPermission(tool, permissionManager, showPermissionPrompt, a
13041
15049
  agentContext.observationQueue.push({ toolName, result: result2 });
13042
15050
  return result2;
13043
15051
  }
13044
- const { useCliStore: useCliStore2 } = await import("./store-FU6NDC2W.js");
15052
+ const { useCliStore: useCliStore2 } = await import("./store-K5MB3SE7.js");
13045
15053
  if (useCliStore2.getState().autoAcceptEdits) {
13046
15054
  const result2 = await executeTool(toolName, args, apiClient, originalFn);
13047
15055
  agentContext.observationQueue.push({ toolName, result: result2 });
@@ -13183,6 +15191,27 @@ function wrapToolWithHooks(tool, hooks, hookContext) {
13183
15191
  }
13184
15192
  };
13185
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
+ }
13186
15215
  var TOOL_NAME_MAPPING = {
13187
15216
  // Claude Code -> B4M
13188
15217
  read: "file_read",
@@ -13207,7 +15236,7 @@ function normalizeToolName(toolName) {
13207
15236
  }
13208
15237
  return TOOL_NAME_MAPPING[toolName] || toolName;
13209
15238
  }
13210
- function generateCliTools(userId, llm, model, permissionManager, showPermissionPrompt, agentContext, configStore, apiClient, toolFilter) {
15239
+ function generateCliTools(userId, llm, model, permissionManager, showPermissionPrompt, agentContext, configStore, apiClient, toolFilter, checkpointStore) {
13211
15240
  const logger2 = new CliLogger();
13212
15241
  const storage = new NoOpStorage();
13213
15242
  const user = {
@@ -13266,9 +15295,17 @@ function generateCliTools(userId, llm, model, permissionManager, showPermissionP
13266
15295
  // imageProcessorLambdaName (not needed for CLI)
13267
15296
  tools_to_generate
13268
15297
  );
13269
- let tools = Object.entries(toolsMap).map(
13270
- ([_, tool]) => wrapToolWithPermission(tool, permissionManager, showPermissionPrompt, agentContext, configStore, apiClient)
13271
- );
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
+ });
13272
15309
  if (toolFilter) {
13273
15310
  const { allowedTools, deniedTools } = toolFilter;
13274
15311
  const normalizedAllowed = allowedTools?.map(normalizeToolName);
@@ -13446,8 +15483,8 @@ function getEnvironmentName(configApiConfig) {
13446
15483
  }
13447
15484
 
13448
15485
  // src/utils/contextLoader.ts
13449
- import * as fs11 from "fs";
13450
- import * as path14 from "path";
15486
+ import * as fs12 from "fs";
15487
+ import * as path16 from "path";
13451
15488
  import { homedir as homedir3 } from "os";
13452
15489
  var CONTEXT_FILE_SIZE_LIMIT = 100 * 1024;
13453
15490
  var PROJECT_CONTEXT_FILES = [
@@ -13468,9 +15505,9 @@ function formatFileSize2(bytes) {
13468
15505
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
13469
15506
  }
13470
15507
  function tryReadContextFile(dir, filename, source) {
13471
- const filePath = path14.join(dir, filename);
15508
+ const filePath = path16.join(dir, filename);
13472
15509
  try {
13473
- const stats = fs11.lstatSync(filePath);
15510
+ const stats = fs12.lstatSync(filePath);
13474
15511
  if (stats.isDirectory()) {
13475
15512
  return null;
13476
15513
  }
@@ -13484,7 +15521,7 @@ function tryReadContextFile(dir, filename, source) {
13484
15521
  error: `${source === "global" ? "Global" : "Project"} ${filename} exceeds 100KB limit (${formatFileSize2(stats.size)})`
13485
15522
  };
13486
15523
  }
13487
- const content = fs11.readFileSync(filePath, "utf-8");
15524
+ const content = fs12.readFileSync(filePath, "utf-8");
13488
15525
  return {
13489
15526
  filename,
13490
15527
  content,
@@ -13536,7 +15573,7 @@ ${project.content}`;
13536
15573
  }
13537
15574
  async function loadContextFiles(projectDir) {
13538
15575
  const errors = [];
13539
- const globalDir = path14.join(homedir3(), ".bike4mind");
15576
+ const globalDir = path16.join(homedir3(), ".bike4mind");
13540
15577
  const projectDirectory = projectDir || process.cwd();
13541
15578
  const [globalResult, projectResult] = await Promise.all([
13542
15579
  Promise.resolve(findContextFile(globalDir, GLOBAL_CONTEXT_FILES, "global")),
@@ -13807,8 +15844,8 @@ function substituteArguments(template, args) {
13807
15844
  // ../../b4m-core/packages/mcp/dist/src/client.js
13808
15845
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
13809
15846
  import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
13810
- import path15 from "path";
13811
- import { existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
15847
+ import path17 from "path";
15848
+ import { existsSync as existsSync11, readdirSync as readdirSync3 } from "fs";
13812
15849
  var MCPClient = class {
13813
15850
  // Note: This class handles MCP server communication with repository filtering
13814
15851
  mcp;
@@ -13849,18 +15886,18 @@ var MCPClient = class {
13849
15886
  const root = process.env.INIT_CWD || process.cwd();
13850
15887
  const candidatePaths = [
13851
15888
  // When running from SST Lambda with node_modules structure (copyFiles)
13852
- 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`),
13853
15890
  // When running from SST Lambda deployed environment (/var/task)
13854
- 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`),
13855
15892
  // When running from SST Lambda (.sst/artifacts/mcpHandler-dev), navigate to monorepo root (3 levels up)
13856
- 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`),
13857
15894
  // When running from packages/client (Next.js app), navigate to monorepo root (2 levels up)
13858
- 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`),
13859
15896
  // Original paths (backward compatibility)
13860
- path15.join(root, `/b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
13861
- 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")
13862
15899
  ];
13863
- const serverScriptPath = candidatePaths.find((p) => existsSync9(p));
15900
+ const serverScriptPath = candidatePaths.find((p) => existsSync11(p));
13864
15901
  if (!serverScriptPath) {
13865
15902
  const getDirectories = (source) => readdirSync3(source, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
13866
15903
  console.error(`[MCP] Server script not found. Tried paths:`, candidatePaths);
@@ -14742,6 +16779,173 @@ var ServerLlmBackend = class {
14742
16779
  }
14743
16780
  };
14744
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
+
14745
16949
  // src/llm/NotifyingLlmBackend.ts
14746
16950
  var NotifyingLlmBackend = class {
14747
16951
  constructor(inner, backgroundManager) {
@@ -14776,6 +16980,253 @@ Please acknowledge these background agent results and incorporate them into your
14776
16980
  }
14777
16981
  };
14778
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
+
14779
17230
  // src/auth/ApiClient.ts
14780
17231
  import axios11 from "axios";
14781
17232
  var ApiClient = class {
@@ -14913,7 +17364,7 @@ import { isAxiosError as isAxiosError2 } from "axios";
14913
17364
  // package.json
14914
17365
  var package_default = {
14915
17366
  name: "@bike4mind/cli",
14916
- version: "0.2.30-subagent-delegation.19188+3a11dfa65",
17367
+ version: "0.2.31-b4m-cli-undo-command.19493+44c80c9bc",
14917
17368
  type: "module",
14918
17369
  description: "Interactive CLI tool for Bike4Mind with ReAct agents",
14919
17370
  license: "UNLICENSED",
@@ -14984,13 +17435,10 @@ var package_default = {
14984
17435
  diff: "^8.0.2",
14985
17436
  dotenv: "^16.3.1",
14986
17437
  "eventsource-parser": "^3.0.6",
14987
- fdir: "^6.5.0",
14988
17438
  "file-type": "^18.7.0",
14989
17439
  "fuse.js": "^7.1.0",
14990
- fzf: "^0.5.2",
14991
17440
  glob: "^13.0.0",
14992
17441
  "gray-matter": "^4.0.3",
14993
- ignore: "^7.0.5",
14994
17442
  ink: "^6.5.1",
14995
17443
  "ink-select-input": "^6.2.0",
14996
17444
  "ink-spinner": "^5.0.0",
@@ -15027,10 +17475,10 @@ var package_default = {
15027
17475
  },
15028
17476
  devDependencies: {
15029
17477
  "@bike4mind/agents": "0.1.0",
15030
- "@bike4mind/common": "2.51.1-subagent-delegation.19188+3a11dfa65",
15031
- "@bike4mind/mcp": "1.30.1-subagent-delegation.19188+3a11dfa65",
15032
- "@bike4mind/services": "2.49.1-subagent-delegation.19188+3a11dfa65",
15033
- "@bike4mind/utils": "2.6.1-subagent-delegation.19188+3a11dfa65",
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",
15034
17482
  "@types/better-sqlite3": "^7.6.13",
15035
17483
  "@types/diff": "^5.0.9",
15036
17484
  "@types/jsonwebtoken": "^9.0.4",
@@ -15039,6 +17487,9 @@ var package_default = {
15039
17487
  "@types/react": "^19.2.7",
15040
17488
  "@types/uuid": "^9.0.7",
15041
17489
  "@types/yargs": "^17.0.32",
17490
+ fdir: "^6.5.0",
17491
+ fzf: "^0.5.2",
17492
+ ignore: "^7.0.5",
15042
17493
  "ink-testing-library": "^4.0.0",
15043
17494
  tsup: "^8.5.1",
15044
17495
  tsx: "^4.21.0",
@@ -15048,14 +17499,9 @@ var package_default = {
15048
17499
  optionalDependencies: {
15049
17500
  "@vscode/ripgrep": "^1.17.0"
15050
17501
  },
15051
- gitHead: "3a11dfa65fb65509a35710beec82a4e720ac5167"
17502
+ gitHead: "44c80c9bcf8bf132a8b75c3635426a901adce99e"
15052
17503
  };
15053
17504
 
15054
- // src/config/constants.ts
15055
- var USAGE_DAYS = 30;
15056
- var MODEL_NAME_COLUMN_WIDTH = 18;
15057
- var USAGE_CACHE_TTL = 5 * 60 * 1e3;
15058
-
15059
17505
  // src/agents/toolFilter.ts
15060
17506
  function matchesToolPattern3(toolName, pattern) {
15061
17507
  const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
@@ -15344,7 +17790,10 @@ var SubagentOrchestrator = class {
15344
17790
  this.deps.showPermissionPrompt,
15345
17791
  agentContext,
15346
17792
  this.deps.configStore,
15347
- this.deps.apiClient
17793
+ this.deps.apiClient,
17794
+ void 0,
17795
+ // toolFilter (applied separately below)
17796
+ this.deps.checkpointStore
15348
17797
  );
15349
17798
  const filteredTools = filterToolsByPatterns2(allTools, toolFilter.allowedTools, toolFilter.deniedTools);
15350
17799
  if (this.deps.customCommandStore) {
@@ -15497,8 +17946,8 @@ var SubagentOrchestrator = class {
15497
17946
  };
15498
17947
 
15499
17948
  // src/agents/AgentStore.ts
15500
- import fs12 from "fs/promises";
15501
- import path16 from "path";
17949
+ import fs13 from "fs/promises";
17950
+ import path18 from "path";
15502
17951
  import os2 from "os";
15503
17952
  import matter2 from "gray-matter";
15504
17953
  var FULL_MODEL_ID_PREFIXES = [
@@ -15647,10 +18096,10 @@ var AgentStore = class {
15647
18096
  const root = projectRoot || process.cwd();
15648
18097
  const home = os2.homedir();
15649
18098
  this.builtinAgentsDir = builtinDir;
15650
- this.globalB4MAgentsDir = path16.join(home, ".bike4mind", "agents");
15651
- this.globalClaudeAgentsDir = path16.join(home, ".claude", "agents");
15652
- this.projectB4MAgentsDir = path16.join(root, ".bike4mind", "agents");
15653
- 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");
15654
18103
  }
15655
18104
  /**
15656
18105
  * Load all agents from all directories
@@ -15674,7 +18123,7 @@ var AgentStore = class {
15674
18123
  */
15675
18124
  async loadAgentsFromDirectory(directory, source) {
15676
18125
  try {
15677
- const stats = await fs12.stat(directory);
18126
+ const stats = await fs13.stat(directory);
15678
18127
  if (!stats.isDirectory()) {
15679
18128
  return;
15680
18129
  }
@@ -15702,9 +18151,9 @@ var AgentStore = class {
15702
18151
  async findAgentFiles(directory) {
15703
18152
  const files = [];
15704
18153
  try {
15705
- const entries = await fs12.readdir(directory, { withFileTypes: true });
18154
+ const entries = await fs13.readdir(directory, { withFileTypes: true });
15706
18155
  for (const entry of entries) {
15707
- const fullPath = path16.join(directory, entry.name);
18156
+ const fullPath = path18.join(directory, entry.name);
15708
18157
  if (entry.isDirectory()) {
15709
18158
  const subFiles = await this.findAgentFiles(fullPath);
15710
18159
  files.push(...subFiles);
@@ -15721,10 +18170,10 @@ var AgentStore = class {
15721
18170
  * Parse a single agent markdown file
15722
18171
  */
15723
18172
  async parseAgentFile(filePath, source) {
15724
- const content = await fs12.readFile(filePath, "utf-8");
18173
+ const content = await fs13.readFile(filePath, "utf-8");
15725
18174
  const { data: frontmatter, content: body } = matter2(content);
15726
18175
  const parsed = AgentFrontmatterSchema.parse(frontmatter);
15727
- const name = path16.basename(filePath, ".md");
18176
+ const name = path18.basename(filePath, ".md");
15728
18177
  const modelInput = parsed.model || DEFAULT_AGENT_MODEL;
15729
18178
  const resolution = resolveModelAlias(modelInput, name, filePath);
15730
18179
  if (!resolution.resolved && resolution.warning) {
@@ -15804,16 +18253,16 @@ var AgentStore = class {
15804
18253
  */
15805
18254
  async createAgentFile(name, isGlobal = false, useClaude = true) {
15806
18255
  const targetDir = isGlobal ? useClaude ? this.globalClaudeAgentsDir : this.globalB4MAgentsDir : useClaude ? this.projectClaudeAgentsDir : this.projectB4MAgentsDir;
15807
- const filePath = path16.join(targetDir, `${name}.md`);
18256
+ const filePath = path18.join(targetDir, `${name}.md`);
15808
18257
  try {
15809
- await fs12.access(filePath);
18258
+ await fs13.access(filePath);
15810
18259
  throw new Error(`Agent file already exists: ${filePath}`);
15811
18260
  } catch (error) {
15812
18261
  if (error.code !== "ENOENT") {
15813
18262
  throw error;
15814
18263
  }
15815
18264
  }
15816
- await fs12.mkdir(targetDir, { recursive: true });
18265
+ await fs13.mkdir(targetDir, { recursive: true });
15817
18266
  const template = `---
15818
18267
  description: ${name} agent description
15819
18268
  model: claude-3-5-haiku-20241022
@@ -15848,7 +18297,7 @@ You are a ${name} specialist. Your job is to [describe primary task].
15848
18297
  ## Output Format
15849
18298
  Describe the expected output format here.
15850
18299
  `;
15851
- await fs12.writeFile(filePath, template, "utf-8");
18300
+ await fs13.writeFile(filePath, template, "utf-8");
15852
18301
  return filePath;
15853
18302
  }
15854
18303
  /**
@@ -16501,7 +18950,7 @@ function createTodoStore(onUpdate) {
16501
18950
 
16502
18951
  // src/tools/findDefinitionTool.ts
16503
18952
  import { stat as stat3 } from "fs/promises";
16504
- import path17 from "path";
18953
+ import path19 from "path";
16505
18954
  import { execFile as execFile2 } from "child_process";
16506
18955
  import { promisify as promisify2 } from "util";
16507
18956
  import { createRequire as createRequire2 } from "module";
@@ -16522,8 +18971,8 @@ var ALL_KEYWORDS = Object.values(KIND_KEYWORDS).flat();
16522
18971
  function getRipgrepPath2() {
16523
18972
  try {
16524
18973
  const ripgrepPath = require3.resolve("@vscode/ripgrep");
16525
- const ripgrepDir = path17.dirname(ripgrepPath);
16526
- 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" : ""));
16527
18976
  } catch {
16528
18977
  throw new Error(
16529
18978
  "ripgrep is not available. Please install @vscode/ripgrep: pnpm add @vscode/ripgrep --filter @bike4mind/services"
@@ -16531,10 +18980,10 @@ function getRipgrepPath2() {
16531
18980
  }
16532
18981
  }
16533
18982
  function isPathWithinWorkspace2(targetPath, baseCwd) {
16534
- const resolvedTarget = path17.resolve(targetPath);
16535
- const resolvedBase = path17.resolve(baseCwd);
16536
- const relativePath = path17.relative(resolvedBase, resolvedTarget);
16537
- 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);
16538
18987
  }
16539
18988
  function escapeRegex(str) {
16540
18989
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -16567,7 +19016,7 @@ async function findDefinitions(params) {
16567
19016
  throw new Error("symbol_name is required");
16568
19017
  }
16569
19018
  const baseCwd = process.cwd();
16570
- const targetDir = search_path ? path17.resolve(baseCwd, search_path) : baseCwd;
19019
+ const targetDir = search_path ? path19.resolve(baseCwd, search_path) : baseCwd;
16571
19020
  if (!isPathWithinWorkspace2(targetDir, baseCwd)) {
16572
19021
  throw new Error(`Path validation failed: "${search_path}" resolves outside the allowed workspace directory`);
16573
19022
  }
@@ -16622,7 +19071,7 @@ async function findDefinitions(params) {
16622
19071
  const lineText = match.data.lines.text.trimEnd();
16623
19072
  if (isLikelyDefinition(lineText)) {
16624
19073
  allMatches.push({
16625
- 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),
16626
19075
  lineNumber: match.data.line_number,
16627
19076
  line: lineText
16628
19077
  });
@@ -16700,8 +19149,8 @@ function createFindDefinitionTool() {
16700
19149
  }
16701
19150
 
16702
19151
  // src/tools/getFileStructure/index.ts
16703
- import { existsSync as existsSync10, promises as fs13, statSync as statSync6 } from "fs";
16704
- import path18 from "path";
19152
+ import { existsSync as existsSync12, promises as fs14, statSync as statSync6 } from "fs";
19153
+ import path20 from "path";
16705
19154
 
16706
19155
  // src/tools/getFileStructure/formatter.ts
16707
19156
  var SECTIONS = [
@@ -16738,35 +19187,35 @@ function formatSection(lines, title, items, format) {
16738
19187
  }
16739
19188
 
16740
19189
  // src/tools/getFileStructure/index.ts
16741
- var MAX_FILE_SIZE3 = 10 * 1024 * 1024;
19190
+ var MAX_FILE_SIZE4 = 10 * 1024 * 1024;
16742
19191
  function createGetFileStructureTool() {
16743
19192
  return {
16744
19193
  toolFn: async (value) => {
16745
19194
  const params = value;
16746
19195
  try {
16747
19196
  const cwd = process.cwd();
16748
- const resolvedPath = path18.resolve(cwd, params.path);
19197
+ const resolvedPath = path20.resolve(cwd, params.path);
16749
19198
  if (!resolvedPath.startsWith(cwd)) {
16750
19199
  return "Error: Access denied - cannot read files outside of current working directory";
16751
19200
  }
16752
- if (!existsSync10(resolvedPath)) {
19201
+ if (!existsSync12(resolvedPath)) {
16753
19202
  return `Error: File not found: ${params.path}`;
16754
19203
  }
16755
19204
  const stats = statSync6(resolvedPath);
16756
19205
  if (stats.isDirectory()) {
16757
19206
  return `Error: Path is a directory, not a file: ${params.path}`;
16758
19207
  }
16759
- if (stats.size > MAX_FILE_SIZE3) {
16760
- 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`;
16761
19210
  }
16762
- const ext = path18.extname(resolvedPath).toLowerCase();
19211
+ const ext = path20.extname(resolvedPath).toLowerCase();
16763
19212
  const { getLanguageForExtension, parseFileStructure, getSupportedLanguages } = await import("./treeSitterEngine-4SGFQDY3.js");
16764
19213
  const languageId = getLanguageForExtension(ext);
16765
19214
  if (!languageId) {
16766
19215
  const supported = getSupportedLanguages();
16767
19216
  return `Error: Unsupported file type "${ext}". Supported languages: ${supported.join(", ")}`;
16768
19217
  }
16769
- const sourceCode = await fs13.readFile(resolvedPath, "utf-8");
19218
+ const sourceCode = await fs14.readFile(resolvedPath, "utf-8");
16770
19219
  const lineCount = sourceCode.split("\n").length;
16771
19220
  const items = await parseFileStructure(sourceCode, languageId);
16772
19221
  return formatStructureOutput(params.path, items, stats.size, lineCount);
@@ -16827,7 +19276,9 @@ function CliApp() {
16827
19276
  agentStore: null,
16828
19277
  abortController: null,
16829
19278
  contextContent: "",
16830
- backgroundManager: null
19279
+ backgroundManager: null,
19280
+ wsManager: null,
19281
+ checkpointStore: null
16831
19282
  });
16832
19283
  const [isInitialized, setIsInitialized] = useState10(false);
16833
19284
  const [initError, setInitError] = useState10(null);
@@ -16855,6 +19306,10 @@ function CliApp() {
16855
19306
  })
16856
19307
  );
16857
19308
  }
19309
+ if (state.wsManager) {
19310
+ state.wsManager.disconnect();
19311
+ setWebSocketToolExecutor(null);
19312
+ }
16858
19313
  if (state.agent) {
16859
19314
  state.agent.removeAllListeners();
16860
19315
  }
@@ -16873,9 +19328,14 @@ function CliApp() {
16873
19328
  setTimeout(() => {
16874
19329
  process.exit(0);
16875
19330
  }, 100);
16876
- }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore]);
19331
+ }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore, state.wsManager]);
16877
19332
  useInput9((input, key) => {
16878
19333
  if (key.escape) {
19334
+ const store = useCliStore.getState();
19335
+ if (store.pastedContent) {
19336
+ store.clearPaste();
19337
+ return;
19338
+ }
16879
19339
  if (state.abortController) {
16880
19340
  logger.debug("[ABORT] ESC pressed - aborting current operation...");
16881
19341
  state.abortController.abort();
@@ -16893,9 +19353,9 @@ function CliApp() {
16893
19353
  });
16894
19354
  } else {
16895
19355
  logger.debug("[EXIT] First Ctrl+C - press again to exit");
16896
- const currentInput = useCliStore.getState().inputValue;
16897
- if (currentInput.length > 0) {
16898
- useCliStore.getState().clearInput();
19356
+ const store = useCliStore.getState();
19357
+ if (store.inputValue.length > 0 || store.pastedContent) {
19358
+ store.clearInput();
16899
19359
  }
16900
19360
  exitTimestamp = now;
16901
19361
  setExitRequested(true);
@@ -16943,7 +19403,7 @@ function CliApp() {
16943
19403
  if (!isAuthenticated) {
16944
19404
  console.log("\u2139\uFE0F AI features disabled. Available commands: /login, /help, /config\n");
16945
19405
  const minimalSession = {
16946
- id: uuidv411(),
19406
+ id: uuidv413(),
16947
19407
  name: `Session ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
16948
19408
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
16949
19409
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -16971,10 +19431,45 @@ function CliApp() {
16971
19431
  console.log(`\u{1F30D} API Environment: ${envName} (${apiBaseURL})`);
16972
19432
  }
16973
19433
  const apiClient = new ApiClient(apiBaseURL, state.configStore);
16974
- const llm = new ServerLlmBackend({
16975
- apiClient,
16976
- model: config.defaultModel
16977
- });
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
+ }
16978
19473
  const models = await llm.getModelInfo();
16979
19474
  if (models.length === 0) {
16980
19475
  throw new Error("No models available from server.");
@@ -16987,7 +19482,7 @@ function CliApp() {
16987
19482
  }
16988
19483
  llm.currentModel = modelInfo.id;
16989
19484
  const newSession = {
16990
- id: uuidv411(),
19485
+ id: uuidv413(),
16991
19486
  name: `Session ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
16992
19487
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
16993
19488
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17037,6 +19532,12 @@ function CliApp() {
17037
19532
  enqueuePermissionPrompt(prompt);
17038
19533
  });
17039
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
+ }
17040
19541
  const agentContext = {
17041
19542
  currentAgent: null,
17042
19543
  observationQueue: []
@@ -17049,7 +19550,10 @@ function CliApp() {
17049
19550
  promptFn,
17050
19551
  agentContext,
17051
19552
  state.configStore,
17052
- apiClient
19553
+ apiClient,
19554
+ void 0,
19555
+ // toolFilter
19556
+ checkpointStore
17053
19557
  );
17054
19558
  startupLog.push(`\u{1F6E0}\uFE0F Loaded ${b4mTools2.length} B4M tool(s)`);
17055
19559
  const mcpManager = new McpManager(config);
@@ -17080,7 +19584,8 @@ function CliApp() {
17080
19584
  apiClient,
17081
19585
  agentStore,
17082
19586
  customCommandStore: state.customCommandStore,
17083
- enableParallelToolExecution: config.preferences.enableParallelToolExecution === true
19587
+ enableParallelToolExecution: config.preferences.enableParallelToolExecution === true,
19588
+ checkpointStore
17084
19589
  });
17085
19590
  const backgroundManager = new BackgroundAgentManager(orchestrator);
17086
19591
  backgroundManager.setOnStatusChange((job) => {
@@ -17201,8 +19706,12 @@ function CliApp() {
17201
19706
  // Store agent store for agent management commands
17202
19707
  contextContent: contextResult.mergedContent,
17203
19708
  // Store raw context for compact instructions
17204
- backgroundManager
19709
+ backgroundManager,
17205
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
17206
19715
  }));
17207
19716
  setStoreSession(newSession);
17208
19717
  const bannerLines = [
@@ -17297,13 +19806,13 @@ function CliApp() {
17297
19806
  messageContent = multimodalMessage.content;
17298
19807
  }
17299
19808
  const userMessage = {
17300
- id: uuidv411(),
19809
+ id: uuidv413(),
17301
19810
  role: "user",
17302
19811
  content: userMessageContent,
17303
19812
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
17304
19813
  };
17305
19814
  const pendingAssistantMessage = {
17306
- id: uuidv411(),
19815
+ id: uuidv413(),
17307
19816
  role: "assistant",
17308
19817
  content: "...",
17309
19818
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17516,13 +20025,13 @@ function CliApp() {
17516
20025
  userMessageContent = message;
17517
20026
  }
17518
20027
  const userMessage = {
17519
- id: uuidv411(),
20028
+ id: uuidv413(),
17520
20029
  role: "user",
17521
20030
  content: userMessageContent,
17522
20031
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
17523
20032
  };
17524
20033
  const pendingAssistantMessage = {
17525
- id: uuidv411(),
20034
+ id: uuidv413(),
17526
20035
  role: "assistant",
17527
20036
  content: "...",
17528
20037
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17602,7 +20111,7 @@ function CliApp() {
17602
20111
  const currentSession = useCliStore.getState().session;
17603
20112
  if (currentSession) {
17604
20113
  const cancelMessage = {
17605
- id: uuidv411(),
20114
+ id: uuidv413(),
17606
20115
  role: "assistant",
17607
20116
  content: "\u26A0\uFE0F Operation cancelled by user",
17608
20117
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17647,7 +20156,7 @@ function CliApp() {
17647
20156
  setState((prev) => ({ ...prev, abortController }));
17648
20157
  try {
17649
20158
  const pendingAssistantMessage = {
17650
- id: uuidv411(),
20159
+ id: uuidv413(),
17651
20160
  role: "assistant",
17652
20161
  content: "...",
17653
20162
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17675,7 +20184,7 @@ function CliApp() {
17675
20184
  const currentSession = useCliStore.getState().session;
17676
20185
  if (!currentSession) return;
17677
20186
  const continuationMessage = {
17678
- id: uuidv411(),
20187
+ id: uuidv413(),
17679
20188
  role: "assistant",
17680
20189
  content: "---\n\n**Background Agent Results:**\n\n" + result.finalAnswer,
17681
20190
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17736,13 +20245,13 @@ function CliApp() {
17736
20245
  isError = true;
17737
20246
  }
17738
20247
  const userMessage = {
17739
- id: uuidv411(),
20248
+ id: uuidv413(),
17740
20249
  role: "user",
17741
20250
  content: `$ ${command}`,
17742
20251
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
17743
20252
  };
17744
20253
  const assistantMessage = {
17745
- id: uuidv411(),
20254
+ id: uuidv413(),
17746
20255
  role: "assistant",
17747
20256
  content: isError ? `\u274C Error:
17748
20257
  ${output}` : output.trim() || "(no output)",
@@ -17895,6 +20404,10 @@ Available commands:
17895
20404
  /exit - Exit the CLI
17896
20405
  /clear - Start a new session
17897
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
17898
20411
  /login - Authenticate with your B4M account
17899
20412
  /logout - Clear authentication and sign out
17900
20413
  /whoami - Show current authenticated user
@@ -17987,6 +20500,9 @@ Keyboard Shortcuts:
17987
20500
  }
17988
20501
  await logger.initialize(loadedSession.id);
17989
20502
  logger.debug("=== Session Resumed ===");
20503
+ if (state.checkpointStore) {
20504
+ state.checkpointStore.setSessionId(loadedSession.id);
20505
+ }
17990
20506
  setState((prev) => ({ ...prev, session: loadedSession }));
17991
20507
  setStoreSession(loadedSession);
17992
20508
  useCliStore.getState().clearPendingMessages();
@@ -18211,7 +20727,7 @@ Keyboard Shortcuts:
18211
20727
  console.clear();
18212
20728
  const model = state.session?.model || state.config?.defaultModel || "claude-sonnet";
18213
20729
  const newSession = {
18214
- id: uuidv411(),
20730
+ id: uuidv413(),
18215
20731
  name: `Session ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
18216
20732
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
18217
20733
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -18225,6 +20741,9 @@ Keyboard Shortcuts:
18225
20741
  };
18226
20742
  await logger.initialize(newSession.id);
18227
20743
  logger.debug("=== New Session Started via /clear ===");
20744
+ if (state.checkpointStore) {
20745
+ state.checkpointStore.setSessionId(newSession.id);
20746
+ }
18228
20747
  setState((prev) => ({ ...prev, session: newSession }));
18229
20748
  setStoreSession(newSession);
18230
20749
  useCliStore.getState().clearPendingMessages();
@@ -18235,8 +20754,7 @@ Keyboard Shortcuts:
18235
20754
  `);
18236
20755
  break;
18237
20756
  }
18238
- case "rewind":
18239
- case "undo": {
20757
+ case "rewind": {
18240
20758
  if (!state.session) {
18241
20759
  console.log("No active session to rewind");
18242
20760
  return;
@@ -18295,6 +20813,103 @@ Keyboard Shortcuts:
18295
20813
  }));
18296
20814
  break;
18297
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
+ }
18298
20913
  case "usage": {
18299
20914
  const usageAuthTokens = await state.configStore.getAuthTokens();
18300
20915
  if (!usageAuthTokens || new Date(usageAuthTokens.expiresAt) <= /* @__PURE__ */ new Date()) {
@@ -18716,9 +21331,9 @@ No usage data available for the last ${USAGE_DAYS} days.`);
18716
21331
  return { ...prev, config: updatedConfig };
18717
21332
  });
18718
21333
  if (modelChanged && state.agent) {
18719
- const llm = state.agent.context.llm;
18720
- if (llm) {
18721
- llm.currentModel = updatedConfig.defaultModel;
21334
+ const backend = state.agent.context.llm;
21335
+ if (backend) {
21336
+ backend.currentModel = updatedConfig.defaultModel;
18722
21337
  }
18723
21338
  }
18724
21339
  };