clawchef 0.1.8 → 0.1.9

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 CHANGED
@@ -15,10 +15,11 @@ Recipe-driven OpenClaw environment orchestrator.
15
15
  - Supports scoped execution via `--scope full|files|workspace`.
16
16
  - `full` scope runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
17
17
  - If `openclaw` is missing, auto-installs the recipe version and skips factory reset.
18
- - Starts OpenClaw gateway service after each recipe execution.
18
+ - Starts OpenClaw gateway after each recipe execution based on `--gateway-mode`.
19
19
  - Creates workspaces and agents (default workspace path: `~/.openclaw/workspace-<workspace-name>`).
20
20
  - Supports workspace-level assets copy via `workspaces[].assets`.
21
21
  - Materializes files into target workspaces.
22
+ - Supports OpenClaw root-level assets/files via `openclaw.root` (default root path: `~/.openclaw`).
22
23
  - Installs skills.
23
24
  - Supports plugin preinstall via `openclaw.plugins[]` and runtime `--plugin` flags.
24
25
  - Configures channels with `openclaw channels add`.
@@ -90,6 +91,14 @@ Skip reset confirmation prompt:
90
91
  clawchef cook recipes/sample.yaml -s
91
92
  ```
92
93
 
94
+ Control gateway startup mode:
95
+
96
+ ```bash
97
+ clawchef cook recipes/sample.yaml --gateway-mode service
98
+ clawchef cook recipes/sample.yaml --gateway-mode run
99
+ clawchef cook recipes/sample.yaml --gateway-mode none
100
+ ```
101
+
93
102
  Warning: `-s/--silent` suppresses the factory-reset confirmation and auto-chooses force reinstall on version mismatch.
94
103
  Use it only in CI/non-interactive flows where destructive reset behavior is expected.
95
104
 
@@ -204,6 +213,7 @@ await scaffold("./my-recipe-project", {
204
213
  - `plugins`: plugin npm specs to preinstall for this run (`string[]`)
205
214
  - `scope`: `full | files | workspace` (default: `full`)
206
215
  - `workspaceName`: required when `scope: "workspace"`
216
+ - `gatewayMode`: `service | run | none` (default: `service`)
207
217
  - `provider`: `command | remote | mock`
208
218
  - `remote`: remote provider config (same fields as CLI remote flags)
209
219
  - `envFile`: custom env file path/URL; when set, default cwd `.env` loading is skipped
@@ -307,6 +317,8 @@ Supported operation values sent by clawchef:
307
317
  - `configure_channel`, `bind_channel_agent`, `login_channel`
308
318
  - `run_agent`
309
319
 
320
+ For `start_gateway`, clawchef sends `{ mode: "service" | "run" }` in payload when gateway mode is enabled.
321
+
310
322
  For `run_agent`, clawchef expects `output` in response for assertions.
311
323
 
312
324
  `command` provider now defaults to the current OpenClaw CLI shape (`openclaw 2026.x`), including:
@@ -352,6 +364,7 @@ For `command` provider, default command templates are:
352
364
  - `install_plugin`: `${bin} plugins install ${plugin_spec_q}`
353
365
  - `factory_reset`: `${bin} reset --scope full --yes --non-interactive`
354
366
  - `start_gateway`: `${bin} gateway start`
367
+ - `run_gateway`: `${bin} gateway run`
355
368
  - `bind_channel_agent`: built-in `openclaw config get/set bindings` upsert (override with `openclaw.commands.bind_channel_agent`)
356
369
  - `login_channel`: `${bin} channels login --channel ${channel_q}${account_arg}`
357
370
  - `create_workspace`: generated from `openclaw.bootstrap` (override with `openclaw.commands.create_workspace`)
@@ -426,9 +439,31 @@ workspaces:
426
439
  assets: "./meetingbot-assets"
427
440
  ```
428
441
 
442
+ ## OpenClaw root files
443
+
444
+ - `openclaw.root.path` is optional.
445
+ - If omitted, clawchef uses `~/.openclaw`.
446
+ - `openclaw.root.assets` is optional and recursively copied into the root directory.
447
+ - `openclaw.root.files[]` runs after assets copy, so explicit file entries can override copied assets.
448
+ - `openclaw.root.assets` is resolved relative to the recipe file path (unless absolute path is given).
449
+ - Direct URL recipes do not support `openclaw.root.assets` (assets must resolve to a local directory).
450
+ - `openclaw.root` currently supports `command` and `mock` providers; `remote` provider is not supported.
451
+
452
+ Example:
453
+
454
+ ```yaml
455
+ openclaw:
456
+ version: "2026.2.9"
457
+ root:
458
+ assets: "./openclaw-root-assets"
459
+ files:
460
+ - path: "AGENTS.md"
461
+ content_from: "./snippets/agents-template.md"
462
+ ```
463
+
429
464
  ## File content references
430
465
 
431
- In `workspaces[].files[]`, set exactly one of:
466
+ In `workspaces[].files[]` and `openclaw.root.files[]`, set exactly one of:
432
467
 
433
468
  - `content`: inline text in recipe
434
469
  - `content_from`: load text from another file/URL (loaded content supports `${var}` template rendering)
