hngr 0.4.0 → 0.5.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
@@ -47,12 +47,19 @@ hngr skills add team-a/pdf-tools --agent claude-code
47
47
  hngr skills add pdf-tools@2026.10.25.1 --agent claude-code
48
48
  ```
49
49
 
50
- Non-interactive runs must pass `--agent` (repeatable) or `--all`. In an interactive
51
- terminal, omitting `--global` first asks whether to install for the current project or
52
- the user; without a terminal it defaults to the project. Use `--global` to skip the
53
- question, `--copy` to skip the canonical `.agents/skills` directory, and `--dry-run` to
54
- preview without writing. Re-running an install replaces the existing target; `--force`
55
- is not needed.
50
+ The shared `.agents/skills` store is always installed, so every universal agent reads it
51
+ without extra work. In an interactive terminal, omitting `--global` first asks whether to
52
+ install for the current project or the user, then shows a multi-select of the agents that
53
+ need their own directory (`claude-code`, `cline`, `junie` or `kiro-cli`), with an
54
+ `All agents` entry; detected agents are preselected, and pressing Enter with none selected
55
+ installs only `.agents/skills`. Without a terminal, pass `--agent` (repeatable) or `--all`,
56
+ and the scope defaults to the project. Use `--copy` to write copies instead of the
57
+ canonical store plus links, and `--dry-run` to preview without writing. Re-running an
58
+ install replaces the existing target; `--force` is not needed.
59
+
60
+ Interactive runs end with a borderless summary on stderr (canonical path, universal
61
+ agents, symlinked/copied agents); stdout carries the JSON payload only when piped or when
62
+ `--json` is passed.
56
63
 
57
64
  ## Publish capabilities
58
65
 
package/dist/index.mjs CHANGED
@@ -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.4.0";
5282
+ var version = "0.5.0";
5283
5283
  // src/generated/completion.ts
5284
5284
  function isCompletionShell(value) {
5285
5285
  return value === "bash" || value === "zsh" || value === "fish";
@@ -6721,6 +6721,13 @@ ${h}
6721
6721
  }
6722
6722
  }).prompt();
6723
6723
  };
6724
+ const outro = (o = "", t) => {
6725
+ const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText("gray", S_BAR)}
6726
+ ${styleText("gray", S_BAR_END)} ` : "";
6727
+ i.write(`${e}${o}
6728
+
6729
+ `);
6730
+ };
6724
6731
  const password = (e) => new u$1({
6725
6732
  validate: e.validate,
6726
6733
  mask: e.mask ?? S_PASSWORD_MASK,
@@ -6898,15 +6905,24 @@ function clackPrompter(streams = {}) {
6898
6905
  initialValue
6899
6906
  }));
6900
6907
  },
6901
- async multiselect(message, options, initialValues = []) {
6908
+ async multiselect(message, options, initialValues = [], hint) {
6909
+ if (hint) output.write(`${import_picocolors.default.dim(hint)}\n`);
6902
6910
  return unwrapPrompt(await multiselect({
6903
6911
  message,
6904
6912
  input,
6905
6913
  output,
6906
6914
  options,
6907
6915
  initialValues,
6908
- required: true
6916
+ required: false
6909
6917
  }));
6918
+ },
6919
+ note(message, title) {
6920
+ output.write(`${import_picocolors.default.green("◇")} ${title}\n`);
6921
+ output.write(`${import_picocolors.default.dim("│")}\n`);
6922
+ for (const line of message.split("\n")) output.write(`${import_picocolors.default.dim("│")} ${line}\n`);
6923
+ },
6924
+ outro(message) {
6925
+ outro(message, { output });
6910
6926
  }
6911
6927
  };
6912
6928
  }
