@folpe/loom 0.2.0 → 0.3.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.
Files changed (45) hide show
  1. package/README.md +82 -16
  2. package/data/agents/backend/AGENT.md +12 -0
  3. package/data/agents/database/AGENT.md +3 -0
  4. package/data/agents/frontend/AGENT.md +10 -0
  5. package/data/agents/marketing/AGENT.md +3 -0
  6. package/data/agents/orchestrator/AGENT.md +1 -19
  7. package/data/agents/security/AGENT.md +3 -0
  8. package/data/agents/tests/AGENT.md +2 -0
  9. package/data/agents/ux-ui/AGENT.md +5 -0
  10. package/data/presets/api-backend.yaml +0 -3
  11. package/data/presets/chrome-extension.yaml +0 -3
  12. package/data/presets/cli-tool.yaml +0 -3
  13. package/data/presets/e-commerce.yaml +0 -3
  14. package/data/presets/expo-mobile.yaml +0 -3
  15. package/data/presets/fullstack-auth.yaml +0 -3
  16. package/data/presets/landing-page.yaml +0 -3
  17. package/data/presets/mvp-lean.yaml +0 -3
  18. package/data/presets/saas-default.yaml +3 -4
  19. package/data/presets/saas-full.yaml +71 -0
  20. package/data/skills/api-design/SKILL.md +43 -2
  21. package/data/skills/auth-rbac/SKILL.md +179 -0
  22. package/data/skills/better-auth-patterns/SKILL.md +212 -0
  23. package/data/skills/chrome-extension-patterns/SKILL.md +13 -6
  24. package/data/skills/cli-development/SKILL.md +11 -3
  25. package/data/skills/drizzle-patterns/SKILL.md +166 -0
  26. package/data/skills/env-validation/SKILL.md +142 -0
  27. package/data/skills/form-validation/SKILL.md +169 -0
  28. package/data/skills/hero-copywriting/SKILL.md +12 -4
  29. package/data/skills/i18n-patterns/SKILL.md +176 -0
  30. package/data/skills/layered-architecture/SKILL.md +131 -0
  31. package/data/skills/nextjs-conventions/SKILL.md +46 -7
  32. package/data/skills/react-native-patterns/SKILL.md +10 -8
  33. package/data/skills/react-query-patterns/SKILL.md +193 -0
  34. package/data/skills/resend-email/SKILL.md +181 -0
  35. package/data/skills/seo-optimization/SKILL.md +10 -2
  36. package/data/skills/server-actions-patterns/SKILL.md +156 -0
  37. package/data/skills/shadcn-ui/SKILL.md +46 -12
  38. package/data/skills/stripe-integration/SKILL.md +11 -3
  39. package/data/skills/supabase-patterns/SKILL.md +23 -6
  40. package/data/skills/table-pagination/SKILL.md +224 -0
  41. package/data/skills/tailwind-patterns/SKILL.md +12 -2
  42. package/data/skills/testing-patterns/SKILL.md +203 -0
  43. package/data/skills/ui-ux-guidelines/SKILL.md +10 -5
  44. package/dist/index.js +254 -100
  45. 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: data.name || slug,
37
- description: data.description || "",
38
- role: data.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 p of presets) {
150
- const meta = pc.dim(`(${p.agentCount} agents, ${p.skillCount} skills)`);
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(p.slug), 30)} ${padEnd(p.name, 25)} ${meta}`
155
+ ` ${padEnd(pc.green(p2.slug), 30)} ${padEnd(p2.name, 25)} ${meta}`
153
156
  );
154
157
  }
155
158
  }
@@ -193,6 +196,12 @@ function writeSkill(slug, content, cwd = process.cwd()) {
193
196
  fs2.writeFileSync(filePath, content, "utf-8");
194
197
  return filePath;
195
198
  }
199
+ function writeOrchestrator(content, cwd = process.cwd()) {
200
+ const filePath = path2.join(cwd, CLAUDE_DIR, "orchestrator.md");
201
+ ensureDir(path2.dirname(filePath));
202
+ fs2.writeFileSync(filePath, content, "utf-8");
203
+ return filePath;
204
+ }
196
205
  function writeClaudeMd(content, cwd = process.cwd()) {
197
206
  const filePath = path2.join(cwd, "CLAUDE.md");
198
207
  fs2.writeFileSync(filePath, content, "utf-8");
@@ -235,9 +244,11 @@ async function addCommand(type, slug) {
235
244
 
236
245
  // src/commands/init.ts
237
246
  import pc3 from "picocolors";
238
- import matter2 from "gray-matter";
247
+ import * as p from "@clack/prompts";
248
+ import matter3 from "gray-matter";
239
249
 
240
250
  // src/lib/generator.ts
251
+ import matter2 from "gray-matter";
241
252
  function generateClaudeMd(preset, agents) {
242
253
  const lines = [];
243
254
  lines.push(`# ${preset.name}`);
