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 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
- - Always runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
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 --keep-openclaw-state
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
- CLAWCHEF_VAR_OPENAI_API_KEY=sk-... clawchef cook recipes/openclaw-from-zero.yaml --verbose
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
- openai_api_key: process.env.OPENAI_API_KEY ?? "",
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`, `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`
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 provider keys, `clawchef` also injects them into runtime env for `openclaw agent --local`.
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 `files[]` can override copied asset files.
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 openai_api_key=...`
437
- - `CLAWCHEF_VAR_OPENAI_API_KEY=...`
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("0.1.6");
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("--keep-openclaw-state", "Preserve existing OpenClaw state (skip factory reset)", false)
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, keepOpenClawState: boolean): Promise<EnsureVersionResult>;
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.openai_api_key)
206
- env.OPENAI_API_KEY = bootstrap.openai_api_key;
207
- if (bootstrap.anthropic_api_key)
208
- env.ANTHROPIC_API_KEY = bootstrap.anthropic_api_key;
209
- if (bootstrap.openrouter_api_key)
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, keepOpenClawState) {
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 (keepOpenClawState) {
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, _keepOpenClawState: boolean): Promise<EnsureVersionResult>;
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, _keepOpenClawState) {
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
  }