hngr 0.2.0 → 0.4.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.
@@ -1,4 +1,4 @@
1
- import { t as commandCatalog } from "./catalog-jU0-wzh3.mjs";
1
+ import { t as commandCatalog } from "./catalog-Bv304SXJ.mjs";
2
2
  // src/commands/commands.ts
3
3
  function run(ctx) {
4
4
  const catalog = commandCatalog();
@@ -86,18 +86,16 @@ async function writeFileAtomically(path, content, options = {}) {
86
86
  }
87
87
  /**
88
88
  * Writes a directory through "temp → rename": the destination is either
89
- * absent or complete, never half-written. With `force`, the old directory is
90
- * moved aside before the rename so a failed swap puts it back.
89
+ * absent or complete, never half-written. An existing directory is moved
90
+ * aside before the rename so a failed swap puts it back.
91
91
  */
92
- async function writeDirectoryAtomically(dest, writeTemp, options) {
92
+ async function writeDirectoryAtomically(dest, writeTemp, options = {}) {
93
93
  const parent = dirname(dest);
94
94
  await mkdir(parent, { recursive: true });
95
95
  const temp = resolve(parent, `.hngr-${basename(dest)}-${randomUUID()}.tmp`);
96
96
  try {
97
97
  await writeTemp(temp);
98
- const exists = await lstat(dest).then(() => true, () => false);
99
- if (exists && !options.force) throw options.existsError();
100
- if (!exists) {
98
+ if (!await lstat(dest).then(() => true, () => false)) {
101
99
  await rename(temp, dest);
102
100
  return;
103
101
  }
@@ -127,6 +125,14 @@ async function writeDirectoryAtomically(dest, writeTemp, options) {
127
125
  }
128
126
  }
129
127
  // src/config.ts
128
+ /**
129
+ * The stored token's audience. Config files written before audiences existed
130
+ * hold a management token by definition, so a missing field reads as
131
+ * `management` instead of failing to load.
132
+ */
133
+ function storedAudience(config) {
134
+ return config?.audience === "mcp" ? "mcp" : "management";
135
+ }
130
136
  /** `$XDG_CONFIG_HOME/hngr/config.json`, falling back to `~/.config/hngr/config.json`. */
131
137
  function configFilePath(env) {
132
138
  const xdg = env.XDG_CONFIG_HOME;
@@ -456,4 +462,4 @@ async function apiFetch(connection, options) {
456
462
  signal: options.signal
457
463
  });
458
464
  }
459
- export { renderError as C, HngrError as S, scrubCredentials as _, toHngrError as a, writeFileAtomically as b, redactUrlForDisplay as c, configFilePath as d, deleteStoredConfig as f, resolveConnection as g, redactUrl as h, buildRequestUrl as i, apiErrorFromResponse as l, readStoredConfig as m, apiGet as n, redactQuery as o, normalizeServerUrl as p, apiRequest as r, redactSensitive as s, apiFetch as t, DEFAULT_SERVER_URL as u, writeStoredConfig as v, CancelledError as x, writeDirectoryAtomically as y };
465
+ export { HngrError as C, CancelledError as S, scrubCredentials as _, toHngrError as a, writeDirectoryAtomically as b, redactUrlForDisplay as c, configFilePath as d, deleteStoredConfig as f, resolveConnection as g, redactUrl as h, buildRequestUrl as i, apiErrorFromResponse as l, readStoredConfig as m, apiGet as n, redactQuery as o, normalizeServerUrl as p, apiRequest as r, redactSensitive as s, apiFetch as t, DEFAULT_SERVER_URL as u, storedAudience as v, renderError as w, writeFileAtomically as x, writeStoredConfig as y };
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-jU0-wzh3.mjs";
2
- import { C as renderError, S as HngrError, c as redactUrlForDisplay, g as resolveConnection, i as buildRequestUrl, o as redactQuery, r as apiRequest, s as redactSensitive, u as DEFAULT_SERVER_URL, x as CancelledError } from "./http-2HWxLIOq.mjs";
3
- import { t as collectPages } from "./pagination-BbKLKSw3.mjs";
1
+ import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-Bv304SXJ.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";
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.2.0";
5282
+ var version = "0.4.0";
5283
5283
  // src/generated/completion.ts
5284
5284
  function isCompletionShell(value) {
5285
5285
  return value === "bash" || value === "zsh" || value === "fish";
@@ -6499,6 +6499,32 @@ let u$1 = class u extends y {
6499
6499
  });
6500
6500
  }
6501
6501
  };
6502
+ let n$1 = class n extends y {
6503
+ options;
6504
+ cursor = 0;
6505
+ get _selectedValue() {
6506
+ return this.options[this.cursor];
6507
+ }
6508
+ changeValue() {
6509
+ const e = this._selectedValue;
6510
+ this.value = e === void 0 ? void 0 : e.value;
6511
+ }
6512
+ constructor(e) {
6513
+ super(e, false), this.options = e.options;
6514
+ const o = this.options.findIndex(({ value: s }) => s === e.initialValue), t = o === -1 ? 0 : o;
6515
+ this.cursor = this.options[t]?.disabled ? findCursor(t, 1, this.options) : t, this.changeValue(), this.on("cursor", (s) => {
6516
+ switch (s) {
6517
+ case "left":
6518
+ case "up":
6519
+ this.cursor = findCursor(this.cursor, -1, this.options);
6520
+ break;
6521
+ case "down":
6522
+ case "right": this.cursor = findCursor(this.cursor, 1, this.options);
6523
+ }
6524
+ this.changeValue();
6525
+ });
6526
+ }
6527
+ };
6502
6528
  var n = class extends y {
6503
6529
  get userInputWithCursor() {
6504
6530
  if (this.state === "submit") return this.userInput;
@@ -6534,6 +6560,8 @@ const S_STEP_ERROR = unicodeOr("▲", "x");
6534
6560
  const S_STEP_SUBMIT = unicodeOr("◇", "o");
6535
6561
  const S_BAR = unicodeOr("│", "|");
6536
6562
  const S_BAR_END = unicodeOr("└", "—");
6563
+ const S_RADIO_ACTIVE = unicodeOr("●", ">");
6564
+ const S_RADIO_INACTIVE = unicodeOr("○", " ");
6537
6565
  const S_CHECKBOX_ACTIVE = unicodeOr("◻", "[•]");
6538
6566
  const S_CHECKBOX_SELECTED = unicodeOr("◼", "[+]");
6539
6567
  const S_CHECKBOX_INACTIVE = unicodeOr("◻", "[ ]");
@@ -6720,7 +6748,64 @@ ${r ? styleText("cyan", S_BAR_END) : ""}
6720
6748
  }
6721
6749
  }
6722
6750
  }).prompt();
