clawchef 0.1.0
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 +405 -0
- package/dist/api.d.ts +14 -0
- package/dist/api.js +49 -0
- package/dist/assertions.d.ts +2 -0
- package/dist/assertions.js +32 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +115 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +14 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +17 -0
- package/dist/openclaw/command-provider.d.ts +15 -0
- package/dist/openclaw/command-provider.js +489 -0
- package/dist/openclaw/factory.d.ts +3 -0
- package/dist/openclaw/factory.js +13 -0
- package/dist/openclaw/mock-provider.d.ts +15 -0
- package/dist/openclaw/mock-provider.js +65 -0
- package/dist/openclaw/provider.d.ts +20 -0
- package/dist/openclaw/provider.js +1 -0
- package/dist/openclaw/remote-provider.d.ts +19 -0
- package/dist/openclaw/remote-provider.js +158 -0
- package/dist/orchestrator.d.ts +4 -0
- package/dist/orchestrator.js +243 -0
- package/dist/recipe.d.ts +20 -0
- package/dist/recipe.js +522 -0
- package/dist/schema.d.ts +626 -0
- package/dist/schema.js +143 -0
- package/dist/template.d.ts +2 -0
- package/dist/template.js +30 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.js +1 -0
- package/package.json +41 -0
- package/recipes/content-from-sample.yaml +20 -0
- package/recipes/openclaw-from-zero.yaml +45 -0
- package/recipes/openclaw-local.yaml +65 -0
- package/recipes/openclaw-remote-http.yaml +38 -0
- package/recipes/openclaw-telegram-mock.yaml +22 -0
- package/recipes/openclaw-telegram.yaml +19 -0
- package/recipes/sample.yaml +49 -0
- package/recipes/snippets/readme-template.md +3 -0
- package/src/api.ts +65 -0
- package/src/assertions.ts +37 -0
- package/src/cli.ts +123 -0
- package/src/env.ts +16 -0
- package/src/errors.ts +6 -0
- package/src/index.ts +20 -0
- package/src/logger.ts +17 -0
- package/src/openclaw/command-provider.ts +594 -0
- package/src/openclaw/factory.ts +16 -0
- package/src/openclaw/mock-provider.ts +104 -0
- package/src/openclaw/provider.ts +44 -0
- package/src/openclaw/remote-provider.ts +264 -0
- package/src/orchestrator.ts +271 -0
- package/src/recipe.ts +621 -0
- package/src/schema.ts +157 -0
- package/src/template.ts +41 -0
- package/src/types.ts +150 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# clawchef
|
|
2
|
+
|
|
3
|
+
Recipe-driven OpenClaw environment orchestrator.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Parses a YAML recipe.
|
|
8
|
+
- Accepts recipe input from local file/dir/archive and HTTP URL/archive.
|
|
9
|
+
- Resolves `${var}` parameters from `--var`, environment, and defaults.
|
|
10
|
+
- Auto-loads environment variables from `.env` in the current working directory.
|
|
11
|
+
- Requires secrets to be injected via `--var` / `CLAWCHEF_VAR_*` (no inline secrets in recipe).
|
|
12
|
+
- Prepares OpenClaw version (install or reuse).
|
|
13
|
+
- When installed OpenClaw version mismatches recipe version, prompts: ignore / abort / force reinstall (silent mode auto-picks force reinstall).
|
|
14
|
+
- Always runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
|
|
15
|
+
- If `openclaw` is missing, auto-installs the recipe version and skips factory reset.
|
|
16
|
+
- Starts OpenClaw gateway service after each recipe execution.
|
|
17
|
+
- Creates workspaces and agents (default workspace path: `~/.openclaw/workspaces/<workspace-name>`).
|
|
18
|
+
- Materializes files into target workspaces.
|
|
19
|
+
- Installs skills.
|
|
20
|
+
- Configures channels with `openclaw channels add`.
|
|
21
|
+
- Enables channel plugins before channel configuration.
|
|
22
|
+
- Supports interactive channel login at the end of execution (`channels[].login: true`).
|
|
23
|
+
- Supports remote HTTP orchestration via runtime flags (`--provider remote`) when OpenClaw is reachable via API.
|
|
24
|
+
- Writes preset conversation messages.
|
|
25
|
+
- Runs agent and validates reply output.
|
|
26
|
+
|
|
27
|
+
## Install and run
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
npm run build
|
|
32
|
+
npm i -g .
|
|
33
|
+
clawchef cook recipes/sample.yaml
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run recipe from URL:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
clawchef cook https://example.com/recipes/sample.yaml --provider remote -s
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Run recipe from archive (default `recipe.yaml`):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
clawchef cook ./bundle.tgz --provider mock -s
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run specific recipe in directory or archive:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
clawchef cook ./recipes-pack:team/recipe-prod.yaml --provider remote -s
|
|
52
|
+
clawchef cook https://example.com/recipes-pack.zip:team/recipe-prod.yaml --provider remote -s
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Dev mode:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
clawchef cook recipes/sample.yaml --verbose
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Run sample with mock provider:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
clawchef cook recipes/sample.yaml --provider mock -s
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Run `content_from` sample:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
clawchef cook recipes/content-from-sample.yaml --provider mock -s
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Skip reset confirmation prompt:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
clawchef cook recipes/sample.yaml -s
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
From-zero OpenClaw bootstrap (recommended):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
CLAWCHEF_VAR_OPENAI_API_KEY=sk-... clawchef cook recipes/openclaw-from-zero.yaml --verbose
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Telegram channel setup only:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
CLAWCHEF_VAR_TELEGRAM_BOT_TOKEN=123456:abc... clawchef cook recipes/openclaw-telegram.yaml -s
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Telegram mock channel setup (for tests):
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
CLAWCHEF_VAR_TELEGRAM_MOCK_API_KEY=test-key clawchef cook recipes/openclaw-telegram-mock.yaml -s
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Remote HTTP orchestration:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
CLAWCHEF_REMOTE_BASE_URL=https://remote-openclaw.example.com \
|
|
101
|
+
CLAWCHEF_REMOTE_API_KEY=secret-token \
|
|
102
|
+
clawchef cook recipes/openclaw-remote-http.yaml --provider remote -s --verbose
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Validate recipe structure only:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
clawchef validate recipes/sample.yaml
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Validate recipe from URL:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
clawchef validate https://example.com/recipes/sample.yaml
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Validate recipe in archive:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
clawchef validate ./bundle.zip
|
|
121
|
+
clawchef validate ./bundle.zip:custom/recipe.yaml
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Node.js API
|
|
125
|
+
|
|
126
|
+
You can call clawchef directly from Node.js (without invoking CLI commands).
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { cook, validate } from "clawchef";
|
|
130
|
+
|
|
131
|
+
await validate("recipes/sample.yaml");
|
|
132
|
+
|
|
133
|
+
await cook("recipes/sample.yaml", {
|
|
134
|
+
provider: "command",
|
|
135
|
+
silent: true,
|
|
136
|
+
vars: {
|
|
137
|
+
openai_api_key: process.env.OPENAI_API_KEY ?? "",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`cook()` options:
|
|
143
|
+
|
|
144
|
+
- `vars`: template variables (`Record<string, string>`)
|
|
145
|
+
- `provider`: `command | remote | mock`
|
|
146
|
+
- `remote`: remote provider config (same fields as CLI remote flags)
|
|
147
|
+
- `dryRun`, `allowMissing`, `verbose`
|
|
148
|
+
- `silent` (default: `true` in Node API)
|
|
149
|
+
- `loadDotEnvFromCwd` (default: `true`)
|
|
150
|
+
|
|
151
|
+
Notes:
|
|
152
|
+
|
|
153
|
+
- `validate()` throws on invalid recipe.
|
|
154
|
+
- `cook()` throws on runtime/configuration errors.
|
|
155
|
+
|
|
156
|
+
## Variable precedence
|
|
157
|
+
|
|
158
|
+
1. `--var key=value`
|
|
159
|
+
2. `CLAWCHEF_VAR_<KEY_IN_UPPERCASE>`
|
|
160
|
+
3. `params.<key>.default`
|
|
161
|
+
|
|
162
|
+
If `params.<key>.required: true` and no value is found, run fails.
|
|
163
|
+
|
|
164
|
+
If `.env` exists in the directory where `clawchef` is executed, it is loaded before recipe parsing.
|
|
165
|
+
|
|
166
|
+
## Recipe reference formats
|
|
167
|
+
|
|
168
|
+
`cook` and `validate` accept:
|
|
169
|
+
|
|
170
|
+
- `path/to/recipe.yaml`
|
|
171
|
+
- `path/to/dir` (loads `path/to/dir/recipe.yaml`)
|
|
172
|
+
- `path/to/archive.zip` (loads `recipe.yaml` from extracted archive)
|
|
173
|
+
- `path/to/dir:custom/recipe.yaml`
|
|
174
|
+
- `path/to/archive.tgz:custom/recipe.yaml`
|
|
175
|
+
- `https://host/recipe.yaml`
|
|
176
|
+
- `https://host/archive.zip` (loads `recipe.yaml` from archive)
|
|
177
|
+
- `https://host/archive.zip:custom/recipe.yaml`
|
|
178
|
+
|
|
179
|
+
Supported archives: `.zip`, `.tar`, `.tar.gz`, `.tgz`.
|
|
180
|
+
|
|
181
|
+
## OpenClaw provider
|
|
182
|
+
|
|
183
|
+
Provider is selected at runtime when running `cook`:
|
|
184
|
+
|
|
185
|
+
- `--provider command` (default)
|
|
186
|
+
- `--provider remote`
|
|
187
|
+
- `--provider mock`
|
|
188
|
+
|
|
189
|
+
`mock` provider is useful for local testing of orchestration and output checks.
|
|
190
|
+
|
|
191
|
+
### Remote HTTP provider
|
|
192
|
+
|
|
193
|
+
Use `--provider remote` when clawchef cannot run commands on the target machine and must drive configuration via an HTTP endpoint.
|
|
194
|
+
|
|
195
|
+
Required runtime config:
|
|
196
|
+
|
|
197
|
+
- `--remote-base-url` or `CLAWCHEF_REMOTE_BASE_URL`
|
|
198
|
+
|
|
199
|
+
Optional runtime config:
|
|
200
|
+
|
|
201
|
+
- `--remote-api-key` or `CLAWCHEF_REMOTE_API_KEY`
|
|
202
|
+
- `--remote-api-header` or `CLAWCHEF_REMOTE_API_HEADER` (default: `Authorization`)
|
|
203
|
+
- `--remote-api-scheme` or `CLAWCHEF_REMOTE_API_SCHEME` (default: `Bearer`; set empty string to send raw key)
|
|
204
|
+
- `--remote-timeout-ms` or `CLAWCHEF_REMOTE_TIMEOUT_MS` (default: `60000`)
|
|
205
|
+
- `--remote-operation-path` or `CLAWCHEF_REMOTE_OPERATION_PATH` (default: `/v1/clawchef/operation`)
|
|
206
|
+
|
|
207
|
+
Request payload format (POST):
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"operation": "create_workspace",
|
|
212
|
+
"recipe_version": "2026.2.9",
|
|
213
|
+
"payload": {
|
|
214
|
+
"workspace": {
|
|
215
|
+
"name": "demo",
|
|
216
|
+
"path": "/home/runner/.openclaw/workspaces/demo"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Expected response format:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"ok": true,
|
|
227
|
+
"message": "workspace created",
|
|
228
|
+
"output": "optional agent output",
|
|
229
|
+
"installed_this_run": false
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Supported operation values sent by clawchef:
|
|
234
|
+
|
|
235
|
+
- `ensure_version`, `factory_reset`, `start_gateway`
|
|
236
|
+
- `create_workspace`, `create_agent`, `materialize_file`, `install_skill`
|
|
237
|
+
- `configure_channel`, `login_channel`
|
|
238
|
+
- `run_agent`
|
|
239
|
+
|
|
240
|
+
For `run_agent`, clawchef expects `output` in response for assertions.
|
|
241
|
+
|
|
242
|
+
`command` provider now defaults to the current OpenClaw CLI shape (`openclaw 2026.x`), including:
|
|
243
|
+
|
|
244
|
+
- version check: `openclaw --version`
|
|
245
|
+
- reset: `openclaw reset --scope full --yes --non-interactive`
|
|
246
|
+
- workspace prep: `openclaw onboard --non-interactive --accept-risk --mode local --flow quickstart --auth-choice skip --skip-channels --skip-skills --skip-health --skip-ui --skip-daemon --workspace <path>`
|
|
247
|
+
- agent create: `openclaw agents add <name> --workspace <path> --model <model> --non-interactive --json`
|
|
248
|
+
- run turn: `openclaw agent --local --agent <name> --message <prompt> --json`
|
|
249
|
+
|
|
250
|
+
### Bootstrap config
|
|
251
|
+
|
|
252
|
+
You can provide OpenClaw onboarding and auth parameters under `openclaw.bootstrap`.
|
|
253
|
+
This is used by default workspace creation unless `openclaw.commands.create_workspace` is explicitly overridden.
|
|
254
|
+
|
|
255
|
+
Supported fields include:
|
|
256
|
+
|
|
257
|
+
- onboarding: `mode`, `flow`, `non_interactive`, `accept_risk`, `reset`
|
|
258
|
+
- setup toggles: `skip_channels`, `skip_skills`, `skip_health`, `skip_ui`, `skip_daemon`, `install_daemon`
|
|
259
|
+
- 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
|
+
|
|
261
|
+
When `openclaw.bootstrap` contains provider keys, `clawchef` also injects them into runtime env for `openclaw agent --local`.
|
|
262
|
+
|
|
263
|
+
For `command` provider, default command templates are:
|
|
264
|
+
|
|
265
|
+
- `use_version`: `${bin} --version`
|
|
266
|
+
- `install_version`: `npm install -g openclaw@${version}`
|
|
267
|
+
- `uninstall_version`: `npm uninstall -g openclaw`
|
|
268
|
+
- `factory_reset`: `${bin} reset --scope full --yes --non-interactive`
|
|
269
|
+
- `start_gateway`: `${bin} gateway start`
|
|
270
|
+
- `enable_plugin`: `${bin} plugins enable ${channel_q}`
|
|
271
|
+
- `login_channel`: `${bin} channels login --channel ${channel_q}${account_arg}`
|
|
272
|
+
- `create_workspace`: generated from `openclaw.bootstrap` (override with `openclaw.commands.create_workspace`)
|
|
273
|
+
- `create_agent`: `${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json`
|
|
274
|
+
- `install_skill`: `${bin} skills check`
|
|
275
|
+
- `send_message`: `true` (messages are staged internally for prompt assembly)
|
|
276
|
+
- `run_agent`: `${bin} agent --local --agent ${agent} --message ${prompt_q} --json`
|
|
277
|
+
|
|
278
|
+
You can override any command under `openclaw.commands` in recipe.
|
|
279
|
+
|
|
280
|
+
## Channels
|
|
281
|
+
|
|
282
|
+
Use `channels[]` to configure accounts via `openclaw channels add`.
|
|
283
|
+
If `login: true` is set, clawchef runs channel login at the end of `cook` (after gateway start).
|
|
284
|
+
|
|
285
|
+
Example:
|
|
286
|
+
|
|
287
|
+
```yaml
|
|
288
|
+
channels:
|
|
289
|
+
- channel: "telegram"
|
|
290
|
+
token: "${telegram_bot_token}"
|
|
291
|
+
account: "default"
|
|
292
|
+
login: true
|
|
293
|
+
|
|
294
|
+
- channel: "slack"
|
|
295
|
+
bot_token: "${slack_bot_token}"
|
|
296
|
+
app_token: "${slack_app_token}"
|
|
297
|
+
name: "team-workspace"
|
|
298
|
+
|
|
299
|
+
- channel: "discord"
|
|
300
|
+
token_file: "${discord_token_file}"
|
|
301
|
+
extra_flags:
|
|
302
|
+
webhook_path: "/discord/webhook"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Supported common fields:
|
|
306
|
+
|
|
307
|
+
- required: `channel`
|
|
308
|
+
- 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
|
+
- advanced passthrough: `extra_flags` (`snake_case` keys become `--kebab-case` CLI flags)
|
|
310
|
+
|
|
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
|
+
## Workspace path behavior
|
|
352
|
+
|
|
353
|
+
- `workspaces[].path` is optional.
|
|
354
|
+
- If omitted, clawchef uses `~/.openclaw/workspaces/<workspace-name>`.
|
|
355
|
+
- If provided, relative paths are resolved from the recipe file directory.
|
|
356
|
+
- For direct URL recipe files, relative workspace paths are resolved from the current working directory.
|
|
357
|
+
- For directory/archive recipe references, relative workspace paths are resolved from the selected recipe file directory.
|
|
358
|
+
|
|
359
|
+
## File content references
|
|
360
|
+
|
|
361
|
+
In `files[]`, set exactly one of:
|
|
362
|
+
|
|
363
|
+
- `content`: inline text in recipe
|
|
364
|
+
- `content_from`: load text from another file/URL
|
|
365
|
+
- `source`: copy raw file bytes from another file/URL
|
|
366
|
+
|
|
367
|
+
`content_from` and `source` accept:
|
|
368
|
+
|
|
369
|
+
- path relative to recipe file location
|
|
370
|
+
- absolute filesystem path
|
|
371
|
+
- HTTP/HTTPS URL
|
|
372
|
+
|
|
373
|
+
Useful placeholders when overriding commands:
|
|
374
|
+
|
|
375
|
+
- common: `${bin}`, `${version}`, `${workspace}`, `${agent}`, `${channel}`, `${channel_q}`, `${account}`, `${account_q}`, `${account_arg}`
|
|
376
|
+
- paths: `${path}` / `${workspace_path}` (shell-quoted), `${path_raw}` / `${workspace_path_raw}` (raw)
|
|
377
|
+
- message: `${content}`, `${content_q}`, `${message_file}`, `${message_file_raw}` (`${role}` is always `user`)
|
|
378
|
+
- run prompt: `${prompt}`, `${prompt_q}`
|
|
379
|
+
|
|
380
|
+
## Secret handling
|
|
381
|
+
|
|
382
|
+
- Do not put plaintext API keys/tokens in recipe files.
|
|
383
|
+
- Use `${var}` placeholders in recipe and pass values via:
|
|
384
|
+
- `--var openai_api_key=...`
|
|
385
|
+
- `CLAWCHEF_VAR_OPENAI_API_KEY=...`
|
|
386
|
+
- Inline secrets in `openclaw.bootstrap.*` are rejected by validation.
|
|
387
|
+
|
|
388
|
+
## Conversation message format
|
|
389
|
+
|
|
390
|
+
`conversations[].messages[]` uses:
|
|
391
|
+
|
|
392
|
+
- `content` (required): the user message sent to agent
|
|
393
|
+
- `expect` (optional): output assertion for that message
|
|
394
|
+
|
|
395
|
+
When `conversations[].run: true`, each message triggers one agent run.
|
|
396
|
+
If `run` is omitted, a message still triggers a run when it has `expect`.
|
|
397
|
+
|
|
398
|
+
`expect` supports:
|
|
399
|
+
|
|
400
|
+
- `contains: ["..."]`
|
|
401
|
+
- `not_contains: ["..."]`
|
|
402
|
+
- `regex: ["..."]`
|
|
403
|
+
- `equals: "..."`
|
|
404
|
+
|
|
405
|
+
All configured assertions must pass.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OpenClawProvider, OpenClawRemoteConfig } from "./types.js";
|
|
2
|
+
export interface CookOptions {
|
|
3
|
+
vars?: Record<string, string>;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
allowMissing?: boolean;
|
|
6
|
+
verbose?: boolean;
|
|
7
|
+
silent?: boolean;
|
|
8
|
+
provider?: OpenClawProvider;
|
|
9
|
+
remote?: Partial<OpenClawRemoteConfig>;
|
|
10
|
+
loadDotEnvFromCwd?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function cook(recipeRef: string, options?: CookOptions): Promise<void>;
|
|
13
|
+
export declare function validate(recipeRef: string): Promise<void>;
|
|
14
|
+
export type { OpenClawProvider, OpenClawRemoteConfig };
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import YAML from "js-yaml";
|
|
2
|
+
import { ClawChefError } from "./errors.js";
|
|
3
|
+
import { importDotEnvFromCwd } from "./env.js";
|
|
4
|
+
import { Logger } from "./logger.js";
|
|
5
|
+
import { runRecipe } from "./orchestrator.js";
|
|
6
|
+
import { loadRecipe, loadRecipeText } from "./recipe.js";
|
|
7
|
+
import { recipeSchema } from "./schema.js";
|
|
8
|
+
function normalizeCookOptions(options) {
|
|
9
|
+
return {
|
|
10
|
+
vars: options.vars ?? {},
|
|
11
|
+
dryRun: Boolean(options.dryRun),
|
|
12
|
+
allowMissing: Boolean(options.allowMissing),
|
|
13
|
+
verbose: Boolean(options.verbose),
|
|
14
|
+
silent: options.silent ?? true,
|
|
15
|
+
provider: options.provider ?? "command",
|
|
16
|
+
remote: options.remote ?? {},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export async function cook(recipeRef, options = {}) {
|
|
20
|
+
if (options.loadDotEnvFromCwd ?? true) {
|
|
21
|
+
importDotEnvFromCwd();
|
|
22
|
+
}
|
|
23
|
+
const runOptions = normalizeCookOptions(options);
|
|
24
|
+
const logger = new Logger(runOptions.verbose);
|
|
25
|
+
const loaded = await loadRecipe(recipeRef, runOptions);
|
|
26
|
+
try {
|
|
27
|
+
await runRecipe(loaded.recipe, loaded.origin, runOptions, logger);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (loaded.cleanup) {
|
|
31
|
+
await loaded.cleanup();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function validate(recipeRef) {
|
|
36
|
+
const loaded = await loadRecipeText(recipeRef);
|
|
37
|
+
try {
|
|
38
|
+
const raw = YAML.load(loaded.source);
|
|
39
|
+
const result = recipeSchema.safeParse(raw);
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
throw new ClawChefError(`Validation failed: ${result.error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (loaded.cleanup) {
|
|
46
|
+
await loaded.cleanup();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ClawChefError } from "./errors.js";
|
|
2
|
+
export function validateReply(reply, expect) {
|
|
3
|
+
if (!expect) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
for (const text of expect.contains ?? []) {
|
|
7
|
+
if (!reply.includes(text)) {
|
|
8
|
+
throw new ClawChefError(`Output assertion failed: must contain -> ${text}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
for (const text of expect.not_contains ?? []) {
|
|
12
|
+
if (reply.includes(text)) {
|
|
13
|
+
throw new ClawChefError(`Output assertion failed: must not contain -> ${text}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (const pattern of expect.regex ?? []) {
|
|
17
|
+
let re;
|
|
18
|
+
try {
|
|
19
|
+
re = new RegExp(pattern, "m");
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23
|
+
throw new ClawChefError(`Output assertion failed: invalid regex ${pattern} (${msg})`);
|
|
24
|
+
}
|
|
25
|
+
if (!re.test(reply)) {
|
|
26
|
+
throw new ClawChefError(`Output assertion failed: regex mismatch -> ${pattern}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (expect.equals !== undefined && reply !== expect.equals) {
|
|
30
|
+
throw new ClawChefError("Output assertion failed: equals mismatch");
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ClawChefError } from "./errors.js";
|
|
3
|
+
import { Logger } from "./logger.js";
|
|
4
|
+
import { runRecipe } from "./orchestrator.js";
|
|
5
|
+
import { loadRecipe, loadRecipeText } from "./recipe.js";
|
|
6
|
+
import { recipeSchema } from "./schema.js";
|
|
7
|
+
import YAML from "js-yaml";
|
|
8
|
+
function parseVarFlags(values) {
|
|
9
|
+
const out = {};
|
|
10
|
+
for (const item of values) {
|
|
11
|
+
const idx = item.indexOf("=");
|
|
12
|
+
if (idx <= 0 || idx === item.length - 1) {
|
|
13
|
+
throw new ClawChefError(`Invalid --var format: ${item}. Expected key=value`);
|
|
14
|
+
}
|
|
15
|
+
const k = item.slice(0, idx).trim();
|
|
16
|
+
const v = item.slice(idx + 1).trim();
|
|
17
|
+
out[k] = v;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function readEnv(name) {
|
|
22
|
+
const value = process.env[name];
|
|
23
|
+
if (value === undefined) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
28
|
+
}
|
|
29
|
+
function parseProvider(value) {
|
|
30
|
+
if (value === "command" || value === "mock" || value === "remote") {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
throw new ClawChefError(`Invalid --provider value: ${value}. Expected command, remote, or mock`);
|
|
34
|
+
}
|
|
35
|
+
function parseOptionalInt(value, fieldName) {
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const parsed = Number(value);
|
|
40
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
41
|
+
throw new ClawChefError(`${fieldName} must be a positive integer`);
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
export function buildCli() {
|
|
46
|
+
const program = new Command();
|
|
47
|
+
program
|
|
48
|
+
.name("clawchef")
|
|
49
|
+
.description("Run OpenClaw environment recipes")
|
|
50
|
+
.version("0.1.0");
|
|
51
|
+
program
|
|
52
|
+
.command("cook")
|
|
53
|
+
.argument("<recipe>", "Recipe path/URL/dir/archive[:file]")
|
|
54
|
+
.option("--var <key=value>", "Template variable", (v, p) => p.concat([v]), [])
|
|
55
|
+
.option("--dry-run", "Print actions without executing", false)
|
|
56
|
+
.option("--allow-missing", "Allow unresolved template variables", false)
|
|
57
|
+
.option("--verbose", "Verbose logging", false)
|
|
58
|
+
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
59
|
+
.option("--provider <provider>", "Execution provider: command | remote | mock")
|
|
60
|
+
.option("--remote-base-url <url>", "Remote OpenClaw API base URL")
|
|
61
|
+
.option("--remote-api-key <key>", "Remote OpenClaw API key")
|
|
62
|
+
.option("--remote-api-header <header>", "Remote auth header name")
|
|
63
|
+
.option("--remote-api-scheme <scheme>", "Remote auth scheme (default: Bearer)")
|
|
64
|
+
.option("--remote-timeout-ms <ms>", "Remote operation timeout in milliseconds")
|
|
65
|
+
.option("--remote-operation-path <path>", "Remote operation endpoint path")
|
|
66
|
+
.action(async (recipeRef, opts) => {
|
|
67
|
+
const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
|
|
68
|
+
const options = {
|
|
69
|
+
vars: parseVarFlags(opts.var),
|
|
70
|
+
dryRun: Boolean(opts.dryRun),
|
|
71
|
+
allowMissing: Boolean(opts.allowMissing),
|
|
72
|
+
verbose: Boolean(opts.verbose),
|
|
73
|
+
silent: Boolean(opts.silent),
|
|
74
|
+
provider,
|
|
75
|
+
remote: {
|
|
76
|
+
base_url: opts.remoteBaseUrl ?? readEnv("CLAWCHEF_REMOTE_BASE_URL"),
|
|
77
|
+
api_key: opts.remoteApiKey ?? readEnv("CLAWCHEF_REMOTE_API_KEY"),
|
|
78
|
+
api_header: opts.remoteApiHeader ?? readEnv("CLAWCHEF_REMOTE_API_HEADER"),
|
|
79
|
+
api_scheme: opts.remoteApiScheme ?? readEnv("CLAWCHEF_REMOTE_API_SCHEME"),
|
|
80
|
+
timeout_ms: parseOptionalInt(opts.remoteTimeoutMs ?? readEnv("CLAWCHEF_REMOTE_TIMEOUT_MS"), "remote-timeout-ms"),
|
|
81
|
+
operation_path: opts.remoteOperationPath ?? readEnv("CLAWCHEF_REMOTE_OPERATION_PATH"),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const logger = new Logger(options.verbose);
|
|
85
|
+
const loaded = await loadRecipe(recipeRef, options);
|
|
86
|
+
try {
|
|
87
|
+
await runRecipe(loaded.recipe, loaded.origin, options, logger);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
if (loaded.cleanup) {
|
|
91
|
+
await loaded.cleanup();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
program
|
|
96
|
+
.command("validate")
|
|
97
|
+
.argument("<recipe>", "Recipe path/URL/dir/archive[:file]")
|
|
98
|
+
.action(async (recipeRef) => {
|
|
99
|
+
const loaded = await loadRecipeText(recipeRef);
|
|
100
|
+
try {
|
|
101
|
+
const raw = YAML.load(loaded.source);
|
|
102
|
+
const result = recipeSchema.safeParse(raw);
|
|
103
|
+
if (!result.success) {
|
|
104
|
+
throw new ClawChefError(`Validation failed: ${result.error.message}`);
|
|
105
|
+
}
|
|
106
|
+
process.stdout.write("Recipe structure is valid\n");
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
if (loaded.cleanup) {
|
|
110
|
+
await loaded.cleanup();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return program;
|
|
115
|
+
}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function importDotEnvFromCwd(): void;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { config as loadDotenv } from "dotenv";
|
|
4
|
+
import { ClawChefError } from "./errors.js";
|
|
5
|
+
export function importDotEnvFromCwd() {
|
|
6
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
7
|
+
if (!existsSync(envPath)) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const result = loadDotenv({ path: envPath, override: false });
|
|
11
|
+
if (result.error) {
|
|
12
|
+
throw new ClawChefError(`Failed to load .env from current directory: ${result.error.message}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildCli } from "./cli.js";
|
|
3
|
+
import { importDotEnvFromCwd } from "./env.js";
|
|
4
|
+
import { ClawChefError } from "./errors.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
importDotEnvFromCwd();
|
|
7
|
+
const program = buildCli();
|
|
8
|
+
await program.parseAsync(process.argv);
|
|
9
|
+
}
|
|
10
|
+
main().catch((err) => {
|
|
11
|
+
if (err instanceof ClawChefError) {
|
|
12
|
+
process.stderr.write(`[ERROR] ${err.message}\n`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const msg = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
16
|
+
process.stderr.write(`[FATAL] ${msg}\n`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|