package/dist/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OpenClawProvider, OpenClawRemoteConfig, RunScope } from "./types.js";
1
+ import type { GatewayMode, 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>;
@@ -9,6 +9,7 @@ export interface CookOptions {
9
9
  silent?: boolean;
10
10
  scope?: RunScope;
11
11
  workspaceName?: string;
12
+ gatewayMode?: GatewayMode;
12
13
  provider?: OpenClawProvider;
13
14
  remote?: Partial<OpenClawRemoteConfig>;
14
15
  envFile?: string;
package/dist/api.js CHANGED
@@ -21,6 +21,7 @@ function normalizeCookOptions(options) {
21
21
  plugins,
22
22
  scope,
23
23
  workspaceName,
24
+ gatewayMode: options.gatewayMode ?? "service",
24
25
  dryRun: Boolean(options.dryRun),
25
26
  allowMissing: Boolean(options.allowMissing),
26
27
  verbose: Boolean(options.verbose),
package/dist/cli.js CHANGED
@@ -61,6 +61,12 @@ function parseScope(value) {
61
61
  }
62
62
  throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, files, or workspace`);
63
63
  }
64
+ function parseGatewayMode(value) {
65
+ if (value === "service" || value === "run" || value === "none") {
66
+ return value;
67
+ }
68
+ throw new ClawChefError(`Invalid --gateway-mode value: ${value}. Expected service, run, or none`);
69
+ }
64
70
  function parseOptionalInt(value, fieldName) {
65
71
  if (value === undefined) {
66
72
  return undefined;
@@ -101,6 +107,7 @@ export function buildCli() {
101
107
  .option("-s, --silent", "Skip reset confirmation prompt", false)
102
108
  .option("--scope <scope>", "Run scope: full | files | workspace", "full")
103
109
  .option("--workspace <name>", "Workspace name (required when --scope workspace)")
110
+ .option("--gateway-mode <mode>", "Gateway mode: service | run | none", "service")
104
111
  .option("--dotenv-ref <path-or-url>", "Load env vars from local file or HTTP URL")
105
112
  .option("--provider <provider>", "Execution provider: command | remote | mock")
106
113
  .option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p) => p.concat([v]), [])
@@ -119,6 +126,7 @@ export function buildCli() {
119
126
  }
120
127
  const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
121
128
  const scope = parseScope(String(opts.scope ?? "full"));
129
+ const gatewayMode = parseGatewayMode(String(opts.gatewayMode ?? "service"));
122
130
  const workspaceName = opts.workspace?.trim() ? String(opts.workspace).trim() : undefined;
123
131
  if (scope === "workspace" && !workspaceName) {
124
132
  throw new ClawChefError("--scope workspace requires --workspace <name>");
@@ -131,6 +139,7 @@ export function buildCli() {
131
139
  plugins: parsePluginFlags(opts.plugin),
132
140
  scope,
133
141
  workspaceName,
142
+ gatewayMode,
134
143
  dryRun: Boolean(opts.dryRun),
135
144
  allowMissing: Boolean(opts.allowMissing),
136
145
  verbose: Boolean(opts.verbose),
@@ -1,11 +1,11 @@
1
- import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../types.js";
1
+ import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection } from "../types.js";
2
2
  import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
3
3
  export declare class CommandOpenClawProvider implements OpenClawProvider {
4
4
  private readonly stagedMessages;
5
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
- startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
8
+ startGateway(config: OpenClawSection, mode: GatewayMode, 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
11
  bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
@@ -13,6 +13,7 @@ const DEFAULT_COMMANDS = {
13
13
  install_plugin: "${bin} plugins install ${plugin_spec_q}",
14
14
  factory_reset: "${bin} reset --scope full --yes --non-interactive",
15
15
  start_gateway: "${bin} gateway start",
16
+ run_gateway: "${bin} gateway run",
16
17
  enable_plugin: "",
17
18
  bind_channel_agent: "",
18
19
  login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
@@ -362,9 +363,13 @@ export class CommandOpenClawProvider {
362
363
  }
363
364
  await runShell(cmd, dryRun);
364
365
  }
365
- async startGateway(config, dryRun) {
366
+ async startGateway(config, mode, dryRun) {
367
+ if (mode === "none") {
368
+ return;
369
+ }
366
370
  const bin = config.bin ?? "openclaw";
367
- const startCmd = commandFor(config, "start_gateway", { bin, version: config.version });
371
+ const key = mode === "run" ? "run_gateway" : "start_gateway";
372
+ const startCmd = commandFor(config, key, { bin, version: config.version });
368
373
  if (!startCmd.trim()) {
369
374
  return;
370
375
  }
@@ -1,11 +1,11 @@
1
- import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../types.js";
1
+ import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection } from "../types.js";
2
2
  import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
3
3
  export declare class MockOpenClawProvider implements OpenClawProvider {
4
4
  private state;
5
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
- startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
8
+ startGateway(_config: OpenClawSection, _mode: GatewayMode, _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
11
  bindChannelAgent(_config: OpenClawSection, _channel: ChannelDef, _agent: string, _dryRun: boolean): Promise<void>;
@@ -35,7 +35,7 @@ export class MockOpenClawProvider {
35
35
  this.state.skills.clear();
36
36
  this.state.messages.clear();
37
37
  }
38
- async startGateway(_config, _dryRun) {
38
+ async startGateway(_config, _mode, _dryRun) {
39
39
  return;
40
40
  }
41
41
  async createWorkspace(_config, workspace, _dryRun) {
@@ -1,4 +1,4 @@
1
- import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection, WorkspaceDef } from "../types.js";
1
+ import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection, WorkspaceDef } from "../types.js";
2
2
  export type ResolvedWorkspaceDef = WorkspaceDef & {
3
3
  path: string;
4
4
  };
@@ -9,7 +9,7 @@ export interface OpenClawProvider {
9
9
  ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean, preserveExistingState: boolean): Promise<EnsureVersionResult>;
10
10
  installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
11
11
  factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
12
- startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
12
+ startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
13
13
  createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
14
14
  configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
15
15
  bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
@@ -1,4 +1,4 @@
1
- import type { AgentDef, ChannelDef, ConversationDef, OpenClawRemoteConfig, OpenClawSection } from "../types.js";
1
+ import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawRemoteConfig, OpenClawSection } from "../types.js";
2
2
  import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
3
3
  export declare class RemoteOpenClawProvider implements OpenClawProvider {
4
4
  private readonly stagedMessages;
@@ -8,7 +8,7 @@ export declare class RemoteOpenClawProvider implements OpenClawProvider {
8
8
  ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean, _preserveExistingState: boolean): Promise<EnsureVersionResult>;
9
9
  installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
10
10
  factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
11
- startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
11
+ startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
12
12
  createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
13
13
  configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
14
14
  bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
@@ -110,8 +110,11 @@ export class RemoteOpenClawProvider {
110
110
  async factoryReset(config, dryRun) {
111
111
  await this.perform(config, "factory_reset", undefined, dryRun);
112
112
  }
113
- async startGateway(config, dryRun) {
114
- await this.perform(config, "start_gateway", undefined, dryRun);
113
+ async startGateway(config, mode, dryRun) {
114
+ if (mode === "none") {
115
+ return;
116
+ }
117
+ await this.perform(config, "start_gateway", { mode }, dryRun);
115
118
  }
116
119
  async createWorkspace(config, workspace, dryRun) {
117
120
  await this.perform(config, "create_workspace", { workspace }, dryRun);
@@ -55,6 +55,18 @@ function resolveWorkspacePath(recipeOrigin, name, configuredPath) {
55
55
  const workspaceName = trimmedName.startsWith("workspace-") ? trimmedName : `workspace-${trimmedName}`;
56
56
  return path.join(homedir(), ".openclaw", workspaceName);
57
57
  }
58
+ function resolveOpenClawRootPath(recipeOrigin, configuredPath) {
59
+ if (configuredPath?.trim()) {
60
+ if (path.isAbsolute(configuredPath)) {
61
+ return configuredPath;
62
+ }
63
+ if (recipeOrigin.kind === "local") {
64
+ return path.resolve(recipeOrigin.recipeDir, configuredPath);
65
+ }
66
+ return path.resolve(configuredPath);
67
+ }
68
+ return path.join(homedir(), ".openclaw");
69
+ }
58
70
  function isHttpUrl(value) {
59
71
  try {
60
72
  const url = new URL(value);
@@ -164,6 +176,72 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
164
176
  await provider.installPlugin(recipe.openclaw, pluginSpec, options.dryRun);
165
177
  logger.info(`Plugin preinstalled: ${pluginSpec}`);
166
178
  }
179
+ const root = recipe.openclaw.root;
180
+ if (root && (root.assets?.trim() || (root.files?.length ?? 0) > 0)) {
181
+ if (remoteMode) {
182
+ throw new ClawChefError("openclaw.root assets/files are not supported with --provider remote");
183
+ }
184
+ const openclawRootPath = resolveOpenClawRootPath(recipeOrigin, root.path);
185
+ if (!options.dryRun) {
186
+ await mkdir(openclawRootPath, { recursive: true });
187
+ }
188
+ if (root.assets?.trim()) {
189
+ const resolvedAssets = resolveFileRef(recipeOrigin, root.assets);
190
+ if (resolvedAssets.kind !== "local") {
191
+ throw new ClawChefError(`openclaw.root.assets must resolve to a local directory: ${root.assets}. Direct URL recipes cannot use openclaw.root.assets.`);
192
+ }
193
+ let assetDirStat;
194
+ try {
195
+ assetDirStat = await stat(resolvedAssets.value);
196
+ }
197
+ catch (err) {
198
+ const message = err instanceof Error ? err.message : String(err);
199
+ throw new ClawChefError(`openclaw.root.assets path is not accessible: ${resolvedAssets.value} (${message})`);
200
+ }
201
+ if (!assetDirStat.isDirectory()) {
202
+ throw new ClawChefError(`openclaw.root.assets must be a directory: ${resolvedAssets.value}`);
203
+ }
204
+ const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
205
+ for (const assetFile of assetFiles) {
206
+ const target = path.resolve(openclawRootPath, assetFile.relativePath);
207
+ if (!options.dryRun) {
208
+ await mkdir(path.dirname(target), { recursive: true });
209
+ await copyFile(assetFile.absolutePath, target);
210
+ }
211
+ logger.info(`OpenClaw root asset copied: ${assetFile.relativePath}`);
212
+ }
213
+ }
214
+ for (const file of root.files ?? []) {
215
+ const target = path.resolve(openclawRootPath, file.path);
216
+ const targetDir = path.dirname(target);
217
+ if (!options.dryRun) {
218
+ await mkdir(targetDir, { recursive: true });
219
+ const alreadyExists = await exists(target);
220
+ if (alreadyExists && file.overwrite === false) {
221
+ logger.warn(`Skipping existing file: ${target}`);
222
+ }
223
+ else if (file.content !== undefined) {
224
+ await writeFile(target, file.content, "utf8");
225
+ }
226
+ else if (file.content_from) {
227
+ const rawContent = await readTextFromRef(recipeOrigin, file.content_from);
228
+ const content = renderTemplateString(rawContent, options.vars, options.allowMissing);
229
+ await writeFile(target, content, "utf8");
230
+ }
231
+ else if (file.source) {
232
+ const resolved = resolveFileRef(recipeOrigin, file.source);
233
+ if (resolved.kind === "local") {
234
+ await copyFile(resolved.value, target);
235
+ }
236
+ else {
237
+ const content = await readBinaryFromRef(recipeOrigin, file.source);
238
+ await writeFile(target, content);
239
+ }
240
+ }
241
+ }
242
+ logger.info(`OpenClaw root file materialized: ${file.path}`);
243
+ }
244
+ }
167
245
  for (const ws of recipe.workspaces ?? []) {
168
246
  const absPath = resolveWorkspacePath(recipeOrigin, ws.name, ws.path);
169
247
  workspacePaths.set(ws.name, absPath);
@@ -323,8 +401,13 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
323
401
  }
324
402
  logger.info(`Preset messages sent: ${conv.workspace}/${conv.agent}`);
325
403
  }
326
- await provider.startGateway(recipe.openclaw, options.dryRun);
327
- logger.info("Gateway started");
404
+ await provider.startGateway(recipe.openclaw, options.gatewayMode, options.dryRun);
405
+ if (options.gatewayMode === "none") {
406
+ logger.info("Gateway start skipped by gateway mode: none");
407
+ }
408
+ else {
409
+ logger.info(`Gateway started (${options.gatewayMode})`);
410
+ }
328
411
  for (const channel of recipe.channels ?? []) {
329
412
  const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
330
413
  ? { ...channel, account: channel.agent.trim() }
package/dist/recipe.js CHANGED
@@ -146,6 +146,18 @@ function filterRecipeByWorkspaceName(recipe, workspaceName) {
146
146
  function semanticValidate(recipe) {
147
147
  const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
148
148
  const agentNameCounts = new Map();
149
+ const root = recipe.openclaw.root;
150
+ if (root?.path !== undefined && !root.path.trim()) {
151
+ throw new ClawChefError("openclaw.root.path cannot be empty");
152
+ }
153
+ if (root?.assets !== undefined && !root.assets.trim()) {
154
+ throw new ClawChefError("openclaw.root.assets cannot be empty");
155
+ }
156
+ for (const file of root?.files ?? []) {
157
+ if (!file.path.trim()) {
158
+ throw new ClawChefError("openclaw.root.files[] has file with empty path");
159
+ }
160
+ }
149
161
  for (const workspace of recipe.workspaces ?? []) {
150
162
  if (workspace.assets !== undefined && !workspace.assets.trim()) {
151
163
  throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
package/dist/schema.d.ts CHANGED
@@ -20,6 +20,61 @@ export declare const recipeSchema: z.ZodObject<{
20
20
  version: z.ZodString;
21
21
  install: z.ZodOptional<z.ZodEnum<["auto", "always", "never"]>>;
22
22
  plugins: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
23
+ root: z.ZodOptional<z.ZodObject<{
24
+ path: z.ZodOptional<z.ZodString>;
25
+ assets: z.ZodOptional<z.ZodString>;
26
+ files: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
27
+ path: z.ZodString;
28
+ content: z.ZodOptional<z.ZodString>;
29
+ content_from: z.ZodOptional<z.ZodString>;
30
+ source: z.ZodOptional<z.ZodString>;
31
+ overwrite: z.ZodOptional<z.ZodBoolean>;
32
+ }, "strict", z.ZodTypeAny, {
33
+ path: string;
34
+ content?: string | undefined;
35
+ overwrite?: boolean | undefined;
36
+ content_from?: string | undefined;
37
+ source?: string | undefined;
38
+ }, {
39
+ path: string;
40
+ content?: string | undefined;
41
+ overwrite?: boolean | undefined;
42
+ content_from?: string | undefined;
43
+ source?: string | undefined;
44
+ }>, {
45
+ path: string;
46
+ content?: string | undefined;
47
+ overwrite?: boolean | undefined;
48
+ content_from?: string | undefined;
49
+ source?: string | undefined;
50
+ }, {
51
+ path: string;
52
+ content?: string | undefined;
53
+ overwrite?: boolean | undefined;
54
+ content_from?: string | undefined;
55
+ source?: string | undefined;
56
+ }>, "many">>;
57
+ }, "strict", z.ZodTypeAny, {
58
+ path?: string | undefined;
59
+ files?: {
60
+ path: string;
61
+ content?: string | undefined;
62
+ overwrite?: boolean | undefined;
63
+ content_from?: string | undefined;
64
+ source?: string | undefined;
65
+ }[] | undefined;
66
+ assets?: string | undefined;
67
+ }, {
68
+ path?: string | undefined;
69
+ files?: {
70
+ path: string;
71
+ content?: string | undefined;
72
+ overwrite?: boolean | undefined;
73
+ content_from?: string | undefined;
74
+ source?: string | undefined;
75
+ }[] | undefined;
76
+ assets?: string | undefined;
77
+ }>>;
23
78
  bootstrap: z.ZodOptional<z.ZodObject<{
24
79
  non_interactive: z.ZodOptional<z.ZodBoolean>;
25
80
  accept_risk: z.ZodOptional<z.ZodBoolean>;
@@ -48,9 +103,9 @@ export declare const recipeSchema: z.ZodObject<{
48
103
  token_provider?: string | undefined;
49
104
  token_profile_id?: string | undefined;
50
105
  llm_api_key?: string | undefined;
106
+ mode?: "remote" | "local" | undefined;
51
107
  non_interactive?: boolean | undefined;
52
108
  accept_risk?: boolean | undefined;
53
- mode?: "remote" | "local" | undefined;
54
109
  flow?: "quickstart" | "advanced" | "manual" | undefined;
55
110
  auth_choice?: string | undefined;
56
111
  reset?: boolean | undefined;
@@ -68,9 +123,9 @@ export declare const recipeSchema: z.ZodObject<{
68
123
  token_provider?: string | undefined;
69
124
  token_profile_id?: string | undefined;
70
125
  llm_api_key?: string | undefined;
126
+ mode?: "remote" | "local" | undefined;
71
127
  non_interactive?: boolean | undefined;
72
128
  accept_risk?: boolean | undefined;
73
- mode?: "remote" | "local" | undefined;
74
129
  flow?: "quickstart" | "advanced" | "manual" | undefined;
75
130
  auth_choice?: string | undefined;
76
131
  reset?: boolean | undefined;
@@ -88,6 +143,7 @@ export declare const recipeSchema: z.ZodObject<{
88
143
  install_plugin: z.ZodOptional<z.ZodString>;
89
144
  factory_reset: z.ZodOptional<z.ZodString>;
90
145
  start_gateway: z.ZodOptional<z.ZodString>;
146
+ run_gateway: z.ZodOptional<z.ZodString>;
91
147
  enable_plugin: z.ZodOptional<z.ZodString>;
92
148
  bind_channel_agent: z.ZodOptional<z.ZodString>;
93
149
  login_channel: z.ZodOptional<z.ZodString>;
@@ -103,6 +159,7 @@ export declare const recipeSchema: z.ZodObject<{
103
159
  install_plugin?: string | undefined;
104
160
  factory_reset?: string | undefined;
105
161
  start_gateway?: string | undefined;
162
+ run_gateway?: string | undefined;
106
163
  enable_plugin?: string | undefined;
107
164
  bind_channel_agent?: string | undefined;
108
165
  login_channel?: string | undefined;
@@ -118,6 +175,7 @@ export declare const recipeSchema: z.ZodObject<{
118
175
  install_plugin?: string | undefined;
119
176
  factory_reset?: string | undefined;
120
177
  start_gateway?: string | undefined;
178
+ run_gateway?: string | undefined;
121
179
  enable_plugin?: string | undefined;
122
180
  bind_channel_agent?: string | undefined;
123
181
  login_channel?: string | undefined;
@@ -132,6 +190,17 @@ export declare const recipeSchema: z.ZodObject<{
132
190
  bin?: string | undefined;
133
191
  install?: "auto" | "always" | "never" | undefined;
134
192
  plugins?: string[] | undefined;
193
+ root?: {
194
+ path?: string | undefined;
195
+ files?: {
196
+ path: string;
197
+ content?: string | undefined;
198
+ overwrite?: boolean | undefined;
199
+ content_from?: string | undefined;
200
+ source?: string | undefined;
201
+ }[] | undefined;
202
+ assets?: string | undefined;
203
+ } | undefined;
135
204
  bootstrap?: {
136
205
  workspace?: string | undefined;
137
206
  cloudflare_ai_gateway_account_id?: string | undefined;
@@ -140,9 +209,9 @@ export declare const recipeSchema: z.ZodObject<{
140
209
  token_provider?: string | undefined;
141
210
  token_profile_id?: string | undefined;
142
211
  llm_api_key?: string | undefined;
212
+ mode?: "remote" | "local" | undefined;
143
213
  non_interactive?: boolean | undefined;
144
214
  accept_risk?: boolean | undefined;
145
- mode?: "remote" | "local" | undefined;
146
215
  flow?: "quickstart" | "advanced" | "manual" | undefined;
147
216
  auth_choice?: string | undefined;
148
217
  reset?: boolean | undefined;
@@ -160,6 +229,7 @@ export declare const recipeSchema: z.ZodObject<{
160
229
  install_plugin?: string | undefined;
161
230
  factory_reset?: string | undefined;
162
231
  start_gateway?: string | undefined;
232
+ run_gateway?: string | undefined;
163
233
  enable_plugin?: string | undefined;
164
234
  bind_channel_agent?: string | undefined;
165
235
  login_channel?: string | undefined;
@@ -174,6 +244,17 @@ export declare const recipeSchema: z.ZodObject<{
174
244
  bin?: string | undefined;
175
245
  install?: "auto" | "always" | "never" | undefined;
176
246
  plugins?: string[] | undefined;
247
+ root?: {
248
+ path?: string | undefined;
249
+ files?: {
250
+ path: string;
251
+ content?: string | undefined;
252
+ overwrite?: boolean | undefined;
253
+ content_from?: string | undefined;
254
+ source?: string | undefined;
255
+ }[] | undefined;
256
+ assets?: string | undefined;
257
+ } | undefined;
177
258
  bootstrap?: {
178
259
  workspace?: string | undefined;
179
260
  cloudflare_ai_gateway_account_id?: string | undefined;
@@ -182,9 +263,9 @@ export declare const recipeSchema: z.ZodObject<{
182
263
  token_provider?: string | undefined;
183
264
  token_profile_id?: string | undefined;
184
265
  llm_api_key?: string | undefined;
266
+ mode?: "remote" | "local" | undefined;
185
267
  non_interactive?: boolean | undefined;
186
268
  accept_risk?: boolean | undefined;
187
- mode?: "remote" | "local" | undefined;
188
269
  flow?: "quickstart" | "advanced" | "manual" | undefined;
189
270
  auth_choice?: string | undefined;
190
271
  reset?: boolean | undefined;
@@ -202,6 +283,7 @@ export declare const recipeSchema: z.ZodObject<{
202
283
  install_plugin?: string | undefined;
203
284
  factory_reset?: string | undefined;
204
285
  start_gateway?: string | undefined;
286
+ run_gateway?: string | undefined;
205
287
  enable_plugin?: string | undefined;
206
288
  bind_channel_agent?: string | undefined;
207
289
  login_channel?: string | undefined;
@@ -416,6 +498,17 @@ export declare const recipeSchema: z.ZodObject<{
416
498
  bin?: string | undefined;
417
499
  install?: "auto" | "always" | "never" | undefined;
418
500
  plugins?: string[] | undefined;
501
+ root?: {
502
+ path?: string | undefined;
503
+ files?: {
504
+ path: string;
505
+ content?: string | undefined;
506
+ overwrite?: boolean | undefined;
507
+ content_from?: string | undefined;
508
+ source?: string | undefined;
509
+ }[] | undefined;
510
+ assets?: string | undefined;
511
+ } | undefined;
419
512
  bootstrap?: {
420
513
  workspace?: string | undefined;
421
514
  cloudflare_ai_gateway_account_id?: string | undefined;
@@ -424,9 +517,9 @@ export declare const recipeSchema: z.ZodObject<{
424
517
  token_provider?: string | undefined;
425
518
  token_profile_id?: string | undefined;
426
519
  llm_api_key?: string | undefined;
520
+ mode?: "remote" | "local" | undefined;
427
521
  non_interactive?: boolean | undefined;
428
522
  accept_risk?: boolean | undefined;
429
- mode?: "remote" | "local" | undefined;
430
523
  flow?: "quickstart" | "advanced" | "manual" | undefined;
431
524
  auth_choice?: string | undefined;
432
525
  reset?: boolean | undefined;
@@ -444,6 +537,7 @@ export declare const recipeSchema: z.ZodObject<{
444
537
  install_plugin?: string | undefined;
445
538
  factory_reset?: string | undefined;
446
539
  start_gateway?: string | undefined;
540
+ run_gateway?: string | undefined;
447
541
  enable_plugin?: string | undefined;
448
542
  bind_channel_agent?: string | undefined;
449
543
  login_channel?: string | undefined;
@@ -519,6 +613,17 @@ export declare const recipeSchema: z.ZodObject<{
519
613
  bin?: string | undefined;
520
614
  install?: "auto" | "always" | "never" | undefined;
521
615
  plugins?: string[] | undefined;
616
+ root?: {
617
+ path?: string | undefined;
618
+ files?: {
619
+ path: string;
620
+ content?: string | undefined;
621
+ overwrite?: boolean | undefined;
622
+ content_from?: string | undefined;
623
+ source?: string | undefined;
624
+ }[] | undefined;
625
+ assets?: string | undefined;
626
+ } | undefined;
522
627
  bootstrap?: {
523
628
  workspace?: string | undefined;
524
629
  cloudflare_ai_gateway_account_id?: string | undefined;
@@ -527,9 +632,9 @@ export declare const recipeSchema: z.ZodObject<{
527
632
  token_provider?: string | undefined;
528
633
  token_profile_id?: string | undefined;
529
634
  llm_api_key?: string | undefined;
635
+ mode?: "remote" | "local" | undefined;
530
636
  non_interactive?: boolean | undefined;
531
637
  accept_risk?: boolean | undefined;
532
- mode?: "remote" | "local" | undefined;
533
638
  flow?: "quickstart" | "advanced" | "manual" | undefined;
534
639
  auth_choice?: string | undefined;
535
640
  reset?: boolean | undefined;
@@ -547,6 +652,7 @@ export declare const recipeSchema: z.ZodObject<{
547
652
  install_plugin?: string | undefined;
548
653
  factory_reset?: string | undefined;
549
654
  start_gateway?: string | undefined;
655
+ run_gateway?: string | undefined;
550
656
  enable_plugin?: string | undefined;
551
657
  bind_channel_agent?: string | undefined;
552
658
  login_channel?: string | undefined;
package/dist/schema.js CHANGED
@@ -12,6 +12,7 @@ const openClawCommandsSchema = z
12
12
  install_plugin: z.string().optional(),
13
13
  factory_reset: z.string().optional(),
14
14
  start_gateway: z.string().optional(),
15
+ run_gateway: z.string().optional(),
15
16
  enable_plugin: z.string().optional(),
16
17
  bind_channel_agent: z.string().optional(),
17
18
  login_channel: z.string().optional(),
@@ -45,12 +46,32 @@ const openClawBootstrapSchema = z
45
46
  token_profile_id: z.string().optional(),
46
47
  })
47
48
  .strict();
49
+ const rootFileSchema = z
50
+ .object({
51
+ path: z.string().min(1),
52
+ content: z.string().optional(),
53
+ content_from: z.string().min(1).optional(),
54
+ source: z.string().optional(),
55
+ overwrite: z.boolean().optional(),
56
+ })
57
+ .strict()
58
+ .refine((v) => [v.content, v.content_from, v.source].filter((item) => item !== undefined).length === 1, {
59
+ message: "openclaw.root.files[] requires exactly one of content, content_from, or source",
60
+ });
61
+ const openClawRootSchema = z
62
+ .object({
63
+ path: z.string().min(1).optional(),
64
+ assets: z.string().min(1).optional(),
65
+ files: z.array(rootFileSchema).optional(),
66
+ })
67
+ .strict();
48
68
  const openClawSchema = z
49
69
  .object({
50
70
  bin: z.string().optional(),
51
71
  version: z.string(),
52
72
  install: z.enum(["auto", "always", "never"]).optional(),
53
73
  plugins: z.array(z.string().min(1)).optional(),
74
+ root: openClawRootSchema.optional(),
54
75
  bootstrap: openClawBootstrapSchema.optional(),
55
76
  commands: openClawCommandsSchema.optional(),
56
77
  })
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type InstallPolicy = "auto" | "always" | "never";
2
2
  export type OpenClawProvider = "command" | "mock" | "remote";
3
3
  export type RunScope = "full" | "files" | "workspace";
4
+ export type GatewayMode = "service" | "run" | "none";
4
5
  export interface OpenClawRemoteConfig {
5
6
  base_url: string;
6
7
  api_key?: string;
@@ -21,6 +22,7 @@ export interface OpenClawCommandOverrides {
21
22
  install_plugin?: string;
22
23
  factory_reset?: string;
23
24
  start_gateway?: string;
25
+ run_gateway?: string;
24
26
  enable_plugin?: string;
25
27
  bind_channel_agent?: string;
26
28
  login_channel?: string;
@@ -56,9 +58,15 @@ export interface OpenClawSection {
56
58
  version: string;
57
59
  install?: InstallPolicy;
58
60
  plugins?: string[];
61
+ root?: OpenClawRootDef;
59
62
  bootstrap?: OpenClawBootstrap;
60
63
  commands?: OpenClawCommandOverrides;
61
64
  }
65
+ export interface OpenClawRootDef {
66
+ path?: string;
67
+ assets?: string;
68
+ files?: WorkspaceFileDef[];
69
+ }
62
70
  export interface WorkspaceDef {
63
71
  name: string;
64
72
  path?: string;
@@ -129,6 +137,7 @@ export interface RunOptions {
129
137
  plugins: string[];
130
138
  scope: RunScope;
131
139
  workspaceName?: string;
140
+ gatewayMode: GatewayMode;
132
141
  dryRun: boolean;
133
142
  allowMissing: boolean;
134
143
  verbose: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawchef",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Recipe-driven OpenClaw environment orchestrator",
5
5
  "homepage": "https://renorzr.github.io/clawchef",
6
6
  "repository": {
@@ -12,6 +12,13 @@ params:
12
12
  openclaw:
13
13
  version: "${openclaw_version}"
14
14
  install: "auto"
15
+ root:
16
+ files:
17
+ - path: "AGENTS.md"
18
+ overwrite: true
19
+ content: |
20
+ # OpenClaw Root
21
+ Project: ${project_name}
15
22
 
16
23
  workspaces:
17
24
  - name: "${project_name}"
package/src/api.ts CHANGED
@@ -6,7 +6,7 @@ import { runRecipe } from "./orchestrator.js";
6
6
  import { loadRecipe, loadRecipeText } from "./recipe.js";
7
7
  import { scaffoldProject } from "./scaffold.js";
8
8
  import { recipeSchema } from "./schema.js";
9
- import type { OpenClawProvider, OpenClawRemoteConfig, RunOptions, RunScope } from "./types.js";
9
+ import type { GatewayMode, OpenClawProvider, OpenClawRemoteConfig, RunOptions, RunScope } from "./types.js";
10
10
  import type { ScaffoldOptions, ScaffoldResult } from "./scaffold.js";
11
11
 
12
12
  export interface CookOptions {
@@ -18,6 +18,7 @@ export interface CookOptions {
18
18
  silent?: boolean;
19
19
  scope?: RunScope;
20
20
  workspaceName?: string;
21
+ gatewayMode?: GatewayMode;
21
22
  provider?: OpenClawProvider;
22
23
  remote?: Partial<OpenClawRemoteConfig>;
23
24
  envFile?: string;
@@ -39,6 +40,7 @@ function normalizeCookOptions(options: CookOptions): RunOptions {
39
40
  plugins,
40
41
  scope,
41
42
  workspaceName,
43
+ gatewayMode: options.gatewayMode ?? "service",
42
44
  dryRun: Boolean(options.dryRun),
43
45
  allowMissing: Boolean(options.allowMissing),
44
46
  verbose: Boolean(options.verbose),
package/src/cli.ts CHANGED
@@ -6,7 +6,7 @@ import { runRecipe } from "./orchestrator.js";
6
6
  import { loadRecipe, loadRecipeText } from "./recipe.js";
7
7
  import { recipeSchema } from "./schema.js";
8
8
  import { scaffoldProject } from "./scaffold.js";
9
- import type { RunOptions, RunScope } from "./types.js";
9
+ import type { GatewayMode, RunOptions, RunScope } from "./types.js";
10
10
  import YAML from "js-yaml";
11
11
  import path from "node:path";
12
12
  import { readFileSync } from "node:fs";
@@ -68,6 +68,13 @@ function parseScope(value: string): RunScope {
68
68
  throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, files, or workspace`);
69
69
  }
70
70
 
71
+ function parseGatewayMode(value: string): GatewayMode {
72
+ if (value === "service" || value === "run" || value === "none") {
73
+ return value;
74
+ }
75
+ throw new ClawChefError(`Invalid --gateway-mode value: ${value}. Expected service, run, or none`);
76
+ }
77
+
71
78
  function parseOptionalInt(value: string | undefined, fieldName: string): number | undefined {
72
79
  if (value === undefined) {
73
80
  return undefined;
@@ -112,6 +119,7 @@ export function buildCli(): Command {
112
119
  .option("-s, --silent", "Skip reset confirmation prompt", false)
113
120
  .option("--scope <scope>", "Run scope: full | files | workspace", "full")
114
121
  .option("--workspace <name>", "Workspace name (required when --scope workspace)")
122
+ .option("--gateway-mode <mode>", "Gateway mode: service | run | none", "service")
115
123
  .option("--dotenv-ref <path-or-url>", "Load env vars from local file or HTTP URL")
116
124
  .option("--provider <provider>", "Execution provider: command | remote | mock")
117
125
  .option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p: string[]) => p.concat([v]), [])
@@ -130,6 +138,7 @@ export function buildCli(): Command {
130
138
 
131
139
  const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
132
140
  const scope = parseScope(String(opts.scope ?? "full"));
141
+ const gatewayMode = parseGatewayMode(String(opts.gatewayMode ?? "service"));
133
142
  const workspaceName = opts.workspace?.trim() ? String(opts.workspace).trim() : undefined;
134
143
  if (scope === "workspace" && !workspaceName) {
135
144
  throw new ClawChefError("--scope workspace requires --workspace <name>");
@@ -142,6 +151,7 @@ export function buildCli(): Command {
142
151
  plugins: parsePluginFlags(opts.plugin),
143
152
  scope,
144
153
  workspaceName,
154
+ gatewayMode,
145
155
  dryRun: Boolean(opts.dryRun),
146
156
  allowMissing: Boolean(opts.allowMissing),
147
157
  verbose: Boolean(opts.verbose),
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { stdin as input, stdout as output } from "node:process";
8
8
  import { ClawChefError } from "../errors.js";
9
- import type { AgentDef, ChannelDef, ConversationDef, OpenClawBootstrap, OpenClawSection } from "../types.js";
9
+ import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawBootstrap, OpenClawSection } from "../types.js";
10
10
  import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
11
11
 
12
12
  const DEFAULT_COMMANDS = {
@@ -16,6 +16,7 @@ const DEFAULT_COMMANDS = {
16
16
  install_plugin: "${bin} plugins install ${plugin_spec_q}",
17
17
  factory_reset: "${bin} reset --scope full --yes --non-interactive",
18
18
  start_gateway: "${bin} gateway start",
19
+ run_gateway: "${bin} gateway run",
19
20
  enable_plugin: "",
20
21
  bind_channel_agent: "",
21
22
  login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
@@ -464,9 +465,14 @@ export class CommandOpenClawProvider implements OpenClawProvider {
464
465
  await runShell(cmd, dryRun);
465
466
  }
466
467
 
467
- async startGateway(config: OpenClawSection, dryRun: boolean): Promise<void> {
468
+ async startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void> {
469
+ if (mode === "none") {
470
+ return;
471
+ }
472
+
468
473
  const bin = config.bin ?? "openclaw";
469
- const startCmd = commandFor(config, "start_gateway", { bin, version: config.version });
474
+ const key = mode === "run" ? "run_gateway" : "start_gateway";
475
+ const startCmd = commandFor(config, key, { bin, version: config.version });
470
476
  if (!startCmd.trim()) {
471
477
  return;
472
478
  }
@@ -1,4 +1,4 @@
1
- import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../types.js";
1
+ import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection } from "../types.js";
2
2
  import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
3
3
 
4
4
  interface MockState {
@@ -57,7 +57,7 @@ export class MockOpenClawProvider implements OpenClawProvider {
57
57
  this.state.messages.clear();
58
58
  }
59
59
 
60
- async startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void> {
60
+ async startGateway(_config: OpenClawSection, _mode: GatewayMode, _dryRun: boolean): Promise<void> {
61
61
  return;
62
62
  }
63
63
 
@@ -1,4 +1,4 @@
1
- import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection, WorkspaceDef } from "../types.js";
1
+ import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection, WorkspaceDef } from "../types.js";
2
2
 
3
3
  export type ResolvedWorkspaceDef = WorkspaceDef & { path: string };
4
4
 
@@ -15,7 +15,7 @@ export interface OpenClawProvider {
15
15
  ): Promise<EnsureVersionResult>;
16
16
  installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
17
17
  factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
18
- startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
18
+ startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
19
19
  createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
20
20
  configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
21
21
  bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
@@ -3,6 +3,7 @@ import type {
3
3
  AgentDef,
4
4
  ChannelDef,
5
5
  ConversationDef,
6
+ GatewayMode,
6
7
  OpenClawRemoteConfig,
7
8
  OpenClawSection,
8
9
  } from "../types.js";
@@ -176,8 +177,11 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
176
177
  await this.perform(config, "factory_reset", undefined, dryRun);
177
178
  }
178
179
 
179
- async startGateway(config: OpenClawSection, dryRun: boolean): Promise<void> {
180
- await this.perform(config, "start_gateway", undefined, dryRun);
180
+ async startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void> {
181
+ if (mode === "none") {
182
+ return;
183
+ }
184
+ await this.perform(config, "start_gateway", { mode }, dryRun);
181
185
  }
182
186
 
183
187
  async createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void> {
@@ -62,6 +62,19 @@ function resolveWorkspacePath(recipeOrigin: RecipeOrigin, name: string, configur
62
62
  return path.join(homedir(), ".openclaw", workspaceName);
63
63
  }
64
64
 
65
+ function resolveOpenClawRootPath(recipeOrigin: RecipeOrigin, configuredPath?: string): string {
66
+ if (configuredPath?.trim()) {
67
+ if (path.isAbsolute(configuredPath)) {
68
+ return configuredPath;
69
+ }
70
+ if (recipeOrigin.kind === "local") {
71
+ return path.resolve(recipeOrigin.recipeDir, configuredPath);
72
+ }
73
+ return path.resolve(configuredPath);
74
+ }
75
+ return path.join(homedir(), ".openclaw");
76
+ }
77
+
65
78
  function isHttpUrl(value: string): boolean {
66
79
  try {
67
80
  const url = new URL(value);
@@ -197,6 +210,76 @@ export async function runRecipe(
197
210
  logger.info(`Plugin preinstalled: ${pluginSpec}`);
198
211
  }
199
212
 
213
+ const root = recipe.openclaw.root;
214
+ if (root && (root.assets?.trim() || (root.files?.length ?? 0) > 0)) {
215
+ if (remoteMode) {
216
+ throw new ClawChefError("openclaw.root assets/files are not supported with --provider remote");
217
+ }
218
+
219
+ const openclawRootPath = resolveOpenClawRootPath(recipeOrigin, root.path);
220
+ if (!options.dryRun) {
221
+ await mkdir(openclawRootPath, { recursive: true });
222
+ }
223
+
224
+ if (root.assets?.trim()) {
225
+ const resolvedAssets = resolveFileRef(recipeOrigin, root.assets);
226
+ if (resolvedAssets.kind !== "local") {
227
+ throw new ClawChefError(
228
+ `openclaw.root.assets must resolve to a local directory: ${root.assets}. Direct URL recipes cannot use openclaw.root.assets.`,
229
+ );
230
+ }
231
+
232
+ let assetDirStat;
233
+ try {
234
+ assetDirStat = await stat(resolvedAssets.value);
235
+ } catch (err) {
236
+ const message = err instanceof Error ? err.message : String(err);
237
+ throw new ClawChefError(`openclaw.root.assets path is not accessible: ${resolvedAssets.value} (${message})`);
238
+ }
239
+ if (!assetDirStat.isDirectory()) {
240
+ throw new ClawChefError(`openclaw.root.assets must be a directory: ${resolvedAssets.value}`);
241
+ }
242
+
243
+ const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
244
+ for (const assetFile of assetFiles) {
245
+ const target = path.resolve(openclawRootPath, assetFile.relativePath);
246
+ if (!options.dryRun) {
247
+ await mkdir(path.dirname(target), { recursive: true });
248
+ await copyFile(assetFile.absolutePath, target);
249
+ }
250
+ logger.info(`OpenClaw root asset copied: ${assetFile.relativePath}`);
251
+ }
252
+ }
253
+
254
+ for (const file of root.files ?? []) {
255
+ const target = path.resolve(openclawRootPath, file.path);
256
+ const targetDir = path.dirname(target);
257
+
258
+ if (!options.dryRun) {
259
+ await mkdir(targetDir, { recursive: true });
260
+ const alreadyExists = await exists(target);
261
+ if (alreadyExists && file.overwrite === false) {
262
+ logger.warn(`Skipping existing file: ${target}`);
263
+ } else if (file.content !== undefined) {
264
+ await writeFile(target, file.content, "utf8");
265
+ } else if (file.content_from) {
266
+ const rawContent = await readTextFromRef(recipeOrigin, file.content_from);
267
+ const content = renderTemplateString(rawContent, options.vars, options.allowMissing);
268
+ await writeFile(target, content, "utf8");
269
+ } else if (file.source) {
270
+ const resolved = resolveFileRef(recipeOrigin, file.source);
271
+ if (resolved.kind === "local") {
272
+ await copyFile(resolved.value, target);
273
+ } else {
274
+ const content = await readBinaryFromRef(recipeOrigin, file.source);
275
+ await writeFile(target, content);
276
+ }
277
+ }
278
+ }
279
+ logger.info(`OpenClaw root file materialized: ${file.path}`);
280
+ }
281
+ }
282
+
200
283
  for (const ws of recipe.workspaces ?? []) {
201
284
  const absPath = resolveWorkspacePath(recipeOrigin, ws.name, ws.path);
202
285
  workspacePaths.set(ws.name, absPath);
@@ -375,8 +458,12 @@ export async function runRecipe(
375
458
  logger.info(`Preset messages sent: ${conv.workspace}/${conv.agent}`);
376
459
  }
377
460
 
378
- await provider.startGateway(recipe.openclaw, options.dryRun);
379
- logger.info("Gateway started");
461
+ await provider.startGateway(recipe.openclaw, options.gatewayMode, options.dryRun);
462
+ if (options.gatewayMode === "none") {
463
+ logger.info("Gateway start skipped by gateway mode: none");
464
+ } else {
465
+ logger.info(`Gateway started (${options.gatewayMode})`);
466
+ }
380
467
 
381
468
  for (const channel of recipe.channels ?? []) {
382
469
  const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
package/src/recipe.ts CHANGED
@@ -194,6 +194,18 @@ function filterRecipeByWorkspaceName(recipe: Recipe, workspaceName: string): Rec
194
194
  function semanticValidate(recipe: Recipe): void {
195
195
  const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
196
196
  const agentNameCounts = new Map<string, number>();
197
+ const root = recipe.openclaw.root;
198
+ if (root?.path !== undefined && !root.path.trim()) {
199
+ throw new ClawChefError("openclaw.root.path cannot be empty");
200
+ }
201
+ if (root?.assets !== undefined && !root.assets.trim()) {
202
+ throw new ClawChefError("openclaw.root.assets cannot be empty");
203
+ }
204
+ for (const file of root?.files ?? []) {
205
+ if (!file.path.trim()) {
206
+ throw new ClawChefError("openclaw.root.files[] has file with empty path");
207
+ }
208
+ }
197
209
  for (const workspace of recipe.workspaces ?? []) {
198
210
  if (workspace.assets !== undefined && !workspace.assets.trim()) {
199
211
  throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
package/src/schema.ts CHANGED
@@ -14,6 +14,7 @@ const openClawCommandsSchema = z
14
14
  install_plugin: z.string().optional(),
15
15
  factory_reset: z.string().optional(),
16
16
  start_gateway: z.string().optional(),
17
+ run_gateway: z.string().optional(),
17
18
  enable_plugin: z.string().optional(),
18
19
  bind_channel_agent: z.string().optional(),
19
20
  login_channel: z.string().optional(),
@@ -49,12 +50,34 @@ const openClawBootstrapSchema = z
49
50
  })
50
51
  .strict();
51
52
 
53
+ const rootFileSchema = z
54
+ .object({
55
+ path: z.string().min(1),
56
+ content: z.string().optional(),
57
+ content_from: z.string().min(1).optional(),
58
+ source: z.string().optional(),
59
+ overwrite: z.boolean().optional(),
60
+ })
61
+ .strict()
62
+ .refine((v) => [v.content, v.content_from, v.source].filter((item) => item !== undefined).length === 1, {
63
+ message: "openclaw.root.files[] requires exactly one of content, content_from, or source",
64
+ });
65
+
66
+ const openClawRootSchema = z
67
+ .object({
68
+ path: z.string().min(1).optional(),
69
+ assets: z.string().min(1).optional(),
70
+ files: z.array(rootFileSchema).optional(),
71
+ })
72
+ .strict();
73
+
52
74
  const openClawSchema = z
53
75
  .object({
54
76
  bin: z.string().optional(),
55
77
  version: z.string(),
56
78
  install: z.enum(["auto", "always", "never"]).optional(),
57
79
  plugins: z.array(z.string().min(1)).optional(),
80
+ root: openClawRootSchema.optional(),
58
81
  bootstrap: openClawBootstrapSchema.optional(),
59
82
  commands: openClawCommandsSchema.optional(),
60
83
  })
package/src/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type InstallPolicy = "auto" | "always" | "never";
2
2
  export type OpenClawProvider = "command" | "mock" | "remote";
3
3
  export type RunScope = "full" | "files" | "workspace";
4
+ export type GatewayMode = "service" | "run" | "none";
4
5
 
5
6
  export interface OpenClawRemoteConfig {
6
7
  base_url: string;
@@ -24,6 +25,7 @@ export interface OpenClawCommandOverrides {
24
25
  install_plugin?: string;
25
26
  factory_reset?: string;
26
27
  start_gateway?: string;
28
+ run_gateway?: string;
27
29
  enable_plugin?: string;
28
30
  bind_channel_agent?: string;
29
31
  login_channel?: string;
@@ -61,10 +63,17 @@ export interface OpenClawSection {
61
63
  version: string;
62
64
  install?: InstallPolicy;
63
65
  plugins?: string[];
66
+ root?: OpenClawRootDef;
64
67
  bootstrap?: OpenClawBootstrap;
65
68
  commands?: OpenClawCommandOverrides;
66
69
  }
67
70
 
71
+ export interface OpenClawRootDef {
72
+ path?: string;
73
+ assets?: string;
74
+ files?: WorkspaceFileDef[];
75
+ }
76
+
68
77
  export interface WorkspaceDef {
69
78
  name: string;
70
79
  path?: string;
@@ -143,6 +152,7 @@ export interface RunOptions {
143
152
  plugins: string[];
144
153
  scope: RunScope;
145
154
  workspaceName?: string;
155
+ gatewayMode: GatewayMode;
146
156
  dryRun: boolean;
147
157
  allowMissing: boolean;
148
158
  verbose: boolean;