@@ -6922,7 +6938,7 @@ const GLOBAL_OPTIONS = {
6922
6938
  json: {
6923
6939
  type: "boolean",
6924
6940
  default: false,
6925
- describe: "Machine mode: errors are single-line JSON on stderr",
6941
+ describe: "Machine mode: print the JSON payload even on a TTY, and errors as JSON",
6926
6942
  global: true
6927
6943
  },
6928
6944
  quiet: {
@@ -7085,7 +7101,7 @@ function buildParser(onMatched) {
7085
7101
  }), (argv) => {
7086
7102
  onMatched({
7087
7103
  argv,
7088
- run: async (ctx, matchedArgv) => (await import("./skills-CxMeJ5PF.mjs")).add(ctx, matchedArgv)
7104
+ run: async (ctx, matchedArgv) => (await import("./skills-ZFmFyZtw.mjs")).add(ctx, matchedArgv)
7089
7105
  });
7090
7106
  });
7091
7107
  skills.command("publish <paths..>", "Publish a local skill directory as a capability or a new version", (publish) => publish.option("namespace", {
@@ -7113,7 +7129,7 @@ function buildParser(onMatched) {
7113
7129
  }), (argv) => {
7114
7130
  onMatched({
7115
7131
  argv,
7116
- run: async (ctx, matchedArgv) => (await import("./skills-CxMeJ5PF.mjs")).publish(ctx, matchedArgv)
7132
+ run: async (ctx, matchedArgv) => (await import("./skills-ZFmFyZtw.mjs")).publish(ctx, matchedArgv)
7117
7133
  });
7118
7134
  });
7119
7135
  return skills.demandCommand(1);
@@ -7135,7 +7151,7 @@ function buildParser(onMatched) {
7135
7151
  }), (argv) => {
7136
7152
  onMatched({
7137
7153
  argv,
7138
- run: async (ctx, matchedArgv) => (await import("./mcp-DWF_ELDU.mjs")).add(ctx, matchedArgv)
7154
+ run: async (ctx, matchedArgv) => (await import("./mcp-B0sbQABp.mjs")).add(ctx, matchedArgv)
7139
7155
  });
7140
7156
  });
7141
7157
  mcp.command("publish <paths..>", "Publish an MCP snapshot as a capability or a new version", (publish) => publish.option("namespace", {
@@ -7163,7 +7179,7 @@ function buildParser(onMatched) {
7163
7179
  }), (argv) => {
7164
7180
  onMatched({
7165
7181
  argv,
7166
- run: async (ctx, matchedArgv) => (await import("./mcp-DWF_ELDU.mjs")).publish(ctx, matchedArgv)
7182
+ run: async (ctx, matchedArgv) => (await import("./mcp-B0sbQABp.mjs")).publish(ctx, matchedArgv)
7167
7183
  });
7168
7184
  });
7169
7185
  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 { _ 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";
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-DVfDeQi2.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";
@@ -391,8 +391,6 @@ async function addOne(ctx, connection, parsed, targets, state) {
391
391
  const snapshot = await downloadMcpSnapshot(connection, capability.id, version);
392
392
  if (snapshot.ignoredKeys.length > 0) ctx.summary(`warning: ignoring unsupported top-level keys in the MCP snapshot: ${snapshot.ignoredKeys.join(", ")}`);
393
393
  const serverNames = Object.keys(snapshot.servers);
394
- const etag = snapshot.etag ? ` (ETag ${snapshot.etag})` : "";
395
- ctx.summary(`resolved ${parsed.namespace}/${parsed.name} to version ${version}${etag}`);
396
394
  if (ctx.flags.dryRun) {
397
395
  const dryTargets = [];
398
396
  let failure = null;
@@ -583,7 +581,7 @@ async function add(ctx, argv) {
583
581
  }
584
582
  }
585
583
  });
586
- ctx.writePayload({ results });
584
+ if (ctx.flags.json || !ctx.io.stdout.isTTY) ctx.writePayload({ results });
587
585
  return failed ? 1 : 0;
588
586
  }
589
587
  export { add, publish };
