agentwheel 0.15.0 → 0.16.1

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
 
@@ -4623,9 +4623,10 @@ function formatPlan(plan) {
4623
4623
  );
4624
4624
  return lines.join("\n");
4625
4625
  }
4626
- function planReport(targets, warnings = []) {
4626
+ function planReport(targets, warnings = [], applied = false) {
4627
4627
  return {
4628
4628
  schemaVersion: 1,
4629
+ applied,
4629
4630
  targets: [...targets].sort(comparePlanReportTargets),
4630
4631
  warnings: [...warnings].sort((a, b) => a.localeCompare(b))
4631
4632
  };
@@ -7759,13 +7760,6 @@ var RegistryClient = class {
7759
7760
  const index = await this.getIndex(options);
7760
7761
  return index.entries.find((entry) => entry.name === name);
7761
7762
  }
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
7763
  async clearCache() {
7770
7764
  await rm8(this.cachePath, { force: true });
7771
7765
  }
@@ -11318,6 +11312,628 @@ function valueAfter(lines, prefix) {
11318
11312
  return lines.find((line) => line.startsWith(prefix))?.slice(prefix.length).trim() ?? null;
11319
11313
  }
11320
11314
 
11315
+ // src/catalogue/client.ts
11316
+ import { createHash as createHash11 } from "crypto";
11317
+ import { readFile as readFile32, rm as rm11 } from "fs/promises";
11318
+ import { homedir as homedir9 } from "os";
11319
+ import { join as join44 } from "path";
11320
+
11321
+ // src/model/catalogue.ts
11322
+ import { z as z13 } from "zod";
11323
+ var searchScopeSchema = z13.enum(["all", "registry", "enriched", "vercel"]);
11324
+ var searchTypeSchema = z13.enum(["package", "skill", "plugin", "mcp", "adapter"]);
11325
+ var searchEcosystemSchema = z13.enum([
11326
+ "official",
11327
+ "openpack",
11328
+ "mcp-registry",
11329
+ "clawhub",
11330
+ "skillkit",
11331
+ "vercel"
11332
+ ]);
11333
+ var catalogueProvenanceSchema = z13.enum(["registry", "enriched", "vercel"]);
11334
+ var installabilitySchema = z13.enum(["registry", "source", "informational"]);
11335
+ var nullableString = z13.string().nullable();
11336
+ var nullableStringArray = z13.array(z13.string()).nullable();
11337
+ var enrichedCatalogueEntrySchema = z13.object({
11338
+ id: z13.string().min(1),
11339
+ name: z13.string().min(1),
11340
+ ecosystem: searchEcosystemSchema.nullable(),
11341
+ type: searchTypeSchema.nullable(),
11342
+ description: nullableString,
11343
+ tags: nullableStringArray,
11344
+ source: nullableString,
11345
+ installCommand: nullableString,
11346
+ repoUrl: nullableString,
11347
+ homepageUrl: nullableString.optional(),
11348
+ homepageLinkLabel: nullableString.optional(),
11349
+ stars: z13.number().finite().nullable().optional(),
11350
+ lastPush: nullableString.optional(),
11351
+ archived: z13.boolean().nullable(),
11352
+ provides: nullableStringArray,
11353
+ version: nullableString,
11354
+ featured: z13.boolean().nullable().optional()
11355
+ });
11356
+ var enrichedCatalogueSchema = z13.object({
11357
+ schemaVersion: z13.literal(1),
11358
+ generatedAt: z13.string().datetime(),
11359
+ entries: z13.array(enrichedCatalogueEntrySchema)
11360
+ }).superRefine((value, context) => {
11361
+ const seen = /* @__PURE__ */ new Set();
11362
+ value.entries.forEach((entry, index) => {
11363
+ if (seen.has(entry.id)) {
11364
+ context.addIssue({
11365
+ code: "custom",
11366
+ path: ["entries", index, "id"],
11367
+ message: `duplicate catalogue id: ${entry.id}`
11368
+ });
11369
+ }
11370
+ seen.add(entry.id);
11371
+ });
11372
+ });
11373
+ var vercelCatalogueEntrySchema = z13.object({
11374
+ o: z13.string().min(1),
11375
+ r: z13.string().min(1),
11376
+ s: z13.string().min(1),
11377
+ d: z13.string().nullable().optional()
11378
+ });
11379
+ var vercelCatalogueSchema = z13.object({
11380
+ schemaVersion: z13.literal(1),
11381
+ generatedAt: z13.string().datetime(),
11382
+ count: z13.number().int().nonnegative(),
11383
+ entries: z13.array(vercelCatalogueEntrySchema)
11384
+ }).superRefine((value, context) => {
11385
+ if (value.count !== value.entries.length) {
11386
+ context.addIssue({
11387
+ code: "custom",
11388
+ path: ["count"],
11389
+ message: `count must equal entries length (${value.entries.length})`
11390
+ });
11391
+ }
11392
+ const seen = /* @__PURE__ */ new Set();
11393
+ value.entries.forEach((entry, index) => {
11394
+ const id = `${entry.o}/${entry.r}/${entry.s}`;
11395
+ if (seen.has(id)) {
11396
+ context.addIssue({
11397
+ code: "custom",
11398
+ path: ["entries", index],
11399
+ message: `duplicate Vercel catalogue id: ${id}`
11400
+ });
11401
+ }
11402
+ seen.add(id);
11403
+ });
11404
+ });
11405
+ var catalogueCacheSchema = z13.object({
11406
+ version: z13.literal(1),
11407
+ fetchedAt: z13.string().datetime(),
11408
+ sources: z13.tuple([z13.string().url(), z13.string().url()]),
11409
+ enriched: enrichedCatalogueSchema,
11410
+ vercel: vercelCatalogueSchema
11411
+ });
11412
+ var catalogueCacheEnvelopeSchema = z13.object({
11413
+ version: z13.literal(1),
11414
+ fetchedAt: z13.string().datetime(),
11415
+ sources: z13.tuple([z13.string().url(), z13.string().url()]),
11416
+ contentHash: z13.string().regex(/^[a-f0-9]{64}$/).optional(),
11417
+ enriched: z13.unknown(),
11418
+ vercel: z13.unknown()
11419
+ });
11420
+ var searchResultSchema = z13.object({
11421
+ id: z13.string().min(1),
11422
+ name: z13.string().min(1),
11423
+ description: z13.string(),
11424
+ type: searchTypeSchema,
11425
+ ecosystem: searchEcosystemSchema.optional(),
11426
+ tags: z13.array(z13.string()),
11427
+ provides: z13.array(z13.string()),
11428
+ source: z13.string().min(1).optional(),
11429
+ repoUrl: z13.string().min(1).optional(),
11430
+ installCommand: z13.string().min(1).optional(),
11431
+ installability: installabilitySchema,
11432
+ provenances: z13.array(catalogueProvenanceSchema).min(1),
11433
+ score: z13.number().int().nonnegative(),
11434
+ matchedFields: z13.array(z13.string())
11435
+ });
11436
+ var searchResponseSchema = z13.object({
11437
+ schemaVersion: z13.literal(1),
11438
+ query: z13.string(),
11439
+ scope: searchScopeSchema,
11440
+ fromCache: z13.boolean(),
11441
+ results: z13.array(searchResultSchema)
11442
+ });
11443
+
11444
+ // src/catalogue/client.ts
11445
+ var DEFAULT_ENRICHED_CATALOGUE_URL = "https://raw.githubusercontent.com/NestDevLab/agentwheel-registry/main/catalogue-data.json";
11446
+ var DEFAULT_VERCEL_CATALOGUE_URL = "https://raw.githubusercontent.com/NestDevLab/agentwheel-registry/main/catalogue-vercel-index.json";
11447
+ var DEFAULT_CATALOGUE_TTL_MS = 24 * 60 * 60 * 1e3;
11448
+ var MAX_CATALOGUE_PAYLOAD_BYTES = 32 * 1024 * 1024;
11449
+ var CatalogueClient = class {
11450
+ constructor(options = {}) {
11451
+ this.options = options;
11452
+ this.cachePath = options.cachePath ?? defaultCatalogueCachePath();
11453
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
11454
+ this.fetchImpl = options.fetch ?? fetch;
11455
+ this.sources = [
11456
+ options.enrichedUrl ?? DEFAULT_ENRICHED_CATALOGUE_URL,
11457
+ options.vercelUrl ?? DEFAULT_VERCEL_CATALOGUE_URL
11458
+ ];
11459
+ }
11460
+ options;
11461
+ cachePath;
11462
+ now;
11463
+ fetchImpl;
11464
+ sources;
11465
+ async getIndex(options = {}) {
11466
+ const cached = await this.readCache();
11467
+ const usableCache = cached && sameSources2(cached.sources, this.sources) ? cached : void 0;
11468
+ const expired = usableCache ? this.isExpired(usableCache) : false;
11469
+ if (this.options.offline) {
11470
+ if (!usableCache) {
11471
+ throw new Error("Offline catalogue cache is missing. Run without --offline first.");
11472
+ }
11473
+ const stale = expired;
11474
+ this.options.warn?.(
11475
+ stale ? "Offline: using stale catalogue cache because refresh is disabled." : "Offline: using cached catalogue data."
11476
+ );
11477
+ return this.fromCache(usableCache, stale);
11478
+ }
11479
+ if (!options.refresh && usableCache && !expired) {
11480
+ return this.fromCache(usableCache, false);
11481
+ }
11482
+ try {
11483
+ const [enriched, vercel] = await Promise.all([
11484
+ this.fetchJson(this.sources[0], enrichedCatalogueSchema),
11485
+ this.fetchJson(this.sources[1], vercelCatalogueSchema)
11486
+ ]);
11487
+ const fetchedAt = this.now().toISOString();
11488
+ const cache = {
11489
+ version: 1,
11490
+ fetchedAt,
11491
+ sources: this.sources,
11492
+ enriched,
11493
+ vercel
11494
+ };
11495
+ const cacheFile = {
11496
+ ...cache,
11497
+ contentHash: catalogueContentHash(enriched, vercel)
11498
+ };
11499
+ await writeJsonAtomic(this.cachePath, cacheFile);
11500
+ return { enriched, vercel, sources: this.sources, fetchedAt, fromCache: false, stale: false };
11501
+ } catch (error) {
11502
+ if (!usableCache) throw error;
11503
+ const reason = error instanceof Error ? error.message : String(error);
11504
+ this.options.warn?.(`Catalogue refresh failed; using stale catalogue cache: ${reason}`);
11505
+ return this.fromCache(usableCache, true);
11506
+ }
11507
+ }
11508
+ async clearCache() {
11509
+ await rm11(this.cachePath, { force: true });
11510
+ }
11511
+ async readCache() {
11512
+ if (!await pathExists(this.cachePath)) return void 0;
11513
+ try {
11514
+ const value = JSON.parse(await readFile32(this.cachePath, "utf8"));
11515
+ const envelope = catalogueCacheEnvelopeSchema.parse(value);
11516
+ if (envelope.contentHash) {
11517
+ const contentHash = catalogueContentHash(envelope.enriched, envelope.vercel);
11518
+ if (contentHash !== envelope.contentHash) {
11519
+ throw new Error("catalogue cache integrity check failed");
11520
+ }
11521
+ return envelope;
11522
+ }
11523
+ return catalogueCacheSchema.parse(value);
11524
+ } catch (error) {
11525
+ const reason = error instanceof Error ? error.message : String(error);
11526
+ this.options.warn?.(`Ignoring invalid catalogue cache: ${reason}`);
11527
+ return void 0;
11528
+ }
11529
+ }
11530
+ isExpired(cache) {
11531
+ const ttlMs = this.options.ttlMs ?? DEFAULT_CATALOGUE_TTL_MS;
11532
+ return this.now().getTime() - new Date(cache.fetchedAt).getTime() > ttlMs;
11533
+ }
11534
+ fromCache(cache, stale) {
11535
+ return {
11536
+ enriched: cache.enriched,
11537
+ vercel: cache.vercel,
11538
+ sources: cache.sources,
11539
+ fetchedAt: cache.fetchedAt,
11540
+ fromCache: true,
11541
+ stale
11542
+ };
11543
+ }
11544
+ async fetchJson(source, schema) {
11545
+ const response = await this.fetchImpl(source);
11546
+ if (!response.ok) {
11547
+ throw new Error(`Catalogue source failed (${response.status}): ${source}`);
11548
+ }
11549
+ const declaredLength = response.headers.get("content-length");
11550
+ if (declaredLength !== null) {
11551
+ const bytes = Number(declaredLength);
11552
+ if (Number.isFinite(bytes) && bytes > MAX_CATALOGUE_PAYLOAD_BYTES) {
11553
+ throw new Error(`Catalogue payload exceeds 32 MiB limit: ${source}`);
11554
+ }
11555
+ }
11556
+ const payload = await response.arrayBuffer();
11557
+ if (payload.byteLength > MAX_CATALOGUE_PAYLOAD_BYTES) {
11558
+ throw new Error(`Catalogue payload exceeds 32 MiB limit: ${source}`);
11559
+ }
11560
+ let value;
11561
+ try {
11562
+ value = JSON.parse(new TextDecoder().decode(payload));
11563
+ } catch {
11564
+ throw new Error(`Catalogue source returned invalid JSON: ${source}`);
11565
+ }
11566
+ return schema.parse(value);
11567
+ }
11568
+ };
11569
+ function defaultCatalogueCachePath() {
11570
+ return join44(homedir9(), ".agentwheel", "catalogue-cache.json");
11571
+ }
11572
+ function sameSources2(a, b) {
11573
+ return a.length === b.length && a.every((source, index) => source === b[index]);
11574
+ }
11575
+ function catalogueContentHash(enriched, vercel) {
11576
+ return createHash11("sha256").update(JSON.stringify({ enriched, vercel })).digest("hex");
11577
+ }
11578
+
11579
+ // src/search/index.ts
11580
+ var SCORE = {
11581
+ exactName: 1e4,
11582
+ namePrefix: 5e3,
11583
+ namePhrase: 3e3,
11584
+ tagProvidesPhrase: 2e3,
11585
+ descriptionPhrase: 1e3,
11586
+ typeEcosystemPhrase: 800,
11587
+ repositoryPhrase: 400,
11588
+ nameToken: 300,
11589
+ nameTokenPrefix: 200,
11590
+ tagProvidesToken: 180,
11591
+ descriptionToken: 80,
11592
+ typeEcosystemToken: 60,
11593
+ repositoryToken: 40,
11594
+ allTerms: 500
11595
+ };
11596
+ var PROVENANCE_ORDER = ["registry", "enriched", "vercel"];
11597
+ var MATCHED_FIELD_ORDER = ["name", "tags", "provides", "description", "type", "ecosystem", "repository"];
11598
+ function buildSearchEntries(input) {
11599
+ const enriched = catalogueEntries(input.enriched);
11600
+ const vercel = catalogueEntries(input.vercel);
11601
+ assertUniqueIdentities(enriched.map((entry) => entry.id), "enriched catalogue");
11602
+ assertUniqueIdentities(vercel.map((entry) => `${entry.o}/${entry.r}/${entry.s}`), "Vercel catalogue");
11603
+ const records = [
11604
+ ...(input.registry ?? []).map(normalizeRegistryEntry),
11605
+ ...enriched.map(normalizeEnrichedEntry),
11606
+ ...vercel.map(normalizeVercelEntry)
11607
+ ];
11608
+ const byId = /* @__PURE__ */ new Map();
11609
+ for (const record of records) {
11610
+ const existing = byId.get(record.id);
11611
+ byId.set(record.id, existing ? mergeEntry(existing, record) : record);
11612
+ }
11613
+ for (const [registryId, registryEntry] of [...byId]) {
11614
+ if (registryEntry.provenances.length !== 1 || registryEntry.provenances[0] !== "registry" || registryEntry.hasRegistrySelectors || !registryEntry.source) {
11615
+ continue;
11616
+ }
11617
+ const canonicalSource = canonicalizeSource(registryEntry.source);
11618
+ const candidates = [...byId.entries()].filter(
11619
+ ([candidateId2, candidate2]) => candidateId2 !== registryId && !candidate2.provenances.includes("registry") && candidate2.name === registryEntry.name && candidate2.source !== void 0 && canonicalizeSource(candidate2.source) === canonicalSource
11620
+ );
11621
+ if (candidates.length !== 1) continue;
11622
+ const [candidateId, candidate] = candidates[0];
11623
+ byId.set(candidateId, mergeEntry(registryEntry, candidate, candidateId));
11624
+ byId.delete(registryId);
11625
+ }
11626
+ return [...byId.values()].sort(compareStableEntries);
11627
+ }
11628
+ function searchEntries(entries, query, options = {}) {
11629
+ const normalizedQuery = normalizeSearchText(query);
11630
+ const queryTokens = tokenizeSearchText(query);
11631
+ if (!normalizedQuery || queryTokens.length === 0) return [];
11632
+ 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(
11633
+ (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)
11634
+ ).map(({ result }) => result);
11635
+ const limit = options.limit === void 0 ? 20 : Math.min(100, Math.max(0, Math.trunc(options.limit)));
11636
+ return results.slice(0, limit);
11637
+ }
11638
+ function normalizeSearchText(value) {
11639
+ return value.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim().replace(/\s+/g, " ");
11640
+ }
11641
+ function tokenizeSearchText(value) {
11642
+ const normalized = normalizeSearchText(value);
11643
+ return normalized ? [...new Set(normalized.split(" "))] : [];
11644
+ }
11645
+ function normalizeRegistryEntry(entry) {
11646
+ return {
11647
+ id: `registry:${entry.name}`,
11648
+ name: entry.name,
11649
+ description: entry.description,
11650
+ type: entry.type,
11651
+ ecosystem: inferEcosystem(entry.source),
11652
+ tags: sortedUniqueStrings(entry.tags),
11653
+ provides: [],
11654
+ source: entry.source,
11655
+ installCommand: `npx agentwheel install ${shellQuote2(entry.name)}`,
11656
+ installability: "registry",
11657
+ provenances: ["registry"],
11658
+ archived: false,
11659
+ featured: false,
11660
+ alternateDescriptions: [],
11661
+ descriptionRank: 2,
11662
+ hasRegistrySelectors: Boolean(entry.select?.length || entry.skills?.length)
11663
+ };
11664
+ }
11665
+ function normalizeEnrichedEntry(entry) {
11666
+ const source = nonEmpty(entry.source);
11667
+ const installCommand = enrichedInstallCommand(entry, source);
11668
+ return {
11669
+ id: entry.id,
11670
+ name: entry.name,
11671
+ description: entry.description ?? "",
11672
+ type: entry.type ?? inferType(entry.ecosystem),
11673
+ ecosystem: entry.ecosystem ?? void 0,
11674
+ tags: sortedUniqueStrings(entry.tags ?? []),
11675
+ provides: sortedUniqueStrings(entry.provides ?? []),
11676
+ source,
11677
+ repoUrl: nonEmpty(entry.repoUrl),
11678
+ installCommand,
11679
+ installability: source || installCommand ? "source" : "informational",
11680
+ provenances: ["enriched"],
11681
+ archived: entry.archived ?? false,
11682
+ featured: entry.featured ?? false,
11683
+ stars: entry.stars ?? void 0,
11684
+ lastPush: nonEmpty(entry.lastPush),
11685
+ alternateDescriptions: [],
11686
+ descriptionRank: 3,
11687
+ hasRegistrySelectors: false
11688
+ };
11689
+ }
11690
+ function normalizeVercelEntry(entry) {
11691
+ const path = `${entry.o}/${entry.r}/${entry.s}`;
11692
+ const source = `vercel:skills.sh/${path}`;
11693
+ return {
11694
+ id: `vercel:${path}`,
11695
+ name: entry.s,
11696
+ description: entry.d ?? "",
11697
+ type: "skill",
11698
+ ecosystem: "vercel",
11699
+ tags: [],
11700
+ provides: ["skills"],
11701
+ source,
11702
+ repoUrl: `https://github.com/${entry.o}/${entry.r}`,
11703
+ installCommand: `npx agentwheel install ${shellQuote2(source)}`,
11704
+ installability: "source",
11705
+ provenances: ["vercel"],
11706
+ archived: false,
11707
+ featured: false,
11708
+ alternateDescriptions: [],
11709
+ descriptionRank: 1,
11710
+ hasRegistrySelectors: false
11711
+ };
11712
+ }
11713
+ function mergeEntry(first, second, id = first.id) {
11714
+ const primary = second.description && second.descriptionRank > first.descriptionRank ? second : first;
11715
+ const secondary = primary === first ? second : first;
11716
+ const descriptions = uniqueStrings([
11717
+ primary.description,
11718
+ ...primary.alternateDescriptions,
11719
+ secondary.description,
11720
+ ...secondary.alternateDescriptions
11721
+ ]).filter(Boolean);
11722
+ const description = descriptions[0] ?? "";
11723
+ return {
11724
+ id,
11725
+ name: first.name || second.name,
11726
+ description,
11727
+ type: first.type ?? second.type,
11728
+ ecosystem: first.ecosystem ?? second.ecosystem,
11729
+ tags: sortedUniqueStrings([...first.tags, ...second.tags]),
11730
+ provides: sortedUniqueStrings([...first.provides, ...second.provides]),
11731
+ source: first.source ?? second.source,
11732
+ repoUrl: first.repoUrl ?? second.repoUrl,
11733
+ installCommand: first.installCommand ?? second.installCommand,
11734
+ installability: betterInstallability(first.installability, second.installability),
11735
+ provenances: PROVENANCE_ORDER.filter(
11736
+ (provenance) => first.provenances.includes(provenance) || second.provenances.includes(provenance)
11737
+ ),
11738
+ archived: first.archived || second.archived,
11739
+ featured: first.featured || second.featured,
11740
+ stars: maxDefined(first.stars, second.stars),
11741
+ lastPush: maxText(first.lastPush, second.lastPush),
11742
+ alternateDescriptions: descriptions.slice(1),
11743
+ descriptionRank: Math.max(first.descriptionRank, second.descriptionRank),
11744
+ hasRegistrySelectors: first.hasRegistrySelectors || second.hasRegistrySelectors
11745
+ };
11746
+ }
11747
+ function scoreEntry(entry, query, queryTokens) {
11748
+ let score = 0;
11749
+ const matched = /* @__PURE__ */ new Set();
11750
+ const name = normalizeSearchText(entry.name);
11751
+ const descriptions = [entry.description, ...entry.alternateDescriptions].map(normalizeSearchText);
11752
+ const tags = entry.tags.map(normalizeSearchText);
11753
+ const provides = entry.provides.map(normalizeSearchText);
11754
+ const type = normalizeSearchText(entry.type);
11755
+ const ecosystem = normalizeSearchText(entry.ecosystem ?? "");
11756
+ const repositories = [entry.source ?? "", entry.repoUrl ?? ""].map(normalizeSearchText);
11757
+ if (name === query) {
11758
+ score += SCORE.exactName;
11759
+ matched.add("name");
11760
+ } else if (name.startsWith(query)) {
11761
+ score += SCORE.namePrefix;
11762
+ matched.add("name");
11763
+ } else if (name.includes(query)) {
11764
+ score += SCORE.namePhrase;
11765
+ matched.add("name");
11766
+ }
11767
+ const tagsPhraseMatch = matchesPhrase(tags, query);
11768
+ const providesPhraseMatch = matchesPhrase(provides, query);
11769
+ if (tagsPhraseMatch || providesPhraseMatch) {
11770
+ score += SCORE.tagProvidesPhrase;
11771
+ if (tagsPhraseMatch) matched.add("tags");
11772
+ if (providesPhraseMatch) matched.add("provides");
11773
+ }
11774
+ if (matchesPhrase(descriptions, query)) {
11775
+ score += SCORE.descriptionPhrase;
11776
+ matched.add("description");
11777
+ }
11778
+ const typePhraseMatch = type.includes(query);
11779
+ const ecosystemPhraseMatch = ecosystem.includes(query);
11780
+ if (typePhraseMatch || ecosystemPhraseMatch) {
11781
+ score += SCORE.typeEcosystemPhrase;
11782
+ if (typePhraseMatch) matched.add("type");
11783
+ if (ecosystemPhraseMatch) matched.add("ecosystem");
11784
+ }
11785
+ if (matchesPhrase(repositories, query)) {
11786
+ score += SCORE.repositoryPhrase;
11787
+ matched.add("repository");
11788
+ }
11789
+ const nameTokens = name.split(" ");
11790
+ const tagTokenText = tags.join(" ");
11791
+ const provideTokenText = provides.join(" ");
11792
+ const descriptionTokenText = descriptions.join(" ");
11793
+ const repositoryTokenText = repositories.join(" ");
11794
+ let allTermsCovered = true;
11795
+ for (const token of queryTokens) {
11796
+ const nameTokenMatch = includesToken(name, token);
11797
+ if (nameTokenMatch) {
11798
+ score += SCORE.nameToken;
11799
+ matched.add("name");
11800
+ } else if (nameTokens.some((candidate) => candidate.startsWith(token))) {
11801
+ score += SCORE.nameTokenPrefix;
11802
+ matched.add("name");
11803
+ }
11804
+ const tagTokenMatch = includesToken(tagTokenText, token);
11805
+ const provideTokenMatch = includesToken(provideTokenText, token);
11806
+ if (tagTokenMatch || provideTokenMatch) {
11807
+ score += SCORE.tagProvidesToken;
11808
+ if (tagTokenMatch) matched.add("tags");
11809
+ if (provideTokenMatch) matched.add("provides");
11810
+ }
11811
+ const descriptionTokenMatch = includesToken(descriptionTokenText, token);
11812
+ if (descriptionTokenMatch) {
11813
+ score += SCORE.descriptionToken;
11814
+ matched.add("description");
11815
+ }
11816
+ const typeTokenMatch = includesToken(type, token);
11817
+ const ecosystemTokenMatch = includesToken(ecosystem, token);
11818
+ if (typeTokenMatch || ecosystemTokenMatch) {
11819
+ score += SCORE.typeEcosystemToken;
11820
+ if (typeTokenMatch) matched.add("type");
11821
+ if (ecosystemTokenMatch) matched.add("ecosystem");
11822
+ }
11823
+ const repositoryTokenMatch = includesToken(repositoryTokenText, token);
11824
+ if (repositoryTokenMatch) {
11825
+ score += SCORE.repositoryToken;
11826
+ matched.add("repository");
11827
+ }
11828
+ if (!nameTokenMatch && !tagTokenMatch && !provideTokenMatch && !descriptionTokenMatch && !typeTokenMatch && !ecosystemTokenMatch && !repositoryTokenMatch) {
11829
+ allTermsCovered = false;
11830
+ }
11831
+ }
11832
+ if (allTermsCovered) {
11833
+ score += SCORE.allTerms;
11834
+ }
11835
+ return {
11836
+ id: entry.id,
11837
+ name: entry.name,
11838
+ description: entry.description,
11839
+ type: entry.type,
11840
+ ...entry.ecosystem ? { ecosystem: entry.ecosystem } : {},
11841
+ tags: entry.tags,
11842
+ provides: entry.provides,
11843
+ ...entry.source ? { source: entry.source } : {},
11844
+ ...entry.repoUrl ? { repoUrl: entry.repoUrl } : {},
11845
+ ...entry.installCommand ? { installCommand: entry.installCommand } : {},
11846
+ installability: entry.installability,
11847
+ provenances: entry.provenances,
11848
+ score,
11849
+ matchedFields: MATCHED_FIELD_ORDER.filter((field) => matched.has(field))
11850
+ };
11851
+ }
11852
+ function catalogueEntries(catalogue) {
11853
+ if (!catalogue) return [];
11854
+ return Array.isArray(catalogue) ? catalogue : catalogue.entries;
11855
+ }
11856
+ function canonicalizeSource(source) {
11857
+ const value = source.normalize("NFKC").trim();
11858
+ const github = value.match(
11859
+ /^(?:github:|git:(?:git\+)?https?:\/\/github\.com\/|(?:git\+)?https?:\/\/github\.com\/)([^/#]+)\/([^#]+?)(?:#(.*))?$/i
11860
+ );
11861
+ if (github) {
11862
+ const owner = github[1].toLowerCase();
11863
+ const repository = github[2].replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase();
11864
+ const ref = github[3];
11865
+ return `github:${owner}/${repository}${ref === void 0 ? "" : `#${ref}`}`;
11866
+ }
11867
+ return value.replace(/\/+$/, "");
11868
+ }
11869
+ function inferEcosystem(source) {
11870
+ const canonical = canonicalizeSource(source);
11871
+ if (canonical.startsWith("vercel:")) return "vercel";
11872
+ if (canonical.startsWith("mcp-registry:")) return "mcp-registry";
11873
+ if (canonical.startsWith("clawhub:")) return "clawhub";
11874
+ if (canonical.startsWith("skillkit:")) return "skillkit";
11875
+ return void 0;
11876
+ }
11877
+ function inferType(ecosystem) {
11878
+ if (ecosystem === "vercel" || ecosystem === "skillkit") return "skill";
11879
+ if (ecosystem === "mcp-registry") return "mcp";
11880
+ if (ecosystem === "clawhub") return "plugin";
11881
+ return "package";
11882
+ }
11883
+ function betterInstallability(a, b) {
11884
+ const rank = { registry: 3, source: 2, informational: 1 };
11885
+ return rank[a] >= rank[b] ? a : b;
11886
+ }
11887
+ function nonEmpty(value) {
11888
+ return value?.trim() ? value : void 0;
11889
+ }
11890
+ function uniqueStrings(values) {
11891
+ return [...new Set(values)];
11892
+ }
11893
+ function sortedUniqueStrings(values) {
11894
+ return uniqueStrings(values).sort(compareText);
11895
+ }
11896
+ function enrichedInstallCommand(entry, source) {
11897
+ const catalogueCommand = nonEmpty(entry.installCommand);
11898
+ if (!source) return catalogueCommand;
11899
+ if (entry.ecosystem === "mcp-registry" || entry.ecosystem === "clawhub") {
11900
+ return catalogueCommand ?? `npx agentwheel install ${shellQuote2(source)}`;
11901
+ }
11902
+ return `npx agentwheel install ${shellQuote2(source)}`;
11903
+ }
11904
+ function shellQuote2(value) {
11905
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
11906
+ }
11907
+ function matchesPhrase(fields, query) {
11908
+ return fields.some((field) => field.includes(query));
11909
+ }
11910
+ function compareStableEntries(a, b) {
11911
+ return compareText(normalizeSearchText(a.name), normalizeSearchText(b.name)) || compareText(a.id, b.id);
11912
+ }
11913
+ function maxDefined(a, b) {
11914
+ if (a === void 0) return b;
11915
+ if (b === void 0) return a;
11916
+ return Math.max(a, b);
11917
+ }
11918
+ function maxText(a, b) {
11919
+ if (a === void 0) return b;
11920
+ if (b === void 0) return a;
11921
+ return a >= b ? a : b;
11922
+ }
11923
+ function assertUniqueIdentities(ids, label) {
11924
+ const seen = /* @__PURE__ */ new Set();
11925
+ for (const id of ids) {
11926
+ if (seen.has(id)) throw new Error(`Duplicate ${label} id: ${id}`);
11927
+ seen.add(id);
11928
+ }
11929
+ }
11930
+ function compareText(a, b) {
11931
+ return a < b ? -1 : a > b ? 1 : 0;
11932
+ }
11933
+ function includesToken(normalizedText, token) {
11934
+ return normalizedText === token || normalizedText.startsWith(`${token} `) || normalizedText.endsWith(` ${token}`) || normalizedText.includes(` ${token} `);
11935
+ }
11936
+
11321
11937
  // src/cli/index.ts
11322
11938
  var CLI_VERSION = resolveCliVersion();
11323
11939
  var COMPANION_SKILL_SOURCE = "github:NestDevLab/agentwheel";
@@ -11361,17 +11977,59 @@ program.command("list").description("list artifacts exposed by a package source"
11361
11977
  const resolvedInput = await resolvePackageSource(source, targetRoot);
11362
11978
  const selectedArtifacts = selectedArtifactsFromOptionsOrRegistry(options, resolvedInput.registryEntry);
11363
11979
  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") }))));
11980
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join45(targetRoot, ".agentwheel", "cache") }))));
11365
11981
  const artifacts = filterArtifactsBySelection(await driver.list(resolved), selectedArtifacts);
