@clawcipes/recipes 0.1.3 → 0.1.5
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 +14 -7
- package/docs/AGENTS_AND_SKILLS.md +37 -13
- package/docs/COMMANDS.md +63 -12
- package/docs/INSTALLATION.md +5 -1
- package/index.ts +318 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
<img src="./clawcipes_cook.jpg" alt="Clawcipes logo" width="240" />
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
+
> **Experimental:** We’re in active development. Installing should not have any negative impacts, but it’s always good to be safe and copy your `~/.openclaw` folder to a backup.
|
|
8
|
+
|
|
7
9
|
Clawcipes is an OpenClaw plugin that provides **CLI-first recipes** for scaffolding specialist agents and teams from Markdown.
|
|
8
10
|
|
|
9
11
|
If you like durable workflows: Clawcipes is built around a **file-first team workspace** (inbox/backlog/in-progress/done) that plays nicely with git.
|
|
@@ -50,9 +52,10 @@ openclaw recipes dispatch \
|
|
|
50
52
|
|
|
51
53
|
## Commands (high level)
|
|
52
54
|
- `openclaw recipes list|show|status`
|
|
53
|
-
- `openclaw recipes scaffold` (agent)
|
|
54
|
-
- `openclaw recipes scaffold-team` (team)
|
|
55
|
-
- `openclaw recipes install <idOrSlug> [--yes]` (
|
|
55
|
+
- `openclaw recipes scaffold` (agent → `workspace-<agentId>`)
|
|
56
|
+
- `openclaw recipes scaffold-team` (team → `workspace-<teamId>` + `roles/<role>/`)
|
|
57
|
+
- `openclaw recipes install <idOrSlug> [--yes] [--global|--agent-id <id>|--team-id <id>]` (skills: global or scoped)
|
|
58
|
+
- `openclaw recipes bind|bindings` (multi-agent routing)
|
|
56
59
|
- `openclaw recipes dispatch ...` (request → inbox + ticket + assignment)
|
|
57
60
|
|
|
58
61
|
For full details, see `docs/COMMANDS.md`.
|
|
@@ -83,7 +86,12 @@ Reference:
|
|
|
83
86
|
|
|
84
87
|
(Also see: GitHub repo https://github.com/rjdjohnston/clawcipes)
|
|
85
88
|
## Notes / principles
|
|
86
|
-
-
|
|
89
|
+
- Workspaces:
|
|
90
|
+
- Standalone agents: `~/.openclaw/workspace-<agentId>/`
|
|
91
|
+
- Teams: `~/.openclaw/workspace-<teamId>/` with `roles/<role>/...`
|
|
92
|
+
- Skills:
|
|
93
|
+
- Global (shared): `~/.openclaw/skills/<skill>`
|
|
94
|
+
- Scoped (agent/team): `~/.openclaw/workspace-*/skills/<skill>`
|
|
87
95
|
- Team IDs end with `-team`; agent IDs are namespaced: `<teamId>-<role>`.
|
|
88
96
|
- Recipe template rendering is intentionally simple: `{{var}}` replacement only.
|
|
89
97
|
|
|
@@ -92,10 +100,9 @@ Clawcipes does not (yet) include a first-class `remove-team` command.
|
|
|
92
100
|
|
|
93
101
|
To remove a scaffolded team created with `scaffold-team --apply-config`, do two things:
|
|
94
102
|
|
|
95
|
-
1) Remove team
|
|
103
|
+
1) Remove the team workspace (recommended: send to trash):
|
|
96
104
|
```bash
|
|
97
|
-
trash ~/.openclaw/workspace
|
|
98
|
-
trash ~/.openclaw/workspace/agents/<teamId>-*
|
|
105
|
+
trash ~/.openclaw/workspace-<teamId>
|
|
99
106
|
```
|
|
100
107
|
|
|
101
108
|
2) Remove the agents from OpenClaw config:
|
|
@@ -9,12 +9,13 @@ In OpenClaw, an **agent** is a configured assistant persona with:
|
|
|
9
9
|
- a **tool policy** (what tools it is allowed to use)
|
|
10
10
|
- a **model** configuration (defaults come from OpenClaw)
|
|
11
11
|
|
|
12
|
-
In Clawcipes,
|
|
12
|
+
In Clawcipes, a **standalone** agent recipe scaffolds a dedicated workspace like:
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
~/.openclaw/workspace
|
|
15
|
+
~/.openclaw/workspace-<agentId>/
|
|
16
16
|
SOUL.md
|
|
17
17
|
AGENTS.md
|
|
18
|
+
TOOLS.md
|
|
18
19
|
...other recipe files...
|
|
19
20
|
```
|
|
20
21
|
|
|
@@ -92,8 +93,14 @@ openclaw recipes install <skill-slug>
|
|
|
92
93
|
openclaw recipes install <skill-slug> --yes
|
|
93
94
|
```
|
|
94
95
|
|
|
95
|
-
This runs ClawHub under the hood and installs into:
|
|
96
|
-
-
|
|
96
|
+
This runs ClawHub under the hood and installs into the **current OpenClaw workspace** skills dir:
|
|
97
|
+
- `<workspace>/skills/<skill-slug>`
|
|
98
|
+
|
|
99
|
+
Examples:
|
|
100
|
+
- standalone agent workspace: `~/.openclaw/workspace-<agentId>/skills/<skill-slug>`
|
|
101
|
+
- team workspace: `~/.openclaw/workspace-<teamId>/skills/<skill-slug>`
|
|
102
|
+
|
|
103
|
+
> Note: in the new workspace policy, standalone agents live in `~/.openclaw/workspace-<agentId>` and teams live in `~/.openclaw/workspace-<teamId>`. Skill install targeting is still being refined during the experimental phase.
|
|
97
104
|
|
|
98
105
|
### Install the skills required by a recipe
|
|
99
106
|
If a recipe declares skills in `requiredSkills` or `optionalSkills`:
|
|
@@ -108,7 +115,7 @@ That installs the recipe’s declared skills.
|
|
|
108
115
|
Clawcipes currently does **not** implement a remove command.
|
|
109
116
|
|
|
110
117
|
To remove a workspace-local skill:
|
|
111
|
-
- delete the folder:
|
|
118
|
+
- delete the folder: `<workspace>/skills/<skill-slug>`
|
|
112
119
|
- restart: `openclaw gateway restart`
|
|
113
120
|
|
|
114
121
|
(We can add `openclaw recipes uninstall <slug>` later if you want it to be first-class.)
|
|
@@ -118,10 +125,9 @@ Clawcipes does not (yet) include a first-class `remove-team` command.
|
|
|
118
125
|
|
|
119
126
|
If you scaffolded a team with `scaffold-team --apply-config`, removal has two parts:
|
|
120
127
|
|
|
121
|
-
1) Remove the team
|
|
128
|
+
1) Remove the team workspace (recommended: send to trash):
|
|
122
129
|
```bash
|
|
123
|
-
trash ~/.openclaw/workspace
|
|
124
|
-
trash ~/.openclaw/workspace/agents/<teamId>-*
|
|
130
|
+
trash ~/.openclaw/workspace-<teamId>
|
|
125
131
|
```
|
|
126
132
|
|
|
127
133
|
2) Remove the agents from OpenClaw config:
|
|
@@ -133,9 +139,26 @@ openclaw gateway restart
|
|
|
133
139
|
```
|
|
134
140
|
|
|
135
141
|
## Teams: shared workspace + multiple agents
|
|
136
|
-
A **team** recipe scaffolds:
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
A **team** recipe scaffolds a **shared workspace root** plus role folders:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
~/.openclaw/workspace-<teamId>/
|
|
146
|
+
TEAM.md
|
|
147
|
+
inbox/
|
|
148
|
+
outbox/
|
|
149
|
+
shared/
|
|
150
|
+
notes/
|
|
151
|
+
work/
|
|
152
|
+
backlog/
|
|
153
|
+
in-progress/
|
|
154
|
+
done/
|
|
155
|
+
assignments/
|
|
156
|
+
roles/
|
|
157
|
+
<role>/
|
|
158
|
+
...role-specific recipe files...
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Each role agent is a separate OpenClaw agent id (`<teamId>-<role>`), but they share the same workspace root (`workspace-<teamId>`) so collaboration is file-based.
|
|
139
162
|
|
|
140
163
|
The shared workspace is the source of truth for:
|
|
141
164
|
- intake (`inbox/`)
|
|
@@ -148,7 +171,8 @@ Once an agent exists, there are **two layers** you can update:
|
|
|
148
171
|
|
|
149
172
|
### 1) The agent’s files (workspace)
|
|
150
173
|
Agents are just folders under:
|
|
151
|
-
- `~/.openclaw/workspace
|
|
174
|
+
- standalone: `~/.openclaw/workspace-<agentId>/`
|
|
175
|
+
- team roles: `~/.openclaw/workspace-<teamId>/roles/<role>/`
|
|
152
176
|
|
|
153
177
|
Common files:
|
|
154
178
|
- `SOUL.md` — the persona / operating style
|
|
@@ -165,7 +189,7 @@ If the agent was created from a recipe, re-running scaffold with `--overwrite` w
|
|
|
165
189
|
openclaw recipes scaffold <recipeId> --agent-id <agentId> --overwrite
|
|
166
190
|
```
|
|
167
191
|
|
|
168
|
-
For teams, you typically re-run `scaffold-team
|
|
192
|
+
For teams, you typically re-run `scaffold-team` (role files live under `roles/<role>/`):
|
|
169
193
|
|
|
170
194
|
```bash
|
|
171
195
|
openclaw recipes scaffold-team <recipeId> --team-id <teamId> --overwrite
|
package/docs/COMMANDS.md
CHANGED
|
@@ -45,11 +45,11 @@ Options:
|
|
|
45
45
|
- `--apply-config` (write/update `agents.list[]` in OpenClaw config)
|
|
46
46
|
|
|
47
47
|
## `scaffold-team <recipeId>`
|
|
48
|
-
Scaffold a team workspace + multiple agents from a **team** recipe.
|
|
48
|
+
Scaffold a shared **team workspace** + multiple agents from a **team** recipe.
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
openclaw recipes scaffold-team development-team \
|
|
52
|
-
--team-id development-team \
|
|
52
|
+
--team-id development-team-team \
|
|
53
53
|
--overwrite \
|
|
54
54
|
--apply-config
|
|
55
55
|
```
|
|
@@ -60,35 +60,86 @@ Options:
|
|
|
60
60
|
- `--overwrite`
|
|
61
61
|
- `--apply-config`
|
|
62
62
|
|
|
63
|
-
Creates a team
|
|
64
|
-
- `teams/<teamId>/{shared,inbox,outbox,notes,work}`
|
|
65
|
-
- `teams/<teamId>/work/{backlog,in-progress,done,assignments}`
|
|
63
|
+
Creates a shared team workspace root:
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
- `~/.openclaw/workspace-<teamId>/...`
|
|
66
|
+
|
|
67
|
+
Standard folders:
|
|
68
|
+
- `inbox/`, `outbox/`, `shared/`, `notes/`
|
|
69
|
+
- `work/{backlog,in-progress,done,assignments}`
|
|
70
|
+
- `roles/<role>/...` (role-specific recipe files)
|
|
71
|
+
|
|
72
|
+
Also creates agent config entries under `agents.list[]` (when `--apply-config`), with agent ids:
|
|
73
|
+
- `<teamId>-<role>`
|
|
69
74
|
|
|
70
75
|
## `install <idOrSlug> [--yes]`
|
|
71
|
-
Install skills
|
|
76
|
+
Install skills from ClawHub (confirmation-gated).
|
|
77
|
+
|
|
78
|
+
Default behavior: **global install** into `~/.openclaw/skills`.
|
|
72
79
|
|
|
73
80
|
```bash
|
|
74
|
-
|
|
75
|
-
openclaw recipes install
|
|
81
|
+
# Global (shared across all agents)
|
|
82
|
+
openclaw recipes install agentchat --yes
|
|
83
|
+
|
|
84
|
+
# Agent-scoped (into workspace-<agentId>/skills)
|
|
85
|
+
openclaw recipes install agentchat --yes --agent-id dev
|
|
86
|
+
|
|
87
|
+
# Team-scoped (into workspace-<teamId>/skills)
|
|
88
|
+
openclaw recipes install agentchat --yes --team-id development-team-team
|
|
76
89
|
```
|
|
77
90
|
|
|
78
91
|
Behavior:
|
|
79
92
|
- If `idOrSlug` matches a recipe id, installs that recipe’s `requiredSkills` + `optionalSkills`.
|
|
80
93
|
- Otherwise treats it as a ClawHub skill slug.
|
|
81
94
|
- Installs via:
|
|
82
|
-
- `npx clawhub@latest --workdir <
|
|
95
|
+
- `npx clawhub@latest --workdir <targetWorkspace> --dir skills install <slug>` (agent/team)
|
|
96
|
+
- `npx clawhub@latest --workdir ~/.openclaw --dir skills install <slug>` (global)
|
|
83
97
|
- Confirmation-gated unless `--yes`.
|
|
84
98
|
- In non-interactive mode (no TTY), requires `--yes`.
|
|
85
99
|
|
|
100
|
+
## `bind`
|
|
101
|
+
Add/update a multi-agent routing binding (writes `bindings[]` in `~/.openclaw/openclaw.json`).
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Route one Telegram DM to an agent
|
|
107
|
+
openclaw recipes bind --agent-id dev --channel telegram --peer-kind dm --peer-id 6477250615
|
|
108
|
+
|
|
109
|
+
# Route all Telegram traffic to an agent (broad match)
|
|
110
|
+
openclaw recipes bind --agent-id dev --channel telegram
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Notes:
|
|
114
|
+
- `peer.kind` must be one of: `dm|group|channel`.
|
|
115
|
+
- Peer-specific bindings are inserted first (more specific wins).
|
|
116
|
+
|
|
117
|
+
## `bindings`
|
|
118
|
+
Print the current `bindings[]` from OpenClaw config.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
openclaw recipes bindings
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## `migrate-team`
|
|
125
|
+
Migrate a legacy team scaffold into the new `workspace-<teamId>` layout.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
openclaw recipes migrate-team --team-id development-team-team --dry-run
|
|
129
|
+
openclaw recipes migrate-team --team-id development-team-team --mode move
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Options:
|
|
133
|
+
- `--dry-run`
|
|
134
|
+
- `--mode move|copy`
|
|
135
|
+
- `--overwrite` (merge into existing destination)
|
|
136
|
+
|
|
86
137
|
## `dispatch`
|
|
87
138
|
Convert a natural-language request into file-first execution artifacts.
|
|
88
139
|
|
|
89
140
|
```bash
|
|
90
141
|
openclaw recipes dispatch \
|
|
91
|
-
--team-id development-team \
|
|
142
|
+
--team-id development-team-team \
|
|
92
143
|
--request "Add a customer-support team recipe" \
|
|
93
144
|
--owner lead
|
|
94
145
|
```
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -73,4 +73,8 @@ openclaw gateway restart
|
|
|
73
73
|
### `recipes install` fails
|
|
74
74
|
- Run `npx clawhub@latest --help` to confirm the CLI can run.
|
|
75
75
|
- Ensure you are logged into ClawHub if required (`npx clawhub@latest login`).
|
|
76
|
-
- Confirm
|
|
76
|
+
- Confirm the install scope you intended:
|
|
77
|
+
- global: `~/.openclaw/skills/<skill>`
|
|
78
|
+
- agent: `~/.openclaw/workspace-<agentId>/skills/<skill>`
|
|
79
|
+
- team: `~/.openclaw/workspace-<teamId>/skills/<skill>`
|
|
80
|
+
- If you change installs or config, restart: `openclaw gateway restart`.
|
package/index.ts
CHANGED
|
@@ -144,10 +144,10 @@ function skillInstallCommands(cfg: Required<RecipesConfig>, skills: string[]) {
|
|
|
144
144
|
return lines;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
async function detectMissingSkills(
|
|
147
|
+
async function detectMissingSkills(installDir: string, skills: string[]) {
|
|
148
148
|
const missing: string[] = [];
|
|
149
149
|
for (const s of skills) {
|
|
150
|
-
const p =
|
|
150
|
+
const p = path.join(installDir, s);
|
|
151
151
|
if (!(await fileExists(p))) missing.push(s);
|
|
152
152
|
}
|
|
153
153
|
return missing;
|
|
@@ -180,6 +180,20 @@ type AgentConfigSnippet = {
|
|
|
180
180
|
tools?: { profile?: string; allow?: string[]; deny?: string[] };
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
type BindingMatch = {
|
|
184
|
+
channel: string;
|
|
185
|
+
accountId?: string;
|
|
186
|
+
// OpenClaw config schema uses: dm | group | channel
|
|
187
|
+
peer?: { kind: "dm" | "group" | "channel"; id: string };
|
|
188
|
+
guildId?: string;
|
|
189
|
+
teamId?: string;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
type BindingSnippet = {
|
|
193
|
+
agentId: string;
|
|
194
|
+
match: BindingMatch;
|
|
195
|
+
};
|
|
196
|
+
|
|
183
197
|
function upsertAgentInConfig(cfgObj: any, snippet: AgentConfigSnippet) {
|
|
184
198
|
if (!cfgObj.agents) cfgObj.agents = {};
|
|
185
199
|
if (!Array.isArray(cfgObj.agents.list)) cfgObj.agents.list = [];
|
|
@@ -242,6 +256,43 @@ function ensureMainFirstInAgentsList(cfgObj: any, api: OpenClawPluginApi) {
|
|
|
242
256
|
list.unshift(main);
|
|
243
257
|
}
|
|
244
258
|
|
|
259
|
+
function stableStringify(x: any) {
|
|
260
|
+
const seen = new WeakSet();
|
|
261
|
+
const sortObj = (v: any): any => {
|
|
262
|
+
if (v && typeof v === "object") {
|
|
263
|
+
if (seen.has(v)) return "[Circular]";
|
|
264
|
+
seen.add(v);
|
|
265
|
+
if (Array.isArray(v)) return v.map(sortObj);
|
|
266
|
+
const out: any = {};
|
|
267
|
+
for (const k of Object.keys(v).sort()) out[k] = sortObj(v[k]);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
return v;
|
|
271
|
+
};
|
|
272
|
+
return JSON.stringify(sortObj(x));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function upsertBindingInConfig(cfgObj: any, binding: BindingSnippet) {
|
|
276
|
+
if (!Array.isArray(cfgObj.bindings)) cfgObj.bindings = [];
|
|
277
|
+
const list: any[] = cfgObj.bindings;
|
|
278
|
+
|
|
279
|
+
const sig = stableStringify({ agentId: binding.agentId, match: binding.match });
|
|
280
|
+
const idx = list.findIndex((b) => stableStringify({ agentId: b?.agentId, match: b?.match }) === sig);
|
|
281
|
+
|
|
282
|
+
if (idx >= 0) {
|
|
283
|
+
// Update in place (preserve ordering)
|
|
284
|
+
list[idx] = { ...list[idx], ...binding };
|
|
285
|
+
return { changed: false, note: "already-present" as const };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Most-specific-first: if a peer match is specified, insert at front so it wins.
|
|
289
|
+
// Otherwise append.
|
|
290
|
+
if (binding.match?.peer) list.unshift(binding);
|
|
291
|
+
else list.push(binding);
|
|
292
|
+
|
|
293
|
+
return { changed: true, note: "added" as const };
|
|
294
|
+
}
|
|
295
|
+
|
|
245
296
|
async function applyAgentSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippets: AgentConfigSnippet[]) {
|
|
246
297
|
// Load the latest config from disk (not the snapshot in api.config).
|
|
247
298
|
const current = (api.runtime as any).config?.loadConfig?.();
|
|
@@ -262,6 +313,20 @@ async function applyAgentSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippe
|
|
|
262
313
|
return { updatedAgents: snippets.map((s) => s.id) };
|
|
263
314
|
}
|
|
264
315
|
|
|
316
|
+
async function applyBindingSnippetsToOpenClawConfig(api: OpenClawPluginApi, snippets: BindingSnippet[]) {
|
|
317
|
+
const current = (api.runtime as any).config?.loadConfig?.();
|
|
318
|
+
if (!current) throw new Error("Failed to load config via api.runtime.config.loadConfig()");
|
|
319
|
+
const cfgObj = (current.cfg ?? current) as any;
|
|
320
|
+
|
|
321
|
+
const results: any[] = [];
|
|
322
|
+
for (const s of snippets) {
|
|
323
|
+
results.push({ ...s, result: upsertBindingInConfig(cfgObj, s) });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await (api.runtime as any).config?.writeConfigFile?.(cfgObj);
|
|
327
|
+
return { updatedBindings: results };
|
|
328
|
+
}
|
|
329
|
+
|
|
265
330
|
async function scaffoldAgentFromRecipe(
|
|
266
331
|
api: OpenClawPluginApi,
|
|
267
332
|
recipe: RecipeFrontmatter,
|
|
@@ -389,7 +454,10 @@ const recipesPlugin = {
|
|
|
389
454
|
const { frontmatter } = parseFrontmatter(md);
|
|
390
455
|
if (id && frontmatter.id !== id) continue;
|
|
391
456
|
const req = frontmatter.requiredSkills ?? [];
|
|
392
|
-
const
|
|
457
|
+
const workspaceRoot = api.config.agents?.defaults?.workspace;
|
|
458
|
+
if (!workspaceRoot) throw new Error("agents.defaults.workspace is not set in config");
|
|
459
|
+
const installDir = path.join(workspaceRoot, cfg.workspaceSkillsDir);
|
|
460
|
+
const missing = await detectMissingSkills(installDir, req);
|
|
393
461
|
out.push({
|
|
394
462
|
id: frontmatter.id,
|
|
395
463
|
requiredSkills: req,
|
|
@@ -401,11 +469,204 @@ const recipesPlugin = {
|
|
|
401
469
|
console.log(JSON.stringify(out, null, 2));
|
|
402
470
|
});
|
|
403
471
|
|
|
472
|
+
cmd
|
|
473
|
+
.command("bind")
|
|
474
|
+
.description("Add/update a multi-agent routing binding (writes openclaw.json bindings[])")
|
|
475
|
+
.requiredOption("--agent-id <agentId>", "Target agent id")
|
|
476
|
+
.requiredOption("--channel <channel>", "Channel name (telegram|whatsapp|discord|slack|...) ")
|
|
477
|
+
.option("--account-id <accountId>", "Channel accountId (if applicable)")
|
|
478
|
+
.option("--peer-kind <kind>", "Peer kind (dm|group|channel) (aliases: direct->dm)")
|
|
479
|
+
.option("--peer-id <id>", "Peer id (DM number/id, group id, or channel id)")
|
|
480
|
+
.option("--guild-id <guildId>", "Discord guildId")
|
|
481
|
+
.option("--team-id <teamId>", "Slack teamId")
|
|
482
|
+
.option("--match <json>", "Full match object as JSON/JSON5 (overrides flags)")
|
|
483
|
+
.action(async (options: any) => {
|
|
484
|
+
const agentId = String(options.agentId);
|
|
485
|
+
let match: BindingMatch;
|
|
486
|
+
|
|
487
|
+
if (options.match) {
|
|
488
|
+
match = JSON5.parse(String(options.match)) as BindingMatch;
|
|
489
|
+
} else {
|
|
490
|
+
match = {
|
|
491
|
+
channel: String(options.channel),
|
|
492
|
+
};
|
|
493
|
+
if (options.accountId) match.accountId = String(options.accountId);
|
|
494
|
+
if (options.guildId) match.guildId = String(options.guildId);
|
|
495
|
+
if (options.teamId) match.teamId = String(options.teamId);
|
|
496
|
+
|
|
497
|
+
if (options.peerKind || options.peerId) {
|
|
498
|
+
if (!options.peerKind || !options.peerId) {
|
|
499
|
+
throw new Error("--peer-kind and --peer-id must be provided together");
|
|
500
|
+
}
|
|
501
|
+
let kind = String(options.peerKind);
|
|
502
|
+
// Back-compat alias
|
|
503
|
+
if (kind === "direct") kind = "dm";
|
|
504
|
+
if (kind !== "dm" && kind !== "group" && kind !== "channel") {
|
|
505
|
+
throw new Error("--peer-kind must be dm|group|channel (or direct as alias for dm)");
|
|
506
|
+
}
|
|
507
|
+
match.peer = { kind, id: String(options.peerId) };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!match?.channel) throw new Error("match.channel is required");
|
|
512
|
+
|
|
513
|
+
const res = await applyBindingSnippetsToOpenClawConfig(api, [{ agentId, match }]);
|
|
514
|
+
console.log(JSON.stringify(res, null, 2));
|
|
515
|
+
console.error("Binding written. Restart gateway if required for changes to take effect.");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
cmd
|
|
519
|
+
.command("bindings")
|
|
520
|
+
.description("Show current bindings from openclaw config")
|
|
521
|
+
.action(async () => {
|
|
522
|
+
const current = (api.runtime as any).config?.loadConfig?.();
|
|
523
|
+
if (!current) throw new Error("Failed to load config via api.runtime.config.loadConfig()");
|
|
524
|
+
const cfgObj = (current.cfg ?? current) as any;
|
|
525
|
+
console.log(JSON.stringify(cfgObj.bindings ?? [], null, 2));
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
cmd
|
|
529
|
+
.command("migrate-team")
|
|
530
|
+
.description("Migrate a legacy team scaffold into the new workspace-<teamId> layout")
|
|
531
|
+
.requiredOption("--team-id <teamId>", "Team id (must end with -team)")
|
|
532
|
+
.option("--mode <mode>", "move|copy", "move")
|
|
533
|
+
.option("--dry-run", "Print the plan without writing anything", false)
|
|
534
|
+
.option("--overwrite", "Allow merging into an existing destination (dangerous)", false)
|
|
535
|
+
.action(async (options: any) => {
|
|
536
|
+
const teamId = String(options.teamId);
|
|
537
|
+
if (!teamId.endsWith("-team")) throw new Error("teamId must end with -team");
|
|
538
|
+
|
|
539
|
+
const mode = String(options.mode ?? "move");
|
|
540
|
+
if (mode !== "move" && mode !== "copy") throw new Error("--mode must be move|copy");
|
|
541
|
+
|
|
542
|
+
const baseWorkspace = api.config.agents?.defaults?.workspace;
|
|
543
|
+
if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
|
|
544
|
+
|
|
545
|
+
const legacyTeamDir = path.resolve(baseWorkspace, "teams", teamId);
|
|
546
|
+
const legacyAgentsDir = path.resolve(baseWorkspace, "agents");
|
|
547
|
+
|
|
548
|
+
const destTeamDir = path.resolve(baseWorkspace, "..", `workspace-${teamId}`);
|
|
549
|
+
const destRolesDir = path.join(destTeamDir, "roles");
|
|
550
|
+
|
|
551
|
+
const exists = async (p: string) => fileExists(p);
|
|
552
|
+
|
|
553
|
+
// Build migration plan
|
|
554
|
+
const plan: any = {
|
|
555
|
+
teamId,
|
|
556
|
+
mode,
|
|
557
|
+
legacy: { teamDir: legacyTeamDir, agentsDir: legacyAgentsDir },
|
|
558
|
+
dest: { teamDir: destTeamDir, rolesDir: destRolesDir },
|
|
559
|
+
steps: [] as any[],
|
|
560
|
+
agentIds: [] as string[],
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const legacyTeamExists = await exists(legacyTeamDir);
|
|
564
|
+
if (!legacyTeamExists) {
|
|
565
|
+
throw new Error(`Legacy team directory not found: ${legacyTeamDir}`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const destExists = await exists(destTeamDir);
|
|
569
|
+
if (destExists && !options.overwrite) {
|
|
570
|
+
throw new Error(`Destination already exists: ${destTeamDir} (re-run with --overwrite to merge)`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 1) Move/copy team shared workspace
|
|
574
|
+
plan.steps.push({ kind: "teamDir", from: legacyTeamDir, to: destTeamDir });
|
|
575
|
+
|
|
576
|
+
// 2) Move/copy each role agent directory into roles/<role>/
|
|
577
|
+
const legacyAgentsExist = await exists(legacyAgentsDir);
|
|
578
|
+
let legacyAgentFolders: string[] = [];
|
|
579
|
+
if (legacyAgentsExist) {
|
|
580
|
+
legacyAgentFolders = (await fs.readdir(legacyAgentsDir)).filter((x) => x.startsWith(`${teamId}-`));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
for (const folder of legacyAgentFolders) {
|
|
584
|
+
const agentId = folder;
|
|
585
|
+
const role = folder.slice((teamId + "-").length);
|
|
586
|
+
const from = path.join(legacyAgentsDir, folder);
|
|
587
|
+
const to = path.join(destRolesDir, role);
|
|
588
|
+
plan.agentIds.push(agentId);
|
|
589
|
+
plan.steps.push({ kind: "roleDir", agentId, role, from, to });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const dryRun = !!options.dryRun;
|
|
593
|
+
if (dryRun) {
|
|
594
|
+
console.log(JSON.stringify({ ok: true, dryRun: true, plan }, null, 2));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Helpers
|
|
599
|
+
const copyDirRecursive = async (src: string, dst: string) => {
|
|
600
|
+
await ensureDir(dst);
|
|
601
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
602
|
+
for (const ent of entries) {
|
|
603
|
+
const s = path.join(src, ent.name);
|
|
604
|
+
const d = path.join(dst, ent.name);
|
|
605
|
+
if (ent.isDirectory()) await copyDirRecursive(s, d);
|
|
606
|
+
else if (ent.isSymbolicLink()) {
|
|
607
|
+
const link = await fs.readlink(s);
|
|
608
|
+
await fs.symlink(link, d);
|
|
609
|
+
} else {
|
|
610
|
+
await ensureDir(path.dirname(d));
|
|
611
|
+
await fs.copyFile(s, d);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const removeDirRecursive = async (p: string) => {
|
|
617
|
+
// node 25 supports fs.rm
|
|
618
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const moveDir = async (src: string, dst: string) => {
|
|
622
|
+
await ensureDir(path.dirname(dst));
|
|
623
|
+
try {
|
|
624
|
+
await fs.rename(src, dst);
|
|
625
|
+
} catch {
|
|
626
|
+
// cross-device or existing: fallback to copy+remove
|
|
627
|
+
await copyDirRecursive(src, dst);
|
|
628
|
+
await removeDirRecursive(src);
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// Execute plan
|
|
633
|
+
if (mode === "copy") {
|
|
634
|
+
await copyDirRecursive(legacyTeamDir, destTeamDir);
|
|
635
|
+
} else {
|
|
636
|
+
await moveDir(legacyTeamDir, destTeamDir);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Ensure roles dir exists
|
|
640
|
+
await ensureDir(destRolesDir);
|
|
641
|
+
|
|
642
|
+
for (const step of plan.steps.filter((s: any) => s.kind === "roleDir")) {
|
|
643
|
+
if (!(await exists(step.from))) continue;
|
|
644
|
+
if (mode === "copy") await copyDirRecursive(step.from, step.to);
|
|
645
|
+
else await moveDir(step.from, step.to);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Update config: set each team agent's workspace to destTeamDir (shared)
|
|
649
|
+
const agentSnippets: AgentConfigSnippet[] = plan.agentIds.map((agentId: string) => ({
|
|
650
|
+
id: agentId,
|
|
651
|
+
workspace: destTeamDir,
|
|
652
|
+
}));
|
|
653
|
+
if (agentSnippets.length) {
|
|
654
|
+
await applyAgentSnippetsToOpenClawConfig(api, agentSnippets);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
console.log(JSON.stringify({ ok: true, migrated: teamId, destTeamDir, agentIds: plan.agentIds }, null, 2));
|
|
658
|
+
});
|
|
659
|
+
|
|
404
660
|
cmd
|
|
405
661
|
.command("install")
|
|
406
|
-
.description(
|
|
662
|
+
.description(
|
|
663
|
+
"Install a skill from ClawHub (confirmation-gated). Default: global (~/.openclaw/skills). Use --agent-id or --team-id for scoped installs.",
|
|
664
|
+
)
|
|
407
665
|
.argument("<idOrSlug>", "Recipe id OR ClawHub skill slug")
|
|
408
666
|
.option("--yes", "Skip confirmation prompt")
|
|
667
|
+
.option("--global", "Install into global shared skills (~/.openclaw/skills) (default when no scope flags)")
|
|
668
|
+
.option("--agent-id <agentId>", "Install into a specific agent workspace (workspace-<agentId>)")
|
|
669
|
+
.option("--team-id <teamId>", "Install into a team workspace (workspace-<teamId>)")
|
|
409
670
|
.action(async (idOrSlug: string, options: any) => {
|
|
410
671
|
const cfg = getCfg(api);
|
|
411
672
|
|
|
@@ -420,10 +681,45 @@ const recipesPlugin = {
|
|
|
420
681
|
recipe = null;
|
|
421
682
|
}
|
|
422
683
|
|
|
423
|
-
const
|
|
424
|
-
if (!
|
|
684
|
+
const baseWorkspace = api.config.agents?.defaults?.workspace;
|
|
685
|
+
if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
|
|
425
686
|
|
|
426
|
-
const
|
|
687
|
+
const stateDir = path.resolve(baseWorkspace, ".."); // ~/.openclaw
|
|
688
|
+
|
|
689
|
+
const scopeFlags = [options.global ? "global" : null, options.agentId ? "agent" : null, options.teamId ? "team" : null].filter(Boolean);
|
|
690
|
+
if (scopeFlags.length > 1) {
|
|
691
|
+
throw new Error("Use only one of: --global, --agent-id, --team-id");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const agentIdOpt = typeof options.agentId === "string" ? options.agentId.trim() : "";
|
|
695
|
+
const teamIdOpt = typeof options.teamId === "string" ? options.teamId.trim() : "";
|
|
696
|
+
|
|
697
|
+
// Default is global install when no scope is provided.
|
|
698
|
+
const scope = scopeFlags[0] ?? "global";
|
|
699
|
+
|
|
700
|
+
let workdir: string;
|
|
701
|
+
let dirName: string;
|
|
702
|
+
let installDir: string;
|
|
703
|
+
|
|
704
|
+
if (scope === "agent") {
|
|
705
|
+
if (!agentIdOpt) throw new Error("--agent-id cannot be empty");
|
|
706
|
+
const agentWorkspace = path.resolve(stateDir, `workspace-${agentIdOpt}`);
|
|
707
|
+
workdir = agentWorkspace;
|
|
708
|
+
dirName = cfg.workspaceSkillsDir;
|
|
709
|
+
installDir = path.join(agentWorkspace, dirName);
|
|
710
|
+
} else if (scope === "team") {
|
|
711
|
+
if (!teamIdOpt) throw new Error("--team-id cannot be empty");
|
|
712
|
+
const teamWorkspace = path.resolve(stateDir, `workspace-${teamIdOpt}`);
|
|
713
|
+
workdir = teamWorkspace;
|
|
714
|
+
dirName = cfg.workspaceSkillsDir;
|
|
715
|
+
installDir = path.join(teamWorkspace, dirName);
|
|
716
|
+
} else {
|
|
717
|
+
workdir = stateDir;
|
|
718
|
+
dirName = "skills";
|
|
719
|
+
installDir = path.join(stateDir, dirName);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
await ensureDir(installDir);
|
|
427
723
|
|
|
428
724
|
const skillsToInstall = recipe
|
|
429
725
|
? Array.from(new Set([...(recipe.requiredSkills ?? []), ...(recipe.optionalSkills ?? [])])).filter(Boolean)
|
|
@@ -434,11 +730,11 @@ const recipesPlugin = {
|
|
|
434
730
|
return;
|
|
435
731
|
}
|
|
436
732
|
|
|
437
|
-
const missing = await detectMissingSkills(
|
|
733
|
+
const missing = await detectMissingSkills(installDir, skillsToInstall);
|
|
438
734
|
const already = skillsToInstall.filter((s) => !missing.includes(s));
|
|
439
735
|
|
|
440
736
|
if (already.length) {
|
|
441
|
-
console.error(`Already present in
|
|
737
|
+
console.error(`Already present in skills dir (${installDir}): ${already.join(", ")}`);
|
|
442
738
|
}
|
|
443
739
|
|
|
444
740
|
if (!missing.length) {
|
|
@@ -446,9 +742,10 @@ const recipesPlugin = {
|
|
|
446
742
|
return;
|
|
447
743
|
}
|
|
448
744
|
|
|
745
|
+
const targetLabel = scope === "agent" ? `agent:${agentIdOpt}` : scope === "team" ? `team:${teamIdOpt}` : "global";
|
|
449
746
|
const header = recipe
|
|
450
|
-
? `Install skills for recipe ${recipe.id} into ${installDir}?\n- ${missing.join("\n- ")}`
|
|
451
|
-
: `Install skill into ${installDir}?\n- ${missing.join("\n- ")}`;
|
|
747
|
+
? `Install skills for recipe ${recipe.id} into ${installDir} (${targetLabel})?\n- ${missing.join("\n- ")}`
|
|
748
|
+
: `Install skill into ${installDir} (${targetLabel})?\n- ${missing.join("\n- ")}`;
|
|
452
749
|
|
|
453
750
|
const requireConfirm = !options.yes;
|
|
454
751
|
if (requireConfirm) {
|
|
@@ -474,12 +771,12 @@ const recipesPlugin = {
|
|
|
474
771
|
console.error(header);
|
|
475
772
|
}
|
|
476
773
|
|
|
477
|
-
// Use clawhub CLI. Force
|
|
774
|
+
// Use clawhub CLI. Force install path based on scope.
|
|
478
775
|
const { spawnSync } = await import("node:child_process");
|
|
479
776
|
for (const slug of missing) {
|
|
480
777
|
const res = spawnSync(
|
|
481
778
|
"npx",
|
|
482
|
-
["clawhub@latest", "--workdir",
|
|
779
|
+
["clawhub@latest", "--workdir", workdir, "--dir", dirName, "install", slug],
|
|
483
780
|
{ stdio: "inherit" },
|
|
484
781
|
);
|
|
485
782
|
if (res.status !== 0) {
|
|
@@ -659,7 +956,10 @@ const recipesPlugin = {
|
|
|
659
956
|
}
|
|
660
957
|
|
|
661
958
|
const cfg = getCfg(api);
|
|
662
|
-
const
|
|
959
|
+
const workspaceRoot = api.config.agents?.defaults?.workspace;
|
|
960
|
+
if (!workspaceRoot) throw new Error("agents.defaults.workspace is not set in config");
|
|
961
|
+
const installDir = path.join(workspaceRoot, cfg.workspaceSkillsDir);
|
|
962
|
+
const missing = await detectMissingSkills(installDir, recipe.requiredSkills ?? []);
|
|
663
963
|
if (missing.length) {
|
|
664
964
|
console.error(`Missing skills for recipe ${recipeId}: ${missing.join(", ")}`);
|
|
665
965
|
console.error(`Install commands (workspace-local):\n${skillInstallCommands(cfg, missing).join("\n")}`);
|
|
@@ -709,7 +1009,10 @@ const recipesPlugin = {
|
|
|
709
1009
|
}
|
|
710
1010
|
|
|
711
1011
|
const cfg = getCfg(api);
|
|
712
|
-
const
|
|
1012
|
+
const baseWorkspace = api.config.agents?.defaults?.workspace;
|
|
1013
|
+
if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
|
|
1014
|
+
const installDir = path.join(baseWorkspace, cfg.workspaceSkillsDir);
|
|
1015
|
+
const missing = await detectMissingSkills(installDir, recipe.requiredSkills ?? []);
|
|
713
1016
|
if (missing.length) {
|
|
714
1017
|
console.error(`Missing skills for recipe ${recipeId}: ${missing.join(", ")}`);
|
|
715
1018
|
console.error(`Install commands (workspace-local):\n${skillInstallCommands(cfg, missing).join("\n")}`);
|
|
@@ -717,9 +1020,6 @@ const recipesPlugin = {
|
|
|
717
1020
|
return;
|
|
718
1021
|
}
|
|
719
1022
|
|
|
720
|
-
const baseWorkspace = api.config.agents?.defaults?.workspace;
|
|
721
|
-
if (!baseWorkspace) throw new Error("agents.defaults.workspace is not set in config");
|
|
722
|
-
|
|
723
1023
|
// Team workspace root (shared by all role agents): ~/.openclaw/workspace-<teamId>
|
|
724
1024
|
const teamDir = path.resolve(baseWorkspace, "..", `workspace-${teamId}`);
|
|
725
1025
|
await ensureDir(teamDir);
|