hngr 0.6.0 → 0.8.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
@@ -43,10 +43,25 @@ hngr mcp add pdf-tools --agent claude-code
43
43
  # another namespace
44
44
  hngr skills add team-a/pdf-tools --agent claude-code
45
45
 
46
+ # choose several skills from a namespace in an interactive terminal
47
+ hngr skills add team-a
48
+
49
+ # expand a namespace without prompting; the trailing slash or /* is explicit
50
+ hngr skills add team-a/ --all-skills --agent claude-code
51
+ hngr skills add 'team-a/*' --all-skills --agent claude-code
52
+
46
53
  # pin a version
47
54
  hngr skills add pdf-tools@2026.10.25.1 --agent claude-code
48
55
  ```
49
56
 
57
+ A bare name such as `team-a` first looks for `system/team-a`; only when that skill
58
+ does not exist is the argument treated as a namespace to choose from. Write `team-a/`
59
+ or `team-a/*` to mean the namespace unconditionally. In an interactive terminal a
60
+ namespace opens a searchable multi-select, including when it holds only one skill;
61
+ without a terminal, pass `--all-skills` to install every skill in it, or name each
62
+ skill as `<namespace>/<name>`. A namespace cannot be version-pinned, and every
63
+ selected skill still installs into its own `<name>` directory.
64
+
50
65
  The shared `.agents/skills` store is always installed, so every universal agent reads it
51
66
  without extra work. In an interactive terminal, omitting `--global` first asks whether to
52
67
  install for the current project or the user, then shows a multi-select of the agents that
@@ -42,6 +42,7 @@ const EXCLUDED_PATHS = [
42
42
  "/api/v1/capabilities/{capability_id}/versions/{version}/content",
43
43
  "/api/v1/capabilities/{capability_id}/versions/{version}/files",
44
44
  "/api/v1/capabilities/{capability_id}/versions/{version}/file",
45
+ "/api/v1/capabilities/{capability_id}/versions/{version}/derive",
45
46
  "/api/v1/auth/methods",
46
47
  "/api/v1/auth/oidc/login",
47
48
  "/api/v1/auth/oidc/callback",
@@ -1479,6 +1480,42 @@ const COMMANDS = deriveCommands({
1479
1480
  }
1480
1481
  }
1481
1482
  },
1483
+ "/api/v1/capabilities/{capability_id}/versions/{version}/derive": { "post": {
1484
+ "operationId": "deriveCapabilityVersion",
1485
+ "summary": "Publish a new Version by editing the files of the current Skill Version.",
1486
+ "description": "Derives a new Version from the package of `{version}` by replacing the\nlisted entries and publishes it through the same pipeline as\n`publishCapabilityVersion`: CalVer allocation, the current-version\nswitch and the `capability.version_published` Audit Event all follow\nthe existing publishing rules. Only a Skill Capability accepts this\noperation; an MCP Capability answers 400.\n\n`{version}` must be the Capability's Current Version; any other CalVer\nanswers 409 `/problems/version-conflict`. The check runs again after\nthe Capability row is locked, so a concurrent publish is reported\nrather than silently replaced. Each `edits` path is matched exactly\nagainst a non-directory entry of the baseline package (case\nsensitive); an unknown path, a directory, a non-text entry, an entry\nlarger than 256 KiB, or more than 1 MiB of edits in total answers 400.\n\nThe new Version inherits the baseline Version's `readme` unchanged,\nincluding a null value, for which the existing display rule falls back\nto the package's `SKILL.md`. The request has no README field. This\noperation does not increment `download_count`.\n\nThe caller must manage the Capability's Namespace, under the same\nrules as `publishCapabilityVersion`: a deleted or unavailable\nCapability, one in an unusable Namespace, and a private one the caller\ndoes not manage answer 404. A replayed `Idempotency-Key` returns the\nfirst response without deriving again.\n",
1487
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1488
+ "parameters": [
1489
+ { "$ref": "#/components/parameters/CapabilityID" },
1490
+ {
1491
+ "name": "version",
1492
+ "in": "path",
1493
+ "required": true,
1494
+ "schema": { "$ref": "#/components/schemas/CalVer" }
1495
+ },
1496
+ { "$ref": "#/components/parameters/IdempotencyKey" }
1497
+ ],
1498
+ "requestBody": {
1499
+ "required": true,
1500
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeriveCapabilityVersionRequest" } } }
1501
+ },
1502
+ "responses": {
1503
+ "201": {
1504
+ "description": "The Capability with its new current version.",
1505
+ "headers": { "Location": {
1506
+ "description": "`/api/v1/capabilities/{capability_id}/versions/{version}`",
1507
+ "schema": { "type": "string" }
1508
+ } },
1509
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Capability" } } }
1510
+ },
1511
+ "400": { "$ref": "#/components/responses/Problem" },
1512
+ "401": { "$ref": "#/components/responses/Problem" },
1513
+ "403": { "$ref": "#/components/responses/Problem" },
1514
+ "404": { "$ref": "#/components/responses/Problem" },
1515
+ "409": { "$ref": "#/components/responses/Problem" },
1516
+ "default": { "$ref": "#/components/responses/Problem" }
1517
+ }
1518
+ } },
1482
1519
  "/api/v1/capabilities/{capability_id}/versions/{version}/content": { "get": {
1483
1520
  "operationId": "lookupCapabilityContent",
1484
1521
  "summary": "Return the Content of one Version of a Capability.",
@@ -3412,6 +3449,34 @@ const COMMANDS = deriveCommands({
3412
3449
  },
3413
3450
  "additionalProperties": false
3414
3451
  },
3452
+ "DeriveCapabilityVersionRequest": {
3453
+ "type": "object",
3454
+ "required": ["edits"],
3455
+ "properties": { "edits": {
3456
+ "type": "array",
3457
+ "description": "Entries to replace in the baseline package. Paths must be unique\nand must exist as non-directory entries of the baseline package.\n",
3458
+ "minItems": 1,
3459
+ "maxItems": 200,
3460
+ "items": { "$ref": "#/components/schemas/DeriveCapabilityVersionEdit" }
3461
+ } },
3462
+ "additionalProperties": false
3463
+ },
3464
+ "DeriveCapabilityVersionEdit": {
3465
+ "type": "object",
3466
+ "required": ["path", "content"],
3467
+ "properties": {
3468
+ "path": {
3469
+ "type": "string",
3470
+ "minLength": 1,
3471
+ "maxLength": 4096
3472
+ },
3473
+ "content": {
3474
+ "type": "string",
3475
+ "description": "UTF-8 text. The server enforces 256 KiB per entry and 1 MiB for\nthe whole request by UTF-8 byte length.\n"
3476
+ }
3477
+ },
3478
+ "additionalProperties": false
3479
+ },
3415
3480
  "Capability": {
3416
3481
  "type": "object",
3417
3482
  "required": [
@@ -4264,7 +4329,7 @@ function workflowCatalog() {
4264
4329
  name: "path",
4265
4330
  required: true,
4266
4331
  variadic: true,
4267
- description: "<name> or <namespace>/<name>, optionally @<version>"
4332
+ description: "<name>, <namespace>/<name>, or <namespace>/ and <namespace>/* to choose from a namespace; optionally @<version>"
4268
4333
  }],
4269
4334
  flags: [
4270
4335
  {
@@ -4278,6 +4343,11 @@ function workflowCatalog() {
4278
4343
  type: "boolean",
4279
4344
  description: "Install to all known agents"
4280
4345
  },
4346
+ {
4347
+ name: "all-skills",
4348
+ type: "boolean",
4349
+ description: "Install every skill in a namespace argument instead of prompting"
4350
+ },
4281
4351
  {
4282
4352
  name: "global",
4283
4353
  type: "boolean",
@@ -1,4 +1,4 @@
1
- import { t as commandCatalog } from "./catalog-r53AMymQ.mjs";
1
+ import { t as commandCatalog } from "./catalog-qfvmyg4e.mjs";
2
2
  // src/commands/commands.ts
3
3
  function run(ctx) {
4
4
  const catalog = commandCatalog();
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-r53AMymQ.mjs";
1
+ import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-qfvmyg4e.mjs";
2
2
  import { C as HngrError, S as CancelledError, c as redactUrlForDisplay, g as resolveConnection, i as buildRequestUrl, o as redactQuery, r as apiRequest, s as redactSensitive, u as DEFAULT_SERVER_URL, w as renderError } from "./http-DgHX7Fux.mjs";
3
3
  import { t as collectPages } from "./pagination-BXyFxGYA.mjs";
4
4
  import { createRequire } from "node:module";
@@ -5279,7 +5279,7 @@ function isYargsInstance(y) {
5279
5279
  // ../../node_modules/.pnpm/yargs@18.2.0/node_modules/yargs/index.mjs
5280
5280
  const Yargs = YargsFactory(esm_default);
5281
5281
  // package.json
5282
- var version = "0.6.0";
5282
+ var version = "0.8.0";
5283
5283
  // src/generated/completion.ts
5284
5284
  function isCompletionShell(value) {
5285
5285
  return value === "bash" || value === "zsh" || value === "fish";
@@ -6429,6 +6429,86 @@ var y = class {
6429
6429
  }
6430
6430
  }
6431
6431
  };
6432
+ function p$1(l, e) {
6433
+ if (l === void 0 || e.length === 0) return 0;
6434
+ const i = e.findIndex((s) => s.value === l);
6435
+ return i !== -1 ? i : 0;
6436
+ }
6437
+ function m$1(l, e) {
6438
+ return (e.label ?? String(e.value)).toLowerCase().includes(l.toLowerCase());
6439
+ }
6440
+ function g(l, e) {
6441
+ if (e) return l ? e : e[0];
6442
+ }
6443
+ let T$1 = class T extends y {
6444
+ filteredOptions;
6445
+ multiple;
6446
+ isNavigating = false;
6447
+ selectedValues = [];
6448
+ focusedValue;
6449
+ #e = 0;
6450
+ #s = "";
6451
+ #t;
6452
+ #i;
6453
+ #n;
6454
+ #l;
6455
+ get cursor() {
6456
+ return this.#e;
6457
+ }
6458
+ get userInputWithCursor() {
6459
+ if (!this.userInput) return styleText(["inverse", "hidden"], "_");
6460
+ if (this._cursor >= this.userInput.length) return `${this.userInput}\u2588`;
6461
+ const e = this.userInput.slice(0, this.cursor), t = this.userInput.slice(this.cursor, this.cursor + 1), i = this.userInput.slice(this.cursor + 1);
6462
+ return `${e}${styleText("inverse", t)}${i}`;
6463
+ }
6464
+ get options() {
6465
+ return typeof this.#i == "function" ? this.#i() : this.#i;
6466
+ }
6467
+ constructor(e) {
6468
+ super(e), this.#i = e.options, this.#n = e.placeholder, this.#l = e.completeOnTab === true;
6469
+ const t = this.options;
6470
+ this.filteredOptions = [...t], this.multiple = e.multiple === true, this.#t = typeof e.options == "function" ? e.filter : e.filter ?? m$1;
6471
+ let i;
6472
+ if (e.initialValue && Array.isArray(e.initialValue) ? this.multiple ? i = e.initialValue : i = e.initialValue.slice(0, 1) : !this.multiple && this.options.length > 0 && (i = [this.options[0]?.value]), i) for (const s of i) {
6473
+ const n = t.findIndex((r) => r.value === s);
6474
+ n !== -1 && (this.toggleSelected(s), this.#e = n);
6475
+ }
6476
+ this.focusedValue = this.options[this.#e]?.value, this.on("key", (s, n) => this.#u(s, n)), this.on("userInput", (s) => this.#o(s));
6477
+ }
6478
+ _isActionKey(e, t) {
6479
+ return e === " " || this.multiple && this.isNavigating && t.name === "space" && e !== void 0 && e !== "";
6480
+ }
6481
+ #u(e, t) {
6482
+ const i = t.name === "up", s = t.name === "down", n = t.name === "return", r = this.userInput === "" || this.userInput === " ", u = this.#n, d = this.options, c = u !== void 0 && u !== "" && d.some((o) => !o.disabled && (this.#t ? this.#t(u, o) : true));
6483
+ if (t.name === "tab" && r && c) {
6484
+ this.userInput === " " && this._clearUserInput(), this._setUserInput(u, true), this.isNavigating = false;
6485
+ return;
6486
+ }
6487
+ if (t.name === "tab" && this.#l && !this.multiple && this.focusedValue !== void 0) {
6488
+ const o = String(this.focusedValue);
6489
+ this._clearUserInput(), this._setUserInput(o, true), this.isNavigating = false;
6490
+ return;
6491
+ }
6492
+ i || s ? (this.#e = findCursor(this.#e, i ? -1 : 1, this.filteredOptions), this.focusedValue = this.filteredOptions[this.#e]?.value, this.multiple || (this.selectedValues = [this.focusedValue]), this.isNavigating = true) : n ? this.value = g(this.multiple, this.selectedValues) : this.multiple ? this.focusedValue !== void 0 && (t.name === "tab" || this.isNavigating && t.name === "space") ? this.toggleSelected(this.focusedValue) : this.isNavigating = false : (this.focusedValue && (this.selectedValues = [this.focusedValue]), this.isNavigating = false);
6493
+ }
6494
+ deselectAll() {
6495
+ this.selectedValues = [];
6496
+ }
6497
+ toggleSelected(e) {
6498
+ this.filteredOptions.length !== 0 && (this.multiple ? this.selectedValues.includes(e) ? this.selectedValues = this.selectedValues.filter((t) => t !== e) : this.selectedValues = [...this.selectedValues, e] : this.selectedValues = [e]);
6499
+ }
6500
+ #o(e) {
6501
+ if (e !== this.#s) {
6502
+ this.#s = e;
6503
+ const t = this.options;
6504
+ e && this.#t ? this.filteredOptions = t.filter((n) => this.#t?.(e, n)) : this.filteredOptions = [...t];
6505
+ const i = p$1(this.focusedValue, this.filteredOptions);
6506
+ this.#e = findCursor(i, 0, this.filteredOptions);
6507
+ const s = this.filteredOptions[this.#e];
6508
+ s && !s.disabled ? this.focusedValue = s.value : this.focusedValue = void 0, this.multiple || (this.focusedValue !== void 0 ? this.toggleSelected(this.focusedValue) : this.deselectAll());
6509
+ }
6510
+ }
6511
+ };
6432
6512
  var r = class extends y {
6433
6513
  get cursor() {
6434
6514
  return this.value ? 0 : 1;
@@ -6650,6 +6730,66 @@ const limitOptions = ({ cursor: l, options: e, style: w, output: p = process.std
6650
6730
  for (const t of s) for (const n of t) x.push(n);
6651
6731
  return c && x.push(M), x;
6652
6732
  };
6733
+ function P(t, c) {
6734
+ if (!t) return true;
6735
+ const n = (c.label ?? String(c.value ?? "")).toLowerCase(), i = (c.hint ?? "").toLowerCase(), r = String(c.value).toLowerCase(), o = t.toLowerCase();
6736
+ return n.includes(o) || i.includes(o) || r.includes(o);
6737
+ }
6738
+ const autocompleteMultiselect = (t) => {
6739
+ const c = (i, r, o, m) => {
6740
+ const p = o.includes(i.value), $ = i.label ?? String(i.value ?? ""), l = i.hint && m !== void 0 && i.value === m ? styleText("dim", ` (${i.hint})`) : "", s = p ? styleText("green", S_CHECKBOX_SELECTED) : styleText("dim", S_CHECKBOX_INACTIVE);
6741
+ return i.disabled ? `${styleText("gray", S_CHECKBOX_INACTIVE)} ${styleText(["strikethrough", "gray"], $)}` : r ? `${s} ${$}${l}` : `${s} ${styleText("dim", $)}`;
6742
+ }, n = new T$1({
6743
+ options: t.options,
6744
+ multiple: true,
6745
+ placeholder: t.placeholder,
6746
+ filter: t.filter ?? ((i, r) => P(i, r)),
6747
+ validate: () => {
6748
+ if (t.required && n.selectedValues.length === 0) return "Please select at least one item";
6749
+ },
6750
+ initialValue: t.initialValues,
6751
+ signal: t.signal,
6752
+ input: t.input,
6753
+ output: t.output,
6754
+ render() {
6755
+ const i = t.withGuide ?? settings.withGuide, r = `${i ? `${styleText("gray", S_BAR)}
6756
+ ` : ""}${symbol(this.state)} ${t.message}
6757
+ `, o = this.userInput, m = t.placeholder, p = o === "" && m !== void 0, $ = this.isNavigating || p ? styleText("dim", p ? m : o) : this.userInputWithCursor, l = this.options, s = this.filteredOptions.length !== l.length ? styleText("dim", ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? "" : "es"})`) : "";
6758
+ switch (this.state) {
6759
+ case "submit": return `${r}${i ? `${styleText("gray", S_BAR)} ` : ""}${styleText("dim", `${this.selectedValues.length} items selected`)}`;
6760
+ case "cancel": return `${r}${i ? `${styleText("gray", S_BAR)} ` : ""}${styleText(["strikethrough", "dim"], o)}`;
6761
+ default: {
6762
+ const a = this.state === "error" ? "yellow" : "cyan", u = i ? `${styleText(a, S_BAR)} ` : "", V = i ? styleText(a, S_BAR_END) : "", b = [
6763
+ `${styleText("dim", "↑/↓")} to navigate`,
6764
+ `${styleText("dim", this.isNavigating ? "Space/Tab:" : "Tab:")} select`,
6765
+ `${styleText("dim", "Enter:")} confirm`,
6766
+ `${styleText("dim", "Type:")} to search`
6767
+ ], y = this.filteredOptions.length === 0 && o ? [`${u}${styleText("yellow", "No matches found")}`] : [], S = this.state === "error" ? [`${u}${styleText("yellow", this.error)}`] : [], g = [
6768
+ ...`${r}${i ? styleText(a, S_BAR) : ""}`.split(`
6769
+ `),
6770
+ `${u}${styleText("dim", "Search:")} ${$}${s}`,
6771
+ ...y,
6772
+ ...S
6773
+ ], O = [`${u}${b.join(" • ")}`, V], d = limitOptions({
6774
+ cursor: this.cursor,
6775
+ options: this.filteredOptions,
6776
+ style: (f, L) => c(f, L, this.selectedValues, this.focusedValue),
6777
+ maxItems: t.maxItems,
6778
+ output: t.output,
6779
+ rowPadding: g.length + O.length
6780
+ });
6781
+ return [
6782
+ ...g,
6783
+ ...d.map((f) => `${u}${f}`),
6784
+ ...O
6785
+ ].join(`
6786
+ `);
6787
+ }
6788
+ }
6789
+ }
6790
+ });
6791
+ return n.prompt();
6792
+ };
6653
6793
  const confirm = (e) => {
6654
6794
  const a = e.active ?? "Yes", o = e.inactive ?? "No";
6655
6795
  return new r({
@@ -6970,6 +7110,19 @@ function clackPrompter(streams = {}) {
6970
7110
  required: false
6971
7111
  }));
6972
7112
  },
7113
+ async searchableMultiselect(message, options, initialValues = [], hint) {
7114
+ if (hint) output.write(`${import_picocolors.default.dim(hint)}\n`);
7115
+ return unwrapPrompt(await autocompleteMultiselect({
7116
+ message,
7117
+ input,
7118
+ output,
7119
+ options,
7120
+ initialValues,
7121
+ required: false,
7122
+ placeholder: "Type to search",
7123
+ maxItems: 12
7124
+ }));
7125
+ },
6973
7126
  note(message, title) {
6974
7127
  output.write(`${import_picocolors.default.green("◇")} ${title}\n`);
6975
7128
  output.write(`${import_picocolors.default.dim("│")}\n`);
@@ -7075,7 +7228,7 @@ function buildParser(onMatched) {
7075
7228
  parser.command("commands", "Print the command catalog", () => {}, (argv) => {
7076
7229
  onMatched({
7077
7230
  argv,
7078
- run: async (ctx) => (await import("./commands-_R93Y2z-.mjs")).run(ctx)
7231
+ run: async (ctx) => (await import("./commands-39DynBwu.mjs")).run(ctx)
7079
7232
  });
7080
7233
  });
7081
7234
  parser.command("auth", "Manage credentials", (auth) => {
@@ -7138,6 +7291,10 @@ function buildParser(onMatched) {
7138
7291
  type: "boolean",
7139
7292
  default: false,
7140
7293
  describe: "Install to every known agent target"
7294
+ }).option("all-skills", {
7295
+ type: "boolean",
7296
+ default: false,
7297
+ describe: "Install every skill in a namespace argument instead of prompting"
7141
7298
  }).option("global", {
7142
7299
  type: "boolean",
7143
7300
  default: false,
@@ -7155,7 +7312,7 @@ function buildParser(onMatched) {
7155
7312
  }), (argv) => {
7156
7313
  onMatched({
7157
7314
  argv,
7158
- run: async (ctx, matchedArgv) => (await import("./skills-B92ip3mJ.mjs")).add(ctx, matchedArgv)
7315
+ run: async (ctx, matchedArgv) => (await import("./skills-oCdToBxi.mjs")).add(ctx, matchedArgv)
7159
7316
  });
7160
7317
  });
7161
7318
  skills.command("publish <paths..>", "Publish a local skill directory as a capability or a new version", (publish) => publish.option("namespace", {
@@ -7183,7 +7340,7 @@ function buildParser(onMatched) {
7183
7340
  }), (argv) => {
7184
7341
  onMatched({
7185
7342
  argv,
7186
- run: async (ctx, matchedArgv) => (await import("./skills-B92ip3mJ.mjs")).publish(ctx, matchedArgv)
7343
+ run: async (ctx, matchedArgv) => (await import("./skills-oCdToBxi.mjs")).publish(ctx, matchedArgv)
7187
7344
  });
7188
7345
  });
7189
7346
  return skills.demandCommand(1);
@@ -7205,7 +7362,7 @@ function buildParser(onMatched) {
7205
7362
  }), (argv) => {
7206
7363
  onMatched({
7207
7364
  argv,
7208
- run: async (ctx, matchedArgv) => (await import("./mcp-DjSU9C8z.mjs")).add(ctx, matchedArgv)
7365
+ run: async (ctx, matchedArgv) => (await import("./mcp-CctR-PSK.mjs")).add(ctx, matchedArgv)
7209
7366
  });
7210
7367
  });
7211
7368
  mcp.command("publish <paths..>", "Publish an MCP snapshot as a capability or a new version", (publish) => publish.option("namespace", {
@@ -7233,7 +7390,7 @@ function buildParser(onMatched) {
7233
7390
  }), (argv) => {
7234
7391
  onMatched({
7235
7392
  argv,
7236
- run: async (ctx, matchedArgv) => (await import("./mcp-DjSU9C8z.mjs")).publish(ctx, matchedArgv)
7393
+ run: async (ctx, matchedArgv) => (await import("./mcp-CctR-PSK.mjs")).publish(ctx, matchedArgv)
7237
7394
  });
7238
7395
  });
7239
7396
  return mcp.demandCommand(1);
@@ -1,5 +1,5 @@
1
1
  import { C as HngrError, g as resolveConnection, s as redactSensitive, t as apiFetch, x as writeFileAtomically } from "./http-DgHX7Fux.mjs";
2
- import { S as resolveCapabilityVersion, _ as downloadContent, a as errorBody, b as parseCapabilityPath, c as runBatch, h as findAgentDefinition, i as runPublish, l as stringValues, m as AGENTS, n as explicitName, o as failedResult, p as pathExists, r as readPublishFlags, s as positionalPaths, t as resolveInstallScope, v as MCP_KIND, x as resolveCapability } from "./scope-D8k2oa_W.mjs";
2
+ import { S as parseCapabilityPath, T as resolveCapabilityVersion, _ as downloadContent, a as errorBody, c as runBatch, h as findAgentDefinition, i as runPublish, l as stringValues, m as AGENTS, n as explicitName, o as failedResult, p as pathExists, r as readPublishFlags, s as positionalPaths, t as resolveInstallScope, v as MCP_KIND, w as resolveCapability } from "./scope-D5BF1Dv_.mjs";
3
3
  import { constants } from "node:fs";
4
4
  import { access, readFile, stat } from "node:fs/promises";
5
5
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -76,6 +76,37 @@ function parseCapabilityPath(raw, kind) {
76
76
  version
77
77
  };
78
78
  }
79
+ /**
80
+ * Parses a position argument into a concrete path, a bare name to probe, or an
81
+ * explicit Namespace. `<namespace>/` and `<namespace>/*` always mean the
82
+ * Namespace; a bare name is resolved by the caller because today it means
83
+ * `system/<name>` while a miss may be a Namespace.
84
+ */
85
+ function parseCapabilityTarget(raw, kind) {
86
+ const value = raw.trim();
87
+ if (value === "") throw usage$1(`A ${kind.noun} path is required`, "Pass `<name>`, `<namespace>/<name>`, `<namespace>/` or `<namespace>/*`.");
88
+ const at = value.lastIndexOf("@");
89
+ const base = at === -1 ? value : value.slice(0, at);
90
+ if (base.endsWith("/") || base.endsWith("/*")) {
91
+ if (at !== -1) throw usage$1(`Invalid ${kind.noun} path ${JSON.stringify(raw)}`, "A version cannot be pinned on a namespace; name each one as `<namespace>/<name>@<version>`.");
92
+ const namespace = base.endsWith("/*") ? base.slice(0, -2) : base.slice(0, -1);
93
+ if (namespace === "" || namespace.includes("/")) throw usage$1(`Invalid ${kind.noun} path ${JSON.stringify(raw)}`, "Use `<namespace>/` or `<namespace>/*`.");
94
+ checkNamePart(namespace, raw, kind.noun);
95
+ return {
96
+ kind: "namespace",
97
+ namespace
98
+ };
99
+ }
100
+ const parsed = parseCapabilityPath(value, kind);
101
+ if (!value.includes("/") && parsed.version === void 0) return {
102
+ kind: "probe",
103
+ parsed
104
+ };
105
+ return {
106
+ kind: "capability",
107
+ parsed
108
+ };
109
+ }
79
110
  function candidateLabel(capability) {
80
111
  return `${capability.id} (${capability.namespace}/${capability.name}, ${capability.kind})`;
81
112
  }
