agentloom 0.1.0 → 0.1.2

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 (53) hide show
  1. package/README.md +91 -72
  2. package/bin/cli.mjs +3 -2
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +149 -17
  5. package/dist/commands/add.d.ts +7 -0
  6. package/dist/commands/add.js +122 -31
  7. package/dist/commands/agent.d.ts +2 -0
  8. package/dist/commands/agent.js +85 -0
  9. package/dist/commands/command.d.ts +2 -0
  10. package/dist/commands/command.js +98 -0
  11. package/dist/commands/delete.d.ts +9 -0
  12. package/dist/commands/delete.js +444 -0
  13. package/dist/commands/entity-utils.d.ts +13 -0
  14. package/dist/commands/entity-utils.js +58 -0
  15. package/dist/commands/find.d.ts +21 -0
  16. package/dist/commands/find.js +944 -0
  17. package/dist/commands/mcp.js +133 -55
  18. package/dist/commands/skills.d.ts +2 -1
  19. package/dist/commands/skills.js +105 -9
  20. package/dist/commands/sync.d.ts +6 -0
  21. package/dist/commands/sync.js +12 -10
  22. package/dist/commands/update.d.ts +7 -0
  23. package/dist/commands/update.js +286 -21
  24. package/dist/core/argv.d.ts +2 -1
  25. package/dist/core/argv.js +42 -2
  26. package/dist/core/commands.d.ts +13 -0
  27. package/dist/core/commands.js +65 -0
  28. package/dist/core/copy.d.ts +6 -0
  29. package/dist/core/copy.js +126 -65
  30. package/dist/core/importer.d.ts +28 -1
  31. package/dist/core/importer.js +1104 -41
  32. package/dist/core/lockfile.js +86 -3
  33. package/dist/core/manage-agents-bootstrap.d.ts +10 -0
  34. package/dist/core/manage-agents-bootstrap.js +40 -0
  35. package/dist/core/manifest.js +7 -1
  36. package/dist/core/router.d.ts +16 -0
  37. package/dist/core/router.js +66 -0
  38. package/dist/core/scope.d.ts +1 -1
  39. package/dist/core/scope.js +12 -8
  40. package/dist/core/settings.d.ts +4 -3
  41. package/dist/core/settings.js +10 -8
  42. package/dist/core/skills.d.ts +23 -0
  43. package/dist/core/skills.js +328 -0
  44. package/dist/core/sources.d.ts +3 -1
  45. package/dist/core/sources.js +31 -1
  46. package/dist/core/telemetry.d.ts +26 -0
  47. package/dist/core/telemetry.js +124 -0
  48. package/dist/core/version-notifier.d.ts +1 -0
  49. package/dist/core/version-notifier.js +22 -4
  50. package/dist/sync/index.d.ts +7 -1
  51. package/dist/sync/index.js +395 -131
  52. package/dist/types.d.ts +16 -1
  53. package/package.json +5 -4
