clawchef 0.1.0
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 +405 -0
- package/dist/api.d.ts +14 -0
- package/dist/api.js +49 -0
- package/dist/assertions.d.ts +2 -0
- package/dist/assertions.js +32 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +115 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +14 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +17 -0
- package/dist/openclaw/command-provider.d.ts +15 -0
- package/dist/openclaw/command-provider.js +489 -0
- package/dist/openclaw/factory.d.ts +3 -0
- package/dist/openclaw/factory.js +13 -0
- package/dist/openclaw/mock-provider.d.ts +15 -0
- package/dist/openclaw/mock-provider.js +65 -0
- package/dist/openclaw/provider.d.ts +20 -0
- package/dist/openclaw/provider.js +1 -0
- package/dist/openclaw/remote-provider.d.ts +19 -0
- package/dist/openclaw/remote-provider.js +158 -0
- package/dist/orchestrator.d.ts +4 -0
- package/dist/orchestrator.js +243 -0
- package/dist/recipe.d.ts +20 -0
- package/dist/recipe.js +522 -0
- package/dist/schema.d.ts +626 -0
- package/dist/schema.js +143 -0
- package/dist/template.d.ts +2 -0
- package/dist/template.js +30 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.js +1 -0
- package/package.json +41 -0
- package/recipes/content-from-sample.yaml +20 -0
- package/recipes/openclaw-from-zero.yaml +45 -0
- package/recipes/openclaw-local.yaml +65 -0
- package/recipes/openclaw-remote-http.yaml +38 -0
- package/recipes/openclaw-telegram-mock.yaml +22 -0
- package/recipes/openclaw-telegram.yaml +19 -0
- package/recipes/sample.yaml +49 -0
- package/recipes/snippets/readme-template.md +3 -0
- package/src/api.ts +65 -0
- package/src/assertions.ts +37 -0
- package/src/cli.ts +123 -0
- package/src/env.ts +16 -0
- package/src/errors.ts +6 -0
- package/src/index.ts +20 -0
- package/src/logger.ts +17 -0
- package/src/openclaw/command-provider.ts +594 -0
- package/src/openclaw/factory.ts +16 -0
- package/src/openclaw/mock-provider.ts +104 -0
- package/src/openclaw/provider.ts +44 -0
- package/src/openclaw/remote-provider.ts +264 -0
- package/src/orchestrator.ts +271 -0
- package/src/recipe.ts +621 -0
- package/src/schema.ts +157 -0
- package/src/template.ts +41 -0
- package/src/types.ts +150 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
8
|
+
import { ClawChefError } from "../errors.js";
|
|
9
|
+
import type { AgentDef, ChannelDef, ConversationDef, OpenClawBootstrap, OpenClawSection } from "../types.js";
|
|
10
|
+
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_COMMANDS = {
|
|
13
|
+
use_version: "${bin} --version",
|
|
14
|
+
install_version: "npm install -g openclaw@${version}",
|
|
15
|
+
uninstall_version: "npm uninstall -g openclaw",
|
|
16
|
+
factory_reset: "${bin} reset --scope full --yes --non-interactive",
|
|
17
|
+
start_gateway: "${bin} gateway start",
|
|
18
|
+
enable_plugin: "${bin} plugins enable ${channel_q}",
|
|
19
|
+
login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
|
|
20
|
+
create_agent:
|
|
21
|
+
"${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json",
|
|
22
|
+
install_skill: "${bin} skills check",
|
|
23
|
+
send_message: "true",
|
|
24
|
+
run_agent: "${bin} agent --local --agent ${agent} --message ${prompt_q} --json",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type CommandKey = keyof typeof DEFAULT_COMMANDS;
|
|
28
|
+
|
|
29
|
+
interface StagedMessage {
|
|
30
|
+
content: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SECRET_FLAG_RE =
|
|
34
|
+
/(--[A-Za-z0-9-]*(?:api-key|token|password|secret)[A-Za-z0-9-]*\s+)(?:'[^']*'|"[^"]*"|\S+)/g;
|
|
35
|
+
|
|
36
|
+
type BootstrapStringField =
|
|
37
|
+
| "openai_api_key"
|
|
38
|
+
| "anthropic_api_key"
|
|
39
|
+
| "openrouter_api_key"
|
|
40
|
+
| "xai_api_key"
|
|
41
|
+
| "gemini_api_key"
|
|
42
|
+
| "ai_gateway_api_key"
|
|
43
|
+
| "cloudflare_ai_gateway_api_key"
|
|
44
|
+
| "cloudflare_ai_gateway_account_id"
|
|
45
|
+
| "cloudflare_ai_gateway_gateway_id"
|
|
46
|
+
| "token"
|
|
47
|
+
| "token_provider"
|
|
48
|
+
| "token_profile_id";
|
|
49
|
+
|
|
50
|
+
const BOOTSTRAP_STRING_FLAGS: Array<[BootstrapStringField, string]> = [
|
|
51
|
+
["openai_api_key", "--openai-api-key"],
|
|
52
|
+
["anthropic_api_key", "--anthropic-api-key"],
|
|
53
|
+
["openrouter_api_key", "--openrouter-api-key"],
|
|
54
|
+
["xai_api_key", "--xai-api-key"],
|
|
55
|
+
["gemini_api_key", "--gemini-api-key"],
|
|
56
|
+
["ai_gateway_api_key", "--ai-gateway-api-key"],
|
|
57
|
+
["cloudflare_ai_gateway_api_key", "--cloudflare-ai-gateway-api-key"],
|
|
58
|
+
["cloudflare_ai_gateway_account_id", "--cloudflare-ai-gateway-account-id"],
|
|
59
|
+
["cloudflare_ai_gateway_gateway_id", "--cloudflare-ai-gateway-gateway-id"],
|
|
60
|
+
["token", "--token"],
|
|
61
|
+
["token_provider", "--token-provider"],
|
|
62
|
+
["token_profile_id", "--token-profile-id"],
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function fillTemplate(input: string, vars: Record<string, string>): string {
|
|
66
|
+
return input.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, key: string) => vars[key] ?? "");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shellQuote(value: string): string {
|
|
70
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sanitizeCommand(command: string): string {
|
|
74
|
+
return command.replace(SECRET_FLAG_RE, "$1***");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function snakeToKebab(value: string): string {
|
|
78
|
+
return value.replace(/_/g, "-");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function commandExists(bin: string): Promise<boolean> {
|
|
82
|
+
try {
|
|
83
|
+
await runShell(`command -v ${shellQuote(bin)}`, false);
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runShell(command: string, dryRun: boolean, extraEnv?: Record<string, string>): Promise<string> {
|
|
91
|
+
if (dryRun) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return new Promise<string>((resolve, reject) => {
|
|
96
|
+
const child = spawn(command, {
|
|
97
|
+
shell: true,
|
|
98
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
99
|
+
env: { ...process.env, ...(extraEnv ?? {}) },
|
|
100
|
+
});
|
|
101
|
+
let stdout = "";
|
|
102
|
+
let stderr = "";
|
|
103
|
+
|
|
104
|
+
child.stdout.on("data", (buf) => {
|
|
105
|
+
stdout += String(buf);
|
|
106
|
+
});
|
|
107
|
+
child.stderr.on("data", (buf) => {
|
|
108
|
+
stderr += String(buf);
|
|
109
|
+
});
|
|
110
|
+
child.on("error", (err) => {
|
|
111
|
+
reject(err);
|
|
112
|
+
});
|
|
113
|
+
child.on("close", (code) => {
|
|
114
|
+
if (code === 0) {
|
|
115
|
+
resolve(stdout.trim());
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
reject(new ClawChefError(`Command failed (${code}): ${sanitizeCommand(command)}\n${stderr.trim()}`));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runShellInteractive(command: string, dryRun: boolean): Promise<void> {
|
|
124
|
+
if (dryRun) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Promise<void>((resolve, reject) => {
|
|
129
|
+
const child = spawn(command, {
|
|
130
|
+
shell: true,
|
|
131
|
+
stdio: "inherit",
|
|
132
|
+
env: process.env,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
child.on("error", (err) => {
|
|
136
|
+
reject(err);
|
|
137
|
+
});
|
|
138
|
+
child.on("close", (code) => {
|
|
139
|
+
if (code === 0) {
|
|
140
|
+
resolve();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
reject(new ClawChefError(`Command failed (${code}): ${sanitizeCommand(command)}`));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function commandFor(config: OpenClawSection, key: CommandKey, vars: Record<string, string>): string {
|
|
149
|
+
const template = config.commands?.[key] ?? DEFAULT_COMMANDS[key];
|
|
150
|
+
return fillTemplate(template, vars);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseVersionOutput(output: string): string {
|
|
154
|
+
const match = output.match(/\b(\d+\.\d+\.\d+)\b/);
|
|
155
|
+
return match?.[1] ?? output.trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type VersionMismatchChoice = "ignore" | "abort" | "force";
|
|
159
|
+
|
|
160
|
+
async function chooseVersionMismatchAction(
|
|
161
|
+
currentVersion: string,
|
|
162
|
+
expectedVersion: string,
|
|
163
|
+
silent: boolean,
|
|
164
|
+
): Promise<VersionMismatchChoice> {
|
|
165
|
+
if (silent) {
|
|
166
|
+
return "force";
|
|
167
|
+
}
|
|
168
|
+
if (!input.isTTY) {
|
|
169
|
+
throw new ClawChefError(
|
|
170
|
+
"OpenClaw version mismatch requires interactive terminal. Use --silent to force reinstall and continue.",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const rl = createInterface({ input, output });
|
|
175
|
+
try {
|
|
176
|
+
while (true) {
|
|
177
|
+
const answer = await rl.question(
|
|
178
|
+
[
|
|
179
|
+
`OpenClaw version mismatch detected: current ${currentVersion}, expected ${expectedVersion}`,
|
|
180
|
+
"Choose action:",
|
|
181
|
+
" 1) Ignore and continue",
|
|
182
|
+
" 2) Abort",
|
|
183
|
+
" 3) Force continue (uninstall + install expected version)",
|
|
184
|
+
"Enter 1/2/3 [default: 2]: ",
|
|
185
|
+
].join("\n"),
|
|
186
|
+
);
|
|
187
|
+
const choice = answer.trim();
|
|
188
|
+
if (choice === "1") return "ignore";
|
|
189
|
+
if (choice === "2" || choice === "") return "abort";
|
|
190
|
+
if (choice === "3") return "force";
|
|
191
|
+
output.write("Invalid choice. Please enter 1, 2, or 3.\n");
|
|
192
|
+
}
|
|
193
|
+
} finally {
|
|
194
|
+
rl.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildPrompt(messages: StagedMessage[]): string {
|
|
199
|
+
return messages.map((m) => `user: ${m.content}`).join("\n");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildBootstrapCommand(bin: string, bootstrap: OpenClawBootstrap | undefined, workspacePath: string): string {
|
|
203
|
+
const cfg = bootstrap ?? {};
|
|
204
|
+
const flags: string[] = [];
|
|
205
|
+
|
|
206
|
+
flags.push(`--mode ${shellQuote(cfg.mode ?? "local")}`);
|
|
207
|
+
flags.push(`--flow ${shellQuote(cfg.flow ?? "quickstart")}`);
|
|
208
|
+
flags.push(`--auth-choice ${shellQuote(cfg.auth_choice ?? "skip")}`);
|
|
209
|
+
|
|
210
|
+
if (cfg.non_interactive ?? true) {
|
|
211
|
+
flags.push("--non-interactive");
|
|
212
|
+
}
|
|
213
|
+
if (cfg.accept_risk ?? true) {
|
|
214
|
+
flags.push("--accept-risk");
|
|
215
|
+
}
|
|
216
|
+
if (cfg.reset) {
|
|
217
|
+
flags.push("--reset");
|
|
218
|
+
}
|
|
219
|
+
if (cfg.skip_channels ?? true) {
|
|
220
|
+
flags.push("--skip-channels");
|
|
221
|
+
}
|
|
222
|
+
if (cfg.skip_skills ?? true) {
|
|
223
|
+
flags.push("--skip-skills");
|
|
224
|
+
}
|
|
225
|
+
if (cfg.skip_health ?? true) {
|
|
226
|
+
flags.push("--skip-health");
|
|
227
|
+
}
|
|
228
|
+
if (cfg.skip_ui ?? true) {
|
|
229
|
+
flags.push("--skip-ui");
|
|
230
|
+
}
|
|
231
|
+
if (cfg.skip_daemon ?? true) {
|
|
232
|
+
flags.push("--skip-daemon");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (cfg.install_daemon === true) {
|
|
236
|
+
flags.push("--install-daemon");
|
|
237
|
+
} else if (cfg.install_daemon === false) {
|
|
238
|
+
flags.push("--no-install-daemon");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const [field, flag] of BOOTSTRAP_STRING_FLAGS) {
|
|
242
|
+
const value = cfg[field];
|
|
243
|
+
if (value && value.trim()) {
|
|
244
|
+
flags.push(`${flag} ${shellQuote(value)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const workspaceValue = cfg.workspace?.trim() ? cfg.workspace : workspacePath;
|
|
249
|
+
flags.push(`--workspace ${shellQuote(workspaceValue)}`);
|
|
250
|
+
return `${bin} onboard ${flags.join(" ")}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function bootstrapRuntimeEnv(bootstrap: OpenClawBootstrap | undefined): Record<string, string> {
|
|
254
|
+
if (!bootstrap) {
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
const env: Record<string, string> = {};
|
|
258
|
+
|
|
259
|
+
if (bootstrap.openai_api_key) env.OPENAI_API_KEY = bootstrap.openai_api_key;
|
|
260
|
+
if (bootstrap.anthropic_api_key) env.ANTHROPIC_API_KEY = bootstrap.anthropic_api_key;
|
|
261
|
+
if (bootstrap.openrouter_api_key) env.OPENROUTER_API_KEY = bootstrap.openrouter_api_key;
|
|
262
|
+
if (bootstrap.xai_api_key) env.XAI_API_KEY = bootstrap.xai_api_key;
|
|
263
|
+
if (bootstrap.gemini_api_key) env.GEMINI_API_KEY = bootstrap.gemini_api_key;
|
|
264
|
+
if (bootstrap.ai_gateway_api_key) env.AI_GATEWAY_API_KEY = bootstrap.ai_gateway_api_key;
|
|
265
|
+
if (bootstrap.cloudflare_ai_gateway_api_key) {
|
|
266
|
+
env.CLOUDFLARE_AI_GATEWAY_API_KEY = bootstrap.cloudflare_ai_gateway_api_key;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return env;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export class CommandOpenClawProvider implements OpenClawProvider {
|
|
273
|
+
private readonly stagedMessages = new Map<string, StagedMessage[]>();
|
|
274
|
+
|
|
275
|
+
async ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean): Promise<EnsureVersionResult> {
|
|
276
|
+
const bin = config.bin ?? "openclaw";
|
|
277
|
+
const installPolicy = config.install ?? "auto";
|
|
278
|
+
const useCmd = commandFor(config, "use_version", { bin, version: config.version });
|
|
279
|
+
const installCmd = commandFor(config, "install_version", { bin, version: config.version });
|
|
280
|
+
const uninstallCmd = commandFor(config, "uninstall_version", { bin, version: config.version });
|
|
281
|
+
|
|
282
|
+
if (dryRun) {
|
|
283
|
+
return { installedThisRun: false };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const existedBeforeRun = await commandExists(bin);
|
|
287
|
+
let installedThisRun = false;
|
|
288
|
+
|
|
289
|
+
if (!existedBeforeRun) {
|
|
290
|
+
if (!installCmd.trim()) {
|
|
291
|
+
throw new ClawChefError(
|
|
292
|
+
`OpenClaw is not installed and install_version is empty; cannot install ${config.version}`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
await runShell(installCmd, false);
|
|
296
|
+
installedThisRun = true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!useCmd.trim()) {
|
|
300
|
+
return { installedThisRun };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (installPolicy === "always") {
|
|
304
|
+
if (!installCmd.trim()) {
|
|
305
|
+
throw new ClawChefError(
|
|
306
|
+
`install=always requires install_version command to install ${config.version}`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
await runShell(installCmd, false);
|
|
310
|
+
installedThisRun = true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let currentVersion: string;
|
|
314
|
+
try {
|
|
315
|
+
const versionOut = await runShell(useCmd, false);
|
|
316
|
+
currentVersion = parseVersionOutput(versionOut);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
if (installPolicy === "never" && !installedThisRun) {
|
|
319
|
+
throw err;
|
|
320
|
+
}
|
|
321
|
+
if (!installCmd.trim()) {
|
|
322
|
+
throw new ClawChefError("Requested version is unavailable and install_version is not configured");
|
|
323
|
+
}
|
|
324
|
+
await runShell(installCmd, false);
|
|
325
|
+
installedThisRun = true;
|
|
326
|
+
const versionOutAfterInstall = await runShell(useCmd, false);
|
|
327
|
+
currentVersion = parseVersionOutput(versionOutAfterInstall);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (currentVersion === config.version) {
|
|
331
|
+
return { installedThisRun };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (installedThisRun) {
|
|
335
|
+
throw new ClawChefError(
|
|
336
|
+
`OpenClaw version mismatch after install: current ${currentVersion}, expected ${config.version}`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const choice = await chooseVersionMismatchAction(currentVersion, config.version, silent);
|
|
341
|
+
|
|
342
|
+
if (choice === "ignore") {
|
|
343
|
+
return { installedThisRun: false };
|
|
344
|
+
}
|
|
345
|
+
if (choice === "abort") {
|
|
346
|
+
throw new ClawChefError("Aborted by user due to OpenClaw version mismatch");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!uninstallCmd.trim()) {
|
|
350
|
+
throw new ClawChefError("Force continue requires openclaw.commands.uninstall_version");
|
|
351
|
+
}
|
|
352
|
+
if (!installCmd.trim()) {
|
|
353
|
+
throw new ClawChefError("Force continue requires openclaw.commands.install_version");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
await runShell(uninstallCmd, false);
|
|
357
|
+
await runShell(installCmd, false);
|
|
358
|
+
installedThisRun = true;
|
|
359
|
+
|
|
360
|
+
const versionOutAfter = await runShell(useCmd, false);
|
|
361
|
+
const installedVersion = parseVersionOutput(versionOutAfter);
|
|
362
|
+
if (installedVersion !== config.version) {
|
|
363
|
+
throw new ClawChefError(
|
|
364
|
+
`Version still mismatched after install: current ${installedVersion}, expected ${config.version}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { installedThisRun };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void> {
|
|
372
|
+
const bin = config.bin ?? "openclaw";
|
|
373
|
+
const resetCmd = commandFor(config, "factory_reset", { bin, version: config.version });
|
|
374
|
+
if (!resetCmd.trim()) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
await runShell(resetCmd, dryRun);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async startGateway(config: OpenClawSection, dryRun: boolean): Promise<void> {
|
|
381
|
+
const bin = config.bin ?? "openclaw";
|
|
382
|
+
const startCmd = commandFor(config, "start_gateway", { bin, version: config.version });
|
|
383
|
+
if (!startCmd.trim()) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
await runShell(startCmd, dryRun);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void> {
|
|
390
|
+
const bin = config.bin ?? "openclaw";
|
|
391
|
+
const cmd = config.commands?.create_workspace
|
|
392
|
+
? fillTemplate(config.commands.create_workspace, {
|
|
393
|
+
bin,
|
|
394
|
+
version: config.version,
|
|
395
|
+
workspace: workspace.name,
|
|
396
|
+
path: shellQuote(workspace.path),
|
|
397
|
+
path_raw: workspace.path,
|
|
398
|
+
})
|
|
399
|
+
: buildBootstrapCommand(bin, config.bootstrap, workspace.path);
|
|
400
|
+
if (!cmd.trim()) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
await runShell(cmd, dryRun);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
|
|
407
|
+
const bin = config.bin ?? "openclaw";
|
|
408
|
+
const enablePluginCmd = commandFor(config, "enable_plugin", {
|
|
409
|
+
bin,
|
|
410
|
+
version: config.version,
|
|
411
|
+
channel: channel.channel,
|
|
412
|
+
channel_q: shellQuote(channel.channel),
|
|
413
|
+
});
|
|
414
|
+
if (enablePluginCmd.trim()) {
|
|
415
|
+
await runShell(enablePluginCmd, dryRun);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const flags: string[] = [
|
|
419
|
+
"--channel",
|
|
420
|
+
shellQuote(channel.channel),
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
const fields: Array<[keyof ChannelDef, string]> = [
|
|
424
|
+
["account", "--account"],
|
|
425
|
+
["name", "--name"],
|
|
426
|
+
["token", "--token"],
|
|
427
|
+
["token_file", "--token-file"],
|
|
428
|
+
["bot_token", "--bot-token"],
|
|
429
|
+
["access_token", "--access-token"],
|
|
430
|
+
["app_token", "--app-token"],
|
|
431
|
+
["webhook_url", "--webhook-url"],
|
|
432
|
+
["webhook_path", "--webhook-path"],
|
|
433
|
+
["signal_number", "--signal-number"],
|
|
434
|
+
["password", "--password"],
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
for (const [field, flag] of fields) {
|
|
438
|
+
const value = channel[field];
|
|
439
|
+
if (typeof value === "string" && value.trim()) {
|
|
440
|
+
flags.push(`${flag} ${shellQuote(value)}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (channel.use_env) {
|
|
445
|
+
flags.push("--use-env");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
for (const [rawKey, rawValue] of Object.entries(channel.extra_flags ?? {})) {
|
|
449
|
+
const key = `--${snakeToKebab(rawKey)}`;
|
|
450
|
+
if (typeof rawValue === "boolean") {
|
|
451
|
+
if (rawValue) {
|
|
452
|
+
flags.push(key);
|
|
453
|
+
}
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
flags.push(`${key} ${shellQuote(String(rawValue))}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const cmd = `${bin} channels add ${flags.join(" ")}`;
|
|
460
|
+
await runShell(cmd, dryRun);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async loginChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void> {
|
|
464
|
+
if (!channel.login) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const bin = config.bin ?? "openclaw";
|
|
469
|
+
const account = channel.login_account?.trim() || channel.account?.trim() || "";
|
|
470
|
+
const accountArg = account ? ` --account ${shellQuote(account)}` : "";
|
|
471
|
+
const cmd = commandFor(config, "login_channel", {
|
|
472
|
+
bin,
|
|
473
|
+
version: config.version,
|
|
474
|
+
channel: channel.channel,
|
|
475
|
+
channel_q: shellQuote(channel.channel),
|
|
476
|
+
account,
|
|
477
|
+
account_q: shellQuote(account),
|
|
478
|
+
account_arg: accountArg,
|
|
479
|
+
});
|
|
480
|
+
if (!cmd.trim()) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
await runShellInteractive(cmd, dryRun);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async createAgent(
|
|
487
|
+
config: OpenClawSection,
|
|
488
|
+
agent: AgentDef,
|
|
489
|
+
workspacePath: string,
|
|
490
|
+
dryRun: boolean,
|
|
491
|
+
): Promise<void> {
|
|
492
|
+
const bin = config.bin ?? "openclaw";
|
|
493
|
+
const model = agent.model ?? "";
|
|
494
|
+
const cmd = commandFor(config, "create_agent", {
|
|
495
|
+
bin,
|
|
496
|
+
version: config.version,
|
|
497
|
+
workspace: agent.workspace,
|
|
498
|
+
workspace_path: shellQuote(workspacePath),
|
|
499
|
+
workspace_path_raw: workspacePath,
|
|
500
|
+
agent: agent.name,
|
|
501
|
+
model: shellQuote(model),
|
|
502
|
+
model_raw: model,
|
|
503
|
+
});
|
|
504
|
+
if (!cmd.trim()) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
await runShell(cmd, dryRun);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
511
|
+
if (msg.includes("already exists")) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
throw err;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async installSkill(
|
|
519
|
+
config: OpenClawSection,
|
|
520
|
+
workspace: string,
|
|
521
|
+
agent: string,
|
|
522
|
+
skill: string,
|
|
523
|
+
dryRun: boolean,
|
|
524
|
+
): Promise<void> {
|
|
525
|
+
const bin = config.bin ?? "openclaw";
|
|
526
|
+
const cmd = commandFor(config, "install_skill", {
|
|
527
|
+
bin,
|
|
528
|
+
version: config.version,
|
|
529
|
+
workspace,
|
|
530
|
+
agent,
|
|
531
|
+
skill,
|
|
532
|
+
skill_q: shellQuote(skill),
|
|
533
|
+
});
|
|
534
|
+
if (!cmd.trim()) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
await runShell(cmd, dryRun);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async sendMessage(
|
|
541
|
+
config: OpenClawSection,
|
|
542
|
+
conversation: ConversationDef,
|
|
543
|
+
content: string,
|
|
544
|
+
dryRun: boolean,
|
|
545
|
+
): Promise<void> {
|
|
546
|
+
const key = `${conversation.workspace}::${conversation.agent}`;
|
|
547
|
+
const staged = this.stagedMessages.get(key) ?? [];
|
|
548
|
+
staged.push({ content });
|
|
549
|
+
this.stagedMessages.set(key, staged);
|
|
550
|
+
|
|
551
|
+
const template = config.commands?.send_message ?? DEFAULT_COMMANDS.send_message;
|
|
552
|
+
if (!template.trim() || template.trim() === "true") {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "clawchef-msg-"));
|
|
557
|
+
try {
|
|
558
|
+
const msgPath = path.join(tempDir, "message.json");
|
|
559
|
+
await writeFile(msgPath, JSON.stringify({ role: "user", content }, null, 2), "utf8");
|
|
560
|
+
const bin = config.bin ?? "openclaw";
|
|
561
|
+
const cmd = commandFor(config, "send_message", {
|
|
562
|
+
bin,
|
|
563
|
+
version: config.version,
|
|
564
|
+
workspace: conversation.workspace,
|
|
565
|
+
agent: conversation.agent,
|
|
566
|
+
role: "user",
|
|
567
|
+
role_q: shellQuote("user"),
|
|
568
|
+
content,
|
|
569
|
+
content_q: shellQuote(content),
|
|
570
|
+
message_file: shellQuote(msgPath),
|
|
571
|
+
message_file_raw: msgPath,
|
|
572
|
+
});
|
|
573
|
+
await runShell(cmd, dryRun);
|
|
574
|
+
} finally {
|
|
575
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async runAgent(config: OpenClawSection, conversation: ConversationDef, dryRun: boolean): Promise<string> {
|
|
580
|
+
const key = `${conversation.workspace}::${conversation.agent}`;
|
|
581
|
+
const staged = this.stagedMessages.get(key) ?? [];
|
|
582
|
+
const prompt = buildPrompt(staged);
|
|
583
|
+
const bin = config.bin ?? "openclaw";
|
|
584
|
+
const cmd = commandFor(config, "run_agent", {
|
|
585
|
+
bin,
|
|
586
|
+
version: config.version,
|
|
587
|
+
workspace: conversation.workspace,
|
|
588
|
+
agent: conversation.agent,
|
|
589
|
+
prompt,
|
|
590
|
+
prompt_q: shellQuote(prompt),
|
|
591
|
+
});
|
|
592
|
+
return runShell(cmd, dryRun, bootstrapRuntimeEnv(config.bootstrap));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RunOptions } from "../types.js";
|
|
2
|
+
import { CommandOpenClawProvider } from "./command-provider.js";
|
|
3
|
+
import { MockOpenClawProvider } from "./mock-provider.js";
|
|
4
|
+
import { RemoteOpenClawProvider } from "./remote-provider.js";
|
|
5
|
+
import type { OpenClawProvider } from "./provider.js";
|
|
6
|
+
|
|
7
|
+
export function createProvider(options: RunOptions): OpenClawProvider {
|
|
8
|
+
const provider = options.provider;
|
|
9
|
+
if (provider === "mock") {
|
|
10
|
+
return new MockOpenClawProvider();
|
|
11
|
+
}
|
|
12
|
+
if (provider === "remote") {
|
|
13
|
+
return new RemoteOpenClawProvider(options.remote);
|
|
14
|
+
}
|
|
15
|
+
return new CommandOpenClawProvider();
|
|
16
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../types.js";
|
|
2
|
+
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
3
|
+
|
|
4
|
+
interface MockState {
|
|
5
|
+
installedVersions: Set<string>;
|
|
6
|
+
currentVersion?: string;
|
|
7
|
+
workspaces: Set<string>;
|
|
8
|
+
channels: Set<string>;
|
|
9
|
+
agents: Set<string>;
|
|
10
|
+
skills: Set<string>;
|
|
11
|
+
messages: Map<string, string[]>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class MockOpenClawProvider implements OpenClawProvider {
|
|
15
|
+
private state: MockState = {
|
|
16
|
+
installedVersions: new Set(),
|
|
17
|
+
workspaces: new Set(),
|
|
18
|
+
channels: new Set(),
|
|
19
|
+
agents: new Set(),
|
|
20
|
+
skills: new Set(),
|
|
21
|
+
messages: new Map(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean): Promise<EnsureVersionResult> {
|
|
25
|
+
const policy = config.install ?? "auto";
|
|
26
|
+
const installed = this.state.installedVersions.has(config.version);
|
|
27
|
+
let installedThisRun = false;
|
|
28
|
+
|
|
29
|
+
if (policy === "always") {
|
|
30
|
+
this.state.installedVersions.add(config.version);
|
|
31
|
+
installedThisRun = true;
|
|
32
|
+
} else if (policy === "auto" && !installed) {
|
|
33
|
+
this.state.installedVersions.add(config.version);
|
|
34
|
+
installedThisRun = true;
|
|
35
|
+
} else if (policy === "never" && !installed) {
|
|
36
|
+
throw new Error(`mock: version ${config.version} is not installed`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.state.currentVersion = config.version;
|
|
40
|
+
return { installedThisRun };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async factoryReset(_config: OpenClawSection, _dryRun: boolean): Promise<void> {
|
|
44
|
+
this.state.workspaces.clear();
|
|
45
|
+
this.state.channels.clear();
|
|
46
|
+
this.state.agents.clear();
|
|
47
|
+
this.state.skills.clear();
|
|
48
|
+
this.state.messages.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void> {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async createWorkspace(_config: OpenClawSection, workspace: ResolvedWorkspaceDef, _dryRun: boolean): Promise<void> {
|
|
56
|
+
this.state.workspaces.add(workspace.name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async configureChannel(_config: OpenClawSection, channel: ChannelDef, _dryRun: boolean): Promise<void> {
|
|
60
|
+
this.state.channels.add(`${channel.channel}::${channel.account ?? "default"}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async loginChannel(_config: OpenClawSection, _channel: ChannelDef, _dryRun: boolean): Promise<void> {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async createAgent(
|
|
68
|
+
_config: OpenClawSection,
|
|
69
|
+
agent: AgentDef,
|
|
70
|
+
_workspacePath: string,
|
|
71
|
+
_dryRun: boolean,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
this.state.agents.add(`${agent.workspace}::${agent.name}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async installSkill(
|
|
77
|
+
_config: OpenClawSection,
|
|
78
|
+
workspace: string,
|
|
79
|
+
agent: string,
|
|
80
|
+
skill: string,
|
|
81
|
+
_dryRun: boolean,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
this.state.skills.add(`${workspace}::${agent}::${skill}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async sendMessage(
|
|
87
|
+
_config: OpenClawSection,
|
|
88
|
+
conversation: ConversationDef,
|
|
89
|
+
content: string,
|
|
90
|
+
_dryRun: boolean,
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
const key = `${conversation.workspace}::${conversation.agent}`;
|
|
93
|
+
const queue = this.state.messages.get(key) ?? [];
|
|
94
|
+
queue.push(`user: ${content}`);
|
|
95
|
+
this.state.messages.set(key, queue);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async runAgent(_config: OpenClawSection, conversation: ConversationDef, _dryRun: boolean): Promise<string> {
|
|
99
|
+
const key = `${conversation.workspace}::${conversation.agent}`;
|
|
100
|
+
const queue = this.state.messages.get(key) ?? [];
|
|
101
|
+
const last = queue[queue.length - 1] ?? "";
|
|
102
|
+
return `mock-reply -> ${last}`;
|
|
103
|
+
}
|
|
104
|
+
}
|