diff --git a/README.md b/README.md index 79a4e0b..2b2e652 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ npm run check:runtime npm run check:inspector npm run check:install npm run pack:check +npm run pack:zip ``` The `Update OpenClaw SDK Surface` workflow automatically checks @@ -154,6 +155,11 @@ Tagged GitHub releases publish the validated package to npm through trusted publishing. The release tag must match `package.json`, for example `v0.0.1` for version `0.0.1`. +`npm pack` remains the canonical npm artifact. `npm run pack:zip` builds the +legacy archive artifact at `dist/openclaw-kitchen-sink-fixture-.zip` +with `package.json`, `openclaw.plugin.json`, `plugin-inspector.config.json`, +`README.md`, and `src/**` at the archive root for old archive installers. + Use the `Draft Release` workflow to create the tag and generated GitHub release notes. Publishing that draft release runs the npm publish workflow. `0.0.x` verification releases publish under the `verification` npm dist-tag so they do diff --git a/package.json b/package.json index 5089902..c29653f 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,16 @@ "compat": { "pluginApi": "2026.4" }, + "install": { + "clawhubSpec": "clawhub:@openclaw/kitchen-sink", + "npmSpec": "@openclaw/kitchen-sink", + "defaultChoice": "clawhub", + "minHostVersion": "2026.4.29" + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + }, "build": { "openclawVersion": "2026.4.29", "pluginSdkVersion": "2026.4.29" @@ -58,6 +68,7 @@ "check:inspector": "npm run plugin:inspect && npm run plugin:inspect:runtime", "check:runtime": "node scripts/check-kitchen-runtime.mjs && node scripts/check-kitchen-contract-probes.mjs", "pack:check": "node scripts/check-pack-payload.mjs", + "pack:zip": "node scripts/pack-zip.mjs", "plugin:inspect": "plugin-inspector check --config plugin-inspector.config.json --no-openclaw", "plugin:inspect:runtime": "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --config plugin-inspector.config.json --no-openclaw --runtime --mock-sdk", "sdk:target-check": "node scripts/check-target-sdk-imports.mjs", diff --git a/reports/kitchen-contract-probes.json b/reports/kitchen-contract-probes.json index 1c20397..6034910 100644 --- a/reports/kitchen-contract-probes.json +++ b/reports/kitchen-contract-probes.json @@ -426,6 +426,12 @@ "kitchen-sink-node-host-command" ] }, + "registerNodeInvokePolicy": { + "count": 1, + "ids": [ + "kitchen-sink-node-invoke-policy" + ] + }, "registerProvider": { "count": 2, "ids": [ diff --git a/reports/kitchen-contract-probes.md b/reports/kitchen-contract-probes.md index 0467a28..94cbd59 100644 --- a/reports/kitchen-contract-probes.md +++ b/reports/kitchen-contract-probes.md @@ -45,6 +45,7 @@ Status: PASS | registerMigrationProvider | 1 | kitchen-sink-migration-provider | | registerMusicGenerationProvider | 2 | kitchen-sink-music, kitchen-sink-music-generation-provider | | registerNodeHostCommand | 1 | kitchen-sink-node-host-command | +| registerNodeInvokePolicy | 1 | kitchen-sink-node-invoke-policy | | registerProvider | 2 | kitchen-sink-llm, kitchen-sink-provider | | registerRealtimeTranscriptionProvider | 2 | kitchen-sink-realtime-transcription, kitchen-sink-realtime-transcription-provider | | registerRealtimeVoiceProvider | 2 | kitchen-sink-realtime-voice, kitchen-sink-realtime-voice-provider | diff --git a/scripts/check-pack-payload.mjs b/scripts/check-pack-payload.mjs index e1c7da2..eb409c9 100644 --- a/scripts/check-pack-payload.mjs +++ b/scripts/check-pack-payload.mjs @@ -100,6 +100,24 @@ if (buildPluginSdkVersion !== buildOpenClawVersion) { if (packageJson.dependencies?.openclaw !== buildOpenClawVersion) { issues.push("dependencies.openclaw must match openclaw.build.openclawVersion"); } +if (packageJson.openclaw?.install?.clawhubSpec !== "clawhub:@openclaw/kitchen-sink") { + issues.push('openclaw.install.clawhubSpec must be "clawhub:@openclaw/kitchen-sink"'); +} +if (packageJson.openclaw?.install?.npmSpec !== "@openclaw/kitchen-sink") { + issues.push('openclaw.install.npmSpec must be "@openclaw/kitchen-sink"'); +} +if (packageJson.openclaw?.install?.defaultChoice !== "clawhub") { + issues.push('openclaw.install.defaultChoice must be "clawhub"'); +} +if (packageJson.openclaw?.install?.minHostVersion !== buildOpenClawVersion) { + issues.push("openclaw.install.minHostVersion must match openclaw.build.openclawVersion"); +} +if (packageJson.openclaw?.release?.publishToClawHub !== true) { + issues.push("openclaw.release.publishToClawHub must be true"); +} +if (packageJson.openclaw?.release?.publishToNpm !== true) { + issues.push("openclaw.release.publishToNpm must be true"); +} if (!packageJson.files?.includes("src/")) { issues.push("package files must include src/"); } diff --git a/scripts/pack-zip.mjs b/scripts/pack-zip.mjs new file mode 100644 index 0000000..df812a5 --- /dev/null +++ b/scripts/pack-zip.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node + +import { Buffer } from "node:buffer"; +import fs from "node:fs/promises"; +import path from "node:path"; + +const repoRoot = process.cwd(); +const packageJson = JSON.parse(await fs.readFile(path.join(repoRoot, "package.json"), "utf8")); +const pluginJson = JSON.parse(await fs.readFile(path.join(repoRoot, "openclaw.plugin.json"), "utf8")); +const packageVersion = packageJson.version; +const pluginId = pluginJson.id; +const crcTable = Array.from({ length: 256 }, (_, index) => { + let value = index; + for (let bit = 0; bit < 8; bit += 1) { + value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; + } + return value >>> 0; +}); + +if (pluginId !== "openclaw-kitchen-sink-fixture") { + throw new Error(`unexpected plugin id: ${pluginId}`); +} +if (typeof packageVersion !== "string" || packageVersion.trim().length === 0) { + throw new Error("package.json version must be a non-empty string"); +} + +const distDir = path.join(repoRoot, "dist"); +const zipName = `${pluginId}-${packageVersion}.zip`; +const zipPath = path.join(distDir, zipName); +const entries = [ + "package.json", + "openclaw.plugin.json", + "plugin-inspector.config.json", + "README.md", + ...(await listFiles("src")), +]; + +await fs.mkdir(distDir, { recursive: true }); +await fs.writeFile(zipPath, await createZip(entries)); + +console.log(`Wrote ${path.relative(repoRoot, zipPath)} (${entries.length} files)`); + +async function listFiles(root) { + const files = []; + const stack = [root]; + + while (stack.length > 0) { + const relativeDir = stack.pop(); + const dirents = await fs.readdir(path.join(repoRoot, relativeDir), { withFileTypes: true }); + for (const dirent of dirents.sort((left, right) => left.name.localeCompare(right.name))) { + const relativePath = path.posix.join(relativeDir, dirent.name); + if (dirent.isDirectory()) { + stack.push(relativePath); + } else if (dirent.isFile()) { + files.push(relativePath); + } + } + } + + return files.sort(); +} + +async function createZip(files) { + const localParts = []; + const centralParts = []; + let offset = 0; + + for (const relativePath of files) { + if (relativePath.startsWith("/") || relativePath.includes("..")) { + throw new Error(`invalid zip entry path: ${relativePath}`); + } + + const data = await fs.readFile(path.join(repoRoot, relativePath)); + const name = Buffer.from(relativePath, "utf8"); + const crc = crc32(data); + const localHeader = Buffer.alloc(30); + localHeader.writeUInt32LE(0x04034b50, 0); + localHeader.writeUInt16LE(20, 4); + localHeader.writeUInt16LE(0x0800, 6); + localHeader.writeUInt16LE(0, 8); + localHeader.writeUInt16LE(0, 10); + localHeader.writeUInt16LE(0, 12); + localHeader.writeUInt32LE(crc, 14); + localHeader.writeUInt32LE(data.length, 18); + localHeader.writeUInt32LE(data.length, 22); + localHeader.writeUInt16LE(name.length, 26); + localHeader.writeUInt16LE(0, 28); + + localParts.push(localHeader, name, data); + + const centralHeader = Buffer.alloc(46); + centralHeader.writeUInt32LE(0x02014b50, 0); + centralHeader.writeUInt16LE(20, 4); + centralHeader.writeUInt16LE(20, 6); + centralHeader.writeUInt16LE(0x0800, 8); + centralHeader.writeUInt16LE(0, 10); + centralHeader.writeUInt16LE(0, 12); + centralHeader.writeUInt16LE(0, 14); + centralHeader.writeUInt32LE(crc, 16); + centralHeader.writeUInt32LE(data.length, 20); + centralHeader.writeUInt32LE(data.length, 24); + centralHeader.writeUInt16LE(name.length, 28); + centralHeader.writeUInt16LE(0, 30); + centralHeader.writeUInt16LE(0, 32); + centralHeader.writeUInt16LE(0, 34); + centralHeader.writeUInt16LE(0, 36); + centralHeader.writeUInt32LE(0, 38); + centralHeader.writeUInt32LE(offset, 42); + centralParts.push(centralHeader, name); + + offset += localHeader.length + name.length + data.length; + } + + const centralDirectory = Buffer.concat(centralParts); + const end = Buffer.alloc(22); + end.writeUInt32LE(0x06054b50, 0); + end.writeUInt16LE(0, 4); + end.writeUInt16LE(0, 6); + end.writeUInt16LE(files.length, 8); + end.writeUInt16LE(files.length, 10); + end.writeUInt32LE(centralDirectory.length, 12); + end.writeUInt32LE(offset, 16); + end.writeUInt16LE(0, 20); + + return Buffer.concat([...localParts, centralDirectory, end]); +} + +function crc32(buffer) { + let crc = 0xffffffff; + for (const byte of buffer) { + crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xff]; + } + return (crc ^ 0xffffffff) >>> 0; +} diff --git a/scripts/sync-surface.mjs b/scripts/sync-surface.mjs index ac265af..0b01046 100644 --- a/scripts/sync-surface.mjs +++ b/scripts/sync-surface.mjs @@ -251,6 +251,18 @@ function renderPackageJson({ packageVersion }) { openclawVersion: packageVersion, pluginSdkVersion: packageVersion, }; + packageJson.openclaw.install = { + ...(packageJson.openclaw.install ?? {}), + clawhubSpec: "clawhub:@openclaw/kitchen-sink", + npmSpec: "@openclaw/kitchen-sink", + defaultChoice: "clawhub", + minHostVersion: packageVersion, + }; + packageJson.openclaw.release = { + ...(packageJson.openclaw.release ?? {}), + publishToClawHub: true, + publishToNpm: true, + }; if (packageJson.dependencies?.openclaw) { packageJson.dependencies.openclaw = packageVersion; }