Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/diffs/**"
- "extensions/diffs-language-pack/**"
"extensions: elevenlabs":
- changed-files:
- any-glob-to-any-file:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ extensions/qa-lab/web/dist/
# Generated bundled plugin runtime dependency manifests
extensions/**/.openclaw-runtime-deps.json
extensions/**/.openclaw-runtime-deps-stamp.json
extensions/diffs-language-pack/assets/viewer-runtime.js

# Output dir for scripts/run-opengrep.sh (local opengrep scans)
/.opengrep-out/
Expand Down
1 change: 1 addition & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"dist-runtime/",
"docs/_layouts/",
"extensions/diffs/assets/viewer-runtime.js",
"extensions/diffs-language-pack/assets/viewer-runtime.js",
"node_modules/",
"patches/",
"pnpm-lock.yaml",
Expand Down
1 change: 1 addition & 0 deletions docs/plugins/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ or npm install metadata. Those belong in your plugin code and `package.json`.
| ------------------------------------ | -------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | Yes | `string` | Canonical plugin id. This is the id used in `plugins.entries.<id>`. |
| `configSchema` | Yes | `object` | Inline JSON Schema for this plugin's config. |
| `requiresPlugins` | No | `string[]` | Plugin ids that must also be installed for this plugin to have an effect. Discovery keeps the plugin loadable but warns when any required plugin is missing. |
| `enabledByDefault` | No | `true` | Marks a bundled plugin as enabled by default. Omit it, or set any non-`true` value, to leave the plugin disabled by default. |
| `enabledByDefaultOnPlatforms` | No | `string[]` | Marks a bundled plugin as enabled by default only on the listed Node.js platforms, for example `["darwin"]`. Explicit config still wins. |
| `legacyPluginIds` | No | `string[]` | Legacy ids that normalize to this canonical plugin id. |
Expand Down
1 change: 1 addition & 0 deletions docs/plugins/plugin-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ commands.
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |
| [diagnostics-prometheus](/plugins/reference/diagnostics-prometheus) | OpenClaw diagnostics Prometheus exporter. | `@openclaw/diagnostics-prometheus`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus` | plugin |
| [diffs](/plugins/reference/diffs) | Read-only diff viewer and file renderer for agents. | `@openclaw/diffs`<br />npm; ClawHub | contracts: tools; skills |
| [diffs-language-pack](/plugins/reference/diffs-language-pack) | Adds syntax highlighting for languages outside the default diffs viewer set. | `@openclaw/diffs-language-pack`<br />npm; ClawHub: `clawhub:@openclaw/diffs-language-pack` | plugin |
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />npm; ClawHub | channels: discord; contracts: transcriptSourceProviders |
| [feishu](/plugins/reference/feishu) | Adds the Feishu channel surface for sending and receiving OpenClaw messages. | `@openclaw/feishu`<br />npm; ClawHub | channels: feishu; contracts: tools; skills |
| [google-meet](/plugins/reference/google-meet) | Join Google Meet calls through Chrome or Twilio transports. | `@openclaw/google-meet`<br />npm; ClawHub | contracts: tools |
Expand Down
1 change: 1 addition & 0 deletions docs/plugins/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pnpm plugins:inventory:gen
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |
| [diagnostics-prometheus](/plugins/reference/diagnostics-prometheus) | OpenClaw diagnostics Prometheus exporter. | `@openclaw/diagnostics-prometheus`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus` | plugin |
| [diffs](/plugins/reference/diffs) | Read-only diff viewer and file renderer for agents. | `@openclaw/diffs`<br />npm; ClawHub | contracts: tools; skills |
| [diffs-language-pack](/plugins/reference/diffs-language-pack) | Adds syntax highlighting for languages outside the default diffs viewer set. | `@openclaw/diffs-language-pack`<br />npm; ClawHub: `clawhub:@openclaw/diffs-language-pack` | plugin |
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />npm; ClawHub | channels: discord; contracts: transcriptSourceProviders |
| [document-extract](/plugins/reference/document-extract) | Extract text and fallback page images from local document attachments. | `@openclaw/document-extract-plugin`<br />included in OpenClaw | contracts: documentExtractors |
| [duckduckgo](/plugins/reference/duckduckgo) | Adds web search provider support. | `@openclaw/duckduckgo-plugin`<br />included in OpenClaw | contracts: webSearchProviders |
Expand Down
19 changes: 19 additions & 0 deletions docs/plugins/reference/diffs-language-pack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
summary: "Adds syntax highlighting for languages outside the default diffs viewer set."
read_when:
- You are installing, configuring, or auditing the diffs-language-pack plugin
title: "Diffs Language Pack plugin"
---

