@openclaw/diffs 2026.5.2 → 2026.5.3-beta.2

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 ADDED
@@ -0,0 +1,2079 @@
1
+ import { definePluginEntry, resolvePreferredOpenClawTmpDir } from "./api.js";
2
+ import { resolveRequestClientIp } from "./runtime-api.js";
3
+ import { createRequire } from "node:module";
4
+ import { buildPluginConfigSchema } from "openclaw/plugin-sdk/plugin-entry";
5
+ import { mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared";
6
+ import { z } from "openclaw/plugin-sdk/zod";
7
+ import path from "node:path";
8
+ import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
9
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
10
+ import crypto from "node:crypto";
11
+ import fs from "node:fs/promises";
12
+ import { fileURLToPath } from "node:url";
13
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
14
+ import { Type } from "typebox";
15
+ import { constants } from "node:fs";
16
+ import { chromium } from "playwright-core";
17
+ import { RegisteredCustomThemes, ResolvedThemes, ResolvingThemes, parsePatchFiles, resolveLanguage } from "@pierre/diffs";
18
+ import { preloadFileDiff, preloadMultiFileDiff } from "@pierre/diffs/ssr";
19
+ //#region extensions/diffs/src/types.ts
20
+ const DIFF_LAYOUTS = ["unified", "split"];
21
+ const DIFF_MODES = [
22
+ "view",
23
+ "image",
24
+ "file",
25
+ "both"
26
+ ];
27
+ const DIFF_THEMES = ["light", "dark"];
28
+ const DIFF_INDICATORS = [
29
+ "bars",
30
+ "classic",
31
+ "none"
32
+ ];
33
+ const DIFF_IMAGE_QUALITY_PRESETS = [
34
+ "standard",
35
+ "hq",
36
+ "print"
37
+ ];
38
+ const DIFF_OUTPUT_FORMATS = ["png", "pdf"];
39
+ const DIFF_ARTIFACT_ID_PATTERN = /^[0-9a-f]{20}$/;
40
+ const DIFF_ARTIFACT_TOKEN_PATTERN = /^[0-9a-f]{48}$/;
41
+ //#endregion
42
+ //#region extensions/diffs/src/url.ts
43
+ const DEFAULT_GATEWAY_PORT = 18789;
44
+ function buildViewerUrl(params) {
45
+ const normalizedBase = normalizeViewerBaseUrl(params.baseUrl?.trim() || resolveGatewayBaseUrl(params.config));
46
+ const viewerPath = params.viewerPath.startsWith("/") ? params.viewerPath : `/${params.viewerPath}`;
47
+ const parsedBase = new URL(normalizedBase);
48
+ parsedBase.pathname = `${parsedBase.pathname === "/" ? "" : parsedBase.pathname.replace(/\/+$/, "")}${viewerPath}`;
49
+ parsedBase.search = "";
50
+ parsedBase.hash = "";
51
+ return parsedBase.toString();
52
+ }
53
+ function normalizeViewerBaseUrl(raw, fieldName = "baseUrl") {
54
+ let parsed;
55
+ try {
56
+ parsed = new URL(raw);
57
+ } catch {
58
+ throw new Error(`Invalid ${fieldName}: ${raw}`);
59
+ }
60
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`${fieldName} must use http or https: ${raw}`);
61
+ if (parsed.search || parsed.hash) throw new Error(`${fieldName} must not include query/hash: ${raw}`);
62
+ parsed.search = "";
63
+ parsed.hash = "";
64
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "");
65
+ return parsed.toString().replace(/\/+$/, "");
66
+ }
67
+ function resolveGatewayBaseUrl(config) {
68
+ const scheme = config.gateway?.tls?.enabled ? "https" : "http";
69
+ const port = typeof config.gateway?.port === "number" ? config.gateway.port : DEFAULT_GATEWAY_PORT;
70
+ const customHost = config.gateway?.customBindHost?.trim();
71
+ if (config.gateway?.bind === "custom" && customHost) return `${scheme}://${customHost}:${port}`;
72
+ return `${scheme}://127.0.0.1:${port}`;
73
+ }
74
+ //#endregion
75
+ //#region extensions/diffs/src/config.ts
76
+ const DEFAULT_IMAGE_QUALITY_PROFILES = {
77
+ standard: {
78
+ scale: 2,
79
+ maxWidth: 960,
80
+ maxPixels: 8e6
81
+ },
82
+ hq: {
83
+ scale: 2.5,
84
+ maxWidth: 1200,
85
+ maxPixels: 14e6
86
+ },
87
+ print: {
88
+ scale: 3,
89
+ maxWidth: 1400,
90
+ maxPixels: 24e6
91
+ }
92
+ };
93
+ const DEFAULT_DIFFS_TOOL_DEFAULTS = {
94
+ fontFamily: "Fira Code",
95
+ fontSize: 15,
96
+ lineSpacing: 1.6,
97
+ layout: "unified",
98
+ showLineNumbers: true,
99
+ diffIndicators: "bars",
100
+ wordWrap: true,
101
+ background: true,
102
+ theme: "dark",
103
+ fileFormat: "png",
104
+ fileQuality: "standard",
105
+ fileScale: DEFAULT_IMAGE_QUALITY_PROFILES.standard.scale,
106
+ fileMaxWidth: DEFAULT_IMAGE_QUALITY_PROFILES.standard.maxWidth,
107
+ mode: "both"
108
+ };
109
+ const DEFAULT_DIFFS_PLUGIN_SECURITY = { allowRemoteViewer: false };
110
+ const VIEWER_BASE_URL_JSON_SCHEMA = {
111
+ type: "string",
112
+ format: "uri",
113
+ pattern: "^[Hh][Tt][Tt][Pp][Ss]?://",
114
+ not: { pattern: "[?#]" }
115
+ };
116
+ const DiffsPluginJsonSchemaSource = z.strictObject({
117
+ viewerBaseUrl: z.string().superRefine((value, ctx) => {
118
+ try {
119
+ normalizeViewerBaseUrl(value, "viewerBaseUrl");
120
+ } catch (error) {
121
+ ctx.addIssue({
122
+ code: "custom",
123
+ message: error instanceof Error ? error.message : "Invalid viewerBaseUrl"
124
+ });
125
+ }
126
+ }).optional(),
127
+ defaults: z.strictObject({
128
+ fontFamily: z.string().default(DEFAULT_DIFFS_TOOL_DEFAULTS.fontFamily).optional(),
129
+ fontSize: z.number().min(10).max(24).default(DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize).optional(),
130
+ lineSpacing: z.number().min(1).max(3).default(DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing).optional(),
131
+ layout: z.enum(DIFF_LAYOUTS).default(DEFAULT_DIFFS_TOOL_DEFAULTS.layout).optional(),
132
+ showLineNumbers: z.boolean().default(DEFAULT_DIFFS_TOOL_DEFAULTS.showLineNumbers).optional(),
133
+ diffIndicators: z.enum(DIFF_INDICATORS).default(DEFAULT_DIFFS_TOOL_DEFAULTS.diffIndicators).optional(),
134
+ wordWrap: z.boolean().default(DEFAULT_DIFFS_TOOL_DEFAULTS.wordWrap).optional(),
135
+ background: z.boolean().default(DEFAULT_DIFFS_TOOL_DEFAULTS.background).optional(),
136
+ theme: z.enum(DIFF_THEMES).default(DEFAULT_DIFFS_TOOL_DEFAULTS.theme).optional(),
137
+ fileFormat: z.enum(DIFF_OUTPUT_FORMATS).default(DEFAULT_DIFFS_TOOL_DEFAULTS.fileFormat).optional(),
138
+ format: z.enum(DIFF_OUTPUT_FORMATS).optional().describe("Deprecated alias for fileFormat."),
139
+ fileQuality: z.enum(DIFF_IMAGE_QUALITY_PRESETS).default(DEFAULT_DIFFS_TOOL_DEFAULTS.fileQuality).optional(),
140
+ fileScale: z.number().min(1).max(4).optional(),
141
+ fileMaxWidth: z.number().min(640).max(2400).optional(),
142
+ imageFormat: z.enum(DIFF_OUTPUT_FORMATS).optional().describe("Deprecated alias for fileFormat."),
143
+ imageQuality: z.enum(DIFF_IMAGE_QUALITY_PRESETS).optional().describe("Deprecated alias for fileQuality."),
144
+ imageScale: z.number().min(1).max(4).optional().describe("Deprecated alias for fileScale."),
145
+ imageMaxWidth: z.number().min(640).max(2400).optional().describe("Deprecated alias for fileMaxWidth."),
146
+ mode: z.enum(DIFF_MODES).default(DEFAULT_DIFFS_TOOL_DEFAULTS.mode).optional()
147
+ }).optional(),
148
+ security: z.strictObject({ allowRemoteViewer: z.boolean().default(DEFAULT_DIFFS_PLUGIN_SECURITY.allowRemoteViewer).optional() }).optional()
149
+ });
150
+ const diffsPluginConfigSchemaBase = buildPluginConfigSchema(DiffsPluginJsonSchemaSource, { safeParse(value) {
151
+ if (value === void 0) return {
152
+ success: true,
153
+ data: void 0
154
+ };
155
+ const result = DiffsPluginJsonSchemaSource.safeParse(value);
156
+ if (result.success) return {
157
+ success: true,
158
+ data: buildDiffsPluginConfigShape(result.data)
159
+ };
160
+ return {
161
+ success: false,
162
+ error: { issues: mapPluginConfigIssues(result.error.issues) }
163
+ };
164
+ } });
165
+ const diffsPluginConfigSchema = {
166
+ ...diffsPluginConfigSchemaBase,
167
+ jsonSchema: {
168
+ ...diffsPluginConfigSchemaBase.jsonSchema,
169
+ properties: {
170
+ ...diffsPluginConfigSchemaBase.jsonSchema.properties,
171
+ viewerBaseUrl: VIEWER_BASE_URL_JSON_SCHEMA
172
+ }
173
+ }
174
+ };
175
+ function resolveConfiguredValue(options) {
176
+ const alias = options.aliases.find((value) => value !== void 0);
177
+ if (alias !== void 0 && options.primary === options.schemaDefault) return alias;
178
+ return options.primary ?? alias;
179
+ }
180
+ function buildDiffsPluginConfigShape(config) {
181
+ const viewerBaseUrl = resolveDiffsPluginViewerBaseUrl(config);
182
+ return {
183
+ ...viewerBaseUrl !== void 0 ? { viewerBaseUrl } : {},
184
+ ...config.defaults !== void 0 ? { defaults: resolveDiffsPluginDefaults(config) } : {},
185
+ ...config.security !== void 0 ? { security: resolveDiffsPluginSecurity(config) } : {}
186
+ };
187
+ }
188
+ function resolveDiffsPluginDefaults(config) {
189
+ if (!config || typeof config !== "object" || Array.isArray(config)) return { ...DEFAULT_DIFFS_TOOL_DEFAULTS };
190
+ const defaults = config.defaults;
191
+ if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) return { ...DEFAULT_DIFFS_TOOL_DEFAULTS };
192
+ const fileQuality = normalizeFileQuality$1(resolveConfiguredValue({
193
+ primary: defaults.fileQuality,
194
+ aliases: [defaults.imageQuality],
195
+ schemaDefault: DEFAULT_DIFFS_TOOL_DEFAULTS.fileQuality
196
+ }));
197
+ const profile = DEFAULT_IMAGE_QUALITY_PROFILES[fileQuality];
198
+ const fileFormat = resolveConfiguredValue({
199
+ primary: defaults.fileFormat,
200
+ aliases: [defaults.imageFormat, defaults.format],
201
+ schemaDefault: DEFAULT_DIFFS_TOOL_DEFAULTS.fileFormat
202
+ });
203
+ const fileScale = resolveConfiguredValue({
204
+ primary: defaults.fileScale,
205
+ aliases: [defaults.imageScale]
206
+ });
207
+ const fileMaxWidth = resolveConfiguredValue({
208
+ primary: defaults.fileMaxWidth,
209
+ aliases: [defaults.imageMaxWidth]
210
+ });
211
+ return {
212
+ fontFamily: normalizeFontFamily(defaults.fontFamily),
213
+ fontSize: normalizeFontSize(defaults.fontSize),
214
+ lineSpacing: normalizeLineSpacing(defaults.lineSpacing),
215
+ layout: normalizeLayout$1(defaults.layout),
216
+ showLineNumbers: defaults.showLineNumbers !== false,
217
+ diffIndicators: normalizeDiffIndicators(defaults.diffIndicators),
218
+ wordWrap: defaults.wordWrap !== false,
219
+ background: defaults.background !== false,
220
+ theme: normalizeTheme$1(defaults.theme),
221
+ fileFormat: normalizeFileFormat(fileFormat),
222
+ fileQuality,
223
+ fileScale: normalizeFileScale(fileScale, profile.scale),
224
+ fileMaxWidth: normalizeFileMaxWidth(fileMaxWidth, profile.maxWidth),
225
+ mode: normalizeMode$1(defaults.mode)
226
+ };
227
+ }
228
+ function resolveDiffsPluginSecurity(config) {
229
+ if (!config || typeof config !== "object" || Array.isArray(config)) return { ...DEFAULT_DIFFS_PLUGIN_SECURITY };
230
+ const security = config.security;
231
+ if (!security || typeof security !== "object" || Array.isArray(security)) return { ...DEFAULT_DIFFS_PLUGIN_SECURITY };
232
+ return { allowRemoteViewer: security.allowRemoteViewer === true };
233
+ }
234
+ function resolveDiffsPluginViewerBaseUrl(config) {
235
+ if (!config || typeof config !== "object" || Array.isArray(config)) return;
236
+ const viewerBaseUrl = config.viewerBaseUrl;
237
+ if (typeof viewerBaseUrl !== "string") return;
238
+ const normalized = viewerBaseUrl.trim();
239
+ return normalized ? normalizeViewerBaseUrl(normalized) : void 0;
240
+ }
241
+ function normalizeFontFamily(fontFamily) {
242
+ return fontFamily?.trim() || DEFAULT_DIFFS_TOOL_DEFAULTS.fontFamily;
243
+ }
244
+ function normalizeFontSize(fontSize) {
245
+ if (fontSize === void 0 || !Number.isFinite(fontSize)) return DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize;
246
+ return Math.min(Math.max(Math.floor(fontSize), 10), 24);
247
+ }
248
+ function normalizeLineSpacing(lineSpacing) {
249
+ if (lineSpacing === void 0 || !Number.isFinite(lineSpacing)) return DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing;
250
+ return Math.min(Math.max(lineSpacing, 1), 3);
251
+ }
252
+ function normalizeLayout$1(layout) {
253
+ return layout && DIFF_LAYOUTS.includes(layout) ? layout : DEFAULT_DIFFS_TOOL_DEFAULTS.layout;
254
+ }
255
+ function normalizeDiffIndicators(diffIndicators) {
256
+ return diffIndicators && DIFF_INDICATORS.includes(diffIndicators) ? diffIndicators : DEFAULT_DIFFS_TOOL_DEFAULTS.diffIndicators;
257
+ }
258
+ function normalizeTheme$1(theme) {
259
+ return theme && DIFF_THEMES.includes(theme) ? theme : DEFAULT_DIFFS_TOOL_DEFAULTS.theme;
260
+ }
261
+ function normalizeFileFormat(fileFormat) {
262
+ return fileFormat && DIFF_OUTPUT_FORMATS.includes(fileFormat) ? fileFormat : DEFAULT_DIFFS_TOOL_DEFAULTS.fileFormat;
263
+ }
264
+ function normalizeFileQuality$1(fileQuality) {
265
+ return fileQuality && DIFF_IMAGE_QUALITY_PRESETS.includes(fileQuality) ? fileQuality : DEFAULT_DIFFS_TOOL_DEFAULTS.fileQuality;
266
+ }
267
+ function normalizeFileScale(fileScale, fallback) {
268
+ if (fileScale === void 0 || !Number.isFinite(fileScale)) return fallback;
269
+ const rounded = Math.round(fileScale * 100) / 100;
270
+ return Math.min(Math.max(rounded, 1), 4);
271
+ }
272
+ function normalizeFileMaxWidth(fileMaxWidth, fallback) {
273
+ if (fileMaxWidth === void 0 || !Number.isFinite(fileMaxWidth)) return fallback;
274
+ return Math.min(Math.max(Math.round(fileMaxWidth), 640), 2400);
275
+ }
276
+ function normalizeMode$1(mode) {
277
+ return mode && DIFF_MODES.includes(mode) ? mode : DEFAULT_DIFFS_TOOL_DEFAULTS.mode;
278
+ }
279
+ function resolveDiffImageRenderOptions(params) {
280
+ const format = normalizeFileFormat(params.fileFormat ?? params.imageFormat ?? params.format ?? params.defaults.fileFormat);
281
+ const qualityOverrideProvided = params.fileQuality !== void 0 || params.imageQuality !== void 0;
282
+ const qualityPreset = normalizeFileQuality$1(params.fileQuality ?? params.imageQuality ?? params.defaults.fileQuality);
283
+ const profile = DEFAULT_IMAGE_QUALITY_PROFILES[qualityPreset];
284
+ return {
285
+ format,
286
+ qualityPreset,
287
+ scale: normalizeFileScale(params.fileScale ?? params.imageScale, qualityOverrideProvided ? profile.scale : params.defaults.fileScale),
288
+ maxWidth: normalizeFileMaxWidth(params.fileMaxWidth ?? params.imageMaxWidth, qualityOverrideProvided ? profile.maxWidth : params.defaults.fileMaxWidth),
289
+ maxPixels: profile.maxPixels
290
+ };
291
+ }
292
+ //#endregion
293
+ //#region extensions/diffs/src/viewer-assets.ts
294
+ const VIEWER_ASSET_PREFIX = "/plugins/diffs/assets/";
295
+ const VIEWER_LOADER_PATH = `${VIEWER_ASSET_PREFIX}viewer.js`;
296
+ const VIEWER_RUNTIME_PATH = `${VIEWER_ASSET_PREFIX}viewer-runtime.js`;
297
+ const VIEWER_RUNTIME_RELATIVE_IMPORT_PATH = "./viewer-runtime.js";
298
+ const VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS = ["./assets/viewer-runtime.js", "../assets/viewer-runtime.js"];
299
+ let runtimeAssetCache = null;
300
+ function isMissingFileError(error) {
301
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
302
+ }
303
+ async function resolveViewerRuntimeFileUrl(params = {}) {
304
+ const baseUrl = params.baseUrl ?? import.meta.url;
305
+ const stat = params.stat ?? ((path) => fs.stat(path));
306
+ let missingFileError = null;
307
+ for (const relativePath of VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS) {
308
+ const candidateUrl = new URL(relativePath, baseUrl);
309
+ try {
310
+ await stat(fileURLToPath(candidateUrl));
311
+ return candidateUrl;
312
+ } catch (error) {
313
+ if (isMissingFileError(error)) {
314
+ missingFileError = error;
315
+ continue;
316
+ }
317
+ throw error;
318
+ }
319
+ }
320
+ if (missingFileError) throw missingFileError;
321
+ throw new Error("viewer runtime asset candidates were not checked");
322
+ }
323
+ async function getServedViewerAsset(pathname) {
324
+ if (pathname !== VIEWER_LOADER_PATH && pathname !== VIEWER_RUNTIME_PATH) return null;
325
+ const assets = await loadViewerAssets();
326
+ if (pathname === VIEWER_LOADER_PATH) return {
327
+ body: assets.loaderBody,
328
+ contentType: "text/javascript; charset=utf-8"
329
+ };
330
+ if (pathname === VIEWER_RUNTIME_PATH) return {
331
+ body: assets.runtimeBody,
332
+ contentType: "text/javascript; charset=utf-8"
333
+ };
334
+ return null;
335
+ }
336
+ async function loadViewerAssets() {
337
+ const runtimePath = fileURLToPath(await resolveViewerRuntimeFileUrl());
338
+ const runtimeStat = await fs.stat(runtimePath);
339
+ if (runtimeAssetCache && runtimeAssetCache.mtimeMs === runtimeStat.mtimeMs) return runtimeAssetCache;
340
+ const runtimeBody = await fs.readFile(runtimePath);
341
+ const hash = crypto.createHash("sha1").update(runtimeBody).digest("hex").slice(0, 12);
342
+ runtimeAssetCache = {
343
+ mtimeMs: runtimeStat.mtimeMs,
344
+ runtimeBody,
345
+ loaderBody: `import "${VIEWER_RUNTIME_RELATIVE_IMPORT_PATH}?v=${hash}";\n`
346
+ };
347
+ return runtimeAssetCache;
348
+ }
349
+ //#endregion
350
+ //#region extensions/diffs/src/http.ts
351
+ const VIEW_PREFIX = "/plugins/diffs/view/";
352
+ const VIEWER_MAX_FAILURES_PER_WINDOW = 40;
353
+ const VIEWER_FAILURE_WINDOW_MS = 6e4;
354
+ const VIEWER_LOCKOUT_MS = 6e4;
355
+ const VIEWER_LIMITER_MAX_KEYS = 2048;
356
+ const VIEWER_CONTENT_SECURITY_POLICY = [
357
+ "default-src 'none'",
358
+ "script-src 'self'",
359
+ "style-src 'unsafe-inline'",
360
+ "img-src 'self' data:",
361
+ "font-src 'self' data:",
362
+ "connect-src 'none'",
363
+ "base-uri 'none'",
364
+ "frame-ancestors 'self'",
365
+ "object-src 'none'"
366
+ ].join("; ");
367
+ function createDiffsHttpHandler(params) {
368
+ const viewerFailureLimiter = new ViewerFailureLimiter();
369
+ return async (req, res) => {
370
+ const parsed = parseRequestUrl(req.url);
371
+ if (!parsed) return false;
372
+ if (parsed.pathname.startsWith("/plugins/diffs/assets/")) return await serveAsset(req, res, parsed.pathname, params.logger);
373
+ if (!parsed.pathname.startsWith(VIEW_PREFIX)) return false;
374
+ const accessConfig = params.resolveAccessConfig?.() ?? {
375
+ allowRemoteViewer: params.allowRemoteViewer,
376
+ trustedProxies: params.trustedProxies,
377
+ allowRealIpFallback: params.allowRealIpFallback
378
+ };
379
+ const access = resolveViewerAccess(req, {
380
+ trustedProxies: accessConfig.trustedProxies,
381
+ allowRealIpFallback: accessConfig.allowRealIpFallback
382
+ });
383
+ if (!access.localRequest && accessConfig.allowRemoteViewer !== true) {
384
+ respondText(res, 404, "Diff not found");
385
+ return true;
386
+ }
387
+ if (req.method !== "GET" && req.method !== "HEAD") {
388
+ respondText(res, 405, "Method not allowed");
389
+ return true;
390
+ }
391
+ if (!access.localRequest) {
392
+ const throttled = viewerFailureLimiter.check(access.remoteKey);
393
+ if (!throttled.allowed) {
394
+ res.statusCode = 429;
395
+ setSharedHeaders(res, "text/plain; charset=utf-8");
396
+ res.setHeader("Retry-After", String(Math.max(1, Math.ceil(throttled.retryAfterMs / 1e3))));
397
+ res.end("Too Many Requests");
398
+ return true;
399
+ }
400
+ }
401
+ const pathParts = parsed.pathname.split("/").filter(Boolean);
402
+ const id = pathParts[3];
403
+ const token = pathParts[4];
404
+ if (!id || !token || !DIFF_ARTIFACT_ID_PATTERN.test(id) || !DIFF_ARTIFACT_TOKEN_PATTERN.test(token)) {
405
+ recordRemoteFailure(viewerFailureLimiter, access);
406
+ respondText(res, 404, "Diff not found");
407
+ return true;
408
+ }
409
+ if (!await params.store.getArtifact(id, token)) {
410
+ recordRemoteFailure(viewerFailureLimiter, access);
411
+ respondText(res, 404, "Diff not found or expired");
412
+ return true;
413
+ }
414
+ try {
415
+ const html = await params.store.readHtml(id);
416
+ resetRemoteFailures(viewerFailureLimiter, access);
417
+ res.statusCode = 200;
418
+ setSharedHeaders(res, "text/html; charset=utf-8");
419
+ res.setHeader("content-security-policy", VIEWER_CONTENT_SECURITY_POLICY);
420
+ if (req.method === "HEAD") res.end();
421
+ else res.end(html);
422
+ return true;
423
+ } catch (error) {
424
+ recordRemoteFailure(viewerFailureLimiter, access);
425
+ params.logger?.warn(`Failed to serve diff artifact ${id}: ${String(error)}`);
426
+ respondText(res, 500, "Failed to load diff");
427
+ return true;
428
+ }
429
+ };
430
+ }
431
+ function parseRequestUrl(rawUrl) {
432
+ if (!rawUrl) return null;
433
+ try {
434
+ return new URL(rawUrl, "http://127.0.0.1");
435
+ } catch {
436
+ return null;
437
+ }
438
+ }
439
+ async function serveAsset(req, res, pathname, logger) {
440
+ if (req.method !== "GET" && req.method !== "HEAD") {
441
+ respondText(res, 405, "Method not allowed");
442
+ return true;
443
+ }
444
+ try {
445
+ const asset = await getServedViewerAsset(pathname);
446
+ if (!asset) {
447
+ respondText(res, 404, "Asset not found");
448
+ return true;
449
+ }
450
+ res.statusCode = 200;
451
+ setSharedHeaders(res, asset.contentType);
452
+ if (req.method === "HEAD") res.end();
453
+ else res.end(asset.body);
454
+ return true;
455
+ } catch (error) {
456
+ logger?.warn(`Failed to serve diffs asset ${pathname}: ${String(error)}`);
457
+ respondText(res, 500, "Failed to load asset");
458
+ return true;
459
+ }
460
+ }
461
+ function respondText(res, statusCode, body) {
462
+ res.statusCode = statusCode;
463
+ setSharedHeaders(res, "text/plain; charset=utf-8");
464
+ res.end(body);
465
+ }
466
+ function setSharedHeaders(res, contentType) {
467
+ res.setHeader("cache-control", "no-store, max-age=0");
468
+ res.setHeader("content-type", contentType);
469
+ res.setHeader("x-content-type-options", "nosniff");
470
+ res.setHeader("referrer-policy", "no-referrer");
471
+ }
472
+ function normalizeRemoteClientKey(remoteAddress) {
473
+ const normalized = normalizeLowercaseStringOrEmpty(remoteAddress);
474
+ if (!normalized) return "unknown";
475
+ return normalized.startsWith("::ffff:") ? normalized.slice(7) : normalized;
476
+ }
477
+ function isLoopbackClientIp(clientIp) {
478
+ return clientIp === "127.0.0.1" || clientIp === "::1";
479
+ }
480
+ function hasProxyForwardingHints(req) {
481
+ const headers = req.headers ?? {};
482
+ return Boolean(headers["x-forwarded-for"] || headers["x-real-ip"] || headers.forwarded || headers["x-forwarded-host"] || headers["x-forwarded-proto"]);
483
+ }
484
+ function resolveViewerAccess(req, params) {
485
+ const proxyHintsPresent = hasProxyForwardingHints(req);
486
+ const clientIp = proxyHintsPresent || (params.trustedProxies?.length ?? 0) > 0 ? resolveRequestClientIp(req, params.trustedProxies ? [...params.trustedProxies] : void 0, params.allowRealIpFallback === true) : req.socket?.remoteAddress;
487
+ const remoteKey = normalizeRemoteClientKey(clientIp ?? req.socket?.remoteAddress);
488
+ return {
489
+ remoteKey,
490
+ localRequest: !proxyHintsPresent && typeof clientIp === "string" && isLoopbackClientIp(remoteKey)
491
+ };
492
+ }
493
+ function recordRemoteFailure(limiter, access) {
494
+ if (!access.localRequest) limiter.recordFailure(access.remoteKey);
495
+ }
496
+ function resetRemoteFailures(limiter, access) {
497
+ if (!access.localRequest) limiter.reset(access.remoteKey);
498
+ }
499
+ var ViewerFailureLimiter = class {
500
+ constructor() {
501
+ this.failures = /* @__PURE__ */ new Map();
502
+ }
503
+ check(key) {
504
+ this.prune();
505
+ const state = this.failures.get(key);
506
+ if (!state) return {
507
+ allowed: true,
508
+ retryAfterMs: 0
509
+ };
510
+ const now = Date.now();
511
+ if (state.lockUntilMs > now) return {
512
+ allowed: false,
513
+ retryAfterMs: state.lockUntilMs - now
514
+ };
515
+ if (now - state.windowStartMs >= VIEWER_FAILURE_WINDOW_MS) {
516
+ this.failures.delete(key);
517
+ return {
518
+ allowed: true,
519
+ retryAfterMs: 0
520
+ };
521
+ }
522
+ return {
523
+ allowed: true,
524
+ retryAfterMs: 0
525
+ };
526
+ }
527
+ recordFailure(key) {
528
+ this.prune();
529
+ const now = Date.now();
530
+ const current = this.failures.get(key);
531
+ const next = !current || now - current.windowStartMs >= VIEWER_FAILURE_WINDOW_MS ? {
532
+ windowStartMs: now,
533
+ failures: 1,
534
+ lockUntilMs: 0
535
+ } : {
536
+ ...current,
537
+ failures: current.failures + 1
538
+ };
539
+ if (next.failures >= VIEWER_MAX_FAILURES_PER_WINDOW) next.lockUntilMs = now + VIEWER_LOCKOUT_MS;
540
+ this.failures.set(key, next);
541
+ }
542
+ reset(key) {
543
+ this.failures.delete(key);
544
+ }
545
+ prune() {
546
+ if (this.failures.size < VIEWER_LIMITER_MAX_KEYS) return;
547
+ const now = Date.now();
548
+ for (const [key, state] of this.failures) {
549
+ if (state.lockUntilMs <= now && now - state.windowStartMs >= VIEWER_FAILURE_WINDOW_MS) this.failures.delete(key);
550
+ if (this.failures.size < VIEWER_LIMITER_MAX_KEYS) return;
551
+ }
552
+ if (this.failures.size >= VIEWER_LIMITER_MAX_KEYS) this.failures.clear();
553
+ }
554
+ };
555
+ //#endregion
556
+ //#region extensions/diffs/src/prompt-guidance.ts
557
+ const DIFFS_AGENT_GUIDANCE = [
558
+ "When you need to show edits as a real diff, prefer the `diffs` tool instead of writing a manual summary.",
559
+ "It accepts either `before` + `after` text or a unified `patch`.",
560
+ "`mode=view` returns `details.viewerUrl` for canvas use; `mode=file` returns `details.filePath`; `mode=both` returns both.",
561
+ "If you need to send the rendered file, use the `message` tool with `path` or `filePath`.",
562
+ "Include `path` when you know the filename, and omit presentation overrides unless needed."
563
+ ].join("\n");
564
+ //#endregion
565
+ //#region extensions/diffs/src/store.ts
566
+ const DEFAULT_TTL_MS = 1800 * 1e3;
567
+ const MAX_TTL_MS = 360 * 60 * 1e3;
568
+ const SWEEP_FALLBACK_AGE_MS = 1440 * 60 * 1e3;
569
+ const DEFAULT_CLEANUP_INTERVAL_MS = 300 * 1e3;
570
+ const VIEWER_PREFIX = "/plugins/diffs/view";
571
+ var DiffArtifactStore = class {
572
+ constructor(params) {
573
+ this.cleanupInFlight = null;
574
+ this.nextCleanupAt = 0;
575
+ this.rootDir = path.resolve(params.rootDir);
576
+ this.logger = params.logger;
577
+ this.cleanupIntervalMs = params.cleanupIntervalMs === void 0 ? DEFAULT_CLEANUP_INTERVAL_MS : Math.max(0, Math.floor(params.cleanupIntervalMs));
578
+ }
579
+ async createArtifact(params) {
580
+ await this.ensureRoot();
581
+ const id = crypto.randomBytes(10).toString("hex");
582
+ const token = crypto.randomBytes(24).toString("hex");
583
+ const artifactDir = this.artifactDir(id);
584
+ const htmlPath = path.join(artifactDir, "viewer.html");
585
+ const ttlMs = normalizeTtlMs$1(params.ttlMs);
586
+ const createdAt = /* @__PURE__ */ new Date();
587
+ const expiresAt = new Date(createdAt.getTime() + ttlMs);
588
+ const meta = {
589
+ id,
590
+ token,
591
+ title: params.title,
592
+ inputKind: params.inputKind,
593
+ fileCount: params.fileCount,
594
+ createdAt: createdAt.toISOString(),
595
+ expiresAt: expiresAt.toISOString(),
596
+ viewerPath: `${VIEWER_PREFIX}/${id}/${token}`,
597
+ htmlPath,
598
+ ...params.context ? { context: params.context } : {}
599
+ };
600
+ await fs.mkdir(artifactDir, { recursive: true });
601
+ await fs.writeFile(htmlPath, params.html, "utf8");
602
+ await this.writeMeta(meta);
603
+ this.scheduleCleanup();
604
+ return meta;
605
+ }
606
+ async getArtifact(id, token) {
607
+ const meta = await this.readMeta(id);
608
+ if (!meta) return null;
609
+ if (meta.token !== token) return null;
610
+ if (isExpired(meta)) {
611
+ await this.deleteArtifact(id);
612
+ return null;
613
+ }
614
+ return meta;
615
+ }
616
+ async readHtml(id) {
617
+ const meta = await this.readMeta(id);
618
+ if (!meta) throw new Error(`Diff artifact not found: ${id}`);
619
+ const htmlPath = this.normalizeStoredPath(meta.htmlPath, "htmlPath");
620
+ return await fs.readFile(htmlPath, "utf8");
621
+ }
622
+ async updateFilePath(id, filePath) {
623
+ const meta = await this.readMeta(id);
624
+ if (!meta) throw new Error(`Diff artifact not found: ${id}`);
625
+ const normalizedFilePath = this.normalizeStoredPath(filePath, "filePath");
626
+ const next = {
627
+ ...meta,
628
+ filePath: normalizedFilePath,
629
+ imagePath: normalizedFilePath
630
+ };
631
+ await this.writeMeta(next);
632
+ return next;
633
+ }
634
+ async updateImagePath(id, imagePath) {
635
+ return this.updateFilePath(id, imagePath);
636
+ }
637
+ allocateFilePath(id, format = "png") {
638
+ return path.join(this.artifactDir(id), `preview.${format}`);
639
+ }
640
+ async createStandaloneFileArtifact(params = {}) {
641
+ await this.ensureRoot();
642
+ const id = crypto.randomBytes(10).toString("hex");
643
+ const artifactDir = this.artifactDir(id);
644
+ const format = params.format ?? "png";
645
+ const filePath = path.join(artifactDir, `preview.${format}`);
646
+ const ttlMs = normalizeTtlMs$1(params.ttlMs);
647
+ const createdAt = /* @__PURE__ */ new Date();
648
+ const expiresAt = new Date(createdAt.getTime() + ttlMs).toISOString();
649
+ const meta = {
650
+ kind: "standalone_file",
651
+ id,
652
+ createdAt: createdAt.toISOString(),
653
+ expiresAt,
654
+ filePath: this.normalizeStoredPath(filePath, "filePath"),
655
+ ...params.context ? { context: params.context } : {}
656
+ };
657
+ await fs.mkdir(artifactDir, { recursive: true });
658
+ await this.writeStandaloneMeta(meta);
659
+ this.scheduleCleanup();
660
+ return {
661
+ id,
662
+ filePath: meta.filePath,
663
+ expiresAt: meta.expiresAt,
664
+ ...meta.context ? { context: meta.context } : {}
665
+ };
666
+ }
667
+ allocateImagePath(id, format = "png") {
668
+ return this.allocateFilePath(id, format);
669
+ }
670
+ scheduleCleanup() {
671
+ this.maybeCleanupExpired();
672
+ }
673
+ async cleanupExpired() {
674
+ await this.ensureRoot();
675
+ const entries = await fs.readdir(this.rootDir, { withFileTypes: true }).catch(() => []);
676
+ const now = Date.now();
677
+ await Promise.all(entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
678
+ const id = entry.name;
679
+ const meta = await this.readMeta(id);
680
+ if (meta) {
681
+ if (isExpired(meta)) await this.deleteArtifact(id);
682
+ return;
683
+ }
684
+ const standaloneMeta = await this.readStandaloneMeta(id);
685
+ if (standaloneMeta) {
686
+ if (isExpired(standaloneMeta)) await this.deleteArtifact(id);
687
+ return;
688
+ }
689
+ const artifactPath = this.artifactDir(id);
690
+ const stat = await fs.stat(artifactPath).catch(() => null);
691
+ if (!stat) return;
692
+ if (now - stat.mtimeMs > SWEEP_FALLBACK_AGE_MS) await this.deleteArtifact(id);
693
+ }));
694
+ }
695
+ async ensureRoot() {
696
+ await fs.mkdir(this.rootDir, { recursive: true });
697
+ }
698
+ maybeCleanupExpired() {
699
+ const now = Date.now();
700
+ if (this.cleanupInFlight || now < this.nextCleanupAt) return;
701
+ this.nextCleanupAt = now + this.cleanupIntervalMs;
702
+ const cleanupPromise = this.cleanupExpired().catch((error) => {
703
+ this.nextCleanupAt = 0;
704
+ this.logger?.warn(`Failed to clean expired diff artifacts: ${String(error)}`);
705
+ }).finally(() => {
706
+ if (this.cleanupInFlight === cleanupPromise) this.cleanupInFlight = null;
707
+ });
708
+ this.cleanupInFlight = cleanupPromise;
709
+ }
710
+ artifactDir(id) {
711
+ return this.resolveWithinRoot(id);
712
+ }
713
+ async writeMeta(meta) {
714
+ await this.writeJsonMeta(meta.id, "meta.json", meta);
715
+ }
716
+ async readMeta(id) {
717
+ const parsed = await this.readJsonMeta(id, "meta.json", "diff artifact");
718
+ if (!parsed) return null;
719
+ return parsed;
720
+ }
721
+ async writeStandaloneMeta(meta) {
722
+ await this.writeJsonMeta(meta.id, "file-meta.json", meta);
723
+ }
724
+ async readStandaloneMeta(id) {
725
+ const parsed = await this.readJsonMeta(id, "file-meta.json", "standalone diff");
726
+ if (!parsed) return null;
727
+ try {
728
+ const value = parsed;
729
+ if (value.kind !== "standalone_file" || typeof value.id !== "string" || typeof value.createdAt !== "string" || typeof value.expiresAt !== "string" || typeof value.filePath !== "string") return null;
730
+ return {
731
+ kind: value.kind,
732
+ id: value.id,
733
+ createdAt: value.createdAt,
734
+ expiresAt: value.expiresAt,
735
+ filePath: this.normalizeStoredPath(value.filePath, "filePath"),
736
+ ...value.context ? { context: normalizeArtifactContext(value.context) } : {}
737
+ };
738
+ } catch (error) {
739
+ this.logger?.warn(`Failed to normalize standalone diff metadata for ${id}: ${String(error)}`);
740
+ return null;
741
+ }
742
+ }
743
+ metaFilePath(id, fileName) {
744
+ return path.join(this.artifactDir(id), fileName);
745
+ }
746
+ async writeJsonMeta(id, fileName, data) {
747
+ await fs.writeFile(this.metaFilePath(id, fileName), JSON.stringify(data, null, 2), "utf8");
748
+ }
749
+ async readJsonMeta(id, fileName, context) {
750
+ try {
751
+ const raw = await fs.readFile(this.metaFilePath(id, fileName), "utf8");
752
+ return JSON.parse(raw);
753
+ } catch (error) {
754
+ if (isFileNotFound(error)) return null;
755
+ this.logger?.warn(`Failed to read ${context} metadata for ${id}: ${String(error)}`);
756
+ return null;
757
+ }
758
+ }
759
+ async deleteArtifact(id) {
760
+ await fs.rm(this.artifactDir(id), {
761
+ recursive: true,
762
+ force: true
763
+ }).catch(() => {});
764
+ }
765
+ resolveWithinRoot(...parts) {
766
+ const candidate = path.resolve(this.rootDir, ...parts);
767
+ this.assertWithinRoot(candidate);
768
+ return candidate;
769
+ }
770
+ normalizeStoredPath(rawPath, label) {
771
+ const candidate = path.isAbsolute(rawPath) ? path.resolve(rawPath) : path.resolve(this.rootDir, rawPath);
772
+ this.assertWithinRoot(candidate, label);
773
+ return candidate;
774
+ }
775
+ assertWithinRoot(candidate, label = "path") {
776
+ const relative = path.relative(this.rootDir, candidate);
777
+ if (relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)) return;
778
+ throw new Error(`Diff artifact ${label} escapes store root: ${candidate}`);
779
+ }
780
+ };
781
+ function normalizeTtlMs$1(value) {
782
+ if (!Number.isFinite(value) || value === void 0) return DEFAULT_TTL_MS;
783
+ const rounded = Math.floor(value);
784
+ if (rounded <= 0) return DEFAULT_TTL_MS;
785
+ return Math.min(rounded, MAX_TTL_MS);
786
+ }
787
+ function isExpired(meta) {
788
+ const expiresAt = Date.parse(meta.expiresAt);
789
+ if (!Number.isFinite(expiresAt)) return true;
790
+ return Date.now() >= expiresAt;
791
+ }
792
+ function isFileNotFound(error) {
793
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
794
+ }
795
+ function normalizeArtifactContext(value) {
796
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
797
+ const raw = value;
798
+ const context = {
799
+ agentId: normalizeOptionalString(raw.agentId),
800
+ sessionId: normalizeOptionalString(raw.sessionId),
801
+ messageChannel: normalizeOptionalString(raw.messageChannel),
802
+ agentAccountId: normalizeOptionalString(raw.agentAccountId)
803
+ };
804
+ return Object.values(context).some((entry) => entry !== void 0) ? context : void 0;
805
+ }
806
+ //#endregion
807
+ //#region extensions/diffs/src/browser.ts
808
+ const DEFAULT_BROWSER_IDLE_MS = 3e4;
809
+ const SHARED_BROWSER_KEY = "__default__";
810
+ const IMAGE_SIZE_LIMIT_ERROR = "Diff frame did not render within image size limits.";
811
+ const PDF_REFERENCE_PAGE_HEIGHT_PX = 1056;
812
+ const MAX_PDF_PAGES = 50;
813
+ const LOCAL_VIEWER_BASE_HREF = "http://127.0.0.1/plugins/diffs/view/local/local";
814
+ let sharedBrowserState = null;
815
+ let executablePathCache = null;
816
+ var PlaywrightDiffScreenshotter = class {
817
+ constructor(params) {
818
+ this.config = params.config;
819
+ this.browserIdleMs = params.browserIdleMs ?? DEFAULT_BROWSER_IDLE_MS;
820
+ }
821
+ async screenshotHtml(params) {
822
+ await fs.mkdir(path.dirname(params.outputPath), { recursive: true });
823
+ const lease = await acquireSharedBrowser({
824
+ config: this.config,
825
+ idleMs: this.browserIdleMs
826
+ });
827
+ let page;
828
+ let currentScale = params.image.scale;
829
+ const maxRetries = 2;
830
+ try {
831
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
832
+ page = await lease.browser.newPage({
833
+ viewport: {
834
+ width: Math.max(Math.ceil(params.image.maxWidth + 240), 1200),
835
+ height: 900
836
+ },
837
+ deviceScaleFactor: currentScale,
838
+ colorScheme: params.theme
839
+ });
840
+ await page.route("**/*", async (route) => {
841
+ const requestUrl = route.request().url();
842
+ if (requestUrl === "about:blank" || requestUrl.startsWith("data:")) {
843
+ await route.continue();
844
+ return;
845
+ }
846
+ let parsed;
847
+ try {
848
+ parsed = new URL(requestUrl);
849
+ } catch {
850
+ await route.abort();
851
+ return;
852
+ }
853
+ if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1") {
854
+ await route.abort();
855
+ return;
856
+ }
857
+ if (!parsed.pathname.startsWith("/plugins/diffs/assets/")) {
858
+ await route.abort();
859
+ return;
860
+ }
861
+ const pathname = parsed.pathname;
862
+ const asset = await getServedViewerAsset(pathname);
863
+ if (!asset) {
864
+ await route.abort();
865
+ return;
866
+ }
867
+ await route.fulfill({
868
+ status: 200,
869
+ contentType: asset.contentType,
870
+ body: asset.body
871
+ });
872
+ });
873
+ await page.setContent(injectBaseHref(params.html), { waitUntil: "load" });
874
+ await page.waitForFunction(() => {
875
+ if (document.documentElement.dataset.openclawDiffsReady === "true") return true;
876
+ return [...document.querySelectorAll("[data-openclaw-diff-host]")].every((element) => {
877
+ return element instanceof HTMLElement && element.shadowRoot?.querySelector("[data-diffs]");
878
+ });
879
+ }, { timeout: 1e4 });
880
+ await page.evaluate(async () => {
881
+ await document.fonts.ready;
882
+ });
883
+ await page.evaluate(() => {
884
+ const frame = document.querySelector(".oc-frame");
885
+ if (frame instanceof HTMLElement) frame.dataset.renderMode = "image";
886
+ });
887
+ const frame = page.locator(".oc-frame");
888
+ await frame.waitFor();
889
+ const initialBox = await frame.boundingBox();
890
+ if (!initialBox) throw new Error("Diff frame did not render.");
891
+ const isPdf = params.image.format === "pdf";
892
+ const padding = isPdf ? 0 : 20;
893
+ const clipWidth = Math.ceil(initialBox.width + padding * 2);
894
+ const clipHeight = Math.ceil(Math.max(initialBox.height + padding * 2, 320));
895
+ await page.setViewportSize({
896
+ width: Math.max(clipWidth + padding, 900),
897
+ height: Math.max(clipHeight + padding, 700)
898
+ });
899
+ const box = await frame.boundingBox();
900
+ if (!box) throw new Error("Diff frame was lost after resizing.");
901
+ if (isPdf) {
902
+ await page.emulateMedia({ media: "screen" });
903
+ await page.evaluate(() => {
904
+ const html = document.documentElement;
905
+ const body = document.body;
906
+ const frame = document.querySelector(".oc-frame");
907
+ html.style.background = "transparent";
908
+ body.style.margin = "0";
909
+ body.style.padding = "0";
910
+ body.style.background = "transparent";
911
+ body.style.setProperty("-webkit-print-color-adjust", "exact");
912
+ if (frame instanceof HTMLElement) frame.style.margin = "0";
913
+ });
914
+ const pdfBox = await frame.boundingBox();
915
+ if (!pdfBox) throw new Error("Diff frame was lost before PDF render.");
916
+ const pdfWidth = Math.max(Math.ceil(pdfBox.width), 1);
917
+ const pdfHeight = Math.max(Math.ceil(pdfBox.height), 1);
918
+ const estimatedPixels = pdfWidth * pdfHeight;
919
+ const estimatedPages = Math.ceil(pdfHeight / PDF_REFERENCE_PAGE_HEIGHT_PX);
920
+ if (estimatedPixels > params.image.maxPixels || estimatedPages > MAX_PDF_PAGES) throw new Error(IMAGE_SIZE_LIMIT_ERROR);
921
+ await page.pdf({
922
+ path: params.outputPath,
923
+ width: `${pdfWidth}px`,
924
+ height: `${pdfHeight}px`,
925
+ printBackground: true,
926
+ margin: {
927
+ top: "0",
928
+ right: "0",
929
+ bottom: "0",
930
+ left: "0"
931
+ }
932
+ });
933
+ return params.outputPath;
934
+ }
935
+ const dpr = await page.evaluate(() => window.devicePixelRatio || 1);
936
+ const rawX = Math.max(box.x - padding, 0);
937
+ const rawY = Math.max(box.y - padding, 0);
938
+ const rawRight = rawX + clipWidth;
939
+ const rawBottom = rawY + clipHeight;
940
+ const x = Math.floor(rawX * dpr) / dpr;
941
+ const y = Math.floor(rawY * dpr) / dpr;
942
+ const right = Math.ceil(rawRight * dpr) / dpr;
943
+ const bottom = Math.ceil(rawBottom * dpr) / dpr;
944
+ const cssWidth = Math.max(right - x, 1);
945
+ const cssHeight = Math.max(bottom - y, 1);
946
+ if (cssWidth * cssHeight * dpr * dpr > params.image.maxPixels) {
947
+ if (currentScale > 1) {
948
+ const maxScaleForPixels = Math.sqrt(params.image.maxPixels / (cssWidth * cssHeight));
949
+ const reducedScale = Math.max(1, Math.round(Math.min(currentScale, maxScaleForPixels) * 100) / 100);
950
+ if (reducedScale < currentScale - .01 && attempt < maxRetries) {
951
+ await page.close().catch(() => {});
952
+ page = void 0;
953
+ currentScale = reducedScale;
954
+ continue;
955
+ }
956
+ }
957
+ throw new Error(IMAGE_SIZE_LIMIT_ERROR);
958
+ }
959
+ await page.screenshot({
960
+ path: params.outputPath,
961
+ type: "png",
962
+ scale: "device",
963
+ clip: {
964
+ x,
965
+ y,
966
+ width: cssWidth,
967
+ height: cssHeight
968
+ }
969
+ });
970
+ return params.outputPath;
971
+ }
972
+ throw new Error(IMAGE_SIZE_LIMIT_ERROR);
973
+ } catch (error) {
974
+ if (error instanceof Error && error.message === IMAGE_SIZE_LIMIT_ERROR) throw error;
975
+ const reason = formatErrorMessage(error);
976
+ throw new Error(`Diff PNG/PDF rendering requires a Chromium-compatible browser. Set browser.executablePath or install Chrome/Chromium. ${reason}`, { cause: error });
977
+ } finally {
978
+ await page?.close().catch(() => {});
979
+ await lease.release();
980
+ }
981
+ }
982
+ };
983
+ function injectBaseHref(html) {
984
+ if (html.includes("<base ")) return html;
985
+ return html.replace("<head>", `<head><base href="${LOCAL_VIEWER_BASE_HREF}" />`);
986
+ }
987
+ async function resolveBrowserExecutablePath(config) {
988
+ const cacheKey = JSON.stringify({
989
+ configPath: config.browser?.executablePath?.trim() || "",
990
+ env: [
991
+ process.env.OPENCLAW_BROWSER_EXECUTABLE_PATH ?? "",
992
+ process.env.BROWSER_EXECUTABLE_PATH ?? "",
993
+ process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ?? ""
994
+ ],
995
+ path: process.env.PATH ?? ""
996
+ });
997
+ if (executablePathCache?.key === cacheKey) return await executablePathCache.valuePromise;
998
+ const valuePromise = resolveBrowserExecutablePathUncached(config).catch((error) => {
999
+ if (executablePathCache?.valuePromise === valuePromise) executablePathCache = null;
1000
+ throw error;
1001
+ });
1002
+ executablePathCache = {
1003
+ key: cacheKey,
1004
+ valuePromise
1005
+ };
1006
+ return await valuePromise;
1007
+ }
1008
+ async function resolveBrowserExecutablePathUncached(config) {
1009
+ const configPath = config.browser?.executablePath?.trim();
1010
+ if (configPath) {
1011
+ await assertExecutable(configPath, "browser.executablePath");
1012
+ return configPath;
1013
+ }
1014
+ const envCandidates = [
1015
+ process.env.OPENCLAW_BROWSER_EXECUTABLE_PATH,
1016
+ process.env.BROWSER_EXECUTABLE_PATH,
1017
+ process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
1018
+ ].map((value) => value?.trim()).filter((value) => Boolean(value));
1019
+ for (const candidate of envCandidates) if (await isExecutable(candidate)) return candidate;
1020
+ for (const candidate of await collectExecutableCandidates()) if (await isExecutable(candidate)) return candidate;
1021
+ }
1022
+ async function acquireSharedBrowser(params) {
1023
+ const executablePath = await resolveBrowserExecutablePath(params.config);
1024
+ const desiredKey = executablePath || SHARED_BROWSER_KEY;
1025
+ if (sharedBrowserState && sharedBrowserState.key !== desiredKey) await closeSharedBrowser();
1026
+ if (!sharedBrowserState) {
1027
+ const browserPromise = chromium.launch({
1028
+ headless: true,
1029
+ ...executablePath ? { executablePath } : {},
1030
+ args: ["--disable-dev-shm-usage"]
1031
+ }).then((browser) => {
1032
+ if (sharedBrowserState?.browserPromise === browserPromise) {
1033
+ sharedBrowserState.browser = browser;
1034
+ browser.on("disconnected", () => {
1035
+ if (sharedBrowserState?.browser === browser) {
1036
+ clearIdleTimer(sharedBrowserState);
1037
+ sharedBrowserState = null;
1038
+ }
1039
+ });
1040
+ }
1041
+ return browser;
1042
+ }).catch((error) => {
1043
+ if (sharedBrowserState?.browserPromise === browserPromise) sharedBrowserState = null;
1044
+ throw error;
1045
+ });
1046
+ sharedBrowserState = {
1047
+ browserPromise,
1048
+ idleTimer: null,
1049
+ key: desiredKey,
1050
+ users: 0
1051
+ };
1052
+ }
1053
+ clearIdleTimer(sharedBrowserState);
1054
+ const state = sharedBrowserState;
1055
+ const browser = await state.browserPromise;
1056
+ state.users += 1;
1057
+ let released = false;
1058
+ return {
1059
+ browser,
1060
+ release: async () => {
1061
+ if (released) return;
1062
+ released = true;
1063
+ state.users = Math.max(0, state.users - 1);
1064
+ if (state.users === 0) scheduleIdleBrowserClose(state, params.idleMs);
1065
+ }
1066
+ };
1067
+ }
1068
+ function scheduleIdleBrowserClose(state, idleMs) {
1069
+ clearIdleTimer(state);
1070
+ state.idleTimer = setTimeout(() => {
1071
+ if (sharedBrowserState === state && state.users === 0) closeSharedBrowser();
1072
+ }, idleMs);
1073
+ }
1074
+ function clearIdleTimer(state) {
1075
+ if (!state.idleTimer) return;
1076
+ clearTimeout(state.idleTimer);
1077
+ state.idleTimer = null;
1078
+ }
1079
+ async function closeSharedBrowser() {
1080
+ const state = sharedBrowserState;
1081
+ if (!state) return;
1082
+ sharedBrowserState = null;
1083
+ clearIdleTimer(state);
1084
+ await (state.browser ?? await state.browserPromise.catch(() => null))?.close().catch(() => {});
1085
+ }
1086
+ async function collectExecutableCandidates() {
1087
+ const candidates = /* @__PURE__ */ new Set();
1088
+ for (const command of pathCommandsForPlatform()) {
1089
+ const resolved = await findExecutableInPath(command);
1090
+ if (resolved) candidates.add(resolved);
1091
+ }
1092
+ for (const candidate of commonExecutablePathsForPlatform()) candidates.add(candidate);
1093
+ return [...candidates];
1094
+ }
1095
+ function pathCommandsForPlatform() {
1096
+ if (process.platform === "win32") return [
1097
+ "chrome.exe",
1098
+ "msedge.exe",
1099
+ "brave.exe"
1100
+ ];
1101
+ if (process.platform === "darwin") return [
1102
+ "google-chrome",
1103
+ "chromium",
1104
+ "msedge",
1105
+ "brave-browser",
1106
+ "brave"
1107
+ ];
1108
+ return [
1109
+ "chromium",
1110
+ "chromium-browser",
1111
+ "google-chrome",
1112
+ "google-chrome-stable",
1113
+ "msedge",
1114
+ "brave-browser",
1115
+ "brave"
1116
+ ];
1117
+ }
1118
+ function commonExecutablePathsForPlatform() {
1119
+ if (process.platform === "darwin") return [
1120
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1121
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
1122
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
1123
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
1124
+ ];
1125
+ if (process.platform === "win32") {
1126
+ const localAppData = process.env.LOCALAPPDATA ?? "";
1127
+ const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
1128
+ const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
1129
+ return [
1130
+ path.join(localAppData, "Google", "Chrome", "Application", "chrome.exe"),
1131
+ path.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
1132
+ path.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
1133
+ path.join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
1134
+ path.join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
1135
+ path.join(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
1136
+ path.join(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
1137
+ ];
1138
+ }
1139
+ return [
1140
+ "/usr/bin/chromium",
1141
+ "/usr/bin/chromium-browser",
1142
+ "/usr/bin/google-chrome",
1143
+ "/usr/bin/google-chrome-stable",
1144
+ "/usr/bin/msedge",
1145
+ "/usr/bin/brave-browser",
1146
+ "/snap/bin/chromium"
1147
+ ];
1148
+ }
1149
+ async function findExecutableInPath(command) {
1150
+ const pathValue = process.env.PATH;
1151
+ if (!pathValue) return;
1152
+ for (const directory of pathValue.split(path.delimiter)) {
1153
+ if (!directory) continue;
1154
+ const candidate = path.join(directory, command);
1155
+ if (await isExecutable(candidate)) return candidate;
1156
+ }
1157
+ }
1158
+ async function assertExecutable(candidate, label) {
1159
+ if (!await isExecutable(candidate)) throw new Error(`${label} not found or not executable: ${candidate}`);
1160
+ }
1161
+ async function isExecutable(candidate) {
1162
+ try {
1163
+ await fs.access(candidate, constants.X_OK);
1164
+ return true;
1165
+ } catch {
1166
+ return false;
1167
+ }
1168
+ }
1169
+ //#endregion
1170
+ //#region extensions/diffs/src/language-hints.ts
1171
+ const PASSTHROUGH_LANGUAGE_HINTS = new Set(["ansi", "text"]);
1172
+ function normalizeOptionalString$1(value) {
1173
+ if (typeof value !== "string") return;
1174
+ const trimmed = value.trim();
1175
+ return trimmed ? trimmed : void 0;
1176
+ }
1177
+ async function normalizeSupportedLanguageHint(value) {
1178
+ const normalized = normalizeOptionalString$1(value);
1179
+ if (!normalized) return;
1180
+ if (PASSTHROUGH_LANGUAGE_HINTS.has(normalized)) return normalized;
1181
+ try {
1182
+ await resolveLanguage(normalized);
1183
+ return normalized;
1184
+ } catch {
1185
+ return;
1186
+ }
1187
+ }
1188
+ async function normalizeSupportedLanguageHints(values, options) {
1189
+ const supported = /* @__PURE__ */ new Set();
1190
+ for (const value of values) {
1191
+ const normalized = await normalizeSupportedLanguageHint(value);
1192
+ if (!normalized) continue;
1193
+ supported.add(normalized);
1194
+ }
1195
+ if (options.fallbackToText && supported.size === 0) supported.add("text");
1196
+ return [...supported];
1197
+ }
1198
+ function collectDiffPayloadLanguageHints(payload) {
1199
+ const langs = /* @__PURE__ */ new Set();
1200
+ if (payload.fileDiff?.lang) langs.add(payload.fileDiff.lang);
1201
+ if (payload.oldFile?.lang) langs.add(payload.oldFile.lang);
1202
+ if (payload.newFile?.lang) langs.add(payload.newFile.lang);
1203
+ return [...langs];
1204
+ }
1205
+ async function normalizeDiffPayloadFileLanguage(file) {
1206
+ if (!file) return;
1207
+ if (typeof file.lang !== "string") return file;
1208
+ const normalized = await normalizeSupportedLanguageHint(file.lang);
1209
+ if (file.lang === normalized) return file;
1210
+ if (!normalized) return {
1211
+ ...file,
1212
+ lang: "text"
1213
+ };
1214
+ return {
1215
+ ...file,
1216
+ lang: normalized
1217
+ };
1218
+ }
1219
+ async function normalizeDiffViewerPayloadLanguages(payload) {
1220
+ const [fileDiff, oldFile, newFile, payloadLangs] = await Promise.all([
1221
+ normalizeDiffPayloadFileLanguage(payload.fileDiff),
1222
+ normalizeDiffPayloadFileLanguage(payload.oldFile),
1223
+ normalizeDiffPayloadFileLanguage(payload.newFile),
1224
+ normalizeSupportedLanguageHints(payload.langs, { fallbackToText: false })
1225
+ ]);
1226
+ const langs = new Set(payloadLangs);
1227
+ for (const lang of collectDiffPayloadLanguageHints({
1228
+ fileDiff,
1229
+ oldFile,
1230
+ newFile
1231
+ })) langs.add(lang);
1232
+ if (langs.size === 0) langs.add("text");
1233
+ return {
1234
+ ...payload,
1235
+ fileDiff,
1236
+ oldFile,
1237
+ newFile,
1238
+ langs: [...langs]
1239
+ };
1240
+ }
1241
+ //#endregion
1242
+ //#region extensions/diffs/src/pierre-themes.ts
1243
+ const themeRequire = createRequire(import.meta.url);
1244
+ const PIERRE_THEME_SPECS = [["pierre-dark", "@pierre/theme/themes/pierre-dark.json"], ["pierre-light", "@pierre/theme/themes/pierre-light.json"]];
1245
+ function createThemeLoader(themeName, themeSpecifier) {
1246
+ let cachedTheme;
1247
+ return async () => {
1248
+ if (cachedTheme) return cachedTheme;
1249
+ const themePath = themeRequire.resolve(themeSpecifier);
1250
+ cachedTheme = {
1251
+ ...JSON.parse(await fs.readFile(themePath, "utf8")),
1252
+ name: themeName
1253
+ };
1254
+ return cachedTheme;
1255
+ };
1256
+ }
1257
+ const PIERRE_THEME_LOADERS = new Map(PIERRE_THEME_SPECS.map(([themeName, themeSpecifier]) => [themeName, createThemeLoader(themeName, themeSpecifier)]));
1258
+ function ensurePierreThemesRegistered() {
1259
+ let replacedThemeLoader = false;
1260
+ for (const [themeName, loader] of PIERRE_THEME_LOADERS) if (RegisteredCustomThemes.get(themeName) !== loader) {
1261
+ RegisteredCustomThemes.set(themeName, loader);
1262
+ replacedThemeLoader = true;
1263
+ }
1264
+ if (!replacedThemeLoader) return;
1265
+ for (const [themeName] of PIERRE_THEME_LOADERS) {
1266
+ ResolvedThemes.delete(themeName);
1267
+ ResolvingThemes.delete(themeName);
1268
+ }
1269
+ }
1270
+ //#endregion
1271
+ //#region extensions/diffs/src/render.ts
1272
+ const DEFAULT_FILE_NAME = "diff.txt";
1273
+ const MAX_PATCH_FILE_COUNT = 128;
1274
+ const MAX_PATCH_TOTAL_LINES = 12e4;
1275
+ const VIEWER_LOADER_DOCUMENT_PATH = "../../assets/viewer.js";
1276
+ function escapeCssString(value) {
1277
+ return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
1278
+ }
1279
+ function escapeHtml(value) {
1280
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
1281
+ }
1282
+ function escapeJsonScript(value) {
1283
+ return JSON.stringify(value).replaceAll("<", "\\u003c");
1284
+ }
1285
+ function buildDiffTitle(input) {
1286
+ if (input.title?.trim()) return input.title.trim();
1287
+ if (input.kind === "before_after") return input.path?.trim() || "Text diff";
1288
+ return "Patch diff";
1289
+ }
1290
+ function resolveBeforeAfterFileName(params) {
1291
+ const { input, lang } = params;
1292
+ if (input.path?.trim()) return input.path.trim();
1293
+ if (lang && lang !== "text") return `diff.${lang.replace(/^\.+/, "")}`;
1294
+ return DEFAULT_FILE_NAME;
1295
+ }
1296
+ function buildDiffOptions(options) {
1297
+ const fontFamily = escapeCssString(options.presentation.fontFamily);
1298
+ const fontSize = Math.max(10, Math.floor(options.presentation.fontSize));
1299
+ const lineHeight = Math.max(20, Math.round(fontSize * options.presentation.lineSpacing));
1300
+ return {
1301
+ theme: {
1302
+ light: "pierre-light",
1303
+ dark: "pierre-dark"
1304
+ },
1305
+ diffStyle: options.presentation.layout,
1306
+ diffIndicators: options.presentation.diffIndicators,
1307
+ disableLineNumbers: !options.presentation.showLineNumbers,
1308
+ expandUnchanged: options.expandUnchanged,
1309
+ themeType: options.presentation.theme,
1310
+ backgroundEnabled: options.presentation.background,
1311
+ overflow: options.presentation.wordWrap ? "wrap" : "scroll",
1312
+ unsafeCSS: `
1313
+ :host {
1314
+ --diffs-font-family: "${fontFamily}", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1315
+ --diffs-header-font-family: "${fontFamily}", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1316
+ --diffs-font-size: ${fontSize}px;
1317
+ --diffs-line-height: ${lineHeight}px;
1318
+ }
1319
+
1320
+ [data-diffs-header] {
1321
+ min-height: 64px;
1322
+ padding-inline: 18px 14px;
1323
+ }
1324
+
1325
+ [data-header-content] {
1326
+ gap: 10px;
1327
+ }
1328
+
1329
+ [data-metadata] {
1330
+ gap: 10px;
1331
+ }
1332
+
1333
+ .oc-diff-toolbar {
1334
+ display: inline-flex;
1335
+ align-items: center;
1336
+ gap: 6px;
1337
+ margin-inline-start: 6px;
1338
+ flex: 0 0 auto;
1339
+ }
1340
+
1341
+ .oc-diff-toolbar-button {
1342
+ display: inline-flex;
1343
+ align-items: center;
1344
+ justify-content: center;
1345
+ width: 24px;
1346
+ height: 24px;
1347
+ padding: 0;
1348
+ margin: 0;
1349
+ border: 0;
1350
+ border-radius: 0;
1351
+ background: transparent;
1352
+ color: inherit;
1353
+ cursor: pointer;
1354
+ opacity: 0.6;
1355
+ line-height: 0;
1356
+ overflow: visible;
1357
+ transition: opacity 120ms ease;
1358
+ flex: 0 0 auto;
1359
+ }
1360
+
1361
+ .oc-diff-toolbar-button:hover {
1362
+ opacity: 1;
1363
+ }
1364
+
1365
+ .oc-diff-toolbar-button[data-active="true"] {
1366
+ opacity: 0.92;
1367
+ }
1368
+
1369
+ .oc-diff-toolbar-button svg {
1370
+ display: block;
1371
+ width: 16px;
1372
+ height: 16px;
1373
+ min-width: 16px;
1374
+ min-height: 16px;
1375
+ overflow: visible;
1376
+ flex: 0 0 auto;
1377
+ color: inherit;
1378
+ fill: currentColor;
1379
+ pointer-events: none;
1380
+ }
1381
+ `
1382
+ };
1383
+ }
1384
+ function buildImageRenderOptions(options) {
1385
+ return {
1386
+ ...options,
1387
+ presentation: {
1388
+ ...options.presentation,
1389
+ fontSize: Math.max(16, options.presentation.fontSize)
1390
+ }
1391
+ };
1392
+ }
1393
+ function shouldRenderViewer(target) {
1394
+ return target === "viewer" || target === "both";
1395
+ }
1396
+ function shouldRenderImage(target) {
1397
+ return target === "image" || target === "both";
1398
+ }
1399
+ function buildRenderVariants(params) {
1400
+ return {
1401
+ ...shouldRenderViewer(params.target) ? { viewerOptions: buildDiffOptions(params.options) } : {},
1402
+ ...shouldRenderImage(params.target) ? { imageOptions: buildDiffOptions(buildImageRenderOptions(params.options)) } : {}
1403
+ };
1404
+ }
1405
+ function renderDiffCard(payload) {
1406
+ return `<section class="oc-diff-card">
1407
+ <diffs-container class="oc-diff-host" data-openclaw-diff-host>
1408
+ <template shadowrootmode="open">${payload.prerenderedHTML}</template>
1409
+ </diffs-container>
1410
+ <script type="application/json" data-openclaw-diff-payload>${escapeJsonScript(payload)}<\/script>
1411
+ </section>`;
1412
+ }
1413
+ function buildHtmlDocument(params) {
1414
+ return `<!doctype html>
1415
+ <html lang="en">
1416
+ <head>
1417
+ <meta charset="utf-8" />
1418
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1419
+ <meta name="color-scheme" content="dark light" />
1420
+ <title>${escapeHtml(params.title)}</title>
1421
+ <style>
1422
+ * {
1423
+ box-sizing: border-box;
1424
+ }
1425
+
1426
+ html,
1427
+ body {
1428
+ min-height: 100%;
1429
+ }
1430
+
1431
+ html {
1432
+ background: #05070b;
1433
+ }
1434
+
1435
+ body {
1436
+ margin: 0;
1437
+ min-height: 100vh;
1438
+ padding: 22px;
1439
+ font-family:
1440
+ "Fira Code",
1441
+ "SF Mono",
1442
+ Monaco,
1443
+ Consolas,
1444
+ monospace;
1445
+ background: #05070b;
1446
+ color: #f8fafc;
1447
+ }
1448
+
1449
+ body[data-theme="light"] {
1450
+ background: #f3f5f8;
1451
+ color: #0f172a;
1452
+ }
1453
+
1454
+ .oc-frame {
1455
+ max-width: 1560px;
1456
+ margin: 0 auto;
1457
+ }
1458
+
1459
+ .oc-frame[data-render-mode="image"] {
1460
+ max-width: ${Math.max(640, Math.round(params.imageMaxWidth))}px;
1461
+ }
1462
+
1463
+ [data-openclaw-diff-root] {
1464
+ display: grid;
1465
+ gap: 18px;
1466
+ }
1467
+
1468
+ .oc-diff-card {
1469
+ overflow: hidden;
1470
+ border-radius: 18px;
1471
+ border: 1px solid rgba(148, 163, 184, 0.16);
1472
+ background: rgba(15, 23, 42, 0.14);
1473
+ box-shadow: 0 18px 48px rgba(2, 6, 23, 0.22);
1474
+ }
1475
+
1476
+ body[data-theme="light"] .oc-diff-card {
1477
+ border-color: rgba(148, 163, 184, 0.22);
1478
+ background: rgba(255, 255, 255, 0.92);
1479
+ box-shadow: 0 14px 32px rgba(15, 23, 42, 0.08);
1480
+ }
1481
+
1482
+ .oc-diff-host {
1483
+ display: block;
1484
+ }
1485
+
1486
+ .oc-frame[data-render-mode="image"] .oc-diff-card {
1487
+ min-height: 240px;
1488
+ }
1489
+
1490
+ @media (max-width: 720px) {
1491
+ body {
1492
+ padding: 12px;
1493
+ }
1494
+
1495
+ [data-openclaw-diff-root] {
1496
+ gap: 12px;
1497
+ }
1498
+ }
1499
+ </style>
1500
+ </head>
1501
+ <body data-theme="${params.theme}">
1502
+ <main class="oc-frame" data-render-mode="${params.runtimeMode}">
1503
+ <div data-openclaw-diff-root>
1504
+ ${params.bodyHtml}
1505
+ </div>
1506
+ </main>
1507
+ <script type="module" src="${VIEWER_LOADER_DOCUMENT_PATH}"><\/script>
1508
+ </body>
1509
+ </html>`;
1510
+ }
1511
+ function buildRenderedSection(params) {
1512
+ return {
1513
+ ...params.viewerPayload ? { viewer: renderDiffCard(params.viewerPayload) } : {},
1514
+ ...params.imagePayload ? { image: renderDiffCard(params.imagePayload) } : {}
1515
+ };
1516
+ }
1517
+ function buildRenderedBodies(sections) {
1518
+ const viewerSections = sections.flatMap((section) => section.viewer ? [section.viewer] : []);
1519
+ const imageSections = sections.flatMap((section) => section.image ? [section.image] : []);
1520
+ return {
1521
+ ...viewerSections.length > 0 ? { viewerBodyHtml: viewerSections.join("\n") } : {},
1522
+ ...imageSections.length > 0 ? { imageBodyHtml: imageSections.join("\n") } : {}
1523
+ };
1524
+ }
1525
+ async function renderBeforeAfterDiff(input, options, target) {
1526
+ ensurePierreThemesRegistered();
1527
+ const lang = await normalizeSupportedLanguageHint(input.lang);
1528
+ const fileName = resolveBeforeAfterFileName({
1529
+ input,
1530
+ lang
1531
+ });
1532
+ const oldFile = {
1533
+ name: fileName,
1534
+ contents: input.before,
1535
+ ...lang ? { lang } : {}
1536
+ };
1537
+ const newFile = {
1538
+ name: fileName,
1539
+ contents: input.after,
1540
+ ...lang ? { lang } : {}
1541
+ };
1542
+ const { viewerOptions, imageOptions } = buildRenderVariants({
1543
+ options,
1544
+ target
1545
+ });
1546
+ const [viewerResult, imageResult] = await Promise.all([viewerOptions ? preloadMultiFileDiffWithFallback({
1547
+ oldFile,
1548
+ newFile,
1549
+ options: viewerOptions
1550
+ }) : Promise.resolve(void 0), imageOptions ? preloadMultiFileDiffWithFallback({
1551
+ oldFile,
1552
+ newFile,
1553
+ options: imageOptions
1554
+ }) : Promise.resolve(void 0)]);
1555
+ const [viewerPayload, imagePayload] = await Promise.all([viewerResult && viewerOptions ? normalizeDiffViewerPayloadLanguages({
1556
+ prerenderedHTML: viewerResult.prerenderedHTML,
1557
+ oldFile: viewerResult.oldFile,
1558
+ newFile: viewerResult.newFile,
1559
+ options: viewerOptions,
1560
+ langs: collectDiffPayloadLanguageHints({
1561
+ oldFile: viewerResult.oldFile,
1562
+ newFile: viewerResult.newFile
1563
+ })
1564
+ }) : Promise.resolve(void 0), imageResult && imageOptions ? normalizeDiffViewerPayloadLanguages({
1565
+ prerenderedHTML: imageResult.prerenderedHTML,
1566
+ oldFile: imageResult.oldFile,
1567
+ newFile: imageResult.newFile,
1568
+ options: imageOptions,
1569
+ langs: collectDiffPayloadLanguageHints({
1570
+ oldFile: imageResult.oldFile,
1571
+ newFile: imageResult.newFile
1572
+ })
1573
+ }) : Promise.resolve(void 0)]);
1574
+ return {
1575
+ ...buildRenderedBodies([buildRenderedSection({
1576
+ ...viewerPayload ? { viewerPayload } : {},
1577
+ ...imagePayload ? { imagePayload } : {}
1578
+ })]),
1579
+ fileCount: 1
1580
+ };
1581
+ }
1582
+ async function renderPatchDiff(input, options, target) {
1583
+ ensurePierreThemesRegistered();
1584
+ const files = parsePatchFiles(input.patch).flatMap((entry) => entry.files ?? []);
1585
+ if (files.length === 0) throw new Error("Patch input did not contain any file diffs.");
1586
+ if (files.length > MAX_PATCH_FILE_COUNT) throw new Error(`Patch input contains too many files (max ${MAX_PATCH_FILE_COUNT}).`);
1587
+ if (files.reduce((sum, fileDiff) => {
1588
+ const splitLines = Number.isFinite(fileDiff.splitLineCount) ? fileDiff.splitLineCount : 0;
1589
+ const unifiedLines = Number.isFinite(fileDiff.unifiedLineCount) ? fileDiff.unifiedLineCount : 0;
1590
+ return sum + Math.max(splitLines, unifiedLines, 0);
1591
+ }, 0) > MAX_PATCH_TOTAL_LINES) throw new Error(`Patch input is too large to render (max ${MAX_PATCH_TOTAL_LINES} lines).`);
1592
+ const { viewerOptions, imageOptions } = buildRenderVariants({
1593
+ options,
1594
+ target
1595
+ });
1596
+ return {
1597
+ ...buildRenderedBodies(await Promise.all(files.map(async (fileDiff) => {
1598
+ const [viewerResult, imageResult] = await Promise.all([viewerOptions ? preloadFileDiffWithFallback({
1599
+ fileDiff,
1600
+ options: viewerOptions
1601
+ }) : Promise.resolve(void 0), imageOptions ? preloadFileDiffWithFallback({
1602
+ fileDiff,
1603
+ options: imageOptions
1604
+ }) : Promise.resolve(void 0)]);
1605
+ const [viewerPayload, imagePayload] = await Promise.all([viewerResult && viewerOptions ? normalizeDiffViewerPayloadLanguages({
1606
+ prerenderedHTML: viewerResult.prerenderedHTML,
1607
+ fileDiff: viewerResult.fileDiff,
1608
+ options: viewerOptions,
1609
+ langs: collectDiffPayloadLanguageHints({ fileDiff: viewerResult.fileDiff })
1610
+ }) : Promise.resolve(void 0), imageResult && imageOptions ? normalizeDiffViewerPayloadLanguages({
1611
+ prerenderedHTML: imageResult.prerenderedHTML,
1612
+ fileDiff: imageResult.fileDiff,
1613
+ options: imageOptions,
1614
+ langs: collectDiffPayloadLanguageHints({ fileDiff: imageResult.fileDiff })
1615
+ }) : Promise.resolve(void 0)]);
1616
+ return buildRenderedSection({
1617
+ ...viewerPayload ? { viewerPayload } : {},
1618
+ ...imagePayload ? { imagePayload } : {}
1619
+ });
1620
+ }))),
1621
+ fileCount: files.length
1622
+ };
1623
+ }
1624
+ async function renderDiffDocument(input, options, target = "both") {
1625
+ const title = buildDiffTitle(input);
1626
+ const rendered = input.kind === "before_after" ? await renderBeforeAfterDiff(input, options, target) : await renderPatchDiff(input, options, target);
1627
+ return {
1628
+ ...rendered.viewerBodyHtml ? { html: buildHtmlDocument({
1629
+ title,
1630
+ bodyHtml: rendered.viewerBodyHtml,
1631
+ theme: options.presentation.theme,
1632
+ imageMaxWidth: options.image.maxWidth,
1633
+ runtimeMode: "viewer"
1634
+ }) } : {},
1635
+ ...rendered.imageBodyHtml ? { imageHtml: buildHtmlDocument({
1636
+ title,
1637
+ bodyHtml: rendered.imageBodyHtml,
1638
+ theme: options.presentation.theme,
1639
+ imageMaxWidth: options.image.maxWidth,
1640
+ runtimeMode: "image"
1641
+ }) } : {},
1642
+ title,
1643
+ fileCount: rendered.fileCount,
1644
+ inputKind: input.kind
1645
+ };
1646
+ }
1647
+ function shouldFallbackToClientHydration(error) {
1648
+ return error instanceof TypeError && error.message.includes("needs an import attribute of \"type: json\"");
1649
+ }
1650
+ async function preloadFileDiffWithFallback(params) {
1651
+ try {
1652
+ return await preloadFileDiff(params);
1653
+ } catch (error) {
1654
+ if (!shouldFallbackToClientHydration(error)) throw error;
1655
+ return {
1656
+ fileDiff: params.fileDiff,
1657
+ prerenderedHTML: ""
1658
+ };
1659
+ }
1660
+ }
1661
+ async function preloadMultiFileDiffWithFallback(params) {
1662
+ try {
1663
+ return await preloadMultiFileDiff(params);
1664
+ } catch (error) {
1665
+ if (!shouldFallbackToClientHydration(error)) throw error;
1666
+ return {
1667
+ oldFile: params.oldFile,
1668
+ newFile: params.newFile,
1669
+ prerenderedHTML: ""
1670
+ };
1671
+ }
1672
+ }
1673
+ //#endregion
1674
+ //#region extensions/diffs/src/tool.ts
1675
+ const MAX_BEFORE_AFTER_BYTES = 512 * 1024;
1676
+ const MAX_PATCH_BYTES = 2 * 1024 * 1024;
1677
+ const MAX_TITLE_BYTES = 1024;
1678
+ const MAX_PATH_BYTES = 2048;
1679
+ const MAX_LANG_BYTES = 128;
1680
+ function stringEnum(values, description, options = {}) {
1681
+ return Type.Unsafe({
1682
+ type: "string",
1683
+ enum: [...values],
1684
+ description,
1685
+ ...options
1686
+ });
1687
+ }
1688
+ const DiffsToolSchema = Type.Object({
1689
+ before: Type.Optional(Type.String({ description: "Original text content." })),
1690
+ after: Type.Optional(Type.String({ description: "Updated text content." })),
1691
+ patch: Type.Optional(Type.String({
1692
+ description: "Unified diff or patch text.",
1693
+ maxLength: MAX_PATCH_BYTES
1694
+ })),
1695
+ path: Type.Optional(Type.String({
1696
+ description: "Display path for before/after input.",
1697
+ maxLength: MAX_PATH_BYTES
1698
+ })),
1699
+ lang: Type.Optional(Type.String({
1700
+ description: "Optional language override for before/after input.",
1701
+ maxLength: MAX_LANG_BYTES
1702
+ })),
1703
+ title: Type.Optional(Type.String({
1704
+ description: "Optional title for the rendered diff.",
1705
+ maxLength: MAX_TITLE_BYTES
1706
+ })),
1707
+ mode: Type.Optional(stringEnum(DIFF_MODES, "Output mode: view, file, image (deprecated alias for file), or both. Default: both.")),
1708
+ theme: Type.Optional(stringEnum(DIFF_THEMES, "Viewer theme. Default: dark.")),
1709
+ layout: Type.Optional(stringEnum(DIFF_LAYOUTS, "Diff layout. Default: unified.")),
1710
+ fileQuality: Type.Optional(stringEnum(DIFF_IMAGE_QUALITY_PRESETS, "File quality preset: standard, hq, or print.")),
1711
+ fileFormat: Type.Optional(stringEnum(DIFF_OUTPUT_FORMATS, "Rendered file format: png or pdf.")),
1712
+ fileScale: Type.Optional(Type.Number({
1713
+ description: "Optional rendered-file device scale factor override (1-4).",
1714
+ minimum: 1,
1715
+ maximum: 4
1716
+ })),
1717
+ fileMaxWidth: Type.Optional(Type.Number({
1718
+ description: "Optional rendered-file max width in CSS pixels (640-2400).",
1719
+ minimum: 640,
1720
+ maximum: 2400
1721
+ })),
1722
+ /** @deprecated Use fileQuality. */
1723
+ imageQuality: Type.Optional(stringEnum(DIFF_IMAGE_QUALITY_PRESETS, "Deprecated alias for fileQuality.", { deprecated: true })),
1724
+ /** @deprecated Use fileFormat. */
1725
+ imageFormat: Type.Optional(stringEnum(DIFF_OUTPUT_FORMATS, "Deprecated alias for fileFormat.", { deprecated: true })),
1726
+ /** @deprecated Use fileScale. */
1727
+ imageScale: Type.Optional(Type.Number({
1728
+ description: "Deprecated alias for fileScale.",
1729
+ deprecated: true,
1730
+ minimum: 1,
1731
+ maximum: 4
1732
+ })),
1733
+ /** @deprecated Use fileMaxWidth. */
1734
+ imageMaxWidth: Type.Optional(Type.Number({
1735
+ description: "Deprecated alias for fileMaxWidth.",
1736
+ deprecated: true,
1737
+ minimum: 640,
1738
+ maximum: 2400
1739
+ })),
1740
+ expandUnchanged: Type.Optional(Type.Boolean({ description: "Expand unchanged sections instead of collapsing them." })),
1741
+ ttlSeconds: Type.Optional(Type.Number({
1742
+ description: "Artifact lifetime in seconds. Default: 1800. Maximum: 21600.",
1743
+ minimum: 1,
1744
+ maximum: 21600
1745
+ })),
1746
+ baseUrl: Type.Optional(Type.String({ description: "Optional gateway base URL override used when building the viewer URL. Overrides configured viewerBaseUrl, for example https://gateway.example.com." }))
1747
+ }, { additionalProperties: false });
1748
+ function createDiffsTool(params) {
1749
+ return {
1750
+ name: "diffs",
1751
+ label: "Diffs",
1752
+ description: "Create a read-only diff viewer from before/after text or a unified patch. Returns a gateway viewer URL for canvas use and can also render the same diff to a PNG or PDF.",
1753
+ parameters: DiffsToolSchema,
1754
+ execute: async (_toolCallId, rawParams) => {
1755
+ const toolParams = rawParams;
1756
+ const artifactContext = buildArtifactContext(params.context);
1757
+ const input = normalizeDiffInput(toolParams);
1758
+ const mode = normalizeMode(toolParams.mode, params.defaults.mode);
1759
+ const theme = normalizeTheme(toolParams.theme, params.defaults.theme);
1760
+ const layout = normalizeLayout(toolParams.layout, params.defaults.layout);
1761
+ const expandUnchanged = toolParams.expandUnchanged === true;
1762
+ const ttlMs = normalizeTtlMs(toolParams.ttlSeconds);
1763
+ const image = resolveDiffImageRenderOptions({
1764
+ defaults: params.defaults,
1765
+ fileFormat: normalizeOutputFormat(toolParams.fileFormat ?? toolParams.imageFormat ?? toolParams.format),
1766
+ fileQuality: normalizeFileQuality(toolParams.fileQuality ?? toolParams.imageQuality),
1767
+ fileScale: toolParams.fileScale ?? toolParams.imageScale,
1768
+ fileMaxWidth: toolParams.fileMaxWidth ?? toolParams.imageMaxWidth
1769
+ });
1770
+ const renderTarget = resolveRenderTarget(mode);
1771
+ const rendered = await renderDiffDocument(input, {
1772
+ presentation: {
1773
+ ...params.defaults,
1774
+ layout,
1775
+ theme
1776
+ },
1777
+ image,
1778
+ expandUnchanged
1779
+ }, renderTarget);
1780
+ const screenshotter = params.screenshotter ?? new PlaywrightDiffScreenshotter({ config: params.api.config });
1781
+ if (isArtifactOnlyMode(mode)) {
1782
+ const artifactFile = await renderDiffArtifactFile({
1783
+ screenshotter,
1784
+ store: params.store,
1785
+ html: requireRenderedHtml(rendered.imageHtml, "image"),
1786
+ theme,
1787
+ image,
1788
+ ttlMs,
1789
+ context: artifactContext
1790
+ });
1791
+ return {
1792
+ content: [{
1793
+ type: "text",
1794
+ text: buildFileArtifactMessage({
1795
+ format: image.format,
1796
+ filePath: artifactFile.path
1797
+ })
1798
+ }],
1799
+ details: buildArtifactDetails({
1800
+ baseDetails: {
1801
+ ...artifactFile.artifactId ? { artifactId: artifactFile.artifactId } : {},
1802
+ ...artifactFile.expiresAt ? { expiresAt: artifactFile.expiresAt } : {},
1803
+ title: rendered.title,
1804
+ inputKind: rendered.inputKind,
1805
+ fileCount: rendered.fileCount,
1806
+ mode,
1807
+ ...artifactContext ? { context: artifactContext } : {}
1808
+ },
1809
+ artifactFile,
1810
+ image
1811
+ })
1812
+ };
1813
+ }
1814
+ const artifact = await params.store.createArtifact({
1815
+ html: requireRenderedHtml(rendered.html, "viewer"),
1816
+ title: rendered.title,
1817
+ inputKind: rendered.inputKind,
1818
+ fileCount: rendered.fileCount,
1819
+ ttlMs,
1820
+ context: artifactContext
1821
+ });
1822
+ const viewerUrl = buildViewerUrl({
1823
+ config: params.api.config,
1824
+ viewerPath: artifact.viewerPath,
1825
+ baseUrl: normalizeBaseUrl(toolParams.baseUrl) ?? params.viewerBaseUrl
1826
+ });
1827
+ const baseDetails = {
1828
+ artifactId: artifact.id,
1829
+ viewerUrl,
1830
+ viewerPath: artifact.viewerPath,
1831
+ title: artifact.title,
1832
+ expiresAt: artifact.expiresAt,
1833
+ inputKind: artifact.inputKind,
1834
+ fileCount: artifact.fileCount,
1835
+ mode,
1836
+ ...artifactContext ? { context: artifactContext } : {}
1837
+ };
1838
+ if (mode === "view") return {
1839
+ content: [{
1840
+ type: "text",
1841
+ text: `Diff viewer ready.\n${viewerUrl}`
1842
+ }],
1843
+ details: baseDetails
1844
+ };
1845
+ try {
1846
+ const artifactFile = await renderDiffArtifactFile({
1847
+ screenshotter,
1848
+ store: params.store,
1849
+ artifactId: artifact.id,
1850
+ html: requireRenderedHtml(rendered.imageHtml, "image"),
1851
+ theme,
1852
+ image
1853
+ });
1854
+ await params.store.updateFilePath(artifact.id, artifactFile.path);
1855
+ return {
1856
+ content: [{
1857
+ type: "text",
1858
+ text: buildFileArtifactMessage({
1859
+ format: image.format,
1860
+ filePath: artifactFile.path,
1861
+ viewerUrl
1862
+ })
1863
+ }],
1864
+ details: buildArtifactDetails({
1865
+ baseDetails,
1866
+ artifactFile,
1867
+ image
1868
+ })
1869
+ };
1870
+ } catch (error) {
1871
+ if (mode === "both") {
1872
+ const errorMessage = formatErrorMessage(error);
1873
+ return {
1874
+ content: [{
1875
+ type: "text",
1876
+ text: `Diff viewer ready.\n${viewerUrl}\nFile rendering failed: ${errorMessage}`
1877
+ }],
1878
+ details: {
1879
+ ...baseDetails,
1880
+ fileError: errorMessage,
1881
+ imageError: errorMessage
1882
+ }
1883
+ };
1884
+ }
1885
+ throw error;
1886
+ }
1887
+ }
1888
+ };
1889
+ }
1890
+ function normalizeFileQuality(fileQuality) {
1891
+ return fileQuality && DIFF_IMAGE_QUALITY_PRESETS.includes(fileQuality) ? fileQuality : void 0;
1892
+ }
1893
+ function normalizeOutputFormat(format) {
1894
+ return format && DIFF_OUTPUT_FORMATS.includes(format) ? format : void 0;
1895
+ }
1896
+ function isArtifactOnlyMode(mode) {
1897
+ return mode === "image" || mode === "file";
1898
+ }
1899
+ function resolveRenderTarget(mode) {
1900
+ if (mode === "view") return "viewer";
1901
+ if (isArtifactOnlyMode(mode)) return "image";
1902
+ return "both";
1903
+ }
1904
+ function requireRenderedHtml(html, target) {
1905
+ if (html !== void 0) return html;
1906
+ throw new Error(`Missing ${target} render output.`);
1907
+ }
1908
+ function buildArtifactDetails(params) {
1909
+ return {
1910
+ ...params.baseDetails,
1911
+ filePath: params.artifactFile.path,
1912
+ imagePath: params.artifactFile.path,
1913
+ path: params.artifactFile.path,
1914
+ fileBytes: params.artifactFile.bytes,
1915
+ imageBytes: params.artifactFile.bytes,
1916
+ format: params.image.format,
1917
+ fileFormat: params.image.format,
1918
+ fileQuality: params.image.qualityPreset,
1919
+ imageQuality: params.image.qualityPreset,
1920
+ fileScale: params.image.scale,
1921
+ imageScale: params.image.scale,
1922
+ fileMaxWidth: params.image.maxWidth,
1923
+ imageMaxWidth: params.image.maxWidth
1924
+ };
1925
+ }
1926
+ function buildFileArtifactMessage(params) {
1927
+ const lines = params.viewerUrl ? [`Diff viewer: ${params.viewerUrl}`] : [];
1928
+ lines.push(`Diff ${params.format.toUpperCase()} generated at: ${params.filePath}`);
1929
+ lines.push("Use the `message` tool with `path` or `filePath` to send this file.");
1930
+ return lines.join("\n");
1931
+ }
1932
+ async function renderDiffArtifactFile(params) {
1933
+ const standaloneArtifact = params.artifactId ? void 0 : await params.store.createStandaloneFileArtifact({
1934
+ format: params.image.format,
1935
+ ttlMs: params.ttlMs,
1936
+ context: params.context
1937
+ });
1938
+ const outputPath = params.artifactId ? params.store.allocateFilePath(params.artifactId, params.image.format) : standaloneArtifact.filePath;
1939
+ await params.screenshotter.screenshotHtml({
1940
+ html: params.html,
1941
+ outputPath,
1942
+ theme: params.theme,
1943
+ image: params.image
1944
+ });
1945
+ return {
1946
+ path: outputPath,
1947
+ bytes: (await fs.stat(outputPath)).size,
1948
+ ...standaloneArtifact?.id ? { artifactId: standaloneArtifact.id } : {},
1949
+ ...standaloneArtifact?.expiresAt ? { expiresAt: standaloneArtifact.expiresAt } : {}
1950
+ };
1951
+ }
1952
+ function buildArtifactContext(context) {
1953
+ if (!context) return;
1954
+ const artifactContext = {
1955
+ agentId: normalizeOptionalString(context.agentId),
1956
+ sessionId: normalizeOptionalString(context.sessionId),
1957
+ messageChannel: normalizeOptionalString(context.messageChannel),
1958
+ agentAccountId: normalizeOptionalString(context.agentAccountId)
1959
+ };
1960
+ return Object.values(artifactContext).some((value) => value !== void 0) ? artifactContext : void 0;
1961
+ }
1962
+ function normalizeDiffInput(params) {
1963
+ const patch = params.patch?.trim();
1964
+ const before = params.before;
1965
+ const after = params.after;
1966
+ if (patch) {
1967
+ assertMaxBytes(patch, "patch", MAX_PATCH_BYTES);
1968
+ if (before !== void 0 || after !== void 0) throw new PluginToolInputError("Provide either patch or before/after input, not both.");
1969
+ const title = params.title?.trim();
1970
+ if (title) assertMaxBytes(title, "title", MAX_TITLE_BYTES);
1971
+ return {
1972
+ kind: "patch",
1973
+ patch,
1974
+ title
1975
+ };
1976
+ }
1977
+ if (before === void 0 || after === void 0) throw new PluginToolInputError("Provide patch or both before and after text.");
1978
+ assertMaxBytes(before, "before", MAX_BEFORE_AFTER_BYTES);
1979
+ assertMaxBytes(after, "after", MAX_BEFORE_AFTER_BYTES);
1980
+ const path = normalizeOptionalString(params.path);
1981
+ const lang = normalizeOptionalString(params.lang);
1982
+ const title = normalizeOptionalString(params.title);
1983
+ if (path) assertMaxBytes(path, "path", MAX_PATH_BYTES);
1984
+ if (lang) assertMaxBytes(lang, "lang", MAX_LANG_BYTES);
1985
+ if (title) assertMaxBytes(title, "title", MAX_TITLE_BYTES);
1986
+ return {
1987
+ kind: "before_after",
1988
+ before,
1989
+ after,
1990
+ path,
1991
+ lang,
1992
+ title
1993
+ };
1994
+ }
1995
+ function assertMaxBytes(value, label, maxBytes) {
1996
+ if (Buffer.byteLength(value, "utf8") <= maxBytes) return;
1997
+ throw new PluginToolInputError(`${label} exceeds maximum size (${maxBytes} bytes).`);
1998
+ }
1999
+ function normalizeBaseUrl(baseUrl) {
2000
+ const normalized = baseUrl?.trim();
2001
+ if (!normalized) return;
2002
+ try {
2003
+ return normalizeViewerBaseUrl(normalized);
2004
+ } catch {
2005
+ throw new PluginToolInputError(`Invalid baseUrl: ${normalized}`);
2006
+ }
2007
+ }
2008
+ function normalizeMode(mode, fallback) {
2009
+ return mode && DIFF_MODES.includes(mode) ? mode : fallback;
2010
+ }
2011
+ function normalizeTheme(theme, fallback) {
2012
+ return theme && DIFF_THEMES.includes(theme) ? theme : fallback;
2013
+ }
2014
+ function normalizeLayout(layout, fallback) {
2015
+ return layout && DIFF_LAYOUTS.includes(layout) ? layout : fallback;
2016
+ }
2017
+ function normalizeTtlMs(ttlSeconds) {
2018
+ if (!Number.isFinite(ttlSeconds) || ttlSeconds === void 0) return;
2019
+ return Math.floor(ttlSeconds * 1e3);
2020
+ }
2021
+ var PluginToolInputError = class extends Error {
2022
+ constructor(message) {
2023
+ super(message);
2024
+ this.name = "ToolInputError";
2025
+ }
2026
+ };
2027
+ //#endregion
2028
+ //#region extensions/diffs/src/plugin.ts
2029
+ function registerDiffsPlugin(api) {
2030
+ const store = new DiffArtifactStore({
2031
+ rootDir: path.join(resolvePreferredOpenClawTmpDir(), "openclaw-diffs"),
2032
+ logger: api.logger
2033
+ });
2034
+ const resolveCurrentPluginConfig = () => resolveLivePluginConfigObject(api.runtime.config?.current ? () => api.runtime.config.current() : void 0, "diffs", api.pluginConfig) ?? {};
2035
+ const resolveCurrentAccessConfig = () => {
2036
+ const currentConfig = api.runtime.config?.current?.() ?? api.config;
2037
+ return {
2038
+ allowRemoteViewer: resolveDiffsPluginSecurity(resolveCurrentPluginConfig()).allowRemoteViewer,
2039
+ trustedProxies: currentConfig.gateway?.trustedProxies,
2040
+ allowRealIpFallback: currentConfig.gateway?.allowRealIpFallback === true
2041
+ };
2042
+ };
2043
+ const initialAccessConfig = resolveCurrentAccessConfig();
2044
+ api.registerTool((ctx) => {
2045
+ const pluginConfig = resolveCurrentPluginConfig();
2046
+ return createDiffsTool({
2047
+ api,
2048
+ store,
2049
+ defaults: resolveDiffsPluginDefaults(pluginConfig),
2050
+ viewerBaseUrl: resolveDiffsPluginViewerBaseUrl(pluginConfig),
2051
+ context: ctx
2052
+ });
2053
+ }, { name: "diffs" });
2054
+ api.registerHttpRoute({
2055
+ path: "/plugins/diffs",
2056
+ auth: "plugin",
2057
+ match: "prefix",
2058
+ handler: createDiffsHttpHandler({
2059
+ store,
2060
+ logger: api.logger,
2061
+ allowRemoteViewer: initialAccessConfig.allowRemoteViewer,
2062
+ trustedProxies: initialAccessConfig.trustedProxies,
2063
+ allowRealIpFallback: initialAccessConfig.allowRealIpFallback,
2064
+ resolveAccessConfig: resolveCurrentAccessConfig
2065
+ })
2066
+ });
2067
+ api.on("before_prompt_build", async () => ({ prependSystemContext: DIFFS_AGENT_GUIDANCE }));
2068
+ }
2069
+ //#endregion
2070
+ //#region extensions/diffs/index.ts
2071
+ var diffs_default = definePluginEntry({
2072
+ id: "diffs",
2073
+ name: "Diffs",
2074
+ description: "Read-only diff viewer and PNG/PDF renderer for agents.",
2075
+ configSchema: diffsPluginConfigSchema,
2076
+ register: registerDiffsPlugin
2077
+ });
2078
+ //#endregion
2079
+ export { diffs_default as default };