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 +18 -6
- package/dist/{catalog-Bv304SXJ.mjs → catalog-r53AMymQ.mjs} +269 -1
- package/dist/{commands-CLqLwQHY.mjs → commands-_R93Y2z-.mjs} +1 -1
- package/dist/index.mjs +26 -10
- package/dist/{mcp-DWF_ELDU.mjs → mcp-DjSU9C8z.mjs} +53 -7
- package/dist/{scope-Ch6wZCgE.mjs → scope-D8k2oa_W.mjs} +82 -5
- package/dist/{skills-CxMeJ5PF.mjs → skills-C3cphIAl.mjs} +65 -102
- package/package.json +1 -1
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
|
-
|
|
51
|
-
terminal, omitting `--global` first asks whether to
|
|
52
|
-
the
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.",
|
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-
|
|
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.
|
|
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:
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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 =
|
|
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 {
|
|
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,
|
|
2
|
-
import { _ as
|
|
3
|
-
import { lstat,
|
|
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
|
-
/**
|
|
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
|
|
564
|
-
*
|
|
565
|
-
*
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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 };
|