# Diffs Language Pack plugin

Adds syntax highlighting for languages outside the default diffs viewer set.

## Distribution

- Package: `@openclaw/diffs-language-pack`
- Install route: npm; ClawHub: `clawhub:@openclaw/diffs-language-pack`

## Surface

plugin
21 changes: 20 additions & 1 deletion docs/tools/diffs.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,10 @@ All fields are optional unless noted.
Display filename for before and after mode.
</ParamField>
<ParamField path="lang" type="string">
Language override hint for before and after mode. Unknown values fall back to plain text.
Language override hint for before and after mode. Unknown values and languages outside the default viewer set fall back to plain text unless the
Diff Viewer Language Pack plugin is installed.
</ParamField>

<ParamField path="title" type="string">
Viewer title override.
</ParamField>
Expand Down Expand Up @@ -200,6 +202,22 @@ All fields are optional unless noted.
</Accordion>
</AccordionGroup>

## Syntax highlighting

OpenClaw includes syntax highlighting for common source, config, and documentation languages:

`javascript`, `typescript`, `tsx`, `jsx`, `json`, `markdown`, `yaml`, `css`, `html`, `sh`, `python`, `go`, `rust`, `java`, `c`, `cpp`, `csharp`, `php`, `sql`, and `docker`.

Common aliases such as `js`, `ts`, `bash`, `md`, `yml`, `c++`, and `dockerfile` are normalized to those default languages.

Install the Diff Viewer Language Pack plugin to highlight other languages:

```bash
openclaw plugins install diffs-language-pack
```

With the language pack available, OpenClaw automatically uses it for languages outside the default list. Without it, those files stay readable as plain text.

## Output details contract

The tool returns structured metadata under `details`.
Expand Down Expand Up @@ -385,6 +403,7 @@ Viewer assets:

- `/plugins/diffs/assets/viewer.js`
- `/plugins/diffs/assets/viewer-runtime.js`
- `/plugins/diffs-language-pack/assets/viewer.js` when the diff uses a language from the Diff Viewer Language Pack

The viewer document resolves those assets relative to the viewer URL, so an optional `baseUrl` path prefix is preserved for both asset requests too.

Expand Down
6 changes: 6 additions & 0 deletions extensions/diffs-language-pack/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {
definePluginEntry,
type OpenClawPluginApi,
type OpenClawPluginHttpRouteHandler,
type PluginLogger,
} from "openclaw/plugin-sdk/plugin-entry";
9 changes: 9 additions & 0 deletions extensions/diffs-language-pack/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { definePluginEntry } from "./api.js";
import { registerDiffsLanguagePackPlugin } from "./src/plugin.js";

