clawchef 0.1.11 → 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 +54 -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 +272 -41
- 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 +177 -82
- package/dist/recipe.js +50 -1
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +3 -2
- 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 +309 -43
- 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 +199 -94
- package/src/recipe.ts +61 -2
- package/src/schema.ts +3 -2
- 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();
|
|
@@ -76,6 +100,14 @@ function isHttpUrl(value) {
|
|
|
76
100
|
return false;
|
|
77
101
|
}
|
|
78
102
|
}
|
|
103
|
+
function shouldAutoDisableTelegramChannel(channel) {
|
|
104
|
+
if (channel.channel !== "telegram") {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const emptyToken = channel.token !== undefined && channel.token.trim().length === 0;
|
|
108
|
+
const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
|
|
109
|
+
return emptyToken || emptyBotToken;
|
|
110
|
+
}
|
|
79
111
|
function isNotFoundError(err) {
|
|
80
112
|
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
81
113
|
}
|
|
@@ -154,30 +186,37 @@ async function confirmFactoryReset(options) {
|
|
|
154
186
|
export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
155
187
|
const provider = createProvider(options);
|
|
156
188
|
const remoteMode = options.provider === "remote";
|
|
189
|
+
const filesOnlyScope = options.scope === "files";
|
|
190
|
+
const fileFilterEnabled = filesOnlyScope && options.filePatterns.length > 0;
|
|
157
191
|
const workspacePaths = new Map();
|
|
158
192
|
const preserveExistingState = options.scope !== "full";
|
|
159
193
|
logger.info(`Running recipe: ${recipe.name}`);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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}`);
|
|
172
216
|
}
|
|
173
|
-
await provider.factoryReset(recipe.openclaw, options.dryRun);
|
|
174
|
-
logger.info("Factory reset completed");
|
|
175
217
|
}
|
|
176
|
-
|
|
177
|
-
.
|
|
178
|
-
for (const pluginSpec of pluginSpecs) {
|
|
179
|
-
await provider.installPlugin(recipe.openclaw, pluginSpec, options.dryRun);
|
|
180
|
-
logger.info(`Plugin preinstalled: ${pluginSpec}`);
|
|
218
|
+
else {
|
|
219
|
+
logger.info("Scope files: only syncing root/workspace assets and files");
|
|
181
220
|
}
|
|
182
221
|
const root = recipe.openclaw.root;
|
|
183
222
|
if (root && (root.assets?.trim() || (root.files?.length ?? 0) > 0)) {
|
|
@@ -216,6 +255,11 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
216
255
|
else {
|
|
217
256
|
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
218
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
|
+
}
|
|
219
263
|
const target = path.resolve(openclawRootPath, assetFile.relativePath);
|
|
220
264
|
if (!options.dryRun) {
|
|
221
265
|
await mkdir(path.dirname(target), { recursive: true });
|
|
@@ -226,6 +270,11 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
226
270
|
}
|
|
227
271
|
}
|
|
228
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
|
+
}
|
|
229
278
|
const target = path.resolve(openclawRootPath, file.path);
|
|
230
279
|
const targetDir = path.dirname(target);
|
|
231
280
|
if (!options.dryRun) {
|
|
@@ -262,8 +311,10 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
262
311
|
if (!options.dryRun && !remoteMode) {
|
|
263
312
|
await mkdir(absPath, { recursive: true });
|
|
264
313
|
}
|
|
265
|
-
|
|
266
|
-
|
|
314
|
+
if (!filesOnlyScope) {
|
|
315
|
+
await provider.createWorkspace(recipe.openclaw, { ...ws, path: absPath }, options.dryRun);
|
|
316
|
+
logger.info(`Workspace created: ${ws.name}`);
|
|
317
|
+
}
|
|
267
318
|
if (!ws.assets?.trim()) {
|
|
268
319
|
continue;
|
|
269
320
|
}
|
|
@@ -288,6 +339,15 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
288
339
|
}
|
|
289
340
|
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
290
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
|
+
}
|
|
291
351
|
if (provider.materializeFile) {
|
|
292
352
|
const content = await readFile(assetFile.absolutePath, "utf8");
|
|
293
353
|
await provider.materializeFile(recipe.openclaw, ws.name, assetFile.relativePath, content, true, options.dryRun);
|
|
@@ -302,31 +362,64 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
302
362
|
logger.info(`Workspace asset copied: ${ws.name}/${assetFile.relativePath}`);
|
|
303
363
|
}
|
|
304
364
|
}
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
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}`);
|
|
309
374
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
}
|
|
389
|
+
}
|
|
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
|
+
}
|
|
322
402
|
}
|
|
323
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
|
+
}
|
|
324
408
|
for (const workspace of recipe.workspaces ?? []) {
|
|
325
409
|
const wsPath = workspacePaths.get(workspace.name);
|
|
326
410
|
if (!wsPath) {
|
|
327
411
|
throw new ClawChefError(`Workspace does not exist for files: ${workspace.name}`);
|
|
328
412
|
}
|
|
329
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
|
+
}
|
|
330
423
|
if (provider.materializeFile) {
|
|
331
424
|
let content = file.content;
|
|
332
425
|
if (content === undefined && file.content_from) {
|
|
@@ -385,59 +478,61 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
385
478
|
logger.info(`File materialized: ${workspace.name}/${file.path}`);
|
|
386
479
|
}
|
|
387
480
|
}
|
|
388
|
-
|
|
389
|
-
for (const
|
|
390
|
-
|
|
391
|
-
|
|
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
|
+
}
|
|
392
487
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
const reply = await provider.runAgent(recipe.openclaw, conv, options.dryRun);
|
|
404
|
-
if (msg.expect) {
|
|
405
|
-
try {
|
|
406
|
-
validateReply(reply, msg.expect);
|
|
407
|
-
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;
|
|
408
496
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
+
}
|
|
412
507
|
}
|
|
508
|
+
else {
|
|
509
|
+
logger.info(`Agent executed: ${conv.workspace}/${conv.agent}`);
|
|
510
|
+
}
|
|
511
|
+
logger.debug(`Agent output: ${reply}`);
|
|
413
512
|
}
|
|
414
|
-
else {
|
|
415
|
-
logger.info(`Agent executed: ${conv.workspace}/${conv.agent}`);
|
|
416
|
-
}
|
|
417
|
-
logger.debug(`Agent output: ${reply}`);
|
|
418
513
|
}
|
|
514
|
+
logger.info(`Preset messages sent: ${conv.workspace}/${conv.agent}`);
|
|
419
515
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
else {
|
|
427
|
-
logger.info(`Gateway started (${options.gatewayMode})`);
|
|
428
|
-
}
|
|
429
|
-
for (const channel of recipe.channels ?? []) {
|
|
430
|
-
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
431
|
-
? { ...channel, account: channel.agent.trim() }
|
|
432
|
-
: channel;
|
|
433
|
-
if (!effectiveChannel.login) {
|
|
434
|
-
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})`);
|
|
435
522
|
}
|
|
436
|
-
|
|
437
|
-
|
|
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}` : ""}`);
|
|
438
535
|
}
|
|
439
|
-
await provider.loginChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
440
|
-
logger.info(`Channel login completed: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
441
536
|
}
|
|
442
537
|
logger.info("Recipe execution completed");
|
|
443
538
|
}
|
package/dist/recipe.js
CHANGED
|
@@ -44,6 +44,35 @@ 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
|
+
}
|
|
68
|
+
function hasExplicitEmptyTelegramToken(channel) {
|
|
69
|
+
if (channel.channel !== "telegram") {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const emptyToken = channel.token !== undefined && channel.token.trim().length === 0;
|
|
73
|
+
const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
|
|
74
|
+
return emptyToken || emptyBotToken;
|
|
75
|
+
}
|
|
47
76
|
function assertNoInlineSecrets(recipe) {
|
|
48
77
|
const bootstrap = recipe.openclaw.bootstrap;
|
|
49
78
|
if (bootstrap) {
|
|
@@ -57,6 +86,9 @@ function assertNoInlineSecrets(recipe) {
|
|
|
57
86
|
}
|
|
58
87
|
}
|
|
59
88
|
}
|
|
89
|
+
if (recipe.openclaw.config_patch) {
|
|
90
|
+
assertNoInlineSecretsInObject(recipe.openclaw.config_patch, "openclaw.config_patch");
|
|
91
|
+
}
|
|
60
92
|
for (const channel of recipe.channels ?? []) {
|
|
61
93
|
for (const field of CHANNEL_SECRET_FIELDS) {
|
|
62
94
|
const value = channel[field];
|
|
@@ -118,6 +150,20 @@ function collectVars(recipe, cliVars, requiredKeys) {
|
|
|
118
150
|
return vars;
|
|
119
151
|
}
|
|
120
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
|
+
}
|
|
121
167
|
if (options.scope !== "workspace") {
|
|
122
168
|
return recipe;
|
|
123
169
|
}
|
|
@@ -222,6 +268,9 @@ function semanticValidate(recipe) {
|
|
|
222
268
|
}
|
|
223
269
|
return /(token|password|secret|api[_-]?key|webhook)/i.test(key) && String(value).trim().length > 0;
|
|
224
270
|
});
|
|
271
|
+
if (hasExplicitEmptyTelegramToken(channel)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
225
274
|
if (!hasAuth) {
|
|
226
275
|
throw new ClawChefError(`channels[] entry for ${channel.channel} requires at least one auth input (for example token/bot_token/access_token/token_file/use_env)`);
|
|
227
276
|
}
|
|
@@ -662,7 +711,7 @@ export async function loadRecipe(recipePath, options) {
|
|
|
662
711
|
}
|
|
663
712
|
const projected = projectRecipeForScope(firstParse.data, options);
|
|
664
713
|
assertNoInlineSecrets(projected);
|
|
665
|
-
const requiredKeys = options.scope === "workspace" ? new Set() : undefined;
|
|
714
|
+
const requiredKeys = options.scope === "workspace" || options.scope === "files" ? new Set() : undefined;
|
|
666
715
|
const vars = collectVars(projected, options.vars, requiredKeys);
|
|
667
716
|
const rendered = deepResolveTemplates(projected, vars, options.allowMissing);
|
|
668
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
|
})
|
|
@@ -107,10 +108,10 @@ const channelSchema = z
|
|
|
107
108
|
login_mode: z.enum(["interactive"]).optional(),
|
|
108
109
|
login_account: z.string().min(1).optional(),
|
|
109
110
|
name: z.string().min(1).optional(),
|
|
110
|
-
token: z.string().
|
|
111
|
+
token: z.string().optional(),
|
|
111
112
|
token_file: z.string().min(1).optional(),
|
|
112
113
|
use_env: z.boolean().optional(),
|
|
113
|
-
bot_token: z.string().
|
|
114
|
+
bot_token: z.string().optional(),
|
|
114
115
|
access_token: z.string().min(1).optional(),
|
|
115
116
|
app_token: z.string().min(1).optional(),
|
|
116
117
|
webhook_url: z.string().min(1).optional(),
|
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;
|