clawchef 0.1.12 → 0.1.14
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 -2
- 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 +252 -51
- 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 -54
- 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
|
|
@@ -420,7 +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.
|
|
423
|
-
If `channel: "telegram"` has `token: ""` or `bot_token: ""`, clawchef
|
|
445
|
+
If `channel: "telegram"` has `token: ""` or `bot_token: ""`, clawchef skips that telegram account and does not run 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
|
+
```
|
|
424
476
|
|
|
425
477
|
## Workspace path behavior
|
|
426
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,13 +165,6 @@ function telegramGroupPolicyPath(account) {
|
|
|
136
165
|
}
|
|
137
166
|
return `channels.telegram.accounts[${trimmed}].groupPolicy`;
|
|
138
167
|
}
|
|
139
|
-
function telegramEnabledPath(account) {
|
|
140
|
-
const trimmed = account?.trim();
|
|
141
|
-
if (!trimmed) {
|
|
142
|
-
return "channels.telegram.enabled";
|
|
143
|
-
}
|
|
144
|
-
return `channels.telegram.accounts[${trimmed}].enabled`;
|
|
145
|
-
}
|
|
146
168
|
function shouldAutoDisableTelegramChannel(channel) {
|
|
147
169
|
if (channel.channel !== "telegram") {
|
|
148
170
|
return false;
|
|
@@ -151,6 +173,158 @@ function shouldAutoDisableTelegramChannel(channel) {
|
|
|
151
173
|
const emptyBotToken = channel.bot_token !== undefined && channel.bot_token.trim().length === 0;
|
|
152
174
|
return emptyToken || emptyBotToken;
|
|
153
175
|
}
|
|
176
|
+
function isPlainObject(value) {
|
|
177
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
178
|
+
}
|
|
179
|
+
function splitConfigPath(pathExpression) {
|
|
180
|
+
const segments = [];
|
|
181
|
+
let token = "";
|
|
182
|
+
for (let i = 0; i < pathExpression.length; i += 1) {
|
|
183
|
+
const ch = pathExpression[i];
|
|
184
|
+
if (ch === ".") {
|
|
185
|
+
if (token) {
|
|
186
|
+
segments.push(token);
|
|
187
|
+
token = "";
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (ch === "[") {
|
|
192
|
+
if (token) {
|
|
193
|
+
segments.push(token);
|
|
194
|
+
token = "";
|
|
195
|
+
}
|
|
196
|
+
const end = pathExpression.indexOf("]", i + 1);
|
|
197
|
+
if (end < 0) {
|
|
198
|
+
throw new ClawChefError(`Invalid config path expression: ${pathExpression}`);
|
|
199
|
+
}
|
|
200
|
+
const bracketToken = pathExpression.slice(i + 1, end).trim();
|
|
201
|
+
if (!bracketToken) {
|
|
202
|
+
throw new ClawChefError(`Invalid empty bracket token in config path: ${pathExpression}`);
|
|
203
|
+
}
|
|
204
|
+
segments.push(bracketToken);
|
|
205
|
+
i = end;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
token += ch;
|
|
209
|
+
}
|
|
210
|
+
if (token) {
|
|
211
|
+
segments.push(token);
|
|
212
|
+
}
|
|
213
|
+
if (segments.length === 0) {
|
|
214
|
+
throw new ClawChefError(`Invalid config path expression: ${pathExpression}`);
|
|
215
|
+
}
|
|
216
|
+
return segments;
|
|
217
|
+
}
|
|
218
|
+
function getConfigValue(root, pathExpression) {
|
|
219
|
+
const segments = splitConfigPath(pathExpression);
|
|
220
|
+
let cursor = root;
|
|
221
|
+
for (const segment of segments) {
|
|
222
|
+
if (!isPlainObject(cursor)) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
cursor = cursor[segment];
|
|
226
|
+
}
|
|
227
|
+
return cursor;
|
|
228
|
+
}
|
|
229
|
+
function setConfigValue(root, pathExpression, value) {
|
|
230
|
+
const segments = splitConfigPath(pathExpression);
|
|
231
|
+
let cursor = root;
|
|
232
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
233
|
+
const segment = segments[i];
|
|
234
|
+
const existing = cursor[segment];
|
|
235
|
+
if (isPlainObject(existing)) {
|
|
236
|
+
cursor = existing;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const next = {};
|
|
240
|
+
cursor[segment] = next;
|
|
241
|
+
cursor = next;
|
|
242
|
+
}
|
|
243
|
+
cursor[segments[segments.length - 1]] = value;
|
|
244
|
+
}
|
|
245
|
+
function deepMergeConfig(base, patch) {
|
|
246
|
+
if (!isPlainObject(patch)) {
|
|
247
|
+
return patch;
|
|
248
|
+
}
|
|
249
|
+
const baseObject = isPlainObject(base) ? base : {};
|
|
250
|
+
const result = {};
|
|
251
|
+
for (const [k, v] of Object.entries(baseObject)) {
|
|
252
|
+
result[k] = toJsonPatchValue(v, `base.${k}`);
|
|
253
|
+
}
|
|
254
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
255
|
+
const current = result[k];
|
|
256
|
+
if (isPlainObject(v) && isPlainObject(current)) {
|
|
257
|
+
result[k] = deepMergeConfig(current, v);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
result[k] = v;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
function configPath() {
|
|
266
|
+
const fromEnv = process.env.OPENCLAW_CONFIG_PATH?.trim();
|
|
267
|
+
if (fromEnv) {
|
|
268
|
+
return fromEnv;
|
|
269
|
+
}
|
|
270
|
+
return path.join(homedir(), ".openclaw", "openclaw.json");
|
|
271
|
+
}
|
|
272
|
+
async function loadConfigJson(configFilePath) {
|
|
273
|
+
let raw;
|
|
274
|
+
try {
|
|
275
|
+
raw = await readFile(configFilePath, "utf8");
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
279
|
+
throw new ClawChefError(`Failed to read OpenClaw config at ${configFilePath}: ${message}`);
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const parsed = JSON.parse(raw);
|
|
283
|
+
if (!isPlainObject(parsed)) {
|
|
284
|
+
throw new ClawChefError(`OpenClaw config root must be an object: ${configFilePath}`);
|
|
285
|
+
}
|
|
286
|
+
return parsed;
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (err instanceof ClawChefError) {
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
293
|
+
throw new ClawChefError(`Failed to parse OpenClaw config JSON at ${configFilePath}: ${message}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function saveConfigJson(configFilePath, config, dryRun) {
|
|
297
|
+
if (dryRun) {
|
|
298
|
+
traceDebug(`CONFIG DRY-RUN write: ${configFilePath}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const dir = path.dirname(configFilePath);
|
|
302
|
+
await mkdir(dir, { recursive: true });
|
|
303
|
+
const tempPath = `${configFilePath}.tmp-${process.pid}-${Date.now()}`;
|
|
304
|
+
const payload = `${JSON.stringify(config, null, 2)}\n`;
|
|
305
|
+
await writeFile(tempPath, payload, "utf8");
|
|
306
|
+
await rename(tempPath, configFilePath);
|
|
307
|
+
traceDebug(`CONFIG WRITE: ${configFilePath}`);
|
|
308
|
+
}
|
|
309
|
+
function toJsonPatchValue(value, pathLabel) {
|
|
310
|
+
if (value === null ||
|
|
311
|
+
typeof value === "string" ||
|
|
312
|
+
typeof value === "number" ||
|
|
313
|
+
typeof value === "boolean") {
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
if (Array.isArray(value)) {
|
|
317
|
+
return value.map((item, index) => toJsonPatchValue(item, `${pathLabel}[${index}]`));
|
|
318
|
+
}
|
|
319
|
+
if (isPlainObject(value)) {
|
|
320
|
+
const out = {};
|
|
321
|
+
for (const [k, v] of Object.entries(value)) {
|
|
322
|
+
out[k] = toJsonPatchValue(v, `${pathLabel}.${k}`);
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
throw new ClawChefError(`openclaw.config_patch contains unsupported value at ${pathLabel}`);
|
|
327
|
+
}
|
|
154
328
|
async function chooseVersionMismatchAction(currentVersion, expectedVersion, silent) {
|
|
155
329
|
if (silent) {
|
|
156
330
|
return "force";
|
|
@@ -265,24 +439,21 @@ function isAccountLevelBinding(item, channel, account) {
|
|
|
265
439
|
&& match.teamId === undefined
|
|
266
440
|
&& match.roles === undefined);
|
|
267
441
|
}
|
|
268
|
-
function
|
|
269
|
-
if (
|
|
442
|
+
function parseBindingsValue(value) {
|
|
443
|
+
if (value === undefined || value === null) {
|
|
270
444
|
return [];
|
|
271
445
|
}
|
|
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}`);
|
|
446
|
+
if (!Array.isArray(value)) {
|
|
447
|
+
throw new ClawChefError("openclaw config bindings is not an array");
|
|
282
448
|
}
|
|
449
|
+
return value;
|
|
283
450
|
}
|
|
284
451
|
export class CommandOpenClawProvider {
|
|
285
452
|
stagedMessages = new Map();
|
|
453
|
+
enabledChannelPlugins = new Set();
|
|
454
|
+
constructor(verboseEnabled = false) {
|
|
455
|
+
TRACE_VERBOSE = verboseEnabled;
|
|
456
|
+
}
|
|
286
457
|
async ensureVersion(config, dryRun, silent, preserveExistingState) {
|
|
287
458
|
const bin = config.bin ?? "openclaw";
|
|
288
459
|
const installPolicy = config.install ?? "auto";
|
|
@@ -418,14 +589,13 @@ export class CommandOpenClawProvider {
|
|
|
418
589
|
}
|
|
419
590
|
async configureChannel(config, channel, dryRun) {
|
|
420
591
|
const bin = config.bin ?? "openclaw";
|
|
592
|
+
const cfgPath = configPath();
|
|
421
593
|
if (shouldAutoDisableTelegramChannel(channel)) {
|
|
422
|
-
|
|
423
|
-
const disableCmd = `${bin} config set ${shellQuote(enabledPath)} false --strict-json`;
|
|
424
|
-
await runShell(disableCmd, dryRun);
|
|
594
|
+
traceDebug(`Skip telegram channel with empty token: ${channel.channel}${channel.account ? `/${channel.account}` : ""}`);
|
|
425
595
|
return;
|
|
426
596
|
}
|
|
427
597
|
const enablePluginTemplate = config.commands?.enable_plugin;
|
|
428
|
-
if (enablePluginTemplate?.trim()) {
|
|
598
|
+
if (enablePluginTemplate?.trim() && !this.enabledChannelPlugins.has(channel.channel)) {
|
|
429
599
|
const enablePluginCmd = fillTemplate(enablePluginTemplate, {
|
|
430
600
|
bin,
|
|
431
601
|
version: config.version,
|
|
@@ -435,6 +605,10 @@ export class CommandOpenClawProvider {
|
|
|
435
605
|
if (enablePluginCmd.trim()) {
|
|
436
606
|
await runShell(enablePluginCmd, dryRun);
|
|
437
607
|
}
|
|
608
|
+
this.enabledChannelPlugins.add(channel.channel);
|
|
609
|
+
}
|
|
610
|
+
else if (enablePluginTemplate?.trim()) {
|
|
611
|
+
traceDebug(`Skip plugin enable for channel=${channel.channel}; already enabled in this run`);
|
|
438
612
|
}
|
|
439
613
|
const flags = [
|
|
440
614
|
"--channel",
|
|
@@ -475,12 +649,61 @@ export class CommandOpenClawProvider {
|
|
|
475
649
|
const cmd = `${bin} channels add ${flags.join(" ")}`;
|
|
476
650
|
await runShell(cmd, dryRun);
|
|
477
651
|
if (channel.channel === "telegram" && channel.group_policy) {
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
await runShell(setPolicyCmd, dryRun);
|
|
652
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
653
|
+
setConfigValue(openclawConfig, telegramGroupPolicyPath(channel.account), channel.group_policy);
|
|
654
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
482
655
|
}
|
|
483
656
|
}
|
|
657
|
+
async applyConfigPatch(config, patch, dryRun) {
|
|
658
|
+
const normalized = toJsonPatchValue(patch, "openclaw.config_patch");
|
|
659
|
+
if (!isPlainObject(normalized)) {
|
|
660
|
+
throw new ClawChefError("openclaw.config_patch must be an object");
|
|
661
|
+
}
|
|
662
|
+
const cfgPath = configPath();
|
|
663
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
664
|
+
const merged = deepMergeConfig(openclawConfig, normalized);
|
|
665
|
+
if (!isPlainObject(merged)) {
|
|
666
|
+
throw new ClawChefError("Merged OpenClaw config must be an object");
|
|
667
|
+
}
|
|
668
|
+
await saveConfigJson(cfgPath, merged, dryRun);
|
|
669
|
+
}
|
|
670
|
+
async bindChannelAgents(config, bindingsInput, dryRun) {
|
|
671
|
+
if (bindingsInput.length === 0) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const customTemplate = config.commands?.bind_channel_agent;
|
|
675
|
+
if (customTemplate?.trim()) {
|
|
676
|
+
for (const binding of bindingsInput) {
|
|
677
|
+
await this.bindChannelAgent(config, binding.channel, binding.agent, dryRun);
|
|
678
|
+
}
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const cfgPath = configPath();
|
|
682
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
683
|
+
const bindings = parseBindingsValue(getConfigValue(openclawConfig, "bindings"));
|
|
684
|
+
for (const binding of bindingsInput) {
|
|
685
|
+
const account = binding.channel.account?.trim();
|
|
686
|
+
if (!account) {
|
|
687
|
+
throw new ClawChefError(`Channel ${binding.channel.channel} requires account for agent binding`);
|
|
688
|
+
}
|
|
689
|
+
const nextBinding = {
|
|
690
|
+
agentId: binding.agent,
|
|
691
|
+
match: {
|
|
692
|
+
channel: binding.channel.channel,
|
|
693
|
+
accountId: account,
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
const index = bindings.findIndex((item) => isAccountLevelBinding(item, binding.channel.channel, account));
|
|
697
|
+
if (index >= 0) {
|
|
698
|
+
bindings[index] = nextBinding;
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
bindings.push(nextBinding);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
setConfigValue(openclawConfig, "bindings", bindings);
|
|
705
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
706
|
+
}
|
|
484
707
|
async bindChannelAgent(config, channel, agent, dryRun) {
|
|
485
708
|
const account = channel.account?.trim();
|
|
486
709
|
if (!account) {
|
|
@@ -504,29 +727,7 @@ export class CommandOpenClawProvider {
|
|
|
504
727
|
}
|
|
505
728
|
return;
|
|
506
729
|
}
|
|
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);
|
|
730
|
+
await this.bindChannelAgents(config, [{ channel, agent }], dryRun);
|
|
530
731
|
}
|
|
531
732
|
async loginChannel(config, channel, dryRun) {
|
|
532
733
|
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
|
}
|