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/src/cli.ts
CHANGED
|
@@ -45,6 +45,10 @@ function parsePluginFlags(values: string[]): string[] {
|
|
|
45
45
|
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function parseFileFlags(values: string[]): string[] {
|
|
49
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
function readEnv(name: string): string | undefined {
|
|
49
53
|
const value = process.env[name];
|
|
50
54
|
if (value === undefined) {
|
|
@@ -62,10 +66,10 @@ function parseProvider(value: string): "command" | "mock" | "remote" {
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
function parseScope(value: string): RunScope {
|
|
65
|
-
if (value === "full" || value === "files" || value === "workspace") {
|
|
69
|
+
if (value === "full" || value === "stateful" || value === "files" || value === "workspace") {
|
|
66
70
|
return value;
|
|
67
71
|
}
|
|
68
|
-
throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, files, or workspace`);
|
|
72
|
+
throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, stateful, files, or workspace`);
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
function parseGatewayMode(value: string): GatewayMode {
|
|
@@ -117,7 +121,8 @@ export function buildCli(): Command {
|
|
|
117
121
|
.option("--allow-missing", "Allow unresolved template variables", false)
|
|
118
122
|
.option("--verbose", "Verbose logging", false)
|
|
119
123
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
120
|
-
.option("--scope <scope>", "Run scope: full | files | workspace", "full")
|
|
124
|
+
.option("--scope <scope>", "Run scope: full | stateful | files | workspace", "full")
|
|
125
|
+
.option("--file <pattern>", "File pattern filter (only with --scope files, repeatable)", (v, p: string[]) => p.concat([v]), [])
|
|
121
126
|
.option("--workspace <name>", "Workspace name (required when --scope workspace)")
|
|
122
127
|
.option("--gateway-mode <mode>", "Gateway mode: service | run | none", "service")
|
|
123
128
|
.option("--dotenv-ref <path-or-url>", "Load env vars from local file or HTTP URL")
|
|
@@ -139,6 +144,7 @@ export function buildCli(): Command {
|
|
|
139
144
|
const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
|
|
140
145
|
const scope = parseScope(String(opts.scope ?? "full"));
|
|
141
146
|
const gatewayMode = parseGatewayMode(String(opts.gatewayMode ?? "service"));
|
|
147
|
+
const filePatterns = parseFileFlags(opts.file);
|
|
142
148
|
const workspaceName = opts.workspace?.trim() ? String(opts.workspace).trim() : undefined;
|
|
143
149
|
if (scope === "workspace" && !workspaceName) {
|
|
144
150
|
throw new ClawChefError("--scope workspace requires --workspace <name>");
|
|
@@ -146,9 +152,13 @@ export function buildCli(): Command {
|
|
|
146
152
|
if (scope !== "workspace" && workspaceName) {
|
|
147
153
|
throw new ClawChefError("--workspace is only allowed when --scope workspace");
|
|
148
154
|
}
|
|
155
|
+
if (scope !== "files" && filePatterns.length > 0) {
|
|
156
|
+
throw new ClawChefError("--file is only allowed when --scope files");
|
|
157
|
+
}
|
|
149
158
|
const options: RunOptions = {
|
|
150
159
|
vars: parseVarFlags(opts.var),
|
|
151
160
|
plugins: parsePluginFlags(opts.plugin),
|
|
161
|
+
filePatterns,
|
|
152
162
|
scope,
|
|
153
163
|
workspaceName,
|
|
154
164
|
gatewayMode,
|
package/src/logger.ts
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
export class Logger {
|
|
2
2
|
constructor(private readonly verboseEnabled: boolean) {}
|
|
3
3
|
|
|
4
|
+
private timestamp(): string {
|
|
5
|
+
const now = new Date();
|
|
6
|
+
const year = now.getFullYear();
|
|
7
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
8
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
9
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
10
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
11
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
12
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
info(message: string): void {
|
|
5
|
-
process.stdout.write(`[INFO] ${message}\n`);
|
|
16
|
+
process.stdout.write(`[${this.timestamp()}] [INFO] ${message}\n`);
|
|
6
17
|
}
|
|
7
18
|
|
|
8
19
|
warn(message: string): void {
|
|
9
|
-
process.stdout.write(`[WARN] ${message}\n`);
|
|
20
|
+
process.stdout.write(`[${this.timestamp()}] [WARN] ${message}\n`);
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
debug(message: string): void {
|
|
13
24
|
if (this.verboseEnabled) {
|
|
14
|
-
process.stdout.write(`[DEBUG] ${message}\n`);
|
|
25
|
+
process.stdout.write(`[${this.timestamp()}] [DEBUG] ${message}\n`);
|
|
15
26
|
}
|
|
16
27
|
}
|
|
17
28
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
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";
|
|
8
8
|
import { ClawChefError } from "../errors.js";
|
|
9
9
|
import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawBootstrap, OpenClawSection } from "../types.js";
|
|
10
|
-
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
10
|
+
import type { ChannelAgentBinding, EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_COMMANDS = {
|
|
13
13
|
use_version: "${bin} --version",
|
|
@@ -50,6 +50,26 @@ interface BindingItem {
|
|
|
50
50
|
const SECRET_FLAG_RE =
|
|
51
51
|
/(--[A-Za-z0-9-]*(?:api-key|token|password|secret)[A-Za-z0-9-]*\s+)(?:'[^']*'|"[^"]*"|\S+)/g;
|
|
52
52
|
|
|
53
|
+
let TRACE_VERBOSE = false;
|
|
54
|
+
|
|
55
|
+
function timestamp(): string {
|
|
56
|
+
const now = new Date();
|
|
57
|
+
const year = now.getFullYear();
|
|
58
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
59
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
60
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
61
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
62
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
63
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function traceDebug(message: string): void {
|
|
67
|
+
if (!TRACE_VERBOSE) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
process.stdout.write(`[${timestamp()}] [DEBUG] ${message}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
type BootstrapStringField =
|
|
54
74
|
| "cloudflare_ai_gateway_account_id"
|
|
55
75
|
| "cloudflare_ai_gateway_gateway_id"
|
|
@@ -111,10 +131,15 @@ async function commandExists(bin: string): Promise<boolean> {
|
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
async function runShell(command: string, dryRun: boolean, extraEnv?: Record<string, string>): Promise<string> {
|
|
134
|
+
const sanitized = sanitizeCommand(command);
|
|
114
135
|
if (dryRun) {
|
|
136
|
+
traceDebug(`CMD DRY-RUN: ${sanitized}`);
|
|
115
137
|
return "";
|
|
116
138
|
}
|
|
117
139
|
|
|
140
|
+
const startedAt = Date.now();
|
|
141
|
+
traceDebug(`CMD START: ${sanitized}`);
|
|
142
|
+
|
|
118
143
|
return new Promise<string>((resolve, reject) => {
|
|
119
144
|
const child = spawn(command, {
|
|
120
145
|
shell: true,
|
|
@@ -135,19 +160,26 @@ async function runShell(command: string, dryRun: boolean, extraEnv?: Record<stri
|
|
|
135
160
|
});
|
|
136
161
|
child.on("close", (code) => {
|
|
137
162
|
if (code === 0) {
|
|
163
|
+
traceDebug(`CMD DONE (${Date.now() - startedAt}ms): ${sanitized}`);
|
|
138
164
|
resolve(stdout.trim());
|
|
139
165
|
return;
|
|
140
166
|
}
|
|
167
|
+
traceDebug(`CMD FAIL (${Date.now() - startedAt}ms) code=${String(code)}: ${sanitized}`);
|
|
141
168
|
reject(new ClawChefError(`Command failed (${code}): ${sanitizeCommand(command)}\n${stderr.trim()}`));
|
|
142
169
|
});
|
|
143
170
|
});
|
|
144
171
|
}
|
|
145
172
|
|
|
146
173
|
async function runShellInteractive(command: string, dryRun: boolean): Promise<void> {
|
|
174
|
+
const sanitized = sanitizeCommand(command);
|
|
147
175
|
if (dryRun) {
|
|
176
|
+
traceDebug(`CMD DRY-RUN (interactive): ${sanitized}`);
|
|
148
177
|
return;
|
|
149
178
|
}
|
|
150
179
|
|
|
180
|
+
const startedAt = Date.now();
|
|
181
|
+
traceDebug(`CMD START (interactive): ${sanitized}`);
|
|
182
|
+
|
|
151
183
|
return new Promise<void>((resolve, reject) => {
|
|
152
184
|
const child = spawn(command, {
|
|
153
185
|
shell: true,
|
|
@@ -160,9 +192,11 @@ async function runShellInteractive(command: string, dryRun: boolean): Promise<vo
|
|
|
160
192
|
});
|
|
161
193
|
child.on("close", (code) => {
|
|
162
194
|
if (code === 0) {
|
|
195
|
+
traceDebug(`CMD DONE (interactive, ${Date.now() - startedAt}ms): ${sanitized}`);
|
|
163
196
|
resolve();
|
|
164
197
|
return;
|
|
165
198
|
}
|
|
199
|
+
traceDebug(`CMD FAIL (interactive, ${Date.now() - startedAt}ms) code=${String(code)}: ${sanitized}`);
|
|
166
200
|
reject(new ClawChefError(`Command failed (${code}): ${sanitizeCommand(command)}`));
|
|
167
201
|
});
|
|
168
202
|
});
|
|
@@ -205,6 +239,177 @@ function shouldAutoDisableTelegramChannel(channel: ChannelDef): boolean {
|
|
|
205
239
|
|
|
206
240
|
type VersionMismatchChoice = "ignore" | "abort" | "force";
|
|
207
241
|
|
|
242
|
+
type JsonPatchValue = null | boolean | number | string | JsonPatchValue[] | { [key: string]: JsonPatchValue };
|
|
243
|
+
type ConfigJson = { [key: string]: unknown };
|
|
244
|
+
|
|
245
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
246
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function splitConfigPath(pathExpression: string): string[] {
|
|
250
|
+
const segments: string[] = [];
|
|
251
|
+
let token = "";
|
|
252
|
+
for (let i = 0; i < pathExpression.length; i += 1) {
|
|
253
|
+
const ch = pathExpression[i];
|
|
254
|
+
if (ch === ".") {
|
|
255
|
+
if (token) {
|
|
256
|
+
segments.push(token);
|
|
257
|
+
token = "";
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (ch === "[") {
|
|
262
|
+
if (token) {
|
|
263
|
+
segments.push(token);
|
|
264
|
+
token = "";
|
|
265
|
+
}
|
|
266
|
+
const end = pathExpression.indexOf("]", i + 1);
|
|
267
|
+
if (end < 0) {
|
|
268
|
+
throw new ClawChefError(`Invalid config path expression: ${pathExpression}`);
|
|
269
|
+
}
|
|
270
|
+
const bracketToken = pathExpression.slice(i + 1, end).trim();
|
|
271
|
+
if (!bracketToken) {
|
|
272
|
+
throw new ClawChefError(`Invalid empty bracket token in config path: ${pathExpression}`);
|
|
273
|
+
}
|
|
274
|
+
segments.push(bracketToken);
|
|
275
|
+
i = end;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
token += ch;
|
|
279
|
+
}
|
|
280
|
+
if (token) {
|
|
281
|
+
segments.push(token);
|
|
282
|
+
}
|
|
283
|
+
if (segments.length === 0) {
|
|
284
|
+
throw new ClawChefError(`Invalid config path expression: ${pathExpression}`);
|
|
285
|
+
}
|
|
286
|
+
return segments;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getConfigValue(root: unknown, pathExpression: string): unknown {
|
|
290
|
+
const segments = splitConfigPath(pathExpression);
|
|
291
|
+
let cursor: unknown = root;
|
|
292
|
+
for (const segment of segments) {
|
|
293
|
+
if (!isPlainObject(cursor)) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
297
|
+
}
|
|
298
|
+
return cursor;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function setConfigValue(root: ConfigJson, pathExpression: string, value: unknown): void {
|
|
302
|
+
const segments = splitConfigPath(pathExpression);
|
|
303
|
+
let cursor: Record<string, unknown> = root;
|
|
304
|
+
|
|
305
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
306
|
+
const segment = segments[i];
|
|
307
|
+
const existing = cursor[segment];
|
|
308
|
+
if (isPlainObject(existing)) {
|
|
309
|
+
cursor = existing;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const next: Record<string, unknown> = {};
|
|
313
|
+
cursor[segment] = next;
|
|
314
|
+
cursor = next;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
cursor[segments[segments.length - 1]] = value;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function deepMergeConfig(base: unknown, patch: JsonPatchValue): JsonPatchValue {
|
|
321
|
+
if (!isPlainObject(patch)) {
|
|
322
|
+
return patch;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const baseObject = isPlainObject(base) ? base : {};
|
|
326
|
+
const result: Record<string, JsonPatchValue> = {};
|
|
327
|
+
|
|
328
|
+
for (const [k, v] of Object.entries(baseObject)) {
|
|
329
|
+
result[k] = toJsonPatchValue(v, `base.${k}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
333
|
+
const current = result[k];
|
|
334
|
+
if (isPlainObject(v) && isPlainObject(current)) {
|
|
335
|
+
result[k] = deepMergeConfig(current, v);
|
|
336
|
+
} else {
|
|
337
|
+
result[k] = v;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function configPath(): string {
|
|
345
|
+
const fromEnv = process.env.OPENCLAW_CONFIG_PATH?.trim();
|
|
346
|
+
if (fromEnv) {
|
|
347
|
+
return fromEnv;
|
|
348
|
+
}
|
|
349
|
+
return path.join(homedir(), ".openclaw", "openclaw.json");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function loadConfigJson(configFilePath: string): Promise<ConfigJson> {
|
|
353
|
+
let raw: string;
|
|
354
|
+
try {
|
|
355
|
+
raw = await readFile(configFilePath, "utf8");
|
|
356
|
+
} catch (err) {
|
|
357
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
358
|
+
throw new ClawChefError(`Failed to read OpenClaw config at ${configFilePath}: ${message}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
363
|
+
if (!isPlainObject(parsed)) {
|
|
364
|
+
throw new ClawChefError(`OpenClaw config root must be an object: ${configFilePath}`);
|
|
365
|
+
}
|
|
366
|
+
return parsed;
|
|
367
|
+
} catch (err) {
|
|
368
|
+
if (err instanceof ClawChefError) {
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
372
|
+
throw new ClawChefError(`Failed to parse OpenClaw config JSON at ${configFilePath}: ${message}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function saveConfigJson(configFilePath: string, config: ConfigJson, dryRun: boolean): Promise<void> {
|
|
377
|
+
if (dryRun) {
|
|
378
|
+
traceDebug(`CONFIG DRY-RUN write: ${configFilePath}`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const dir = path.dirname(configFilePath);
|
|
383
|
+
await mkdir(dir, { recursive: true });
|
|
384
|
+
const tempPath = `${configFilePath}.tmp-${process.pid}-${Date.now()}`;
|
|
385
|
+
const payload = `${JSON.stringify(config, null, 2)}\n`;
|
|
386
|
+
await writeFile(tempPath, payload, "utf8");
|
|
387
|
+
await rename(tempPath, configFilePath);
|
|
388
|
+
traceDebug(`CONFIG WRITE: ${configFilePath}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function toJsonPatchValue(value: unknown, pathLabel: string): JsonPatchValue {
|
|
392
|
+
if (
|
|
393
|
+
value === null ||
|
|
394
|
+
typeof value === "string" ||
|
|
395
|
+
typeof value === "number" ||
|
|
396
|
+
typeof value === "boolean"
|
|
397
|
+
) {
|
|
398
|
+
return value;
|
|
399
|
+
}
|
|
400
|
+
if (Array.isArray(value)) {
|
|
401
|
+
return value.map((item, index) => toJsonPatchValue(item, `${pathLabel}[${index}]`));
|
|
402
|
+
}
|
|
403
|
+
if (isPlainObject(value)) {
|
|
404
|
+
const out: { [key: string]: JsonPatchValue } = {};
|
|
405
|
+
for (const [k, v] of Object.entries(value)) {
|
|
406
|
+
out[k] = toJsonPatchValue(v, `${pathLabel}.${k}`);
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
throw new ClawChefError(`openclaw.config_patch contains unsupported value at ${pathLabel}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
208
413
|
async function chooseVersionMismatchAction(
|
|
209
414
|
currentVersion: string,
|
|
210
415
|
expectedVersion: string,
|
|
@@ -338,24 +543,23 @@ function isAccountLevelBinding(item: BindingItem, channel: string, account: stri
|
|
|
338
543
|
);
|
|
339
544
|
}
|
|
340
545
|
|
|
341
|
-
function
|
|
342
|
-
if (
|
|
546
|
+
function parseBindingsValue(value: unknown): BindingItem[] {
|
|
547
|
+
if (value === undefined || value === null) {
|
|
343
548
|
return [];
|
|
344
549
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (!Array.isArray(parsed)) {
|
|
348
|
-
throw new ClawChefError("openclaw config bindings is not an array");
|
|
349
|
-
}
|
|
350
|
-
return parsed as BindingItem[];
|
|
351
|
-
} catch (err) {
|
|
352
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
353
|
-
throw new ClawChefError(`Failed to parse openclaw bindings JSON: ${message}`);
|
|
550
|
+
if (!Array.isArray(value)) {
|
|
551
|
+
throw new ClawChefError("openclaw config bindings is not an array");
|
|
354
552
|
}
|
|
553
|
+
return value as BindingItem[];
|
|
355
554
|
}
|
|
356
555
|
|
|
357
556
|
export class CommandOpenClawProvider implements OpenClawProvider {
|
|
358
557
|
private readonly stagedMessages = new Map<string, StagedMessage[]>();
|
|
558
|
+
private readonly enabledChannelPlugins = new Set<string>();
|
|
559
|
+
|
|
560
|
+
constructor(verboseEnabled = false) {
|
|
561
|
+
TRACE_VERBOSE = verboseEnabled;
|
|
562
|
+
}
|
|
359
563
|
|
|
360
564
|
async ensureVersion(
|
|
361
565
|
config: OpenClawSection,
|
|
@@ -527,16 +731,17 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
527
731
|
|
|
528
732
|
async configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
|
|
529
733
|
const bin = config.bin ?? "openclaw";
|
|
734
|
+
const cfgPath = configPath();
|
|
530
735
|
|
|
531
736
|
if (shouldAutoDisableTelegramChannel(channel)) {
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
await
|
|
737
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
738
|
+
setConfigValue(openclawConfig, telegramEnabledPath(channel.account), false);
|
|
739
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
535
740
|
return;
|
|
536
741
|
}
|
|
537
742
|
|
|
538
743
|
const enablePluginTemplate = config.commands?.enable_plugin;
|
|
539
|
-
if (enablePluginTemplate?.trim()) {
|
|
744
|
+
if (enablePluginTemplate?.trim() && !this.enabledChannelPlugins.has(channel.channel)) {
|
|
540
745
|
const enablePluginCmd = fillTemplate(enablePluginTemplate, {
|
|
541
746
|
bin,
|
|
542
747
|
version: config.version,
|
|
@@ -546,6 +751,9 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
546
751
|
if (enablePluginCmd.trim()) {
|
|
547
752
|
await runShell(enablePluginCmd, dryRun);
|
|
548
753
|
}
|
|
754
|
+
this.enabledChannelPlugins.add(channel.channel);
|
|
755
|
+
} else if (enablePluginTemplate?.trim()) {
|
|
756
|
+
traceDebug(`Skip plugin enable for channel=${channel.channel}; already enabled in this run`);
|
|
549
757
|
}
|
|
550
758
|
|
|
551
759
|
const flags: string[] = [
|
|
@@ -593,13 +801,70 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
593
801
|
await runShell(cmd, dryRun);
|
|
594
802
|
|
|
595
803
|
if (channel.channel === "telegram" && channel.group_policy) {
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
await runShell(setPolicyCmd, dryRun);
|
|
804
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
805
|
+
setConfigValue(openclawConfig, telegramGroupPolicyPath(channel.account), channel.group_policy);
|
|
806
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
600
807
|
}
|
|
601
808
|
}
|
|
602
809
|
|
|
810
|
+
async applyConfigPatch(config: OpenClawSection, patch: Record<string, unknown>, dryRun: boolean): Promise<void> {
|
|
811
|
+
const normalized = toJsonPatchValue(patch, "openclaw.config_patch");
|
|
812
|
+
if (!isPlainObject(normalized)) {
|
|
813
|
+
throw new ClawChefError("openclaw.config_patch must be an object");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const cfgPath = configPath();
|
|
817
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
818
|
+
const merged = deepMergeConfig(openclawConfig, normalized);
|
|
819
|
+
if (!isPlainObject(merged)) {
|
|
820
|
+
throw new ClawChefError("Merged OpenClaw config must be an object");
|
|
821
|
+
}
|
|
822
|
+
await saveConfigJson(cfgPath, merged, dryRun);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async bindChannelAgents(config: OpenClawSection, bindingsInput: ChannelAgentBinding[], dryRun: boolean): Promise<void> {
|
|
826
|
+
if (bindingsInput.length === 0) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const customTemplate = config.commands?.bind_channel_agent;
|
|
831
|
+
if (customTemplate?.trim()) {
|
|
832
|
+
for (const binding of bindingsInput) {
|
|
833
|
+
await this.bindChannelAgent(config, binding.channel, binding.agent, dryRun);
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const cfgPath = configPath();
|
|
839
|
+
const openclawConfig = await loadConfigJson(cfgPath);
|
|
840
|
+
const bindings = parseBindingsValue(getConfigValue(openclawConfig, "bindings"));
|
|
841
|
+
|
|
842
|
+
for (const binding of bindingsInput) {
|
|
843
|
+
const account = binding.channel.account?.trim();
|
|
844
|
+
if (!account) {
|
|
845
|
+
throw new ClawChefError(`Channel ${binding.channel.channel} requires account for agent binding`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const nextBinding: BindingItem = {
|
|
849
|
+
agentId: binding.agent,
|
|
850
|
+
match: {
|
|
851
|
+
channel: binding.channel.channel,
|
|
852
|
+
accountId: account,
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const index = bindings.findIndex((item: BindingItem) => isAccountLevelBinding(item, binding.channel.channel, account));
|
|
857
|
+
if (index >= 0) {
|
|
858
|
+
bindings[index] = nextBinding;
|
|
859
|
+
} else {
|
|
860
|
+
bindings.push(nextBinding);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
setConfigValue(openclawConfig, "bindings", bindings);
|
|
865
|
+
await saveConfigJson(cfgPath, openclawConfig, dryRun);
|
|
866
|
+
}
|
|
867
|
+
|
|
603
868
|
async bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void> {
|
|
604
869
|
const account = channel.account?.trim();
|
|
605
870
|
if (!account) {
|
|
@@ -625,31 +890,7 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
625
890
|
return;
|
|
626
891
|
}
|
|
627
892
|
|
|
628
|
-
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const getCmd = `${bin} config get bindings --json 2>/dev/null || printf '[]'`;
|
|
633
|
-
const rawBindings = await runShell(getCmd, false);
|
|
634
|
-
const bindings = parseBindingsJson(rawBindings);
|
|
635
|
-
const nextBinding: BindingItem = {
|
|
636
|
-
agentId: agent,
|
|
637
|
-
match: {
|
|
638
|
-
channel: channel.channel,
|
|
639
|
-
accountId: account,
|
|
640
|
-
},
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
const index = bindings.findIndex((item) => isAccountLevelBinding(item, channel.channel, account));
|
|
644
|
-
if (index >= 0) {
|
|
645
|
-
bindings[index] = nextBinding;
|
|
646
|
-
} else {
|
|
647
|
-
bindings.push(nextBinding);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const json = JSON.stringify(bindings);
|
|
651
|
-
const setCmd = `${bin} config set bindings ${shellQuote(json)} --json`;
|
|
652
|
-
await runShell(setCmd, false);
|
|
893
|
+
await this.bindChannelAgents(config, [{ channel, agent }], dryRun);
|
|
653
894
|
}
|
|
654
895
|
|
|
655
896
|
async loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
|
package/src/openclaw/factory.ts
CHANGED
|
@@ -10,7 +10,7 @@ export function createProvider(options: RunOptions): OpenClawProvider {
|
|
|
10
10
|
return new MockOpenClawProvider();
|
|
11
11
|
}
|
|
12
12
|
if (provider === "remote") {
|
|
13
|
-
return new RemoteOpenClawProvider(options.remote);
|
|
13
|
+
return new RemoteOpenClawProvider(options.remote, options.verbose);
|
|
14
14
|
}
|
|
15
|
-
return new CommandOpenClawProvider();
|
|
15
|
+
return new CommandOpenClawProvider(options.verbose);
|
|
16
16
|
}
|
|
@@ -69,6 +69,10 @@ export class MockOpenClawProvider implements OpenClawProvider {
|
|
|
69
69
|
this.state.channels.add(`${channel.channel}::${channel.account ?? "default"}`);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
async applyConfigPatch(_config: OpenClawSection, _patch: Record<string, unknown>, _dryRun: boolean): Promise<void> {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
async bindChannelAgent(
|
|
73
77
|
_config: OpenClawSection,
|
|
74
78
|
_channel: ChannelDef,
|
package/src/openclaw/provider.ts
CHANGED
|
@@ -6,6 +6,11 @@ export interface EnsureVersionResult {
|
|
|
6
6
|
installedThisRun: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface ChannelAgentBinding {
|
|
10
|
+
channel: ChannelDef;
|
|
11
|
+
agent: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
export interface OpenClawProvider {
|
|
10
15
|
ensureVersion(
|
|
11
16
|
config: OpenClawSection,
|
|
@@ -18,6 +23,8 @@ export interface OpenClawProvider {
|
|
|
18
23
|
startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
|
|
19
24
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
20
25
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
26
|
+
applyConfigPatch(config: OpenClawSection, patch: Record<string, unknown>, dryRun: boolean): Promise<void>;
|
|
27
|
+
bindChannelAgents?(config: OpenClawSection, bindings: ChannelAgentBinding[], dryRun: boolean): Promise<void>;
|
|
21
28
|
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
22
29
|
loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
23
30
|
materializeFile?(
|
|
@@ -29,6 +29,17 @@ interface RemoteOperationResponse {
|
|
|
29
29
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
30
30
|
const DEFAULT_OPERATION_PATH = "/v1/clawchef/operation";
|
|
31
31
|
|
|
32
|
+
function timestamp(): string {
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const year = now.getFullYear();
|
|
35
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
36
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
37
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
38
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
39
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
40
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
function parseResponseBody(raw: string): RemoteOperationResponse {
|
|
33
44
|
if (!raw.trim()) {
|
|
34
45
|
return {};
|
|
@@ -84,9 +95,18 @@ function assertRemoteConfig(remote: Partial<OpenClawRemoteConfig>): OpenClawRemo
|
|
|
84
95
|
export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
85
96
|
private readonly stagedMessages = new Map<string, StagedMessage[]>();
|
|
86
97
|
private readonly remoteConfig: Partial<OpenClawRemoteConfig>;
|
|
98
|
+
private readonly verboseEnabled: boolean;
|
|
87
99
|
|
|
88
|
-
constructor(remoteConfig: Partial<OpenClawRemoteConfig
|
|
100
|
+
constructor(remoteConfig: Partial<OpenClawRemoteConfig>, verboseEnabled = false) {
|
|
89
101
|
this.remoteConfig = remoteConfig;
|
|
102
|
+
this.verboseEnabled = verboseEnabled;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private debug(message: string): void {
|
|
106
|
+
if (!this.verboseEnabled) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
process.stdout.write(`[${timestamp()}] [DEBUG] ${message}\n`);
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
private async perform(
|
|
@@ -96,6 +116,7 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
96
116
|
dryRun: boolean,
|
|
97
117
|
): Promise<RemoteOperationResponse> {
|
|
98
118
|
if (dryRun) {
|
|
119
|
+
this.debug(`REMOTE DRY-RUN op=${operation}`);
|
|
99
120
|
return { ok: true };
|
|
100
121
|
}
|
|
101
122
|
|
|
@@ -109,6 +130,8 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
109
130
|
recipe_version: config.version,
|
|
110
131
|
payload,
|
|
111
132
|
};
|
|
133
|
+
const startedAt = Date.now();
|
|
134
|
+
this.debug(`REMOTE START op=${operation} url=${operationUrl(remote)}`);
|
|
112
135
|
|
|
113
136
|
try {
|
|
114
137
|
const response = await fetch(operationUrl(remote), {
|
|
@@ -130,8 +153,11 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
130
153
|
throw new ClawChefError(`Remote operation failed for ${operation}: ${parsed.message ?? "unknown error"}`);
|
|
131
154
|
}
|
|
132
155
|
|
|
156
|
+
this.debug(`REMOTE DONE op=${operation} status=${response.status} (${Date.now() - startedAt}ms)`);
|
|
157
|
+
|
|
133
158
|
return parsed;
|
|
134
159
|
} catch (err) {
|
|
160
|
+
this.debug(`REMOTE FAIL op=${operation} (${Date.now() - startedAt}ms)`);
|
|
135
161
|
if (err instanceof ClawChefError) {
|
|
136
162
|
throw err;
|
|
137
163
|
}
|
|
@@ -192,6 +218,10 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
192
218
|
await this.perform(config, "configure_channel", { channel }, dryRun);
|
|
193
219
|
}
|
|
194
220
|
|
|
221
|
+
async applyConfigPatch(config: OpenClawSection, patch: Record<string, unknown>, dryRun: boolean): Promise<void> {
|
|
222
|
+
await this.perform(config, "apply_config_patch", { patch }, dryRun);
|
|
223
|
+
}
|
|
224
|
+
|
|
195
225
|
async bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void> {
|
|
196
226
|
await this.perform(
|
|
197
227
|
config,
|