@@ -99,6 +130,19 @@ async function findCapability(connection, parsed, kind, signal) {
99
130
  });
100
131
  return matches[0] ?? null;
101
132
  }
133
+ /**
134
+ * Lists every readable Capability of one kind in one Namespace, newest-first
135
+ * pages included, then sorted by name so a picker is stable across requests.
136
+ * The server has no total; an empty array means nothing is readable there.
137
+ */
138
+ async function listCapabilityNamespace(connection, namespace, kind, signal) {
139
+ return (await collectPages(CAPABILITIES_PATH, (cursor) => apiGet(connection, CAPABILITIES_PATH, {
140
+ kind: kind.kind,
141
+ namespace,
142
+ limit: 100,
143
+ cursor
144
+ }, { signal }))).filter((item) => item !== null && typeof item === "object" && item.kind === kind.kind && item.namespace === namespace && typeof item.name === "string").sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
145
+ }
102
146
  /** Resolves a path to exactly one Capability, or reports that it does not exist. */
103
147
  async function resolveCapability(connection, parsed, kind, signal) {
104
148
  const capability = await findCapability(connection, parsed, kind, signal);
@@ -637,4 +681,4 @@ async function resolveInstallScope(ctx, global) {
637
681
  if (!select || ctx.flags.json || !ctx.io.stdin.isTTY || !ctx.io.stdout.isTTY) return "project";
638
682
  return await select("Install scope", SCOPE_OPTIONS, "project") === "global" ? "global" : "project";
639
683
  }
640
- export { resolveCapabilityVersion as S, downloadContent as _, errorBody as a, parseCapabilityPath as b, runBatch as c, installAtomically as d, linkAtomically as f, homeDir as g, findAgentDefinition as h, runPublish as i, stringValues as l, AGENTS as m, explicitName as n, failedResult as o, pathExists as p, readPublishFlags as r, positionalPaths as s, resolveInstallScope as t, copyAtomically as u, MCP_KIND as v, resolveCapability as x, SKILL_KIND as y };
684
+ export { parseCapabilityTarget as C, parseCapabilityPath as S, resolveCapabilityVersion as T, downloadContent as _, errorBody as a, findCapability as b, runBatch as c, installAtomically as d, linkAtomically as f, homeDir as g, findAgentDefinition as h, runPublish as i, stringValues as l, AGENTS as m, explicitName as n, failedResult as o, pathExists as p, readPublishFlags as r, positionalPaths as s, resolveInstallScope as t, copyAtomically as u, MCP_KIND as v, resolveCapability as w, listCapabilityNamespace as x, SKILL_KIND as y };
@@ -1,5 +1,5 @@
1
1
  import { C as HngrError, g as resolveConnection, r as apiRequest } from "./http-DgHX7Fux.mjs";
2
- import { S as resolveCapabilityVersion, _ as downloadContent, a as errorBody, b as parseCapabilityPath, c as runBatch, d as installAtomically, f as linkAtomically, g as homeDir, i as runPublish, l as stringValues, m as AGENTS, n as explicitName, o as failedResult, p as pathExists, r as readPublishFlags, s as positionalPaths, t as resolveInstallScope, u as copyAtomically, x as resolveCapability, y as SKILL_KIND } from "./scope-D8k2oa_W.mjs";
2
+ import { C as parseCapabilityTarget, S as parseCapabilityPath, T as resolveCapabilityVersion, _ as downloadContent, a as errorBody, b as findCapability, c as runBatch, d as installAtomically, f as linkAtomically, g as homeDir, i as runPublish, l as stringValues, m as AGENTS, n as explicitName, o as failedResult, p as pathExists, r as readPublishFlags, s as positionalPaths, t as resolveInstallScope, u as copyAtomically, w as resolveCapability, x as listCapabilityNamespace, y as SKILL_KIND } from "./scope-D5BF1Dv_.mjs";
3
3
  import { lstat, readFile, readdir } from "node:fs/promises";
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
@@ -241,12 +241,12 @@ function readSkillPackage(buffer) {
241
241
  if (seen.has(folded)) throw invalid$1(`entry ${JSON.stringify(name)} appears more than once`);
242
242
  seen.add(folded);
243
243
  }
244
- const manifests = files.filter((entry) => lastSegment$1(entry.name).toLowerCase() === "skill.md");
245
- if (manifests.length === 0) throw invalid$1("the package has no SKILL.md");
246
- if (manifests.length > 1) throw invalid$1(`the package has more than one SKILL.md: ${manifests.map((entry) => entry.name).join(", ")}`);
247
244
  const root = packageRoot$1(names);
248
245
  const expectedManifest = `${root}SKILL.md`;
249
- if (manifests[0]?.name !== expectedManifest) throw invalid$1(`found ${manifests[0]?.name}, but the package needs SKILL.md at its root or inside its single top-level directory`);
246
+ if (!files.find((entry) => entry.name === expectedManifest)) {
247
+ const manifests = files.filter((entry) => lastSegment$1(entry.name).toLowerCase() === "skill.md");
248
+ throw invalid$1(manifests.length === 0 ? "the package has no SKILL.md" : `found ${manifests.map((entry) => entry.name).join(", ")}, but the package needs SKILL.md at its root or inside its single top-level directory`);
249
+ }
250
250
  let actualBytes = 0;
251
251
  const entries = [];
252
252
  let manifest;
@@ -455,17 +455,30 @@ async function packageSkillDirectory(dir) {
455
455
  if (seen.has(folded)) throw invalid(`entry ${JSON.stringify(entry.path)} appears more than once`);
456
456
  seen.add(folded);
457
457
  }
458
- const manifests = entries.filter((entry) => lastSegment(entry.path).toLowerCase() === "skill.md");
459
- if (manifests.length === 0) throw invalid("the directory has no SKILL.md");
460
- if (manifests.length > 1) throw invalid(`the directory has more than one SKILL.md: ${manifests.map((entry) => entry.path).join(", ")}`);
461
458
  const expected = `${packageRoot(entries.map((entry) => entry.path))}SKILL.md`;
462
- if (manifests[0]?.path !== expected) throw invalid(`found ${manifests[0]?.path}, but the package needs SKILL.md at its root or inside its single top-level directory`);
459
+ if (!entries.some((entry) => entry.path === expected)) {
460
+ const manifests = entries.filter((entry) => lastSegment(entry.path).toLowerCase() === "skill.md");
461
+ throw invalid(manifests.length === 0 ? "the directory has no SKILL.md" : `found ${manifests.map((entry) => entry.path).join(", ")}, but the package needs SKILL.md at its root or inside its single top-level directory`);
462
+ }
463
463
  return {
464
464
  zip: buildZip(entries),
465
465
  fileCount: entries.length,
466
466
  byteCount: totals.bytes
467
467
  };
468
468
  }
