diff --git a/bun.lock b/bun.lock index 0db26632..071b94b8 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ "@fontsource/noto-sans-sc": "5.3.0", "@monaco-editor/react": "4.7.0", "@openclaw/carapace": "git+https://github.com/openclaw/carapace.git#v0.2.0", - "@openclaw/plugin-inspector": "0.3.17", + "@openclaw/plugin-inspector": "github:openclaw/plugin-inspector#021c6586af78884d27f1be8acedca492acd9f9ae", "@radix-ui/react-avatar": "1.2.3", "@radix-ui/react-dialog": "1.1.20", "@radix-ui/react-dropdown-menu": "2.1.21", @@ -108,7 +108,7 @@ }, "dependencies": { "@clack/prompts": "1.7.0", - "@openclaw/plugin-inspector": "0.3.17", + "@openclaw/plugin-inspector": "github:openclaw/plugin-inspector#021c6586af78884d27f1be8acedca492acd9f9ae", "arktype": "2.2.3", "commander": "15.0.0", "croner": "10.0.1", @@ -404,6 +404,8 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -452,7 +454,7 @@ "@openclaw/clawhub-admin": ["@openclaw/clawhub-admin@workspace:packages/clawhub-admin"], - "@openclaw/plugin-inspector": ["@openclaw/plugin-inspector@0.3.17", "", { "bin": { "plugin-inspector": "src/cli.js" } }, "sha512-JPPHPhiXMsIvrV8UR8RQjhflMjRZX/uIhy9meE81dup7MMSnRJcsTGOXYACohv6e4z2P95z2QuE7nZkWT6Ysuw=="], + "@openclaw/plugin-inspector": ["@openclaw/plugin-inspector@github:openclaw/plugin-inspector#021c658", { "dependencies": { "semver": "^7.8.5", "tar": "^7.5.22" }, "bin": { "plugin-inspector": "src/cli.js" } }, "openclaw-plugin-inspector-021c658", "sha512-Cz3NDscaCxvySTNmBz1mQnImKHWdmk50da1VhZnQYKE/EK3qlV/Z/UpYo/7440EEQJ7TQ90V5Eyr6I2811dp6Q=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -1096,6 +1098,8 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + "citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], @@ -1606,6 +1610,8 @@ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "monaco-editor": ["monaco-editor@0.56.0", "", { "dependencies": { "dompurify": "3.4.8", "marked": "14.0.0" } }, "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1882,6 +1888,8 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], + "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -2002,7 +2010,7 @@ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], @@ -2146,6 +2154,8 @@ "vitest/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], "@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], diff --git a/convex/devSeed.localFixtures.test.ts b/convex/devSeed.localFixtures.test.ts index ff357839..99072338 100644 --- a/convex/devSeed.localFixtures.test.ts +++ b/convex/devSeed.localFixtures.test.ts @@ -1374,12 +1374,20 @@ describe("devSeed local fixtures", () => { packageName: scannedPluginName, findingKind: "warning", code: "legacy-before-agent-start", + targetOpenClawVersion: "2026.3.24-beta.2", + authorRemediation: { + summary: "Replace the legacy before_agent_start hook with the current lifecycle API.", + }, }), expect.objectContaining({ packageName: scannedPluginName, findingKind: "error", code: "missing-expected-seam", scanSource: "nightly", + targetOpenClawVersion: "2026.4.0", + authorRemediation: { + summary: "Replace registerTool with an API available in the selected OpenClaw version.", + }, }), ]), ); diff --git a/convex/devSeed.ts b/convex/devSeed.ts index a22b94e0..4046dbdb 100644 --- a/convex/devSeed.ts +++ b/convex/devSeed.ts @@ -3703,6 +3703,9 @@ export async function seedLocalModerationFixturesHandler( deprecated: true, message: "legacy before_agent_start hook is deprecated for the current OpenClaw plugin API", evidence: ["src/index.ts:4", "hook:before_agent_start"], + authorRemediation: { + summary: "Replace the legacy before_agent_start hook with the current lifecycle API.", + }, inspectorFindingId: "local-scanned-runtime-plugin:legacy-before-agent-start", createdAt: now, }); @@ -3723,6 +3726,9 @@ export async function seedLocalModerationFixturesHandler( issueClass: "compatibility-error", message: "registerTool is no longer available on the target OpenClaw compatibility surface", evidence: ["src/index.ts:12", "target:OpenClaw 2026.4.0"], + authorRemediation: { + summary: "Replace registerTool with an API available in the selected OpenClaw version.", + }, inspectorFindingId: "local-scanned-runtime-plugin:missing-expected-seam", createdAt: now + 1, }); diff --git a/convex/lib/emails.test.ts b/convex/lib/emails.test.ts index 0e45bb8a..982deaac 100644 --- a/convex/lib/emails.test.ts +++ b/convex/lib/emails.test.ts @@ -306,7 +306,9 @@ describe("moderation notification email copy", () => { expect(email.text).toContain("OpenClaw Version: 0.9.0"); expect(email.text).toContain("Address the findings below in your plugin package."); expect(email.text).toContain("Run the validation command locally against your changes."); - expect(email.text).toContain("clawhub package validate "); + expect(email.text).toContain( + "clawhub package validate --openclaw-version 0.9.0", + ); expect(email.text).toContain( "- **WARNING** `legacy-before-agent-start` (deprecation-warning, P2)", ); @@ -330,7 +332,9 @@ describe("moderation notification email copy", () => { expectFooterLinksUnderlined(email.html); expect(email.html).toContain("OpenClaw Version"); expect(email.html).toContain("0.9.0"); - expect(email.html).toContain("clawhub package validate <path-to-plugin>"); + expect(email.html).toContain( + "clawhub package validate <path-to-plugin> --openclaw-version 0.9.0", + ); expect(email.html).toContain("legacy-before-agent-start"); expect(email.html).toContain("legacy-before-agent-start · deprecation-warning · P2"); expect(email.html).toContain("Fix"); @@ -350,6 +354,49 @@ describe("moderation notification email copy", () => { expect(email.html).not.toContain("published successfully"); }); + it("includes one exact validation command per recorded OpenClaw target", async () => { + const email = await buildPackageInspectorFindingsEmail({ + packageName: "demo-plugin", + version: "1.0.0", + findings: [ + { + findingKind: "warning", + code: "legacy-before-agent-start", + message: "legacy hook is deprecated", + targetOpenClawVersion: "0.9.0", + }, + { + findingKind: "error", + code: "missing-expected-seam", + message: "registerTool is no longer available", + targetOpenClawVersion: "0.10.0", + }, + ], + }); + + expect(email.text).toContain("OpenClaw Versions: 0.9.0, 0.10.0"); + expect(email.text).toContain( + "clawhub package validate --openclaw-version 0.9.0", + ); + expect(email.text).toContain( + "clawhub package validate --openclaw-version 0.10.0", + ); + expect(email.text).toContain( + "`legacy-before-agent-start`\n legacy hook is deprecated\n OpenClaw target: 0.9.0", + ); + expect(email.text).toContain( + "`missing-expected-seam`\n registerTool is no longer available\n OpenClaw target: 0.10.0", + ); + expect(email.html).toContain( + "clawhub package validate <path-to-plugin> --openclaw-version 0.9.0", + ); + expect(email.html).toContain( + "clawhub package validate <path-to-plugin> --openclaw-version 0.10.0", + ); + expect(email.html).toContain("legacy-before-agent-start · OpenClaw 0.9.0"); + expect(email.html).toContain("missing-expected-seam · OpenClaw 0.10.0"); + }); + it("builds plugin inspector error copy without publish-time wording", async () => { const email = await buildPackageInspectorFindingsEmail({ packageName: "demo-plugin", @@ -372,7 +419,9 @@ describe("moderation notification email copy", () => { expect(email.text).toContain("We found 1 issue with version 1.0.1 of demo-plugin."); expect(email.text).toContain("Address the findings below in your plugin package."); expect(email.text).toContain("Run the validation command locally against your changes."); - expect(email.text).toContain("clawhub package validate "); + expect(email.text).toContain( + "clawhub package validate --openclaw-version 0.10.0", + ); expect(email.text).toContain("- **ERROR** `missing-expected-seam` (compatibility-error, P0)"); expect(email.text).not.toContain("Your plugin was published"); expect(email.text).not.toContain("was published, but"); diff --git a/convex/lib/emails.ts b/convex/lib/emails.ts index a89ea759..11021825 100644 --- a/convex/lib/emails.ts +++ b/convex/lib/emails.ts @@ -324,8 +324,9 @@ async function renderSecretBlockedPublishTemplate(args: { return rendered.html; } -function buildPluginValidateCommand() { - return "clawhub package validate "; +function buildPluginValidateCommand(openClawVersion?: string) { + const command = "clawhub package validate "; + return openClawVersion ? `${command} --openclaw-version ${openClawVersion}` : command; } function normalizeEmailFindingSummary(value: string | undefined) { @@ -534,10 +535,16 @@ export async function buildSecretBlockedPublishEmail(args: SecretBlockedPublishE } export async function buildPackageInspectorFindingsEmail(args: PackageInspectorFindingsEmailArgs) { - const targetOpenClawVersion = args.findings.find( - (finding) => finding.targetOpenClawVersion, - )?.targetOpenClawVersion; - const validateCommand = buildPluginValidateCommand(); + const targetOpenClawVersions = Array.from( + new Set( + args.findings + .map((finding) => finding.targetOpenClawVersion?.trim()) + .filter((target): target is string => Boolean(target)), + ), + ); + const validateCommands = targetOpenClawVersions.length + ? targetOpenClawVersions.map(buildPluginValidateCommand) + : [buildPluginValidateCommand()]; const subject = `Plugin Inspector findings for ${args.packageName}@${args.version}`; const findingCount = args.findings.length; const intro = `We found ${findingCount} ${findingCount === 1 ? "issue" : "issues"} with version ${args.version} of ${args.packageName}.`; @@ -549,7 +556,9 @@ export async function buildPackageInspectorFindingsEmail(args: PackageInspectorF const findingLines = formatPackageInspectorFindingsText(args.findings); const metadataLines = [ `Plugin: ${args.packageName}@${args.version}`, - targetOpenClawVersion ? `OpenClaw Version: ${targetOpenClawVersion}` : null, + targetOpenClawVersions.length + ? `OpenClaw Version${targetOpenClawVersions.length === 1 ? "" : "s"}: ${targetOpenClawVersions.join(", ")}` + : null, ].filter((line): line is string => line !== null); const lines = [ greeting(args.handle), @@ -565,23 +574,28 @@ export async function buildPackageInspectorFindingsEmail(args: PackageInspectorF ...findingLines, "", "Validate a local fix:", - validateCommand, + ...validateCommands, ]; const { renderPluginInspectorFindingsEmail } = await import("./emailRendering"); const rendered = await renderPluginInspectorFindingsEmail({ packageName: args.packageName, version: args.version, - ...(targetOpenClawVersion ? { openClawVersion: targetOpenClawVersion } : {}), + ...(targetOpenClawVersions.length + ? { openClawVersion: targetOpenClawVersions.join(", ") } + : {}), findings: args.findings.map((finding) => ({ code: finding.code, kind: finding.findingKind, meta: [finding.code, finding.issueClass, finding.severity].filter(Boolean).join(" · "), message: finding.message, + ...(finding.targetOpenClawVersion + ? { targetOpenClawVersion: finding.targetOpenClawVersion } + : {}), ...(finding.authorRemediation?.summary ? { fix: finding.authorRemediation.summary } : {}), ...(finding.authorRemediation?.docsUrl ? { docsUrl: finding.authorRemediation.docsUrl } : {}), })), - validateCommand, + validateCommands, preheader: intro, }); @@ -672,6 +686,9 @@ function formatPackageInspectorFindingsText(findings: PackageInspectorEmailFindi `- **${finding.findingKind.toUpperCase()}** \`${finding.code}\`${formatFindingMetaText(finding)}`, ` ${finding.message}`, ]; + if (finding.targetOpenClawVersion) { + lines.push(` OpenClaw target: ${finding.targetOpenClawVersion}`); + } if (finding.authorRemediation?.summary) { lines.push(" Fix:"); lines.push(` ${finding.authorRemediation.summary}`); diff --git a/convex/openclaw-plugin-inspector.d.ts b/convex/openclaw-plugin-inspector.d.ts index 6a2488d3..59c63806 100644 --- a/convex/openclaw-plugin-inspector.d.ts +++ b/convex/openclaw-plugin-inspector.d.ts @@ -19,6 +19,7 @@ declare module "@openclaw/plugin-inspector" { runCheck(options?: { pluginRoot?: string; openclawPath?: string | false; + openclawVersion?: string; outDir?: string; capture?: boolean; mockSdk?: boolean; diff --git a/convex/packageInspectorNode.test.ts b/convex/packageInspectorNode.test.ts index f1f32c3a..c1b07116 100644 --- a/convex/packageInspectorNode.test.ts +++ b/convex/packageInspectorNode.test.ts @@ -1,9 +1,23 @@ /* @vitest-environment node */ import { describe, expect, it } from "vitest"; -import { normalizeInspectorReportForPublish } from "./packageInspectorNode"; +import { + buildPublishInspectorRunCheckOptions, + normalizeInspectorReportForPublish, +} from "./packageInspectorNode"; describe("package inspector publish normalization", () => { + it("targets latest stable OpenClaw for publish-time inspection", () => { + expect(buildPublishInspectorRunCheckOptions("/tmp/plugin", "2026-07-30T00:00:00.000Z")).toEqual( + expect.objectContaining({ + pluginRoot: "/tmp/plugin", + openclawPath: false, + openclawVersion: "latest", + authorFacing: true, + }), + ); + }); + it("keeps legacy author-facing hard findings without remediation metadata", () => { const result = normalizeInspectorReportForPublish({ status: "fail", @@ -97,4 +111,98 @@ describe("package inspector publish normalization", () => { ], }); }); + + it("keeps missing-API findings with generic remediation and no docs URL", () => { + const result = normalizeInspectorReportForPublish({ + status: "fail", + summary: { breakageCount: 1, warningCount: 0, issueCount: 1 }, + issues: [ + { + code: "missing-openclaw-api", + level: "breakage", + issueClass: "compatibility-error", + message: "registerMemoryRuntime is unavailable in the selected OpenClaw target", + authorRemediation: { + summary: "Replace this call with an API available in the selected OpenClaw version.", + }, + }, + ], + }); + + expect(result.breakages).toEqual([ + expect.objectContaining({ + code: "missing-openclaw-api", + authorRemediation: { + summary: "Replace this call with an API available in the selected OpenClaw version.", + docsUrl: undefined, + }, + }), + ]); + }); + + it("keeps static and compatibility findings with the exact resolved target", () => { + const result = normalizeInspectorReportForPublish({ + status: "fail", + targetOpenClaw: { + requested: "beta", + version: "2026.8.0-beta.4", + }, + issues: [ + { + code: "package-entrypoint-missing", + level: "breakage", + issueClass: "package-integrity", + message: "declared OpenClaw entrypoint does not exist", + }, + { + code: "missing-openclaw-api", + level: "breakage", + issueClass: "compatibility-error", + message: "registerMemoryRuntime is unavailable in the selected OpenClaw target", + authorRemediation: { + summary: "Replace this call with an API available in the selected OpenClaw version.", + }, + }, + ], + }); + + expect(result.metadata.targetOpenClawVersion).toBe("2026.8.0-beta.4"); + expect(result.breakages.map((finding) => finding.code)).toEqual([ + "package-entrypoint-missing", + "missing-openclaw-api", + ]); + }); + + it("keeps out-of-range compatibility information non-blocking", () => { + const result = normalizeInspectorReportForPublish({ + status: "pass", + targetOpenClaw: { version: "2026.7.2-beta.4" }, + issues: [ + { + code: "unknown-registration-name", + level: "suggestion", + severity: "P2", + issueClass: "compatibility-information", + message: + "plugin calls registrars missing from target OpenClaw outside its declared range", + authorRemediation: { + summary: "Widen the declared range only after updating these registrations.", + }, + }, + ], + }); + + expect(result).toMatchObject({ + status: "pass", + summary: { breakageCount: 0, warningCount: 1 }, + breakages: [], + warnings: [ + { + code: "unknown-registration-name", + level: "suggestion", + issueClass: "compatibility-information", + }, + ], + }); + }); }); diff --git a/convex/packageInspectorNode.ts b/convex/packageInspectorNode.ts index b8e7774d..e8535b63 100644 --- a/convex/packageInspectorNode.ts +++ b/convex/packageInspectorNode.ts @@ -127,6 +127,23 @@ const inspectorMetadataValidator = v.object({ targetOpenClawVersion: v.optional(v.string()), }); +export function buildPublishInspectorRunCheckOptions(root: string, generatedAt: string) { + return { + pluginRoot: root, + openclawPath: false, + openclawVersion: "latest", + outDir: "reports", + capture: false, + mockSdk: true, + allowExecution: false, + authorFacing: true, + generatedAt, + } as Parameters<(typeof import("@openclaw/plugin-inspector"))["pluginRoot"]["runCheck"]>[0] & { + authorFacing: true; + generatedAt: string; + }; +} + export const runPackageInspectorForPublishInternal = internalAction({ args: { packageName: v.string(), @@ -164,19 +181,7 @@ export const runPackageInspectorForPublishInternal = internalAction({ await writeSyntheticInspectorConfigIfNeeded(root, args.files, args.packageName); const { pluginRoot } = await import("@openclaw/plugin-inspector"); - const runCheckOptions = { - pluginRoot: root, - openclawPath: false, - outDir: "reports", - capture: false, - mockSdk: true, - allowExecution: false, - authorFacing: true, - generatedAt: new Date().toISOString(), - } as Parameters[0] & { - authorFacing: true; - generatedAt: string; - }; + const runCheckOptions = buildPublishInspectorRunCheckOptions(root, new Date().toISOString()); const { report } = await pluginRoot.runCheck(runCheckOptions); return normalizeInspectorReportForPublish(report); diff --git a/convex/packages.public.test.ts b/convex/packages.public.test.ts index 7913df64..75429c6e 100644 --- a/convex/packages.public.test.ts +++ b/convex/packages.public.test.ts @@ -11391,6 +11391,10 @@ describe("packages public queries", () => { }, }, ], + metadata: { + inspectorVersion: "0.3.19", + targetOpenClawVersion: "2026.7.1-2", + }, })), scheduler: { runAfter: vi.fn(), @@ -11475,6 +11479,8 @@ describe("packages public queries", () => { releaseId: "packageReleases:demo-1", packageName: "demo-plugin", version: "1.0.0", + inspectorVersion: "0.3.19", + targetOpenClawVersion: "2026.7.1-2", findings: [ expect.objectContaining({ code: "legacy-before-agent-start", diff --git a/emails/plugin-inspector-findings.tsx b/emails/plugin-inspector-findings.tsx index def7ed75..d444f108 100644 --- a/emails/plugin-inspector-findings.tsx +++ b/emails/plugin-inspector-findings.tsx @@ -12,6 +12,7 @@ import { export type PluginInspectorFindingEmailItem = FindingCardProps & { code: string; + targetOpenClawVersion?: string; }; export type PluginInspectorFindingsEmailProps = { @@ -19,7 +20,7 @@ export type PluginInspectorFindingsEmailProps = { version: string; openClawVersion?: string; findings: PluginInspectorFindingEmailItem[]; - validateCommand: string; + validateCommands: string[]; preheader: string; }; @@ -28,7 +29,7 @@ export default function PluginInspectorFindingsEmail({ version, openClawVersion, findings, - validateCommand, + validateCommands, preheader, }: PluginInspectorFindingsEmailProps) { const issueText = `${findings.length} ${findings.length === 1 ? "issue" : "issues"}`; @@ -44,11 +45,21 @@ export default function PluginInspectorFindingsEmail({ ]} /> Findings - {findings.map((finding) => ( - + {findings.map((finding, index) => ( + ))} Validate a local fix - {validateCommand} + {validateCommands.map((command) => ( + {command} + ))} ); } @@ -72,7 +83,7 @@ PluginInspectorFindingsEmail.PreviewProps = { packageName: "demo-plugin", version: "1.0.0", openClawVersion: "2026.4.0", - validateCommand: "clawhub package validate ", + validateCommands: ["clawhub package validate "], preheader: "Plugin Inspector found 1 issue with demo-plugin@1.0.0.", findings: [ { diff --git a/package.json b/package.json index d228b31d..c4eee20c 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "@fontsource/noto-sans-sc": "5.3.0", "@monaco-editor/react": "4.7.0", "@openclaw/carapace": "git+https://github.com/openclaw/carapace.git#v0.2.0", - "@openclaw/plugin-inspector": "0.3.17", + "@openclaw/plugin-inspector": "github:openclaw/plugin-inspector#021c6586af78884d27f1be8acedca492acd9f9ae", "@radix-ui/react-avatar": "1.2.3", "@radix-ui/react-dialog": "1.1.20", "@radix-ui/react-dropdown-menu": "2.1.21", diff --git a/packages/clawhub/package.json b/packages/clawhub/package.json index b2f3ce5b..3d04ae1c 100644 --- a/packages/clawhub/package.json +++ b/packages/clawhub/package.json @@ -38,7 +38,7 @@ }, "dependencies": { "@clack/prompts": "1.7.0", - "@openclaw/plugin-inspector": "0.3.17", + "@openclaw/plugin-inspector": "github:openclaw/plugin-inspector#021c6586af78884d27f1be8acedca492acd9f9ae", "arktype": "2.2.3", "commander": "15.0.0", "croner": "10.0.1", diff --git a/packages/clawhub/src/cli.ts b/packages/clawhub/src/cli.ts index fd4390e1..e4573361 100644 --- a/packages/clawhub/src/cli.ts +++ b/packages/clawhub/src/cli.ts @@ -630,6 +630,7 @@ registerCommand(packageCmd, ["package", "validate"]) .argument("", "Package folder path") .option("--out ", "Directory for Plugin Inspector reports", "reports") .option("--openclaw ", "Optional local OpenClaw checkout to inspect against") + .option("--openclaw-version ", "OpenClaw target: latest, beta, or an exact version") .option("--runtime", "Enable runtime capture; imports plugin code") .option("--allow-execute", "Allow runtime capture in an isolated workspace") .option("--no-mock-sdk", "Disable mocked OpenClaw SDK during runtime capture") diff --git a/packages/clawhub/src/cli/commands/packages.test.ts b/packages/clawhub/src/cli/commands/packages.test.ts index f7eb1161..fb7ba64b 100644 --- a/packages/clawhub/src/cli/commands/packages.test.ts +++ b/packages/clawhub/src/cli/commands/packages.test.ts @@ -258,6 +258,7 @@ describe("package commands", () => { configPath: expect.stringContaining("plugin-inspector.config.json"), mockSdk: true, openclawPath: false, + openclawVersion: "latest", outDir: "reports", pluginRoot: folder, authorFacing: true, @@ -276,6 +277,51 @@ describe("package commands", () => { } }); + it.each(["latest", "beta", "2026.7.2-beta.4"])( + "validates against the requested OpenClaw target %s", + async (openclawVersion) => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "targeted-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + '{"name":"targeted-plugin","version":"1.0.0"}\n', + ); + inspectorMocks.pluginRoot.runCheck.mockResolvedValueOnce({ + report: { status: "pass", summary: { breakageCount: 0 } }, + paths: { jsonPath: join(folder, "reports", "plugin-inspector-report.json") }, + }); + + await cmdValidatePackage(makeOpts(workdir), "targeted-plugin", { openclawVersion }); + + expect(inspectorMocks.pluginRoot.runCheck).toHaveBeenCalledWith( + expect.objectContaining({ openclawVersion }), + ); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }, + ); + + it("rejects combining a local OpenClaw checkout with a version target", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "targeted-plugin"); + await mkdir(folder, { recursive: true }); + + await expect( + cmdValidatePackage(makeOpts(workdir), "targeted-plugin", { + openclaw: "../openclaw", + openclawVersion: "latest", + }), + ).rejects.toThrow("Choose either --openclaw or --openclaw-version"); + expect(inspectorMocks.pluginRoot.runCheck).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + it("fails package validation when Plugin Inspector reports hard breakages", async () => { const workdir = await makeTmpWorkdir(); try { @@ -340,6 +386,104 @@ describe("package commands", () => { } }); + it("keeps missing-API findings with generic remediation and no docs URL", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "removed-api-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + '{"name":"removed-api-plugin","version":"1.0.0"}\n', + ); + inspectorMocks.pluginRoot.runCheck.mockResolvedValueOnce({ + report: { + status: "fail", + summary: { breakageCount: 1, warningCount: 0, issueCount: 1 }, + issues: [ + { + code: "missing-openclaw-api", + level: "breakage", + issueClass: "compatibility-error", + message: "registerMemoryRuntime is unavailable in the selected OpenClaw target", + authorRemediation: { + summary: + "Replace this call with an API available in the selected OpenClaw version.", + }, + }, + ], + }, + paths: { jsonPath: join(folder, "reports", "plugin-inspector-report.json") }, + }); + + await expect(cmdValidatePackage(makeOpts(workdir), "removed-api-plugin", {})).rejects.toThrow( + "Plugin Inspector found 1 hard error", + ); + + const output = mockLog.mock.calls.join("\n"); + expect(output).toContain("ERROR missing-openclaw-api (compatibility-error)"); + expect(output).toContain( + "Fix: Replace this call with an API available in the selected OpenClaw version.", + ); + expect(output).not.toContain("Docs:"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("surfaces Honcho-equivalent removed registrations against the affected exact beta", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "honcho-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, "package.json"), '{"name":"honcho-plugin","version":"1.0.0"}\n'); + inspectorMocks.pluginRoot.runCheck.mockResolvedValueOnce({ + report: { + status: "fail", + targetOpenClaw: { + requestedVersion: "2026.7.2-beta.4", + version: "2026.7.2-beta.4", + eligibilityVersion: "2026.7.2", + }, + summary: { breakageCount: 1, warningCount: 0, issueCount: 1 }, + issues: [ + { + code: "unknown-registration-name", + level: "breakage", + severity: "P0", + issueClass: "live-issue", + message: "fixture calls registrars missing from target OpenClaw", + evidence: [ + "registerMemoryPromptSection @ index.ts:97", + "registerMemoryRuntime @ runtime.ts:276", + ], + authorRemediation: { + summary: + "Update the plugin to use APIs available in the target OpenClaw version, or narrow its declared compatibility range.", + }, + }, + ], + }, + paths: { jsonPath: join(folder, "reports", "plugin-inspector-report.json") }, + }); + + await expect( + cmdValidatePackage(makeOpts(workdir), "honcho-plugin", { + openclawVersion: "2026.7.2-beta.4", + }), + ).rejects.toThrow("Plugin Inspector found 1 hard error"); + + expect(inspectorMocks.pluginRoot.runCheck).toHaveBeenCalledWith( + expect.objectContaining({ openclawVersion: "2026.7.2-beta.4" }), + ); + const output = mockLog.mock.calls.join("\n"); + expect(output).toContain("registerMemoryPromptSection @ index.ts:97"); + expect(output).toContain("registerMemoryRuntime @ runtime.ts:276"); + expect(output).not.toContain("Docs:"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + it("prints author-facing package validation findings before report paths by default", async () => { const workdir = await makeTmpWorkdir(); try { diff --git a/packages/clawhub/src/cli/commands/packages.ts b/packages/clawhub/src/cli/commands/packages.ts index 0e501f46..22721a66 100644 --- a/packages/clawhub/src/cli/commands/packages.ts +++ b/packages/clawhub/src/cli/commands/packages.ts @@ -175,6 +175,7 @@ type PackagePackOptions = { type PackageValidateOptions = { out?: string; openclaw?: string; + openclawVersion?: string; runtime?: boolean; allowExecute?: boolean; mockSdk?: boolean; @@ -716,9 +717,13 @@ export async function cmdValidatePackage( const sourcePath = resolvedSource.path; const sourceStat = await stat(sourcePath).catch(() => null); if (!sourceStat?.isDirectory()) fail("Path must be a package folder"); + if (options.openclaw?.trim() && options.openclawVersion?.trim()) { + fail("Choose either --openclaw or --openclaw-version"); + } const outDir = options.out?.trim() || "reports"; const openclawPath = options.openclaw?.trim() ? resolve(opts.workdir, options.openclaw) : false; + const openclawVersion = options.openclawVersion?.trim() || (openclawPath ? undefined : "latest"); const generatedConfig = await createPluginInspectorConfigIfNeeded(sourcePath); let report: Awaited>["report"]; let paths: Awaited>["paths"]; @@ -730,6 +735,7 @@ export async function cmdValidatePackage( configPath: generatedConfig?.path, mockSdk: options.mockSdk !== false, openclawPath, + ...(openclawVersion ? { openclawVersion } : {}), outDir, pluginRoot: sourcePath, } as Parameters[0] & { authorFacing: true }; diff --git a/packages/clawhub/src/types/plugin-inspector.d.ts b/packages/clawhub/src/types/plugin-inspector.d.ts index a4bee804..4f47360c 100644 --- a/packages/clawhub/src/types/plugin-inspector.d.ts +++ b/packages/clawhub/src/types/plugin-inspector.d.ts @@ -22,6 +22,7 @@ declare module "@openclaw/plugin-inspector" { configPath?: string; mockSdk?: boolean; openclawPath?: string | false; + openclawVersion?: string; outDir?: string; pluginRoot?: string; }): Promise<{ diff --git a/src/__tests__/package-detail-route.test.tsx b/src/__tests__/package-detail-route.test.tsx index 0247ab4e..f7863aa1 100644 --- a/src/__tests__/package-detail-route.test.tsx +++ b/src/__tests__/package-detail-route.test.tsx @@ -2016,6 +2016,11 @@ describe("plugin detail route", () => { expect(within(validationRegion).getByText("v1.0.0")).toBeTruthy(); expect(within(validationRegion).getByText("Target")).toBeTruthy(); expect(within(validationRegion).getByText("OpenClaw 0.9.0")).toBeTruthy(); + expect( + within(validationRegion).getByText( + "clawhub package validate --openclaw-version 0.9.0", + ), + ).toBeTruthy(); expect(screen.getByText(/Legacy before_agent_start hook is deprecated\./)).toBeTruthy(); expect(screen.getByText(/We found/)).toBeTruthy(); expect(screen.queryByText(/Hey,/)).toBeNull(); diff --git a/src/components/dashboard/DashboardNeedsAttention.tsx b/src/components/dashboard/DashboardNeedsAttention.tsx index dbf64019..3566d755 100644 --- a/src/components/dashboard/DashboardNeedsAttention.tsx +++ b/src/components/dashboard/DashboardNeedsAttention.tsx @@ -57,6 +57,12 @@ type PluginInspectorFinding = { const PLUGIN_VALIDATE_CLI = "clawhub package validate "; +function pluginValidateCommand(openClawVersion?: string) { + return openClawVersion + ? `${PLUGIN_VALIDATE_CLI} --openclaw-version ${openClawVersion}` + : PLUGIN_VALIDATE_CLI; +} + export function DashboardNeedsAttention({ items }: DashboardNeedsAttentionProps) { const [selectedGroup, setSelectedGroup] = useState(null); const reviewScrollRef = useRef(null); @@ -353,7 +359,23 @@ function PluginValidationSheetReview({ const errors = displayedFindings.filter((finding) => finding.findingKind === "error"); const warnings = displayedFindings.filter((finding) => finding.findingKind !== "error"); const version = displayedFindings.find((finding) => finding.version)?.version ?? null; - const instructions = buildValidationInstructions(group.title, version, displayedFindings); + const targetOpenClawVersions = Array.from( + new Set( + displayedFindings + .map((finding) => finding.targetOpenClawVersion?.trim()) + .filter((target): target is string => Boolean(target)), + ), + ); + const validateCommands = targetOpenClawVersions.length + ? targetOpenClawVersions.map(pluginValidateCommand) + : [pluginValidateCommand()]; + const validateCommand = validateCommands.join("\n"); + const instructions = buildValidationInstructions( + group.title, + version, + displayedFindings, + validateCommands, + ); return ( <> @@ -383,12 +405,20 @@ function PluginValidationSheetReview({ Validate locally before publishing -
+
1 ? " is-multi-target" : "" + }`} + >
- {PLUGIN_VALIDATE_CLI} + {validateCommand} @@ -542,14 +572,17 @@ function buildValidationInstructions( packageName: string, version: string | null, findings: PluginInspectorFinding[], + validateCommands: string[], ) { const lines = [ `Fix the following OpenClaw plugin validation findings for package "${packageName}".`, ...(version ? [`Validated release: v${version}.`] : []), "", "Make the minimum code and manifest changes needed to resolve every issue below.", - "After editing, run locally:", - PLUGIN_VALIDATE_CLI, + validateCommands.length === 1 + ? "After editing, run locally:" + : "After editing, run locally against every recorded OpenClaw target:", + ...validateCommands, "", ]; for (const finding of findings) { diff --git a/src/routes/-dashboard.test.tsx b/src/routes/-dashboard.test.tsx index c185a109..af669f7e 100644 --- a/src/routes/-dashboard.test.tsx +++ b/src/routes/-dashboard.test.tsx @@ -49,6 +49,8 @@ const mocks = vi.hoisted(() => ({ rerenderDashboard: null as null | (() => void), })); +const writeTextMock = vi.fn(); + vi.mock("convex/react", () => ({ useQuery: (...args: unknown[]) => mocks.useQuery(...args), usePaginatedQuery: (...args: unknown[]) => mocks.usePaginatedQuery(...args), @@ -366,6 +368,12 @@ describe("Dashboard rows", () => { mocks.dashboardSearch = {}; mocks.rerenderDashboard = null; window.localStorage.clear(); + writeTextMock.mockReset(); + writeTextMock.mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText: writeTextMock }, + }); mocks.usePaginatedQuery.mockReturnValue({ results: [], status: "LoadingFirstPage", @@ -687,21 +695,43 @@ describe("Dashboard rows", () => { }); it("links public plugin finding counts to the plugin validation tab", () => { - arrangeDashboard({ - packages: [ - createPackage({ - inspectorWarningCount: 2, - scanStatus: "clean", - stats: { downloads: 42, installs: 9, stars: 0, versions: 1 }, - latestRelease: { + const packages = [ + createPackage({ + inspectorWarningCount: 2, + scanStatus: "clean", + stats: { downloads: 42, installs: 9, stars: 0, versions: 1 }, + latestRelease: { + version: "1.0.0", + createdAt: 1, + vtStatus: "clean", + llmStatus: "clean", + staticScanStatus: "clean", + }, + }), + ]; + arrangeDashboard({ packages }); + mocks.useQuery.mockImplementation((query: unknown, args: unknown) => { + if (args === "skip") return undefined; + const name = getFunctionName(query as never); + if (name === "publishers:listMine") return publishers; + if (name === "packages:list") return packages; + if (name === "dashboard:getDownloadMetrics") return downloadMetrics; + if (name === "packages:listPackageInspectorWarningsForManager") { + return [ + { + _id: "packageInspectorWarnings:1", + packageName: "local-scanned-runtime-plugin", version: "1.0.0", - createdAt: 1, - vtStatus: "clean", - llmStatus: "clean", - staticScanStatus: "clean", + findingKind: "warning", + code: "legacy-before-agent-start", + issueClass: "deprecation-warning", + severity: "P2", + message: "legacy before_agent_start hook is deprecated", + targetOpenClawVersion: "0.9.0", }, - }), - ], + ]; + } + return []; }); renderDashboard(); @@ -715,6 +745,86 @@ describe("Dashboard rows", () => { screen.getByRole("dialog", { name: "Local Flagged Runtime Plugin review" }), ).toBeTruthy(); expect(screen.getByRole("heading", { name: "Validation" })).toBeTruthy(); + expect( + screen.getByText("clawhub package validate --openclaw-version 0.9.0"), + ).toBeTruthy(); + }); + + it("copies one exact validation command per recorded OpenClaw target", async () => { + const packages = [ + createPackage({ + inspectorWarningCount: 2, + scanStatus: "clean", + latestRelease: { + version: "1.0.0", + createdAt: 1, + vtStatus: "clean", + llmStatus: "clean", + staticScanStatus: "clean", + }, + }), + ]; + arrangeDashboard({ packages }); + mocks.useQuery.mockImplementation((query: unknown, args: unknown) => { + if (args === "skip") return undefined; + const name = getFunctionName(query as never); + if (name === "publishers:listMine") return publishers; + if (name === "packages:list") return packages; + if (name === "dashboard:getDownloadMetrics") return downloadMetrics; + if (name === "packages:listPackageInspectorWarningsForManager") { + return [ + { + _id: "packageInspectorWarnings:1", + findingKind: "warning", + code: "legacy-before-agent-start", + message: "legacy hook is deprecated", + targetOpenClawVersion: "0.9.0", + }, + { + _id: "packageInspectorWarnings:2", + findingKind: "error", + code: "missing-expected-seam", + message: "registerTool is no longer available", + targetOpenClawVersion: "0.10.0", + }, + ]; + } + return []; + }); + + renderDashboard(); + fireEvent.click( + screen.getByRole("button", { name: /Local Flagged Runtime Plugin\. 1 issue/i }), + ); + expect(document.querySelector(".plugin-validation-command")?.textContent).toContain( + "clawhub package validate --openclaw-version 0.9.0", + ); + expect(document.querySelector(".plugin-validation-command")?.textContent).toContain( + "clawhub package validate --openclaw-version 0.10.0", + ); + fireEvent.click(screen.getByRole("button", { name: "Copy validate commands" })); + await waitFor(() => { + expect(writeTextMock).toHaveBeenCalledWith( + [ + "clawhub package validate --openclaw-version 0.9.0", + "clawhub package validate --openclaw-version 0.10.0", + ].join("\n"), + ); + }); + fireEvent.click(screen.getByRole("button", { name: "Copy fix instructions" })); + + await waitFor(() => { + expect(writeTextMock).toHaveBeenCalledWith( + expect.stringContaining( + "clawhub package validate --openclaw-version 0.9.0", + ), + ); + expect(writeTextMock).toHaveBeenCalledWith( + expect.stringContaining( + "clawhub package validate --openclaw-version 0.10.0", + ), + ); + }); }); it("shows a publisher selector and loads org packages when switching publishers", async () => { diff --git a/src/routes/plugins/$name.tsx b/src/routes/plugins/$name.tsx index 2c4b3a35..567e55a2 100644 --- a/src/routes/plugins/$name.tsx +++ b/src/routes/plugins/$name.tsx @@ -690,6 +690,12 @@ function PluginManifestSkillsPanel({ } const PLUGIN_VALIDATE_CLI = "clawhub package validate "; + +function pluginValidateCommand(openClawVersion?: string) { + return openClawVersion + ? `${PLUGIN_VALIDATE_CLI} --openclaw-version ${openClawVersion}` + : PLUGIN_VALIDATE_CLI; +} const PLUGIN_VALIDATE_TOOLBAR_LABEL = "Validate locally before publishing"; const INSPECTOR_ISSUE_CLASS_LABELS: Record = { @@ -954,10 +960,18 @@ function PluginValidationFindingCard({
) : null} {finding.targetOpenClawVersion ? ( -
-
Target
-
OpenClaw {finding.targetOpenClawVersion}
-
+ <> +
+
Target
+
OpenClaw {finding.targetOpenClawVersion}
+
+
+
Reproduce
+
+ {pluginValidateCommand(finding.targetOpenClawVersion)} +
+
+ ) : null} ) : null; diff --git a/src/styles.css b/src/styles.css index cff8e36c..fbcd9f15 100644 --- a/src/styles.css +++ b/src/styles.css @@ -5192,6 +5192,20 @@ code { width: 100%; } +.plugin-validation-toolbar.is-multi-target { + height: auto; + align-items: flex-start; +} + +.plugin-validation-toolbar.is-multi-target .plugin-validation-toolbar-cli { + align-items: flex-start; +} + +.plugin-validation-toolbar.is-multi-target .plugin-validation-command { + white-space: pre-wrap; + line-height: 1.45; +} + .plugin-validation-toolbar-copy.skill-install-copy-button { flex: 0 0 auto; width: 2rem;