@wji.ai/cli 0.6.1 → 0.7.1
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 +23 -3
- package/dist/index.js +7162 -76
- package/package.json +6 -9
- package/skill/backreel/SKILL.md +171 -0
- package/skill/backreel/reference/crowdfunding.md +82 -0
- package/skill/backreel/reference/enquiries.md +80 -0
- package/skill/backreel/reference/episodes.md +104 -0
- package/skill/backreel/reference/images.md +76 -0
- package/skill/npc-character/SKILL.md +111 -0
- package/skill/npc-character/examples/nagi.character.md +117 -0
- package/skill/npc-character/examples/nagi.stage.md +34 -0
- package/skill/npc-character/examples/yuzu.character.md +110 -0
- package/skill/npc-character/examples/yuzu.stage.md +30 -0
- package/skill/npc-character/reference/character-craft.md +153 -0
- package/skill/npc-character/reference/document-format.md +151 -0
- package/skill/npc-character/reference/interview.md +44 -0
- package/skill/npc-character/reference/life.md +92 -0
- package/skill/npc-character/reference/runtime.md +95 -0
- package/skill/npc-character/reference/stage-craft.md +109 -0
- package/skill/npc-character/reference/visual-kit.md +81 -0
- package/skill/npc-character/reference/voice.md +101 -0
- package/skill/wji/SKILL.md +1 -1
- package/skill/wji/reference/commands.md +25 -5
- package/dist/commands/agent.js +0 -294
- package/dist/commands/asset.js +0 -241
- package/dist/commands/auth.js +0 -216
- package/dist/commands/backreel.js +0 -320
- package/dist/commands/canvas.js +0 -1706
- package/dist/commands/class.js +0 -395
- package/dist/commands/config.js +0 -113
- package/dist/commands/crew.js +0 -205
- package/dist/commands/fsnode.js +0 -288
- package/dist/commands/gen.js +0 -352
- package/dist/commands/model.js +0 -136
- package/dist/commands/npc.js +0 -413
- package/dist/commands/org.js +0 -85
- package/dist/commands/preset.js +0 -176
- package/dist/commands/profile.js +0 -129
- package/dist/commands/skill.js +0 -272
- package/dist/commands/workspace.js +0 -470
- package/dist/core/archive.js +0 -95
- package/dist/core/client.js +0 -252
- package/dist/core/config.js +0 -95
- package/dist/core/crew.js +0 -546
- package/dist/core/members.js +0 -54
- package/dist/core/npc.js +0 -108
- package/dist/core/output.js +0 -174
- package/dist/core/prompt.js +0 -118
- package/dist/core/zip.js +0 -87
package/dist/core/crew.js
DELETED
|
@@ -1,546 +0,0 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { configDir } from './config.js';
|
|
6
|
-
import { createZip } from './zip.js';
|
|
7
|
-
import { readTarGz, readZip, stripCommonRoot } from './archive.js';
|
|
8
|
-
/**
|
|
9
|
-
* A crew pack is a folder (or archive of one) shaped like:
|
|
10
|
-
*
|
|
11
|
-
* crew.json roles, skill allowlists, capabilities, delegation, channels
|
|
12
|
-
* agents/<slug>.md one persona per seat: Claude-agent frontmatter + body
|
|
13
|
-
* skills/<name>/… Agent Skills (SKILL.md + reference/ + scripts/ …)
|
|
14
|
-
* AGENTS.md shared operating rules (optional)
|
|
15
|
-
*
|
|
16
|
-
* `wji crew install` copies the skills where each host discovers them and
|
|
17
|
-
* writes each host's agent format from the same persona files.
|
|
18
|
-
*/
|
|
19
|
-
export const TARGETS = ['claude', 'codex', 'openclaw', 'grok'];
|
|
20
|
-
/** Where a named crew is fetched from when no --from is given. */
|
|
21
|
-
export const CREW_REGISTRY = {
|
|
22
|
-
'short-drama': { repo: 'ultra-instinct-ai/short-drama-crew', defaultRef: 'main' },
|
|
23
|
-
};
|
|
24
|
-
// ── frontmatter ─────────────────────────────────────────────────────────────
|
|
25
|
-
/** The YAML subset the persona files use: `key: value`, `key:` + ` - item` lists, `>-` folded blocks. */
|
|
26
|
-
export function parseFrontmatter(raw) {
|
|
27
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
|
|
28
|
-
if (!m)
|
|
29
|
-
return { frontmatter: {}, body: raw };
|
|
30
|
-
const fm = {};
|
|
31
|
-
const lines = m[1].split(/\r?\n/);
|
|
32
|
-
let key;
|
|
33
|
-
let folded = false;
|
|
34
|
-
for (const line of lines) {
|
|
35
|
-
const top = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(line);
|
|
36
|
-
if (top && !/^\s/.test(line)) {
|
|
37
|
-
key = top[1];
|
|
38
|
-
const value = top[2].trim();
|
|
39
|
-
if (value === '>-' || value === '>' || value === '|' || value === '|-') {
|
|
40
|
-
fm[key] = '';
|
|
41
|
-
folded = true;
|
|
42
|
-
}
|
|
43
|
-
else if (value === '') {
|
|
44
|
-
fm[key] = [];
|
|
45
|
-
folded = false;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
fm[key] = value.replace(/^["'](.*)["']$/, '$1');
|
|
49
|
-
folded = false;
|
|
50
|
-
}
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (!key)
|
|
54
|
-
continue;
|
|
55
|
-
const item = /^\s+-\s+(.*)$/.exec(line);
|
|
56
|
-
if (item && Array.isArray(fm[key]))
|
|
57
|
-
fm[key].push(item[1].trim());
|
|
58
|
-
else if (folded && typeof fm[key] === 'string')
|
|
59
|
-
fm[key] = `${fm[key]}${fm[key] ? ' ' : ''}${line.trim()}`;
|
|
60
|
-
}
|
|
61
|
-
return { frontmatter: fm, body: raw.slice(m[0].length).replace(/^\s*\n/, '') };
|
|
62
|
-
}
|
|
63
|
-
const listOf = (v) => Array.isArray(v) ? v : typeof v === 'string' && v ? v.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
64
|
-
// ── loading ─────────────────────────────────────────────────────────────────
|
|
65
|
-
function walk(dir, rel = '') {
|
|
66
|
-
const out = new Map();
|
|
67
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
-
if (entry.name.startsWith('.') && entry.name !== '.sync-source')
|
|
69
|
-
continue;
|
|
70
|
-
const full = join(dir, entry.name);
|
|
71
|
-
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
72
|
-
if (entry.isDirectory())
|
|
73
|
-
for (const [p, c] of walk(full, path))
|
|
74
|
-
out.set(p, c);
|
|
75
|
-
else if (entry.isFile())
|
|
76
|
-
out.set(path, readFileSync(full));
|
|
77
|
-
}
|
|
78
|
-
return out;
|
|
79
|
-
}
|
|
80
|
-
export function loadPackFromDir(dir, source = dir, ref) {
|
|
81
|
-
const root = resolve(dir);
|
|
82
|
-
const manifestPath = join(root, 'crew.json');
|
|
83
|
-
if (!existsSync(manifestPath))
|
|
84
|
-
throw new Error(`${dir} is not a crew pack (no crew.json)`);
|
|
85
|
-
const crew = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
86
|
-
const skillsDir = join(root, crew.skillsDir || 'skills');
|
|
87
|
-
const skills = new Map();
|
|
88
|
-
if (existsSync(skillsDir)) {
|
|
89
|
-
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
90
|
-
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
91
|
-
continue;
|
|
92
|
-
skills.set(entry.name, walk(join(skillsDir, entry.name)));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const agents = {};
|
|
96
|
-
for (const [slug, spec] of Object.entries(crew.agents || {})) {
|
|
97
|
-
const file = join(root, spec.prompt);
|
|
98
|
-
if (!existsSync(file))
|
|
99
|
-
throw new Error(`crew.json: agent ${slug} points at missing ${spec.prompt}`);
|
|
100
|
-
const raw = readFileSync(file, 'utf8');
|
|
101
|
-
const { frontmatter, body } = parseFrontmatter(raw);
|
|
102
|
-
agents[slug] = { slug, frontmatter, body, raw };
|
|
103
|
-
}
|
|
104
|
-
const agentsMdPath = join(root, 'AGENTS.md');
|
|
105
|
-
return {
|
|
106
|
-
root,
|
|
107
|
-
crew,
|
|
108
|
-
agents,
|
|
109
|
-
skills,
|
|
110
|
-
agentsMd: existsSync(agentsMdPath) ? readFileSync(agentsMdPath, 'utf8') : undefined,
|
|
111
|
-
source,
|
|
112
|
-
ref,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
export function crewCacheDir() {
|
|
116
|
-
return join(configDir(), 'crews');
|
|
117
|
-
}
|
|
118
|
-
function unpackTo(files, dir) {
|
|
119
|
-
rmSync(dir, { recursive: true, force: true });
|
|
120
|
-
for (const [path, content] of files) {
|
|
121
|
-
if (path.includes('..'))
|
|
122
|
-
continue;
|
|
123
|
-
const full = join(dir, path);
|
|
124
|
-
mkdirSync(dirname(full), { recursive: true });
|
|
125
|
-
writeFileSync(full, content);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Resolve `--from` (a folder, a .zip / .tar.gz file, or an http(s) URL) or a
|
|
130
|
-
* registry name into a loaded pack. Archives are unpacked into the cache.
|
|
131
|
-
*/
|
|
132
|
-
export async function fetchPack(nameOrFrom, opts = {}) {
|
|
133
|
-
const from = opts.from;
|
|
134
|
-
if (from && !/^https?:\/\//.test(from)) {
|
|
135
|
-
const path = resolve(from);
|
|
136
|
-
if (!existsSync(path))
|
|
137
|
-
throw new Error(`No such file or directory: ${from}`);
|
|
138
|
-
if (statSync(path).isDirectory())
|
|
139
|
-
return loadPackFromDir(path, path);
|
|
140
|
-
const buffer = readFileSync(path);
|
|
141
|
-
const files = stripCommonRoot(/\.zip$/i.test(path) ? readZip(buffer) : readTarGz(buffer));
|
|
142
|
-
const dir = join(crewCacheDir(), basename(path).replace(/\.(zip|tar\.gz|tgz)$/i, ''), 'local');
|
|
143
|
-
unpackTo(files, dir);
|
|
144
|
-
return loadPackFromDir(dir, path);
|
|
145
|
-
}
|
|
146
|
-
let url = from;
|
|
147
|
-
let ref = opts.ref;
|
|
148
|
-
let name = nameOrFrom;
|
|
149
|
-
if (!url) {
|
|
150
|
-
const entry = CREW_REGISTRY[nameOrFrom];
|
|
151
|
-
if (!entry)
|
|
152
|
-
throw new Error(`Unknown crew "${nameOrFrom}". Known: ${Object.keys(CREW_REGISTRY).join(', ')} — or pass --from <dir|zip|url>.`);
|
|
153
|
-
ref = ref || entry.defaultRef;
|
|
154
|
-
const kind = /^v?\d/.test(ref) ? 'tags' : 'heads';
|
|
155
|
-
url = `https://codeload.github.com/${entry.repo}/tar.gz/refs/${kind}/${ref}`;
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
name = basename(new URL(url).pathname).replace(/\.(zip|tar\.gz|tgz)$/i, '') || nameOrFrom;
|
|
159
|
-
ref = ref || 'url';
|
|
160
|
-
}
|
|
161
|
-
const res = await fetch(url);
|
|
162
|
-
if (!res.ok)
|
|
163
|
-
throw new Error(`Download failed: ${res.status} ${res.statusText} — ${url}`);
|
|
164
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
165
|
-
const files = stripCommonRoot(/\.zip($|\?)/i.test(url) ? readZip(buffer) : readTarGz(buffer));
|
|
166
|
-
const dir = join(crewCacheDir(), name, ref);
|
|
167
|
-
unpackTo(files, dir);
|
|
168
|
-
return loadPackFromDir(dir, url, ref);
|
|
169
|
-
}
|
|
170
|
-
// ── validation ──────────────────────────────────────────────────────────────
|
|
171
|
-
export function validatePack(pack) {
|
|
172
|
-
const errors = [];
|
|
173
|
-
const { crew } = pack;
|
|
174
|
-
if (!crew.name || !/^[a-z0-9][a-z0-9-]*$/.test(crew.name))
|
|
175
|
-
errors.push('crew.json: name must be a kebab-case slug');
|
|
176
|
-
if (!crew.agents || !Object.keys(crew.agents).length)
|
|
177
|
-
errors.push('crew.json: no agents');
|
|
178
|
-
for (const [name, files] of pack.skills) {
|
|
179
|
-
const skill = files.get('SKILL.md');
|
|
180
|
-
if (!skill) {
|
|
181
|
-
errors.push(`skills/${name}: no SKILL.md`);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
const { frontmatter } = parseFrontmatter(skill.toString('utf8'));
|
|
185
|
-
if (frontmatter.name !== name)
|
|
186
|
-
errors.push(`skills/${name}/SKILL.md: name "${frontmatter.name}" must equal the folder name`);
|
|
187
|
-
const desc = String(frontmatter.description || '');
|
|
188
|
-
if (!desc)
|
|
189
|
-
errors.push(`skills/${name}/SKILL.md: description is required`);
|
|
190
|
-
else if (desc.length > 1024)
|
|
191
|
-
errors.push(`skills/${name}/SKILL.md: description is ${desc.length} chars (max 1024)`);
|
|
192
|
-
// relative links must resolve inside the pack
|
|
193
|
-
const text = skill.toString('utf8');
|
|
194
|
-
for (const m of text.matchAll(/\]\(((?:\.\.\/|reference\/|scripts\/)[^)#]+)(?:#[^)]*)?\)/g)) {
|
|
195
|
-
const target = m[1];
|
|
196
|
-
const parts = target.split('/');
|
|
197
|
-
const resolved = parts[0] === '..' ? pack.skills.get(parts[1])?.has(parts.slice(2).join('/')) : files.has(target);
|
|
198
|
-
if (!resolved)
|
|
199
|
-
errors.push(`skills/${name}/SKILL.md: link ${target} does not resolve`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
for (const [slug, spec] of Object.entries(crew.agents || {})) {
|
|
203
|
-
const persona = pack.agents[slug];
|
|
204
|
-
if (!persona)
|
|
205
|
-
continue;
|
|
206
|
-
if (persona.frontmatter.name !== slug)
|
|
207
|
-
errors.push(`${spec.prompt}: frontmatter name "${persona.frontmatter.name}" must equal the slug ${slug}`);
|
|
208
|
-
if (!persona.frontmatter.description)
|
|
209
|
-
errors.push(`${spec.prompt}: description is required`);
|
|
210
|
-
const fmSkills = listOf(persona.frontmatter.skills).sort().join(',');
|
|
211
|
-
if (fmSkills !== [...spec.skills].sort().join(','))
|
|
212
|
-
errors.push(`${spec.prompt}: frontmatter skills [${fmSkills}] differ from crew.json [${spec.skills.join(',')}]`);
|
|
213
|
-
for (const s of spec.skills)
|
|
214
|
-
if (!pack.skills.has(s))
|
|
215
|
-
errors.push(`crew.json: agent ${slug} lists unknown skill ${s}`);
|
|
216
|
-
for (const d of spec.delegatesTo || [])
|
|
217
|
-
if (!crew.agents[d])
|
|
218
|
-
errors.push(`crew.json: agent ${slug} delegates to unknown ${d}`);
|
|
219
|
-
const tools = listOf(persona.frontmatter.tools);
|
|
220
|
-
if (!spec.capabilities.write && tools.some((t) => /^(Edit|Write|MultiEdit|NotebookEdit)$/.test(t)))
|
|
221
|
-
errors.push(`${spec.prompt}: read-only seat lists a write tool`);
|
|
222
|
-
if (spec.capabilities.spawn && !tools.includes('Agent'))
|
|
223
|
-
errors.push(`${spec.prompt}: a spawning seat needs the Agent tool`);
|
|
224
|
-
}
|
|
225
|
-
for (const [id, ch] of Object.entries(crew.channels || {}))
|
|
226
|
-
for (const m of ch.members)
|
|
227
|
-
if (!crew.agents[m])
|
|
228
|
-
errors.push(`crew.json: channel ${id} has unknown member ${m}`);
|
|
229
|
-
return errors;
|
|
230
|
-
}
|
|
231
|
-
const home = (o) => o.home || process.env.HOME || homedir();
|
|
232
|
-
export function claudeRoot(o) {
|
|
233
|
-
return o.project ? join(resolve(o.project), '.claude') : join(home(o), '.claude');
|
|
234
|
-
}
|
|
235
|
-
export function agentsSkillsRoot(o) {
|
|
236
|
-
return o.project ? join(resolve(o.project), '.agents', 'skills') : join(home(o), '.agents', 'skills');
|
|
237
|
-
}
|
|
238
|
-
export function codexAgentsRoot(o) {
|
|
239
|
-
return o.project ? join(resolve(o.project), '.codex', 'agents') : join(home(o), '.codex', 'agents');
|
|
240
|
-
}
|
|
241
|
-
export function openclawRoot(pack, o) {
|
|
242
|
-
return o.project ? join(resolve(o.project), 'openclaw') : join(home(o), '.openclaw', 'workspaces', pack.crew.name);
|
|
243
|
-
}
|
|
244
|
-
function skillOps(pack, root, o, note) {
|
|
245
|
-
const ops = [];
|
|
246
|
-
for (const [name, files] of pack.skills) {
|
|
247
|
-
const path = join(root, name);
|
|
248
|
-
if (o.link)
|
|
249
|
-
ops.push({ kind: 'link', path, target: join(pack.root, pack.crew.skillsDir || 'skills', name), note: `${note} skill ${name} (symlink)` });
|
|
250
|
-
else
|
|
251
|
-
ops.push({ kind: 'dir', path, files, note: `${note} skill ${name} (${files.size} files)` });
|
|
252
|
-
}
|
|
253
|
-
return ops;
|
|
254
|
-
}
|
|
255
|
-
export function planClaude(pack, o) {
|
|
256
|
-
const root = claudeRoot(o);
|
|
257
|
-
const ops = skillOps(pack, join(root, 'skills'), o, 'claude');
|
|
258
|
-
for (const [slug, persona] of Object.entries(pack.agents)) {
|
|
259
|
-
const path = join(root, 'agents', `${slug}.md`);
|
|
260
|
-
if (o.link)
|
|
261
|
-
ops.push({ kind: 'link', path, target: join(pack.root, pack.crew.agents[slug].prompt), note: `claude agent ${slug} (symlink)` });
|
|
262
|
-
else
|
|
263
|
-
ops.push({ kind: 'file', path, content: persona.raw, note: `claude agent ${slug}` });
|
|
264
|
-
}
|
|
265
|
-
return ops;
|
|
266
|
-
}
|
|
267
|
-
/** Codex `.codex/agents/<slug>.toml` — the persona body becomes developer_instructions. */
|
|
268
|
-
export function codexAgentToml(pack, slug, skillsBase) {
|
|
269
|
-
const spec = pack.crew.agents[slug];
|
|
270
|
-
const persona = pack.agents[slug];
|
|
271
|
-
const q = (s) => JSON.stringify(s);
|
|
272
|
-
const sandbox = spec.capabilities.write ? 'workspace-write' : 'read-only';
|
|
273
|
-
const extra = [
|
|
274
|
-
'',
|
|
275
|
-
`Skills for this seat live under ${skillsBase}/: ${spec.skills.join(', ')}.`,
|
|
276
|
-
spec.delegatesTo.length ? `Delegate with spawn_agent to: ${spec.delegatesTo.join(', ')}.` : 'You do not spawn other agents; report back to the seat that assigned you.',
|
|
277
|
-
spec.capabilities.spend ? 'You may run wji generation commands after the cost has been approved.' : 'You never run wji generation or spend credits.',
|
|
278
|
-
].join('\n');
|
|
279
|
-
const body = `${persona.body.trimEnd()}\n${extra}\n`.replace(/"""/g, '\\"\\"\\"');
|
|
280
|
-
const lines = [
|
|
281
|
-
`# GENERATED by \`wji crew install\` from ${pack.crew.name}/agents/${slug}.md — edit the pack, not this file.`,
|
|
282
|
-
slug === 'wji-operator' || spec.capabilities.network
|
|
283
|
-
? `# This seat needs network access: set [sandbox_workspace_write] network_access = true in ~/.codex/config.toml.`
|
|
284
|
-
: '',
|
|
285
|
-
`name = ${q(slug)}`,
|
|
286
|
-
`description = ${q(String(persona.frontmatter.description || spec.name))}`,
|
|
287
|
-
];
|
|
288
|
-
if (spec.model)
|
|
289
|
-
lines.push(`model = ${q(spec.model)}`);
|
|
290
|
-
lines.push(`sandbox_mode = ${q(sandbox)}`, `developer_instructions = """`, body, `"""`, '');
|
|
291
|
-
for (const s of spec.skills)
|
|
292
|
-
lines.push('[[skills.config]]', `path = ${q(`${skillsBase}/${s}`)}`, 'enabled = true', '');
|
|
293
|
-
return lines.filter((l) => l !== undefined).join('\n');
|
|
294
|
-
}
|
|
295
|
-
export function planCodex(pack, o) {
|
|
296
|
-
const skillsRoot = agentsSkillsRoot(o);
|
|
297
|
-
const ops = skillOps(pack, skillsRoot, o, 'codex');
|
|
298
|
-
const base = o.project ? '.agents/skills' : skillsRoot;
|
|
299
|
-
for (const slug of Object.keys(pack.agents)) {
|
|
300
|
-
ops.push({ kind: 'file', path: join(codexAgentsRoot(o), `${slug}.toml`), content: codexAgentToml(pack, slug, base), note: `codex agent ${slug}` });
|
|
301
|
-
}
|
|
302
|
-
return ops;
|
|
303
|
-
}
|
|
304
|
-
/** OpenClaw tool ids per capability; one table so a rename is a one-line fix. */
|
|
305
|
-
const OPENCLAW_TOOLS = {
|
|
306
|
-
read: ['read'],
|
|
307
|
-
write: ['write', 'edit'],
|
|
308
|
-
exec: ['exec'],
|
|
309
|
-
spawn: ['sessions_spawn', 'sessions_send', 'agents_list'],
|
|
310
|
-
network: ['web_fetch', 'browser'],
|
|
311
|
-
};
|
|
312
|
-
export function openclawSeatAgentsMd(pack, slug) {
|
|
313
|
-
const spec = pack.crew.agents[slug];
|
|
314
|
-
const caps = spec.capabilities;
|
|
315
|
-
const seat = [
|
|
316
|
-
`# Seat: ${slug} (${spec.name})`,
|
|
317
|
-
'',
|
|
318
|
-
`You are seat \`${slug}\` of the ${pack.crew.title || pack.crew.name} crew.`,
|
|
319
|
-
`- Skills allowed: ${spec.skills.join(', ')}`,
|
|
320
|
-
`- May spawn: ${spec.delegatesTo.length ? spec.delegatesTo.join(', ') : 'nobody — report back to the seat that assigned you'}`,
|
|
321
|
-
`- Spend credits / run generation: ${caps.spend ? 'yes, after the approved cost is relayed' : 'never'}`,
|
|
322
|
-
caps.exec === 'readonly' ? '- Shell use is read-only (validate / checkup / manifest / grep / diff); never edit files.' : caps.exec === 'engine' ? '- Shell use is the short-drama engine and read-only wji commands only.' : '',
|
|
323
|
-
!caps.write ? '- You do not write files.' : '',
|
|
324
|
-
'',
|
|
325
|
-
]
|
|
326
|
-
.filter((l) => l !== '')
|
|
327
|
-
.join('\n');
|
|
328
|
-
return `${seat}\n${pack.agentsMd ? `\n---\n\n${pack.agentsMd.trimEnd()}\n` : ''}`;
|
|
329
|
-
}
|
|
330
|
-
export function openclawFragment(pack, workspacesRoot, repoPlaceholder = true) {
|
|
331
|
-
const ws = (slug) => (repoPlaceholder ? `\${WORKSPACES}/${slug}` : join(workspacesRoot, slug));
|
|
332
|
-
const entries = [];
|
|
333
|
-
for (const [slug, spec] of Object.entries(pack.crew.agents)) {
|
|
334
|
-
const c = spec.capabilities;
|
|
335
|
-
const allow = [...OPENCLAW_TOOLS.read, ...(c.write ? OPENCLAW_TOOLS.write : []), ...(c.exec ? OPENCLAW_TOOLS.exec : []), ...(c.spawn ? OPENCLAW_TOOLS.spawn : [])];
|
|
336
|
-
const deny = [...(c.write ? [] : OPENCLAW_TOOLS.write), ...(c.network ? [] : OPENCLAW_TOOLS.network), ...(c.spawn ? [] : ['sessions_spawn'])];
|
|
337
|
-
const peers = [...new Set([...spec.delegatesTo, ...Object.entries(pack.crew.agents).filter(([, s]) => s.delegatesTo.includes(slug)).map(([id]) => id)])];
|
|
338
|
-
entries.push(` ${JSON.stringify(slug)}: {`, ` name: ${JSON.stringify(spec.name)},`, ` workspace: ${JSON.stringify(ws(slug))},`, spec.model ? ` model: ${JSON.stringify(spec.model)},` : '', ` skills: ${JSON.stringify(spec.skills)},`, ` tools: { allow: ${JSON.stringify(allow)}, deny: ${JSON.stringify(deny)} },`, ` subagents: { allowAgents: ${JSON.stringify(spec.delegatesTo)}${spec.delegatesTo.length ? ', delegationMode: "prefer"' : ''} },`, ` // agent-to-agent messaging peers: ${peers.join(', ') || 'none'}`, ` },`);
|
|
339
|
-
}
|
|
340
|
-
const bindings = [];
|
|
341
|
-
for (const [chId, ch] of Object.entries(pack.crew.channels || {})) {
|
|
342
|
-
bindings.push(` // channel "${ch.name}" (${chId}): members ${ch.members.join(', ')} — fill in the real channel / group id`);
|
|
343
|
-
for (const m of ch.members)
|
|
344
|
-
bindings.push(` { agentId: ${JSON.stringify(m)}, match: { channel: "TODO", peer: { kind: "group", id: "TODO:${chId}" } } },`);
|
|
345
|
-
}
|
|
346
|
-
return [
|
|
347
|
-
`// GENERATED by \`wji crew install\` from the ${pack.crew.name} pack. Merge into ~/.openclaw/openclaw.json`,
|
|
348
|
-
`// (agents.entries + bindings). ${repoPlaceholder ? '${WORKSPACES} = ' + workspacesRoot : ''}`,
|
|
349
|
-
'{',
|
|
350
|
-
' agents: {',
|
|
351
|
-
` defaults: { subagents: { allowAgents: ["*"], maxConcurrent: 4, maxSpawnDepth: 2 } },`,
|
|
352
|
-
' entries: {',
|
|
353
|
-
...entries.filter(Boolean),
|
|
354
|
-
' },',
|
|
355
|
-
' },',
|
|
356
|
-
` tools: { agentToAgent: { enabled: true, allow: ${JSON.stringify(Object.keys(pack.crew.agents))} } },`,
|
|
357
|
-
' bindings: [',
|
|
358
|
-
...bindings,
|
|
359
|
-
' ],',
|
|
360
|
-
'}',
|
|
361
|
-
'',
|
|
362
|
-
].join('\n');
|
|
363
|
-
}
|
|
364
|
-
export function findOpenclaw() {
|
|
365
|
-
const candidates = [join(homedir(), '.openclaw', 'bin', 'openclaw')];
|
|
366
|
-
for (const c of candidates)
|
|
367
|
-
if (existsSync(c))
|
|
368
|
-
return c;
|
|
369
|
-
const which = spawnSync('which', ['openclaw'], { encoding: 'utf8' });
|
|
370
|
-
const path = which.status === 0 ? which.stdout.trim() : '';
|
|
371
|
-
return path || undefined;
|
|
372
|
-
}
|
|
373
|
-
export function planOpenclaw(pack, o, opts = {}) {
|
|
374
|
-
const skillsRoot = agentsSkillsRoot(o);
|
|
375
|
-
const ops = skillOps(pack, skillsRoot, o, 'openclaw');
|
|
376
|
-
const root = openclawRoot(pack, o);
|
|
377
|
-
for (const [slug, persona] of Object.entries(pack.agents)) {
|
|
378
|
-
const ws = join(root, slug);
|
|
379
|
-
ops.push({ kind: 'file', path: join(ws, 'SOUL.md'), content: `# ${pack.crew.agents[slug].name}\n\n${persona.body.trimEnd()}\n`, note: `openclaw ${slug}/SOUL.md` });
|
|
380
|
-
ops.push({ kind: 'file', path: join(ws, 'AGENTS.md'), content: openclawSeatAgentsMd(pack, slug), note: `openclaw ${slug}/AGENTS.md` });
|
|
381
|
-
ops.push({ kind: 'link', path: join(ws, 'skills'), target: skillsRoot, note: `openclaw ${slug}/skills → shared skills` });
|
|
382
|
-
}
|
|
383
|
-
ops.push({ kind: 'file', path: join(root, 'openclaw.agents.json5'), content: openclawFragment(pack, root, false), note: 'openclaw config fragment (merge by hand)' });
|
|
384
|
-
const bin = opts.register ? findOpenclaw() : undefined;
|
|
385
|
-
if (bin) {
|
|
386
|
-
const listed = spawnSync(bin, ['agents', 'list', '--json'], { encoding: 'utf8' });
|
|
387
|
-
const existing = new Set();
|
|
388
|
-
try {
|
|
389
|
-
const parsed = JSON.parse(listed.stdout || '[]');
|
|
390
|
-
for (const a of Array.isArray(parsed) ? parsed : parsed.agents || [])
|
|
391
|
-
existing.add(String(a.id || a.name));
|
|
392
|
-
}
|
|
393
|
-
catch {
|
|
394
|
-
/* unreadable list — register everything and let openclaw refuse duplicates */
|
|
395
|
-
}
|
|
396
|
-
for (const slug of Object.keys(pack.agents)) {
|
|
397
|
-
if (existing.has(slug))
|
|
398
|
-
continue;
|
|
399
|
-
ops.push({ kind: 'exec', argv: [bin, 'agents', 'add', slug, '--workspace', join(root, slug), '--non-interactive'], note: `openclaw agents add ${slug}` });
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return ops;
|
|
403
|
-
}
|
|
404
|
-
/** The export layout the Grok bot platform imports: skills/ + bots/*.json + channels/*.json. */
|
|
405
|
-
export function grokZip(pack) {
|
|
406
|
-
const entries = [];
|
|
407
|
-
const rootName = `${pack.crew.name}-export`;
|
|
408
|
-
const rewrite = (text) => text
|
|
409
|
-
// sibling-skill links become the platform's skill links
|
|
410
|
-
.replace(/\]\(\.\.\/([a-z0-9-]+)\/SKILL\.md\)/g, '](sand-workflow:$1)')
|
|
411
|
-
// sibling paths become the `{skill}/…` placeholder the platform's skills use
|
|
412
|
-
.replace(/\.\.\/([a-z0-9-]+)\//g, '{$1}/');
|
|
413
|
-
for (const [name, files] of pack.skills) {
|
|
414
|
-
for (const [path, content] of files) {
|
|
415
|
-
const text = /\.(md|mjs|js|json|txt|sh)$/i.test(path) ? rewrite(content.toString('utf8')) : content;
|
|
416
|
-
entries.push({ path: `${rootName}/skills/${name}/${path}`, content: text });
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
const bots = Object.entries(pack.crew.agents).map(([slug, spec]) => {
|
|
420
|
-
const persona = pack.agents[slug];
|
|
421
|
-
const description = `${String(persona.frontmatter.description || '')}\n\n${rewrite(persona.body).trim()}`;
|
|
422
|
-
return { slug, name: spec.name, title: spec.name, description };
|
|
423
|
-
});
|
|
424
|
-
for (const bot of bots)
|
|
425
|
-
entries.push({ path: `${rootName}/bots/${bot.slug}.json`, content: JSON.stringify(bot, null, 2) + '\n' });
|
|
426
|
-
const channels = Object.entries(pack.crew.channels || {}).map(([slug, ch]) => ({ slug, name: ch.name, memberSlugs: ch.members }));
|
|
427
|
-
for (const ch of channels)
|
|
428
|
-
entries.push({ path: `${rootName}/channels/${ch.slug}.json`, content: JSON.stringify(ch, null, 2) + '\n' });
|
|
429
|
-
const manifest = {
|
|
430
|
-
version: 2,
|
|
431
|
-
exportedAt: new Date().toISOString().slice(0, 10),
|
|
432
|
-
bots: bots.map((b) => ({ slug: b.slug, name: b.name, title: b.title, description: b.description })),
|
|
433
|
-
channels,
|
|
434
|
-
skills: [...pack.skills.keys()].sort(),
|
|
435
|
-
notes: `Portable ${pack.crew.title || pack.crew.name} pack built by \`wji crew install --for grok\`. Recreate bots from bots/*.json (name + description), channels from channels/*.json memberSlugs, copy every skills/<name> folder into the shared skills library. Account-local ids are never included.`,
|
|
436
|
-
};
|
|
437
|
-
entries.push({ path: `${rootName}/bots/crew-manifest.json`, content: JSON.stringify(manifest, null, 2) + '\n' });
|
|
438
|
-
const readme = [
|
|
439
|
-
`# ${pack.crew.title || pack.crew.name} export`,
|
|
440
|
-
'',
|
|
441
|
-
'Portable pack: skill folders + bot personas + channel recipes.',
|
|
442
|
-
'',
|
|
443
|
-
'1. **Skills** — copy each folder under `skills/` into the account\'s shared skills library.',
|
|
444
|
-
'2. **Bots** — create one agent per `bots/*.json` using the same `name` and `description`' + (pack.crew.entry ? ` (start with ${pack.crew.agents[pack.crew.entry]?.name || pack.crew.entry}).` : '.'),
|
|
445
|
-
'3. **Channels** — create groups from `channels/*.json` and add members by the listed slugs.',
|
|
446
|
-
'',
|
|
447
|
-
'| Bot | Owns |',
|
|
448
|
-
'| --- | --- |',
|
|
449
|
-
...bots.map((b) => `| ${b.name} | ${String(pack.agents[b.slug].frontmatter.description || '').split(/[.。]/)[0]} |`),
|
|
450
|
-
'',
|
|
451
|
-
].join('\n');
|
|
452
|
-
entries.push({ path: `${rootName}/README.md`, content: readme });
|
|
453
|
-
return { zip: createZip(entries), entries: entries.length };
|
|
454
|
-
}
|
|
455
|
-
export function installedPath() {
|
|
456
|
-
return join(crewCacheDir(), 'installed.json');
|
|
457
|
-
}
|
|
458
|
-
export function readInstalled() {
|
|
459
|
-
const p = installedPath();
|
|
460
|
-
if (!existsSync(p))
|
|
461
|
-
return {};
|
|
462
|
-
try {
|
|
463
|
-
return JSON.parse(readFileSync(p, 'utf8'));
|
|
464
|
-
}
|
|
465
|
-
catch {
|
|
466
|
-
return {};
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
export function writeInstalled(data) {
|
|
470
|
-
mkdirSync(dirname(installedPath()), { recursive: true });
|
|
471
|
-
writeFileSync(installedPath(), JSON.stringify(data, null, 2) + '\n');
|
|
472
|
-
}
|
|
473
|
-
function removePath(path) {
|
|
474
|
-
if (!existsSync(path) && !isSymlink(path))
|
|
475
|
-
return;
|
|
476
|
-
if (isSymlink(path) || !lstatSync(path).isDirectory())
|
|
477
|
-
rmSync(path, { force: true });
|
|
478
|
-
else
|
|
479
|
-
rmSync(path, { recursive: true, force: true });
|
|
480
|
-
}
|
|
481
|
-
const isSymlink = (p) => {
|
|
482
|
-
try {
|
|
483
|
-
return lstatSync(p).isSymbolicLink();
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
return false;
|
|
487
|
-
}
|
|
488
|
-
};
|
|
489
|
-
/** Run the ops; returns every path written plus the notes of executed commands. */
|
|
490
|
-
export function applyOps(ops, log) {
|
|
491
|
-
const paths = [];
|
|
492
|
-
const notes = [];
|
|
493
|
-
for (const op of ops) {
|
|
494
|
-
if (op.kind === 'exec') {
|
|
495
|
-
const res = spawnSync(op.argv[0], op.argv.slice(1), { encoding: 'utf8' });
|
|
496
|
-
const tail = (res.stdout || res.stderr || '').trim().split('\n').slice(-1)[0] || '';
|
|
497
|
-
notes.push(`${op.note}: ${res.status === 0 ? 'ok' : `exit ${res.status}`}${tail ? ` — ${tail}` : ''}`);
|
|
498
|
-
log(` ${res.status === 0 ? '✓' : '✗'} ${op.note}${tail ? ` — ${tail}` : ''}`);
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
mkdirSync(dirname(op.path), { recursive: true });
|
|
502
|
-
if (op.kind === 'link') {
|
|
503
|
-
removePath(op.path);
|
|
504
|
-
symlinkSync(op.target, op.path);
|
|
505
|
-
}
|
|
506
|
-
else if (op.kind === 'file') {
|
|
507
|
-
writeFileSync(op.path, op.content);
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
removePath(op.path);
|
|
511
|
-
for (const [rel, content] of op.files) {
|
|
512
|
-
const full = join(op.path, rel);
|
|
513
|
-
mkdirSync(dirname(full), { recursive: true });
|
|
514
|
-
writeFileSync(full, content);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
paths.push(op.path);
|
|
518
|
-
log(` ✓ ${op.note} → ${op.path}`);
|
|
519
|
-
}
|
|
520
|
-
return { paths, notes };
|
|
521
|
-
}
|
|
522
|
-
export function describeOp(op) {
|
|
523
|
-
if (op.kind === 'exec')
|
|
524
|
-
return `run ${op.argv.join(' ')}`;
|
|
525
|
-
if (op.kind === 'link')
|
|
526
|
-
return `link ${op.path} → ${op.target}`;
|
|
527
|
-
if (op.kind === 'file')
|
|
528
|
-
return `write ${op.path}`;
|
|
529
|
-
return `copy ${op.path} (${op.files.size} files)`;
|
|
530
|
-
}
|
|
531
|
-
export function pathState(path) {
|
|
532
|
-
if (isSymlink(path))
|
|
533
|
-
return existsSync(path) ? 'link' : 'missing';
|
|
534
|
-
return existsSync(path) ? 'ok' : 'missing';
|
|
535
|
-
}
|
|
536
|
-
export function uninstallPaths(paths) {
|
|
537
|
-
for (const p of paths)
|
|
538
|
-
removePath(p);
|
|
539
|
-
}
|
|
540
|
-
export function relOrAbs(path) {
|
|
541
|
-
const rel = relative(process.cwd(), path);
|
|
542
|
-
return rel && !rel.startsWith('..') && !isAbsolute(rel) ? rel : path;
|
|
543
|
-
}
|
|
544
|
-
export function linkTarget(path) {
|
|
545
|
-
return isSymlink(path) ? readlinkSync(path) : undefined;
|
|
546
|
-
}
|
package/dist/core/members.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
export async function loadMembers(client, workspaceId) {
|
|
2
|
-
const ws = await client.getFresh(`/workspace/list/${workspaceId}`);
|
|
3
|
-
const members = (ws.members || []).map((m) => ({
|
|
4
|
-
memberId: String(m.id),
|
|
5
|
-
userId: String(m.user?.id ?? m.user ?? ''),
|
|
6
|
-
name: String(m.user?.name || ''),
|
|
7
|
-
email: String(m.user?.email || ''),
|
|
8
|
-
role: m.role,
|
|
9
|
-
}));
|
|
10
|
-
return { workspace: ws, members };
|
|
11
|
-
}
|
|
12
|
-
/** What people call a member in a list: the name, else the email, else the id. */
|
|
13
|
-
export const memberLabel = (m) => m.name || m.email || m.userId;
|
|
14
|
-
/**
|
|
15
|
-
* One member from what a person would type: user id, membership id, email or
|
|
16
|
-
* name (case-insensitive), else a unique name prefix. Ambiguity is an error,
|
|
17
|
-
* never a guess.
|
|
18
|
-
*/
|
|
19
|
-
export function resolveMember(members, token) {
|
|
20
|
-
const t = token.trim();
|
|
21
|
-
const lower = t.toLowerCase();
|
|
22
|
-
const exact = members.find((m) => m.userId === t || m.memberId === t);
|
|
23
|
-
if (exact)
|
|
24
|
-
return exact;
|
|
25
|
-
const byEmail = members.filter((m) => m.email.toLowerCase() === lower);
|
|
26
|
-
if (byEmail.length === 1)
|
|
27
|
-
return byEmail[0];
|
|
28
|
-
const byName = members.filter((m) => m.name.toLowerCase() === lower);
|
|
29
|
-
if (byName.length === 1)
|
|
30
|
-
return byName[0];
|
|
31
|
-
if (byName.length > 1) {
|
|
32
|
-
throw new Error(`Several members are called "${t}": ${byName.map((m) => `${m.email || m.userId}`).join(', ')} — use the email or user id`);
|
|
33
|
-
}
|
|
34
|
-
const prefix = members.filter((m) => m.name.toLowerCase().startsWith(lower) || m.email.toLowerCase().startsWith(lower));
|
|
35
|
-
if (prefix.length === 1)
|
|
36
|
-
return prefix[0];
|
|
37
|
-
if (prefix.length > 1) {
|
|
38
|
-
throw new Error(`"${t}" matches several members: ${prefix.map(memberLabel).join(', ')}`);
|
|
39
|
-
}
|
|
40
|
-
throw new Error(`No member "${t}" in this workspace. See \`wji ws member list\`.`);
|
|
41
|
-
}
|
|
42
|
-
/** Run `fn` over `items` with at most `limit` in flight, keeping input order. */
|
|
43
|
-
export async function mapLimit(items, limit, fn) {
|
|
44
|
-
const out = new Array(items.length);
|
|
45
|
-
let next = 0;
|
|
46
|
-
const worker = async () => {
|
|
47
|
-
while (next < items.length) {
|
|
48
|
-
const i = next++;
|
|
49
|
-
out[i] = await fn(items[i], i);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
53
|
-
return out;
|
|
54
|
-
}
|