export default definePluginEntry({
id: "diffs-language-pack",
name: "Diff Viewer Language Pack",
description: "Adds syntax highlighting for languages outside the default diffs viewer set.",
register: registerDiffsLanguagePackPlugin,
});
12 changes: 12 additions & 0 deletions extensions/diffs-language-pack/npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions extensions/diffs-language-pack/openclaw.plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"id": "diffs-language-pack",
"requiresPlugins": ["diffs"],
"activation": {
"onStartup": true
},
"name": "Diff Viewer Language Pack",
"description": "Adds syntax highlighting for languages outside the default diffs viewer set.",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
47 changes: 47 additions & 0 deletions extensions/diffs-language-pack/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@openclaw/diffs-language-pack",
"version": "2026.5.26",
"description": "OpenClaw diffs viewer syntax highlighting language pack",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"scripts": {
"build:viewer": "node scripts/build-viewer.mjs"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
],
"install": {
"npmSpec": "@openclaw/diffs-language-pack",
"clawhubSpec": "clawhub:@openclaw/diffs-language-pack",
"localPath": "extensions/diffs-language-pack",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.30"
},
"compat": {
"pluginApi": ">=2026.5.26"
},
"assetScripts": {
"build": "pnpm build:viewer"
},
"build": {
"openclawVersion": "2026.5.26",
"staticAssets": [
{
"source": "./assets/viewer-runtime.js",
"output": "assets/viewer-runtime.js"
}
]
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}
15 changes: 15 additions & 0 deletions extensions/diffs-language-pack/scripts/build-viewer.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node

import { spawnSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";

const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = path.resolve(pluginRoot, "../..");
const scriptPath = path.join(repoRoot, "scripts", "build-diffs-viewer-runtime.mjs");

const result = spawnSync(process.execPath, [scriptPath, "full"], {
cwd: repoRoot,
stdio: "inherit",
});
process.exit(result.status ?? 1);
64 changes: 64 additions & 0 deletions extensions/diffs-language-pack/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawPluginApi } from "../api.js";
import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js";

export function registerDiffsLanguagePackPlugin(api: OpenClawPluginApi): void {
api.registerHttpRoute({
path: "/plugins/diffs-language-pack",
auth: "plugin",
match: "prefix",
handler: createDiffsLanguagePackHttpHandler(),
});
}

function createDiffsLanguagePackHttpHandler() {
return async (req: IncomingMessage, res: ServerResponse): Promise<boolean> => {
const parsed = parseRequestUrl(req.url);
if (!parsed?.pathname.startsWith(VIEWER_ASSET_PREFIX)) {
return false;
}
if (req.method !== "GET" && req.method !== "HEAD") {
respondText(res, 405, "Method not allowed");
return true;
}

const asset = await getServedViewerAsset(parsed.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;
};
}

function parseRequestUrl(rawUrl?: string): URL | null {
if (!rawUrl) {
return null;
}
try {
return new URL(rawUrl, "http://127.0.0.1");
} catch {
return null;
}
}

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");
}
90 changes: 90 additions & 0 deletions extensions/diffs-language-pack/src/viewer-assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import { fileURLToPath } from "node:url";

export const VIEWER_ASSET_PREFIX = "/plugins/diffs-language-pack/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_RELATIVE_IMPORT_PATH = "./viewer-runtime.js";
const VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS = [
"./assets/viewer-runtime.js",
"../assets/viewer-runtime.js",
] as const;

type ServedViewerAsset = {
body: string | Buffer;
contentType: string;
};

type RuntimeAssetCache = {
mtimeMs: number;
runtimeBody: Buffer;
loaderBody: string;
};

let runtimeAssetCache: RuntimeAssetCache | null = null;

function isMissingFileError(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error && "code" in error && error.code === "ENOENT";
}

export async function resolveViewerRuntimeFileUrl(): Promise<URL> {
let missingFileError: NodeJS.ErrnoException | null = null;

for (const relativePath of VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS) {
const candidateUrl = new URL(relativePath, import.meta.url);
try {
await fs.stat(fileURLToPath(candidateUrl));
return candidateUrl;
} catch (error) {
if (isMissingFileError(error)) {
missingFileError = error;
continue;
}
throw error;
}
}

if (missingFileError) {
throw missingFileError;
}

throw new Error("viewer runtime asset candidates were not checked");
}

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",
};
}

return {
body: assets.runtimeBody,
contentType: "text/javascript; charset=utf-8",
};
}

async function loadViewerAssets(): Promise<RuntimeAssetCache> {
const runtimeUrl = await resolveViewerRuntimeFileUrl();
const runtimePath = fileURLToPath(runtimeUrl);
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_RELATIVE_IMPORT_PATH}?v=${hash}";\n`,
};
return runtimeAssetCache;
}
Loading
Loading