11366
11982
  for (const artifact of artifacts) {
11367
11983
  console.log(`${artifact.type} ${artifact.name} ${artifact.relativePath}`);
11368
11984
  }
11369
11985
  });
11986
+ 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) => {
11987
+ const trimmedQuery = query.trim();
11988
+ if (!trimmedQuery) {
11989
+ throw new Error("Search query must not be empty.");
11990
+ }
11991
+ const scope = parseSearchScope(options.scope);
11992
+ const type = options.type === void 0 ? void 0 : parseSearchType(options.type);
11993
+ const ecosystem = options.ecosystem === void 0 ? void 0 : parseSearchEcosystem(options.ecosystem);
11994
+ const limit = parseSearchLimit(options.limit);
11995
+ if (options.refresh && options.offline) {
11996
+ throw new Error("--refresh cannot be used with --offline.");
11997
+ }
11998
+ const warning = (message) => console.error(message);
11999
+ const targetRoot = normalizeTargetRoot(options.targetRoot);
12000
+ const registryRequest = scope === "all" || scope === "registry" ? new RegistryClient({ workspaceRoot: targetRoot, offline: options.offline, warn: warning }).getIndex({ refresh: options.refresh }) : void 0;
12001
+ const catalogueRequest = scope === "all" || scope === "enriched" || scope === "vercel" ? new CatalogueClient({ offline: options.offline, warn: warning }).getIndex({ refresh: options.refresh }) : void 0;
12002
+ const [registryIndex, catalogueIndex] = await Promise.all([registryRequest, catalogueRequest]);
12003
+ const entries = buildSearchEntries({
12004
+ registry: registryIndex?.entries,
12005
+ enriched: scope === "all" || scope === "enriched" ? catalogueIndex?.enriched : void 0,
12006
+ vercel: scope === "all" || scope === "vercel" ? catalogueIndex?.vercel : void 0
12007
+ });
12008
+ const results = searchEntries(entries, trimmedQuery, {
12009
+ type,
12010
+ ecosystem,
12011
+ limit,
12012
+ includeArchived: options.includeArchived
12013
+ });
12014
+ const loadedIndexes = [registryIndex, catalogueIndex].filter((index) => index !== void 0);
12015
+ const response = {
12016
+ schemaVersion: 1,
12017
+ query: trimmedQuery,
12018
+ scope,
12019
+ fromCache: loadedIndexes.every((index) => index.fromCache),
12020
+ results
12021
+ };
12022
+ if (options.json) {
12023
+ console.log(JSON.stringify(response, null, 2));
12024
+ return;
12025
+ }
12026
+ printSearchResults(trimmedQuery, results);
12027
+ });
11370
12028
  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
