Files
openclaw/extensions/skill-workshop/src/tool.ts
Peter Steinberger 77d9ac30bb refactor: reuse shared coercion helpers (#86419)
* refactor: share talk event metric extraction

* refactor: reuse shared coercion helpers

* refactor: reuse shared primitive guards

* refactor: reuse shared record guard

* refactor: reuse shared primitive helpers

* refactor: reuse shared string guards

* refactor: reuse shared non-empty string guard

* refactor: share plugin primitive coercion helpers

* refactor: reuse plugin coercion helpers

* refactor: reuse plugin coercion helpers in more plugins

* refactor: reuse channel coercion helpers

* refactor: reuse monitor coercion helpers

* refactor: reuse provider coercion helpers

* refactor: reuse core coercion helpers

* refactor: reuse runtime coercion helpers

* refactor: reuse helper coercion in codex paths

* refactor: reuse helper coercion in runtime paths

* refactor: reuse codex app-server coercion helpers

* refactor: reuse codex record helpers

* refactor: reuse migration and qa record helpers

* refactor: reuse feishu and core helper guards

* refactor: reuse browser and policy coercion helpers

* refactor: reuse memory wiki record helper

* refactor: share boolean coercion helpers

* refactor: reuse finite number coercion

* refactor: reuse trimmed string list helpers

* refactor: reuse string list normalization

* refactor: reuse remaining string list helpers

* refactor: reuse string entry normalizer

* refactor: share sorted string helpers

* refactor: share string list normalization

* test: preserve command registry browser imports

* refactor: reuse trimmed list helpers

* refactor: reuse string dedupe helpers

* refactor: reuse local dedupe helpers

* refactor: reuse more string dedupe helpers

* refactor: reuse command string dedupe helpers

* refactor: dedupe memory path lists with helper

* refactor: expose string dedupe helpers to plugins

* refactor: reuse core string dedupe helpers

* refactor: reuse shared unique value helpers

* refactor: reuse unique helpers in agent utilities

* refactor: reuse unique helpers in config plumbing

* refactor: reuse unique helpers in extensions

* refactor: reuse unique helpers in core utilities

* refactor: reuse unique helpers in qa plugins

* refactor: reuse unique helpers in memory plugins

* refactor: reuse unique helpers in channel plugins

* refactor: reuse unique helpers in core tails

* refactor: reuse unique helper in comfy workflow

* refactor: reuse unique helpers in test utilities

* refactor: expose unique value helper to plugins

* refactor: reuse unique helpers for numeric lists

* refactor: replace index dedupe filters

* refactor: reuse string entry normalization

* refactor: reuse string normalization in plugin helpers

* refactor: reuse string normalization in extension helpers

* refactor: reuse string normalization in channel parsers

* refactor: reuse string normalization in memory search

* refactor: reuse string normalization in provider parsers

* refactor: reuse string normalization in qa helpers

* refactor: reuse string normalization in infra parsers

* refactor: reuse string normalization in messaging parsers

* refactor: reuse string normalization in core parsers

* refactor: reuse string normalization in extension parsers

* refactor: reuse string normalization in remaining parsers

* refactor: reuse string normalization in final parser spots

* refactor: reuse string normalization in qa media helpers

* refactor: reuse normalization in provider and media lists

* refactor: reuse normalization for remaining set filters

* refactor: reuse normalization in policy allowlists

* refactor: reuse normalization in session and owner lists

* refactor: centralize primitive string lists

* refactor: reuse lowercase entry helpers

* refactor: reuse sorted string helpers

* refactor: reuse unique trimmed helpers

* refactor: reuse string normalization helpers

* refactor: reuse catalog string helpers

* refactor: reuse remaining string helpers

* refactor: simplify remaining list normalization

* refactor: reuse codex auth order normalization

* chore: refresh plugin sdk api baseline

* fix: make shared string sorting deterministic

* chore: refresh plugin sdk api baseline

* fix: align host env security ordering
2026-05-25 21:20:41 +01:00

198 lines
6.5 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { normalizeOptionalString as readString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { Type } from "typebox";
import { jsonResult, type OpenClawPluginApi } from "../api.js";
import type { SkillWorkshopConfig } from "./config.js";
import { applyProposalToWorkspace, normalizeSkillName, writeSupportFile } from "./skills.js";
import type { SkillChange, SkillProposal, SkillWorkshopStatus } from "./types.js";
import { applyOrStoreProposal, createStoreForContext, resolveWorkspaceDir } from "./workshop.js";
type ToolParams = {
action?: string;
id?: string;
status?: SkillWorkshopStatus;
skillName?: string;
title?: string;
reason?: string;
description?: string;
body?: string;
section?: string;
oldText?: string;
newText?: string;
relativePath?: string;
apply?: boolean;
};
function buildProposal(params: {
workspaceDir: string;
raw: ToolParams;
source: "tool";
}): SkillProposal {
const skillName = normalizeSkillName(readString(params.raw.skillName) ?? "");
if (!skillName) {
throw new Error("skillName required");
}
const now = Date.now();
const title = readString(params.raw.title) ?? `Skill update: ${skillName}`;
const reason = readString(params.raw.reason) ?? "Tool-created skill update";
const body = readString(params.raw.body);
const description = readString(params.raw.description) ?? title;
let change: SkillChange;
if (params.raw.oldText !== undefined || params.raw.newText !== undefined) {
const oldText = readString(params.raw.oldText);
const newText = readString(params.raw.newText);
if (!oldText || !newText) {
throw new Error("oldText and newText required for replace");
}
change = { kind: "replace", oldText, newText };
} else if (readString(params.raw.section)) {
if (!body) {
throw new Error("body required");
}
change = {
kind: "append",
section: readString(params.raw.section) ?? "Workflow",
body,
description,
};
} else {
if (!body) {
throw new Error("body required");
}
change = { kind: "create", description, body };
}
return {
id: randomUUID(),
createdAt: now,
updatedAt: now,
workspaceDir: params.workspaceDir,
skillName,
title,
reason,
source: params.source,
status: "pending",
change,
};
}
export function createSkillWorkshopTool(params: {
api: OpenClawPluginApi;
config: SkillWorkshopConfig;
ctx: { workspaceDir?: string };
}) {
return {
name: "skill_workshop",
label: "Skill Workshop",
description:
"Create, queue, inspect, approve, or safely apply workspace skill updates for repeatable workflows.",
parameters: Type.Object({
action: Type.String({
enum: [
"status",
"list_pending",
"list_quarantine",
"inspect",
"suggest",
"apply",
"reject",
"write_support_file",
],
}),
id: Type.Optional(Type.String()),
status: Type.Optional(
Type.String({ enum: ["pending", "applied", "rejected", "quarantined"] }),
),
skillName: Type.Optional(Type.String()),
title: Type.Optional(Type.String()),
reason: Type.Optional(Type.String()),
description: Type.Optional(Type.String()),
body: Type.Optional(Type.String()),
section: Type.Optional(Type.String()),
oldText: Type.Optional(Type.String()),
newText: Type.Optional(Type.String()),
relativePath: Type.Optional(Type.String()),
apply: Type.Optional(Type.Boolean()),
}),
async execute(_toolCallId: string, rawParams: Record<string, unknown>) {
const raw = rawParams as ToolParams;
const action = raw.action ?? "status";
const workspaceDir = resolveWorkspaceDir(params);
const store = createStoreForContext(params);
if (action === "status") {
const all = await store.list();
return jsonResult({
workspaceDir,
pending: all.filter((item) => item.status === "pending").length,
quarantined: all.filter((item) => item.status === "quarantined").length,
applied: all.filter((item) => item.status === "applied").length,
rejected: all.filter((item) => item.status === "rejected").length,
});
}
if (action === "list_pending") {
return jsonResult(await store.list(raw.status ?? "pending"));
}
if (action === "list_quarantine") {
return jsonResult(await store.list("quarantined"));
}
if (action === "inspect") {
if (!raw.id) {
throw new Error("id required");
}
return jsonResult(await store.get(raw.id));
}
if (action === "suggest") {
const proposal = buildProposal({ workspaceDir, raw, source: "tool" });
const result = await applyOrStoreProposal({
proposal,
store,
config: params.config,
workspaceDir,
skipAutoApply: raw.apply === false,
});
return jsonResult(result);
}
if (action === "apply") {
if (!raw.id) {
throw new Error("id required");
}
const proposal = await store.get(raw.id);
if (!proposal) {
throw new Error(`proposal not found: ${raw.id}`);
}
if (proposal.status === "quarantined") {
throw new Error("quarantined proposal cannot be applied");
}
const applied = await applyProposalToWorkspace({
proposal,
maxSkillBytes: params.config.maxSkillBytes,
});
const updated = await store.updateStatus(raw.id, "applied");
return jsonResult({ status: "applied", skillPath: applied.skillPath, proposal: updated });
}
if (action === "reject") {
if (!raw.id) {
throw new Error("id required");
}
return jsonResult(await store.updateStatus(raw.id, "rejected"));
}
if (action === "write_support_file") {
const skillName = readString(raw.skillName);
const relativePath = readString(raw.relativePath);
const body = raw.body;
if (!skillName || !relativePath || typeof body !== "string") {
throw new Error("skillName, relativePath, and body required");
}
const filePath = await writeSupportFile({
workspaceDir,
skillName,
relativePath,
content: body,
maxBytes: params.config.maxSkillBytes,
});
return jsonResult({ status: "written", filePath });
}
throw new Error(`unknown action: ${action}`);
},
};
}