@@ -246,8 +257,8 @@ function generateClaudeMd(preset, agents) {
246
257
  lines.push("");
247
258
  if (preset.constitution.principles.length > 0) {
248
259
  lines.push("## Principles");
249
- for (const p of preset.constitution.principles) {
250
- lines.push(`- ${p}`);
260
+ for (const p2 of preset.constitution.principles) {
261
+ lines.push(`- ${p2}`);
251
262
  }
252
263
  lines.push("");
253
264
  }
@@ -279,100 +290,44 @@ function generateClaudeMd(preset, agents) {
279
290
  }
280
291
  lines.push("");
281
292
  }
282
- if (preset.claudemd.orchestratorRef) {
283
- lines.push("## Orchestrator");
284
- lines.push(preset.claudemd.orchestratorRef);
285
- lines.push("");
286
- }
293
+ lines.push("## Orchestrator");
294
+ lines.push("");
295
+ lines.push("Use the orchestrator agent (`.claude/orchestrator.md`) as the main coordinator. It will analyze tasks, break them into subtasks, and delegate to the appropriate specialized agents listed above.");
296
+ lines.push("");
287
297
  return lines.join("\n");
288
298
  }
299
+ function generateOrchestrator(templateContent, agents, presetSkills) {
300
+ const { data: frontmatter, content } = matter2(templateContent);
301
+ const rules = [];
302
+ const delegatesTo = [];
303
+ for (const agent of agents) {
304
+ if (agent.slug === "orchestrator") continue;
305
+ delegatesTo.push(agent.slug);
306
+ const relevantSkills = agent.skills.filter((s) => presetSkills.includes(s));
307
+ let line = `- **${agent.slug}**: ${agent.description}`;
308
+ if (relevantSkills.length > 0) {
309
+ line += `. Skills: ${relevantSkills.join(", ")}`;
310
+ }
311
+ rules.push(line);
312
+ }
313
+ const newFrontmatter = { ...frontmatter, "delegates-to": delegatesTo };
314
+ const newContent = content.replace("{{DELEGATION_RULES}}", rules.join("\n"));
315
+ return matter2.stringify(newContent, newFrontmatter);
316
+ }
289
317
 
290
318
  // src/commands/init.ts
