agentwheel 0.15.0 → 0.16.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/README.md CHANGED
@@ -98,6 +98,7 @@ Fragments are Agentwheel composition inputs, not runtime file-drop targets.
98
98
 
99
99
  | Command | Meaning |
100
100
  |---|---|
101
+ | `agentwheel search <query>` | Search configured registries and the public enriched/Vercel catalogue; supports stable JSON output for agent reranking. |
101
102
  | `agentwheel add <source>` | Validate and save a package entry in `.agentwheel/config.json`; does not touch runtimes. |
102
103
  | `agentwheel plan [name-or-source]` | Preview what `install` would reconcile without writing; supports `--profile <name>` and `--json`. |
103
104
  | `agentwheel install` | Reconcile configured packages into the current target or selected fleet. Uses the graph lock as input by default. |
@@ -152,6 +153,28 @@ agentwheel plan
152
153
  agentwheel install
153
154
  ```
154
155
 
156
+ ## Artifact Discovery
157
+
158
+ Search configured registries and the complete public catalogue with one command:
159
+
160
+ ```bash
161
+ agentwheel search "conversation memory"
162
+ agentwheel search "telegram integration" --type skill
163
+ agentwheel search "message recall" --json --limit 10
164
+ ```
165
+
166
+ Use `--scope registry`, `--scope enriched`, or `--scope vercel` to restrict a query. The default
167
+ `--scope all` combines every source, deduplicates equivalent artifacts, and reports every
168
+ provenance plus the safe installation route.
169
+
170
+ Search is deterministic and lexical. The companion skill adds semantic behavior at the agent
171
+ layer: it can generate a small set of related queries, merge and rerank the JSON results against
172
+ the original request, and suggest at most three artifacts. Search never installs or changes
173
+ configuration by itself.
174
+
175
+ Registry maintenance remains available through `agentwheel registry update` and
176
+ `agentwheel registry list`. Registry short names continue to resolve during add/install.
177
+
155
178
  ## Source Inputs
156
179
 
157
180
  Agentwheel can install from explicit local paths, Git sources, catalogue short names, provider
package/dist/index.js CHANGED
@@ -9,11 +9,11 @@ import {
9
9
  } from "./chunk-PKAPR55N.js";
10
10
 
11
11
  // src/cli/index.ts
12
- import { createHash as createHash11 } from "crypto";
12
+ import { createHash as createHash12 } from "crypto";
13
13
  import { existsSync } from "fs";
14
- import { mkdir as mkdir23, rm as rm11, writeFile as writeFile22 } from "fs/promises";
15
- import { homedir as homedir9 } from "os";
16
- import { dirname as dirname32, join as join44, resolve as resolve22 } from "path";
14
+ import { mkdir as mkdir23, rm as rm12, writeFile as writeFile22 } from "fs/promises";
15
+ import { homedir as homedir10 } from "os";
16
+ import { dirname as dirname32, join as join45, resolve as resolve22 } from "path";
17
17
  import { fileURLToPath as fileURLToPath3 } from "url";
18
18
  import { Command } from "commander";
19
19
 
@@ -7759,13 +7759,6 @@ var RegistryClient = class {
7759
7759
  const index = await this.getIndex(options);
7760
7760
  return index.entries.find((entry) => entry.name === name);
7761
7761
  }
7762
- async search(query, options = {}) {
7763
- const q = query.toLowerCase();
7764
- const index = await this.getIndex(options);
7765
- return index.entries.filter(
7766
- (entry) => entry.name.toLowerCase().includes(q) || entry.description.toLowerCase().includes(q) || entry.tags.some((tag) => tag.toLowerCase().includes(q))
7767
- );
7768
- }
7769
7762
  async clearCache() {
7770
7763
  await rm8(this.cachePath, { force: true });
7771
7764
  }
@@ -11318,6 +11311,628 @@ function valueAfter(lines, prefix) {
11318
11311
  return lines.find((line) => line.startsWith(prefix))?.slice(prefix.length).trim() ?? null;
11319
11312
  }
11320
11313
 
11314
+ // src/catalogue/client.ts
11315
+ import { createHash as createHash11 } from "crypto";
11316
+ import { readFile as readFile32, rm as rm11 } from "fs/promises";
11317
+ import { homedir as homedir9 } from "os";
11318
+ import { join as join44 } from "path";
11319
+
11320
+ // src/model/catalogue.ts
11321
+ import { z as z13 } from "zod";
11322
+ var searchScopeSchema = z13.enum(["all", "registry", "enriched", "vercel"]);
11323
+ var searchTypeSchema = z13.enum(["package", "skill", "plugin", "mcp", "adapter"]);
11324
+ var searchEcosystemSchema = z13.enum([
11325
+ "official",
11326
+ "openpack",
11327
+ "mcp-registry",
11328
+ "clawhub",
11329
+ "skillkit",
11330
+ "vercel"
11331
+ ]);
11332
+ var catalogueProvenanceSchema = z13.enum(["registry", "enriched", "vercel"]);
11333
+ var installabilitySchema = z13.enum(["registry", "source", "informational"]);
11334
+ var nullableString = z13.string().nullable();
11335
+ var nullableStringArray = z13.array(z13.string()).nullable();
11336
+ var enrichedCatalogueEntrySchema = z13.object({
11337
+ id: z13.string().min(1),
11338
+ name: z13.string().min(1),
11339
+ ecosystem: searchEcosystemSchema.nullable(),
11340
+ type: searchTypeSchema.nullable(),
11341
+ description: nullableString,
11342
+ tags: nullableStringArray,
11343
+ source: nullableString,
11344
+ installCommand: nullableString,
11345
+ repoUrl: nullableString,
11346
+ homepageUrl: nullableString.optional(),
11347
+ homepageLinkLabel: nullableString.optional(),
11348
+ stars: z13.number().finite().nullable().optional(),
11349
+ lastPush: nullableString.optional(),
11350
+ archived: z13.boolean().nullable(),
11351
+ provides: nullableStringArray,
11352
+ version: nullableString,
11353
+ featured: z13.boolean().nullable().optional()
11354
+ });
11355
+ var enrichedCatalogueSchema = z13.object({
11356
+ schemaVersion: z13.literal(1),
11357
+ generatedAt: z13.string().datetime(),
11358
+ entries: z13.array(enrichedCatalogueEntrySchema)
11359
+ }).superRefine((value, context) => {
11360
+ const seen = /* @__PURE__ */ new Set();
11361
+ value.entries.forEach((entry, index) => {
11362
+ if (seen.has(entry.id)) {
11363
+ context.addIssue({
11364
+ code: "custom",
11365
+ path: ["entries", index, "id"],
11366
+ message: `duplicate catalogue id: ${entry.id}`
11367
+ });
11368
+ }
11369
+ seen.add(entry.id);
11370
+ });
11371
+ });
11372
+ var vercelCatalogueEntrySchema = z13.object({
11373
+ o: z13.string().min(1),
11374
+ r: z13.string().min(1),
11375
+ s: z13.string().min(1),
11376
+ d: z13.string().nullable().optional()
11377
+ });
11378
+ var vercelCatalogueSchema = z13.object({
11379
+ schemaVersion: z13.literal(1),
11380
+ generatedAt: z13.string().datetime(),
11381
+ count: z13.number().int().nonnegative(),
11382
+ entries: z13.array(vercelCatalogueEntrySchema)
11383
+ }).superRefine((value, context) => {
11384
+ if (value.count !== value.entries.length) {
11385
+ context.addIssue({
11386
+ code: "custom",
11387
+ path: ["count"],
11388
+ message: `count must equal entries length (${value.entries.length})`
11389
+ });
11390
+ }
11391
+ const seen = /* @__PURE__ */ new Set();
11392
+ value.entries.forEach((entry, index) => {
11393
+ const id = `${entry.o}/${entry.r}/${entry.s}`;
11394
+ if (seen.has(id)) {
11395
+ context.addIssue({
11396
+ code: "custom",
11397
+ path: ["entries", index],
11398
+ message: `duplicate Vercel catalogue id: ${id}`
11399
+ });
11400
+ }
11401
+ seen.add(id);
11402
+ });
11403
+ });
11404
+ var catalogueCacheSchema = z13.object({
11405
+ version: z13.literal(1),
11406
+ fetchedAt: z13.string().datetime(),
11407
+ sources: z13.tuple([z13.string().url(), z13.string().url()]),
11408
+ enriched: enrichedCatalogueSchema,
11409
+ vercel: vercelCatalogueSchema
11410
+ });
11411
+ var catalogueCacheEnvelopeSchema = z13.object({
11412
+ version: z13.literal(1),
11413
+ fetchedAt: z13.string().datetime(),
11414
+ sources: z13.tuple([z13.string().url(), z13.string().url()]),
11415
+ contentHash: z13.string().regex(/^[a-f0-9]{64}$/).optional(),
11416
+ enriched: z13.unknown(),
11417
+ vercel: z13.unknown()
11418
+ });
11419
+ var searchResultSchema = z13.object({
11420
+ id: z13.string().min(1),
11421
+ name: z13.string().min(1),
11422
+ description: z13.string(),
11423
+ type: searchTypeSchema,
11424
+ ecosystem: searchEcosystemSchema.optional(),
11425
+ tags: z13.array(z13.string()),
11426
+ provides: z13.array(z13.string()),
11427
+ source: z13.string().min(1).optional(),
11428
+ repoUrl: z13.string().min(1).optional(),
11429
+ installCommand: z13.string().min(1).optional(),
11430
+ installability: installabilitySchema,
11431
+ provenances: z13.array(catalogueProvenanceSchema).min(1),
11432
+ score: z13.number().int().nonnegative(),
11433
+ matchedFields: z13.array(z13.string())
11434
+ });
11435
+ var searchResponseSchema = z13.object({
11436
+ schemaVersion: z13.literal(1),
11437
+ query: z13.string(),
11438
+ scope: searchScopeSchema,
11439
+ fromCache: z13.boolean(),
11440
+ results: z13.array(searchResultSchema)
11441
+ });
11442
+
11443
+ // src/catalogue/client.ts
11444
+ var DEFAULT_ENRICHED_CATALOGUE_URL = "https://raw.githubusercontent.com/NestDevLab/agentwheel-registry/main/catalogue-data.json";
11445
+ var DEFAULT_VERCEL_CATALOGUE_URL = "https://raw.githubusercontent.com/NestDevLab/agentwheel-registry/main/catalogue-vercel-index.json";
11446
+ var DEFAULT_CATALOGUE_TTL_MS = 24 * 60 * 60 * 1e3;
11447
+ var MAX_CATALOGUE_PAYLOAD_BYTES = 32 * 1024 * 1024;
11448
+ var CatalogueClient = class {
11449
+ constructor(options = {}) {
11450
+ this.options = options;
11451
+ this.cachePath = options.cachePath ?? defaultCatalogueCachePath();
11452
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
11453
+ this.fetchImpl = options.fetch ?? fetch;
11454
+ this.sources = [
11455
+ options.enrichedUrl ?? DEFAULT_ENRICHED_CATALOGUE_URL,
11456
+ options.vercelUrl ?? DEFAULT_VERCEL_CATALOGUE_URL
11457
+ ];
11458
+ }
11459
+ options;
11460
+ cachePath;
11461
+ now;
11462
+ fetchImpl;
11463
+ sources;
11464
+ async getIndex(options = {}) {
11465
+ const cached = await this.readCache();
11466
+ const usableCache = cached && sameSources2(cached.sources, this.sources) ? cached : void 0;
11467
+ const expired = usableCache ? this.isExpired(usableCache) : false;
11468
+ if (this.options.offline) {
11469
+ if (!usableCache) {
11470
+ throw new Error("Offline catalogue cache is missing. Run without --offline first.");
11471
+ }
11472
+ const stale = expired;
11473
+ this.options.warn?.(
11474
+ stale ? "Offline: using stale catalogue cache because refresh is disabled." : "Offline: using cached catalogue data."
11475
+ );
11476
+ return this.fromCache(usableCache, stale);
11477
+ }
11478
+ if (!options.refresh && usableCache && !expired) {
11479
+ return this.fromCache(usableCache, false);
11480
+ }
11481
+ try {
11482
+ const [enriched, vercel] = await Promise.all([
11483
+ this.fetchJson(this.sources[0], enrichedCatalogueSchema),
11484
+ this.fetchJson(this.sources[1], vercelCatalogueSchema)
11485
+ ]);
11486
+ const fetchedAt = this.now().toISOString();
11487
+ const cache = {
11488
+ version: 1,
11489
+ fetchedAt,
11490
+ sources: this.sources,
11491
+ enriched,
11492
+ vercel
11493
+ };
11494
+ const cacheFile = {
11495
+ ...cache,
11496
+ contentHash: catalogueContentHash(enriched, vercel)
11497
+ };
11498
+ await writeJsonAtomic(this.cachePath, cacheFile);
11499
+ return { enriched, vercel, sources: this.sources, fetchedAt, fromCache: false, stale: false };
11500
+ } catch (error) {
11501
+ if (!usableCache) throw error;
11502
+ const reason = error instanceof Error ? error.message : String(error);
11503
+ this.options.warn?.(`Catalogue refresh failed; using stale catalogue cache: ${reason}`);
11504
+ return this.fromCache(usableCache, true);
11505
+ }
11506
+ }
11507
+ async clearCache() {
11508
+ await rm11(this.cachePath, { force: true });
11509
+ }
11510
+ async readCache() {
11511
+ if (!await pathExists(this.cachePath)) return void 0;
11512
+ try {
11513
+ const value = JSON.parse(await readFile32(this.cachePath, "utf8"));
11514
+ const envelope = catalogueCacheEnvelopeSchema.parse(value);
11515
+ if (envelope.contentHash) {
11516
+ const contentHash = catalogueContentHash(envelope.enriched, envelope.vercel);
11517
+ if (contentHash !== envelope.contentHash) {
11518
+ throw new Error("catalogue cache integrity check failed");
11519
+ }
11520
+ return envelope;
11521
+ }
11522
+ return catalogueCacheSchema.parse(value);
11523
+ } catch (error) {
11524
+ const reason = error instanceof Error ? error.message : String(error);
11525
+ this.options.warn?.(`Ignoring invalid catalogue cache: ${reason}`);
11526
+ return void 0;
11527
+ }
11528
+ }
11529
+ isExpired(cache) {
11530
+ const ttlMs = this.options.ttlMs ?? DEFAULT_CATALOGUE_TTL_MS;
11531
+ return this.now().getTime() - new Date(cache.fetchedAt).getTime() > ttlMs;
11532
+ }
11533
+ fromCache(cache, stale) {
11534
+ return {
11535
+ enriched: cache.enriched,
11536
+ vercel: cache.vercel,
11537
+ sources: cache.sources,
11538
+ fetchedAt: cache.fetchedAt,
11539
+ fromCache: true,
11540
+ stale
11541
+ };
11542
+ }
11543
+ async fetchJson(source, schema) {
11544
+ const response = await this.fetchImpl(source);
11545
+ if (!response.ok) {
11546
+ throw new Error(`Catalogue source failed (${response.status}): ${source}`);
11547
+ }
11548
+ const declaredLength = response.headers.get("content-length");
11549
+ if (declaredLength !== null) {
11550
+ const bytes = Number(declaredLength);
11551
+ if (Number.isFinite(bytes) && bytes > MAX_CATALOGUE_PAYLOAD_BYTES) {
11552
+ throw new Error(`Catalogue payload exceeds 32 MiB limit: ${source}`);
11553
+ }
11554
+ }
11555
+ const payload = await response.arrayBuffer();
11556
+ if (payload.byteLength > MAX_CATALOGUE_PAYLOAD_BYTES) {
11557
+ throw new Error(`Catalogue payload exceeds 32 MiB limit: ${source}`);
11558
+ }
11559
+ let value;
11560
+ try {
11561
+ value = JSON.parse(new TextDecoder().decode(payload));
11562
+ } catch {
11563
+ throw new Error(`Catalogue source returned invalid JSON: ${source}`);
11564
+ }
11565
+ return schema.parse(value);
11566
+ }
11567
+ };
11568
+ function defaultCatalogueCachePath() {
11569
+ return join44(homedir9(), ".agentwheel", "catalogue-cache.json");
11570
+ }
11571
+ function sameSources2(a, b) {
11572
+ return a.length === b.length && a.every((source, index) => source === b[index]);
11573
+ }
11574
+ function catalogueContentHash(enriched, vercel) {
11575
+ return createHash11("sha256").update(JSON.stringify({ enriched, vercel })).digest("hex");
11576
+ }
11577
+
11578
+ // src/search/index.ts
11579
+ var SCORE = {
11580
+ exactName: 1e4,
11581
+ namePrefix: 5e3,
11582
+ namePhrase: 3e3,
11583
+ tagProvidesPhrase: 2e3,
11584
+ descriptionPhrase: 1e3,
11585
+ typeEcosystemPhrase: 800,
11586
+ repositoryPhrase: 400,
11587
+ nameToken: 300,
11588
+ nameTokenPrefix: 200,
11589
+ tagProvidesToken: 180,
11590
+ descriptionToken: 80,
11591
+ typeEcosystemToken: 60,
11592
+ repositoryToken: 40,
11593
+ allTerms: 500
11594
+ };
11595
+ var PROVENANCE_ORDER = ["registry", "enriched", "vercel"];
11596
+ var MATCHED_FIELD_ORDER = ["name", "tags", "provides", "description", "type", "ecosystem", "repository"];
11597
+ function buildSearchEntries(input) {
11598
+ const enriched = catalogueEntries(input.enriched);
11599
+ const vercel = catalogueEntries(input.vercel);
11600
+ assertUniqueIdentities(enriched.map((entry) => entry.id), "enriched catalogue");
11601
+ assertUniqueIdentities(vercel.map((entry) => `${entry.o}/${entry.r}/${entry.s}`), "Vercel catalogue");
11602
+ const records = [
11603
+ ...(input.registry ?? []).map(normalizeRegistryEntry),
11604
+ ...enriched.map(normalizeEnrichedEntry),
11605
+ ...vercel.map(normalizeVercelEntry)
11606
+ ];
11607
+ const byId = /* @__PURE__ */ new Map();
11608
+ for (const record of records) {
11609
+ const existing = byId.get(record.id);
11610
+ byId.set(record.id, existing ? mergeEntry(existing, record) : record);
11611
+ }
11612
+ for (const [registryId, registryEntry] of [...byId]) {
11613
+ if (registryEntry.provenances.length !== 1 || registryEntry.provenances[0] !== "registry" || registryEntry.hasRegistrySelectors || !registryEntry.source) {
11614
+ continue;
11615
+ }
11616
+ const canonicalSource = canonicalizeSource(registryEntry.source);
11617
+ const candidates = [...byId.entries()].filter(
11618
+ ([candidateId2, candidate2]) => candidateId2 !== registryId && !candidate2.provenances.includes("registry") && candidate2.name === registryEntry.name && candidate2.source !== void 0 && canonicalizeSource(candidate2.source) === canonicalSource
11619
+ );
11620
+ if (candidates.length !== 1) continue;
11621
+ const [candidateId, candidate] = candidates[0];
11622
+ byId.set(candidateId, mergeEntry(registryEntry, candidate, candidateId));
11623
+ byId.delete(registryId);
11624
+ }
11625
+ return [...byId.values()].sort(compareStableEntries);
11626
+ }
11627
+ function searchEntries(entries, query, options = {}) {
11628
+ const normalizedQuery = normalizeSearchText(query);
11629
+ const queryTokens = tokenizeSearchText(query);
11630
+ if (!normalizedQuery || queryTokens.length === 0) return [];
11631
+ const results = entries.filter((entry) => options.includeArchived || !entry.archived).filter((entry) => options.type === void 0 || entry.type === options.type).filter((entry) => options.ecosystem === void 0 || entry.ecosystem === options.ecosystem).map((entry) => ({ entry, result: scoreEntry(entry, normalizedQuery, queryTokens) })).filter(({ result }) => result.score > 0).sort(
11632
+ (a, b) => b.result.score - a.result.score || Number(b.entry.featured) - Number(a.entry.featured) || (b.entry.stars ?? Number.NEGATIVE_INFINITY) - (a.entry.stars ?? Number.NEGATIVE_INFINITY) || compareText(b.entry.lastPush ?? "", a.entry.lastPush ?? "") || compareText(normalizeSearchText(a.result.name), normalizeSearchText(b.result.name)) || compareText(a.result.id, b.result.id)
11633
+ ).map(({ result }) => result);
11634
+ const limit = options.limit === void 0 ? 20 : Math.min(100, Math.max(0, Math.trunc(options.limit)));
11635
+ return results.slice(0, limit);
11636
+ }
11637
+ function normalizeSearchText(value) {
11638
+ return value.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim().replace(/\s+/g, " ");
11639
+ }
11640
+ function tokenizeSearchText(value) {
11641
+ const normalized = normalizeSearchText(value);
11642
+ return normalized ? [...new Set(normalized.split(" "))] : [];
11643
+ }
11644
+ function normalizeRegistryEntry(entry) {
11645
+ return {
11646
+ id: `registry:${entry.name}`,
11647
+ name: entry.name,
11648
+ description: entry.description,
11649
+ type: entry.type,
11650
+ ecosystem: inferEcosystem(entry.source),
11651
+ tags: sortedUniqueStrings(entry.tags),
11652
+ provides: [],
11653
+ source: entry.source,
11654
+ installCommand: `npx agentwheel install ${shellQuote2(entry.name)}`,
11655
+ installability: "registry",
11656
+ provenances: ["registry"],
11657
+ archived: false,
11658
+ featured: false,
11659
+ alternateDescriptions: [],
11660
+ descriptionRank: 2,
11661
+ hasRegistrySelectors: Boolean(entry.select?.length || entry.skills?.length)
11662
+ };
11663
+ }
11664
+ function normalizeEnrichedEntry(entry) {
11665
+ const source = nonEmpty(entry.source);
11666
+ const installCommand = enrichedInstallCommand(entry, source);
11667
+ return {
11668
+ id: entry.id,
11669
+ name: entry.name,
11670
+ description: entry.description ?? "",
11671
+ type: entry.type ?? inferType(entry.ecosystem),
11672
+ ecosystem: entry.ecosystem ?? void 0,
11673
+ tags: sortedUniqueStrings(entry.tags ?? []),
11674
+ provides: sortedUniqueStrings(entry.provides ?? []),
11675
+ source,
11676
+ repoUrl: nonEmpty(entry.repoUrl),
11677
+ installCommand,
11678
+ installability: source || installCommand ? "source" : "informational",
11679
+ provenances: ["enriched"],
11680
+ archived: entry.archived ?? false,
11681
+ featured: entry.featured ?? false,
11682
+ stars: entry.stars ?? void 0,
11683
+ lastPush: nonEmpty(entry.lastPush),
11684
+ alternateDescriptions: [],
11685
+ descriptionRank: 3,
11686
+ hasRegistrySelectors: false
11687
+ };
11688
+ }
11689
+ function normalizeVercelEntry(entry) {
11690
+ const path = `${entry.o}/${entry.r}/${entry.s}`;
11691
+ const source = `vercel:skills.sh/${path}`;
11692
+ return {
11693
+ id: `vercel:${path}`,
11694
+ name: entry.s,
11695
+ description: entry.d ?? "",
11696
+ type: "skill",
11697
+ ecosystem: "vercel",
11698
+ tags: [],
11699
+ provides: ["skills"],
11700
+ source,
11701
+ repoUrl: `https://github.com/${entry.o}/${entry.r}`,
11702
+ installCommand: `npx agentwheel install ${shellQuote2(source)}`,
11703
+ installability: "source",
11704
+ provenances: ["vercel"],
11705
+ archived: false,
11706
+ featured: false,
11707
+ alternateDescriptions: [],
11708
+ descriptionRank: 1,
11709
+ hasRegistrySelectors: false
11710
+ };
11711
+ }
11712
+ function mergeEntry(first, second, id = first.id) {
11713
+ const primary = second.description && second.descriptionRank > first.descriptionRank ? second : first;
11714
+ const secondary = primary === first ? second : first;
11715
+ const descriptions = uniqueStrings([
11716
+ primary.description,
11717
+ ...primary.alternateDescriptions,
11718
+ secondary.description,
11719
+ ...secondary.alternateDescriptions
11720
+ ]).filter(Boolean);
11721
+ const description = descriptions[0] ?? "";
11722
+ return {
11723
+ id,
11724
+ name: first.name || second.name,
11725
+ description,
11726
+ type: first.type ?? second.type,
11727
+ ecosystem: first.ecosystem ?? second.ecosystem,
11728
+ tags: sortedUniqueStrings([...first.tags, ...second.tags]),
11729
+ provides: sortedUniqueStrings([...first.provides, ...second.provides]),
11730
+ source: first.source ?? second.source,
11731
+ repoUrl: first.repoUrl ?? second.repoUrl,
11732
+ installCommand: first.installCommand ?? second.installCommand,
11733
+ installability: betterInstallability(first.installability, second.installability),
11734
+ provenances: PROVENANCE_ORDER.filter(
11735
+ (provenance) => first.provenances.includes(provenance) || second.provenances.includes(provenance)
11736
+ ),
11737
+ archived: first.archived || second.archived,
11738
+ featured: first.featured || second.featured,
11739
+ stars: maxDefined(first.stars, second.stars),
11740
+ lastPush: maxText(first.lastPush, second.lastPush),
11741
+ alternateDescriptions: descriptions.slice(1),
11742
+ descriptionRank: Math.max(first.descriptionRank, second.descriptionRank),
11743
+ hasRegistrySelectors: first.hasRegistrySelectors || second.hasRegistrySelectors
11744
+ };
11745
+ }
11746
+ function scoreEntry(entry, query, queryTokens) {
11747
+ let score = 0;
11748
+ const matched = /* @__PURE__ */ new Set();
11749
+ const name = normalizeSearchText(entry.name);
11750
+ const descriptions = [entry.description, ...entry.alternateDescriptions].map(normalizeSearchText);
11751
+ const tags = entry.tags.map(normalizeSearchText);
11752
+ const provides = entry.provides.map(normalizeSearchText);
11753
+ const type = normalizeSearchText(entry.type);
11754
+ const ecosystem = normalizeSearchText(entry.ecosystem ?? "");
11755
+ const repositories = [entry.source ?? "", entry.repoUrl ?? ""].map(normalizeSearchText);
11756
+ if (name === query) {
11757
+ score += SCORE.exactName;
11758
+ matched.add("name");
11759
+ } else if (name.startsWith(query)) {
11760
+ score += SCORE.namePrefix;
11761
+ matched.add("name");
11762
+ } else if (name.includes(query)) {
11763
+ score += SCORE.namePhrase;
11764
+ matched.add("name");
11765
+ }
11766
+ const tagsPhraseMatch = matchesPhrase(tags, query);
11767
+ const providesPhraseMatch = matchesPhrase(provides, query);
11768
+ if (tagsPhraseMatch || providesPhraseMatch) {
11769
+ score += SCORE.tagProvidesPhrase;
11770
+ if (tagsPhraseMatch) matched.add("tags");
11771
+ if (providesPhraseMatch) matched.add("provides");
11772
+ }
11773
+ if (matchesPhrase(descriptions, query)) {
11774
+ score += SCORE.descriptionPhrase;
11775
+ matched.add("description");
11776
+ }
11777
+ const typePhraseMatch = type.includes(query);
11778
+ const ecosystemPhraseMatch = ecosystem.includes(query);
11779
+ if (typePhraseMatch || ecosystemPhraseMatch) {
11780
+ score += SCORE.typeEcosystemPhrase;
11781
+ if (typePhraseMatch) matched.add("type");
11782
+ if (ecosystemPhraseMatch) matched.add("ecosystem");
11783
+ }
11784
+ if (matchesPhrase(repositories, query)) {
11785
+ score += SCORE.repositoryPhrase;
11786
+ matched.add("repository");
11787
+ }
11788
+ const nameTokens = name.split(" ");
11789
+ const tagTokenText = tags.join(" ");
11790
+ const provideTokenText = provides.join(" ");
11791
+ const descriptionTokenText = descriptions.join(" ");
11792
+ const repositoryTokenText = repositories.join(" ");
11793
+ let allTermsCovered = true;
11794
+ for (const token of queryTokens) {
11795
+ const nameTokenMatch = includesToken(name, token);
11796
+ if (nameTokenMatch) {
11797
+ score += SCORE.nameToken;
11798
+ matched.add("name");
11799
+ } else if (nameTokens.some((candidate) => candidate.startsWith(token))) {
11800
+ score += SCORE.nameTokenPrefix;
11801
+ matched.add("name");
11802
+ }
11803
+ const tagTokenMatch = includesToken(tagTokenText, token);
11804
+ const provideTokenMatch = includesToken(provideTokenText, token);
11805
+ if (tagTokenMatch || provideTokenMatch) {
11806
+ score += SCORE.tagProvidesToken;
11807
+ if (tagTokenMatch) matched.add("tags");
11808
+ if (provideTokenMatch) matched.add("provides");
11809
+ }
11810
+ const descriptionTokenMatch = includesToken(descriptionTokenText, token);
11811
+ if (descriptionTokenMatch) {
11812
+ score += SCORE.descriptionToken;
11813
+ matched.add("description");
11814
+ }
11815
+ const typeTokenMatch = includesToken(type, token);
11816
+ const ecosystemTokenMatch = includesToken(ecosystem, token);
11817
+ if (typeTokenMatch || ecosystemTokenMatch) {
11818
+ score += SCORE.typeEcosystemToken;
11819
+ if (typeTokenMatch) matched.add("type");
11820
+ if (ecosystemTokenMatch) matched.add("ecosystem");
11821
+ }
11822
+ const repositoryTokenMatch = includesToken(repositoryTokenText, token);
11823
+ if (repositoryTokenMatch) {
11824
+ score += SCORE.repositoryToken;
11825
+ matched.add("repository");
11826
+ }
11827
+ if (!nameTokenMatch && !tagTokenMatch && !provideTokenMatch && !descriptionTokenMatch && !typeTokenMatch && !ecosystemTokenMatch && !repositoryTokenMatch) {
11828
+ allTermsCovered = false;
11829
+ }
11830
+ }
11831
+ if (allTermsCovered) {
11832
+ score += SCORE.allTerms;
11833
+ }
11834
+ return {
11835
+ id: entry.id,
11836
+ name: entry.name,
11837
+ description: entry.description,
11838
+ type: entry.type,
11839
+ ...entry.ecosystem ? { ecosystem: entry.ecosystem } : {},
11840
+ tags: entry.tags,
11841
+ provides: entry.provides,
11842
+ ...entry.source ? { source: entry.source } : {},
11843
+ ...entry.repoUrl ? { repoUrl: entry.repoUrl } : {},
11844
+ ...entry.installCommand ? { installCommand: entry.installCommand } : {},
11845
+ installability: entry.installability,
11846
+ provenances: entry.provenances,
11847
+ score,
11848
+ matchedFields: MATCHED_FIELD_ORDER.filter((field) => matched.has(field))
11849
+ };
11850
+ }
11851
+ function catalogueEntries(catalogue) {
11852
+ if (!catalogue) return [];
11853
+ return Array.isArray(catalogue) ? catalogue : catalogue.entries;
11854
+ }
11855
+ function canonicalizeSource(source) {
11856
+ const value = source.normalize("NFKC").trim();
11857
+ const github = value.match(
11858
+ /^(?:github:|git:(?:git\+)?https?:\/\/github\.com\/|(?:git\+)?https?:\/\/github\.com\/)([^/#]+)\/([^#]+?)(?:#(.*))?$/i
11859
+ );
11860
+ if (github) {
11861
+ const owner = github[1].toLowerCase();
11862
+ const repository = github[2].replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase();
11863
+ const ref = github[3];
11864
+ return `github:${owner}/${repository}${ref === void 0 ? "" : `#${ref}`}`;
11865
+ }
11866
+ return value.replace(/\/+$/, "");
11867
+ }
11868
+ function inferEcosystem(source) {
11869
+ const canonical = canonicalizeSource(source);
11870
+ if (canonical.startsWith("vercel:")) return "vercel";
11871
+ if (canonical.startsWith("mcp-registry:")) return "mcp-registry";
11872
+ if (canonical.startsWith("clawhub:")) return "clawhub";
11873
+ if (canonical.startsWith("skillkit:")) return "skillkit";
11874
+ return void 0;
11875
+ }
11876
+ function inferType(ecosystem) {
11877
+ if (ecosystem === "vercel" || ecosystem === "skillkit") return "skill";
11878
+ if (ecosystem === "mcp-registry") return "mcp";
11879
+ if (ecosystem === "clawhub") return "plugin";
11880
+ return "package";
11881
+ }
11882
+ function betterInstallability(a, b) {
11883
+ const rank = { registry: 3, source: 2, informational: 1 };
11884
+ return rank[a] >= rank[b] ? a : b;
11885
+ }
11886
+ function nonEmpty(value) {
11887
+ return value?.trim() ? value : void 0;
11888
+ }
11889
+ function uniqueStrings(values) {
11890
+ return [...new Set(values)];
11891
+ }
11892
+ function sortedUniqueStrings(values) {
11893
+ return uniqueStrings(values).sort(compareText);
11894
+ }
11895
+ function enrichedInstallCommand(entry, source) {
11896
+ const catalogueCommand = nonEmpty(entry.installCommand);
11897
+ if (!source) return catalogueCommand;
11898
+ if (entry.ecosystem === "mcp-registry" || entry.ecosystem === "clawhub") {
11899
+ return catalogueCommand ?? `npx agentwheel install ${shellQuote2(source)}`;
11900
+ }
11901
+ return `npx agentwheel install ${shellQuote2(source)}`;
11902
+ }
11903
+ function shellQuote2(value) {
11904
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
11905
+ }
11906
+ function matchesPhrase(fields, query) {
11907
+ return fields.some((field) => field.includes(query));
11908
+ }
11909
+ function compareStableEntries(a, b) {
11910
+ return compareText(normalizeSearchText(a.name), normalizeSearchText(b.name)) || compareText(a.id, b.id);
11911
+ }
11912
+ function maxDefined(a, b) {
11913
+ if (a === void 0) return b;
11914
+ if (b === void 0) return a;
11915
+ return Math.max(a, b);
11916
+ }
11917
+ function maxText(a, b) {
11918
+ if (a === void 0) return b;
11919
+ if (b === void 0) return a;
11920
+ return a >= b ? a : b;
11921
+ }
11922
+ function assertUniqueIdentities(ids, label) {
11923
+ const seen = /* @__PURE__ */ new Set();
11924
+ for (const id of ids) {
11925
+ if (seen.has(id)) throw new Error(`Duplicate ${label} id: ${id}`);
11926
+ seen.add(id);
11927
+ }
11928
+ }
11929
+ function compareText(a, b) {
11930
+ return a < b ? -1 : a > b ? 1 : 0;
11931
+ }
11932
+ function includesToken(normalizedText, token) {
11933
+ return normalizedText === token || normalizedText.startsWith(`${token} `) || normalizedText.endsWith(` ${token}`) || normalizedText.includes(` ${token} `);
11934
+ }
11935
+
11321
11936
  // src/cli/index.ts
11322
11937
  var CLI_VERSION = resolveCliVersion();
11323
11938
  var COMPANION_SKILL_SOURCE = "github:NestDevLab/agentwheel";
@@ -11361,17 +11976,59 @@ program.command("list").description("list artifacts exposed by a package source"
11361
11976
  const resolvedInput = await resolvePackageSource(source, targetRoot);
11362
11977
  const selectedArtifacts = selectedArtifactsFromOptionsOrRegistry(options, resolvedInput.registryEntry);
11363
11978
  const driver = getSourceDriver(options.driver ?? inferSourceDriverName(resolvedInput.source));
11364
- const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join44(targetRoot, ".agentwheel", "cache") }))));
11979
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join45(targetRoot, ".agentwheel", "cache") }))));
11365
11980
  const artifacts = filterArtifactsBySelection(await driver.list(resolved), selectedArtifacts);
