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
package/src/orchestrator.ts
CHANGED
|
@@ -27,6 +27,34 @@ function truncateForLog(text: string, maxLength = 500): string {
|
|
|
27
27
|
return `${text.slice(0, maxLength)}... [truncated ${text.length - maxLength} chars]`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function toPosixPath(value: string): string {
|
|
31
|
+
return value.replaceAll("\\", "/");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function wildcardToRegExp(pattern: string): RegExp {
|
|
35
|
+
const escaped = toPosixPath(pattern)
|
|
36
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
37
|
+
.replace(/\*\*/g, "___DOUBLE_STAR___")
|
|
38
|
+
.replace(/\*/g, "[^/]*")
|
|
39
|
+
.replace(/___DOUBLE_STAR___/g, ".*");
|
|
40
|
+
return new RegExp(`^${escaped}$`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function matchesFilePatterns(patterns: string[], relativePath: string): boolean {
|
|
44
|
+
if (patterns.length === 0) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const normalized = toPosixPath(relativePath);
|
|
48
|
+
return patterns.some((pattern) => wildcardToRegExp(pattern).test(normalized));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function matchesAnyFilePattern(patterns: string[], candidates: string[]): boolean {
|
|
52
|
+
if (patterns.length === 0) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return candidates.some((candidate) => matchesFilePatterns(patterns, candidate));
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
function renderTemplateString(input: string, vars: Record<string, string>, allowMissing: boolean): string {
|
|
31
59
|
return input.replace(/\$\{([^}]+)\}/g, (_match, rawKey: string) => {
|
|
32
60
|
const key = String(rawKey).trim();
|
|
@@ -191,36 +219,42 @@ export async function runRecipe(
|
|
|
191
219
|
): Promise<void> {
|
|
192
220
|
const provider = createProvider(options);
|
|
193
221
|
const remoteMode = options.provider === "remote";
|
|
222
|
+
const filesOnlyScope = options.scope === "files";
|
|
223
|
+
const fileFilterEnabled = filesOnlyScope && options.filePatterns.length > 0;
|
|
194
224
|
const workspacePaths = new Map<string, string>();
|
|
195
225
|
const preserveExistingState = options.scope !== "full";
|
|
196
226
|
|
|
197
227
|
logger.info(`Running recipe: ${recipe.name}`);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
228
|
+
if (!filesOnlyScope) {
|
|
229
|
+
const versionResult = await provider.ensureVersion(
|
|
230
|
+
recipe.openclaw,
|
|
231
|
+
options.dryRun,
|
|
232
|
+
options.silent,
|
|
233
|
+
preserveExistingState,
|
|
234
|
+
);
|
|
235
|
+
logger.info(`OpenClaw version ready: ${recipe.openclaw.version}`);
|
|
236
|
+
|
|
237
|
+
if (versionResult.installedThisRun) {
|
|
238
|
+
logger.info("OpenClaw was installed in this run; skipping factory reset");
|
|
239
|
+
} else if (preserveExistingState) {
|
|
240
|
+
logger.info("Keeping existing OpenClaw state; skipping factory reset");
|
|
241
|
+
} else {
|
|
242
|
+
const confirmed = await confirmFactoryReset(options);
|
|
243
|
+
if (!confirmed) {
|
|
244
|
+
throw new ClawChefError("Aborted by user before factory reset");
|
|
245
|
+
}
|
|
246
|
+
await provider.factoryReset(recipe.openclaw, options.dryRun);
|
|
247
|
+
logger.info("Factory reset completed");
|
|
214
248
|
}
|
|
215
|
-
await provider.factoryReset(recipe.openclaw, options.dryRun);
|
|
216
|
-
logger.info("Factory reset completed");
|
|
217
|
-
}
|
|
218
249
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
250
|
+
const pluginSpecs = Array.from(new Set([...(recipe.openclaw.plugins ?? []), ...options.plugins].map((v) => v.trim())))
|
|
251
|
+
.filter((v) => v.length > 0);
|
|
252
|
+
for (const pluginSpec of pluginSpecs) {
|
|
253
|
+
await provider.installPlugin(recipe.openclaw, pluginSpec, options.dryRun);
|
|
254
|
+
logger.info(`Plugin preinstalled: ${pluginSpec}`);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
logger.info("Scope files: only syncing root/workspace assets and files");
|
|
224
258
|
}
|
|
225
259
|
|
|
226
260
|
const root = recipe.openclaw.root;
|
|
@@ -261,6 +295,11 @@ export async function runRecipe(
|
|
|
261
295
|
} else {
|
|
262
296
|
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
263
297
|
for (const assetFile of assetFiles) {
|
|
298
|
+
const patternCandidates = [assetFile.relativePath, `root/${assetFile.relativePath}`];
|
|
299
|
+
if (fileFilterEnabled && !matchesAnyFilePattern(options.filePatterns, patternCandidates)) {
|
|
300
|
+
logger.debug(`Filtered root asset: ${assetFile.relativePath}`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
264
303
|
const target = path.resolve(openclawRootPath, assetFile.relativePath);
|
|
265
304
|
if (!options.dryRun) {
|
|
266
305
|
await mkdir(path.dirname(target), { recursive: true });
|
|
@@ -272,6 +311,11 @@ export async function runRecipe(
|
|
|
272
311
|
}
|
|
273
312
|
|
|
274
313
|
for (const file of root.files ?? []) {
|
|
314
|
+
const patternCandidates = [file.path, `root/${file.path}`];
|
|
315
|
+
if (fileFilterEnabled && !matchesAnyFilePattern(options.filePatterns, patternCandidates)) {
|
|
316
|
+
logger.debug(`Filtered root file: ${file.path}`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
275
319
|
const target = path.resolve(openclawRootPath, file.path);
|
|
276
320
|
const targetDir = path.dirname(target);
|
|
277
321
|
|
|
@@ -306,8 +350,10 @@ export async function runRecipe(
|
|
|
306
350
|
if (!options.dryRun && !remoteMode) {
|
|
307
351
|
await mkdir(absPath, { recursive: true });
|
|
308
352
|
}
|
|
309
|
-
|
|
310
|
-
|
|
353
|
+
if (!filesOnlyScope) {
|
|
354
|
+
await provider.createWorkspace(recipe.openclaw, { ...ws, path: absPath }, options.dryRun);
|
|
355
|
+
logger.info(`Workspace created: ${ws.name}`);
|
|
356
|
+
}
|
|
311
357
|
|
|
312
358
|
if (!ws.assets?.trim()) {
|
|
313
359
|
continue;
|
|
@@ -337,6 +383,15 @@ export async function runRecipe(
|
|
|
337
383
|
|
|
338
384
|
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
339
385
|
for (const assetFile of assetFiles) {
|
|
386
|
+
const patternCandidates = [
|
|
387
|
+
assetFile.relativePath,
|
|
388
|
+
`${ws.name}/${assetFile.relativePath}`,
|
|
389
|
+
`workspace-${ws.name}/${assetFile.relativePath}`,
|
|
390
|
+
];
|
|
391
|
+
if (fileFilterEnabled && !matchesAnyFilePattern(options.filePatterns, patternCandidates)) {
|
|
392
|
+
logger.debug(`Filtered workspace asset: ${ws.name}/${assetFile.relativePath}`);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
340
395
|
if (provider.materializeFile) {
|
|
341
396
|
const content = await readFile(assetFile.absolutePath, "utf8");
|
|
342
397
|
await provider.materializeFile(
|
|
@@ -358,38 +413,60 @@ export async function runRecipe(
|
|
|
358
413
|
}
|
|
359
414
|
}
|
|
360
415
|
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
416
|
+
if (!filesOnlyScope) {
|
|
417
|
+
const pendingChannelBindings: Array<{ channel: ChannelDef; agent: string }> = [];
|
|
418
|
+
|
|
419
|
+
for (const agent of recipe.agents ?? []) {
|
|
420
|
+
const workspacePath = workspacePaths.get(agent.workspace);
|
|
421
|
+
if (!workspacePath) {
|
|
422
|
+
throw new ClawChefError(`Agent references missing workspace: ${agent.workspace}`);
|
|
423
|
+
}
|
|
424
|
+
await provider.createAgent(recipe.openclaw, agent, workspacePath, options.dryRun);
|
|
425
|
+
logger.info(`Agent created: ${agent.workspace}/${agent.name}`);
|
|
365
426
|
}
|
|
366
|
-
await provider.createAgent(recipe.openclaw, agent, workspacePath, options.dryRun);
|
|
367
|
-
logger.info(`Agent created: ${agent.workspace}/${agent.name}`);
|
|
368
|
-
}
|
|
369
427
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
428
|
+
for (const channel of recipe.channels ?? []) {
|
|
429
|
+
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
430
|
+
? { ...channel, account: channel.agent.trim() }
|
|
431
|
+
: channel;
|
|
432
|
+
const autoDisabledTelegram = shouldAutoDisableTelegramChannel(effectiveChannel);
|
|
375
433
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
434
|
+
await provider.configureChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
435
|
+
if (autoDisabledTelegram) {
|
|
436
|
+
logger.info(
|
|
437
|
+
`Telegram channel disabled due to empty bot token: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`,
|
|
438
|
+
);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
logger.info(`Channel configured: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
443
|
+
if (effectiveChannel.agent?.trim()) {
|
|
444
|
+
pendingChannelBindings.push({ channel: effectiveChannel, agent: effectiveChannel.agent });
|
|
445
|
+
}
|
|
382
446
|
}
|
|
383
447
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
448
|
+
if (pendingChannelBindings.length > 0) {
|
|
449
|
+
if (provider.bindChannelAgents) {
|
|
450
|
+
await provider.bindChannelAgents(recipe.openclaw, pendingChannelBindings, options.dryRun);
|
|
451
|
+
} else {
|
|
452
|
+
for (const binding of pendingChannelBindings) {
|
|
453
|
+
await provider.bindChannelAgent(recipe.openclaw, binding.channel, binding.agent, options.dryRun);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (const binding of pendingChannelBindings) {
|
|
458
|
+
logger.info(
|
|
459
|
+
`Channel bound to agent: ${binding.channel.channel}${binding.channel.account ? `/${binding.channel.account}` : ""} -> ${binding.agent}`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
390
462
|
}
|
|
391
463
|
}
|
|
392
464
|
|
|
465
|
+
if (!filesOnlyScope && recipe.openclaw.config_patch) {
|
|
466
|
+
await provider.applyConfigPatch(recipe.openclaw, recipe.openclaw.config_patch, options.dryRun);
|
|
467
|
+
logger.info("OpenClaw config patch applied");
|
|
468
|
+
}
|
|
469
|
+
|
|
393
470
|
for (const workspace of recipe.workspaces ?? []) {
|
|
394
471
|
const wsPath = workspacePaths.get(workspace.name);
|
|
395
472
|
if (!wsPath) {
|
|
@@ -397,6 +474,15 @@ export async function runRecipe(
|
|
|
397
474
|
}
|
|
398
475
|
|
|
399
476
|
for (const file of workspace.files ?? []) {
|
|
477
|
+
const patternCandidates = [
|
|
478
|
+
file.path,
|
|
479
|
+
`${workspace.name}/${file.path}`,
|
|
480
|
+
`workspace-${workspace.name}/${file.path}`,
|
|
481
|
+
];
|
|
482
|
+
if (fileFilterEnabled && !matchesAnyFilePattern(options.filePatterns, patternCandidates)) {
|
|
483
|
+
logger.debug(`Filtered workspace file: ${workspace.name}/${file.path}`);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
400
486
|
if (provider.materializeFile) {
|
|
401
487
|
let content = file.content;
|
|
402
488
|
if (content === undefined && file.content_from) {
|
|
@@ -453,64 +539,66 @@ export async function runRecipe(
|
|
|
453
539
|
}
|
|
454
540
|
}
|
|
455
541
|
|
|
456
|
-
|
|
457
|
-
for (const
|
|
458
|
-
|
|
459
|
-
|
|
542
|
+
if (!filesOnlyScope) {
|
|
543
|
+
for (const agent of recipe.agents ?? []) {
|
|
544
|
+
for (const skill of agent.skills ?? []) {
|
|
545
|
+
await provider.installSkill(recipe.openclaw, agent.workspace, agent.name, skill, options.dryRun);
|
|
546
|
+
logger.info(`Skill installed: ${agent.workspace}/${agent.name} -> ${skill}`);
|
|
547
|
+
}
|
|
460
548
|
}
|
|
461
|
-
}
|
|
462
549
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
550
|
+
for (const conv of recipe.conversations ?? []) {
|
|
551
|
+
for (const msg of conv.messages) {
|
|
552
|
+
await provider.sendMessage(recipe.openclaw, conv, msg.content, options.dryRun);
|
|
466
553
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
const reply = await provider.runAgent(recipe.openclaw, conv, options.dryRun);
|
|
474
|
-
if (msg.expect) {
|
|
475
|
-
try {
|
|
476
|
-
validateReply(reply, msg.expect);
|
|
477
|
-
logger.info(`Output assertions passed: ${conv.workspace}/${conv.agent}`);
|
|
478
|
-
} catch (err) {
|
|
479
|
-
logger.warn(
|
|
480
|
-
`Assertion failed reply (truncated): ${truncateForLog(reply)}`,
|
|
481
|
-
);
|
|
482
|
-
throw err;
|
|
554
|
+
const shouldRun = conv.run ?? Boolean(msg.expect);
|
|
555
|
+
if (shouldRun) {
|
|
556
|
+
if (options.dryRun) {
|
|
557
|
+
logger.info(`dry-run: skipping execution and output assertions: ${conv.workspace}/${conv.agent}`);
|
|
558
|
+
continue;
|
|
483
559
|
}
|
|
484
|
-
|
|
485
|
-
|
|
560
|
+
const reply = await provider.runAgent(recipe.openclaw, conv, options.dryRun);
|
|
561
|
+
if (msg.expect) {
|
|
562
|
+
try {
|
|
563
|
+
validateReply(reply, msg.expect);
|
|
564
|
+
logger.info(`Output assertions passed: ${conv.workspace}/${conv.agent}`);
|
|
565
|
+
} catch (err) {
|
|
566
|
+
logger.warn(
|
|
567
|
+
`Assertion failed reply (truncated): ${truncateForLog(reply)}`,
|
|
568
|
+
);
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
logger.info(`Agent executed: ${conv.workspace}/${conv.agent}`);
|
|
573
|
+
}
|
|
574
|
+
logger.debug(`Agent output: ${reply}`);
|
|
486
575
|
}
|
|
487
|
-
logger.debug(`Agent output: ${reply}`);
|
|
488
576
|
}
|
|
577
|
+
logger.info(`Preset messages sent: ${conv.workspace}/${conv.agent}`);
|
|
489
578
|
}
|
|
490
|
-
logger.info(`Preset messages sent: ${conv.workspace}/${conv.agent}`);
|
|
491
|
-
}
|
|
492
579
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
for (const channel of recipe.channels ?? []) {
|
|
501
|
-
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
502
|
-
? { ...channel, account: channel.agent.trim() }
|
|
503
|
-
: channel;
|
|
504
|
-
if (!effectiveChannel.login) {
|
|
505
|
-
continue;
|
|
580
|
+
await provider.startGateway(recipe.openclaw, options.gatewayMode, options.dryRun);
|
|
581
|
+
if (options.gatewayMode === "none") {
|
|
582
|
+
logger.info("Gateway start skipped by gateway mode: none");
|
|
583
|
+
} else {
|
|
584
|
+
logger.info(`Gateway started (${options.gatewayMode})`);
|
|
506
585
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
586
|
+
|
|
587
|
+
for (const channel of recipe.channels ?? []) {
|
|
588
|
+
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
589
|
+
? { ...channel, account: channel.agent.trim() }
|
|
590
|
+
: channel;
|
|
591
|
+
if (!effectiveChannel.login) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (!options.dryRun && !input.isTTY) {
|
|
595
|
+
throw new ClawChefError(
|
|
596
|
+
`Channel login for ${effectiveChannel.channel} requires an interactive terminal session`,
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
await provider.loginChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
600
|
+
logger.info(`Channel login completed: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
511
601
|
}
|
|
512
|
-
await provider.loginChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
513
|
-
logger.info(`Channel login completed: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
514
602
|
}
|
|
515
603
|
|
|
516
604
|
logger.info("Recipe execution completed");
|
package/src/recipe.ts
CHANGED
|
@@ -73,6 +73,33 @@ const CHANNEL_SECRET_FIELDS = ["token", "bot_token", "access_token", "app_token"
|
|
|
73
73
|
|
|
74
74
|
const TEMPLATE_TOKEN_RE = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/;
|
|
75
75
|
|
|
76
|
+
function assertNoInlineSecretsInObject(value: unknown, pathLabel: string): void {
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
79
|
+
assertNoInlineSecretsInObject(value[i], `${pathLabel}[${i}]`);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!value || typeof value !== "object") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
88
|
+
const nextPath = `${pathLabel}.${key}`;
|
|
89
|
+
if (
|
|
90
|
+
typeof nestedValue === "string" &&
|
|
91
|
+
/(token|password|secret|api[_-]?key|webhook)/i.test(key) &&
|
|
92
|
+
nestedValue.trim().length > 0 &&
|
|
93
|
+
!TEMPLATE_TOKEN_RE.test(nestedValue)
|
|
94
|
+
) {
|
|
95
|
+
throw new ClawChefError(
|
|
96
|
+
`Inline secret in ${nextPath} is not allowed. Use \${var} and pass it via --var or CLAWCHEF_VAR_*`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
assertNoInlineSecretsInObject(nestedValue, nextPath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
function hasExplicitEmptyTelegramToken(channel: ChannelDef): boolean {
|
|
77
104
|
if (channel.channel !== "telegram") {
|
|
78
105
|
return false;
|
|
@@ -98,6 +125,10 @@ function assertNoInlineSecrets(recipe: Recipe): void {
|
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
127
|
|
|
128
|
+
if (recipe.openclaw.config_patch) {
|
|
129
|
+
assertNoInlineSecretsInObject(recipe.openclaw.config_patch, "openclaw.config_patch");
|
|
130
|
+
}
|
|
131
|
+
|
|
101
132
|
for (const channel of recipe.channels ?? []) {
|
|
102
133
|
for (const field of CHANNEL_SECRET_FIELDS) {
|
|
103
134
|
const value = channel[field];
|
|
@@ -171,6 +202,21 @@ function collectVars(recipe: Recipe, cliVars: Record<string, string>, requiredKe
|
|
|
171
202
|
}
|
|
172
203
|
|
|
173
204
|
function projectRecipeForScope(recipe: Recipe, options: RunOptions): Recipe {
|
|
205
|
+
if (options.scope === "files") {
|
|
206
|
+
return {
|
|
207
|
+
...recipe,
|
|
208
|
+
openclaw: {
|
|
209
|
+
...recipe.openclaw,
|
|
210
|
+
version: "0.0.0",
|
|
211
|
+
bootstrap: undefined,
|
|
212
|
+
plugins: [],
|
|
213
|
+
},
|
|
214
|
+
channels: [],
|
|
215
|
+
agents: [],
|
|
216
|
+
conversations: [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
174
220
|
if (options.scope !== "workspace") {
|
|
175
221
|
return recipe;
|
|
176
222
|
}
|
|
@@ -832,7 +878,7 @@ export async function loadRecipe(recipePath: string, options: RunOptions): Promi
|
|
|
832
878
|
|
|
833
879
|
assertNoInlineSecrets(projected);
|
|
834
880
|
|
|
835
|
-
const requiredKeys = options.scope === "workspace" ? new Set<string>() : undefined;
|
|
881
|
+
const requiredKeys = options.scope === "workspace" || options.scope === "files" ? new Set<string>() : undefined;
|
|
836
882
|
const vars = collectVars(projected, options.vars, requiredKeys);
|
|
837
883
|
const rendered = deepResolveTemplates(projected, vars, options.allowMissing);
|
|
838
884
|
const secondParse = recipeSchema.safeParse(rendered);
|
package/src/schema.ts
CHANGED
|
@@ -78,6 +78,7 @@ const openClawSchema = z
|
|
|
78
78
|
install: z.enum(["auto", "always", "never"]).optional(),
|
|
79
79
|
plugins: z.array(z.string().min(1)).optional(),
|
|
80
80
|
root: openClawRootSchema.optional(),
|
|
81
|
+
config_patch: z.record(z.unknown()).optional(),
|
|
81
82
|
bootstrap: openClawBootstrapSchema.optional(),
|
|
82
83
|
commands: openClawCommandsSchema.optional(),
|
|
83
84
|
})
|
package/src/types.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
|
|
|
6
6
|
export interface OpenClawRemoteConfig {
|
|
@@ -64,6 +64,7 @@ export interface OpenClawSection {
|
|
|
64
64
|
install?: InstallPolicy;
|
|
65
65
|
plugins?: string[];
|
|
66
66
|
root?: OpenClawRootDef;
|
|
67
|
+
config_patch?: Record<string, unknown>;
|
|
67
68
|
bootstrap?: OpenClawBootstrap;
|
|
68
69
|
commands?: OpenClawCommandOverrides;
|
|
69
70
|
}
|
|
@@ -151,6 +152,7 @@ export interface Recipe {
|
|
|
151
152
|
export interface RunOptions {
|
|
152
153
|
vars: Record<string, string>;
|
|
153
154
|
plugins: string[];
|
|
155
|
+
filePatterns: string[];
|
|
154
156
|
scope: RunScope;
|
|
155
157
|
workspaceName?: string;
|
|
156
158
|
gatewayMode: GatewayMode;
|