hngr 0.7.0 → 0.9.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
@@ -13,13 +13,13 @@ hngr --version
13
13
  ## Authenticate
14
14
 
15
15
  ```sh
16
- # Interactive: username and password against http://10.199.100.24:26434 (the
16
+ # Interactive: username and password against https://hangar.paykka.com (the
17
17
  # built-in default), then an access token is written to
18
18
  # ~/.config/hngr/config.json (0600).
19
19
  hngr auth login
20
20
 
21
- # A different deployment. The built-in default is plain http on a private
22
- # network; any other host needs https:// (or an explicit --insecure).
21
+ # A different deployment. Any non-local host needs https:// (or an explicit
22
+ # --insecure).
23
23
  hngr auth login --server https://hangar.example.com
24
24
 
25
25
  # CI or containers: read an existing access token from stdin.
@@ -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
@@ -1,4 +1,4 @@
1
- import { C as HngrError, d as configFilePath, f as deleteStoredConfig, g as resolveConnection, h as redactUrl, m as readStoredConfig, n as apiGet, p as normalizeServerUrl, t as apiFetch, v as storedAudience, y as writeStoredConfig } from "./http-DgHX7Fux.mjs";
1
+ import { C as HngrError, d as configFilePath, f as deleteStoredConfig, g as resolveConnection, h as redactUrl, m as readStoredConfig, n as apiGet, p as normalizeServerUrl, t as apiFetch, v as storedAudience, y as writeStoredConfig } from "./http-CLG2jrIE.mjs";
2
2
  import { hostname } from "node:os";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { spawn } from "node:child_process";
@@ -60,8 +60,8 @@ function canPrompt(ctx) {
60
60
  return ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && ctx.io.prompts !== void 0;
61
61
  }
62
62
  /**
63
- * Enforces https everywhere; http only for localhost, 127.0.0.1, [::1], the
64
- * built-in default, or with an explicit --insecure.
63
+ * Enforces https everywhere; http only for localhost, 127.0.0.1, [::1], or
64
+ * with an explicit --insecure.
65
65
  */
66
66
  function assertServerUrlAllowed(raw, insecure) {
67
67
  const server = normalizeServerUrl(raw);
@@ -72,7 +72,7 @@ function assertServerUrlAllowed(raw, insecure) {
72
72
  "localhost",
73
73
  "127.0.0.1",
74
74
  "[::1]"
75
- ].includes(url.hostname) || insecure || server === "http://10.199.100.24:26434") return server;
75
+ ].includes(url.hostname) || insecure) return server;
76
76
  throw new HngrError({
77
77
  type: "config",
78
78
  title: `Refusing insecure server URL ${redactUrl(server)}`,
@@ -114,7 +114,7 @@ async function revokePreviousToken(ctx, stored) {
114
114
  async function login(ctx, argv) {
115
115
  if (ctx.env.HANGAR_TOKEN) throw envTokenRefusal();
116
116
  const stored = await readStoredConfig(ctx.env);
117
- const server = assertServerUrlAllowed(ctx.flags.server ?? ctx.env.HANGAR_URL ?? stored?.server ?? "http://10.199.100.24:26434", ctx.flags.insecure);
117
+ const server = assertServerUrlAllowed(ctx.flags.server ?? ctx.env.HANGAR_URL ?? stored?.server ?? "https://hangar.paykka.com", ctx.flags.insecure);
118
118
  if (stored?.token && !ctx.flags.force) try {
119
119
  const me = asStoredPrincipal(await apiGet({
120
120
  server,
@@ -1242,7 +1242,7 @@ const COMMANDS = deriveCommands({
1242
1242
  "get": {
1243
1243
  "operationId": "listCapabilities",
1244
1244
  "summary": "List the Capabilities the caller can read, newest first.",
1245
- "description": "Reads follow the rules of `lookupCapability`. Filters combine with\nAND. The list is ordered by `created_at` then `id`, both descending,\nand paged by cursor without a total.\n",
1245
+ "description": "Reads follow the rules of `lookupCapability`. Filters combine with\nAND. The list is ordered by `created_at` then `id`, both descending,\nand paged by cursor. `total` counts every Capability matching the\nfilters, independent of the cursor.\n",
1246
1246
  "security": [
1247
1247
  {},
1248
1248
  { "sessionCookie": [] },
@@ -3660,13 +3660,23 @@ const COMMANDS = deriveCommands({
3660
3660
  },
3661
3661
  "CapabilityList": {
3662
3662
  "type": "object",
3663
- "required": ["items", "next_cursor"],
3663
+ "required": [
3664
+ "items",
3665
+ "next_cursor",
3666
+ "total"
3667
+ ],
3664
3668
  "properties": {
3665
3669
  "items": {
3666
3670
  "type": "array",
3667
3671
  "items": { "$ref": "#/components/schemas/Capability" }
3668
3672
  },
3669
- "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
3673
+ "next_cursor": { "$ref": "#/components/schemas/NextCursor" },
3674
+ "total": {
3675
+ "type": "integer",
3676
+ "format": "int64",
3677
+ "minimum": 0,
3678
+ "description": "The number of Capabilities matching the filters, independent of the cursor."
3679
+ }
3670
3680
  }
3671
3681
  },
3672
3682
  "VersionSummary": {
@@ -4329,7 +4339,7 @@ function workflowCatalog() {
4329
4339
  name: "path",
4330
4340
  required: true,
4331
4341
  variadic: true,
4332
- description: "<name> or <namespace>/<name>, optionally @<version>"
4342
+ description: "<name>, <namespace>/<name>, or <namespace>/ and <namespace>/* to choose from a namespace; optionally @<version>"
4333
4343
  }],
4334
4344
  flags: [
4335
4345
  {
@@ -4343,6 +4353,11 @@ function workflowCatalog() {
4343
4353
  type: "boolean",
4344
4354
  description: "Install to all known agents"
4345
4355
  },
4356
+ {
4357
+ name: "all-skills",
4358
+ type: "boolean",
4359
+ description: "Install every skill in a namespace argument instead of prompting"
4360
+ },
4346
4361
  {
4347
4362
  name: "global",
4348
4363
  type: "boolean",
@@ -1,4 +1,4 @@
1
- import { t as commandCatalog } from "./catalog-yVsOzNKU.mjs";
1
+ import { t as commandCatalog } from "./catalog-CmtREMFr.mjs";
2
2
  // src/commands/commands.ts
3
3
  function run(ctx) {
4
4
  const catalog = commandCatalog();
@@ -171,7 +171,7 @@ async function readStoredConfig(env) {
171
171
  * The instance the CLI talks to when `--server`, `HANGAR_URL` and the config
172
172
  * file are all absent. Point a deployment elsewhere with any of those.
173
173
  */
174
- const DEFAULT_SERVER_URL = "http://10.199.100.24:26434";
174
+ const DEFAULT_SERVER_URL = "https://hangar.paykka.com";
175
175
  const INVALID_SERVER_DETAIL = "Pass --server <url>, set HANGAR_URL, or run `hngr auth login` to store one in the config file.";
176
176
  /** Parses and normalizes a server URL (no trailing slash); invalid input is a config error. */
177
177
  function normalizeServerUrl(server) {
@@ -225,7 +225,7 @@ async function deleteStoredConfig(env) {
225
225
  */
226
226
  async function resolveConnection(flags, env) {
227
227
  const config = await readStoredConfig(env);
228
- const server = flags.server ?? env.HANGAR_URL ?? config?.server ?? "http://10.199.100.24:26434";
228
+ const server = flags.server ?? env.HANGAR_URL ?? config?.server ?? "https://hangar.paykka.com";
229
229
  const token = env.HANGAR_TOKEN ?? config?.token;
230
230
  return {
231
231
  server: normalizeServerUrl(server),
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-yVsOzNKU.mjs";
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
- import { t as collectPages } from "./pagination-BXyFxGYA.mjs";
1
+ import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-CmtREMFr.mjs";
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-CLG2jrIE.mjs";
3
+ import { t as collectPages } from "./pagination-DtoXmj2F.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { notStrictEqual, strictEqual } from "assert";
6
6
  import { basename, dirname, extname, join, normalize, relative, resolve } from "path";
@@ -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.7.0";
5282
+ var version = "0.9.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-KKAq0m6z.mjs")).run(ctx)
7231
+ run: async (ctx) => (await import("./commands-Bjui8-Nt.mjs")).run(ctx)
7079
7232
  });
7080
7233
  });
7081
7234
  parser.command("auth", "Manage credentials", (auth) => {
@@ -7088,13 +7241,13 @@ function buildParser(onMatched) {
7088
7241
  }).conflicts("password", "token"), (argv) => {
7089
7242
  onMatched({
7090
7243
  argv,
7091
- run: async (ctx, matchedArgv) => (await import("./auth-ByGgd_Yn.mjs")).login(ctx, matchedArgv)
7244
+ run: async (ctx, matchedArgv) => (await import("./auth-CQkOtVxv.mjs")).login(ctx, matchedArgv)
7092
7245
  });
7093
7246
  });
7094
7247
  auth.command("logout", "Revoke the stored token and delete the local config", () => {}, (argv) => {
7095
7248
  onMatched({
7096
7249
  argv,
7097
- run: async (ctx) => (await import("./auth-ByGgd_Yn.mjs")).logout(ctx)
7250
+ run: async (ctx) => (await import("./auth-CQkOtVxv.mjs")).logout(ctx)
7098
7251
  });
7099
7252
  });
7100
7253
  auth.command("status", "Show credential source, server, principal, token prefix and audience", (status) => status.option("offline", {
@@ -7104,7 +7257,7 @@ function buildParser(onMatched) {
7104
7257
  }), (argv) => {
7105
7258
  onMatched({
7106
7259
  argv,
7107
- run: async (ctx, matchedArgv) => (await import("./auth-ByGgd_Yn.mjs")).status(ctx, matchedArgv)
7260
+ run: async (ctx, matchedArgv) => (await import("./auth-CQkOtVxv.mjs")).status(ctx, matchedArgv)
7108
7261
  });
7109
7262
  });
7110
7263
  auth.command("token", "Print the access token to stdout and nothing else", (token) => token.option("audience", {
@@ -7115,13 +7268,13 @@ function buildParser(onMatched) {
7115
7268
  }), (argv) => {
7116
7269
  onMatched({
7117
7270
  argv,
7118
- run: async (ctx, matchedArgv) => (await import("./auth-ByGgd_Yn.mjs")).token(ctx, matchedArgv)
7271
+ run: async (ctx, matchedArgv) => (await import("./auth-CQkOtVxv.mjs")).token(ctx, matchedArgv)
7119
7272
  });
7120
7273
  });
7121
7274
  auth.command("whoami", "Print the GET /me payload", () => {}, (argv) => {
7122
7275
  onMatched({
7123
7276
  argv,
7124
- run: async (ctx) => (await import("./auth-ByGgd_Yn.mjs")).whoami(ctx)
7277
+ run: async (ctx) => (await import("./auth-CQkOtVxv.mjs")).whoami(ctx)
7125
7278
  });
7126
7279
  });
7127
7280
  registerGeneratedAuth(auth, onMatched);
@@ -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-BH59pgRw.mjs")).add(ctx, matchedArgv)
7315
+ run: async (ctx, matchedArgv) => (await import("./skills-CqHGxo_M.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-BH59pgRw.mjs")).publish(ctx, matchedArgv)
7343
+ run: async (ctx, matchedArgv) => (await import("./skills-CqHGxo_M.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-C-oJ6J5B.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-C-oJ6J5B.mjs")).publish(ctx, matchedArgv)
7237
7394
  });
7238
7395
  });
7239
7396
  return mcp.demandCommand(1);
@@ -1,5 +1,5 @@
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";
1
+ import { C as HngrError, g as resolveConnection, s as redactSensitive, t as apiFetch, x as writeFileAtomically } from "./http-CLG2jrIE.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-8BZjgIXy.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";
@@ -1,4 +1,4 @@
1
- import { C as HngrError } from "./http-DgHX7Fux.mjs";
1
+ import { C as HngrError } from "./http-CLG2jrIE.mjs";
2
2
  // src/pagination.ts
3
3
  function asCursorPage(body, path) {
4
4
  if (body === null || typeof body !== "object" || !Array.isArray(body.items) || !("next_cursor" in body)) throw new HngrError({
@@ -1,5 +1,5 @@
1
- import { C as HngrError, _ as scrubCredentials, a as toHngrError, b as writeDirectoryAtomically, g as resolveConnection, h as redactUrl, i as buildRequestUrl, l as apiErrorFromResponse, n as apiGet } from "./http-DgHX7Fux.mjs";
2
- import { t as collectPages } from "./pagination-BXyFxGYA.mjs";
1
+ import { C as HngrError, _ as scrubCredentials, a as toHngrError, b as writeDirectoryAtomically, g as resolveConnection, h as redactUrl, i as buildRequestUrl, l as apiErrorFromResponse, n as apiGet } from "./http-CLG2jrIE.mjs";
2
+ import { t as collectPages } from "./pagination-DtoXmj2F.mjs";
3
3
  import { lstat, mkdir, readFile, rename, rm, symlink, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname, 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
- 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";
1
+ import { C as HngrError, g as resolveConnection, r as apiRequest } from "./http-CLG2jrIE.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-8BZjgIXy.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";
@@ -466,6 +466,19 @@ async function packageSkillDirectory(dir) {
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.7.0",
3
+ "version": "0.9.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",