11366
11981
  for (const artifact of artifacts) {
11367
11982
  console.log(`${artifact.type} ${artifact.name} ${artifact.relativePath}`);
11368
11983
  }
11369
11984
  });
11985
+ program.command("search").description("search registry and public catalogue artifacts").argument("<query>", "search query").option("--json", "print the versioned search response as JSON", false).option("--scope <scope>", "search scope: all, registry, enriched, or vercel", "all").option("--type <type>", "artifact type: package, skill, plugin, mcp, or adapter").option("--ecosystem <ecosystem>", "ecosystem: official, openpack, mcp-registry, clawhub, skillkit, or vercel").option("--limit <n>", "maximum number of results (1-100)", "20").option("--include-archived", "include archived catalogue entries", false).option("--refresh", "refresh registry and catalogue caches", false).option("--offline", "use compatible local caches without network access", false).option("-t, --target-root <path>", "workspace root", process.cwd()).action(async (query, options) => {
11986
+ const trimmedQuery = query.trim();
11987
+ if (!trimmedQuery) {
11988
+ throw new Error("Search query must not be empty.");
11989
+ }
11990
+ const scope = parseSearchScope(options.scope);
11991
+ const type = options.type === void 0 ? void 0 : parseSearchType(options.type);
11992
+ const ecosystem = options.ecosystem === void 0 ? void 0 : parseSearchEcosystem(options.ecosystem);
11993
+ const limit = parseSearchLimit(options.limit);
11994
+ if (options.refresh && options.offline) {
11995
+ throw new Error("--refresh cannot be used with --offline.");
11996
+ }
11997
+ const warning = (message) => console.error(message);
11998
+ const targetRoot = normalizeTargetRoot(options.targetRoot);
11999
+ const registryRequest = scope === "all" || scope === "registry" ? new RegistryClient({ workspaceRoot: targetRoot, offline: options.offline, warn: warning }).getIndex({ refresh: options.refresh }) : void 0;
12000
+ const catalogueRequest = scope === "all" || scope === "enriched" || scope === "vercel" ? new CatalogueClient({ offline: options.offline, warn: warning }).getIndex({ refresh: options.refresh }) : void 0;
12001
+ const [registryIndex, catalogueIndex] = await Promise.all([registryRequest, catalogueRequest]);
12002
+ const entries = buildSearchEntries({
12003
+ registry: registryIndex?.entries,
12004
+ enriched: scope === "all" || scope === "enriched" ? catalogueIndex?.enriched : void 0,
12005
+ vercel: scope === "all" || scope === "vercel" ? catalogueIndex?.vercel : void 0
12006
+ });
12007
+ const results = searchEntries(entries, trimmedQuery, {
12008
+ type,
12009
+ ecosystem,
12010
+ limit,
12011
+ includeArchived: options.includeArchived
12012
+ });
12013
+ const loadedIndexes = [registryIndex, catalogueIndex].filter((index) => index !== void 0);
12014
+ const response = {
12015
+ schemaVersion: 1,
12016
+ query: trimmedQuery,
12017
+ scope,
12018
+ fromCache: loadedIndexes.every((index) => index.fromCache),
12019
+ results
12020
+ };
12021
+ if (options.json) {
12022
+ console.log(JSON.stringify(response, null, 2));
12023
+ return;
12024
+ }
12025
+ printSearchResults(trimmedQuery, results);
12026
+ });
11370
12027
  program.command("scan").description("scan a package source for validation findings").argument("<source>", "package source").option("--driver <driver>", "source driver").option("-t, --target-root <path>", "workspace root", process.cwd()).action(async (source, options) => {
11371
12028
  const targetRoot = normalizeTargetRoot(options.targetRoot);
11372
12029
  const resolvedInput = await resolvePackageSource(source, targetRoot);
11373
12030
  const driver = getSourceDriver(options.driver ?? inferSourceDriverName(resolvedInput.source));
11374
- const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join44(targetRoot, ".agentwheel", "cache") }))));
12031
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join45(targetRoot, ".agentwheel", "cache") }))));
11375
12032
  const result = await driver.scan(resolved);
