clawchef 0.1.12 → 0.1.13
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 +53 -1
- package/dist/api.d.ts +1 -0
- package/dist/api.js +5 -0
- package/dist/cli.js +12 -3
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +13 -3
- package/dist/openclaw/command-provider.d.ts +5 -1
- package/dist/openclaw/command-provider.js +254 -44
- package/dist/openclaw/factory.js +2 -2
- package/dist/openclaw/mock-provider.d.ts +1 -0
- package/dist/openclaw/mock-provider.js +3 -0
- package/dist/openclaw/provider.d.ts +6 -0
- package/dist/openclaw/remote-provider.d.ts +4 -1
- package/dist/openclaw/remote-provider.js +27 -1
- package/dist/orchestrator.js +168 -86
- package/dist/recipe.js +39 -1
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +1 -0
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
- package/src/api.ts +8 -0
- package/src/cli.ts +13 -3
- package/src/logger.ts +14 -3
- package/src/openclaw/command-provider.ts +287 -46
- package/src/openclaw/factory.ts +2 -2
- package/src/openclaw/mock-provider.ts +4 -0
- package/src/openclaw/provider.ts +7 -0
- package/src/openclaw/remote-provider.ts +31 -1
- package/src/orchestrator.ts +186 -98
- package/src/recipe.ts +47 -1
- package/src/schema.ts +1 -0
- package/src/types.ts +3 -1
|
@@ -5,6 +5,10 @@ export type ResolvedWorkspaceDef = WorkspaceDef & {
|
|
|
5
5
|
export interface EnsureVersionResult {
|
|
6
6
|
installedThisRun: boolean;
|
|
7
7
|
}
|
|
8
|
+
export interface ChannelAgentBinding {
|
|
9
|
+
channel: ChannelDef;
|
|
10
|
+
agent: string;
|
|
11
|
+
}
|
|
8
12
|
export interface OpenClawProvider {
|
|
9
13
|
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean, preserveExistingState: boolean): Promise<EnsureVersionResult>;
|
|
10
14
|
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
@@ -12,6 +16,8 @@ export interface OpenClawProvider {
|
|
|
12
16
|
startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
|
|
13
17
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
14
18
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
19
|
+
applyConfigPatch(config: OpenClawSection, patch: Record<string, unknown>, dryRun: boolean): Promise<void>;
|
|
20
|
+
bindChannelAgents?(config: OpenClawSection, bindings: ChannelAgentBinding[], dryRun: boolean): Promise<void>;
|
|
15
21
|
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
16
22
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
17
23
|
materializeFile?(config: OpenClawSection, workspace: string, filePath: string, content: string, overwrite: boolean | undefined, dryRun: boolean): Promise<void>;
|
|
@@ -3,7 +3,9 @@ import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from
|
|
|
3
3
|
export declare class RemoteOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private readonly stagedMessages;
|
|
5
5
|
private readonly remoteConfig;
|
|
6
|
-
|
|
6
|
+
private readonly verboseEnabled;
|
|
7
|
+
constructor(remoteConfig: Partial<OpenClawRemoteConfig>, verboseEnabled?: boolean);
|
|
8
|
+
private debug;
|
|
7
9
|
private perform;
|
|
8
10
|
ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean, _preserveExistingState: boolean): Promise<EnsureVersionResult>;
|
|
9
11
|
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
@@ -11,6 +13,7 @@ export declare class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
11
13
|
startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
|
|
12
14
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
13
15
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
16
|
+
applyConfigPatch(config: OpenClawSection, patch: Record<string, unknown>, dryRun: boolean): Promise<void>;
|
|
14
17
|
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
15
18
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
16
19
|
materializeFile(config: OpenClawSection, workspace: string, filePath: string, content: string, overwrite: boolean | undefined, dryRun: boolean): Promise<void>;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { ClawChefError } from "../errors.js";
|
|
2
2
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
3
3
|
const DEFAULT_OPERATION_PATH = "/v1/clawchef/operation";
|
|
4
|
+
function timestamp() {
|
|
5
|
+
const now = new Date();
|
|
6
|
+
const year = now.getFullYear();
|
|
7
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
8
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
9
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
10
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
11
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
12
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
13
|
+
}
|
|
4
14
|
function parseResponseBody(raw) {
|
|
5
15
|
if (!raw.trim()) {
|
|
6
16
|
return {};
|
|
@@ -50,11 +60,20 @@ function assertRemoteConfig(remote) {
|
|
|
50
60
|
export class RemoteOpenClawProvider {
|
|
51
61
|
stagedMessages = new Map();
|
|
52
62
|
remoteConfig;
|
|
53
|
-
|
|
63
|
+
verboseEnabled;
|
|
64
|
+
constructor(remoteConfig, verboseEnabled = false) {
|
|
54
65
|
this.remoteConfig = remoteConfig;
|
|
66
|
+
this.verboseEnabled = verboseEnabled;
|
|
67
|
+
}
|
|
68
|
+
debug(message) {
|
|
69
|
+
if (!this.verboseEnabled) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
process.stdout.write(`[${timestamp()}] [DEBUG] ${message}\n`);
|
|
55
73
|
}
|
|
56
74
|
async perform(config, operation, payload, dryRun) {
|
|
57
75
|
if (dryRun) {
|
|
76
|
+
this.debug(`REMOTE DRY-RUN op=${operation}`);
|
|
58
77
|
return { ok: true };
|
|
59
78
|
}
|
|
60
79
|
const remote = assertRemoteConfig(this.remoteConfig);
|
|
@@ -66,6 +85,8 @@ export class RemoteOpenClawProvider {
|
|
|
66
85
|
recipe_version: config.version,
|
|
67
86
|
payload,
|
|
68
87
|
};
|
|
88
|
+
const startedAt = Date.now();
|
|
89
|
+
this.debug(`REMOTE START op=${operation} url=${operationUrl(remote)}`);
|
|
69
90
|
try {
|
|
70
91
|
const response = await fetch(operationUrl(remote), {
|
|
71
92
|
method: "POST",
|
|
@@ -81,9 +102,11 @@ export class RemoteOpenClawProvider {
|
|
|
81
102
|
if (parsed.ok === false) {
|
|
82
103
|
throw new ClawChefError(`Remote operation failed for ${operation}: ${parsed.message ?? "unknown error"}`);
|
|
83
104
|
}
|
|
105
|
+
this.debug(`REMOTE DONE op=${operation} status=${response.status} (${Date.now() - startedAt}ms)`);
|
|
84
106
|
return parsed;
|
|
85
107
|
}
|
|
86
108
|
catch (err) {
|
|
109
|
+
this.debug(`REMOTE FAIL op=${operation} (${Date.now() - startedAt}ms)`);
|
|
87
110
|
if (err instanceof ClawChefError) {
|
|
88
111
|
throw err;
|
|
89
112
|
}
|
|
@@ -122,6 +145,9 @@ export class RemoteOpenClawProvider {
|
|
|
122
145
|
async configureChannel(config, channel, dryRun) {
|
|
123
146
|
await this.perform(config, "configure_channel", { channel }, dryRun);
|
|
124
147
|
}
|
|
148
|
+
async applyConfigPatch(config, patch, dryRun) {
|
|
149
|
+
await this.perform(config, "apply_config_patch", { patch }, dryRun);
|
|
150
|
+
}
|
|
125
151
|
async bindChannelAgent(config, channel, agent, dryRun) {
|
|
126
152
|
await this.perform(config, "bind_channel_agent", {
|
|
127
153
|
channel: channel.channel,
|
package/dist/orchestrator.js
CHANGED
|
@@ -22,6 +22,30 @@ function truncateForLog(text, maxLength = 500) {
|
|
|
22
22
|
}
|
|
23
23
|
return `${text.slice(0, maxLength)}... [truncated ${text.length - maxLength} chars]`;
|
|
24
24
|
}
|
|
25
|
+
function toPosixPath(value) {
|
|
26
|
+
return value.replaceAll("\\", "/");
|
|
27
|
+
}
|
|
28
|
+
function wildcardToRegExp(pattern) {
|
|
29
|
+
const escaped = toPosixPath(pattern)
|
|
30
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
31
|
+
.replace(/\*\*/g, "___DOUBLE_STAR___")
|
|
32
|
+
.replace(/\*/g, "[^/]*")
|
|
33
|
+
.replace(/___DOUBLE_STAR___/g, ".*");
|
|
34
|
+
return new RegExp(`^${escaped}$`);
|
|
35
|
+
}
|
|
36
|
+
function matchesFilePatterns(patterns, relativePath) {
|
|
37
|
+
if (patterns.length === 0) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const normalized = toPosixPath(relativePath);
|
|
41
|
+
return patterns.some((pattern) => wildcardToRegExp(pattern).test(normalized));
|
|
42
|
+
}
|
|
43
|
+
function matchesAnyFilePattern(patterns, candidates) {
|
|
44
|
+
if (patterns.length === 0) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return candidates.some((candidate) => matchesFilePatterns(patterns, candidate));
|
|
48
|
+
}
|
|
25
49
|
function renderTemplateString(input, vars, allowMissing) {
|
|
26
50
|
return input.replace(/\$\{([^}]+)\}/g, (_match, rawKey) => {
|
|
27
51
|
const key = String(rawKey).trim();
|
|
@@ -162,30 +186,37 @@ async function confirmFactoryReset(options) {
|
|
|
162
186
|
export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
163
187
|
const provider = createProvider(options);
|
|
164
188
|
const remoteMode = options.provider === "remote";
|
|
189
|
+
const filesOnlyScope = options.scope === "files";
|
|
190
|
+
const fileFilterEnabled = filesOnlyScope && options.filePatterns.length > 0;
|
|
165
191
|
const workspacePaths = new Map();
|
|
166
192
|
const preserveExistingState = options.scope !== "full";
|
|
167
193
|
logger.info(`Running recipe: ${recipe.name}`);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
194
|
+
if (!filesOnlyScope) {
|
|
195
|
+
const versionResult = await provider.ensureVersion(recipe.openclaw, options.dryRun, options.silent, preserveExistingState);
|
|
196
|
+
logger.info(`OpenClaw version ready: ${recipe.openclaw.version}`);
|
|
197
|
+
if (versionResult.installedThisRun) {
|
|
198
|
+
logger.info("OpenClaw was installed in this run; skipping factory reset");
|
|
199
|
+
}
|
|
200
|
+
else if (preserveExistingState) {
|
|
201
|
+
logger.info("Keeping existing OpenClaw state; skipping factory reset");
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const confirmed = await confirmFactoryReset(options);
|
|
205
|
+
if (!confirmed) {
|
|
206
|
+
throw new ClawChefError("Aborted by user before factory reset");
|
|
207
|
+
}
|
|
208
|
+
await provider.factoryReset(recipe.openclaw, options.dryRun);
|
|
209
|
+
logger.info("Factory reset completed");
|
|
210
|
+
}
|
|
211
|
+
const pluginSpecs = Array.from(new Set([...(recipe.openclaw.plugins ?? []), ...options.plugins].map((v) => v.trim())))
|
|
212
|
+
.filter((v) => v.length > 0);
|
|
213
|
+
for (const pluginSpec of pluginSpecs) {
|
|
214
|
+
await provider.installPlugin(recipe.openclaw, pluginSpec, options.dryRun);
|
|
215
|
+
logger.info(`Plugin preinstalled: ${pluginSpec}`);
|
|
180
216
|
}
|
|
181
|
-
await provider.factoryReset(recipe.openclaw, options.dryRun);
|
|
182
|
-
logger.info("Factory reset completed");
|
|
183
217
|
}
|
|
184
|
-
|
|
185
|
-
.
|
|
186
|
-
for (const pluginSpec of pluginSpecs) {
|
|
187
|
-
await provider.installPlugin(recipe.openclaw, pluginSpec, options.dryRun);
|
|
188
|
-
logger.info(`Plugin preinstalled: ${pluginSpec}`);
|
|
218
|
+
else {
|
|
219
|
+
logger.info("Scope files: only syncing root/workspace assets and files");
|
|
189
220
|
}
|
|
190
221
|
const root = recipe.openclaw.root;
|
|
191
222
|
if (root && (root.assets?.trim() || (root.files?.length ?? 0) > 0)) {
|
|
@@ -224,6 +255,11 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
224
255
|
else {
|
|
225
256
|
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
226
257
|
for (const assetFile of assetFiles) {
|
|
258
|
+
const patternCandidates = [assetFile.relativePath, `root/${assetFile.relativePath}`];
|
|
259
|
+
if (fileFilterEnabled && !matchesAnyFilePattern(options.filePatterns, patternCandidates)) {
|
|
260
|
+
logger.debug(`Filtered root asset: ${assetFile.relativePath}`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
227
263
|
const target = path.resolve(openclawRootPath, assetFile.relativePath);
|
|
228
264
|
if (!options.dryRun) {
|
|
229
265
|
await mkdir(path.dirname(target), { recursive: true });
|
|
@@ -234,6 +270,11 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
234
270
|
}
|
|
235
271
|
}
|
|
236
272
|
for (const file of root.files ?? []) {
|
|
273
|
+
const patternCandidates = [file.path, `root/${file.path}`];
|
|
274
|
+
if (fileFilterEnabled && !matchesAnyFilePattern(options.filePatterns, patternCandidates)) {
|
|
275
|
+
logger.debug(`Filtered root file: ${file.path}`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
237
278
|
const target = path.resolve(openclawRootPath, file.path);
|
|
238
279
|
const targetDir = path.dirname(target);
|
|
239
280
|
if (!options.dryRun) {
|
|
@@ -270,8 +311,10 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
270
311
|
if (!options.dryRun && !remoteMode) {
|
|
271
312
|
await mkdir(absPath, { recursive: true });
|
|
272
313
|
}
|
|
273
|
-
|
|
274
|
-
|
|
314
|
+
if (!filesOnlyScope) {
|
|
315
|
+
await provider.createWorkspace(recipe.openclaw, { ...ws, path: absPath }, options.dryRun);
|
|
316
|
+
logger.info(`Workspace created: ${ws.name}`);
|
|
317
|
+
}
|
|
275
318
|
if (!ws.assets?.trim()) {
|
|
276
319
|
continue;
|
|
277
320
|
}
|
|
@@ -296,6 +339,15 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
296
339
|
}
|
|
297
340
|
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
298
341
|
for (const assetFile of assetFiles) {
|
|
342
|
+
const patternCandidates = [
|
|
343
|
+
assetFile.relativePath,
|
|
344
|
+
`${ws.name}/${assetFile.relativePath}`,
|
|
345
|
+
`workspace-${ws.name}/${assetFile.relativePath}`,
|
|
346
|
+
];
|
|
347
|
+
if (fileFilterEnabled && !matchesAnyFilePattern(options.filePatterns, patternCandidates)) {
|
|
348
|
+
logger.debug(`Filtered workspace asset: ${ws.name}/${assetFile.relativePath}`);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
299
351
|
if (provider.materializeFile) {
|
|
300
352
|
const content = await readFile(assetFile.absolutePath, "utf8");
|
|
301
353
|
await provider.materializeFile(recipe.openclaw, ws.name, assetFile.relativePath, content, true, options.dryRun);
|
|
@@ -310,36 +362,64 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
310
362
|
logger.info(`Workspace asset copied: ${ws.name}/${assetFile.relativePath}`);
|
|
311
363
|
}
|
|
312
364
|
}
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
365
|
+
if (!filesOnlyScope) {
|
|
366
|
+
const pendingChannelBindings = [];
|
|
367
|
+
for (const agent of recipe.agents ?? []) {
|
|
368
|
+
const workspacePath = workspacePaths.get(agent.workspace);
|
|
369
|
+
if (!workspacePath) {
|
|
370
|
+
throw new ClawChefError(`Agent references missing workspace: ${agent.workspace}`);
|
|
371
|
+
}
|
|
372
|
+
await provider.createAgent(recipe.openclaw, agent, workspacePath, options.dryRun);
|
|
373
|
+
logger.info(`Agent created: ${agent.workspace}/${agent.name}`);
|
|
317
374
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
logger.info(`
|
|
329
|
-
|
|
375
|
+
for (const channel of recipe.channels ?? []) {
|
|
376
|
+
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
377
|
+
? { ...channel, account: channel.agent.trim() }
|
|
378
|
+
: channel;
|
|
379
|
+
const autoDisabledTelegram = shouldAutoDisableTelegramChannel(effectiveChannel);
|
|
380
|
+
await provider.configureChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
381
|
+
if (autoDisabledTelegram) {
|
|
382
|
+
logger.info(`Telegram channel disabled due to empty bot token: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
logger.info(`Channel configured: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
386
|
+
if (effectiveChannel.agent?.trim()) {
|
|
387
|
+
pendingChannelBindings.push({ channel: effectiveChannel, agent: effectiveChannel.agent });
|
|
388
|
+
}
|
|
330
389
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
390
|
+
if (pendingChannelBindings.length > 0) {
|
|
391
|
+
if (provider.bindChannelAgents) {
|
|
392
|
+
await provider.bindChannelAgents(recipe.openclaw, pendingChannelBindings, options.dryRun);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
for (const binding of pendingChannelBindings) {
|
|
396
|
+
await provider.bindChannelAgent(recipe.openclaw, binding.channel, binding.agent, options.dryRun);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const binding of pendingChannelBindings) {
|
|
400
|
+
logger.info(`Channel bound to agent: ${binding.channel.channel}${binding.channel.account ? `/${binding.channel.account}` : ""} -> ${binding.agent}`);
|
|
401
|
+
}
|
|
335
402
|
}
|
|
336
403
|
}
|
|
404
|
+
if (!filesOnlyScope && recipe.openclaw.config_patch) {
|
|
405
|
+
await provider.applyConfigPatch(recipe.openclaw, recipe.openclaw.config_patch, options.dryRun);
|
|
406
|
+
logger.info("OpenClaw config patch applied");
|
|
407
|
+
}
|
|
337
408
|
for (const workspace of recipe.workspaces ?? []) {
|
|
338
409
|
const wsPath = workspacePaths.get(workspace.name);
|
|
339
410
|
if (!wsPath) {
|
|
340
411
|
throw new ClawChefError(`Workspace does not exist for files: ${workspace.name}`);
|
|
341
412
|
}
|
|
342
413
|
for (const file of workspace.files ?? []) {
|
|
414
|
+
const patternCandidates = [
|
|
415
|
+
file.path,
|
|
416
|
+
`${workspace.name}/${file.path}`,
|
|
417
|
+
`workspace-${workspace.name}/${file.path}`,
|
|
418
|
+
];
|
|
419
|
+
if (fileFilterEnabled && !matchesAnyFilePattern(options.filePatterns, patternCandidates)) {
|
|
420
|
+
logger.debug(`Filtered workspace file: ${workspace.name}/${file.path}`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
343
423
|
if (provider.materializeFile) {
|
|
344
424
|
let content = file.content;
|
|
345
425
|
if (content === undefined && file.content_from) {
|
|
@@ -398,59 +478,61 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
398
478
|
logger.info(`File materialized: ${workspace.name}/${file.path}`);
|
|
399
479
|
}
|
|
400
480
|
}
|
|
401
|
-
|
|
402
|
-
for (const
|
|
403
|
-
|
|
404
|
-
|
|
481
|
+
if (!filesOnlyScope) {
|
|
482
|
+
for (const agent of recipe.agents ?? []) {
|
|
483
|
+
for (const skill of agent.skills ?? []) {
|
|
484
|
+
await provider.installSkill(recipe.openclaw, agent.workspace, agent.name, skill, options.dryRun);
|
|
485
|
+
logger.info(`Skill installed: ${agent.workspace}/${agent.name} -> ${skill}`);
|
|
486
|
+
}
|
|
405
487
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
const reply = await provider.runAgent(recipe.openclaw, conv, options.dryRun);
|
|
417
|
-
if (msg.expect) {
|
|
418
|
-
try {
|
|
419
|
-
validateReply(reply, msg.expect);
|
|
420
|
-
logger.info(`Output assertions passed: ${conv.workspace}/${conv.agent}`);
|
|
488
|
+
for (const conv of recipe.conversations ?? []) {
|
|
489
|
+
for (const msg of conv.messages) {
|
|
490
|
+
await provider.sendMessage(recipe.openclaw, conv, msg.content, options.dryRun);
|
|
491
|
+
const shouldRun = conv.run ?? Boolean(msg.expect);
|
|
492
|
+
if (shouldRun) {
|
|
493
|
+
if (options.dryRun) {
|
|
494
|
+
logger.info(`dry-run: skipping execution and output assertions: ${conv.workspace}/${conv.agent}`);
|
|
495
|
+
continue;
|
|
421
496
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
497
|
+
const reply = await provider.runAgent(recipe.openclaw, conv, options.dryRun);
|
|
498
|
+
if (msg.expect) {
|
|
499
|
+
try {
|
|
500
|
+
validateReply(reply, msg.expect);
|
|
501
|
+
logger.info(`Output assertions passed: ${conv.workspace}/${conv.agent}`);
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
logger.warn(`Assertion failed reply (truncated): ${truncateForLog(reply)}`);
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
425
507
|
}
|
|
508
|
+
else {
|
|
509
|
+
logger.info(`Agent executed: ${conv.workspace}/${conv.agent}`);
|
|
510
|
+
}
|
|
511
|
+
logger.debug(`Agent output: ${reply}`);
|
|
426
512
|
}
|
|
427
|
-
else {
|
|
428
|
-
logger.info(`Agent executed: ${conv.workspace}/${conv.agent}`);
|
|
429
|
-
}
|
|
430
|
-
logger.debug(`Agent output: ${reply}`);
|
|
431
513
|
}
|
|
514
|
+
logger.info(`Preset messages sent: ${conv.workspace}/${conv.agent}`);
|
|
432
515
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
else {
|
|
440
|
-
logger.info(`Gateway started (${options.gatewayMode})`);
|
|
441
|
-
}
|
|
442
|
-
for (const channel of recipe.channels ?? []) {
|
|
443
|
-
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
444
|
-
? { ...channel, account: channel.agent.trim() }
|
|
445
|
-
: channel;
|
|
446
|
-
if (!effectiveChannel.login) {
|
|
447
|
-
continue;
|
|
516
|
+
await provider.startGateway(recipe.openclaw, options.gatewayMode, options.dryRun);
|
|
517
|
+
if (options.gatewayMode === "none") {
|
|
518
|
+
logger.info("Gateway start skipped by gateway mode: none");
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
logger.info(`Gateway started (${options.gatewayMode})`);
|
|
448
522
|
}
|
|
449
|
-
|
|
450
|
-
|
|
523
|
+
for (const channel of recipe.channels ?? []) {
|
|
524
|
+
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
525
|
+
? { ...channel, account: channel.agent.trim() }
|
|
526
|
+
: channel;
|
|
527
|
+
if (!effectiveChannel.login) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (!options.dryRun && !input.isTTY) {
|
|
531
|
+
throw new ClawChefError(`Channel login for ${effectiveChannel.channel} requires an interactive terminal session`);
|
|
532
|
+
}
|
|
533
|
+
await provider.loginChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
534
|
+
logger.info(`Channel login completed: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
451
535
|
}
|
|
452
|
-
await provider.loginChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
453
|
-
logger.info(`Channel login completed: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
454
536
|
}
|
|
455
537
|
logger.info("Recipe execution completed");
|
|
456
538
|
}
|
package/dist/recipe.js
CHANGED
|
@@ -44,6 +44,27 @@ const ALLOWED_CHANNELS = new Set([
|
|
|
44
44
|
]);
|
|
45
45
|
const CHANNEL_SECRET_FIELDS = ["token", "bot_token", "access_token", "app_token", "password"];
|
|
46
46
|
const TEMPLATE_TOKEN_RE = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/;
|
|
47
|
+
function assertNoInlineSecretsInObject(value, pathLabel) {
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
50
|
+
assertNoInlineSecretsInObject(value[i], `${pathLabel}[${i}]`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!value || typeof value !== "object") {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
58
|
+
const nextPath = `${pathLabel}.${key}`;
|
|
59
|
+
if (typeof nestedValue === "string" &&
|
|
60
|
+
/(token|password|secret|api[_-]?key|webhook)/i.test(key) &&
|
|
61
|
+
nestedValue.trim().length > 0 &&
|
|
62
|
+
!TEMPLATE_TOKEN_RE.test(nestedValue)) {
|
|
63
|
+
throw new ClawChefError(`Inline secret in ${nextPath} is not allowed. Use \${var} and pass it via --var or CLAWCHEF_VAR_*`);
|
|
64
|
+
}
|
|
65
|
+
assertNoInlineSecretsInObject(nestedValue, nextPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
47
68
|
function hasExplicitEmptyTelegramToken(channel) {
|
|
48
69
|
if (channel.channel !== "telegram") {
|
|
49
70
|
return false;
|
|
@@ -65,6 +86,9 @@ function assertNoInlineSecrets(recipe) {
|
|
|
65
86
|
}
|
|
66
87
|
}
|
|
67
88
|
}
|
|
89
|
+
if (recipe.openclaw.config_patch) {
|
|
90
|
+
assertNoInlineSecretsInObject(recipe.openclaw.config_patch, "openclaw.config_patch");
|
|
91
|
+
}
|
|
68
92
|
for (const channel of recipe.channels ?? []) {
|
|
69
93
|
for (const field of CHANNEL_SECRET_FIELDS) {
|
|
70
94
|
const value = channel[field];
|
|
@@ -126,6 +150,20 @@ function collectVars(recipe, cliVars, requiredKeys) {
|
|
|
126
150
|
return vars;
|
|
127
151
|
}
|
|
128
152
|
function projectRecipeForScope(recipe, options) {
|
|
153
|
+
if (options.scope === "files") {
|
|
154
|
+
return {
|
|
155
|
+
...recipe,
|
|
156
|
+
openclaw: {
|
|
157
|
+
...recipe.openclaw,
|
|
158
|
+
version: "0.0.0",
|
|
159
|
+
bootstrap: undefined,
|
|
160
|
+
plugins: [],
|
|
161
|
+
},
|
|
162
|
+
channels: [],
|
|
163
|
+
agents: [],
|
|
164
|
+
conversations: [],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
129
167
|
if (options.scope !== "workspace") {
|
|
130
168
|
return recipe;
|
|
131
169
|
}
|
|
@@ -673,7 +711,7 @@ export async function loadRecipe(recipePath, options) {
|
|
|
673
711
|
}
|
|
674
712
|
const projected = projectRecipeForScope(firstParse.data, options);
|
|
675
713
|
assertNoInlineSecrets(projected);
|
|
676
|
-
const requiredKeys = options.scope === "workspace" ? new Set() : undefined;
|
|
714
|
+
const requiredKeys = options.scope === "workspace" || options.scope === "files" ? new Set() : undefined;
|
|
677
715
|
const vars = collectVars(projected, options.vars, requiredKeys);
|
|
678
716
|
const rendered = deepResolveTemplates(projected, vars, options.allowMissing);
|
|
679
717
|
const secondParse = recipeSchema.safeParse(rendered);
|
package/dist/schema.d.ts
CHANGED
|
@@ -75,6 +75,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
75
75
|
}[] | undefined;
|
|
76
76
|
assets?: string | undefined;
|
|
77
77
|
}>>;
|
|
78
|
+
config_patch: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
78
79
|
bootstrap: z.ZodOptional<z.ZodObject<{
|
|
79
80
|
non_interactive: z.ZodOptional<z.ZodBoolean>;
|
|
80
81
|
accept_risk: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -201,6 +202,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
201
202
|
}[] | undefined;
|
|
202
203
|
assets?: string | undefined;
|
|
203
204
|
} | undefined;
|
|
205
|
+
config_patch?: Record<string, unknown> | undefined;
|
|
204
206
|
bootstrap?: {
|
|
205
207
|
workspace?: string | undefined;
|
|
206
208
|
cloudflare_ai_gateway_account_id?: string | undefined;
|
|
@@ -255,6 +257,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
255
257
|
}[] | undefined;
|
|
256
258
|
assets?: string | undefined;
|
|
257
259
|
} | undefined;
|
|
260
|
+
config_patch?: Record<string, unknown> | undefined;
|
|
258
261
|
bootstrap?: {
|
|
259
262
|
workspace?: string | undefined;
|
|
260
263
|
cloudflare_ai_gateway_account_id?: string | undefined;
|
|
@@ -512,6 +515,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
512
515
|
}[] | undefined;
|
|
513
516
|
assets?: string | undefined;
|
|
514
517
|
} | undefined;
|
|
518
|
+
config_patch?: Record<string, unknown> | undefined;
|
|
515
519
|
bootstrap?: {
|
|
516
520
|
workspace?: string | undefined;
|
|
517
521
|
cloudflare_ai_gateway_account_id?: string | undefined;
|
|
@@ -628,6 +632,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
628
632
|
}[] | undefined;
|
|
629
633
|
assets?: string | undefined;
|
|
630
634
|
} | undefined;
|
|
635
|
+
config_patch?: Record<string, unknown> | undefined;
|
|
631
636
|
bootstrap?: {
|
|
632
637
|
workspace?: string | undefined;
|
|
633
638
|
cloudflare_ai_gateway_account_id?: string | undefined;
|
package/dist/schema.js
CHANGED
|
@@ -72,6 +72,7 @@ const openClawSchema = z
|
|
|
72
72
|
install: z.enum(["auto", "always", "never"]).optional(),
|
|
73
73
|
plugins: z.array(z.string().min(1)).optional(),
|
|
74
74
|
root: openClawRootSchema.optional(),
|
|
75
|
+
config_patch: z.record(z.unknown()).optional(),
|
|
75
76
|
bootstrap: openClawBootstrapSchema.optional(),
|
|
76
77
|
commands: openClawCommandsSchema.optional(),
|
|
77
78
|
})
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type InstallPolicy = "auto" | "always" | "never";
|
|
2
2
|
export type OpenClawProvider = "command" | "mock" | "remote";
|
|
3
|
-
export type RunScope = "full" | "files" | "workspace";
|
|
3
|
+
export type RunScope = "full" | "stateful" | "files" | "workspace";
|
|
4
4
|
export type GatewayMode = "service" | "run" | "none";
|
|
5
5
|
export interface OpenClawRemoteConfig {
|
|
6
6
|
base_url: string;
|
|
@@ -59,6 +59,7 @@ export interface OpenClawSection {
|
|
|
59
59
|
install?: InstallPolicy;
|
|
60
60
|
plugins?: string[];
|
|
61
61
|
root?: OpenClawRootDef;
|
|
62
|
+
config_patch?: Record<string, unknown>;
|
|
62
63
|
bootstrap?: OpenClawBootstrap;
|
|
63
64
|
commands?: OpenClawCommandOverrides;
|
|
64
65
|
}
|
|
@@ -136,6 +137,7 @@ export interface Recipe {
|
|
|
136
137
|
export interface RunOptions {
|
|
137
138
|
vars: Record<string, string>;
|
|
138
139
|
plugins: string[];
|
|
140
|
+
filePatterns: string[];
|
|
139
141
|
scope: RunScope;
|
|
140
142
|
workspaceName?: string;
|
|
141
143
|
gatewayMode: GatewayMode;
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ScaffoldOptions, ScaffoldResult } from "./scaffold.js";
|
|
|
12
12
|
export interface CookOptions {
|
|
13
13
|
vars?: Record<string, string>;
|
|
14
14
|
plugins?: string[];
|
|
15
|
+
filePatterns?: string[];
|
|
15
16
|
dryRun?: boolean;
|
|
16
17
|
allowMissing?: boolean;
|
|
17
18
|
verbose?: boolean;
|
|
@@ -27,6 +28,9 @@ export interface CookOptions {
|
|
|
27
28
|
|
|
28
29
|
function normalizeCookOptions(options: CookOptions): RunOptions {
|
|
29
30
|
const plugins = Array.from(new Set((options.plugins ?? []).map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
31
|
+
const filePatterns = Array.from(
|
|
32
|
+
new Set((options.filePatterns ?? []).map((value) => value.trim()).filter((value) => value.length > 0)),
|
|
33
|
+
);
|
|
30
34
|
const scope = options.scope ?? "full";
|
|
31
35
|
const workspaceName = options.workspaceName?.trim() || undefined;
|
|
32
36
|
if (scope === "workspace" && !workspaceName) {
|
|
@@ -35,9 +39,13 @@ function normalizeCookOptions(options: CookOptions): RunOptions {
|
|
|
35
39
|
if (scope !== "workspace" && workspaceName) {
|
|
36
40
|
throw new ClawChefError("workspaceName is only allowed when scope=workspace");
|
|
37
41
|
}
|
|
42
|
+
if (scope !== "files" && filePatterns.length > 0) {
|
|
43
|
+
throw new ClawChefError("filePatterns is only allowed when scope=files");
|
|
44
|
+
}
|
|
38
45
|
return {
|
|
39
46
|
vars: options.vars ?? {},
|
|
40
47
|
plugins,
|
|
48
|
+
filePatterns,
|
|
41
49
|
scope,
|
|
42
50
|
workspaceName,
|
|
43
51
|
gatewayMode: options.gatewayMode ?? "service",
|