@folpe/loom 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +332 -41
  2. package/package.json +7 -7
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,34 +336,36 @@ 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
- function ensureDir(dirPath) {
182
- 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 });
183
343
  }
184
344
  function writeAgent(target, slug, content, cwd = process.cwd()) {
185
- const dir = path2.join(cwd, target.dir, target.agentsSubdir, slug);
186
- ensureDir(dir);
187
- const filePath = path2.join(dir, "AGENT.md");
188
- fs2.writeFileSync(filePath, content, "utf-8");
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");
189
349
  return filePath;
190
350
  }
191
- function writeSkill(target, slug, content, cwd = process.cwd()) {
192
- const dir = path2.join(cwd, target.dir, target.skillsSubdir, slug);
193
- ensureDir(dir);
194
- const filePath = path2.join(dir, "SKILL.md");
195
- fs2.writeFileSync(filePath, content, "utf-8");
196
- 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;
197
359
  }
198
360
  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");
361
+ const filePath = path3.join(cwd, target.dir, target.orchestratorFile);
362
+ ensureDir2(path3.dirname(filePath));
363
+ fs3.writeFileSync(filePath, content, "utf-8");
202
364
  return filePath;
203
365
  }
204
366
  function writeContextFile(target, content, cwd = process.cwd()) {
205
- const filePath = path2.join(cwd, target.contextFile);
206
- fs2.writeFileSync(filePath, content, "utf-8");
367
+ const filePath = path3.join(cwd, target.contextFile);
368
+ fs3.writeFileSync(filePath, content, "utf-8");
207
369
  return filePath;
208
370
  }
209
371
 
@@ -223,20 +385,39 @@ async function addCommand(type, slug, target) {
223
385
  \u2713 Agent "${slug}" written to ${filePath}
224
386
  `));
225
387
  } else {
226
- const skill = await getSkill(slug);
227
- const filePath = writeSkill(target, slug, skill.rawContent);
388
+ const skill = await getSkillWithFiles(slug);
389
+ const dirPath = writeSkillDir(target, slug, skill.files);
390
+ const fileCount = skill.files.length;
228
391
  console.log(pc2.green(`
229
- \u2713 Skill "${slug}" written to ${filePath}
392
+ \u2713 Skill "${slug}" written to ${dirPath} (${fileCount} file${fileCount !== 1 ? "s" : ""})
230
393
  `));
231
394
  }
232
- } catch (error) {
233
- if (error instanceof Error) {
234
- console.error(pc2.red(`
235
- 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)")}
236
402
  `));
403
+ return;
404
+ }
237
405
  } else {
238
- 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
+ }
239
415
  }
416
+ console.error(pc2.red(`
417
+ Error: ${type} "${slug}" not found.
418
+ `));
419
+ console.log(pc2.dim(` Try: loom marketplace search ${slug}
420
+ `));
240
421
  process.exit(1);
241
422
  }
242
423
  }
@@ -392,8 +573,8 @@ function resolveTarget(targetName, customDir, customContextFile) {
392
573
  }
393
574
 
394
575
  // src/lib/config.ts
395
- import fs3 from "fs";
396
- import path3 from "path";
576
+ import fs4 from "fs";
577
+ import path4 from "path";
397
578
  var CONFIG_FILE = "loom.config.json";
398
579
  function saveConfig(target, cwd = process.cwd()) {
399
580
  const config = {
@@ -401,14 +582,14 @@ function saveConfig(target, cwd = process.cwd()) {
401
582
  targetDir: target.dir,
402
583
  contextFile: target.contextFile
403
584
  };
404
- const filePath = path3.join(cwd, CONFIG_FILE);
405
- fs3.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
585
+ const filePath = path4.join(cwd, CONFIG_FILE);
586
+ fs4.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
406
587
  }
407
588
  function loadConfig(cwd = process.cwd()) {
408
- const filePath = path3.join(cwd, CONFIG_FILE);
409
- if (!fs3.existsSync(filePath)) return null;
589
+ const filePath = path4.join(cwd, CONFIG_FILE);
590
+ if (!fs4.existsSync(filePath)) return null;
410
591
  try {
411
- const raw = fs3.readFileSync(filePath, "utf-8");
592
+ const raw = fs4.readFileSync(filePath, "utf-8");
412
593
  const config = JSON.parse(raw);
413
594
  return resolveTarget(config.target, config.targetDir, config.contextFile);
414
595
  } catch {
@@ -611,7 +792,7 @@ async function generateAndWrite(preset, agentSlugs, skillSlugs, target) {
611
792
  agentSlugs.map((slug) => getAgent(slug))
612
793
  );
613
794
  const skillResults = await Promise.allSettled(
614
- skillSlugs.map((slug) => getSkill(slug))
795
+ skillSlugs.map((slug) => getSkillWithFiles(slug))
615
796
  );
616
797
  const agentInfos = [];
617
798
  const agentsWithSkills = [];
@@ -657,8 +838,9 @@ async function generateAndWrite(preset, agentSlugs, skillSlugs, target) {
657
838
  const slug = skillSlugs[i];
658
839
  const result = skillResults[i];
659
840
  if (result.status === "fulfilled") {
660
- writeSkill(target, slug, result.value.rawContent);
661
- 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" : ""})`));
662
844
  } else {
663
845
  console.log(pc3.yellow(` \u26A0 Skill "${slug}" skipped: ${result.reason}`));
664
846
  }
@@ -685,6 +867,106 @@ function computeAvailableSkills(preset, selectedAgentSlugs, allAgents, allSkillS
685
867
  });
686
868
  }
687
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
+
688
970
  // src/index.ts
689
971
  var require2 = createRequire(import.meta.url);
690
972
  var { version } = require2("../package.json");
@@ -726,4 +1008,13 @@ program.command("init").description("Initialize a project with a preset (agents
726
1008
  targetExplicit
727
1009
  });
728
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);
1019
+ });
729
1020
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@folpe/loom",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "CLI to scaffold Claude Code projects with curated agents, skills, and presets",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,11 @@
29
29
  "dist",
30
30
  "data"
31
31
  ],
32
+ "scripts": {
33
+ "prebuild": "node scripts/sync-library.js",
34
+ "build": "tsup",
35
+ "dev": "tsup --watch"
36
+ },
32
37
  "dependencies": {
33
38
  "@clack/prompts": "^1.0.1",
34
39
  "commander": "^13.1.0",
@@ -43,10 +48,5 @@
43
48
  "@types/node": "^20",
44
49
  "tsup": "^8.4.0",
45
50
  "typescript": "^5"
46
- },
47
- "scripts": {
48
- "prebuild": "node scripts/sync-library.js",
49
- "build": "tsup",
50
- "dev": "tsup --watch"
51
51
  }
52
- }
52
+ }