clawchef 0.1.6 → 0.1.8
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/AGENTS.md +159 -0
- package/README.md +32 -14
- package/dist/api.d.ts +3 -1
- package/dist/api.js +10 -1
- package/dist/cli.js +34 -3
- package/dist/openclaw/command-provider.d.ts +2 -1
- package/dist/openclaw/command-provider.js +109 -23
- package/dist/openclaw/mock-provider.d.ts +2 -1
- package/dist/openclaw/mock-provider.js +4 -1
- package/dist/openclaw/provider.d.ts +2 -1
- package/dist/openclaw/remote-provider.d.ts +2 -1
- package/dist/openclaw/remote-provider.js +8 -1
- package/dist/orchestrator.js +90 -56
- package/dist/recipe.js +71 -24
- package/dist/schema.d.ts +84 -107
- package/dist/schema.js +17 -21
- package/dist/types.d.ts +8 -11
- package/package.json +1 -1
- package/recipes/content-from-sample.yaml +13 -9
- package/recipes/openclaw-from-zero.yaml +2 -2
- package/recipes/openclaw-local.yaml +8 -10
- package/recipes/openclaw-remote-http.yaml +6 -8
- package/recipes/sample.yaml +6 -8
- package/recipes/snippets/readme-template.md +3 -1
- package/src/api.ts +13 -2
- package/src/cli.ts +36 -4
- package/src/openclaw/command-provider.ts +134 -24
- package/src/openclaw/mock-provider.ts +10 -1
- package/src/openclaw/provider.ts +2 -1
- package/src/openclaw/remote-provider.ts +14 -1
- package/src/orchestrator.ts +93 -55
- package/src/recipe.ts +82 -24
- package/src/schema.ts +19 -22
- package/src/types.ts +8 -11
package/AGENTS.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# AGENTS Guide for `clawchef`
|
|
2
|
+
|
|
3
|
+
This file is for coding agents operating in this repository.
|
|
4
|
+
It documents the current build/test workflow and code conventions inferred from the codebase.
|
|
5
|
+
|
|
6
|
+
## Scope and Source of Truth
|
|
7
|
+
|
|
8
|
+
- Primary language: TypeScript (ESM, NodeNext).
|
|
9
|
+
- Runtime target: Node.js `>=20`.
|
|
10
|
+
- Package manager: npm (`package-lock.json` is present).
|
|
11
|
+
- Compiler config: `tsconfig.json` with `strict: true` and `rootDir: src`, `outDir: dist`.
|
|
12
|
+
- Public entry points: CLI (`src/index.ts`) and Node API (`src/api.ts`).
|
|
13
|
+
|
|
14
|
+
## Repository Rules Discovery
|
|
15
|
+
|
|
16
|
+
I checked for additional agent instruction files:
|
|
17
|
+
|
|
18
|
+
- `.cursor/rules/`: not present
|
|
19
|
+
- `.cursorrules`: not present
|
|
20
|
+
- `.github/copilot-instructions.md`: not present
|
|
21
|
+
|
|
22
|
+
So this `AGENTS.md` is the repo-local guidance file for agents.
|
|
23
|
+
|
|
24
|
+
## Build, Lint, and Test Commands
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
- `npm install`
|
|
29
|
+
|
|
30
|
+
## Build
|
|
31
|
+
|
|
32
|
+
- `npm run build`
|
|
33
|
+
- Runs `tsc -p tsconfig.json`
|
|
34
|
+
- Emits JS + declarations into `dist/`
|
|
35
|
+
|
|
36
|
+
## Type Check
|
|
37
|
+
|
|
38
|
+
- `npm run typecheck`
|
|
39
|
+
- Runs `tsc --noEmit -p tsconfig.json`
|
|
40
|
+
- Use this as the closest thing to linting gate today
|
|
41
|
+
|
|
42
|
+
## Lint
|
|
43
|
+
|
|
44
|
+
- There is currently **no configured lint script** (`npm run lint` does not exist).
|
|
45
|
+
- Do not assume ESLint/Prettier are configured.
|
|
46
|
+
- Keep formatting and style aligned with existing files.
|
|
47
|
+
|
|
48
|
+
## Tests
|
|
49
|
+
|
|
50
|
+
- There is currently **no test script** in this repository’s `package.json`.
|
|
51
|
+
- There is no committed `test/` directory in this repo at the moment.
|
|
52
|
+
- For now, validation is typically: build + typecheck + targeted manual CLI/API smoke checks.
|
|
53
|
+
|
|
54
|
+
## Running a single test (when test files exist)
|
|
55
|
+
|
|
56
|
+
If you add Node test files (for example `*.test.mjs`), run one test file directly with:
|
|
57
|
+
|
|
58
|
+
- `node --test path/to/file.test.mjs`
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
|
|
62
|
+
- `node --test test/recipe-smoke.test.mjs`
|
|
63
|
+
|
|
64
|
+
Notes:
|
|
65
|
+
|
|
66
|
+
- Scaffolded recipe projects generated by `clawchef scaffold` include `test/recipe-smoke.test.mjs`.
|
|
67
|
+
- Those scaffolded projects also include `npm run test:recipe` and `npm test`, but this repo currently does not.
|
|
68
|
+
|
|
69
|
+
## Coding Style Guidelines
|
|
70
|
+
|
|
71
|
+
These conventions are based on current source in `src/`.
|
|
72
|
+
|
|
73
|
+
## Formatting and syntax
|
|
74
|
+
|
|
75
|
+
- Use 2-space indentation.
|
|
76
|
+
- Use semicolons.
|
|
77
|
+
- Use double quotes for strings.
|
|
78
|
+
- Prefer trailing commas in multiline literals/calls.
|
|
79
|
+
- Keep functions focused and relatively small unless complexity requires expansion.
|
|
80
|
+
|
|
81
|
+
## Module system and imports
|
|
82
|
+
|
|
83
|
+
- Use ESM imports/exports only.
|
|
84
|
+
- For local imports, include `.js` extension in import specifiers (important for NodeNext output).
|
|
85
|
+
- Use `import type` for type-only imports.
|
|
86
|
+
- Prefer Node built-ins via `node:` prefix.
|
|
87
|
+
- Import ordering is not rigidly enforced; keep it consistent within each edited file.
|
|
88
|
+
|
|
89
|
+
## Types and strictness
|
|
90
|
+
|
|
91
|
+
- Preserve strict TypeScript compatibility (`strict: true`).
|
|
92
|
+
- Avoid `any`; use `unknown` when needed and narrow safely.
|
|
93
|
+
- Prefer explicit interfaces/types for structured data (see `src/types.ts`).
|
|
94
|
+
- Prefer narrow unions for finite modes/options (`RunScope`, provider names, etc.).
|
|
95
|
+
- Keep runtime validation in sync with static types.
|
|
96
|
+
- If a recipe shape changes, update both `src/types.ts` and `src/schema.ts`.
|
|
97
|
+
|
|
98
|
+
## Naming conventions
|
|
99
|
+
|
|
100
|
+
- `camelCase` for variables/functions.
|
|
101
|
+
- `PascalCase` for classes, interfaces, and type aliases intended as entities.
|
|
102
|
+
- `UPPER_SNAKE_CASE` for top-level constants with fixed meaning.
|
|
103
|
+
- Keep naming domain-oriented (`workspace`, `channel`, `agent`, `bootstrap`, `scope`).
|
|
104
|
+
|
|
105
|
+
## Error handling conventions
|
|
106
|
+
|
|
107
|
+
- Throw `ClawChefError` for expected/user-facing failures.
|
|
108
|
+
- Include actionable, specific messages.
|
|
109
|
+
- For caught unknown errors, convert to readable message:
|
|
110
|
+
- `err instanceof Error ? err.message : String(err)`
|
|
111
|
+
- At CLI top-level, only `ClawChefError` should map to normal error output; unknown errors become fatal.
|
|
112
|
+
|
|
113
|
+
## Logging and output
|
|
114
|
+
|
|
115
|
+
- Use `Logger` (`info`, `warn`, `debug`) for orchestration flow messages.
|
|
116
|
+
- `debug` logs should remain behind `--verbose`.
|
|
117
|
+
- Keep logs concise and operationally useful.
|
|
118
|
+
- Use `process.stdout.write` / `process.stderr.write` for CLI I/O (consistent with existing code).
|
|
119
|
+
|
|
120
|
+
## Async and filesystem patterns
|
|
121
|
+
|
|
122
|
+
- Use async/await with `node:fs/promises` APIs.
|
|
123
|
+
- Prefer non-blocking I/O, except tiny startup reads where sync is already established (`readPackageVersion`).
|
|
124
|
+
- Wrap external command execution and network operations with clear errors.
|
|
125
|
+
- Preserve cleanup behavior for temp dirs/files via `try/finally`.
|
|
126
|
+
|
|
127
|
+
## Recipe/schema evolution rules
|
|
128
|
+
|
|
129
|
+
- Schema is strict (`zod` `.strict()` used heavily); unknown keys are rejected.
|
|
130
|
+
- Enforce semantic constraints in `semanticValidate` (`src/recipe.ts`) after schema parse.
|
|
131
|
+
- Keep secret-handling guardrails intact:
|
|
132
|
+
- Inline secrets should be rejected where required.
|
|
133
|
+
- Template placeholders (`${var}`) are expected for secrets.
|
|
134
|
+
|
|
135
|
+
## CLI/API change guidelines
|
|
136
|
+
|
|
137
|
+
- Keep CLI flags in `src/cli.ts` aligned with Node API options in `src/api.ts` when feature parity is intended.
|
|
138
|
+
- Validate mutually dependent options early (for example, scope + workspace pairing).
|
|
139
|
+
- Maintain provider parity (`command`, `remote`, `mock`) when adding operations.
|
|
140
|
+
- If provider interface changes, update all provider implementations and factory wiring.
|
|
141
|
+
|
|
142
|
+
## Suggested pre-commit checklist for agents
|
|
143
|
+
|
|
144
|
+
- `npm run typecheck`
|
|
145
|
+
- `npm run build`
|
|
146
|
+
- If tests were added: run relevant Node tests, at least one targeted file
|
|
147
|
+
- Verify README snippets if CLI flags/behavior changed
|
|
148
|
+
- Confirm `src/types.ts` and `src/schema.ts` remain synchronized
|
|
149
|
+
|
|
150
|
+
## Quick architecture map
|
|
151
|
+
|
|
152
|
+
- `src/cli.ts`: command parsing + CLI runtime wiring
|
|
153
|
+
- `src/api.ts`: Node API (`cook`, `validate`, `scaffold`)
|
|
154
|
+
- `src/recipe.ts`: recipe loading, template resolution, semantic validation
|
|
155
|
+
- `src/orchestrator.ts`: execution flow across workspaces/agents/channels/files
|
|
156
|
+
- `src/openclaw/*.ts`: provider abstraction and implementations
|
|
157
|
+
- `src/schema.ts` + `src/types.ts`: validation schema and TS domain model
|
|
158
|
+
|
|
159
|
+
When in doubt, follow existing patterns in adjacent files and keep changes minimal, explicit, and type-safe.
|
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
12
12
|
- Requires secrets to be injected via `--var` / `CLAWCHEF_VAR_*` (no inline secrets in recipe).
|
|
13
13
|
- Prepares OpenClaw version (install or reuse).
|
|
14
14
|
- When installed OpenClaw version mismatches recipe version, prompts: ignore / abort / force reinstall (silent mode auto-picks force reinstall).
|
|
15
|
-
-
|
|
15
|
+
- Supports scoped execution via `--scope full|files|workspace`.
|
|
16
|
+
- `full` scope runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
|
|
16
17
|
- If `openclaw` is missing, auto-installs the recipe version and skips factory reset.
|
|
17
18
|
- Starts OpenClaw gateway service after each recipe execution.
|
|
18
19
|
- Creates workspaces and agents (default workspace path: `~/.openclaw/workspace-<workspace-name>`).
|
|
@@ -20,7 +21,6 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
20
21
|
- Materializes files into target workspaces.
|
|
21
22
|
- Installs skills.
|
|
22
23
|
- Supports plugin preinstall via `openclaw.plugins[]` and runtime `--plugin` flags.
|
|
23
|
-
- Supports preserving existing OpenClaw state via `--keep-openclaw-state` (skip factory reset).
|
|
24
24
|
- Configures channels with `openclaw channels add`.
|
|
25
25
|
- Supports interactive channel login at the end of execution (`channels[].login: true`) for channels that expose login.
|
|
26
26
|
- Supports remote HTTP orchestration via runtime flags (`--provider remote`) when OpenClaw is reachable via API.
|
|
@@ -96,13 +96,19 @@ Use it only in CI/non-interactive flows where destructive reset behavior is expe
|
|
|
96
96
|
Keep existing OpenClaw state (skip reset and keep current version on mismatch):
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
|
-
clawchef cook recipes/sample.yaml --
|
|
99
|
+
clawchef cook recipes/sample.yaml --scope files
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Update only one workspace:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
clawchef cook recipes/sample.yaml --scope workspace --workspace workspace-meeting
|
|
100
106
|
```
|
|
101
107
|
|
|
102
108
|
From-zero OpenClaw bootstrap (recommended):
|
|
103
109
|
|
|
104
110
|
```bash
|
|
105
|
-
|
|
111
|
+
CLAWCHEF_VAR_LLM_API_KEY=sk-... clawchef cook recipes/openclaw-from-zero.yaml --verbose
|
|
106
112
|
```
|
|
107
113
|
|
|
108
114
|
Telegram channel setup only:
|
|
@@ -183,7 +189,7 @@ await cook("recipes/sample.yaml", {
|
|
|
183
189
|
provider: "command",
|
|
184
190
|
silent: true,
|
|
185
191
|
vars: {
|
|
186
|
-
|
|
192
|
+
llm_api_key: process.env.OPENAI_API_KEY ?? "",
|
|
187
193
|
},
|
|
188
194
|
});
|
|
189
195
|
|
|
@@ -196,6 +202,8 @@ await scaffold("./my-recipe-project", {
|
|
|
196
202
|
|
|
197
203
|
- `vars`: template variables (`Record<string, string>`)
|
|
198
204
|
- `plugins`: plugin npm specs to preinstall for this run (`string[]`)
|
|
205
|
+
- `scope`: `full | files | workspace` (default: `full`)
|
|
206
|
+
- `workspaceName`: required when `scope: "workspace"`
|
|
199
207
|
- `provider`: `command | remote | mock`
|
|
200
208
|
- `remote`: remote provider config (same fields as CLI remote flags)
|
|
201
209
|
- `envFile`: custom env file path/URL; when set, default cwd `.env` loading is skipped
|
|
@@ -296,7 +304,7 @@ Supported operation values sent by clawchef:
|
|
|
296
304
|
- `ensure_version`, `factory_reset`, `start_gateway`
|
|
297
305
|
- `install_plugin`
|
|
298
306
|
- `create_workspace`, `create_agent`, `materialize_file`, `install_skill`
|
|
299
|
-
- `configure_channel`, `login_channel`
|
|
307
|
+
- `configure_channel`, `bind_channel_agent`, `login_channel`
|
|
300
308
|
- `run_agent`
|
|
301
309
|
|
|
302
310
|
For `run_agent`, clawchef expects `output` in response for assertions.
|
|
@@ -318,7 +326,7 @@ Supported fields include:
|
|
|
318
326
|
|
|
319
327
|
- onboarding: `mode`, `flow`, `non_interactive`, `accept_risk`, `reset`
|
|
320
328
|
- setup toggles: `skip_channels`, `skip_skills`, `skip_health`, `skip_ui`, `skip_daemon`, `install_daemon`
|
|
321
|
-
- auth/provider: `auth_choice`, `
|
|
329
|
+
- auth/provider: `auth_choice`, `llm_api_key`, `token`, `token_provider`, `token_profile_id`
|
|
322
330
|
|
|
323
331
|
### Plugin preinstall
|
|
324
332
|
|
|
@@ -334,7 +342,7 @@ openclaw:
|
|
|
334
342
|
- "@scope/custom-channel-plugin@1.2.3"
|
|
335
343
|
```
|
|
336
344
|
|
|
337
|
-
When `openclaw.bootstrap` contains
|
|
345
|
+
When `openclaw.bootstrap` contains `llm_api_key`, clawchef maps it to the provider-specific runtime env by `auth_choice` for `openclaw agent --local`.
|
|
338
346
|
|
|
339
347
|
For `command` provider, default command templates are:
|
|
340
348
|
|
|
@@ -344,6 +352,7 @@ For `command` provider, default command templates are:
|
|
|
344
352
|
- `install_plugin`: `${bin} plugins install ${plugin_spec_q}`
|
|
345
353
|
- `factory_reset`: `${bin} reset --scope full --yes --non-interactive`
|
|
346
354
|
- `start_gateway`: `${bin} gateway start`
|
|
355
|
+
- `bind_channel_agent`: built-in `openclaw config get/set bindings` upsert (override with `openclaw.commands.bind_channel_agent`)
|
|
347
356
|
- `login_channel`: `${bin} channels login --channel ${channel_q}${account_arg}`
|
|
348
357
|
- `create_workspace`: generated from `openclaw.bootstrap` (override with `openclaw.commands.create_workspace`)
|
|
349
358
|
- `create_agent`: `${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json`
|
|
@@ -369,6 +378,12 @@ channels:
|
|
|
369
378
|
- channel: "telegram"
|
|
370
379
|
token: "${telegram_bot_token}"
|
|
371
380
|
account: "default"
|
|
381
|
+
agent: "main"
|
|
382
|
+
|
|
383
|
+
- channel: "telegram"
|
|
384
|
+
token: "${alerts_bot_token}"
|
|
385
|
+
account: "alerts"
|
|
386
|
+
agent: "alerts"
|
|
372
387
|
|
|
373
388
|
- channel: "slack"
|
|
374
389
|
bot_token: "${slack_bot_token}"
|
|
@@ -384,9 +399,12 @@ channels:
|
|
|
384
399
|
Supported common fields:
|
|
385
400
|
|
|
386
401
|
- required: `channel`
|
|
387
|
-
- 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`
|
|
402
|
+
- optional: `account`, `agent`, `name`, `token`, `token_file`, `use_env`, `bot_token`, `access_token`, `app_token`, `webhook_url`, `webhook_path`, `signal_number`, `password`, `login`, `login_mode`, `login_account`
|
|
388
403
|
- advanced passthrough: `extra_flags` (`snake_case` keys become `--kebab-case` CLI flags)
|
|
389
404
|
|
|
405
|
+
`channels[].agent` currently supports `channel: "telegram"` only.
|
|
406
|
+
If `agent` is set and `account` is omitted, clawchef defaults `account` to the same value as `agent`.
|
|
407
|
+
|
|
390
408
|
## Workspace path behavior
|
|
391
409
|
|
|
392
410
|
- `workspaces[].path` is optional.
|
|
@@ -394,7 +412,7 @@ Supported common fields:
|
|
|
394
412
|
- `workspaces[].assets` is optional.
|
|
395
413
|
- If `assets` is set, clawchef recursively copies files from that directory into the workspace root.
|
|
396
414
|
- `assets` is resolved relative to the recipe file path (unless absolute path is given).
|
|
397
|
-
- `files[]` runs after assets copy, so
|
|
415
|
+
- `workspaces[].files[]` runs after assets copy, so explicit file entries can override copied asset files.
|
|
398
416
|
- Direct URL recipes do not support `workspaces[].assets` (assets must resolve to a local directory).
|
|
399
417
|
- If provided, relative paths are resolved from the recipe file directory.
|
|
400
418
|
- For direct URL recipe files, relative workspace paths are resolved from the current working directory.
|
|
@@ -410,10 +428,10 @@ workspaces:
|
|
|
410
428
|
|
|
411
429
|
## File content references
|
|
412
430
|
|
|
413
|
-
In `files[]`, set exactly one of:
|
|
431
|
+
In `workspaces[].files[]`, set exactly one of:
|
|
414
432
|
|
|
415
433
|
- `content`: inline text in recipe
|
|
416
|
-
- `content_from`: load text from another file/URL
|
|
434
|
+
- `content_from`: load text from another file/URL (loaded content supports `${var}` template rendering)
|
|
417
435
|
- `source`: copy raw file bytes from another file/URL
|
|
418
436
|
|
|
419
437
|
`content_from` and `source` accept:
|
|
@@ -433,8 +451,8 @@ Useful placeholders when overriding commands:
|
|
|
433
451
|
|
|
434
452
|
- Do not put plaintext API keys/tokens in recipe files.
|
|
435
453
|
- Use `${var}` placeholders in recipe and pass values via:
|
|
436
|
-
- `--var
|
|
437
|
-
- `
|
|
454
|
+
- `--var llm_api_key=...`
|
|
455
|
+
- `CLAWCHEF_VAR_LLM_API_KEY=...`
|
|
438
456
|
- Inline secrets in `openclaw.bootstrap.*` are rejected by validation.
|
|
439
457
|
|
|
440
458
|
## Conversation message format
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawProvider, OpenClawRemoteConfig } from "./types.js";
|
|
1
|
+
import type { OpenClawProvider, OpenClawRemoteConfig, RunScope } from "./types.js";
|
|
2
2
|
import type { ScaffoldOptions, ScaffoldResult } from "./scaffold.js";
|
|
3
3
|
export interface CookOptions {
|
|
4
4
|
vars?: Record<string, string>;
|
|
@@ -7,6 +7,8 @@ export interface CookOptions {
|
|
|
7
7
|
allowMissing?: boolean;
|
|
8
8
|
verbose?: boolean;
|
|
9
9
|
silent?: boolean;
|
|
10
|
+
scope?: RunScope;
|
|
11
|
+
workspaceName?: string;
|
|
10
12
|
provider?: OpenClawProvider;
|
|
11
13
|
remote?: Partial<OpenClawRemoteConfig>;
|
|
12
14
|
envFile?: string;
|
package/dist/api.js
CHANGED
|
@@ -8,14 +8,23 @@ import { scaffoldProject } from "./scaffold.js";
|
|
|
8
8
|
import { recipeSchema } from "./schema.js";
|
|
9
9
|
function normalizeCookOptions(options) {
|
|
10
10
|
const plugins = Array.from(new Set((options.plugins ?? []).map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
11
|
+
const scope = options.scope ?? "full";
|
|
12
|
+
const workspaceName = options.workspaceName?.trim() || undefined;
|
|
13
|
+
if (scope === "workspace" && !workspaceName) {
|
|
14
|
+
throw new ClawChefError("scope=workspace requires workspaceName");
|
|
15
|
+
}
|
|
16
|
+
if (scope !== "workspace" && workspaceName) {
|
|
17
|
+
throw new ClawChefError("workspaceName is only allowed when scope=workspace");
|
|
18
|
+
}
|
|
11
19
|
return {
|
|
12
20
|
vars: options.vars ?? {},
|
|
13
21
|
plugins,
|
|
22
|
+
scope,
|
|
23
|
+
workspaceName,
|
|
14
24
|
dryRun: Boolean(options.dryRun),
|
|
15
25
|
allowMissing: Boolean(options.allowMissing),
|
|
16
26
|
verbose: Boolean(options.verbose),
|
|
17
27
|
silent: options.silent ?? true,
|
|
18
|
-
keepOpenClawState: false,
|
|
19
28
|
provider: options.provider ?? "command",
|
|
20
29
|
remote: options.remote ?? {},
|
|
21
30
|
};
|
package/dist/cli.js
CHANGED
|
@@ -8,8 +8,23 @@ import { recipeSchema } from "./schema.js";
|
|
|
8
8
|
import { scaffoldProject } from "./scaffold.js";
|
|
9
9
|
import YAML from "js-yaml";
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
11
12
|
import { createInterface } from "node:readline/promises";
|
|
12
13
|
import { stdin as input, stdout as output } from "node:process";
|
|
14
|
+
function readPackageVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const pkgPath = new URL("../package.json", import.meta.url);
|
|
17
|
+
const content = readFileSync(pkgPath, "utf8");
|
|
18
|
+
const parsed = JSON.parse(content);
|
|
19
|
+
if (parsed.version?.trim()) {
|
|
20
|
+
return parsed.version;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// ignore and use fallback
|
|
25
|
+
}
|
|
26
|
+
return "0.0.0";
|
|
27
|
+
}
|
|
13
28
|
function parseVarFlags(values) {
|
|
14
29
|
const out = {};
|
|
15
30
|
for (const item of values) {
|
|
@@ -40,6 +55,12 @@ function parseProvider(value) {
|
|
|
40
55
|
}
|
|
41
56
|
throw new ClawChefError(`Invalid --provider value: ${value}. Expected command, remote, or mock`);
|
|
42
57
|
}
|
|
58
|
+
function parseScope(value) {
|
|
59
|
+
if (value === "full" || value === "files" || value === "workspace") {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, files, or workspace`);
|
|
63
|
+
}
|
|
43
64
|
function parseOptionalInt(value, fieldName) {
|
|
44
65
|
if (value === undefined) {
|
|
45
66
|
return undefined;
|
|
@@ -69,7 +90,7 @@ export function buildCli() {
|
|
|
69
90
|
program
|
|
70
91
|
.name("clawchef")
|
|
71
92
|
.description("Run OpenClaw environment recipes")
|
|
72
|
-
.version(
|
|
93
|
+
.version(readPackageVersion());
|
|
73
94
|
program
|
|
74
95
|
.command("cook")
|
|
75
96
|
.argument("<recipe>", "Recipe path/URL/dir/archive[:file]")
|
|
@@ -78,7 +99,8 @@ export function buildCli() {
|
|
|
78
99
|
.option("--allow-missing", "Allow unresolved template variables", false)
|
|
79
100
|
.option("--verbose", "Verbose logging", false)
|
|
80
101
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
81
|
-
.option("--
|
|
102
|
+
.option("--scope <scope>", "Run scope: full | files | workspace", "full")
|
|
103
|
+
.option("--workspace <name>", "Workspace name (required when --scope workspace)")
|
|
82
104
|
.option("--dotenv-ref <path-or-url>", "Load env vars from local file or HTTP URL")
|
|
83
105
|
.option("--provider <provider>", "Execution provider: command | remote | mock")
|
|
84
106
|
.option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p) => p.concat([v]), [])
|
|
@@ -96,14 +118,23 @@ export function buildCli() {
|
|
|
96
118
|
importDotEnvFromCwd();
|
|
97
119
|
}
|
|
98
120
|
const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
|
|
121
|
+
const scope = parseScope(String(opts.scope ?? "full"));
|
|
122
|
+
const workspaceName = opts.workspace?.trim() ? String(opts.workspace).trim() : undefined;
|
|
123
|
+
if (scope === "workspace" && !workspaceName) {
|
|
124
|
+
throw new ClawChefError("--scope workspace requires --workspace <name>");
|
|
125
|
+
}
|
|
126
|
+
if (scope !== "workspace" && workspaceName) {
|
|
127
|
+
throw new ClawChefError("--workspace is only allowed when --scope workspace");
|
|
128
|
+
}
|
|
99
129
|
const options = {
|
|
100
130
|
vars: parseVarFlags(opts.var),
|
|
101
131
|
plugins: parsePluginFlags(opts.plugin),
|
|
132
|
+
scope,
|
|
133
|
+
workspaceName,
|
|
102
134
|
dryRun: Boolean(opts.dryRun),
|
|
103
135
|
allowMissing: Boolean(opts.allowMissing),
|
|
104
136
|
verbose: Boolean(opts.verbose),
|
|
105
137
|
silent: Boolean(opts.silent),
|
|
106
|
-
keepOpenClawState: Boolean(opts.keepOpenclawState),
|
|
107
138
|
provider,
|
|
108
139
|
remote: {
|
|
109
140
|
base_url: opts.remoteBaseUrl ?? readEnv("CLAWCHEF_REMOTE_BASE_URL"),
|
|
@@ -2,12 +2,13 @@ import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../
|
|
|
2
2
|
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
3
3
|
export declare class CommandOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private readonly stagedMessages;
|
|
5
|
-
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean,
|
|
5
|
+
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean, preserveExistingState: boolean): Promise<EnsureVersionResult>;
|
|
6
6
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
7
7
|
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
8
8
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
9
9
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
10
10
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
11
|
+
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
11
12
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
12
13
|
createAgent(config: OpenClawSection, agent: AgentDef, workspacePath: string, dryRun: boolean): Promise<void>;
|
|
13
14
|
installSkill(config: OpenClawSection, workspace: string, agent: string, skill: string, dryRun: boolean): Promise<void>;
|
|
@@ -14,6 +14,7 @@ const DEFAULT_COMMANDS = {
|
|
|
14
14
|
factory_reset: "${bin} reset --scope full --yes --non-interactive",
|
|
15
15
|
start_gateway: "${bin} gateway start",
|
|
16
16
|
enable_plugin: "",
|
|
17
|
+
bind_channel_agent: "",
|
|
17
18
|
login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
|
|
18
19
|
create_agent: "${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json",
|
|
19
20
|
install_skill: "${bin} skills check",
|
|
@@ -21,14 +22,25 @@ const DEFAULT_COMMANDS = {
|
|
|
21
22
|
run_agent: "${bin} agent --local --agent ${agent} --message ${prompt_q} --json",
|
|
22
23
|
};
|
|
23
24
|
const SECRET_FLAG_RE = /(--[A-Za-z0-9-]*(?:api-key|token|password|secret)[A-Za-z0-9-]*\s+)(?:'[^']*'|"[^"]*"|\S+)/g;
|
|
25
|
+
const AUTH_CHOICE_TO_LLM_FLAG = {
|
|
26
|
+
"openai-api-key": "--openai-api-key",
|
|
27
|
+
"anthropic-api-key": "--anthropic-api-key",
|
|
28
|
+
"openrouter-api-key": "--openrouter-api-key",
|
|
29
|
+
"xai-api-key": "--xai-api-key",
|
|
30
|
+
"gemini-api-key": "--gemini-api-key",
|
|
31
|
+
"ai-gateway-api-key": "--ai-gateway-api-key",
|
|
32
|
+
"cloudflare-ai-gateway-api-key": "--cloudflare-ai-gateway-api-key",
|
|
33
|
+
};
|
|
34
|
+
const AUTH_CHOICE_TO_LLM_ENV = {
|
|
35
|
+
"openai-api-key": "OPENAI_API_KEY",
|
|
36
|
+
"anthropic-api-key": "ANTHROPIC_API_KEY",
|
|
37
|
+
"openrouter-api-key": "OPENROUTER_API_KEY",
|
|
38
|
+
"xai-api-key": "XAI_API_KEY",
|
|
39
|
+
"gemini-api-key": "GEMINI_API_KEY",
|
|
40
|
+
"ai-gateway-api-key": "AI_GATEWAY_API_KEY",
|
|
41
|
+
"cloudflare-ai-gateway-api-key": "CLOUDFLARE_AI_GATEWAY_API_KEY",
|
|
42
|
+
};
|
|
24
43
|
const BOOTSTRAP_STRING_FLAGS = [
|
|
25
|
-
["openai_api_key", "--openai-api-key"],
|
|
26
|
-
["anthropic_api_key", "--anthropic-api-key"],
|
|
27
|
-
["openrouter_api_key", "--openrouter-api-key"],
|
|
28
|
-
["xai_api_key", "--xai-api-key"],
|
|
29
|
-
["gemini_api_key", "--gemini-api-key"],
|
|
30
|
-
["ai_gateway_api_key", "--ai-gateway-api-key"],
|
|
31
|
-
["cloudflare_ai_gateway_api_key", "--cloudflare-ai-gateway-api-key"],
|
|
32
44
|
["cloudflare_ai_gateway_account_id", "--cloudflare-ai-gateway-account-id"],
|
|
33
45
|
["cloudflare_ai_gateway_gateway_id", "--cloudflare-ai-gateway-gateway-id"],
|
|
34
46
|
["token", "--token"],
|
|
@@ -187,6 +199,12 @@ function buildBootstrapCommand(bin, bootstrap, workspacePath) {
|
|
|
187
199
|
else if (cfg.install_daemon === false) {
|
|
188
200
|
flags.push("--no-install-daemon");
|
|
189
201
|
}
|
|
202
|
+
if (cfg.llm_api_key?.trim()) {
|
|
203
|
+
const llmFlag = AUTH_CHOICE_TO_LLM_FLAG[cfg.auth_choice ?? ""];
|
|
204
|
+
if (llmFlag) {
|
|
205
|
+
flags.push(`${llmFlag} ${shellQuote(cfg.llm_api_key)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
190
208
|
for (const [field, flag] of BOOTSTRAP_STRING_FLAGS) {
|
|
191
209
|
const value = cfg[field];
|
|
192
210
|
if (value && value.trim()) {
|
|
@@ -202,26 +220,47 @@ function bootstrapRuntimeEnv(bootstrap) {
|
|
|
202
220
|
return {};
|
|
203
221
|
}
|
|
204
222
|
const env = {};
|
|
205
|
-
if (bootstrap.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
env.OPENROUTER_API_KEY = bootstrap.openrouter_api_key;
|
|
211
|
-
if (bootstrap.xai_api_key)
|
|
212
|
-
env.XAI_API_KEY = bootstrap.xai_api_key;
|
|
213
|
-
if (bootstrap.gemini_api_key)
|
|
214
|
-
env.GEMINI_API_KEY = bootstrap.gemini_api_key;
|
|
215
|
-
if (bootstrap.ai_gateway_api_key)
|
|
216
|
-
env.AI_GATEWAY_API_KEY = bootstrap.ai_gateway_api_key;
|
|
217
|
-
if (bootstrap.cloudflare_ai_gateway_api_key) {
|
|
218
|
-
env.CLOUDFLARE_AI_GATEWAY_API_KEY = bootstrap.cloudflare_ai_gateway_api_key;
|
|
223
|
+
if (bootstrap.llm_api_key?.trim()) {
|
|
224
|
+
const envKey = AUTH_CHOICE_TO_LLM_ENV[bootstrap.auth_choice ?? ""];
|
|
225
|
+
if (envKey) {
|
|
226
|
+
env[envKey] = bootstrap.llm_api_key;
|
|
227
|
+
}
|
|
219
228
|
}
|
|
220
229
|
return env;
|
|
221
230
|
}
|
|
231
|
+
function isAccountLevelBinding(item, channel, account) {
|
|
232
|
+
const match = item.match;
|
|
233
|
+
if (!match || typeof match !== "object") {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (match.channel !== channel || match.accountId !== account) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return (match.peer === undefined
|
|
240
|
+
&& match.parentPeer === undefined
|
|
241
|
+
&& match.guildId === undefined
|
|
242
|
+
&& match.teamId === undefined
|
|
243
|
+
&& match.roles === undefined);
|
|
244
|
+
}
|
|
245
|
+
function parseBindingsJson(raw) {
|
|
246
|
+
if (!raw.trim()) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const parsed = JSON.parse(raw);
|
|
251
|
+
if (!Array.isArray(parsed)) {
|
|
252
|
+
throw new ClawChefError("openclaw config bindings is not an array");
|
|
253
|
+
}
|
|
254
|
+
return parsed;
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
258
|
+
throw new ClawChefError(`Failed to parse openclaw bindings JSON: ${message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
222
261
|
export class CommandOpenClawProvider {
|
|
223
262
|
stagedMessages = new Map();
|
|
224
|
-
async ensureVersion(config, dryRun, silent,
|
|
263
|
+
async ensureVersion(config, dryRun, silent, preserveExistingState) {
|
|
225
264
|
const bin = config.bin ?? "openclaw";
|
|
226
265
|
const installPolicy = config.install ?? "auto";
|
|
227
266
|
const useCmd = commandFor(config, "use_version", { bin, version: config.version });
|
|
@@ -272,7 +311,7 @@ export class CommandOpenClawProvider {
|
|
|
272
311
|
if (installedThisRun) {
|
|
273
312
|
throw new ClawChefError(`OpenClaw version mismatch after install: current ${currentVersion}, expected ${config.version}`);
|
|
274
313
|
}
|
|
275
|
-
if (
|
|
314
|
+
if (preserveExistingState) {
|
|
276
315
|
return { installedThisRun: false };
|
|
277
316
|
}
|
|
278
317
|
const choice = await chooseVersionMismatchAction(currentVersion, config.version, silent);
|
|
@@ -400,6 +439,53 @@ export class CommandOpenClawProvider {
|
|
|
400
439
|
const cmd = `${bin} channels add ${flags.join(" ")}`;
|
|
401
440
|
await runShell(cmd, dryRun);
|
|
402
441
|
}
|
|
442
|
+
async bindChannelAgent(config, channel, agent, dryRun) {
|
|
443
|
+
const account = channel.account?.trim();
|
|
444
|
+
if (!account) {
|
|
445
|
+
throw new ClawChefError(`Channel ${channel.channel} requires account for agent binding`);
|
|
446
|
+
}
|
|
447
|
+
const bin = config.bin ?? "openclaw";
|
|
448
|
+
const customTemplate = config.commands?.bind_channel_agent;
|
|
449
|
+
if (customTemplate?.trim()) {
|
|
450
|
+
const customCmd = fillTemplate(customTemplate, {
|
|
451
|
+
bin,
|
|
452
|
+
version: config.version,
|
|
453
|
+
channel: channel.channel,
|
|
454
|
+
channel_q: shellQuote(channel.channel),
|
|
455
|
+
account,
|
|
456
|
+
account_q: shellQuote(account),
|
|
457
|
+
agent,
|
|
458
|
+
agent_q: shellQuote(agent),
|
|
459
|
+
});
|
|
460
|
+
if (customCmd.trim()) {
|
|
461
|
+
await runShell(customCmd, dryRun);
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (dryRun) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const getCmd = `${bin} config get bindings --json 2>/dev/null || printf '[]'`;
|
|
469
|
+
const rawBindings = await runShell(getCmd, false);
|
|
470
|
+
const bindings = parseBindingsJson(rawBindings);
|
|
471
|
+
const nextBinding = {
|
|
472
|
+
agentId: agent,
|
|
473
|
+
match: {
|
|
474
|
+
channel: channel.channel,
|
|
475
|
+
accountId: account,
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
const index = bindings.findIndex((item) => isAccountLevelBinding(item, channel.channel, account));
|
|
479
|
+
if (index >= 0) {
|
|
480
|
+
bindings[index] = nextBinding;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
bindings.push(nextBinding);
|
|
484
|
+
}
|
|
485
|
+
const json = JSON.stringify(bindings);
|
|
486
|
+
const setCmd = `${bin} config set bindings ${shellQuote(json)} --json`;
|
|
487
|
+
await runShell(setCmd, false);
|
|
488
|
+
}
|
|
403
489
|
async loginChannel(config, channel, dryRun) {
|
|
404
490
|
if (!channel.login) {
|
|
405
491
|
return;
|
|
@@ -2,12 +2,13 @@ import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../
|
|
|
2
2
|
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
3
3
|
export declare class MockOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private state;
|
|
5
|
-
ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean,
|
|
5
|
+
ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean, _preserveExistingState: boolean): Promise<EnsureVersionResult>;
|
|
6
6
|
installPlugin(_config: OpenClawSection, _pluginSpec: string, _dryRun: boolean): Promise<void>;
|
|
7
7
|
factoryReset(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
|
|
8
8
|
startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
|
|
9
9
|
createWorkspace(_config: OpenClawSection, workspace: ResolvedWorkspaceDef, _dryRun: boolean): Promise<void>;
|
|
10
10
|
configureChannel(_config: OpenClawSection, channel: ChannelDef, _dryRun: boolean): Promise<void>;
|
|
11
|
+
bindChannelAgent(_config: OpenClawSection, _channel: ChannelDef, _agent: string, _dryRun: boolean): Promise<void>;
|
|
11
12
|
loginChannel(_config: OpenClawSection, _channel: ChannelDef, _dryRun: boolean): Promise<void>;
|
|
12
13
|
createAgent(_config: OpenClawSection, agent: AgentDef, _workspacePath: string, _dryRun: boolean): Promise<void>;
|
|
13
14
|
installSkill(_config: OpenClawSection, workspace: string, agent: string, skill: string, _dryRun: boolean): Promise<void>;
|
|
@@ -7,7 +7,7 @@ export class MockOpenClawProvider {
|
|
|
7
7
|
skills: new Set(),
|
|
8
8
|
messages: new Map(),
|
|
9
9
|
};
|
|
10
|
-
async ensureVersion(config, _dryRun, _silent,
|
|
10
|
+
async ensureVersion(config, _dryRun, _silent, _preserveExistingState) {
|
|
11
11
|
const policy = config.install ?? "auto";
|
|
12
12
|
const installed = this.state.installedVersions.has(config.version);
|
|
13
13
|
let installedThisRun = false;
|
|
@@ -44,6 +44,9 @@ export class MockOpenClawProvider {
|
|
|
44
44
|
async configureChannel(_config, channel, _dryRun) {
|
|
45
45
|
this.state.channels.add(`${channel.channel}::${channel.account ?? "default"}`);
|
|
46
46
|
}
|
|
47
|
+
async bindChannelAgent(_config, _channel, _agent, _dryRun) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
47
50
|
async loginChannel(_config, _channel, _dryRun) {
|
|
48
51
|
return;
|
|
49
52
|
}
|