Adapt plugin to OpenClaw 2026.5.28
This commit is contained in:
parent
260380531b
commit
9bc52e7861
29
README.md
29
README.md
@ -67,10 +67,11 @@ Equivalent config shape for a linked checkout:
|
||||
## Contract
|
||||
|
||||
Prepare request params are supplied by the OpenClaw host, bridge, or APP
|
||||
runtime. The plugin treats `sessionKey`, `runId`, and `workspaceDir` as the
|
||||
trusted mapping into OpenClaw's built-in multi-session model; it does not parse
|
||||
paths from chat text and does not invent fallback session/run identities.
|
||||
Gateway methods accept these fields from bridge/app runtime params. The optional
|
||||
runtime. On OpenClaw runtimes that expose a trusted plugin `sessionScope`, the
|
||||
plugin uses that scope first. Otherwise it falls back to bridge/app runtime
|
||||
params. The plugin treats `sessionKey`, `runId`, and `workspaceDir` as the
|
||||
mapping into OpenClaw's built-in multi-session model; it does not parse paths
|
||||
from chat text and does not invent fallback session/run identities. The optional
|
||||
agent tool does not expose these fields to the model; it only uses host-injected
|
||||
tool context.
|
||||
|
||||
@ -139,15 +140,13 @@ Export response payload:
|
||||
|
||||
Files at or below `maxInlineBytes` also include `encoding: "base64"` and `content`.
|
||||
When `artifactScope` is omitted, export/list defaults to the current task scope
|
||||
derived from `sessionKey/runId`. If `sinceUnixMs` is provided, export also
|
||||
adopts files created or changed in the workspace root during the current run by
|
||||
copying them into that task scope before returning the manifest. This covers
|
||||
agents that save output as `./file.md` while still keeping XWorkmate sync scoped
|
||||
to `tasks/<session>/<run>`.
|
||||
derived from `sessionKey/runId`. `sinceUnixMs` is only a filter inside that task
|
||||
scope. The plugin does not adopt files from the workspace root; agents must
|
||||
write final deliverables directly into the prepared `artifactDirectory`.
|
||||
|
||||
Without `sinceUnixMs`, export/list only reads the current task scope. The plugin
|
||||
never scans `tasks/`, `owners/*/threads/*`, or any previous thread workspace as
|
||||
a fallback and does not borrow artifacts from earlier task scopes.
|
||||
The plugin never scans workspace root, `owners/*/threads/*`, or any previous
|
||||
thread workspace as a fallback and does not borrow artifacts from earlier task
|
||||
scopes.
|
||||
|
||||
Each exported artifact includes `artifactRef`, a plugin-signed reference over
|
||||
the issued session/run scope, artifact scope, path, size, and SHA-256 digest. `read` accepts
|
||||
@ -205,9 +204,9 @@ only remote file access path.
|
||||
## Limits
|
||||
|
||||
- Only files inside the resolved OpenClaw workspace are exported.
|
||||
- `.git`, `.openclaw`, `.xworkmate`, `.pi`, build outputs, and dependency folders are excluded from task artifact exports.
|
||||
- Workspace-root files are adopted only when `sinceUnixMs` is provided; adopted files are copied into the current `tasks/<safe-session-key>/<safe-run-id>` scope before listing or reading.
|
||||
- Export never adopts files from OpenClaw owner/thread workspaces; agents must write into the prepared task scope or into the current-run workspace root for timestamp-gated adoption.
|
||||
- `.git`, `.openclaw`, `.xworkmate`, `.pi`, transient framework state, and dependency folders are excluded from task artifact exports.
|
||||
- `dist/`, `build/`, and other delivery directories inside the prepared task scope are exported recursively.
|
||||
- Export never adopts files from the workspace root or OpenClaw owner/thread workspaces; agents must write into the prepared task scope.
|
||||
- Symlinks are skipped to avoid workspace escape.
|
||||
- Files larger than `maxInlineBytes` are listed with metadata and a warning, but are not inlined.
|
||||
- `artifactScope` must be `tasks/<safe-session-key>/<safe-run-id>`.
|
||||
|
||||
47
dist/index.js
vendored
47
dist/index.js
vendored
@ -1,5 +1,34 @@
|
||||
import { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import { exportXWorkmateArtifacts, prepareXWorkmateArtifacts, readXWorkmateArtifact, } from "./src/exportArtifacts.js";
|
||||
import { runXWorkmateBridgeAgents } from "./src/bridgeAgents.js";
|
||||
function scopedGatewayParams(params) {
|
||||
const sessionScope = getPluginRuntimeGatewayRequestScope()?.sessionScope;
|
||||
const runScope = resolveRunScope({ sessionScope });
|
||||
if (!runScope) {
|
||||
return params;
|
||||
}
|
||||
return {
|
||||
...params,
|
||||
sessionKey: runScope.sessionKey,
|
||||
runId: runScope.runId,
|
||||
...(runScope.workspaceDir ? { workspaceDir: runScope.workspaceDir } : {}),
|
||||
...(runScope.artifactScope ? { artifactScope: runScope.artifactScope } : {}),
|
||||
};
|
||||
}
|
||||
function resolveRunScope(ctx) {
|
||||
const scope = ctx.sessionScope;
|
||||
const sessionKey = scope?.sessionKey || ctx.sessionKey;
|
||||
const runId = scope?.runId || ctx.runId || "";
|
||||
if (!sessionKey || !runId) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
sessionKey,
|
||||
runId,
|
||||
...(scope?.workspaceDir || ctx.workspaceDir ? { workspaceDir: scope?.workspaceDir || ctx.workspaceDir } : {}),
|
||||
...(scope?.relativeTaskDirectory ? { artifactScope: scope.relativeTaskDirectory } : {}),
|
||||
};
|
||||
}
|
||||
const plugin = {
|
||||
id: "openclaw-multi-session-plugins",
|
||||
name: "openclaw-multi-session-plugins",
|
||||
@ -11,7 +40,7 @@ function register(api) {
|
||||
api.registerGatewayMethod("xworkmate.artifacts.prepare", async (opts) => {
|
||||
try {
|
||||
const payload = await prepareXWorkmateArtifacts({
|
||||
params: opts.params,
|
||||
params: scopedGatewayParams(opts.params),
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
@ -27,7 +56,7 @@ function register(api) {
|
||||
api.registerGatewayMethod("xworkmate.artifacts.export", async (opts) => {
|
||||
try {
|
||||
const payload = await exportXWorkmateArtifacts({
|
||||
params: opts.params,
|
||||
params: scopedGatewayParams(opts.params),
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
@ -43,7 +72,7 @@ function register(api) {
|
||||
api.registerGatewayMethod("xworkmate.artifacts.list", async (opts) => {
|
||||
try {
|
||||
const payload = await exportXWorkmateArtifacts({
|
||||
params: { ...opts.params, includeContent: false },
|
||||
params: { ...scopedGatewayParams(opts.params), includeContent: false },
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
@ -59,7 +88,7 @@ function register(api) {
|
||||
api.registerGatewayMethod("xworkmate.artifacts.read", async (opts) => {
|
||||
try {
|
||||
const payload = await readXWorkmateArtifact({
|
||||
params: opts.params,
|
||||
params: scopedGatewayParams(opts.params),
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
@ -75,7 +104,7 @@ function register(api) {
|
||||
api.registerGatewayMethod("xworkmate.agents.run", async (opts) => {
|
||||
try {
|
||||
const payload = await runXWorkmateBridgeAgents({
|
||||
params: opts.params,
|
||||
params: scopedGatewayParams(opts.params),
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
@ -140,21 +169,23 @@ function createXWorkmateArtifactsTool(api, ctx) {
|
||||
},
|
||||
async execute(_id, params) {
|
||||
const action = typeof params.action === "string" ? params.action : "";
|
||||
const runScope = resolveRunScope(ctx);
|
||||
const sessionKey = ctx.sessionScope?.sessionKey || ctx.sessionKey;
|
||||
const runId = ctx.sessionScope?.runId || ctx.runId || "";
|
||||
const workspaceDir = ctx.sessionScope?.workspaceDir || ctx.workspaceDir;
|
||||
if (!sessionKey) {
|
||||
throw new Error("sessionKey required");
|
||||
}
|
||||
if (!runId) {
|
||||
throw new Error("runId required");
|
||||
}
|
||||
const workspaceDir = ctx.sessionScope?.workspaceDir || ctx.workspaceDir;
|
||||
const { sessionKey: _ignoredSessionKey, runId: _ignoredRunId, workspaceDir: _ignoredWorkspaceDir, ...operationParams } = params;
|
||||
const baseParams = {
|
||||
...operationParams,
|
||||
sessionKey,
|
||||
runId,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
...(runScope?.artifactScope ? { artifactScope: runScope.artifactScope } : {}),
|
||||
};
|
||||
if (action === "list") {
|
||||
const payload = await exportXWorkmateArtifacts({
|
||||
@ -241,15 +272,16 @@ function createXWorkmateAgentsTool(api, ctx) {
|
||||
required: ["taskPrompt"],
|
||||
},
|
||||
async execute(_id, params) {
|
||||
const runScope = resolveRunScope(ctx);
|
||||
const sessionKey = ctx.sessionScope?.sessionKey || ctx.sessionKey;
|
||||
const runId = ctx.sessionScope?.runId || ctx.runId || "";
|
||||
const workspaceDir = ctx.sessionScope?.workspaceDir || ctx.workspaceDir;
|
||||
if (!sessionKey) {
|
||||
throw new Error("sessionKey required");
|
||||
}
|
||||
if (!runId) {
|
||||
throw new Error("runId required");
|
||||
}
|
||||
const workspaceDir = ctx.sessionScope?.workspaceDir || ctx.workspaceDir;
|
||||
const { sessionKey: _ignoredSessionKey, runId: _ignoredRunId, workspaceDir: _ignoredWorkspaceDir, ...operationParams } = params;
|
||||
const payload = await runXWorkmateBridgeAgents({
|
||||
params: {
|
||||
@ -257,6 +289,7 @@ function createXWorkmateAgentsTool(api, ctx) {
|
||||
sessionKey,
|
||||
runId,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
...(runScope?.artifactScope ? { artifactScope: runScope.artifactScope } : {}),
|
||||
},
|
||||
config: ctx.config ?? api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
|
||||
67
dist/src/exportArtifacts.js
vendored
67
dist/src/exportArtifacts.js
vendored
@ -14,8 +14,6 @@ const SKIPPED_DIRS = new Set([
|
||||
".dart_tool",
|
||||
".next",
|
||||
".turbo",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
]);
|
||||
export async function prepareXWorkmateArtifacts(input) {
|
||||
@ -85,22 +83,11 @@ export async function exportXWorkmateArtifacts(input) {
|
||||
scanRoot: scopeRoot,
|
||||
relativeRoot: scopeRoot,
|
||||
sinceUnixMs,
|
||||
skipTaskScopeRoot: false,
|
||||
warnSkippedSymlinks: true,
|
||||
warnings,
|
||||
})
|
||||
: [];
|
||||
const adoptedCandidates = sinceUnixMs > 0
|
||||
? await adoptWorkspaceRootCandidatesIntoScope({
|
||||
workspaceRoot,
|
||||
scopeRoot,
|
||||
artifactScope,
|
||||
sinceUnixMs,
|
||||
existingRelativePaths: new Set(scopedCandidates.map((candidate) => candidate.relativePath)),
|
||||
warnings,
|
||||
})
|
||||
: [];
|
||||
const candidates = [...scopedCandidates, ...adoptedCandidates];
|
||||
const candidates = scopedCandidates;
|
||||
if (!scopePrepared && candidates.length === 0) {
|
||||
warnings.push("artifact scope is not prepared for this task run");
|
||||
}
|
||||
@ -297,45 +284,6 @@ export function formatArtifactManifestMarkdown(input) {
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
async function adoptWorkspaceRootCandidatesIntoScope(input) {
|
||||
const rootCandidates = await collectCandidates({
|
||||
scanRoot: input.workspaceRoot,
|
||||
relativeRoot: input.workspaceRoot,
|
||||
sinceUnixMs: input.sinceUnixMs,
|
||||
skipTaskScopeRoot: true,
|
||||
warnSkippedSymlinks: false,
|
||||
warnings: input.warnings,
|
||||
});
|
||||
const adopted = [];
|
||||
for (const candidate of rootCandidates) {
|
||||
if (input.existingRelativePaths.has(candidate.relativePath)) {
|
||||
continue;
|
||||
}
|
||||
const targetPath = path.join(input.scopeRoot, candidate.relativePath.split("/").join(path.sep));
|
||||
if (!isWithinRoot(input.scopeRoot, targetPath)) {
|
||||
input.warnings.push(`skipped path outside task scope ${candidate.relativePath}`);
|
||||
continue;
|
||||
}
|
||||
if (await fileExists(targetPath)) {
|
||||
input.existingRelativePaths.add(candidate.relativePath);
|
||||
continue;
|
||||
}
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await fs.copyFile(candidate.absolutePath, targetPath);
|
||||
const stat = await fs.stat(targetPath);
|
||||
const realPath = await fs.realpath(targetPath);
|
||||
adopted.push({
|
||||
absolutePath: realPath,
|
||||
relativePath: candidate.relativePath,
|
||||
sizeBytes: stat.size,
|
||||
mtimeMs: candidate.mtimeMs,
|
||||
artifactScope: input.artifactScope,
|
||||
scopeKind: "task",
|
||||
});
|
||||
input.existingRelativePaths.add(candidate.relativePath);
|
||||
}
|
||||
return adopted;
|
||||
}
|
||||
async function collectCandidates(input) {
|
||||
const candidates = [];
|
||||
await walk(input.scanRoot);
|
||||
@ -362,9 +310,6 @@ async function collectCandidates(input) {
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
if (input.skipTaskScopeRoot && currentDir === input.relativeRoot && entry.name === TASK_SCOPE_ROOT) {
|
||||
continue;
|
||||
}
|
||||
if (SKIPPED_DIRS.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
@ -419,6 +364,7 @@ function safeScopeSegment(value) {
|
||||
.trim()
|
||||
.replace(/[\\/]+/g, "_")
|
||||
.replace(/[^A-Za-z0-9._-]+/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^[._-]+|[._-]+$/g, "")
|
||||
.slice(0, 96) || "scope";
|
||||
}
|
||||
@ -468,15 +414,6 @@ async function directoryExists(absolutePath) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function fileExists(absolutePath) {
|
||||
try {
|
||||
const stat = await fs.stat(absolutePath);
|
||||
return stat.isFile();
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function safeArtifactRefRunScope(value) {
|
||||
try {
|
||||
return safeTaskArtifactScope(value);
|
||||
|
||||
@ -339,7 +339,15 @@ describe("plugin registration", () => {
|
||||
const factory = tools[0]?.tool as (ctx: Record<string, unknown>) => {
|
||||
execute: (id: string, params: Record<string, unknown>) => Promise<{ content: Array<{ text: string }> }>;
|
||||
};
|
||||
const tool = factory({ sessionKey: "thread-main", runId: "turn-1", workspaceDir: root });
|
||||
const tool = factory({
|
||||
sessionScope: {
|
||||
scopeKind: "run",
|
||||
sessionKey: "thread-main",
|
||||
runId: "turn-1",
|
||||
workspaceDir: root,
|
||||
relativeTaskDirectory: "tasks/thread-main/turn-1",
|
||||
},
|
||||
});
|
||||
const result = await tool.execute("call-1", {
|
||||
action: "list",
|
||||
sessionKey: "thread-other",
|
||||
|
||||
71
index.ts
71
index.ts
@ -3,6 +3,7 @@ import type {
|
||||
GatewayRequestHandlerOptions,
|
||||
OpenClawPluginApi,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import {
|
||||
exportXWorkmateArtifacts,
|
||||
prepareXWorkmateArtifacts,
|
||||
@ -15,13 +16,63 @@ type XWorkmateToolContext = {
|
||||
workspaceDir?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
sessionScope?: {
|
||||
sessionScope?: XWorkmatePluginSessionScope;
|
||||
};
|
||||
|
||||
type XWorkmatePluginSessionScope = {
|
||||
scopeKind?: "global" | "session" | "run";
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
workspaceDir?: string;
|
||||
relativeTaskDirectory?: string;
|
||||
};
|
||||
|
||||
type XWorkmateResolvedRunScope = {
|
||||
sessionKey: string;
|
||||
runId: string;
|
||||
workspaceDir?: string;
|
||||
artifactScope?: string;
|
||||
};
|
||||
|
||||
type XWorkmateGatewayRequestScope = {
|
||||
sessionScope?: XWorkmatePluginSessionScope;
|
||||
};
|
||||
|
||||
function scopedGatewayParams(params: Record<string, unknown>): Record<string, unknown> {
|
||||
const sessionScope = (getPluginRuntimeGatewayRequestScope() as XWorkmateGatewayRequestScope | undefined)?.sessionScope;
|
||||
const runScope = resolveRunScope({ sessionScope });
|
||||
if (!runScope) {
|
||||
return params;
|
||||
}
|
||||
return {
|
||||
...params,
|
||||
sessionKey: runScope.sessionKey,
|
||||
runId: runScope.runId,
|
||||
...(runScope.workspaceDir ? { workspaceDir: runScope.workspaceDir } : {}),
|
||||
...(runScope.artifactScope ? { artifactScope: runScope.artifactScope } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRunScope(ctx: {
|
||||
sessionScope?: XWorkmatePluginSessionScope;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
workspaceDir?: string;
|
||||
}): XWorkmateResolvedRunScope | undefined {
|
||||
const scope = ctx.sessionScope;
|
||||
const sessionKey = scope?.sessionKey || ctx.sessionKey;
|
||||
const runId = scope?.runId || ctx.runId || "";
|
||||
if (!sessionKey || !runId) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
sessionKey,
|
||||
runId,
|
||||
...(scope?.workspaceDir || ctx.workspaceDir ? { workspaceDir: scope?.workspaceDir || ctx.workspaceDir } : {}),
|
||||
...(scope?.relativeTaskDirectory ? { artifactScope: scope.relativeTaskDirectory } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
id: "openclaw-multi-session-plugins",
|
||||
name: "openclaw-multi-session-plugins",
|
||||
@ -35,7 +86,7 @@ function register(api: OpenClawPluginApi) {
|
||||
api.registerGatewayMethod("xworkmate.artifacts.prepare", async (opts: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const payload = await prepareXWorkmateArtifacts({
|
||||
params: opts.params,
|
||||
params: scopedGatewayParams(opts.params),
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
@ -50,7 +101,7 @@ function register(api: OpenClawPluginApi) {
|
||||
api.registerGatewayMethod("xworkmate.artifacts.export", async (opts: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const payload = await exportXWorkmateArtifacts({
|
||||
params: opts.params,
|
||||
params: scopedGatewayParams(opts.params),
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
@ -65,7 +116,7 @@ function register(api: OpenClawPluginApi) {
|
||||
api.registerGatewayMethod("xworkmate.artifacts.list", async (opts: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const payload = await exportXWorkmateArtifacts({
|
||||
params: { ...opts.params, includeContent: false },
|
||||
params: { ...scopedGatewayParams(opts.params), includeContent: false },
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
@ -80,7 +131,7 @@ function register(api: OpenClawPluginApi) {
|
||||
api.registerGatewayMethod("xworkmate.artifacts.read", async (opts: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const payload = await readXWorkmateArtifact({
|
||||
params: opts.params,
|
||||
params: scopedGatewayParams(opts.params),
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
@ -95,7 +146,7 @@ function register(api: OpenClawPluginApi) {
|
||||
api.registerGatewayMethod("xworkmate.agents.run", async (opts: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const payload = await runXWorkmateBridgeAgents({
|
||||
params: opts.params,
|
||||
params: scopedGatewayParams(opts.params),
|
||||
config: api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
});
|
||||
@ -164,15 +215,16 @@ function createXWorkmateArtifactsTool(
|
||||
},
|
||||
async execute(_id: string, params: Record<string, unknown>) {
|
||||
const action = typeof params.action === "string" ? params.action : "";
|
||||
const runScope = resolveRunScope(ctx);
|
||||
const sessionKey = ctx.sessionScope?.sessionKey || ctx.sessionKey;
|
||||
const runId = ctx.sessionScope?.runId || ctx.runId || "";
|
||||
const workspaceDir = ctx.sessionScope?.workspaceDir || ctx.workspaceDir;
|
||||
if (!sessionKey) {
|
||||
throw new Error("sessionKey required");
|
||||
}
|
||||
if (!runId) {
|
||||
throw new Error("runId required");
|
||||
}
|
||||
const workspaceDir = ctx.sessionScope?.workspaceDir || ctx.workspaceDir;
|
||||
const {
|
||||
sessionKey: _ignoredSessionKey,
|
||||
runId: _ignoredRunId,
|
||||
@ -184,6 +236,7 @@ function createXWorkmateArtifactsTool(
|
||||
sessionKey,
|
||||
runId,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
...(runScope?.artifactScope ? { artifactScope: runScope.artifactScope } : {}),
|
||||
};
|
||||
if (action === "list") {
|
||||
const payload = await exportXWorkmateArtifacts({
|
||||
@ -275,15 +328,16 @@ function createXWorkmateAgentsTool(
|
||||
required: ["taskPrompt"],
|
||||
},
|
||||
async execute(_id: string, params: Record<string, unknown>) {
|
||||
const runScope = resolveRunScope(ctx);
|
||||
const sessionKey = ctx.sessionScope?.sessionKey || ctx.sessionKey;
|
||||
const runId = ctx.sessionScope?.runId || ctx.runId || "";
|
||||
const workspaceDir = ctx.sessionScope?.workspaceDir || ctx.workspaceDir;
|
||||
if (!sessionKey) {
|
||||
throw new Error("sessionKey required");
|
||||
}
|
||||
if (!runId) {
|
||||
throw new Error("runId required");
|
||||
}
|
||||
const workspaceDir = ctx.sessionScope?.workspaceDir || ctx.workspaceDir;
|
||||
const {
|
||||
sessionKey: _ignoredSessionKey,
|
||||
runId: _ignoredRunId,
|
||||
@ -296,6 +350,7 @@ function createXWorkmateAgentsTool(
|
||||
sessionKey,
|
||||
runId,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
...(runScope?.artifactScope ? { artifactScope: runScope.artifactScope } : {}),
|
||||
},
|
||||
config: ctx.config ?? api.config,
|
||||
pluginConfig: api.pluginConfig,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw-multi-session-plugins",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"description": "OpenClaw multi-session plugin runtime support for scoped XWorkmate artifacts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@ -44,10 +44,9 @@
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"openclaw": "2026.5.3-1",
|
||||
"openclaw": "2026.5.28",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
|
||||
3197
pnpm-lock.yaml
generated
3197
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -30,6 +30,17 @@ describe("exportXWorkmateArtifacts", () => {
|
||||
expect(first.scopeKind).toBe("task");
|
||||
});
|
||||
|
||||
it("normalizes task scope segments like the OpenClaw session scope runtime", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||
|
||||
const prepared = await prepareXWorkmateArtifacts({
|
||||
params: { sessionKey: "agent::main:main", runId: "run alpha" },
|
||||
pluginConfig: { workspaceDir: root },
|
||||
});
|
||||
|
||||
expect(prepared.artifactScope).toBe("tasks/agent_main_main/run_alpha");
|
||||
});
|
||||
|
||||
it("exports changed files with metadata and base64 content", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||
const prepared = await prepareXWorkmateArtifacts({
|
||||
@ -149,6 +160,32 @@ describe("exportXWorkmateArtifacts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("exports nested dist and build deliverables inside the task scope", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||
const prepared = await prepareXWorkmateArtifacts({
|
||||
params: { sessionKey: "thread-main", runId: "turn-1" },
|
||||
pluginConfig: { workspaceDir: root },
|
||||
});
|
||||
await fs.mkdir(path.join(prepared.artifactDirectory, "dist"), { recursive: true });
|
||||
await fs.mkdir(path.join(prepared.artifactDirectory, "build", "assets"), { recursive: true });
|
||||
await fs.writeFile(path.join(prepared.artifactDirectory, "dist", "final.pdf"), "pdf");
|
||||
await fs.writeFile(path.join(prepared.artifactDirectory, "build", "assets", "cover.png"), "png");
|
||||
|
||||
const result = await exportXWorkmateArtifacts({
|
||||
params: {
|
||||
sessionKey: "thread-main",
|
||||
runId: "turn-1",
|
||||
maxInlineBytes: 0,
|
||||
},
|
||||
pluginConfig: { workspaceDir: root },
|
||||
});
|
||||
|
||||
expect(result.artifacts.map((entry) => entry.relativePath)).toEqual([
|
||||
"build/assets/cover.png",
|
||||
"dist/final.pdf",
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses the current task scope when artifactScope is omitted", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||
const current = await prepareXWorkmateArtifacts({
|
||||
@ -198,63 +235,7 @@ describe("exportXWorkmateArtifacts", () => {
|
||||
expect(result.manifestMarkdown).toContain("Artifact scope: `tasks/thread-main/turn-1`");
|
||||
});
|
||||
|
||||
it("adopts current-run workspace root files into the task artifact scope", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||
await prepareXWorkmateArtifacts({
|
||||
params: { sessionKey: "thread-main", runId: "turn-1" },
|
||||
pluginConfig: { workspaceDir: root },
|
||||
});
|
||||
const sinceUnixMs = Date.now() - 1_000;
|
||||
await fs.writeFile(path.join(root, "xhs_account_security.md"), "# Account security\n");
|
||||
|
||||
const result = await exportXWorkmateArtifacts({
|
||||
params: {
|
||||
sessionKey: "thread-main",
|
||||
runId: "turn-1",
|
||||
sinceUnixMs,
|
||||
},
|
||||
pluginConfig: { workspaceDir: root },
|
||||
});
|
||||
|
||||
expect(result.scopeKind).toBe("task");
|
||||
expect(result.artifactScope).toBe("tasks/thread-main/turn-1");
|
||||
expect(result.artifacts.map((entry) => entry.relativePath)).toEqual(["xhs_account_security.md"]);
|
||||
expect(result.artifacts[0]).toMatchObject({
|
||||
artifactScope: "tasks/thread-main/turn-1",
|
||||
scopeKind: "task",
|
||||
contentType: "text/markdown",
|
||||
encoding: "base64",
|
||||
content: Buffer.from("# Account security\n").toString("base64"),
|
||||
});
|
||||
expect(await fs.readFile(path.join(root, "tasks", "thread-main", "turn-1", "xhs_account_security.md"), "utf8")).toBe(
|
||||
"# Account security\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("creates the current task scope when adopting root files after bridge export", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||
const sinceUnixMs = Date.now() - 1_000;
|
||||
await fs.mkdir(path.join(root, "reports"), { recursive: true });
|
||||
await fs.writeFile(path.join(root, "reports", "final.md"), "final");
|
||||
|
||||
const result = await exportXWorkmateArtifacts({
|
||||
params: {
|
||||
sessionKey: "thread-main",
|
||||
runId: "turn-1",
|
||||
sinceUnixMs,
|
||||
},
|
||||
pluginConfig: { workspaceDir: root },
|
||||
});
|
||||
|
||||
expect(result.artifactScope).toBe("tasks/thread-main/turn-1");
|
||||
expect(result.artifacts.map((entry) => entry.relativePath)).toEqual(["reports/final.md"]);
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(await fs.readFile(path.join(root, "tasks", "thread-main", "turn-1", "reports", "final.md"), "utf8")).toBe(
|
||||
"final",
|
||||
);
|
||||
});
|
||||
|
||||
it("adopts root Word documents into only the current task scope", async () => {
|
||||
it("does not adopt workspace root files even with a current-run timestamp", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-plugins-"));
|
||||
const sinceUnixMs = Date.now() - 1_000;
|
||||
await fs.writeFile(path.join(root, "article.docx"), "docx-content");
|
||||
@ -270,18 +251,8 @@ describe("exportXWorkmateArtifacts", () => {
|
||||
});
|
||||
|
||||
expect(result.artifactScope).toBe("tasks/draft-article/openclaw-run-1");
|
||||
expect(result.artifacts).toHaveLength(1);
|
||||
expect(result.artifacts[0]).toMatchObject({
|
||||
relativePath: "article.docx",
|
||||
artifactScope: "tasks/draft-article/openclaw-run-1",
|
||||
scopeKind: "task",
|
||||
contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
});
|
||||
expect(result.artifacts[0]?.encoding).toBeUndefined();
|
||||
expect(await fs.readFile(path.join(root, "tasks", "draft-article", "openclaw-run-1", "article.docx"), "utf8")).toBe(
|
||||
"docx-content",
|
||||
);
|
||||
await expect(fs.stat(path.join(root, "tasks", "draft-article", "turn-1", "article.docx"))).rejects.toThrow();
|
||||
expect(result.artifacts).toEqual([]);
|
||||
await expect(fs.stat(path.join(root, "tasks", "draft-article", "openclaw-run-1", "article.docx"))).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("does not adopt old workspace root files into a later task scope", async () => {
|
||||
|
||||
@ -16,8 +16,6 @@ const SKIPPED_DIRS = new Set([
|
||||
".dart_tool",
|
||||
".next",
|
||||
".turbo",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
]);
|
||||
|
||||
@ -166,23 +164,11 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
||||
scanRoot: scopeRoot,
|
||||
relativeRoot: scopeRoot,
|
||||
sinceUnixMs,
|
||||
skipTaskScopeRoot: false,
|
||||
warnSkippedSymlinks: true,
|
||||
warnings,
|
||||
})
|
||||
: [];
|
||||
const adoptedCandidates =
|
||||
sinceUnixMs > 0
|
||||
? await adoptWorkspaceRootCandidatesIntoScope({
|
||||
workspaceRoot,
|
||||
scopeRoot,
|
||||
artifactScope,
|
||||
sinceUnixMs,
|
||||
existingRelativePaths: new Set(scopedCandidates.map((candidate) => candidate.relativePath)),
|
||||
warnings,
|
||||
})
|
||||
: [];
|
||||
const candidates = [...scopedCandidates, ...adoptedCandidates];
|
||||
const candidates = scopedCandidates;
|
||||
if (!scopePrepared && candidates.length === 0) {
|
||||
warnings.push("artifact scope is not prepared for this task run");
|
||||
}
|
||||
@ -403,58 +389,10 @@ export function formatArtifactManifestMarkdown(input: {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function adoptWorkspaceRootCandidatesIntoScope(input: {
|
||||
workspaceRoot: string;
|
||||
scopeRoot: string;
|
||||
artifactScope: string;
|
||||
sinceUnixMs: number;
|
||||
existingRelativePaths: Set<string>;
|
||||
warnings: string[];
|
||||
}): Promise<Candidate[]> {
|
||||
const rootCandidates = await collectCandidates({
|
||||
scanRoot: input.workspaceRoot,
|
||||
relativeRoot: input.workspaceRoot,
|
||||
sinceUnixMs: input.sinceUnixMs,
|
||||
skipTaskScopeRoot: true,
|
||||
warnSkippedSymlinks: false,
|
||||
warnings: input.warnings,
|
||||
});
|
||||
const adopted: Candidate[] = [];
|
||||
for (const candidate of rootCandidates) {
|
||||
if (input.existingRelativePaths.has(candidate.relativePath)) {
|
||||
continue;
|
||||
}
|
||||
const targetPath = path.join(input.scopeRoot, candidate.relativePath.split("/").join(path.sep));
|
||||
if (!isWithinRoot(input.scopeRoot, targetPath)) {
|
||||
input.warnings.push(`skipped path outside task scope ${candidate.relativePath}`);
|
||||
continue;
|
||||
}
|
||||
if (await fileExists(targetPath)) {
|
||||
input.existingRelativePaths.add(candidate.relativePath);
|
||||
continue;
|
||||
}
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await fs.copyFile(candidate.absolutePath, targetPath);
|
||||
const stat = await fs.stat(targetPath);
|
||||
const realPath = await fs.realpath(targetPath);
|
||||
adopted.push({
|
||||
absolutePath: realPath,
|
||||
relativePath: candidate.relativePath,
|
||||
sizeBytes: stat.size,
|
||||
mtimeMs: candidate.mtimeMs,
|
||||
artifactScope: input.artifactScope,
|
||||
scopeKind: "task",
|
||||
});
|
||||
input.existingRelativePaths.add(candidate.relativePath);
|
||||
}
|
||||
return adopted;
|
||||
}
|
||||
|
||||
async function collectCandidates(input: {
|
||||
scanRoot: string;
|
||||
relativeRoot: string;
|
||||
sinceUnixMs: number;
|
||||
skipTaskScopeRoot: boolean;
|
||||
warnSkippedSymlinks: boolean;
|
||||
warnings: string[];
|
||||
}): Promise<Candidate[]> {
|
||||
@ -483,9 +421,6 @@ async function collectCandidates(input: {
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
if (input.skipTaskScopeRoot && currentDir === input.relativeRoot && entry.name === TASK_SCOPE_ROOT) {
|
||||
continue;
|
||||
}
|
||||
if (SKIPPED_DIRS.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
@ -552,6 +487,7 @@ function safeScopeSegment(value: string): string {
|
||||
.trim()
|
||||
.replace(/[\\/]+/g, "_")
|
||||
.replace(/[^A-Za-z0-9._-]+/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^[._-]+|[._-]+$/g, "")
|
||||
.slice(0, 96) || "scope";
|
||||
}
|
||||
@ -604,15 +540,6 @@ async function directoryExists(absolutePath: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
async function fileExists(absolutePath: string): Promise<boolean> {
|
||||
try {
|
||||
const stat = await fs.stat(absolutePath);
|
||||
return stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function safeArtifactRefRunScope(value: unknown): string {
|
||||
try {
|
||||
return safeTaskArtifactScope(value);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user