mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
diffs plugin
This commit is contained in:
124
extensions/diffs/README.md
Normal file
124
extensions/diffs/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# @openclaw/diffs
|
||||
|
||||
Read-only diff viewer plugin for **OpenClaw** agents.
|
||||
|
||||
It gives agents one tool, `diffs`, that can:
|
||||
|
||||
- render a gateway-hosted diff viewer for canvas use
|
||||
- render the same diff to a PNG image
|
||||
- accept either arbitrary `before`/`after` text or a unified patch
|
||||
|
||||
## What Agents Get
|
||||
|
||||
The tool can return:
|
||||
|
||||
- `details.viewerUrl`: a gateway URL that can be opened in the canvas
|
||||
- `details.imagePath`: a local PNG artifact when image rendering is requested
|
||||
|
||||
This means an agent can:
|
||||
|
||||
- call `diffs` with `mode=view`, then pass `details.viewerUrl` to `canvas present`
|
||||
- call `diffs` with `mode=image`, then send the PNG through the normal `message` tool using `path` or `filePath`
|
||||
- call `diffs` with `mode=both` when it wants both outputs
|
||||
|
||||
## Tool Inputs
|
||||
|
||||
Before/after:
|
||||
|
||||
```json
|
||||
{
|
||||
"before": "# Hello\n\nOne",
|
||||
"after": "# Hello\n\nTwo",
|
||||
"path": "docs/example.md",
|
||||
"mode": "view"
|
||||
}
|
||||
```
|
||||
|
||||
Patch:
|
||||
|
||||
```json
|
||||
{
|
||||
"patch": "diff --git a/src/example.ts b/src/example.ts\n--- a/src/example.ts\n+++ b/src/example.ts\n@@ -1 +1 @@\n-const x = 1;\n+const x = 2;\n",
|
||||
"mode": "both"
|
||||
}
|
||||
```
|
||||
|
||||
Useful options:
|
||||
|
||||
- `mode`: `view`, `image`, or `both`
|
||||
- `layout`: `unified` or `split`
|
||||
- `theme`: `light` or `dark` (default: `dark`)
|
||||
- `expandUnchanged`: expand unchanged sections
|
||||
- `path`: display name for before/after input
|
||||
- `title`: explicit viewer title
|
||||
- `ttlSeconds`: artifact lifetime
|
||||
- `baseUrl`: override the gateway base URL used in the returned viewer link
|
||||
|
||||
## Example Agent Prompts
|
||||
|
||||
Open in canvas:
|
||||
|
||||
```text
|
||||
Use the `diffs` tool in `view` mode for this before/after content, then open the returned viewer URL in the canvas.
|
||||
|
||||
Path: docs/example.md
|
||||
|
||||
Before:
|
||||
# Hello
|
||||
|
||||
This is version one.
|
||||
|
||||
After:
|
||||
# Hello
|
||||
|
||||
This is version two.
|
||||
```
|
||||
|
||||
Render a PNG:
|
||||
|
||||
```text
|
||||
Use the `diffs` tool in `image` mode for this before/after input. After it returns `details.imagePath`, use the `message` tool with `path` or `filePath` to send me the rendered diff image.
|
||||
|
||||
Path: README.md
|
||||
|
||||
Before:
|
||||
OpenClaw supports plugins.
|
||||
|
||||
After:
|
||||
OpenClaw supports plugins and hosted diff views.
|
||||
```
|
||||
|
||||
Do both:
|
||||
|
||||
```text
|
||||
Use the `diffs` tool in `both` mode for this diff. Open the viewer in the canvas and then send the rendered PNG by passing `details.imagePath` to the `message` tool.
|
||||
|
||||
Path: src/demo.ts
|
||||
|
||||
Before:
|
||||
const status = "old";
|
||||
|
||||
After:
|
||||
const status = "new";
|
||||
```
|
||||
|
||||
Patch input:
|
||||
|
||||
```text
|
||||
Use the `diffs` tool with this unified patch in `view` mode. After it returns the viewer URL, present it in the canvas.
|
||||
|
||||
diff --git a/src/example.ts b/src/example.ts
|
||||
--- a/src/example.ts
|
||||
+++ b/src/example.ts
|
||||
@@ -1,3 +1,3 @@
|
||||
export function add(a: number, b: number) {
|
||||
- return a + b;
|
||||
+ return a + b + 1;
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The viewer is hosted locally through the gateway under `/plugins/diffs/...`.
|
||||
- Artifacts are ephemeral and stored in the local temp directory.
|
||||
- PNG rendering requires a Chromium-compatible browser. Set `browser.executablePath` if auto-detection is not enough.
|
||||
1309
extensions/diffs/assets/viewer-runtime.js
Normal file
1309
extensions/diffs/assets/viewer-runtime.js
Normal file
File diff suppressed because one or more lines are too long
43
extensions/diffs/index.test.ts
Normal file
43
extensions/diffs/index.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import plugin from "./index.js";
|
||||
|
||||
describe("diffs plugin registration", () => {
|
||||
it("registers the tool, http handler, and prompt guidance hook", () => {
|
||||
const registerTool = vi.fn();
|
||||
const registerHttpHandler = vi.fn();
|
||||
const on = vi.fn();
|
||||
|
||||
plugin.register?.({
|
||||
id: "diffs",
|
||||
name: "Diffs",
|
||||
description: "Diffs",
|
||||
source: "test",
|
||||
config: {},
|
||||
runtime: {} as never,
|
||||
logger: {
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
},
|
||||
registerTool,
|
||||
registerHook() {},
|
||||
registerHttpHandler,
|
||||
registerHttpRoute() {},
|
||||
registerChannel() {},
|
||||
registerGatewayMethod() {},
|
||||
registerCli() {},
|
||||
registerService() {},
|
||||
registerProvider() {},
|
||||
registerCommand() {},
|
||||
resolvePath(input: string) {
|
||||
return input;
|
||||
},
|
||||
on,
|
||||
});
|
||||
|
||||
expect(registerTool).toHaveBeenCalledTimes(1);
|
||||
expect(registerHttpHandler).toHaveBeenCalledTimes(1);
|
||||
expect(on).toHaveBeenCalledTimes(1);
|
||||
expect(on.mock.calls[0]?.[0]).toBe("before_prompt_build");
|
||||
});
|
||||
});
|
||||
28
extensions/diffs/index.ts
Normal file
28
extensions/diffs/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import path from "node:path";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { emptyPluginConfigSchema, resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk";
|
||||
import { createDiffsHttpHandler } from "./src/http.js";
|
||||
import { DIFFS_AGENT_GUIDANCE } from "./src/prompt-guidance.js";
|
||||
import { DiffArtifactStore } from "./src/store.js";
|
||||
import { createDiffsTool } from "./src/tool.js";
|
||||
|
||||
const plugin = {
|
||||
id: "diffs",
|
||||
name: "Diffs",
|
||||
description: "Read-only diff viewer and PNG renderer for agents.",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api: OpenClawPluginApi) {
|
||||
const store = new DiffArtifactStore({
|
||||
rootDir: path.join(resolvePreferredOpenClawTmpDir(), "openclaw-diffs"),
|
||||
logger: api.logger,
|
||||
});
|
||||
|
||||
api.registerTool(createDiffsTool({ api, store }));
|
||||
api.registerHttpHandler(createDiffsHttpHandler({ store, logger: api.logger }));
|
||||
api.on("before_prompt_build", async () => ({
|
||||
prependContext: DIFFS_AGENT_GUIDANCE,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
10
extensions/diffs/openclaw.plugin.json
Normal file
10
extensions/diffs/openclaw.plugin.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "diffs",
|
||||
"name": "Diffs",
|
||||
"description": "Read-only diff viewer and image renderer for agents.",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
20
extensions/diffs/package.json
Normal file
20
extensions/diffs/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@openclaw/diffs",
|
||||
"version": "2026.2.27",
|
||||
"private": true,
|
||||
"description": "OpenClaw diff viewer plugin",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:viewer": "bun build src/viewer-client.ts --target browser --format esm --minify --outfile assets/viewer-runtime.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pierre/diffs": "1.0.11",
|
||||
"@sinclair/typebox": "0.34.48",
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
261
extensions/diffs/src/browser.ts
Normal file
261
extensions/diffs/src/browser.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { chromium } from "playwright-core";
|
||||
import type { DiffTheme } from "./types.js";
|
||||
import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js";
|
||||
|
||||
export type DiffScreenshotter = {
|
||||
screenshotHtml(params: { html: string; outputPath: string; theme: DiffTheme }): Promise<string>;
|
||||
};
|
||||
|
||||
export class PlaywrightDiffScreenshotter implements DiffScreenshotter {
|
||||
private readonly config: OpenClawConfig;
|
||||
|
||||
constructor(params: { config: OpenClawConfig }) {
|
||||
this.config = params.config;
|
||||
}
|
||||
|
||||
async screenshotHtml(params: {
|
||||
html: string;
|
||||
outputPath: string;
|
||||
theme: DiffTheme;
|
||||
}): Promise<string> {
|
||||
await fs.mkdir(path.dirname(params.outputPath), { recursive: true });
|
||||
const executablePath = await resolveBrowserExecutablePath(this.config);
|
||||
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined;
|
||||
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
...(executablePath ? { executablePath } : {}),
|
||||
args: ["--disable-dev-shm-usage", "--disable-gpu"],
|
||||
});
|
||||
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1200, height: 900 },
|
||||
colorScheme: params.theme,
|
||||
});
|
||||
await page.route(`http://127.0.0.1${VIEWER_ASSET_PREFIX}*`, async (route) => {
|
||||
const pathname = new URL(route.request().url()).pathname;
|
||||
const asset = await getServedViewerAsset(pathname);
|
||||
if (!asset) {
|
||||
await route.abort();
|
||||
return;
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: asset.contentType,
|
||||
body: asset.body,
|
||||
});
|
||||
});
|
||||
await page.setContent(injectBaseHref(params.html), { waitUntil: "load" });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
if (document.documentElement.dataset.openclawDiffsReady === "true") {
|
||||
return true;
|
||||
}
|
||||
return [...document.querySelectorAll("[data-openclaw-diff-host]")].every((element) => {
|
||||
return (
|
||||
element instanceof HTMLElement && element.shadowRoot?.querySelector("[data-diffs]")
|
||||
);
|
||||
});
|
||||
},
|
||||
{
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
const frame = document.querySelector(".oc-frame");
|
||||
if (frame instanceof HTMLElement) {
|
||||
frame.dataset.renderMode = "image";
|
||||
}
|
||||
});
|
||||
|
||||
const frame = page.locator(".oc-frame");
|
||||
await frame.waitFor();
|
||||
const initialBox = await frame.boundingBox();
|
||||
if (!initialBox) {
|
||||
throw new Error("Diff frame did not render.");
|
||||
}
|
||||
|
||||
const padding = 20;
|
||||
const clipWidth = Math.ceil(initialBox.width + padding * 2);
|
||||
const clipHeight = Math.ceil(Math.max(initialBox.height + padding * 2, 320));
|
||||
await page.setViewportSize({
|
||||
width: Math.max(clipWidth + padding, 900),
|
||||
height: Math.max(clipHeight + padding, 700),
|
||||
});
|
||||
|
||||
const box = await frame.boundingBox();
|
||||
if (!box) {
|
||||
throw new Error("Diff frame was lost after resizing.");
|
||||
}
|
||||
|
||||
await page.screenshot({
|
||||
path: params.outputPath,
|
||||
type: "png",
|
||||
clip: {
|
||||
x: Math.max(box.x - padding, 0),
|
||||
y: Math.max(box.y - padding, 0),
|
||||
width: clipWidth,
|
||||
height: clipHeight,
|
||||
},
|
||||
});
|
||||
return params.outputPath;
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(
|
||||
`Diff image rendering requires a Chromium-compatible browser. Set browser.executablePath or install Chrome/Chromium. ${reason}`,
|
||||
);
|
||||
} finally {
|
||||
await browser?.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function injectBaseHref(html: string): string {
|
||||
if (html.includes("<base ")) {
|
||||
return html;
|
||||
}
|
||||
return html.replace("<head>", '<head><base href="http://127.0.0.1/" />');
|
||||
}
|
||||
|
||||
async function resolveBrowserExecutablePath(config: OpenClawConfig): Promise<string | undefined> {
|
||||
const configPath = config.browser?.executablePath?.trim();
|
||||
if (configPath) {
|
||||
await assertExecutable(configPath, "browser.executablePath");
|
||||
return configPath;
|
||||
}
|
||||
|
||||
const envCandidates = [
|
||||
process.env.OPENCLAW_BROWSER_EXECUTABLE_PATH,
|
||||
process.env.BROWSER_EXECUTABLE_PATH,
|
||||
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
|
||||
]
|
||||
.map((value) => value?.trim())
|
||||
.filter((value): value is string => Boolean(value));
|
||||
|
||||
for (const candidate of envCandidates) {
|
||||
if (await isExecutable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidate of await collectExecutableCandidates()) {
|
||||
if (await isExecutable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function collectExecutableCandidates(): Promise<string[]> {
|
||||
const candidates = new Set<string>();
|
||||
|
||||
for (const command of pathCommandsForPlatform()) {
|
||||
const resolved = await findExecutableInPath(command);
|
||||
if (resolved) {
|
||||
candidates.add(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidate of commonExecutablePathsForPlatform()) {
|
||||
candidates.add(candidate);
|
||||
}
|
||||
|
||||
return [...candidates];
|
||||
}
|
||||
|
||||
function pathCommandsForPlatform(): string[] {
|
||||
if (process.platform === "win32") {
|
||||
return ["chrome.exe", "msedge.exe", "brave.exe"];
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
return ["google-chrome", "chromium", "msedge", "brave-browser", "brave"];
|
||||
}
|
||||
return [
|
||||
"chromium",
|
||||
"chromium-browser",
|
||||
"google-chrome",
|
||||
"google-chrome-stable",
|
||||
"msedge",
|
||||
"brave-browser",
|
||||
"brave",
|
||||
];
|
||||
}
|
||||
|
||||
function commonExecutablePathsForPlatform(): string[] {
|
||||
if (process.platform === "darwin") {
|
||||
return [
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
||||
];
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
const localAppData = process.env.LOCALAPPDATA ?? "";
|
||||
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
|
||||
const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
||||
return [
|
||||
path.join(localAppData, "Google", "Chrome", "Application", "chrome.exe"),
|
||||
path.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
|
||||
path.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
|
||||
path.join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||
path.join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||
path.join(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
||||
path.join(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
"/usr/bin/msedge",
|
||||
"/usr/bin/brave-browser",
|
||||
"/snap/bin/chromium",
|
||||
];
|
||||
}
|
||||
|
||||
async function findExecutableInPath(command: string): Promise<string | undefined> {
|
||||
const pathValue = process.env.PATH;
|
||||
if (!pathValue) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const directory of pathValue.split(path.delimiter)) {
|
||||
if (!directory) {
|
||||
continue;
|
||||
}
|
||||
const candidate = path.join(directory, command);
|
||||
if (await isExecutable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function assertExecutable(candidate: string, label: string): Promise<void> {
|
||||
if (!(await isExecutable(candidate))) {
|
||||
throw new Error(`${label} not found or not executable: ${candidate}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function isExecutable(candidate: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(candidate, fsConstants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
115
extensions/diffs/src/http.test.ts
Normal file
115
extensions/diffs/src/http.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import fs from "node:fs/promises";
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { createMockServerResponse } from "../../../src/test-utils/mock-http-response.js";
|
||||
import { createDiffsHttpHandler } from "./http.js";
|
||||
import { DiffArtifactStore } from "./store.js";
|
||||
|
||||
describe("createDiffsHttpHandler", () => {
|
||||
let rootDir: string;
|
||||
let store: DiffArtifactStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-diffs-http-"));
|
||||
store = new DiffArtifactStore({ rootDir });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("serves a stored diff document", async () => {
|
||||
const artifact = await store.createArtifact({
|
||||
html: "<html>viewer</html>",
|
||||
title: "Demo",
|
||||
inputKind: "before_after",
|
||||
fileCount: 1,
|
||||
});
|
||||
|
||||
const handler = createDiffsHttpHandler({ store });
|
||||
const res = createMockServerResponse();
|
||||
const handled = await handler(
|
||||
{
|
||||
method: "GET",
|
||||
url: artifact.viewerPath,
|
||||
} as IncomingMessage,
|
||||
res,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toBe("<html>viewer</html>");
|
||||
expect(res.getHeader("content-security-policy")).toContain("default-src 'none'");
|
||||
});
|
||||
|
||||
it("rejects invalid tokens", async () => {
|
||||
const artifact = await store.createArtifact({
|
||||
html: "<html>viewer</html>",
|
||||
title: "Demo",
|
||||
inputKind: "before_after",
|
||||
fileCount: 1,
|
||||
});
|
||||
|
||||
const handler = createDiffsHttpHandler({ store });
|
||||
const res = createMockServerResponse();
|
||||
const handled = await handler(
|
||||
{
|
||||
method: "GET",
|
||||
url: artifact.viewerPath.replace(artifact.token, "bad-token"),
|
||||
} as IncomingMessage,
|
||||
res,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it("rejects malformed artifact ids before reading from disk", async () => {
|
||||
const handler = createDiffsHttpHandler({ store });
|
||||
const res = createMockServerResponse();
|
||||
const handled = await handler(
|
||||
{
|
||||
method: "GET",
|
||||
url: "/plugins/diffs/view/not-a-real-id/not-a-real-token",
|
||||
} as IncomingMessage,
|
||||
res,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it("serves the shared viewer asset", async () => {
|
||||
const handler = createDiffsHttpHandler({ store });
|
||||
const res = createMockServerResponse();
|
||||
const handled = await handler(
|
||||
{
|
||||
method: "GET",
|
||||
url: "/plugins/diffs/assets/viewer.js",
|
||||
} as IncomingMessage,
|
||||
res,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(String(res.body)).toContain("/plugins/diffs/assets/viewer-runtime.js?v=");
|
||||
});
|
||||
|
||||
it("serves the shared viewer runtime asset", async () => {
|
||||
const handler = createDiffsHttpHandler({ store });
|
||||
const res = createMockServerResponse();
|
||||
const handled = await handler(
|
||||
{
|
||||
method: "GET",
|
||||
url: "/plugins/diffs/assets/viewer-runtime.js",
|
||||
} as IncomingMessage,
|
||||
res,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(String(res.body)).toContain("openclawDiffsReady");
|
||||
});
|
||||
});
|
||||
136
extensions/diffs/src/http.ts
Normal file
136
extensions/diffs/src/http.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { PluginLogger } from "openclaw/plugin-sdk";
|
||||
import type { DiffArtifactStore } from "./store.js";
|
||||
import { DIFF_ARTIFACT_ID_PATTERN, DIFF_ARTIFACT_TOKEN_PATTERN } from "./types.js";
|
||||
import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js";
|
||||
|
||||
const VIEW_PREFIX = "/plugins/diffs/view/";
|
||||
const VIEWER_CONTENT_SECURITY_POLICY = [
|
||||
"default-src 'none'",
|
||||
"script-src 'self'",
|
||||
"style-src 'unsafe-inline'",
|
||||
"img-src 'self' data:",
|
||||
"font-src 'self' data:",
|
||||
"connect-src 'none'",
|
||||
"base-uri 'none'",
|
||||
"frame-ancestors 'self'",
|
||||
"object-src 'none'",
|
||||
].join("; ");
|
||||
|
||||
export function createDiffsHttpHandler(params: {
|
||||
store: DiffArtifactStore;
|
||||
logger?: PluginLogger;
|
||||
}) {
|
||||
return async (req: IncomingMessage, res: ServerResponse): Promise<boolean> => {
|
||||
const parsed = parseRequestUrl(req.url);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parsed.pathname.startsWith(VIEWER_ASSET_PREFIX)) {
|
||||
return await serveAsset(req, res, parsed.pathname, params.logger);
|
||||
}
|
||||
|
||||
if (!parsed.pathname.startsWith(VIEW_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
respondText(res, 405, "Method not allowed");
|
||||
return true;
|
||||
}
|
||||
|
||||
const pathParts = parsed.pathname.split("/").filter(Boolean);
|
||||
const id = pathParts[3];
|
||||
const token = pathParts[4];
|
||||
if (
|
||||
!id ||
|
||||
!token ||
|
||||
!DIFF_ARTIFACT_ID_PATTERN.test(id) ||
|
||||
!DIFF_ARTIFACT_TOKEN_PATTERN.test(token)
|
||||
) {
|
||||
respondText(res, 404, "Diff not found");
|
||||
return true;
|
||||
}
|
||||
|
||||
const artifact = await params.store.getArtifact(id, token);
|
||||
if (!artifact) {
|
||||
respondText(res, 404, "Diff not found or expired");
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const html = await params.store.readHtml(id);
|
||||
res.statusCode = 200;
|
||||
setSharedHeaders(res, "text/html; charset=utf-8");
|
||||
res.setHeader("content-security-policy", VIEWER_CONTENT_SECURITY_POLICY);
|
||||
if (req.method === "HEAD") {
|
||||
res.end();
|
||||
} else {
|
||||
res.end(html);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
params.logger?.warn(`Failed to serve diff artifact ${id}: ${String(error)}`);
|
||||
respondText(res, 500, "Failed to load diff");
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function parseRequestUrl(rawUrl?: string): URL | null {
|
||||
if (!rawUrl) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new URL(rawUrl, "http://127.0.0.1");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function serveAsset(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
pathname: string,
|
||||
logger?: PluginLogger,
|
||||
): Promise<boolean> {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
respondText(res, 405, "Method not allowed");
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const asset = await getServedViewerAsset(pathname);
|
||||
if (!asset) {
|
||||
respondText(res, 404, "Asset not found");
|
||||
return true;
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
setSharedHeaders(res, asset.contentType);
|
||||
if (req.method === "HEAD") {
|
||||
res.end();
|
||||
} else {
|
||||
res.end(asset.body);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger?.warn(`Failed to serve diffs asset ${pathname}: ${String(error)}`);
|
||||
respondText(res, 500, "Failed to load asset");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function respondText(res: ServerResponse, statusCode: number, body: string): void {
|
||||
res.statusCode = statusCode;
|
||||
setSharedHeaders(res, "text/plain; charset=utf-8");
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function setSharedHeaders(res: ServerResponse, contentType: string): void {
|
||||
res.setHeader("cache-control", "no-store, max-age=0");
|
||||
res.setHeader("content-type", contentType);
|
||||
res.setHeader("x-content-type-options", "nosniff");
|
||||
res.setHeader("referrer-policy", "no-referrer");
|
||||
}
|
||||
9
extensions/diffs/src/prompt-guidance.ts
Normal file
9
extensions/diffs/src/prompt-guidance.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const DIFFS_AGENT_GUIDANCE = [
|
||||
"When you need to show edits as a real diff, prefer the `diffs` tool instead of writing a manual summary.",
|
||||
"The `diffs` tool accepts either `before` + `after` text, or a unified `patch` string.",
|
||||
"Use `mode=view` when you want an interactive gateway-hosted viewer. After the tool returns, use `details.viewerUrl` with the canvas tool via `canvas present` or `canvas navigate`.",
|
||||
"Use `mode=image` when you need a rendered PNG. The tool result includes `details.imagePath` for the generated file.",
|
||||
"When you need to deliver the PNG to a user or channel, do not rely on the raw tool-result image renderer. Instead, call the `message` tool and pass `details.imagePath` through `path` or `filePath`.",
|
||||
"Use `mode=both` when you want both the gateway viewer URL and the PNG artifact.",
|
||||
"Good defaults: `theme=dark` for canvas rendering, `layout=unified` for most diffs, and include `path` for before/after text when you know the file name.",
|
||||
].join("\n");
|
||||
61
extensions/diffs/src/render.test.ts
Normal file
61
extensions/diffs/src/render.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderDiffDocument } from "./render.js";
|
||||
|
||||
describe("renderDiffDocument", () => {
|
||||
it("renders before/after input into a complete viewer document", async () => {
|
||||
const rendered = await renderDiffDocument(
|
||||
{
|
||||
kind: "before_after",
|
||||
before: "const value = 1;\n",
|
||||
after: "const value = 2;\n",
|
||||
path: "src/example.ts",
|
||||
},
|
||||
{
|
||||
layout: "unified",
|
||||
expandUnchanged: false,
|
||||
theme: "light",
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.title).toBe("src/example.ts");
|
||||
expect(rendered.fileCount).toBe(1);
|
||||
expect(rendered.html).toContain("data-openclaw-diff-root");
|
||||
expect(rendered.html).toContain("src/example.ts");
|
||||
expect(rendered.html).toContain("/plugins/diffs/assets/viewer.js");
|
||||
expect(rendered.html).not.toContain("fonts.googleapis.com");
|
||||
});
|
||||
|
||||
it("renders multi-file patch input", async () => {
|
||||
const patch = [
|
||||
"diff --git a/a.ts b/a.ts",
|
||||
"--- a/a.ts",
|
||||
"+++ b/a.ts",
|
||||
"@@ -1 +1 @@",
|
||||
"-const a = 1;",
|
||||
"+const a = 2;",
|
||||
"diff --git a/b.ts b/b.ts",
|
||||
"--- a/b.ts",
|
||||
"+++ b/b.ts",
|
||||
"@@ -1 +1 @@",
|
||||
"-const b = 1;",
|
||||
"+const b = 2;",
|
||||
].join("\n");
|
||||
|
||||
const rendered = await renderDiffDocument(
|
||||
{
|
||||
kind: "patch",
|
||||
patch,
|
||||
title: "Workspace patch",
|
||||
},
|
||||
{
|
||||
layout: "split",
|
||||
expandUnchanged: true,
|
||||
theme: "dark",
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.title).toBe("Workspace patch");
|
||||
expect(rendered.fileCount).toBe(2);
|
||||
expect(rendered.html).toContain("Workspace patch");
|
||||
});
|
||||
});
|
||||
351
extensions/diffs/src/render.ts
Normal file
351
extensions/diffs/src/render.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import type { FileContents, FileDiffMetadata, SupportedLanguages } from "@pierre/diffs";
|
||||
import { parsePatchFiles } from "@pierre/diffs";
|
||||
import { preloadFileDiff, preloadMultiFileDiff } from "@pierre/diffs/ssr";
|
||||
import type {
|
||||
DiffInput,
|
||||
DiffRenderOptions,
|
||||
DiffViewerOptions,
|
||||
DiffViewerPayload,
|
||||
RenderedDiffDocument,
|
||||
} from "./types.js";
|
||||
import { VIEWER_LOADER_PATH } from "./viewer-assets.js";
|
||||
|
||||
const DEFAULT_FILE_NAME = "diff.txt";
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function escapeJsonScript(value: unknown): string {
|
||||
return JSON.stringify(value).replaceAll("<", "\\u003c");
|
||||
}
|
||||
|
||||
function buildDiffTitle(input: DiffInput): string {
|
||||
if (input.title?.trim()) {
|
||||
return input.title.trim();
|
||||
}
|
||||
if (input.kind === "before_after") {
|
||||
return input.path?.trim() || "Text diff";
|
||||
}
|
||||
return "Patch diff";
|
||||
}
|
||||
|
||||
function resolveBeforeAfterFileName(input: Extract<DiffInput, { kind: "before_after" }>): string {
|
||||
if (input.path?.trim()) {
|
||||
return input.path.trim();
|
||||
}
|
||||
if (input.lang?.trim()) {
|
||||
return `diff.${input.lang.trim().replace(/^\.+/, "")}`;
|
||||
}
|
||||
return DEFAULT_FILE_NAME;
|
||||
}
|
||||
|
||||
function buildDiffOptions(options: DiffRenderOptions): DiffViewerOptions {
|
||||
return {
|
||||
theme: {
|
||||
light: "pierre-light",
|
||||
dark: "pierre-dark",
|
||||
},
|
||||
diffStyle: options.layout,
|
||||
expandUnchanged: options.expandUnchanged,
|
||||
themeType: options.theme,
|
||||
overflow: "wrap" as const,
|
||||
unsafeCSS: `
|
||||
:host {
|
||||
--diffs-font-family: "Fira Code", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--diffs-header-font-family: "Fira Code", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--diffs-font-size: 15px;
|
||||
--diffs-line-height: 24px;
|
||||
}
|
||||
|
||||
[data-diffs-header] {
|
||||
min-height: 64px;
|
||||
padding-inline: 18px 14px;
|
||||
}
|
||||
|
||||
[data-header-content] {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
[data-metadata] {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.oc-diff-toolbar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-inline-start: 6px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.oc-diff-toolbar-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
line-height: 0;
|
||||
overflow: visible;
|
||||
transition: opacity 120ms ease;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.oc-diff-toolbar-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.oc-diff-toolbar-button[data-active="true"] {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.oc-diff-toolbar-button svg {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
overflow: visible;
|
||||
flex: 0 0 auto;
|
||||
color: inherit;
|
||||
fill: currentColor;
|
||||
stroke: currentColor;
|
||||
pointer-events: none;
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSupportedLanguage(value?: string): SupportedLanguages | undefined {
|
||||
const normalized = value?.trim();
|
||||
return normalized ? (normalized as SupportedLanguages) : undefined;
|
||||
}
|
||||
|
||||
function buildPayloadLanguages(payload: {
|
||||
fileDiff?: FileDiffMetadata;
|
||||
oldFile?: FileContents;
|
||||
newFile?: FileContents;
|
||||
}): SupportedLanguages[] {
|
||||
const langs = new Set<SupportedLanguages>();
|
||||
if (payload.fileDiff?.lang) {
|
||||
langs.add(payload.fileDiff.lang);
|
||||
}
|
||||
if (payload.oldFile?.lang) {
|
||||
langs.add(payload.oldFile.lang);
|
||||
}
|
||||
if (payload.newFile?.lang) {
|
||||
langs.add(payload.newFile.lang);
|
||||
}
|
||||
if (langs.size === 0) {
|
||||
langs.add("text");
|
||||
}
|
||||
return [...langs];
|
||||
}
|
||||
|
||||
function renderDiffCard(payload: DiffViewerPayload): string {
|
||||
return `<section class="oc-diff-card">
|
||||
<diffs-container class="oc-diff-host" data-openclaw-diff-host>
|
||||
<template shadowrootmode="open">${payload.prerenderedHTML}</template>
|
||||
</diffs-container>
|
||||
<script type="application/json" data-openclaw-diff-payload>${escapeJsonScript(payload)}</script>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function buildHtmlDocument(params: {
|
||||
title: string;
|
||||
bodyHtml: string;
|
||||
theme: DiffRenderOptions["theme"];
|
||||
}): string {
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<title>${escapeHtml(params.title)}</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
background: #05070b;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 22px;
|
||||
font-family:
|
||||
"Fira Code",
|
||||
"SF Mono",
|
||||
Monaco,
|
||||
Consolas,
|
||||
monospace;
|
||||
background: #05070b;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
body[data-theme="light"] {
|
||||
background: #f3f5f8;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.oc-frame {
|
||||
max-width: 1560px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.oc-frame[data-render-mode="image"] {
|
||||
max-width: 1120px;
|
||||
}
|
||||
|
||||
[data-openclaw-diff-root] {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.oc-diff-card {
|
||||
overflow: hidden;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
background: rgba(15, 23, 42, 0.14);
|
||||
box-shadow: 0 18px 48px rgba(2, 6, 23, 0.22);
|
||||
}
|
||||
|
||||
body[data-theme="light"] .oc-diff-card {
|
||||
border-color: rgba(148, 163, 184, 0.22);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.oc-diff-host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.oc-frame[data-render-mode="image"] .oc-diff-card {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
[data-openclaw-diff-root] {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-theme="${params.theme}">
|
||||
<main class="oc-frame" data-render-mode="viewer">
|
||||
<div data-openclaw-diff-root>
|
||||
${params.bodyHtml}
|
||||
</div>
|
||||
</main>
|
||||
<script type="module" src="${VIEWER_LOADER_PATH}"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
async function renderBeforeAfterDiff(
|
||||
input: Extract<DiffInput, { kind: "before_after" }>,
|
||||
options: DiffRenderOptions,
|
||||
): Promise<{ bodyHtml: string; fileCount: number }> {
|
||||
const fileName = resolveBeforeAfterFileName(input);
|
||||
const lang = normalizeSupportedLanguage(input.lang);
|
||||
const oldFile: FileContents = {
|
||||
name: fileName,
|
||||
contents: input.before,
|
||||
...(lang ? { lang } : {}),
|
||||
};
|
||||
const newFile: FileContents = {
|
||||
name: fileName,
|
||||
contents: input.after,
|
||||
...(lang ? { lang } : {}),
|
||||
};
|
||||
const payloadOptions = buildDiffOptions(options);
|
||||
const result = await preloadMultiFileDiff({
|
||||
oldFile,
|
||||
newFile,
|
||||
options: payloadOptions,
|
||||
});
|
||||
|
||||
return {
|
||||
bodyHtml: renderDiffCard({
|
||||
prerenderedHTML: result.prerenderedHTML,
|
||||
oldFile: result.oldFile,
|
||||
newFile: result.newFile,
|
||||
options: payloadOptions,
|
||||
langs: buildPayloadLanguages({ oldFile: result.oldFile, newFile: result.newFile }),
|
||||
}),
|
||||
fileCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
async function renderPatchDiff(
|
||||
input: Extract<DiffInput, { kind: "patch" }>,
|
||||
options: DiffRenderOptions,
|
||||
): Promise<{ bodyHtml: string; fileCount: number }> {
|
||||
const files = parsePatchFiles(input.patch).flatMap((entry) => entry.files ?? []);
|
||||
if (files.length === 0) {
|
||||
throw new Error("Patch input did not contain any file diffs.");
|
||||
}
|
||||
|
||||
const payloadOptions = buildDiffOptions(options);
|
||||
const sections = await Promise.all(
|
||||
files.map(async (fileDiff) => {
|
||||
const result = await preloadFileDiff({
|
||||
fileDiff,
|
||||
options: payloadOptions,
|
||||
});
|
||||
|
||||
return renderDiffCard({
|
||||
prerenderedHTML: result.prerenderedHTML,
|
||||
fileDiff: result.fileDiff,
|
||||
options: payloadOptions,
|
||||
langs: buildPayloadLanguages({ fileDiff: result.fileDiff }),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
bodyHtml: sections.join("\n"),
|
||||
fileCount: files.length,
|
||||
};
|
||||
}
|
||||
|
||||
export async function renderDiffDocument(
|
||||
input: DiffInput,
|
||||
options: DiffRenderOptions,
|
||||
): Promise<RenderedDiffDocument> {
|
||||
const title = buildDiffTitle(input);
|
||||
const rendered =
|
||||
input.kind === "before_after"
|
||||
? await renderBeforeAfterDiff(input, options)
|
||||
: await renderPatchDiff(input, options);
|
||||
|
||||
return {
|
||||
html: buildHtmlDocument({
|
||||
title,
|
||||
bodyHtml: rendered.bodyHtml,
|
||||
theme: options.theme,
|
||||
}),
|
||||
title,
|
||||
fileCount: rendered.fileCount,
|
||||
inputKind: input.kind,
|
||||
};
|
||||
}
|
||||
64
extensions/diffs/src/store.test.ts
Normal file
64
extensions/diffs/src/store.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DiffArtifactStore } from "./store.js";
|
||||
|
||||
describe("DiffArtifactStore", () => {
|
||||
let rootDir: string;
|
||||
let store: DiffArtifactStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-diffs-store-"));
|
||||
store = new DiffArtifactStore({ rootDir });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
await fs.rm(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("creates and retrieves an artifact", async () => {
|
||||
const artifact = await store.createArtifact({
|
||||
html: "<html>demo</html>",
|
||||
title: "Demo",
|
||||
inputKind: "before_after",
|
||||
fileCount: 1,
|
||||
});
|
||||
|
||||
const loaded = await store.getArtifact(artifact.id, artifact.token);
|
||||
expect(loaded?.id).toBe(artifact.id);
|
||||
expect(await store.readHtml(artifact.id)).toBe("<html>demo</html>");
|
||||
});
|
||||
|
||||
it("expires artifacts after the ttl", async () => {
|
||||
vi.useFakeTimers();
|
||||
const now = new Date("2026-02-27T16:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const artifact = await store.createArtifact({
|
||||
html: "<html>demo</html>",
|
||||
title: "Demo",
|
||||
inputKind: "patch",
|
||||
fileCount: 2,
|
||||
ttlMs: 1_000,
|
||||
});
|
||||
|
||||
vi.setSystemTime(new Date(now.getTime() + 2_000));
|
||||
const loaded = await store.getArtifact(artifact.id, artifact.token);
|
||||
expect(loaded).toBeNull();
|
||||
});
|
||||
|
||||
it("updates the stored image path", async () => {
|
||||
const artifact = await store.createArtifact({
|
||||
html: "<html>demo</html>",
|
||||
title: "Demo",
|
||||
inputKind: "before_after",
|
||||
fileCount: 1,
|
||||
});
|
||||
|
||||
const imagePath = store.allocateImagePath(artifact.id);
|
||||
const updated = await store.updateImagePath(artifact.id, imagePath);
|
||||
expect(updated.imagePath).toBe(imagePath);
|
||||
});
|
||||
});
|
||||
183
extensions/diffs/src/store.ts
Normal file
183
extensions/diffs/src/store.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { PluginLogger } from "openclaw/plugin-sdk";
|
||||
import type { DiffArtifactMeta } from "./types.js";
|
||||
|
||||
const DEFAULT_TTL_MS = 30 * 60 * 1000;
|
||||
const MAX_TTL_MS = 6 * 60 * 60 * 1000;
|
||||
const SWEEP_FALLBACK_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
const VIEWER_PREFIX = "/plugins/diffs/view";
|
||||
|
||||
type CreateArtifactParams = {
|
||||
html: string;
|
||||
title: string;
|
||||
inputKind: DiffArtifactMeta["inputKind"];
|
||||
fileCount: number;
|
||||
ttlMs?: number;
|
||||
};
|
||||
|
||||
export class DiffArtifactStore {
|
||||
private readonly rootDir: string;
|
||||
private readonly logger?: PluginLogger;
|
||||
|
||||
constructor(params: { rootDir: string; logger?: PluginLogger }) {
|
||||
this.rootDir = params.rootDir;
|
||||
this.logger = params.logger;
|
||||
}
|
||||
|
||||
async createArtifact(params: CreateArtifactParams): Promise<DiffArtifactMeta> {
|
||||
await this.ensureRoot();
|
||||
await this.cleanupExpired();
|
||||
|
||||
const id = crypto.randomBytes(10).toString("hex");
|
||||
const token = crypto.randomBytes(24).toString("hex");
|
||||
const artifactDir = this.artifactDir(id);
|
||||
const htmlPath = path.join(artifactDir, "viewer.html");
|
||||
const ttlMs = normalizeTtlMs(params.ttlMs);
|
||||
const createdAt = new Date();
|
||||
const expiresAt = new Date(createdAt.getTime() + ttlMs);
|
||||
const meta: DiffArtifactMeta = {
|
||||
id,
|
||||
token,
|
||||
title: params.title,
|
||||
inputKind: params.inputKind,
|
||||
fileCount: params.fileCount,
|
||||
createdAt: createdAt.toISOString(),
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
viewerPath: `${VIEWER_PREFIX}/${id}/${token}`,
|
||||
htmlPath,
|
||||
};
|
||||
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
await fs.writeFile(htmlPath, params.html, "utf8");
|
||||
await this.writeMeta(meta);
|
||||
return meta;
|
||||
}
|
||||
|
||||
async getArtifact(id: string, token: string): Promise<DiffArtifactMeta | null> {
|
||||
const meta = await this.readMeta(id);
|
||||
if (!meta) {
|
||||
return null;
|
||||
}
|
||||
if (meta.token !== token) {
|
||||
return null;
|
||||
}
|
||||
if (isExpired(meta)) {
|
||||
await this.deleteArtifact(id);
|
||||
return null;
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
async readHtml(id: string): Promise<string> {
|
||||
const meta = await this.readMeta(id);
|
||||
if (!meta) {
|
||||
throw new Error(`Diff artifact not found: ${id}`);
|
||||
}
|
||||
return await fs.readFile(meta.htmlPath, "utf8");
|
||||
}
|
||||
|
||||
async updateImagePath(id: string, imagePath: string): Promise<DiffArtifactMeta> {
|
||||
const meta = await this.readMeta(id);
|
||||
if (!meta) {
|
||||
throw new Error(`Diff artifact not found: ${id}`);
|
||||
}
|
||||
const next: DiffArtifactMeta = {
|
||||
...meta,
|
||||
imagePath,
|
||||
};
|
||||
await this.writeMeta(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
allocateImagePath(id: string): string {
|
||||
return path.join(this.artifactDir(id), "preview.png");
|
||||
}
|
||||
|
||||
async cleanupExpired(): Promise<void> {
|
||||
await this.ensureRoot();
|
||||
const entries = await fs.readdir(this.rootDir, { withFileTypes: true }).catch(() => []);
|
||||
const now = Date.now();
|
||||
|
||||
await Promise.all(
|
||||
entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map(async (entry) => {
|
||||
const id = entry.name;
|
||||
const meta = await this.readMeta(id);
|
||||
if (meta) {
|
||||
if (isExpired(meta)) {
|
||||
await this.deleteArtifact(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const artifactPath = this.artifactDir(id);
|
||||
const stat = await fs.stat(artifactPath).catch(() => null);
|
||||
if (!stat) {
|
||||
return;
|
||||
}
|
||||
if (now - stat.mtimeMs > SWEEP_FALLBACK_AGE_MS) {
|
||||
await this.deleteArtifact(id);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureRoot(): Promise<void> {
|
||||
await fs.mkdir(this.rootDir, { recursive: true });
|
||||
}
|
||||
|
||||
private artifactDir(id: string): string {
|
||||
return path.join(this.rootDir, id);
|
||||
}
|
||||
|
||||
private metaPath(id: string): string {
|
||||
return path.join(this.artifactDir(id), "meta.json");
|
||||
}
|
||||
|
||||
private async writeMeta(meta: DiffArtifactMeta): Promise<void> {
|
||||
await fs.writeFile(this.metaPath(meta.id), JSON.stringify(meta, null, 2), "utf8");
|
||||
}
|
||||
|
||||
private async readMeta(id: string): Promise<DiffArtifactMeta | null> {
|
||||
try {
|
||||
const raw = await fs.readFile(this.metaPath(id), "utf8");
|
||||
return JSON.parse(raw) as DiffArtifactMeta;
|
||||
} catch (error) {
|
||||
if (isFileNotFound(error)) {
|
||||
return null;
|
||||
}
|
||||
this.logger?.warn(`Failed to read diff artifact metadata for ${id}: ${String(error)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteArtifact(id: string): Promise<void> {
|
||||
await fs.rm(this.artifactDir(id), { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTtlMs(value?: number): number {
|
||||
if (!Number.isFinite(value) || value === undefined) {
|
||||
return DEFAULT_TTL_MS;
|
||||
}
|
||||
const rounded = Math.floor(value);
|
||||
if (rounded <= 0) {
|
||||
return DEFAULT_TTL_MS;
|
||||
}
|
||||
return Math.min(rounded, MAX_TTL_MS);
|
||||
}
|
||||
|
||||
function isExpired(meta: DiffArtifactMeta): boolean {
|
||||
const expiresAt = Date.parse(meta.expiresAt);
|
||||
if (!Number.isFinite(expiresAt)) {
|
||||
return true;
|
||||
}
|
||||
return Date.now() >= expiresAt;
|
||||
}
|
||||
|
||||
function isFileNotFound(error: unknown): boolean {
|
||||
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
||||
}
|
||||
147
extensions/diffs/src/tool.test.ts
Normal file
147
extensions/diffs/src/tool.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DiffArtifactStore } from "./store.js";
|
||||
import { createDiffsTool } from "./tool.js";
|
||||
|
||||
describe("diffs tool", () => {
|
||||
let rootDir: string;
|
||||
let store: DiffArtifactStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-diffs-tool-"));
|
||||
store = new DiffArtifactStore({ rootDir });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("returns a viewer URL in view mode", async () => {
|
||||
const tool = createDiffsTool({
|
||||
api: createApi(),
|
||||
store,
|
||||
});
|
||||
|
||||
const result = await tool.execute?.("tool-1", {
|
||||
before: "one\n",
|
||||
after: "two\n",
|
||||
path: "README.md",
|
||||
mode: "view",
|
||||
});
|
||||
|
||||
const text = readTextContent(result, 0);
|
||||
expect(text).toContain("http://127.0.0.1:18789/plugins/diffs/view/");
|
||||
expect((result?.details as Record<string, unknown>).viewerUrl).toBeDefined();
|
||||
});
|
||||
|
||||
it("returns an image artifact in image mode", async () => {
|
||||
const screenshotter = {
|
||||
screenshotHtml: vi.fn(async ({ outputPath }: { outputPath: string }) => {
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, Buffer.from("png"));
|
||||
return outputPath;
|
||||
}),
|
||||
};
|
||||
|
||||
const tool = createDiffsTool({
|
||||
api: createApi(),
|
||||
store,
|
||||
screenshotter,
|
||||
});
|
||||
|
||||
const result = await tool.execute?.("tool-2", {
|
||||
before: "one\n",
|
||||
after: "two\n",
|
||||
mode: "image",
|
||||
});
|
||||
|
||||
expect(screenshotter.screenshotHtml).toHaveBeenCalledTimes(1);
|
||||
expect(readTextContent(result, 0)).toContain("Diff image generated at:");
|
||||
expect(readTextContent(result, 0)).toContain("Use the `message` tool");
|
||||
expect(result?.content).toHaveLength(1);
|
||||
expect((result?.details as Record<string, unknown>).imagePath).toBeDefined();
|
||||
});
|
||||
|
||||
it("falls back to view output when both mode cannot render an image", async () => {
|
||||
const tool = createDiffsTool({
|
||||
api: createApi(),
|
||||
store,
|
||||
screenshotter: {
|
||||
screenshotHtml: vi.fn(async () => {
|
||||
throw new Error("browser missing");
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await tool.execute?.("tool-3", {
|
||||
before: "one\n",
|
||||
after: "two\n",
|
||||
mode: "both",
|
||||
});
|
||||
|
||||
expect(result?.content).toHaveLength(1);
|
||||
expect(readTextContent(result, 0)).toContain("Image rendering failed");
|
||||
expect((result?.details as Record<string, unknown>).imageError).toBe("browser missing");
|
||||
});
|
||||
|
||||
it("rejects invalid base URLs as tool input errors", async () => {
|
||||
const tool = createDiffsTool({
|
||||
api: createApi(),
|
||||
store,
|
||||
});
|
||||
|
||||
await expect(
|
||||
tool.execute?.("tool-4", {
|
||||
before: "one\n",
|
||||
after: "two\n",
|
||||
mode: "view",
|
||||
baseUrl: "javascript:alert(1)",
|
||||
}),
|
||||
).rejects.toThrow("Invalid baseUrl");
|
||||
});
|
||||
});
|
||||
|
||||
function createApi(): OpenClawPluginApi {
|
||||
return {
|
||||
id: "diffs",
|
||||
name: "Diffs",
|
||||
description: "Diffs",
|
||||
source: "test",
|
||||
config: {
|
||||
gateway: {
|
||||
port: 18789,
|
||||
bind: "loopback",
|
||||
},
|
||||
},
|
||||
runtime: {} as OpenClawPluginApi["runtime"],
|
||||
logger: {
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
},
|
||||
registerTool() {},
|
||||
registerHook() {},
|
||||
registerHttpHandler() {},
|
||||
registerHttpRoute() {},
|
||||
registerChannel() {},
|
||||
registerGatewayMethod() {},
|
||||
registerCli() {},
|
||||
registerService() {},
|
||||
registerProvider() {},
|
||||
registerCommand() {},
|
||||
resolvePath(input: string) {
|
||||
return input;
|
||||
},
|
||||
on() {},
|
||||
};
|
||||
}
|
||||
|
||||
function readTextContent(result: unknown, index: number): string {
|
||||
const content = (result as { content?: Array<{ type?: string; text?: string }> } | undefined)
|
||||
?.content;
|
||||
const entry = content?.[index];
|
||||
return entry?.type === "text" ? (entry.text ?? "") : "";
|
||||
}
|
||||
245
extensions/diffs/src/tool.ts
Normal file
245
extensions/diffs/src/tool.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { PlaywrightDiffScreenshotter, type DiffScreenshotter } from "./browser.js";
|
||||
import { renderDiffDocument } from "./render.js";
|
||||
import type { DiffArtifactStore } from "./store.js";
|
||||
import {
|
||||
DIFF_LAYOUTS,
|
||||
DIFF_MODES,
|
||||
DIFF_THEMES,
|
||||
type DiffInput,
|
||||
type DiffLayout,
|
||||
type DiffMode,
|
||||
type DiffTheme,
|
||||
} from "./types.js";
|
||||
import { buildViewerUrl, normalizeViewerBaseUrl } from "./url.js";
|
||||
|
||||
function stringEnum<T extends readonly string[]>(values: T, description: string) {
|
||||
return Type.Unsafe<T[number]>({
|
||||
type: "string",
|
||||
enum: [...values],
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
const DiffsToolSchema = Type.Object(
|
||||
{
|
||||
before: Type.Optional(Type.String({ description: "Original text content." })),
|
||||
after: Type.Optional(Type.String({ description: "Updated text content." })),
|
||||
patch: Type.Optional(Type.String({ description: "Unified diff or patch text." })),
|
||||
path: Type.Optional(Type.String({ description: "Display path for before/after input." })),
|
||||
lang: Type.Optional(
|
||||
Type.String({ description: "Optional language override for before/after input." }),
|
||||
),
|
||||
title: Type.Optional(Type.String({ description: "Optional title for the rendered diff." })),
|
||||
mode: Type.Optional(
|
||||
stringEnum(DIFF_MODES, "Output mode: view, image, or both. Default: both."),
|
||||
),
|
||||
theme: Type.Optional(stringEnum(DIFF_THEMES, "Viewer theme. Default: dark.")),
|
||||
layout: Type.Optional(stringEnum(DIFF_LAYOUTS, "Diff layout. Default: unified.")),
|
||||
expandUnchanged: Type.Optional(
|
||||
Type.Boolean({ description: "Expand unchanged sections instead of collapsing them." }),
|
||||
),
|
||||
ttlSeconds: Type.Optional(
|
||||
Type.Number({
|
||||
description: "Artifact lifetime in seconds. Default: 1800. Maximum: 21600.",
|
||||
minimum: 1,
|
||||
maximum: 21_600,
|
||||
}),
|
||||
),
|
||||
baseUrl: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Optional gateway base URL override used when building the viewer URL, for example https://gateway.example.com.",
|
||||
}),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
type DiffsToolParams = Static<typeof DiffsToolSchema>;
|
||||
|
||||
export function createDiffsTool(params: {
|
||||
api: OpenClawPluginApi;
|
||||
store: DiffArtifactStore;
|
||||
screenshotter?: DiffScreenshotter;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
name: "diffs",
|
||||
label: "Diffs",
|
||||
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.",
|
||||
parameters: DiffsToolSchema,
|
||||
execute: async (_toolCallId, rawParams) => {
|
||||
const toolParams = rawParams as DiffsToolParams;
|
||||
const input = normalizeDiffInput(toolParams);
|
||||
const mode = normalizeMode(toolParams.mode);
|
||||
const theme = normalizeTheme(toolParams.theme);
|
||||
const layout = normalizeLayout(toolParams.layout);
|
||||
const expandUnchanged = toolParams.expandUnchanged === true;
|
||||
const ttlMs = normalizeTtlMs(toolParams.ttlSeconds);
|
||||
|
||||
const rendered = await renderDiffDocument(input, {
|
||||
layout,
|
||||
expandUnchanged,
|
||||
theme,
|
||||
});
|
||||
|
||||
const artifact = await params.store.createArtifact({
|
||||
html: rendered.html,
|
||||
title: rendered.title,
|
||||
inputKind: rendered.inputKind,
|
||||
fileCount: rendered.fileCount,
|
||||
ttlMs,
|
||||
});
|
||||
|
||||
const viewerUrl = buildViewerUrl({
|
||||
config: params.api.config,
|
||||
viewerPath: artifact.viewerPath,
|
||||
baseUrl: normalizeBaseUrl(toolParams.baseUrl),
|
||||
});
|
||||
|
||||
const baseDetails = {
|
||||
artifactId: artifact.id,
|
||||
viewerUrl,
|
||||
viewerPath: artifact.viewerPath,
|
||||
title: artifact.title,
|
||||
expiresAt: artifact.expiresAt,
|
||||
inputKind: artifact.inputKind,
|
||||
fileCount: artifact.fileCount,
|
||||
mode,
|
||||
};
|
||||
|
||||
if (mode === "view") {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Diff viewer ready.\n${viewerUrl}`,
|
||||
},
|
||||
],
|
||||
details: baseDetails,
|
||||
};
|
||||
}
|
||||
|
||||
const screenshotter =
|
||||
params.screenshotter ?? new PlaywrightDiffScreenshotter({ config: params.api.config });
|
||||
|
||||
try {
|
||||
const imagePath = params.store.allocateImagePath(artifact.id);
|
||||
await screenshotter.screenshotHtml({
|
||||
html: rendered.html,
|
||||
outputPath: imagePath,
|
||||
theme,
|
||||
});
|
||||
await params.store.updateImagePath(artifact.id, imagePath);
|
||||
const imageStats = await fs.stat(imagePath);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`Diff viewer: ${viewerUrl}\n` +
|
||||
`Diff image generated at: ${imagePath}\n` +
|
||||
"Use the `message` tool with `path` or `filePath` to send the PNG.",
|
||||
},
|
||||
],
|
||||
details: {
|
||||
...baseDetails,
|
||||
imagePath,
|
||||
path: imagePath,
|
||||
imageBytes: imageStats.size,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (mode === "both") {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`Diff viewer ready.\n${viewerUrl}\n` +
|
||||
`Image rendering failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
details: {
|
||||
...baseDetails,
|
||||
imageError: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDiffInput(params: DiffsToolParams): DiffInput {
|
||||
const patch = params.patch?.trim();
|
||||
const before = params.before;
|
||||
const after = params.after;
|
||||
|
||||
if (patch) {
|
||||
if (before !== undefined || after !== undefined) {
|
||||
throw new PluginToolInputError("Provide either patch or before/after input, not both.");
|
||||
}
|
||||
return {
|
||||
kind: "patch",
|
||||
patch,
|
||||
title: params.title?.trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (before === undefined || after === undefined) {
|
||||
throw new PluginToolInputError("Provide patch or both before and after text.");
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "before_after",
|
||||
before,
|
||||
after,
|
||||
path: params.path?.trim() || undefined,
|
||||
lang: params.lang?.trim() || undefined,
|
||||
title: params.title?.trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(baseUrl?: string): string | undefined {
|
||||
const normalized = baseUrl?.trim();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return normalizeViewerBaseUrl(normalized);
|
||||
} catch {
|
||||
throw new PluginToolInputError(`Invalid baseUrl: ${normalized}`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMode(mode?: DiffMode): DiffMode {
|
||||
return mode && DIFF_MODES.includes(mode) ? mode : "both";
|
||||
}
|
||||
|
||||
function normalizeTheme(theme?: DiffTheme): DiffTheme {
|
||||
return theme && DIFF_THEMES.includes(theme) ? theme : "dark";
|
||||
}
|
||||
|
||||
function normalizeLayout(layout?: DiffLayout): DiffLayout {
|
||||
return layout && DIFF_LAYOUTS.includes(layout) ? layout : "unified";
|
||||
}
|
||||
|
||||
function normalizeTtlMs(ttlSeconds?: number): number | undefined {
|
||||
if (!Number.isFinite(ttlSeconds) || ttlSeconds === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.floor(ttlSeconds * 1000);
|
||||
}
|
||||
|
||||
class PluginToolInputError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ToolInputError";
|
||||
}
|
||||
}
|
||||
76
extensions/diffs/src/types.ts
Normal file
76
extensions/diffs/src/types.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { FileContents, FileDiffMetadata, SupportedLanguages } from "@pierre/diffs";
|
||||
|
||||
export const DIFF_LAYOUTS = ["unified", "split"] as const;
|
||||
export const DIFF_MODES = ["view", "image", "both"] as const;
|
||||
export const DIFF_THEMES = ["light", "dark"] as const;
|
||||
|
||||
export type DiffLayout = (typeof DIFF_LAYOUTS)[number];
|
||||
export type DiffMode = (typeof DIFF_MODES)[number];
|
||||
export type DiffTheme = (typeof DIFF_THEMES)[number];
|
||||
|
||||
export type BeforeAfterDiffInput = {
|
||||
kind: "before_after";
|
||||
before: string;
|
||||
after: string;
|
||||
path?: string;
|
||||
lang?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type PatchDiffInput = {
|
||||
kind: "patch";
|
||||
patch: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type DiffInput = BeforeAfterDiffInput | PatchDiffInput;
|
||||
|
||||
export type DiffRenderOptions = {
|
||||
layout: DiffLayout;
|
||||
expandUnchanged: boolean;
|
||||
theme: DiffTheme;
|
||||
};
|
||||
|
||||
export type DiffViewerOptions = {
|
||||
theme: {
|
||||
light: "pierre-light";
|
||||
dark: "pierre-dark";
|
||||
};
|
||||
diffStyle: DiffLayout;
|
||||
expandUnchanged: boolean;
|
||||
themeType: DiffTheme;
|
||||
overflow: "scroll" | "wrap";
|
||||
unsafeCSS: string;
|
||||
};
|
||||
|
||||
export type DiffViewerPayload = {
|
||||
prerenderedHTML: string;
|
||||
options: DiffViewerOptions;
|
||||
langs: SupportedLanguages[];
|
||||
oldFile?: FileContents;
|
||||
newFile?: FileContents;
|
||||
fileDiff?: FileDiffMetadata;
|
||||
};
|
||||
|
||||
export type RenderedDiffDocument = {
|
||||
html: string;
|
||||
title: string;
|
||||
fileCount: number;
|
||||
inputKind: DiffInput["kind"];
|
||||
};
|
||||
|
||||
export type DiffArtifactMeta = {
|
||||
id: string;
|
||||
token: string;
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
title: string;
|
||||
inputKind: DiffInput["kind"];
|
||||
fileCount: number;
|
||||
viewerPath: string;
|
||||
htmlPath: string;
|
||||
imagePath?: string;
|
||||
};
|
||||
|
||||
export const DIFF_ARTIFACT_ID_PATTERN = /^[0-9a-f]{20}$/;
|
||||
export const DIFF_ARTIFACT_TOKEN_PATTERN = /^[0-9a-f]{48}$/;
|
||||
120
extensions/diffs/src/url.ts
Normal file
120
extensions/diffs/src/url.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import os from "node:os";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
|
||||
const DEFAULT_GATEWAY_PORT = 18789;
|
||||
|
||||
export function buildViewerUrl(params: {
|
||||
config: OpenClawConfig;
|
||||
viewerPath: string;
|
||||
baseUrl?: string;
|
||||
}): string {
|
||||
const baseUrl = params.baseUrl?.trim() || resolveGatewayBaseUrl(params.config);
|
||||
const normalizedBase = normalizeViewerBaseUrl(baseUrl);
|
||||
const normalizedPath = params.viewerPath.startsWith("/")
|
||||
? params.viewerPath
|
||||
: `/${params.viewerPath}`;
|
||||
return `${normalizedBase}${normalizedPath}`;
|
||||
}
|
||||
|
||||
export function normalizeViewerBaseUrl(raw: string): string {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(raw);
|
||||
} catch {
|
||||
throw new Error(`Invalid baseUrl: ${raw}`);
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
throw new Error(`baseUrl must use http or https: ${raw}`);
|
||||
}
|
||||
const withoutTrailingSlash = parsed.toString().replace(/\/+$/, "");
|
||||
return withoutTrailingSlash;
|
||||
}
|
||||
|
||||
function resolveGatewayBaseUrl(config: OpenClawConfig): string {
|
||||
const scheme = config.gateway?.tls?.enabled ? "https" : "http";
|
||||
const port =
|
||||
typeof config.gateway?.port === "number" ? config.gateway.port : DEFAULT_GATEWAY_PORT;
|
||||
const bind = config.gateway?.bind ?? "loopback";
|
||||
|
||||
if (bind === "custom" && config.gateway?.customBindHost?.trim()) {
|
||||
return `${scheme}://${config.gateway.customBindHost.trim()}:${port}`;
|
||||
}
|
||||
|
||||
if (bind === "lan") {
|
||||
return `${scheme}://${pickPrimaryLanIPv4() ?? "127.0.0.1"}:${port}`;
|
||||
}
|
||||
|
||||
if (bind === "tailnet") {
|
||||
return `${scheme}://${pickPrimaryTailnetIPv4() ?? "127.0.0.1"}:${port}`;
|
||||
}
|
||||
|
||||
return `${scheme}://127.0.0.1:${port}`;
|
||||
}
|
||||
|
||||
function pickPrimaryLanIPv4(): string | undefined {
|
||||
const nets = os.networkInterfaces();
|
||||
const preferredNames = ["en0", "eth0"];
|
||||
|
||||
for (const name of preferredNames) {
|
||||
const candidate = pickPrivateAddress(nets[name]);
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
for (const entries of Object.values(nets)) {
|
||||
const candidate = pickPrivateAddress(entries);
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function pickPrimaryTailnetIPv4(): string | undefined {
|
||||
const nets = os.networkInterfaces();
|
||||
for (const entries of Object.values(nets)) {
|
||||
const candidate = entries?.find((entry) => isTailnetIPv4(entry.address) && !entry.internal);
|
||||
if (candidate?.address) {
|
||||
return candidate.address;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function pickPrivateAddress(entries: os.NetworkInterfaceInfo[] | undefined): string | undefined {
|
||||
return entries?.find(
|
||||
(entry) => entry.family === "IPv4" && !entry.internal && isPrivateIPv4(entry.address),
|
||||
)?.address;
|
||||
}
|
||||
|
||||
function isPrivateIPv4(address: string): boolean {
|
||||
const octets = parseIpv4(address);
|
||||
if (!octets) {
|
||||
return false;
|
||||
}
|
||||
const [a, b] = octets;
|
||||
return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
|
||||
}
|
||||
|
||||
function isTailnetIPv4(address: string): boolean {
|
||||
const octets = parseIpv4(address);
|
||||
if (!octets) {
|
||||
return false;
|
||||
}
|
||||
const [a, b] = octets;
|
||||
return a === 100 && b >= 64 && b <= 127;
|
||||
}
|
||||
|
||||
function parseIpv4(address: string): number[] | null {
|
||||
const parts = address.split(".");
|
||||
if (parts.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
const octets = parts.map((part) => Number.parseInt(part, 10));
|
||||
if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
||||
return null;
|
||||
}
|
||||
return octets;
|
||||
}
|
||||
22
extensions/diffs/src/viewer-assets.test.ts
Normal file
22
extensions/diffs/src/viewer-assets.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getServedViewerAsset, VIEWER_LOADER_PATH, VIEWER_RUNTIME_PATH } from "./viewer-assets.js";
|
||||
|
||||
describe("viewer assets", () => {
|
||||
it("serves a stable loader that points at the current runtime bundle", async () => {
|
||||
const loader = await getServedViewerAsset(VIEWER_LOADER_PATH);
|
||||
|
||||
expect(loader?.contentType).toBe("text/javascript; charset=utf-8");
|
||||
expect(String(loader?.body)).toContain(`${VIEWER_RUNTIME_PATH}?v=`);
|
||||
});
|
||||
|
||||
it("serves the runtime bundle body", async () => {
|
||||
const runtime = await getServedViewerAsset(VIEWER_RUNTIME_PATH);
|
||||
|
||||
expect(runtime?.contentType).toBe("text/javascript; charset=utf-8");
|
||||
expect(String(runtime?.body)).toContain("openclawDiffsReady");
|
||||
});
|
||||
|
||||
it("returns null for unknown asset paths", async () => {
|
||||
await expect(getServedViewerAsset("/plugins/diffs/assets/not-real.js")).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
62
extensions/diffs/src/viewer-assets.ts
Normal file
62
extensions/diffs/src/viewer-assets.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const VIEWER_ASSET_PREFIX = "/plugins/diffs/assets/";
|
||||
export const VIEWER_LOADER_PATH = `${VIEWER_ASSET_PREFIX}viewer.js`;
|
||||
export const VIEWER_RUNTIME_PATH = `${VIEWER_ASSET_PREFIX}viewer-runtime.js`;
|
||||
|
||||
const VIEWER_RUNTIME_FILE_URL = new URL("../assets/viewer-runtime.js", import.meta.url);
|
||||
|
||||
export type ServedViewerAsset = {
|
||||
body: string | Buffer;
|
||||
contentType: string;
|
||||
};
|
||||
|
||||
type RuntimeAssetCache = {
|
||||
mtimeMs: number;
|
||||
runtimeBody: Buffer;
|
||||
loaderBody: string;
|
||||
};
|
||||
|
||||
let runtimeAssetCache: RuntimeAssetCache | null = null;
|
||||
|
||||
export async function getServedViewerAsset(pathname: string): Promise<ServedViewerAsset | null> {
|
||||
if (pathname !== VIEWER_LOADER_PATH && pathname !== VIEWER_RUNTIME_PATH) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assets = await loadViewerAssets();
|
||||
if (pathname === VIEWER_LOADER_PATH) {
|
||||
return {
|
||||
body: assets.loaderBody,
|
||||
contentType: "text/javascript; charset=utf-8",
|
||||
};
|
||||
}
|
||||
|
||||
if (pathname === VIEWER_RUNTIME_PATH) {
|
||||
return {
|
||||
body: assets.runtimeBody,
|
||||
contentType: "text/javascript; charset=utf-8",
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadViewerAssets(): Promise<RuntimeAssetCache> {
|
||||
const runtimePath = fileURLToPath(VIEWER_RUNTIME_FILE_URL);
|
||||
const runtimeStat = await fs.stat(runtimePath);
|
||||
if (runtimeAssetCache && runtimeAssetCache.mtimeMs === runtimeStat.mtimeMs) {
|
||||
return runtimeAssetCache;
|
||||
}
|
||||
|
||||
const runtimeBody = await fs.readFile(runtimePath);
|
||||
const hash = crypto.createHash("sha1").update(runtimeBody).digest("hex").slice(0, 12);
|
||||
runtimeAssetCache = {
|
||||
mtimeMs: runtimeStat.mtimeMs,
|
||||
runtimeBody,
|
||||
loaderBody: `import "${VIEWER_RUNTIME_PATH}?v=${hash}";\n`,
|
||||
};
|
||||
return runtimeAssetCache;
|
||||
}
|
||||
295
extensions/diffs/src/viewer-client.ts
Normal file
295
extensions/diffs/src/viewer-client.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import { FileDiff, preloadHighlighter } from "@pierre/diffs";
|
||||
import type {
|
||||
FileContents,
|
||||
FileDiffMetadata,
|
||||
FileDiffOptions,
|
||||
SupportedLanguages,
|
||||
} from "@pierre/diffs";
|
||||
import type { DiffViewerPayload, DiffLayout, DiffTheme } from "./types.js";
|
||||
|
||||
type ViewerState = {
|
||||
theme: DiffTheme;
|
||||
layout: DiffLayout;
|
||||
backgroundEnabled: boolean;
|
||||
wrapEnabled: boolean;
|
||||
};
|
||||
|
||||
type DiffController = {
|
||||
payload: DiffViewerPayload;
|
||||
diff: FileDiff;
|
||||
};
|
||||
|
||||
const controllers: DiffController[] = [];
|
||||
|
||||
const viewerState: ViewerState = {
|
||||
theme: "dark",
|
||||
layout: "unified",
|
||||
backgroundEnabled: true,
|
||||
wrapEnabled: true,
|
||||
};
|
||||
|
||||
function parsePayload(element: HTMLScriptElement): DiffViewerPayload {
|
||||
const raw = element.textContent?.trim();
|
||||
if (!raw) {
|
||||
throw new Error("Diff payload was empty.");
|
||||
}
|
||||
return JSON.parse(raw) as DiffViewerPayload;
|
||||
}
|
||||
|
||||
function getCards(): Array<{ host: HTMLElement; payload: DiffViewerPayload }> {
|
||||
return [...document.querySelectorAll<HTMLElement>(".oc-diff-card")].flatMap((card) => {
|
||||
const host = card.querySelector<HTMLElement>("[data-openclaw-diff-host]");
|
||||
const payloadNode = card.querySelector<HTMLScriptElement>("[data-openclaw-diff-payload]");
|
||||
if (!host || !payloadNode) {
|
||||
return [];
|
||||
}
|
||||
return [{ host, payload: parsePayload(payloadNode) }];
|
||||
});
|
||||
}
|
||||
|
||||
function ensureShadowRoot(host: HTMLElement): void {
|
||||
if (host.shadowRoot) {
|
||||
return;
|
||||
}
|
||||
const template = host.querySelector<HTMLTemplateElement>(
|
||||
":scope > template[shadowrootmode='open']",
|
||||
);
|
||||
if (!template) {
|
||||
return;
|
||||
}
|
||||
const shadowRoot = host.attachShadow({ mode: "open" });
|
||||
shadowRoot.append(template.content.cloneNode(true));
|
||||
template.remove();
|
||||
}
|
||||
|
||||
function getHydrateProps(payload: DiffViewerPayload): {
|
||||
fileDiff?: FileDiffMetadata;
|
||||
oldFile?: FileContents;
|
||||
newFile?: FileContents;
|
||||
} {
|
||||
if (payload.fileDiff) {
|
||||
return { fileDiff: payload.fileDiff };
|
||||
}
|
||||
return {
|
||||
oldFile: payload.oldFile,
|
||||
newFile: payload.newFile,
|
||||
};
|
||||
}
|
||||
|
||||
function createToolbarButton(params: {
|
||||
title: string;
|
||||
active: boolean;
|
||||
iconMarkup: string;
|
||||
onClick: () => void;
|
||||
}): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "oc-diff-toolbar-button";
|
||||
button.dataset.active = String(params.active);
|
||||
button.title = params.title;
|
||||
button.setAttribute("aria-label", params.title);
|
||||
button.innerHTML = params.iconMarkup;
|
||||
button.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
params.onClick();
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
function splitIcon(): string {
|
||||
return `<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill="currentColor" d="M14 0H8.5v16H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2m-1.5 6.5v1h1a.5.5 0 0 1 0 1h-1v1a.5.5 0 0 1-1 0v-1h-1a.5.5 0 0 1 0-1h1v-1a.5.5 0 0 1 1 0"></path>
|
||||
<path fill="currentColor" opacity="0.35" d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h5.5V0zm.5 7.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1"></path>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function unifiedIcon(): string {
|
||||
return `<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V8.5h16zm-8-4a.5.5 0 0 0-.5.5v1h-1a.5.5 0 0 0 0 1h1v1a.5.5 0 0 0 1 0v-1h1a.5.5 0 0 0 0-1h-1v-1A.5.5 0 0 0 8 10" clip-rule="evenodd"></path>
|
||||
<path fill="currentColor" fill-rule="evenodd" opacity="0.4" d="M14 0a2 2 0 0 1 2 2v5.5H0V2a2 2 0 0 1 2-2zM6.5 3.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z" clip-rule="evenodd"></path>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function wrapIcon(active: boolean): string {
|
||||
if (active) {
|
||||
return `<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill="currentColor" opacity="0.88" d="M2 4.25h8.25a2.75 2.75 0 1 1 0 5.5H7.5a.75.75 0 0 0 0 1.5h3.1l-1.07 1.06a.75.75 0 1 0 1.06 1.06l2.35-2.34a.75.75 0 0 0 0-1.06l-2.35-2.34a.75.75 0 1 0-1.06 1.06l1.07 1.06H10.25a1.25 1.25 0 1 0 0-2.5H2z"></path>
|
||||
<rect x="2" y="11.75" width="4.75" height="1.5" rx=".75" fill="currentColor" opacity="0.55"></rect>
|
||||
</svg>`;
|
||||
}
|
||||
return `<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<rect x="2" y="4" width="12" height="1.5" rx=".75" fill="currentColor"></rect>
|
||||
<rect x="2" y="7.25" width="12" height="1.5" rx=".75" fill="currentColor" opacity="0.82"></rect>
|
||||
<rect x="2" y="10.5" width="12" height="1.5" rx=".75" fill="currentColor" opacity="0.64"></rect>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function backgroundIcon(active: boolean): string {
|
||||
if (active) {
|
||||
return `<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill="currentColor" opacity="0.4" d="M0 2.25a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 2.25"></path>
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M15 5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zM2.5 9a.5.5 0 0 0 0 1h8a.5.5 0 0 0 0-1zm0-2a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1z" clip-rule="evenodd"></path>
|
||||
<path fill="currentColor" opacity="0.4" d="M0 14.75A.75.75 0 0 1 .75 14h5.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75"></path>
|
||||
</svg>`;
|
||||
}
|
||||
return `<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill="currentColor" opacity="0.22" d="M0 2.25a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 2.25"></path>
|
||||
<path fill="currentColor" opacity="0.22" fill-rule="evenodd" d="M15 5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zM2.5 9a.5.5 0 0 0 0 1h8a.5.5 0 0 0 0-1zm0-2a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1z" clip-rule="evenodd"></path>
|
||||
<path fill="currentColor" opacity="0.22" d="M0 14.75A.75.75 0 0 1 .75 14h5.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75"></path>
|
||||
<path d="M2.5 13.5 13.5 2.5" stroke="currentColor" stroke-width="1.35" stroke-linecap="round"></path>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function themeIcon(theme: DiffTheme): string {
|
||||
if (theme === "dark") {
|
||||
return `<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill="currentColor" d="M10.794 3.647a.217.217 0 0 1 .412 0l.387 1.162c.173.518.58.923 1.097 1.096l1.162.388a.217.217 0 0 1 0 .412l-1.162.386a1.73 1.73 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.74 1.74 0 0 0 9.31 7.092l-1.162-.386a.217.217 0 0 1 0-.412l1.162-.388a1.73 1.73 0 0 0 1.097-1.096zM13.863.598a.144.144 0 0 1 .221-.071.14.14 0 0 1 .053.07l.258.775c.115.345.386.616.732.731l.774.258a.145.145 0 0 1 0 .274l-.774.259a1.16 1.16 0 0 0-.732.732l-.258.773a.145.145 0 0 1-.274 0l-.258-.773a1.16 1.16 0 0 0-.732-.732l-.774-.259a.145.145 0 0 1 0-.273l.774-.259c.346-.115.617-.386.732-.732z"></path>
|
||||
<path fill="currentColor" d="M6.25 1.742a.67.67 0 0 1 .07.75 6.3 6.3 0 0 0-.768 3.028c0 2.746 1.746 5.084 4.193 5.979H1.774A7.2 7.2 0 0 1 1 8.245c0-3.013 1.85-5.598 4.484-6.694a.66.66 0 0 1 .766.19M.75 12.499a.75.75 0 0 0 0 1.5h14.5a.75.75 0 0 0 0-1.5z"></path>
|
||||
</svg>`;
|
||||
}
|
||||
return `<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill="currentColor" d="M6.856.764a.75.75 0 0 1 .094 1.035A5.75 5.75 0 0 0 13.81 10.95a.75.75 0 1 1 1.13.99A7.251 7.251 0 1 1 6.762.858a.75.75 0 0 1 .094-.094"></path>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function createToolbar(): HTMLElement {
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.className = "oc-diff-toolbar";
|
||||
|
||||
toolbar.append(
|
||||
createToolbarButton({
|
||||
title: viewerState.layout === "unified" ? "Switch to split diff" : "Switch to unified diff",
|
||||
active: viewerState.layout === "split",
|
||||
iconMarkup: viewerState.layout === "split" ? splitIcon() : unifiedIcon(),
|
||||
onClick: () => {
|
||||
viewerState.layout = viewerState.layout === "unified" ? "split" : "unified";
|
||||
syncAllControllers();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
toolbar.append(
|
||||
createToolbarButton({
|
||||
title: viewerState.wrapEnabled ? "Disable word wrap" : "Enable word wrap",
|
||||
active: viewerState.wrapEnabled,
|
||||
iconMarkup: wrapIcon(viewerState.wrapEnabled),
|
||||
onClick: () => {
|
||||
viewerState.wrapEnabled = !viewerState.wrapEnabled;
|
||||
syncAllControllers();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
toolbar.append(
|
||||
createToolbarButton({
|
||||
title: viewerState.backgroundEnabled
|
||||
? "Hide background highlights"
|
||||
: "Show background highlights",
|
||||
active: viewerState.backgroundEnabled,
|
||||
iconMarkup: backgroundIcon(viewerState.backgroundEnabled),
|
||||
onClick: () => {
|
||||
viewerState.backgroundEnabled = !viewerState.backgroundEnabled;
|
||||
syncAllControllers();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
toolbar.append(
|
||||
createToolbarButton({
|
||||
title: viewerState.theme === "dark" ? "Switch to light theme" : "Switch to dark theme",
|
||||
active: viewerState.theme === "dark",
|
||||
iconMarkup: themeIcon(viewerState.theme),
|
||||
onClick: () => {
|
||||
viewerState.theme = viewerState.theme === "dark" ? "light" : "dark";
|
||||
syncAllControllers();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
function createRenderOptions(payload: DiffViewerPayload): FileDiffOptions<undefined> {
|
||||
return {
|
||||
theme: payload.options.theme,
|
||||
themeType: viewerState.theme,
|
||||
diffStyle: viewerState.layout,
|
||||
expandUnchanged: payload.options.expandUnchanged,
|
||||
overflow: viewerState.wrapEnabled ? "wrap" : "scroll",
|
||||
disableBackground: !viewerState.backgroundEnabled,
|
||||
unsafeCSS: payload.options.unsafeCSS,
|
||||
renderHeaderMetadata: () => createToolbar(),
|
||||
};
|
||||
}
|
||||
|
||||
function syncDocumentTheme(): void {
|
||||
document.body.dataset.theme = viewerState.theme;
|
||||
}
|
||||
|
||||
function applyState(controller: DiffController): void {
|
||||
controller.diff.setOptions(createRenderOptions(controller.payload));
|
||||
controller.diff.rerender();
|
||||
}
|
||||
|
||||
function syncAllControllers(): void {
|
||||
syncDocumentTheme();
|
||||
for (const controller of controllers) {
|
||||
applyState(controller);
|
||||
}
|
||||
}
|
||||
|
||||
async function hydrateViewer(): Promise<void> {
|
||||
const cards = getCards();
|
||||
const langs = new Set<SupportedLanguages>();
|
||||
const firstPayload = cards[0]?.payload;
|
||||
|
||||
if (firstPayload) {
|
||||
viewerState.theme = firstPayload.options.themeType;
|
||||
viewerState.layout = firstPayload.options.diffStyle;
|
||||
viewerState.wrapEnabled = firstPayload.options.overflow === "wrap";
|
||||
}
|
||||
|
||||
for (const { payload } of cards) {
|
||||
for (const lang of payload.langs) {
|
||||
langs.add(lang);
|
||||
}
|
||||
}
|
||||
|
||||
await preloadHighlighter({
|
||||
themes: ["pierre-light", "pierre-dark"],
|
||||
langs: langs.size > 0 ? [...langs] : ["text"],
|
||||
});
|
||||
|
||||
syncDocumentTheme();
|
||||
|
||||
for (const { host, payload } of cards) {
|
||||
ensureShadowRoot(host);
|
||||
const diff = new FileDiff(createRenderOptions(payload));
|
||||
diff.hydrate({
|
||||
fileContainer: host,
|
||||
prerenderedHTML: payload.prerenderedHTML,
|
||||
...getHydrateProps(payload),
|
||||
});
|
||||
const controller = { payload, diff };
|
||||
controllers.push(controller);
|
||||
applyState(controller);
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
await hydrateViewer();
|
||||
document.documentElement.dataset.openclawDiffsReady = "true";
|
||||
} catch (error) {
|
||||
document.documentElement.dataset.openclawDiffsError = "true";
|
||||
console.error("Failed to hydrate diff viewer", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
void main();
|
||||
});
|
||||
} else {
|
||||
void main();
|
||||
}
|
||||
388
pnpm-lock.yaml
generated
388
pnpm-lock.yaml
generated
@@ -307,6 +307,18 @@ importers:
|
||||
specifier: ^1.39.0
|
||||
version: 1.39.0
|
||||
|
||||
extensions/diffs:
|
||||
dependencies:
|
||||
'@pierre/diffs':
|
||||
specifier: 1.0.11
|
||||
version: 1.0.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@sinclair/typebox':
|
||||
specifier: 0.34.48
|
||||
version: 0.34.48
|
||||
playwright-core:
|
||||
specifier: 1.58.2
|
||||
version: 1.58.2
|
||||
|
||||
extensions/discord: {}
|
||||
|
||||
extensions/feishu:
|
||||
@@ -2153,6 +2165,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@pierre/diffs@1.0.11':
|
||||
resolution: {integrity: sha512-j6zIEoyImQy1HfcJqbrDwP0O5I7V2VNXAaw53FqQ+SykRfaNwABeZHs9uibXO4supaXPmTx6LEH9Lffr03e1Tw==}
|
||||
peerDependencies:
|
||||
react: ^18.3.1 || ^19.0.0
|
||||
react-dom: ^18.3.1 || ^19.0.0
|
||||
|
||||
'@pinojs/redact@0.4.0':
|
||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||
|
||||
@@ -2465,6 +2483,30 @@ packages:
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@shikijs/core@3.23.0':
|
||||
resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==}
|
||||
|
||||
'@shikijs/engine-javascript@3.23.0':
|
||||
resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==}
|
||||
|
||||
'@shikijs/engine-oniguruma@3.23.0':
|
||||
resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==}
|
||||
|
||||
'@shikijs/langs@3.23.0':
|
||||
resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==}
|
||||
|
||||
'@shikijs/themes@3.23.0':
|
||||
resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==}
|
||||
|
||||
'@shikijs/transformers@3.23.0':
|
||||
resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==}
|
||||
|
||||
'@shikijs/types@3.23.0':
|
||||
resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==}
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
|
||||
'@silvia-odwyer/photon-node@0.3.4':
|
||||
resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==}
|
||||
|
||||
@@ -2868,6 +2910,9 @@ packages:
|
||||
'@types/express@5.0.6':
|
||||
resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
'@types/http-errors@2.0.5':
|
||||
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||
|
||||
@@ -2886,6 +2931,9 @@ packages:
|
||||
'@types/markdown-it@14.1.2':
|
||||
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||
|
||||
'@types/mdurl@2.0.0':
|
||||
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||
|
||||
@@ -2943,6 +2991,9 @@ packages:
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
@@ -2992,6 +3043,9 @@ packages:
|
||||
resolution: {integrity: sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
'@urbit/aura@3.0.0':
|
||||
resolution: {integrity: sha512-N8/FHc/lmlMDCumMuTXyRHCxlov5KZY6unmJ9QR2GOw+OpROZMBsXYGwE+ZMtvN21ql9+Xb8KhGNBj08IrG3Wg==}
|
||||
engines: {node: '>=16', npm: '>=8'}
|
||||
@@ -3077,8 +3131,8 @@ packages:
|
||||
link-preview-js:
|
||||
optional: true
|
||||
|
||||
'@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
|
||||
resolution: {tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67}
|
||||
'@whiskeysockets/libsignal-node@git+https://github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
|
||||
resolution: {commit: 1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67, repo: https://github.com/whiskeysockets/libsignal-node.git, type: git}
|
||||
version: 2.0.1
|
||||
|
||||
abbrev@1.1.1:
|
||||
@@ -3344,6 +3398,9 @@ packages:
|
||||
caseless@0.12.0:
|
||||
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
chai@6.2.2:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3360,6 +3417,12 @@ packages:
|
||||
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
character-entities-html4@2.1.0:
|
||||
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
|
||||
|
||||
character-entities-legacy@3.0.0:
|
||||
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
|
||||
|
||||
chmodrp@1.0.2:
|
||||
resolution: {integrity: sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w==}
|
||||
|
||||
@@ -3425,6 +3488,9 @@ packages:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
command-line-args@5.2.1:
|
||||
resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
@@ -3553,6 +3619,10 @@ packages:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
destroy@1.2.0:
|
||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
@@ -3561,6 +3631,9 @@ packages:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
|
||||
diff@8.0.3:
|
||||
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
@@ -3981,6 +4054,12 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hast-util-to-html@9.0.5:
|
||||
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
|
||||
highlight.js@10.7.3:
|
||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||
|
||||
@@ -4008,6 +4087,9 @@ packages:
|
||||
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
htmlencode@0.0.4:
|
||||
resolution: {integrity: sha512-0uDvNVpzj/E2TfvLLyyXhKBRvF1y84aZsyRxRXFsQobnHaL4pcaXk+Y9cnFlvnxrBLeXDNq/VJBD+ngdBgQG1w==}
|
||||
|
||||
@@ -4420,6 +4502,9 @@ packages:
|
||||
lru-memoizer@2.3.0:
|
||||
resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==}
|
||||
|
||||
lru_map@0.4.1:
|
||||
resolution: {integrity: sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
@@ -4452,6 +4537,9 @@ packages:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mdast-util-to-hast@13.2.1:
|
||||
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
|
||||
|
||||
mdurl@2.0.0:
|
||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||
|
||||
@@ -4474,6 +4562,21 @@ packages:
|
||||
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
micromark-util-character@2.1.1:
|
||||
resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
|
||||
|
||||
micromark-util-encode@2.0.1:
|
||||
resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
|
||||
|
||||
micromark-util-sanitize-uri@2.0.1:
|
||||
resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
|
||||
|
||||
micromark-util-symbol@2.0.1:
|
||||
resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
|
||||
|
||||
micromark-util-types@2.0.2:
|
||||
resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -4685,6 +4788,12 @@ packages:
|
||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
oniguruma-parser@0.12.1:
|
||||
resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
|
||||
|
||||
oniguruma-to-es@4.3.4:
|
||||
resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==}
|
||||
|
||||
openai@6.10.0:
|
||||
resolution: {integrity: sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A==}
|
||||
hasBin: true
|
||||
@@ -4941,6 +5050,9 @@ packages:
|
||||
proper-lockfile@4.1.2:
|
||||
resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
|
||||
|
||||
property-information@7.1.0:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
protobufjs@6.8.8:
|
||||
resolution: {integrity: sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==}
|
||||
hasBin: true
|
||||
@@ -5018,6 +5130,15 @@ packages:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
react-dom@19.2.4:
|
||||
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
||||
peerDependencies:
|
||||
react: ^19.2.4
|
||||
|
||||
react@19.2.4:
|
||||
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
@@ -5036,6 +5157,15 @@ packages:
|
||||
reflect-metadata@0.2.2:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
|
||||
regex-recursion@6.0.2:
|
||||
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
|
||||
|
||||
regex-utilities@2.3.0:
|
||||
resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
|
||||
|
||||
regex@6.1.0:
|
||||
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
|
||||
|
||||
request-promise-core@1.1.3:
|
||||
resolution: {integrity: sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -5130,6 +5260,9 @@ packages:
|
||||
sanitize-html@2.17.1:
|
||||
resolution: {integrity: sha512-ehFCW+q1a4CSOWRAdX97BX/6/PDEkCqw7/0JXZAGQV57FQB3YOkTa/rrzHPeJ+Aghy4vZAFfWMYyfxIiB7F/gw==}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
selderee@0.11.0:
|
||||
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
|
||||
|
||||
@@ -5179,6 +5312,9 @@ packages:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shiki@3.23.0:
|
||||
resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==}
|
||||
|
||||
side-channel-list@1.0.0:
|
||||
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5268,6 +5404,9 @@ packages:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
split2@4.2.0:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
@@ -5359,6 +5498,9 @@ packages:
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -5453,6 +5595,9 @@ packages:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
|
||||
trim-lines@3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
|
||||
ts-algebra@2.0.0:
|
||||
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||
|
||||
@@ -5550,6 +5695,21 @@ packages:
|
||||
resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unist-util-is@6.0.1:
|
||||
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
|
||||
|
||||
unist-util-position@5.0.0:
|
||||
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
|
||||
|
||||
unist-util-stringify-position@4.0.0:
|
||||
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
|
||||
|
||||
unist-util-visit-parents@6.0.2:
|
||||
resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
|
||||
|
||||
unist-util-visit@5.1.0:
|
||||
resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
|
||||
|
||||
universal-github-app-jwt@2.2.2:
|
||||
resolution: {integrity: sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==}
|
||||
|
||||
@@ -5611,6 +5771,12 @@ packages:
|
||||
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
|
||||
engines: {'0': node >=0.6.0}
|
||||
|
||||
vfile-message@4.0.3:
|
||||
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
||||
|
||||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
vite@7.3.1:
|
||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -5796,6 +5962,9 @@ packages:
|
||||
zod@4.3.6:
|
||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||
|
||||
zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@agentclientprotocol/sdk@0.14.1(zod@4.3.6)':
|
||||
@@ -7728,6 +7897,18 @@ snapshots:
|
||||
'@oxlint/binding-win32-x64-msvc@1.50.0':
|
||||
optional: true
|
||||
|
||||
'@pierre/diffs@1.0.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@shikijs/core': 3.23.0
|
||||
'@shikijs/engine-javascript': 3.23.0
|
||||
'@shikijs/transformers': 3.23.0
|
||||
diff: 8.0.3
|
||||
hast-util-to-html: 9.0.5
|
||||
lru_map: 0.4.1
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
shiki: 3.23.0
|
||||
|
||||
'@pinojs/redact@0.4.0': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
@@ -7934,6 +8115,44 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@shikijs/core@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.23.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-html: 9.0.5
|
||||
|
||||
'@shikijs/engine-javascript@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.23.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
oniguruma-to-es: 4.3.4
|
||||
|
||||
'@shikijs/engine-oniguruma@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.23.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@shikijs/langs@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.23.0
|
||||
|
||||
'@shikijs/themes@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.23.0
|
||||
|
||||
'@shikijs/transformers@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/core': 3.23.0
|
||||
'@shikijs/types': 3.23.0
|
||||
|
||||
'@shikijs/types@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2': {}
|
||||
|
||||
'@silvia-odwyer/photon-node@0.3.4': {}
|
||||
|
||||
'@sinclair/typebox@0.34.48': {}
|
||||
@@ -8509,6 +8728,10 @@ snapshots:
|
||||
'@types/express-serve-static-core': 5.1.1
|
||||
'@types/serve-static': 2.2.0
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/http-errors@2.0.5': {}
|
||||
|
||||
'@types/jsesc@2.5.1': {}
|
||||
@@ -8527,6 +8750,10 @@ snapshots:
|
||||
'@types/linkify-it': 5.0.0
|
||||
'@types/mdurl': 2.0.0
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/mdurl@2.0.0': {}
|
||||
|
||||
'@types/mime-types@2.1.4': {}
|
||||
@@ -8588,6 +8815,8 @@ snapshots:
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.3.1
|
||||
@@ -8636,6 +8865,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@urbit/aura@3.0.0': {}
|
||||
|
||||
'@vector-im/matrix-bot-sdk@0.8.0-element.3(@cypress/request@3.0.10)':
|
||||
@@ -8776,7 +9007,7 @@ snapshots:
|
||||
'@cacheable/node-cache': 1.7.6
|
||||
'@hapi/boom': 9.1.4
|
||||
async-mutex: 0.5.0
|
||||
libsignal: '@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67'
|
||||
libsignal: '@whiskeysockets/libsignal-node@git+https://github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67'
|
||||
lru-cache: 11.2.6
|
||||
music-metadata: 11.12.1
|
||||
p-queue: 9.1.0
|
||||
@@ -8791,7 +9022,7 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
|
||||
'@whiskeysockets/libsignal-node@git+https://github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
|
||||
dependencies:
|
||||
curve25519-js: 0.0.4
|
||||
protobufjs: 6.8.8
|
||||
@@ -9066,6 +9297,8 @@ snapshots:
|
||||
|
||||
caseless@0.12.0: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
|
||||
chalk-template@0.4.0:
|
||||
@@ -9079,6 +9312,10 @@ snapshots:
|
||||
|
||||
chalk@5.6.2: {}
|
||||
|
||||
character-entities-html4@2.1.0: {}
|
||||
|
||||
character-entities-legacy@3.0.0: {}
|
||||
|
||||
chmodrp@1.0.2: {}
|
||||
|
||||
chokidar@5.0.0:
|
||||
@@ -9150,6 +9387,8 @@ snapshots:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
command-line-args@5.2.1:
|
||||
dependencies:
|
||||
array-back: 3.1.0
|
||||
@@ -9248,10 +9487,16 @@ snapshots:
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
destroy@1.2.0: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
devlop@1.1.0:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
diff@8.0.3: {}
|
||||
|
||||
discord-api-types@0.38.37: {}
|
||||
@@ -9785,6 +10030,24 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hast-util-to-html@9.0.5:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
ccount: 2.0.1
|
||||
comma-separated-tokens: 2.0.3
|
||||
hast-util-whitespace: 3.0.0
|
||||
html-void-elements: 3.0.0
|
||||
mdast-util-to-hast: 13.2.1
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
stringify-entities: 4.0.4
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
highlight.js@10.7.3: {}
|
||||
|
||||
hono@4.11.10:
|
||||
@@ -9810,6 +10073,8 @@ snapshots:
|
||||
htmlparser2: 8.0.2
|
||||
selderee: 0.11.0
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
htmlencode@0.0.4: {}
|
||||
|
||||
htmlparser2@10.1.0:
|
||||
@@ -10234,6 +10499,8 @@ snapshots:
|
||||
lodash.clonedeep: 4.5.0
|
||||
lru-cache: 6.0.0
|
||||
|
||||
lru_map@0.4.1: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -10268,6 +10535,18 @@ snapshots:
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mdast-util-to-hast@13.2.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
devlop: 1.1.0
|
||||
micromark-util-sanitize-uri: 2.0.1
|
||||
trim-lines: 3.0.1
|
||||
unist-util-position: 5.0.0
|
||||
unist-util-visit: 5.1.0
|
||||
vfile: 6.0.3
|
||||
|
||||
mdurl@2.0.0: {}
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
@@ -10280,6 +10559,23 @@ snapshots:
|
||||
|
||||
methods@1.1.2: {}
|
||||
|
||||
micromark-util-character@2.1.1:
|
||||
dependencies:
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-util-encode@2.0.1: {}
|
||||
|
||||
micromark-util-sanitize-uri@2.0.1:
|
||||
dependencies:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-encode: 2.0.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
|
||||
micromark-util-symbol@2.0.1: {}
|
||||
|
||||
micromark-util-types@2.0.2: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-db@1.54.0: {}
|
||||
@@ -10528,6 +10824,14 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-function: 5.0.1
|
||||
|
||||
oniguruma-parser@0.12.1: {}
|
||||
|
||||
oniguruma-to-es@4.3.4:
|
||||
dependencies:
|
||||
oniguruma-parser: 0.12.1
|
||||
regex: 6.1.0
|
||||
regex-recursion: 6.0.2
|
||||
|
||||
openai@6.10.0(ws@8.19.0)(zod@4.3.6):
|
||||
optionalDependencies:
|
||||
ws: 8.19.0
|
||||
@@ -10864,6 +11168,8 @@ snapshots:
|
||||
retry: 0.12.0
|
||||
signal-exit: 3.0.7
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
protobufjs@6.8.8:
|
||||
dependencies:
|
||||
'@protobufjs/aspromise': 1.1.2
|
||||
@@ -10987,6 +11293,13 @@ snapshots:
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
|
||||
react-dom@19.2.4(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
scheduler: 0.27.0
|
||||
|
||||
react@19.2.4: {}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
@@ -11010,6 +11323,16 @@ snapshots:
|
||||
|
||||
reflect-metadata@0.2.2: {}
|
||||
|
||||
regex-recursion@6.0.2:
|
||||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
|
||||
regex-utilities@2.3.0: {}
|
||||
|
||||
regex@6.1.0:
|
||||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
|
||||
request-promise-core@1.1.3(@cypress/request@3.0.10):
|
||||
dependencies:
|
||||
lodash: 4.17.23
|
||||
@@ -11143,6 +11466,8 @@ snapshots:
|
||||
parse-srcset: 1.0.2
|
||||
postcss: 8.5.6
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
selderee@0.11.0:
|
||||
dependencies:
|
||||
parseley: 0.12.1
|
||||
@@ -11248,6 +11573,17 @@ snapshots:
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
shiki@3.23.0:
|
||||
dependencies:
|
||||
'@shikijs/core': 3.23.0
|
||||
'@shikijs/engine-javascript': 3.23.0
|
||||
'@shikijs/engine-oniguruma': 3.23.0
|
||||
'@shikijs/langs': 3.23.0
|
||||
'@shikijs/themes': 3.23.0
|
||||
'@shikijs/types': 3.23.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
side-channel-list@1.0.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -11355,6 +11691,8 @@ snapshots:
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
split2@4.2.0: {}
|
||||
|
||||
sqlite-vec-darwin-arm64@0.1.7-alpha.2:
|
||||
@@ -11456,6 +11794,11 @@ snapshots:
|
||||
safe-buffer: 5.2.1
|
||||
optional: true
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
dependencies:
|
||||
character-entities-html4: 2.1.0
|
||||
character-entities-legacy: 3.0.0
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
@@ -11552,6 +11895,8 @@ snapshots:
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
trim-lines@3.0.1: {}
|
||||
|
||||
ts-algebra@2.0.0: {}
|
||||
|
||||
tsdown@0.20.3(@typescript/native-preview@7.0.0-dev.20260225.1)(typescript@5.9.3):
|
||||
@@ -11636,6 +11981,29 @@ snapshots:
|
||||
|
||||
undici@7.22.0: {}
|
||||
|
||||
unist-util-is@6.0.1:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-position@5.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-stringify-position@4.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-visit-parents@6.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-is: 6.0.1
|
||||
|
||||
unist-util-visit@5.1.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-is: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
universal-github-app-jwt@2.2.2: {}
|
||||
|
||||
universal-user-agent@7.0.3: {}
|
||||
@@ -11675,6 +12043,16 @@ snapshots:
|
||||
core-util-is: 1.0.2
|
||||
extsprintf: 1.3.0
|
||||
|
||||
vfile-message@4.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-stringify-position: 4.0.0
|
||||
|
||||
vfile@6.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite@7.3.1(@types/node@25.3.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
@@ -11829,3 +12207,5 @@ snapshots:
|
||||
zod@3.25.76: {}
|
||||
|
||||
zod@4.3.6: {}
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
@@ -7,6 +7,7 @@ export function createMockServerResponse(): ServerResponse & { body?: string } {
|
||||
statusCode: number;
|
||||
body?: string;
|
||||
setHeader: (key: string, value: string) => unknown;
|
||||
getHeader: (key: string) => string | undefined;
|
||||
end: (body?: string) => unknown;
|
||||
} = {
|
||||
headersSent: false,
|
||||
@@ -15,6 +16,7 @@ export function createMockServerResponse(): ServerResponse & { body?: string } {
|
||||
headers[key.toLowerCase()] = value;
|
||||
return res;
|
||||
},
|
||||
getHeader: (key: string) => headers[key.toLowerCase()],
|
||||
end: (body?: string) => {
|
||||
res.headersSent = true;
|
||||
res.body = body;
|
||||
|
||||
Reference in New Issue
Block a user