6723
- `${styleText("dim", "↑/↓")}`, `${styleText("dim", "Enter:")}`;
6751
+ const SELECT_INSTRUCTIONS = [`${styleText("dim", "↑/↓")} to navigate`, `${styleText("dim", "Enter:")} confirm`];
6752
+ const c = (t, o) => t.includes(`
6753
+ `) ? t.split(`
6754
+ `).map((d) => o(d)).join(`
6755
+ `) : o(t);
6756
+ const select = (t) => {
6757
+ const o = (n, m) => {
6758
+ if (n === void 0) return "";
6759
+ const s = n.label ?? String(n.value);
6760
+ switch (m) {
6761
+ case "disabled": return `${styleText("gray", S_RADIO_INACTIVE)} ${c(s, (i) => styleText("gray", i))}${n.hint ? ` ${styleText("dim", `(${n.hint ?? "disabled"})`)}` : ""}`;
6762
+ case "selected": return `${c(s, (i) => styleText("dim", i))}`;
6763
+ case "active": return `${styleText("green", S_RADIO_ACTIVE)} ${s}${n.hint ? ` ${styleText("dim", `(${n.hint})`)}` : ""}`;
6764
+ case "cancelled": return `${c(s, (i) => styleText(["strikethrough", "dim"], i))}`;
6765
+ default: return `${styleText("dim", S_RADIO_INACTIVE)} ${c(s, (i) => styleText("dim", i))}`;
6766
+ }
6767
+ }, d = t.showInstructions ?? true;
6768
+ return new n$1({
6769
+ options: t.options,
6770
+ signal: t.signal,
6771
+ input: t.input,
6772
+ output: t.output,
6773
+ initialValue: t.initialValue,
6774
+ render() {
6775
+ const n = t.withGuide ?? settings.withGuide, m = `${symbol(this.state)} `, s = `${symbolBar(this.state)} `, i = wrapTextWithPrefix(t.output, t.message, s, m), u = `${n ? `${styleText("gray", S_BAR)}
6776
+ ` : ""}${i}
6777
+ `;
6778
+ switch (this.state) {
6779
+ case "submit": {
6780
+ const r = n ? `${styleText("gray", S_BAR)} ` : "";
6781
+ return `${u}${wrapTextWithPrefix(t.output, o(this.options[this.cursor], "selected"), r)}`;
6782
+ }
6783
+ case "cancel": {
6784
+ const r = n ? `${styleText("gray", S_BAR)} ` : "";
6785
+ return `${u}${wrapTextWithPrefix(t.output, o(this.options[this.cursor], "cancelled"), r)}${n ? `
6786
+ ${styleText("gray", S_BAR)}` : ""}`;
6787
+ }
6788
+ default: {
6789
+ const r = n ? `${styleText("cyan", S_BAR)} ` : "", a = u.split(`
6790
+ `).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS, n) : n ? [styleText("cyan", S_BAR_END)] : [], f = p.join(`
6791
+ `), b = p.length + 1;
6792
+ return `${u}${r}${limitOptions({
6793
+ output: t.output,
6794
+ cursor: this.cursor,
6795
+ options: this.options,
6796
+ maxItems: t.maxItems,
6797
+ columnPadding: r.length,
6798
+ rowPadding: a + b,
6799
+ style: (g, x) => o(g, g.disabled ? "disabled" : x ? "active" : "inactive")
6800
+ }).join(`
6801
+ ${r}`)}
6802
+ ${f}
6803
+ `;
6804
+ }
6805
+ }
6806
+ }
6807
+ }).prompt();
6808
+ };
6724
6809
  `${styleText("gray", S_BAR)}`;
