clawchef 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ export interface ScaffoldOptions {
2
+ projectName?: string;
3
+ }
4
+ export interface ScaffoldResult {
5
+ targetDir: string;
6
+ projectName: string;
7
+ }
8
+ export declare function scaffoldProject(targetDirArg?: string, options?: ScaffoldOptions): Promise<ScaffoldResult>;
@@ -0,0 +1,172 @@
1
+ import path from "node:path";
2
+ import { access, mkdir, readdir, writeFile } from "node:fs/promises";
3
+ import { constants } from "node:fs";
4
+ import { ClawChefError } from "./errors.js";
5
+ function normalizeProjectName(value) {
6
+ const normalized = value
7
+ .trim()
8
+ .toLowerCase()
9
+ .replace(/[\s_]+/g, "-")
10
+ .replace(/[^a-z0-9-]/g, "-")
11
+ .replace(/-+/g, "-")
12
+ .replace(/^-|-$/g, "");
13
+ if (!normalized) {
14
+ throw new ClawChefError("Project name is empty after normalization. Use letters, numbers, spaces, underscore, or dash.");
15
+ }
16
+ return normalized;
17
+ }
18
+ async function pathExists(filePath) {
19
+ try {
20
+ await access(filePath, constants.F_OK);
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ async function ensureDirectoryEmpty(targetDir) {
28
+ const exists = await pathExists(targetDir);
29
+ if (!exists) {
30
+ await mkdir(targetDir, { recursive: true });
31
+ return;
32
+ }
33
+ const entries = await readdir(targetDir);
34
+ if (entries.length > 0) {
35
+ throw new ClawChefError(`Target directory is not empty: ${targetDir}`);
36
+ }
37
+ }
38
+ function makePackageJson(projectName) {
39
+ const content = {
40
+ name: `${projectName}-recipe`,
41
+ version: "0.1.0",
42
+ private: true,
43
+ type: "module",
44
+ scripts: {
45
+ "test:recipe": "node --test test/recipe-smoke.test.mjs",
46
+ test: "node --test \"test/**/*.test.mjs\"",
47
+ },
48
+ devDependencies: {
49
+ "telegram-api-mock-server": "^0.1.5",
50
+ },
51
+ };
52
+ return `${JSON.stringify(content, null, 2)}\n`;
53
+ }
54
+ function makeRecipeYaml(projectName) {
55
+ return `version: "1"
56
+ name: "${projectName}"
57
+
58
+ params:
59
+ openclaw_version:
60
+ default: "2026.2.9"
61
+ workspace_name:
62
+ default: "${projectName}"
63
+ agent_name:
64
+ default: "${projectName}"
65
+ agent_model:
66
+ default: "openai/gpt-4.1"
67
+ telegram_mock_api_key:
68
+ required: true
69
+
70
+ openclaw:
71
+ bin: "openclaw"
72
+ version: "\${openclaw_version}"
73
+ install: "never"
74
+ plugins:
75
+ - "openclaw-telegram-mock-channel"
76
+
77
+ workspaces:
78
+ - name: "\${workspace_name}"
79
+ assets: "./${projectName}-assets"
80
+
81
+ agents:
82
+ - workspace: "\${workspace_name}"
83
+ name: "\${agent_name}"
84
+ model: "\${agent_model}"
85
+
86
+ channels:
87
+ - channel: "telegram-mock"
88
+ account: "default"
89
+ token: "\${telegram_mock_api_key}"
90
+ extra_flags:
91
+ mock_bind: "127.0.0.1:18790"
92
+ mock_api_key: "\${telegram_mock_api_key}"
93
+ mode: "webhook"
94
+ `;
95
+ }
96
+ function makeAgentsDoc(projectName) {
97
+ return `# ${projectName}
98
+
99
+ Project scaffold generated by clawchef.
100
+ `;
101
+ }
102
+ function makeIdentityDoc(projectName) {
103
+ return `# Identity
104
+
105
+ You are the ${projectName} assistant.
106
+ `;
107
+ }
108
+ function makeSoulDoc() {
109
+ return `# Soul
110
+
111
+ Keep responses practical, concise, and action-oriented.
112
+ `;
113
+ }
114
+ function makeToolsDoc() {
115
+ return `# Tools
116
+
117
+ - Use available workspace files first.
118
+ - Ask for missing secrets explicitly.
119
+ `;
120
+ }
121
+ function makeSchedulingScript() {
122
+ return `#!/usr/bin/env node
123
+ import process from "node:process";
124
+
125
+ const message = process.argv.slice(2).join(" ").trim();
126
+ if (!message) {
127
+ console.error("Usage: node scripts/scheduling.mjs <message>");
128
+ process.exit(1);
129
+ }
130
+
131
+ console.log("[scheduling] " + message);
132
+ `;
133
+ }
134
+ function makeRecipeSmokeTest() {
135
+ return `import test from "node:test";
136
+ import assert from "node:assert/strict";
137
+ import { access } from "node:fs/promises";
138
+
139
+ test("recipe scaffold files exist", async () => {
140
+ await access("src/recipe.yaml");
141
+ await access("package.json");
142
+ assert.ok(true);
143
+ });
144
+ `;
145
+ }
146
+ export async function scaffoldProject(targetDirArg, options = {}) {
147
+ const targetDir = path.resolve(targetDirArg?.trim() ? targetDirArg : process.cwd());
148
+ await ensureDirectoryEmpty(targetDir);
149
+ const defaultName = path.basename(targetDir);
150
+ const rawProjectName = options.projectName?.trim() || defaultName;
151
+ const projectName = normalizeProjectName(rawProjectName);
152
+ const srcDir = path.join(targetDir, "src");
153
+ const assetsDir = path.join(srcDir, `${projectName}-assets`);
154
+ const assetsScriptsDir = path.join(assetsDir, "scripts");
155
+ const testDir = path.join(targetDir, "test");
156
+ await mkdir(srcDir, { recursive: true });
157
+ await mkdir(assetsDir, { recursive: true });
158
+ await mkdir(assetsScriptsDir, { recursive: true });
159
+ await mkdir(testDir, { recursive: true });
160
+ await writeFile(path.join(targetDir, "package.json"), makePackageJson(projectName), "utf8");
161
+ await writeFile(path.join(srcDir, "recipe.yaml"), makeRecipeYaml(projectName), "utf8");
162
+ await writeFile(path.join(assetsDir, "AGENTS.md"), makeAgentsDoc(projectName), "utf8");
163
+ await writeFile(path.join(assetsDir, "IDENTITY.md"), makeIdentityDoc(projectName), "utf8");
164
+ await writeFile(path.join(assetsDir, "SOUL.md"), makeSoulDoc(), "utf8");
165
+ await writeFile(path.join(assetsDir, "TOOLS.md"), makeToolsDoc(), "utf8");
166
+ await writeFile(path.join(assetsScriptsDir, "scheduling.mjs"), makeSchedulingScript(), "utf8");
167
+ await writeFile(path.join(testDir, "recipe-smoke.test.mjs"), makeRecipeSmokeTest(), "utf8");
168
+ return {
169
+ targetDir,
170
+ projectName,
171
+ };
172
+ }
package/dist/schema.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare const recipeSchema: z.ZodObject<{
19
19
  bin: z.ZodOptional<z.ZodString>;
20
20
  version: z.ZodString;
21
21
  install: z.ZodOptional<z.ZodEnum<["auto", "always", "never"]>>;
22
+ plugins: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
22
23
  bootstrap: z.ZodOptional<z.ZodObject<{
23
24
  non_interactive: z.ZodOptional<z.ZodBoolean>;
24
25
  accept_risk: z.ZodOptional<z.ZodBoolean>;
@@ -102,6 +103,7 @@ export declare const recipeSchema: z.ZodObject<{
102
103
  use_version: z.ZodOptional<z.ZodString>;
103
104
  install_version: z.ZodOptional<z.ZodString>;
104
105
  uninstall_version: z.ZodOptional<z.ZodString>;
106
+ install_plugin: z.ZodOptional<z.ZodString>;
105
107
  factory_reset: z.ZodOptional<z.ZodString>;
106
108
  start_gateway: z.ZodOptional<z.ZodString>;
107
109
  enable_plugin: z.ZodOptional<z.ZodString>;
@@ -115,6 +117,7 @@ export declare const recipeSchema: z.ZodObject<{
115
117
  use_version?: string | undefined;
116
118
  install_version?: string | undefined;
117
119
  uninstall_version?: string | undefined;
120
+ install_plugin?: string | undefined;
118
121
  factory_reset?: string | undefined;
119
122
  start_gateway?: string | undefined;
120
123
  enable_plugin?: string | undefined;
@@ -128,6 +131,7 @@ export declare const recipeSchema: z.ZodObject<{
128
131
  use_version?: string | undefined;
129
132
  install_version?: string | undefined;
130
133
  uninstall_version?: string | undefined;
134
+ install_plugin?: string | undefined;
131
135
  factory_reset?: string | undefined;
132
136
  start_gateway?: string | undefined;
133
137
  enable_plugin?: string | undefined;
@@ -142,6 +146,7 @@ export declare const recipeSchema: z.ZodObject<{
142
146
  version: string;
143
147
  bin?: string | undefined;
144
148
  install?: "auto" | "always" | "never" | undefined;
149
+ plugins?: string[] | undefined;
145
150
  bootstrap?: {
146
151
  openai_api_key?: string | undefined;
147
152
  anthropic_api_key?: string | undefined;
@@ -173,6 +178,7 @@ export declare const recipeSchema: z.ZodObject<{
173
178
  use_version?: string | undefined;
174
179
  install_version?: string | undefined;
175
180
  uninstall_version?: string | undefined;
181
+ install_plugin?: string | undefined;
176
182
  factory_reset?: string | undefined;
177
183
  start_gateway?: string | undefined;
178
184
  enable_plugin?: string | undefined;
@@ -187,6 +193,7 @@ export declare const recipeSchema: z.ZodObject<{
187
193
  version: string;
188
194
  bin?: string | undefined;
189
195
  install?: "auto" | "always" | "never" | undefined;
196
+ plugins?: string[] | undefined;
190
197
  bootstrap?: {
191
198
  openai_api_key?: string | undefined;
192
199
  anthropic_api_key?: string | undefined;
@@ -218,6 +225,7 @@ export declare const recipeSchema: z.ZodObject<{
218
225
  use_version?: string | undefined;
219
226
  install_version?: string | undefined;
220
227
  uninstall_version?: string | undefined;
228
+ install_plugin?: string | undefined;
221
229
  factory_reset?: string | undefined;
222
230
  start_gateway?: string | undefined;
223
231
  enable_plugin?: string | undefined;
@@ -232,12 +240,15 @@ export declare const recipeSchema: z.ZodObject<{
232
240
  workspaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
233
241
  name: z.ZodString;
234
242
  path: z.ZodOptional<z.ZodString>;
243
+ assets: z.ZodOptional<z.ZodString>;
235
244
  }, "strict", z.ZodTypeAny, {
236
245
  name: string;
237
246
  path?: string | undefined;
247
+ assets?: string | undefined;
238
248
  }, {
239
249
  name: string;
240
250
  path?: string | undefined;
251
+ assets?: string | undefined;
241
252
  }>, "many">>;
242
253
  channels: z.ZodOptional<z.ZodArray<z.ZodObject<{
243
254
  channel: z.ZodString;
@@ -417,6 +428,7 @@ export declare const recipeSchema: z.ZodObject<{
417
428
  version: string;
418
429
  bin?: string | undefined;
419
430
  install?: "auto" | "always" | "never" | undefined;
431
+ plugins?: string[] | undefined;
420
432
  bootstrap?: {
421
433
  openai_api_key?: string | undefined;
422
434
  anthropic_api_key?: string | undefined;
@@ -448,6 +460,7 @@ export declare const recipeSchema: z.ZodObject<{
448
460
  use_version?: string | undefined;
449
461
  install_version?: string | undefined;
450
462
  uninstall_version?: string | undefined;
463
+ install_plugin?: string | undefined;
451
464
  factory_reset?: string | undefined;
452
465
  start_gateway?: string | undefined;
453
466
  enable_plugin?: string | undefined;
@@ -469,6 +482,7 @@ export declare const recipeSchema: z.ZodObject<{
469
482
  workspaces?: {
470
483
  name: string;
471
484
  path?: string | undefined;
485
+ assets?: string | undefined;
472
486
  }[] | undefined;
473
487
  channels?: {
474
488
  channel: string;
@@ -522,6 +536,7 @@ export declare const recipeSchema: z.ZodObject<{
522
536
  version: string;
523
537
  bin?: string | undefined;
524
538
  install?: "auto" | "always" | "never" | undefined;
539
+ plugins?: string[] | undefined;
525
540
  bootstrap?: {
526
541
  openai_api_key?: string | undefined;
527
542
  anthropic_api_key?: string | undefined;
@@ -553,6 +568,7 @@ export declare const recipeSchema: z.ZodObject<{
553
568
  use_version?: string | undefined;
554
569
  install_version?: string | undefined;
555
570
  uninstall_version?: string | undefined;
571
+ install_plugin?: string | undefined;
556
572
  factory_reset?: string | undefined;
557
573
  start_gateway?: string | undefined;
558
574
  enable_plugin?: string | undefined;
@@ -574,6 +590,7 @@ export declare const recipeSchema: z.ZodObject<{
574
590
  workspaces?: {
575
591
  name: string;
576
592
  path?: string | undefined;
593
+ assets?: string | undefined;
577
594
  }[] | undefined;
578
595
  channels?: {
579
596
  channel: string;
package/dist/schema.js CHANGED
@@ -9,6 +9,7 @@ const openClawCommandsSchema = z
9
9
  use_version: z.string().optional(),
10
10
  install_version: z.string().optional(),
11
11
  uninstall_version: z.string().optional(),
12
+ install_plugin: z.string().optional(),
12
13
  factory_reset: z.string().optional(),
13
14
  start_gateway: z.string().optional(),
14
15
  enable_plugin: z.string().optional(),
@@ -54,6 +55,7 @@ const openClawSchema = z
54
55
  bin: z.string().optional(),
55
56
  version: z.string(),
56
57
  install: z.enum(["auto", "always", "never"]).optional(),
58
+ plugins: z.array(z.string().min(1)).optional(),
57
59
  bootstrap: openClawBootstrapSchema.optional(),
58
60
  commands: openClawCommandsSchema.optional(),
59
61
  })
@@ -62,6 +64,7 @@ const workspaceSchema = z
62
64
  .object({
63
65
  name: z.string().min(1),
64
66
  path: z.string().min(1).optional(),
67
+ assets: z.string().min(1).optional(),
65
68
  })
66
69
  .strict();
67
70
  const channelSchema = z
package/dist/types.d.ts CHANGED
@@ -17,6 +17,7 @@ export interface OpenClawCommandOverrides {
17
17
  use_version?: string;
18
18
  install_version?: string;
19
19
  uninstall_version?: string;
20
+ install_plugin?: string;
20
21
  factory_reset?: string;
21
22
  start_gateway?: string;
22
23
  enable_plugin?: string;
@@ -58,12 +59,14 @@ export interface OpenClawSection {
58
59
  bin?: string;
59
60
  version: string;
60
61
  install?: InstallPolicy;
62
+ plugins?: string[];
61
63
  bootstrap?: OpenClawBootstrap;
62
64
  commands?: OpenClawCommandOverrides;
63
65
  }
64
66
  export interface WorkspaceDef {
65
67
  name: string;
66
68
  path?: string;
69
+ assets?: string;
67
70
  }
68
71
  export interface ChannelDef {
69
72
  channel: string;
@@ -127,6 +130,7 @@ export interface Recipe {
127
130
  }
128
131
  export interface RunOptions {
129
132
  vars: Record<string, string>;
133
+ plugins: string[];
130
134
  dryRun: boolean;
131
135
  allowMissing: boolean;
132
136
  verbose: boolean;
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "clawchef",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Recipe-driven OpenClaw environment orchestrator",
5
+ "homepage": "https://renorzr.github.io/clawchef",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/renorzr/clawchef.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/renorzr/clawchef/issues"
12
+ },
5
13
  "type": "module",
6
14
  "main": "dist/api.js",
7
15
  "types": "dist/api.d.ts",
@@ -52,7 +52,6 @@ channels:
52
52
  - channel: "telegram"
53
53
  token: "${telegram_bot_token}"
54
54
  account: "default"
55
- login: true
56
55
 
57
56
  conversations:
58
57
  - workspace: "${project_name}"
@@ -11,6 +11,8 @@ openclaw:
11
11
  bin: "openclaw"
12
12
  version: "${openclaw_version}"
13
13
  install: "never"
14
+ plugins:
15
+ - "openclaw-telegram-mock-channel"
14
16
 
15
17
  channels:
16
18
  - channel: "telegram-mock"
@@ -16,4 +16,3 @@ channels:
16
16
  - channel: "telegram"
17
17
  token: "${telegram_bot_token}"
18
18
  account: "default"
19
- login: true
package/src/api.ts CHANGED
@@ -4,11 +4,14 @@ import { importDotEnvFromCwd } from "./env.js";
4
4
  import { Logger } from "./logger.js";
5
5
  import { runRecipe } from "./orchestrator.js";
6
6
  import { loadRecipe, loadRecipeText } from "./recipe.js";
7
+ import { scaffoldProject } from "./scaffold.js";
7
8
  import { recipeSchema } from "./schema.js";
8
9
  import type { OpenClawProvider, OpenClawRemoteConfig, RunOptions } from "./types.js";
10
+ import type { ScaffoldOptions, ScaffoldResult } from "./scaffold.js";
9
11
 
10
12
  export interface CookOptions {
11
13
  vars?: Record<string, string>;
14
+ plugins?: string[];
12
15
  dryRun?: boolean;
13
16
  allowMissing?: boolean;
14
17
  verbose?: boolean;
@@ -19,8 +22,10 @@ export interface CookOptions {
19
22
  }
20
23
 
21
24
  function normalizeCookOptions(options: CookOptions): RunOptions {
25
+ const plugins = Array.from(new Set((options.plugins ?? []).map((value) => value.trim()).filter((value) => value.length > 0)));
22
26
  return {
23
27
  vars: options.vars ?? {},
28
+ plugins,
24
29
  dryRun: Boolean(options.dryRun),
25
30
  allowMissing: Boolean(options.allowMissing),
26
31
  verbose: Boolean(options.verbose),
@@ -62,4 +67,9 @@ export async function validate(recipeRef: string): Promise<void> {
62
67
  }
63
68
  }
64
69
 
70
+ export async function scaffold(targetDir?: string, options: ScaffoldOptions = {}): Promise<ScaffoldResult> {
71
+ return scaffoldProject(targetDir, options);
72
+ }
73
+
65
74
  export type { OpenClawProvider, OpenClawRemoteConfig };
75
+ export type { ScaffoldOptions, ScaffoldResult };
package/src/cli.ts CHANGED
@@ -4,8 +4,12 @@ import { Logger } from "./logger.js";
4
4
  import { runRecipe } from "./orchestrator.js";
5
5
  import { loadRecipe, loadRecipeText } from "./recipe.js";
6
6
  import { recipeSchema } from "./schema.js";
7
+ import { scaffoldProject } from "./scaffold.js";
7
8
  import type { RunOptions } from "./types.js";
8
9
  import YAML from "js-yaml";
10
+ import path from "node:path";
11
+ import { createInterface } from "node:readline/promises";
12
+ import { stdin as input, stdout as output } from "node:process";
9
13
 
10
14
  function parseVarFlags(values: string[]): Record<string, string> {
11
15
  const out: Record<string, string> = {};
@@ -21,6 +25,10 @@ function parseVarFlags(values: string[]): Record<string, string> {
21
25
  return out;
22
26
  }
23
27
 
28
+ function parsePluginFlags(values: string[]): string[] {
29
+ return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
30
+ }
31
+
24
32
  function readEnv(name: string): string | undefined {
25
33
  const value = process.env[name];
26
34
  if (value === undefined) {
@@ -48,6 +56,21 @@ function parseOptionalInt(value: string | undefined, fieldName: string): number
48
56
  return parsed;
49
57
  }
50
58
 
59
+ async function promptProjectName(defaultValue: string): Promise<string> {
60
+ if (!input.isTTY) {
61
+ return defaultValue;
62
+ }
63
+
64
+ const rl = createInterface({ input, output });
65
+ try {
66
+ const answer = await rl.question(`Project name [${defaultValue}]: `);
67
+ const value = answer.trim();
68
+ return value || defaultValue;
69
+ } finally {
70
+ rl.close();
71
+ }
72
+ }
73
+
51
74
  export function buildCli(): Command {
52
75
  const program = new Command();
53
76
 
@@ -65,6 +88,7 @@ export function buildCli(): Command {
65
88
  .option("--verbose", "Verbose logging", false)
66
89
  .option("-s, --silent", "Skip reset confirmation prompt", false)
67
90
  .option("--provider <provider>", "Execution provider: command | remote | mock")
91
+ .option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p: string[]) => p.concat([v]), [])
68
92
  .option("--remote-base-url <url>", "Remote OpenClaw API base URL")
69
93
  .option("--remote-api-key <key>", "Remote OpenClaw API key")
70
94
  .option("--remote-api-header <header>", "Remote auth header name")
@@ -75,6 +99,7 @@ export function buildCli(): Command {
75
99
  const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
76
100
  const options: RunOptions = {
77
101
  vars: parseVarFlags(opts.var),
102
+ plugins: parsePluginFlags(opts.plugin),
78
103
  dryRun: Boolean(opts.dryRun),
79
104
  allowMissing: Boolean(opts.allowMissing),
80
105
  verbose: Boolean(opts.verbose),
@@ -100,6 +125,20 @@ export function buildCli(): Command {
100
125
  }
101
126
  });
102
127
 
128
+ program
129
+ .command("scaffold")
130
+ .argument("[dir]", "Target directory (default: current directory)")
131
+ .option("--name <project-name>", "Project name (default: directory name)")
132
+ .action(async (dir: string | undefined, opts) => {
133
+ const resolvedDir = path.resolve(dir?.trim() ? dir : process.cwd());
134
+ const defaultName = path.basename(resolvedDir);
135
+ const projectName = opts.name?.trim() ? opts.name.trim() : await promptProjectName(defaultName);
136
+ const result = await scaffoldProject(resolvedDir, { projectName });
137
+ process.stdout.write(`Scaffold created at ${result.targetDir}\n`);
138
+ process.stdout.write(`Project name: ${result.projectName}\n`);
139
+ process.stdout.write("Next: run npm install\n");
140
+ });
141
+
103
142
  program
104
143
  .command("validate")
105
144
  .argument("<recipe>", "Recipe path/URL/dir/archive[:file]")
@@ -13,9 +13,10 @@ const DEFAULT_COMMANDS = {
13
13
  use_version: "${bin} --version",
14
14
  install_version: "npm install -g openclaw@${version}",
15
15
  uninstall_version: "npm uninstall -g openclaw",
16
+ install_plugin: "${bin} plugins install ${plugin_spec_q}",
16
17
  factory_reset: "${bin} reset --scope full --yes --non-interactive",
17
18
  start_gateway: "${bin} gateway start",
18
- enable_plugin: "${bin} plugins enable ${channel_q}",
19
+ enable_plugin: "",
19
20
  login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
20
21
  create_agent:
21
22
  "${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json",
@@ -377,6 +378,25 @@ export class CommandOpenClawProvider implements OpenClawProvider {
377
378
  await runShell(resetCmd, dryRun);
378
379
  }
379
380
 
381
+ async installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void> {
382
+ const trimmed = pluginSpec.trim();
383
+ if (!trimmed) {
384
+ return;
385
+ }
386
+
387
+ const bin = config.bin ?? "openclaw";
388
+ const cmd = commandFor(config, "install_plugin", {
389
+ bin,
390
+ version: config.version,
391
+ plugin_spec: trimmed,
392
+ plugin_spec_q: shellQuote(trimmed),
393
+ });
394
+ if (!cmd.trim()) {
395
+ return;
396
+ }
397
+ await runShell(cmd, dryRun);
398
+ }
399
+
380
400
  async startGateway(config: OpenClawSection, dryRun: boolean): Promise<void> {
381
401
  const bin = config.bin ?? "openclaw";
382
402
  const startCmd = commandFor(config, "start_gateway", { bin, version: config.version });
@@ -405,14 +425,17 @@ export class CommandOpenClawProvider implements OpenClawProvider {
405
425
 
406
426
  async configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
407
427
  const bin = config.bin ?? "openclaw";
408
- const enablePluginCmd = commandFor(config, "enable_plugin", {
409
- bin,
410
- version: config.version,
411
- channel: channel.channel,
412
- channel_q: shellQuote(channel.channel),
413
- });
414
- if (enablePluginCmd.trim()) {
415
- await runShell(enablePluginCmd, dryRun);
428
+ const enablePluginTemplate = config.commands?.enable_plugin;
429
+ if (enablePluginTemplate?.trim()) {
430
+ const enablePluginCmd = fillTemplate(enablePluginTemplate, {
431
+ bin,
432
+ version: config.version,
433
+ channel: channel.channel,
434
+ channel_q: shellQuote(channel.channel),
435
+ });
436
+ if (enablePluginCmd.trim()) {
437
+ await runShell(enablePluginCmd, dryRun);
438
+ }
416
439
  }
417
440
 
418
441
  const flags: string[] = [
@@ -40,6 +40,10 @@ export class MockOpenClawProvider implements OpenClawProvider {
40
40
  return { installedThisRun };
41
41
  }
42
42
 
43
+ async installPlugin(_config: OpenClawSection, _pluginSpec: string, _dryRun: boolean): Promise<void> {
44
+ return;
45
+ }
46
+
43
47
  async factoryReset(_config: OpenClawSection, _dryRun: boolean): Promise<void> {
44
48
  this.state.workspaces.clear();
45
49
  this.state.channels.clear();
@@ -8,6 +8,7 @@ export interface EnsureVersionResult {
8
8
 
9
9
  export interface OpenClawProvider {
10
10
  ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean): Promise<EnsureVersionResult>;
11
+ installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
11
12
  factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
12
13
  startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
13
14
  createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
@@ -156,6 +156,17 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
156
156
  };
157
157
  }
158
158
 
159
+ async installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void> {
160
+ await this.perform(
161
+ config,
162
+ "install_plugin",
163
+ {
164
+ plugin_spec: pluginSpec,
165
+ },
166
+ dryRun,
167
+ );
168
+ }
169
+
159
170
  async factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void> {
160
171
  await this.perform(config, "factory_reset", undefined, dryRun);
161
172
  }