hngr 0.1.0 → 0.2.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.
@@ -0,0 +1,512 @@
1
+ import { S as HngrError, _ as scrubCredentials, a as toHngrError, g as resolveConnection, h as redactUrl, i as buildRequestUrl, l as apiErrorFromResponse, n as apiGet } from "./http-2HWxLIOq.mjs";
2
+ import { t as collectPages } from "./pagination-BbKLKSw3.mjs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ // ../client/src/capability-path.ts
7
+ /** The Namespace every bare capability reference means. */
8
+ const SYSTEM_NAMESPACE = "system";
9
+ /**
10
+ * Splits a capability reference into its Namespace and name. A bare name is in
11
+ * the system Namespace; every other reference is `<namespace>/<name>`.
12
+ * Returns null for any other segment count so callers can report their own
13
+ * usage error.
14
+ */
15
+ function splitCapabilityPath(path) {
16
+ const segments = path.split("/");
17
+ if (segments.length === 1) return {
18
+ namespace: SYSTEM_NAMESPACE,
19
+ name: path
20
+ };
21
+ if (segments.length === 2) return {
22
+ namespace: segments[0] ?? "",
23
+ name: segments[1] ?? ""
24
+ };
25
+ return null;
26
+ }
27
+ // src/capability-resolve.ts
28
+ const CAPABILITIES_PATH = "/api/v1/capabilities";
29
+ const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
30
+ const CALVER_PATTERN = /^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.(0|[1-9][0-9]*)$/;
31
+ const SKILL_KIND = {
32
+ kind: "skill",
33
+ noun: "skill",
34
+ filtersKind: false
35
+ };
36
+ const MCP_KIND = {
37
+ kind: "mcp",
38
+ noun: "MCP capability",
39
+ filtersKind: true
40
+ };
41
+ function usage$1(title, detail) {
42
+ return new HngrError({
43
+ type: "usage",
44
+ title,
45
+ detail
46
+ });
47
+ }
48
+ /** Validates a namespace/name pair without parsing a full capability path. */
49
+ function validateCapabilityName(namespace, name, kind) {
50
+ const raw = `${namespace}/${name}`;
51
+ checkNamePart(namespace, raw, kind.noun);
52
+ checkNamePart(name, raw, kind.noun);
53
+ }
54
+ function checkNamePart(part, raw, noun) {
55
+ if (!NAME_PATTERN.test(part) || part.length > 64) throw usage$1(`Invalid ${noun} path ${JSON.stringify(raw)}`, "Names are 1-64 lowercase letters, digits and hyphens, for example `pdf` or `team-a/pdf`.");
56
+ }
57
+ /** Parses `<name>` or `<namespace>/<name>`, with an optional `@YYYY.MM.DD.N`. */
58
+ function parseCapabilityPath(raw, kind) {
59
+ if (raw.trim() === "") throw usage$1(`A ${kind.noun} path is required`, "Pass `<name>` or `<namespace>/<name>`.");
60
+ let path = raw;
61
+ let version;
62
+ const at = raw.lastIndexOf("@");
63
+ if (at !== -1) {
64
+ path = raw.slice(0, at);
65
+ version = raw.slice(at + 1);
66
+ if (!CALVER_PATTERN.test(version)) throw usage$1(`Invalid version in ${JSON.stringify(raw)}`, "A version is `@YYYY.MM.DD.N`, for example `pdf@2026.10.25.0`.");
67
+ }
68
+ const split = splitCapabilityPath(path);
69
+ if (split === null) throw usage$1(`Invalid ${kind.noun} path ${JSON.stringify(raw)}`, "Use `<name>` for the system namespace or `<namespace>/<name>`.");
70
+ checkNamePart(split.name, raw, kind.noun);
71
+ checkNamePart(split.namespace, raw, kind.noun);
72
+ return {
73
+ namespace: split.namespace,
74
+ name: split.name,
75
+ version
76
+ };
77
+ }
78
+ function candidateLabel(capability) {
79
+ return `${capability.id} (${capability.namespace}/${capability.name}, ${capability.kind})`;
80
+ }
81
+ /**
82
+ * Resolves one parsed path to exactly one Capability of the expected kind. The
83
+ * list `q` is a case-insensitive substring filter, so kind/namespace/name are
84
+ * matched here; zero or several matches are errors that name the candidates.
85
+ */
86
+ async function findCapability(connection, parsed, kind, signal) {
87
+ const matches = (await collectPages(CAPABILITIES_PATH, (cursor) => apiGet(connection, CAPABILITIES_PATH, {
88
+ ...kind.filtersKind ? { kind: kind.kind } : {},
89
+ namespace: parsed.namespace,
90
+ q: parsed.name,
91
+ cursor
92
+ }, { signal }))).filter((item) => item !== null && typeof item === "object" && item.kind === kind.kind && item.namespace === parsed.namespace && item.name === parsed.name);
93
+ const label = `${parsed.namespace}/${parsed.name}`;
94
+ if (matches.length > 1) throw new HngrError({
95
+ type: "api",
96
+ title: `${label} matches ${matches.length} capabilities`,
97
+ detail: `Candidates: ${matches.map(candidateLabel).join("; ")}.`
98
+ });
99
+ return matches[0] ?? null;
100
+ }
101
+ /** Resolves a path to exactly one Capability, or reports that it does not exist. */
102
+ async function resolveCapability(connection, parsed, kind, signal) {
103
+ const capability = await findCapability(connection, parsed, kind, signal);
104
+ if (capability === null) {
105
+ const label = `${parsed.namespace}/${parsed.name}`;
106
+ throw new HngrError({
107
+ type: "api",
108
+ title: `No ${kind.noun} named ${label}`,
109
+ detail: "Check the name and namespace, or list the catalog with `hngr capabilities list`."
110
+ });
111
+ }
112
+ return capability;
113
+ }
114
+ /** The pinned version, or the capability's Current Version. */
115
+ function resolveCapabilityVersion(capability, parsed, kind) {
116
+ if (parsed.version) return parsed.version;
117
+ if (!capability.current_version) throw new HngrError({
118
+ type: "api",
119
+ title: `The ${kind.noun} ${capability.namespace}/${capability.name} has no Current Version`
120
+ });
121
+ return capability.current_version;
122
+ }
123
+ // src/capability-download.ts
124
+ /**
125
+ * Downloads one Capability Version's content with the requested media type.
126
+ * Bearer credentials are only sent when configured; a 401 maps to exit code 4
127
+ * through the shared contract.
128
+ */
129
+ async function downloadContent(connection, capabilityId, version, accept, signal) {
130
+ const path = `/api/v1/capabilities/${encodeURIComponent(capabilityId)}/versions/${encodeURIComponent(version)}/content`;
131
+ const { url } = buildRequestUrl(connection, path);
132
+ const headers = { Accept: accept };
133
+ if (connection.token) headers.Authorization = `Bearer ${connection.token}`;
134
+ let response;
135
+ try {
136
+ response = await fetch(url, {
137
+ method: "GET",
138
+ headers,
139
+ signal
140
+ });
141
+ } catch (err) {
142
+ const origin = redactUrl(connection.server);
143
+ const cause = err instanceof Error ? err.message : String(err);
144
+ throw new HngrError({
145
+ type: "network",
146
+ title: `Could not reach the Hangar server at ${origin}`,
147
+ detail: scrubCredentials(cause, connection.server)
148
+ });
149
+ }
150
+ if (!response.ok) {
151
+ const apiError = await apiErrorFromResponse(response, response.headers.get("X-Request-ID") ?? void 0);
152
+ throw toHngrError(apiError);
153
+ }
154
+ return {
155
+ bytes: Buffer.from(await response.arrayBuffer()),
156
+ etag: response.headers.get("ETag") ?? void 0
157
+ };
158
+ }
159
+ // src/agents.ts
160
+ /** The user's home directory, honouring the environment used by the CLI. */
161
+ function homeDir(env) {
162
+ return env.HOME ?? homedir();
163
+ }
164
+ function underHome(...segments) {
165
+ return (env) => join(homeDir(env), ...segments);
166
+ }
167
+ /** Claude Code's config directory: `CLAUDE_CONFIG_DIR` or `~/.claude`. */
168
+ function claudeConfigDir(env) {
169
+ return env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR !== "" ? env.CLAUDE_CONFIG_DIR : join(homeDir(env), ".claude");
170
+ }
171
+ /** `XDG_CONFIG_HOME` or `~/.config`; Devin's user-level MCP config lives below it. */
172
+ function xdgConfigDir(env) {
173
+ return env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME !== "" ? env.XDG_CONFIG_HOME : join(homeDir(env), ".config");
174
+ }
175
+ const universalSkills = {
176
+ projectDir: null,
177
+ globalDir: null,
178
+ link: false
179
+ };
180
+ /**
181
+ * Every agent this version knows, with its skills and MCP locations in one
182
+ * place so adding an agent is a single edit. Universal agents read the
183
+ * canonical `.agents/skills` store directly and have no per-agent directory.
184
+ */
185
+ const AGENTS = [
186
+ {
187
+ id: "claude-code",
188
+ skills: {
189
+ projectDir: ".claude/skills",
190
+ globalDir: (env) => join(claudeConfigDir(env), "skills"),
191
+ link: true
192
+ },
193
+ mcp: {
194
+ kind: "claude-cli",
195
+ configPath: (env) => join(homeDir(env), ".claude.json")
196
+ }
197
+ },
198
+ {
199
+ id: "codex",
200
+ skills: universalSkills,
201
+ mcp: {
202
+ kind: "unsupported",
203
+ reason: "Codex stores MCP servers as TOML in .codex/config.toml."
204
+ }
205
+ },
206
+ {
207
+ id: "gemini-cli",
208
+ skills: universalSkills,
209
+ mcp: {
210
+ kind: "json",
211
+ project: ".gemini/settings.json",
212
+ global: underHome(".gemini", "settings.json")
213
+ }
214
+ },
215
+ {
216
+ id: "opencode",
217
+ skills: universalSkills,
218
+ mcp: {
219
+ kind: "unsupported",
220
+ reason: "OpenCode uses a top-level `mcp` object with local/remote entries."
221
+ }
222
+ },
223
+ {
224
+ id: "amp",
225
+ skills: universalSkills,
226
+ mcp: {
227
+ kind: "unsupported",
228
+ reason: "Amp uses a top-level `amp.mcpServers` object."
229
+ }
230
+ },
231
+ {
232
+ id: "cursor",
233
+ skills: universalSkills,
234
+ mcp: {
235
+ kind: "json",
236
+ project: ".cursor/mcp.json",
237
+ global: underHome(".cursor", "mcp.json")
238
+ }
239
+ },
240
+ {
241
+ id: "copilot",
242
+ skills: universalSkills,
243
+ mcp: {
244
+ kind: "unsupported",
245
+ reason: "VS Code and Copilot use a top-level `servers` object."
246
+ }
247
+ },
248
+ {
249
+ id: "cline",
250
+ skills: {
251
+ projectDir: ".cline/skills",
252
+ globalDir: underHome(".cline", "skills"),
253
+ link: true
254
+ },
255
+ mcp: { kind: "none" }
256
+ },
257
+ {
258
+ id: "zed",
259
+ skills: universalSkills,
260
+ mcp: {
261
+ kind: "unsupported",
262
+ reason: "Zed uses a top-level `context_servers` object."
263
+ }
264
+ },
265
+ {
266
+ id: "junie",
267
+ skills: {
268
+ projectDir: ".junie/skills",
269
+ globalDir: underHome(".junie", "skills"),
270
+ link: true
271
+ },
272
+ mcp: {
273
+ kind: "json",
274
+ project: ".junie/mcp/mcp.json",
275
+ global: underHome(".junie", "mcp", "mcp.json")
276
+ }
277
+ },
278
+ {
279
+ id: "qwen-code",
280
+ skills: universalSkills,
281
+ mcp: {
282
+ kind: "json",
283
+ project: ".qwen/settings.json",
284
+ global: underHome(".qwen", "settings.json")
285
+ }
286
+ },
287
+ {
288
+ id: "kiro-cli",
289
+ skills: {
290
+ projectDir: ".kiro/skills",
291
+ globalDir: underHome(".kiro", "skills"),
292
+ link: true
293
+ },
294
+ mcp: {
295
+ kind: "json",
296
+ project: ".kiro/settings/mcp.json",
297
+ global: underHome(".kiro", "settings", "mcp.json")
298
+ }
299
+ },
300
+ {
301
+ id: "devin",
302
+ skills: universalSkills,
303
+ mcp: {
304
+ kind: "json",
305
+ project: null,
306
+ global: (env) => join(xdgConfigDir(env), "devin", "mcp_config.json")
307
+ }
308
+ }
309
+ ];
310
+ function findAgentDefinition(id) {
311
+ return AGENTS.find((agent) => agent.id === id);
312
+ }
313
+ // src/commands/batch.ts
314
+ /** A repeatable string flag can arrive as one value or an array of values. */
315
+ function stringValues(value) {
316
+ if (typeof value === "string" && value !== "") return [value];
317
+ if (Array.isArray(value)) return value.flatMap((item) => stringValues(item));
318
+ return [];
319
+ }
320
+ /** Positional paths arrive as a string for one and an array for several. */
321
+ function positionalPaths(value) {
322
+ if (typeof value === "string") return [value];
323
+ if (Array.isArray(value)) return value.map((item) => String(item));
324
+ return [];
325
+ }
326
+ function errorBody(err) {
327
+ if (err instanceof HngrError) return err.body;
328
+ return {
329
+ type: "io",
330
+ title: err instanceof Error ? err.message : String(err)
331
+ };
332
+ }
333
+ /** The common fields of a per-path failure; callers spread in their extras. */
334
+ function failedResult(parsed, err, version) {
335
+ return {
336
+ name: parsed.name,
337
+ namespace: parsed.namespace,
338
+ version,
339
+ status: "failed",
340
+ targets: [],
341
+ error: errorBody(err)
342
+ };
343
+ }
344
+ /**
345
+ * Runs one command body per path. A per-path failure is reported as a result
346
+ * and does not stop the remaining paths; an error with its own exit code
347
+ * (authentication, cancellation) is command-level and escapes immediately.
348
+ */
349
+ async function runBatch(options) {
350
+ const { ctx, paths, run, failure, onResult } = options;
351
+ const results = [];
352
+ for (const parsed of paths) {
353
+ const state = { version: parsed.version ?? null };
354
+ try {
355
+ const result = await run(parsed, state);
356
+ results.push(result);
357
+ onResult?.(result);
358
+ } catch (err) {
359
+ if (err instanceof HngrError && err.exitCode !== 1) throw err;
360
+ const result = failure(parsed, err, state.version);
361
+ results.push(result);
362
+ ctx.summary(`error: ${parsed.namespace}/${parsed.name}: ${result.error?.title ?? "install failed"}`);
363
+ }
364
+ }
365
+ return {
366
+ results,
367
+ failed: results.some((result) => result.status === "failed")
368
+ };
369
+ }
370
+ // src/commands/publish.ts
371
+ const VISIBILITIES = [
372
+ "public",
373
+ "internal",
374
+ "private"
375
+ ];
376
+ function usage(title, detail) {
377
+ return new HngrError({
378
+ type: "usage",
379
+ title,
380
+ detail
381
+ });
382
+ }
383
+ function stringFlag(argv, name) {
384
+ const value = argv[name];
385
+ return typeof value === "string" && value !== "" ? value : void 0;
386
+ }
387
+ async function readOptionalFile(path) {
388
+ if (path === void 0) return;
389
+ try {
390
+ return await readFile(resolve(path), "utf8");
391
+ } catch (err) {
392
+ throw new HngrError({
393
+ type: "io",
394
+ title: `Could not read ${path}`,
395
+ detail: err instanceof Error ? err.message : String(err)
396
+ });
397
+ }
398
+ }
399
+ /** Reads the shared publish flags; a bad value fails before any request. */
400
+ async function readPublishFlags(argv, kind) {
401
+ const visibility = stringFlag(argv, "visibility") ?? "private";
402
+ if (!VISIBILITIES.includes(visibility)) throw usage(`Invalid --visibility ${JSON.stringify(visibility)}`, `Use one of: ${VISIBILITIES.join(", ")}.`);
403
+ const tags = stringValues(argv.tag);
404
+ if (tags.length > 8) throw usage("Too many --tag values", "At most eight tags are allowed.");
405
+ for (const tag of tags) {
406
+ if (tag.trim() === "") throw usage("Empty --tag value", "Pass a non-empty tag.");
407
+ if (kind === "skill" && tag.includes(",")) throw usage(`Invalid --tag ${JSON.stringify(tag)}`, "A Skill tag cannot contain a comma; the multipart form joins tags with commas.");
408
+ }
409
+ return {
410
+ namespace: stringFlag(argv, "namespace") ?? "system",
411
+ description: stringFlag(argv, "description"),
412
+ visibility,
413
+ readme: await readOptionalFile(stringFlag(argv, "readme")),
414
+ tags,
415
+ forceNew: argv.new === true
416
+ };
417
+ }
418
+ /** The `--name` flag applies to one target; several targets each use their own name. */
419
+ function explicitName(argv, targetCount) {
420
+ const name = stringFlag(argv, "name");
421
+ if (name !== void 0 && targetCount > 1) throw usage("--name cannot be combined with several targets", "Publish each target separately or drop --name to derive it from the path.");
422
+ return name;
423
+ }
424
+ function asRecord(value) {
425
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
426
+ }
427
+ function printPreview(ctx, kind, namespace, name, preview) {
428
+ ctx.detail(`dry-run: would publish ${kind} ${namespace}/${name}`);
429
+ for (const [key, value] of Object.entries(preview)) {
430
+ const shown = typeof value === "string" ? value : JSON.stringify(value);
431
+ ctx.detail(` ${key}: ${shown}`);
432
+ }
433
+ }
434
+ /**
435
+ * Runs one publish per target. Local validation and the capability lookup
436
+ * happen per target, so one bad directory does not stop the others; a target
437
+ * failure is reported in the results and the command exits 1.
438
+ */
439
+ async function runPublish(options) {
440
+ const { ctx } = options;
441
+ const connection = await resolveConnection(ctx.flags, ctx.env);
442
+ const results = [];
443
+ for (const item of options.items) {
444
+ const base = {
445
+ name: item.name,
446
+ namespace: options.namespace,
447
+ kind: options.kind,
448
+ version: null,
449
+ status: "failed",
450
+ capabilityId: null,
451
+ mode: null,
452
+ error: null
453
+ };
454
+ try {
455
+ validateCapabilityName(options.namespace, item.name, options.info);
456
+ const metadata = {
457
+ namespace: options.namespace,
458
+ name: item.name,
459
+ description: options.description,
460
+ visibility: options.visibility,
461
+ readme: options.readme,
462
+ tags: options.tags
463
+ };
464
+ const prepared = await item.prepare(metadata);
465
+ if (ctx.flags.dryRun) {
466
+ printPreview(ctx, options.kind, options.namespace, item.name, prepared.preview());
467
+ results.push({
468
+ ...base,
469
+ status: "dry-run"
470
+ });
471
+ continue;
472
+ }
473
+ const existing = await findCapability(connection, {
474
+ namespace: options.namespace,
475
+ name: item.name
476
+ }, options.info);
477
+ if (existing !== null && options.forceNew) throw usage(`The ${options.info.noun} ${options.namespace}/${item.name} already exists`, "Drop --new to publish a new version of it.");
478
+ let response;
479
+ let mode;
480
+ if (existing !== null) {
481
+ response = await prepared.version(connection, existing.id);
482
+ mode = "version";
483
+ } else {
484
+ if (options.description === void 0) throw usage("Missing --description", "Creating a capability requires --description <text>.");
485
+ response = await prepared.create(connection);
486
+ mode = "created";
487
+ }
488
+ const body = asRecord(response);
489
+ const result = {
490
+ ...base,
491
+ version: typeof body?.current_version === "string" ? body.current_version : null,
492
+ status: "published",
493
+ capabilityId: typeof body?.id === "string" ? body.id : null,
494
+ mode
495
+ };
496
+ results.push(result);
497
+ ctx.progress(`published ${options.namespace}/${item.name}${result.version ? `@${result.version}` : ""} (${mode})`);
498
+ } catch (err) {
499
+ if (err instanceof HngrError && err.exitCode !== 1) throw err;
500
+ const error = errorBody(err);
501
+ results.push({
502
+ ...base,
503
+ status: "failed",
504
+ error
505
+ });
506
+ ctx.summary(`error: ${options.namespace}/${item.name}: ${error.title}`);
507
+ }
508
+ }
509
+ ctx.writePayload({ results });
510
+ return results.some((result) => result.status === "failed") ? 1 : 0;
511
+ }
512
+ export { resolveCapabilityVersion as _, failedResult as a, stringValues as c, homeDir as d, downloadContent as f, resolveCapability as g, parseCapabilityPath as h, errorBody as i, AGENTS as l, SKILL_KIND as m, readPublishFlags as n, positionalPaths as o, MCP_KIND as p, runPublish as r, runBatch as s, explicitName as t, findAgentDefinition as u };