clawchef 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -1
- package/dist/api.d.ts +1 -0
- package/dist/api.js +5 -0
- package/dist/cli.js +12 -3
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +13 -3
- package/dist/openclaw/command-provider.d.ts +5 -1
- package/dist/openclaw/command-provider.js +254 -44
- package/dist/openclaw/factory.js +2 -2
- package/dist/openclaw/mock-provider.d.ts +1 -0
- package/dist/openclaw/mock-provider.js +3 -0
- package/dist/openclaw/provider.d.ts +6 -0
- package/dist/openclaw/remote-provider.d.ts +4 -1
- package/dist/openclaw/remote-provider.js +27 -1
- package/dist/orchestrator.js +168 -86
- package/dist/recipe.js +39 -1
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +1 -0
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
- package/src/api.ts +8 -0
- package/src/cli.ts +13 -3
- package/src/logger.ts +14 -3
- package/src/openclaw/command-provider.ts +287 -46
- package/src/openclaw/factory.ts +2 -2
- package/src/openclaw/mock-provider.ts +4 -0
- package/src/openclaw/provider.ts +7 -0
- package/src/openclaw/remote-provider.ts +31 -1
- package/src/orchestrator.ts +186 -98
- package/src/recipe.ts +47 -1
- package/src/schema.ts +1 -0
- package/src/types.ts +3 -1
package/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
|
|
@@ -422,6 +444,36 @@ If `agent` is set and `account` is omitted, clawchef defaults `account` to the s
|
|
|
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.
|
|
423
445
|
If `channel: "telegram"` has `token: ""` or `bot_token: ""`, clawchef auto-disables that telegram account (`enabled=false`) and skips channel add/bind.
|
|
424
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
|
+
```
|
|
476
|
+
|
|
425
477
|
## Workspace path behavior
|
|
426
478
|
|
|
427
479
|
- `workspaces[].path` is optional.
|
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
|
});
|
|
@@ -151,6 +180,158 @@ function shouldAutoDisableTelegramChannel(channel) {
|
|
|
151
180
|
const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
|
|
152
181
|
return emptyToken || emptyBotToken;
|
|
153
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
|
+
}
|
|
154
335
|
async function chooseVersionMismatchAction(currentVersion, expectedVersion, silent) {
|
|
155
336
|
if (silent) {
|
|
156
337
|
return "force";
|
|
@@ -265,24 +446,21 @@ function isAccountLevelBinding(item, channel, account) {
|
|
|
265
446
|
&& match.teamId === undefined
|
|
266
447
|
&& match.roles === undefined);
|
|
267
448
|
}
|
|
268
|
-
function
|
|
269
|
-
if (
|
|
449
|
+
function parseBindingsValue(value) {
|
|
450
|
+
if (value === undefined || value === null) {
|
|
270
451
|
return [];
|
|
271
452
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (!Array.isArray(parsed)) {
|
|
275
|
-
throw new ClawChefError("openclaw config bindings is not an array");
|
|
276
|
-
}
|
|
277
|
-
return parsed;
|
|
278
|
-
}
|
|
279
|
-
catch (err) {
|
|
280
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
281
|
-
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");
|
|
282
455
|
}
|
|
456
|
+
return value;
|
|
283
457
|
}
|
|
284
458
|
export class CommandOpenClawProvider {
|
|
285
459
|
stagedMessages = new Map();
|
|
460
|
+
enabledChannelPlugins = new Set();
|
|
461
|
+
constructor(verboseEnabled = false) {
|
|
462
|
+
TRACE_VERBOSE = verboseEnabled;
|
|
463
|
+
}
|
|
286
464
|
async ensureVersion(config, dryRun, silent, preserveExistingState) {
|
|
287
465
|
const bin = config.bin ?? "openclaw";
|
|
288
466
|
const installPolicy = config.install ?? "auto";
|
|
@@ -418,14 +596,15 @@ export class CommandOpenClawProvider {
|
|
|
418
596
|
}
|
|
419
597
|
async configureChannel(config, channel, dryRun) {
|
|
420
598
|
const bin = config.bin ?? "openclaw";
|
|
599
|
+
const cfgPath = configPath();
|
|
421
600
|
if (shouldAutoDisableTelegramChannel(channel)) {
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
await
|
|
601
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
602
|
+
setConfigValue(openclawConfig, telegramEnabledPath(channel.account), false);
|
|
603
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
425
604
|
return;
|
|
426
605
|
}
|
|
427
606
|
const enablePluginTemplate = config.commands?.enable_plugin;
|
|
428
|
-
if (enablePluginTemplate?.trim()) {
|
|
607
|
+
if (enablePluginTemplate?.trim() && !this.enabledChannelPlugins.has(channel.channel)) {
|
|
429
608
|
const enablePluginCmd = fillTemplate(enablePluginTemplate, {
|
|
430
609
|
bin,
|
|
431
610
|
version: config.version,
|
|
@@ -435,6 +614,10 @@ export class CommandOpenClawProvider {
|
|
|
435
614
|
if (enablePluginCmd.trim()) {
|
|
436
615
|
await runShell(enablePluginCmd, dryRun);
|
|
437
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`);
|
|
438
621
|
}
|
|
439
622
|
const flags = [
|
|
440
623
|
"--channel",
|
|
@@ -475,11 +658,60 @@ export class CommandOpenClawProvider {
|
|
|
475
658
|
const cmd = `${bin} channels add ${flags.join(" ")}`;
|
|
476
659
|
await runShell(cmd, dryRun);
|
|
477
660
|
if (channel.channel === "telegram" && channel.group_policy) {
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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");
|
|
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");
|
|
482
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);
|
|
483
715
|
}
|
|
484
716
|
async bindChannelAgent(config, channel, agent, dryRun) {
|
|
485
717
|
const account = channel.account?.trim();
|
|
@@ -504,29 +736,7 @@ export class CommandOpenClawProvider {
|
|
|
504
736
|
}
|
|
505
737
|
return;
|
|
506
738
|
}
|
|
507
|
-
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
const getCmd = `${bin} config get bindings --json 2>/dev/null || printf '[]'`;
|
|
511
|
-
const rawBindings = await runShell(getCmd, false);
|
|
512
|
-
const bindings = parseBindingsJson(rawBindings);
|
|
513
|
-
const nextBinding = {
|
|
514
|
-
agentId: agent,
|
|
515
|
-
match: {
|
|
516
|
-
channel: channel.channel,
|
|
517
|
-
accountId: account,
|
|
518
|
-
},
|
|
519
|
-
};
|
|
520
|
-
const index = bindings.findIndex((item) => isAccountLevelBinding(item, channel.channel, account));
|
|
521
|
-
if (index >= 0) {
|
|
522
|
-
bindings[index] = nextBinding;
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
bindings.push(nextBinding);
|
|
526
|
-
}
|
|
527
|
-
const json = JSON.stringify(bindings);
|
|
528
|
-
const setCmd = `${bin} config set bindings ${shellQuote(json)} --json`;
|
|
529
|
-
await runShell(setCmd, false);
|
|
739
|
+
await this.bindChannelAgents(config, [{ channel, agent }], dryRun);
|
|
530
740
|
}
|
|
531
741
|
async loginChannel(config, channel, dryRun) {
|
|
532
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
|
}
|