@jiggai/recipes 0.2.17 → 0.2.18
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 +2 -2
- package/docs/COMMANDS.md +12 -0
- package/index.ts +129 -8
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -56,8 +56,8 @@ openclaw recipes dispatch \
|
|
|
56
56
|
|
|
57
57
|
## Commands (high level)
|
|
58
58
|
- `openclaw recipes list|show|status`
|
|
59
|
-
- `openclaw recipes scaffold` (agent → `workspace-<agentId>`)
|
|
60
|
-
- `openclaw recipes scaffold-team` (team → `workspace-<teamId>` + `roles/<role>/`)
|
|
59
|
+
- `openclaw recipes scaffold` (agent → `workspace-<agentId>` + writes workspace recipe `~/.openclaw/workspace/recipes/<agentId>.md` by default)
|
|
60
|
+
- `openclaw recipes scaffold-team` (team → `workspace-<teamId>` + `roles/<role>/` + writes workspace recipe `~/.openclaw/workspace/recipes/<teamId>.md` by default)
|
|
61
61
|
- `openclaw recipes install <idOrSlug> [--yes] [--global|--agent-id <id>|--team-id <id>]` (skills: global or scoped)
|
|
62
62
|
- `openclaw recipes bind|unbind|bindings` (multi-agent routing)
|
|
63
63
|
- `openclaw recipes dispatch ...` (request → inbox + ticket + assignment)
|
package/docs/COMMANDS.md
CHANGED
|
@@ -41,9 +41,15 @@ openclaw recipes scaffold project-manager --agent-id pm --name "Project Manager"
|
|
|
41
41
|
Options:
|
|
42
42
|
- `--agent-id <id>` (required)
|
|
43
43
|
- `--name <name>`
|
|
44
|
+
- `--recipe-id <recipeId>` (workspace recipe id to write; default: `<agentId>`)
|
|
45
|
+
- `--auto-increment` (if the workspace recipe id is taken, pick `<agentId>-2/-3/...`)
|
|
46
|
+
- `--overwrite-recipe` (overwrite the generated workspace recipe file if it already exists)
|
|
44
47
|
- `--overwrite` (overwrite recipe-managed files)
|
|
45
48
|
- `--apply-config` (write/update `agents.list[]` in OpenClaw config)
|
|
46
49
|
|
|
50
|
+
Also writes a workspace recipe file:
|
|
51
|
+
- `~/.openclaw/workspace/recipes/<recipeId>.md`
|
|
52
|
+
|
|
47
53
|
## `scaffold-team <recipeId>`
|
|
48
54
|
|
|
49
55
|
### Cron installation config
|
|
@@ -64,9 +70,15 @@ openclaw recipes scaffold-team development-team \
|
|
|
64
70
|
Options:
|
|
65
71
|
- `--team-id <teamId>` (required)
|
|
66
72
|
- **Must end with `-team`** (enforced)
|
|
73
|
+
- `--recipe-id <recipeId>` (workspace recipe id to write; default: `<teamId>`)
|
|
74
|
+
- `--auto-increment` (if the workspace recipe id is taken, pick `<teamId>-2/-3/...`)
|
|
75
|
+
- `--overwrite-recipe` (overwrite the generated workspace recipe file if it already exists)
|
|
67
76
|
- `--overwrite`
|
|
68
77
|
- `--apply-config`
|
|
69
78
|
|
|
79
|
+
Also writes a workspace recipe file:
|
|
80
|
+
- `~/.openclaw/workspace/recipes/<recipeId>.md`
|
|
81
|
+
|
|
70
82
|
Creates a shared team workspace root:
|
|
71
83
|
|
|
72
84
|
- `~/.openclaw/workspace-<teamId>/...`
|
package/index.ts
CHANGED
|
@@ -2092,7 +2092,10 @@ const recipesPlugin = {
|
|
|
2092
2092
|
.argument("<recipeId>", "Recipe id")
|
|
2093
2093
|
.requiredOption("--agent-id <id>", "Agent id")
|
|
2094
2094
|
.option("--name <name>", "Agent display name")
|
|
2095
|
+
.option("--recipe-id <recipeId>", "Workspace recipe id to write (default: <agentId>)")
|
|
2095
2096
|
.option("--overwrite", "Overwrite existing recipe-managed files")
|
|
2097
|
+
.option("--overwrite-recipe", "Overwrite the generated workspace recipe file (workspace/recipes/<agentId>.md) if it already exists")
|
|
2098
|
+
.option("--auto-increment", "If the workspace recipe id is taken, pick the next available <agentId>-2/-3/...")
|
|
2096
2099
|
.option("--apply-config", "Write the agent into openclaw config (agents.list)")
|
|
2097
2100
|
.action(async (recipeId: string, options: any) => {
|
|
2098
2101
|
const loaded = await loadRecipeById(api, recipeId);
|
|
@@ -2113,19 +2116,86 @@ const recipesPlugin = {
|
|
|
2113
2116
|
return;
|
|
2114
2117
|
}
|
|
2115
2118
|
|
|
2119
|
+
const agentId = String(options.agentId);
|
|
2116
2120
|
const baseWorkspace = api.config.agents?.defaults?.workspace ?? "~/.openclaw/workspace";
|
|
2117
2121
|
// Put standalone agent workspaces alongside the default workspace (same parent dir).
|
|
2118
|
-
const resolvedWorkspaceRoot = path.resolve(baseWorkspace, "..", `workspace-${
|
|
2122
|
+
const resolvedWorkspaceRoot = path.resolve(baseWorkspace, "..", `workspace-${agentId}`);
|
|
2123
|
+
|
|
2124
|
+
// Also create a workspace recipe file for this installed agent.
|
|
2125
|
+
// This establishes a stable, editable recipe id that matches the agent id (no custom- prefix).
|
|
2126
|
+
const recipesDir = path.join(workspaceRoot, "recipes");
|
|
2127
|
+
await ensureDir(recipesDir);
|
|
2128
|
+
|
|
2129
|
+
const overwriteRecipe = !!options.overwriteRecipe;
|
|
2130
|
+
const autoIncrement = !!options.autoIncrement;
|
|
2131
|
+
|
|
2132
|
+
function suggestedRecipeIds(baseId: string) {
|
|
2133
|
+
return [`${baseId}-2`, `${baseId}-3`, `${baseId}-4`];
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
async function recipeIdTaken(id: string) {
|
|
2137
|
+
const filePath = path.join(recipesDir, `${id}.md`);
|
|
2138
|
+
if (await fileExists(filePath)) return true;
|
|
2139
|
+
try {
|
|
2140
|
+
await loadRecipeById(api, id);
|
|
2141
|
+
return true;
|
|
2142
|
+
} catch {
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
async function pickRecipeId(baseId: string) {
|
|
2148
|
+
if (!(await recipeIdTaken(baseId))) return baseId;
|
|
2149
|
+
if (overwriteRecipe) {
|
|
2150
|
+
const basePath = path.join(recipesDir, `${baseId}.md`);
|
|
2151
|
+
if (!(await fileExists(basePath))) {
|
|
2152
|
+
throw new Error(
|
|
2153
|
+
`Recipe id is already taken by a non-workspace recipe: ${baseId}. Choose a different id (e.g. ${baseId}-2) or pass --auto-increment.`,
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
return baseId;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
if (autoIncrement) {
|
|
2160
|
+
let n = 2;
|
|
2161
|
+
while (n < 1000) {
|
|
2162
|
+
const candidate = `${baseId}-${n}`;
|
|
2163
|
+
if (!(await recipeIdTaken(candidate))) return candidate;
|
|
2164
|
+
n += 1;
|
|
2165
|
+
}
|
|
2166
|
+
throw new Error(`No available recipe id found for ${baseId} (tried up to -999)`);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
const suggestions = suggestedRecipeIds(baseId);
|
|
2170
|
+
const msg = [
|
|
2171
|
+
`Recipe id already exists: ${baseId}`,
|
|
2172
|
+
`Refusing to overwrite workspace recipe: recipes/${baseId}.md`,
|
|
2173
|
+
`Pick a different recipe id and re-run with --recipe-id. Suggestions: ${suggestions.join(", ")}`,
|
|
2174
|
+
...suggestions.map((s) => ` openclaw recipes scaffold ${recipeId} --agent-id ${agentId} --recipe-id ${s}`),
|
|
2175
|
+
`Or re-run with --auto-increment to pick ${baseId}-2/-3/... automatically, or pass --overwrite-recipe to overwrite the existing workspace recipe file.`,
|
|
2176
|
+
].join("\n");
|
|
2177
|
+
throw new Error(msg);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
const explicitRecipeId = typeof options.recipeId === "string" ? String(options.recipeId).trim() : "";
|
|
2181
|
+
const baseRecipeId = explicitRecipeId || agentId;
|
|
2182
|
+
const workspaceRecipeId = await pickRecipeId(baseRecipeId);
|
|
2183
|
+
|
|
2184
|
+
const recipeFilePath = path.join(recipesDir, `${workspaceRecipeId}.md`);
|
|
2185
|
+
const parsed = parseFrontmatter(loaded.md);
|
|
2186
|
+
const fm = { ...parsed.frontmatter, id: workspaceRecipeId, name: parsed.frontmatter.name ?? recipe.name ?? workspaceRecipeId };
|
|
2187
|
+
const nextMd = `---\n${YAML.stringify(fm)}---\n${parsed.body}`;
|
|
2188
|
+
await writeFileSafely(recipeFilePath, nextMd, overwriteRecipe ? "overwrite" : "createOnly");
|
|
2119
2189
|
|
|
2120
2190
|
const result = await scaffoldAgentFromRecipe(api, recipe, {
|
|
2121
|
-
agentId
|
|
2191
|
+
agentId,
|
|
2122
2192
|
agentName: options.name,
|
|
2123
2193
|
update: !!options.overwrite,
|
|
2124
2194
|
filesRootDir: resolvedWorkspaceRoot,
|
|
2125
2195
|
workspaceRootDir: resolvedWorkspaceRoot,
|
|
2126
2196
|
vars: {
|
|
2127
|
-
agentId
|
|
2128
|
-
agentName: options.name ?? recipe.name ??
|
|
2197
|
+
agentId,
|
|
2198
|
+
agentName: options.name ?? recipe.name ?? agentId,
|
|
2129
2199
|
},
|
|
2130
2200
|
});
|
|
2131
2201
|
|
|
@@ -2147,8 +2217,11 @@ const recipesPlugin = {
|
|
|
2147
2217
|
.command("scaffold-team")
|
|
2148
2218
|
.description("Scaffold a team (shared workspace + multiple agents) from a team recipe")
|
|
2149
2219
|
.argument("<recipeId>", "Recipe id")
|
|
2150
|
-
.requiredOption("-t, --team-id <teamId>", "Team id
|
|
2220
|
+
.requiredOption("-t, --team-id <teamId>", "Team id")
|
|
2221
|
+
.option("--recipe-id <recipeId>", "Custom workspace recipe id to write (default: <teamId>)")
|
|
2151
2222
|
.option("--overwrite", "Overwrite existing recipe-managed files")
|
|
2223
|
+
.option("--overwrite-recipe", "Overwrite the generated workspace recipe file (workspace/recipes/<teamId>.md) if it already exists")
|
|
2224
|
+
.option("--auto-increment", "If the workspace recipe id is taken, pick the next available <teamId>-2/-3/...")
|
|
2152
2225
|
.option("--apply-config", "Write all team agents into openclaw config (agents.list)")
|
|
2153
2226
|
.action(async (recipeId: string, options: any) => {
|
|
2154
2227
|
const loaded = await loadRecipeById(api, recipeId);
|
|
@@ -2157,9 +2230,6 @@ const recipesPlugin = {
|
|
|
2157
2230
|
throw new Error(`Recipe is not a team recipe: kind=${recipe.kind}`);
|
|
2158
2231
|
}
|
|
2159
2232
|
const teamId = String(options.teamId);
|
|
2160
|
-
if (!teamId.endsWith("-team")) {
|
|
2161
|
-
throw new Error("teamId must end with -team");
|
|
2162
|
-
}
|
|
2163
2233
|
|
|
2164
2234
|
const cfg = getCfg(api);
|
|
2165
2235
|
const baseWorkspace = api.config.agents?.defaults?.workspace;
|
|
@@ -2177,6 +2247,57 @@ const recipesPlugin = {
|
|
|
2177
2247
|
const teamDir = path.resolve(baseWorkspace, "..", `workspace-${teamId}`);
|
|
2178
2248
|
await ensureDir(teamDir);
|
|
2179
2249
|
|
|
2250
|
+
|
|
2251
|
+
// Also create a workspace recipe file for this installed team.
|
|
2252
|
+
// This establishes a stable, editable recipe id that matches the team id (no custom- prefix).
|
|
2253
|
+
const recipesDir = path.join(baseWorkspace, "recipes");
|
|
2254
|
+
await ensureDir(recipesDir);
|
|
2255
|
+
|
|
2256
|
+
const overwriteRecipe = !!options.overwriteRecipe;
|
|
2257
|
+
const autoIncrement = !!options.autoIncrement;
|
|
2258
|
+
|
|
2259
|
+
function suggestedRecipeIds(baseId: string) {
|
|
2260
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
2261
|
+
return [`${baseId}-v2`, `${baseId}-${today}`, `${baseId}-alt`];
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
async function pickRecipeId(baseId: string) {
|
|
2265
|
+
const basePath = path.join(recipesDir, `${baseId}.md`);
|
|
2266
|
+
if (!(await fileExists(basePath))) return baseId;
|
|
2267
|
+
if (overwriteRecipe) return baseId;
|
|
2268
|
+
if (autoIncrement) {
|
|
2269
|
+
let n = 2;
|
|
2270
|
+
while (n < 1000) {
|
|
2271
|
+
const candidate = `${baseId}-${n}`;
|
|
2272
|
+
const candidatePath = path.join(recipesDir, `${candidate}.md`);
|
|
2273
|
+
if (!(await fileExists(candidatePath))) return candidate;
|
|
2274
|
+
n += 1;
|
|
2275
|
+
}
|
|
2276
|
+
throw new Error(`No available recipe id found for ${baseId} (tried up to -999)`);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
const suggestions = suggestedRecipeIds(baseId);
|
|
2280
|
+
const msg = [
|
|
2281
|
+
`Workspace recipe already exists: recipes/${baseId}.md`,
|
|
2282
|
+
`Choose a different recipe id (recommended) and re-run with --recipe-id, for example:`,
|
|
2283
|
+
...suggestions.map((s) => ` openclaw recipes scaffold-team ${recipeId} -t ${teamId} --recipe-id ${s}`),
|
|
2284
|
+
`Or re-run with --auto-increment to pick ${baseId}-2/-3/... automatically, or --overwrite-recipe to overwrite the existing file.`,
|
|
2285
|
+
].join("\n");
|
|
2286
|
+
throw new Error(msg);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
const explicitRecipeId = typeof options.recipeId === "string" ? String(options.recipeId).trim() : "";
|
|
2290
|
+
const baseRecipeId = explicitRecipeId || teamId;
|
|
2291
|
+
const workspaceRecipeId = await pickRecipeId(baseRecipeId);
|
|
2292
|
+
|
|
2293
|
+
// Write the recipe file, copying the source recipe markdown but forcing frontmatter.id to the chosen id.
|
|
2294
|
+
// Default: createOnly; overwrite only when --overwrite-recipe is set.
|
|
2295
|
+
const recipeFilePath = path.join(recipesDir, `${workspaceRecipeId}.md`);
|
|
2296
|
+
const parsed = parseFrontmatter(loaded.md);
|
|
2297
|
+
const fm = { ...parsed.frontmatter, id: workspaceRecipeId, name: parsed.frontmatter.name ?? recipe.name ?? workspaceRecipeId };
|
|
2298
|
+
const nextMd = `---\n${YAML.stringify(fm)}---\n${parsed.body}`;
|
|
2299
|
+
await writeFileSafely(recipeFilePath, nextMd, overwriteRecipe ? "overwrite" : "createOnly");
|
|
2300
|
+
|
|
2180
2301
|
const rolesDir = path.join(teamDir, "roles");
|
|
2181
2302
|
await ensureDir(rolesDir);
|
|
2182
2303
|
const notesDir = path.join(teamDir, "notes");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jiggai/recipes",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"test": "vitest run",
|
|
26
26
|
"test:smoke": "node scripts/scaffold-smoke.mjs",
|
|
27
|
+
"test:provenance-smoke": "node scripts/scaffold-team-provenance-smoke.mjs",
|
|
28
|
+
"test:agent-recipefile-smoke": "node scripts/scaffold-agent-recipefile-smoke.mjs",
|
|
27
29
|
"smoke": "node scripts/scaffold-smoke.mjs",
|
|
28
30
|
"scaffold:smoke": "node scripts/scaffold-smoke.mjs"
|
|
29
31
|
},
|