Skip to content

Commit 44e26d5

Browse files
macclaude
authored andcommitted
feat: add OpenClaw plugin integration (dex openclaw init/export)
Add `dex openclaw init` to generate an OpenClaw plugin in the current directory and `dex openclaw export` to write a ready-to-use plugin package to ~/.openclaw/plugins/dex/. Each built-in dex skill is registered as an OpenClaw tool with a SKILL.md and proper parameter schemas. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4c5b648 commit 44e26d5

2 files changed

Lines changed: 286 additions & 0 deletions

File tree

src/cli/commands/openclaw.ts

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import { Command } from "commander";
2+
import { join } from "node:path";
3+
import { mkdir, writeFile, cp } from "node:fs/promises";
4+
import { existsSync } from "node:fs";
5+
import chalk from "chalk";
6+
import type { SkillRegistry } from "../../skills/registry.js";
7+
import type { Logger } from "../../core/logger.js";
8+
9+
/** Map a dex skill name to an OpenClaw-safe tool name (underscores, prefixed). */
10+
function toolName(skillName: string): string {
11+
return `dex_${skillName.replace(/-/g, "_")}`;
12+
}
13+
14+
interface SkillMeta {
15+
name: string;
16+
description: string;
17+
args?: { name: string; description: string; required?: boolean }[];
18+
flags?: {
19+
name: string;
20+
short?: string;
21+
type: "string" | "boolean" | "number";
22+
description?: string;
23+
default?: string | boolean | number;
24+
}[];
25+
}
26+
27+
function buildParameterSchema(skill: SkillMeta): string {
28+
const fields: string[] = [];
29+
30+
if (skill.args) {
31+
for (const arg of skill.args) {
32+
if (arg.required) {
33+
fields.push(` ${arg.name}: Type.String(),`);
34+
} else {
35+
fields.push(` ${arg.name}: Type.Optional(Type.String()),`);
36+
}
37+
}
38+
}
39+
40+
if (skill.flags) {
41+
for (const flag of skill.flags) {
42+
const typeStr =
43+
flag.type === "boolean"
44+
? "Type.Boolean()"
45+
: flag.type === "number"
46+
? "Type.Number()"
47+
: "Type.String()";
48+
fields.push(` ${flag.name}: Type.Optional(${typeStr}),`);
49+
}
50+
}
51+
52+
if (fields.length === 0) {
53+
return "Type.Object({})";
54+
}
55+
56+
return `Type.Object({\n${fields.join("\n")}\n })`;
57+
}
58+
59+
function buildCliArgs(skill: SkillMeta): string {
60+
const lines: string[] = [];
61+
62+
if (skill.args) {
63+
for (const arg of skill.args) {
64+
lines.push(` if (params.${arg.name}) args.push(params.${arg.name});`);
65+
}
66+
}
67+
68+
if (skill.flags) {
69+
for (const flag of skill.flags) {
70+
if (flag.type === "boolean") {
71+
lines.push(
72+
` if (params.${flag.name}) args.push("--${flag.name}");`,
73+
);
74+
} else {
75+
lines.push(
76+
` if (params.${flag.name} !== undefined) args.push("--${flag.name}", String(params.${flag.name}));`,
77+
);
78+
}
79+
}
80+
}
81+
82+
return lines.join("\n");
83+
}
84+
85+
function generateIndexTs(skills: SkillMeta[]): string {
86+
const registrations = skills
87+
.map((skill) => {
88+
const tName = toolName(skill.name);
89+
const paramSchema = buildParameterSchema(skill);
90+
const cliArgs = buildCliArgs(skill);
91+
92+
return ` api.registerTool({
93+
name: "${tName}",
94+
description: ${JSON.stringify(skill.description)},
95+
parameters: ${paramSchema},
96+
async execute(_id: string, params: Record<string, unknown>) {
97+
const args = ["run", "${skill.name}"];
98+
${cliArgs}
99+
const result = await execPromise("dex", args);
100+
return { content: [{ type: "text" as const, text: result }] };
101+
},
102+
});`;
103+
})
104+
.join("\n\n");
105+
106+
return `import { execFile } from "node:child_process";
107+
import { promisify } from "node:util";
108+
109+
const execFileAsync = promisify(execFile);
110+
111+
async function execPromise(cmd: string, args: string[]): Promise<string> {
112+
const { stdout, stderr } = await execFileAsync(cmd, args, {
113+
timeout: 120_000,
114+
maxBuffer: 10 * 1024 * 1024,
115+
});
116+
return (stdout + (stderr ? "\\nSTDERR:\\n" + stderr : "")).trim();
117+
}
118+
119+
// OpenClaw plugin entry point
120+
export default function register(api: any, Type: any) {
121+
${registrations}
122+
}
123+
`;
124+
}
125+
126+
function generateSkillMd(skill: SkillMeta): string {
127+
const tName = toolName(skill.name);
128+
const paramDocs: string[] = [];
129+
130+
if (skill.args) {
131+
for (const arg of skill.args) {
132+
const req = arg.required ? " (required)" : "";
133+
paramDocs.push(`- \`${arg.name}\`: ${arg.description}${req}`);
134+
}
135+
}
136+
137+
if (skill.flags) {
138+
for (const flag of skill.flags) {
139+
const defStr =
140+
flag.default !== undefined ? ` (default: ${flag.default})` : "";
141+
paramDocs.push(
142+
`- \`${flag.name}\`: ${flag.description ?? flag.name}${defStr}`,
143+
);
144+
}
145+
}
146+
147+
const paramSection =
148+
paramDocs.length > 0 ? `\n\nParameters:\n${paramDocs.join("\n")}` : "";
149+
150+
return `---
151+
name: ${tName}
152+
description: ${skill.description}
153+
---
154+
Use the \`${tName}\` tool to ${skill.description.toLowerCase()}.${paramSection}
155+
`;
156+
}
157+
158+
function generateManifest(skills: SkillMeta[]): object {
159+
return {
160+
id: "dex",
161+
name: "dex",
162+
description:
163+
"AI development tools — code review, commit messages, refactoring, and more",
164+
version: "1.1.0",
165+
skills: skills.map((s) => `skills/${s.name}`),
166+
};
167+
}
168+
169+
function extractSkillMetas(registry: SkillRegistry): SkillMeta[] {
170+
return registry.list().map((s) => ({
171+
name: s.manifest.name,
172+
description: s.manifest.description,
173+
args: s.manifest.inputs.args,
174+
flags: s.manifest.inputs.flags,
175+
}));
176+
}
177+
178+
export function createOpenClawCommand(
179+
registry: SkillRegistry,
180+
logger: Logger,
181+
): Command {
182+
const cmd = new Command("openclaw").description(
183+
"Generate OpenClaw plugin integration",
184+
);
185+
186+
cmd
187+
.command("init")
188+
.description(
189+
"Generate an OpenClaw plugin in the current directory",
190+
)
191+
.action(async () => {
192+
const cwd = process.cwd();
193+
const skills = extractSkillMetas(registry);
194+
195+
if (skills.length === 0) {
196+
logger.error("No skills loaded. Cannot generate plugin.");
197+
process.exitCode = 1;
198+
return;
199+
}
200+
201+
// Write manifest.json
202+
const manifest = generateManifest(skills);
203+
await writeFile(
204+
join(cwd, "manifest.json"),
205+
JSON.stringify(manifest, null, 2) + "\n",
206+
);
207+
console.log(chalk.green(" Created manifest.json"));
208+
209+
// Write index.ts
210+
await writeFile(join(cwd, "index.ts"), generateIndexTs(skills));
211+
console.log(chalk.green(" Created index.ts"));
212+
213+
// Write skills/ directory with SKILL.md for each skill
214+
const skillsDir = join(cwd, "skills");
215+
await mkdir(skillsDir, { recursive: true });
216+
217+
for (const skill of skills) {
218+
const skillDir = join(skillsDir, skill.name);
219+
await mkdir(skillDir, { recursive: true });
220+
await writeFile(join(skillDir, "SKILL.md"), generateSkillMd(skill));
221+
}
222+
console.log(
223+
chalk.green(` Created skills/ with ${skills.length} SKILL.md files`),
224+
);
225+
226+
console.log(
227+
chalk.cyan(
228+
"\nOpenClaw plugin generated. Place it in ~/.openclaw/plugins/dex/",
229+
),
230+
);
231+
});
232+
233+
cmd
234+
.command("export")
235+
.description(
236+
"Export a standalone OpenClaw plugin package to a target directory",
237+
)
238+
.argument(
239+
"[target]",
240+
"Target directory",
241+
join(process.env.HOME ?? "~", ".openclaw", "plugins", "dex"),
242+
)
243+
.action(async (target: string) => {
244+
const skills = extractSkillMetas(registry);
245+
246+
if (skills.length === 0) {
247+
logger.error("No skills loaded. Cannot generate plugin.");
248+
process.exitCode = 1;
249+
return;
250+
}
251+
252+
// Create target directory
253+
await mkdir(target, { recursive: true });
254+
255+
// Write manifest.json
256+
const manifest = generateManifest(skills);
257+
await writeFile(
258+
join(target, "manifest.json"),
259+
JSON.stringify(manifest, null, 2) + "\n",
260+
);
261+
262+
// Write index.ts
263+
await writeFile(join(target, "index.ts"), generateIndexTs(skills));
264+
265+
// Write skills/ directory
266+
const skillsDir = join(target, "skills");
267+
await mkdir(skillsDir, { recursive: true });
268+
269+
for (const skill of skills) {
270+
const skillDir = join(skillsDir, skill.name);
271+
await mkdir(skillDir, { recursive: true });
272+
await writeFile(join(skillDir, "SKILL.md"), generateSkillMd(skill));
273+
}
274+
275+
console.log(chalk.green(`OpenClaw plugin exported to ${target}`));
276+
console.log(
277+
chalk.gray(
278+
` ${skills.length} skills registered as OpenClaw tools`,
279+
),
280+
);
281+
});
282+
283+
return cmd;
284+
}

src/cli/program.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { createChatCommand } from "./commands/chat.js";
1818
import { createLoginCommand, createLogoutCommand } from "./commands/login.js";
1919
import { createUsageCommand } from "./commands/usage.js";
2020
import { createChainCommand } from "./commands/chain.js";
21+
import { createOpenClawCommand } from "./commands/openclaw.js";
2122
import { getVersion } from "../core/version.js";
2223

2324
export async function createProgram(): Promise<Command> {
@@ -82,6 +83,7 @@ export async function createProgram(): Promise<Command> {
8283
program.addCommand(createLogoutCommand());
8384
program.addCommand(createUsageCommand());
8485
program.addCommand(createChainCommand(registry, config, logger));
86+
program.addCommand(createOpenClawCommand(registry, logger));
8587

8688
// Init command
8789
program

0 commit comments

Comments
 (0)