clawchef 0.1.0 → 0.1.1
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 +56 -8
- package/dist/api.d.ts +4 -0
- package/dist/api.js +6 -0
- package/dist/cli.js +36 -0
- package/dist/openclaw/command-provider.d.ts +1 -0
- package/dist/openclaw/command-provider.js +30 -9
- package/dist/openclaw/mock-provider.d.ts +1 -0
- package/dist/openclaw/mock-provider.js +3 -0
- package/dist/openclaw/provider.d.ts +1 -0
- package/dist/openclaw/remote-provider.d.ts +1 -0
- package/dist/openclaw/remote-provider.js +5 -0
- package/dist/orchestrator.js +6 -0
- package/dist/recipe.js +4 -0
- package/dist/scaffold.d.ts +8 -0
- package/dist/scaffold.js +197 -0
- package/dist/schema.d.ts +12 -0
- package/dist/schema.js +2 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
- package/recipes/openclaw-local.yaml +0 -1
- package/recipes/openclaw-telegram-mock.yaml +2 -0
- package/recipes/openclaw-telegram.yaml +0 -1
- package/src/api.ts +10 -0
- package/src/cli.ts +39 -0
- package/src/openclaw/command-provider.ts +32 -9
- package/src/openclaw/mock-provider.ts +4 -0
- package/src/openclaw/provider.ts +1 -0
- package/src/openclaw/remote-provider.ts +11 -0
- package/src/orchestrator.ts +7 -0
- package/src/recipe.ts +9 -0
- package/src/scaffold.ts +222 -0
- package/src/schema.ts +2 -0
- package/src/types.ts +3 -0
package/README.md
CHANGED
|
@@ -17,9 +17,9 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
17
17
|
- Creates workspaces and agents (default workspace path: `~/.openclaw/workspaces/<workspace-name>`).
|
|
18
18
|
- Materializes files into target workspaces.
|
|
19
19
|
- Installs skills.
|
|
20
|
+
- Supports plugin preinstall via `openclaw.plugins[]` and runtime `--plugin` flags.
|
|
20
21
|
- Configures channels with `openclaw channels add`.
|
|
21
|
-
-
|
|
22
|
-
- Supports interactive channel login at the end of execution (`channels[].login: true`).
|
|
22
|
+
- Supports interactive channel login at the end of execution (`channels[].login: true`) for channels that expose login.
|
|
23
23
|
- Supports remote HTTP orchestration via runtime flags (`--provider remote`) when OpenClaw is reachable via API.
|
|
24
24
|
- Writes preset conversation messages.
|
|
25
25
|
- Runs agent and validates reply output.
|
|
@@ -27,9 +27,7 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
27
27
|
## Install and run
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
npm
|
|
31
|
-
npm run build
|
|
32
|
-
npm i -g .
|
|
30
|
+
npm i -g clawchef
|
|
33
31
|
clawchef cook recipes/sample.yaml
|
|
34
32
|
```
|
|
35
33
|
|
|
@@ -94,6 +92,12 @@ Telegram mock channel setup (for tests):
|
|
|
94
92
|
CLAWCHEF_VAR_TELEGRAM_MOCK_API_KEY=test-key clawchef cook recipes/openclaw-telegram-mock.yaml -s
|
|
95
93
|
```
|
|
96
94
|
|
|
95
|
+
Install plugin only for this run:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
clawchef cook recipes/openclaw-telegram-mock.yaml --plugin openclaw-telegram-mock-channel -s
|
|
99
|
+
```
|
|
100
|
+
|
|
97
101
|
Remote HTTP orchestration:
|
|
98
102
|
|
|
99
103
|
```bash
|
|
@@ -121,12 +125,32 @@ clawchef validate ./bundle.zip
|
|
|
121
125
|
clawchef validate ./bundle.zip:custom/recipe.yaml
|
|
122
126
|
```
|
|
123
127
|
|
|
128
|
+
Create a new recipe project scaffold:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
clawchef scaffold
|
|
132
|
+
clawchef scaffold ./my-recipe-project
|
|
133
|
+
clawchef scaffold ./my-recipe-project --name meetingbot
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`scaffold` prompts for project name (default: target directory name).
|
|
137
|
+
|
|
138
|
+
Scaffold output:
|
|
139
|
+
|
|
140
|
+
- `package.json` with `telegram-api-mock-server` in `devDependencies`
|
|
141
|
+
- `src/recipe.yaml` with `telegram-mock` channel and plugin preinstall
|
|
142
|
+
- `src/<project-name>-assets/{AGENTS.md,IDENTITY.md,SOUL.md,TOOLS.md}`
|
|
143
|
+
- `src/<project-name>-assets/scripts/scheduling.mjs`
|
|
144
|
+
- `test/recipe-smoke.test.mjs`
|
|
145
|
+
|
|
146
|
+
By default scaffold only writes files; it does not run `npm install`.
|
|
147
|
+
|
|
124
148
|
## Node.js API
|
|
125
149
|
|
|
126
150
|
You can call clawchef directly from Node.js (without invoking CLI commands).
|
|
127
151
|
|
|
128
152
|
```ts
|
|
129
|
-
import { cook, validate } from "clawchef";
|
|
153
|
+
import { cook, scaffold, validate } from "clawchef";
|
|
130
154
|
|
|
131
155
|
await validate("recipes/sample.yaml");
|
|
132
156
|
|
|
@@ -137,11 +161,16 @@ await cook("recipes/sample.yaml", {
|
|
|
137
161
|
openai_api_key: process.env.OPENAI_API_KEY ?? "",
|
|
138
162
|
},
|
|
139
163
|
});
|
|
164
|
+
|
|
165
|
+
await scaffold("./my-recipe-project", {
|
|
166
|
+
projectName: "my-recipe-project",
|
|
167
|
+
});
|
|
140
168
|
```
|
|
141
169
|
|
|
142
170
|
`cook()` options:
|
|
143
171
|
|
|
144
172
|
- `vars`: template variables (`Record<string, string>`)
|
|
173
|
+
- `plugins`: plugin npm specs to preinstall for this run (`string[]`)
|
|
145
174
|
- `provider`: `command | remote | mock`
|
|
146
175
|
- `remote`: remote provider config (same fields as CLI remote flags)
|
|
147
176
|
- `dryRun`, `allowMissing`, `verbose`
|
|
@@ -152,6 +181,7 @@ Notes:
|
|
|
152
181
|
|
|
153
182
|
- `validate()` throws on invalid recipe.
|
|
154
183
|
- `cook()` throws on runtime/configuration errors.
|
|
184
|
+
- `scaffold()` creates `package.json`, `src/recipe.yaml`, `src/<project-name>-assets`, and `test/`.
|
|
155
185
|
|
|
156
186
|
## Variable precedence
|
|
157
187
|
|
|
@@ -233,6 +263,7 @@ Expected response format:
|
|
|
233
263
|
Supported operation values sent by clawchef:
|
|
234
264
|
|
|
235
265
|
- `ensure_version`, `factory_reset`, `start_gateway`
|
|
266
|
+
- `install_plugin`
|
|
236
267
|
- `create_workspace`, `create_agent`, `materialize_file`, `install_skill`
|
|
237
268
|
- `configure_channel`, `login_channel`
|
|
238
269
|
- `run_agent`
|
|
@@ -258,6 +289,20 @@ Supported fields include:
|
|
|
258
289
|
- setup toggles: `skip_channels`, `skip_skills`, `skip_health`, `skip_ui`, `skip_daemon`, `install_daemon`
|
|
259
290
|
- auth/provider: `auth_choice`, `openai_api_key`, `anthropic_api_key`, `openrouter_api_key`, `xai_api_key`, `gemini_api_key`, `ai_gateway_api_key`, `cloudflare_ai_gateway_api_key`, `token`, `token_provider`, `token_profile_id`
|
|
260
291
|
|
|
292
|
+
### Plugin preinstall
|
|
293
|
+
|
|
294
|
+
Use `openclaw.plugins` to preinstall plugin packages before workspace/channel setup.
|
|
295
|
+
|
|
296
|
+
Example:
|
|
297
|
+
|
|
298
|
+
```yaml
|
|
299
|
+
openclaw:
|
|
300
|
+
version: "2026.2.9"
|
|
301
|
+
plugins:
|
|
302
|
+
- "openclaw-telegram-mock-channel"
|
|
303
|
+
- "@scope/custom-channel-plugin@1.2.3"
|
|
304
|
+
```
|
|
305
|
+
|
|
261
306
|
When `openclaw.bootstrap` contains provider keys, `clawchef` also injects them into runtime env for `openclaw agent --local`.
|
|
262
307
|
|
|
263
308
|
For `command` provider, default command templates are:
|
|
@@ -265,9 +310,9 @@ For `command` provider, default command templates are:
|
|
|
265
310
|
- `use_version`: `${bin} --version`
|
|
266
311
|
- `install_version`: `npm install -g openclaw@${version}`
|
|
267
312
|
- `uninstall_version`: `npm uninstall -g openclaw`
|
|
313
|
+
- `install_plugin`: `${bin} plugins install ${plugin_spec_q}`
|
|
268
314
|
- `factory_reset`: `${bin} reset --scope full --yes --non-interactive`
|
|
269
315
|
- `start_gateway`: `${bin} gateway start`
|
|
270
|
-
- `enable_plugin`: `${bin} plugins enable ${channel_q}`
|
|
271
316
|
- `login_channel`: `${bin} channels login --channel ${channel_q}${account_arg}`
|
|
272
317
|
- `create_workspace`: generated from `openclaw.bootstrap` (override with `openclaw.commands.create_workspace`)
|
|
273
318
|
- `create_agent`: `${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json`
|
|
@@ -277,10 +322,14 @@ For `command` provider, default command templates are:
|
|
|
277
322
|
|
|
278
323
|
You can override any command under `openclaw.commands` in recipe.
|
|
279
324
|
|
|
325
|
+
By default, clawchef does not auto-run `openclaw plugins enable <channel>` during channel configuration.
|
|
326
|
+
If you need custom enable behavior, set `openclaw.commands.enable_plugin` explicitly.
|
|
327
|
+
|
|
280
328
|
## Channels
|
|
281
329
|
|
|
282
330
|
Use `channels[]` to configure accounts via `openclaw channels add`.
|
|
283
331
|
If `login: true` is set, clawchef runs channel login at the end of `cook` (after gateway start).
|
|
332
|
+
Telegram does not support `openclaw channels login`; do not set `login` for `channel: "telegram"`.
|
|
284
333
|
|
|
285
334
|
Example:
|
|
286
335
|
|
|
@@ -289,7 +338,6 @@ channels:
|
|
|
289
338
|
- channel: "telegram"
|
|
290
339
|
token: "${telegram_bot_token}"
|
|
291
340
|
account: "default"
|
|
292
|
-
login: true
|
|
293
341
|
|
|
294
342
|
- channel: "slack"
|
|
295
343
|
bot_token: "${slack_bot_token}"
|
package/dist/api.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { OpenClawProvider, OpenClawRemoteConfig } from "./types.js";
|
|
2
|
+
import type { ScaffoldOptions, ScaffoldResult } from "./scaffold.js";
|
|
2
3
|
export interface CookOptions {
|
|
3
4
|
vars?: Record<string, string>;
|
|
5
|
+
plugins?: string[];
|
|
4
6
|
dryRun?: boolean;
|
|
5
7
|
allowMissing?: boolean;
|
|
6
8
|
verbose?: boolean;
|
|
@@ -11,4 +13,6 @@ export interface CookOptions {
|
|
|
11
13
|
}
|
|
12
14
|
export declare function cook(recipeRef: string, options?: CookOptions): Promise<void>;
|
|
13
15
|
export declare function validate(recipeRef: string): Promise<void>;
|
|
16
|
+
export declare function scaffold(targetDir?: string, options?: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
14
17
|
export type { OpenClawProvider, OpenClawRemoteConfig };
|
|
18
|
+
export type { ScaffoldOptions, ScaffoldResult };
|
package/dist/api.js
CHANGED
|
@@ -4,10 +4,13 @@ import { importDotEnvFromCwd } from "./env.js";
|
|
|
4
4
|
import { Logger } from "./logger.js";
|
|
5
5
|
import { runRecipe } from "./orchestrator.js";
|
|
6
6
|
import { loadRecipe, loadRecipeText } from "./recipe.js";
|
|
7
|
+
import { scaffoldProject } from "./scaffold.js";
|
|
7
8
|
import { recipeSchema } from "./schema.js";
|
|
8
9
|
function normalizeCookOptions(options) {
|
|
10
|
+
const plugins = Array.from(new Set((options.plugins ?? []).map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
9
11
|
return {
|
|
10
12
|
vars: options.vars ?? {},
|
|
13
|
+
plugins,
|
|
11
14
|
dryRun: Boolean(options.dryRun),
|
|
12
15
|
allowMissing: Boolean(options.allowMissing),
|
|
13
16
|
verbose: Boolean(options.verbose),
|
|
@@ -47,3 +50,6 @@ export async function validate(recipeRef) {
|
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
}
|
|
53
|
+
export async function scaffold(targetDir, options = {}) {
|
|
54
|
+
return scaffoldProject(targetDir, options);
|
|
55
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,11 @@ import { Logger } from "./logger.js";
|
|
|
4
4
|
import { runRecipe } from "./orchestrator.js";
|
|
5
5
|
import { loadRecipe, loadRecipeText } from "./recipe.js";
|
|
6
6
|
import { recipeSchema } from "./schema.js";
|
|
7
|
+
import { scaffoldProject } from "./scaffold.js";
|
|
7
8
|
import YAML from "js-yaml";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
11
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
8
12
|
function parseVarFlags(values) {
|
|
9
13
|
const out = {};
|
|
10
14
|
for (const item of values) {
|
|
@@ -18,6 +22,9 @@ function parseVarFlags(values) {
|
|
|
18
22
|
}
|
|
19
23
|
return out;
|
|
20
24
|
}
|
|
25
|
+
function parsePluginFlags(values) {
|
|
26
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
27
|
+
}
|
|
21
28
|
function readEnv(name) {
|
|
22
29
|
const value = process.env[name];
|
|
23
30
|
if (value === undefined) {
|
|
@@ -42,6 +49,20 @@ function parseOptionalInt(value, fieldName) {
|
|
|
42
49
|
}
|
|
43
50
|
return parsed;
|
|
44
51
|
}
|
|
52
|
+
async function promptProjectName(defaultValue) {
|
|
53
|
+
if (!input.isTTY) {
|
|
54
|
+
return defaultValue;
|
|
55
|
+
}
|
|
56
|
+
const rl = createInterface({ input, output });
|
|
57
|
+
try {
|
|
58
|
+
const answer = await rl.question(`Project name [${defaultValue}]: `);
|
|
59
|
+
const value = answer.trim();
|
|
60
|
+
return value || defaultValue;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
rl.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
45
66
|
export function buildCli() {
|
|
46
67
|
const program = new Command();
|
|
47
68
|
program
|
|
@@ -57,6 +78,7 @@ export function buildCli() {
|
|
|
57
78
|
.option("--verbose", "Verbose logging", false)
|
|
58
79
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
59
80
|
.option("--provider <provider>", "Execution provider: command | remote | mock")
|
|
81
|
+
.option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p) => p.concat([v]), [])
|
|
60
82
|
.option("--remote-base-url <url>", "Remote OpenClaw API base URL")
|
|
61
83
|
.option("--remote-api-key <key>", "Remote OpenClaw API key")
|
|
62
84
|
.option("--remote-api-header <header>", "Remote auth header name")
|
|
@@ -67,6 +89,7 @@ export function buildCli() {
|
|
|
67
89
|
const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
|
|
68
90
|
const options = {
|
|
69
91
|
vars: parseVarFlags(opts.var),
|
|
92
|
+
plugins: parsePluginFlags(opts.plugin),
|
|
70
93
|
dryRun: Boolean(opts.dryRun),
|
|
71
94
|
allowMissing: Boolean(opts.allowMissing),
|
|
72
95
|
verbose: Boolean(opts.verbose),
|
|
@@ -92,6 +115,19 @@ export function buildCli() {
|
|
|
92
115
|
}
|
|
93
116
|
}
|
|
94
117
|
});
|
|
118
|
+
program
|
|
119
|
+
.command("scaffold")
|
|
120
|
+
.argument("[dir]", "Target directory (default: current directory)")
|
|
121
|
+
.option("--name <project-name>", "Project name (default: directory name)")
|
|
122
|
+
.action(async (dir, opts) => {
|
|
123
|
+
const resolvedDir = path.resolve(dir?.trim() ? dir : process.cwd());
|
|
124
|
+
const defaultName = path.basename(resolvedDir);
|
|
125
|
+
const projectName = opts.name?.trim() ? opts.name.trim() : await promptProjectName(defaultName);
|
|
126
|
+
const result = await scaffoldProject(resolvedDir, { projectName });
|
|
127
|
+
process.stdout.write(`Scaffold created at ${result.targetDir}\n`);
|
|
128
|
+
process.stdout.write(`Project name: ${result.projectName}\n`);
|
|
129
|
+
process.stdout.write("Next: run npm install\n");
|
|
130
|
+
});
|
|
95
131
|
program
|
|
96
132
|
.command("validate")
|
|
97
133
|
.argument("<recipe>", "Recipe path/URL/dir/archive[:file]")
|
|
@@ -4,6 +4,7 @@ export declare class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
4
4
|
private readonly stagedMessages;
|
|
5
5
|
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean): Promise<EnsureVersionResult>;
|
|
6
6
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
7
|
+
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
7
8
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
8
9
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
9
10
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
@@ -10,9 +10,10 @@ const DEFAULT_COMMANDS = {
|
|
|
10
10
|
use_version: "${bin} --version",
|
|
11
11
|
install_version: "npm install -g openclaw@${version}",
|
|
12
12
|
uninstall_version: "npm uninstall -g openclaw",
|
|
13
|
+
install_plugin: "${bin} plugins install ${plugin_spec_q}",
|
|
13
14
|
factory_reset: "${bin} reset --scope full --yes --non-interactive",
|
|
14
15
|
start_gateway: "${bin} gateway start",
|
|
15
|
-
enable_plugin: "
|
|
16
|
+
enable_plugin: "",
|
|
16
17
|
login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
|
|
17
18
|
create_agent: "${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json",
|
|
18
19
|
install_skill: "${bin} skills check",
|
|
@@ -302,6 +303,23 @@ export class CommandOpenClawProvider {
|
|
|
302
303
|
}
|
|
303
304
|
await runShell(resetCmd, dryRun);
|
|
304
305
|
}
|
|
306
|
+
async installPlugin(config, pluginSpec, dryRun) {
|
|
307
|
+
const trimmed = pluginSpec.trim();
|
|
308
|
+
if (!trimmed) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const bin = config.bin ?? "openclaw";
|
|
312
|
+
const cmd = commandFor(config, "install_plugin", {
|
|
313
|
+
bin,
|
|
314
|
+
version: config.version,
|
|
315
|
+
plugin_spec: trimmed,
|
|
316
|
+
plugin_spec_q: shellQuote(trimmed),
|
|
317
|
+
});
|
|
318
|
+
if (!cmd.trim()) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
await runShell(cmd, dryRun);
|
|
322
|
+
}
|
|
305
323
|
async startGateway(config, dryRun) {
|
|
306
324
|
const bin = config.bin ?? "openclaw";
|
|
307
325
|
const startCmd = commandFor(config, "start_gateway", { bin, version: config.version });
|
|
@@ -328,14 +346,17 @@ export class CommandOpenClawProvider {
|
|
|
328
346
|
}
|
|
329
347
|
async configureChannel(config, channel, dryRun) {
|
|
330
348
|
const bin = config.bin ?? "openclaw";
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
349
|
+
const enablePluginTemplate = config.commands?.enable_plugin;
|
|
350
|
+
if (enablePluginTemplate?.trim()) {
|
|
351
|
+
const enablePluginCmd = fillTemplate(enablePluginTemplate, {
|
|
352
|
+
bin,
|
|
353
|
+
version: config.version,
|
|
354
|
+
channel: channel.channel,
|
|
355
|
+
channel_q: shellQuote(channel.channel),
|
|
356
|
+
});
|
|
357
|
+
if (enablePluginCmd.trim()) {
|
|
358
|
+
await runShell(enablePluginCmd, dryRun);
|
|
359
|
+
}
|
|
339
360
|
}
|
|
340
361
|
const flags = [
|
|
341
362
|
"--channel",
|
|
@@ -3,6 +3,7 @@ import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from
|
|
|
3
3
|
export declare class MockOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private state;
|
|
5
5
|
ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean): Promise<EnsureVersionResult>;
|
|
6
|
+
installPlugin(_config: OpenClawSection, _pluginSpec: string, _dryRun: boolean): Promise<void>;
|
|
6
7
|
factoryReset(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
|
|
7
8
|
startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
|
|
8
9
|
createWorkspace(_config: OpenClawSection, workspace: ResolvedWorkspaceDef, _dryRun: boolean): Promise<void>;
|
|
@@ -25,6 +25,9 @@ export class MockOpenClawProvider {
|
|
|
25
25
|
this.state.currentVersion = config.version;
|
|
26
26
|
return { installedThisRun };
|
|
27
27
|
}
|
|
28
|
+
async installPlugin(_config, _pluginSpec, _dryRun) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
28
31
|
async factoryReset(_config, _dryRun) {
|
|
29
32
|
this.state.workspaces.clear();
|
|
30
33
|
this.state.channels.clear();
|
|
@@ -7,6 +7,7 @@ export interface EnsureVersionResult {
|
|
|
7
7
|
}
|
|
8
8
|
export interface OpenClawProvider {
|
|
9
9
|
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean): Promise<EnsureVersionResult>;
|
|
10
|
+
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
10
11
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
11
12
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
12
13
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
@@ -6,6 +6,7 @@ export declare class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
6
6
|
constructor(remoteConfig: Partial<OpenClawRemoteConfig>);
|
|
7
7
|
private perform;
|
|
8
8
|
ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean): Promise<EnsureVersionResult>;
|
|
9
|
+
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
9
10
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
10
11
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
11
12
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
@@ -102,6 +102,11 @@ export class RemoteOpenClawProvider {
|
|
|
102
102
|
installedThisRun: Boolean(result.installed_this_run),
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
|
+
async installPlugin(config, pluginSpec, dryRun) {
|
|
106
|
+
await this.perform(config, "install_plugin", {
|
|
107
|
+
plugin_spec: pluginSpec,
|
|
108
|
+
}, dryRun);
|
|
109
|
+
}
|
|
105
110
|
async factoryReset(config, dryRun) {
|
|
106
111
|
await this.perform(config, "factory_reset", undefined, dryRun);
|
|
107
112
|
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -112,6 +112,12 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
112
112
|
await provider.factoryReset(recipe.openclaw, options.dryRun);
|
|
113
113
|
logger.info("Factory reset completed");
|
|
114
114
|
}
|
|
115
|
+
const pluginSpecs = Array.from(new Set([...(recipe.openclaw.plugins ?? []), ...options.plugins].map((v) => v.trim())))
|
|
116
|
+
.filter((v) => v.length > 0);
|
|
117
|
+
for (const pluginSpec of pluginSpecs) {
|
|
118
|
+
await provider.installPlugin(recipe.openclaw, pluginSpec, options.dryRun);
|
|
119
|
+
logger.info(`Plugin preinstalled: ${pluginSpec}`);
|
|
120
|
+
}
|
|
115
121
|
for (const ws of recipe.workspaces ?? []) {
|
|
116
122
|
const absPath = resolveWorkspacePath(recipeOrigin, ws.name, ws.path);
|
|
117
123
|
workspacePaths.set(ws.name, absPath);
|
package/dist/recipe.js
CHANGED
|
@@ -148,6 +148,10 @@ function semanticValidate(recipe) {
|
|
|
148
148
|
if (!ALLOWED_CHANNELS.has(channel.channel)) {
|
|
149
149
|
throw new ClawChefError(`Unsupported channel: ${channel.channel}. Allowed: ${Array.from(ALLOWED_CHANNELS).join(", ")}`);
|
|
150
150
|
}
|
|
151
|
+
if (channel.channel === "telegram" &&
|
|
152
|
+
(channel.login || channel.login_mode !== undefined || channel.login_account !== undefined)) {
|
|
153
|
+
throw new ClawChefError("channels[] entry for telegram does not support login/login_mode/login_account. Configure token (or use_env/token_file), then start gateway.");
|
|
154
|
+
}
|
|
151
155
|
const hasAuth = Boolean(channel.use_env) ||
|
|
152
156
|
Boolean(channel.token?.trim()) ||
|
|
153
157
|
Boolean(channel.token_file?.trim()) ||
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { access, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { ClawChefError } from "./errors.js";
|
|
5
|
+
function normalizeProjectName(value) {
|
|
6
|
+
const normalized = value
|
|
7
|
+
.trim()
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[\s_]+/g, "-")
|
|
10
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
11
|
+
.replace(/-+/g, "-")
|
|
12
|
+
.replace(/^-|-$/g, "");
|
|
13
|
+
if (!normalized) {
|
|
14
|
+
throw new ClawChefError("Project name is empty after normalization. Use letters, numbers, spaces, underscore, or dash.");
|
|
15
|
+
}
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
async function pathExists(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
await access(filePath, constants.F_OK);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function ensureDirectoryEmpty(targetDir) {
|
|
28
|
+
const exists = await pathExists(targetDir);
|
|
29
|
+
if (!exists) {
|
|
30
|
+
await mkdir(targetDir, { recursive: true });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const entries = await readdir(targetDir);
|
|
34
|
+
if (entries.length > 0) {
|
|
35
|
+
throw new ClawChefError(`Target directory is not empty: ${targetDir}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function makePackageJson(projectName) {
|
|
39
|
+
const content = {
|
|
40
|
+
name: `${projectName}-recipe`,
|
|
41
|
+
version: "0.1.0",
|
|
42
|
+
private: true,
|
|
43
|
+
type: "module",
|
|
44
|
+
scripts: {
|
|
45
|
+
"test:recipe": "node --test test/recipe-smoke.test.mjs",
|
|
46
|
+
test: "node --test \"test/**/*.test.mjs\"",
|
|
47
|
+
},
|
|
48
|
+
devDependencies: {
|
|
49
|
+
"telegram-api-mock-server": "^0.1.5",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
return `${JSON.stringify(content, null, 2)}\n`;
|
|
53
|
+
}
|
|
54
|
+
function makeRecipeYaml(projectName) {
|
|
55
|
+
return `version: "1"
|
|
56
|
+
name: "${projectName}"
|
|
57
|
+
|
|
58
|
+
params:
|
|
59
|
+
openclaw_version:
|
|
60
|
+
default: "2026.2.9"
|
|
61
|
+
workspace_name:
|
|
62
|
+
default: "${projectName}"
|
|
63
|
+
agent_name:
|
|
64
|
+
default: "${projectName}"
|
|
65
|
+
agent_model:
|
|
66
|
+
default: "openai/gpt-4.1"
|
|
67
|
+
telegram_mock_api_key:
|
|
68
|
+
required: true
|
|
69
|
+
|
|
70
|
+
openclaw:
|
|
71
|
+
bin: "openclaw"
|
|
72
|
+
version: "\${openclaw_version}"
|
|
73
|
+
install: "never"
|
|
74
|
+
plugins:
|
|
75
|
+
- "openclaw-telegram-mock-channel"
|
|
76
|
+
|
|
77
|
+
workspaces:
|
|
78
|
+
- name: "\${workspace_name}"
|
|
79
|
+
|
|
80
|
+
agents:
|
|
81
|
+
- workspace: "\${workspace_name}"
|
|
82
|
+
name: "\${agent_name}"
|
|
83
|
+
model: "\${agent_model}"
|
|
84
|
+
|
|
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
|
+
channels:
|
|
112
|
+
- channel: "telegram-mock"
|
|
113
|
+
account: "default"
|
|
114
|
+
token: "\${telegram_mock_api_key}"
|
|
115
|
+
extra_flags:
|
|
116
|
+
mock_bind: "127.0.0.1:18790"
|
|
117
|
+
mock_api_key: "\${telegram_mock_api_key}"
|
|
118
|
+
mode: "webhook"
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
function makeAgentsDoc(projectName) {
|
|
122
|
+
return `# ${projectName}
|
|
123
|
+
|
|
124
|
+
Project scaffold generated by clawchef.
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
function makeIdentityDoc(projectName) {
|
|
128
|
+
return `# Identity
|
|
129
|
+
|
|
130
|
+
You are the ${projectName} assistant.
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
function makeSoulDoc() {
|
|
134
|
+
return `# Soul
|
|
135
|
+
|
|
136
|
+
Keep responses practical, concise, and action-oriented.
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
function makeToolsDoc() {
|
|
140
|
+
return `# Tools
|
|
141
|
+
|
|
142
|
+
- Use available workspace files first.
|
|
143
|
+
- Ask for missing secrets explicitly.
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
function makeSchedulingScript() {
|
|
147
|
+
return `#!/usr/bin/env node
|
|
148
|
+
import process from "node:process";
|
|
149
|
+
|
|
150
|
+
const message = process.argv.slice(2).join(" ").trim();
|
|
151
|
+
if (!message) {
|
|
152
|
+
console.error("Usage: node scripts/scheduling.mjs <message>");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log("[scheduling] " + message);
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
function makeRecipeSmokeTest() {
|
|
160
|
+
return `import test from "node:test";
|
|
161
|
+
import assert from "node:assert/strict";
|
|
162
|
+
import { access } from "node:fs/promises";
|
|
163
|
+
|
|
164
|
+
test("recipe scaffold files exist", async () => {
|
|
165
|
+
await access("src/recipe.yaml");
|
|
166
|
+
await access("package.json");
|
|
167
|
+
assert.ok(true);
|
|
168
|
+
});
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
export async function scaffoldProject(targetDirArg, options = {}) {
|
|
172
|
+
const targetDir = path.resolve(targetDirArg?.trim() ? targetDirArg : process.cwd());
|
|
173
|
+
await ensureDirectoryEmpty(targetDir);
|
|
174
|
+
const defaultName = path.basename(targetDir);
|
|
175
|
+
const rawProjectName = options.projectName?.trim() || defaultName;
|
|
176
|
+
const projectName = normalizeProjectName(rawProjectName);
|
|
177
|
+
const srcDir = path.join(targetDir, "src");
|
|
178
|
+
const assetsDir = path.join(srcDir, `${projectName}-assets`);
|
|
179
|
+
const assetsScriptsDir = path.join(assetsDir, "scripts");
|
|
180
|
+
const testDir = path.join(targetDir, "test");
|
|
181
|
+
await mkdir(srcDir, { recursive: true });
|
|
182
|
+
await mkdir(assetsDir, { recursive: true });
|
|
183
|
+
await mkdir(assetsScriptsDir, { recursive: true });
|
|
184
|
+
await mkdir(testDir, { recursive: true });
|
|
185
|
+
await writeFile(path.join(targetDir, "package.json"), makePackageJson(projectName), "utf8");
|
|
186
|
+
await writeFile(path.join(srcDir, "recipe.yaml"), makeRecipeYaml(projectName), "utf8");
|
|
187
|
+
await writeFile(path.join(assetsDir, "AGENTS.md"), makeAgentsDoc(projectName), "utf8");
|
|
188
|
+
await writeFile(path.join(assetsDir, "IDENTITY.md"), makeIdentityDoc(projectName), "utf8");
|
|
189
|
+
await writeFile(path.join(assetsDir, "SOUL.md"), makeSoulDoc(), "utf8");
|
|
190
|
+
await writeFile(path.join(assetsDir, "TOOLS.md"), makeToolsDoc(), "utf8");
|
|
191
|
+
await writeFile(path.join(assetsScriptsDir, "scheduling.mjs"), makeSchedulingScript(), "utf8");
|
|
192
|
+
await writeFile(path.join(testDir, "recipe-smoke.test.mjs"), makeRecipeSmokeTest(), "utf8");
|
|
193
|
+
return {
|
|
194
|
+
targetDir,
|
|
195
|
+
projectName,
|
|
196
|
+
};
|
|
197
|
+
}
|