clawchef 0.1.7 → 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/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,7 +16,9 @@ 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: "",
21
+ bind_channel_agent: "",
20
22
  login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
21
23
  create_agent:
22
24
  "${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json",
@@ -31,6 +33,20 @@ interface StagedMessage {
31
33
  content: string;
32
34
  }
33
35
 
36
+ interface BindingItem {
37
+ agentId?: unknown;
38
+ match?: {
39
+ channel?: unknown;
40
+ accountId?: unknown;
41
+ peer?: unknown;
42
+ parentPeer?: unknown;
43
+ guildId?: unknown;
44
+ teamId?: unknown;
45
+ roles?: unknown;
46
+ };
47
+ [key: string]: unknown;
48
+ }
49
+
34
50
  const SECRET_FLAG_RE =
35
51
  /(--[A-Za-z0-9-]*(?:api-key|token|password|secret)[A-Za-z0-9-]*\s+)(?:'[^']*'|"[^"]*"|\S+)/g;
36
52
 
@@ -280,6 +296,39 @@ function bootstrapRuntimeEnv(bootstrap: OpenClawBootstrap | undefined): Record<s
280
296
  return env;
281
297
  }
282
298
 
299
+ function isAccountLevelBinding(item: BindingItem, channel: string, account: string): boolean {
300
+ const match = item.match;
301
+ if (!match || typeof match !== "object") {
302
+ return false;
303
+ }
304
+ if (match.channel !== channel || match.accountId !== account) {
305
+ return false;
306
+ }
307
+ return (
308
+ match.peer === undefined
309
+ && match.parentPeer === undefined
310
+ && match.guildId === undefined
311
+ && match.teamId === undefined
312
+ && match.roles === undefined
313
+ );
314
+ }
315
+
316
+ function parseBindingsJson(raw: string): BindingItem[] {
317
+ if (!raw.trim()) {
318
+ return [];
319
+ }
320
+ try {
321
+ const parsed = JSON.parse(raw) as unknown;
322
+ if (!Array.isArray(parsed)) {
323
+ throw new ClawChefError("openclaw config bindings is not an array");
324
+ }
325
+ return parsed as BindingItem[];
326
+ } catch (err) {
327
+ const message = err instanceof Error ? err.message : String(err);
328
+ throw new ClawChefError(`Failed to parse openclaw bindings JSON: ${message}`);
329
+ }
330
+ }
331
+
283
332
  export class CommandOpenClawProvider implements OpenClawProvider {
284
333
  private readonly stagedMessages = new Map<string, StagedMessage[]>();
285
334
 
@@ -416,9 +465,14 @@ export class CommandOpenClawProvider implements OpenClawProvider {
416
465
  await runShell(cmd, dryRun);
417
466
  }
418
467
 
419
- 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
+
420
473
  const bin = config.bin ?? "openclaw";
421
- 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 });
422
476
  if (!startCmd.trim()) {
423
477
  return;
424
478
  }
@@ -502,6 +556,58 @@ export class CommandOpenClawProvider implements OpenClawProvider {
502
556
  await runShell(cmd, dryRun);
503
557
  }
504
558
 
559
+ async bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void> {
560
+ const account = channel.account?.trim();
561
+ if (!account) {
562
+ throw new ClawChefError(`Channel ${channel.channel} requires account for agent binding`);
563
+ }
564
+
565
+ const bin = config.bin ?? "openclaw";
566
+ const customTemplate = config.commands?.bind_channel_agent;
567
+ if (customTemplate?.trim()) {
568
+ const customCmd = fillTemplate(customTemplate, {
569
+ bin,
570
+ version: config.version,
571
+ channel: channel.channel,
572
+ channel_q: shellQuote(channel.channel),
573
+ account,
574
+ account_q: shellQuote(account),
575
+ agent,
576
+ agent_q: shellQuote(agent),
577
+ });
578
+ if (customCmd.trim()) {
579
+ await runShell(customCmd, dryRun);
580
+ }
581
+ return;
582
+ }
583
+
584
+ if (dryRun) {
585
+ return;
586
+ }
587
+
588
+ const getCmd = `${bin} config get bindings --json 2>/dev/null || printf '[]'`;
589
+ const rawBindings = await runShell(getCmd, false);
590
+ const bindings = parseBindingsJson(rawBindings);
591
+ const nextBinding: BindingItem = {
592
+ agentId: agent,
593
+ match: {
594
+ channel: channel.channel,
595
+ accountId: account,
596
+ },
597
+ };
598
+
599
+ const index = bindings.findIndex((item) => isAccountLevelBinding(item, channel.channel, account));
600
+ if (index >= 0) {
601
+ bindings[index] = nextBinding;
602
+ } else {
603
+ bindings.push(nextBinding);
604
+ }
605
+
606
+ const json = JSON.stringify(bindings);
607
+ const setCmd = `${bin} config set bindings ${shellQuote(json)} --json`;
608
+ await runShell(setCmd, false);
609
+ }
610
+
505
611
  async loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