12029
  const targetRoot = normalizeTargetRoot(options.targetRoot);
11372
12030
  const resolvedInput = await resolvePackageSource(source, targetRoot);
11373
12031
  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") }))));
12032
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join45(targetRoot, ".agentwheel", "cache") }))));
11375
12033
  const result = await driver.scan(resolved);
11376
12034
  if (result.findings.length === 0) {
11377
12035
  console.log("Scan ok: no findings");
@@ -11434,7 +12092,7 @@ program.command("deps").description("inspect the OpenPack dependency graph").add
11434
12092
  for (const decision of result.bundle.graphLock.canonical.overrides) {
11435
12093
  console.log(`OVERRIDE ${decision.graphNodeId}:${decision.type}/${decision.name} replaces ${decision.overriddenGraphNodeId}:${decision.type}/${decision.name} via ${decision.rootId} (${decision.selector})`);
11436
12094
  }
11437
- await rm11(result.bundle.root, { recursive: true, force: true });
12095
+ await rm12(result.bundle.root, { recursive: true, force: true });
11438
12096
  }
11439
12097
  continue;
11440
12098
  }
@@ -11466,11 +12124,6 @@ program.command("registry").description("manage optional registry indexes").addC
11466
12124
  const client = new RegistryClient({ workspaceRoot: normalizeTargetRoot(options.targetRoot), warn: (message) => console.warn(message) });