6725
6810
  const text = (t) => new n({
6726
6811
  validate: t.validate,
@@ -6804,6 +6889,15 @@ function clackPrompter(streams = {}) {
6804
6889
  }
6805
6890
  }));
6806
6891
  },
6892
+ async select(message, options, initialValue) {
6893
+ return unwrapPrompt(await select({
6894
+ message,
6895
+ input,
6896
+ output,
6897
+ options,
6898
+ initialValue
6899
+ }));
6900
+ },
6807
6901
  async multiselect(message, options, initialValues = []) {
6808
6902
  return unwrapPrompt(await multiselect({
6809
6903
  message,
@@ -6846,7 +6940,7 @@ const GLOBAL_OPTIONS = {
6846
6940
  force: {
6847
6941
  type: "boolean",
6848
6942
  default: false,
6849
- describe: "Overwrite existing targets; the only way to skip destructive confirmations",
6943
+ describe: "Skip destructive confirmations",
6850
6944
  global: true
6851
6945
  },
6852
6946
  "dry-run": {
@@ -6911,46 +7005,53 @@ function buildParser(onMatched) {
6911
7005
  parser.command("commands", "Print the command catalog", () => {}, (argv) => {
6912
7006
  onMatched({
6913
7007
  argv,
6914
- run: async (ctx) => (await import("./commands-BchmocR_.mjs")).run(ctx)
7008
+ run: async (ctx) => (await import("./commands-CLqLwQHY.mjs")).run(ctx)
6915
7009
  });
6916
7010
  });
6917
7011
  parser.command("auth", "Manage credentials", (auth) => {
6918
- auth.command("login", "Log in with username and password and mint an access token", (login) => login.option("token", {
7012
+ auth.command("login", "Log in through the browser device flow, or with --password / --token", (login) => login.option("password", {
7013
+ type: "boolean",
7014
+ describe: "Prompt for a username and password instead of the device flow"
7015
+ }).option("token", {
6919
7016
  type: "boolean",
6920
- default: false,
6921
7017
  describe: "Read an access token from stdin instead of prompting"
6922
- }), (argv) => {
7018
+ }).conflicts("password", "token"), (argv) => {
6923
7019
  onMatched({
6924
7020
  argv,
6925
- run: async (ctx, matchedArgv) => (await import("./auth-BT8HuCif.mjs")).login(ctx, matchedArgv)
7021
+ run: async (ctx, matchedArgv) => (await import("./auth-ByGgd_Yn.mjs")).login(ctx, matchedArgv)
6926
7022
  });
6927
7023
  });
6928
7024
  auth.command("logout", "Revoke the stored token and delete the local config", () => {}, (argv) => {
6929
7025
  onMatched({
6930
7026
  argv,
6931
- run: async (ctx) => (await import("./auth-BT8HuCif.mjs")).logout(ctx)
7027
+ run: async (ctx) => (await import("./auth-ByGgd_Yn.mjs")).logout(ctx)
6932
7028
  });
6933
7029
  });
6934
- auth.command("status", "Show credential source, server, principal and token prefix", (status) => status.option("offline", {
7030
+ auth.command("status", "Show credential source, server, principal, token prefix and audience", (status) => status.option("offline", {
6935
7031
  type: "boolean",
6936
7032
  default: false,
6937
7033
  describe: "Skip the live GET /me check"
6938
7034
  }), (argv) => {
6939
7035
  onMatched({
6940
7036
  argv,
6941
- run: async (ctx, matchedArgv) => (await import("./auth-BT8HuCif.mjs")).status(ctx, matchedArgv)
7037
+ run: async (ctx, matchedArgv) => (await import("./auth-ByGgd_Yn.mjs")).status(ctx, matchedArgv)
6942
7038
  });
6943
7039
  });
6944
- auth.command("token", "Print the access token to stdout and nothing else", () => {}, (argv) => {
7040
+ auth.command("token", "Print the access token to stdout and nothing else", (token) => token.option("audience", {
7041
+ type: "string",
7042
+ choices: ["management", "mcp"],
7043
+ default: "management",
7044
+ describe: "management prints the stored token; mcp mints a new MCP token that can read and write the catalog content you have access to, without changing the stored credential"
7045
+ }), (argv) => {
6945
7046
  onMatched({
6946
7047
  argv,
6947
- run: async (ctx) => (await import("./auth-BT8HuCif.mjs")).token(ctx)
7048
+ run: async (ctx, matchedArgv) => (await import("./auth-ByGgd_Yn.mjs")).token(ctx, matchedArgv)
6948
7049
  });
6949
7050
  });
6950
7051
  auth.command("whoami", "Print the GET /me payload", () => {}, (argv) => {
6951
7052
  onMatched({
6952
7053
  argv,
6953
- run: async (ctx) => (await import("./auth-BT8HuCif.mjs")).whoami(ctx)
7054
+ run: async (ctx) => (await import("./auth-ByGgd_Yn.mjs")).whoami(ctx)
6954
7055
  });
6955
7056
  });
6956
7057
  registerGeneratedAuth(auth, onMatched);
@@ -6984,7 +7085,7 @@ function buildParser(onMatched) {
6984
7085
  }), (argv) => {
6985
7086
  onMatched({
6986
7087
  argv,
6987
- run: async (ctx, matchedArgv) => (await import("./skills-DwoDzsXd.mjs")).add(ctx, matchedArgv)
7088
+ run: async (ctx, matchedArgv) => (await import("./skills-CxMeJ5PF.mjs")).add(ctx, matchedArgv)
6988
7089
  });
6989
7090
  });
6990
7091
  skills.command("publish <paths..>", "Publish a local skill directory as a capability or a new version", (publish) => publish.option("namespace", {
@@ -7002,11 +7103,9 @@ function buildParser(onMatched) {
7002
7103
  }).option("readme", {
7003
7104
  type: "string",
7004
7105
  describe: "Markdown file to attach as the README"
7005
- }).option("tag", {
7106
+ }).option("category", {
7006
7107
  type: "string",
7007
- array: true,
7008
- nargs: 1,
7009
- describe: "Capability tag (repeatable, at most eight)"
7108
+ describe: "Category slug in the target namespace (create only; resolved even with --dry-run)"
7010
7109
  }).option("new", {
7011
7110
  type: "boolean",
7012
7111
  default: false,
@@ -7014,7 +7113,7 @@ function buildParser(onMatched) {
7014
7113
  }), (argv) => {
7015
7114
  onMatched({
7016
7115
  argv,
7017
- run: async (ctx, matchedArgv) => (await import("./skills-DwoDzsXd.mjs")).publish(ctx, matchedArgv)
7116
+ run: async (ctx, matchedArgv) => (await import("./skills-CxMeJ5PF.mjs")).publish(ctx, matchedArgv)
7018
7117
  });
7019
7118
  });
7020
7119
  return skills.demandCommand(1);
@@ -7036,7 +7135,7 @@ function buildParser(onMatched) {
7036
7135
  }), (argv) => {
7037
7136
  onMatched({
7038
7137
  argv,
7039
- run: async (ctx, matchedArgv) => (await import("./mcp-DeWHHgQK.mjs")).add(ctx, matchedArgv)
7138
+ run: async (ctx, matchedArgv) => (await import("./mcp-DWF_ELDU.mjs")).add(ctx, matchedArgv)
7040
7139
  });
7041
7140
  });
7042
7141
  mcp.command("publish <paths..>", "Publish an MCP snapshot as a capability or a new version", (publish) => publish.option("namespace", {
@@ -7054,11 +7153,9 @@ function buildParser(onMatched) {
7054
7153
  }).option("readme", {
7055
7154
  type: "string",
7056
7155
  describe: "Markdown file to attach as the README"
7057
- }).option("tag", {
7156
+ }).option("category", {
7058
7157
  type: "string",
7059
- array: true,
7060
- nargs: 1,
7061
- describe: "Capability tag (repeatable, at most eight)"
7158
+ describe: "Category slug in the target namespace (create only; resolved even with --dry-run)"
7062
7159
  }).option("new", {
7063
7160
  type: "boolean",
7064
7161
  default: false,
@@ -7066,7 +7163,7 @@ function buildParser(onMatched) {
7066
7163
  }), (argv) => {
7067
7164
  onMatched({
7068
7165
  argv,
7069
- run: async (ctx, matchedArgv) => (await import("./mcp-DeWHHgQK.mjs")).publish(ctx, matchedArgv)
7166
+ run: async (ctx, matchedArgv) => (await import("./mcp-DWF_ELDU.mjs")).publish(ctx, matchedArgv)
7070
7167
  });
7071
7168
  });
7072
7169
  return mcp.demandCommand(1);
@@ -1,5 +1,5 @@
1
- import { S as HngrError, b as writeFileAtomically, g as resolveConnection, s as redactSensitive, t as apiFetch } from "./http-2HWxLIOq.mjs";
2
- import { _ as resolveCapabilityVersion, a as failedResult, c as stringValues, f as downloadContent, g as resolveCapability, h as parseCapabilityPath, i as errorBody, l as AGENTS, n as readPublishFlags, o as positionalPaths, p as MCP_KIND, r as runPublish, s as runBatch, t as explicitName, u as findAgentDefinition } from "./publish-D7zaQl9x.mjs";
1
+ import { C as HngrError, g as resolveConnection, s as redactSensitive, t as apiFetch, x as writeFileAtomically } from "./http-DgHX7Fux.mjs";
2
+ import { _ as resolveCapability, a as errorBody, c as runBatch, d as findAgentDefinition, g as parseCapabilityPath, i as runPublish, l as stringValues, m as MCP_KIND, n as explicitName, o as failedResult, p as downloadContent, r as readPublishFlags, s as positionalPaths, t as resolveInstallScope, u as AGENTS, v as resolveCapabilityVersion } from "./scope-Ch6wZCgE.mjs";
3
3
  import { constants } from "node:fs";
4
4
  import { access, readFile, stat } from "node:fs/promises";
5
5
  import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -98,31 +98,24 @@ async function userServerState(executable, name, env) {
98
98
  /**
99
99
  * Installs one user-scope server through Claude Code itself.
100
100
  * `~/.claude.json` is never edited directly: Claude Code owns that file and may
101
- * hold live session state. A conflicting user-scope server fails without
102
- * `force`; with `force` it is removed through the CLI before the new entry is
103
- * added, so every write still goes through Claude Code.
101
+ * hold live session state. Re-installing replaces an existing user-scope
102
+ * server through the CLI before the new entry is added, so every write still
103
+ * goes through Claude Code.
104
104
  */
105
- async function addJsonWithClaude(env, name, config, force) {
105
+ async function addJsonWithClaude(env, name, config) {
106
106
  const executable = await findExecutable(EXECUTABLE, env);
107
107
  if (!executable) throw new HngrError({
108
108
  type: "config",
109
109
  title: "Claude Code CLI not found",
110
110
  detail: "`claude` must be on PATH to install user-level MCP servers; install Claude Code or use project-level (no --global) installation."
111
111
  });
112
- if (await userServerState(executable, name, env) === "present") {
113
- if (!force) throw new HngrError({
114
- type: "io",
115
- title: `MCP server ${JSON.stringify(name)} already exists in Claude Code's user config`,
116
- detail: "Pass --force to replace it."
117
- });
118
- await runChecked(executable, [
119
- "mcp",
120
- "remove",
121
- "--scope",
122
- "user",
123
- name
124
- ], env, name);
125
- }
112
+ if (await userServerState(executable, name, env) === "present") await runChecked(executable, [
113
+ "mcp",
114
+ "remove",
115
+ "--scope",
116
+ "user",
117
+ name
118
+ ], env, name);
126
119
  await runChecked(executable, [
127
120
  "mcp",
128
121
  "add-json",
@@ -202,8 +195,8 @@ function readTargetDocument(raw, label) {
202
195
  }
203
196
  /**
204
197
  * Merges the incoming servers into the document's `mcpServers`. Existing
205
- * servers and other top-level keys survive. A name collision fails unless
206
- * `force`, which replaces only the colliding entries.
198
+ * servers and other top-level keys survive; a name collision is replaced, so
199
+ * re-installing a capability is idempotent.
207
200
  */
208
201
  function mergeSnapshot(document, incoming, options) {
209
202
  const existing = document.mcpServers;
@@ -211,21 +204,13 @@ function mergeSnapshot(document, incoming, options) {
211
204
  if (existing === null || typeof existing !== "object" || Array.isArray(existing)) throw ioError(`"mcpServers" in ${options.label} is not an object`, "Fix the target file before installing MCP servers.");
212
205
  }
213
206
  const servers = { ...existing ?? {} };
214
- const conflicts = [];
215
207
  const added = [];
216
208
  const replaced = [];
217
- for (const [name, entry] of Object.entries(incoming)) if (Object.hasOwn(servers, name)) {
218
- if (!options.force) {
219
- conflicts.push(name);
220
- continue;
221
- }
209
+ for (const [name, entry] of Object.entries(incoming)) {
210
+ if (Object.hasOwn(servers, name)) replaced.push(name);
211
+ else added.push(name);
222
212
  servers[name] = entry;
223
- replaced.push(name);
224
- } else {
225
- servers[name] = entry;
226
- added.push(name);
227
213
  }
228
- if (conflicts.length > 0) throw ioError(`MCP ${conflicts.length === 1 ? "server" : "servers"} already configured in ${options.label}: ${conflicts.join(", ")}`, "Pass --force to replace only the conflicting entries.");
229
214
  return {
230
215
  document: {
231
216
  ...document,
@@ -381,11 +366,8 @@ async function readExisting(path, label) {
381
366
  * redacted. The file is never written by dry-run, so this only affects the
382
367
  * preview.
383
368
  */
384
- function previewJsonTarget(plan, existing, servers, force) {
385
- const merged = mergeSnapshot(readTargetDocument(existing, plan.path), servers, {
386
- force,
387
- label: plan.path
388
- });
369
+ function previewJsonTarget(plan, existing, servers) {
370
+ const merged = mergeSnapshot(readTargetDocument(existing, plan.path), servers, { label: plan.path });
389
371
  return serializeTargetDocument(redactSensitive(merged.document));
390
372
  }
391
373
  /** Reports one target's failure without aborting the remaining targets. */
@@ -393,16 +375,13 @@ function reportTargetFailure(ctx, plan, err) {
393
375
  const body = errorBody(err);
394
376
  ctx.summary(`error: ${plan.agent} ${plan.path}: ${body.title}`);
395
377
  }
396
- async function installJsonTarget(plan, servers, force) {
378
+ async function installJsonTarget(plan, servers) {
397
379
  const filePath = plan.filePath;
398
380
  if (!filePath) throw new HngrError({
399
381
  type: "io",
400
382
  title: `No file path configured for ${plan.agent}`
401
383
  });
402
- const merged = mergeSnapshot(readTargetDocument(await readExisting(filePath, plan.path), plan.path), servers, {
403
- force,
404
- label: plan.path
405
- });
384
+ const merged = mergeSnapshot(readTargetDocument(await readExisting(filePath, plan.path), plan.path), servers, { label: plan.path });
406
385
  await writeFileAtomically(filePath, serializeTargetDocument(merged.document), { label: plan.path });
407
386
  }
408
387
  async function addOne(ctx, connection, parsed, targets, state) {
@@ -419,7 +398,7 @@ async function addOne(ctx, connection, parsed, targets, state) {
419
398
  let failure = null;
420
399
  for (const plan of targets) try {
421
400
  if (plan.kind === "json") {
422
- const preview = previewJsonTarget(plan, plan.filePath ? await readExisting(plan.filePath, plan.path) : void 0, snapshot.servers, ctx.flags.force);
401
+ const preview = previewJsonTarget(plan, plan.filePath ? await readExisting(plan.filePath, plan.path) : void 0, snapshot.servers);
423
402
  ctx.detail(`dry-run: would write ${plan.path}\n${preview}`);
424
403
  } else for (const [name, entry] of Object.entries(snapshot.servers)) ctx.detail(`dry-run: claude mcp add-json --scope user ${name} ${JSON.stringify(redactSensitive(entry))}`);
425
404
  dryTargets.push({
@@ -449,8 +428,8 @@ async function addOne(ctx, connection, parsed, targets, state) {
449
428
  const installedTargets = [];
450
429
  let failure = null;
451
430
  for (const plan of targets) try {
452
- if (plan.kind === "json") await installJsonTarget(plan, snapshot.servers, ctx.flags.force);
453
- else for (const [name, entry] of Object.entries(snapshot.servers)) await addJsonWithClaude(ctx.env, name, entry, ctx.flags.force);
431
+ if (plan.kind === "json") await installJsonTarget(plan, snapshot.servers);
432
+ else for (const [name, entry] of Object.entries(snapshot.servers)) await addJsonWithClaude(ctx.env, name, entry);
454
433
  installedTargets.push({
455
434
  agent: plan.agent,
456
435
  path: plan.path,
@@ -517,7 +496,7 @@ function mcpPrepared(config, metadata) {
517
496
  visibility: metadata.visibility
518
497
  };
519
498
  if (metadata.readme !== void 0) body.readme = metadata.readme;
520
- if (metadata.tags.length > 0) body.tags = metadata.tags;
499
+ if (metadata.category !== void 0) body.category = metadata.category;
521
500
  body.config = config;
522
501
  return body;
523
502
  };
@@ -536,7 +515,7 @@ function mcpPrepared(config, metadata) {
536
515
  if (metadata.description !== void 0) preview.description = metadata.description;
537
516
  preview.visibility = metadata.visibility;
538
517
  if (metadata.readme !== void 0) preview.readme = metadata.readme;
539
- if (metadata.tags.length > 0) preview.tags = metadata.tags;
518
+ if (metadata.category !== void 0) preview.category = metadata.category;
540
519
  preview.config = `<${serverCount} server${serverCount === 1 ? "" : "s"}>`;
541
520
  return preview;
542
521
  },
@@ -569,7 +548,7 @@ function defaultMcpName(file) {
569
548
  async function publish(ctx, argv) {
570
549
  const rawPaths = positionalPaths(argv.paths);
571
550
  if (rawPaths.length === 0) throw usage("No MCP snapshot given", "Pass one or more JSON snapshot files.");
572
- const flags = await readPublishFlags(argv, "mcp");
551
+ const flags = await readPublishFlags(argv);
573
552
  const forcedName = explicitName(argv, rawPaths.length);
574
553
  const items = rawPaths.map((file) => ({
575
554
  name: forcedName ?? defaultMcpName(file),
@@ -587,7 +566,7 @@ async function add(ctx, argv) {
587
566
  const rawPaths = positionalPaths(argv.paths);
588
567
  if (rawPaths.length === 0) throw usage("No capability path given", "Pass one or more `<name>` or `<namespace>/<name>` arguments.");
589
568
  const parsedPaths = rawPaths.map(parseMcpPath);
590
- const scope = argv.global === true ? "global" : "project";
569
+ const scope = await resolveInstallScope(ctx, argv.global === true);
591
570
  const targets = selectMcpTargets(stringValues(argv.agent), argv.all === true, scope, process.cwd(), ctx.env);
592
571
  const connection = await resolveConnection(ctx.flags, ctx.env);
593
572
  let approvalHintShown = false;
@@ -1,4 +1,4 @@
1
- import { S as HngrError } from "./http-2HWxLIOq.mjs";
1
+ import { C as HngrError } from "./http-DgHX7Fux.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 { S as HngrError, _ as scrubCredentials, a as toHngrError, g as resolveConnection, h as redactUrl, i as buildRequestUrl, l as apiErrorFromResponse, n as apiGet } from "./http-2HWxLIOq.mjs";
2
- import { t as collectPages } from "./pagination-BbKLKSw3.mjs";
1
+ import { C as HngrError, _ as scrubCredentials, a as toHngrError, 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";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { join, resolve } from "node:path";
@@ -368,6 +368,7 @@ async function runBatch(options) {
368
368
  };
369
369
  }
370
370
  // src/commands/publish.ts
371
+ const CATEGORIES_PATH = "/api/v1/categories";
371
372
  const VISIBILITIES = [
372
373
  "public",
373
374
  "internal",
@@ -397,21 +398,15 @@ async function readOptionalFile(path) {
397
398
  }
398
399
  }
399
400
  /** Reads the shared publish flags; a bad value fails before any request. */
400
- async function readPublishFlags(argv, kind) {
401
+ async function readPublishFlags(argv) {
401
402
  const visibility = stringFlag(argv, "visibility") ?? "private";
402
403
  if (!VISIBILITIES.includes(visibility)) throw usage(`Invalid --visibility ${JSON.stringify(visibility)}`, `Use one of: ${VISIBILITIES.join(", ")}.`);
403
- const tags = stringValues(argv.tag);
404
- if (tags.length > 8) throw usage("Too many --tag values", "At most eight tags are allowed.");
405
- for (const tag of tags) {
406
- if (tag.trim() === "") throw usage("Empty --tag value", "Pass a non-empty tag.");
407
- if (kind === "skill" && tag.includes(",")) throw usage(`Invalid --tag ${JSON.stringify(tag)}`, "A Skill tag cannot contain a comma; the multipart form joins tags with commas.");
408
- }
409
404
  return {
410
405
  namespace: stringFlag(argv, "namespace") ?? "system",
411
406
  description: stringFlag(argv, "description"),
412
407
  visibility,
413
408
  readme: await readOptionalFile(stringFlag(argv, "readme")),
414
- tags,
409
+ category: stringFlag(argv, "category"),
415
410
  forceNew: argv.new === true
416
411
  };
417
412
  }
@@ -424,6 +419,39 @@ function explicitName(argv, targetCount) {
424
419
  function asRecord(value) {
425
420
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
426
421
  }
422
+ /** Reads one taxonomy page and narrows the fields resolution needs. */
423
+ async function categoryPage(connection, namespace, query) {
424
+ const body = await apiGet(connection, CATEGORIES_PATH, {
425
+ namespace,
426
+ limit: 200,
427
+ ...query === void 0 ? {} : { q: query }
428
+ });
429
+ const items = asRecord(body)?.items;
430
+ if (!Array.isArray(items)) throw new HngrError({
431
+ type: "api",
432
+ title: "The server returned an unexpected response shape",
433
+ detail: `Expected {items: [...], truncated: ...} from GET ${CATEGORIES_PATH}.`
434
+ });
435
+ return {
436
+ slugs: items.map((item) => asRecord(item)?.slug).filter((value) => typeof value === "string"),
437
+ truncated: asRecord(body)?.truncated === true
438
+ };
439
+ }
440
+ /**
441
+ * Resolves a Category slug against the target Namespace's taxonomy. A slug the
442
+ * Namespace does not own is a usage error; any transport or HTTP failure
443
+ * propagates, so the caller never uploads after a failed lookup. A truncated
444
+ * first page is re-queried by slug, and only an exact match counts, so a large
445
+ * taxonomy does not turn a valid Category into an unknown one.
446
+ */
447
+ async function resolveCategorySlug(connection, namespace, slug) {
448
+ const first = await categoryPage(connection, namespace);
449
+ if (first.slugs.includes(slug)) return slug;
450
+ if (first.truncated) {
451
+ if ((await categoryPage(connection, namespace, slug)).slugs.includes(slug)) return slug;
452
+ }
453
+ throw usage(`Unknown --category ${JSON.stringify(slug)}`, first.slugs.length > 0 ? `Available in ${namespace}: ${first.slugs.join(", ")}.` : `No Categories exist in the ${namespace} namespace.`);
454
+ }
427
455
  function printPreview(ctx, kind, namespace, name, preview) {
428
456
  ctx.detail(`dry-run: would publish ${kind} ${namespace}/${name}`);
429
457
  for (const [key, value] of Object.entries(preview)) {
@@ -439,6 +467,7 @@ function printPreview(ctx, kind, namespace, name, preview) {
439
467
  async function runPublish(options) {
440
468
  const { ctx } = options;
441
469
  const connection = await resolveConnection(ctx.flags, ctx.env);
470
+ if (options.category !== void 0) await resolveCategorySlug(connection, options.namespace, options.category);
442
471
  const results = [];
443
472
  for (const item of options.items) {
444
473
  const base = {
@@ -459,7 +488,7 @@ async function runPublish(options) {
459
488
  description: options.description,
460
489
  visibility: options.visibility,
461
490
  readme: options.readme,
462
- tags: options.tags
491
+ category: options.category
463
492
  };
464
493
  const prepared = await item.prepare(metadata);
465
494
  if (ctx.flags.dryRun) {
@@ -475,6 +504,7 @@ async function runPublish(options) {
475
504
  name: item.name
476
505
  }, options.info);
477
506
  if (existing !== null && options.forceNew) throw usage(`The ${options.info.noun} ${options.namespace}/${item.name} already exists`, "Drop --new to publish a new version of it.");
507
+ if (existing !== null && options.category !== void 0) throw usage(`Cannot change the category of the existing ${options.info.noun} ${options.namespace}/${item.name}`, "Drop --category to publish a new version; change the category in the Web console.");
478
508
  let response;
479
509
  let mode;
480
510
  if (existing !== null) {
@@ -509,4 +539,25 @@ async function runPublish(options) {
509
539
  ctx.writePayload({ results });
510
540
  return results.some((result) => result.status === "failed") ? 1 : 0;
511
541
  }
512
- export { resolveCapabilityVersion as _, failedResult as a, stringValues as c, homeDir as d, downloadContent as f, resolveCapability as g, parseCapabilityPath as h, errorBody as i, AGENTS as l, SKILL_KIND as m, readPublishFlags as n, positionalPaths as o, MCP_KIND as p, runPublish as r, runBatch as s, explicitName as t, findAgentDefinition as u };
542
+ // src/commands/scope.ts
543
+ const SCOPE_OPTIONS = [{
544
+ value: "project",
545
+ label: "Project",
546
+ hint: "install into the current directory"
547
+ }, {
548
+ value: "global",
549
+ label: "Global",
550
+ hint: "install for the current user"
551
+ }];
552
+ /**
553
+ * Resolves project vs global install scope. `--global` wins without asking.
554
+ * Without an interactive prompter the current-directory default is kept, so
555
+ * scripts, pipes and `--json` runs never block on a prompt.
556
+ */
557
+ async function resolveInstallScope(ctx, global) {
558
+ if (global) return "global";
559
+ const select = ctx.io.prompts?.select;
560
+ if (!select || !ctx.io.stdin.isTTY || !ctx.io.stdout.isTTY) return "project";
561
+ return await select("Install scope", SCOPE_OPTIONS, "project") === "global" ? "global" : "project";
562
+ }
563
+ export { resolveCapability as _, errorBody as a, runBatch as c, findAgentDefinition as d, homeDir as f, parseCapabilityPath as g, SKILL_KIND as h, runPublish as i, stringValues as l, MCP_KIND as m, explicitName as n, failedResult as o, downloadContent as p, readPublishFlags as r, positionalPaths as s, resolveInstallScope as t, AGENTS as u, resolveCapabilityVersion as v };