11376
12033
  if (result.findings.length === 0) {
11377
12034
  console.log("Scan ok: no findings");
@@ -11434,7 +12091,7 @@ program.command("deps").description("inspect the OpenPack dependency graph").add
11434
12091
  for (const decision of result.bundle.graphLock.canonical.overrides) {
11435
12092
  console.log(`OVERRIDE ${decision.graphNodeId}:${decision.type}/${decision.name} replaces ${decision.overriddenGraphNodeId}:${decision.type}/${decision.name} via ${decision.rootId} (${decision.selector})`);
11436
12093
  }
11437
- await rm11(result.bundle.root, { recursive: true, force: true });
12094
+ await rm12(result.bundle.root, { recursive: true, force: true });
11438
12095
  }
11439
12096
  continue;
11440
12097
  }
@@ -11466,11 +12123,6 @@ program.command("registry").description("manage optional registry indexes").addC
11466
12123
  const client = new RegistryClient({ workspaceRoot: normalizeTargetRoot(options.targetRoot), warn: (message) => console.warn(message) });
11467
12124
  printRegistryEntries((await client.getIndex()).entries);
11468
12125
  })
11469
- ).addCommand(
11470
- new Command("search").description("search registry entries").argument("<query>", "search query").option("-t, --target-root <path>", "workspace root", process.cwd()).action(async (query, options) => {
11471
- const client = new RegistryClient({ workspaceRoot: normalizeTargetRoot(options.targetRoot), warn: (message) => console.warn(message) });
11472
- printRegistryEntries(await client.search(query));
11473
- })
11474
12126
  ).addCommand(
11475
12127
  new Command("publish").description("draft a catalogue submission for a public source").argument("<source>", "public resource source or GitHub URL").option("--name <name>", "registry short name").option("--type <type>", "entry type (package, skill, plugin, mcp, or adapter)").option("--description <text>", "short catalogue description").option("--tag <tag>", "search tag (repeatable or comma-separated)", collectTagOption, []).option("--select <type/name>", "selected artifact inside a larger package (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "selected skill inside a larger package (repeatable or comma-separated)", collectSkillOption, []).option("--json", "print only the registry entry JSON", false).action(async (source, options) => {
11476
12128
  const draft = createRegistryPublishDraft(source, {
@@ -11644,7 +12296,7 @@ journalCommand.command("list").description("show pending apply journals for reso
11644
12296
  if (!journal) continue;
11645
12297
  pending += 1;
11646
12298
  console.log(`PENDING ${state.adapter.name}/${state.installationType} at ${state.installRoot}`);
11647
- console.log(` journal: ${join44(state.installRoot, ".agentwheel", `${state.state.stateKey}.apply-journal.json`)}`);
12299
+ console.log(` journal: ${join45(state.installRoot, ".agentwheel", `${state.state.stateKey}.apply-journal.json`)}`);
11648
12300
  console.log(` stateKey: ${state.state.stateKey}`);
11649
12301
  console.log(` createdAt: ${journal.createdAt}`);
11650
12302
  console.log(` updatedAt: ${journal.updatedAt}`);
@@ -11784,7 +12436,7 @@ async function runInstallCommand(nameOrSource, options, behavior) {
11784
12436
  console.log(`Applied ${result.plan.adapter} at ${result.plan.targetRoot}.`);
11785
12437
  if (reloaded) console.log(`Reloaded runtime via ${formatReloadCommands(target.reloadCommands)}.`);
11786
12438
  }
11787
- await rm11(result.bundle.root, { recursive: true, force: true });
12439
+ await rm12(result.bundle.root, { recursive: true, force: true });
11788
12440
  if (result.plan.hasBlockingChanges) process.exitCode = 1;
11789
12441
  }
11790
12442
  if (behavior.apply && extraPackage && !targetOptions.onlySource) {
@@ -11879,7 +12531,7 @@ async function buildPlanReport(nameOrSource, options) {
11879
12531
  for (const result of results) {
11880
12532
  reportTargets.push(installPlanReportTarget(result.plan, result.graphLockDigest));
11881
12533
  reportWarnings.push(...result.warnings);
11882
- await rm11(result.bundle.root, { recursive: true, force: true });
12534
+ await rm12(result.bundle.root, { recursive: true, force: true });
11883
12535
  }
11884
12536
  }
11885
12537
  return planReport(reportTargets, reportWarnings);
@@ -11935,7 +12587,7 @@ async function packageEntryFromSource(source, targetRoot, options) {
11935
12587
  const bundle = await stageSource(driver, resolvedSource, {
11936
12588
  workspaceRoot: targetRoot,
11937
12589
  adapter,
11938
- cacheRoot: join44(targetRoot, ".agentwheel", "cache"),
12590
+ cacheRoot: join45(targetRoot, ".agentwheel", "cache"),
11939
12591
  mode: options.mode,
11940
12592
  ref: initialVersion?.ref,
11941
12593
  frozenLock: lockMode,
@@ -11961,7 +12613,7 @@ async function packageEntryFromSource(source, targetRoot, options) {
11961
12613
  overrides: overrideArtifactsFromOptions(options)
11962
12614
  };
11963
12615
  } finally {
11964
- await rm11(bundle.root, { recursive: true, force: true });
12616
+ await rm12(bundle.root, { recursive: true, force: true });
11965
12617
  }
11966
12618
  }
11967
12619
  function findConfiguredPackage(packages, value) {
@@ -12112,7 +12764,7 @@ async function runConfiguredGraphPackages(target, options, behavior) {
12112
12764
  console.log(`Applied ${result.plan.adapter} at ${result.plan.targetRoot}.`);
12113
12765
  if (reloaded) console.log(`Reloaded runtime via ${formatReloadCommands(target.reloadCommands)}.`);
12114
12766
  }
12115
- await rm11(result.bundle.root, { recursive: true, force: true });
12767
+ await rm12(result.bundle.root, { recursive: true, force: true });
12116
12768
  if (result.plan.hasBlockingChanges) process.exitCode = 1;
12117
12769
  }
12118
12770
  }
@@ -12329,7 +12981,7 @@ function scopeUpdatePlanToDependencies(result, selectors, previousLock, manifest
12329
12981
  selectedPreviousNodeIds,
12330
12982
  selectedRootIds
12331
12983
  );
12332
- const graphLockDigest = createHash11("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
12984
+ const graphLockDigest = createHash12("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
12333
12985
  return {
12334
12986
  ...result,
12335
12987
  bundle: { ...result.bundle, graphLock },
@@ -12497,7 +13149,7 @@ function scopeUpdatePlanToRoot(result, rootId, previousLock, manifest) {
12497
13149
  selectedPreviousNodeIds,
12498
13150
  /* @__PURE__ */ new Set([rootId])
12499
13151
  );
12500
- const graphLockDigest = createHash11("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
13152
+ const graphLockDigest = createHash12("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
12501
13153
  return {
12502
13154
  ...scoped,
12503
13155
  bundle: { ...scoped.bundle, graphLock },
@@ -12569,7 +13221,7 @@ function keepManifestEntryOperation(entry, targetRoot, scopeDescription, operati
12569
13221
  artifactType: entry.artifactType,
12570
13222
  artifactName: entry.artifactName,
12571
13223
  kind: entry.kind,
12572
- destPath: operation?.destPath ?? join44(targetRoot, entry.path),
13224
+ destPath: operation?.destPath ?? join45(targetRoot, entry.path),
12573
13225
  relativeDestPath: entry.path,
12574
13226
  desiredHash: entry.sourceHash,
12575
13227
  currentHash: operation?.currentHash ?? entry.hash,
@@ -12686,7 +13338,7 @@ async function uninstallConfiguredPackage(target, packageName, options) {
12686
13338
  if (!options.dryRun) {
12687
13339
  console.log(formatUninstallResult(result));
12688
13340
  }
12689
- if (renderedRoot) await rm11(renderedRoot, { recursive: true, force: true });
13341
+ if (renderedRoot) await rm12(renderedRoot, { recursive: true, force: true });
12690
13342
  if (plan.hasBlockingChanges) process.exitCode = 1;
12691
13343
  }
12692
13344
  }
@@ -13137,7 +13789,7 @@ async function collectPendingInstallWork(target, options) {
13137
13789
  const message = error instanceof Error ? error.message : String(error);
13138
13790
  return { pendingCount: 0, driftCount: 0, conflictCount: 0, counts: {}, error: message };
13139
13791
  } finally {
13140
- await Promise.all(results.map((result) => rm11(result.bundle.root, { recursive: true, force: true })));
13792
+ await Promise.all(results.map((result) => rm12(result.bundle.root, { recursive: true, force: true })));
13141
13793
  }
13142
13794
  }
13143
13795
  async function printDoctor(target, options) {
@@ -13154,12 +13806,12 @@ async function printDoctor(target, options) {
13154
13806
  const requestedSkills = doctorSkillRequests(target, options);
13155
13807
  const skills = [];
13156
13808
  for (const request of requestedSkills) {
13157
- const skillPath = join44(state.installRoot, targetMapping.dest, request.name);
13809
+ const skillPath = join45(state.installRoot, targetMapping.dest, request.name);
13158
13810
  const exists = await pathExists(skillPath);
13159
13811
  const manifestEntry = manifest?.entries.find((entry) => {
13160
13812
  if (entry.artifactType !== "skills") return false;
13161
13813
  const legacyInstallName = "installName" in entry && typeof entry.installName === "string" ? entry.installName : void 0;
13162
- return entry.artifactName === request.name || legacyInstallName === request.name || entry.path === join44(targetMapping.dest, request.name);
13814
+ return entry.artifactName === request.name || legacyInstallName === request.name || entry.path === join45(targetMapping.dest, request.name);
13163
13815
  });
13164
13816
  const status = manifestEntry ? "managed" : exists ? "present-unmanaged" : "missing";
13165
13817
  skills.push({
@@ -13239,7 +13891,7 @@ function doctorSkillLabel(name) {
13239
13891
  return `${name} skill`;
13240
13892
  }
13241
13893
  function isSyncwheelWorkspace(targetRoot) {
13242
- return existsSync(join44(targetRoot, ".syncwheel", "manifest.json"));
13894
+ return existsSync(join45(targetRoot, ".syncwheel", "manifest.json"));
13243
13895
  }
13244
13896
  function skillInstallCommand(adapter, installationType, options, skill, behavior = {}) {
13245
13897
  const args = [
@@ -13307,7 +13959,7 @@ function normalizeRuntimeScopeOptions(options, behavior = {}) {
13307
13959
  }
13308
13960
  const canDefaultTargetRoot = !options.agent && !options.all && !options.allDetected && !options.profile;
13309
13961
  if (!targetRoot && canDefaultTargetRoot && (options.user || installationType === "user" || behavior.defaultUser)) {
13310
- targetRoot = homedir9();
13962
+ targetRoot = homedir10();
13311
13963
  }
13312
13964
  if (!installationType && behavior.defaultUser) {
13313
13965
  installationType = "user";
@@ -13327,12 +13979,12 @@ function looksLikeSourceSpecifier(value) {
13327
13979
  return value.includes(":") || value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value === "~" || value.startsWith("~/");
13328
13980
  }
13329
13981
  function normalizeCliPath(value) {
13330
- if (value === "~") return homedir9();
13331
- if (value.startsWith("~/")) return resolve22(homedir9(), value.slice(2));
13982
+ if (value === "~") return homedir10();
13983
+ if (value.startsWith("~/")) return resolve22(homedir10(), value.slice(2));
13332
13984
  return resolve22(value);
13333
13985
  }
13334
13986
  function isHomePath(path) {
13335
- return resolve22(path) === resolve22(homedir9());
13987
+ return resolve22(path) === resolve22(homedir10());
13336
13988
  }
13337
13989
  function adapterListFromOption(adapter) {
13338
13990
  if (!adapter) return [];
@@ -13387,10 +14039,10 @@ function filterUninstallPlanBySelection(plan, selected) {
13387
14039
  };
13388
14040
  }
13389
14041
  async function initPackage(root) {
13390
- await mkdir23(join44(root, "instructions"), { recursive: true });
13391
- await mkdir23(join44(root, "rules"), { recursive: true });
13392
- await mkdir23(join44(root, "skills"), { recursive: true });
13393
- const manifestPath = join44(root, "openpack.json");
14042
+ await mkdir23(join45(root, "instructions"), { recursive: true });
14043
+ await mkdir23(join45(root, "rules"), { recursive: true });
14044
+ await mkdir23(join45(root, "skills"), { recursive: true });
14045
+ const manifestPath = join45(root, "openpack.json");
13394
14046
  const manifest = {
13395
14047
  schemaVersion: 2,
13396
14048
  name: "example/agentwheel-package",
@@ -13403,7 +14055,7 @@ async function initPackage(root) {
13403
14055
  };
13404
14056
  await writeFile22(manifestPath, `${JSON.stringify(manifest, null, 2)}
13405
14057
  `, "utf8");
13406
- await writeFile22(join44(root, "instructions", "AGENTS.md"), "# Agent Instructions\n", "utf8");
14058
+ await writeFile22(join45(root, "instructions", "AGENTS.md"), "# Agent Instructions\n", "utf8");
13407
14059
  }
13408
14060
  async function defaultBootstrapPackage(_root) {
13409
14061
  const packageRoot = await findAgentwheelPackageRoot(dirname32(fileURLToPath3(import.meta.url)));
@@ -13474,6 +14126,49 @@ function printRegistryEntries(entries) {
13474
14126
  console.log(`${entry.name} ${entry.type} ${entry.source} ${entry.description}${tags}`);
13475
14127
  }
13476
14128
  }
14129
+ function parseSearchScope(value) {
14130
+ const parsed = searchScopeSchema.safeParse(value);
14131
+ if (parsed.success) return parsed.data;
14132
+ throw new Error(`Invalid search scope: ${value}. Expected one of: ${searchScopeSchema.options.join(", ")}.`);
14133
+ }
14134
+ function parseSearchType(value) {
14135
+ const parsed = searchTypeSchema.safeParse(value);
14136
+ if (parsed.success) return parsed.data;
14137
+ throw new Error(`Invalid artifact type: ${value}. Expected one of: ${searchTypeSchema.options.join(", ")}.`);
14138
+ }
14139
+ function parseSearchEcosystem(value) {
14140
+ const parsed = searchEcosystemSchema.safeParse(value);
14141
+ if (parsed.success) return parsed.data;
14142
+ throw new Error(`Invalid ecosystem: ${value}. Expected one of: ${searchEcosystemSchema.options.join(", ")}.`);
14143
+ }
14144
+ function parseSearchLimit(value) {
14145
+ const limit = Number(value);
14146
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
14147
+ throw new Error(`Invalid search limit: ${value}. Expected an integer from 1 to 100.`);
14148
+ }
14149
+ return limit;
14150
+ }
14151
+ function printSearchResults(query, results) {
14152
+ if (results.length === 0) {
14153
+ console.log(`No artifacts found for ${JSON.stringify(query)}.`);
14154
+ return;
14155
+ }
14156
+ for (const [index, result] of results.entries()) {
14157
+ const ecosystem = result.ecosystem ?? "unknown";
14158
+ const provenances = result.provenances.join("+");
14159
+ console.log(
14160
+ `${index + 1}. ${result.name} [type=${result.type}; ecosystem=${ecosystem}; installability=${result.installability}; provenance=${provenances}]`
14161
+ );
14162
+ console.log(` ${result.description || "(no description)"}`);
14163
+ if (result.installCommand) {
14164
+ console.log(` Install: ${result.installCommand}`);
14165
+ } else if (result.source) {
14166
+ console.log(` Source: ${result.source}`);
14167
+ } else {
14168
+ console.log(" Install: unavailable");
14169
+ }
14170
+ }
14171
+ }
13477
14172
  async function main() {
13478
14173
  await maybeCheckForUpdate({
13479
14174
  currentVersion: CLI_VERSION,
package/openpack.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "name": "NestDevLab/agentwheel",
4
- "version": "0.15.0",
4
+ "version": "0.16.0",
5
5
  "provides": [
6
6
  { "type": "skills", "path": "skills" }
7
7
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentwheel",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Weave skills, rules, and instructions across every AI agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: agentwheel
3
- description: Use agentwheel to discover, add, install, update, customize, eject, and uninstall agent skills, rules, instructions, commands, MCP, hooks, settings, and plugin artifacts across runtimes.
3
+ description: Discover or manage reusable agent artifacts with Agentwheel. Use when a requested agent capability, integration, workflow, policy, or tool may already exist, and when adding, installing, updating, customizing, ejecting, or uninstalling skills, rules, instructions, commands, MCP, hooks, settings, plugins, or subagents across runtimes.
4
4
  allowed-tools: [Bash]
5
5
  license: MIT
6
6
  metadata:
7
7
  author: NestDevLab
8
- version: "0.15.0"
8
+ version: "0.16.0"
9
9
  ---
10
10
 
11
11
  # agentwheel
12
12
 
13
- Use this skill when a user wants to install, manage, update, remove, or inspect agent skills or other agentwheel-managed artifacts.
13
+ Agentwheel discovers reusable artifacts and manages their desired state across runtimes.
14
14
 
15
15
  agentwheel is the control plane. It reads packages from sources, stores desired state in `.agentwheel/config.json`, plans runtime changes, and writes only through `install`. Treat runtime output directories as generated files.
16
16
 
@@ -29,11 +29,12 @@ Mental model:
29
29
  - Gmail, Drive, registry publishing, git commits, pushes, and runtime reloads/restarts are separate external side effects. Get explicit approval for them.
30
30
  - Programmatic adapters execute local code. Use `--adapter-module` only with `--allow-adapter-code` after the user approves that local code execution.
31
31
  - OpenClaw plugin artifacts are only planned by default. Use `--execute-plugins` only after explicit approval.
32
+ - Search results are proposals, not approval. Never add, install, enable, or change configuration until the user confirms the artifact and target scope.
32
33
 
33
34
  ## Core Flow
34
35
 
35
36
  ```bash
36
- agentwheel registry search tmux
37
+ agentwheel search tmux
37
38
  agentwheel add github:NestDevLab/agent-mesh --skill codex-tmux --adapter codex --installation-type local --mode tracking
38
39
  agentwheel plan
39
40
  agentwheel install
@@ -72,14 +73,34 @@ agentwheel init package
72
73
 
73
74
  `agentwheel init package` creates `openpack.json`, `instructions/`, `rules/`, `skills/`, and `instructions/AGENTS.md`.
74
75
 
75
- ## Discovery
76
+ ## Discovery And Recommendations
76
77
 
77
- Refresh and search the optional registry:
78
+ Search configured registries and public catalogue sources:
79
+
80
+ ```bash
81
+ agentwheel search "<query>"
82
+ agentwheel search "<query>" --json --limit 10
83
+ ```
84
+
85
+ When a reusable artifact could satisfy the request:
86
+
87
+ 1. Extract the capability and constraints from the complete request.
88
+ 2. Generate one to four short lexical queries using capability terms, synonyms, runtime names, and artifact types. Prefer English catalogue terms when the request uses another language.
89
+ 3. Run one `agentwheel search "<query>" --json --limit 10` per variant. Stop after four calls; do not recursively refine without new user requirements.
90
+ 4. Merge results by stable `id`. Treat CLI scores as retrieval signals, not semantic confidence.
91
+ 5. Rerank against the original request using capabilities, runtime or ecosystem, artifact type, description, tags, `provides`, and installability. Do not infer capabilities absent from result metadata.
92
+ 6. Suggest zero to three distinct artifacts. For each, give its name or source, one evidence-based match reason, installability, and a safe next command.
93
+ 7. Wait for explicit approval before `add`, `install`, plugin execution, or configuration changes.
94
+
95
+ For automatic suggestions, skip discovery when the user explicitly wants custom implementation, has already selected an artifact, an installed artifact clearly satisfies the request, candidates are only weak lexical matches, or the same suggestion was already shown without new evidence.
96
+
97
+ Search recommendations are conversational only: they do not select OpenPack `suggests`, mutate desired state, or imply installation approval.
98
+
99
+ Registry maintenance remains explicit:
78
100
 
79
101
  ```bash
80
102
  agentwheel registry update
81
103
  agentwheel registry list
82
- agentwheel registry search <query>
83
104
  ```
84
105
 
85
106
  Inspect an explicit source before adding it:
@@ -91,7 +112,7 @@ agentwheel list ./local-agent-pack
91
112
  agentwheel scan ./local-agent-pack
92
113
  ```
93
114
 
94
- Filter discovery to specific artifacts:
115
+ Filter source inspection to specific artifacts:
95
116
 
96
117
  ```bash
97
118
  agentwheel list github:owner/repo --select skills/review --select rules/core.md
@@ -498,11 +519,11 @@ If a selected artifact is missing:
498
519
  agentwheel list <source>
499
520
  ```
500
521
 
501
- If registry short names fail:
522
+ If a registry short name fails:
502
523
 
503
524
  ```bash
504
525
  agentwheel registry update
505
- agentwheel registry search <query>
526
+ agentwheel search "<query>" --scope registry
506
527
  ```
507
528
 
508
529
  If npm update checks are noisy: