clawchef 0.1.0 → 0.1.2
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 +88 -60
- package/dist/api.d.ts +4 -0
- package/dist/api.js +6 -0
- package/dist/cli.js +36 -0
- package/dist/openclaw/command-provider.d.ts +1 -0
- package/dist/openclaw/command-provider.js +30 -9
- package/dist/openclaw/mock-provider.d.ts +1 -0
- package/dist/openclaw/mock-provider.js +3 -0
- package/dist/openclaw/provider.d.ts +1 -0
- package/dist/openclaw/remote-provider.d.ts +1 -0
- package/dist/openclaw/remote-provider.js +5 -0
- package/dist/orchestrator.js +64 -2
- package/dist/recipe.js +9 -0
- package/dist/scaffold.d.ts +8 -0
- package/dist/scaffold.js +172 -0
- package/dist/schema.d.ts +17 -0
- package/dist/schema.js +3 -0
- package/dist/types.d.ts +4 -0
- package/package.json +9 -1
- package/recipes/openclaw-local.yaml +0 -1
- package/recipes/openclaw-telegram-mock.yaml +2 -0
- package/recipes/openclaw-telegram.yaml +0 -1
- package/src/api.ts +10 -0
- package/src/cli.ts +39 -0
- package/src/openclaw/command-provider.ts +32 -9
- package/src/openclaw/mock-provider.ts +4 -0
- package/src/openclaw/provider.ts +1 -0
- package/src/openclaw/remote-provider.ts +11 -0
- package/src/orchestrator.ts +84 -2
- package/src/recipe.ts +14 -0
- package/src/scaffold.ts +197 -0
- package/src/schema.ts +3 -0
- package/src/types.ts +4 -0
package/src/orchestrator.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { mkdir, access, copyFile, writeFile, readFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, access, copyFile, writeFile, readFile, readdir, stat } from "node:fs/promises";
|
|
4
4
|
import { constants } from "node:fs";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -37,7 +37,9 @@ function resolveWorkspacePath(recipeOrigin: RecipeOrigin, name: string, configur
|
|
|
37
37
|
}
|
|
38
38
|
return path.resolve(configuredPath);
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
const trimmedName = name.trim() || name;
|
|
41
|
+
const workspaceName = trimmedName.startsWith("workspace-") ? trimmedName : `workspace-${trimmedName}`;
|
|
42
|
+
return path.join(homedir(), ".openclaw", workspaceName);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
function isHttpUrl(value: string): boolean {
|
|
@@ -87,6 +89,35 @@ async function readBinaryFromRef(recipeOrigin: RecipeOrigin, reference: string):
|
|
|
87
89
|
return Buffer.from(bytes);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
interface LocalAssetFile {
|
|
93
|
+
absolutePath: string;
|
|
94
|
+
relativePath: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function collectLocalAssetFiles(rootDir: string, relDir = ""): Promise<LocalAssetFile[]> {
|
|
98
|
+
const currentDir = relDir ? path.join(rootDir, relDir) : rootDir;
|
|
99
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
100
|
+
const out: LocalAssetFile[] = [];
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const nextRel = relDir ? path.join(relDir, entry.name) : entry.name;
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
out.push(...await collectLocalAssetFiles(rootDir, nextRel));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (entry.isFile()) {
|
|
109
|
+
out.push({
|
|
110
|
+
absolutePath: path.join(rootDir, nextRel),
|
|
111
|
+
relativePath: nextRel,
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
throw new ClawChefError(`Unsupported entry in assets directory: ${path.join(rootDir, nextRel)}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
90
121
|
async function confirmFactoryReset(options: RunOptions): Promise<boolean> {
|
|
91
122
|
if (options.silent || options.dryRun) {
|
|
92
123
|
return true;
|
|
@@ -131,6 +162,13 @@ export async function runRecipe(
|
|
|
131
162
|
logger.info("Factory reset completed");
|
|
132
163
|
}
|
|
133
164
|
|
|
165
|
+
const pluginSpecs = Array.from(new Set([...(recipe.openclaw.plugins ?? []), ...options.plugins].map((v) => v.trim())))
|
|
166
|
+
.filter((v) => v.length > 0);
|
|
167
|
+
for (const pluginSpec of pluginSpecs) {
|
|
168
|
+
await provider.installPlugin(recipe.openclaw, pluginSpec, options.dryRun);
|
|
169
|
+
logger.info(`Plugin preinstalled: ${pluginSpec}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
134
172
|
for (const ws of recipe.workspaces ?? []) {
|
|
135
173
|
const absPath = resolveWorkspacePath(recipeOrigin, ws.name, ws.path);
|
|
136
174
|
workspacePaths.set(ws.name, absPath);
|
|
@@ -139,6 +177,50 @@ export async function runRecipe(
|
|
|
139
177
|
}
|
|
140
178
|
await provider.createWorkspace(recipe.openclaw, { ...ws, path: absPath }, options.dryRun);
|
|
141
179
|
logger.info(`Workspace created: ${ws.name}`);
|
|
180
|
+
|
|
181
|
+
if (!ws.assets?.trim()) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const resolvedAssets = resolveFileRef(recipeOrigin, ws.assets);
|
|
186
|
+
if (resolvedAssets.kind !== "local") {
|
|
187
|
+
throw new ClawChefError(
|
|
188
|
+
`Workspace assets must resolve to a local directory: ${ws.assets}. Direct URL recipes cannot use workspaces[].assets.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let assetDirStat;
|
|
193
|
+
try {
|
|
194
|
+
assetDirStat = await stat(resolvedAssets.value);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
197
|
+
throw new ClawChefError(`Workspace assets path is not accessible: ${resolvedAssets.value} (${message})`);
|
|
198
|
+
}
|
|
199
|
+
if (!assetDirStat.isDirectory()) {
|
|
200
|
+
throw new ClawChefError(`Workspace assets must be a directory: ${resolvedAssets.value}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
204
|
+
for (const assetFile of assetFiles) {
|
|
205
|
+
if (provider.materializeFile) {
|
|
206
|
+
const content = await readFile(assetFile.absolutePath, "utf8");
|
|
207
|
+
await provider.materializeFile(
|
|
208
|
+
recipe.openclaw,
|
|
209
|
+
ws.name,
|
|
210
|
+
assetFile.relativePath,
|
|
211
|
+
content,
|
|
212
|
+
true,
|
|
213
|
+
options.dryRun,
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
const target = path.resolve(absPath, assetFile.relativePath);
|
|
217
|
+
if (!options.dryRun) {
|
|
218
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
219
|
+
await copyFile(assetFile.absolutePath, target);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
logger.info(`Workspace asset copied: ${ws.name}/${assetFile.relativePath}`);
|
|
223
|
+
}
|
|
142
224
|
}
|
|
143
225
|
|
|
144
226
|
for (const agent of recipe.agents ?? []) {
|
package/src/recipe.ts
CHANGED
|
@@ -169,6 +169,11 @@ function collectVars(recipe: Recipe, cliVars: Record<string, string>): Record<st
|
|
|
169
169
|
|
|
170
170
|
function semanticValidate(recipe: Recipe): void {
|
|
171
171
|
const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
|
|
172
|
+
for (const workspace of recipe.workspaces ?? []) {
|
|
173
|
+
if (workspace.assets !== undefined && !workspace.assets.trim()) {
|
|
174
|
+
throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
172
177
|
for (const agent of recipe.agents ?? []) {
|
|
173
178
|
if (!ws.has(agent.workspace)) {
|
|
174
179
|
throw new ClawChefError(`Agent ${agent.name} references missing workspace: ${agent.workspace}`);
|
|
@@ -198,6 +203,15 @@ function semanticValidate(recipe: Recipe): void {
|
|
|
198
203
|
);
|
|
199
204
|
}
|
|
200
205
|
|
|
206
|
+
if (
|
|
207
|
+
channel.channel === "telegram" &&
|
|
208
|
+
(channel.login || channel.login_mode !== undefined || channel.login_account !== undefined)
|
|
209
|
+
) {
|
|
210
|
+
throw new ClawChefError(
|
|
211
|
+
"channels[] entry for telegram does not support login/login_mode/login_account. Configure token (or use_env/token_file), then start gateway.",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
201
215
|
const hasAuth =
|
|
202
216
|
Boolean(channel.use_env) ||
|
|
203
217
|
Boolean(channel.token?.trim()) ||
|
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { access, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { ClawChefError } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
export interface ScaffoldOptions {
|
|
7
|
+
projectName?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ScaffoldResult {
|
|
11
|
+
targetDir: string;
|
|
12
|
+
projectName: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeProjectName(value: string): string {
|
|
16
|
+
const normalized = value
|
|
17
|
+
.trim()
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[\s_]+/g, "-")
|
|
20
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
21
|
+
.replace(/-+/g, "-")
|
|
22
|
+
.replace(/^-|-$/g, "");
|
|
23
|
+
if (!normalized) {
|
|
24
|
+
throw new ClawChefError("Project name is empty after normalization. Use letters, numbers, spaces, underscore, or dash.");
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
30
|
+
try {
|
|
31
|
+
await access(filePath, constants.F_OK);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ensureDirectoryEmpty(targetDir: string): Promise<void> {
|
|
39
|
+
const exists = await pathExists(targetDir);
|
|
40
|
+
if (!exists) {
|
|
41
|
+
await mkdir(targetDir, { recursive: true });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const entries = await readdir(targetDir);
|
|
45
|
+
if (entries.length > 0) {
|
|
46
|
+
throw new ClawChefError(`Target directory is not empty: ${targetDir}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makePackageJson(projectName: string): string {
|
|
51
|
+
const content = {
|
|
52
|
+
name: `${projectName}-recipe`,
|
|
53
|
+
version: "0.1.0",
|
|
54
|
+
private: true,
|
|
55
|
+
type: "module",
|
|
56
|
+
scripts: {
|
|
57
|
+
"test:recipe": "node --test test/recipe-smoke.test.mjs",
|
|
58
|
+
test: "node --test \"test/**/*.test.mjs\"",
|
|
59
|
+
},
|
|
60
|
+
devDependencies: {
|
|
61
|
+
"telegram-api-mock-server": "^0.1.5",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
return `${JSON.stringify(content, null, 2)}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeRecipeYaml(projectName: string): string {
|
|
68
|
+
return `version: "1"
|
|
69
|
+
name: "${projectName}"
|
|
70
|
+
|
|
71
|
+
params:
|
|
72
|
+
openclaw_version:
|
|
73
|
+
default: "2026.2.9"
|
|
74
|
+
workspace_name:
|
|
75
|
+
default: "${projectName}"
|
|
76
|
+
agent_name:
|
|
77
|
+
default: "${projectName}"
|
|
78
|
+
agent_model:
|
|
79
|
+
default: "openai/gpt-4.1"
|
|
80
|
+
telegram_mock_api_key:
|
|
81
|
+
required: true
|
|
82
|
+
|
|
83
|
+
openclaw:
|
|
84
|
+
bin: "openclaw"
|
|
85
|
+
version: "\${openclaw_version}"
|
|
86
|
+
install: "never"
|
|
87
|
+
plugins:
|
|
88
|
+
- "openclaw-telegram-mock-channel"
|
|
89
|
+
|
|
90
|
+
workspaces:
|
|
91
|
+
- name: "\${workspace_name}"
|
|
92
|
+
assets: "./${projectName}-assets"
|
|
93
|
+
|
|
94
|
+
agents:
|
|
95
|
+
- workspace: "\${workspace_name}"
|
|
96
|
+
name: "\${agent_name}"
|
|
97
|
+
model: "\${agent_model}"
|
|
98
|
+
|
|
99
|
+
channels:
|
|
100
|
+
- channel: "telegram-mock"
|
|
101
|
+
account: "default"
|
|
102
|
+
token: "\${telegram_mock_api_key}"
|
|
103
|
+
extra_flags:
|
|
104
|
+
mock_bind: "127.0.0.1:18790"
|
|
105
|
+
mock_api_key: "\${telegram_mock_api_key}"
|
|
106
|
+
mode: "webhook"
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function makeAgentsDoc(projectName: string): string {
|
|
111
|
+
return `# ${projectName}
|
|
112
|
+
|
|
113
|
+
Project scaffold generated by clawchef.
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function makeIdentityDoc(projectName: string): string {
|
|
118
|
+
return `# Identity
|
|
119
|
+
|
|
120
|
+
You are the ${projectName} assistant.
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function makeSoulDoc(): string {
|
|
125
|
+
return `# Soul
|
|
126
|
+
|
|
127
|
+
Keep responses practical, concise, and action-oriented.
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function makeToolsDoc(): string {
|
|
132
|
+
return `# Tools
|
|
133
|
+
|
|
134
|
+
- Use available workspace files first.
|
|
135
|
+
- Ask for missing secrets explicitly.
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function makeSchedulingScript(): string {
|
|
140
|
+
return `#!/usr/bin/env node
|
|
141
|
+
import process from "node:process";
|
|
142
|
+
|
|
143
|
+
const message = process.argv.slice(2).join(" ").trim();
|
|
144
|
+
if (!message) {
|
|
145
|
+
console.error("Usage: node scripts/scheduling.mjs <message>");
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log("[scheduling] " + message);
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function makeRecipeSmokeTest(): string {
|
|
154
|
+
return `import test from "node:test";
|
|
155
|
+
import assert from "node:assert/strict";
|
|
156
|
+
import { access } from "node:fs/promises";
|
|
157
|
+
|
|
158
|
+
test("recipe scaffold files exist", async () => {
|
|
159
|
+
await access("src/recipe.yaml");
|
|
160
|
+
await access("package.json");
|
|
161
|
+
assert.ok(true);
|
|
162
|
+
});
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function scaffoldProject(targetDirArg?: string, options: ScaffoldOptions = {}): Promise<ScaffoldResult> {
|
|
167
|
+
const targetDir = path.resolve(targetDirArg?.trim() ? targetDirArg : process.cwd());
|
|
168
|
+
await ensureDirectoryEmpty(targetDir);
|
|
169
|
+
|
|
170
|
+
const defaultName = path.basename(targetDir);
|
|
171
|
+
const rawProjectName = options.projectName?.trim() || defaultName;
|
|
172
|
+
const projectName = normalizeProjectName(rawProjectName);
|
|
173
|
+
|
|
174
|
+
const srcDir = path.join(targetDir, "src");
|
|
175
|
+
const assetsDir = path.join(srcDir, `${projectName}-assets`);
|
|
176
|
+
const assetsScriptsDir = path.join(assetsDir, "scripts");
|
|
177
|
+
const testDir = path.join(targetDir, "test");
|
|
178
|
+
|
|
179
|
+
await mkdir(srcDir, { recursive: true });
|
|
180
|
+
await mkdir(assetsDir, { recursive: true });
|
|
181
|
+
await mkdir(assetsScriptsDir, { recursive: true });
|
|
182
|
+
await mkdir(testDir, { recursive: true });
|
|
183
|
+
|
|
184
|
+
await writeFile(path.join(targetDir, "package.json"), makePackageJson(projectName), "utf8");
|
|
185
|
+
await writeFile(path.join(srcDir, "recipe.yaml"), makeRecipeYaml(projectName), "utf8");
|
|
186
|
+
await writeFile(path.join(assetsDir, "AGENTS.md"), makeAgentsDoc(projectName), "utf8");
|
|
187
|
+
await writeFile(path.join(assetsDir, "IDENTITY.md"), makeIdentityDoc(projectName), "utf8");
|
|
188
|
+
await writeFile(path.join(assetsDir, "SOUL.md"), makeSoulDoc(), "utf8");
|
|
189
|
+
await writeFile(path.join(assetsDir, "TOOLS.md"), makeToolsDoc(), "utf8");
|
|
190
|
+
await writeFile(path.join(assetsScriptsDir, "scheduling.mjs"), makeSchedulingScript(), "utf8");
|
|
191
|
+
await writeFile(path.join(testDir, "recipe-smoke.test.mjs"), makeRecipeSmokeTest(), "utf8");
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
targetDir,
|
|
195
|
+
projectName,
|
|
196
|
+
};
|
|
197
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -11,6 +11,7 @@ const openClawCommandsSchema = z
|
|
|
11
11
|
use_version: z.string().optional(),
|
|
12
12
|
install_version: z.string().optional(),
|
|
13
13
|
uninstall_version: z.string().optional(),
|
|
14
|
+
install_plugin: z.string().optional(),
|
|
14
15
|
factory_reset: z.string().optional(),
|
|
15
16
|
start_gateway: z.string().optional(),
|
|
16
17
|
enable_plugin: z.string().optional(),
|
|
@@ -58,6 +59,7 @@ const openClawSchema = z
|
|
|
58
59
|
bin: z.string().optional(),
|
|
59
60
|
version: z.string(),
|
|
60
61
|
install: z.enum(["auto", "always", "never"]).optional(),
|
|
62
|
+
plugins: z.array(z.string().min(1)).optional(),
|
|
61
63
|
bootstrap: openClawBootstrapSchema.optional(),
|
|
62
64
|
commands: openClawCommandsSchema.optional(),
|
|
63
65
|
})
|
|
@@ -67,6 +69,7 @@ const workspaceSchema = z
|
|
|
67
69
|
.object({
|
|
68
70
|
name: z.string().min(1),
|
|
69
71
|
path: z.string().min(1).optional(),
|
|
72
|
+
assets: z.string().min(1).optional(),
|
|
70
73
|
})
|
|
71
74
|
.strict();
|
|
72
75
|
|
package/src/types.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface OpenClawCommandOverrides {
|
|
|
20
20
|
use_version?: string;
|
|
21
21
|
install_version?: string;
|
|
22
22
|
uninstall_version?: string;
|
|
23
|
+
install_plugin?: string;
|
|
23
24
|
factory_reset?: string;
|
|
24
25
|
start_gateway?: string;
|
|
25
26
|
enable_plugin?: string;
|
|
@@ -63,6 +64,7 @@ export interface OpenClawSection {
|
|
|
63
64
|
bin?: string;
|
|
64
65
|
version: string;
|
|
65
66
|
install?: InstallPolicy;
|
|
67
|
+
plugins?: string[];
|
|
66
68
|
bootstrap?: OpenClawBootstrap;
|
|
67
69
|
commands?: OpenClawCommandOverrides;
|
|
68
70
|
}
|
|
@@ -70,6 +72,7 @@ export interface OpenClawSection {
|
|
|
70
72
|
export interface WorkspaceDef {
|
|
71
73
|
name: string;
|
|
72
74
|
path?: string;
|
|
75
|
+
assets?: string;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
export interface ChannelDef {
|
|
@@ -141,6 +144,7 @@ export interface Recipe {
|
|
|
141
144
|
|
|
142
145
|
export interface RunOptions {
|
|
143
146
|
vars: Record<string, string>;
|
|
147
|
+
plugins: string[];
|
|
144
148
|
dryRun: boolean;
|
|
145
149
|
allowMissing: boolean;
|
|
146
150
|
verbose: boolean;
|