hngr 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,12 +47,24 @@ 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.
63
+
64
+ MCP installs ask for the same scope and then for the MCP targets to write, with an
65
+ `All targets` entry; targets whose config already exists are preselected, and Claude
66
+ Code's `.mcp.json` is preselected for project installs. Without a terminal, pass
67
+ `--agent` (repeatable) or `--all`.
56
68
 
57
69
  ## Publish capabilities
58
70
 
@@ -50,6 +50,8 @@ const EXCLUDED_PATHS = [
50
50
  "/api/v1/auth/device/lookup",
51
51
  "/api/v1/auth/device/approve",
52
52
  "/api/v1/auth/device/deny",
53
+ "/api/v1/teams/{team_id}/invitations",
54
+ "/api/v1/team-invitations",
53
55
  "/api/v1/oauth"
54
56
  ];
55
57
  /**
@@ -1816,7 +1818,10 @@ const COMMANDS = deriveCommands({
1816
1818
  "team.deleted",
1817
1819
  "team.member_added",
1818
1820
  "team.member_role_changed",
1819
- "team.member_removed"
1821
+ "team.member_removed",
1822
+ "team.invitation_created",
1823
+ "team.invitation_revoked",
1824
+ "team.invitation_accepted"
1820
1825
  ]
1821
1826
  }
1822
1827
  },
@@ -2094,6 +2099,125 @@ const COMMANDS = deriveCommands({
2094
2099
  }
2095
2100
  }
2096
2101
  },
2102
+ "/api/v1/teams/{team_id}/invitations": {
2103
+ "parameters": [{ "$ref": "#/components/parameters/TeamID" }],
2104
+ "post": {
2105
+ "operationId": "createTeamInvitation",
2106
+ "summary": "Create an invitation link for a Team.",
2107
+ "description": "Allowed for an effective Team Admin and for a Platform Admin of an\nenabled Team. The Team must be enabled; a disabled Team answers 409 for\nevery caller, Platform Admins included. The invitation always adds\nmembers as `member`: the request carries no role and no use limit. The\nresponse is the only time the plaintext `token` and the absolute `url`\nare returned; the platform stores only the token's keyed digest and\nlater lists expose only `token_prefix`. `expires_in_days` defaults to 7\nand must be between 1 and 30.\n",
2108
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
2109
+ "requestBody": {
2110
+ "required": true,
2111
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateTeamInvitationRequest" } } }
2112
+ },
2113
+ "responses": {
2114
+ "201": {
2115
+ "description": "The invitation and the one-time token and link.",
2116
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IssuedTeamInvitation" } } }
2117
+ },
2118
+ "400": { "$ref": "#/components/responses/Problem" },
2119
+ "401": { "$ref": "#/components/responses/Problem" },
2120
+ "403": { "$ref": "#/components/responses/Problem" },
2121
+ "404": { "$ref": "#/components/responses/Problem" },
2122
+ "409": { "$ref": "#/components/responses/Problem" },
2123
+ "default": { "$ref": "#/components/responses/Problem" }
2124
+ }
2125
+ },
2126
+ "get": {
2127
+ "operationId": "listTeamInvitations",
2128
+ "summary": "List a Team's invitation links, newest first.",
2129
+ "description": "Allowed for an effective Team Admin and for a Platform Admin. A\ndisabled Team stays readable and its invitations stay revocable.\nUnknown and deleted Teams answer 404; a caller who does not govern the\nTeam answers 403. The list\nnever contains the token or its digest, only `token_prefix`; `uses` is\nthe number of Users the link has actually added. `state` is derived by\nthe server with priority revoked, then expired, then active. The list\nis ordered by `created_at` then `id`, both descending, and paged by\ncursor without a total.\n",
2130
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
2131
+ "parameters": [{ "$ref": "#/components/parameters/Cursor" }, { "$ref": "#/components/parameters/Limit" }],
2132
+ "responses": {
2133
+ "200": {
2134
+ "description": "One page of invitations.",
2135
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamInvitationList" } } }
2136
+ },
2137
+ "400": { "$ref": "#/components/responses/Problem" },
2138
+ "401": { "$ref": "#/components/responses/Problem" },
2139
+ "403": { "$ref": "#/components/responses/Problem" },
2140
+ "404": { "$ref": "#/components/responses/Problem" },
2141
+ "default": { "$ref": "#/components/responses/Problem" }
2142
+ }
2143
+ }
2144
+ },
2145
+ "/api/v1/teams/{team_id}/invitations/{invitation_id}": {
2146
+ "parameters": [{ "$ref": "#/components/parameters/TeamID" }, { "$ref": "#/components/parameters/InvitationID" }],
2147
+ "delete": {
2148
+ "operationId": "revokeTeamInvitation",
2149
+ "summary": "Revoke an invitation link.",
2150
+ "description": "Allowed for an effective Team Admin and for a Platform Admin, also\nwhile the Team is disabled: revoking changes no membership. Revoking an\nalready-revoked invitation answers 204 without changing `revoked_at`. An\ninvitation of another Team, or an unknown one, answers 404.\n",
2151
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
2152
+ "responses": {
2153
+ "204": { "description": "The invitation is revoked." },
2154
+ "400": { "$ref": "#/components/responses/Problem" },
2155
+ "401": { "$ref": "#/components/responses/Problem" },
2156
+ "403": { "$ref": "#/components/responses/Problem" },
2157
+ "404": { "$ref": "#/components/responses/Problem" },
2158
+ "default": { "$ref": "#/components/responses/Problem" }
2159
+ }
2160
+ }
2161
+ },
2162
+ "/api/v1/team-invitations/preview": { "post": {
2163
+ "operationId": "previewTeamInvitation",
2164
+ "summary": "Preview an invitation link without joining.",
2165
+ "description": "Anonymous. Resolves the token to the Team it invites to, without\nrevealing the roster or anything else about the Team. A live Team\nanswers 200 even when the invitation is expired or revoked, with\n`state` carrying the reason; the Web page explains it. An unknown\ntoken answers 404, and a Team that has been deleted makes its links\nunknown too. `already_member` is resolved from the Session cookie when\nthe caller is signed in and is always false for an anonymous caller.\nThe endpoint is rate-limited per client address and every response is\n`no-store`.\n",
2166
+ "security": [],
2167
+ "requestBody": {
2168
+ "required": true,
2169
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamInvitationToken" } } }
2170
+ },
2171
+ "responses": {
2172
+ "200": {
2173
+ "description": "The Team and the invitation's derived state.",
2174
+ "headers": { "Cache-Control": {
2175
+ "description": "Always `no-store`.",
2176
+ "schema": { "type": "string" }
2177
+ } },
2178
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamInvitationPreview" } } }
2179
+ },
2180
+ "400": { "$ref": "#/components/responses/Problem" },
2181
+ "404": { "$ref": "#/components/responses/Problem" },
2182
+ "429": { "$ref": "#/components/responses/Problem" },
2183
+ "default": { "$ref": "#/components/responses/Problem" }
2184
+ }
2185
+ } },
2186
+ "/api/v1/team-invitations/accept": { "post": {
2187
+ "operationId": "acceptTeamInvitation",
2188
+ "summary": "Join a Team through an invitation link.",
2189
+ "description": "Session-only: a Bearer Access Token is never accepted, so an Agent\ncannot join through a link. Membership is always created with the\n`member` role; the request carries no role. A new membership answers\n201 and an existing one answers 200 without changing any role, which\nmakes replay safe. Both answer with the same `TeamMember` shape as\n`putTeamMember`. An unknown token or a deleted Team answers 404; an\nexpired or revoked invitation, or a disabled Team, answers 409.\n`uses` increments only when a membership is actually inserted, and the\nendpoint is rate-limited per client address. Every response is\n`no-store`.\n",
2190
+ "security": [{ "sessionCookie": [] }],
2191
+ "requestBody": {
2192
+ "required": true,
2193
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamInvitationToken" } } }
2194
+ },
2195
+ "responses": {
2196
+ "200": {
2197
+ "description": "The caller was already a member; nothing changed.",
2198
+ "headers": { "Cache-Control": {
2199
+ "description": "Always `no-store`.",
2200
+ "schema": { "type": "string" }
2201
+ } },
2202
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamMember" } } }
2203
+ },
2204
+ "201": {
2205
+ "description": "The caller was added to the Team as a member.",
2206
+ "headers": { "Cache-Control": {
2207
+ "description": "Always `no-store`.",
2208
+ "schema": { "type": "string" }
2209
+ } },
2210
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamMember" } } }
2211
+ },
2212
+ "400": { "$ref": "#/components/responses/Problem" },
2213
+ "401": { "$ref": "#/components/responses/Problem" },
2214
+ "403": { "$ref": "#/components/responses/Problem" },
2215
+ "404": { "$ref": "#/components/responses/Problem" },
2216
+ "409": { "$ref": "#/components/responses/Problem" },
2217
+ "429": { "$ref": "#/components/responses/Problem" },
2218
+ "default": { "$ref": "#/components/responses/Problem" }
2219
+ }
2220
+ } },
2097
2221
  "/api/v1/teams/{team_id}/members/{principal_id}": {
2098
2222
  "parameters": [{ "$ref": "#/components/parameters/TeamID" }, { "$ref": "#/components/parameters/PrincipalID" }],
2099
2223
  "put": {
@@ -2208,6 +2332,15 @@ const COMMANDS = deriveCommands({
2208
2332
  "format": "uuid"
2209
2333
  }
2210
2334
  },
2335
+ "InvitationID": {
2336
+ "name": "invitation_id",
2337
+ "in": "path",
2338
+ "required": true,
2339
+ "schema": {
2340
+ "type": "string",
2341
+ "format": "uuid"
2342
+ }
2343
+ },
2211
2344
  "TokenID": {
2212
2345
  "name": "token_id",
2213
2346
  "in": "path",
@@ -3854,6 +3987,141 @@ const COMMANDS = deriveCommands({
3854
3987
  "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
3855
3988
  }
3856
3989
  },
3990
+ "TeamInvitationState": {
3991
+ "type": "string",
3992
+ "enum": [
3993
+ "active",
3994
+ "expired",
3995
+ "revoked"
3996
+ ],
3997
+ "description": "Derived by the server; revoked wins over expired, which wins over active."
3998
+ },
3999
+ "TeamInvitation": {
4000
+ "type": "object",
4001
+ "required": [
4002
+ "id",
4003
+ "token_prefix",
4004
+ "uses",
4005
+ "expires_at",
4006
+ "revoked_at",
4007
+ "state",
4008
+ "created_at",
4009
+ "created_by"
4010
+ ],
4011
+ "properties": {
4012
+ "id": {
4013
+ "type": "string",
4014
+ "format": "uuid"
4015
+ },
4016
+ "token_prefix": {
4017
+ "type": "string",
4018
+ "description": "The `hngi_` scheme plus the first 8 random characters, for recognising the link in a list."
4019
+ },
4020
+ "uses": {
4021
+ "type": "integer",
4022
+ "description": "How many Users the link has actually added to the Team."
4023
+ },
4024
+ "expires_at": {
4025
+ "type": "string",
4026
+ "format": "date-time"
4027
+ },
4028
+ "revoked_at": {
4029
+ "type": ["string", "null"],
4030
+ "format": "date-time"
4031
+ },
4032
+ "state": { "$ref": "#/components/schemas/TeamInvitationState" },
4033
+ "created_at": {
4034
+ "type": "string",
4035
+ "format": "date-time"
4036
+ },
4037
+ "created_by": {
4038
+ "type": ["string", "null"],
4039
+ "format": "uuid"
4040
+ }
4041
+ }
4042
+ },
4043
+ "TeamInvitationList": {
4044
+ "type": "object",
4045
+ "required": ["items", "next_cursor"],
4046
+ "properties": {
4047
+ "items": {
4048
+ "type": "array",
4049
+ "items": { "$ref": "#/components/schemas/TeamInvitation" }
4050
+ },
4051
+ "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
4052
+ }
4053
+ },
4054
+ "IssuedTeamInvitation": {
4055
+ "description": "The TeamInvitation plus the plaintext token and link, returned only once.",
4056
+ "allOf": [{ "$ref": "#/components/schemas/TeamInvitation" }, {
4057
+ "type": "object",
4058
+ "required": ["token", "url"],
4059
+ "properties": {
4060
+ "token": { "type": "string" },
4061
+ "url": { "type": "string" }
4062
+ }
4063
+ }]
4064
+ },
4065
+ "CreateTeamInvitationRequest": {
4066
+ "type": "object",
4067
+ "properties": { "expires_in_days": {
4068
+ "type": "integer",
4069
+ "minimum": 1,
4070
+ "maximum": 30,
4071
+ "default": 7
4072
+ } },
4073
+ "additionalProperties": false
4074
+ },
4075
+ "TeamInvitationToken": {
4076
+ "type": "object",
4077
+ "description": "The opaque token a join link carries. It travels in the body, never in a path or query.",
4078
+ "required": ["token"],
4079
+ "properties": { "token": {
4080
+ "type": "string",
4081
+ "minLength": 1,
4082
+ "maxLength": 128
4083
+ } },
4084
+ "additionalProperties": false
4085
+ },
4086
+ "TeamInvitationPreview": {
4087
+ "type": "object",
4088
+ "description": "What a join page may show before the visitor acts. It never carries\nthe roster or the token. `already_member` is false for anonymous\ncallers. `state` is derived on the server with priority revoked, then\nexpired, then active.\n",
4089
+ "required": [
4090
+ "team",
4091
+ "state",
4092
+ "expires_at",
4093
+ "team_disabled",
4094
+ "already_member"
4095
+ ],
4096
+ "properties": {
4097
+ "team": {
4098
+ "type": "object",
4099
+ "required": [
4100
+ "id",
4101
+ "name",
4102
+ "display_name"
4103
+ ],
4104
+ "properties": {
4105
+ "id": {
4106
+ "type": "string",
4107
+ "format": "uuid"
4108
+ },
4109
+ "name": {
4110
+ "type": "string",
4111
+ "description": "The Team's Namespace name."
4112
+ },
4113
+ "display_name": { "type": ["string", "null"] }
4114
+ }
4115
+ },
4116
+ "state": { "$ref": "#/components/schemas/TeamInvitationState" },
4117
+ "expires_at": {
4118
+ "type": "string",
4119
+ "format": "date-time"
4120
+ },
4121
+ "team_disabled": { "type": "boolean" },
4122
+ "already_member": { "type": "boolean" }
4123
+ }
4124
+ },
3857
4125
  "CalVer": {
3858
4126
  "type": "string",
3859
4127
  "description": "`YYYY.MM.DD.N` in UTC; N counts from 0 per Capability per day.",
@@ -1,4 +1,4 @@
1
- import { t as commandCatalog } from "./catalog-Bv304SXJ.mjs";
1
+ import { t as commandCatalog } from "./catalog-r53AMymQ.mjs";
2
2
  // src/commands/commands.ts
3
3
  function run(ctx) {
4
4
  const catalog = commandCatalog();
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-Bv304SXJ.mjs";
1
+ import { a as kebabCase, i as normalizeCommandName, n as COMMANDS, o as COMMAND_ALIASES, r as findCommand, t as commandCatalog } from "./catalog-r53AMymQ.mjs";
2
2
  import { C as HngrError, S as CancelledError, c as redactUrlForDisplay, g as resolveConnection, i as buildRequestUrl, o as redactQuery, r as apiRequest, s as redactSensitive, u as DEFAULT_SERVER_URL, w as renderError } from "./http-DgHX7Fux.mjs";
3
3
  import { t as collectPages } from "./pagination-BXyFxGYA.mjs";
4
4
  import { createRequire } from "node:module";
@@ -5279,7 +5279,7 @@ function isYargsInstance(y) {
5279
5279
  // ../../node_modules/.pnpm/yargs@18.2.0/node_modules/yargs/index.mjs
5280
5280
  const Yargs = YargsFactory(esm_default);
5281
5281
  // package.json
5282
- var version = "0.4.0";
5282
+ var version = "0.5.1";
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: {
@@ -7005,7 +7021,7 @@ function buildParser(onMatched) {
7005
7021
  parser.command("commands", "Print the command catalog", () => {}, (argv) => {
7006
7022
  onMatched({
7007
7023
  argv,
7008
- run: async (ctx) => (await import("./commands-CLqLwQHY.mjs")).run(ctx)
7024
+ run: async (ctx) => (await import("./commands-_R93Y2z-.mjs")).run(ctx)
7009
7025
  });
7010
7026
  });
7011
7027
  parser.command("auth", "Manage credentials", (auth) => {
@@ -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-C3cphIAl.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-C3cphIAl.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-DjSU9C8z.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-DjSU9C8z.mjs")).publish(ctx, matchedArgv)
7167
7183
  });
7168
7184
  });
7169
7185
  return mcp.demandCommand(1);
@@ -1,8 +1,8 @@
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 { 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";
3
3
  import { constants } from "node:fs";
4
4
  import { access, readFile, stat } from "node:fs/promises";
5
- import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { spawn } from "node:child_process";
7
7
  // src/mcp/claude-cli.ts
8
8
  const EXECUTABLE = "claude";
@@ -128,6 +128,12 @@ async function addJsonWithClaude(env, name, config) {
128
128
  // src/mcp/content.ts
129
129
  /** Server names Claude Code accepts as `mcpServers` keys. */
