@fieldwangai/agentflow 0.1.167 → 0.1.168

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.
@@ -93,6 +93,28 @@ function cursorResultErrorText(event) {
93
93
  return "";
94
94
  }
95
95
 
96
+ export function isCursorAgentLoopingError(error = "") {
97
+ const text = String(error?.message || error || "");
98
+ return /agent looping detected|got stuck in a repeating response pattern/i.test(text);
99
+ }
100
+
101
+ function isCursorReadOnlyToolCall(toolName = "") {
102
+ const name = String(toolName || "").trim();
103
+ if (!name) return false;
104
+ return /^(read|glob|grep|search|semanticSearch|list|find|fetch|webSearch|view|inspect)/i.test(name);
105
+ }
106
+
107
+ function annotateCursorFailure(error, { hadToolActivity = false, hadMutatingToolActivity = false } = {}) {
108
+ const failure = error instanceof Error ? error : new Error(String(error || "Cursor Agent failed."));
109
+ failure.cursorHadToolActivity = Boolean(hadToolActivity);
110
+ failure.cursorHadMutatingToolActivity = Boolean(hadMutatingToolActivity);
111
+ if (isCursorAgentLoopingError(failure)) {
112
+ failure.code = "CURSOR_AGENT_LOOPING";
113
+ failure.agentflowFailureCategory = "agent_looping";
114
+ }
115
+ return failure;
116
+ }
117
+
96
118
  function envFlag(name, defaultValue = false) {
97
119
  const raw = process.env[name];
98
120
  if (raw == null || raw === "") return defaultValue;
@@ -459,6 +481,14 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
459
481
  let lastResult = null;
460
482
  let hadError = false;
461
483
  let hadToolActivity = false;
484
+ let hadMutatingToolActivity = false;
485
+ const annotateFailure = (error) => {
486
+ const failure = annotateCursorFailure(error, { hadToolActivity, hadMutatingToolActivity });
487
+ failure.cursorModelLane = cursorModelSelection.lane;
488
+ failure.cursorModelId = cursorModelSelection.modelId;
489
+ failure.cursorModelName = cursorModelSelection.modelName;
490
+ return failure;
491
+ };
462
492
  const STDERR_CAP_BYTES = 1024 * 1024;
463
493
  const stderrChunks = [];
464
494
  let stderrTotalBytes = 0;
@@ -546,6 +576,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
546
576
  hadToolActivity = true;
547
577
  const toolName =
548
578
  event.tool_call && typeof event.tool_call === "object" ? Object.keys(event.tool_call)[0] ?? "?" : "?";
579
+ if (!isCursorReadOnlyToolCall(toolName)) hadMutatingToolActivity = true;
549
580
  const subtype = event.subtype ?? "";
550
581
  const statusLine = `工具 ${toolName}${subtype ? ` (${subtype})` : ""}`;
551
582
  emit({ type: "status", line: statusLine });
@@ -583,13 +614,18 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
583
614
  if (line.includes('"type":"tool_call"') || line.includes('"type": "tool_call"')) {
584
615
  hadToolActivity = true;
585
616
  let subtype = "?";
617
+ let toolName = "?";
586
618
  try {
587
619
  const ev = JSON.parse(line);
588
- if (ev && ev.type === "tool_call") subtype = ev.subtype ?? "?";
620
+ if (ev && ev.type === "tool_call") {
621
+ subtype = ev.subtype ?? "?";
622
+ toolName = ev.tool_call && typeof ev.tool_call === "object" ? Object.keys(ev.tool_call)[0] ?? "?" : "?";
623
+ }
589
624
  } catch {
590
625
  const m = line.match(/"subtype"\s*:\s*"([^"]+)"/);
591
626
  if (m) subtype = m[1];
592
627
  }
628
+ if (!isCursorReadOnlyToolCall(toolName)) hadMutatingToolActivity = true;
593
629
  emit({ type: "status", line: t("runner.tool_call", { subtype }) });
594
630
  } else if (isLikelyBase64(line)) {
595
631
  emit({ type: "status", line: t("runner.base64_data", { len: line.length }) });
@@ -624,44 +660,55 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
624
660
  const rest = stderrComposerBuffer.trim();
625
661
  emit({ type: "status", line: `[stderr] ${truncateComposerLine(rest)}` });
626
662
  }
