@datafog/fogclaw 0.2.0 → 0.3.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.
- package/CHANGELOG.md +11 -0
- package/dist/backlog-tools.d.ts +57 -0
- package/dist/backlog-tools.d.ts.map +1 -0
- package/dist/backlog-tools.js +173 -0
- package/dist/backlog-tools.js.map +1 -0
- package/dist/backlog.d.ts +82 -0
- package/dist/backlog.d.ts.map +1 -0
- package/dist/backlog.js +169 -0
- package/dist/backlog.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +87 -2
- package/dist/index.js.map +1 -1
- package/dist/message-sending-handler.d.ts +2 -1
- package/dist/message-sending-handler.d.ts.map +1 -1
- package/dist/message-sending-handler.js +5 -1
- package/dist/message-sending-handler.js.map +1 -1
- package/dist/tool-result-handler.d.ts +2 -1
- package/dist/tool-result-handler.d.ts.map +1 -1
- package/dist/tool-result-handler.js +5 -1
- package/dist/tool-result-handler.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/openclaw.plugin.json +11 -1
- package/package.json +7 -1
- package/.github/workflows/harness-docs.yml +0 -30
- package/AGENTS.md +0 -28
- package/docs/DATA.md +0 -28
- package/docs/DESIGN.md +0 -17
- package/docs/DOMAIN_DOCS.md +0 -30
- package/docs/FRONTEND.md +0 -24
- package/docs/OBSERVABILITY.md +0 -32
- package/docs/PLANS.md +0 -171
- package/docs/PRODUCT_SENSE.md +0 -20
- package/docs/RELIABILITY.md +0 -60
- package/docs/SECURITY.md +0 -52
- package/docs/design-docs/core-beliefs.md +0 -17
- package/docs/design-docs/index.md +0 -8
- package/docs/generated/README.md +0 -36
- package/docs/generated/memory.md +0 -1
- package/docs/plans/2026-02-16-fogclaw-design.md +0 -172
- package/docs/plans/2026-02-16-fogclaw-implementation.md +0 -1606
- package/docs/plans/README.md +0 -15
- package/docs/plans/active/2026-02-16-feat-openclaw-official-submission-plan.md +0 -386
- package/docs/plans/active/2026-02-17-feat-release-fogclaw-via-datafog-package-plan.md +0 -328
- package/docs/plans/active/2026-02-17-feat-submit-fogclaw-to-openclaw-plan.md +0 -244
- package/docs/plans/active/2026-02-17-feat-tool-result-pii-scanning-plan.md +0 -293
- package/docs/plans/tech-debt-tracker.md +0 -42
- package/docs/plugins/fogclaw.md +0 -101
- package/docs/runbooks/address-review-findings.md +0 -30
- package/docs/runbooks/ci-failures.md +0 -46
- package/docs/runbooks/code-review.md +0 -34
- package/docs/runbooks/merge-change.md +0 -28
- package/docs/runbooks/pull-request.md +0 -45
- package/docs/runbooks/record-evidence.md +0 -43
- package/docs/runbooks/reproduce-bug.md +0 -42
- package/docs/runbooks/respond-to-feedback.md +0 -42
- package/docs/runbooks/review-findings.md +0 -31
- package/docs/runbooks/submit-openclaw-plugin.md +0 -68
- package/docs/runbooks/update-agents-md.md +0 -59
- package/docs/runbooks/update-domain-docs.md +0 -42
- package/docs/runbooks/validate-current-state.md +0 -41
- package/docs/runbooks/verify-release.md +0 -69
- package/docs/specs/2026-02-16-feat-openclaw-official-submission-spec.md +0 -115
- package/docs/specs/2026-02-17-feat-outbound-message-pii-scanning-spec.md +0 -93
- package/docs/specs/2026-02-17-feat-submit-fogclaw-to-openclaw.md +0 -125
- package/docs/specs/2026-02-17-feat-tool-result-pii-scanning-spec.md +0 -122
- package/docs/specs/README.md +0 -5
- package/docs/specs/index.md +0 -8
- package/docs/spikes/README.md +0 -8
- package/fogclaw.config.example.json +0 -33
- package/scripts/ci/he-docs-config.json +0 -123
- package/scripts/ci/he-docs-drift.sh +0 -112
- package/scripts/ci/he-docs-lint.sh +0 -234
- package/scripts/ci/he-plans-lint.sh +0 -354
- package/scripts/ci/he-runbooks-lint.sh +0 -445
- package/scripts/ci/he-specs-lint.sh +0 -258
- package/scripts/ci/he-spikes-lint.sh +0 -249
- package/scripts/runbooks/select-runbooks.sh +0 -154
- package/src/config.ts +0 -183
- package/src/engines/gliner.ts +0 -240
- package/src/engines/regex.ts +0 -71
- package/src/extract.ts +0 -98
- package/src/index.ts +0 -381
- package/src/message-sending-handler.ts +0 -87
- package/src/redactor.ts +0 -51
- package/src/scanner.ts +0 -196
- package/src/tool-result-handler.ts +0 -133
- package/src/types.ts +0 -75
- package/tests/config.test.ts +0 -78
- package/tests/extract.test.ts +0 -185
- package/tests/gliner.test.ts +0 -289
- package/tests/message-sending-handler.test.ts +0 -244
- package/tests/plugin-smoke.test.ts +0 -250
- package/tests/redactor.test.ts +0 -320
- package/tests/regex.test.ts +0 -345
- package/tests/scanner.test.ts +0 -348
- package/tests/tool-result-handler.test.ts +0 -329
- package/tsconfig.json +0 -20
package/src/engines/gliner.ts
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { env } from "@xenova/transformers";
|
|
4
|
-
|
|
5
|
-
import type { Entity } from "../types.js";
|
|
6
|
-
import { canonicalType } from "../types.js";
|
|
7
|
-
|
|
8
|
-
const DEFAULT_NER_LABELS = [
|
|
9
|
-
"person",
|
|
10
|
-
"organization",
|
|
11
|
-
"location",
|
|
12
|
-
"address",
|
|
13
|
-
"date of birth",
|
|
14
|
-
"medical record number",
|
|
15
|
-
"account number",
|
|
16
|
-
"passport number",
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
const GLINER_MODEL_FILES = [
|
|
20
|
-
"onnx/model_q4f16.onnx",
|
|
21
|
-
"onnx/model_q4.onnx",
|
|
22
|
-
"onnx/model_bnb4.onnx",
|
|
23
|
-
"onnx/model_int8.onnx",
|
|
24
|
-
"onnx/model_uint8.onnx",
|
|
25
|
-
"onnx/model_quantized.onnx",
|
|
26
|
-
"onnx/model_fp16.onnx",
|
|
27
|
-
"onnx/model.onnx",
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
const MODEL_DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
31
|
-
|
|
32
|
-
function isLikelyLocalPath(modelPath: string): boolean {
|
|
33
|
-
const trimmed = modelPath.trim();
|
|
34
|
-
if (!trimmed) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const lower = trimmed.toLowerCase();
|
|
39
|
-
const hasExtension = [".onnx", ".ort", ".bin"].some((ext) => lower.endsWith(ext));
|
|
40
|
-
if (hasExtension) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (trimmed.startsWith(".") || path.isAbsolute(trimmed)) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function toAbsolutePath(value: string): string {
|
|
52
|
-
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getModelCacheDir(): string {
|
|
56
|
-
return env.localModelPath ?? path.join(process.cwd(), ".cache");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function sanitizeModelReference(modelPath: string): string {
|
|
60
|
-
return modelPath.trim();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function fileExists(filePath: string): Promise<boolean> {
|
|
64
|
-
try {
|
|
65
|
-
await fs.access(filePath);
|
|
66
|
-
return true;
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function downloadModelIfNeeded(modelRepo: string, filename: string): Promise<string> {
|
|
73
|
-
const cacheDir = getModelCacheDir();
|
|
74
|
-
const localPath = path.join(cacheDir, modelRepo, filename);
|
|
75
|
-
|
|
76
|
-
if (await fileExists(localPath)) {
|
|
77
|
-
return localPath;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const url = `https://huggingface.co/${modelRepo}/resolve/main/${filename}`;
|
|
81
|
-
const headers = new Headers();
|
|
82
|
-
const token = process.env.HF_TOKEN ?? process.env.HF_ACCESS_TOKEN;
|
|
83
|
-
if (token) {
|
|
84
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const controller = new AbortController();
|
|
88
|
-
const timeout = setTimeout(() => controller.abort(), MODEL_DOWNLOAD_TIMEOUT_MS);
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const response = await fetch(url, { headers, signal: controller.signal });
|
|
92
|
-
if (!response.ok) {
|
|
93
|
-
throw new Error(`Unable to download model artifact: ${response.status}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
97
|
-
await fs.mkdir(path.dirname(localPath), { recursive: true });
|
|
98
|
-
await fs.writeFile(localPath, bytes);
|
|
99
|
-
|
|
100
|
-
return localPath;
|
|
101
|
-
} catch (err) {
|
|
102
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
103
|
-
throw new Error(`Model download timed out after ${MODEL_DOWNLOAD_TIMEOUT_MS}ms`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
throw err;
|
|
107
|
-
} finally {
|
|
108
|
-
clearTimeout(timeout);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function resolveModelPath(modelPath: string): Promise<string> {
|
|
113
|
-
const sanitized = sanitizeModelReference(modelPath);
|
|
114
|
-
if (!sanitized) {
|
|
115
|
-
throw new Error("Model path is empty");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (isLikelyLocalPath(sanitized)) {
|
|
119
|
-
const absolutePath = toAbsolutePath(sanitized);
|
|
120
|
-
if (!(await fileExists(absolutePath))) {
|
|
121
|
-
throw new Error(`Local GLiNER model file not found at: ${absolutePath}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return absolutePath;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const candidates = GLINER_MODEL_FILES;
|
|
128
|
-
let lastError: Error | undefined;
|
|
129
|
-
|
|
130
|
-
for (const filename of candidates) {
|
|
131
|
-
const localPath = path.join(getModelCacheDir(), sanitized, filename);
|
|
132
|
-
if (await fileExists(localPath)) {
|
|
133
|
-
return localPath;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
for (const filename of candidates) {
|
|
138
|
-
try {
|
|
139
|
-
return await downloadModelIfNeeded(sanitized, filename);
|
|
140
|
-
} catch (err) {
|
|
141
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
throw new Error(
|
|
146
|
-
`Failed to resolve GLiNER model "${sanitized}". Tried ${candidates.join(", ")}: ${
|
|
147
|
-
lastError?.message ?? "unknown"
|
|
148
|
-
}`,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export class GlinerEngine {
|
|
153
|
-
private model: any = null;
|
|
154
|
-
private modelPath: string;
|
|
155
|
-
private threshold: number;
|
|
156
|
-
private customLabels: string[] = [];
|
|
157
|
-
private initialized = false;
|
|
158
|
-
|
|
159
|
-
constructor(modelPath: string, threshold: number = 0.5) {
|
|
160
|
-
this.modelPath = modelPath;
|
|
161
|
-
this.threshold = threshold;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async initialize(): Promise<void> {
|
|
165
|
-
if (this.initialized) return;
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const resolvedModelPath = await resolveModelPath(this.modelPath);
|
|
169
|
-
const glinerModule = await import("gliner/node").catch(async () => import("gliner"));
|
|
170
|
-
const { Gliner } = glinerModule;
|
|
171
|
-
this.model = new Gliner({
|
|
172
|
-
tokenizerPath: this.modelPath,
|
|
173
|
-
onnxSettings: {
|
|
174
|
-
modelPath: resolvedModelPath,
|
|
175
|
-
executionProvider: "cpu",
|
|
176
|
-
},
|
|
177
|
-
maxWidth: 12,
|
|
178
|
-
modelType: "span-level",
|
|
179
|
-
});
|
|
180
|
-
await this.model.initialize();
|
|
181
|
-
this.initialized = true;
|
|
182
|
-
} catch (err) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
`Failed to initialize GLiNER model "${this.modelPath}": ${err instanceof Error ? err.message : String(err)}`,
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
setCustomLabels(labels: string[]): void {
|
|
190
|
-
this.customLabels = labels;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async scan(text: string, extraLabels?: string[]): Promise<Entity[]> {
|
|
194
|
-
if (!text) return [];
|
|
195
|
-
if (!this.model) {
|
|
196
|
-
throw new Error("GLiNER engine not initialized. Call initialize() first.");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const labels = [
|
|
200
|
-
...DEFAULT_NER_LABELS,
|
|
201
|
-
...this.customLabels,
|
|
202
|
-
...(extraLabels ?? []),
|
|
203
|
-
];
|
|
204
|
-
|
|
205
|
-
// Deduplicate labels
|
|
206
|
-
const uniqueLabels = [...new Set(labels)];
|
|
207
|
-
|
|
208
|
-
const rawResults = await this.model.inference({
|
|
209
|
-
texts: [text],
|
|
210
|
-
entities: uniqueLabels,
|
|
211
|
-
flatNer: false,
|
|
212
|
-
threshold: this.threshold,
|
|
213
|
-
});
|
|
214
|
-
const flatResults = Array.isArray(rawResults) ? rawResults.flat() : [];
|
|
215
|
-
|
|
216
|
-
return flatResults.map(
|
|
217
|
-
(
|
|
218
|
-
r: {
|
|
219
|
-
spanText?: string;
|
|
220
|
-
text: string;
|
|
221
|
-
label: string;
|
|
222
|
-
score: number;
|
|
223
|
-
start: number;
|
|
224
|
-
end: number;
|
|
225
|
-
},
|
|
226
|
-
) => ({
|
|
227
|
-
text: r.spanText ?? r.text,
|
|
228
|
-
label: canonicalType(r.label),
|
|
229
|
-
start: r.start,
|
|
230
|
-
end: r.end,
|
|
231
|
-
confidence: r.score,
|
|
232
|
-
source: "gliner" as const,
|
|
233
|
-
}),
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
get isInitialized(): boolean {
|
|
238
|
-
return this.initialized;
|
|
239
|
-
}
|
|
240
|
-
}
|
package/src/engines/regex.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { Entity } from "../types.js";
|
|
2
|
-
|
|
3
|
-
interface PatternDef {
|
|
4
|
-
label: string;
|
|
5
|
-
pattern: RegExp;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const PATTERNS: PatternDef[] = [
|
|
9
|
-
{
|
|
10
|
-
label: "EMAIL",
|
|
11
|
-
pattern:
|
|
12
|
-
/(?<![A-Za-z0-9._%+\-@])(?![A-Za-z_]{2,20}=)[A-Za-z0-9!#$%&*+\-/=^_`{|}~][A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]*@(?:\.?[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?=$|[^A-Za-z])/gi,
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
label: "PHONE",
|
|
16
|
-
pattern:
|
|
17
|
-
/(?<![A-Za-z0-9])(?:(?:(?:\+?1)[-.\s]?)?(?:\(\d{3}\)|\d{3})[-.\s]?\d{3}[-.\s]?\d{4}|\+\d{1,3}[\s\-.]?\d{1,4}(?:[\s\-.]?\d{2,4}){2,3})(?![-A-Za-z0-9])/gi,
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
label: "SSN",
|
|
21
|
-
pattern:
|
|
22
|
-
/(?<!\d)(?:(?!000|666)\d{3}-(?!00)\d{2}-(?!0000)\d{4}|(?!000|666)\d{3}(?!00)\d{2}(?!0000)\d{4})(?!\d)/g,
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
label: "CREDIT_CARD",
|
|
26
|
-
pattern:
|
|
27
|
-
/\b(?:4\d{12}(?:\d{3})?|5[1-5]\d{14}|3[47]\d{13}|(?:(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4})|(?:3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}))\b/g,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
label: "IP_ADDRESS",
|
|
31
|
-
pattern:
|
|
32
|
-
/\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d))\b/g,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
label: "DATE",
|
|
36
|
-
pattern:
|
|
37
|
-
/\b(?:(?:0?[1-9]|1[0-2])[/-](?:0?[1-9]|[12]\d|3[01])[/-](?:\d{2}|\d{4})|(?:\d{4})-(?:0?[1-9]|1[0-2])-(?:0?[1-9]|[12]\d|3[01])|(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\s+(?:0?[1-9]|[12]\d|3[01]),\s+(?:19|20)\d{2})\b/gi,
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
label: "ZIP_CODE",
|
|
41
|
-
pattern: /\b\d{5}(?:-\d{4})?\b/g,
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
export class RegexEngine {
|
|
46
|
-
scan(text: string): Entity[] {
|
|
47
|
-
const entities: Entity[] = [];
|
|
48
|
-
|
|
49
|
-
for (const { label, pattern } of PATTERNS) {
|
|
50
|
-
// Reset lastIndex to avoid stale state from previous calls
|
|
51
|
-
pattern.lastIndex = 0;
|
|
52
|
-
|
|
53
|
-
let match: RegExpExecArray | null;
|
|
54
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
55
|
-
entities.push({
|
|
56
|
-
text: match[0],
|
|
57
|
-
label,
|
|
58
|
-
start: match.index,
|
|
59
|
-
end: match.index + match[0].length,
|
|
60
|
-
confidence: 1.0,
|
|
61
|
-
source: "regex",
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Sort by start position for deterministic output
|
|
67
|
-
entities.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
68
|
-
|
|
69
|
-
return entities;
|
|
70
|
-
}
|
|
71
|
-
}
|
package/src/extract.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utilities for extracting text from AgentMessage tool result payloads
|
|
3
|
-
* and replacing text content after redaction.
|
|
4
|
-
*
|
|
5
|
-
* AgentMessage shapes handled:
|
|
6
|
-
* - Plain string
|
|
7
|
-
* - Object with `content: string`
|
|
8
|
-
* - Object with `content: [{ type: "text", text: "..." }, ...]`
|
|
9
|
-
*
|
|
10
|
-
* When multiple text blocks exist in a content array, they are joined
|
|
11
|
-
* with a null byte separator (\0) so entity offsets stay valid across
|
|
12
|
-
* the concatenated string. replaceText splits on the same separator
|
|
13
|
-
* to map redacted text back to individual blocks.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// Separator between text segments from content block arrays.
|
|
17
|
-
// Null byte won't appear in regex PII patterns or normal text content.
|
|
18
|
-
const SEGMENT_SEP = "\0";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Extract all text content from an AgentMessage tool result payload.
|
|
22
|
-
* Returns an empty string if no text content is found.
|
|
23
|
-
*/
|
|
24
|
-
export function extractText(message: unknown): string {
|
|
25
|
-
if (message == null) return "";
|
|
26
|
-
if (typeof message === "string") return message;
|
|
27
|
-
if (typeof message !== "object") return "";
|
|
28
|
-
|
|
29
|
-
const msg = message as Record<string, unknown>;
|
|
30
|
-
const content = msg.content;
|
|
31
|
-
|
|
32
|
-
if (content == null) return "";
|
|
33
|
-
if (typeof content === "string") return content;
|
|
34
|
-
|
|
35
|
-
if (Array.isArray(content)) {
|
|
36
|
-
const textParts: string[] = [];
|
|
37
|
-
for (const block of content) {
|
|
38
|
-
if (
|
|
39
|
-
block != null &&
|
|
40
|
-
typeof block === "object" &&
|
|
41
|
-
(block as Record<string, unknown>).type === "text" &&
|
|
42
|
-
typeof (block as Record<string, unknown>).text === "string"
|
|
43
|
-
) {
|
|
44
|
-
textParts.push((block as Record<string, unknown>).text as string);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (textParts.length === 0) return "";
|
|
48
|
-
return textParts.join(SEGMENT_SEP);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return "";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Replace text content in an AgentMessage tool result payload with
|
|
56
|
-
* the redacted version. Returns a shallow copy; does not mutate.
|
|
57
|
-
*
|
|
58
|
-
* If the message shape is not recognized or has no text, returns
|
|
59
|
-
* the original message unchanged.
|
|
60
|
-
*/
|
|
61
|
-
export function replaceText(message: unknown, redactedText: string): unknown {
|
|
62
|
-
if (message == null) return message;
|
|
63
|
-
if (typeof message === "string") return redactedText;
|
|
64
|
-
if (typeof message !== "object") return message;
|
|
65
|
-
|
|
66
|
-
const msg = message as Record<string, unknown>;
|
|
67
|
-
const content = msg.content;
|
|
68
|
-
|
|
69
|
-
if (content == null) return message;
|
|
70
|
-
|
|
71
|
-
if (typeof content === "string") {
|
|
72
|
-
return { ...msg, content: redactedText };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (Array.isArray(content)) {
|
|
76
|
-
const segments = redactedText.split(SEGMENT_SEP);
|
|
77
|
-
let segmentIndex = 0;
|
|
78
|
-
|
|
79
|
-
const newContent = content.map((block) => {
|
|
80
|
-
if (
|
|
81
|
-
block != null &&
|
|
82
|
-
typeof block === "object" &&
|
|
83
|
-
(block as Record<string, unknown>).type === "text" &&
|
|
84
|
-
typeof (block as Record<string, unknown>).text === "string" &&
|
|
85
|
-
segmentIndex < segments.length
|
|
86
|
-
) {
|
|
87
|
-
const replaced = { ...(block as Record<string, unknown>), text: segments[segmentIndex] };
|
|
88
|
-
segmentIndex++;
|
|
89
|
-
return replaced;
|
|
90
|
-
}
|
|
91
|
-
return block;
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return { ...msg, content: newContent };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return message;
|
|
98
|
-
}
|