506
612
  if (!channel.login) {
507
613
  return;
@@ -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
 
@@ -69,6 +69,15 @@ export class MockOpenClawProvider implements OpenClawProvider {
69
69
  this.state.channels.add(`${channel.channel}::${channel.account ?? "default"}`);
70
70
  }
71
71
 
72
+ async bindChannelAgent(
73
+ _config: OpenClawSection,
74
+ _channel: ChannelDef,
75
+ _agent: string,
76
+ _dryRun: boolean,
77
+ ): Promise<void> {
78
+ return;
79
+ }
80
+
72
81
  async loginChannel(_config: OpenClawSection, _channel: ChannelDef, _dryRun: boolean): Promise<void> {
73
82
  return;
74
83
  }
@@ -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,9 +15,10 @@ 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
+ bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
21
22
  loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
22
23
  materializeFile?(
23
24
  config: OpenClawSection,
@@ -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> {
@@ -188,6 +192,19 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
188
192
  await this.perform(config, "configure_channel", { channel }, dryRun);
189
193
  }
190
194
 
195
+ async bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void> {
196
+ await this.perform(
197
+ config,
198
+ "bind_channel_agent",
199
+ {
200
+ channel: channel.channel,
201
+ account: channel.account,
202
+ agent,
203
+ },
204
+ dryRun,
205
+ );
206
+ }
207
+
191
208
  async loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
192
209
  await this.perform(config, "login_channel", { channel }, dryRun);
193
210
  }
@@ -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);
@@ -261,8 +344,18 @@ export async function runRecipe(
261
344
  }
262
345
 
263
346
  for (const channel of recipe.channels ?? []) {
264
- await provider.configureChannel(recipe.openclaw, channel, options.dryRun);
265
- logger.info(`Channel configured: ${channel.channel}${channel.account ? `/${channel.account}` : ""}`);
347
+ const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
348
+ ? { ...channel, account: channel.agent.trim() }
349
+ : channel;
350
+
351
+ await provider.configureChannel(recipe.openclaw, effectiveChannel, options.dryRun);
352
+ logger.info(`Channel configured: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
353
+ if (effectiveChannel.agent?.trim()) {
354
+ await provider.bindChannelAgent(recipe.openclaw, effectiveChannel, effectiveChannel.agent, options.dryRun);
355
+ logger.info(
356
+ `Channel bound to agent: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""} -> ${effectiveChannel.agent}`,
357
+ );
358
+ }
266
359
  }
267
360
 
268
361
  for (const workspace of recipe.workspaces ?? []) {
@@ -365,20 +458,27 @@ export async function runRecipe(
365
458
  logger.info(`Preset messages sent: ${conv.workspace}/${conv.agent}`);
366
459
  }
367
460
 
368
- await provider.startGateway(recipe.openclaw, options.dryRun);
369
- 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
+ }
370
467
 
371
468
  for (const channel of recipe.channels ?? []) {
372
- if (!channel.login) {
469
+ const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
470
+ ? { ...channel, account: channel.agent.trim() }
471
+ : channel;
472
+ if (!effectiveChannel.login) {
373
473
  continue;
374
474
  }
375
475
  if (!options.dryRun && !input.isTTY) {
376
476
  throw new ClawChefError(
377
- `Channel login for ${channel.channel} requires an interactive terminal session`,
477
+ `Channel login for ${effectiveChannel.channel} requires an interactive terminal session`,
378
478
  );
379
479
  }
380
- await provider.loginChannel(recipe.openclaw, channel, options.dryRun);
381
- logger.info(`Channel login completed: ${channel.channel}${channel.account ? `/${channel.account}` : ""}`);
480
+ await provider.loginChannel(recipe.openclaw, effectiveChannel, options.dryRun);
481
+ logger.info(`Channel login completed: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
382
482
  }
383
483
 
384
484
  logger.info("Recipe execution completed");
package/src/recipe.ts CHANGED
@@ -193,12 +193,26 @@ function filterRecipeByWorkspaceName(recipe: Recipe, workspaceName: string): Rec
193
193
 
194
194
  function semanticValidate(recipe: Recipe): void {
195
195
  const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
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
+ }
196
209
  for (const workspace of recipe.workspaces ?? []) {
197
210
  if (workspace.assets !== undefined && !workspace.assets.trim()) {
198
211
  throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
199
212
  }
200
213
  }
201
214
  for (const agent of recipe.agents ?? []) {
215
+ agentNameCounts.set(agent.name, (agentNameCounts.get(agent.name) ?? 0) + 1);
202
216
  if (!ws.has(agent.workspace)) {
203
217
  throw new ClawChefError(`Agent ${agent.name} references missing workspace: ${agent.workspace}`);
204
218
  }
@@ -238,6 +252,23 @@ function semanticValidate(recipe: Recipe): void {
238
252
  );
239
253
  }
240
254
 
255
+ if (channel.agent?.trim()) {
256
+ if (channel.channel !== "telegram") {
257
+ throw new ClawChefError(
258
+ `channels[] entry for ${channel.channel} does not support agent binding. Use channel: telegram with agent.`,
259
+ );
260
+ }
261
+ const matched = agentNameCounts.get(channel.agent) ?? 0;
262
+ if (matched === 0) {
263
+ throw new ClawChefError(`channels[] entry references missing agent by name: ${channel.agent}`);
264
+ }
265
+ if (matched > 1) {
266
+ throw new ClawChefError(
267
+ `channels[] entry references duplicate agent name: ${channel.agent}. Agent names must be unique for channel binding.`,
268
+ );
269
+ }
270
+ }
271
+
241
272
  const hasAuth =
242
273
  Boolean(channel.use_env) ||
243
274
  Boolean(channel.token?.trim()) ||
package/src/schema.ts CHANGED
@@ -14,7 +14,9 @@ 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(),
19
+ bind_channel_agent: z.string().optional(),
18
20
  login_channel: z.string().optional(),
19
21
  create_workspace: z.string().optional(),
20
22
  create_agent: z.string().optional(),
@@ -48,12 +50,34 @@ const openClawBootstrapSchema = z
48
50
  })
49
51
  .strict();
50
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
+
51
74
  const openClawSchema = z
52
75
  .object({
53
76
  bin: z.string().optional(),
54
77
  version: z.string(),
55
78
  install: z.enum(["auto", "always", "never"]).optional(),
56
79
  plugins: z.array(z.string().min(1)).optional(),
80
+ root: openClawRootSchema.optional(),
57
81
  bootstrap: openClawBootstrapSchema.optional(),
58
82
  commands: openClawCommandsSchema.optional(),
59
83
  })
@@ -87,6 +111,7 @@ const channelSchema = z
87
111
  .object({
88
112
  channel: z.string().min(1),
89
113
  account: z.string().min(1).optional(),
114
+ agent: z.string().min(1).optional(),
90
115
  login: z.boolean().optional(),
91
116
  login_mode: z.enum(["interactive"]).optional(),
92
117
  login_account: z.string().min(1).optional(),
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,7 +25,9 @@ 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;
30
+ bind_channel_agent?: string;
28
31
  login_channel?: string;
29
32
  create_workspace?: string;
30
33
  create_agent?: string;
@@ -60,10 +63,17 @@ export interface OpenClawSection {
60
63
  version: string;
61
64
  install?: InstallPolicy;
62
65
  plugins?: string[];
66
+ root?: OpenClawRootDef;
63
67
  bootstrap?: OpenClawBootstrap;
64
68
  commands?: OpenClawCommandOverrides;
65
69
  }
66
70
 
71
+ export interface OpenClawRootDef {
72
+ path?: string;
73
+ assets?: string;
74
+ files?: WorkspaceFileDef[];
75
+ }
76
+
67
77
  export interface WorkspaceDef {
68
78
  name: string;
69
79
  path?: string;
@@ -74,6 +84,7 @@ export interface WorkspaceDef {
74
84
  export interface ChannelDef {
75
85
  channel: string;
76
86
  account?: string;
87
+ agent?: string;
77
88
  login?: boolean;
78
89
  login_mode?: "interactive";
79
90
  login_account?: string;
@@ -141,6 +152,7 @@ export interface RunOptions {
141
152
  plugins: string[];
142
153
  scope: RunScope;
143
154
  workspaceName?: string;
155
+ gatewayMode: GatewayMode;
144
156
  dryRun: boolean;
145
157
  allowMissing: boolean;
146
158
  verbose: boolean;