@sema-agent/core 5.43.0 → 5.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { delimitUntrusted } from "../../core/untrusted-text.js";
2
2
  import { MAX_EXEC_OUTPUT_BYTES } from "../../core/exec-output-tail.js";
3
- import { hasBinaryExtension, isAbsolutePathForm } from "./safety.js";
3
+ import { hasBinaryExtension, isAbsolutePathForm, isWinFormPath } from "./safety.js";
4
4
  const WALK_MAX_FILES = 5000;
5
5
  const WALK_MAX_DEPTH = 32;
6
6
  const GREP_DEFAULT_CAP = 250;
@@ -35,6 +35,25 @@ const TYPE_GLOBS = {
35
35
  css: "*.{css,scss,less}",
36
36
  };
37
37
  const basename = (p) => p.slice(Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")) + 1);
38
+ function collapseDotSegments(p) {
39
+ const win = isWinFormPath(p);
40
+ const lead = (win ? /^[\\/]*/ : /^\/*/).exec(p)?.[0] ?? "";
41
+ const sep = win ? "\\" : "/";
42
+ const parts = p.slice(lead.length).split(win ? /[\\/]+/ : /\/+/);
43
+ const floor = !win ? 0 : lead.length >= 2 ? 2 : /^[A-Za-z]:$/.test(parts[0] ?? "") ? 1 : 0;
44
+ const kept = [];
45
+ for (const part of parts) {
46
+ if (part === "" || part === ".")
47
+ continue;
48
+ if (part === "..") {
49
+ if (kept.length > floor)
50
+ kept.pop();
51
+ continue;
52
+ }
53
+ kept.push(part);
54
+ }
55
+ return kept.length === 0 ? "" : lead + kept.join(sep);
56
+ }
38
57
  const NO_MATCHES = "No matches.";
39
58
  const GREP_LINE_CLIP = 500;
40
59
  function clipLine(line) {
@@ -759,10 +778,10 @@ function multilineSpans(content, p, captureText = false, deadline = Infinity) {
759
778
  }
760
779
  return spans;
761
780
  }
762
- export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
781
+ export async function jsGrepDetailed(env, root, p, signal, guards, deny, denyOut) {
763
782
  const analysis = analyzeRedos(p.pattern, p.ignore_case === true);
764
783
  if (analysis.risk)
765
- return redosRejection(analysis.risk);
784
+ return { text: redosRejection(analysis.risk) };
766
785
  const budgetMs = guards?.budgetMs ?? GREP_FILE_BUDGET_MS;
767
786
  const longLineLimit = guards?.longLineLimit ?? GREP_GRAY_LINE_MAX;
768
787
  const grayPattern = analysis.quantifiedGroup || analysis.unboundedCount >= 2;
@@ -776,7 +795,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
776
795
  test = compilePattern(p);
777
796
  }
778
797
  catch (e) {
779
- return `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}`;
798
+ return { text: `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}` };
780
799
  }
781
800
  const ignore = await buildIgnore(env, root, signal);
782
801
  const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
@@ -816,9 +835,20 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
816
835
  const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
817
836
  const mode = p.output_mode ?? "files_with_matches";
818
837
  const rel = (abs) => (abs.startsWith(rootPrefix) ? abs.slice(rootPrefix.length) : abs);
838
+ const startWin = isWinFormPath(start);
839
+ const startNorm = collapseDotSegments(start);
840
+ const targetSpell = p.path ? p.path.replace(startWin ? /[\\/]+$/ : /\/+$/, "") : "";
841
+ const isBoundary = (c) => c === "/" || (startWin && c === "\\");
819
842
  const relOut = (abs) => {
820
- const r = rel(abs);
821
- return p.path || r === abs ? r : `./${r}`;
843
+ if (!p.path) {
844
+ const r = rel(abs);
845
+ return r === abs ? r : `./${r}`;
846
+ }
847
+ if (abs === startNorm || abs === start)
848
+ return targetSpell;
849
+ if (abs.startsWith(startNorm) && isBoundary(abs[startNorm.length]))
850
+ return targetSpell + abs.slice(startNorm.length);
851
+ return rel(abs);
822
852
  };
823
853
  if (p.glob) {
824
854
  const globRes = splitGlobParam(p.glob).map((g) => globTokenToRegExp(normalizeGlobToken(g), globIsAnchored(g), true));
@@ -846,7 +876,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
846
876
  let outBytes = 0;
847
877
  let outputTruncated = false;
848
878
  let rowsBeforeWindow = 0;
849
- const pushRow = (row) => {
879
+ const pushRow = (row, path) => {
850
880
  if (outputTruncated)
851
881
  return;
852
882
  if (rowsBeforeWindow < off) {
@@ -861,7 +891,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
861
891
  return;
862
892
  }
863
893
  outBytes += size;
864
- out.push(row);
894
+ out.push({ path, text: row });
865
895
  };
866
896
  const fileMatches = [];
867
897
  const counts = [];
@@ -889,6 +919,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
889
919
  const lines = read.value.split("\n");
890
920
  if (lines.length > 1 && lines.at(-1) === "")
891
921
  lines.pop();
922
+ const shownPath = relOut(f);
892
923
  let fileCount = 0;
893
924
  const fileMatchStart = monotonicNow();
894
925
  if (p.multiline) {
@@ -902,8 +933,8 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
902
933
  }
903
934
  catch (e) {
904
935
  if (e instanceof GrepBudgetExceeded)
905
- return budgetError(relOut(f), monotonicNow() - fileMatchStart);
906
- return `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}`;
936
+ return { text: budgetError(shownPath, monotonicNow() - fileMatchStart) };
937
+ return { text: `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}` };
907
938
  }
908
939
  fileCount = spans.length;
909
940
  if (mode === "content") {
@@ -911,12 +942,12 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
911
942
  if (p.only_matching && text !== undefined) {
912
943
  const parts = text.split("\n");
913
944
  for (let k = 0; k < parts.length; k++) {
914
- pushRow(`${relOut(f)}:${s + k}:${clipLine(parts[k])}`);
945
+ pushRow(`${shownPath}:${s + k}:${clipLine(parts[k])}`, shownPath);
915
946
  }
916
947
  continue;
917
948
  }
918
949
  for (let j = Math.max(0, s - 1 - ctxB); j <= Math.min(lines.length - 1, eL - 1 + ctxA); j++) {
919
- pushRow(`${relOut(f)}:${j + 1}:${clipLine(lines[j])}`);
950
+ pushRow(`${shownPath}:${j + 1}:${clipLine(lines[j])}`, shownPath);
920
951
  }
921
952
  }
922
953
  }
@@ -926,7 +957,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
926
957
  if (grayPattern || (i > 0 && i % GREP_LINE_CHECK_EVERY === 0)) {
927
958
  const elapsed = monotonicNow() - fileMatchStart;
928
959
  if (elapsed >= budgetMs)
929
- return budgetError(relOut(f), elapsed);
960
+ return { text: budgetError(shownPath, elapsed) };
930
961
  }
931
962
  if (i > 0 && i % GREP_LINE_CHECK_EVERY === 0) {
932
963
  await yieldEventLoop();
@@ -945,16 +976,16 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
945
976
  if (mode === "content") {
946
977
  if (p.only_matching) {
947
978
  for (const part of matchedParts(lines[i], p)) {
948
- pushRow(`${relOut(f)}:${i + 1}:${clipLine(part)}`);
979
+ pushRow(`${shownPath}:${i + 1}:${clipLine(part)}`, shownPath);
949
980
  }
950
981
  }
951
982
  else if (ctx > 0) {
952
983
  for (let j = Math.max(0, i - ctxB); j <= Math.min(lines.length - 1, i + ctxA); j++) {
953
- pushRow(`${relOut(f)}:${j + 1}:${clipLine(lines[j])}`);
984
+ pushRow(`${shownPath}:${j + 1}:${clipLine(lines[j])}`, shownPath);
954
985
  }
955
986
  }
956
987
  else {
957
- pushRow(`${relOut(f)}:${i + 1}:${clipLine(lines[i])}`);
988
+ pushRow(`${shownPath}:${i + 1}:${clipLine(lines[i])}`, shownPath);
958
989
  }
959
990
  }
960
991
  }
@@ -968,7 +999,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
968
999
  if (mode === "files_with_matches")
969
1000
  fileMatches.push(f);
970
1001
  else if (mode === "count" && counts.length < collectCap)
971
- counts.push(`${relOut(f)}:${fileCount}`);
1002
+ counts.push({ path: shownPath, text: `${shownPath}:${fileCount}` });
972
1003
  }
973
1004
  }
974
1005
  if (scanAborted || skippedUnreadableFiles > 0 || skippedLongLines > 0 || multilineSkippedFiles > 0) {
@@ -988,7 +1019,7 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
988
1019
  }
989
1020
  }
990
1021
  const caveat = honestyCaveat + denyNote;
991
- const paged = (arr) => (off > 0 ? arr.slice(off, off + cap) : arr);
1022
+ const paged = (arr) => (off > 0 ? arr.slice(off, off + cap) : [...arr]);
992
1023
  const offNote = off > 0 ? `\n[offset ${off}]` : "";
993
1024
  if (mode === "files_with_matches") {
994
1025
  const haveMtimes = fileMatches.length > 0 && fileMatches.every((f) => walked.mtimes.has(f));
@@ -999,26 +1030,37 @@ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
999
1030
  .map((x) => x.r)
1000
1031
  : fileMatches.map(relOut).sort();
1001
1032
  const body = sortedFileMatches.slice(off, off + cap);
1002
- return body.length === 0
1003
- ? NO_MATCHES + offNote + typeNote + caveat
1004
- : body.join("\n") + (totalFiles > off + cap ? `\n…[capped at ${cap} of ${totalFiles}]` : "") + offNote + typeNote + caveat;
1033
+ const rows = body.map((r) => ({ path: r, text: r }));
1034
+ return {
1035
+ text: body.length === 0
1036
+ ? NO_MATCHES + offNote + typeNote + caveat
1037
+ : body.join("\n") + (totalFiles > off + cap ? `\n…[capped at ${cap} of ${totalFiles}]` : "") + offNote + typeNote + caveat,
1038
+ rows,
1039
+ };
1005
1040
  }
1006
1041
  if (mode === "count") {
1007
1042
  const body = paged(counts);
1008
- return body.length === 0
1009
- ? NO_MATCHES + offNote + typeNote + caveat
1010
- : body.join("\n") + (totalFiles > off + cap ? `\n…[capped at ${cap} of ${totalFiles}]` : "") + offNote + typeNote + caveat;
1043
+ return {
1044
+ text: body.length === 0
1045
+ ? NO_MATCHES + offNote + typeNote + caveat
1046
+ : body.map((r) => r.text).join("\n") + (totalFiles > off + cap ? `\n…[capped at ${cap} of ${totalFiles}]` : "") + offNote + typeNote + caveat,
1047
+ rows: body,
1048
+ };
1011
1049
  }
1012
1050
  const body = out;
1013
1051
  const byteNote = outputTruncated ? `\n…[output truncated at ${outputMaxBytes} bytes — narrow the pattern or set a smaller head_limit]` : "";
1014
1052
  if (body.length === 0)
1015
- return NO_MATCHES + byteNote + offNote + typeNote + caveat;
1053
+ return { text: NO_MATCHES + byteNote + offNote + typeNote + caveat, rows: [] };
1054
+ const bodyText = body.map((r) => r.text).join("\n");
1016
1055
  if (out.length < cap)
1017
- return body.join("\n") + byteNote + offNote + typeNote + caveat;
1056
+ return { text: bodyText + byteNote + offNote + typeNote + caveat, rows: body };
1018
1057
  const marker = ctx > 0 || p.multiline || p.only_matching
1019
1058
  ? `\n…[capped at ${cap}; ${totalContent}+ match(es) found, more output omitted]`
1020
1059
  : `\n…[capped at ${cap} of ${totalContent}]`;
1021
- return body.join("\n") + marker + byteNote + offNote + typeNote + caveat;
1060
+ return { text: bodyText + marker + byteNote + offNote + typeNote + caveat, rows: body };
1061
+ }
1062
+ export async function jsGrep(env, root, p, signal, guards, deny, denyOut) {
1063
+ return (await jsGrepDetailed(env, root, p, signal, guards, deny, denyOut)).text;
1022
1064
  }
1023
1065
  const rgCache = new WeakMap();
1024
1066
  export function detectRipgrep(env) {
@@ -1124,23 +1166,25 @@ export function rgOutputDenyTripwire(stdout, mode, judge, opts = {}) {
1124
1166
  }
1125
1167
  async function jsGrepDenyTripFallback(env, root, p, signal, trip, deny) {
1126
1168
  const denyOut = {};
1127
- const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
1169
+ const { text, rows } = await jsGrepDetailed(env, root, p, signal, undefined, deny, denyOut);
1128
1170
  const why = trip.reason === "deny-hit" ? "its output involved sensitive-path deny-listed entries the exclusion globs cannot express" : "its output contained a record the line format cannot account for";
1129
1171
  return {
1130
1172
  text: text.startsWith("Error (grep)")
1131
1173
  ? text
1132
1174
  : `${text}\n[note: the ripgrep pass was abandoned (${why}); results are from the fallback scanner, which prunes with the authoritative deny judge]`,
1175
+ ...(rows !== undefined ? { rows } : {}),
1133
1176
  degraded: { fallback: "js-scan", reason: `ripgrep output tripped the deny tripwire (${trip.reason})` },
1134
1177
  ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}),
1135
1178
  };
1136
1179
  }
1137
1180
  async function jsGrepFallback(env, root, p, signal, reason, deny) {
1138
1181
  const denyOut = {};
1139
- const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
1182
+ const { text, rows } = await jsGrepDetailed(env, root, p, signal, undefined, deny, denyOut);
1140
1183
  return {
1141
1184
  text: text.startsWith("Error (grep)")
1142
1185
  ? text
1143
1186
  : `${text}\n[note: ripgrep failed (${reason}); results are from a fallback scanner]`,
1187
+ ...(rows !== undefined ? { rows } : {}),
1144
1188
  degraded: { fallback: "js-scan", reason: `ripgrep failed (${reason})` },
1145
1189
  ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}),
1146
1190
  };
@@ -1288,8 +1332,8 @@ export async function runGrepDetailed(env, root, p, signal, deny) {
1288
1332
  if (await detectRipgrep(env))
1289
1333
  return rgGrepDetailed(env, root, p, signal, deny);
1290
1334
  const denyOut = {};
1291
- const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
1292
- return { text, ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}) };
1335
+ const { text, rows } = await jsGrepDetailed(env, root, p, signal, undefined, deny, denyOut);
1336
+ return { text, ...(rows !== undefined ? { rows } : {}), ...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}) };
1293
1337
  }
1294
1338
  export async function runGrep(env, root, p, signal) {
1295
1339
  return (await runGrepDetailed(env, root, p, signal)).text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.43.0",
3
+ "version": "5.45.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -65,7 +65,8 @@
65
65
  "gate:single-mint": "node scripts/verify-single-mint.mjs",
66
66
  "gate:nuia": "node scripts/verify-nuia-baseline.mjs",
67
67
  "gate:field-liveness": "node scripts/verify-field-liveness.mjs",
68
- "gate:error-surface": "node scripts/verify-error-surface.mjs"
68
+ "gate:error-surface": "node scripts/verify-error-surface.mjs",
69
+ "gate:criteria": "node scripts/criteria-lint.mjs"
69
70
  },
70
71
  "dependencies": {
71
72
  "@modelcontextprotocol/sdk": "1.30.0",