clawchef 0.1.1 → 0.1.2
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 +34 -54
- package/dist/orchestrator.js +58 -2
- package/dist/recipe.js +5 -0
- package/dist/scaffold.js +1 -26
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +1 -0
- package/dist/types.d.ts +1 -0
- package/package.json +9 -1
- package/src/orchestrator.ts +77 -2
- package/src/recipe.ts +5 -0
- package/src/scaffold.ts +1 -26
- package/src/schema.ts +1 -0
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
14
14
|
- Always runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
|
|
15
15
|
- If `openclaw` is missing, auto-installs the recipe version and skips factory reset.
|
|
16
16
|
- Starts OpenClaw gateway service after each recipe execution.
|
|
17
|
-
- Creates workspaces and agents (default workspace path: `~/.openclaw/
|
|
17
|
+
- Creates workspaces and agents (default workspace path: `~/.openclaw/workspace-<workspace-name>`).
|
|
18
|
+
- Supports workspace-level assets copy via `workspaces[].assets`.
|
|
18
19
|
- Materializes files into target workspaces.
|
|
19
20
|
- Installs skills.
|
|
20
21
|
- Supports plugin preinstall via `openclaw.plugins[]` and runtime `--plugin` flags.
|
|
@@ -34,20 +35,20 @@ clawchef cook recipes/sample.yaml
|
|
|
34
35
|
Run recipe from URL:
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
|
-
clawchef cook https://example.com/recipes/sample.yaml --provider remote
|
|
38
|
+
clawchef cook https://example.com/recipes/sample.yaml --provider remote
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
Run recipe from archive (default `recipe.yaml`):
|
|
41
42
|
|
|
42
43
|
```bash
|
|
43
|
-
clawchef cook ./bundle.tgz --provider mock
|
|
44
|
+
clawchef cook ./bundle.tgz --provider mock
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
Run specific recipe in directory or archive:
|
|
47
48
|
|
|
48
49
|
```bash
|
|
49
|
-
clawchef cook ./recipes-pack:team/recipe-prod.yaml --provider remote
|
|
50
|
-
clawchef cook https://example.com/recipes-pack.zip:team/recipe-prod.yaml --provider remote
|
|
50
|
+
clawchef cook ./recipes-pack:team/recipe-prod.yaml --provider remote
|
|
51
|
+
clawchef cook https://example.com/recipes-pack.zip:team/recipe-prod.yaml --provider remote
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
Dev mode:
|
|
@@ -59,13 +60,13 @@ clawchef cook recipes/sample.yaml --verbose
|
|
|
59
60
|
Run sample with mock provider:
|
|
60
61
|
|
|
61
62
|
```bash
|
|
62
|
-
clawchef cook recipes/sample.yaml --provider mock
|
|
63
|
+
clawchef cook recipes/sample.yaml --provider mock
|
|
63
64
|
```
|
|
64
65
|
|
|
65
66
|
Run `content_from` sample:
|
|
66
67
|
|
|
67
68
|
```bash
|
|
68
|
-
clawchef cook recipes/content-from-sample.yaml --provider mock
|
|
69
|
+
clawchef cook recipes/content-from-sample.yaml --provider mock
|
|
69
70
|
```
|
|
70
71
|
|
|
71
72
|
Skip reset confirmation prompt:
|
|
@@ -74,6 +75,9 @@ Skip reset confirmation prompt:
|
|
|
74
75
|
clawchef cook recipes/sample.yaml -s
|
|
75
76
|
```
|
|
76
77
|
|
|
78
|
+
Warning: `-s/--silent` suppresses the factory-reset confirmation and auto-chooses force reinstall on version mismatch.
|
|
79
|
+
Use it only in CI/non-interactive flows where destructive reset behavior is expected.
|
|
80
|
+
|
|
77
81
|
From-zero OpenClaw bootstrap (recommended):
|
|
78
82
|
|
|
79
83
|
```bash
|
|
@@ -83,19 +87,19 @@ CLAWCHEF_VAR_OPENAI_API_KEY=sk-... clawchef cook recipes/openclaw-from-zero.yaml
|
|
|
83
87
|
Telegram channel setup only:
|
|
84
88
|
|
|
85
89
|
```bash
|
|
86
|
-
CLAWCHEF_VAR_TELEGRAM_BOT_TOKEN=123456:abc... clawchef cook recipes/openclaw-telegram.yaml
|
|
90
|
+
CLAWCHEF_VAR_TELEGRAM_BOT_TOKEN=123456:abc... clawchef cook recipes/openclaw-telegram.yaml
|
|
87
91
|
```
|
|
88
92
|
|
|
89
93
|
Telegram mock channel setup (for tests):
|
|
90
94
|
|
|
91
95
|
```bash
|
|
92
|
-
CLAWCHEF_VAR_TELEGRAM_MOCK_API_KEY=test-key clawchef cook recipes/openclaw-telegram-mock.yaml
|
|
96
|
+
CLAWCHEF_VAR_TELEGRAM_MOCK_API_KEY=test-key clawchef cook recipes/openclaw-telegram-mock.yaml
|
|
93
97
|
```
|
|
94
98
|
|
|
95
99
|
Install plugin only for this run:
|
|
96
100
|
|
|
97
101
|
```bash
|
|
98
|
-
clawchef cook recipes/openclaw-telegram-mock.yaml --plugin openclaw-telegram-mock-channel
|
|
102
|
+
clawchef cook recipes/openclaw-telegram-mock.yaml --plugin openclaw-telegram-mock-channel
|
|
99
103
|
```
|
|
100
104
|
|
|
101
105
|
Remote HTTP orchestration:
|
|
@@ -103,7 +107,7 @@ Remote HTTP orchestration:
|
|
|
103
107
|
```bash
|
|
104
108
|
CLAWCHEF_REMOTE_BASE_URL=https://remote-openclaw.example.com \
|
|
105
109
|
CLAWCHEF_REMOTE_API_KEY=secret-token \
|
|
106
|
-
clawchef cook recipes/openclaw-remote-http.yaml --provider remote
|
|
110
|
+
clawchef cook recipes/openclaw-remote-http.yaml --provider remote --verbose
|
|
107
111
|
```
|
|
108
112
|
|
|
109
113
|
Validate recipe structure only:
|
|
@@ -138,7 +142,7 @@ clawchef scaffold ./my-recipe-project --name meetingbot
|
|
|
138
142
|
Scaffold output:
|
|
139
143
|
|
|
140
144
|
- `package.json` with `telegram-api-mock-server` in `devDependencies`
|
|
141
|
-
- `src/recipe.yaml` with `telegram-mock` channel
|
|
145
|
+
- `src/recipe.yaml` with `telegram-mock` channel, plugin preinstall, and `workspaces[].assets`
|
|
142
146
|
- `src/<project-name>-assets/{AGENTS.md,IDENTITY.md,SOUL.md,TOOLS.md}`
|
|
143
147
|
- `src/<project-name>-assets/scripts/scheduling.mjs`
|
|
144
148
|
- `test/recipe-smoke.test.mjs`
|
|
@@ -177,6 +181,9 @@ await scaffold("./my-recipe-project", {
|
|
|
177
181
|
- `silent` (default: `true` in Node API)
|
|
178
182
|
- `loadDotEnvFromCwd` (default: `true`)
|
|
179
183
|
|
|
184
|
+
Node API `silent: true` has the same risk as CLI `-s`: no reset confirmation and force reinstall on version mismatch.
|
|
185
|
+
Set `silent: false` when you want an interactive safety prompt.
|
|
186
|
+
|
|
180
187
|
Notes:
|
|
181
188
|
|
|
182
189
|
- `validate()` throws on invalid recipe.
|
|
@@ -243,7 +250,7 @@ Request payload format (POST):
|
|
|
243
250
|
"payload": {
|
|
244
251
|
"workspace": {
|
|
245
252
|
"name": "demo",
|
|
246
|
-
"path": "/home/runner/.openclaw/
|
|
253
|
+
"path": "/home/runner/.openclaw/workspace-demo"
|
|
247
254
|
}
|
|
248
255
|
}
|
|
249
256
|
}
|
|
@@ -356,54 +363,27 @@ Supported common fields:
|
|
|
356
363
|
- optional: `account`, `name`, `token`, `token_file`, `use_env`, `bot_token`, `access_token`, `app_token`, `webhook_url`, `webhook_path`, `signal_number`, `password`, `login`, `login_mode`, `login_account`
|
|
357
364
|
- advanced passthrough: `extra_flags` (`snake_case` keys become `--kebab-case` CLI flags)
|
|
358
365
|
|
|
359
|
-
### Telegram mock channel (for recipe tests)
|
|
360
|
-
|
|
361
|
-
Use `channel: "telegram-mock"` when you need to test Telegram-related recipe flows without connecting to the real Telegram network.
|
|
362
|
-
|
|
363
|
-
`clawchef` treats `telegram-mock` like any other channel and passes mock-specific flags through `extra_flags`.
|
|
364
|
-
|
|
365
|
-
Example:
|
|
366
|
-
|
|
367
|
-
```yaml
|
|
368
|
-
params:
|
|
369
|
-
telegram_mock_api_key:
|
|
370
|
-
required: true
|
|
371
|
-
|
|
372
|
-
channels:
|
|
373
|
-
- channel: "telegram-mock"
|
|
374
|
-
account: "testbot"
|
|
375
|
-
token: "${telegram_mock_api_key}"
|
|
376
|
-
extra_flags:
|
|
377
|
-
mock_bind: "127.0.0.1:18790"
|
|
378
|
-
mock_api_key: "${telegram_mock_api_key}"
|
|
379
|
-
mode: "webhook"
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
Typical test setup:
|
|
383
|
-
|
|
384
|
-
- Start OpenClaw with the telegram-mock plugin enabled.
|
|
385
|
-
- Run `clawchef cook ...` to configure workspaces/agents/channels.
|
|
386
|
-
- Use your external test program (HTTP API or Node.js SDK) to inject inbound mock messages and assert outbound events.
|
|
387
|
-
|
|
388
|
-
Login fields:
|
|
389
|
-
|
|
390
|
-
- `login: true` enables channel login step
|
|
391
|
-
- `login_mode`: currently supports `interactive`
|
|
392
|
-
- `login_account`: override account used for login (defaults to `account`)
|
|
393
|
-
|
|
394
|
-
Security rules:
|
|
395
|
-
|
|
396
|
-
- Do not inline secret values in `channels[]`.
|
|
397
|
-
- Use `${var}` placeholders and inject values via `--var` / `CLAWCHEF_VAR_*`.
|
|
398
|
-
|
|
399
366
|
## Workspace path behavior
|
|
400
367
|
|
|
401
368
|
- `workspaces[].path` is optional.
|
|
402
|
-
- If omitted, clawchef uses `~/.openclaw/
|
|
369
|
+
- If omitted, clawchef uses `~/.openclaw/workspace-<workspace-name>`.
|
|
370
|
+
- `workspaces[].assets` is optional.
|
|
371
|
+
- If `assets` is set, clawchef recursively copies files from that directory into the workspace root.
|
|
372
|
+
- `assets` is resolved relative to the recipe file path (unless absolute path is given).
|
|
373
|
+
- `files[]` runs after assets copy, so `files[]` can override copied asset files.
|
|
374
|
+
- Direct URL recipes do not support `workspaces[].assets` (assets must resolve to a local directory).
|
|
403
375
|
- If provided, relative paths are resolved from the recipe file directory.
|
|
404
376
|
- For direct URL recipe files, relative workspace paths are resolved from the current working directory.
|
|
405
377
|
- For directory/archive recipe references, relative workspace paths are resolved from the selected recipe file directory.
|
|
406
378
|
|
|
379
|
+
Example:
|
|
380
|
+
|
|
381
|
+
```yaml
|
|
382
|
+
workspaces:
|
|
383
|
+
- name: "workspace-meeting"
|
|
384
|
+
assets: "./meetingbot-assets"
|
|
385
|
+
```
|
|
386
|
+
|
|
407
387
|
## File content references
|
|
408
388
|
|
|
409
389
|
In `files[]`, set exactly one of:
|
package/dist/orchestrator.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { mkdir, access, copyFile, writeFile, readFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, access, copyFile, writeFile, readFile, readdir, stat } from "node:fs/promises";
|
|
4
4
|
import { constants } from "node:fs";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -32,7 +32,9 @@ function resolveWorkspacePath(recipeOrigin, name, configuredPath) {
|
|
|
32
32
|
}
|
|
33
33
|
return path.resolve(configuredPath);
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
const trimmedName = name.trim() || name;
|
|
36
|
+
const workspaceName = trimmedName.startsWith("workspace-") ? trimmedName : `workspace-${trimmedName}`;
|
|
37
|
+
return path.join(homedir(), ".openclaw", workspaceName);
|
|
36
38
|
}
|
|
37
39
|
function isHttpUrl(value) {
|
|
38
40
|
try {
|
|
@@ -78,6 +80,27 @@ async function readBinaryFromRef(recipeOrigin, reference) {
|
|
|
78
80
|
const bytes = await response.arrayBuffer();
|
|
79
81
|
return Buffer.from(bytes);
|
|
80
82
|
}
|
|
83
|
+
async function collectLocalAssetFiles(rootDir, relDir = "") {
|
|
84
|
+
const currentDir = relDir ? path.join(rootDir, relDir) : rootDir;
|
|
85
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const nextRel = relDir ? path.join(relDir, entry.name) : entry.name;
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
out.push(...await collectLocalAssetFiles(rootDir, nextRel));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (entry.isFile()) {
|
|
94
|
+
out.push({
|
|
95
|
+
absolutePath: path.join(rootDir, nextRel),
|
|
96
|
+
relativePath: nextRel,
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
throw new ClawChefError(`Unsupported entry in assets directory: ${path.join(rootDir, nextRel)}`);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
81
104
|
async function confirmFactoryReset(options) {
|
|
82
105
|
if (options.silent || options.dryRun) {
|
|
83
106
|
return true;
|
|
@@ -126,6 +149,39 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
126
149
|
}
|
|
127
150
|
await provider.createWorkspace(recipe.openclaw, { ...ws, path: absPath }, options.dryRun);
|
|
128
151
|
logger.info(`Workspace created: ${ws.name}`);
|
|
152
|
+
if (!ws.assets?.trim()) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const resolvedAssets = resolveFileRef(recipeOrigin, ws.assets);
|
|
156
|
+
if (resolvedAssets.kind !== "local") {
|
|
157
|
+
throw new ClawChefError(`Workspace assets must resolve to a local directory: ${ws.assets}. Direct URL recipes cannot use workspaces[].assets.`);
|
|
158
|
+
}
|
|
159
|
+
let assetDirStat;
|
|
160
|
+
try {
|
|
161
|
+
assetDirStat = await stat(resolvedAssets.value);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
165
|
+
throw new ClawChefError(`Workspace assets path is not accessible: ${resolvedAssets.value} (${message})`);
|
|
166
|
+
}
|
|
167
|
+
if (!assetDirStat.isDirectory()) {
|
|
168
|
+
throw new ClawChefError(`Workspace assets must be a directory: ${resolvedAssets.value}`);
|
|
169
|
+
}
|
|
170
|
+
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
171
|
+
for (const assetFile of assetFiles) {
|
|
172
|
+
if (provider.materializeFile) {
|
|
173
|
+
const content = await readFile(assetFile.absolutePath, "utf8");
|
|
174
|
+
await provider.materializeFile(recipe.openclaw, ws.name, assetFile.relativePath, content, true, options.dryRun);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const target = path.resolve(absPath, assetFile.relativePath);
|
|
178
|
+
if (!options.dryRun) {
|
|
179
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
180
|
+
await copyFile(assetFile.absolutePath, target);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
logger.info(`Workspace asset copied: ${ws.name}/${assetFile.relativePath}`);
|
|
184
|
+
}
|
|
129
185
|
}
|
|
130
186
|
for (const agent of recipe.agents ?? []) {
|
|
131
187
|
const workspacePath = workspacePaths.get(agent.workspace);
|
package/dist/recipe.js
CHANGED
|
@@ -125,6 +125,11 @@ function collectVars(recipe, cliVars) {
|
|
|
125
125
|
}
|
|
126
126
|
function semanticValidate(recipe) {
|
|
127
127
|
const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
|
|
128
|
+
for (const workspace of recipe.workspaces ?? []) {
|
|
129
|
+
if (workspace.assets !== undefined && !workspace.assets.trim()) {
|
|
130
|
+
throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
128
133
|
for (const agent of recipe.agents ?? []) {
|
|
129
134
|
if (!ws.has(agent.workspace)) {
|
|
130
135
|
throw new ClawChefError(`Agent ${agent.name} references missing workspace: ${agent.workspace}`);
|
package/dist/scaffold.js
CHANGED
|
@@ -76,38 +76,13 @@ openclaw:
|
|
|
76
76
|
|
|
77
77
|
workspaces:
|
|
78
78
|
- name: "\${workspace_name}"
|
|
79
|
+
assets: "./${projectName}-assets"
|
|
79
80
|
|
|
80
81
|
agents:
|
|
81
82
|
- workspace: "\${workspace_name}"
|
|
82
83
|
name: "\${agent_name}"
|
|
83
84
|
model: "\${agent_model}"
|
|
84
85
|
|
|
85
|
-
files:
|
|
86
|
-
- workspace: "\${workspace_name}"
|
|
87
|
-
path: "AGENTS.md"
|
|
88
|
-
overwrite: true
|
|
89
|
-
content_from: "./${projectName}-assets/AGENTS.md"
|
|
90
|
-
|
|
91
|
-
- workspace: "\${workspace_name}"
|
|
92
|
-
path: "IDENTITY.md"
|
|
93
|
-
overwrite: true
|
|
94
|
-
content_from: "./${projectName}-assets/IDENTITY.md"
|
|
95
|
-
|
|
96
|
-
- workspace: "\${workspace_name}"
|
|
97
|
-
path: "SOUL.md"
|
|
98
|
-
overwrite: true
|
|
99
|
-
content_from: "./${projectName}-assets/SOUL.md"
|
|
100
|
-
|
|
101
|
-
- workspace: "\${workspace_name}"
|
|
102
|
-
path: "TOOLS.md"
|
|
103
|
-
overwrite: true
|
|
104
|
-
content_from: "./${projectName}-assets/TOOLS.md"
|
|
105
|
-
|
|
106
|
-
- workspace: "\${workspace_name}"
|
|
107
|
-
path: "scripts/scheduling.mjs"
|
|
108
|
-
overwrite: true
|
|
109
|
-
content_from: "./${projectName}-assets/scripts/scheduling.mjs"
|
|
110
|
-
|
|
111
86
|
channels:
|
|
112
87
|
- channel: "telegram-mock"
|
|
113
88
|
account: "default"
|
package/dist/schema.d.ts
CHANGED
|
@@ -240,12 +240,15 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
240
240
|
workspaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
241
241
|
name: z.ZodString;
|
|
242
242
|
path: z.ZodOptional<z.ZodString>;
|
|
243
|
+
assets: z.ZodOptional<z.ZodString>;
|
|
243
244
|
}, "strict", z.ZodTypeAny, {
|
|
244
245
|
name: string;
|
|
245
246
|
path?: string | undefined;
|
|
247
|
+
assets?: string | undefined;
|
|
246
248
|
}, {
|
|
247
249
|
name: string;
|
|
248
250
|
path?: string | undefined;
|
|
251
|
+
assets?: string | undefined;
|
|
249
252
|
}>, "many">>;
|
|
250
253
|
channels: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
251
254
|
channel: z.ZodString;
|
|
@@ -479,6 +482,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
479
482
|
workspaces?: {
|
|
480
483
|
name: string;
|
|
481
484
|
path?: string | undefined;
|
|
485
|
+
assets?: string | undefined;
|
|
482
486
|
}[] | undefined;
|
|
483
487
|
channels?: {
|
|
484
488
|
channel: string;
|
|
@@ -586,6 +590,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
586
590
|
workspaces?: {
|
|
587
591
|
name: string;
|
|
588
592
|
path?: string | undefined;
|
|
593
|
+
assets?: string | undefined;
|
|
589
594
|
}[] | undefined;
|
|
590
595
|
channels?: {
|
|
591
596
|
channel: string;
|
package/dist/schema.js
CHANGED
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawchef",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Recipe-driven OpenClaw environment orchestrator",
|
|
5
|
+
"homepage": "https://renorzr.github.io/clawchef",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/renorzr/clawchef.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/renorzr/clawchef/issues"
|
|
12
|
+
},
|
|
5
13
|
"type": "module",
|
|
6
14
|
"main": "dist/api.js",
|
|
7
15
|
"types": "dist/api.d.ts",
|
package/src/orchestrator.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { mkdir, access, copyFile, writeFile, readFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, access, copyFile, writeFile, readFile, readdir, stat } from "node:fs/promises";
|
|
4
4
|
import { constants } from "node:fs";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -37,7 +37,9 @@ function resolveWorkspacePath(recipeOrigin: RecipeOrigin, name: string, configur
|
|
|
37
37
|
}
|
|
38
38
|
return path.resolve(configuredPath);
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
const trimmedName = name.trim() || name;
|
|
41
|
+
const workspaceName = trimmedName.startsWith("workspace-") ? trimmedName : `workspace-${trimmedName}`;
|
|
42
|
+
return path.join(homedir(), ".openclaw", workspaceName);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
function isHttpUrl(value: string): boolean {
|
|
@@ -87,6 +89,35 @@ async function readBinaryFromRef(recipeOrigin: RecipeOrigin, reference: string):
|
|
|
87
89
|
return Buffer.from(bytes);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
interface LocalAssetFile {
|
|
93
|
+
absolutePath: string;
|
|
94
|
+
relativePath: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function collectLocalAssetFiles(rootDir: string, relDir = ""): Promise<LocalAssetFile[]> {
|
|
98
|
+
const currentDir = relDir ? path.join(rootDir, relDir) : rootDir;
|
|
99
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
100
|
+
const out: LocalAssetFile[] = [];
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const nextRel = relDir ? path.join(relDir, entry.name) : entry.name;
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
out.push(...await collectLocalAssetFiles(rootDir, nextRel));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (entry.isFile()) {
|
|
109
|
+
out.push({
|
|
110
|
+
absolutePath: path.join(rootDir, nextRel),
|
|
111
|
+
relativePath: nextRel,
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
throw new ClawChefError(`Unsupported entry in assets directory: ${path.join(rootDir, nextRel)}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
90
121
|
async function confirmFactoryReset(options: RunOptions): Promise<boolean> {
|
|
91
122
|
if (options.silent || options.dryRun) {
|
|
92
123
|
return true;
|
|
@@ -146,6 +177,50 @@ export async function runRecipe(
|
|
|
146
177
|
}
|
|
147
178
|
await provider.createWorkspace(recipe.openclaw, { ...ws, path: absPath }, options.dryRun);
|
|
148
179
|
logger.info(`Workspace created: ${ws.name}`);
|
|
180
|
+
|
|
181
|
+
if (!ws.assets?.trim()) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const resolvedAssets = resolveFileRef(recipeOrigin, ws.assets);
|
|
186
|
+
if (resolvedAssets.kind !== "local") {
|
|
187
|
+
throw new ClawChefError(
|
|
188
|
+
`Workspace assets must resolve to a local directory: ${ws.assets}. Direct URL recipes cannot use workspaces[].assets.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let assetDirStat;
|
|
193
|
+
try {
|
|
194
|
+
assetDirStat = await stat(resolvedAssets.value);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
197
|
+
throw new ClawChefError(`Workspace assets path is not accessible: ${resolvedAssets.value} (${message})`);
|
|
198
|
+
}
|
|
199
|
+
if (!assetDirStat.isDirectory()) {
|
|
200
|
+
throw new ClawChefError(`Workspace assets must be a directory: ${resolvedAssets.value}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
204
|
+
for (const assetFile of assetFiles) {
|
|
205
|
+
if (provider.materializeFile) {
|
|
206
|
+
const content = await readFile(assetFile.absolutePath, "utf8");
|
|
207
|
+
await provider.materializeFile(
|
|
208
|
+
recipe.openclaw,
|
|
209
|
+
ws.name,
|
|
210
|
+
assetFile.relativePath,
|
|
211
|
+
content,
|
|
212
|
+
true,
|
|
213
|
+
options.dryRun,
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
const target = path.resolve(absPath, assetFile.relativePath);
|
|
217
|
+
if (!options.dryRun) {
|
|
218
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
219
|
+
await copyFile(assetFile.absolutePath, target);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
logger.info(`Workspace asset copied: ${ws.name}/${assetFile.relativePath}`);
|
|
223
|
+
}
|
|
149
224
|
}
|
|
150
225
|
|
|
151
226
|
for (const agent of recipe.agents ?? []) {
|
package/src/recipe.ts
CHANGED
|
@@ -169,6 +169,11 @@ function collectVars(recipe: Recipe, cliVars: Record<string, string>): Record<st
|
|
|
169
169
|
|
|
170
170
|
function semanticValidate(recipe: Recipe): void {
|
|
171
171
|
const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
|
|
172
|
+
for (const workspace of recipe.workspaces ?? []) {
|
|
173
|
+
if (workspace.assets !== undefined && !workspace.assets.trim()) {
|
|
174
|
+
throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
172
177
|
for (const agent of recipe.agents ?? []) {
|
|
173
178
|
if (!ws.has(agent.workspace)) {
|
|
174
179
|
throw new ClawChefError(`Agent ${agent.name} references missing workspace: ${agent.workspace}`);
|
package/src/scaffold.ts
CHANGED
|
@@ -89,38 +89,13 @@ openclaw:
|
|
|
89
89
|
|
|
90
90
|
workspaces:
|
|
91
91
|
- name: "\${workspace_name}"
|
|
92
|
+
assets: "./${projectName}-assets"
|
|
92
93
|
|
|
93
94
|
agents:
|
|
94
95
|
- workspace: "\${workspace_name}"
|
|
95
96
|
name: "\${agent_name}"
|
|
96
97
|
model: "\${agent_model}"
|
|
97
98
|
|
|
98
|
-
files:
|
|
99
|
-
- workspace: "\${workspace_name}"
|
|
100
|
-
path: "AGENTS.md"
|
|
101
|
-
overwrite: true
|
|
102
|
-
content_from: "./${projectName}-assets/AGENTS.md"
|
|
103
|
-
|
|
104
|
-
- workspace: "\${workspace_name}"
|
|
105
|
-
path: "IDENTITY.md"
|
|
106
|
-
overwrite: true
|
|
107
|
-
content_from: "./${projectName}-assets/IDENTITY.md"
|
|
108
|
-
|
|
109
|
-
- workspace: "\${workspace_name}"
|
|
110
|
-
path: "SOUL.md"
|
|
111
|
-
overwrite: true
|
|
112
|
-
content_from: "./${projectName}-assets/SOUL.md"
|
|
113
|
-
|
|
114
|
-
- workspace: "\${workspace_name}"
|
|
115
|
-
path: "TOOLS.md"
|
|
116
|
-
overwrite: true
|
|
117
|
-
content_from: "./${projectName}-assets/TOOLS.md"
|
|
118
|
-
|
|
119
|
-
- workspace: "\${workspace_name}"
|
|
120
|
-
path: "scripts/scheduling.mjs"
|
|
121
|
-
overwrite: true
|
|
122
|
-
content_from: "./${projectName}-assets/scripts/scheduling.mjs"
|
|
123
|
-
|
|
124
99
|
channels:
|
|
125
100
|
- channel: "telegram-mock"
|
|
126
101
|
account: "default"
|
package/src/schema.ts
CHANGED