11467
12125
  printRegistryEntries((await client.getIndex()).entries);
11468
12126
  })
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
12127
  ).addCommand(
11475
12128
  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
12129
  const draft = createRegistryPublishDraft(source, {
@@ -11644,7 +12297,7 @@ journalCommand.command("list").description("show pending apply journals for reso
11644
12297
  if (!journal) continue;
11645
12298
  pending += 1;
11646
12299
  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`)}`);
12300
+ console.log(` journal: ${join45(state.installRoot, ".agentwheel", `${state.state.stateKey}.apply-journal.json`)}`);
11648
12301
  console.log(` stateKey: ${state.state.stateKey}`);
11649
12302
  console.log(` createdAt: ${journal.createdAt}`);
11650
12303
  console.log(` updatedAt: ${journal.updatedAt}`);
@@ -11696,6 +12349,14 @@ async function runInstallCommand(nameOrSource, options, behavior) {
11696
12349
  if (outputFormat !== "human") {
11697
12350
  const report = await buildPlanReport(nameOrSource, normalizedOptions);
11698
12351
  if (report.targets.some((target) => target.hasBlockingChanges)) process.exitCode = 1;
12352
+ if (behavior.apply) {
12353
+ await runInstallCommand(
12354
+ nameOrSource,
12355
+ { ...normalizedOptions, format: "human", json: false },
12356
+ { apply: true, quiet: true }
12357
+ );
12358
+ report.applied = true;
12359
+ }
11699
12360
  process.stdout.write(`${renderReport(report, outputFormat)}
11700
12361
  `);
11701
12362
  return;
@@ -11735,12 +12396,14 @@ async function runInstallCommand(nameOrSource, options, behavior) {
11735
12396
  warn: (message) => console.warn(message)
11736
12397
  });
11737
12398
  for (const result of results) {
11738
- console.log(`Profile ${normalizedOptions.profile} / ${result.runtime} / ${result.packageName} at ${result.targetRoot} (${result.transport}):`);
11739
- console.log(formatPlan(result.plan));
11740
- if (result.reloaded) console.log(`Reloaded runtime via ${result.reloadCommandSummary}.`);
12399
+ if (!behavior.quiet) {
12400
+ console.log(`Profile ${normalizedOptions.profile} / ${result.runtime} / ${result.packageName} at ${result.targetRoot} (${result.transport}):`);
12401
+ console.log(formatPlan(result.plan));
12402
+ if (result.reloaded) console.log(`Reloaded runtime via ${result.reloadCommandSummary}.`);
12403
+ }
11741
12404
  if (result.plan.hasBlockingChanges) process.exitCode = 1;
11742
12405
  }
11743
- if (behavior.apply) {
12406
+ if (behavior.apply && !behavior.quiet) {
11744
12407
  console.log("Applied.");
11745
12408
  }
11746
12409
  return;
@@ -11767,7 +12430,7 @@ async function runInstallCommand(nameOrSource, options, behavior) {
11767
12430
  }
11768
12431
  }
11769
12432
  for (const result of await buildGraphPlansForTarget(target, source, { ...targetOptions, scope, extraPackage, reportFormat: outputFormat }, { mode: "install" })) {
11770
- console.log(formatGraphPlan(result));
12433
+ if (!behavior.quiet) console.log(formatGraphPlan(result));
11771
12434
  if (behavior.apply) {
11772
12435
  const transport = transportForTarget(target);
11773
12436
  const executePlugins = target.executePlugins ?? targetOptions.executePlugins;
@@ -11781,10 +12444,12 @@ async function runInstallCommand(nameOrSource, options, behavior) {
11781
12444
  enabled: target.reloadRuntimes ?? shouldReloadRuntimes(targetOptions),
11782
12445
  executePlugins
11783
12446
  });
11784
- console.log(`Applied ${result.plan.adapter} at ${result.plan.targetRoot}.`);
11785
- if (reloaded) console.log(`Reloaded runtime via ${formatReloadCommands(target.reloadCommands)}.`);
12447
+ if (!behavior.quiet) {
12448
+ console.log(`Applied ${result.plan.adapter} at ${result.plan.targetRoot}.`);
12449
+ if (reloaded) console.log(`Reloaded runtime via ${formatReloadCommands(target.reloadCommands)}.`);
12450
+ }
11786
12451
  }
11787
- await rm11(result.bundle.root, { recursive: true, force: true });
12452
+ await rm12(result.bundle.root, { recursive: true, force: true });
11788
12453
  if (result.plan.hasBlockingChanges) process.exitCode = 1;
11789
12454
  }
11790
12455
  if (behavior.apply && extraPackage && !targetOptions.onlySource) {
@@ -11879,7 +12544,7 @@ async function buildPlanReport(nameOrSource, options) {
11879
12544
  for (const result of results) {
11880
12545
  reportTargets.push(installPlanReportTarget(result.plan, result.graphLockDigest));
11881
12546
  reportWarnings.push(...result.warnings);
11882
- await rm11(result.bundle.root, { recursive: true, force: true });
12547
+ await rm12(result.bundle.root, { recursive: true, force: true });
11883
12548
  }
11884
12549
  }
11885
12550
  return planReport(reportTargets, reportWarnings);
@@ -11935,7 +12600,7 @@ async function packageEntryFromSource(source, targetRoot, options) {
11935
12600
  const bundle = await stageSource(driver, resolvedSource, {
11936
12601
  workspaceRoot: targetRoot,
11937
12602
  adapter,
11938
- cacheRoot: join44(targetRoot, ".agentwheel", "cache"),
12603
+ cacheRoot: join45(targetRoot, ".agentwheel", "cache"),
11939
12604
  mode: options.mode,
11940
12605
  ref: initialVersion?.ref,
11941
12606
  frozenLock: lockMode,
@@ -11961,7 +12626,7 @@ async function packageEntryFromSource(source, targetRoot, options) {
11961
12626
  overrides: overrideArtifactsFromOptions(options)
11962
12627
  };
11963
12628
  } finally {
11964
- await rm11(bundle.root, { recursive: true, force: true });
12629
+ await rm12(bundle.root, { recursive: true, force: true });
11965
12630
  }
11966
12631
  }
11967
12632
  function findConfiguredPackage(packages, value) {
@@ -12112,7 +12777,7 @@ async function runConfiguredGraphPackages(target, options, behavior) {
12112
12777
  console.log(`Applied ${result.plan.adapter} at ${result.plan.targetRoot}.`);
12113
12778
  if (reloaded) console.log(`Reloaded runtime via ${formatReloadCommands(target.reloadCommands)}.`);
12114
12779
  }
12115
- await rm11(result.bundle.root, { recursive: true, force: true });
12780
+ await rm12(result.bundle.root, { recursive: true, force: true });
12116
12781
  if (result.plan.hasBlockingChanges) process.exitCode = 1;
12117
12782
  }
12118
12783
  }
@@ -12329,7 +12994,7 @@ function scopeUpdatePlanToDependencies(result, selectors, previousLock, manifest
12329
12994
  selectedPreviousNodeIds,
12330
12995
  selectedRootIds
12331
12996
  );
12332
- const graphLockDigest = createHash11("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
12997
+ const graphLockDigest = createHash12("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
12333
12998
  return {
12334
12999
  ...result,
12335
13000
  bundle: { ...result.bundle, graphLock },
@@ -12497,7 +13162,7 @@ function scopeUpdatePlanToRoot(result, rootId, previousLock, manifest) {
12497
13162
  selectedPreviousNodeIds,
12498
13163
  /* @__PURE__ */ new Set([rootId])
12499
13164
  );
12500
- const graphLockDigest = createHash11("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
13165
+ const graphLockDigest = createHash12("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
12501
13166
  return {
12502
13167
  ...scoped,
12503
13168
  bundle: { ...scoped.bundle, graphLock },
@@ -12569,7 +13234,7 @@ function keepManifestEntryOperation(entry, targetRoot, scopeDescription, operati
12569
13234
  artifactType: entry.artifactType,
12570
13235
  artifactName: entry.artifactName,
12571
13236
  kind: entry.kind,
12572
- destPath: operation?.destPath ?? join44(targetRoot, entry.path),
13237
+ destPath: operation?.destPath ?? join45(targetRoot, entry.path),
12573
13238
  relativeDestPath: entry.path,
12574
13239
  desiredHash: entry.sourceHash,
12575
13240
  currentHash: operation?.currentHash ?? entry.hash,
@@ -12686,7 +13351,7 @@ async function uninstallConfiguredPackage(target, packageName, options) {
12686
13351
  if (!options.dryRun) {
12687
13352
  console.log(formatUninstallResult(result));
12688
13353
  }
12689
- if (renderedRoot) await rm11(renderedRoot, { recursive: true, force: true });
13354
+ if (renderedRoot) await rm12(renderedRoot, { recursive: true, force: true });
12690
13355
  if (plan.hasBlockingChanges) process.exitCode = 1;
12691
13356
  }
12692
13357
  }
@@ -13137,7 +13802,7 @@ async function collectPendingInstallWork(target, options) {
13137
13802
  const message = error instanceof Error ? error.message : String(error);
13138
13803
  return { pendingCount: 0, driftCount: 0, conflictCount: 0, counts: {}, error: message };
13139
13804
  } finally {
13140
- await Promise.all(results.map((result) => rm11(result.bundle.root, { recursive: true, force: true })));
13805
+ await Promise.all(results.map((result) => rm12(result.bundle.root, { recursive: true, force: true })));
13141
13806
  }
13142
13807
  }
13143
13808
  async function printDoctor(target, options) {
@@ -13154,12 +13819,12 @@ async function printDoctor(target, options) {
13154
13819
  const requestedSkills = doctorSkillRequests(target, options);
13155
13820
  const skills = [];
13156
13821
  for (const request of requestedSkills) {
13157
- const skillPath = join44(state.installRoot, targetMapping.dest, request.name);
13822
+ const skillPath = join45(state.installRoot, targetMapping.dest, request.name);
13158
13823
  const exists = await pathExists(skillPath);
13159
13824
  const manifestEntry = manifest?.entries.find((entry) => {
13160
13825
  if (entry.artifactType !== "skills") return false;
13161
13826
  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);
13827
+ return entry.artifactName === request.name || legacyInstallName === request.name || entry.path === join45(targetMapping.dest, request.name);
13163
13828
  });
13164
13829
  const status = manifestEntry ? "managed" : exists ? "present-unmanaged" : "missing";
13165
13830
  skills.push({
@@ -13239,7 +13904,7 @@ function doctorSkillLabel(name) {
13239
13904
  return `${name} skill`;
13240
13905
  }
13241
13906
  function isSyncwheelWorkspace(targetRoot) {
13242
- return existsSync(join44(targetRoot, ".syncwheel", "manifest.json"));
13907
+ return existsSync(join45(targetRoot, ".syncwheel", "manifest.json"));
13243
13908
  }
13244
13909
  function skillInstallCommand(adapter, installationType, options, skill, behavior = {}) {
13245
13910
  const args = [
@@ -13307,7 +13972,7 @@ function normalizeRuntimeScopeOptions(options, behavior = {}) {
13307
13972
  }
13308
13973
  const canDefaultTargetRoot = !options.agent && !options.all && !options.allDetected && !options.profile;
13309
13974
  if (!targetRoot && canDefaultTargetRoot && (options.user || installationType === "user" || behavior.defaultUser)) {
13310
- targetRoot = homedir9();
13975
+ targetRoot = homedir10();
13311
13976
  }
13312
13977
  if (!installationType && behavior.defaultUser) {
13313
13978
  installationType = "user";
@@ -13327,12 +13992,12 @@ function looksLikeSourceSpecifier(value) {
13327
13992
  return value.includes(":") || value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value === "~" || value.startsWith("~/");
13328
13993
  }
13329
13994
  function normalizeCliPath(value) {
13330
- if (value === "~") return homedir9();
13331
- if (value.startsWith("~/")) return resolve22(homedir9(), value.slice(2));
13995
+ if (value === "~") return homedir10();
13996
+ if (value.startsWith("~/")) return resolve22(homedir10(), value.slice(2));
13332
13997
  return resolve22(value);
13333
13998
  }
13334
13999
  function isHomePath(path) {
13335
- return resolve22(path) === resolve22(homedir9());
14000
+ return resolve22(path) === resolve22(homedir10());
13336
14001
  }
13337
14002
  function adapterListFromOption(adapter) {
13338
14003
  if (!adapter) return [];
@@ -13387,10 +14052,10 @@ function filterUninstallPlanBySelection(plan, selected) {
13387
14052
  };
13388
14053
  }
13389
14054
  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");
14055
+ await mkdir23(join45(root, "instructions"), { recursive: true });
14056
+ await mkdir23(join45(root, "rules"), { recursive: true });
14057
+ await mkdir23(join45(root, "skills"), { recursive: true });
14058
+ const manifestPath = join45(root, "openpack.json");
13394
14059
  const manifest = {
13395
14060
  schemaVersion: 2,
13396
14061
  name: "example/agentwheel-package",
@@ -13403,7 +14068,7 @@ async function initPackage(root) {
13403
14068
  };
13404
14069
  await writeFile22(manifestPath, `${JSON.stringify(manifest, null, 2)}
13405
14070
  `, "utf8");
13406
- await writeFile22(join44(root, "instructions", "AGENTS.md"), "# Agent Instructions\n", "utf8");
14071
+ await writeFile22(join45(root, "instructions", "AGENTS.md"), "# Agent Instructions\n", "utf8");
13407
14072
  }
13408
14073
  async function defaultBootstrapPackage(_root) {
13409
14074
  const packageRoot = await findAgentwheelPackageRoot(dirname32(fileURLToPath3(import.meta.url)));
@@ -13474,6 +14139,49 @@ function printRegistryEntries(entries) {
13474
14139
  console.log(`${entry.name} ${entry.type} ${entry.source} ${entry.description}${tags}`);
13475
14140
  }
13476
14141
  }
14142
+ function parseSearchScope(value) {
14143
+ const parsed = searchScopeSchema.safeParse(value);
14144
+ if (parsed.success) return parsed.data;
14145
+ throw new Error(`Invalid search scope: ${value}. Expected one of: ${searchScopeSchema.options.join(", ")}.`);
14146
+ }
14147
+ function parseSearchType(value) {
14148
+ const parsed = searchTypeSchema.safeParse(value);
14149
+ if (parsed.success) return parsed.data;
14150
+ throw new Error(`Invalid artifact type: ${value}. Expected one of: ${searchTypeSchema.options.join(", ")}.`);
14151
+ }
14152
+ function parseSearchEcosystem(value) {
14153
+ const parsed = searchEcosystemSchema.safeParse(value);
14154
+ if (parsed.success) return parsed.data;
14155
+ throw new Error(`Invalid ecosystem: ${value}. Expected one of: ${searchEcosystemSchema.options.join(", ")}.`);
14156
+ }
14157
+ function parseSearchLimit(value) {
14158
+ const limit = Number(value);
14159
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
14160
+ throw new Error(`Invalid search limit: ${value}. Expected an integer from 1 to 100.`);
14161
+ }
14162
+ return limit;
14163
+ }
14164
+ function printSearchResults(query, results) {
14165
+ if (results.length === 0) {
14166
+ console.log(`No artifacts found for ${JSON.stringify(query)}.`);
14167
+ return;
14168
+ }
14169
+ for (const [index, result] of results.entries()) {
14170
+ const ecosystem = result.ecosystem ?? "unknown";
14171
+ const provenances = result.provenances.join("+");
14172
+ console.log(
14173
+ `${index + 1}. ${result.name} [type=${result.type}; ecosystem=${ecosystem}; installability=${result.installability}; provenance=${provenances}]`
14174
+ );
14175
+ console.log(` ${result.description || "(no description)"}`);
14176
+ if (result.installCommand) {
14177
+ console.log(` Install: ${result.installCommand}`);
14178
+ } else if (result.source) {
14179
+ console.log(` Source: ${result.source}`);
14180
+ } else {
14181
+ console.log(" Install: unavailable");
14182
+ }
14183
+ }
14184
+ }
13477
14185
  async function main() {
13478
14186
  await maybeCheckForUpdate({
13479
14187
  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.1",
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.1",
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.1"
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: