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/AGENTS.md +159 -0
- package/README.md +49 -4
- package/dist/api.d.ts +2 -1
- package/dist/api.js +1 -0
- package/dist/cli.js +9 -0
- package/dist/openclaw/command-provider.d.ts +3 -2
- package/dist/openclaw/command-provider.js +85 -2
- package/dist/openclaw/mock-provider.d.ts +3 -2
- package/dist/openclaw/mock-provider.js +4 -1
- package/dist/openclaw/provider.d.ts +3 -2
- package/dist/openclaw/remote-provider.d.ts +3 -2
- package/dist/openclaw/remote-provider.js +12 -2
- package/dist/orchestrator.js +101 -8
- package/dist/recipe.js +26 -0
- package/dist/schema.d.ts +124 -6
- package/dist/schema.js +23 -0
- package/dist/types.d.ts +11 -0
- package/package.json +1 -1
- package/recipes/sample.yaml +7 -0
- package/src/api.ts +3 -1
- package/src/cli.ts +11 -1
- package/src/openclaw/command-provider.ts +109 -3
- package/src/openclaw/mock-provider.ts +11 -2
- package/src/openclaw/provider.ts +3 -2
- package/src/openclaw/remote-provider.ts +19 -2
- package/src/orchestrator.ts +108 -8
- package/src/recipe.ts +31 -0
- package/src/schema.ts +25 -0
- package/src/types.ts +12 -0
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
|
|
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
|
}
|
package/src/openclaw/provider.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
477
|
+
`Channel login for ${effectiveChannel.channel} requires an interactive terminal session`,
|
|
378
478
|
);
|
|
379
479
|
}
|
|
380
|
-
await provider.loginChannel(recipe.openclaw,
|
|
381
|
-
logger.info(`Channel login completed: ${
|
|
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;
|