clawchef 0.1.7 → 0.1.8
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/AGENTS.md +159 -0
- package/README.md +12 -2
- package/dist/openclaw/command-provider.d.ts +1 -0
- package/dist/openclaw/command-provider.js +78 -0
- 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 +7 -0
- package/dist/orchestrator.js +16 -6
- package/dist/recipe.js +14 -0
- package/dist/schema.d.ts +12 -0
- package/dist/schema.js +2 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/openclaw/command-provider.ts +100 -0
- package/src/openclaw/mock-provider.ts +9 -0
- package/src/openclaw/provider.ts +1 -0
- package/src/openclaw/remote-provider.ts +13 -0
- package/src/orchestrator.ts +19 -6
- package/src/recipe.ts +19 -0
- package/src/schema.ts +2 -0
- package/src/types.ts +2 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# AGENTS Guide for `clawchef`
|
|
2
|
+
|
|
3
|
+
This file is for coding agents operating in this repository.
|
|
4
|
+
It documents the current build/test workflow and code conventions inferred from the codebase.
|
|
5
|
+
|
|
6
|
+
## Scope and Source of Truth
|
|
7
|
+
|
|
8
|
+
- Primary language: TypeScript (ESM, NodeNext).
|
|
9
|
+
- Runtime target: Node.js `>=20`.
|
|
10
|
+
- Package manager: npm (`package-lock.json` is present).
|
|
11
|
+
- Compiler config: `tsconfig.json` with `strict: true` and `rootDir: src`, `outDir: dist`.
|
|
12
|
+
- Public entry points: CLI (`src/index.ts`) and Node API (`src/api.ts`).
|
|
13
|
+
|
|
14
|
+
## Repository Rules Discovery
|
|
15
|
+
|
|
16
|
+
I checked for additional agent instruction files:
|
|
17
|
+
|
|
18
|
+
- `.cursor/rules/`: not present
|
|
19
|
+
- `.cursorrules`: not present
|
|
20
|
+
- `.github/copilot-instructions.md`: not present
|
|
21
|
+
|
|
22
|
+
So this `AGENTS.md` is the repo-local guidance file for agents.
|
|
23
|
+
|
|
24
|
+
## Build, Lint, and Test Commands
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
- `npm install`
|
|
29
|
+
|
|
30
|
+
## Build
|
|
31
|
+
|
|
32
|
+
- `npm run build`
|
|
33
|
+
- Runs `tsc -p tsconfig.json`
|
|
34
|
+
- Emits JS + declarations into `dist/`
|
|
35
|
+
|
|
36
|
+
## Type Check
|
|
37
|
+
|
|
38
|
+
- `npm run typecheck`
|
|
39
|
+
- Runs `tsc --noEmit -p tsconfig.json`
|
|
40
|
+
- Use this as the closest thing to linting gate today
|
|
41
|
+
|
|
42
|
+
## Lint
|
|
43
|
+
|
|
44
|
+
- There is currently **no configured lint script** (`npm run lint` does not exist).
|
|
45
|
+
- Do not assume ESLint/Prettier are configured.
|
|
46
|
+
- Keep formatting and style aligned with existing files.
|
|
47
|
+
|
|
48
|
+
## Tests
|
|
49
|
+
|
|
50
|
+
- There is currently **no test script** in this repository’s `package.json`.
|
|
51
|
+
- There is no committed `test/` directory in this repo at the moment.
|
|
52
|
+
- For now, validation is typically: build + typecheck + targeted manual CLI/API smoke checks.
|
|
53
|
+
|
|
54
|
+
## Running a single test (when test files exist)
|
|
55
|
+
|
|
56
|
+
If you add Node test files (for example `*.test.mjs`), run one test file directly with:
|
|
57
|
+
|
|
58
|
+
- `node --test path/to/file.test.mjs`
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
|
|
62
|
+
- `node --test test/recipe-smoke.test.mjs`
|
|
63
|
+
|
|
64
|
+
Notes:
|
|
65
|
+
|
|
66
|
+
- Scaffolded recipe projects generated by `clawchef scaffold` include `test/recipe-smoke.test.mjs`.
|
|
67
|
+
- Those scaffolded projects also include `npm run test:recipe` and `npm test`, but this repo currently does not.
|
|
68
|
+
|
|
69
|
+
## Coding Style Guidelines
|
|
70
|
+
|
|
71
|
+
These conventions are based on current source in `src/`.
|
|
72
|
+
|
|
73
|
+
## Formatting and syntax
|
|
74
|
+
|
|
75
|
+
- Use 2-space indentation.
|
|
76
|
+
- Use semicolons.
|
|
77
|
+
- Use double quotes for strings.
|
|
78
|
+
- Prefer trailing commas in multiline literals/calls.
|
|
79
|
+
- Keep functions focused and relatively small unless complexity requires expansion.
|
|
80
|
+
|
|
81
|
+
## Module system and imports
|
|
82
|
+
|
|
83
|
+
- Use ESM imports/exports only.
|
|
84
|
+
- For local imports, include `.js` extension in import specifiers (important for NodeNext output).
|
|
85
|
+
- Use `import type` for type-only imports.
|
|
86
|
+
- Prefer Node built-ins via `node:` prefix.
|
|
87
|
+
- Import ordering is not rigidly enforced; keep it consistent within each edited file.
|
|
88
|
+
|
|
89
|
+
## Types and strictness
|
|
90
|
+
|
|
91
|
+
- Preserve strict TypeScript compatibility (`strict: true`).
|
|
92
|
+
- Avoid `any`; use `unknown` when needed and narrow safely.
|
|
93
|
+
- Prefer explicit interfaces/types for structured data (see `src/types.ts`).
|
|
94
|
+
- Prefer narrow unions for finite modes/options (`RunScope`, provider names, etc.).
|
|
95
|
+
- Keep runtime validation in sync with static types.
|
|
96
|
+
- If a recipe shape changes, update both `src/types.ts` and `src/schema.ts`.
|
|
97
|
+
|
|
98
|
+
## Naming conventions
|
|
99
|
+
|
|
100
|
+
- `camelCase` for variables/functions.
|
|
101
|
+
- `PascalCase` for classes, interfaces, and type aliases intended as entities.
|
|
102
|
+
- `UPPER_SNAKE_CASE` for top-level constants with fixed meaning.
|
|
103
|
+
- Keep naming domain-oriented (`workspace`, `channel`, `agent`, `bootstrap`, `scope`).
|
|
104
|
+
|
|
105
|
+
## Error handling conventions
|
|
106
|
+
|
|
107
|
+
- Throw `ClawChefError` for expected/user-facing failures.
|
|
108
|
+
- Include actionable, specific messages.
|
|
109
|
+
- For caught unknown errors, convert to readable message:
|
|
110
|
+
- `err instanceof Error ? err.message : String(err)`
|
|
111
|
+
- At CLI top-level, only `ClawChefError` should map to normal error output; unknown errors become fatal.
|
|
112
|
+
|
|
113
|
+
## Logging and output
|
|
114
|
+
|
|
115
|
+
- Use `Logger` (`info`, `warn`, `debug`) for orchestration flow messages.
|
|
116
|
+
- `debug` logs should remain behind `--verbose`.
|
|
117
|
+
- Keep logs concise and operationally useful.
|
|
118
|
+
- Use `process.stdout.write` / `process.stderr.write` for CLI I/O (consistent with existing code).
|
|
119
|
+
|
|
120
|
+
## Async and filesystem patterns
|
|
121
|
+
|
|
122
|
+
- Use async/await with `node:fs/promises` APIs.
|
|
123
|
+
- Prefer non-blocking I/O, except tiny startup reads where sync is already established (`readPackageVersion`).
|
|
124
|
+
- Wrap external command execution and network operations with clear errors.
|
|
125
|
+
- Preserve cleanup behavior for temp dirs/files via `try/finally`.
|
|
126
|
+
|
|
127
|
+
## Recipe/schema evolution rules
|
|
128
|
+
|
|
129
|
+
- Schema is strict (`zod` `.strict()` used heavily); unknown keys are rejected.
|
|
130
|
+
- Enforce semantic constraints in `semanticValidate` (`src/recipe.ts`) after schema parse.
|
|
131
|
+
- Keep secret-handling guardrails intact:
|
|
132
|
+
- Inline secrets should be rejected where required.
|
|
133
|
+
- Template placeholders (`${var}`) are expected for secrets.
|
|
134
|
+
|
|
135
|
+
## CLI/API change guidelines
|
|
136
|
+
|
|
137
|
+
- Keep CLI flags in `src/cli.ts` aligned with Node API options in `src/api.ts` when feature parity is intended.
|
|
138
|
+
- Validate mutually dependent options early (for example, scope + workspace pairing).
|
|
139
|
+
- Maintain provider parity (`command`, `remote`, `mock`) when adding operations.
|
|
140
|
+
- If provider interface changes, update all provider implementations and factory wiring.
|
|
141
|
+
|
|
142
|
+
## Suggested pre-commit checklist for agents
|
|
143
|
+
|
|
144
|
+
- `npm run typecheck`
|
|
145
|
+
- `npm run build`
|
|
146
|
+
- If tests were added: run relevant Node tests, at least one targeted file
|
|
147
|
+
- Verify README snippets if CLI flags/behavior changed
|
|
148
|
+
- Confirm `src/types.ts` and `src/schema.ts` remain synchronized
|
|
149
|
+
|
|
150
|
+
## Quick architecture map
|
|
151
|
+
|
|
152
|
+
- `src/cli.ts`: command parsing + CLI runtime wiring
|
|
153
|
+
- `src/api.ts`: Node API (`cook`, `validate`, `scaffold`)
|
|
154
|
+
- `src/recipe.ts`: recipe loading, template resolution, semantic validation
|
|
155
|
+
- `src/orchestrator.ts`: execution flow across workspaces/agents/channels/files
|
|
156
|
+
- `src/openclaw/*.ts`: provider abstraction and implementations
|
|
157
|
+
- `src/schema.ts` + `src/types.ts`: validation schema and TS domain model
|
|
158
|
+
|
|
159
|
+
When in doubt, follow existing patterns in adjacent files and keep changes minimal, explicit, and type-safe.
|
package/README.md
CHANGED
|
@@ -304,7 +304,7 @@ Supported operation values sent by clawchef:
|
|
|
304
304
|
- `ensure_version`, `factory_reset`, `start_gateway`
|
|
305
305
|
- `install_plugin`
|
|
306
306
|
- `create_workspace`, `create_agent`, `materialize_file`, `install_skill`
|
|
307
|
-
- `configure_channel`, `login_channel`
|
|
307
|
+
- `configure_channel`, `bind_channel_agent`, `login_channel`
|
|
308
308
|
- `run_agent`
|
|
309
309
|
|
|
310
310
|
For `run_agent`, clawchef expects `output` in response for assertions.
|
|
@@ -352,6 +352,7 @@ For `command` provider, default command templates are:
|
|
|
352
352
|
- `install_plugin`: `${bin} plugins install ${plugin_spec_q}`
|
|
353
353
|
- `factory_reset`: `${bin} reset --scope full --yes --non-interactive`
|
|
354
354
|
- `start_gateway`: `${bin} gateway start`
|
|
355
|
+
- `bind_channel_agent`: built-in `openclaw config get/set bindings` upsert (override with `openclaw.commands.bind_channel_agent`)
|
|
355
356
|
- `login_channel`: `${bin} channels login --channel ${channel_q}${account_arg}`
|
|
356
357
|
- `create_workspace`: generated from `openclaw.bootstrap` (override with `openclaw.commands.create_workspace`)
|
|
357
358
|
- `create_agent`: `${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json`
|
|
@@ -377,6 +378,12 @@ channels:
|
|
|
377
378
|
- channel: "telegram"
|
|
378
379
|
token: "${telegram_bot_token}"
|
|
379
380
|
account: "default"
|
|
381
|
+
agent: "main"
|
|
382
|
+
|
|
383
|
+
- channel: "telegram"
|
|
384
|
+
token: "${alerts_bot_token}"
|
|
385
|
+
account: "alerts"
|
|
386
|
+
agent: "alerts"
|
|
380
387
|
|
|
381
388
|
- channel: "slack"
|
|
382
389
|
bot_token: "${slack_bot_token}"
|
|
@@ -392,9 +399,12 @@ channels:
|
|
|
392
399
|
Supported common fields:
|
|
393
400
|
|
|
394
401
|
- required: `channel`
|
|
395
|
-
- 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`
|
|
402
|
+
- optional: `account`, `agent`, `name`, `token`, `token_file`, `use_env`, `bot_token`, `access_token`, `app_token`, `webhook_url`, `webhook_path`, `signal_number`, `password`, `login`, `login_mode`, `login_account`
|
|
396
403
|
- advanced passthrough: `extra_flags` (`snake_case` keys become `--kebab-case` CLI flags)
|
|
397
404
|
|
|
405
|
+
`channels[].agent` currently supports `channel: "telegram"` only.
|
|
406
|
+
If `agent` is set and `account` is omitted, clawchef defaults `account` to the same value as `agent`.
|
|
407
|
+
|
|
398
408
|
## Workspace path behavior
|
|
399
409
|
|
|
400
410
|
- `workspaces[].path` is optional.
|
|
@@ -8,6 +8,7 @@ export declare class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
8
8
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
9
9
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
10
10
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
11
|
+
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
11
12
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
12
13
|
createAgent(config: OpenClawSection, agent: AgentDef, workspacePath: string, dryRun: boolean): Promise<void>;
|
|
13
14
|
installSkill(config: OpenClawSection, workspace: string, agent: string, skill: string, dryRun: boolean): Promise<void>;
|
|
@@ -14,6 +14,7 @@ const DEFAULT_COMMANDS = {
|
|
|
14
14
|
factory_reset: "${bin} reset --scope full --yes --non-interactive",
|
|
15
15
|
start_gateway: "${bin} gateway start",
|
|
16
16
|
enable_plugin: "",
|
|
17
|
+
bind_channel_agent: "",
|
|
17
18
|
login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
|
|
18
19
|
create_agent: "${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json",
|
|
19
20
|
install_skill: "${bin} skills check",
|
|
@@ -227,6 +228,36 @@ function bootstrapRuntimeEnv(bootstrap) {
|
|
|
227
228
|
}
|
|
228
229
|
return env;
|
|
229
230
|
}
|
|
231
|
+
function isAccountLevelBinding(item, channel, account) {
|
|
232
|
+
const match = item.match;
|
|
233
|
+
if (!match || typeof match !== "object") {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (match.channel !== channel || match.accountId !== account) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return (match.peer === undefined
|
|
240
|
+
&& match.parentPeer === undefined
|
|
241
|
+
&& match.guildId === undefined
|
|
242
|
+
&& match.teamId === undefined
|
|
243
|
+
&& match.roles === undefined);
|
|
244
|
+
}
|
|
245
|
+
function parseBindingsJson(raw) {
|
|
246
|
+
if (!raw.trim()) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const parsed = JSON.parse(raw);
|
|
251
|
+
if (!Array.isArray(parsed)) {
|
|
252
|
+
throw new ClawChefError("openclaw config bindings is not an array");
|
|
253
|
+
}
|
|
254
|
+
return parsed;
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
258
|
+
throw new ClawChefError(`Failed to parse openclaw bindings JSON: ${message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
230
261
|
export class CommandOpenClawProvider {
|
|
231
262
|
stagedMessages = new Map();
|
|
232
263
|
async ensureVersion(config, dryRun, silent, preserveExistingState) {
|
|
@@ -408,6 +439,53 @@ export class CommandOpenClawProvider {
|
|
|
408
439
|
const cmd = `${bin} channels add ${flags.join(" ")}`;
|
|
409
440
|
await runShell(cmd, dryRun);
|
|
410
441
|
}
|
|
442
|
+
async bindChannelAgent(config, channel, agent, dryRun) {
|
|
443
|
+
const account = channel.account?.trim();
|
|
444
|
+
if (!account) {
|
|
445
|
+
throw new ClawChefError(`Channel ${channel.channel} requires account for agent binding`);
|
|
446
|
+
}
|
|
447
|
+
const bin = config.bin ?? "openclaw";
|
|
448
|
+
const customTemplate = config.commands?.bind_channel_agent;
|
|
449
|
+
if (customTemplate?.trim()) {
|
|
450
|
+
const customCmd = fillTemplate(customTemplate, {
|
|
451
|
+
bin,
|
|
452
|
+
version: config.version,
|
|
453
|
+
channel: channel.channel,
|
|
454
|
+
channel_q: shellQuote(channel.channel),
|
|
455
|
+
account,
|
|
456
|
+
account_q: shellQuote(account),
|
|
457
|
+
agent,
|
|
458
|
+
agent_q: shellQuote(agent),
|
|
459
|
+
});
|
|
460
|
+
if (customCmd.trim()) {
|
|
461
|
+
await runShell(customCmd, dryRun);
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (dryRun) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const getCmd = `${bin} config get bindings --json 2>/dev/null || printf '[]'`;
|
|
469
|
+
const rawBindings = await runShell(getCmd, false);
|
|
470
|
+
const bindings = parseBindingsJson(rawBindings);
|
|
471
|
+
const nextBinding = {
|
|
472
|
+
agentId: agent,
|
|
473
|
+
match: {
|
|
474
|
+
channel: channel.channel,
|
|
475
|
+
accountId: account,
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
const index = bindings.findIndex((item) => isAccountLevelBinding(item, channel.channel, account));
|
|
479
|
+
if (index >= 0) {
|
|
480
|
+
bindings[index] = nextBinding;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
bindings.push(nextBinding);
|
|
484
|
+
}
|
|
485
|
+
const json = JSON.stringify(bindings);
|
|
486
|
+
const setCmd = `${bin} config set bindings ${shellQuote(json)} --json`;
|
|
487
|
+
await runShell(setCmd, false);
|
|
488
|
+
}
|
|
411
489
|
async loginChannel(config, channel, dryRun) {
|
|
412
490
|
if (!channel.login) {
|
|
413
491
|
return;
|
|
@@ -8,6 +8,7 @@ export declare class MockOpenClawProvider implements OpenClawProvider {
|
|
|
8
8
|
startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
|
|
9
9
|
createWorkspace(_config: OpenClawSection, workspace: ResolvedWorkspaceDef, _dryRun: boolean): Promise<void>;
|
|
10
10
|
configureChannel(_config: OpenClawSection, channel: ChannelDef, _dryRun: boolean): Promise<void>;
|
|
11
|
+
bindChannelAgent(_config: OpenClawSection, _channel: ChannelDef, _agent: string, _dryRun: boolean): Promise<void>;
|
|
11
12
|
loginChannel(_config: OpenClawSection, _channel: ChannelDef, _dryRun: boolean): Promise<void>;
|
|
12
13
|
createAgent(_config: OpenClawSection, agent: AgentDef, _workspacePath: string, _dryRun: boolean): Promise<void>;
|
|
13
14
|
installSkill(_config: OpenClawSection, workspace: string, agent: string, skill: string, _dryRun: boolean): Promise<void>;
|
|
@@ -44,6 +44,9 @@ export class MockOpenClawProvider {
|
|
|
44
44
|
async configureChannel(_config, channel, _dryRun) {
|
|
45
45
|
this.state.channels.add(`${channel.channel}::${channel.account ?? "default"}`);
|
|
46
46
|
}
|
|
47
|
+
async bindChannelAgent(_config, _channel, _agent, _dryRun) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
47
50
|
async loginChannel(_config, _channel, _dryRun) {
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
@@ -12,6 +12,7 @@ export interface OpenClawProvider {
|
|
|
12
12
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
13
13
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
14
14
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
15
|
+
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
15
16
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
16
17
|
materializeFile?(config: OpenClawSection, workspace: string, filePath: string, content: string, overwrite: boolean | undefined, dryRun: boolean): Promise<void>;
|
|
17
18
|
createAgent(config: OpenClawSection, agent: AgentDef, workspacePath: string, dryRun: boolean): Promise<void>;
|
|
@@ -11,6 +11,7 @@ export declare class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
11
11
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
12
12
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
13
13
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
14
|
+
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
14
15
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
15
16
|
materializeFile(config: OpenClawSection, workspace: string, filePath: string, content: string, overwrite: boolean | undefined, dryRun: boolean): Promise<void>;
|
|
16
17
|
createAgent(config: OpenClawSection, agent: AgentDef, workspacePath: string, dryRun: boolean): Promise<void>;
|
|
@@ -119,6 +119,13 @@ export class RemoteOpenClawProvider {
|
|
|
119
119
|
async configureChannel(config, channel, dryRun) {
|
|
120
120
|
await this.perform(config, "configure_channel", { channel }, dryRun);
|
|
121
121
|
}
|
|
122
|
+
async bindChannelAgent(config, channel, agent, dryRun) {
|
|
123
|
+
await this.perform(config, "bind_channel_agent", {
|
|
124
|
+
channel: channel.channel,
|
|
125
|
+
account: channel.account,
|
|
126
|
+
agent,
|
|
127
|
+
}, dryRun);
|
|
128
|
+
}
|
|
122
129
|
async loginChannel(config, channel, dryRun) {
|
|
123
130
|
await this.perform(config, "login_channel", { channel }, dryRun);
|
|
124
131
|
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -215,8 +215,15 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
215
215
|
logger.info(`Agent created: ${agent.workspace}/${agent.name}`);
|
|
216
216
|
}
|
|
217
217
|
for (const channel of recipe.channels ?? []) {
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
219
|
+
? { ...channel, account: channel.agent.trim() }
|
|
220
|
+
: channel;
|
|
221
|
+
await provider.configureChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
222
|
+
logger.info(`Channel configured: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
223
|
+
if (effectiveChannel.agent?.trim()) {
|
|
224
|
+
await provider.bindChannelAgent(recipe.openclaw, effectiveChannel, effectiveChannel.agent, options.dryRun);
|
|
225
|
+
logger.info(`Channel bound to agent: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""} -> ${effectiveChannel.agent}`);
|
|
226
|
+
}
|
|
220
227
|
}
|
|
221
228
|
for (const workspace of recipe.workspaces ?? []) {
|
|
222
229
|
const wsPath = workspacePaths.get(workspace.name);
|
|
@@ -319,14 +326,17 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
319
326
|
await provider.startGateway(recipe.openclaw, options.dryRun);
|
|
320
327
|
logger.info("Gateway started");
|
|
321
328
|
for (const channel of recipe.channels ?? []) {
|
|
322
|
-
|
|
329
|
+
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
330
|
+
? { ...channel, account: channel.agent.trim() }
|
|
331
|
+
: channel;
|
|
332
|
+
if (!effectiveChannel.login) {
|
|
323
333
|
continue;
|
|
324
334
|
}
|
|
325
335
|
if (!options.dryRun && !input.isTTY) {
|
|
326
|
-
throw new ClawChefError(`Channel login for ${
|
|
336
|
+
throw new ClawChefError(`Channel login for ${effectiveChannel.channel} requires an interactive terminal session`);
|
|
327
337
|
}
|
|
328
|
-
await provider.loginChannel(recipe.openclaw,
|
|
329
|
-
logger.info(`Channel login completed: ${
|
|
338
|
+
await provider.loginChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
339
|
+
logger.info(`Channel login completed: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
330
340
|
}
|
|
331
341
|
logger.info("Recipe execution completed");
|
|
332
342
|
}
|
package/dist/recipe.js
CHANGED
|
@@ -145,12 +145,14 @@ function filterRecipeByWorkspaceName(recipe, workspaceName) {
|
|
|
145
145
|
}
|
|
146
146
|
function semanticValidate(recipe) {
|
|
147
147
|
const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
|
|
148
|
+
const agentNameCounts = new Map();
|
|
148
149
|
for (const workspace of recipe.workspaces ?? []) {
|
|
149
150
|
if (workspace.assets !== undefined && !workspace.assets.trim()) {
|
|
150
151
|
throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
for (const agent of recipe.agents ?? []) {
|
|
155
|
+
agentNameCounts.set(agent.name, (agentNameCounts.get(agent.name) ?? 0) + 1);
|
|
154
156
|
if (!ws.has(agent.workspace)) {
|
|
155
157
|
throw new ClawChefError(`Agent ${agent.name} references missing workspace: ${agent.workspace}`);
|
|
156
158
|
}
|
|
@@ -179,6 +181,18 @@ function semanticValidate(recipe) {
|
|
|
179
181
|
(channel.login || channel.login_mode !== undefined || channel.login_account !== undefined)) {
|
|
180
182
|
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.");
|
|
181
183
|
}
|
|
184
|
+
if (channel.agent?.trim()) {
|
|
185
|
+
if (channel.channel !== "telegram") {
|
|
186
|
+
throw new ClawChefError(`channels[] entry for ${channel.channel} does not support agent binding. Use channel: telegram with agent.`);
|
|
187
|
+
}
|
|
188
|
+
const matched = agentNameCounts.get(channel.agent) ?? 0;
|
|
189
|
+
if (matched === 0) {
|
|
190
|
+
throw new ClawChefError(`channels[] entry references missing agent by name: ${channel.agent}`);
|
|
191
|
+
}
|
|
192
|
+
if (matched > 1) {
|
|
193
|
+
throw new ClawChefError(`channels[] entry references duplicate agent name: ${channel.agent}. Agent names must be unique for channel binding.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
182
196
|
const hasAuth = Boolean(channel.use_env) ||
|
|
183
197
|
Boolean(channel.token?.trim()) ||
|
|
184
198
|
Boolean(channel.token_file?.trim()) ||
|
package/dist/schema.d.ts
CHANGED
|
@@ -89,6 +89,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
89
89
|
factory_reset: z.ZodOptional<z.ZodString>;
|
|
90
90
|
start_gateway: z.ZodOptional<z.ZodString>;
|
|
91
91
|
enable_plugin: z.ZodOptional<z.ZodString>;
|
|
92
|
+
bind_channel_agent: z.ZodOptional<z.ZodString>;
|
|
92
93
|
login_channel: z.ZodOptional<z.ZodString>;
|
|
93
94
|
create_workspace: z.ZodOptional<z.ZodString>;
|
|
94
95
|
create_agent: z.ZodOptional<z.ZodString>;
|
|
@@ -103,6 +104,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
103
104
|
factory_reset?: string | undefined;
|
|
104
105
|
start_gateway?: string | undefined;
|
|
105
106
|
enable_plugin?: string | undefined;
|
|
107
|
+
bind_channel_agent?: string | undefined;
|
|
106
108
|
login_channel?: string | undefined;
|
|
107
109
|
create_agent?: string | undefined;
|
|
108
110
|
install_skill?: string | undefined;
|
|
@@ -117,6 +119,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
117
119
|
factory_reset?: string | undefined;
|
|
118
120
|
start_gateway?: string | undefined;
|
|
119
121
|
enable_plugin?: string | undefined;
|
|
122
|
+
bind_channel_agent?: string | undefined;
|
|
120
123
|
login_channel?: string | undefined;
|
|
121
124
|
create_agent?: string | undefined;
|
|
122
125
|
install_skill?: string | undefined;
|
|
@@ -158,6 +161,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
158
161
|
factory_reset?: string | undefined;
|
|
159
162
|
start_gateway?: string | undefined;
|
|
160
163
|
enable_plugin?: string | undefined;
|
|
164
|
+
bind_channel_agent?: string | undefined;
|
|
161
165
|
login_channel?: string | undefined;
|
|
162
166
|
create_agent?: string | undefined;
|
|
163
167
|
install_skill?: string | undefined;
|
|
@@ -199,6 +203,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
199
203
|
factory_reset?: string | undefined;
|
|
200
204
|
start_gateway?: string | undefined;
|
|
201
205
|
enable_plugin?: string | undefined;
|
|
206
|
+
bind_channel_agent?: string | undefined;
|
|
202
207
|
login_channel?: string | undefined;
|
|
203
208
|
create_agent?: string | undefined;
|
|
204
209
|
install_skill?: string | undefined;
|
|
@@ -268,6 +273,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
268
273
|
channels: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
269
274
|
channel: z.ZodString;
|
|
270
275
|
account: z.ZodOptional<z.ZodString>;
|
|
276
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
271
277
|
login: z.ZodOptional<z.ZodBoolean>;
|
|
272
278
|
login_mode: z.ZodOptional<z.ZodEnum<["interactive"]>>;
|
|
273
279
|
login_account: z.ZodOptional<z.ZodString>;
|
|
@@ -287,6 +293,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
287
293
|
channel: string;
|
|
288
294
|
token?: string | undefined;
|
|
289
295
|
account?: string | undefined;
|
|
296
|
+
agent?: string | undefined;
|
|
290
297
|
login?: boolean | undefined;
|
|
291
298
|
login_mode?: "interactive" | undefined;
|
|
292
299
|
login_account?: string | undefined;
|
|
@@ -305,6 +312,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
305
312
|
channel: string;
|
|
306
313
|
token?: string | undefined;
|
|
307
314
|
account?: string | undefined;
|
|
315
|
+
agent?: string | undefined;
|
|
308
316
|
login?: boolean | undefined;
|
|
309
317
|
login_mode?: "interactive" | undefined;
|
|
310
318
|
login_account?: string | undefined;
|
|
@@ -437,6 +445,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
437
445
|
factory_reset?: string | undefined;
|
|
438
446
|
start_gateway?: string | undefined;
|
|
439
447
|
enable_plugin?: string | undefined;
|
|
448
|
+
bind_channel_agent?: string | undefined;
|
|
440
449
|
login_channel?: string | undefined;
|
|
441
450
|
create_agent?: string | undefined;
|
|
442
451
|
install_skill?: string | undefined;
|
|
@@ -468,6 +477,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
468
477
|
channel: string;
|
|
469
478
|
token?: string | undefined;
|
|
470
479
|
account?: string | undefined;
|
|
480
|
+
agent?: string | undefined;
|
|
471
481
|
login?: boolean | undefined;
|
|
472
482
|
login_mode?: "interactive" | undefined;
|
|
473
483
|
login_account?: string | undefined;
|
|
@@ -538,6 +548,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
538
548
|
factory_reset?: string | undefined;
|
|
539
549
|
start_gateway?: string | undefined;
|
|
540
550
|
enable_plugin?: string | undefined;
|
|
551
|
+
bind_channel_agent?: string | undefined;
|
|
541
552
|
login_channel?: string | undefined;
|
|
542
553
|
create_agent?: string | undefined;
|
|
543
554
|
install_skill?: string | undefined;
|
|
@@ -569,6 +580,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
569
580
|
channel: string;
|
|
570
581
|
token?: string | undefined;
|
|
571
582
|
account?: string | undefined;
|
|
583
|
+
agent?: string | undefined;
|
|
572
584
|
login?: boolean | undefined;
|
|
573
585
|
login_mode?: "interactive" | undefined;
|
|
574
586
|
login_account?: string | undefined;
|
package/dist/schema.js
CHANGED
|
@@ -13,6 +13,7 @@ const openClawCommandsSchema = z
|
|
|
13
13
|
factory_reset: z.string().optional(),
|
|
14
14
|
start_gateway: z.string().optional(),
|
|
15
15
|
enable_plugin: z.string().optional(),
|
|
16
|
+
bind_channel_agent: z.string().optional(),
|
|
16
17
|
login_channel: z.string().optional(),
|
|
17
18
|
create_workspace: z.string().optional(),
|
|
18
19
|
create_agent: z.string().optional(),
|
|
@@ -79,6 +80,7 @@ const channelSchema = z
|
|
|
79
80
|
.object({
|
|
80
81
|
channel: z.string().min(1),
|
|
81
82
|
account: z.string().min(1).optional(),
|
|
83
|
+
agent: z.string().min(1).optional(),
|
|
82
84
|
login: z.boolean().optional(),
|
|
83
85
|
login_mode: z.enum(["interactive"]).optional(),
|
|
84
86
|
login_account: z.string().min(1).optional(),
|
package/dist/types.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface OpenClawCommandOverrides {
|
|
|
22
22
|
factory_reset?: string;
|
|
23
23
|
start_gateway?: string;
|
|
24
24
|
enable_plugin?: string;
|
|
25
|
+
bind_channel_agent?: string;
|
|
25
26
|
login_channel?: string;
|
|
26
27
|
create_workspace?: string;
|
|
27
28
|
create_agent?: string;
|
|
@@ -67,6 +68,7 @@ export interface WorkspaceDef {
|
|
|
67
68
|
export interface ChannelDef {
|
|
68
69
|
channel: string;
|
|
69
70
|
account?: string;
|
|
71
|
+
agent?: string;
|
|
70
72
|
login?: boolean;
|
|
71
73
|
login_mode?: "interactive";
|
|
72
74
|
login_account?: string;
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ const DEFAULT_COMMANDS = {
|
|
|
17
17
|
factory_reset: "${bin} reset --scope full --yes --non-interactive",
|
|
18
18
|
start_gateway: "${bin} gateway start",
|
|
19
19
|
enable_plugin: "",
|
|
20
|
+
bind_channel_agent: "",
|
|
20
21
|
login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
|
|
21
22
|
create_agent:
|
|
22
23
|
"${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json",
|
|
@@ -31,6 +32,20 @@ interface StagedMessage {
|
|
|
31
32
|
content: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
interface BindingItem {
|
|
36
|
+
agentId?: unknown;
|
|
37
|
+
match?: {
|
|
38
|
+
channel?: unknown;
|
|
39
|
+
accountId?: unknown;
|
|
40
|
+
peer?: unknown;
|
|
41
|
+
parentPeer?: unknown;
|
|
42
|
+
guildId?: unknown;
|
|
43
|
+
teamId?: unknown;
|
|
44
|
+
roles?: unknown;
|
|
45
|
+
};
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
const SECRET_FLAG_RE =
|
|
35
50
|
/(--[A-Za-z0-9-]*(?:api-key|token|password|secret)[A-Za-z0-9-]*\s+)(?:'[^']*'|"[^"]*"|\S+)/g;
|
|
36
51
|
|
|
@@ -280,6 +295,39 @@ function bootstrapRuntimeEnv(bootstrap: OpenClawBootstrap | undefined): Record<s
|
|
|
280
295
|
return env;
|
|
281
296
|
}
|
|
282
297
|
|
|
298
|
+
function isAccountLevelBinding(item: BindingItem, channel: string, account: string): boolean {
|
|
299
|
+
const match = item.match;
|
|
300
|
+
if (!match || typeof match !== "object") {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
if (match.channel !== channel || match.accountId !== account) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
return (
|
|
307
|
+
match.peer === undefined
|
|
308
|
+
&& match.parentPeer === undefined
|
|
309
|
+
&& match.guildId === undefined
|
|
310
|
+
&& match.teamId === undefined
|
|
311
|
+
&& match.roles === undefined
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function parseBindingsJson(raw: string): BindingItem[] {
|
|
316
|
+
if (!raw.trim()) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
321
|
+
if (!Array.isArray(parsed)) {
|
|
322
|
+
throw new ClawChefError("openclaw config bindings is not an array");
|
|
323
|
+
}
|
|
324
|
+
return parsed as BindingItem[];
|
|
325
|
+
} catch (err) {
|
|
326
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
327
|
+
throw new ClawChefError(`Failed to parse openclaw bindings JSON: ${message}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
283
331
|
export class CommandOpenClawProvider implements OpenClawProvider {
|
|
284
332
|
private readonly stagedMessages = new Map<string, StagedMessage[]>();
|
|
285
333
|
|
|
@@ -502,6 +550,58 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
502
550
|
await runShell(cmd, dryRun);
|
|
503
551
|
}
|
|
504
552
|
|
|
553
|
+
async bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void> {
|
|
554
|
+
const account = channel.account?.trim();
|
|
555
|
+
if (!account) {
|
|
556
|
+
throw new ClawChefError(`Channel ${channel.channel} requires account for agent binding`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const bin = config.bin ?? "openclaw";
|
|
560
|
+
const customTemplate = config.commands?.bind_channel_agent;
|
|
561
|
+
if (customTemplate?.trim()) {
|
|
562
|
+
const customCmd = fillTemplate(customTemplate, {
|
|
563
|
+
bin,
|
|
564
|
+
version: config.version,
|
|
565
|
+
channel: channel.channel,
|
|
566
|
+
channel_q: shellQuote(channel.channel),
|
|
567
|
+
account,
|
|
568
|
+
account_q: shellQuote(account),
|
|
569
|
+
agent,
|
|
570
|
+
agent_q: shellQuote(agent),
|
|
571
|
+
});
|
|
572
|
+
if (customCmd.trim()) {
|
|
573
|
+
await runShell(customCmd, dryRun);
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (dryRun) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const getCmd = `${bin} config get bindings --json 2>/dev/null || printf '[]'`;
|
|
583
|
+
const rawBindings = await runShell(getCmd, false);
|
|
584
|
+
const bindings = parseBindingsJson(rawBindings);
|
|
585
|
+
const nextBinding: BindingItem = {
|
|
586
|
+
agentId: agent,
|
|
587
|
+
match: {
|
|
588
|
+
channel: channel.channel,
|
|
589
|
+
accountId: account,
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const index = bindings.findIndex((item) => isAccountLevelBinding(item, channel.channel, account));
|
|
594
|
+
if (index >= 0) {
|
|
595
|
+
bindings[index] = nextBinding;
|
|
596
|
+
} else {
|
|
597
|
+
bindings.push(nextBinding);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const json = JSON.stringify(bindings);
|
|
601
|
+
const setCmd = `${bin} config set bindings ${shellQuote(json)} --json`;
|
|
602
|
+
await runShell(setCmd, false);
|
|
603
|
+
}
|
|
604
|
+
|
|
505
605
|
async loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
|
|
506
606
|
if (!channel.login) {
|
|
507
607
|
return;
|
|
@@ -69,6 +69,15 @@ export class MockOpenClawProvider implements OpenClawProvider {
|
|
|
69
69
|
this.state.channels.add(`${channel.channel}::${channel.account ?? "default"}`);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
async bindChannelAgent(
|
|
73
|
+
_config: OpenClawSection,
|
|
74
|
+
_channel: ChannelDef,
|
|
75
|
+
_agent: string,
|
|
76
|
+
_dryRun: boolean,
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
async loginChannel(_config: OpenClawSection, _channel: ChannelDef, _dryRun: boolean): Promise<void> {
|
|
73
82
|
return;
|
|
74
83
|
}
|
package/src/openclaw/provider.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface OpenClawProvider {
|
|
|
18
18
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
19
19
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
20
20
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
21
|
+
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
21
22
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
22
23
|
materializeFile?(
|
|
23
24
|
config: OpenClawSection,
|
|
@@ -188,6 +188,19 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
188
188
|
await this.perform(config, "configure_channel", { channel }, dryRun);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
async bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void> {
|
|
192
|
+
await this.perform(
|
|
193
|
+
config,
|
|
194
|
+
"bind_channel_agent",
|
|
195
|
+
{
|
|
196
|
+
channel: channel.channel,
|
|
197
|
+
account: channel.account,
|
|
198
|
+
agent,
|
|
199
|
+
},
|
|
200
|
+
dryRun,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
191
204
|
async loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
|
|
192
205
|
await this.perform(config, "login_channel", { channel }, dryRun);
|
|
193
206
|
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -261,8 +261,18 @@ export async function runRecipe(
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
for (const channel of recipe.channels ?? []) {
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
265
|
+
? { ...channel, account: channel.agent.trim() }
|
|
266
|
+
: channel;
|
|
267
|
+
|
|
268
|
+
await provider.configureChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
269
|
+
logger.info(`Channel configured: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
270
|
+
if (effectiveChannel.agent?.trim()) {
|
|
271
|
+
await provider.bindChannelAgent(recipe.openclaw, effectiveChannel, effectiveChannel.agent, options.dryRun);
|
|
272
|
+
logger.info(
|
|
273
|
+
`Channel bound to agent: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""} -> ${effectiveChannel.agent}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
266
276
|
}
|
|
267
277
|
|
|
268
278
|
for (const workspace of recipe.workspaces ?? []) {
|
|
@@ -369,16 +379,19 @@ export async function runRecipe(
|
|
|
369
379
|
logger.info("Gateway started");
|
|
370
380
|
|
|
371
381
|
for (const channel of recipe.channels ?? []) {
|
|
372
|
-
|
|
382
|
+
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
383
|
+
? { ...channel, account: channel.agent.trim() }
|
|
384
|
+
: channel;
|
|
385
|
+
if (!effectiveChannel.login) {
|
|
373
386
|
continue;
|
|
374
387
|
}
|
|
375
388
|
if (!options.dryRun && !input.isTTY) {
|
|
376
389
|
throw new ClawChefError(
|
|
377
|
-
`Channel login for ${
|
|
390
|
+
`Channel login for ${effectiveChannel.channel} requires an interactive terminal session`,
|
|
378
391
|
);
|
|
379
392
|
}
|
|
380
|
-
await provider.loginChannel(recipe.openclaw,
|
|
381
|
-
logger.info(`Channel login completed: ${
|
|
393
|
+
await provider.loginChannel(recipe.openclaw, effectiveChannel, options.dryRun);
|
|
394
|
+
logger.info(`Channel login completed: ${effectiveChannel.channel}${effectiveChannel.account ? `/${effectiveChannel.account}` : ""}`);
|
|
382
395
|
}
|
|
383
396
|
|
|
384
397
|
logger.info("Recipe execution completed");
|
package/src/recipe.ts
CHANGED
|
@@ -193,12 +193,14 @@ function filterRecipeByWorkspaceName(recipe: Recipe, workspaceName: string): Rec
|
|
|
193
193
|
|
|
194
194
|
function semanticValidate(recipe: Recipe): void {
|
|
195
195
|
const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
|
|
196
|
+
const agentNameCounts = new Map<string, number>();
|
|
196
197
|
for (const workspace of recipe.workspaces ?? []) {
|
|
197
198
|
if (workspace.assets !== undefined && !workspace.assets.trim()) {
|
|
198
199
|
throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
for (const agent of recipe.agents ?? []) {
|
|
203
|
+
agentNameCounts.set(agent.name, (agentNameCounts.get(agent.name) ?? 0) + 1);
|
|
202
204
|
if (!ws.has(agent.workspace)) {
|
|
203
205
|
throw new ClawChefError(`Agent ${agent.name} references missing workspace: ${agent.workspace}`);
|
|
204
206
|
}
|
|
@@ -238,6 +240,23 @@ function semanticValidate(recipe: Recipe): void {
|
|
|
238
240
|
);
|
|
239
241
|
}
|
|
240
242
|
|
|
243
|
+
if (channel.agent?.trim()) {
|
|
244
|
+
if (channel.channel !== "telegram") {
|
|
245
|
+
throw new ClawChefError(
|
|
246
|
+
`channels[] entry for ${channel.channel} does not support agent binding. Use channel: telegram with agent.`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const matched = agentNameCounts.get(channel.agent) ?? 0;
|
|
250
|
+
if (matched === 0) {
|
|
251
|
+
throw new ClawChefError(`channels[] entry references missing agent by name: ${channel.agent}`);
|
|
252
|
+
}
|
|
253
|
+
if (matched > 1) {
|
|
254
|
+
throw new ClawChefError(
|
|
255
|
+
`channels[] entry references duplicate agent name: ${channel.agent}. Agent names must be unique for channel binding.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
241
260
|
const hasAuth =
|
|
242
261
|
Boolean(channel.use_env) ||
|
|
243
262
|
Boolean(channel.token?.trim()) ||
|
package/src/schema.ts
CHANGED
|
@@ -15,6 +15,7 @@ const openClawCommandsSchema = z
|
|
|
15
15
|
factory_reset: z.string().optional(),
|
|
16
16
|
start_gateway: z.string().optional(),
|
|
17
17
|
enable_plugin: z.string().optional(),
|
|
18
|
+
bind_channel_agent: z.string().optional(),
|
|
18
19
|
login_channel: z.string().optional(),
|
|
19
20
|
create_workspace: z.string().optional(),
|
|
20
21
|
create_agent: z.string().optional(),
|
|
@@ -87,6 +88,7 @@ const channelSchema = z
|
|
|
87
88
|
.object({
|
|
88
89
|
channel: z.string().min(1),
|
|
89
90
|
account: z.string().min(1).optional(),
|
|
91
|
+
agent: z.string().min(1).optional(),
|
|
90
92
|
login: z.boolean().optional(),
|
|
91
93
|
login_mode: z.enum(["interactive"]).optional(),
|
|
92
94
|
login_account: z.string().min(1).optional(),
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface OpenClawCommandOverrides {
|
|
|
25
25
|
factory_reset?: string;
|
|
26
26
|
start_gateway?: string;
|
|
27
27
|
enable_plugin?: string;
|
|
28
|
+
bind_channel_agent?: string;
|
|
28
29
|
login_channel?: string;
|
|
29
30
|
create_workspace?: string;
|
|
30
31
|
create_agent?: string;
|
|
@@ -74,6 +75,7 @@ export interface WorkspaceDef {
|
|
|
74
75
|
export interface ChannelDef {
|
|
75
76
|
channel: string;
|
|
76
77
|
account?: string;
|
|
78
|
+
agent?: string;
|
|
77
79
|
login?: boolean;
|
|
78
80
|
login_mode?: "interactive";
|
|
79
81
|
login_account?: string;
|