clawchef 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +159 -0
- package/README.md +32 -14
- package/dist/api.d.ts +3 -1
- package/dist/api.js +10 -1
- package/dist/cli.js +34 -3
- package/dist/openclaw/command-provider.d.ts +2 -1
- package/dist/openclaw/command-provider.js +109 -23
- package/dist/openclaw/mock-provider.d.ts +2 -1
- package/dist/openclaw/mock-provider.js +4 -1
- package/dist/openclaw/provider.d.ts +2 -1
- package/dist/openclaw/remote-provider.d.ts +2 -1
- package/dist/openclaw/remote-provider.js +8 -1
- package/dist/orchestrator.js +90 -56
- package/dist/recipe.js +71 -24
- package/dist/schema.d.ts +84 -107
- package/dist/schema.js +17 -21
- package/dist/types.d.ts +8 -11
- package/package.json +1 -1
- package/recipes/content-from-sample.yaml +13 -9
- package/recipes/openclaw-from-zero.yaml +2 -2
- package/recipes/openclaw-local.yaml +8 -10
- package/recipes/openclaw-remote-http.yaml +6 -8
- package/recipes/sample.yaml +6 -8
- package/recipes/snippets/readme-template.md +3 -1
- package/src/api.ts +13 -2
- package/src/cli.ts +36 -4
- package/src/openclaw/command-provider.ts +134 -24
- package/src/openclaw/mock-provider.ts +10 -1
- package/src/openclaw/provider.ts +2 -1
- package/src/openclaw/remote-provider.ts +14 -1
- package/src/orchestrator.ts +93 -55
- package/src/recipe.ts +82 -24
- package/src/schema.ts +19 -22
- package/src/types.ts +8 -11
|
@@ -6,12 +6,13 @@ export interface EnsureVersionResult {
|
|
|
6
6
|
installedThisRun: boolean;
|
|
7
7
|
}
|
|
8
8
|
export interface OpenClawProvider {
|
|
9
|
-
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean,
|
|
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
12
|
startGateway(config: OpenClawSection, 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
|
+
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
15
16
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
16
17
|
materializeFile?(config: OpenClawSection, workspace: string, filePath: string, content: string, overwrite: boolean | undefined, dryRun: boolean): Promise<void>;
|
|
17
18
|
createAgent(config: OpenClawSection, agent: AgentDef, workspacePath: string, dryRun: boolean): Promise<void>;
|
|
@@ -5,12 +5,13 @@ export declare class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
5
5
|
private readonly remoteConfig;
|
|
6
6
|
constructor(remoteConfig: Partial<OpenClawRemoteConfig>);
|
|
7
7
|
private perform;
|
|
8
|
-
ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean,
|
|
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
11
|
startGateway(config: OpenClawSection, 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
|
+
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
14
15
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
15
16
|
materializeFile(config: OpenClawSection, workspace: string, filePath: string, content: string, overwrite: boolean | undefined, dryRun: boolean): Promise<void>;
|
|
16
17
|
createAgent(config: OpenClawSection, agent: AgentDef, workspacePath: string, dryRun: boolean): Promise<void>;
|
|
@@ -94,7 +94,7 @@ export class RemoteOpenClawProvider {
|
|
|
94
94
|
clearTimeout(timeout);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
async ensureVersion(config, dryRun, _silent,
|
|
97
|
+
async ensureVersion(config, dryRun, _silent, _preserveExistingState) {
|
|
98
98
|
const result = await this.perform(config, "ensure_version", {
|
|
99
99
|
install: config.install,
|
|
100
100
|
}, dryRun);
|
|
@@ -119,6 +119,13 @@ export class RemoteOpenClawProvider {
|
|
|
119
119
|
async configureChannel(config, channel, dryRun) {
|
|
120
120
|
await this.perform(config, "configure_channel", { channel }, dryRun);
|
|
121
121
|
}
|
|
122
|
+
async bindChannelAgent(config, channel, agent, dryRun) {
|
|
123
|
+
await this.perform(config, "bind_channel_agent", {
|
|
124
|
+
channel: channel.channel,
|
|
125
|
+
account: channel.account,
|
|
126
|
+
agent,
|
|
127
|
+
}, dryRun);
|
|
128
|
+
}
|
|
122
129
|
async loginChannel(config, channel, dryRun) {
|
|
123
130
|
await this.perform(config, "login_channel", { channel }, dryRun);
|
|
124
131
|
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -22,6 +22,25 @@ function truncateForLog(text, maxLength = 500) {
|
|
|
22
22
|
}
|
|
23
23
|
return `${text.slice(0, maxLength)}... [truncated ${text.length - maxLength} chars]`;
|
|
24
24
|
}
|
|
25
|
+
function renderTemplateString(input, vars, allowMissing) {
|
|
26
|
+
return input.replace(/\$\{([^}]+)\}/g, (_match, rawKey) => {
|
|
27
|
+
const key = String(rawKey).trim();
|
|
28
|
+
if (!key) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
if (Object.prototype.hasOwnProperty.call(vars, key)) {
|
|
32
|
+
return vars[key] ?? "";
|
|
33
|
+
}
|
|
34
|
+
const lowerKey = key.toLowerCase();
|
|
35
|
+
if (Object.prototype.hasOwnProperty.call(vars, lowerKey)) {
|
|
36
|
+
return vars[lowerKey] ?? "";
|
|
37
|
+
}
|
|
38
|
+
if (allowMissing) {
|
|
39
|
+
return `\${${key}}`;
|
|
40
|
+
}
|
|
41
|
+
throw new ClawChefError(`Missing template variable in file content: ${key}`);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
25
44
|
function resolveWorkspacePath(recipeOrigin, name, configuredPath) {
|
|
26
45
|
if (configuredPath?.trim()) {
|
|
27
46
|
if (path.isAbsolute(configuredPath)) {
|
|
@@ -121,13 +140,14 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
121
140
|
const provider = createProvider(options);
|
|
122
141
|
const remoteMode = options.provider === "remote";
|
|
123
142
|
const workspacePaths = new Map();
|
|
143
|
+
const preserveExistingState = options.scope !== "full";
|
|
124
144
|
logger.info(`Running recipe: ${recipe.name}`);
|
|
125
|
-
const versionResult = await provider.ensureVersion(recipe.openclaw, options.dryRun, options.silent,
|
|
145
|
+
const versionResult = await provider.ensureVersion(recipe.openclaw, options.dryRun, options.silent, preserveExistingState);
|
|
126
146
|
logger.info(`OpenClaw version ready: ${recipe.openclaw.version}`);
|
|
127
147
|
if (versionResult.installedThisRun) {
|
|
128
148
|
logger.info("OpenClaw was installed in this run; skipping factory reset");
|
|
129
149
|
}
|
|
130
|
-
else if (
|
|
150
|
+
else if (preserveExistingState) {
|
|
131
151
|
logger.info("Keeping existing OpenClaw state; skipping factory reset");
|
|
132
152
|
}
|
|
133
153
|
else {
|
|
@@ -195,68 +215,79 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
195
215
|
logger.info(`Agent created: ${agent.workspace}/${agent.name}`);
|
|
196
216
|
}
|
|
197
217
|
for (const channel of recipe.channels ?? []) {
|
|
198
|
-
|
|
199
|
-
|
|
218
|
+
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
219
|
+
? { ...channel, account: channel.agent.trim() }
|
|
220
|
+
: channel;
|
|
221
|
+
await provider.configureChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
222
|
+
logger.info(`Channel configured: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
223
|
+
if (effectiveChannel.agent?.trim()) {
|
|
224
|
+
await provider.bindChannelAgent(recipe.openclaw, effectiveChannel, effectiveChannel.agent, options.dryRun);
|
|
225
|
+
logger.info(`Channel bound to agent: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""} -> ${effectiveChannel.agent}`);
|
|
226
|
+
}
|
|
200
227
|
}
|
|
201
|
-
for (const
|
|
202
|
-
const wsPath = workspacePaths.get(
|
|
228
|
+
for (const workspace of recipe.workspaces ?? []) {
|
|
229
|
+
const wsPath = workspacePaths.get(workspace.name);
|
|
203
230
|
if (!wsPath) {
|
|
204
|
-
throw new ClawChefError(`
|
|
231
|
+
throw new ClawChefError(`Workspace does not exist for files: ${workspace.name}`);
|
|
205
232
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
|
|
233
|
+
for (const file of workspace.files ?? []) {
|
|
234
|
+
if (provider.materializeFile) {
|
|
235
|
+
let content = file.content;
|
|
236
|
+
if (content === undefined && file.content_from) {
|
|
237
|
+
if (!options.dryRun) {
|
|
238
|
+
const rawContent = await readTextFromRef(recipeOrigin, file.content_from);
|
|
239
|
+
content = renderTemplateString(rawContent, options.vars, options.allowMissing);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const resolved = resolveFileRef(recipeOrigin, file.content_from);
|
|
243
|
+
content = `__dry_run_content_from__:${resolved.value}`;
|
|
244
|
+
}
|
|
211
245
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
246
|
+
if (content === undefined && file.source) {
|
|
247
|
+
if (!options.dryRun) {
|
|
248
|
+
content = await readTextFromRef(recipeOrigin, file.source);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const resolved = resolveFileRef(recipeOrigin, file.source);
|
|
252
|
+
content = `__dry_run_source__:${resolved.value}`;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (content === undefined) {
|
|
256
|
+
throw new ClawChefError(`File ${file.path} requires content, content_from, or source`);
|
|
215
257
|
}
|
|
258
|
+
await provider.materializeFile(recipe.openclaw, workspace.name, file.path, content, file.overwrite, options.dryRun);
|
|
259
|
+
logger.info(`File materialized: ${workspace.name}/${file.path}`);
|
|
260
|
+
continue;
|
|
216
261
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
262
|
+
const target = path.resolve(wsPath, file.path);
|
|
263
|
+
const targetDir = path.dirname(target);
|
|
264
|
+
if (!options.dryRun) {
|
|
265
|
+
await mkdir(targetDir, { recursive: true });
|
|
266
|
+
const alreadyExists = await exists(target);
|
|
267
|
+
if (alreadyExists && file.overwrite === false) {
|
|
268
|
+
logger.warn(`Skipping existing file: ${target}`);
|
|
220
269
|
}
|
|
221
|
-
else {
|
|
222
|
-
|
|
223
|
-
content = `__dry_run_source__:${resolved.value}`;
|
|
270
|
+
else if (file.content !== undefined) {
|
|
271
|
+
await writeFile(target, file.content, "utf8");
|
|
224
272
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
await provider.materializeFile(recipe.openclaw, file.workspace, file.path, content, file.overwrite, options.dryRun);
|
|
230
|
-
logger.info(`File materialized: ${file.workspace}/${file.path}`);
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const target = path.resolve(wsPath, file.path);
|
|
234
|
-
const targetDir = path.dirname(target);
|
|
235
|
-
if (!options.dryRun) {
|
|
236
|
-
await mkdir(targetDir, { recursive: true });
|
|
237
|
-
const alreadyExists = await exists(target);
|
|
238
|
-
if (alreadyExists && file.overwrite === false) {
|
|
239
|
-
logger.warn(`Skipping existing file: ${target}`);
|
|
240
|
-
}
|
|
241
|
-
else if (file.content !== undefined) {
|
|
242
|
-
await writeFile(target, file.content, "utf8");
|
|
243
|
-
}
|
|
244
|
-
else if (file.content_from) {
|
|
245
|
-
const content = await readTextFromRef(recipeOrigin, file.content_from);
|
|
246
|
-
await writeFile(target, content, "utf8");
|
|
247
|
-
}
|
|
248
|
-
else if (file.source) {
|
|
249
|
-
const resolved = resolveFileRef(recipeOrigin, file.source);
|
|
250
|
-
if (resolved.kind === "local") {
|
|
251
|
-
await copyFile(resolved.value, target);
|
|
273
|
+
else if (file.content_from) {
|
|
274
|
+
const rawContent = await readTextFromRef(recipeOrigin, file.content_from);
|
|
275
|
+
const content = renderTemplateString(rawContent, options.vars, options.allowMissing);
|
|
276
|
+
await writeFile(target, content, "utf8");
|
|
252
277
|
}
|
|
253
|
-
else {
|
|
254
|
-
const
|
|
255
|
-
|
|
278
|
+
else if (file.source) {
|
|
279
|
+
const resolved = resolveFileRef(recipeOrigin, file.source);
|
|
280
|
+
if (resolved.kind === "local") {
|
|
281
|
+
await copyFile(resolved.value, target);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const content = await readBinaryFromRef(recipeOrigin, file.source);
|
|
285
|
+
await writeFile(target, content);
|
|
286
|
+
}
|
|
256
287
|
}
|
|
257
288
|
}
|
|
289
|
+
logger.info(`File materialized: ${workspace.name}/${file.path}`);
|
|
258
290
|
}
|
|
259
|
-
logger.info(`File materialized: ${file.workspace}/${file.path}`);
|
|
260
291
|
}
|
|
261
292
|
for (const agent of recipe.agents ?? []) {
|
|
262
293
|
for (const skill of agent.skills ?? []) {
|
|
@@ -295,14 +326,17 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
295
326
|
await provider.startGateway(recipe.openclaw, options.dryRun);
|
|
296
327
|
logger.info("Gateway started");
|
|
297
328
|
for (const channel of recipe.channels ?? []) {
|
|
298
|
-
|
|
329
|
+
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
330
|
+
? { ...channel, account: channel.agent.trim() }
|
|
331
|
+
: channel;
|
|
332
|
+
if (!effectiveChannel.login) {
|
|
299
333
|
continue;
|
|
300
334
|
}
|
|
301
335
|
if (!options.dryRun && !input.isTTY) {
|
|
302
|
-
throw new ClawChefError(`Channel login for ${
|
|
336
|
+
throw new ClawChefError(`Channel login for ${effectiveChannel.channel} requires an interactive terminal session`);
|
|
303
337
|
}
|
|
304
|
-
await provider.loginChannel(recipe.openclaw,
|
|
305
|
-
logger.info(`Channel login completed: ${
|
|
338
|
+
await provider.loginChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
339
|
+
logger.info(`Channel login completed: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
306
340
|
}
|
|
307
341
|
logger.info("Recipe execution completed");
|
|
308
342
|
}
|
package/dist/recipe.js
CHANGED
|
@@ -8,23 +8,17 @@ import { recipeSchema } from "./schema.js";
|
|
|
8
8
|
import { ClawChefError } from "./errors.js";
|
|
9
9
|
import { deepResolveTemplates } from "./template.js";
|
|
10
10
|
const AUTH_CHOICE_TO_FIELD = {
|
|
11
|
-
"openai-api-key": "
|
|
12
|
-
"anthropic-api-key": "
|
|
13
|
-
"openrouter-api-key": "
|
|
14
|
-
"xai-api-key": "
|
|
15
|
-
"gemini-api-key": "
|
|
16
|
-
"ai-gateway-api-key": "
|
|
17
|
-
"cloudflare-ai-gateway-api-key": "
|
|
11
|
+
"openai-api-key": "llm_api_key",
|
|
12
|
+
"anthropic-api-key": "llm_api_key",
|
|
13
|
+
"openrouter-api-key": "llm_api_key",
|
|
14
|
+
"xai-api-key": "llm_api_key",
|
|
15
|
+
"gemini-api-key": "llm_api_key",
|
|
16
|
+
"ai-gateway-api-key": "llm_api_key",
|
|
17
|
+
"cloudflare-ai-gateway-api-key": "llm_api_key",
|
|
18
18
|
token: "token",
|
|
19
19
|
};
|
|
20
20
|
const SECRET_BOOTSTRAP_FIELDS = [
|
|
21
|
-
"
|
|
22
|
-
"anthropic_api_key",
|
|
23
|
-
"openrouter_api_key",
|
|
24
|
-
"xai_api_key",
|
|
25
|
-
"gemini_api_key",
|
|
26
|
-
"ai_gateway_api_key",
|
|
27
|
-
"cloudflare_ai_gateway_api_key",
|
|
21
|
+
"llm_api_key",
|
|
28
22
|
"token",
|
|
29
23
|
];
|
|
30
24
|
const ALLOWED_CHANNELS = new Set([
|
|
@@ -86,7 +80,7 @@ function assertNoInlineSecrets(recipe) {
|
|
|
86
80
|
}
|
|
87
81
|
}
|
|
88
82
|
}
|
|
89
|
-
function collectVars(recipe, cliVars) {
|
|
83
|
+
function collectVars(recipe, cliVars, requiredKeys) {
|
|
90
84
|
const vars = {};
|
|
91
85
|
const params = recipe.params ?? {};
|
|
92
86
|
for (const [envKey, envValue] of Object.entries(process.env)) {
|
|
@@ -114,7 +108,7 @@ function collectVars(recipe, cliVars) {
|
|
|
114
108
|
vars[key] = def.default;
|
|
115
109
|
continue;
|
|
116
110
|
}
|
|
117
|
-
if (def.required) {
|
|
111
|
+
if (def.required && (requiredKeys === undefined || requiredKeys.has(key))) {
|
|
118
112
|
throw new ClawChefError(`Parameter ${key} is required but was not provided via --var or environment`);
|
|
119
113
|
}
|
|
120
114
|
}
|
|
@@ -123,21 +117,51 @@ function collectVars(recipe, cliVars) {
|
|
|
123
117
|
}
|
|
124
118
|
return vars;
|
|
125
119
|
}
|
|
120
|
+
function projectRecipeForScope(recipe, options) {
|
|
121
|
+
if (options.scope !== "workspace") {
|
|
122
|
+
return recipe;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...recipe,
|
|
126
|
+
openclaw: {
|
|
127
|
+
...recipe.openclaw,
|
|
128
|
+
bootstrap: undefined,
|
|
129
|
+
},
|
|
130
|
+
channels: [],
|
|
131
|
+
conversations: [],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function filterRecipeByWorkspaceName(recipe, workspaceName) {
|
|
135
|
+
const workspace = (recipe.workspaces ?? []).find((ws) => ws.name === workspaceName);
|
|
136
|
+
if (!workspace) {
|
|
137
|
+
throw new ClawChefError(`Workspace not found in recipe: ${workspaceName}`);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...recipe,
|
|
141
|
+
workspaces: [workspace],
|
|
142
|
+
agents: (recipe.agents ?? []).filter((agent) => agent.workspace === workspaceName),
|
|
143
|
+
conversations: (recipe.conversations ?? []).filter((conv) => conv.workspace === workspaceName),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
126
146
|
function semanticValidate(recipe) {
|
|
127
147
|
const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
|
|
148
|
+
const agentNameCounts = new Map();
|
|
128
149
|
for (const workspace of recipe.workspaces ?? []) {
|
|
129
150
|
if (workspace.assets !== undefined && !workspace.assets.trim()) {
|
|
130
151
|
throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
|
|
131
152
|
}
|
|
132
153
|
}
|
|
133
154
|
for (const agent of recipe.agents ?? []) {
|
|
155
|
+
agentNameCounts.set(agent.name, (agentNameCounts.get(agent.name) ?? 0) + 1);
|
|
134
156
|
if (!ws.has(agent.workspace)) {
|
|
135
157
|
throw new ClawChefError(`Agent ${agent.name} references missing workspace: ${agent.workspace}`);
|
|
136
158
|
}
|
|
137
159
|
}
|
|
138
|
-
for (const
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
for (const workspace of recipe.workspaces ?? []) {
|
|
161
|
+
for (const file of workspace.files ?? []) {
|
|
162
|
+
if (!file.path.trim()) {
|
|
163
|
+
throw new ClawChefError(`Workspace ${workspace.name} has file with empty path`);
|
|
164
|
+
}
|
|
141
165
|
}
|
|
142
166
|
}
|
|
143
167
|
const agents = new Set((recipe.agents ?? []).map((a) => `${a.workspace}::${a.name}`));
|
|
@@ -157,6 +181,18 @@ function semanticValidate(recipe) {
|
|
|
157
181
|
(channel.login || channel.login_mode !== undefined || channel.login_account !== undefined)) {
|
|
158
182
|
throw new ClawChefError("channels[] entry for telegram does not support login/login_mode/login_account. Configure token (or use_env/token_file), then start gateway.");
|
|
159
183
|
}
|
|
184
|
+
if (channel.agent?.trim()) {
|
|
185
|
+
if (channel.channel !== "telegram") {
|
|
186
|
+
throw new ClawChefError(`channels[] entry for ${channel.channel} does not support agent binding. Use channel: telegram with agent.`);
|
|
187
|
+
}
|
|
188
|
+
const matched = agentNameCounts.get(channel.agent) ?? 0;
|
|
189
|
+
if (matched === 0) {
|
|
190
|
+
throw new ClawChefError(`channels[] entry references missing agent by name: ${channel.agent}`);
|
|
191
|
+
}
|
|
192
|
+
if (matched > 1) {
|
|
193
|
+
throw new ClawChefError(`channels[] entry references duplicate agent name: ${channel.agent}. Agent names must be unique for channel binding.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
160
196
|
const hasAuth = Boolean(channel.use_env) ||
|
|
161
197
|
Boolean(channel.token?.trim()) ||
|
|
162
198
|
Boolean(channel.token_file?.trim()) ||
|
|
@@ -609,16 +645,27 @@ export async function loadRecipe(recipePath, options) {
|
|
|
609
645
|
if (!firstParse.success) {
|
|
610
646
|
throw new ClawChefError(`Recipe format is invalid: ${firstParse.error.message}`);
|
|
611
647
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const
|
|
648
|
+
const projected = projectRecipeForScope(firstParse.data, options);
|
|
649
|
+
assertNoInlineSecrets(projected);
|
|
650
|
+
const requiredKeys = options.scope === "workspace" ? new Set() : undefined;
|
|
651
|
+
const vars = collectVars(projected, options.vars, requiredKeys);
|
|
652
|
+
const rendered = deepResolveTemplates(projected, vars, options.allowMissing);
|
|
615
653
|
const secondParse = recipeSchema.safeParse(rendered);
|
|
616
654
|
if (!secondParse.success) {
|
|
617
655
|
throw new ClawChefError(`Recipe is invalid after parameter resolution: ${secondParse.error.message}`);
|
|
618
656
|
}
|
|
619
|
-
|
|
657
|
+
const scopedRecipe = (() => {
|
|
658
|
+
if (options.scope !== "workspace") {
|
|
659
|
+
return secondParse.data;
|
|
660
|
+
}
|
|
661
|
+
if (!options.workspaceName) {
|
|
662
|
+
throw new ClawChefError("scope=workspace requires a workspace name");
|
|
663
|
+
}
|
|
664
|
+
return filterRecipeByWorkspaceName(secondParse.data, options.workspaceName);
|
|
665
|
+
})();
|
|
666
|
+
semanticValidate(scopedRecipe);
|
|
620
667
|
return {
|
|
621
|
-
recipe:
|
|
668
|
+
recipe: scopedRecipe,
|
|
622
669
|
origin: recipeRef.origin,
|
|
623
670
|
};
|
|
624
671
|
});
|