@@ -0,0 +1,944 @@
1
+ import fs from "node:fs";
2
+ import https from "node:https";
3
+ import path from "node:path";
4
+ import matter from "gray-matter";
5
+ import { parseAgentMarkdown, parseAgentsDir } from "../core/agents.js";
6
+ import { parseCommandsDir } from "../core/commands.js";
7
+ import { formatUsageError, getFindHelpText } from "../core/copy.js";
8
+ import { readCanonicalMcp } from "../core/mcp.js";
9
+ import { buildScopePaths } from "../core/scope.js";
10
+ import { parseSkillsDir } from "../core/skills.js";
11
+ const FIND_API_BASE = process.env.AGENTLOOM_FIND_API_BASE || "https://api.github.com";
12
+ const REQUEST_TIMEOUT_MS = 3500;
13
+ const DEFAULT_RESULT_LIMIT = 8;
14
+ const DEFAULT_REPO_SCAN_LIMIT = 10;
15
+ const MAX_INSTALLABILITY_CHECKS = 24;
16
+ const INSTALLABILITY_CHECK_BATCH_SIZE = 6;
17
+ export async function runFindCommand(argv, searchClient = searchAgentsWithDiagnostics) {
18
+ const target = searchClient === searchAgentsWithDiagnostics ? "all" : "agent";
19
+ await runScopedFindCommand(argv, target, searchClient);
20
+ }
21
+ export async function runScopedFindCommand(argv, target, searchClient = searchAgentsWithDiagnostics) {
22
+ if (argv.help) {
23
+ console.log(getFindHelpText());
24
+ return;
25
+ }
26
+ const query = buildQueryFromArgs(argv);
27
+ if (!query) {
28
+ throw new Error(formatUsageError({
29
+ issue: "Missing required <query>.",
30
+ usage: "agentloom find <query>",
31
+ example: "agentloom find reviewer",
32
+ }));
33
+ }
34
+ const shouldSearchAgents = target === "all" || target === "agent";
35
+ const shouldSearchCommands = target === "all" || target === "command";
36
+ const shouldSearchMcp = target === "all" || target === "mcp";
37
+ const shouldSearchSkills = target === "all" || target === "skill";
38
+ const agentResult = shouldSearchAgents
39
+ ? normalizeSearchResult(await searchClient(query, DEFAULT_RESULT_LIMIT))
40
+ : { agents: [], failures: [] };
41
+ const commandResult = shouldSearchCommands
42
+ ? await searchCommandsWithDiagnostics(query, DEFAULT_RESULT_LIMIT, FIND_API_BASE)
43
+ : { items: [], failures: [] };
44
+ const mcpResult = shouldSearchMcp
45
+ ? await searchMcpWithDiagnostics(query, DEFAULT_RESULT_LIMIT, FIND_API_BASE)
46
+ : { items: [], failures: [] };
47
+ const skillResult = shouldSearchSkills
48
+ ? await searchSkillsWithDiagnostics(query, DEFAULT_RESULT_LIMIT, FIND_API_BASE)
49
+ : { items: [], failures: [] };
50
+ if (target === "agent") {
51
+ const results = agentResult.agents;
52
+ const failures = agentResult.failures;
53
+ if (results.length === 0) {
54
+ if (failures.length > 0) {
55
+ const details = failures.slice(0, 3).join("; ");
56
+ throw new Error(`Agent search could not complete reliably (${failures.length} repository/file checks failed): ${details}`);
57
+ }
58
+ console.log(`No shared agents found for "${query}".`);
59
+ return;
60
+ }
61
+ const partialSuffix = failures.length > 0
62
+ ? ` (partial results: ${failures.length} repository scan${failures.length === 1 ? "" : "s"} failed)`
63
+ : "";
64
+ console.log(`Found ${results.length} matching agent${results.length === 1 ? "" : "s"} for "${query}"${partialSuffix}:`);
65
+ console.log("");
66
+ if (failures.length > 0) {
67
+ console.log("Scan warnings:");
68
+ for (const failure of failures.slice(0, 3)) {
69
+ console.log(` - ${failure}`);
70
+ }
71
+ if (failures.length > 3) {
72
+ console.log(` - ...and ${failures.length - 3} more`);
73
+ }
74
+ console.log("");
75
+ }
76
+ for (const result of results) {
77
+ console.log(`${result.repo}@${result.agentName}${formatStars(result.stars)} (${result.filePath})`);
78
+ console.log(` ${result.fileUrl}`);
79
+ console.log(` Install: ${buildInstallCommand(result)}`);
80
+ console.log("");
81
+ }
82
+ return;
83
+ }
84
+ const local = searchLocalMatches(query, target);
85
+ const total = agentResult.agents.length +
86
+ commandResult.items.length +
87
+ mcpResult.items.length +
88
+ skillResult.items.length +
89
+ local.total;
90
+ if (total === 0) {
91
+ const failureCount = agentResult.failures.length +
92
+ commandResult.failures.length +
93
+ mcpResult.failures.length +
94
+ skillResult.failures.length;
95
+ if (failureCount > 0) {
96
+ throw new Error(`Search returned no results and encountered ${failureCount} remote scan failure(s).`);
97
+ }
98
+ console.log(`No matches found for "${query}".`);
99
+ return;
100
+ }
101
+ console.log(`Found ${total} match(es) for "${query}".`);
102
+ console.log("");
103
+ if (local.total > 0) {
104
+ console.log("Local matches:");
105
+ for (const item of local.lines) {
106
+ console.log(` - ${item}`);
107
+ }
108
+ console.log("");
109
+ }
110
+ if (agentResult.agents.length > 0) {
111
+ console.log("Agent matches:");
112
+ for (const result of agentResult.agents) {
113
+ console.log(` - ${result.repo}@${result.agentName}${formatStars(result.stars)} (${result.filePath})`);
114
+ console.log(` ${result.fileUrl}`);
115
+ console.log(` Install: ${buildInstallCommand(result)}`);
116
+ }
117
+ console.log("");
118
+ }
119
+ if (commandResult.items.length > 0) {
120
+ console.log("Command matches:");
121
+ for (const result of commandResult.items) {
122
+ console.log(` - ${result.repo}@${result.commandName}${formatStars(result.stars)} (${result.filePath})`);
123
+ console.log(` ${result.fileUrl}`);
124
+ console.log(` Install: ${buildCommandInstallCommand(result)}`);
125
+ }
126
+ console.log("");
127
+ }
128
+ if (mcpResult.items.length > 0) {
129
+ console.log("MCP matches:");
130
+ for (const result of mcpResult.items) {
131
+ console.log(` - ${result.repo}@${result.serverName}${formatStars(result.stars)} (${result.filePath})`);
132
+ console.log(` ${result.fileUrl}`);
133
+ console.log(` Install: ${buildMcpInstallCommand(result)}`);
134
+ }
135
+ console.log("");
136
+ }
137
+ if (skillResult.items.length > 0) {
138
+ console.log("Skill matches:");
139
+ for (const result of skillResult.items) {
140
+ console.log(` - ${result.repo}@${result.skillName}${formatStars(result.stars)} (${result.filePath})`);
141
+ console.log(` ${result.fileUrl}`);
142
+ console.log(` Install: ${buildSkillInstallCommand(result)}`);
143
+ }
144
+ console.log("");
145
+ }
146
+ const failures = [
147
+ ...agentResult.failures,
148
+ ...commandResult.failures,
149
+ ...mcpResult.failures,
150
+ ...skillResult.failures,
151
+ ];
152
+ if (failures.length > 0) {
153
+ console.log("Warnings:");
154
+ for (const failure of failures.slice(0, 5)) {
155
+ console.log(` - ${failure}`);
156
+ }
157
+ }
158
+ }
159
+ function buildQueryFromArgs(argv) {
160
+ const root = String(argv._[0] ?? "")
161
+ .trim()
162
+ .toLowerCase();
163
+ const action = String(argv._[1] ?? "")
164
+ .trim()
165
+ .toLowerCase();
166
+ const positionalStartIndex = root === "find" ? 1 : action === "find" ? 2 : 1;
167
+ const positionalTokens = argv._.slice(positionalStartIndex)
168
+ .map((value) => String(value).trim())
169
+ .filter(Boolean);
170
+ const passthroughTokens = Array.isArray(argv["--"])
171
+ ? argv["--"].map((value) => String(value).trim()).filter(Boolean)
172
+ : [];
173
+ return positionalTokens.concat(passthroughTokens).join(" ").trim();
174
+ }
175
+ export async function searchAgents(query, limit = DEFAULT_RESULT_LIMIT, apiBase = FIND_API_BASE) {
176
+ const result = await searchAgentsWithDiagnostics(query, limit, apiBase);
177
+ return result.agents;
178
+ }
179
+ export async function searchAgentsWithDiagnostics(query, limit = DEFAULT_RESULT_LIMIT, apiBase = FIND_API_BASE) {
180
+ const tokens = query
181
+ .toLowerCase()
182
+ .split(/\s+/)
183
+ .map((token) => token.trim())
184
+ .filter(Boolean);
185
+ const repos = await searchReposByQuery(query, apiBase);
186
+ if (repos.length === 0) {
187
+ return { agents: [], failures: [] };
188
+ }
189
+ const candidates = repos.slice(0, DEFAULT_REPO_SCAN_LIMIT);
190
+ const scanned = await Promise.allSettled(candidates.map(async (repo) => {
191
+ try {
192
+ return await findAgentsInRepo(repo, tokens, apiBase);
193
+ }
194
+ catch (error) {
195
+ const reason = error instanceof Error ? error.message : String(error);
196
+ throw new Error(`${repo.fullName}: ${reason}`);
197
+ }
198
+ }));
199
+ const flattened = [];
200
+ const failures = [];
201
+ for (const result of scanned) {
202
+ if (result.status === "fulfilled") {
203
+ flattened.push(...result.value);
204
+ continue;
205
+ }
206
+ failures.push(result.reason instanceof Error
207
+ ? result.reason.message
208
+ : String(result.reason));
209
+ }
210
+ if (flattened.length === 0 && failures.length > 0) {
211
+ const details = failures.slice(0, 3).join("; ");
212
+ throw new Error(`Agent search could not complete repository scans (${failures.length} failed): ${details}`);
213
+ }
214
+ flattened.sort(compareRankedFoundAgents);
215
+ const { selected: installable, failures: installabilityFailures } = await selectInstallableCandidates(flattened, limit, apiBase);
216
+ if (installable.length === 0 && installabilityFailures.length > 0) {
217
+ const details = installabilityFailures.slice(0, 3).join("; ");
218
+ throw new Error(`Agent search could not validate candidate agents (${installabilityFailures.length} file fetch${installabilityFailures.length === 1 ? "" : "es"} failed): ${details}`);
219
+ }
220
+ const allFailures = failures.concat(installabilityFailures);
221
+ return {
222
+ agents: installable.map((item) => item.agent),
223
+ failures: allFailures,
224
+ };
225
+ }
226
+ async function searchCommandsWithDiagnostics(query, limit, apiBase) {
227
+ const tokens = tokenizeQuery(query);
228
+ const repos = await searchReposByQuery(query, apiBase);
229
+ const candidates = repos.slice(0, DEFAULT_REPO_SCAN_LIMIT);
230
+ const scanned = await Promise.allSettled(candidates.map(async (repo) => {
231
+ const tree = await getRepoTree(repo, apiBase);
232
+ const matches = [];
233
+ for (const entry of tree) {
234
+ if (entry.type !== "blob")
235
+ continue;
236
+ const filePath = toTrimmedString(entry.path);
237
+ if (!filePath)
238
+ continue;
239
+ const parsed = parseCommandPath(filePath);
240
+ if (!parsed)
241
+ continue;
242
+ const haystack = `${repo.fullName} ${filePath} ${parsed.commandName}`.toLowerCase();
243
+ if (tokens.length > 0 &&
244
+ !tokens.some((token) => haystack.includes(token))) {
245
+ continue;
246
+ }
247
+ matches.push({
248
+ repo: repo.fullName,
249
+ commandName: parsed.commandName,
250
+ filePath,
251
+ fileUrl: `${repo.repoWebUrl}/blob/${repo.defaultBranch}/${filePath}`,
252
+ source: repo.installSource,
253
+ stars: repo.stars,
254
+ subdir: parsed.subdir,
255
+ });
256
+ }
257
+ return matches;
258
+ }));
259
+ return normalizeEntityScanResults(scanned, limit);
260
+ }
261
+ async function searchMcpWithDiagnostics(query, limit, apiBase) {
262
+ const tokens = tokenizeQuery(query);
263
+ const repos = await searchReposByQuery(query, apiBase);
264
+ const candidates = repos.slice(0, DEFAULT_REPO_SCAN_LIMIT);
265
+ const scanned = await Promise.allSettled(candidates.map(async (repo) => {
266
+ const tree = await getRepoTree(repo, apiBase);
267
+ const mcpFiles = tree
268
+ .filter((entry) => entry.type === "blob")
269
+ .map((entry) => toTrimmedString(entry.path))
270
+ .filter(Boolean)
271
+ .filter((filePath) => isMcpPath(filePath));
272
+ const matches = [];
273
+ for (const filePath of mcpFiles) {
274
+ try {
275
+ const content = await fetchRepositoryFile(repo.fullName, filePath, apiBase);
276
+ const parsed = JSON.parse(content);
277
+ const serverNames = Object.keys(parsed.mcpServers ?? {});
278
+ for (const serverName of serverNames) {
279
+ const haystack = `${repo.fullName} ${filePath} ${serverName}`.toLowerCase();
280
+ if (tokens.length > 0 &&
281
+ !tokens.some((token) => haystack.includes(token))) {
282
+ continue;
283
+ }
284
+ matches.push({
285
+ repo: repo.fullName,
286
+ serverName,
287
+ filePath,
288
+ fileUrl: `${repo.repoWebUrl}/blob/${repo.defaultBranch}/${filePath}`,
289
+ source: repo.installSource,
290
+ stars: repo.stars,
291
+ });
292
+ }
293
+ }
294
+ catch {
295
+ // Ignore invalid mcp files and continue scanning.
296
+ }
297
+ }
298
+ return matches;
299
+ }));
300
+ return normalizeEntityScanResults(scanned, limit, (item) => `${item.repo}::${item.filePath}::${item.serverName.toLowerCase()}`);
301
+ }
302
+ async function searchSkillsWithDiagnostics(query, limit, apiBase) {
303
+ const tokens = tokenizeQuery(query);
304
+ const repos = await searchReposByQuery(query, apiBase);
305
+ const candidates = repos.slice(0, DEFAULT_REPO_SCAN_LIMIT);
306
+ const scanned = await Promise.allSettled(candidates.map(async (repo) => {
307
+ const tree = await getRepoTree(repo, apiBase);
308
+ const matches = [];
309
+ for (const entry of tree) {
310
+ if (entry.type !== "blob")
311
+ continue;
312
+ const filePath = toTrimmedString(entry.path);
313
+ if (!filePath)
314
+ continue;
315
+ const parsed = parseSkillPath(filePath);
316
+ if (!parsed)
317
+ continue;
318
+ let skillName = parsed.skillName;
319
+ let installSkillSelector = parsed.skillName;
320
+ if (parsed.isRootSkill) {
321
+ installSkillSelector = undefined;
322
+ try {
323
+ const content = await fetchRepositoryFile(repo.fullName, filePath, apiBase);
324
+ const frontmatterName = parseSkillNameFromMarkdown(content);
325
+ if (frontmatterName) {
326
+ skillName = frontmatterName;
327
+ installSkillSelector = frontmatterName;
328
+ }
329
+ }
330
+ catch {
331
+ // Keep root-skill fallback naming when source content cannot be fetched.
332
+ }
333
+ }
334
+ const haystack = `${repo.fullName} ${filePath} ${skillName}`.toLowerCase();
335
+ if (tokens.length > 0 &&
336
+ !tokens.some((token) => haystack.includes(token))) {
337
+ continue;
338
+ }
339
+ matches.push({
340
+ repo: repo.fullName,
341
+ skillName,
342
+ installSkillSelector,
343
+ filePath,
344
+ fileUrl: `${repo.repoWebUrl}/blob/${repo.defaultBranch}/${filePath}`,
345
+ source: repo.installSource,
346
+ stars: repo.stars,
347
+ subdir: parsed.subdir,
348
+ });
349
+ }
350
+ return matches;
351
+ }));
352
+ return normalizeEntityScanResults(scanned, limit);
353
+ }
354
+ async function searchReposByQuery(query, apiBase) {
355
+ const url = buildApiUrl(apiBase, "search/repositories");
356
+ url.searchParams.set("q", `${query} in:name,description,readme`);
357
+ url.searchParams.set("sort", "stars");
358
+ url.searchParams.set("order", "desc");
359
+ url.searchParams.set("per_page", "20");
360
+ const payload = await requestJson(url);
361
+ if (!isGitHubRepoSearchResponse(payload))
362
+ return [];
363
+ const repos = [];
364
+ for (const item of payload.items) {
365
+ const fullName = toTrimmedString(item.full_name);
366
+ const defaultBranch = toTrimmedString(item.default_branch) || "main";
367
+ if (!fullName)
368
+ continue;
369
+ const starsRaw = item.stargazers_count;
370
+ const stars = typeof starsRaw === "number" && Number.isFinite(starsRaw) && starsRaw > 0
371
+ ? Math.floor(starsRaw)
372
+ : 0;
373
+ const htmlUrl = normalizeUrl(toTrimmedString(item.html_url));
374
+ const cloneUrl = toTrimmedString(item.clone_url);
375
+ const repoWebUrl = htmlUrl ||
376
+ deriveRepoWebUrlFromClone(cloneUrl) ||
377
+ deriveRepoWebUrlFromApiBase(apiBase, fullName);
378
+ const installSource = deriveInstallSource({
379
+ fullName,
380
+ cloneUrl,
381
+ apiBase,
382
+ repoWebUrl,
383
+ });
384
+ repos.push({
385
+ fullName,
386
+ stars,
387
+ defaultBranch,
388
+ repoWebUrl,
389
+ installSource,
390
+ });
391
+ }
392
+ return repos;
393
+ }
394
+ async function findAgentsInRepo(repo, tokens, apiBase) {
395
+ const tree = await getRepoTree(repo, apiBase);
396
+ const matches = [];
397
+ for (const entry of tree) {
398
+ if (entry.type !== "blob")
399
+ continue;
400
+ const filePath = toTrimmedString(entry.path);
401
+ if (!filePath)
402
+ continue;
403
+ const parsed = parseAgentPath(filePath);
404
+ if (!parsed)
405
+ continue;
406
+ const haystack = `${repo.fullName} ${filePath} ${parsed.agentName}`.toLowerCase();
407
+ const matchCount = tokens.filter((token) => haystack.includes(token)).length;
408
+ if (tokens.length > 0 && matchCount === 0)
409
+ continue;
410
+ matches.push({
411
+ score: matchCount,
412
+ agent: {
413
+ repo: repo.fullName,
414
+ agentName: parsed.agentName,
415
+ filePath,
416
+ fileUrl: `${repo.repoWebUrl}/blob/${repo.defaultBranch}/${filePath}`,
417
+ source: repo.installSource,
418
+ stars: repo.stars,
419
+ subdir: parsed.subdir,
420
+ },
421
+ });
422
+ }
423
+ return matches;
424
+ }
425
+ async function getRepoTree(repo, apiBase) {
426
+ const branch = encodeURIComponent(repo.defaultBranch);
427
+ const url = buildApiUrl(apiBase, `repos/${repo.fullName}/git/trees/${branch}`);
428
+ url.searchParams.set("recursive", "1");
429
+ const payload = await requestJson(url);
430
+ if (!isGitHubTreeResponse(payload)) {
431
+ throw new Error(`Agent search returned an invalid tree response for ${repo.fullName}.`);
432
+ }
433
+ return payload.tree;
434
+ }
435
+ function parseAgentPath(filePath) {
436
+ const directAgentloom = filePath.match(/^\.agents\/agents\/([^/]+)\.md$/);
437
+ if (directAgentloom) {
438
+ return { agentName: directAgentloom[1] };
439
+ }
440
+ const nestedAgentloom = filePath.match(/^(.+)\/\.agents\/agents\/([^/]+)\.md$/);
441
+ if (nestedAgentloom) {
442
+ return {
443
+ subdir: nestedAgentloom[1],
444
+ agentName: nestedAgentloom[2],
445
+ };
446
+ }
447
+ const directAgents = filePath.match(/^agents\/([^/]+)\.md$/);
448
+ if (directAgents) {
449
+ return { agentName: directAgents[1] };
450
+ }
451
+ const nestedAgents = filePath.match(/^(.+)\/agents\/([^/]+)\.md$/);
452
+ if (nestedAgents) {
453
+ return {
454
+ subdir: nestedAgents[1],
455
+ agentName: nestedAgents[2],
456
+ };
457
+ }
458
+ return null;
459
+ }
460
+ function parseCommandPath(filePath) {
461
+ const directAgentloom = filePath.match(/^\.agents\/commands\/([^/]+)\.(md|mdc)$/i);
462
+ if (directAgentloom) {
463
+ return { commandName: directAgentloom[1] };
464
+ }
465
+ const nestedAgentloom = filePath.match(/^(.+)\/\.agents\/commands\/([^/]+)\.(md|mdc)$/i);
466
+ if (nestedAgentloom) {
467
+ return {
468
+ subdir: nestedAgentloom[1],
469
+ commandName: nestedAgentloom[2],
470
+ };
471
+ }
472
+ const directCommands = filePath.match(/^commands\/([^/]+)\.(md|mdc)$/i);
473
+ if (directCommands) {
474
+ return { commandName: directCommands[1] };
475
+ }
476
+ const nestedCommands = filePath.match(/^(.+)\/commands\/([^/]+)\.(md|mdc)$/i);
477
+ if (nestedCommands) {
478
+ return {
479
+ subdir: nestedCommands[1],
480
+ commandName: nestedCommands[2],
481
+ };
482
+ }
483
+ const directPrompts = filePath.match(/^prompts\/([^/]+)\.(md|mdc)$/i);
484
+ if (directPrompts) {
485
+ return { commandName: directPrompts[1] };
486
+ }
487
+ const nestedPrompts = filePath.match(/^(.+)\/prompts\/([^/]+)\.(md|mdc)$/i);
488
+ if (nestedPrompts) {
489
+ return {
490
+ subdir: nestedPrompts[1],
491
+ commandName: nestedPrompts[2],
492
+ };
493
+ }
494
+ return null;
495
+ }
496
+ function parseSkillPath(filePath) {
497
+ const directAgentloom = filePath.match(/^\.agents\/skills\/([^/]+)\/SKILL\.md$/i);
498
+ if (directAgentloom) {
499
+ return { skillName: directAgentloom[1] };
500
+ }
501
+ const nestedAgentloom = filePath.match(/^(.+)\/\.agents\/skills\/([^/]+)\/SKILL\.md$/i);
502
+ if (nestedAgentloom) {
503
+ return {
504
+ subdir: nestedAgentloom[1],
505
+ skillName: nestedAgentloom[2],
506
+ };
507
+ }
508
+ const directSkills = filePath.match(/^skills\/([^/]+)\/SKILL\.md$/i);
509
+ if (directSkills) {
510
+ return { skillName: directSkills[1] };
511
+ }
512
+ const nestedSkills = filePath.match(/^(.+)\/skills\/([^/]+)\/SKILL\.md$/i);
513
+ if (nestedSkills) {
514
+ return {
515
+ subdir: nestedSkills[1],
516
+ skillName: nestedSkills[2],
517
+ };
518
+ }
519
+ const directRootSkill = filePath.match(/^SKILL\.md$/i);
520
+ if (directRootSkill) {
521
+ return {
522
+ skillName: "SKILL",
523
+ isRootSkill: true,
524
+ };
525
+ }
526
+ const nestedRootSkill = filePath.match(/^(.+)\/SKILL\.md$/i);
527
+ if (nestedRootSkill) {
528
+ return {
529
+ subdir: nestedRootSkill[1],
530
+ skillName: path.basename(nestedRootSkill[1]),
531
+ isRootSkill: true,
532
+ };
533
+ }
534
+ return null;
535
+ }
536
+ function isMcpPath(filePath) {
537
+ return (/^mcp\.json$/i.test(filePath) ||
538
+ /^\.agents\/mcp\.json$/i.test(filePath) ||
539
+ /\/mcp\.json$/i.test(filePath));
540
+ }
541
+ async function selectInstallableCandidates(candidates, limit, apiBase) {
542
+ const selected = [];
543
+ const failures = [];
544
+ const selectedKeys = new Set();
545
+ let checked = 0;
546
+ for (let index = 0; index < candidates.length &&
547
+ selected.length < limit &&
548
+ checked < MAX_INSTALLABILITY_CHECKS; index += INSTALLABILITY_CHECK_BATCH_SIZE) {
549
+ const maxBatchSize = Math.min(INSTALLABILITY_CHECK_BATCH_SIZE, MAX_INSTALLABILITY_CHECKS - checked);
550
+ const batch = candidates
551
+ .slice(index, index + maxBatchSize)
552
+ .filter((candidate) => !selectedKeys.has(getAgentIdentityKey(candidate.agent)));
553
+ checked += batch.length;
554
+ if (batch.length === 0)
555
+ continue;
556
+ const validated = await Promise.all(batch.map(async (candidate) => ({
557
+ candidate,
558
+ result: await isInstallableCandidate(candidate.agent, apiBase),
559
+ })));
560
+ for (const item of validated) {
561
+ const key = getAgentIdentityKey(item.candidate.agent);
562
+ if (!item.result.installable) {
563
+ if (item.result.failure) {
564
+ failures.push(item.result.failure);
565
+ }
566
+ continue;
567
+ }
568
+ if (selectedKeys.has(key))
569
+ continue;
570
+ selected.push(item.candidate);
571
+ selectedKeys.add(key);
572
+ if (selected.length >= limit) {
573
+ break;
574
+ }
575
+ }
576
+ }
577
+ return { selected, failures };
578
+ }
579
+ function compareRankedFoundAgents(a, b) {
580
+ if (b.score !== a.score)
581
+ return b.score - a.score;
582
+ if (b.agent.stars !== a.agent.stars)
583
+ return b.agent.stars - a.agent.stars;
584
+ const pathRankDelta = getPathRank(a.agent) - getPathRank(b.agent);
585
+ if (pathRankDelta !== 0)
586
+ return pathRankDelta;
587
+ if (a.agent.filePath.length !== b.agent.filePath.length) {
588
+ return a.agent.filePath.length - b.agent.filePath.length;
589
+ }
590
+ const repoDelta = a.agent.repo.localeCompare(b.agent.repo);
591
+ if (repoDelta !== 0)
592
+ return repoDelta;
593
+ return a.agent.filePath.localeCompare(b.agent.filePath);
594
+ }
595
+ function getPathRank(agent) {
596
+ const subdir = agent.subdir?.trim();
597
+ if (!subdir)
598
+ return 0;
599
+ const hasHiddenSegment = subdir
600
+ .split("/")
601
+ .some((segment) => segment.startsWith("."));
602
+ return hasHiddenSegment ? 2 : 1;
603
+ }
604
+ async function isInstallableCandidate(agent, apiBase) {
605
+ let markdown;
606
+ try {
607
+ markdown = await fetchRepositoryFile(agent.repo, agent.filePath, apiBase);
608
+ }
609
+ catch (error) {
610
+ return {
611
+ installable: false,
612
+ failure: `${agent.repo}/${agent.filePath}: ${error instanceof Error ? error.message : String(error)}`,
613
+ };
614
+ }
615
+ try {
616
+ parseAgentMarkdown(markdown, `${agent.repo}/${agent.filePath}`);
617
+ return { installable: true };
618
+ }
619
+ catch {
620
+ return { installable: false };
621
+ }
622
+ }
623
+ function getAgentIdentityKey(agent) {
624
+ return `${agent.repo}::${normalizeSubdirForIdentity(agent.subdir)}::${agent.agentName}`;
625
+ }
626
+ async function fetchRepositoryFile(repo, filePath, apiBase) {
627
+ const encodedPath = filePath
628
+ .split("/")
629
+ .map((segment) => encodeURIComponent(segment))
630
+ .join("/");
631
+ const url = buildApiUrl(apiBase, `repos/${repo}/contents/${encodedPath}`);
632
+ const payload = await requestJson(url);
633
+ if (!isGitHubContentsResponse(payload)) {
634
+ throw new Error(`Invalid repository file response for ${repo}/${filePath}.`);
635
+ }
636
+ const base64 = payload.content.replace(/\n/g, "");
637
+ return Buffer.from(base64, "base64").toString("utf8");
638
+ }
639
+ function normalizeSearchResult(value) {
640
+ if (Array.isArray(value)) {
641
+ return { agents: value, failures: [] };
642
+ }
643
+ return value;
644
+ }
645
+ function normalizeEntityScanResults(scanned, limit, getKey = (item) => `${item.repo}::${item.filePath}`) {
646
+ const items = [];
647
+ const failures = [];
648
+ const seen = new Set();
649
+ for (const result of scanned) {
650
+ if (result.status === "fulfilled") {
651
+ for (const item of result.value) {
652
+ const key = getKey(item);
653
+ if (seen.has(key))
654
+ continue;
655
+ seen.add(key);
656
+ items.push(item);
657
+ }
658
+ continue;
659
+ }
660
+ failures.push(result.reason instanceof Error
661
+ ? result.reason.message
662
+ : String(result.reason));
663
+ }
664
+ return {
665
+ items: items.slice(0, limit),
666
+ failures,
667
+ };
668
+ }
669
+ function buildInstallCommand(result) {
670
+ const source = result.source?.trim() || result.repo;
671
+ const repoArg = quoteShellArg(source);
672
+ if (result.subdir && result.subdir.trim()) {
673
+ return `agentloom add ${repoArg} --subdir ${quoteShellArg(result.subdir)}`;
674
+ }
675
+ return `agentloom add ${repoArg}`;
676
+ }
677
+ function buildCommandInstallCommand(result) {
678
+ const source = result.source?.trim() || result.repo;
679
+ const repoArg = quoteShellArg(source);
680
+ if (result.subdir && result.subdir.trim()) {
681
+ return `agentloom command add ${repoArg} --subdir ${quoteShellArg(result.subdir)}`;
682
+ }
683
+ return `agentloom command add ${repoArg}`;
684
+ }
685
+ function buildMcpInstallCommand(result) {
686
+ const source = result.source?.trim() || result.repo;
687
+ return `agentloom mcp add ${quoteShellArg(source)} --mcps ${quoteShellArg(result.serverName)}`;
688
+ }
689
+ function buildSkillInstallCommand(result) {
690
+ const source = result.source?.trim() || result.repo;
691
+ const repoArg = quoteShellArg(source);
692
+ const selector = result.installSkillSelector?.trim();
693
+ if (result.subdir && result.subdir.trim()) {
694
+ if (!selector) {
695
+ return `agentloom skill add ${repoArg} --subdir ${quoteShellArg(result.subdir)}`;
696
+ }
697
+ return `agentloom skill add ${repoArg} --subdir ${quoteShellArg(result.subdir)} --skills ${quoteShellArg(selector)}`;
698
+ }
699
+ if (!selector) {
700
+ return `agentloom skill add ${repoArg}`;
701
+ }
702
+ return `agentloom skill add ${repoArg} --skills ${quoteShellArg(selector)}`;
703
+ }
704
+ function tokenizeQuery(query) {
705
+ return query
706
+ .toLowerCase()
707
+ .split(/\s+/)
708
+ .map((token) => token.trim())
709
+ .filter(Boolean);
710
+ }
711
+ function parseSkillNameFromMarkdown(markdown) {
712
+ try {
713
+ const parsed = matter(markdown);
714
+ if (typeof parsed.data.name !== "string")
715
+ return undefined;
716
+ const name = parsed.data.name.trim();
717
+ return name.length > 0 ? name : undefined;
718
+ }
719
+ catch {
720
+ return undefined;
721
+ }
722
+ }
723
+ function searchLocalMatches(query, target) {
724
+ const tokens = tokenizeQuery(query);
725
+ const paths = buildScopePaths(process.cwd(), "local");
726
+ const lines = [];
727
+ if ((target === "all" || target === "agent") && fsExists(paths.agentsDir)) {
728
+ const agents = parseAgentsDir(paths.agentsDir);
729
+ for (const agent of agents) {
730
+ if (!matchesTokens(`${agent.name} ${agent.fileName}`, tokens))
731
+ continue;
732
+ lines.push(`agent: ${agent.name} (${agent.fileName})`);
733
+ }
734
+ }
735
+ if ((target === "all" || target === "command") &&
736
+ fsExists(paths.commandsDir)) {
737
+ const commands = parseCommandsDir(paths.commandsDir);
738
+ for (const command of commands) {
739
+ if (!matchesTokens(command.fileName, tokens))
740
+ continue;
741
+ lines.push(`command: ${command.fileName}`);
742
+ }
743
+ }
744
+ if ((target === "all" || target === "mcp") && fsExists(paths.mcpPath)) {
745
+ const mcp = readCanonicalMcp(paths);
746
+ for (const name of Object.keys(mcp.mcpServers)) {
747
+ if (!matchesTokens(name, tokens))
748
+ continue;
749
+ lines.push(`mcp: ${name}`);
750
+ }
751
+ }
752
+ if ((target === "all" || target === "skill") && fsExists(paths.skillsDir)) {
753
+ const skills = parseSkillsDir(paths.skillsDir);
754
+ for (const skill of skills) {
755
+ if (!matchesTokens(skill.name, tokens))
756
+ continue;
757
+ lines.push(`skill: ${skill.name}`);
758
+ }
759
+ }
760
+ return {
761
+ lines,
762
+ total: lines.length,
763
+ };
764
+ }
765
+ function matchesTokens(value, tokens) {
766
+ if (tokens.length === 0)
767
+ return true;
768
+ const haystack = value.toLowerCase();
769
+ return tokens.every((token) => haystack.includes(token));
770
+ }
771
+ function fsExists(value) {
772
+ return fs.existsSync(value);
773
+ }
774
+ function normalizeSubdirForIdentity(subdir) {
775
+ if (!subdir)
776
+ return "";
777
+ return subdir
778
+ .split("/")
779
+ .map((segment) => segment.trim())
780
+ .filter(Boolean)
781
+ .filter((segment) => !segment.startsWith("."))
782
+ .join("/");
783
+ }
784
+ function normalizeUrl(value) {
785
+ if (!value)
786
+ return "";
787
+ return value.replace(/\/+$/, "");
788
+ }
789
+ function deriveRepoWebUrlFromClone(cloneUrl) {
790
+ if (!cloneUrl)
791
+ return "";
792
+ const gitSshMatch = cloneUrl.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
793
+ if (gitSshMatch) {
794
+ return `https://${gitSshMatch[1]}/${gitSshMatch[2]}`;
795
+ }
796
+ try {
797
+ const parsed = new URL(cloneUrl);
798
+ parsed.pathname = parsed.pathname.replace(/\.git$/, "");
799
+ parsed.search = "";
800
+ parsed.hash = "";
801
+ return normalizeUrl(parsed.toString());
802
+ }
803
+ catch {
804
+ return "";
805
+ }
806
+ }
807
+ function deriveRepoWebUrlFromApiBase(apiBase, fullName) {
808
+ const webBase = deriveWebBaseFromApiBase(apiBase);
809
+ if (!webBase.pathname.endsWith("/")) {
810
+ webBase.pathname = `${webBase.pathname}/`;
811
+ }
812
+ const repoUrl = new URL(fullName, webBase);
813
+ return normalizeUrl(repoUrl.toString());
814
+ }
815
+ function deriveInstallSource(options) {
816
+ const cloneUrl = options.cloneUrl.trim();
817
+ if (cloneUrl) {
818
+ if (isPublicGitHubCloneUrl(cloneUrl)) {
819
+ return options.fullName;
820
+ }
821
+ return cloneUrl;
822
+ }
823
+ if (isPublicGitHubApi(options.apiBase)) {
824
+ return options.fullName;
825
+ }
826
+ return `${options.repoWebUrl}.git`;
827
+ }
828
+ function deriveWebBaseFromApiBase(apiBase) {
829
+ const base = new URL(apiBase);
830
+ base.search = "";
831
+ base.hash = "";
832
+ let pathname = base.pathname.replace(/\/+$/, "");
833
+ if (pathname.endsWith("/api/v3")) {
834
+ pathname = pathname.slice(0, -"/api/v3".length);
835
+ }
836
+ else if (pathname.endsWith("/api")) {
837
+ pathname = pathname.slice(0, -"/api".length);
838
+ }
839
+ base.pathname = pathname || "/";
840
+ return base;
841
+ }
842
+ function isPublicGitHubApi(apiBase) {
843
+ try {
844
+ const host = new URL(apiBase).hostname.toLowerCase();
845
+ return host === "api.github.com" || host === "github.com";
846
+ }
847
+ catch {
848
+ return false;
849
+ }
850
+ }
851
+ function isPublicGitHubCloneUrl(cloneUrl) {
852
+ if (cloneUrl.startsWith("git@github.com:"))
853
+ return true;
854
+ try {
855
+ return new URL(cloneUrl).hostname.toLowerCase() === "github.com";
856
+ }
857
+ catch {
858
+ return false;
859
+ }
860
+ }
861
+ function quoteShellArg(value) {
862
+ if (value.length === 0)
863
+ return "''";
864
+ if (/^[A-Za-z0-9_./-]+$/.test(value) && !value.startsWith("-"))
865
+ return value;
866
+ return `'${value.replace(/'/g, "'\"'\"'")}'`;
867
+ }
868
+ function buildApiUrl(apiBase, relativePath) {
869
+ const baseUrl = new URL(apiBase);
870
+ if (!baseUrl.pathname.endsWith("/")) {
871
+ baseUrl.pathname = `${baseUrl.pathname}/`;
872
+ }
873
+ return new URL(relativePath.replace(/^\/+/, ""), baseUrl);
874
+ }
875
+ function formatStars(stars) {
876
+ if (!stars || stars <= 0)
877
+ return "";
878
+ return ` (${stars}★)`;
879
+ }
880
+ function toTrimmedString(value) {
881
+ if (typeof value !== "string")
882
+ return "";
883
+ return value.trim();
884
+ }
885
+ function isGitHubRepoSearchResponse(value) {
886
+ return (typeof value === "object" &&
887
+ value !== null &&
888
+ Array.isArray(value.items));
889
+ }
890
+ function isGitHubTreeResponse(value) {
891
+ return (typeof value === "object" &&
892
+ value !== null &&
893
+ Array.isArray(value.tree));
894
+ }
895
+ function isGitHubContentsResponse(value) {
896
+ return (typeof value === "object" &&
897
+ value !== null &&
898
+ value.type === "file" &&
899
+ typeof value.content === "string" &&
900
+ value.encoding === "base64");
901
+ }
902
+ function requestJson(url) {
903
+ return new Promise((resolve, reject) => {
904
+ const headers = {
905
+ Accept: "application/vnd.github+json",
906
+ "User-Agent": "agentloom-cli",
907
+ "X-GitHub-Api-Version": "2022-11-28",
908
+ };
909
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
910
+ if (token) {
911
+ headers.Authorization = `Bearer ${token}`;
912
+ }
913
+ const req = https.get(url, {
914
+ headers,
915
+ }, (res) => {
916
+ const chunks = [];
917
+ res.on("data", (chunk) => {
918
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
919
+ });
920
+ res.on("end", () => {
921
+ const raw = Buffer.concat(chunks).toString("utf8");
922
+ if (!res.statusCode ||
923
+ res.statusCode < 200 ||
924
+ res.statusCode >= 300) {
925
+ reject(new Error(`Agent search failed with status ${res.statusCode ?? 0}. ${raw ? `Response: ${raw}` : ""}`.trim()));
926
+ return;
927
+ }
928
+ try {
929
+ resolve(JSON.parse(raw));
930
+ }
931
+ catch {
932
+ reject(new Error("Agent search returned invalid JSON."));
933
+ }
934
+ });
935
+ });
936
+ req.setTimeout(REQUEST_TIMEOUT_MS, () => {
937
+ req.destroy();
938
+ reject(new Error("Agent search request timed out."));
939
+ });
940
+ req.on("error", (error) => {
941
+ reject(new Error(`Agent search request failed: ${error instanceof Error ? error.message : String(error)}`));
942
+ });
943
+ });
944
+ }