@@ -172,6 +172,10 @@ function claudeConfigDir(env) {
172
172
  function xdgConfigDir(env) {
173
173
  return env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME !== "" ? env.XDG_CONFIG_HOME : join(homeDir(env), ".config");
174
174
  }
175
+ /** Pi's agent directory: `PI_CODING_AGENT_DIR` or `~/.pi/agent`. */
176
+ function piAgentDir(env) {
177
+ return env.PI_CODING_AGENT_DIR && env.PI_CODING_AGENT_DIR !== "" ? env.PI_CODING_AGENT_DIR : join(homeDir(env), ".pi", "agent");
178
+ }
175
179
  const universalSkills = {
176
180
  projectDir: null,
177
181
  globalDir: null,
@@ -220,6 +224,15 @@ const AGENTS = [
220
224
  reason: "OpenCode uses a top-level `mcp` object with local/remote entries."
221
225
  }
222
226
  },
227
+ {
228
+ id: "pi",
229
+ skills: universalSkills,
230
+ mcp: {
231
+ kind: "json",
232
+ project: ".pi/mcp.json",
233
+ global: (env) => join(piAgentDir(env), "mcp.json")
234
+ }
235
+ },
223
236
  {
224
237
  id: "amp",
225
238
  skills: universalSkills,
@@ -557,7 +570,7 @@ const SCOPE_OPTIONS = [{
557
570
  async function resolveInstallScope(ctx, global) {
558
571
  if (global) return "global";
559
572
  const select = ctx.io.prompts?.select;
560
- if (!select || !ctx.io.stdin.isTTY || !ctx.io.stdout.isTTY) return "project";
573
+ if (!select || ctx.flags.json || !ctx.io.stdin.isTTY || !ctx.io.stdout.isTTY) return "project";
561
574
  return await select("Install scope", SCOPE_OPTIONS, "project") === "global" ? "global" : "project";
562
575
  }
563
576
  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 };
@@ -1,5 +1,5 @@
1
1
  import { C as HngrError, b as writeDirectoryAtomically, g as resolveConnection, r as apiRequest } from "./http-DgHX7Fux.mjs";
2
- import { _ as resolveCapability, a as errorBody, c as runBatch, f as homeDir, g as parseCapabilityPath, h as SKILL_KIND, i as runPublish, l as stringValues, 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";
2
+ import { _ as resolveCapability, a as errorBody, c as runBatch, f as homeDir, g as parseCapabilityPath, h as SKILL_KIND, i as runPublish, l as stringValues, 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-DVfDeQi2.mjs";
3
3
  import { lstat, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
@@ -552,17 +552,19 @@ function failureResult(parsed, err, version) {
552
552
  canonicalPath: null
553
553
  };
554
554
  }
555
- /** Prompts need both stdin and stdout on a TTY plus a prompter that can multi-select. */
555
+ /** Agent prompts need a TTY on both ends, a multi-select prompter, and human output mode. */
556
556
  function canPrompt(ctx) {
557
- return ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && ctx.io.prompts?.multiselect !== void 0;
557
+ return !ctx.flags.json && ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && ctx.io.prompts?.multiselect !== void 0;
558
558
  }
559
559
  function noAgentSelectionError(examplePath, global) {
560
560
  return usage("No agent target selected", `Pass --agent <id> (repeatable) or --all. For example: hngr skills add ${examplePath} --agent claude-code${global ? " --global" : ""}`);
561
561
  }
562
+ const ALL_AGENTS = "__all__";
562
563
  /**
563
- * One multi-select for every Agent Target, pre-selecting those whose skills
564
- * directory is detected. Detection is only a pre-selection hint; a non-TTY
565
- * invocation never consults the filesystem and must pass --agent or --all.
564
+ * One multi-select for the agents that keep their own skills directory.
565
+ * `.agents` is always installed, so universal agents never appear here.
566
+ * Detection only preselects; a non-TTY invocation never consults the
567
+ * filesystem and must pass --agent or --all.
566
568
  */
567
569
  async function selectAgents(ctx, argv, examplePath, global) {
568
570
  const requested = stringValues(argv.agent);
@@ -572,26 +574,28 @@ async function selectAgents(ctx, argv, examplePath, global) {
572
574
  return AGENT_TARGETS.filter((agent) => requested.includes(agent.id));
573
575
  }
574
576
  if (argv.all === true) return [...AGENT_TARGETS];
575
- if (canPrompt(ctx)) {
576
- const multiselect = ctx.io.prompts?.multiselect;
577
- if (!multiselect) throw noAgentSelectionError(examplePath, global);
578
- const cwd = process.cwd();
579
- const choices = [];
580
- for (const agent of AGENT_TARGETS) {
581
- const dir = targetSkillsDir(agent, cwd, ctx.env, global);
582
- choices.push({
583
- option: {
584
- value: agent.id,
585
- label: `${agent.id} ${displayPath(dir, cwd)}`
586
- },
587
- detected: await pathExists(dirname(dir))
588
- });
589
- }
590
- const selected = await multiselect("Select agent targets", choices.map((choice) => choice.option), choices.filter((choice) => choice.detected).map((choice) => choice.option.value));
591
- if (selected.length === 0) throw noAgentSelectionError(examplePath, global);
592
- return AGENT_TARGETS.filter((agent) => selected.includes(agent.id));
577
+ const multiselect = ctx.io.prompts?.multiselect;
578
+ if (!canPrompt(ctx) || !multiselect) throw noAgentSelectionError(examplePath, global);
579
+ const cwd = process.cwd();
580
+ const linkAgents = AGENT_TARGETS.filter((agent) => agent.link);
581
+ const options = [{
582
+ value: ALL_AGENTS,
583
+ label: "All agents",
584
+ hint: "select every agent below"
585
+ }];
586
+ const initialValues = [];
587
+ for (const agent of linkAgents) {
588
+ const dir = targetSkillsDir(agent, cwd, ctx.env, global);
589
+ options.push({
590
+ value: agent.id,
591
+ label: agent.id,
592
+ hint: displayPath(dir, cwd)
593
+ });
594
+ if (await pathExists(dirname(dir))) initialValues.push(agent.id);
593
595
  }
594
- throw noAgentSelectionError(examplePath, global);
596
+ const selected = await multiselect("Install for another agent", options, initialValues, ".agents/skills is always installed; press Enter to skip");
597
+ const chosen = selected.includes(ALL_AGENTS) ? new Set(linkAgents.map((agent) => agent.id)) : new Set(selected);
598
+ return linkAgents.filter((agent) => chosen.has(agent.id));
595
599
  }
596
600
  function frontmatterName(manifest) {
597
601
  const text = manifest.toString("utf8");
@@ -683,6 +687,26 @@ async function install(ctx, plan, entries) {
683
687
  failure
684
688
  };
685
689
  }
690
+ function withDot(path) {
691
+ return path.startsWith("/") ? path : `./${path}`;
692
+ }
693
+ /** The boxed result shown after an interactive install. */
694
+ function installSummary(outcome, canonicalPath) {
695
+ const lines = [];
696
+ const installed = outcome.targets.filter((target) => target.status === "installed");
697
+ if (canonicalPath) {
698
+ lines.push(`✓ ${withDot(canonicalPath)}`);
699
+ const universal = AGENT_TARGETS.filter((agent) => !agent.link).map((agent) => agent.id);
700
+ if (universal.length > 0) lines.push(` universal: ${universal.join(", ")}`);
701
+ } else for (const target of installed) lines.push(`✓ ${withDot(target.path)}`);
702
+ const symlinked = installed.filter((target) => target.mode === "symlink" && target.agent).map((target) => target.agent);
703
+ const copied = installed.filter((target) => target.mode === "copy" && target.agent).map((target) => target.agent);
704
+ if (symlinked.length > 0) lines.push(` symlinked: ${symlinked.join(", ")}`);
705
+ if (copied.length > 0) lines.push(` copied: ${copied.join(", ")}`);
706
+ const failed = outcome.targets.filter((target) => target.status === "failed");
707
+ if (failed.length > 0) lines.push(` failed: ${failed.map((target) => target.path).join(", ")}`);
708
+ return lines.join("\n");
709
+ }
686
710
  async function addOne(ctx, connection, parsed, destName, agents, dir, global, copy, state) {
687
711
  const capability = await resolveSkill(connection, parsed);
688
712
  const version = resolveVersion(capability, parsed);
@@ -694,7 +718,9 @@ async function addOne(ctx, connection, parsed, destName, agents, dir, global, co
694
718
  });
695
719
  const canonicalPath = plan.canonical ? displayPath(plan.canonical, process.cwd()) : null;
696
720
  if (ctx.flags.dryRun) {
697
- ctx.summary(`resolved ${parsed.namespace}/${parsed.name} to version ${version}`);
721
+ ctx.detail(`dry-run: would install ${parsed.namespace}/${parsed.name}@${version}`);
722
+ if (canonicalPath) ctx.detail(` ${canonicalPath}`);
723
+ for (const target of plan.targets) ctx.detail(` ${displayPath(target.path, process.cwd())} (${target.mode})`);
698
724
  return { result: {
699
725
  name: capability.name,
700
726
  namespace: capability.namespace,
@@ -710,19 +736,18 @@ async function addOne(ctx, connection, parsed, destName, agents, dir, global, co
710
736
  error: null
711
737
  } };
712
738
  }
713
- const download = await downloadSkillPackage(connection, capability.id, version);
714
- const pkg = readSkillPackage(download.bytes);
715
- ctx.summary(`resolved ${parsed.namespace}/${parsed.name} to version ${version}`);
739
+ const pkg = readSkillPackage((await downloadSkillPackage(connection, capability.id, version)).bytes);
716
740
  const declaredName = frontmatterName(pkg.manifest);
717
741
  if (declaredName !== void 0 && declaredName !== destName) ctx.summary(`warning: SKILL.md declares name ${JSON.stringify(declaredName)} but the directory is ${JSON.stringify(destName)}`);
718
742
  const outcome = await install(ctx, plan, pkg.entries);
719
- const universal = agents.filter((agent) => !agent.link).map((agent) => agent.id);
720
- const etag = download.etag ? ` (ETag ${download.etag})` : "";
721
- const verb = outcome.failure ? "partially installed" : "installed";
722
- ctx.progress(`${verb} ${capability.namespace}/${capability.name}@${version}${etag}`);
723
- if (canonicalPath) ctx.progress(` canonical ${canonicalPath}`);
724
- if (universal.length > 0) ctx.progress(` universal: ${universal.join(", ")}`);
725
- for (const target of outcome.targets) ctx.progress(target.status === "failed" ? ` failed ${target.path}` : ` ${target.mode} ${target.path}`);
743
+ const verb = outcome.failure ? "Partially installed" : "Installed";
744
+ const note = ctx.io.prompts?.note;
745
+ if (!ctx.flags.json && !ctx.flags.quiet && ctx.io.stderr.isTTY && note) note(installSummary(outcome, canonicalPath), `${verb} ${capability.namespace}/${capability.name}@${version}`);
746
+ else {
747
+ ctx.progress(`${verb.toLowerCase()} ${capability.namespace}/${capability.name}@${version}`);
748
+ if (canonicalPath) ctx.progress(` ${canonicalPath}`);
749
+ for (const target of outcome.targets) ctx.progress(target.status === "failed" ? ` failed ${target.path}` : ` ${target.path} (${target.mode})`);
750
+ }
726
751
  return { result: {
727
752
  name: capability.name,
728
753
  namespace: capability.namespace,
@@ -849,7 +874,8 @@ async function add(ctx, argv) {
849
874
  },
850
875
  failure: failureResult
851
876
  });
852
- ctx.writePayload({ results });
877
+ if (ctx.flags.json || !ctx.io.stdout.isTTY) ctx.writePayload({ results });
878
+ if (!ctx.flags.json && !ctx.flags.dryRun && !ctx.flags.quiet && !failed && ctx.io.stderr.isTTY) ctx.io.prompts?.outro?.("Done! Review skills before use; they run with full agent permissions.");
853
879
  return failed ? 1 : 0;
854
880
  }
855
881
  export { add, publish };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hngr",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",