@probelabs/probe 0.6.0-rc285 → 0.6.0-rc287

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/cjs/index.cjs CHANGED
@@ -1098,7 +1098,11 @@ async function extractBinary(assetPath, outputDir) {
1098
1098
  if (process.env.DEBUG === "1" || process.env.VERBOSE === "1") {
1099
1099
  console.log(`Extracting zip to ${extractDir}...`);
1100
1100
  }
1101
- await exec(`unzip -q "${assetPath}" -d "${extractDir}"`);
1101
+ if (isWindows) {
1102
+ await exec(`powershell -NoProfile -Command "Expand-Archive -Path '${assetPath}' -DestinationPath '${extractDir}' -Force"`);
1103
+ } else {
1104
+ await exec(`unzip -q "${assetPath}" -d "${extractDir}"`);
1105
+ }
1102
1106
  } else {
1103
1107
  if (process.env.DEBUG === "1" || process.env.VERBOSE === "1") {
1104
1108
  console.log(`Copying binary directly to ${binaryPath}`);
@@ -27053,7 +27057,14 @@ function parseTargets(targets) {
27053
27057
  }
27054
27058
  function parseAndResolvePaths(pathStr, cwd) {
27055
27059
  if (!pathStr) return [];
27056
- const paths = pathStr.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
27060
+ let paths = pathStr.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
27061
+ paths = paths.flatMap((p) => {
27062
+ if (!/\s/.test(p)) return [p];
27063
+ const parts = p.split(/\s+/).filter(Boolean);
27064
+ if (parts.length <= 1) return [p];
27065
+ const allLookLikePaths = parts.every((part) => /[/\\]/.test(part) || /\.\w+/.test(part));
27066
+ return allLookLikePaths ? parts : [p];
27067
+ });
27057
27068
  return paths.map((p) => {
27058
27069
  if ((0, import_path5.isAbsolute)(p)) {
27059
27070
  return p;
@@ -46716,6 +46727,7 @@ var init_parser2 = __esm({
46716
46727
  { ALT: () => this.CONSUME(Identifier) },
46717
46728
  { ALT: () => this.CONSUME(Text) },
46718
46729
  { ALT: () => this.CONSUME(NumberLiteral) },
46730
+ { ALT: () => this.CONSUME(ColorValue) },
46719
46731
  // Note: RoundOpen and RoundClose (parentheses) are NOT allowed in unquoted labels
46720
46732
  // to match Mermaid's behavior - use quoted labels like ["text (with parens)"] instead
46721
46733
  // Allow HTML-like tags (e.g., <br/>) inside labels
@@ -46807,6 +46819,7 @@ var init_parser2 = __esm({
46807
46819
  { ALT: () => this.CONSUME(Identifier) },
46808
46820
  { ALT: () => this.CONSUME(Text) },
46809
46821
  { ALT: () => this.CONSUME(NumberLiteral) },
46822
+ { ALT: () => this.CONSUME(ColorValue) },
46810
46823
  // Allow HTML-like angle brackets and slashes for <br/>, <i>, etc.
46811
46824
  { ALT: () => this.CONSUME(AngleLess) },
46812
46825
  { ALT: () => this.CONSUME(AngleOpen) },
@@ -47982,13 +47995,24 @@ function mapFlowchartParserError(err, text) {
47982
47995
  length: len
47983
47996
  };
47984
47997
  }
47985
- if (tokType === "QuotedString" || tokType === "SquareOpen" || tokType === "SquareClose") {
47998
+ if (tokType === "QuotedString" || tokType === "SquareOpen" || tokType === "SquareClose" || tokType === "DiamondOpen" || tokType === "DiamondClose") {
47986
47999
  const context = err?.context;
47987
48000
  const inLinkRule = context?.ruleStack?.includes("linkTextInline") || context?.ruleStack?.includes("link") || false;
47988
48001
  const lineContent = allLines[Math.max(0, line - 1)] || "";
47989
48002
  const beforeQuote = lineContent.slice(0, Math.max(0, column - 1));
47990
48003
  const hasLinkBefore = beforeQuote.match(/--\s*$|==\s*$|-\.\s*$|-\.-\s*$|\[\s*$/);
47991
48004
  if (inLinkRule || hasLinkBefore) {
48005
+ if (tokType === "DiamondOpen" || tokType === "DiamondClose") {
48006
+ return {
48007
+ line,
48008
+ column,
48009
+ severity: "error",
48010
+ code: "FL-EDGE-LABEL-CURLY-IN-PIPES",
48011
+ message: "Curly braces { } are not supported inside pipe-delimited edge labels.",
48012
+ hint: "Use HTML entities &#123; and &#125; inside |...|, e.g., --|Resolve $&#123;VAR&#125;|-->",
48013
+ length: len
48014
+ };
48015
+ }
47992
48016
  if (tokType === "SquareOpen" || tokType === "SquareClose") {
47993
48017
  return {
47994
48018
  line,
@@ -48050,6 +48074,17 @@ function mapFlowchartParserError(err, text) {
48050
48074
  length: len
48051
48075
  };
48052
48076
  }
48077
+ if (tokType === "DiamondOpen" || tokType === "DiamondClose") {
48078
+ return {
48079
+ line,
48080
+ column,
48081
+ severity: "error",
48082
+ code: "FL-LABEL-CURLY-IN-UNQUOTED",
48083
+ message: "Curly braces are not supported inside unquoted node labels.",
48084
+ hint: "Use &#123; and &#125; for literal braces, e.g., C[Substitute &#123;params&#125;].",
48085
+ length: len
48086
+ };
48087
+ }
48053
48088
  {
48054
48089
  const caret0 = Math.max(0, column - 1);
48055
48090
  const openIdx = lineStr.lastIndexOf("[", caret0);
@@ -48091,9 +48126,20 @@ function mapFlowchartParserError(err, text) {
48091
48126
  length: len
48092
48127
  };
48093
48128
  }
48129
+ if (seg.includes("{") || seg.includes("}")) {
48130
+ return {
48131
+ line,
48132
+ column,
48133
+ severity: "error",
48134
+ code: "FL-LABEL-CURLY-IN-UNQUOTED",
48135
+ message: "Curly braces are not supported inside unquoted node labels.",
48136
+ hint: "Use &#123; and &#125; for literal braces, e.g., C[Substitute &#123;params&#125;].",
48137
+ length: len
48138
+ };
48139
+ }
48094
48140
  }
48095
48141
  }
48096
- if (tokType === "QuotedString" || tokType === "SquareOpen" || tokType === "SquareClose") {
48142
+ if (tokType === "QuotedString") {
48097
48143
  return {
48098
48144
  line,
48099
48145
  column,
@@ -48104,6 +48150,17 @@ function mapFlowchartParserError(err, text) {
48104
48150
  length: len
48105
48151
  };
48106
48152
  }
48153
+ if (tokType === "SquareOpen" || tokType === "SquareClose") {
48154
+ return {
48155
+ line,
48156
+ column,
48157
+ severity: "error",
48158
+ code: "FL-LABEL-BRACKET-IN-UNQUOTED",
48159
+ message: "Square brackets are not supported inside unquoted node labels.",
48160
+ hint: 'Use &#91; and &#93; for literal brackets or wrap the label in quotes, e.g., C["bind_paths[]"]',
48161
+ length: len
48162
+ };
48163
+ }
48107
48164
  const q = findInnerQuoteIssue("[");
48108
48165
  if (q?.kind === "escaped") {
48109
48166
  return { line, column: q.column, severity: "error", code: "FL-LABEL-ESCAPED-QUOTE", message: 'Escaped quotes (\\") in node labels are not supported by Mermaid. Use &quot; instead.', hint: 'Prefer "He said &quot;Hi&quot;".', length: 2 };
@@ -48114,7 +48171,7 @@ function mapFlowchartParserError(err, text) {
48114
48171
  return { line, column, severity: "error", code: "FL-NODE-UNCLOSED-BRACKET", message: "Unclosed '['. Add a matching ']' before the arrow or newline.", hint: "Example: A[Label] --> B", length: 1 };
48115
48172
  }
48116
48173
  if (expecting(err, "RoundClose")) {
48117
- if (tokType === "QuotedString" || tokType === "SquareOpen" || tokType === "SquareClose") {
48174
+ if (tokType === "QuotedString") {
48118
48175
  return {
48119
48176
  line,
48120
48177
  column,
@@ -48125,6 +48182,17 @@ function mapFlowchartParserError(err, text) {
48125
48182
  length: len
48126
48183
  };
48127
48184
  }
48185
+ if (tokType === "SquareOpen" || tokType === "SquareClose") {
48186
+ return {
48187
+ line,
48188
+ column,
48189
+ severity: "error",
48190
+ code: "FL-LABEL-BRACKET-IN-UNQUOTED",
48191
+ message: "Square brackets are not supported inside unquoted node labels.",
48192
+ hint: 'Use &#91; and &#93; for literal brackets or wrap the label in quotes, e.g., C["bind_paths[]"]',
48193
+ length: len
48194
+ };
48195
+ }
48128
48196
  {
48129
48197
  const caret0 = Math.max(0, column - 1);
48130
48198
  const openIdx = lineStr.lastIndexOf("(", caret0);
@@ -48153,7 +48221,7 @@ function mapFlowchartParserError(err, text) {
48153
48221
  return { line, column, severity: "error", code: "FL-NODE-UNCLOSED-BRACKET", message: "Unclosed '('. Add a matching ')'.", hint: "Example: B(Label)", length: 1 };
48154
48222
  }
48155
48223
  if (expecting(err, "DiamondClose")) {
48156
- if (tokType === "QuotedString" || tokType === "SquareOpen" || tokType === "SquareClose") {
48224
+ if (tokType === "QuotedString") {
48157
48225
  return {
48158
48226
  line,
48159
48227
  column,
@@ -48164,6 +48232,17 @@ function mapFlowchartParserError(err, text) {
48164
48232
  length: len
48165
48233
  };
48166
48234
  }
48235
+ if (tokType === "SquareOpen" || tokType === "SquareClose") {
48236
+ return {
48237
+ line,
48238
+ column,
48239
+ severity: "error",
48240
+ code: "FL-LABEL-BRACKET-IN-UNQUOTED",
48241
+ message: "Square brackets are not supported inside unquoted node labels.",
48242
+ hint: 'Use &#91; and &#93; for literal brackets or wrap the label in quotes, e.g., C["bind_paths[]"]',
48243
+ length: len
48244
+ };
48245
+ }
48167
48246
  {
48168
48247
  const caret0 = Math.max(0, column - 1);
48169
48248
  const openIdx = lineStr.lastIndexOf("{", caret0);
@@ -49006,7 +49085,7 @@ function validateFlowchart(text, options = {}) {
49006
49085
  const byLine = /* @__PURE__ */ new Map();
49007
49086
  const collect = (arr) => {
49008
49087
  for (const e of arr || []) {
49009
- if (e && (e.code === "FL-LABEL-PARENS-UNQUOTED" || e.code === "FL-LABEL-AT-IN-UNQUOTED" || e.code === "FL-LABEL-QUOTE-IN-UNQUOTED" || e.code === "FL-LABEL-SLASH-UNQUOTED")) {
49088
+ if (e && (e.code === "FL-LABEL-PARENS-UNQUOTED" || e.code === "FL-LABEL-AT-IN-UNQUOTED" || e.code === "FL-LABEL-QUOTE-IN-UNQUOTED" || e.code === "FL-LABEL-SLASH-UNQUOTED" || e.code === "FL-LABEL-CURLY-IN-UNQUOTED" || e.code === "FL-LABEL-BRACKET-IN-UNQUOTED")) {
49010
49089
  const ln = e.line ?? 0;
49011
49090
  const col = e.column ?? 1;
49012
49091
  const list = byLine.get(ln) || [];
@@ -49088,6 +49167,9 @@ function validateFlowchart(text, options = {}) {
49088
49167
  const hasParens = seg.includes("(") || seg.includes(")");
49089
49168
  const hasAt = seg.includes("@");
49090
49169
  const hasQuote = seg.includes('"');
49170
+ const hasCurly = seg.includes("{") || seg.includes("}");
49171
+ const hasBracket = seg.includes("[") || seg.includes("]");
49172
+ const isDoubleSquare = raw.slice(i, i + 2) === "[[" && raw.slice(j - 2, j) === "]]";
49091
49173
  const isSingleQuoted = /^'[^]*'$/.test(trimmed);
49092
49174
  const hasLeadingSlash = lsp === "/" || lsp === "\\";
49093
49175
  if (!covered && !isQuoted && !isParenWrapped && hasParens) {
@@ -49105,6 +49187,16 @@ function validateFlowchart(text, options = {}) {
49105
49187
  existing.push({ start: startCol, end: endCol });
49106
49188
  byLine.set(ln, existing);
49107
49189
  }
49190
+ if (!covered && !isQuoted && !isSlashPair && hasCurly) {
49191
+ errs.push({ line: ln, column: startCol, severity: "error", code: "FL-LABEL-CURLY-IN-UNQUOTED", message: "Curly braces are not supported inside unquoted node labels.", hint: "Use &#123; and &#125; for literal braces, e.g., C[Substitute &#123;params&#125;]." });
49192
+ existing.push({ start: startCol, end: endCol });
49193
+ byLine.set(ln, existing);
49194
+ }
49195
+ if (!covered && !isQuoted && !isSlashPair && !isDoubleSquare && hasBracket) {
49196
+ errs.push({ line: ln, column: startCol, severity: "error", code: "FL-LABEL-BRACKET-IN-UNQUOTED", message: "Square brackets are not supported inside unquoted node labels.", hint: 'Use &#91; and &#93; for literal brackets or wrap the label in quotes, e.g., C["bind_paths[]"]' });
49197
+ existing.push({ start: startCol, end: endCol });
49198
+ byLine.set(ln, existing);
49199
+ }
49108
49200
  if (!covered && !isQuoted && !isSlashPair && hasQuote && !isSingleQuoted) {
49109
49201
  errs.push({ line: ln, column: startCol, severity: "error", code: "FL-LABEL-QUOTE-IN-UNQUOTED", message: "Quotes are not allowed inside unquoted node labels. Use &quot; for quotes or wrap the entire label in quotes.", hint: 'Example: C["HTML Output: data-trigger-visibility=&quot;true&quot;"]' });
49110
49202
  existing.push({ start: startCol, end: endCol });
@@ -51465,7 +51557,7 @@ function computeFixes(text, errors, level = "safe") {
51465
51557
  }
51466
51558
  continue;
51467
51559
  }
51468
- if (is("FL-EDGE-LABEL-BRACKET", e)) {
51560
+ if (is("FL-EDGE-LABEL-BRACKET", e) || is("FL-EDGE-LABEL-CURLY-IN-PIPES", e)) {
51469
51561
  const lineText = lineTextAt(text, e.line);
51470
51562
  const firstBar = lineText.indexOf("|");
51471
51563
  const secondBar = firstBar >= 0 ? lineText.indexOf("|", firstBar + 1) : -1;
@@ -51473,7 +51565,8 @@ function computeFixes(text, errors, level = "safe") {
51473
51565
  const before = lineText.slice(0, firstBar + 1);
51474
51566
  const label = lineText.slice(firstBar + 1, secondBar);
51475
51567
  const after = lineText.slice(secondBar);
51476
- const fixedLabel = label.replace(/\[/g, "&#91;").replace(/\]/g, "&#93;");
51568
+ let fixedLabel = label.replace(/\[/g, "&#91;").replace(/\]/g, "&#93;");
51569
+ fixedLabel = fixedLabel.replace(/\{/g, "&#123;").replace(/\}/g, "&#125;");
51477
51570
  const fixedLine = before + fixedLabel + after;
51478
51571
  const finalLine = fixedLine.replace(/\[([^\]]*)\]/g, (m, seg) => "[" + String(seg).replace(/`/g, "") + "]");
51479
51572
  edits.push({ start: { line: e.line, column: 1 }, end: { line: e.line, column: lineText.length + 1 }, newText: finalLine });
@@ -52113,7 +52206,7 @@ function computeFixes(text, errors, level = "safe") {
52113
52206
  }
52114
52207
  continue;
52115
52208
  }
52116
- if (is("FL-LABEL-PARENS-UNQUOTED", e) || is("FL-LABEL-AT-IN-UNQUOTED", e) || is("FL-LABEL-SLASH-UNQUOTED", e)) {
52209
+ if (is("FL-LABEL-PARENS-UNQUOTED", e) || is("FL-LABEL-AT-IN-UNQUOTED", e) || is("FL-LABEL-SLASH-UNQUOTED", e) || is("FL-LABEL-CURLY-IN-UNQUOTED", e) || is("FL-LABEL-BRACKET-IN-UNQUOTED", e)) {
52117
52210
  if (level === "safe" || level === "all") {
52118
52211
  if (patchedLines.has(e.line))
52119
52212
  continue;
@@ -52154,7 +52247,7 @@ function computeFixes(text, errors, level = "safe") {
52154
52247
  if (openIdx === -1)
52155
52248
  break;
52156
52249
  const contentStart = openIdx + shape.open.length;
52157
- const closeIdx = shape.open === "(" && shape.close === ")" ? findMatchingCloser(lineText, openIdx, shape.open, shape.close) : lineText.indexOf(shape.close, contentStart);
52250
+ const closeIdx = shape.open === "(" && shape.close === ")" || shape.open === "[" && shape.close === "]" ? findMatchingCloser(lineText, openIdx, shape.open, shape.close) : lineText.indexOf(shape.close, contentStart);
52158
52251
  if (closeIdx === -1)
52159
52252
  break;
52160
52253
  if (openIdx <= caret0 && caret0 < closeIdx) {
@@ -52174,11 +52267,21 @@ function computeFixes(text, errors, level = "safe") {
52174
52267
  const isSlashPair = (l, r) => l === "/" && r === "/" || l === "\\" && r === "\\" || l === "/" && r === "\\" || l === "\\" && r === "/";
52175
52268
  const isParallelogramShape = core.length >= 2 && isSlashPair(left, right);
52176
52269
  let replaced;
52177
- if (!isParallelogramShape) {
52178
- const escaped = inner.replace(/`/g, "").replace(/\"/g, "&quot;").replace(/"/g, "&quot;");
52270
+ if (is("FL-LABEL-BRACKET-IN-UNQUOTED", e) && !isParallelogramShape) {
52271
+ const hasOtherHazards = /[(){}@]/.test(inner) || inner.includes('"') || inner.includes('\\"');
52272
+ if (hasOtherHazards) {
52273
+ const escaped = inner.replace(/`/g, "").replace(/\\"/g, "&quot;").replace(/"/g, "&quot;");
52274
+ replaced = '"' + escaped + '"';
52275
+ } else {
52276
+ replaced = inner.replace(/\[/g, "&#91;").replace(/\]/g, "&#93;");
52277
+ }
52278
+ } else if (is("FL-LABEL-CURLY-IN-UNQUOTED", e)) {
52279
+ replaced = inner.replace(/\{/g, "&#123;").replace(/\}/g, "&#125;");
52280
+ } else if (!isParallelogramShape) {
52281
+ const escaped = inner.replace(/`/g, "").replace(/\\"/g, "&quot;").replace(/"/g, "&quot;");
52179
52282
  replaced = '"' + escaped + '"';
52180
52283
  } else {
52181
- replaced = inner.replace(/`/g, "").replace(/\(/g, "&#40;").replace(/\)/g, "&#41;").replace(/\"/g, "&quot;").replace(/"/g, "&quot;");
52284
+ replaced = inner.replace(/`/g, "").replace(/\(/g, "&#40;").replace(/\)/g, "&#41;").replace(/\[/g, "&#91;").replace(/\]/g, "&#93;").replace(/\\"/g, "&quot;").replace(/"/g, "&quot;");
52182
52285
  }
52183
52286
  if (replaced !== inner) {
52184
52287
  edits.push({ start: { line: e.line, column: contentStart + 1 }, end: { line: e.line, column: closeIdx + 1 }, newText: replaced });
@@ -82266,14 +82369,6 @@ var init_FallbackManager = __esm({
82266
82369
  });
82267
82370
 
82268
82371
  // src/agent/contextCompactor.js
82269
- var contextCompactor_exports = {};
82270
- __export(contextCompactor_exports, {
82271
- calculateCompactionStats: () => calculateCompactionStats,
82272
- compactMessages: () => compactMessages,
82273
- handleContextLimitError: () => handleContextLimitError,
82274
- identifyMessageSegments: () => identifyMessageSegments,
82275
- isContextLimitError: () => isContextLimitError
82276
- });
82277
82372
  function isContextLimitError(error40) {
82278
82373
  if (!error40) return false;
82279
82374
  const errorMessage = (typeof error40 === "string" ? error40 : error40?.message || "").toLowerCase();
@@ -98870,6 +98965,16 @@ You are working with a workspace. Available paths: ${workspaceDesc}
98870
98965
  userMessage
98871
98966
  ];
98872
98967
  }
98968
+ if (this.history.length > 0) {
98969
+ const compacted = compactMessages(currentMessages, { keepLastSegment: true, minSegmentsToKeep: 1 });
98970
+ if (compacted.length < currentMessages.length) {
98971
+ const stats = calculateCompactionStats(currentMessages, compacted);
98972
+ if (this.debug) {
98973
+ console.log(`[DEBUG] Proactive history compaction: ${currentMessages.length} \u2192 ${compacted.length} messages (${stats.reductionPercent}% reduction, ~${stats.tokensSaved} tokens saved)`);
98974
+ }
98975
+ currentMessages = compacted;
98976
+ }
98977
+ }
98873
98978
  let currentIteration = 0;
98874
98979
  let finalResult = "I was unable to complete your request due to reaching the maximum number of tool iterations.";
98875
98980
  const baseMaxIterations = options._maxIterationsOverride || this.maxIterations || MAX_TOOL_ITERATIONS;
@@ -99570,7 +99675,6 @@ Double-check your response based on the criteria above. If everything looks good
99570
99675
  * @returns {Object} Compaction statistics
99571
99676
  */
99572
99677
  async compactHistory(options = {}) {
99573
- const { compactMessages: compactMessages2, calculateCompactionStats: calculateCompactionStats2 } = await Promise.resolve().then(() => (init_contextCompactor(), contextCompactor_exports));
99574
99678
  if (this.history.length === 0) {
99575
99679
  if (this.debug) {
99576
99680
  console.log(`[DEBUG] No history to compact for session ${this.sessionId}`);
@@ -99585,8 +99689,8 @@ Double-check your response based on the criteria above. If everything looks good
99585
99689
  tokensSaved: 0
99586
99690
  };
99587
99691
  }
99588
- const compactedMessages = compactMessages2(this.history, options);
99589
- const stats = calculateCompactionStats2(this.history, compactedMessages);
99692
+ const compactedMessages = compactMessages(this.history, options);
99693
+ const stats = calculateCompactionStats(this.history, compactedMessages);
99590
99694
  this.history = compactedMessages;
99591
99695
  try {
99592
99696
  await this.storageAdapter.clearHistory(this.sessionId);
@@ -100915,11 +101019,24 @@ function normalizeTargets(targets) {
100915
101019
  if (typeof target !== "string") continue;
100916
101020
  const trimmed = target.trim();
100917
101021
  if (!trimmed || seen.has(trimmed)) continue;
100918
- seen.add(trimmed);
100919
- normalized.push(trimmed);
101022
+ const subTargets = splitSpaceSeparatedPaths(trimmed);
101023
+ for (const sub of subTargets) {
101024
+ if (!seen.has(sub)) {
101025
+ seen.add(sub);
101026
+ normalized.push(sub);
101027
+ }
101028
+ }
100920
101029
  }
100921
101030
  return normalized;
100922
101031
  }
101032
+ function splitSpaceSeparatedPaths(target) {
101033
+ if (!/\s/.test(target)) return [target];
101034
+ const parts = target.split(/\s+/).filter(Boolean);
101035
+ if (parts.length <= 1) return [target];
101036
+ const allLookLikePaths = parts.every((p) => /[/\\]/.test(p) || /\.\w+/.test(p));
101037
+ if (allLookLikePaths) return parts;
101038
+ return [target];
101039
+ }
100923
101040
  function extractJsonSnippet(text) {
100924
101041
  const jsonBlockMatch = text.match(/```json\s*([\s\S]*?)```/i);
100925
101042
  if (jsonBlockMatch) {
@@ -101405,14 +101522,15 @@ var init_vercel = __esm({
101405
101522
  const parsedTargets = parseTargets(targets);
101406
101523
  extractFiles = parsedTargets.map((target) => resolveTargetPath(target, effectiveCwd));
101407
101524
  if (options.allowedFolders && options.allowedFolders.length > 0) {
101525
+ const { join: pathJoin, sep: pathSep } = await import("path");
101408
101526
  extractFiles = extractFiles.map((target) => {
101409
101527
  const { filePart, suffix } = splitTargetSuffix(target);
101410
101528
  if ((0, import_fs11.existsSync)(filePart)) return target;
101411
- const cwdPrefix = effectiveCwd.endsWith("/") ? effectiveCwd : effectiveCwd + "/";
101529
+ const cwdPrefix = effectiveCwd.endsWith(pathSep) ? effectiveCwd : effectiveCwd + pathSep;
101412
101530
  const relativePart = filePart.startsWith(cwdPrefix) ? filePart.slice(cwdPrefix.length) : null;
101413
101531
  if (relativePart) {
101414
101532
  for (const folder of options.allowedFolders) {
101415
- const candidate = folder + "/" + relativePart;
101533
+ const candidate = pathJoin(folder, relativePart);
101416
101534
  if ((0, import_fs11.existsSync)(candidate)) {
101417
101535
  if (debug) console.error(`[extract] Auto-fixed path: ${filePart} \u2192 ${candidate}`);
101418
101536
  return candidate + suffix;
@@ -101420,11 +101538,12 @@ var init_vercel = __esm({
101420
101538
  }
101421
101539
  }
101422
101540
  for (const folder of options.allowedFolders) {
101423
- const folderPrefix = folder.endsWith("/") ? folder : folder + "/";
101424
- const wsParent = folderPrefix.replace(/[^/]+\/$/, "");
101541
+ const folderPrefix = folder.endsWith(pathSep) ? folder : folder + pathSep;
101542
+ const sepEscaped = pathSep === "\\" ? "\\\\" : pathSep;
101543
+ const wsParent = folderPrefix.replace(new RegExp("[^" + sepEscaped + "]+" + sepEscaped + "$"), "");
101425
101544
  if (filePart.startsWith(wsParent)) {
101426
101545
  const tail = filePart.slice(wsParent.length);
101427
- const candidate = folderPrefix + tail;
101546
+ const candidate = pathJoin(folderPrefix, tail);
101428
101547
  if (candidate !== filePart && (0, import_fs11.existsSync)(candidate)) {
101429
101548
  if (debug) console.error(`[extract] Auto-fixed path via workspace: ${filePart} \u2192 ${candidate}`);
101430
101549
  return candidate + suffix;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc285",
3
+ "version": "0.6.0-rc287",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -80,11 +80,11 @@
80
80
  "@anthropic-ai/claude-agent-sdk": "^0.1.46",
81
81
  "@modelcontextprotocol/sdk": "^1.0.0",
82
82
  "@nyariv/sandboxjs": "github:probelabs/SandboxJS",
83
- "@probelabs/maid": "^0.0.26",
83
+ "@probelabs/maid": "^0.0.27",
84
84
  "acorn": "^8.15.0",
85
85
  "acorn-walk": "^8.3.4",
86
86
  "adm-zip": "^0.5.16",
87
- "ai": "^6.0.121",
87
+ "ai": "^6.0.0",
88
88
  "ajv": "^8.17.1",
89
89
  "astring": "^1.9.0",
90
90
  "axios": "^1.8.3",
@@ -93,7 +93,7 @@ import { formatAvailableSkillsXml as formatAvailableSkills } from './skills/form
93
93
  import { createSkillToolInstances } from './skills/tools.js';
94
94
  import { RetryManager, createRetryManagerFromEnv } from './RetryManager.js';
95
95
  import { FallbackManager, createFallbackManagerFromEnv, buildFallbackProvidersFromEnv } from './FallbackManager.js';
96
- import { handleContextLimitError } from './contextCompactor.js';
96
+ import { handleContextLimitError, compactMessages, calculateCompactionStats } from './contextCompactor.js';
97
97
  import { formatErrorForAI, ParameterError } from '../utils/error-types.js';
98
98
  import { getCommonPrefix, toRelativePath, safeRealpath } from '../utils/path-validation.js';
99
99
  import { truncateIfNeeded, getMaxOutputTokens } from './outputTruncator.js';
@@ -3227,6 +3227,25 @@ Follow these instructions carefully:
3227
3227
  ];
3228
3228
  }
3229
3229
 
3230
+ // Proactively compact for multi-turn conversations.
3231
+ // On turn 2+, previous turns contain full tool call/result history which can
3232
+ // be 50K+ tokens. This drowns out the new user message and causes the model to
3233
+ // focus on prior context rather than the new question.
3234
+ // compactMessages strips intermediate monologue from completed segments,
3235
+ // keeping user messages + final answers from prior turns.
3236
+ // Must run AFTER adding the new user message so the compactor sees 2+ segments
3237
+ // (completed prior turns + the new incomplete turn), preserving the latest segment.
3238
+ if (this.history.length > 0) {
3239
+ const compacted = compactMessages(currentMessages, { keepLastSegment: true, minSegmentsToKeep: 1 });
3240
+ if (compacted.length < currentMessages.length) {
3241
+ const stats = calculateCompactionStats(currentMessages, compacted);
3242
+ if (this.debug) {
3243
+ console.log(`[DEBUG] Proactive history compaction: ${currentMessages.length} → ${compacted.length} messages (${stats.reductionPercent}% reduction, ~${stats.tokensSaved} tokens saved)`);
3244
+ }
3245
+ currentMessages = compacted;
3246
+ }
3247
+ }
3248
+
3230
3249
  let currentIteration = 0;
3231
3250
  let finalResult = 'I was unable to complete your request due to reaching the maximum number of tool iterations.';
3232
3251
 
@@ -4141,8 +4160,6 @@ Double-check your response based on the criteria above. If everything looks good
4141
4160
  * @returns {Object} Compaction statistics
4142
4161
  */
4143
4162
  async compactHistory(options = {}) {
4144
- const { compactMessages, calculateCompactionStats } = await import('./contextCompactor.js');
4145
-
4146
4163
  if (this.history.length === 0) {
4147
4164
  if (this.debug) {
4148
4165
  console.log(`[DEBUG] No history to compact for session ${this.sessionId}`);
package/src/downloader.js CHANGED
@@ -743,7 +743,11 @@ async function extractBinary(assetPath, outputDir) {
743
743
  if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
744
744
  console.log(`Extracting zip to ${extractDir}...`);
745
745
  }
746
- await exec(`unzip -q "${assetPath}" -d "${extractDir}"`);
746
+ if (isWindows) {
747
+ await exec(`powershell -NoProfile -Command "Expand-Archive -Path '${assetPath}' -DestinationPath '${extractDir}' -Force"`);
748
+ } else {
749
+ await exec(`unzip -q "${assetPath}" -d "${extractDir}"`);
750
+ }
747
751
  } else {
748
752
  // Assume it's a direct binary
749
753
  if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
@@ -228,7 +228,17 @@ export function parseAndResolvePaths(pathStr, cwd) {
228
228
  if (!pathStr) return [];
229
229
 
230
230
  // Split on comma and trim whitespace
231
- const paths = pathStr.split(',').map(p => p.trim()).filter(p => p.length > 0);
231
+ let paths = pathStr.split(',').map(p => p.trim()).filter(p => p.length > 0);
232
+
233
+ // Auto-fix: model sometimes passes space-separated file paths as one string
234
+ // e.g. "src/ranking.rs src/simd_ranking.rs" — split if each part looks like a path
235
+ paths = paths.flatMap(p => {
236
+ if (!/\s/.test(p)) return [p];
237
+ const parts = p.split(/\s+/).filter(Boolean);
238
+ if (parts.length <= 1) return [p];
239
+ const allLookLikePaths = parts.every(part => /[/\\]/.test(part) || /\.\w+/.test(part));
240
+ return allLookLikePaths ? parts : [p];
241
+ });
232
242
 
233
243
  // Resolve relative paths against cwd
234
244
  return paths.map(p => {
@@ -105,13 +105,42 @@ function normalizeTargets(targets) {
105
105
  if (typeof target !== 'string') continue;
106
106
  const trimmed = target.trim();
107
107
  if (!trimmed || seen.has(trimmed)) continue;
108
- seen.add(trimmed);
109
- normalized.push(trimmed);
108
+
109
+ // Auto-fix: model sometimes puts multiple space-separated file paths in one string
110
+ // e.g. "src/ranking.rs src/simd_ranking.rs" — split them apart
111
+ const subTargets = splitSpaceSeparatedPaths(trimmed);
112
+ for (const sub of subTargets) {
113
+ if (!seen.has(sub)) {
114
+ seen.add(sub);
115
+ normalized.push(sub);
116
+ }
117
+ }
110
118
  }
111
119
 
112
120
  return normalized;
113
121
  }
114
122
 
123
+ /**
124
+ * Split a string that may contain multiple space-separated file paths.
125
+ * Detects patterns like "path/file.ext path2/file2.ext" and splits them.
126
+ * Preserves single paths and paths with suffixes like ":10-20" or "#Symbol".
127
+ */
128
+ function splitSpaceSeparatedPaths(target) {
129
+ // If no spaces, it's a single target
130
+ if (!/\s/.test(target)) return [target];
131
+
132
+ // Split on whitespace and check if parts look like file paths
133
+ const parts = target.split(/\s+/).filter(Boolean);
134
+ if (parts.length <= 1) return [target];
135
+
136
+ // Check if each part looks like a file path (has a dot extension or path separator)
137
+ const allLookLikePaths = parts.every(p => /[/\\]/.test(p) || /\.\w+/.test(p));
138
+ if (allLookLikePaths) return parts;
139
+
140
+ // Not confident these are separate paths — return as-is
141
+ return [target];
142
+ }
143
+
115
144
  function extractJsonSnippet(text) {
116
145
  const jsonBlockMatch = text.match(/```json\s*([\s\S]*?)```/i);
117
146
  if (jsonBlockMatch) {
@@ -691,19 +720,20 @@ export const extractTool = (options = {}) => {
691
720
  // model constructs wrong absolute paths (e.g., /workspace/gateway/file.go
692
721
  // instead of /workspace/tyk/gateway/file.go)
693
722
  if (options.allowedFolders && options.allowedFolders.length > 0) {
723
+ const { join: pathJoin, sep: pathSep } = await import('path');
694
724
  extractFiles = extractFiles.map(target => {
695
725
  const { filePart, suffix } = splitTargetSuffix(target);
696
726
  if (existsSync(filePart)) return target;
697
727
 
698
728
  // Try resolving the relative tail against each allowedFolder
699
- const cwdPrefix = (effectiveCwd.endsWith('/') ? effectiveCwd : effectiveCwd + '/');
729
+ const cwdPrefix = effectiveCwd.endsWith(pathSep) ? effectiveCwd : effectiveCwd + pathSep;
700
730
  const relativePart = filePart.startsWith(cwdPrefix)
701
731
  ? filePart.slice(cwdPrefix.length)
702
732
  : null;
703
733
 
704
734
  if (relativePart) {
705
735
  for (const folder of options.allowedFolders) {
706
- const candidate = folder + '/' + relativePart;
736
+ const candidate = pathJoin(folder, relativePart);
707
737
  if (existsSync(candidate)) {
708
738
  if (debug) console.error(`[extract] Auto-fixed path: ${filePart} → ${candidate}`);
709
739
  return candidate + suffix;
@@ -714,11 +744,12 @@ export const extractTool = (options = {}) => {
714
744
  // Try stripping workspace prefix and resolving against allowedFolders
715
745
  // e.g., /tmp/visor-workspaces/abc/gateway/file.go → try each folder + gateway/file.go
716
746
  for (const folder of options.allowedFolders) {
717
- const folderPrefix = folder.endsWith('/') ? folder : folder + '/';
718
- const wsParent = folderPrefix.replace(/[^/]+\/$/, '');
747
+ const folderPrefix = folder.endsWith(pathSep) ? folder : folder + pathSep;
748
+ const sepEscaped = pathSep === '\\' ? '\\\\' : pathSep;
749
+ const wsParent = folderPrefix.replace(new RegExp('[^' + sepEscaped + ']+' + sepEscaped + '$'), '');
719
750
  if (filePart.startsWith(wsParent)) {
720
751
  const tail = filePart.slice(wsParent.length);
721
- const candidate = folderPrefix + tail;
752
+ const candidate = pathJoin(folderPrefix, tail);
722
753
  if (candidate !== filePart && existsSync(candidate)) {
723
754
  if (debug) console.error(`[extract] Auto-fixed path via workspace: ${filePart} → ${candidate}`);
724
755
  return candidate + suffix;