469
+ // src/terminal-text.ts
470
+ /**
471
+ * Catalog text is publisher-controlled. Keep a single line safe to draw in a
472
+ * terminal: drop ANSI escape sequences and remaining control characters,
473
+ * collapse whitespace, and cap the length. Skill names are already validated,
474
+ * but descriptions are not.
475
+ */
476
+ const ESCAPE_SEQUENCE = /\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\)|[P^_][\s\S]*?\u001b\\|[@-_])/g;
477
+ const CONTROL_CHARACTER = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
478
+ function terminalText(value, maxLength = 80) {
479
+ const cleaned = value.replace(ESCAPE_SEQUENCE, "").replace(CONTROL_CHARACTER, " ").replace(/\s+/g, " ").trim();
480
+ return Array.from(cleaned).slice(0, maxLength).join("");
481
+ }
469
482
  // src/commands/skills.ts
470
483
  function usage(title, detail) {
471
484
  return new HngrError({
@@ -498,12 +511,11 @@ function noAgentSelectionError(examplePath, global) {
498
511
  }
499
512
  const ALL_AGENTS = "__all__";
500
513
  /**
501
- * One multi-select for the agents that keep their own skills directory.
502
- * `.agents` is always installed, so universal agents never appear here.
503
- * Detection only preselects; a non-TTY invocation never consults the
504
- * filesystem and must pass --agent or --all.
514
+ * The agents named by flags, or null when a terminal must choose. Resolving
515
+ * this without prompting lets a non-TTY invocation fail before any network
516
+ * call, even when the arguments also include a namespace to expand.
505
517
  */
506
- async function selectAgents(ctx, argv, examplePath, global) {
518
+ function requestedAgents(argv) {
507
519
  const requested = stringValues(argv.agent);
508
520
  if (requested.length > 0) {
509
521
  const unknown = requested.filter((id) => findAgent(id) === void 0);
@@ -511,6 +523,16 @@ async function selectAgents(ctx, argv, examplePath, global) {
511
523
  return AGENT_TARGETS.filter((agent) => requested.includes(agent.id));
512
524
  }
513
525
  if (argv.all === true) return [...AGENT_TARGETS];
526
+ return null;
527
+ }
528
+ /**
529
+ * One multi-select for the agents that keep their own skills directory.
530
+ * `.agents` is always installed, so universal agents never appear here.
531
+ * Detection only preselects; a non-TTY invocation never consults the
532
+ * filesystem and must pass --agent or --all.
533
+ */
534
+ async function selectAgents(ctx, requested, examplePath, global) {
535
+ if (requested !== null) return requested;
514
536
  const multiselect = ctx.io.prompts?.multiselect;
515
537
  if (!canPrompt(ctx) || !multiselect) throw noAgentSelectionError(examplePath, global);
516
538
  const cwd = process.cwd();
@@ -546,6 +568,217 @@ function frontmatterName(manifest) {
546
568
  function assertNotReserved(name) {
547
569
  if (isReservedDirName(name)) throw usage(`Reserved directory name: ${name}`, "The names `synced` and `anthropic-skills` are reserved for agent-owned skill stores.");
548
570
  }
571
+ function capabilityRef(namespace, name) {
572
+ return `${namespace}/${name}`;
573
+ }
574
+ function skillHint(capability) {
575
+ const description = typeof capability.description === "string" ? terminalText(capability.description) : "";
576
+ if (description !== "") return description;
577
+ return capability.current_version ? `v${capability.current_version}` : "";
578
+ }
579
+ /** A namespace picker needs a TTY and either multi-select implementation. */
580
+ function canSelectSkills(ctx) {
581
+ return !ctx.flags.json && ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && (ctx.io.prompts?.searchableMultiselect !== void 0 || ctx.io.prompts?.multiselect !== void 0);
582
+ }
583
+ async function promptForSkills(ctx, candidates) {
584
+ const namespaces = new Set(candidates.map((capability) => capability.namespace));
585
+ const options = candidates.map((capability) => ({
586
+ value: capability.id,
587
+ label: namespaces.size === 1 ? capability.name : capabilityRef(capability.namespace, capability.name),
588
+ hint: skillHint(capability)
589
+ }));
590
+ const prompt = ctx.io.prompts;
591
+ const message = "Select skills to install";
592
+ const hint = "space to select · enter to install · nothing selected installs nothing";
593
+ let selected = [];
594
+ if (prompt?.searchableMultiselect) selected = await prompt.searchableMultiselect(message, options, [], hint);
595
+ else if (prompt?.multiselect) selected = await prompt.multiselect(message, options, [], hint);
596
+ const byId = new Map(candidates.map((capability) => [capability.id, capability]));
597
+ const chosen = selected.flatMap((id) => {
598
+ const capability = byId.get(id);
599
+ return capability ? [capability] : [];
600
+ });
601
+ if (chosen.length === 0) ctx.summary("No skills selected.");
602
+ return chosen;
603
+ }
604
+ function emptyExpansionError(expansion) {
605
+ if (expansion.origin !== void 0) return new HngrError({
606
+ type: "api",
607
+ title: `No skill named ${expansion.origin.namespace}/${expansion.origin.name}`,
608
+ detail: `There are also no readable skills in the ${JSON.stringify(expansion.namespace)} namespace.`
609
+ });
610
+ return new HngrError({
611
+ type: "api",
612
+ title: `No readable skills in namespace ${expansion.namespace}`,
613
+ detail: "Check the Namespace name, or list the catalog with `hngr capabilities list`."
614
+ });
615
+ }
616
+ /** Keeps the first occurrence, but an explicitly pinned version outranks a bare one. */
617
+ function dedupeUnits(units) {
618
+ const index = /* @__PURE__ */ new Map();
619
+ const result = [];
620
+ for (const unit of units) {
621
+ const key = capabilityRef(unit.parsed.namespace, unit.parsed.name);
622
+ const existing = index.get(key);
623
+ if (existing === void 0) {
624
+ index.set(key, result.length);
625
+ result.push(unit);
626
+ continue;
627
+ }
628
+ const current = result[existing];
629
+ if (current === void 0) continue;
630
+ if (unit.kind === "skill" && unit.parsed.version !== void 0 && current.kind === "skill" && current.parsed.version === void 0) result[existing] = unit;
631
+ }
632
+ return result;
633
+ }
634
+ /**
635
+ * Resolves position arguments into install units. Explicit paths stay in input
636
+ * order; each namespace's selection is inserted at its first position. A bare
637
+ * name probes `system/<name>` first, so today's meaning always wins.
638
+ */
639
+ async function resolveAddUnits(ctx, connection, targets, allSkills) {
640
+ const pending = [];
641
+ const expansions = /* @__PURE__ */ new Map();
642
+ const expand = (namespace, origin) => {
643
+ const existing = expansions.get(namespace);
644
+ if (existing === void 0) expansions.set(namespace, {
645
+ namespace,
646
+ origin,
647
+ candidates: []
648
+ });
649
+ else if (existing.origin === void 0) existing.origin = origin;
650
+ pending.push({
651
+ kind: "namespace",
652
+ namespace
653
+ });
654
+ };
655
+ for (const target of targets) {
656
+ if (target.kind === "capability") {
657
+ pending.push({
658
+ kind: "unit",
659
+ unit: {
660
+ kind: "skill",
661
+ parsed: target.parsed
662
+ }
663
+ });
664
+ continue;
665
+ }
666
+ if (target.kind === "probe") {
667
+ try {
668
+ const found = await findCapability(connection, target.parsed, SKILL_KIND);
669
+ if (found !== null) {
670
+ pending.push({
671
+ kind: "unit",
672
+ unit: {
673
+ kind: "skill",
674
+ parsed: target.parsed,
675
+ capability: found
676
+ }
677
+ });
678
+ continue;
679
+ }
680
+ } catch (err) {
681
+ if (err instanceof HngrError && err.exitCode !== 1) throw err;
682
+ pending.push({
683
+ kind: "unit",
684
+ unit: {
685
+ kind: "failure",
686
+ parsed: target.parsed,
687
+ error: err
688
+ }
689
+ });
690
+ continue;
691
+ }
692
+ expand(target.parsed.name, target.parsed);
693
+ continue;
694
+ }
695
+ expand(target.namespace);
696
+ }
697
+ const candidates = [];
698
+ for (const expansion of expansions.values()) {
699
+ try {
700
+ expansion.candidates = await listCapabilityNamespace(connection, expansion.namespace, SKILL_KIND);
701
+ } catch (err) {
702
+ if (err instanceof HngrError && err.exitCode !== 1) throw err;
703
+ expansion.error = err;
704
+ continue;
705
+ }
706
+ if (expansion.candidates.length > 0) candidates.push(...expansion.candidates);
707
+ }
708
+ const unique = /* @__PURE__ */ new Map();
709
+ for (const capability of candidates) unique.set(capabilityRef(capability.namespace, capability.name), capability);
710
+ const allCandidates = [...unique.values()].sort((a, b) => {
711
+ const left = capabilityRef(a.namespace, a.name);
712
+ const right = capabilityRef(b.namespace, b.name);
713
+ return left < right ? -1 : left > right ? 1 : 0;
714
+ });
715
+ let chosen;
716
+ if (allCandidates.length === 0) chosen = [];
717
+ else if (allSkills) chosen = allCandidates;
718
+ else if (!canSelectSkills(ctx)) throw usage("Cannot choose skills from a namespace without a terminal", "Pass --all-skills to install every skill in the namespace, or name each skill as `<namespace>/<name>`.");
719
+ else chosen = await promptForSkills(ctx, allCandidates);
720
+ const chosenRefs = new Set(chosen.map((capability) => capabilityRef(capability.namespace, capability.name)));
721
+ const units = [];
722
+ for (const entry of pending) {
723
+ if (entry.kind === "unit") {
724
+ units.push(entry.unit);
725
+ continue;
726
+ }
727
+ const expansion = expansions.get(entry.namespace);
728
+ if (expansion === void 0) continue;
729
+ if (expansion.error !== void 0) {
730
+ units.push({
731
+ kind: "failure",
732
+ parsed: expansion.origin ?? {
733
+ namespace: expansion.namespace,
734
+ name: "*"
735
+ },
736
+ error: expansion.error
737
+ });
738
+ continue;
739
+ }
740
+ if (expansion.candidates.length === 0) {
741
+ units.push({
742
+ kind: "failure",
743
+ parsed: expansion.origin ?? {
744
+ namespace: expansion.namespace,
745
+ name: "*"
746
+ },
747
+ error: emptyExpansionError(expansion)
748
+ });
749
+ continue;
750
+ }
751
+ for (const capability of expansion.candidates) {
752
+ if (!chosenRefs.has(capabilityRef(capability.namespace, capability.name))) continue;
753
+ units.push({
754
+ kind: "skill",
755
+ parsed: {
756
+ namespace: capability.namespace,
757
+ name: capability.name
758
+ },
759
+ capability
760
+ });
761
+ }
762
+ }
763
+ return {
764
+ units: dedupeUnits(units),
765
+ expanded: expansions.size > 0
766
+ };
767
+ }
768
+ function assertAddPlan(units, as, expanded) {
769
+ if (as !== void 0 && expanded) throw usage("--as cannot be used with a namespace", "Pass one explicit `<namespace>/<name>` instead.");
770
+ const skills = units.filter((unit) => unit.kind === "skill");
771
+ if (as !== void 0 && skills.length !== 1) throw usage("--as requires exactly one skill", `This command resolves to ${skills.length} skills; install them without --as.`);
772
+ const destinations = /* @__PURE__ */ new Map();
773
+ for (const unit of skills) {
774
+ const destName = as ?? unit.parsed.name;
775
+ assertNotReserved(destName);
776
+ const ref = capabilityRef(unit.parsed.namespace, unit.parsed.name);
777
+ const existing = destinations.get(destName);
778
+ if (existing !== void 0 && existing !== ref) throw usage(`Two skills would install as ${JSON.stringify(destName)}`, `${existing} and ${ref} both use that directory name; install them separately.`);
779
+ destinations.set(destName, ref);
780
+ }
781
+ }
549
782
  function planInstall(cwd, env, destName, agents, options) {
550
783
  if (options.dir) {
551
784
  const base = resolve(cwd, options.dir);
@@ -644,8 +877,8 @@ function installSummary(outcome, canonicalPath) {
644
877
  if (failed.length > 0) lines.push(` failed: ${failed.map((target) => target.path).join(", ")}`);
645
878
  return lines.join("\n");
646
879
  }
647
- async function addOne(ctx, connection, parsed, destName, agents, dir, global, copy, state) {
648
- const capability = await resolveSkill(connection, parsed);
880
+ async function addOne(ctx, connection, parsed, destName, agents, dir, global, copy, state, resolved) {
881
+ const capability = resolved ?? await resolveSkill(connection, parsed);
649
882
  const version = resolveVersion(capability, parsed);
650
883
  state.version = version;
651
884
  const plan = planInstall(process.cwd(), ctx.env, destName, agents, {
@@ -830,30 +1063,45 @@ async function offerFindCapabilities(ctx, connection, destNames, agents, options
830
1063
  }
831
1064
  async function add(ctx, argv) {
832
1065
  const rawPaths = positionalPaths(argv.paths);
833
- if (rawPaths.length === 0) throw usage("No skill path given", "Pass one or more `<name>` or `<namespace>/<name>` arguments.");
834
- const parsedPaths = rawPaths.map(parseSkillPath);
1066
+ if (rawPaths.length === 0) throw usage("No skill path given", "Pass one or more `<name>`, `<namespace>/<name>`, `<namespace>/` or `<namespace>/*` arguments.");
1067
+ const targets = rawPaths.map((raw) => parseCapabilityTarget(raw, SKILL_KIND));
835
1068
  const dir = stringFlag(argv, "dir");
836
1069
  const as = stringFlag(argv, "as");
837
1070
  if (as !== void 0 && (as.includes("/") || as.includes("\\") || as === "." || as === "..")) throw usage(`Invalid --as name ${JSON.stringify(as)}`, "Pass a single directory name.");
838
1071
  const requestedGlobal = argv.global === true;
839
1072
  const copy = argv.copy === true;
1073
+ const allSkills = argv.allSkills === true;
840
1074
  if (dir !== void 0 && requestedGlobal) throw usage("--dir cannot be combined with --global");
841
1075
  if (dir !== void 0 && (stringValues(argv.agent).length > 0 || argv.all === true)) throw usage("--dir cannot be combined with --agent or --all");
842
1076
  if (as !== void 0) assertNotReserved(as);
843
- for (const parsed of parsedPaths) assertNotReserved(as ?? parsed.name);
844
- const global = dir === void 0 ? await resolveInstallScope(ctx, requestedGlobal) === "global" : false;
845
- const agents = dir === void 0 ? await selectAgents(ctx, argv, rawPaths[0] ?? "", global) : [];
1077
+ if (as !== void 0 && targets.some((target) => target.kind === "namespace")) throw usage("--as cannot be used with a namespace", "Pass one explicit `<namespace>/<name>` instead.");
1078
+ for (const target of targets) {
1079
+ if (target.kind === "namespace") continue;
1080
+ assertNotReserved(as ?? target.parsed.name);
1081
+ }
1082
+ const requested = dir === void 0 ? requestedAgents(argv) : [];
1083
+ if (dir === void 0 && requested === null && !canPrompt(ctx)) throw noAgentSelectionError(rawPaths[0] ?? "", requestedGlobal);
846
1084
  const connection = await resolveConnection(ctx.flags, ctx.env);
1085
+ const { units, expanded } = await resolveAddUnits(ctx, connection, targets, allSkills);
1086
+ if (units.length === 0) return 0;
1087
+ assertAddPlan(units, as, expanded);
1088
+ const installable = units.some((unit) => unit.kind === "skill");
1089
+ const global = dir === void 0 && installable ? await resolveInstallScope(ctx, requestedGlobal) === "global" : false;
1090
+ const agents = dir === void 0 && installable ? await selectAgents(ctx, requested, rawPaths[0] ?? "", global) : [];
1091
+ const unitByRef = new Map(units.map((unit) => [capabilityRef(unit.parsed.namespace, unit.parsed.name), unit]));
847
1092
  const { results, failed } = await runBatch({
848
1093
  ctx,
849
- paths: parsedPaths,
1094
+ paths: units.map((unit) => unit.parsed),
850
1095
  run: async (parsed, state) => {
851
- return (await addOne(ctx, connection, parsed, as ?? parsed.name, agents, dir, global, copy, state)).result;
1096
+ const unit = unitByRef.get(capabilityRef(parsed.namespace, parsed.name));
1097
+ if (unit?.kind === "failure") throw unit.error;
1098
+ return (await addOne(ctx, connection, parsed, as ?? parsed.name, agents, dir, global, copy, state, unit?.capability)).result;
852
1099
  },
853
1100
  failure: failureResult
854
1101
  });
855
1102
  if (ctx.flags.json || !ctx.io.stdout.isTTY) ctx.writePayload({ results });
856
- if (!ctx.flags.json && !ctx.flags.dryRun && !ctx.flags.quiet && !failed && canConfirm(ctx)) await offerFindCapabilities(ctx, connection, parsedPaths.map((parsed) => as ?? parsed.name), agents, {
1103
+ const destNames = units.filter((unit) => unit.kind === "skill").map((unit) => as ?? unit.parsed.name);
1104
+ if (!ctx.flags.json && !ctx.flags.dryRun && !ctx.flags.quiet && !failed && canConfirm(ctx)) await offerFindCapabilities(ctx, connection, destNames, agents, {
857
1105
  dir,
858
1106
  global,
859
1107
  copy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hngr",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "The Hangar CLI: install skills and MCP servers into coding agents, and drive the Management API.",
5
5
  "keywords": [
6
6
  "hangar",