@folpe/loom 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -16
- package/data/agents/backend/AGENT.md +35 -3
- package/data/agents/database/AGENT.md +16 -4
- package/data/agents/frontend/AGENT.md +39 -5
- package/data/agents/marketing/AGENT.md +19 -2
- package/data/agents/orchestrator/AGENT.md +3 -21
- package/data/agents/performance/AGENT.md +9 -2
- package/data/agents/review-qa/AGENT.md +7 -3
- package/data/agents/security/AGENT.md +10 -4
- package/data/agents/tests/AGENT.md +11 -2
- package/data/agents/ux-ui/AGENT.md +16 -3
- package/data/presets/api-backend.yaml +2 -11
- package/data/presets/chrome-extension.yaml +2 -11
- package/data/presets/cli-tool.yaml +2 -10
- package/data/presets/e-commerce.yaml +2 -14
- package/data/presets/expo-mobile.yaml +2 -12
- package/data/presets/fullstack-auth.yaml +2 -14
- package/data/presets/landing-page.yaml +2 -11
- package/data/presets/mvp-lean.yaml +2 -12
- package/data/presets/saas-default.yaml +5 -14
- package/data/presets/saas-full.yaml +58 -0
- package/data/skills/api-design/SKILL.md +43 -2
- package/data/skills/auth-rbac/SKILL.md +179 -0
- package/data/skills/better-auth-patterns/SKILL.md +212 -0
- package/data/skills/chrome-extension-patterns/SKILL.md +13 -6
- package/data/skills/cli-development/SKILL.md +11 -3
- package/data/skills/drizzle-patterns/SKILL.md +166 -0
- package/data/skills/env-validation/SKILL.md +142 -0
- package/data/skills/form-validation/SKILL.md +169 -0
- package/data/skills/hero-copywriting/SKILL.md +12 -4
- package/data/skills/i18n-patterns/SKILL.md +176 -0
- package/data/skills/layered-architecture/SKILL.md +131 -0
- package/data/skills/nextjs-conventions/SKILL.md +46 -7
- package/data/skills/react-native-patterns/SKILL.md +10 -8
- package/data/skills/react-query-patterns/SKILL.md +193 -0
- package/data/skills/resend-email/SKILL.md +181 -0
- package/data/skills/seo-optimization/SKILL.md +10 -2
- package/data/skills/server-actions-patterns/SKILL.md +156 -0
- package/data/skills/shadcn-ui/SKILL.md +46 -12
- package/data/skills/stripe-integration/SKILL.md +11 -3
- package/data/skills/supabase-patterns/SKILL.md +23 -6
- package/data/skills/table-pagination/SKILL.md +224 -0
- package/data/skills/tailwind-patterns/SKILL.md +12 -2
- package/data/skills/testing-patterns/SKILL.md +203 -0
- package/data/skills/ui-ux-guidelines/SKILL.md +10 -5
- package/dist/index.js +451 -122
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/commands/list.ts
|
|
@@ -31,14 +32,16 @@ async function listAgents() {
|
|
|
31
32
|
try {
|
|
32
33
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
33
34
|
const { data } = matter(raw);
|
|
35
|
+
const fm = data;
|
|
34
36
|
agents.push({
|
|
35
37
|
slug,
|
|
36
|
-
name:
|
|
37
|
-
description:
|
|
38
|
-
role:
|
|
38
|
+
name: fm.name || slug,
|
|
39
|
+
description: fm.description || "",
|
|
40
|
+
role: fm.role || "",
|
|
41
|
+
skills: Array.isArray(fm.skills) ? fm.skills : []
|
|
39
42
|
});
|
|
40
43
|
} catch {
|
|
41
|
-
agents.push({ slug, name: slug, description: "", role: "" });
|
|
44
|
+
agents.push({ slug, name: slug, description: "", role: "", skills: [] });
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
return agents;
|
|
@@ -146,10 +149,10 @@ async function listCommand(type) {
|
|
|
146
149
|
if (presets.length === 0) {
|
|
147
150
|
console.log(pc.dim(" No presets found."));
|
|
148
151
|
}
|
|
149
|
-
for (const
|
|
150
|
-
const meta = pc.dim(`(${
|
|
152
|
+
for (const p2 of presets) {
|
|
153
|
+
const meta = pc.dim(`(${p2.agentCount} agents, ${p2.skillCount} skills)`);
|
|
151
154
|
console.log(
|
|
152
|
-
` ${padEnd(pc.green(
|
|
155
|
+
` ${padEnd(pc.green(p2.slug), 30)} ${padEnd(p2.name, 25)} ${meta}`
|
|
153
156
|
);
|
|
154
157
|
}
|
|
155
158
|
}
|
|
@@ -175,32 +178,37 @@ import pc2 from "picocolors";
|
|
|
175
178
|
// src/lib/writer.ts
|
|
176
179
|
import fs2 from "fs";
|
|
177
180
|
import path2 from "path";
|
|
178
|
-
var CLAUDE_DIR = ".claude";
|
|
179
181
|
function ensureDir(dirPath) {
|
|
180
182
|
fs2.mkdirSync(dirPath, { recursive: true });
|
|
181
183
|
}
|
|
182
|
-
function writeAgent(slug, content, cwd = process.cwd()) {
|
|
183
|
-
const dir = path2.join(cwd,
|
|
184
|
+
function writeAgent(target, slug, content, cwd = process.cwd()) {
|
|
185
|
+
const dir = path2.join(cwd, target.dir, target.agentsSubdir, slug);
|
|
184
186
|
ensureDir(dir);
|
|
185
|
-
const filePath = path2.join(dir,
|
|
187
|
+
const filePath = path2.join(dir, "AGENT.md");
|
|
186
188
|
fs2.writeFileSync(filePath, content, "utf-8");
|
|
187
189
|
return filePath;
|
|
188
190
|
}
|
|
189
|
-
function writeSkill(slug, content, cwd = process.cwd()) {
|
|
190
|
-
const dir = path2.join(cwd,
|
|
191
|
+
function writeSkill(target, slug, content, cwd = process.cwd()) {
|
|
192
|
+
const dir = path2.join(cwd, target.dir, target.skillsSubdir, slug);
|
|
191
193
|
ensureDir(dir);
|
|
192
|
-
const filePath = path2.join(dir,
|
|
194
|
+
const filePath = path2.join(dir, "SKILL.md");
|
|
193
195
|
fs2.writeFileSync(filePath, content, "utf-8");
|
|
194
196
|
return filePath;
|
|
195
197
|
}
|
|
196
|
-
function
|
|
197
|
-
const filePath = path2.join(cwd,
|
|
198
|
+
function writeOrchestrator(target, content, cwd = process.cwd()) {
|
|
199
|
+
const filePath = path2.join(cwd, target.dir, target.orchestratorFile);
|
|
200
|
+
ensureDir(path2.dirname(filePath));
|
|
201
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
202
|
+
return filePath;
|
|
203
|
+
}
|
|
204
|
+
function writeContextFile(target, content, cwd = process.cwd()) {
|
|
205
|
+
const filePath = path2.join(cwd, target.contextFile);
|
|
198
206
|
fs2.writeFileSync(filePath, content, "utf-8");
|
|
199
207
|
return filePath;
|
|
200
208
|
}
|
|
201
209
|
|
|
202
210
|
// src/commands/add.ts
|
|
203
|
-
async function addCommand(type, slug) {
|
|
211
|
+
async function addCommand(type, slug, target) {
|
|
204
212
|
if (type !== "agent" && type !== "skill") {
|
|
205
213
|
console.error(pc2.red(`
|
|
206
214
|
Error: Invalid type "${type}". Use "agent" or "skill".
|
|
@@ -210,13 +218,13 @@ async function addCommand(type, slug) {
|
|
|
210
218
|
try {
|
|
211
219
|
if (type === "agent") {
|
|
212
220
|
const agent = await getAgent(slug);
|
|
213
|
-
const filePath = writeAgent(slug, agent.rawContent);
|
|
221
|
+
const filePath = writeAgent(target, slug, agent.rawContent);
|
|
214
222
|
console.log(pc2.green(`
|
|
215
223
|
\u2713 Agent "${slug}" written to ${filePath}
|
|
216
224
|
`));
|
|
217
225
|
} else {
|
|
218
226
|
const skill = await getSkill(slug);
|
|
219
|
-
const filePath = writeSkill(slug, skill.rawContent);
|
|
227
|
+
const filePath = writeSkill(target, slug, skill.rawContent);
|
|
220
228
|
console.log(pc2.green(`
|
|
221
229
|
\u2713 Skill "${slug}" written to ${filePath}
|
|
222
230
|
`));
|
|
@@ -235,31 +243,28 @@ async function addCommand(type, slug) {
|
|
|
235
243
|
|
|
236
244
|
// src/commands/init.ts
|
|
237
245
|
import pc3 from "picocolors";
|
|
238
|
-
import
|
|
246
|
+
import * as p from "@clack/prompts";
|
|
247
|
+
import matter3 from "gray-matter";
|
|
239
248
|
|
|
240
249
|
// src/lib/generator.ts
|
|
241
|
-
|
|
250
|
+
import matter2 from "gray-matter";
|
|
251
|
+
function generateContextFile(preset, agents, target, skillSlugs = []) {
|
|
242
252
|
const lines = [];
|
|
243
253
|
lines.push(`# ${preset.name}`);
|
|
244
254
|
lines.push("");
|
|
245
|
-
lines.push(preset.
|
|
255
|
+
lines.push(preset.context.projectDescription.trim());
|
|
246
256
|
lines.push("");
|
|
247
257
|
if (preset.constitution.principles.length > 0) {
|
|
248
258
|
lines.push("## Principles");
|
|
249
|
-
for (const p of preset.constitution.principles) {
|
|
250
|
-
lines.push(`- ${p}`);
|
|
251
|
-
}
|
|
252
259
|
lines.push("");
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
lines.push("## Stack");
|
|
256
|
-
for (const s of preset.constitution.stack) {
|
|
257
|
-
lines.push(`- ${s}`);
|
|
260
|
+
for (const p2 of preset.constitution.principles) {
|
|
261
|
+
lines.push(`- ${p2}`);
|
|
258
262
|
}
|
|
259
263
|
lines.push("");
|
|
260
264
|
}
|
|
261
265
|
if (preset.constitution.conventions.length > 0) {
|
|
262
266
|
lines.push("## Conventions");
|
|
267
|
+
lines.push("");
|
|
263
268
|
for (const c of preset.constitution.conventions) {
|
|
264
269
|
lines.push(`- ${c}`);
|
|
265
270
|
}
|
|
@@ -268,133 +273,457 @@ function generateClaudeMd(preset, agents) {
|
|
|
268
273
|
if (preset.constitution.customSections) {
|
|
269
274
|
for (const [title, content] of Object.entries(preset.constitution.customSections)) {
|
|
270
275
|
lines.push(`## ${title}`);
|
|
276
|
+
lines.push("");
|
|
271
277
|
lines.push(content);
|
|
272
278
|
lines.push("");
|
|
273
279
|
}
|
|
274
280
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
281
|
+
lines.push("## Commands");
|
|
282
|
+
lines.push("");
|
|
283
|
+
lines.push("```bash");
|
|
284
|
+
lines.push("npm run dev # Start development server");
|
|
285
|
+
lines.push("npm run build # Build for production");
|
|
286
|
+
lines.push("npm run lint # Run linter");
|
|
287
|
+
lines.push("npm test # Run tests");
|
|
288
|
+
lines.push("```");
|
|
289
|
+
lines.push("");
|
|
290
|
+
lines.push("<!-- loom:agents:start -->");
|
|
291
|
+
lines.push("## Agents");
|
|
292
|
+
lines.push("");
|
|
293
|
+
const nonOrchestrator = agents.filter((a) => a.slug !== "orchestrator");
|
|
294
|
+
if (nonOrchestrator.length > 0) {
|
|
295
|
+
lines.push(`This project uses ${nonOrchestrator.length} specialized agents coordinated by an orchestrator (\`${target.dir}/${target.orchestratorFile}\`).`);
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push("| Agent | Role | Description |");
|
|
298
|
+
lines.push("|-------|------|-------------|");
|
|
299
|
+
for (const agent of nonOrchestrator) {
|
|
300
|
+
lines.push(`| \`${agent.slug}\` | ${agent.name} | ${agent.description} |`);
|
|
279
301
|
}
|
|
280
302
|
lines.push("");
|
|
281
303
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
304
|
+
lines.push("<!-- loom:agents:end -->");
|
|
305
|
+
lines.push("");
|
|
306
|
+
if (skillSlugs.length > 0) {
|
|
307
|
+
lines.push("<!-- loom:skills:start -->");
|
|
308
|
+
lines.push("## Skills");
|
|
309
|
+
lines.push("");
|
|
310
|
+
lines.push("Installed skills providing domain-specific conventions and patterns:");
|
|
311
|
+
lines.push("");
|
|
312
|
+
for (const slug of skillSlugs) {
|
|
313
|
+
lines.push(`- \`${slug}\``);
|
|
314
|
+
}
|
|
315
|
+
lines.push("");
|
|
316
|
+
lines.push("<!-- loom:skills:end -->");
|
|
285
317
|
lines.push("");
|
|
286
318
|
}
|
|
319
|
+
lines.push("## How to use");
|
|
320
|
+
lines.push("");
|
|
321
|
+
lines.push(`The orchestrator agent (\`${target.dir}/${target.orchestratorFile}\`) is the main entry point. It analyzes tasks, breaks them into subtasks, and delegates to the appropriate specialized agents. Each agent has access to its assigned skills for domain-specific guidance.`);
|
|
322
|
+
lines.push("");
|
|
287
323
|
return lines.join("\n");
|
|
288
324
|
}
|
|
325
|
+
function generateOrchestrator(templateContent, agents, presetSkills) {
|
|
326
|
+
const { data: frontmatter, content } = matter2(templateContent);
|
|
327
|
+
const rules = [];
|
|
328
|
+
const delegatesTo = [];
|
|
329
|
+
for (const agent of agents) {
|
|
330
|
+
if (agent.slug === "orchestrator") continue;
|
|
331
|
+
delegatesTo.push(agent.slug);
|
|
332
|
+
const relevantSkills = agent.skills.filter((s) => presetSkills.includes(s));
|
|
333
|
+
let line = `- **${agent.slug}**: ${agent.description}`;
|
|
334
|
+
if (relevantSkills.length > 0) {
|
|
335
|
+
line += `. Skills: ${relevantSkills.join(", ")}`;
|
|
336
|
+
}
|
|
337
|
+
rules.push(line);
|
|
338
|
+
}
|
|
339
|
+
const newFrontmatter = { ...frontmatter, "delegates-to": delegatesTo };
|
|
340
|
+
const newContent = content.replace("{{DELEGATION_RULES}}", rules.join("\n"));
|
|
341
|
+
return matter2.stringify(newContent, newFrontmatter);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/lib/target.ts
|
|
345
|
+
var BUILTIN_TARGETS = {
|
|
346
|
+
"claude-code": {
|
|
347
|
+
name: "claude-code",
|
|
348
|
+
description: "Claude Code \u2014 .claude/ + CLAUDE.md",
|
|
349
|
+
dir: ".claude",
|
|
350
|
+
agentsSubdir: "agents",
|
|
351
|
+
skillsSubdir: "skills",
|
|
352
|
+
orchestratorFile: "orchestrator.md",
|
|
353
|
+
contextFile: "CLAUDE.md"
|
|
354
|
+
},
|
|
355
|
+
cursor: {
|
|
356
|
+
name: "cursor",
|
|
357
|
+
description: "Cursor \u2014 .cursor/ + .cursorrules",
|
|
358
|
+
dir: ".cursor",
|
|
359
|
+
agentsSubdir: "agents",
|
|
360
|
+
skillsSubdir: "skills",
|
|
361
|
+
orchestratorFile: "orchestrator.md",
|
|
362
|
+
contextFile: ".cursorrules"
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
var DEFAULT_TARGET = "claude-code";
|
|
366
|
+
function listTargetNames() {
|
|
367
|
+
return Object.keys(BUILTIN_TARGETS);
|
|
368
|
+
}
|
|
369
|
+
function resolveTarget(targetName, customDir, customContextFile) {
|
|
370
|
+
const builtin = BUILTIN_TARGETS[targetName];
|
|
371
|
+
if (builtin) return builtin;
|
|
372
|
+
if (targetName === "custom") {
|
|
373
|
+
if (!customDir || !customContextFile) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
'Target "custom" requires --target-dir and --context-file.'
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
name: "custom",
|
|
380
|
+
description: `Custom \u2014 ${customDir}/ + ${customContextFile}`,
|
|
381
|
+
dir: customDir,
|
|
382
|
+
agentsSubdir: "agents",
|
|
383
|
+
skillsSubdir: "skills",
|
|
384
|
+
orchestratorFile: "orchestrator.md",
|
|
385
|
+
contextFile: customContextFile
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const available = [...listTargetNames(), "custom"].join(", ");
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Unknown target "${targetName}". Available: ${available}.`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/lib/config.ts
|
|
395
|
+
import fs3 from "fs";
|
|
396
|
+
import path3 from "path";
|
|
397
|
+
var CONFIG_FILE = "loom.config.json";
|
|
398
|
+
function saveConfig(target, cwd = process.cwd()) {
|
|
399
|
+
const config = {
|
|
400
|
+
target: target.name,
|
|
401
|
+
targetDir: target.dir,
|
|
402
|
+
contextFile: target.contextFile
|
|
403
|
+
};
|
|
404
|
+
const filePath = path3.join(cwd, CONFIG_FILE);
|
|
405
|
+
fs3.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
406
|
+
}
|
|
407
|
+
function loadConfig(cwd = process.cwd()) {
|
|
408
|
+
const filePath = path3.join(cwd, CONFIG_FILE);
|
|
409
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
410
|
+
try {
|
|
411
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
412
|
+
const config = JSON.parse(raw);
|
|
413
|
+
return resolveTarget(config.target, config.targetDir, config.contextFile);
|
|
414
|
+
} catch {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
289
418
|
|
|
290
419
|
// src/commands/init.ts
|
|
291
|
-
async function initCommand(presetSlug) {
|
|
420
|
+
async function initCommand(presetSlug, opts = {}) {
|
|
292
421
|
try {
|
|
293
|
-
|
|
294
|
-
if (!presetSlug) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
422
|
+
const hasFlags = !!(opts.addAgent || opts.removeAgent || opts.addSkill || opts.removeSkill);
|
|
423
|
+
if (!presetSlug && hasFlags) {
|
|
424
|
+
console.error(pc3.red("\n Error: flags require a preset argument. Usage: loom init <preset> [flags]\n"));
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
if (!presetSlug && !hasFlags) {
|
|
428
|
+
await interactiveInit(opts.target, opts.targetExplicit);
|
|
429
|
+
} else {
|
|
430
|
+
await nonInteractiveInit(presetSlug, opts);
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (error instanceof Error) {
|
|
434
|
+
console.error(pc3.red(`
|
|
435
|
+
Error: ${error.message}
|
|
436
|
+
`));
|
|
437
|
+
} else {
|
|
438
|
+
console.error(pc3.red("\n An unknown error occurred.\n"));
|
|
439
|
+
}
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function interactiveInit(target, targetExplicit) {
|
|
444
|
+
p.intro(pc3.bgCyan(pc3.black(" loom init ")));
|
|
445
|
+
if (!targetExplicit) {
|
|
446
|
+
const builtinEntries = Object.values(BUILTIN_TARGETS);
|
|
447
|
+
const targetChoice = await p.select({
|
|
448
|
+
message: "Choose a target runtime",
|
|
449
|
+
options: [
|
|
450
|
+
...builtinEntries.map((t) => ({
|
|
451
|
+
value: t.name,
|
|
452
|
+
label: t.description
|
|
453
|
+
})),
|
|
454
|
+
{ value: "custom", label: "Custom \u2014 choose directory and context file" }
|
|
455
|
+
],
|
|
456
|
+
initialValue: target.name
|
|
457
|
+
});
|
|
458
|
+
if (p.isCancel(targetChoice)) {
|
|
459
|
+
p.cancel("Operation cancelled.");
|
|
460
|
+
process.exit(0);
|
|
461
|
+
}
|
|
462
|
+
if (targetChoice === "custom") {
|
|
463
|
+
const customDir = await p.text({
|
|
464
|
+
message: "Target directory",
|
|
465
|
+
placeholder: ".myruntime",
|
|
466
|
+
validate: (v) => !v || v.length === 0 ? "Required" : void 0
|
|
308
467
|
});
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
468
|
+
if (p.isCancel(customDir)) {
|
|
469
|
+
p.cancel("Operation cancelled.");
|
|
470
|
+
process.exit(0);
|
|
471
|
+
}
|
|
472
|
+
const customFile = await p.text({
|
|
473
|
+
message: "Context file name",
|
|
474
|
+
placeholder: "CONTEXT.md",
|
|
475
|
+
validate: (v) => !v || v.length === 0 ? "Required" : void 0
|
|
314
476
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
process.exit(1);
|
|
477
|
+
if (p.isCancel(customFile)) {
|
|
478
|
+
p.cancel("Operation cancelled.");
|
|
479
|
+
process.exit(0);
|
|
319
480
|
}
|
|
320
|
-
|
|
321
|
-
preset = await getPreset(presetSlug);
|
|
481
|
+
target = resolveTarget("custom", customDir, customFile);
|
|
322
482
|
} else {
|
|
323
|
-
|
|
483
|
+
target = BUILTIN_TARGETS[targetChoice];
|
|
324
484
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
|
|
485
|
+
} else {
|
|
486
|
+
p.log.info(`Target: ${target.description}`);
|
|
487
|
+
}
|
|
488
|
+
const presets = await listPresets();
|
|
489
|
+
if (presets.length === 0) {
|
|
490
|
+
p.cancel("No presets available.");
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
const presetSlug = await p.select({
|
|
494
|
+
message: "Choose a preset",
|
|
495
|
+
options: presets.map((pr) => ({
|
|
496
|
+
value: pr.slug,
|
|
497
|
+
label: pr.name,
|
|
498
|
+
hint: `${pr.agentCount} agents, ${pr.skillCount} skills`
|
|
499
|
+
}))
|
|
500
|
+
});
|
|
501
|
+
if (p.isCancel(presetSlug)) {
|
|
502
|
+
p.cancel("Operation cancelled.");
|
|
503
|
+
process.exit(0);
|
|
504
|
+
}
|
|
505
|
+
const preset = await getPreset(presetSlug);
|
|
506
|
+
const allAgents = await listAgents();
|
|
507
|
+
const allSkillSlugs = (await listSkills()).map((s2) => s2.slug);
|
|
508
|
+
const nonOrchestratorAgents = allAgents.filter((a) => a.slug !== "orchestrator");
|
|
509
|
+
const presetAgentSet = new Set(preset.agents);
|
|
510
|
+
const selectedAgents = await p.multiselect({
|
|
511
|
+
message: "Select agents",
|
|
512
|
+
options: nonOrchestratorAgents.map((a) => ({
|
|
513
|
+
value: a.slug,
|
|
514
|
+
label: a.name,
|
|
515
|
+
hint: a.description
|
|
516
|
+
})),
|
|
517
|
+
initialValues: nonOrchestratorAgents.filter((a) => presetAgentSet.has(a.slug)).map((a) => a.slug),
|
|
518
|
+
required: true
|
|
519
|
+
});
|
|
520
|
+
if (p.isCancel(selectedAgents)) {
|
|
521
|
+
p.cancel("Operation cancelled.");
|
|
522
|
+
process.exit(0);
|
|
523
|
+
}
|
|
524
|
+
const agentSlugs = ["orchestrator", ...selectedAgents];
|
|
525
|
+
const skillOptions = computeAvailableSkills(preset, selectedAgents, allAgents, allSkillSlugs);
|
|
526
|
+
const selectedSkills = await p.multiselect({
|
|
527
|
+
message: "Select skills",
|
|
528
|
+
options: skillOptions.map((s2) => ({
|
|
529
|
+
value: s2.slug,
|
|
530
|
+
label: s2.slug,
|
|
531
|
+
hint: s2.preSelected ? "recommended" : void 0
|
|
532
|
+
})),
|
|
533
|
+
initialValues: skillOptions.filter((s2) => s2.preSelected).map((s2) => s2.slug),
|
|
534
|
+
required: false
|
|
535
|
+
});
|
|
536
|
+
if (p.isCancel(selectedSkills)) {
|
|
537
|
+
p.cancel("Operation cancelled.");
|
|
538
|
+
process.exit(0);
|
|
539
|
+
}
|
|
540
|
+
const skillSlugs = selectedSkills;
|
|
541
|
+
const confirmed = await p.confirm({
|
|
542
|
+
message: `Scaffold with ${agentSlugs.length} agents and ${skillSlugs.length} skills?`
|
|
543
|
+
});
|
|
544
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
545
|
+
p.cancel("Operation cancelled.");
|
|
546
|
+
process.exit(0);
|
|
547
|
+
}
|
|
548
|
+
const s = p.spinner();
|
|
549
|
+
s.start("Generating project files...");
|
|
550
|
+
await generateAndWrite(preset, agentSlugs, skillSlugs, target);
|
|
551
|
+
saveConfig(target);
|
|
552
|
+
s.stop("Project files generated.");
|
|
553
|
+
p.outro(pc3.green(`Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), ${target.contextFile} ready.`));
|
|
554
|
+
}
|
|
555
|
+
async function nonInteractiveInit(presetSlug, opts) {
|
|
556
|
+
const target = opts.target;
|
|
557
|
+
const preset = await getPreset(presetSlug);
|
|
558
|
+
const allAgents = await listAgents();
|
|
559
|
+
let agentSlugs = [...preset.agents];
|
|
560
|
+
if (opts.addAgent) {
|
|
561
|
+
for (const slug of opts.addAgent) {
|
|
562
|
+
if (!agentSlugs.includes(slug)) agentSlugs.push(slug);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (opts.removeAgent) {
|
|
566
|
+
agentSlugs = agentSlugs.filter(
|
|
567
|
+
(s) => s === "orchestrator" || !opts.removeAgent.includes(s)
|
|
333
568
|
);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const fm = data;
|
|
342
|
-
agentInfos.push({
|
|
343
|
-
slug,
|
|
344
|
-
name: fm.name || slug,
|
|
345
|
-
role: fm.role || ""
|
|
346
|
-
});
|
|
347
|
-
console.log(pc3.green(` \u2713 Agent: ${slug}`));
|
|
348
|
-
} else {
|
|
349
|
-
console.log(pc3.yellow(` \u26A0 Agent "${slug}" skipped: ${result.reason}`));
|
|
350
|
-
}
|
|
569
|
+
}
|
|
570
|
+
const selectedNonOrch = agentSlugs.filter((s) => s !== "orchestrator");
|
|
571
|
+
const linkedToSelected = /* @__PURE__ */ new Set();
|
|
572
|
+
const linkedToRemoved = /* @__PURE__ */ new Set();
|
|
573
|
+
for (const agent of allAgents) {
|
|
574
|
+
if (selectedNonOrch.includes(agent.slug)) {
|
|
575
|
+
for (const sk of agent.skills) linkedToSelected.add(sk);
|
|
351
576
|
}
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
const result = skillResults[i];
|
|
355
|
-
if (result.status === "fulfilled") {
|
|
356
|
-
writeSkill(slug, result.value.rawContent);
|
|
357
|
-
console.log(pc3.green(` \u2713 Skill: ${slug}`));
|
|
358
|
-
} else {
|
|
359
|
-
console.log(pc3.yellow(` \u26A0 Skill "${slug}" skipped: ${result.reason}`));
|
|
360
|
-
}
|
|
577
|
+
if (opts.removeAgent?.includes(agent.slug)) {
|
|
578
|
+
for (const sk of agent.skills) linkedToRemoved.add(sk);
|
|
361
579
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
580
|
+
}
|
|
581
|
+
const orphanSkills = /* @__PURE__ */ new Set();
|
|
582
|
+
for (const sk of linkedToRemoved) {
|
|
583
|
+
if (!linkedToSelected.has(sk)) orphanSkills.add(sk);
|
|
584
|
+
}
|
|
585
|
+
let skillSlugs = preset.skills.filter((s) => !orphanSkills.has(s));
|
|
586
|
+
if (opts.addSkill) {
|
|
587
|
+
for (const slug of opts.addSkill) {
|
|
588
|
+
if (!skillSlugs.includes(slug)) skillSlugs.push(slug);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (opts.removeSkill) {
|
|
592
|
+
skillSlugs = skillSlugs.filter((s) => !opts.removeSkill.includes(s));
|
|
593
|
+
}
|
|
594
|
+
console.log(pc3.bold(pc3.cyan(`
|
|
595
|
+
Initializing preset "${preset.name}"...
|
|
596
|
+
`)));
|
|
597
|
+
await generateAndWrite(preset, agentSlugs, skillSlugs, target);
|
|
598
|
+
saveConfig(target);
|
|
599
|
+
console.log(
|
|
600
|
+
pc3.bold(
|
|
601
|
+
pc3.cyan(
|
|
602
|
+
`
|
|
603
|
+
Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), ${target.contextFile} ready.
|
|
372
604
|
`
|
|
373
|
-
)
|
|
374
605
|
)
|
|
606
|
+
)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
async function generateAndWrite(preset, agentSlugs, skillSlugs, target) {
|
|
610
|
+
const agentResults = await Promise.allSettled(
|
|
611
|
+
agentSlugs.map((slug) => getAgent(slug))
|
|
612
|
+
);
|
|
613
|
+
const skillResults = await Promise.allSettled(
|
|
614
|
+
skillSlugs.map((slug) => getSkill(slug))
|
|
615
|
+
);
|
|
616
|
+
const agentInfos = [];
|
|
617
|
+
const agentsWithSkills = [];
|
|
618
|
+
let orchestratorTemplate = null;
|
|
619
|
+
for (let i = 0; i < agentSlugs.length; i++) {
|
|
620
|
+
const slug = agentSlugs[i];
|
|
621
|
+
const result = agentResults[i];
|
|
622
|
+
if (result.status === "fulfilled") {
|
|
623
|
+
const { data } = matter3(result.value.rawContent);
|
|
624
|
+
const fm = data;
|
|
625
|
+
if (slug === "orchestrator") {
|
|
626
|
+
orchestratorTemplate = result.value.rawContent;
|
|
627
|
+
} else {
|
|
628
|
+
writeAgent(target, slug, result.value.rawContent);
|
|
629
|
+
console.log(pc3.green(` \u2713 Agent: ${slug}`));
|
|
630
|
+
}
|
|
631
|
+
agentInfos.push({
|
|
632
|
+
slug,
|
|
633
|
+
name: fm.name || slug,
|
|
634
|
+
role: fm.role || "",
|
|
635
|
+
description: fm.description || ""
|
|
636
|
+
});
|
|
637
|
+
agentsWithSkills.push({
|
|
638
|
+
slug,
|
|
639
|
+
name: fm.name || slug,
|
|
640
|
+
description: fm.description || "",
|
|
641
|
+
skills: Array.isArray(fm.skills) ? fm.skills : []
|
|
642
|
+
});
|
|
643
|
+
} else {
|
|
644
|
+
console.log(pc3.yellow(` \u26A0 Agent "${slug}" skipped: ${result.reason}`));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (orchestratorTemplate) {
|
|
648
|
+
const orchestratorContent = generateOrchestrator(
|
|
649
|
+
orchestratorTemplate,
|
|
650
|
+
agentsWithSkills,
|
|
651
|
+
skillSlugs
|
|
375
652
|
);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
653
|
+
writeOrchestrator(target, orchestratorContent);
|
|
654
|
+
console.log(pc3.green(` \u2713 ${target.orchestratorFile} generated`));
|
|
655
|
+
}
|
|
656
|
+
for (let i = 0; i < skillSlugs.length; i++) {
|
|
657
|
+
const slug = skillSlugs[i];
|
|
658
|
+
const result = skillResults[i];
|
|
659
|
+
if (result.status === "fulfilled") {
|
|
660
|
+
writeSkill(target, slug, result.value.rawContent);
|
|
661
|
+
console.log(pc3.green(` \u2713 Skill: ${slug}`));
|
|
381
662
|
} else {
|
|
382
|
-
console.
|
|
663
|
+
console.log(pc3.yellow(` \u26A0 Skill "${slug}" skipped: ${result.reason}`));
|
|
383
664
|
}
|
|
384
|
-
process.exit(1);
|
|
385
665
|
}
|
|
666
|
+
const contextContent = generateContextFile(preset, agentInfos, target, skillSlugs);
|
|
667
|
+
writeContextFile(target, contextContent);
|
|
668
|
+
console.log(pc3.green(` \u2713 ${target.contextFile} generated`));
|
|
669
|
+
}
|
|
670
|
+
function computeAvailableSkills(preset, selectedAgentSlugs, allAgents, allSkillSlugs) {
|
|
671
|
+
const linkedToSelected = /* @__PURE__ */ new Set();
|
|
672
|
+
for (const agent of allAgents) {
|
|
673
|
+
if (selectedAgentSlugs.includes(agent.slug)) {
|
|
674
|
+
for (const sk of agent.skills) linkedToSelected.add(sk);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const linkedToAny = /* @__PURE__ */ new Set();
|
|
678
|
+
for (const agent of allAgents) {
|
|
679
|
+
for (const sk of agent.skills) linkedToAny.add(sk);
|
|
680
|
+
}
|
|
681
|
+
const presetSkillSet = new Set(preset.skills);
|
|
682
|
+
return allSkillSlugs.map((slug) => {
|
|
683
|
+
const preSelected = linkedToSelected.has(slug) || presetSkillSet.has(slug) && !linkedToAny.has(slug);
|
|
684
|
+
return { slug, preSelected };
|
|
685
|
+
});
|
|
386
686
|
}
|
|
387
687
|
|
|
388
688
|
// src/index.ts
|
|
689
|
+
var require2 = createRequire(import.meta.url);
|
|
690
|
+
var { version } = require2("../package.json");
|
|
389
691
|
var program = new Command();
|
|
390
|
-
program.name("loom").description("Integrate Loom library (agents, skills, presets) into your project").version(
|
|
692
|
+
program.name("loom").description("Integrate Loom library (agents, skills, presets) into your project").version(version);
|
|
391
693
|
program.command("list").description("List available agents, skills, and presets").argument("[type]", "Filter by type: agents, skills, or presets").action(async (type) => {
|
|
392
694
|
await listCommand(type);
|
|
393
695
|
});
|
|
394
|
-
program.command("add").description("Download an agent or skill from the library").argument("<type>", "Type: agent or skill").argument("<slug>", "Slug of the agent or skill").action(async (type, slug) => {
|
|
395
|
-
|
|
696
|
+
program.command("add").description("Download an agent or skill from the library").argument("<type>", "Type: agent or skill").argument("<slug>", "Slug of the agent or skill").option("--target <name>", `Output target: ${[...listTargetNames(), "custom"].join(", ")}`, DEFAULT_TARGET).option("--target-dir <dir>", "Custom target directory").option("--context-file <file>", "Custom context file name").action(async (type, slug, opts) => {
|
|
697
|
+
const savedConfig = loadConfig();
|
|
698
|
+
const target = opts.target !== DEFAULT_TARGET || opts.targetDir || opts.contextFile ? resolveTarget(opts.target, opts.targetDir, opts.contextFile) : savedConfig ?? BUILTIN_TARGETS[DEFAULT_TARGET];
|
|
699
|
+
await addCommand(type, slug, target);
|
|
396
700
|
});
|
|
397
|
-
program.command("init").description("Initialize a project with a preset (agents + skills +
|
|
398
|
-
|
|
701
|
+
program.command("init").description("Initialize a project with a preset (agents + skills + context file)").argument("[preset]", "Preset slug (interactive if omitted)").option("--add-agent <slugs...>", "Add extra agents").option("--remove-agent <slugs...>", "Remove agents from preset").option("--add-skill <slugs...>", "Add extra skills").option("--remove-skill <slugs...>", "Remove skills from preset").option("--claude", "Use Claude Code target (.claude/ + CLAUDE.md)").option("--cursor", "Use Cursor target (.cursor/ + .cursorrules)").option("--target <name>", `Output target: ${[...listTargetNames(), "custom"].join(", ")}`).option("--target-dir <dir>", "Custom target directory").option("--context-file <file>", "Custom context file name").action(async (preset, opts) => {
|
|
702
|
+
let target;
|
|
703
|
+
let targetExplicit = false;
|
|
704
|
+
if (opts.claude) {
|
|
705
|
+
target = BUILTIN_TARGETS["claude-code"];
|
|
706
|
+
targetExplicit = true;
|
|
707
|
+
} else if (opts.cursor) {
|
|
708
|
+
target = BUILTIN_TARGETS["cursor"];
|
|
709
|
+
targetExplicit = true;
|
|
710
|
+
} else if (opts.target) {
|
|
711
|
+
target = resolveTarget(
|
|
712
|
+
opts.target,
|
|
713
|
+
opts.targetDir,
|
|
714
|
+
opts.contextFile
|
|
715
|
+
);
|
|
716
|
+
targetExplicit = true;
|
|
717
|
+
} else {
|
|
718
|
+
target = BUILTIN_TARGETS[DEFAULT_TARGET];
|
|
719
|
+
}
|
|
720
|
+
await initCommand(preset, {
|
|
721
|
+
addAgent: opts.addAgent,
|
|
722
|
+
removeAgent: opts.removeAgent,
|
|
723
|
+
addSkill: opts.addSkill,
|
|
724
|
+
removeSkill: opts.removeSkill,
|
|
725
|
+
target,
|
|
726
|
+
targetExplicit
|
|
727
|
+
});
|
|
399
728
|
});
|
|
400
729
|
program.parse();
|