clawchef 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -1
- package/dist/api.d.ts +1 -0
- package/dist/api.js +5 -0
- package/dist/cli.js +12 -3
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +13 -3
- package/dist/openclaw/command-provider.d.ts +5 -1
- package/dist/openclaw/command-provider.js +272 -41
- package/dist/openclaw/factory.js +2 -2
- package/dist/openclaw/mock-provider.d.ts +1 -0
- package/dist/openclaw/mock-provider.js +3 -0
- package/dist/openclaw/provider.d.ts +6 -0
- package/dist/openclaw/remote-provider.d.ts +4 -1
- package/dist/openclaw/remote-provider.js +27 -1
- package/dist/orchestrator.js +177 -82
- package/dist/recipe.js +50 -1
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +3 -2
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
- package/src/api.ts +8 -0
- package/src/cli.ts +13 -3
- package/src/logger.ts +14 -3
- package/src/openclaw/command-provider.ts +309 -43
- package/src/openclaw/factory.ts +2 -2
- package/src/openclaw/mock-provider.ts +4 -0
- package/src/openclaw/provider.ts +7 -0
- package/src/openclaw/remote-provider.ts +31 -1
- package/src/orchestrator.ts +199 -94
- package/src/recipe.ts +61 -2
- package/src/schema.ts +3 -2
- package/src/types.ts +3 -1
package/README.md
CHANGED
|
@@ -8,11 +8,12 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
8
8
|
- Accepts recipe input from local file/dir/archive and HTTP URL/archive.
|
|
9
9
|
- Resolves `${var}` parameters from `--var`, environment, and defaults.
|
|
10
10
|
- Auto-loads environment variables from `.env` in the current working directory.
|
|
11
|
+
- `--verbose` prints step-level debug logs including executed commands and operation timing.
|
|
11
12
|
- Supports loading env vars from a custom `.env` path/URL via `--dotenv-ref`.
|
|
12
13
|
- Requires secrets to be injected via `--var` / `CLAWCHEF_VAR_*` (no inline secrets in recipe).
|
|
13
14
|
- Prepares OpenClaw version (install or reuse).
|
|
14
15
|
- When installed OpenClaw version mismatches recipe version, prompts: ignore / abort / force reinstall (silent mode auto-picks force reinstall).
|
|
15
|
-
- Supports scoped execution via `--scope full|files|workspace`.
|
|
16
|
+
- Supports scoped execution via `--scope full|stateful|files|workspace`.
|
|
16
17
|
- `full` scope runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
|
|
17
18
|
- Factory reset includes removing local `~/.openclaw` directory.
|
|
18
19
|
- If `openclaw` is missing, auto-installs the recipe version and skips factory reset.
|
|
@@ -105,10 +106,31 @@ Use it only in CI/non-interactive flows where destructive reset behavior is expe
|
|
|
105
106
|
|
|
106
107
|
Keep existing OpenClaw state (skip reset and keep current version on mismatch):
|
|
107
108
|
|
|
109
|
+
```bash
|
|
110
|
+
clawchef cook recipes/sample.yaml --scope stateful
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Sync only files and assets (`openclaw.root.assets/files` + `workspaces[].assets/files`) without touching version/reset/workspace/agent/channel/skills/conversations/gateway:
|
|
114
|
+
|
|
108
115
|
```bash
|
|
109
116
|
clawchef cook recipes/sample.yaml --scope files
|
|
110
117
|
```
|
|
111
118
|
|
|
119
|
+
Limit files-scope changes to relative paths only:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
clawchef cook recipes/sample.yaml --scope files --file "README.md"
|
|
123
|
+
clawchef cook recipes/sample.yaml --scope files --file "src/**" --file "docs/*.md"
|
|
124
|
+
clawchef cook recipes/sample.yaml --scope files --file "workspace-product-designer/**"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`--file` is repeatable and only works with `--scope files`. Patterns can match any of:
|
|
128
|
+
|
|
129
|
+
- file relative path (for example `README.md`, `src/**`)
|
|
130
|
+
- workspace-prefixed path (for example `product-designer/**`)
|
|
131
|
+
- default workspace-dir-prefixed path (for example `workspace-product-designer/**`)
|
|
132
|
+
- root-prefixed path (`root/**`) for `openclaw.root.assets/files`
|
|
133
|
+
|
|
112
134
|
Update only one workspace:
|
|
113
135
|
|
|
114
136
|
```bash
|
|
@@ -420,6 +442,37 @@ Supported common fields:
|
|
|
420
442
|
`channels[].agent` currently supports `channel: "telegram"` only.
|
|
421
443
|
If `agent` is set and `account` is omitted, clawchef defaults `account` to the same value as `agent`.
|
|
422
444
|
`channels[].group_policy` currently supports `channel: "telegram"` only and is applied after `channels add` via `openclaw config set` so it is not overwritten by add-flow writes.
|
|
445
|
+
If `channel: "telegram"` has `token: ""` or `bot_token: ""`, clawchef auto-disables that telegram account (`enabled=false`) and skips channel add/bind.
|
|
446
|
+
|
|
447
|
+
## OpenClaw config patch
|
|
448
|
+
|
|
449
|
+
Use `openclaw.config_patch` to apply a deep config patch to active OpenClaw config (same target used by `openclaw config set`).
|
|
450
|
+
|
|
451
|
+
Merge behavior:
|
|
452
|
+
|
|
453
|
+
- objects: merged recursively by setting nested keys
|
|
454
|
+
- arrays: replaced as a whole
|
|
455
|
+
- scalars: replaced
|
|
456
|
+
|
|
457
|
+
Applied during `cook` after channel setup and before gateway start. Not applied in `--scope files`.
|
|
458
|
+
|
|
459
|
+
```yaml
|
|
460
|
+
openclaw:
|
|
461
|
+
version: "2026.3.2"
|
|
462
|
+
config_patch:
|
|
463
|
+
channels:
|
|
464
|
+
telegram:
|
|
465
|
+
accounts:
|
|
466
|
+
frontend-dev:
|
|
467
|
+
capabilities:
|
|
468
|
+
inlineButtons: "all"
|
|
469
|
+
groups:
|
|
470
|
+
"*":
|
|
471
|
+
requireMention: false
|
|
472
|
+
enabled: true
|
|
473
|
+
actions:
|
|
474
|
+
sendMessage: true
|
|
475
|
+
```
|
|
423
476
|
|
|
424
477
|
## Workspace path behavior
|
|
425
478
|
|
package/dist/api.d.ts
CHANGED
package/dist/api.js
CHANGED
|
@@ -8,6 +8,7 @@ import { scaffoldProject } from "./scaffold.js";
|
|
|
8
8
|
import { recipeSchema } from "./schema.js";
|
|
9
9
|
function normalizeCookOptions(options) {
|
|
10
10
|
const plugins = Array.from(new Set((options.plugins ?? []).map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
11
|
+
const filePatterns = Array.from(new Set((options.filePatterns ?? []).map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
11
12
|
const scope = options.scope ?? "full";
|
|
12
13
|
const workspaceName = options.workspaceName?.trim() || undefined;
|
|
13
14
|
if (scope === "workspace" && !workspaceName) {
|
|
@@ -16,9 +17,13 @@ function normalizeCookOptions(options) {
|
|
|
16
17
|
if (scope !== "workspace" && workspaceName) {
|
|
17
18
|
throw new ClawChefError("workspaceName is only allowed when scope=workspace");
|
|
18
19
|
}
|
|
20
|
+
if (scope !== "files" && filePatterns.length > 0) {
|
|
21
|
+
throw new ClawChefError("filePatterns is only allowed when scope=files");
|
|
22
|
+
}
|
|
19
23
|
return {
|
|
20
24
|
vars: options.vars ?? {},
|
|
21
25
|
plugins,
|
|
26
|
+
filePatterns,
|
|
22
27
|
scope,
|
|
23
28
|
workspaceName,
|
|
24
29
|
gatewayMode: options.gatewayMode ?? "service",
|
package/dist/cli.js
CHANGED
|
@@ -41,6 +41,9 @@ function parseVarFlags(values) {
|
|
|
41
41
|
function parsePluginFlags(values) {
|
|
42
42
|
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
43
43
|
}
|
|
44
|
+
function parseFileFlags(values) {
|
|
45
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
46
|
+
}
|
|
44
47
|
function readEnv(name) {
|
|
45
48
|
const value = process.env[name];
|
|
46
49
|
if (value === undefined) {
|
|
@@ -56,10 +59,10 @@ function parseProvider(value) {
|
|
|
56
59
|
throw new ClawChefError(`Invalid --provider value: ${value}. Expected command, remote, or mock`);
|
|
57
60
|
}
|
|
58
61
|
function parseScope(value) {
|
|
59
|
-
if (value === "full" || value === "files" || value === "workspace") {
|
|
62
|
+
if (value === "full" || value === "stateful" || value === "files" || value === "workspace") {
|
|
60
63
|
return value;
|
|
61
64
|
}
|
|
62
|
-
throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, files, or workspace`);
|
|
65
|
+
throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, stateful, files, or workspace`);
|
|
63
66
|
}
|
|
64
67
|
function parseGatewayMode(value) {
|
|
65
68
|
if (value === "service" || value === "run" || value === "none") {
|
|
@@ -105,7 +108,8 @@ export function buildCli() {
|
|
|
105
108
|
.option("--allow-missing", "Allow unresolved template variables", false)
|
|
106
109
|
.option("--verbose", "Verbose logging", false)
|
|
107
110
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
108
|
-
.option("--scope <scope>", "Run scope: full | files | workspace", "full")
|
|
111
|
+
.option("--scope <scope>", "Run scope: full | stateful | files | workspace", "full")
|
|
112
|
+
.option("--file <pattern>", "File pattern filter (only with --scope files, repeatable)", (v, p) => p.concat([v]), [])
|
|
109
113
|
.option("--workspace <name>", "Workspace name (required when --scope workspace)")
|
|
110
114
|
.option("--gateway-mode <mode>", "Gateway mode: service | run | none", "service")
|
|
111
115
|
.option("--dotenv-ref <path-or-url>", "Load env vars from local file or HTTP URL")
|
|
@@ -127,6 +131,7 @@ export function buildCli() {
|
|
|
127
131
|
const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
|
|
128
132
|
const scope = parseScope(String(opts.scope ?? "full"));
|
|
129
133
|
const gatewayMode = parseGatewayMode(String(opts.gatewayMode ?? "service"));
|
|
134
|
+
const filePatterns = parseFileFlags(opts.file);
|
|
130
135
|
const workspaceName = opts.workspace?.trim() ? String(opts.workspace).trim() : undefined;
|
|
131
136
|
if (scope === "workspace" && !workspaceName) {
|
|
132
137
|
throw new ClawChefError("--scope workspace requires --workspace <name>");
|
|
@@ -134,9 +139,13 @@ export function buildCli() {
|
|
|
134
139
|
if (scope !== "workspace" && workspaceName) {
|
|
135
140
|
throw new ClawChefError("--workspace is only allowed when --scope workspace");
|
|
136
141
|
}
|
|
142
|
+
if (scope !== "files" && filePatterns.length > 0) {
|
|
143
|
+
throw new ClawChefError("--file is only allowed when --scope files");
|
|
144
|
+
}
|
|
137
145
|
const options = {
|
|
138
146
|
vars: parseVarFlags(opts.var),
|
|
139
147
|
plugins: parsePluginFlags(opts.plugin),
|
|
148
|
+
filePatterns,
|
|
140
149
|
scope,
|
|
141
150
|
workspaceName,
|
|
142
151
|
gatewayMode,
|
package/dist/logger.d.ts
CHANGED
package/dist/logger.js
CHANGED
|
@@ -3,15 +3,25 @@ export class Logger {
|
|
|
3
3
|
constructor(verboseEnabled) {
|
|
4
4
|
this.verboseEnabled = verboseEnabled;
|
|
5
5
|
}
|
|
6
|
+
timestamp() {
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const year = now.getFullYear();
|
|
9
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
10
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
11
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
12
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
13
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
14
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
15
|
+
}
|
|
6
16
|
info(message) {
|
|
7
|
-
process.stdout.write(`[INFO] ${message}\n`);
|
|
17
|
+
process.stdout.write(`[${this.timestamp()}] [INFO] ${message}\n`);
|
|
8
18
|
}
|
|
9
19
|
warn(message) {
|
|
10
|
-
process.stdout.write(`[WARN] ${message}\n`);
|
|
20
|
+
process.stdout.write(`[${this.timestamp()}] [WARN] ${message}\n`);
|
|
11
21
|
}
|
|
12
22
|
debug(message) {
|
|
13
23
|
if (this.verboseEnabled) {
|
|
14
|
-
process.stdout.write(`[DEBUG] ${message}\n`);
|
|
24
|
+
process.stdout.write(`[${this.timestamp()}] [DEBUG] ${message}\n`);
|
|
15
25
|
}
|
|
16
26
|
}
|
|
17
27
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection } from "../types.js";
|
|
2
|
-
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
2
|
+
import type { ChannelAgentBinding, EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
3
3
|
export declare class CommandOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private readonly stagedMessages;
|
|
5
|
+
private readonly enabledChannelPlugins;
|
|
6
|
+
constructor(verboseEnabled?: boolean);
|
|
5
7
|
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean, preserveExistingState: boolean): Promise<EnsureVersionResult>;
|
|
6
8
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
7
9
|
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
8
10
|
startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
|
|
9
11
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
10
12
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
13
|
+
applyConfigPatch(config: OpenClawSection, patch: Record<string, unknown>, dryRun: boolean): Promise<void>;
|
|
14
|
+
bindChannelAgents(config: OpenClawSection, bindingsInput: ChannelAgentBinding[], dryRun: boolean): Promise<void>;
|
|
11
15
|
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
12
16
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
13
17
|
createAgent(config: OpenClawSection, agent: AgentDef, workspacePath: string, dryRun: boolean): Promise<void>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir, tmpdir } from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { mkdtemp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -23,6 +23,23 @@ const DEFAULT_COMMANDS = {
|
|
|
23
23
|
run_agent: "${bin} agent --local --agent ${agent} --message ${prompt_q} --json",
|
|
24
24
|
};
|
|
25
25
|
const SECRET_FLAG_RE = /(--[A-Za-z0-9-]*(?:api-key|token|password|secret)[A-Za-z0-9-]*\s+)(?:'[^']*'|"[^"]*"|\S+)/g;
|
|
26
|
+
let TRACE_VERBOSE = false;
|
|
27
|
+
function timestamp() {
|
|
28
|
+
const now = new Date();
|
|
29
|
+
const year = now.getFullYear();
|
|
30
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
31
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
32
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
33
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
34
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
35
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
36
|
+
}
|
|
37
|
+
function traceDebug(message) {
|
|
38
|
+
if (!TRACE_VERBOSE) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
process.stdout.write(`[${timestamp()}] [DEBUG] ${message}\n`);
|
|
42
|
+
}
|
|
26
43
|
const AUTH_CHOICE_TO_LLM_FLAG = {
|
|
27
44
|
"openai-api-key": "--openai-api-key",
|
|
28
45
|
"anthropic-api-key": "--anthropic-api-key",
|
|
@@ -70,9 +87,13 @@ async function commandExists(bin) {
|
|
|
70
87
|
}
|
|
71
88
|
}
|
|
72
89
|
async function runShell(command, dryRun, extraEnv) {
|
|
90
|
+
const sanitized = sanitizeCommand(command);
|
|
73
91
|
if (dryRun) {
|
|
92
|
+
traceDebug(`CMD DRY-RUN: ${sanitized}`);
|
|
74
93
|
return "";
|
|
75
94
|
}
|
|
95
|
+
const startedAt = Date.now();
|
|
96
|
+
traceDebug(`CMD START: ${sanitized}`);
|
|
76
97
|
return new Promise((resolve, reject) => {
|
|
77
98
|
const child = spawn(command, {
|
|
78
99
|
shell: true,
|
|
@@ -92,17 +113,23 @@ async function runShell(command, dryRun, extraEnv) {
|
|
|
92
113
|
});
|
|
93
114
|
child.on("close", (code) => {
|
|
94
115
|
if (code === 0) {
|
|
116
|
+
traceDebug(`CMD DONE (${Date.now() - startedAt}ms): ${sanitized}`);
|
|
95
117
|
resolve(stdout.trim());
|
|
96
118
|
return;
|
|
97
119
|
}
|
|
120
|
+
traceDebug(`CMD FAIL (${Date.now() - startedAt}ms) code=${String(code)}: ${sanitized}`);
|
|
98
121
|
reject(new ClawChefError(`Command failed (${code}): ${sanitizeCommand(command)}\n${stderr.trim()}`));
|
|
99
122
|
});
|
|
100
123
|
});
|
|
101
124
|
}
|
|
102
125
|
async function runShellInteractive(command, dryRun) {
|
|
126
|
+
const sanitized = sanitizeCommand(command);
|
|
103
127
|
if (dryRun) {
|
|
128
|
+
traceDebug(`CMD DRY-RUN (interactive): ${sanitized}`);
|
|
104
129
|
return;
|
|
105
130
|
}
|
|
131
|
+
const startedAt = Date.now();
|
|
132
|
+
traceDebug(`CMD START (interactive): ${sanitized}`);
|
|
106
133
|
return new Promise((resolve, reject) => {
|
|
107
134
|
const child = spawn(command, {
|
|
108
135
|
shell: true,
|
|
@@ -114,9 +141,11 @@ async function runShellInteractive(command, dryRun) {
|
|
|
114
141
|
});
|
|
115
142
|
child.on("close", (code) => {
|
|
116
143
|
if (code === 0) {
|
|
144
|
+
traceDebug(`CMD DONE (interactive, ${Date.now() - startedAt}ms): ${sanitized}`);
|
|
117
145
|
resolve();
|
|
118
146
|
return;
|
|
119
147
|
}
|
|
148
|
+
traceDebug(`CMD FAIL (interactive, ${Date.now() - startedAt}ms) code=${String(code)}: ${sanitized}`);
|
|
120
149
|
reject(new ClawChefError(`Command failed (${code}): ${sanitizeCommand(command)}`));
|
|
121
150
|
});
|
|
122
151
|
});
|
|
@@ -136,6 +165,173 @@ function telegramGroupPolicyPath(account) {
|
|
|
136
165
|
}
|
|
137
166
|
return `channels.telegram.accounts[${trimmed}].groupPolicy`;
|
|
138
167
|
}
|
|
168
|
+
function telegramEnabledPath(account) {
|
|
169
|
+
const trimmed = account?.trim();
|
|
170
|
+
if (!trimmed) {
|
|
171
|
+
return "channels.telegram.enabled";
|
|
172
|
+
}
|
|
173
|
+
return `channels.telegram.accounts[${trimmed}].enabled`;
|
|
174
|
+
}
|
|
175
|
+
function shouldAutoDisableTelegramChannel(channel) {
|
|
176
|
+
if (channel.channel !== "telegram") {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const emptyToken = channel.token !== undefined && channel.token.trim().length === 0;
|
|
180
|
+
const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
|
|
181
|
+
return emptyToken || emptyBotToken;
|
|
182
|
+
}
|
|
183
|
+
function isPlainObject(value) {
|
|
184
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
185
|
+
}
|
|
186
|
+
function splitConfigPath(pathExpression) {
|
|
187
|
+
const segments = [];
|
|
188
|
+
let token = "";
|
|
189
|
+
for (let i = 0; i < pathExpression.length; i += 1) {
|
|
190
|
+
const ch = pathExpression[i];
|
|
191
|
+
if (ch === ".") {
|
|
192
|
+
if (token) {
|
|
193
|
+
segments.push(token);
|
|
194
|
+
token = "";
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (ch === "[") {
|
|
199
|
+
if (token) {
|
|
200
|
+
segments.push(token);
|
|
201
|
+
token = "";
|
|
202
|
+
}
|
|
203
|
+
const end = pathExpression.indexOf("]", i + 1);
|
|
204
|
+
if (end < 0) {
|
|
205
|
+
throw new ClawChefError(`Invalid config path expression: ${pathExpression}`);
|
|
206
|
+
}
|
|
207
|
+
const bracketToken = pathExpression.slice(i + 1, end).trim();
|
|
208
|
+
if (!bracketToken) {
|
|
209
|
+
throw new ClawChefError(`Invalid empty bracket token in config path: ${pathExpression}`);
|
|
210
|
+
}
|
|
211
|
+
segments.push(bracketToken);
|
|
212
|
+
i = end;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
token += ch;
|
|
216
|
+
}
|
|
217
|
+
if (token) {
|
|
218
|
+
segments.push(token);
|
|
219
|
+
}
|
|
220
|
+
if (segments.length === 0) {
|
|
221
|
+
throw new ClawChefError(`Invalid config path expression: ${pathExpression}`);
|
|
222
|
+
}
|
|
223
|
+
return segments;
|
|
224
|
+
}
|
|
225
|
+
function getConfigValue(root, pathExpression) {
|
|
226
|
+
const segments = splitConfigPath(pathExpression);
|
|
227
|
+
let cursor = root;
|
|
228
|
+
for (const segment of segments) {
|
|
229
|
+
if (!isPlainObject(cursor)) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
cursor = cursor[segment];
|
|
233
|
+
}
|
|
234
|
+
return cursor;
|
|
235
|
+
}
|
|
236
|
+
function setConfigValue(root, pathExpression, value) {
|
|
237
|
+
const segments = splitConfigPath(pathExpression);
|
|
238
|
+
let cursor = root;
|
|
239
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
240
|
+
const segment = segments[i];
|
|
241
|
+
const existing = cursor[segment];
|
|
242
|
+
if (isPlainObject(existing)) {
|
|
243
|
+
cursor = existing;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const next = {};
|
|
247
|
+
cursor[segment] = next;
|
|
248
|
+
cursor = next;
|
|
249
|
+
}
|
|
250
|
+
cursor[segments[segments.length - 1]] = value;
|
|
251
|
+
}
|
|
252
|
+
function deepMergeConfig(base, patch) {
|
|
253
|
+
if (!isPlainObject(patch)) {
|
|
254
|
+
return patch;
|
|
255
|
+
}
|
|
256
|
+
const baseObject = isPlainObject(base) ? base : {};
|
|
257
|
+
const result = {};
|
|
258
|
+
for (const [k, v] of Object.entries(baseObject)) {
|
|
259
|
+
result[k] = toJsonPatchValue(v, `base.${k}`);
|
|
260
|
+
}
|
|
261
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
262
|
+
const current = result[k];
|
|
263
|
+
if (isPlainObject(v) && isPlainObject(current)) {
|
|
264
|
+
result[k] = deepMergeConfig(current, v);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
result[k] = v;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
function configPath() {
|
|
273
|
+
const fromEnv = process.env.OPENCLAW_CONFIG_PATH?.trim();
|
|
274
|
+
if (fromEnv) {
|
|
275
|
+
return fromEnv;
|
|
276
|
+
}
|
|
277
|
+
return path.join(homedir(), ".openclaw", "openclaw.json");
|
|
278
|
+
}
|
|
279
|
+
async function loadConfigJson(configFilePath) {
|
|
280
|
+
let raw;
|
|
281
|
+
try {
|
|
282
|
+
raw = await readFile(configFilePath, "utf8");
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
286
|
+
throw new ClawChefError(`Failed to read OpenClaw config at ${configFilePath}: ${message}`);
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const parsed = JSON.parse(raw);
|
|
290
|
+
if (!isPlainObject(parsed)) {
|
|
291
|
+
throw new ClawChefError(`OpenClaw config root must be an object: ${configFilePath}`);
|
|
292
|
+
}
|
|
293
|
+
return parsed;
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
if (err instanceof ClawChefError) {
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
300
|
+
throw new ClawChefError(`Failed to parse OpenClaw config JSON at ${configFilePath}: ${message}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function saveConfigJson(configFilePath, config, dryRun) {
|
|
304
|
+
if (dryRun) {
|
|
305
|
+
traceDebug(`CONFIG DRY-RUN write: ${configFilePath}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const dir = path.dirname(configFilePath);
|
|
309
|
+
await mkdir(dir, { recursive: true });
|
|
310
|
+
const tempPath = `${configFilePath}.tmp-${process.pid}-${Date.now()}`;
|
|
311
|
+
const payload = `${JSON.stringify(config, null, 2)}\n`;
|
|
312
|
+
await writeFile(tempPath, payload, "utf8");
|
|
313
|
+
await rename(tempPath, configFilePath);
|
|
314
|
+
traceDebug(`CONFIG WRITE: ${configFilePath}`);
|
|
315
|
+
}
|
|
316
|
+
function toJsonPatchValue(value, pathLabel) {
|
|
317
|
+
if (value === null ||
|
|
318
|
+
typeof value === "string" ||
|
|
319
|
+
typeof value === "number" ||
|
|
320
|
+
typeof value === "boolean") {
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
if (Array.isArray(value)) {
|
|
324
|
+
return value.map((item, index) => toJsonPatchValue(item, `${pathLabel}[${index}]`));
|
|
325
|
+
}
|
|
326
|
+
if (isPlainObject(value)) {
|
|
327
|
+
const out = {};
|
|
328
|
+
for (const [k, v] of Object.entries(value)) {
|
|
329
|
+
out[k] = toJsonPatchValue(v, `${pathLabel}.${k}`);
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
throw new ClawChefError(`openclaw.config_patch contains unsupported value at ${pathLabel}`);
|
|
334
|
+
}
|
|
139
335
|
async function chooseVersionMismatchAction(currentVersion, expectedVersion, silent) {
|
|
140
336
|
if (silent) {
|
|
141
337
|
return "force";
|
|
@@ -250,24 +446,21 @@ function isAccountLevelBinding(item, channel, account) {
|
|
|
250
446
|
&& match.teamId === undefined
|
|
251
447
|
&& match.roles === undefined);
|
|
252
448
|
}
|
|
253
|
-
function
|
|
254
|
-
if (
|
|
449
|
+
function parseBindingsValue(value) {
|
|
450
|
+
if (value === undefined || value === null) {
|
|
255
451
|
return [];
|
|
256
452
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (!Array.isArray(parsed)) {
|
|
260
|
-
throw new ClawChefError("openclaw config bindings is not an array");
|
|
261
|
-
}
|
|
262
|
-
return parsed;
|
|
263
|
-
}
|
|
264
|
-
catch (err) {
|
|
265
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
266
|
-
throw new ClawChefError(`Failed to parse openclaw bindings JSON: ${message}`);
|
|
453
|
+
if (!Array.isArray(value)) {
|
|
454
|
+
throw new ClawChefError("openclaw config bindings is not an array");
|
|
267
455
|
}
|
|
456
|
+
return value;
|
|
268
457
|
}
|
|
269
458
|
export class CommandOpenClawProvider {
|
|
270
459
|
stagedMessages = new Map();
|
|
460
|
+
enabledChannelPlugins = new Set();
|
|
461
|
+
constructor(verboseEnabled = false) {
|
|
462
|
+
TRACE_VERBOSE = verboseEnabled;
|
|
463
|
+
}
|
|
271
464
|
async ensureVersion(config, dryRun, silent, preserveExistingState) {
|
|
272
465
|
const bin = config.bin ?? "openclaw";
|
|
273
466
|
const installPolicy = config.install ?? "auto";
|
|
@@ -403,8 +596,15 @@ export class CommandOpenClawProvider {
|
|
|
403
596
|
}
|
|
404
597
|
async configureChannel(config, channel, dryRun) {
|
|
405
598
|
const bin = config.bin ?? "openclaw";
|
|
599
|
+
const cfgPath = configPath();
|
|
600
|
+
if (shouldAutoDisableTelegramChannel(channel)) {
|
|
601
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
602
|
+
setConfigValue(openclawConfig, telegramEnabledPath(channel.account), false);
|
|
603
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
406
606
|
const enablePluginTemplate = config.commands?.enable_plugin;
|
|
407
|
-
if (enablePluginTemplate?.trim()) {
|
|
607
|
+
if (enablePluginTemplate?.trim() && !this.enabledChannelPlugins.has(channel.channel)) {
|
|
408
608
|
const enablePluginCmd = fillTemplate(enablePluginTemplate, {
|
|
409
609
|
bin,
|
|
410
610
|
version: config.version,
|
|
@@ -414,6 +614,10 @@ export class CommandOpenClawProvider {
|
|
|
414
614
|
if (enablePluginCmd.trim()) {
|
|
415
615
|
await runShell(enablePluginCmd, dryRun);
|
|
416
616
|
}
|
|
617
|
+
this.enabledChannelPlugins.add(channel.channel);
|
|
618
|
+
}
|
|
619
|
+
else if (enablePluginTemplate?.trim()) {
|
|
620
|
+
traceDebug(`Skip plugin enable for channel=${channel.channel}; already enabled in this run`);
|
|
417
621
|
}
|
|
418
622
|
const flags = [
|
|
419
623
|
"--channel",
|
|
@@ -454,11 +658,60 @@ export class CommandOpenClawProvider {
|
|
|
454
658
|
const cmd = `${bin} channels add ${flags.join(" ")}`;
|
|
455
659
|
await runShell(cmd, dryRun);
|
|
456
660
|
if (channel.channel === "telegram" && channel.group_policy) {
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
661
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
662
|
+
setConfigValue(openclawConfig, telegramGroupPolicyPath(channel.account), channel.group_policy);
|
|
663
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async applyConfigPatch(config, patch, dryRun) {
|
|
667
|
+
const normalized = toJsonPatchValue(patch, "openclaw.config_patch");
|
|
668
|
+
if (!isPlainObject(normalized)) {
|
|
669
|
+
throw new ClawChefError("openclaw.config_patch must be an object");
|
|
461
670
|
}
|
|
671
|
+
const cfgPath = configPath();
|
|
672
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
673
|
+
const merged = deepMergeConfig(openclawConfig, normalized);
|
|
674
|
+
if (!isPlainObject(merged)) {
|
|
675
|
+
throw new ClawChefError("Merged OpenClaw config must be an object");
|
|
676
|
+
}
|
|
677
|
+
await saveConfigJson(cfgPath, merged, dryRun);
|
|
678
|
+
}
|
|
679
|
+
async bindChannelAgents(config, bindingsInput, dryRun) {
|
|
680
|
+
if (bindingsInput.length === 0) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const customTemplate = config.commands?.bind_channel_agent;
|
|
684
|
+
if (customTemplate?.trim()) {
|
|
685
|
+
for (const binding of bindingsInput) {
|
|
686
|
+
await this.bindChannelAgent(config, binding.channel, binding.agent, dryRun);
|
|
687
|
+
}
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const cfgPath = configPath();
|
|
691
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
692
|
+
const bindings = parseBindingsValue(getConfigValue(openclawConfig, "bindings"));
|
|
693
|
+
for (const binding of bindingsInput) {
|
|
694
|
+
const account = binding.channel.account?.trim();
|
|
695
|
+
if (!account) {
|
|
696
|
+
throw new ClawChefError(`Channel ${binding.channel.channel} requires account for agent binding`);
|
|
697
|
+
}
|
|
698
|
+
const nextBinding = {
|
|
699
|
+
agentId: binding.agent,
|
|
700
|
+
match: {
|
|
701
|
+
channel: binding.channel.channel,
|
|
702
|
+
accountId: account,
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
const index = bindings.findIndex((item) => isAccountLevelBinding(item, binding.channel.channel, account));
|
|
706
|
+
if (index >= 0) {
|
|
707
|
+
bindings[index] = nextBinding;
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
bindings.push(nextBinding);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
setConfigValue(openclawConfig, "bindings", bindings);
|
|
714
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
462
715
|
}
|
|
463
716
|
async bindChannelAgent(config, channel, agent, dryRun) {
|
|
464
717
|
const account = channel.account?.trim();
|
|
@@ -483,29 +736,7 @@ export class CommandOpenClawProvider {
|
|
|
483
736
|
}
|
|
484
737
|
return;
|
|
485
738
|
}
|
|
486
|
-
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const getCmd = `${bin} config get bindings --json 2>/dev/null || printf '[]'`;
|
|
490
|
-
const rawBindings = await runShell(getCmd, false);
|
|
491
|
-
const bindings = parseBindingsJson(rawBindings);
|
|
492
|
-
const nextBinding = {
|
|
493
|
-
agentId: agent,
|
|
494
|
-
match: {
|
|
495
|
-
channel: channel.channel,
|
|
496
|
-
accountId: account,
|
|
497
|
-
},
|
|
498
|
-
};
|
|
499
|
-
const index = bindings.findIndex((item) => isAccountLevelBinding(item, channel.channel, account));
|
|
500
|
-
if (index >= 0) {
|
|
501
|
-
bindings[index] = nextBinding;
|
|
502
|
-
}
|
|
503
|
-
else {
|
|
504
|
-
bindings.push(nextBinding);
|
|
505
|
-
}
|
|
506
|
-
const json = JSON.stringify(bindings);
|
|
507
|
-
const setCmd = `${bin} config set bindings ${shellQuote(json)} --json`;
|
|
508
|
-
await runShell(setCmd, false);
|
|
739
|
+
await this.bindChannelAgents(config, [{ channel, agent }], dryRun);
|
|
509
740
|
}
|
|
510
741
|
async loginChannel(config, channel, dryRun) {
|
|
511
742
|
if (!channel.login) {
|
package/dist/openclaw/factory.js
CHANGED
|
@@ -7,7 +7,7 @@ export function createProvider(options) {
|
|
|
7
7
|
return new MockOpenClawProvider();
|
|
8
8
|
}
|
|
9
9
|
if (provider === "remote") {
|
|
10
|
-
return new RemoteOpenClawProvider(options.remote);
|
|
10
|
+
return new RemoteOpenClawProvider(options.remote, options.verbose);
|
|
11
11
|
}
|
|
12
|
-
return new CommandOpenClawProvider();
|
|
12
|
+
return new CommandOpenClawProvider(options.verbose);
|
|
13
13
|
}
|
|
@@ -8,6 +8,7 @@ export declare class MockOpenClawProvider implements OpenClawProvider {
|
|
|
8
8
|
startGateway(_config: OpenClawSection, _mode: GatewayMode, _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
|
+
applyConfigPatch(_config: OpenClawSection, _patch: Record<string, unknown>, _dryRun: boolean): Promise<void>;
|
|
11
12
|
bindChannelAgent(_config: OpenClawSection, _channel: ChannelDef, _agent: string, _dryRun: boolean): Promise<void>;
|
|
12
13
|
loginChannel(_config: OpenClawSection, _channel: ChannelDef, _dryRun: boolean): Promise<void>;
|
|
13
14
|
createAgent(_config: OpenClawSection, agent: AgentDef, _workspacePath: 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 applyConfigPatch(_config, _patch, _dryRun) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
47
50
|
async bindChannelAgent(_config, _channel, _agent, _dryRun) {
|
|
48
51
|
return;
|
|
49
52
|
}
|