From d9491aaa2a53450ede28218e47e8d2fc85f3c3c0 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Tue, 5 May 2026 11:21:54 +0800 Subject: [PATCH] Ship compiled npm plugin package --- .gitignore | 2 - README.md | 4 +- dist/index.d.ts | 2 + dist/index.js | 19 +++ dist/src/exportArtifacts.d.ts | 24 ++++ dist/src/exportArtifacts.js | 261 ++++++++++++++++++++++++++++++++++ package.json | 14 +- tsconfig.build.json | 11 ++ 8 files changed, 328 insertions(+), 9 deletions(-) create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/src/exportArtifacts.d.ts create mode 100644 dist/src/exportArtifacts.js create mode 100644 tsconfig.build.json diff --git a/.gitignore b/.gitignore index 862b70b..0546820 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ -node_modules/ coverage/ -dist/ *.tsbuildinfo diff --git a/README.md b/README.md index 12ebc95..321fa56 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ The method scans the resolved OpenClaw workspace after a run finishes and return ## Install -Install from npm: +Install from the npm package through OpenClaw: ```bash -npm install -g xworkmate-artifacts +openclaw plugins install xworkmate-artifacts openclaw plugins enable xworkmate-artifacts ``` diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..7b6ff93 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,2 @@ +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +export default function register(api: OpenClawPluginApi): void; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..2814b35 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,19 @@ +import { exportXWorkmateArtifacts } from "./src/exportArtifacts.js"; +export default function register(api) { + api.registerGatewayMethod("xworkmate.artifacts.export", async (opts) => { + try { + const payload = await exportXWorkmateArtifacts({ + params: opts.params, + config: api.config, + pluginConfig: api.pluginConfig, + }); + opts.respond(true, payload, undefined); + } + catch (error) { + opts.respond(false, undefined, { + code: "INVALID_REQUEST", + message: error instanceof Error ? error.message : String(error), + }); + } + }); +} diff --git a/dist/src/exportArtifacts.d.ts b/dist/src/exportArtifacts.d.ts new file mode 100644 index 0000000..87147ba --- /dev/null +++ b/dist/src/exportArtifacts.d.ts @@ -0,0 +1,24 @@ +export type XWorkmateArtifact = { + relativePath: string; + label: string; + contentType: string; + sizeBytes: number; + sha256: string; + encoding?: "base64"; + content?: string; +}; +export type XWorkmateArtifactExport = { + runId: string; + sessionKey: string; + remoteWorkingDirectory: string; + remoteWorkspaceRefKind: "remotePath"; + artifacts: XWorkmateArtifact[]; + warnings: string[]; +}; +type ExportInput = { + params: Record; + config?: unknown; + pluginConfig?: Record; +}; +export declare function exportXWorkmateArtifacts(input: ExportInput): Promise; +export {}; diff --git a/dist/src/exportArtifacts.js b/dist/src/exportArtifacts.js new file mode 100644 index 0000000..b2f38ae --- /dev/null +++ b/dist/src/exportArtifacts.js @@ -0,0 +1,261 @@ +import { createHash } from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +const DEFAULT_MAX_FILES = 64; +const DEFAULT_MAX_INLINE_BYTES = 10 * 1024 * 1024; +const SKIPPED_DIRS = new Set([ + ".git", + ".openclaw", + ".pi", + ".dart_tool", + ".next", + ".turbo", + "build", + "dist", + "node_modules", +]); +export async function exportXWorkmateArtifacts(input) { + const params = input.params ?? {}; + const pluginConfig = input.pluginConfig ?? {}; + const runId = optionalString(params.runId); + if (!runId) { + throw new Error("runId required"); + } + const sessionKey = optionalString(params.sessionKey); + if (!sessionKey) { + throw new Error("sessionKey required"); + } + const maxFiles = positiveInteger(params.maxFiles, pluginConfig.maxFiles, DEFAULT_MAX_FILES); + const maxInlineBytes = positiveInteger(params.maxInlineBytes, pluginConfig.maxInlineBytes, DEFAULT_MAX_INLINE_BYTES); + const sinceUnixMs = nonNegativeNumber(params.sinceUnixMs, 0); + const workspaceDir = resolveWorkspaceDir({ + config: input.config, + pluginConfig, + params, + sessionKey, + }); + const workspaceRoot = await fs.realpath(workspaceDir); + const warnings = []; + const candidates = await collectCandidates({ + workspaceRoot, + sinceUnixMs, + warnings, + }); + candidates.sort((left, right) => { + if (right.mtimeMs !== left.mtimeMs) { + return right.mtimeMs - left.mtimeMs; + } + return left.relativePath.localeCompare(right.relativePath); + }); + const artifacts = []; + for (const candidate of candidates) { + if (artifacts.length >= maxFiles) { + warnings.push(`artifact limit reached; skipped remaining files after ${maxFiles}`); + break; + } + const bytes = await fs.readFile(candidate.absolutePath); + const artifact = { + relativePath: candidate.relativePath, + label: path.posix.basename(candidate.relativePath), + contentType: contentTypeForPath(candidate.relativePath), + sizeBytes: bytes.byteLength, + sha256: createHash("sha256").update(bytes).digest("hex"), + }; + if (bytes.byteLength <= maxInlineBytes) { + artifact.encoding = "base64"; + artifact.content = bytes.toString("base64"); + } + else { + warnings.push(`${candidate.relativePath} exceeds maxInlineBytes and was not inlined`); + } + artifacts.push(artifact); + } + return { + runId, + sessionKey, + remoteWorkingDirectory: workspaceRoot, + remoteWorkspaceRefKind: "remotePath", + artifacts, + warnings, + }; +} +async function collectCandidates(input) { + const candidates = []; + await walk(input.workspaceRoot); + return candidates; + async function walk(currentDir) { + let entries; + try { + entries = await fs.readdir(currentDir, { withFileTypes: true }); + } + catch (error) { + input.warnings.push(`cannot read ${safeDisplayPath(input.workspaceRoot, currentDir)}: ${String(error)}`); + return; + } + entries.sort((left, right) => left.name.localeCompare(right.name)); + for (const entry of entries) { + if (entry.name === "." || entry.name === "..") { + continue; + } + const absolutePath = path.join(currentDir, entry.name); + if (entry.isSymbolicLink()) { + input.warnings.push(`skipped symlink ${safeDisplayPath(input.workspaceRoot, absolutePath)}`); + continue; + } + if (entry.isDirectory()) { + if (SKIPPED_DIRS.has(entry.name)) { + continue; + } + await walk(absolutePath); + continue; + } + if (!entry.isFile()) { + continue; + } + const stat = await fs.stat(absolutePath); + const changedAtMs = Math.max(stat.mtimeMs, stat.ctimeMs); + if (changedAtMs < input.sinceUnixMs) { + continue; + } + const realPath = await fs.realpath(absolutePath); + if (!isWithinRoot(input.workspaceRoot, realPath)) { + input.warnings.push(`skipped path outside workspace ${entry.name}`); + continue; + } + const relativePath = safeRelativePath(input.workspaceRoot, realPath); + if (!relativePath) { + continue; + } + candidates.push({ + absolutePath: realPath, + relativePath, + sizeBytes: stat.size, + mtimeMs: changedAtMs, + }); + } + } +} +function resolveWorkspaceDir(input) { + const explicit = optionalString(input.params.workspaceDir) || optionalString(input.pluginConfig.workspaceDir); + if (explicit) { + return expandUserPath(explicit); + } + const config = objectRecord(input.config); + const agents = objectRecord(config.agents); + const agentList = Array.isArray(agents.list) + ? agents.list.map(objectRecord).filter((entry) => Object.keys(entry).length > 0) + : []; + const agentId = agentIdFromSessionKey(input.sessionKey); + const selected = (agentId ? agentList.find((entry) => optionalString(entry.id) === agentId) : undefined) ?? + agentList.find((entry) => entry.default === true) ?? + agentList[0]; + const selectedWorkspace = selected ? optionalString(selected.workspace) : ""; + if (selectedWorkspace) { + return expandUserPath(selectedWorkspace); + } + const defaults = objectRecord(agents.defaults); + const defaultWorkspace = optionalString(defaults.workspace); + if (defaultWorkspace) { + return expandUserPath(defaultWorkspace); + } + const profile = process.env.OPENCLAW_PROFILE?.trim(); + if (profile && profile.toLowerCase() !== "default") { + return path.join(os.homedir(), ".openclaw", `workspace-${profile}`); + } + return path.join(os.homedir(), ".openclaw", "workspace"); +} +function agentIdFromSessionKey(sessionKey) { + const parts = sessionKey.split(":"); + if (parts.length >= 3 && parts[0] === "agent") { + return parts[1]?.trim() ?? ""; + } + return ""; +} +function safeRelativePath(root, target) { + const relative = path.relative(root, target); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return ""; + } + const normalized = relative.split(path.sep).join(path.posix.sep); + if (normalized.split("/").some((part) => part === ".." || part === "")) { + return ""; + } + return normalized; +} +function safeDisplayPath(root, target) { + return safeRelativePath(root, target) || path.basename(target); +} +function isWithinRoot(root, target) { + const relative = path.relative(root, target); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} +function contentTypeForPath(relativePath) { + switch (path.extname(relativePath).toLowerCase()) { + case ".md": + case ".markdown": + return "text/markdown"; + case ".txt": + case ".log": + return "text/plain"; + case ".json": + return "application/json"; + case ".csv": + return "text/csv"; + case ".html": + case ".htm": + return "text/html"; + case ".pdf": + return "application/pdf"; + case ".pptx": + return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + case ".docx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + case ".xlsx": + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + case ".png": + return "image/png"; + case ".jpg": + case ".jpeg": + return "image/jpeg"; + case ".gif": + return "image/gif"; + case ".svg": + return "image/svg+xml"; + default: + return "application/octet-stream"; + } +} +function objectRecord(value) { + return value && typeof value === "object" && !Array.isArray(value) + ? value + : {}; +} +function optionalString(value) { + return typeof value === "string" ? value.trim() : ""; +} +function positiveInteger(primary, secondary, fallback) { + for (const value of [primary, secondary]) { + const numeric = Number(value); + if (Number.isFinite(numeric) && numeric > 0) { + return Math.floor(numeric); + } + } + return fallback; +} +function nonNegativeNumber(value, fallback) { + const numeric = Number(value); + if (Number.isFinite(numeric) && numeric >= 0) { + return numeric; + } + return fallback; +} +function expandUserPath(value) { + if (value === "~") { + return os.homedir(); + } + if (value.startsWith("~/")) { + return path.join(os.homedir(), value.slice(2)); + } + return path.resolve(value); +} diff --git a/package.json b/package.json index 23e144a..afd8d0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xworkmate-artifacts", - "version": "0.1.1", + "version": "0.1.2", "description": "XWorkmate artifact export plugin for OpenClaw Gateway", "type": "module", "license": "MIT", @@ -22,16 +22,20 @@ "engines": { "node": ">=22" }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "files": [ "README.md", - "index.ts", "openclaw.plugin.json", - "src/exportArtifacts.ts" + "dist/**/*.js", + "dist/**/*.d.ts" ], "scripts": { + "build": "tsc -p tsconfig.build.json", "test": "vitest run", "typecheck": "tsc --noEmit", - "pack:check": "npm pack --dry-run" + "pack:check": "pnpm build && npm pack --dry-run", + "prepack": "pnpm build" }, "publishConfig": { "access": "public", @@ -46,7 +50,7 @@ }, "openclaw": { "extensions": [ - "./index.ts" + "./dist/index.js" ] } } diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..131364a --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": false, + "noEmit": false, + "outDir": "dist", + "rootDir": "." + }, + "include": ["index.ts", "src/exportArtifacts.ts"] +}