hngr 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/{catalog-Bv304SXJ.mjs → catalog-r53AMymQ.mjs} +269 -1
- package/dist/{commands-CLqLwQHY.mjs → commands-_R93Y2z-.mjs} +1 -1
- package/dist/index.mjs +61 -7
- package/dist/{mcp-B0sbQABp.mjs → mcp-DjSU9C8z.mjs} +52 -4
- package/dist/{scope-DVfDeQi2.mjs → scope-D8k2oa_W.mjs} +68 -4
- package/dist/{skills-ZFmFyZtw.mjs → skills-B92ip3mJ.mjs} +49 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,15 @@ install replaces the existing target; `--force` is not needed.
|
|
|
59
59
|
|
|
60
60
|
Interactive runs end with a borderless summary on stderr (canonical path, universal
|
|
61
61
|
agents, symlinked/copied agents); stdout carries the JSON payload only when piped or when
|
|
62
|
-
`--json` is passed.
|
|
62
|
+
`--json` is passed. After a successful interactive install the CLI asks once whether to
|
|
63
|
+
also install `system/find-capabilities` (the catalog's discovery skill, which finds both
|
|
64
|
+
Skills and MCP servers) for the same scope and agents; it is skipped silently when a copy
|
|
65
|
+
already exists in either scope, and a failed follow-up install only prints a warning.
|
|
66
|
+
|
|
67
|
+
MCP installs ask for the same scope and then for the MCP targets to write, with an
|
|
68
|
+
`All targets` entry; targets whose config already exists are preselected, and Claude
|
|
69
|
+
Code's `.mcp.json` is preselected for project installs. Without a terminal, pass
|
|
70
|
+
`--agent` (repeatable) or `--all`.
|
|
63
71
|
|
|
64
72
|
## Publish capabilities
|
|
65
73
|
|
|
@@ -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.6.0";
|
|
5283
5283
|
// src/generated/completion.ts
|
|
5284
5284
|
function isCompletionShell(value) {
|
|
5285
5285
|
return value === "bash" || value === "zsh" || value === "fish";
|
|
@@ -6429,6 +6429,23 @@ var y = class {
|
|
|
6429
6429
|
}
|
|
6430
6430
|
}
|
|
6431
6431
|
};
|
|
6432
|
+
var r = class extends y {
|
|
6433
|
+
get cursor() {
|
|
6434
|
+
return this.value ? 0 : 1;
|
|
6435
|
+
}
|
|
6436
|
+
get _value() {
|
|
6437
|
+
return this.cursor === 0;
|
|
6438
|
+
}
|
|
6439
|
+
constructor(t) {
|
|
6440
|
+
super(t, false), this.value = !!t.initialValue, this.on("userInput", () => {
|
|
6441
|
+
this.value = this._value;
|
|
6442
|
+
}), this.on("confirm", (i) => {
|
|
6443
|
+
this.output.write(import_src.cursor.move(0, -1)), this.value = i, this.state = "submit", this.close();
|
|
6444
|
+
}), this.on("cursor", () => {
|
|
6445
|
+
this.value = !this.value;
|
|
6446
|
+
});
|
|
6447
|
+
}
|
|
6448
|
+
};
|
|
6432
6449
|
var a = class extends y {
|
|
6433
6450
|
options;
|
|
6434
6451
|
cursor = 0;
|
|
@@ -6633,6 +6650,35 @@ const limitOptions = ({ cursor: l, options: e, style: w, output: p = process.std
|
|
|
6633
6650
|
for (const t of s) for (const n of t) x.push(n);
|
|
6634
6651
|
return c && x.push(M), x;
|
|
6635
6652
|
};
|
|
6653
|
+
const confirm = (e) => {
|
|
6654
|
+
const a = e.active ?? "Yes", o = e.inactive ?? "No";
|
|
6655
|
+
return new r({
|
|
6656
|
+
active: a,
|
|
6657
|
+
inactive: o,
|
|
6658
|
+
signal: e.signal,
|
|
6659
|
+
input: e.input,
|
|
6660
|
+
output: e.output,
|
|
6661
|
+
initialValue: e.initialValue ?? true,
|
|
6662
|
+
render() {
|
|
6663
|
+
const i = e.withGuide ?? settings.withGuide, u = `${symbol(this.state)} `, l = i ? `${styleText("gray", S_BAR)} ` : "", f = wrapTextWithPrefix(e.output, e.message, l, u), s = `${i ? `${styleText("gray", S_BAR)}
|
|
6664
|
+
` : ""}${f}
|
|
6665
|
+
`, c = this.value ? a : o;
|
|
6666
|
+
switch (this.state) {
|
|
6667
|
+
case "submit": return `${s}${i ? `${styleText("gray", S_BAR)} ` : ""}${styleText("dim", c)}`;
|
|
6668
|
+
case "cancel": return `${s}${i ? `${styleText("gray", S_BAR)} ` : ""}${styleText(["strikethrough", "dim"], c)}${i ? `
|
|
6669
|
+
${styleText("gray", S_BAR)}` : ""}`;
|
|
6670
|
+
default: {
|
|
6671
|
+
const r = i ? `${styleText("cyan", S_BAR)} ` : "", g = i ? styleText("cyan", S_BAR_END) : "";
|
|
6672
|
+
return `${s}${r}${this.value ? `${styleText("green", S_RADIO_ACTIVE)} ${a}` : `${styleText("dim", S_RADIO_INACTIVE)} ${styleText("dim", a)}`}${e.vertical ? i ? `
|
|
6673
|
+
${styleText("cyan", S_BAR)} ` : `
|
|
6674
|
+
` : ` ${styleText("dim", "/")} `}${this.value ? `${styleText("dim", S_RADIO_INACTIVE)} ${styleText("dim", o)}` : `${styleText("green", S_RADIO_ACTIVE)} ${o}`}
|
|
6675
|
+
${g}
|
|
6676
|
+
`;
|
|
6677
|
+
}
|
|
6678
|
+
}
|
|
6679
|
+
}
|
|
6680
|
+
}).prompt();
|
|
6681
|
+
};
|
|
6636
6682
|
const MULTISELECT_INSTRUCTIONS = [
|
|
6637
6683
|
`${styleText("dim", "↑/↓")} to navigate`,
|
|
6638
6684
|
`${styleText("dim", "Space:")} select`,
|
|
@@ -6905,6 +6951,14 @@ function clackPrompter(streams = {}) {
|
|
|
6905
6951
|
initialValue
|
|
6906
6952
|
}));
|
|
6907
6953
|
},
|
|
6954
|
+
async confirm(message, initialValue = true) {
|
|
6955
|
+
return unwrapPrompt(await confirm({
|
|
6956
|
+
message,
|
|
6957
|
+
input,
|
|
6958
|
+
output,
|
|
6959
|
+
initialValue
|
|
6960
|
+
}));
|
|
6961
|
+
},
|
|
6908
6962
|
async multiselect(message, options, initialValues = [], hint) {
|
|
6909
6963
|
if (hint) output.write(`${import_picocolors.default.dim(hint)}\n`);
|
|
6910
6964
|
return unwrapPrompt(await multiselect({
|
|
@@ -7021,7 +7075,7 @@ function buildParser(onMatched) {
|
|
|
7021
7075
|
parser.command("commands", "Print the command catalog", () => {}, (argv) => {
|
|
7022
7076
|
onMatched({
|
|
7023
7077
|
argv,
|
|
7024
|
-
run: async (ctx) => (await import("./commands-
|
|
7078
|
+
run: async (ctx) => (await import("./commands-_R93Y2z-.mjs")).run(ctx)
|
|
7025
7079
|
});
|
|
7026
7080
|
});
|
|
7027
7081
|
parser.command("auth", "Manage credentials", (auth) => {
|
|
@@ -7101,7 +7155,7 @@ function buildParser(onMatched) {
|
|
|
7101
7155
|
}), (argv) => {
|
|
7102
7156
|
onMatched({
|
|
7103
7157
|
argv,
|
|
7104
|
-
run: async (ctx, matchedArgv) => (await import("./skills-
|
|
7158
|
+
run: async (ctx, matchedArgv) => (await import("./skills-B92ip3mJ.mjs")).add(ctx, matchedArgv)
|
|
7105
7159
|
});
|
|
7106
7160
|
});
|
|
7107
7161
|
skills.command("publish <paths..>", "Publish a local skill directory as a capability or a new version", (publish) => publish.option("namespace", {
|
|
@@ -7129,7 +7183,7 @@ function buildParser(onMatched) {
|
|
|
7129
7183
|
}), (argv) => {
|
|
7130
7184
|
onMatched({
|
|
7131
7185
|
argv,
|
|
7132
|
-
run: async (ctx, matchedArgv) => (await import("./skills-
|
|
7186
|
+
run: async (ctx, matchedArgv) => (await import("./skills-B92ip3mJ.mjs")).publish(ctx, matchedArgv)
|
|
7133
7187
|
});
|
|
7134
7188
|
});
|
|
7135
7189
|
return skills.demandCommand(1);
|
|
@@ -7151,7 +7205,7 @@ function buildParser(onMatched) {
|
|
|
7151
7205
|
}), (argv) => {
|
|
7152
7206
|
onMatched({
|
|
7153
7207
|
argv,
|
|
7154
|
-
run: async (ctx, matchedArgv) => (await import("./mcp-
|
|
7208
|
+
run: async (ctx, matchedArgv) => (await import("./mcp-DjSU9C8z.mjs")).add(ctx, matchedArgv)
|
|
7155
7209
|
});
|
|
7156
7210
|
});
|
|
7157
7211
|
mcp.command("publish <paths..>", "Publish an MCP snapshot as a capability or a new version", (publish) => publish.option("namespace", {
|
|
@@ -7179,7 +7233,7 @@ function buildParser(onMatched) {
|
|
|
7179
7233
|
}), (argv) => {
|
|
7180
7234
|
onMatched({
|
|
7181
7235
|
argv,
|
|
7182
|
-
run: async (ctx, matchedArgv) => (await import("./mcp-
|
|
7236
|
+
run: async (ctx, matchedArgv) => (await import("./mcp-DjSU9C8z.mjs")).publish(ctx, matchedArgv)
|
|
7183
7237
|
});
|
|
7184
7238
|
});
|
|
7185
7239
|
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: [],
|
|
@@ -565,7 +613,7 @@ async function add(ctx, argv) {
|
|
|
565
613
|
if (rawPaths.length === 0) throw usage("No capability path given", "Pass one or more `<name>` or `<namespace>/<name>` arguments.");
|
|
566
614
|
const parsedPaths = rawPaths.map(parseMcpPath);
|
|
567
615
|
const scope = await resolveInstallScope(ctx, argv.global === true);
|
|
568
|
-
const targets =
|
|
616
|
+
const targets = await selectTargets(ctx, argv, scope);
|
|
569
617
|
const connection = await resolveConnection(ctx.flags, ctx.env);
|
|
570
618
|
let approvalHintShown = false;
|
|
571
619
|
const { results, failed } = await runBatch({
|
|
@@ -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";
|
|
@@ -323,6 +324,69 @@ const AGENTS = [
|
|
|
323
324
|
function findAgentDefinition(id) {
|
|
324
325
|
return AGENTS.find((agent) => agent.id === id);
|
|
325
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
|
+
}
|
|
326
390
|
// src/commands/batch.ts
|
|
327
391
|
/** A repeatable string flag can arrive as one value or an array of values. */
|
|
328
392
|
function stringValues(value) {
|
|
@@ -573,4 +637,4 @@ async function resolveInstallScope(ctx, global) {
|
|
|
573
637
|
if (!select || ctx.flags.json || !ctx.io.stdin.isTTY || !ctx.io.stdout.isTTY) return "project";
|
|
574
638
|
return await select("Install scope", SCOPE_OPTIONS, "project") === "global" ? "global" : "project";
|
|
575
639
|
}
|
|
576
|
-
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) {
|
|
@@ -850,6 +787,47 @@ async function publish(ctx, argv) {
|
|
|
850
787
|
items
|
|
851
788
|
});
|
|
852
789
|
}
|
|
790
|
+
const FIND_CAPABILITIES = "find-capabilities";
|
|
791
|
+
/** The follow-up question needs a yes/no prompter on both ends of a terminal. */
|
|
792
|
+
function canConfirm(ctx) {
|
|
793
|
+
return ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && ctx.io.prompts?.confirm !== void 0;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Whether any scope the caller would install into already holds find-capabilities.
|
|
797
|
+
* A project install does not ask again when the global store has it, and the
|
|
798
|
+
* reverse, so the offer appears only when nothing is installed yet.
|
|
799
|
+
*/
|
|
800
|
+
async function findCapabilitiesInstalled(ctx, agents, options) {
|
|
801
|
+
const scopes = options.dir ? [options.global] : [false, true];
|
|
802
|
+
for (const global of scopes) {
|
|
803
|
+
const plan = planInstall(process.cwd(), ctx.env, FIND_CAPABILITIES, agents, {
|
|
804
|
+
...options,
|
|
805
|
+
global
|
|
806
|
+
});
|
|
807
|
+
const paths = [...plan.canonical ? [plan.canonical] : [], ...plan.targets.map((target) => target.path)];
|
|
808
|
+
for (const path of paths) if (await pathExists(path)) return true;
|
|
809
|
+
}
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Offers the catalog's capability-discovery skill after a successful install so
|
|
814
|
+
* an agent that just gained one capability can find the rest, Skills and MCP
|
|
815
|
+
* servers alike. It is a convenience, not part of the request: an
|
|
816
|
+
* already-installed copy or a failed download never makes the add itself fail
|
|
817
|
+
* (a cancelled prompt still exits 130).
|
|
818
|
+
*/
|
|
819
|
+
async function offerFindCapabilities(ctx, connection, destNames, agents, options) {
|
|
820
|
+
const confirm = ctx.io.prompts?.confirm;
|
|
821
|
+
if (!confirm || destNames.includes(FIND_CAPABILITIES)) return;
|
|
822
|
+
if (await findCapabilitiesInstalled(ctx, agents, options)) return;
|
|
823
|
+
if (!await confirm(`Also install ${FIND_CAPABILITIES} (discover more capabilities)?`, true)) return;
|
|
824
|
+
try {
|
|
825
|
+
await addOne(ctx, connection, parseSkillPath(FIND_CAPABILITIES), FIND_CAPABILITIES, agents, options.dir, options.global, options.copy, { version: null });
|
|
826
|
+
} catch (err) {
|
|
827
|
+
if (err instanceof HngrError && err.exitCode !== 1) throw err;
|
|
828
|
+
ctx.summary(`warning: could not install ${FIND_CAPABILITIES}: ${err instanceof Error ? err.message : String(err)}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
853
831
|
async function add(ctx, argv) {
|
|
854
832
|
const rawPaths = positionalPaths(argv.paths);
|
|
855
833
|
if (rawPaths.length === 0) throw usage("No skill path given", "Pass one or more `<name>` or `<namespace>/<name>` arguments.");
|
|
@@ -875,6 +853,11 @@ async function add(ctx, argv) {
|
|
|
875
853
|
failure: failureResult
|
|
876
854
|
});
|
|
877
855
|
if (ctx.flags.json || !ctx.io.stdout.isTTY) ctx.writePayload({ results });
|
|
856
|
+
if (!ctx.flags.json && !ctx.flags.dryRun && !ctx.flags.quiet && !failed && canConfirm(ctx)) await offerFindCapabilities(ctx, connection, parsedPaths.map((parsed) => as ?? parsed.name), agents, {
|
|
857
|
+
dir,
|
|
858
|
+
global,
|
|
859
|
+
copy
|
|
860
|
+
});
|
|
878
861
|
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.");
|
|
879
862
|
return failed ? 1 : 0;
|
|
880
863
|
}
|