@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.
Files changed (47) hide show
  1. package/README.md +82 -16
  2. package/data/agents/backend/AGENT.md +35 -3
  3. package/data/agents/database/AGENT.md +16 -4
  4. package/data/agents/frontend/AGENT.md +39 -5
  5. package/data/agents/marketing/AGENT.md +19 -2
  6. package/data/agents/orchestrator/AGENT.md +3 -21
  7. package/data/agents/performance/AGENT.md +9 -2
  8. package/data/agents/review-qa/AGENT.md +7 -3
  9. package/data/agents/security/AGENT.md +10 -4
  10. package/data/agents/tests/AGENT.md +11 -2
  11. package/data/agents/ux-ui/AGENT.md +16 -3
  12. package/data/presets/api-backend.yaml +2 -11
  13. package/data/presets/chrome-extension.yaml +2 -11
  14. package/data/presets/cli-tool.yaml +2 -10
  15. package/data/presets/e-commerce.yaml +2 -14
  16. package/data/presets/expo-mobile.yaml +2 -12
  17. package/data/presets/fullstack-auth.yaml +2 -14
  18. package/data/presets/landing-page.yaml +2 -11
  19. package/data/presets/mvp-lean.yaml +2 -12
  20. package/data/presets/saas-default.yaml +5 -14
  21. package/data/presets/saas-full.yaml +58 -0
  22. package/data/skills/api-design/SKILL.md +43 -2
  23. package/data/skills/auth-rbac/SKILL.md +179 -0
  24. package/data/skills/better-auth-patterns/SKILL.md +212 -0
  25. package/data/skills/chrome-extension-patterns/SKILL.md +13 -6
  26. package/data/skills/cli-development/SKILL.md +11 -3
  27. package/data/skills/drizzle-patterns/SKILL.md +166 -0
  28. package/data/skills/env-validation/SKILL.md +142 -0
  29. package/data/skills/form-validation/SKILL.md +169 -0
  30. package/data/skills/hero-copywriting/SKILL.md +12 -4
  31. package/data/skills/i18n-patterns/SKILL.md +176 -0
  32. package/data/skills/layered-architecture/SKILL.md +131 -0
  33. package/data/skills/nextjs-conventions/SKILL.md +46 -7
  34. package/data/skills/react-native-patterns/SKILL.md +10 -8
  35. package/data/skills/react-query-patterns/SKILL.md +193 -0
  36. package/data/skills/resend-email/SKILL.md +181 -0
  37. package/data/skills/seo-optimization/SKILL.md +10 -2
  38. package/data/skills/server-actions-patterns/SKILL.md +156 -0
  39. package/data/skills/shadcn-ui/SKILL.md +46 -12
  40. package/data/skills/stripe-integration/SKILL.md +11 -3
  41. package/data/skills/supabase-patterns/SKILL.md +23 -6
  42. package/data/skills/table-pagination/SKILL.md +224 -0
  43. package/data/skills/tailwind-patterns/SKILL.md +12 -2
  44. package/data/skills/testing-patterns/SKILL.md +203 -0
  45. package/data/skills/ui-ux-guidelines/SKILL.md +10 -5
  46. package/dist/index.js +451 -122
  47. 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
  }
@@ -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, CLAUDE_DIR, "agents");
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, `${slug}.md`);
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, CLAUDE_DIR, "skills");
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, `${slug}.md`);
194
+ const filePath = path2.join(dir, "SKILL.md");
193
195
  fs2.writeFileSync(filePath, content, "utf-8");
194
196
  return filePath;
195
197
  }
196
- function writeClaudeMd(content, cwd = process.cwd()) {
197
- const filePath = path2.join(cwd, "CLAUDE.md");
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 matter2 from "gray-matter";
246
+ import * as p from "@clack/prompts";
247
+ import matter3 from "gray-matter";
239
248
 
240
249
  // src/lib/generator.ts
241
- function generateClaudeMd(preset, agents) {
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.claudemd.projectDescription);
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
- if (preset.constitution.stack.length > 0) {
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
- if (agents.length > 0) {
276
- lines.push("## Agents");
277
- for (const agent of agents) {
278
- lines.push(`- **${agent.slug}**: ${agent.name} \u2014 ${agent.role}`);
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
- if (preset.claudemd.orchestratorRef) {
283
- lines.push("## Orchestrator");
284
- lines.push(preset.claudemd.orchestratorRef);
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
- 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
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
- 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
- });
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
- 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);
477
+ if (p.isCancel(customFile)) {
478
+ p.cancel("Operation cancelled.");
479
+ process.exit(0);
319
480
  }
320
- presetSlug = presets[index].slug;
321
- preset = await getPreset(presetSlug);
481
+ target = resolveTarget("custom", customDir, customFile);
322
482
  } else {
323
- preset = await getPreset(presetSlug);
483
+ target = BUILTIN_TARGETS[targetChoice];
324
484
  }
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))
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
- 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
- }
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
- 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
- }
577
+ if (opts.removeAgent?.includes(agent.slug)) {
578
+ for (const sk of agent.skills) linkedToRemoved.add(sk);
361
579
  }
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.
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
- } catch (error) {
377
- if (error instanceof Error) {
378
- console.error(pc3.red(`
379
- Error: ${error.message}
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.error(pc3.red("\n An unknown error occurred.\n"));
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("0.1.0");
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
- await addCommand(type, slug);
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 + CLAUDE.md)").argument("[preset]", "Preset slug (interactive if omitted)").action(async (preset) => {
398
- await initCommand(preset);
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();