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.
- package/README.md +88 -60
- package/dist/api.d.ts +4 -0
- package/dist/api.js +6 -0
- package/dist/cli.js +36 -0
- package/dist/openclaw/command-provider.d.ts +1 -0
- package/dist/openclaw/command-provider.js +30 -9
- package/dist/openclaw/mock-provider.d.ts +1 -0
- package/dist/openclaw/mock-provider.js +3 -0
- package/dist/openclaw/provider.d.ts +1 -0
- package/dist/openclaw/remote-provider.d.ts +1 -0
- package/dist/openclaw/remote-provider.js +5 -0
- package/dist/orchestrator.js +64 -2
- package/dist/recipe.js +9 -0
- package/dist/scaffold.d.ts +8 -0
- package/dist/scaffold.js +172 -0
- package/dist/schema.d.ts +17 -0
- package/dist/schema.js +3 -0
- package/dist/types.d.ts +4 -0
- package/package.json +9 -1
- package/recipes/openclaw-local.yaml +0 -1
- package/recipes/openclaw-telegram-mock.yaml +2 -0
- package/recipes/openclaw-telegram.yaml +0 -1
- package/src/api.ts +10 -0
- package/src/cli.ts +39 -0
- package/src/openclaw/command-provider.ts +32 -9
- package/src/openclaw/mock-provider.ts +4 -0
- package/src/openclaw/provider.ts +1 -0
- package/src/openclaw/remote-provider.ts +11 -0
- package/src/orchestrator.ts +84 -2
- package/src/recipe.ts +14 -0
- package/src/scaffold.ts +197 -0
- package/src/schema.ts +3 -0
- package/src/types.ts +4 -0
package/README.md
CHANGED
|
@@ -14,12 +14,13 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
14
14
|
- Always runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
|
|
15
15
|
- If `openclaw` is missing, auto-installs the recipe version and skips factory reset.
|
|
16
16
|
- Starts OpenClaw gateway service after each recipe execution.
|
|
17
|
-
- Creates workspaces and agents (default workspace path: `~/.openclaw/
|
|
17
|
+
- Creates workspaces and agents (default workspace path: `~/.openclaw/workspace-<workspace-name>`).
|
|
18
|
+
- Supports workspace-level assets copy via `workspaces[].assets`.
|
|
18
19
|
- Materializes files into target workspaces.
|
|
19
20
|
- Installs skills.
|
|
21
|
+
- Supports plugin preinstall via `openclaw.plugins[]` and runtime `--plugin` flags.
|
|
20
22
|
- Configures channels with `openclaw channels add`.
|
|
21
|
-
-
|
|
22
|
-
- Supports interactive channel login at the end of execution (`channels[].login: true`).
|
|
23
|
+
- Supports interactive channel login at the end of execution (`channels[].login: true`) for channels that expose login.
|
|
23
24
|
- Supports remote HTTP orchestration via runtime flags (`--provider remote`) when OpenClaw is reachable via API.
|
|
24
25
|
- Writes preset conversation messages.
|
|
25
26
|
- Runs agent and validates reply output.
|
|
@@ -27,29 +28,27 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
27
28
|
## Install and run
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
|
-
npm
|
|
31
|
-
npm run build
|
|
32
|
-
npm i -g .
|
|
31
|
+
npm i -g clawchef
|
|
33
32
|
clawchef cook recipes/sample.yaml
|
|
34
33
|
```
|
|
35
34
|
|
|
36
35
|
Run recipe from URL:
|
|
37
36
|
|
|
38
37
|
```bash
|
|
39
|
-
clawchef cook https://example.com/recipes/sample.yaml --provider remote
|
|
38
|
+
clawchef cook https://example.com/recipes/sample.yaml --provider remote
|
|
40
39
|
```
|
|
41
40
|
|
|
42
41
|
Run recipe from archive (default `recipe.yaml`):
|
|
43
42
|
|
|
44
43
|
```bash
|
|
45
|
-
clawchef cook ./bundle.tgz --provider mock
|
|
44
|
+
clawchef cook ./bundle.tgz --provider mock
|
|
46
45
|
```
|
|
47
46
|
|
|
48
47
|
Run specific recipe in directory or archive:
|
|
49
48
|
|
|
50
49
|
```bash
|
|
51
|
-
clawchef cook ./recipes-pack:team/recipe-prod.yaml --provider remote
|
|
52
|
-
clawchef cook https://example.com/recipes-pack.zip:team/recipe-prod.yaml --provider remote
|
|
50
|
+
clawchef cook ./recipes-pack:team/recipe-prod.yaml --provider remote
|
|
51
|
+
clawchef cook https://example.com/recipes-pack.zip:team/recipe-prod.yaml --provider remote
|
|
53
52
|
```
|
|
54
53
|
|
|
55
54
|
Dev mode:
|
|
@@ -61,13 +60,13 @@ clawchef cook recipes/sample.yaml --verbose
|
|
|
61
60
|
Run sample with mock provider:
|
|
62
61
|
|
|
63
62
|
```bash
|
|
64
|
-
clawchef cook recipes/sample.yaml --provider mock
|
|
63
|
+
clawchef cook recipes/sample.yaml --provider mock
|
|
65
64
|
```
|
|
66
65
|
|
|
67
66
|
Run `content_from` sample:
|
|
68
67
|
|
|
69
68
|
```bash
|
|
70
|
-
clawchef cook recipes/content-from-sample.yaml --provider mock
|
|
69
|
+
clawchef cook recipes/content-from-sample.yaml --provider mock
|
|
71
70
|
```
|
|
72
71
|
|
|
73
72
|
Skip reset confirmation prompt:
|
|
@@ -76,6 +75,9 @@ Skip reset confirmation prompt:
|
|
|
76
75
|
clawchef cook recipes/sample.yaml -s
|
|
77
76
|
```
|
|
78
77
|
|
|
78
|
+
Warning: `-s/--silent` suppresses the factory-reset confirmation and auto-chooses force reinstall on version mismatch.
|
|
79
|
+
Use it only in CI/non-interactive flows where destructive reset behavior is expected.
|
|
80
|
+
|
|
79
81
|
From-zero OpenClaw bootstrap (recommended):
|
|
80
82
|
|
|
81
83
|
```bash
|
|
@@ -85,13 +87,19 @@ CLAWCHEF_VAR_OPENAI_API_KEY=sk-... clawchef cook recipes/openclaw-from-zero.yaml
|
|
|
85
87
|
Telegram channel setup only:
|
|
86
88
|
|
|
87
89
|
```bash
|
|
88
|
-
CLAWCHEF_VAR_TELEGRAM_BOT_TOKEN=123456:abc... clawchef cook recipes/openclaw-telegram.yaml
|
|
90
|
+
CLAWCHEF_VAR_TELEGRAM_BOT_TOKEN=123456:abc... clawchef cook recipes/openclaw-telegram.yaml
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
Telegram mock channel setup (for tests):
|
|
92
94
|
|
|
93
95
|
```bash
|
|
94
|
-
CLAWCHEF_VAR_TELEGRAM_MOCK_API_KEY=test-key clawchef cook recipes/openclaw-telegram-mock.yaml
|
|
96
|
+
CLAWCHEF_VAR_TELEGRAM_MOCK_API_KEY=test-key clawchef cook recipes/openclaw-telegram-mock.yaml
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Install plugin only for this run:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
clawchef cook recipes/openclaw-telegram-mock.yaml --plugin openclaw-telegram-mock-channel
|
|
95
103
|
```
|
|
96
104
|
|
|
97
105
|
Remote HTTP orchestration:
|
|
@@ -99,7 +107,7 @@ Remote HTTP orchestration:
|
|
|
99
107
|
```bash
|
|
100
108
|
CLAWCHEF_REMOTE_BASE_URL=https://remote-openclaw.example.com \
|
|
101
109
|
CLAWCHEF_REMOTE_API_KEY=secret-token \
|
|
102
|
-
clawchef cook recipes/openclaw-remote-http.yaml --provider remote
|
|
110
|
+
clawchef cook recipes/openclaw-remote-http.yaml --provider remote --verbose
|
|
103
111
|
```
|
|
104
112
|
|
|
105
113
|
Validate recipe structure only:
|
|
@@ -121,12 +129,32 @@ clawchef validate ./bundle.zip
|
|
|
121
129
|
clawchef validate ./bundle.zip:custom/recipe.yaml
|
|
122
130
|
```
|
|
123
131
|
|
|
132
|
+
Create a new recipe project scaffold:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
clawchef scaffold
|
|
136
|
+
clawchef scaffold ./my-recipe-project
|
|
137
|
+
clawchef scaffold ./my-recipe-project --name meetingbot
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`scaffold` prompts for project name (default: target directory name).
|
|
141
|
+
|
|
142
|
+
Scaffold output:
|
|
143
|
+
|
|
144
|
+
- `package.json` with `telegram-api-mock-server` in `devDependencies`
|
|
145
|
+
- `src/recipe.yaml` with `telegram-mock` channel, plugin preinstall, and `workspaces[].assets`
|
|
146
|
+
- `src/<project-name>-assets/{AGENTS.md,IDENTITY.md,SOUL.md,TOOLS.md}`
|
|
147
|
+
- `src/<project-name>-assets/scripts/scheduling.mjs`
|
|
148
|
+
- `test/recipe-smoke.test.mjs`
|
|
149
|
+
|
|
150
|
+
By default scaffold only writes files; it does not run `npm install`.
|
|
151
|
+
|
|
124
152
|
## Node.js API
|
|
125
153
|
|
|
126
154
|
You can call clawchef directly from Node.js (without invoking CLI commands).
|
|
127
155
|
|
|
128
156
|
```ts
|
|
129
|
-
import { cook, validate } from "clawchef";
|
|
157
|
+
import { cook, scaffold, validate } from "clawchef";
|
|
130
158
|
|
|
131
159
|
await validate("recipes/sample.yaml");
|
|
132
160
|
|
|
@@ -137,21 +165,30 @@ await cook("recipes/sample.yaml", {
|
|
|
137
165
|
openai_api_key: process.env.OPENAI_API_KEY ?? "",
|
|
138
166
|
},
|
|
139
167
|
});
|
|
168
|
+
|
|
169
|
+
await scaffold("./my-recipe-project", {
|
|
170
|
+
projectName: "my-recipe-project",
|
|
171
|
+
});
|
|
140
172
|
```
|
|
141
173
|
|
|
142
174
|
`cook()` options:
|
|
143
175
|
|
|
144
176
|
- `vars`: template variables (`Record<string, string>`)
|
|
177
|
+
- `plugins`: plugin npm specs to preinstall for this run (`string[]`)
|
|
145
178
|
- `provider`: `command | remote | mock`
|
|
146
179
|
- `remote`: remote provider config (same fields as CLI remote flags)
|
|
147
180
|
- `dryRun`, `allowMissing`, `verbose`
|
|
148
181
|
- `silent` (default: `true` in Node API)
|
|
149
182
|
- `loadDotEnvFromCwd` (default: `true`)
|
|
150
183
|
|
|
184
|
+
Node API `silent: true` has the same risk as CLI `-s`: no reset confirmation and force reinstall on version mismatch.
|
|
185
|
+
Set `silent: false` when you want an interactive safety prompt.
|
|
186
|
+
|
|
151
187
|
Notes:
|
|
152
188
|
|
|
153
189
|
- `validate()` throws on invalid recipe.
|
|
154
190
|
- `cook()` throws on runtime/configuration errors.
|
|
191
|
+
- `scaffold()` creates `package.json`, `src/recipe.yaml`, `src/<project-name>-assets`, and `test/`.
|
|
155
192
|
|
|
156
193
|
## Variable precedence
|
|
157
194
|
|
|
@@ -213,7 +250,7 @@ Request payload format (POST):
|
|
|
213
250
|
"payload": {
|
|
214
251
|
"workspace": {
|
|
215
252
|
"name": "demo",
|
|
216
|
-
"path": "/home/runner/.openclaw/
|
|
253
|
+
"path": "/home/runner/.openclaw/workspace-demo"
|
|
217
254
|
}
|
|
218
255
|
}
|
|
219
256
|
}
|
|
@@ -233,6 +270,7 @@ Expected response format:
|
|
|
233
270
|
Supported operation values sent by clawchef:
|
|
234
271
|
|
|
235
272
|
- `ensure_version`, `factory_reset`, `start_gateway`
|
|
273
|
+
- `install_plugin`
|
|
236
274
|
- `create_workspace`, `create_agent`, `materialize_file`, `install_skill`
|
|
237
275
|
- `configure_channel`, `login_channel`
|
|
238
276
|
- `run_agent`
|
|
@@ -258,6 +296,20 @@ Supported fields include:
|
|
|
258
296
|
- setup toggles: `skip_channels`, `skip_skills`, `skip_health`, `skip_ui`, `skip_daemon`, `install_daemon`
|
|
259
297
|
- auth/provider: `auth_choice`, `openai_api_key`, `anthropic_api_key`, `openrouter_api_key`, `xai_api_key`, `gemini_api_key`, `ai_gateway_api_key`, `cloudflare_ai_gateway_api_key`, `token`, `token_provider`, `token_profile_id`
|
|
260
298
|
|
|
299
|
+
### Plugin preinstall
|
|
300
|
+
|
|
301
|
+
Use `openclaw.plugins` to preinstall plugin packages before workspace/channel setup.
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
|
|
305
|
+
```yaml
|
|
306
|
+
openclaw:
|
|
307
|
+
version: "2026.2.9"
|
|
308
|
+
plugins:
|
|
309
|
+
- "openclaw-telegram-mock-channel"
|
|
310
|
+
- "@scope/custom-channel-plugin@1.2.3"
|
|
311
|
+
```
|
|
312
|
+
|
|
261
313
|
When `openclaw.bootstrap` contains provider keys, `clawchef` also injects them into runtime env for `openclaw agent --local`.
|
|
262
314
|
|
|
263
315
|
For `command` provider, default command templates are:
|
|
@@ -265,9 +317,9 @@ For `command` provider, default command templates are:
|
|
|
265
317
|
- `use_version`: `${bin} --version`
|
|
266
318
|
- `install_version`: `npm install -g openclaw@${version}`
|
|
267
319
|
- `uninstall_version`: `npm uninstall -g openclaw`
|
|
320
|
+
- `install_plugin`: `${bin} plugins install ${plugin_spec_q}`
|
|
268
321
|
- `factory_reset`: `${bin} reset --scope full --yes --non-interactive`
|
|
269
322
|
- `start_gateway`: `${bin} gateway start`
|
|
270
|
-
- `enable_plugin`: `${bin} plugins enable ${channel_q}`
|
|
271
323
|
- `login_channel`: `${bin} channels login --channel ${channel_q}${account_arg}`
|
|
272
324
|
- `create_workspace`: generated from `openclaw.bootstrap` (override with `openclaw.commands.create_workspace`)
|
|
273
325
|
- `create_agent`: `${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json`
|
|
@@ -277,10 +329,14 @@ For `command` provider, default command templates are:
|
|
|
277
329
|
|
|
278
330
|
You can override any command under `openclaw.commands` in recipe.
|
|
279
331
|
|
|
332
|
+
By default, clawchef does not auto-run `openclaw plugins enable <channel>` during channel configuration.
|
|
333
|
+
If you need custom enable behavior, set `openclaw.commands.enable_plugin` explicitly.
|
|
334
|
+
|
|
280
335
|
## Channels
|
|
281
336
|
|
|
282
337
|
Use `channels[]` to configure accounts via `openclaw channels add`.
|
|
283
338
|
If `login: true` is set, clawchef runs channel login at the end of `cook` (after gateway start).
|
|
339
|
+
Telegram does not support `openclaw channels login`; do not set `login` for `channel: "telegram"`.
|
|
284
340
|
|
|
285
341
|
Example:
|
|
286
342
|
|
|
@@ -289,7 +345,6 @@ channels:
|
|
|
289
345
|
- channel: "telegram"
|
|
290
346
|
token: "${telegram_bot_token}"
|
|
291
347
|
account: "default"
|
|
292
|
-
login: true
|
|
293
348
|
|
|
294
349
|
- channel: "slack"
|
|
295
350
|
bot_token: "${slack_bot_token}"
|
|
@@ -308,54 +363,27 @@ Supported common fields:
|
|
|
308
363
|
- optional: `account`, `name`, `token`, `token_file`, `use_env`, `bot_token`, `access_token`, `app_token`, `webhook_url`, `webhook_path`, `signal_number`, `password`, `login`, `login_mode`, `login_account`
|
|
309
364
|
- advanced passthrough: `extra_flags` (`snake_case` keys become `--kebab-case` CLI flags)
|
|
310
365
|
|
|
311
|
-
### Telegram mock channel (for recipe tests)
|
|
312
|
-
|
|
313
|
-
Use `channel: "telegram-mock"` when you need to test Telegram-related recipe flows without connecting to the real Telegram network.
|
|
314
|
-
|
|
315
|
-
`clawchef` treats `telegram-mock` like any other channel and passes mock-specific flags through `extra_flags`.
|
|
316
|
-
|
|
317
|
-
Example:
|
|
318
|
-
|
|
319
|
-
```yaml
|
|
320
|
-
params:
|
|
321
|
-
telegram_mock_api_key:
|
|
322
|
-
required: true
|
|
323
|
-
|
|
324
|
-
channels:
|
|
325
|
-
- channel: "telegram-mock"
|
|
326
|
-
account: "testbot"
|
|
327
|
-
token: "${telegram_mock_api_key}"
|
|
328
|
-
extra_flags:
|
|
329
|
-
mock_bind: "127.0.0.1:18790"
|
|
330
|
-
mock_api_key: "${telegram_mock_api_key}"
|
|
331
|
-
mode: "webhook"
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
Typical test setup:
|
|
335
|
-
|
|
336
|
-
- Start OpenClaw with the telegram-mock plugin enabled.
|
|
337
|
-
- Run `clawchef cook ...` to configure workspaces/agents/channels.
|
|
338
|
-
- Use your external test program (HTTP API or Node.js SDK) to inject inbound mock messages and assert outbound events.
|
|
339
|
-
|
|
340
|
-
Login fields:
|
|
341
|
-
|
|
342
|
-
- `login: true` enables channel login step
|
|
343
|
-
- `login_mode`: currently supports `interactive`
|
|
344
|
-
- `login_account`: override account used for login (defaults to `account`)
|
|
345
|
-
|
|
346
|
-
Security rules:
|
|
347
|
-
|
|
348
|
-
- Do not inline secret values in `channels[]`.
|
|
349
|
-
- Use `${var}` placeholders and inject values via `--var` / `CLAWCHEF_VAR_*`.
|
|
350
|
-
|
|
351
366
|
## Workspace path behavior
|
|
352
367
|
|
|
353
368
|
- `workspaces[].path` is optional.
|
|
354
|
-
- If omitted, clawchef uses `~/.openclaw/
|
|
369
|
+
- If omitted, clawchef uses `~/.openclaw/workspace-<workspace-name>`.
|
|
370
|
+
- `workspaces[].assets` is optional.
|
|
371
|
+
- If `assets` is set, clawchef recursively copies files from that directory into the workspace root.
|
|
372
|
+
- `assets` is resolved relative to the recipe file path (unless absolute path is given).
|
|
373
|
+
- `files[]` runs after assets copy, so `files[]` can override copied asset files.
|
|
374
|
+
- Direct URL recipes do not support `workspaces[].assets` (assets must resolve to a local directory).
|
|
355
375
|
- If provided, relative paths are resolved from the recipe file directory.
|
|
356
376
|
- For direct URL recipe files, relative workspace paths are resolved from the current working directory.
|
|
357
377
|
- For directory/archive recipe references, relative workspace paths are resolved from the selected recipe file directory.
|
|
358
378
|
|
|
379
|
+
Example:
|
|
380
|
+
|
|
381
|
+
```yaml
|
|
382
|
+
workspaces:
|
|
383
|
+
- name: "workspace-meeting"
|
|
384
|
+
assets: "./meetingbot-assets"
|
|
385
|
+
```
|
|
386
|
+
|
|
359
387
|
## File content references
|
|
360
388
|
|
|
361
389
|
In `files[]`, set exactly one of:
|
package/dist/api.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { OpenClawProvider, OpenClawRemoteConfig } from "./types.js";
|
|
2
|
+
import type { ScaffoldOptions, ScaffoldResult } from "./scaffold.js";
|
|
2
3
|
export interface CookOptions {
|
|
3
4
|
vars?: Record<string, string>;
|
|
5
|
+
plugins?: string[];
|
|
4
6
|
dryRun?: boolean;
|
|
5
7
|
allowMissing?: boolean;
|
|
6
8
|
verbose?: boolean;
|
|
@@ -11,4 +13,6 @@ export interface CookOptions {
|
|
|
11
13
|
}
|
|
12
14
|
export declare function cook(recipeRef: string, options?: CookOptions): Promise<void>;
|
|
13
15
|
export declare function validate(recipeRef: string): Promise<void>;
|
|
16
|
+
export declare function scaffold(targetDir?: string, options?: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
14
17
|
export type { OpenClawProvider, OpenClawRemoteConfig };
|
|
18
|
+
export type { ScaffoldOptions, ScaffoldResult };
|
package/dist/api.js
CHANGED
|
@@ -4,10 +4,13 @@ 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
|
function normalizeCookOptions(options) {
|
|
10
|
+
const plugins = Array.from(new Set((options.plugins ?? []).map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
9
11
|
return {
|
|
10
12
|
vars: options.vars ?? {},
|
|
13
|
+
plugins,
|
|
11
14
|
dryRun: Boolean(options.dryRun),
|
|
12
15
|
allowMissing: Boolean(options.allowMissing),
|
|
13
16
|
verbose: Boolean(options.verbose),
|
|
@@ -47,3 +50,6 @@ export async function validate(recipeRef) {
|
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
}
|
|
53
|
+
export async function scaffold(targetDir, options = {}) {
|
|
54
|
+
return scaffoldProject(targetDir, options);
|
|
55
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,11 @@ 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 YAML from "js-yaml";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
11
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
8
12
|
function parseVarFlags(values) {
|
|
9
13
|
const out = {};
|
|
10
14
|
for (const item of values) {
|
|
@@ -18,6 +22,9 @@ function parseVarFlags(values) {
|
|
|
18
22
|
}
|
|
19
23
|
return out;
|
|
20
24
|
}
|
|
25
|
+
function parsePluginFlags(values) {
|
|
26
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
27
|
+
}
|
|
21
28
|
function readEnv(name) {
|
|
22
29
|
const value = process.env[name];
|
|
23
30
|
if (value === undefined) {
|
|
@@ -42,6 +49,20 @@ function parseOptionalInt(value, fieldName) {
|
|
|
42
49
|
}
|
|
43
50
|
return parsed;
|
|
44
51
|
}
|
|
52
|
+
async function promptProjectName(defaultValue) {
|
|
53
|
+
if (!input.isTTY) {
|
|
54
|
+
return defaultValue;
|
|
55
|
+
}
|
|
56
|
+
const rl = createInterface({ input, output });
|
|
57
|
+
try {
|
|
58
|
+
const answer = await rl.question(`Project name [${defaultValue}]: `);
|
|
59
|
+
const value = answer.trim();
|
|
60
|
+
return value || defaultValue;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
rl.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
45
66
|
export function buildCli() {
|
|
46
67
|
const program = new Command();
|
|
47
68
|
program
|
|
@@ -57,6 +78,7 @@ export function buildCli() {
|
|
|
57
78
|
.option("--verbose", "Verbose logging", false)
|
|
58
79
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
59
80
|
.option("--provider <provider>", "Execution provider: command | remote | mock")
|
|
81
|
+
.option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p) => p.concat([v]), [])
|
|
60
82
|
.option("--remote-base-url <url>", "Remote OpenClaw API base URL")
|
|
61
83
|
.option("--remote-api-key <key>", "Remote OpenClaw API key")
|
|
62
84
|
.option("--remote-api-header <header>", "Remote auth header name")
|
|
@@ -67,6 +89,7 @@ export function buildCli() {
|
|
|
67
89
|
const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
|
|
68
90
|
const options = {
|
|
69
91
|
vars: parseVarFlags(opts.var),
|
|
92
|
+
plugins: parsePluginFlags(opts.plugin),
|
|
70
93
|
dryRun: Boolean(opts.dryRun),
|
|
71
94
|
allowMissing: Boolean(opts.allowMissing),
|
|
72
95
|
verbose: Boolean(opts.verbose),
|
|
@@ -92,6 +115,19 @@ export function buildCli() {
|
|
|
92
115
|
}
|
|
93
116
|
}
|
|
94
117
|
});
|
|
118
|
+
program
|
|
119
|
+
.command("scaffold")
|
|
120
|
+
.argument("[dir]", "Target directory (default: current directory)")
|
|
121
|
+
.option("--name <project-name>", "Project name (default: directory name)")
|
|
122
|
+
.action(async (dir, opts) => {
|
|
123
|
+
const resolvedDir = path.resolve(dir?.trim() ? dir : process.cwd());
|
|
124
|
+
const defaultName = path.basename(resolvedDir);
|
|
125
|
+
const projectName = opts.name?.trim() ? opts.name.trim() : await promptProjectName(defaultName);
|
|
126
|
+
const result = await scaffoldProject(resolvedDir, { projectName });
|
|
127
|
+
process.stdout.write(`Scaffold created at ${result.targetDir}\n`);
|
|
128
|
+
process.stdout.write(`Project name: ${result.projectName}\n`);
|
|
129
|
+
process.stdout.write("Next: run npm install\n");
|
|
130
|
+
});
|
|
95
131
|
program
|
|
96
132
|
.command("validate")
|
|
97
133
|
.argument("<recipe>", "Recipe path/URL/dir/archive[:file]")
|
|
@@ -4,6 +4,7 @@ export declare class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
4
4
|
private readonly stagedMessages;
|
|
5
5
|
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean): Promise<EnsureVersionResult>;
|
|
6
6
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
7
|
+
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
7
8
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
8
9
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
9
10
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
@@ -10,9 +10,10 @@ const DEFAULT_COMMANDS = {
|
|
|
10
10
|
use_version: "${bin} --version",
|
|
11
11
|
install_version: "npm install -g openclaw@${version}",
|
|
12
12
|
uninstall_version: "npm uninstall -g openclaw",
|
|
13
|
+
install_plugin: "${bin} plugins install ${plugin_spec_q}",
|
|
13
14
|
factory_reset: "${bin} reset --scope full --yes --non-interactive",
|
|
14
15
|
start_gateway: "${bin} gateway start",
|
|
15
|
-
enable_plugin: "
|
|
16
|
+
enable_plugin: "",
|
|
16
17
|
login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
|
|
17
18
|
create_agent: "${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json",
|
|
18
19
|
install_skill: "${bin} skills check",
|
|
@@ -302,6 +303,23 @@ export class CommandOpenClawProvider {
|
|
|
302
303
|
}
|
|
303
304
|
await runShell(resetCmd, dryRun);
|
|
304
305
|
}
|
|
306
|
+
async installPlugin(config, pluginSpec, dryRun) {
|
|
307
|
+
const trimmed = pluginSpec.trim();
|
|
308
|
+
if (!trimmed) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const bin = config.bin ?? "openclaw";
|
|
312
|
+
const cmd = commandFor(config, "install_plugin", {
|
|
313
|
+
bin,
|
|
314
|
+
version: config.version,
|
|
315
|
+
plugin_spec: trimmed,
|
|
316
|
+
plugin_spec_q: shellQuote(trimmed),
|
|
317
|
+
});
|
|
318
|
+
if (!cmd.trim()) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
await runShell(cmd, dryRun);
|
|
322
|
+
}
|
|
305
323
|
async startGateway(config, dryRun) {
|
|
306
324
|
const bin = config.bin ?? "openclaw";
|
|
307
325
|
const startCmd = commandFor(config, "start_gateway", { bin, version: config.version });
|
|
@@ -328,14 +346,17 @@ export class CommandOpenClawProvider {
|
|
|
328
346
|
}
|
|
329
347
|
async configureChannel(config, channel, dryRun) {
|
|
330
348
|
const bin = config.bin ?? "openclaw";
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
349
|
+
const enablePluginTemplate = config.commands?.enable_plugin;
|
|
350
|
+
if (enablePluginTemplate?.trim()) {
|
|
351
|
+
const enablePluginCmd = fillTemplate(enablePluginTemplate, {
|
|
352
|
+
bin,
|
|
353
|
+
version: config.version,
|
|
354
|
+
channel: channel.channel,
|
|
355
|
+
channel_q: shellQuote(channel.channel),
|
|
356
|
+
});
|
|
357
|
+
if (enablePluginCmd.trim()) {
|
|
358
|
+
await runShell(enablePluginCmd, dryRun);
|
|
359
|
+
}
|
|
339
360
|
}
|
|
340
361
|
const flags = [
|
|
341
362
|
"--channel",
|
|
@@ -3,6 +3,7 @@ import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from
|
|
|
3
3
|
export declare class MockOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private state;
|
|
5
5
|
ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean): Promise<EnsureVersionResult>;
|
|
6
|
+
installPlugin(_config: OpenClawSection, _pluginSpec: string, _dryRun: boolean): Promise<void>;
|
|
6
7
|
factoryReset(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
|
|
7
8
|
startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
|
|
8
9
|
createWorkspace(_config: OpenClawSection, workspace: ResolvedWorkspaceDef, _dryRun: boolean): Promise<void>;
|
|
@@ -25,6 +25,9 @@ export class MockOpenClawProvider {
|
|
|
25
25
|
this.state.currentVersion = config.version;
|
|
26
26
|
return { installedThisRun };
|
|
27
27
|
}
|
|
28
|
+
async installPlugin(_config, _pluginSpec, _dryRun) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
28
31
|
async factoryReset(_config, _dryRun) {
|
|
29
32
|
this.state.workspaces.clear();
|
|
30
33
|
this.state.channels.clear();
|
|
@@ -7,6 +7,7 @@ export interface EnsureVersionResult {
|
|
|
7
7
|
}
|
|
8
8
|
export interface OpenClawProvider {
|
|
9
9
|
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean): Promise<EnsureVersionResult>;
|
|
10
|
+
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
10
11
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
11
12
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
12
13
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
@@ -6,6 +6,7 @@ export declare class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
6
6
|
constructor(remoteConfig: Partial<OpenClawRemoteConfig>);
|
|
7
7
|
private perform;
|
|
8
8
|
ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean): Promise<EnsureVersionResult>;
|
|
9
|
+
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
9
10
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
10
11
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
11
12
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
@@ -102,6 +102,11 @@ export class RemoteOpenClawProvider {
|
|
|
102
102
|
installedThisRun: Boolean(result.installed_this_run),
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
|
+
async installPlugin(config, pluginSpec, dryRun) {
|
|
106
|
+
await this.perform(config, "install_plugin", {
|
|
107
|
+
plugin_spec: pluginSpec,
|
|
108
|
+
}, dryRun);
|
|
109
|
+
}
|
|
105
110
|
async factoryReset(config, dryRun) {
|
|
106
111
|
await this.perform(config, "factory_reset", undefined, dryRun);
|
|
107
112
|
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { mkdir, access, copyFile, writeFile, readFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, access, copyFile, writeFile, readFile, readdir, stat } from "node:fs/promises";
|
|
4
4
|
import { constants } from "node:fs";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -32,7 +32,9 @@ function resolveWorkspacePath(recipeOrigin, name, configuredPath) {
|
|
|
32
32
|
}
|
|
33
33
|
return path.resolve(configuredPath);
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
const trimmedName = name.trim() || name;
|
|
36
|
+
const workspaceName = trimmedName.startsWith("workspace-") ? trimmedName : `workspace-${trimmedName}`;
|
|
37
|
+
return path.join(homedir(), ".openclaw", workspaceName);
|
|
36
38
|
}
|
|
37
39
|
function isHttpUrl(value) {
|
|
38
40
|
try {
|
|
@@ -78,6 +80,27 @@ async function readBinaryFromRef(recipeOrigin, reference) {
|
|
|
78
80
|
const bytes = await response.arrayBuffer();
|
|
79
81
|
return Buffer.from(bytes);
|
|
80
82
|
}
|
|
83
|
+
async function collectLocalAssetFiles(rootDir, relDir = "") {
|
|
84
|
+
const currentDir = relDir ? path.join(rootDir, relDir) : rootDir;
|
|
85
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const nextRel = relDir ? path.join(relDir, entry.name) : entry.name;
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
out.push(...await collectLocalAssetFiles(rootDir, nextRel));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (entry.isFile()) {
|
|
94
|
+
out.push({
|
|
95
|
+
absolutePath: path.join(rootDir, nextRel),
|
|
96
|
+
relativePath: nextRel,
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
throw new ClawChefError(`Unsupported entry in assets directory: ${path.join(rootDir, nextRel)}`);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
81
104
|
async function confirmFactoryReset(options) {
|
|
82
105
|
if (options.silent || options.dryRun) {
|
|
83
106
|
return true;
|
|
@@ -112,6 +135,12 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
112
135
|
await provider.factoryReset(recipe.openclaw, options.dryRun);
|
|
113
136
|
logger.info("Factory reset completed");
|
|
114
137
|
}
|
|
138
|
+
const pluginSpecs = Array.from(new Set([...(recipe.openclaw.plugins ?? []), ...options.plugins].map((v) => v.trim())))
|
|
139
|
+
.filter((v) => v.length > 0);
|
|
140
|
+
for (const pluginSpec of pluginSpecs) {
|
|
141
|
+
await provider.installPlugin(recipe.openclaw, pluginSpec, options.dryRun);
|
|
142
|
+
logger.info(`Plugin preinstalled: ${pluginSpec}`);
|
|
143
|
+
}
|
|
115
144
|
for (const ws of recipe.workspaces ?? []) {
|
|
116
145
|
const absPath = resolveWorkspacePath(recipeOrigin, ws.name, ws.path);
|
|
117
146
|
workspacePaths.set(ws.name, absPath);
|
|
@@ -120,6 +149,39 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
120
149
|
}
|
|
121
150
|
await provider.createWorkspace(recipe.openclaw, { ...ws, path: absPath }, options.dryRun);
|
|
122
151
|
logger.info(`Workspace created: ${ws.name}`);
|
|
152
|
+
if (!ws.assets?.trim()) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const resolvedAssets = resolveFileRef(recipeOrigin, ws.assets);
|
|
156
|
+
if (resolvedAssets.kind !== "local") {
|
|
157
|
+
throw new ClawChefError(`Workspace assets must resolve to a local directory: ${ws.assets}. Direct URL recipes cannot use workspaces[].assets.`);
|
|
158
|
+
}
|
|
159
|
+
let assetDirStat;
|
|
160
|
+
try {
|
|
161
|
+
assetDirStat = await stat(resolvedAssets.value);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
165
|
+
throw new ClawChefError(`Workspace assets path is not accessible: ${resolvedAssets.value} (${message})`);
|
|
166
|
+
}
|
|
167
|
+
if (!assetDirStat.isDirectory()) {
|
|
168
|
+
throw new ClawChefError(`Workspace assets must be a directory: ${resolvedAssets.value}`);
|
|
169
|
+
}
|
|
170
|
+
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
171
|
+
for (const assetFile of assetFiles) {
|
|
172
|
+
if (provider.materializeFile) {
|
|
173
|
+
const content = await readFile(assetFile.absolutePath, "utf8");
|
|
174
|
+
await provider.materializeFile(recipe.openclaw, ws.name, assetFile.relativePath, content, true, options.dryRun);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const target = path.resolve(absPath, assetFile.relativePath);
|
|
178
|
+
if (!options.dryRun) {
|
|
179
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
180
|
+
await copyFile(assetFile.absolutePath, target);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
logger.info(`Workspace asset copied: ${ws.name}/${assetFile.relativePath}`);
|
|
184
|
+
}
|
|
123
185
|
}
|
|
124
186
|
for (const agent of recipe.agents ?? []) {
|
|
125
187
|
const workspacePath = workspacePaths.get(agent.workspace);
|
package/dist/recipe.js
CHANGED
|
@@ -125,6 +125,11 @@ function collectVars(recipe, cliVars) {
|
|
|
125
125
|
}
|
|
126
126
|
function semanticValidate(recipe) {
|
|
127
127
|
const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
|
|
128
|
+
for (const workspace of recipe.workspaces ?? []) {
|
|
129
|
+
if (workspace.assets !== undefined && !workspace.assets.trim()) {
|
|
130
|
+
throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
128
133
|
for (const agent of recipe.agents ?? []) {
|
|
129
134
|
if (!ws.has(agent.workspace)) {
|
|
130
135
|
throw new ClawChefError(`Agent ${agent.name} references missing workspace: ${agent.workspace}`);
|
|
@@ -148,6 +153,10 @@ function semanticValidate(recipe) {
|
|
|
148
153
|
if (!ALLOWED_CHANNELS.has(channel.channel)) {
|
|
149
154
|
throw new ClawChefError(`Unsupported channel: ${channel.channel}. Allowed: ${Array.from(ALLOWED_CHANNELS).join(", ")}`);
|
|
150
155
|
}
|
|
156
|
+
if (channel.channel === "telegram" &&
|
|
157
|
+
(channel.login || channel.login_mode !== undefined || channel.login_account !== undefined)) {
|
|
158
|
+
throw new ClawChefError("channels[] entry for telegram does not support login/login_mode/login_account. Configure token (or use_env/token_file), then start gateway.");
|
|
159
|
+
}
|
|
151
160
|
const hasAuth = Boolean(channel.use_env) ||
|
|
152
161
|
Boolean(channel.token?.trim()) ||
|
|
153
162
|
Boolean(channel.token_file?.trim()) ||
|