627
- const retryCursorQuota = (errorText) => {
628
- if (!cursorSelection) return false;
629
- if (hadToolActivity) return false;
630
- if (!isCursorQuotaError(errorText)) return false;
631
- const errorCategory = classifyCursorApiKeyLimitError(errorText);
632
- const cooldownMinutes = cursorApiKeyCooldownMinutes(cursorBaseEnv, errorText);
633
- markCursorApiKeyLaneBlocked(
634
- cursorSelection,
635
- cursorModelSelection.lane,
636
- cooldownMinutes,
637
- errorText,
638
- Date.now(),
639
- {
640
- modelId: cursorModelSelection.modelId,
641
- modelName: cursorModelSelection.modelName,
642
- },
643
- );
663
+ const retryCursorFailure = (errorText) => {
664
+ const quotaFailure = isCursorQuotaError(errorText);
665
+ const loopingFailure = isCursorAgentLoopingError(errorText);
666
+ if (!quotaFailure && !loopingFailure) return false;
667
+ if (quotaFailure && hadToolActivity) return false;
668
+ if (loopingFailure && hadMutatingToolActivity) return false;
669
+ const errorCategory = quotaFailure ? classifyCursorApiKeyLimitError(errorText) : "agent_looping";
670
+ if (quotaFailure && cursorSelection) {
671
+ const cooldownMinutes = cursorApiKeyCooldownMinutes(cursorBaseEnv, errorText);
672
+ markCursorApiKeyLaneBlocked(
673
+ cursorSelection,
674
+ cursorModelSelection.lane,
675
+ cooldownMinutes,
676
+ errorText,
677
+ Date.now(),
678
+ {
679
+ modelId: cursorModelSelection.modelId,
680
+ modelName: cursorModelSelection.modelName,
681
+ },
682
+ );
683
+ }
644
684
  const canTryComposer = !hasExplicitModel
645
685
  && cursorModelSelection.lane === "auto"
646
- && errorCategory === "explicit_limit"
647
- && isCursorAutoFallbackEligible(errorText);
648
- const hasNextKey = cursorAttemptIndex < cursorAttempts.length - 1;
686
+ && (
687
+ loopingFailure
688
+ || (errorCategory === "explicit_limit" && isCursorAutoFallbackEligible(errorText))
689
+ );
690
+ const hasNextKey = quotaFailure && Boolean(cursorSelection) && cursorAttemptIndex < cursorAttempts.length - 1;
649
691
  if (!canTryComposer && !hasNextKey) return false;
650
692
 
651
693
  const retry = async () => {
652
694
  if (canTryComposer) {
695
+ const authLabel = cursorSelection
696
+ ? `API Key ${cursorApiKeyLabel(cursorSelection)}`
697
+ : "login session";
653
698
  emit({
654
699
  type: "status",
655
- line: `Cursor Auto on API Key ${cursorApiKeyLabel(cursorSelection)} is out of usage; discovering Composer fallback...`,
700
+ line: loopingFailure
701
+ ? `Cursor Auto on ${authLabel} entered a response loop; discovering Composer fallback...`
702
+ : `Cursor Auto on ${authLabel} is out of usage; discovering Composer fallback...`,
656
703
  });
657
704
  const catalog = await discoverCursorModels({
658
- keyId: cursorSelection.id,
705
+ keyId: cursorSelection?.id || `login:${cursorBaseEnv.AGENTFLOW_USER_ID || "default"}`,
659
706
  cwd: ws,
660
707
  command: agentCmd,
661
708
  env: childEnv(options, cursorApiKeyEnv(cursorSelection)),
662
709
  });
663
710
  if (catalog.fallbackModel) {
664
- recordCursorApiKeyFallbackModel(cursorSelection, catalog.fallbackModel);
711
+ if (cursorSelection) recordCursorApiKeyFallbackModel(cursorSelection, catalog.fallbackModel);
665
712
  const fallbackSelection = {
666
713
  lane: "fallback",
667
714
  modelId: catalog.fallbackModel.id,
@@ -677,6 +724,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
677
724
  stream: "runner",
678
725
  eventType: "model_fallback",
679
726
  text: `auto -> ${fallbackSelection.modelId}`,
727
+ reason: loopingFailure ? "agent_looping" : "usage_limit",
680
728
  });
681
729
  const fallback = runCursorAgentWithPrompt(
682
730
  cliWorkspace,
@@ -705,7 +753,7 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
705
753
  await next.finished;
706
754
  return;
707
755
  }
708
- throw new Error(errorText || "Cursor API Key reached its limit.");
756
+ throw annotateFailure(new Error(errorText || (loopingFailure ? "Cursor Agent entered a response loop." : "Cursor API Key reached its limit.")));
709
757
  };
710
758
  retry().then(resolve).catch(reject);
711
759
  return true;
@@ -713,9 +761,9 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
713
761
  if (code !== 0 && lastResult == null) {
714
762
  const stderr = Buffer.concat(stderrChunks).toString("utf-8");
715
763
  const stderrTail = stderr ? stderr.trim().slice(-1200) : "";
716
- if (retryCursorQuota(stderrTail)) return;
764
+ if (retryCursorFailure(stderrTail)) return;
717
765
  const stderrSummary = summarizeCursorStderr(stderr);
718
- const err = new Error(`Cursor CLI exited ${code}. ${stderrSummary || "No result event received."}`);
766
+ const err = annotateFailure(new Error(`Cursor CLI exited ${code}. ${stderrSummary || "No result event received."}`));
719
767
  err.cursorStderrTail = stderrTail;
720
768
  emit({ type: "status", line: truncateComposerLine(err.message) });
721
769
  reject(err);
@@ -723,9 +771,9 @@ export function runCursorAgentWithPrompt(cliWorkspace, promptText, options = {})
723
771
  }
724
772
  if (hadError || (lastResult && lastResult.is_error)) {
725
773
  const msg = cursorResultErrorText(lastResult) || "Agent reported error.";
726
- if (retryCursorQuota(msg)) return;
774
+ if (retryCursorFailure(msg)) return;
727
775
  emit({ type: "status", line: truncateComposerLine(msg) });
728
- reject(new Error(msg));
776
+ reject(annotateFailure(new Error(msg)));
729
777
  return;
730
778
  }
731
779
  if (cursorSelection) clearCursorApiKeyLaneCooldown(cursorSelection, cursorModelSelection.lane);
@@ -0,0 +1,101 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parse as acornParse } from "acorn";
4
+
5
+ import { PACKAGE_ROOT } from "./paths.mjs";
6
+
7
+ const WORKSPACE_RUNTIME_PATH = "bin/lib/workspace-server.mjs";
8
+
9
+ // Only modules explicitly listed here are exposed in full. The runtime adapter is
10
+ // extracted from workspace-server.mjs so the review always follows the code that
11
+ // actually dispatches the built-in node without exposing the whole server file.
12
+ const BUILTIN_NODE_IMPLEMENTATION_FILES = new Map([
13
+ ["tool_wecom_send_group_markdown", ["bin/lib/wecom.mjs"]],
14
+ ["tool_wecom_send_app_markdown", ["bin/lib/wecom.mjs"]],
15
+ ]);
16
+
17
+ const sourceCache = new Map();
18
+ let runtimeAstCache = null;
19
+
20
+ function readPackageSource(relativePath) {
21
+ const normalized = String(relativePath || "").replace(/\\/g, "/").replace(/^\/+/, "");
22
+ if (!normalized || normalized.includes("..")) return "";
23
+ if (sourceCache.has(normalized)) return sourceCache.get(normalized);
24
+ const absolutePath = path.join(PACKAGE_ROOT, normalized);
25
+ let source = "";
26
+ try {
27
+ if (fs.statSync(absolutePath).isFile()) source = fs.readFileSync(absolutePath, "utf8").replace(/\r\n/g, "\n");
28
+ } catch {}
29
+ sourceCache.set(normalized, source);
30
+ return source;
31
+ }
32
+
33
+ function walkAst(node, visit) {
34
+ if (!node || typeof node !== "object") return;
35
+ if (typeof node.type === "string") visit(node);
36
+ for (const [key, value] of Object.entries(node)) {
37
+ if (["start", "end", "loc", "type"].includes(key)) continue;
38
+ if (Array.isArray(value)) {
39
+ for (const child of value) walkAst(child, visit);
40
+ } else if (value && typeof value === "object") {
41
+ walkAst(value, visit);
42
+ }
43
+ }
44
+ }
45
+
46
+ function astContainsString(node, expected) {
47
+ let found = false;
48
+ walkAst(node, (candidate) => {
49
+ if (candidate.type === "Literal" && candidate.value === expected) found = true;
50
+ });
51
+ return found;
52
+ }
53
+
54
+ function runtimeAst() {
55
+ const source = readPackageSource(WORKSPACE_RUNTIME_PATH);
56
+ if (!source) return { source: "", ast: null };
57
+ if (runtimeAstCache?.source === source) return runtimeAstCache;
58
+ let ast = null;
59
+ try {
60
+ ast = acornParse(source, { ecmaVersion: "latest", sourceType: "module", allowHashBang: true });
61
+ } catch {}
62
+ runtimeAstCache = { source, ast };
63
+ return runtimeAstCache;
64
+ }
65
+
66
+ function runtimeAdapterSource(definitionId) {
67
+ const { source, ast } = runtimeAst();
68
+ if (!source || !ast) return "";
69
+ const matches = [];
70
+ walkAst(ast, (node) => {
71
+ if (node.type !== "IfStatement" || !astContainsString(node.test, definitionId)) return;
72
+ matches.push(source.slice(node.start, node.end).trim());
73
+ });
74
+ return matches.join("\n\n");
75
+ }
76
+
77
+ export function builtinNodeReviewSources(definitionId = "") {
78
+ const id = String(definitionId || "").trim();
79
+ if (!id) return [];
80
+ const sources = [];
81
+ const adapter = runtimeAdapterSource(id);
82
+ if (adapter) {
83
+ sources.push({
84
+ sourcePath: `builtin/${id}/runtime-adapter.mjs`,
85
+ title: "内置运行适配器",
86
+ kind: "builtin-adapter",
87
+ content: adapter,
88
+ });
89
+ }
90
+ for (const relativePath of BUILTIN_NODE_IMPLEMENTATION_FILES.get(id) || []) {
91
+ const content = readPackageSource(relativePath);
92
+ if (!content) continue;
93
+ sources.push({
94
+ sourcePath: relativePath,
95
+ title: `内置实现 · ${path.basename(relativePath)}`,
96
+ kind: "builtin-implementation",
97
+ content,
98
+ });
99
+ }
100
+ return sources;
101
+ }
@@ -20,7 +20,7 @@ import {
20
20
  workspaceRunPlan,
21
21
  } from "./workspace-server.mjs";
22
22
 
23
- const REPOSITORY_INDEX_VERSION = 1;
23
+ const REPOSITORY_INDEX_VERSION = 2;
24
24
  const REPOSITORY_INDEX_FILENAME = "repository-index.json";
25
25
  const REPOSITORY_INDEX_MAX_AGE_MS = 5 * 60 * 1000;
26
26
  const memoryIndexes = new Map();
@@ -167,10 +167,12 @@ function scanProjectFlows(workspaceRoot, usageStats) {
167
167
  const appendFlow = (ownerId, flow, flowSource = "user", workspaceId = "") => {
168
168
  if (!ownerId || flow.archived || !flow.path) return;
169
169
  let stable;
170
+ let draftGraph;
170
171
  let graph;
171
172
  try {
173
+ draftGraph = readWorkspaceGraph(flow.path, workspaceRoot).graph;
172
174
  stable = readWorkspaceStableRelease(flow.path, workspaceRoot);
173
- graph = stable?.graph || readWorkspaceGraph(flow.path, workspaceRoot).graph;
175
+ graph = stable?.graph || draftGraph;
174
176
  } catch {
175
177
  return;
176
178
  }
@@ -181,6 +183,13 @@ function scanProjectFlows(workspaceRoot, usageStats) {
181
183
  const baseId = projectFlowRepositoryId(ownerId, flowSource, flow.id);
182
184
  const id = runnableEntries.length === 1 ? baseId : `${baseId}:${runnable.entryId}`;
183
185
  const version = stable?.release?.id || `current-${workspaceDesignRevision(graph).slice(0, 12)}`;
186
+ const releaseState = stable?.release?.id ? "stable" : "draft";
187
+ const draftRevision = workspaceDesignRevision(draftGraph);
188
+ const stableRevisions = new Set([
189
+ stable?.release?.designRevision || "",
190
+ stable?.graph ? workspaceDesignRevision(stable.graph) : "",
191
+ ].filter(Boolean));
192
+ const hasUnpublishedChanges = Boolean(stable?.release?.id && !stableRevisions.has(draftRevision));
184
193
  const rawEntryLabel = String(runnable.entry?.label || "").trim();
185
194
  const genericLabel = ["", "Run", "Scheduled Run", "运行", "定时运行"].includes(rawEntryLabel);
186
195
  const exactOwner = flowSource === "user" ? ownerId : "";
@@ -200,6 +209,9 @@ function scanProjectFlows(workspaceRoot, usageStats) {
200
209
  description: flow.description || "",
201
210
  version,
202
211
  versionLabel: stable?.release?.id ? `Stable ${stable.release.id}` : "当前版本",
212
+ releaseState,
213
+ stableReleaseId: stable?.release?.id || "",
214
+ hasUnpublishedChanges,
203
215
  runMode: runnable.runMode,
204
216
  runModeLabel: runnable.runMode === "scheduled" ? "定时运行" : "手动运行",
205
217
  ownerUserId: ownerId,
@@ -27,6 +27,7 @@ import {
27
27
  writeAiExplorationMaterialization,
28
28
  } from "./ai-exploration.mjs";
29
29
  import { buildSkillCompactInjectionBlock, loadResourcesForSkillKeys } from "./composer-skill-router.mjs";
30
+ import { builtinNodeReviewSources } from "./builtin-node-review.mjs";
30
31
  import { execFileBuffered } from "./exec-buffered.mjs";
31
32
  import { runGit } from "./git-worktree.mjs";
32
33
  import {
@@ -393,6 +394,10 @@ function workspaceNodeReviewSnapshot(workspaceRoot, flowRoot, graph, nodeId, use
393
394
  }));
394
395
  }
395
396
 
397
+ if (!marketplaceRef.startsWith("marketplace:")) {
398
+ sources.push(...builtinNodeReviewSources(definitionId).map((source) => workspaceNodeReviewSource(source)));
399
+ }
400
+
396
401
  for (const [ref, kind] of [[instance.scriptRef, "script"], [instance.implementationRef, "implementation"]]) {
397
402
  const source = workspaceNodeReviewFlowFile(flowRoot, ref, kind);
398
403
  if (source?.error) errors.push(source.error);
@@ -1102,7 +1107,7 @@ function listRunnableProjectMarketplaceFlows(workspaceRoot, userCtx = {}, scope
1102
1107
  }
1103
1108
 
1104
1109
  function publicProjectFlowMarketplaceResource(resource) {
1105
- const { _graph, _flowRoot, ...publicResource } = resource;
1110
+ const { _graph, _flowRoot, flowRoot, ...publicResource } = resource;
1106
1111
  return publicResource;
1107
1112
  }
1108
1113
 
@@ -1258,6 +1263,8 @@ async function workspaceRoutes(req, res, ctx) {
1258
1263
  const kind = String(url.searchParams.get("kind") || "flow").trim();
1259
1264
  const requestedScope = String(url.searchParams.get("scope") || "all").trim();
1260
1265
  const scope = ["all", "owned", "installed"].includes(requestedScope) ? requestedScope : "all";
1266
+ const requestedReleaseState = String(url.searchParams.get("releaseState") || "all").trim().toLowerCase();
1267
+ const releaseState = ["all", "stable", "draft"].includes(requestedReleaseState) ? requestedReleaseState : "all";
1261
1268
  const marketplaceScope = scope === "owned" ? "owned" : "all";
1262
1269
  const queryText = String(url.searchParams.get("q") || "").trim().toLowerCase();
1263
1270
  if (kind === "node") {
@@ -1320,6 +1327,7 @@ async function workspaceRoutes(req, res, ctx) {
1320
1327
  const items = sortMarketplaceResources(
1321
1328
  projectFlows
1322
1329
  .filter((item) => scope !== "installed" || item.installed)
1330
+ .filter((item) => releaseState === "all" || item.releaseState === releaseState)
1323
1331
  .filter((item) => marketplaceResourceMatches(item, queryText)),
1324
1332
  );
1325
1333
  const page = paginateMarketplaceResources(items, url);
@@ -1327,6 +1335,7 @@ async function workspaceRoutes(req, res, ctx) {
1327
1335
  json(res, 200, {
1328
1336
  kind,
1329
1337
  scope,
1338
+ releaseState,
1330
1339
  sort: "useCount",
1331
1340
  order: "desc",
1332
1341
  ...page,
@@ -1568,7 +1577,11 @@ async function workspaceRoutes(req, res, ctx) {
1568
1577
  marketplaceAction: action,
1569
1578
  marketplaceInstallFlowId: resource.installFlowId || resource.liveFlowId || resource.definitionId || id,
1570
1579
  });
1571
- if (previewKind === "node") previewParams.set("focusNodeId", "node_preview");
1580
+ if (previewKind === "node") {
1581
+ previewParams.set("focusNodeId", "node_preview");
1582
+ } else if (projectFlow && resource.liveEntryId) {
1583
+ previewParams.set("focusNodeId", resource.liveEntryId);
1584
+ }
1572
1585
  if (action === "open-source") {
1573
1586
  previewParams.set("marketplaceTargetFlowId", resource.liveFlowId || resource.definitionId || "");
1574
1587
  previewParams.set("marketplaceTargetFlowSource", resource.liveFlowSource || "user");
@@ -3353,6 +3366,7 @@ async function workspaceRoutes(req, res, ctx) {
3353
3366
  const scoped = resolveWorkspaceScopeRoot(root, {
3354
3367
  flowId,
3355
3368
  flowSource: flowSource || "user",
3369
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
3356
3370
  archived: url.searchParams.get("archived") === "1",
3357
3371
  }, userCtx);
3358
3372
  if (scoped.error) {
@@ -3388,6 +3402,7 @@ async function workspaceRoutes(req, res, ctx) {
3388
3402
  const scoped = resolveWorkspaceScopeRoot(root, {
3389
3403
  flowId,
3390
3404
  flowSource,
3405
+ adminOwnerId: url.searchParams.get("adminOwnerId") || "",
3391
3406
  archived: url.searchParams.get("archived") === "1",
3392
3407
  }, userCtx);
3393
3408
  if (scoped.error) {
@@ -7,6 +7,11 @@ import { runLedgerId } from "./run-ledger.mjs";
7
7
  const MAX_EVENT_TEXT_CHARS = 20_000;
8
8
  const MAX_INDEX_RECORDS = 10_000;
9
9
  const SECRET_KEY_RE = /(token|password|passwd|secret|webhook|authorization|api[_-]?key|access[_-]?key)/i;
10
+ const SECRET_TEXT_PATTERNS = [
11
+ [/\b(Bearer)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 ***"],
12
+ [/((?:token|password|passwd|secret|authorization|api[_-]?key|access[_-]?key)\s*[=:]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi, "$1***"],
13
+ [/(["'](?:token|password|passwd|secret|authorization|api[_-]?key|access[_-]?key)["']\s*:\s*)("[^"]*"|'[^']*')/gi, "$1\"***\""],
14
+ ];
10
15
 
11
16
  function safeSegment(value, fallback = "run") {
12
17
  return String(value || fallback)
@@ -34,9 +39,15 @@ function truncateText(value) {
34
39
  return `${text.slice(0, MAX_EVENT_TEXT_CHARS)}\n... [truncated ${text.length - MAX_EVENT_TEXT_CHARS} chars]`;
35
40
  }
36
41
 
42
+ function redactText(value) {
43
+ let text = truncateText(value);
44
+ for (const [pattern, replacement] of SECRET_TEXT_PATTERNS) text = text.replace(pattern, replacement);
45
+ return text;
46
+ }
47
+
37
48
  function redact(value, depth = 0) {
38
49
  if (depth > 8) return "[MaxDepth]";
39
- if (typeof value === "string") return truncateText(value);
50
+ if (typeof value === "string") return redactText(value);
40
51
  if (value == null || typeof value === "number" || typeof value === "boolean") return value;
41
52
  if (Array.isArray(value)) return value.slice(0, 200).map((item) => redact(item, depth + 1));
42
53
  if (typeof value === "object") {