@folpe/loom 0.3.0 → 1.0.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/dist/index.js CHANGED
@@ -23,6 +23,34 @@ function listFiles(dir) {
23
23
  if (!fs.existsSync(dir)) return [];
24
24
  return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile()).map((d) => d.name).sort();
25
25
  }
26
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
27
+ ".md",
28
+ ".ts",
29
+ ".js",
30
+ ".sh",
31
+ ".dot",
32
+ ".yaml",
33
+ ".yml",
34
+ ".json",
35
+ ".css",
36
+ ".html"
37
+ ]);
38
+ function walkDir(dir, base = "") {
39
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
40
+ const results = [];
41
+ for (const entry of entries) {
42
+ const rel = base ? `${base}/${entry.name}` : entry.name;
43
+ if (entry.isDirectory()) {
44
+ results.push(...walkDir(path.join(dir, entry.name), rel));
45
+ } else if (entry.isFile()) {
46
+ const ext = path.extname(entry.name).toLowerCase();
47
+ if (TEXT_EXTENSIONS.has(ext)) {
48
+ results.push(rel);
49
+ }
50
+ }
51
+ }
52
+ return results;
53
+ }
26
54
  async function listAgents() {
27
55
  const agentsDir = path.join(DATA_DIR, "agents");
28
56
  const slugs = listSubDirs(agentsDir);
@@ -94,10 +122,16 @@ async function getAgent(slug) {
94
122
  const raw = fs.readFileSync(filePath, "utf-8");
95
123
  return { slug, rawContent: raw };
96
124
  }
97
- async function getSkill(slug) {
98
- const filePath = path.join(DATA_DIR, "skills", slug, "SKILL.md");
99
- const raw = fs.readFileSync(filePath, "utf-8");
100
- return { slug, rawContent: raw };
125
+ async function getSkillWithFiles(slug) {
126
+ const skillDir = path.join(DATA_DIR, "skills", slug);
127
+ const mainPath = path.join(skillDir, "SKILL.md");
128
+ const mainContent = fs.readFileSync(mainPath, "utf-8");
129
+ const relativePaths = walkDir(skillDir);
130
+ const files = relativePaths.map((relativePath) => ({
131
+ relativePath,
132
+ content: fs.readFileSync(path.join(skillDir, relativePath), "utf-8")
133
+ }));
134
+ return { slug, mainContent, files };
101
135
  }
102
136
  async function getPreset(slug) {
103
137
  const filePath = path.join(DATA_DIR, "presets", `${slug}.yaml`);
@@ -106,6 +140,116 @@ async function getPreset(slug) {
106
140
  return { ...data, slug };
107
141
  }
108
142
 
143
+ // src/lib/local-library.ts
144
+ import fs2 from "fs";
145
+ import path2 from "path";
146
+ import os from "os";
147
+ var LIBRARY_DIR = path2.join(os.homedir(), ".loom", "library");
148
+ function ensureDir(dir) {
149
+ fs2.mkdirSync(dir, { recursive: true });
150
+ }
151
+ function saveLocalAgent(slug, content) {
152
+ const dir = path2.join(LIBRARY_DIR, "agents", slug);
153
+ ensureDir(dir);
154
+ const filePath = path2.join(dir, "AGENT.md");
155
+ fs2.writeFileSync(filePath, content, "utf-8");
156
+ return filePath;
157
+ }
158
+ function saveLocalSkill(slug, files) {
159
+ const dir = path2.join(LIBRARY_DIR, "skills", slug);
160
+ for (const file of files) {
161
+ const filePath = path2.join(dir, file.relativePath);
162
+ ensureDir(path2.dirname(filePath));
163
+ fs2.writeFileSync(filePath, file.content, "utf-8");
164
+ }
165
+ return dir;
166
+ }
167
+ function saveLocalPreset(slug, content) {
168
+ const dir = path2.join(LIBRARY_DIR, "presets");
169
+ ensureDir(dir);
170
+ const filePath = path2.join(dir, `${slug}.yaml`);
171
+ fs2.writeFileSync(filePath, content, "utf-8");
172
+ return filePath;
173
+ }
174
+ function getLocalAgent(slug) {
175
+ const filePath = path2.join(LIBRARY_DIR, "agents", slug, "AGENT.md");
176
+ try {
177
+ const raw = fs2.readFileSync(filePath, "utf-8");
178
+ return { slug, rawContent: raw };
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+ var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
184
+ ".md",
185
+ ".ts",
186
+ ".js",
187
+ ".sh",
188
+ ".dot",
189
+ ".yaml",
190
+ ".yml",
191
+ ".json",
192
+ ".css",
193
+ ".html"
194
+ ]);
195
+ function walkDir2(dir, base = "") {
196
+ let entries;
197
+ try {
198
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
199
+ } catch {
200
+ return [];
201
+ }
202
+ const results = [];
203
+ for (const entry of entries) {
204
+ const rel = base ? `${base}/${entry.name}` : entry.name;
205
+ if (entry.isDirectory()) {
206
+ results.push(...walkDir2(path2.join(dir, entry.name), rel));
207
+ } else if (entry.isFile()) {
208
+ const ext = path2.extname(entry.name).toLowerCase();
209
+ if (TEXT_EXTENSIONS2.has(ext)) {
210
+ results.push(rel);
211
+ }
212
+ }
213
+ }
214
+ return results;
215
+ }
216
+ function getLocalSkillWithFiles(slug) {
217
+ const dir = path2.join(LIBRARY_DIR, "skills", slug);
218
+ if (!fs2.existsSync(dir)) return null;
219
+ const relativePaths = walkDir2(dir);
220
+ if (relativePaths.length === 0) return null;
221
+ const files = relativePaths.map((relativePath) => ({
222
+ relativePath,
223
+ content: fs2.readFileSync(path2.join(dir, relativePath), "utf-8")
224
+ }));
225
+ return { slug, files };
226
+ }
227
+ function listSubDirs2(dir) {
228
+ try {
229
+ return fs2.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
230
+ } catch {
231
+ return [];
232
+ }
233
+ }
234
+ function listLocalResources() {
235
+ const items = [];
236
+ for (const slug of listSubDirs2(path2.join(LIBRARY_DIR, "agents"))) {
237
+ items.push({ slug, type: "agent" });
238
+ }
239
+ for (const slug of listSubDirs2(path2.join(LIBRARY_DIR, "skills"))) {
240
+ items.push({ slug, type: "skill" });
241
+ }
242
+ try {
243
+ const presetsDir = path2.join(LIBRARY_DIR, "presets");
244
+ const files = fs2.readdirSync(presetsDir).filter((f) => f.endsWith(".yaml"));
245
+ for (const f of files) {
246
+ items.push({ slug: f.replace(/\.yaml$/, ""), type: "preset" });
247
+ }
248
+ } catch {
249
+ }
250
+ return items;
251
+ }
252
+
109
253
  // src/commands/list.ts
110
254
  function truncate(str, max) {
111
255
  if (str.length <= max) return str;
@@ -116,6 +260,7 @@ function padEnd(str, len) {
116
260
  }
117
261
  async function listCommand(type) {
118
262
  try {
263
+ const bundledSlugs = /* @__PURE__ */ new Set();
119
264
  if (!type || type === "agents") {
120
265
  const agents = await listAgents();
121
266
  console.log(pc.bold(pc.cyan("\n Agents")));
@@ -124,6 +269,7 @@ async function listCommand(type) {
124
269
  console.log(pc.dim(" No agents found."));
125
270
  }
126
271
  for (const a of agents) {
272
+ bundledSlugs.add(`agent:${a.slug}`);
127
273
  console.log(
128
274
  ` ${padEnd(pc.green(a.slug), 30)} ${padEnd(a.name, 25)} ${pc.dim(truncate(a.description, 40))}`
129
275
  );
@@ -137,6 +283,7 @@ async function listCommand(type) {
137
283
  console.log(pc.dim(" No skills found."));
138
284
  }
139
285
  for (const s of skills) {
286
+ bundledSlugs.add(`skill:${s.slug}`);
140
287
  console.log(
141
288
  ` ${padEnd(pc.green(s.slug), 30)} ${padEnd(s.name, 25)} ${pc.dim(truncate(s.description, 40))}`
142
289
  );
@@ -150,12 +297,25 @@ async function listCommand(type) {
150
297
  console.log(pc.dim(" No presets found."));
151
298
  }
152
299
  for (const p2 of presets) {
300
+ bundledSlugs.add(`preset:${p2.slug}`);
153
301
  const meta = pc.dim(`(${p2.agentCount} agents, ${p2.skillCount} skills)`);
154
302
  console.log(
155
303
  ` ${padEnd(pc.green(p2.slug), 30)} ${padEnd(p2.name, 25)} ${meta}`
156
304
  );
157
305
  }
158
306
  }
307
+ const localItems = listLocalResources().filter(
308
+ (item) => !bundledSlugs.has(`${item.type}:${item.slug}`)
309
+ );
310
+ if (localItems.length > 0) {
311
+ console.log(pc.bold(pc.magenta("\n Installed (marketplace)")));
312
+ console.log(pc.dim(" " + "\u2500".repeat(60)));
313
+ for (const item of localItems) {
314
+ console.log(
315
+ ` ${padEnd(pc.green(item.slug), 30)} ${pc.dim(`[${item.type}]`)}`
316
+ );
317
+ }
318
+ }
159
319
  console.log();
160
320
  } catch (error) {
161
321
  handleError(error);
@@ -176,40 +336,41 @@ function handleError(error) {
176
336
  import pc2 from "picocolors";
177
337
 
178
338
  // src/lib/writer.ts
179
- import fs2 from "fs";
180
- import path2 from "path";
181
- var CLAUDE_DIR = ".claude";
182
- function ensureDir(dirPath) {
183
- fs2.mkdirSync(dirPath, { recursive: true });
339
+ import fs3 from "fs";
340
+ import path3 from "path";
341
+ function ensureDir2(dirPath) {
342
+ fs3.mkdirSync(dirPath, { recursive: true });
184
343
  }
185
- function writeAgent(slug, content, cwd = process.cwd()) {
186
- const dir = path2.join(cwd, CLAUDE_DIR, "agents");
187
- ensureDir(dir);
188
- const filePath = path2.join(dir, `${slug}.md`);
189
- fs2.writeFileSync(filePath, content, "utf-8");
344
+ function writeAgent(target, slug, content, cwd = process.cwd()) {
345
+ const dir = path3.join(cwd, target.dir, target.agentsSubdir, slug);
346
+ ensureDir2(dir);
347
+ const filePath = path3.join(dir, "AGENT.md");
348
+ fs3.writeFileSync(filePath, content, "utf-8");
190
349
  return filePath;
191
350
  }
192
- function writeSkill(slug, content, cwd = process.cwd()) {
193
- const dir = path2.join(cwd, CLAUDE_DIR, "skills");
194
- ensureDir(dir);
195
- const filePath = path2.join(dir, `${slug}.md`);
196
- fs2.writeFileSync(filePath, content, "utf-8");
197
- return filePath;
351
+ function writeSkillDir(target, slug, files, cwd = process.cwd()) {
352
+ const dir = path3.join(cwd, target.dir, target.skillsSubdir, slug);
353
+ for (const file of files) {
354
+ const filePath = path3.join(dir, file.relativePath);
355
+ ensureDir2(path3.dirname(filePath));
356
+ fs3.writeFileSync(filePath, file.content, "utf-8");
357
+ }
358
+ return dir;
198
359
  }
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");
360
+ function writeOrchestrator(target, content, cwd = process.cwd()) {
361
+ const filePath = path3.join(cwd, target.dir, target.orchestratorFile);
362
+ ensureDir2(path3.dirname(filePath));
363
+ fs3.writeFileSync(filePath, content, "utf-8");
203
364
  return filePath;
204
365
  }
205
- function writeClaudeMd(content, cwd = process.cwd()) {
206
- const filePath = path2.join(cwd, "CLAUDE.md");
207
- fs2.writeFileSync(filePath, content, "utf-8");
366
+ function writeContextFile(target, content, cwd = process.cwd()) {
367
+ const filePath = path3.join(cwd, target.contextFile);
368
+ fs3.writeFileSync(filePath, content, "utf-8");
208
369
  return filePath;
209
370
  }
210
371
 
211
372
  // src/commands/add.ts
212
- async function addCommand(type, slug) {
373
+ async function addCommand(type, slug, target) {
213
374
  if (type !== "agent" && type !== "skill") {
214
375
  console.error(pc2.red(`
215
376
  Error: Invalid type "${type}". Use "agent" or "skill".
@@ -219,25 +380,44 @@ async function addCommand(type, slug) {
219
380
  try {
220
381
  if (type === "agent") {
221
382
  const agent = await getAgent(slug);
222
- const filePath = writeAgent(slug, agent.rawContent);
383
+ const filePath = writeAgent(target, slug, agent.rawContent);
223
384
  console.log(pc2.green(`
224
385
  \u2713 Agent "${slug}" written to ${filePath}
225
386
  `));
226
387
  } else {
227
- const skill = await getSkill(slug);
228
- const filePath = writeSkill(slug, skill.rawContent);
388
+ const skill = await getSkillWithFiles(slug);
389
+ const dirPath = writeSkillDir(target, slug, skill.files);
390
+ const fileCount = skill.files.length;
229
391
  console.log(pc2.green(`
230
- \u2713 Skill "${slug}" written to ${filePath}
392
+ \u2713 Skill "${slug}" written to ${dirPath} (${fileCount} file${fileCount !== 1 ? "s" : ""})
231
393
  `));
232
394
  }
233
- } catch (error) {
234
- if (error instanceof Error) {
235
- console.error(pc2.red(`
236
- Error: ${error.message}
395
+ } catch {
396
+ if (type === "agent") {
397
+ const local = getLocalAgent(slug);
398
+ if (local) {
399
+ const filePath = writeAgent(target, slug, local.rawContent);
400
+ console.log(pc2.green(`
401
+ \u2713 Agent "${slug}" written to ${filePath} ${pc2.dim("(from ~/.loom/library)")}
237
402
  `));
403
+ return;
404
+ }
238
405
  } else {
239
- console.error(pc2.red("\n An unknown error occurred.\n"));
406
+ const local = getLocalSkillWithFiles(slug);
407
+ if (local) {
408
+ const dirPath = writeSkillDir(target, slug, local.files);
409
+ const fileCount = local.files.length;
410
+ console.log(pc2.green(`
411
+ \u2713 Skill "${slug}" written to ${dirPath} (${fileCount} file${fileCount !== 1 ? "s" : ""}) ${pc2.dim("(from ~/.loom/library)")}
412
+ `));
413
+ return;
414
+ }
240
415
  }
416
+ console.error(pc2.red(`
417
+ Error: ${type} "${slug}" not found.
418
+ `));
419
+ console.log(pc2.dim(` Try: loom marketplace search ${slug}
420
+ `));
241
421
  process.exit(1);
242
422
  }
243
423
  }
@@ -249,28 +429,23 @@ import matter3 from "gray-matter";
249
429
 
250
430
  // src/lib/generator.ts
251
431
  import matter2 from "gray-matter";
252
- function generateClaudeMd(preset, agents) {
432
+ function generateContextFile(preset, agents, target, skillSlugs = []) {
253
433
  const lines = [];
254
434
  lines.push(`# ${preset.name}`);
255
435
  lines.push("");
256
- lines.push(preset.claudemd.projectDescription);
436
+ lines.push(preset.context.projectDescription.trim());
257
437
  lines.push("");
258
438
  if (preset.constitution.principles.length > 0) {
259
439
  lines.push("## Principles");
440
+ lines.push("");
260
441
  for (const p2 of preset.constitution.principles) {
261
442
  lines.push(`- ${p2}`);
262
443
  }
263
444
  lines.push("");
264
445
  }
265
- if (preset.constitution.stack.length > 0) {
266
- lines.push("## Stack");
267
- for (const s of preset.constitution.stack) {
268
- lines.push(`- ${s}`);
269
- }
270
- lines.push("");
271
- }
272
446
  if (preset.constitution.conventions.length > 0) {
273
447
  lines.push("## Conventions");
448
+ lines.push("");
274
449
  for (const c of preset.constitution.conventions) {
275
450
  lines.push(`- ${c}`);
276
451
  }
@@ -279,20 +454,52 @@ function generateClaudeMd(preset, agents) {
279
454
  if (preset.constitution.customSections) {
280
455
  for (const [title, content] of Object.entries(preset.constitution.customSections)) {
281
456
  lines.push(`## ${title}`);
457
+ lines.push("");
282
458
  lines.push(content);
283
459
  lines.push("");
284
460
  }
285
461
  }
286
- if (agents.length > 0) {
287
- lines.push("## Agents");
288
- for (const agent of agents) {
289
- lines.push(`- **${agent.slug}**: ${agent.name} \u2014 ${agent.role}`);
462
+ lines.push("## Commands");
463
+ lines.push("");
464
+ lines.push("```bash");
465
+ lines.push("npm run dev # Start development server");
466
+ lines.push("npm run build # Build for production");
467
+ lines.push("npm run lint # Run linter");
468
+ lines.push("npm test # Run tests");
469
+ lines.push("```");
470
+ lines.push("");
471
+ lines.push("<!-- loom:agents:start -->");
472
+ lines.push("## Agents");
473
+ lines.push("");
474
+ const nonOrchestrator = agents.filter((a) => a.slug !== "orchestrator");
475
+ if (nonOrchestrator.length > 0) {
476
+ lines.push(`This project uses ${nonOrchestrator.length} specialized agents coordinated by an orchestrator (\`${target.dir}/${target.orchestratorFile}\`).`);
477
+ lines.push("");
478
+ lines.push("| Agent | Role | Description |");
479
+ lines.push("|-------|------|-------------|");
480
+ for (const agent of nonOrchestrator) {
481
+ lines.push(`| \`${agent.slug}\` | ${agent.name} | ${agent.description} |`);
482
+ }
483
+ lines.push("");
484
+ }
485
+ lines.push("<!-- loom:agents:end -->");
486
+ lines.push("");
487
+ if (skillSlugs.length > 0) {
488
+ lines.push("<!-- loom:skills:start -->");
489
+ lines.push("## Skills");
490
+ lines.push("");
491
+ lines.push("Installed skills providing domain-specific conventions and patterns:");
492
+ lines.push("");
493
+ for (const slug of skillSlugs) {
494
+ lines.push(`- \`${slug}\``);
290
495
  }
291
496
  lines.push("");
497
+ lines.push("<!-- loom:skills:end -->");
498
+ lines.push("");
292
499
  }
293
- lines.push("## Orchestrator");
500
+ lines.push("## How to use");
294
501
  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.");
502
+ 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.`);
296
503
  lines.push("");
297
504
  return lines.join("\n");
298
505
  }
@@ -315,6 +522,81 @@ function generateOrchestrator(templateContent, agents, presetSkills) {
315
522
  return matter2.stringify(newContent, newFrontmatter);
316
523
  }
317
524
 
525
+ // src/lib/target.ts
526
+ var BUILTIN_TARGETS = {
527
+ "claude-code": {
528
+ name: "claude-code",
529
+ description: "Claude Code \u2014 .claude/ + CLAUDE.md",
530
+ dir: ".claude",
531
+ agentsSubdir: "agents",
532
+ skillsSubdir: "skills",
533
+ orchestratorFile: "orchestrator.md",
534
+ contextFile: "CLAUDE.md"
535
+ },
536
+ cursor: {
537
+ name: "cursor",
538
+ description: "Cursor \u2014 .cursor/ + .cursorrules",
539
+ dir: ".cursor",
540
+ agentsSubdir: "agents",
541
+ skillsSubdir: "skills",
542
+ orchestratorFile: "orchestrator.md",
543
+ contextFile: ".cursorrules"
544
+ }
545
+ };
546
+ var DEFAULT_TARGET = "claude-code";
547
+ function listTargetNames() {
548
+ return Object.keys(BUILTIN_TARGETS);
549
+ }
550
+ function resolveTarget(targetName, customDir, customContextFile) {
551
+ const builtin = BUILTIN_TARGETS[targetName];
552
+ if (builtin) return builtin;
553
+ if (targetName === "custom") {
554
+ if (!customDir || !customContextFile) {
555
+ throw new Error(
556
+ 'Target "custom" requires --target-dir and --context-file.'
557
+ );
558
+ }
559
+ return {
560
+ name: "custom",
561
+ description: `Custom \u2014 ${customDir}/ + ${customContextFile}`,
562
+ dir: customDir,
563
+ agentsSubdir: "agents",
564
+ skillsSubdir: "skills",
565
+ orchestratorFile: "orchestrator.md",
566
+ contextFile: customContextFile
567
+ };
568
+ }
569
+ const available = [...listTargetNames(), "custom"].join(", ");
570
+ throw new Error(
571
+ `Unknown target "${targetName}". Available: ${available}.`
572
+ );
573
+ }
574
+
575
+ // src/lib/config.ts
576
+ import fs4 from "fs";
577
+ import path4 from "path";
578
+ var CONFIG_FILE = "loom.config.json";
579
+ function saveConfig(target, cwd = process.cwd()) {
580
+ const config = {
581
+ target: target.name,
582
+ targetDir: target.dir,
583
+ contextFile: target.contextFile
584
+ };
585
+ const filePath = path4.join(cwd, CONFIG_FILE);
586
+ fs4.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
587
+ }
588
+ function loadConfig(cwd = process.cwd()) {
589
+ const filePath = path4.join(cwd, CONFIG_FILE);
590
+ if (!fs4.existsSync(filePath)) return null;
591
+ try {
592
+ const raw = fs4.readFileSync(filePath, "utf-8");
593
+ const config = JSON.parse(raw);
594
+ return resolveTarget(config.target, config.targetDir, config.contextFile);
595
+ } catch {
596
+ return null;
597
+ }
598
+ }
599
+
318
600
  // src/commands/init.ts
319
601
  async function initCommand(presetSlug, opts = {}) {
320
602
  try {
@@ -324,7 +606,7 @@ async function initCommand(presetSlug, opts = {}) {
324
606
  process.exit(1);
325
607
  }
326
608
  if (!presetSlug && !hasFlags) {
327
- await interactiveInit();
609
+ await interactiveInit(opts.target, opts.targetExplicit);
328
610
  } else {
329
611
  await nonInteractiveInit(presetSlug, opts);
330
612
  }
@@ -339,8 +621,51 @@ async function initCommand(presetSlug, opts = {}) {
339
621
  process.exit(1);
340
622
  }
341
623
  }
342
- async function interactiveInit() {
624
+ async function interactiveInit(target, targetExplicit) {
343
625
  p.intro(pc3.bgCyan(pc3.black(" loom init ")));
626
+ if (!targetExplicit) {
627
+ const builtinEntries = Object.values(BUILTIN_TARGETS);
628
+ const targetChoice = await p.select({
629
+ message: "Choose a target runtime",
630
+ options: [
631
+ ...builtinEntries.map((t) => ({
632
+ value: t.name,
633
+ label: t.description
634
+ })),
635
+ { value: "custom", label: "Custom \u2014 choose directory and context file" }
636
+ ],
637
+ initialValue: target.name
638
+ });
639
+ if (p.isCancel(targetChoice)) {
640
+ p.cancel("Operation cancelled.");
641
+ process.exit(0);
642
+ }
643
+ if (targetChoice === "custom") {
644
+ const customDir = await p.text({
645
+ message: "Target directory",
646
+ placeholder: ".myruntime",
647
+ validate: (v) => !v || v.length === 0 ? "Required" : void 0
648
+ });
649
+ if (p.isCancel(customDir)) {
650
+ p.cancel("Operation cancelled.");
651
+ process.exit(0);
652
+ }
653
+ const customFile = await p.text({
654
+ message: "Context file name",
655
+ placeholder: "CONTEXT.md",
656
+ validate: (v) => !v || v.length === 0 ? "Required" : void 0
657
+ });
658
+ if (p.isCancel(customFile)) {
659
+ p.cancel("Operation cancelled.");
660
+ process.exit(0);
661
+ }
662
+ target = resolveTarget("custom", customDir, customFile);
663
+ } else {
664
+ target = BUILTIN_TARGETS[targetChoice];
665
+ }
666
+ } else {
667
+ p.log.info(`Target: ${target.description}`);
668
+ }
344
669
  const presets = await listPresets();
345
670
  if (presets.length === 0) {
346
671
  p.cancel("No presets available.");
@@ -403,11 +728,13 @@ async function interactiveInit() {
403
728
  }
404
729
  const s = p.spinner();
405
730
  s.start("Generating project files...");
406
- await generateAndWrite(preset, agentSlugs, skillSlugs);
731
+ await generateAndWrite(preset, agentSlugs, skillSlugs, target);
732
+ saveConfig(target);
407
733
  s.stop("Project files generated.");
408
- p.outro(pc3.green(`Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), CLAUDE.md ready.`));
734
+ p.outro(pc3.green(`Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), ${target.contextFile} ready.`));
409
735
  }
410
736
  async function nonInteractiveInit(presetSlug, opts) {
737
+ const target = opts.target;
411
738
  const preset = await getPreset(presetSlug);
412
739
  const allAgents = await listAgents();
413
740
  let agentSlugs = [...preset.agents];
@@ -448,23 +775,24 @@ async function nonInteractiveInit(presetSlug, opts) {
448
775
  console.log(pc3.bold(pc3.cyan(`
449
776
  Initializing preset "${preset.name}"...
450
777
  `)));
451
- await generateAndWrite(preset, agentSlugs, skillSlugs);
778
+ await generateAndWrite(preset, agentSlugs, skillSlugs, target);
779
+ saveConfig(target);
452
780
  console.log(
453
781
  pc3.bold(
454
782
  pc3.cyan(
455
783
  `
456
- Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), CLAUDE.md ready.
784
+ Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), ${target.contextFile} ready.
457
785
  `
458
786
  )
459
787
  )
460
788
  );
461
789
  }
462
- async function generateAndWrite(preset, agentSlugs, skillSlugs) {
790
+ async function generateAndWrite(preset, agentSlugs, skillSlugs, target) {
463
791
  const agentResults = await Promise.allSettled(
464
792
  agentSlugs.map((slug) => getAgent(slug))
465
793
  );
466
794
  const skillResults = await Promise.allSettled(
467
- skillSlugs.map((slug) => getSkill(slug))
795
+ skillSlugs.map((slug) => getSkillWithFiles(slug))
468
796
  );
469
797
  const agentInfos = [];
470
798
  const agentsWithSkills = [];
@@ -478,13 +806,14 @@ async function generateAndWrite(preset, agentSlugs, skillSlugs) {
478
806
  if (slug === "orchestrator") {
479
807
  orchestratorTemplate = result.value.rawContent;
480
808
  } else {
481
- writeAgent(slug, result.value.rawContent);
809
+ writeAgent(target, slug, result.value.rawContent);
482
810
  console.log(pc3.green(` \u2713 Agent: ${slug}`));
483
811
  }
484
812
  agentInfos.push({
485
813
  slug,
486
814
  name: fm.name || slug,
487
- role: fm.role || ""
815
+ role: fm.role || "",
816
+ description: fm.description || ""
488
817
  });
489
818
  agentsWithSkills.push({
490
819
  slug,
@@ -502,22 +831,23 @@ async function generateAndWrite(preset, agentSlugs, skillSlugs) {
502
831
  agentsWithSkills,
503
832
  skillSlugs
504
833
  );
505
- writeOrchestrator(orchestratorContent);
506
- console.log(pc3.green(` \u2713 orchestrator.md generated`));
834
+ writeOrchestrator(target, orchestratorContent);
835
+ console.log(pc3.green(` \u2713 ${target.orchestratorFile} generated`));
507
836
  }
508
837
  for (let i = 0; i < skillSlugs.length; i++) {
509
838
  const slug = skillSlugs[i];
510
839
  const result = skillResults[i];
511
840
  if (result.status === "fulfilled") {
512
- writeSkill(slug, result.value.rawContent);
513
- console.log(pc3.green(` \u2713 Skill: ${slug}`));
841
+ writeSkillDir(target, slug, result.value.files);
842
+ const fileCount = result.value.files.length;
843
+ console.log(pc3.green(` \u2713 Skill: ${slug} (${fileCount} file${fileCount !== 1 ? "s" : ""})`));
514
844
  } else {
515
845
  console.log(pc3.yellow(` \u26A0 Skill "${slug}" skipped: ${result.reason}`));
516
846
  }
517
847
  }
518
- const claudeContent = generateClaudeMd(preset, agentInfos);
519
- writeClaudeMd(claudeContent);
520
- console.log(pc3.green(` \u2713 CLAUDE.md generated`));
848
+ const contextContent = generateContextFile(preset, agentInfos, target, skillSlugs);
849
+ writeContextFile(target, contextContent);
850
+ console.log(pc3.green(` \u2713 ${target.contextFile} generated`));
521
851
  }
522
852
  function computeAvailableSkills(preset, selectedAgentSlugs, allAgents, allSkillSlugs) {
523
853
  const linkedToSelected = /* @__PURE__ */ new Set();
@@ -537,6 +867,106 @@ function computeAvailableSkills(preset, selectedAgentSlugs, allAgents, allSkillS
537
867
  });
538
868
  }
539
869
 
870
+ // src/commands/marketplace.ts
871
+ import pc4 from "picocolors";
872
+ var DEFAULT_API_URL = "https://loom.voidcorp.io";
873
+ function padEnd2(str, len) {
874
+ return str + " ".repeat(Math.max(0, len - str.length));
875
+ }
876
+ function truncate2(str, max) {
877
+ if (str.length <= max) return str;
878
+ return str.slice(0, max - 1) + "\u2026";
879
+ }
880
+ function getApiUrl() {
881
+ return process.env.LOOM_API_URL ?? DEFAULT_API_URL;
882
+ }
883
+ async function marketplaceSearchCommand(query, opts) {
884
+ try {
885
+ const url = new URL("/api/cli/marketplace", getApiUrl());
886
+ if (query) url.searchParams.set("q", query);
887
+ if (opts?.type) url.searchParams.set("type", opts.type);
888
+ if (opts?.sort) url.searchParams.set("sort", opts.sort);
889
+ const res = await fetch(url);
890
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
891
+ const data = await res.json();
892
+ if (data.items.length === 0) {
893
+ console.log(pc4.dim("\n No results found.\n"));
894
+ return;
895
+ }
896
+ console.log(
897
+ pc4.bold(pc4.cyan(`
898
+ Marketplace${query ? ` \u2014 "${query}"` : ""}`))
899
+ );
900
+ console.log(pc4.dim(" " + "\u2500".repeat(70)));
901
+ for (const item of data.items) {
902
+ const type = pc4.dim(`[${item.type}]`);
903
+ const installs = pc4.dim(`\u2193${item.installCount}`);
904
+ const author = item.authorName ? pc4.dim(`by ${item.authorName}`) : "";
905
+ console.log(
906
+ ` ${padEnd2(pc4.green(item.slug), 25)} ${padEnd2(type, 14)} ${padEnd2(truncate2(item.title, 25), 27)} ${installs} ${author}`
907
+ );
908
+ }
909
+ console.log(
910
+ pc4.dim(
911
+ `
912
+ Install with: ${pc4.reset("loom marketplace install <slug>")}
913
+ `
914
+ )
915
+ );
916
+ } catch (error) {
917
+ if (error instanceof Error) {
918
+ console.error(pc4.red(`
919
+ \u2717 ${error.message}
920
+ `));
921
+ } else {
922
+ console.error(pc4.red("\n \u2717 Could not reach the marketplace.\n"));
923
+ }
924
+ process.exit(1);
925
+ }
926
+ }
927
+ async function marketplaceInstallCommand(slug) {
928
+ try {
929
+ const url = new URL("/api/cli/marketplace/install", getApiUrl());
930
+ const res = await fetch(url, {
931
+ method: "POST",
932
+ headers: { "Content-Type": "application/json" },
933
+ body: JSON.stringify({ slug })
934
+ });
935
+ if (!res.ok) {
936
+ const body = await res.json().catch(() => ({}));
937
+ throw new Error(body.error ?? `HTTP ${res.status}`);
938
+ }
939
+ const data = await res.json();
940
+ const r = data.resource;
941
+ if (r.type === "agent") {
942
+ saveLocalAgent(r.slug, r.content);
943
+ } else if (r.type === "skill") {
944
+ const files = r.files?.length ? r.files.map((f) => ({ relativePath: f.relativePath, content: f.content })) : [{ relativePath: "SKILL.md", content: r.content }];
945
+ saveLocalSkill(r.slug, files);
946
+ } else if (r.type === "preset") {
947
+ saveLocalPreset(r.slug, r.content);
948
+ }
949
+ console.log(
950
+ pc4.green(`
951
+ \u2713 Installed "${r.title}" (${r.type}) to ~/.loom/library/
952
+ `)
953
+ );
954
+ console.log(
955
+ pc4.dim(` Use it: loom add ${r.type} ${r.slug}
956
+ `)
957
+ );
958
+ } catch (error) {
959
+ if (error instanceof Error) {
960
+ console.error(pc4.red(`
961
+ \u2717 ${error.message}
962
+ `));
963
+ } else {
964
+ console.error(pc4.red("\n \u2717 Installation failed.\n"));
965
+ }
966
+ process.exit(1);
967
+ }
968
+ }
969
+
540
970
  // src/index.ts
541
971
  var require2 = createRequire(import.meta.url);
542
972
  var { version } = require2("../package.json");
@@ -545,10 +975,46 @@ program.name("loom").description("Integrate Loom library (agents, skills, preset
545
975
  program.command("list").description("List available agents, skills, and presets").argument("[type]", "Filter by type: agents, skills, or presets").action(async (type) => {
546
976
  await listCommand(type);
547
977
  });
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) => {
549
- await addCommand(type, slug);
978
+ 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) => {
979
+ const savedConfig = loadConfig();
980
+ const target = opts.target !== DEFAULT_TARGET || opts.targetDir || opts.contextFile ? resolveTarget(opts.target, opts.targetDir, opts.contextFile) : savedConfig ?? BUILTIN_TARGETS[DEFAULT_TARGET];
981
+ await addCommand(type, slug, target);
550
982
  });
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);
983
+ 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) => {
984
+ let target;
985
+ let targetExplicit = false;
986
+ if (opts.claude) {
987
+ target = BUILTIN_TARGETS["claude-code"];
988
+ targetExplicit = true;
989
+ } else if (opts.cursor) {
990
+ target = BUILTIN_TARGETS["cursor"];
991
+ targetExplicit = true;
992
+ } else if (opts.target) {
993
+ target = resolveTarget(
994
+ opts.target,
995
+ opts.targetDir,
996
+ opts.contextFile
997
+ );
998
+ targetExplicit = true;
999
+ } else {
1000
+ target = BUILTIN_TARGETS[DEFAULT_TARGET];
1001
+ }
1002
+ await initCommand(preset, {
1003
+ addAgent: opts.addAgent,
1004
+ removeAgent: opts.removeAgent,
1005
+ addSkill: opts.addSkill,
1006
+ removeSkill: opts.removeSkill,
1007
+ target,
1008
+ targetExplicit
1009
+ });
1010
+ });
1011
+ var mp = program.command("marketplace").alias("mp").description("Browse and install community resources");
1012
+ mp.command("search").description("Search the marketplace").argument("[query]", "Search query").option("--type <type>", "Filter by type: agent, skill, preset").option("--sort <sort>", "Sort: popular, recent", "popular").action(
1013
+ async (query, opts) => {
1014
+ await marketplaceSearchCommand(query, opts);
1015
+ }
1016
+ );
1017
+ mp.command("install").description("Install a resource from the marketplace").argument("<slug>", "Resource slug to install").action(async (slug) => {
1018
+ await marketplaceInstallCommand(slug);
553
1019
  });
554
1020
  program.parse();