130
130
  const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
131
+ /**
132
+ * `__proto__` matches the pattern but is an accessor on `Object.prototype`:
133
+ * assigning it would silently change the merged object's prototype instead of
134
+ * recording a server.
135
+ */
136
+ const RESERVED_SERVER_NAMES = /* @__PURE__ */ new Set(["__proto__"]);
131
137
  const EXPECTED_SHAPE = "Expected an object shaped {\"mcpServers\": {\"<server-name>\": {\"command\": \"...\", \"args\": [...]}}}.";
132
138
  function invalid(title, detail = EXPECTED_SHAPE) {
133
139
  return new HngrError({
@@ -148,7 +154,7 @@ function validateSnapshot(body) {
148
154
  if (!("mcpServers" in body)) throw invalid("The MCP snapshot is missing \"mcpServers\"");
149
155
  const servers = body.mcpServers;
150
156
  if (!isPlainObject$1(servers)) throw invalid("The MCP snapshot's \"mcpServers\" is not an object");
151
- for (const name of Object.keys(servers)) if (!SERVER_NAME_PATTERN.test(name)) throw invalid(`Invalid MCP server name ${JSON.stringify(name)}`, "Server names must match [A-Za-z0-9_-]+.");
157
+ for (const name of Object.keys(servers)) if (!SERVER_NAME_PATTERN.test(name) || RESERVED_SERVER_NAMES.has(name)) throw invalid(`Invalid MCP server name ${JSON.stringify(name)}`, "Server names must match [A-Za-z0-9_-]+ and must not be __proto__.");
152
158
  return {
153
159
  servers,
154
160
  ignoredKeys: Object.keys(body).filter((key) => key !== "mcpServers")
@@ -343,6 +349,48 @@ function usage(title, detail) {
343
349
  detail
344
350
  });
345
351
  }
352
+ const ALL_TARGETS = "__all__";
353
+ /** Target prompts need a TTY on both ends, a multi-select prompter, and human output mode. */
354
+ function canPrompt(ctx) {
355
+ return !ctx.flags.json && ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && ctx.io.prompts?.multiselect !== void 0;
356
+ }
357
+ function noTargetSelectionError(global) {
358
+ return usage("No agent target selected", `Pass --agent <id> (repeatable) or --all. For example: hngr mcp add pdf --agent claude-code${global ? " --global" : ""}`);
359
+ }
360
+ /**
361
+ * One multi-select for the MCP targets supported at the chosen scope. Detection
362
+ * only preselects: an existing agent config (or its directory) is highlighted,
363
+ * and Claude Code is preselected at project scope because the current directory
364
+ * always exists. A non-TTY invocation never prompts and must pass --agent or --all.
365
+ */
366
+ async function selectTargets(ctx, argv, scope) {
367
+ const requested = stringValues(argv.agent);
368
+ const all = argv.all === true;
369
+ if (requested.length > 0 || all) return selectMcpTargets(requested, all, scope, process.cwd(), ctx.env);
370
+ const multiselect = ctx.io.prompts?.multiselect;
371
+ if (!canPrompt(ctx) || !multiselect) throw noTargetSelectionError(scope === "global");
372
+ const plans = selectMcpTargets([], true, scope, process.cwd(), ctx.env);
373
+ const options = [{
374
+ value: ALL_TARGETS,
375
+ label: "All targets",
376
+ hint: "install for every target below"
377
+ }];
378
+ const initialValues = [];
379
+ for (const plan of plans) {
380
+ options.push({
381
+ value: plan.agent,
382
+ label: plan.agent,
383
+ hint: plan.path
384
+ });
385
+ const configPath = plan.filePath ?? plan.path;
386
+ if (plan.kind === "claude-cli" ? await pathExists(configPath) : await pathExists(dirname(configPath))) initialValues.push(plan.agent);
387
+ }
388
+ const selected = await multiselect("Install targets", options, initialValues, "Existing agent configs are preselected");
389
+ const chosen = selected.includes(ALL_TARGETS) ? new Set(plans.map((plan) => plan.agent)) : new Set(selected);
390
+ const targets = plans.filter((plan) => chosen.has(plan.agent));
391
+ if (targets.length === 0) throw noTargetSelectionError(scope === "global");
392
+ return targets;
393
+ }
346
394
  function failureResult(parsed, err, version) {
347
395
  return {
348
396
  servers: [],
@@ -391,8 +439,6 @@ async function addOne(ctx, connection, parsed, targets, state) {
391
439
  const snapshot = await downloadMcpSnapshot(connection, capability.id, version);
392
440
  if (snapshot.ignoredKeys.length > 0) ctx.summary(`warning: ignoring unsupported top-level keys in the MCP snapshot: ${snapshot.ignoredKeys.join(", ")}`);
393
441
  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
442
  if (ctx.flags.dryRun) {
397
443
  const dryTargets = [];
398
444
  let failure = null;
@@ -567,7 +613,7 @@ async function add(ctx, argv) {
567
613
  if (rawPaths.length === 0) throw usage("No capability path given", "Pass one or more `<name>` or `<namespace>/<name>` arguments.");
568
614
  const parsedPaths = rawPaths.map(parseMcpPath);
569
615
  const scope = await resolveInstallScope(ctx, argv.global === true);
570
- const targets = selectMcpTargets(stringValues(argv.agent), argv.all === true, scope, process.cwd(), ctx.env);
616
+ const targets = await selectTargets(ctx, argv, scope);
571
617
  const connection = await resolveConnection(ctx.flags, ctx.env);
572
618
  let approvalHintShown = false;
573
619
  const { results, failed } = await runBatch({
@@ -583,7 +629,7 @@ async function add(ctx, argv) {
583
629
  }
584
630
  }
585
631
  });
586
- ctx.writePayload({ results });
632
+ if (ctx.flags.json || !ctx.io.stdout.isTTY) ctx.writePayload({ results });
587
633
  return failed ? 1 : 0;
588
634
  }
589
635
  export { add, publish };
@@ -1,8 +1,9 @@
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";
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
2
  import { t as collectPages } from "./pagination-BXyFxGYA.mjs";
3
- import { readFile } from "node:fs/promises";
3
+ import { lstat, mkdir, readFile, rename, rm, symlink, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
- import { join, resolve } from "node:path";
5
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
6
+ import { randomUUID } from "node:crypto";
6
7
  // ../client/src/capability-path.ts
7
8
  /** The Namespace every bare capability reference means. */
8
9
  const SYSTEM_NAMESPACE = "system";
@@ -172,6 +173,10 @@ function claudeConfigDir(env) {
172
173
  function xdgConfigDir(env) {
173
174
  return env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME !== "" ? env.XDG_CONFIG_HOME : join(homeDir(env), ".config");
174
175
  }
176
+ /** Pi's agent directory: `PI_CODING_AGENT_DIR` or `~/.pi/agent`. */
177
+ function piAgentDir(env) {
178
+ return env.PI_CODING_AGENT_DIR && env.PI_CODING_AGENT_DIR !== "" ? env.PI_CODING_AGENT_DIR : join(homeDir(env), ".pi", "agent");
179
+ }
175
180
  const universalSkills = {
176
181
  projectDir: null,
177
182
  globalDir: null,
@@ -220,6 +225,15 @@ const AGENTS = [
220
225
  reason: "OpenCode uses a top-level `mcp` object with local/remote entries."
221
226
  }
222
227
  },
228
+ {
229
+ id: "pi",
230
+ skills: universalSkills,
231
+ mcp: {
232
+ kind: "json",
233
+ project: ".pi/mcp.json",
234
+ global: (env) => join(piAgentDir(env), "mcp.json")
235
+ }
236
+ },
223
237
  {
224
238
  id: "amp",
225
239
  skills: universalSkills,
@@ -310,6 +324,69 @@ const AGENTS = [
310
324
  function findAgentDefinition(id) {
311
325
  return AGENTS.find((agent) => agent.id === id);
312
326
  }
327
+ // src/skills/install.ts
328
+ async function pathExists(path) {
329
+ return lstat(path).then(() => true, () => false);
330
+ }
331
+ async function writeEntries(dest, entries) {
332
+ await mkdir(dest, { recursive: true });
333
+ for (const entry of entries) {
334
+ const path = resolve(dest, entry.path);
335
+ if (path !== dest && !path.startsWith(`${dest}${sep}`)) throw new HngrError({
336
+ type: "io",
337
+ title: `Refusing to write outside ${dest}`,
338
+ detail: `entry ${JSON.stringify(entry.path)} escapes the target directory`
339
+ });
340
+ await mkdir(dirname(path), { recursive: true });
341
+ await writeFile(path, entry.data, { mode: entry.mode & 73 ? 493 : 420 });
342
+ }
343
+ }
344
+ /**
345
+ * Writes a skill through "temp directory → validate → rename": the target is
346
+ * either absent or complete, never half-written. Re-installing replaces the
347
+ * previous directory, with the old one moved aside so a failed swap puts it
348
+ * back.
349
+ */
350
+ async function installAtomically(dest, entries) {
351
+ await writeDirectoryAtomically(dest, (temp) => writeEntries(temp, entries));
352
+ }
353
+ /**
354
+ * Creates a relative symlink to the canonical directory, falling back to a
355
+ * copy when the platform refuses the link (Windows without Developer Mode).
356
+ * Windows prefers a junction, which does not need elevated privileges; its
357
+ * target must be absolute. A temp link plus rename replaces an existing link
358
+ * atomically; re-installing replaces an existing directory or link.
359
+ */
360
+ async function linkAtomically(canonical, target, entries, options = {}) {
361
+ const exists = await pathExists(target);
362
+ await mkdir(dirname(target), { recursive: true });
363
+ const temp = resolve(dirname(target), `.hngr-${basename(target)}-${randomUUID()}.link`);
364
+ const windows = (options.platform ?? process.platform) === "win32";
365
+ const createLink = options.symlink ?? symlink;
366
+ try {
367
+ await createLink(windows ? canonical : relative(dirname(target), canonical), temp, windows ? "junction" : "dir");
368
+ if (exists) {
369
+ const stat = await lstat(target);
370
+ if (stat.isDirectory() && !stat.isSymbolicLink()) await rm(target, {
371
+ recursive: true,
372
+ force: true
373
+ });
374
+ }
375
+ await rename(temp, target);
376
+ return { mode: "symlink" };
377
+ } catch {
378
+ await rm(temp, { force: true }).catch(() => {});
379
+ await installAtomically(target, entries);
380
+ return {
381
+ mode: "copy",
382
+ warning: `warning: could not create a link at ${target}; copied instead`
383
+ };
384
+ }
385
+ }
386
+ /** Copies one target directory atomically, without a canonical store. */
387
+ async function copyAtomically(target, entries) {
388
+ await installAtomically(target, entries);
389
+ }
313
390
  // src/commands/batch.ts
314
391
  /** A repeatable string flag can arrive as one value or an array of values. */
315
392
  function stringValues(value) {
@@ -557,7 +634,7 @@ const SCOPE_OPTIONS = [{
557
634
  async function resolveInstallScope(ctx, global) {
558
635
  if (global) return "global";
559
636
  const select = ctx.io.prompts?.select;
560
- if (!select || !ctx.io.stdin.isTTY || !ctx.io.stdout.isTTY) return "project";
637
+ if (!select || ctx.flags.json || !ctx.io.stdin.isTTY || !ctx.io.stdout.isTTY) return "project";
561
638
  return await select("Install scope", SCOPE_OPTIONS, "project") === "global" ? "global" : "project";
562
639
  }
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 };
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 };
@@ -1,6 +1,6 @@
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";
3
- import { lstat, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises";
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";
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";
6
6
  import { deflateRawSync, inflateRawSync } from "node:zlib";
@@ -88,69 +88,6 @@ function isReservedDirName(name) {
88
88
  function downloadSkillPackage(connection, capabilityId, version, signal) {
89
89
  return downloadContent(connection, capabilityId, version, "application/zip", signal);
90
90
  }
91
- // src/skills/install.ts
92
- async function pathExists(path) {
93
- return lstat(path).then(() => true, () => false);
94
- }
95
- async function writeEntries(dest, entries) {
96
- await mkdir(dest, { recursive: true });
97
- for (const entry of entries) {
98
- const path = resolve(dest, entry.path);
99
- if (path !== dest && !path.startsWith(`${dest}${sep}`)) throw new HngrError({
100
- type: "io",
101
- title: `Refusing to write outside ${dest}`,
102
- detail: `entry ${JSON.stringify(entry.path)} escapes the target directory`
103
- });
104
- await mkdir(dirname(path), { recursive: true });
105
- await writeFile(path, entry.data, { mode: entry.mode & 73 ? 493 : 420 });
106
- }
107
- }
108
- /**
109
- * Writes a skill through "temp directory → validate → rename": the target is
110
- * either absent or complete, never half-written. Re-installing replaces the
111
- * previous directory, with the old one moved aside so a failed swap puts it
112
- * back.
113
- */
114
- async function installAtomically(dest, entries) {
115
- await writeDirectoryAtomically(dest, (temp) => writeEntries(temp, entries));
116
- }
117
- /**
118
- * Creates a relative symlink to the canonical directory, falling back to a
119
- * copy when the platform refuses the link (Windows without Developer Mode).
120
- * Windows prefers a junction, which does not need elevated privileges; its
121
- * target must be absolute. A temp link plus rename replaces an existing link
122
- * atomically; re-installing replaces an existing directory or link.
123
- */
124
- async function linkAtomically(canonical, target, entries, options = {}) {
125
- const exists = await pathExists(target);
126
- await mkdir(dirname(target), { recursive: true });
127
- const temp = resolve(dirname(target), `.hngr-${basename(target)}-${randomUUID()}.link`);
128
- const windows = (options.platform ?? process.platform) === "win32";
129
- const createLink = options.symlink ?? symlink;
130
- try {
131
- await createLink(windows ? canonical : relative(dirname(target), canonical), temp, windows ? "junction" : "dir");
132
- if (exists) {
133
- const stat = await lstat(target);
134
- if (stat.isDirectory() && !stat.isSymbolicLink()) await rm(target, {
135
- recursive: true,
136
- force: true
137
- });
138
- }
139
- await rename(temp, target);
140
- return { mode: "symlink" };
141
- } catch {
142
- await rm(temp, { force: true }).catch(() => {});
143
- await installAtomically(target, entries);
144
- return {
145
- mode: "copy",
146
- warning: `warning: could not create a link at ${target}; copied instead`
147
- };
148
- }
149
- }
150
- /** Copies one target directory atomically, without a canonical store. */
151
- async function copyAtomically(target, entries) {
152
- await installAtomically(target, entries);
153
- }
154
91
  // src/skills/resolve.ts
155
92
  /** Parses `<name>` or `<namespace>/<name>`, with an optional `@YYYY.MM.DD.N`. */
156
93
  function parseSkillPath(raw) {
@@ -552,17 +489,19 @@ function failureResult(parsed, err, version) {
552
489
  canonicalPath: null
553
490
  };
554
491
  }
555
- /** Prompts need both stdin and stdout on a TTY plus a prompter that can multi-select. */
492
+ /** Agent prompts need a TTY on both ends, a multi-select prompter, and human output mode. */
556
493
  function canPrompt(ctx) {
557
- return ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && ctx.io.prompts?.multiselect !== void 0;
494
+ return !ctx.flags.json && ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && ctx.io.prompts?.multiselect !== void 0;
558
495
  }
559
496
  function noAgentSelectionError(examplePath, global) {
560
497
  return usage("No agent target selected", `Pass --agent <id> (repeatable) or --all. For example: hngr skills add ${examplePath} --agent claude-code${global ? " --global" : ""}`);
561
498
  }
499
+ const ALL_AGENTS = "__all__";
562
500
  /**
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.
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.
566
505
  */
567
506
  async function selectAgents(ctx, argv, examplePath, global) {
568
507
  const requested = stringValues(argv.agent);
@@ -572,26 +511,28 @@ async function selectAgents(ctx, argv, examplePath, global) {
572
511
  return AGENT_TARGETS.filter((agent) => requested.includes(agent.id));
573
512
  }
574
513
  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));
514
+ const multiselect = ctx.io.prompts?.multiselect;
515
+ if (!canPrompt(ctx) || !multiselect) throw noAgentSelectionError(examplePath, global);
516
+ const cwd = process.cwd();
517
+ const linkAgents = AGENT_TARGETS.filter((agent) => agent.link);
518
+ const options = [{
519
+ value: ALL_AGENTS,
520
+ label: "All agents",
521
+ hint: "select every agent below"
522
+ }];
523
+ const initialValues = [];
524
+ for (const agent of linkAgents) {
525
+ const dir = targetSkillsDir(agent, cwd, ctx.env, global);
526
+ options.push({
527
+ value: agent.id,
528
+ label: agent.id,
529
+ hint: displayPath(dir, cwd)
530
+ });
531
+ if (await pathExists(dirname(dir))) initialValues.push(agent.id);
593
532
  }
594
- throw noAgentSelectionError(examplePath, global);
533
+ const selected = await multiselect("Install for another agent", options, initialValues, ".agents/skills is always installed; press Enter to skip");
534
+ const chosen = selected.includes(ALL_AGENTS) ? new Set(linkAgents.map((agent) => agent.id)) : new Set(selected);
535
+ return linkAgents.filter((agent) => chosen.has(agent.id));
595
536
  }
596
537
  function frontmatterName(manifest) {
597
538
  const text = manifest.toString("utf8");
@@ -683,6 +624,26 @@ async function install(ctx, plan, entries) {
683
624
  failure
684
625
  };
685
626
  }
627
+ function withDot(path) {
628
+ return path.startsWith("/") ? path : `./${path}`;
629
+ }
630
+ /** The boxed result shown after an interactive install. */
631
+ function installSummary(outcome, canonicalPath) {
632
+ const lines = [];
633
+ const installed = outcome.targets.filter((target) => target.status === "installed");
634
+ if (canonicalPath) {
635
+ lines.push(`✓ ${withDot(canonicalPath)}`);
636
+ const universal = AGENT_TARGETS.filter((agent) => !agent.link).map((agent) => agent.id);
637
+ if (universal.length > 0) lines.push(` universal: ${universal.join(", ")}`);
638
+ } else for (const target of installed) lines.push(`✓ ${withDot(target.path)}`);
639
+ const symlinked = installed.filter((target) => target.mode === "symlink" && target.agent).map((target) => target.agent);
640
+ const copied = installed.filter((target) => target.mode === "copy" && target.agent).map((target) => target.agent);
641
+ if (symlinked.length > 0) lines.push(` symlinked: ${symlinked.join(", ")}`);
642
+ if (copied.length > 0) lines.push(` copied: ${copied.join(", ")}`);
643
+ const failed = outcome.targets.filter((target) => target.status === "failed");
644
+ if (failed.length > 0) lines.push(` failed: ${failed.map((target) => target.path).join(", ")}`);
645
+ return lines.join("\n");
646
+ }
686
647
  async function addOne(ctx, connection, parsed, destName, agents, dir, global, copy, state) {
687
648
  const capability = await resolveSkill(connection, parsed);
688
649
  const version = resolveVersion(capability, parsed);
@@ -694,7 +655,9 @@ async function addOne(ctx, connection, parsed, destName, agents, dir, global, co
694
655
  });
695
656
  const canonicalPath = plan.canonical ? displayPath(plan.canonical, process.cwd()) : null;
696
657
  if (ctx.flags.dryRun) {
697
- ctx.summary(`resolved ${parsed.namespace}/${parsed.name} to version ${version}`);
658
+ ctx.detail(`dry-run: would install ${parsed.namespace}/${parsed.name}@${version}`);
659
+ if (canonicalPath) ctx.detail(` ${canonicalPath}`);
660
+ for (const target of plan.targets) ctx.detail(` ${displayPath(target.path, process.cwd())} (${target.mode})`);
698
661
  return { result: {
699
662
  name: capability.name,
700
663
  namespace: capability.namespace,
@@ -710,19 +673,18 @@ async function addOne(ctx, connection, parsed, destName, agents, dir, global, co
710
673
  error: null
711
674
  } };
712
675
  }
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}`);
676
+ const pkg = readSkillPackage((await downloadSkillPackage(connection, capability.id, version)).bytes);
716
677
  const declaredName = frontmatterName(pkg.manifest);
717
678
  if (declaredName !== void 0 && declaredName !== destName) ctx.summary(`warning: SKILL.md declares name ${JSON.stringify(declaredName)} but the directory is ${JSON.stringify(destName)}`);
718
679
  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}`);
680
+ const verb = outcome.failure ? "Partially installed" : "Installed";
681
+ const note = ctx.io.prompts?.note;
682
+ if (!ctx.flags.json && !ctx.flags.quiet && ctx.io.stderr.isTTY && note) note(installSummary(outcome, canonicalPath), `${verb} ${capability.namespace}/${capability.name}@${version}`);
683
+ else {
684
+ ctx.progress(`${verb.toLowerCase()} ${capability.namespace}/${capability.name}@${version}`);
685
+ if (canonicalPath) ctx.progress(` ${canonicalPath}`);
686
+ for (const target of outcome.targets) ctx.progress(target.status === "failed" ? ` failed ${target.path}` : ` ${target.path} (${target.mode})`);
687
+ }
726
688
  return { result: {
727
689
  name: capability.name,
728
690
  namespace: capability.namespace,
@@ -849,7 +811,8 @@ async function add(ctx, argv) {
849
811
  },
850
812
  failure: failureResult
851
813
  });
852
- ctx.writePayload({ results });
814
+ if (ctx.flags.json || !ctx.io.stdout.isTTY) ctx.writePayload({ results });
815
+ 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
816
  return failed ? 1 : 0;
854
817
  }
855
818
  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.1",
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",