291
- async function initCommand(presetSlug) {
319
+ async function initCommand(presetSlug, opts = {}) {
292
320
  try {
293
- let preset;
294
- if (!presetSlug) {
295
- const presets = await listPresets();
296
- if (presets.length === 0) {
297
- console.error(pc3.red("\n No presets available.\n"));
298
- process.exit(1);
299
- }
300
- console.log(pc3.bold(pc3.cyan("\n Available presets:\n")));
301
- presets.forEach((p, i) => {
302
- console.log(` ${pc3.bold(String(i + 1))}. ${pc3.green(p.slug)} \u2014 ${p.description}`);
303
- });
304
- const { createInterface } = await import("readline");
305
- const rl = createInterface({
306
- input: process.stdin,
307
- output: process.stdout
308
- });
309
- const answer = await new Promise((resolve) => {
310
- rl.question(pc3.cyan("\n Choose a preset (number): "), (ans) => {
311
- rl.close();
312
- resolve(ans.trim());
313
- });
314
- });
315
- const index = parseInt(answer, 10) - 1;
316
- if (isNaN(index) || index < 0 || index >= presets.length) {
317
- console.error(pc3.red("\n Invalid selection.\n"));
318
- process.exit(1);
319
- }
320
- presetSlug = presets[index].slug;
321
- preset = await getPreset(presetSlug);
322
- } else {
323
- preset = await getPreset(presetSlug);
324
- }
325
- console.log(pc3.bold(pc3.cyan(`
326
- Initializing preset "${preset.name}"...
327
- `)));
328
- const agentResults = await Promise.allSettled(
329
- preset.agents.map((slug) => getAgent(slug))
330
- );
331
- const skillResults = await Promise.allSettled(
332
- preset.skills.map((slug) => getSkill(slug))
333
- );
334
- const agentInfos = [];
335
- for (let i = 0; i < preset.agents.length; i++) {
336
- const slug = preset.agents[i];
337
- const result = agentResults[i];
338
- if (result.status === "fulfilled") {
339
- writeAgent(slug, result.value.rawContent);
340
- const { data } = matter2(result.value.rawContent);
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
- }
321
+ const hasFlags = !!(opts.addAgent || opts.removeAgent || opts.addSkill || opts.removeSkill);
322
+ if (!presetSlug && hasFlags) {
323
+ console.error(pc3.red("\n Error: flags require a preset argument. Usage: loom init <preset> [flags]\n"));
324
+ process.exit(1);
351
325
  }
352
- for (let i = 0; i < preset.skills.length; i++) {
353
- const slug = preset.skills[i];
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
- }
326
+ if (!presetSlug && !hasFlags) {
327
+ await interactiveInit();
328
+ } else {
329
+ await nonInteractiveInit(presetSlug, opts);
361
330
  }
362
- const claudeContent = generateClaudeMd(preset, agentInfos);
363
- writeClaudeMd(claudeContent);
364
- console.log(pc3.green(` \u2713 CLAUDE.md generated`));
365
- const agentOk = agentResults.filter((r) => r.status === "fulfilled").length;
366
- const skillOk = skillResults.filter((r) => r.status === "fulfilled").length;
367
- console.log(
368
- pc3.bold(
369
- pc3.cyan(
370
- `
371
- Done! ${agentOk} agent(s), ${skillOk} skill(s), CLAUDE.md ready.
372
- `
373
- )
374
- )
375
- );
376
331
  } catch (error) {
377
332
  if (error instanceof Error) {
378
333
  console.error(pc3.red(`
@@ -384,17 +339,216 @@ async function initCommand(presetSlug) {
384
339
  process.exit(1);
385
340
  }
386
341
  }
342
+ async function interactiveInit() {
343
+ p.intro(pc3.bgCyan(pc3.black(" loom init ")));
344
+ const presets = await listPresets();
345
+ if (presets.length === 0) {
346
+ p.cancel("No presets available.");
347
+ process.exit(1);
348
+ }
349
+ const presetSlug = await p.select({
350
+ message: "Choose a preset",
351
+ options: presets.map((pr) => ({
352
+ value: pr.slug,
353
+ label: pr.name,
354
+ hint: `${pr.agentCount} agents, ${pr.skillCount} skills`
355
+ }))
356
+ });
357
+ if (p.isCancel(presetSlug)) {
358
+ p.cancel("Operation cancelled.");
359
+ process.exit(0);
360
+ }
361
+ const preset = await getPreset(presetSlug);
362
+ const allAgents = await listAgents();
363
+ const allSkillSlugs = (await listSkills()).map((s2) => s2.slug);
364
+ const nonOrchestratorAgents = allAgents.filter((a) => a.slug !== "orchestrator");
365
+ const presetAgentSet = new Set(preset.agents);
366
+ const selectedAgents = await p.multiselect({
367
+ message: "Select agents",
368
+ options: nonOrchestratorAgents.map((a) => ({
369
+ value: a.slug,
370
+ label: a.name,
371
+ hint: a.description
372
+ })),
373
+ initialValues: nonOrchestratorAgents.filter((a) => presetAgentSet.has(a.slug)).map((a) => a.slug),
374
+ required: true
375
+ });
376
+ if (p.isCancel(selectedAgents)) {
377
+ p.cancel("Operation cancelled.");
378
+ process.exit(0);
379
+ }
380
+ const agentSlugs = ["orchestrator", ...selectedAgents];
381
+ const skillOptions = computeAvailableSkills(preset, selectedAgents, allAgents, allSkillSlugs);
382
+ const selectedSkills = await p.multiselect({
383
+ message: "Select skills",
384
+ options: skillOptions.map((s2) => ({
385
+ value: s2.slug,
386
+ label: s2.slug,
387
+ hint: s2.preSelected ? "recommended" : void 0
388
+ })),
389
+ initialValues: skillOptions.filter((s2) => s2.preSelected).map((s2) => s2.slug),
390
+ required: false
391
+ });
392
+ if (p.isCancel(selectedSkills)) {
393
+ p.cancel("Operation cancelled.");
394
+ process.exit(0);
395
+ }
396
+ const skillSlugs = selectedSkills;
397
+ const confirmed = await p.confirm({
398
+ message: `Scaffold with ${agentSlugs.length} agents and ${skillSlugs.length} skills?`
399
+ });
400
+ if (p.isCancel(confirmed) || !confirmed) {
401
+ p.cancel("Operation cancelled.");
402
+ process.exit(0);
403
+ }
404
+ const s = p.spinner();
405
+ s.start("Generating project files...");
406
+ await generateAndWrite(preset, agentSlugs, skillSlugs);
407
+ s.stop("Project files generated.");
408
+ p.outro(pc3.green(`Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), CLAUDE.md ready.`));
409
+ }
410
+ async function nonInteractiveInit(presetSlug, opts) {
411
+ const preset = await getPreset(presetSlug);
412
+ const allAgents = await listAgents();
413
+ let agentSlugs = [...preset.agents];
414
+ if (opts.addAgent) {
415
+ for (const slug of opts.addAgent) {
416
+ if (!agentSlugs.includes(slug)) agentSlugs.push(slug);
417
+ }
418
+ }
419
+ if (opts.removeAgent) {
420
+ agentSlugs = agentSlugs.filter(
421
+ (s) => s === "orchestrator" || !opts.removeAgent.includes(s)
422
+ );
423
+ }
424
+ const selectedNonOrch = agentSlugs.filter((s) => s !== "orchestrator");
425
+ const linkedToSelected = /* @__PURE__ */ new Set();
426
+ const linkedToRemoved = /* @__PURE__ */ new Set();
427
+ for (const agent of allAgents) {
428
+ if (selectedNonOrch.includes(agent.slug)) {
429
+ for (const sk of agent.skills) linkedToSelected.add(sk);
430
+ }
431
+ if (opts.removeAgent?.includes(agent.slug)) {
432
+ for (const sk of agent.skills) linkedToRemoved.add(sk);
433
+ }
434
+ }
435
+ const orphanSkills = /* @__PURE__ */ new Set();
436
+ for (const sk of linkedToRemoved) {
437
+ if (!linkedToSelected.has(sk)) orphanSkills.add(sk);
438
+ }
439
+ let skillSlugs = preset.skills.filter((s) => !orphanSkills.has(s));
440
+ if (opts.addSkill) {
441
+ for (const slug of opts.addSkill) {
442
+ if (!skillSlugs.includes(slug)) skillSlugs.push(slug);
443
+ }
444
+ }
445
+ if (opts.removeSkill) {
446
+ skillSlugs = skillSlugs.filter((s) => !opts.removeSkill.includes(s));
447
+ }
448
+ console.log(pc3.bold(pc3.cyan(`
449
+ Initializing preset "${preset.name}"...
450
+ `)));
451
+ await generateAndWrite(preset, agentSlugs, skillSlugs);
452
+ console.log(
453
+ pc3.bold(
454
+ pc3.cyan(
455
+ `
456
+ Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), CLAUDE.md ready.
457
+ `
458
+ )
459
+ )
460
+ );
461
+ }
462
+ async function generateAndWrite(preset, agentSlugs, skillSlugs) {
463
+ const agentResults = await Promise.allSettled(
464
+ agentSlugs.map((slug) => getAgent(slug))
465
+ );
466
+ const skillResults = await Promise.allSettled(
467
+ skillSlugs.map((slug) => getSkill(slug))
468
+ );
469
+ const agentInfos = [];
470
+ const agentsWithSkills = [];
471
+ let orchestratorTemplate = null;
472
+ for (let i = 0; i < agentSlugs.length; i++) {
473
+ const slug = agentSlugs[i];
474
+ const result = agentResults[i];
475
+ if (result.status === "fulfilled") {
476
+ const { data } = matter3(result.value.rawContent);
477
+ const fm = data;
478
+ if (slug === "orchestrator") {
479
+ orchestratorTemplate = result.value.rawContent;
480
+ } else {
481
+ writeAgent(slug, result.value.rawContent);
482
+ console.log(pc3.green(` \u2713 Agent: ${slug}`));
483
+ }
484
+ agentInfos.push({
485
+ slug,
486
+ name: fm.name || slug,
487
+ role: fm.role || ""
488
+ });
489
+ agentsWithSkills.push({
490
+ slug,
491
+ name: fm.name || slug,
492
+ description: fm.description || "",
493
+ skills: Array.isArray(fm.skills) ? fm.skills : []
494
+ });
495
+ } else {
496
+ console.log(pc3.yellow(` \u26A0 Agent "${slug}" skipped: ${result.reason}`));
497
+ }
498
+ }
499
+ if (orchestratorTemplate) {
500
+ const orchestratorContent = generateOrchestrator(
501
+ orchestratorTemplate,
502
+ agentsWithSkills,
503
+ skillSlugs
504
+ );
505
+ writeOrchestrator(orchestratorContent);
506
+ console.log(pc3.green(` \u2713 orchestrator.md generated`));
507
+ }
508
+ for (let i = 0; i < skillSlugs.length; i++) {
509
+ const slug = skillSlugs[i];
510
+ const result = skillResults[i];
511
+ if (result.status === "fulfilled") {
512
+ writeSkill(slug, result.value.rawContent);
513
+ console.log(pc3.green(` \u2713 Skill: ${slug}`));
514
+ } else {
515
+ console.log(pc3.yellow(` \u26A0 Skill "${slug}" skipped: ${result.reason}`));
516
+ }
517
+ }
518
+ const claudeContent = generateClaudeMd(preset, agentInfos);
519
+ writeClaudeMd(claudeContent);
520
+ console.log(pc3.green(` \u2713 CLAUDE.md generated`));
521
+ }
522
+ function computeAvailableSkills(preset, selectedAgentSlugs, allAgents, allSkillSlugs) {
523
+ const linkedToSelected = /* @__PURE__ */ new Set();
524
+ for (const agent of allAgents) {
525
+ if (selectedAgentSlugs.includes(agent.slug)) {
526
+ for (const sk of agent.skills) linkedToSelected.add(sk);
527
+ }
528
+ }
529
+ const linkedToAny = /* @__PURE__ */ new Set();
530
+ for (const agent of allAgents) {
531
+ for (const sk of agent.skills) linkedToAny.add(sk);
532
+ }
533
+ const presetSkillSet = new Set(preset.skills);
534
+ return allSkillSlugs.map((slug) => {
535
+ const preSelected = linkedToSelected.has(slug) || presetSkillSet.has(slug) && !linkedToAny.has(slug);
536
+ return { slug, preSelected };
537
+ });
538
+ }
387
539
 
388
540
  // src/index.ts
541
+ var require2 = createRequire(import.meta.url);
542
+ var { version } = require2("../package.json");
389
543
  var program = new Command();
390
- program.name("loom").description("Integrate Loom library (agents, skills, presets) into your project").version("0.1.0");
544
+ program.name("loom").description("Integrate Loom library (agents, skills, presets) into your project").version(version);
391
545
  program.command("list").description("List available agents, skills, and presets").argument("[type]", "Filter by type: agents, skills, or presets").action(async (type) => {
392
546
  await listCommand(type);
393
547
  });
394
548
  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
549
  await addCommand(type, slug);
396
550
  });
397
- program.command("init").description("Initialize a project with a preset (agents + skills + CLAUDE.md)").argument("[preset]", "Preset slug (interactive if omitted)").action(async (preset) => {
398
- await initCommand(preset);
551
+ program.command("init").description("Initialize a project with a preset (agents + skills + CLAUDE.md)").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").action(async (preset, opts) => {
552
+ await initCommand(preset, opts);
399
553
  });
400
554
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@folpe/loom",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI to scaffold Claude Code projects with curated agents, skills, and presets",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,6 +30,7 @@
30
30
  "data"
31
31
  ],
32
32
  "dependencies": {
33
+ "@clack/prompts": "^1.0.1",
33
34
  "commander": "^13.1.0",
34
35
  "gray-matter": "^4.0.3",
35
36
  "picocolors": "^1.1.1",