hngr 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,9 +47,19 @@ hngr skills add team-a/pdf-tools --agent claude-code
47
47
  hngr skills add pdf-tools@2026.10.25.1 --agent claude-code
48
48
  ```
49
49
 
50
- Non-interactive runs must pass `--agent` (repeatable) or `--all`. Use `--global` for
51
- user-level directories, `--copy` to skip the canonical `.agents/skills` directory, and
52
- `--dry-run` to preview without writing.
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.
53
63
 
54
64
  ## Publish capabilities
55
65
 
@@ -1,4 +1,4 @@
1
- import { S as HngrError, d as configFilePath, f as deleteStoredConfig, g as resolveConnection, h as redactUrl, m as readStoredConfig, n as apiGet, p as normalizeServerUrl, t as apiFetch, v as writeStoredConfig } from "./http-2HWxLIOq.mjs";
1
+ import { C as HngrError, d as configFilePath, f as deleteStoredConfig, g as resolveConnection, h as redactUrl, m as readStoredConfig, n as apiGet, p as normalizeServerUrl, t as apiFetch, v as storedAudience, y as writeStoredConfig } from "./http-DgHX7Fux.mjs";
2
2
  import { hostname } from "node:os";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { spawn } from "node:child_process";
@@ -28,12 +28,33 @@ async function activeToken(ctx) {
28
28
  source: "config"
29
29
  } : void 0;
30
30
  }
31
- async function token(ctx) {
31
+ async function token(ctx, argv) {
32
+ if (argv.audience === "mcp") return mintMCPToken(ctx);
32
33
  const active = await activeToken(ctx);
33
34
  if (!active) throw notLoggedInError();
34
35
  ctx.io.stdout.write(`${active.token}\n`);
35
36
  return 0;
36
37
  }
38
+ /**
39
+ * Mints a static MCP token with the active credential and prints it once.
40
+ * The stored config is never rewritten, so the same machine keeps its
41
+ * management login while handing out MCP tokens.
42
+ *
43
+ * The caller's kind is deliberately not checked here: the server owns the
44
+ * owner rule (a Principal can manage its own tokens), so the request must go
45
+ * out and its Problem, if any, reaches the user through the usual mapping.
46
+ */
47
+ async function mintMCPToken(ctx) {
48
+ const connection = await resolveConnection(ctx.flags, ctx.env);
49
+ if (!connection.token) throw notLoggedInError();
50
+ const minted = await mintAccessToken(connection.server, {
51
+ name: cliMCPTokenName(),
52
+ audience: "mcp",
53
+ headers: { Authorization: `Bearer ${connection.token}` }
54
+ });
55
+ ctx.io.stdout.write(`${minted.token}\n`);
56
+ return 0;
57
+ }
37
58
  /** Prompts only happen with both stdin and stdout on a TTY and a prompter available. */
38
59
  function canPrompt(ctx) {
39
60
  return ctx.io.stdin.isTTY && ctx.io.stdout.isTTY && ctx.io.prompts !== void 0;
@@ -130,6 +151,7 @@ async function loginWithToken(ctx, server, stored) {
130
151
  token_name: matched.name
131
152
  } : {},
132
153
  token_prefix: prefix,
154
+ audience: "management",
133
155
  principal,
134
156
  created_at: (/* @__PURE__ */ new Date()).toISOString()
135
157
  });
@@ -158,7 +180,11 @@ async function loginWithPassword(ctx, server, stored) {
158
180
  }
159
181
  }));
160
182
  if (ctx.flags.force && stored) await revokePreviousToken(ctx, stored);
161
- const minted = await mintAccessToken(server, cookie, cliTokenName());
183
+ const minted = await mintAccessToken(server, {
184
+ name: cliTokenName(),
185
+ audience: "management",
186
+ headers: { Cookie: cookie }
187
+ });
162
188
  const principal = asStoredPrincipal(await apiGet({
163
189
  server,
164
190
  token: minted.token
@@ -171,6 +197,7 @@ async function loginWithPassword(ctx, server, stored) {
171
197
  token_id: minted.id,
172
198
  token_name: minted.name,
173
199
  token_prefix: minted.token_prefix,
200
+ audience: "management",
174
201
  principal,
175
202
  created_at: (/* @__PURE__ */ new Date()).toISOString()
176
203
  });
@@ -308,6 +335,7 @@ async function loginDevice(ctx, server, stored) {
308
335
  token_id: minted.token_id,
309
336
  token_name: minted.name,
310
337
  token_prefix: minted.token_prefix,
338
+ audience: "management",
311
339
  principal,
312
340
  created_at: (/* @__PURE__ */ new Date()).toISOString()
313
341
  });
@@ -332,6 +360,10 @@ function sessionCookieOf(response) {
332
360
  function cliTokenName() {
333
361
  return `hngr-cli@${hostname()}`.slice(0, 64);
334
362
  }
363
+ /** `hngr-mcp@<hostname>`, so MCP tokens are recognisable in the token list. */
364
+ function cliMCPTokenName() {
365
+ return `hngr-mcp@${hostname()}`.slice(0, 64);
366
+ }
335
367
  /**
336
368
  * Finds the stored token's id so logout can revoke it. A token list that is
337
369
  * unavailable, malformed, or ambiguous leaves the id unknown; login itself
@@ -360,20 +392,24 @@ async function lookupTokenMetadata(server, token, prefix) {
360
392
  } : { id: match.id };
361
393
  }
362
394
  /**
363
- * Mints an access token with the session cookie and a random Idempotency-Key.
364
- * A replay response carries no token value: delete the replayed id and mint
365
- * again under a fresh key.
395
+ * Mints an access token with the caller's credentials and a random
396
+ * Idempotency-Key. The caller passes either a session cookie (login) or a
397
+ * Bearer token (minting for an audience). A replay response carries no token
398
+ * value: delete the replayed id and mint again under a fresh key.
366
399
  */
367
- async function mintAccessToken(server, cookie, name) {
400
+ async function mintAccessToken(server, options) {
368
401
  for (let attempt = 0; attempt < 2; attempt++) {
369
402
  const response = await apiFetch({ server }, {
370
403
  method: "POST",
371
404
  path: "/api/v1/me/tokens",
372
405
  headers: {
373
- Cookie: cookie,
406
+ ...options.headers,
374
407
  "Idempotency-Key": randomUUID()
375
408
  },
376
- body: { name }
409
+ body: {
410
+ name: options.name,
411
+ audience: options.audience
412
+ }
377
413
  });
378
414
  const body = await response.json();
379
415
  if (response.status === 201) {
@@ -397,7 +433,7 @@ async function mintAccessToken(server, cookie, name) {
397
433
  await apiFetch({ server }, {
398
434
  method: "DELETE",
399
435
  path: `/api/v1/me/tokens/${encodeURIComponent(body.id)}`,
400
- headers: { Cookie: cookie }
436
+ headers: options.headers
401
437
  });
402
438
  }
403
439
  throw new HngrError({
@@ -447,6 +483,7 @@ async function status(ctx, argv) {
447
483
  const config = await readStoredConfig(ctx.env);
448
484
  const source = active.source === "env" ? "env:HANGAR_TOKEN" : configFilePath(ctx.env);
449
485
  const tokenPrefix = active.source === "config" && config?.token_prefix ? config.token_prefix : tokenPrefixOf(active.token);
486
+ const audience = active.source === "config" ? storedAudience(config) : "management";
450
487
  let principal = config?.principal ?? null;
451
488
  if (argv.offline !== true) try {
452
489
  principal = await apiGet(connection, "/api/v1/me", void 0, { signal: AbortSignal.timeout(5e3) });
@@ -458,7 +495,8 @@ async function status(ctx, argv) {
458
495
  source,
459
496
  server: connection.server,
460
497
  principal,
461
- token_prefix: tokenPrefix
498
+ token_prefix: tokenPrefix,
499
+ audience
462
500
  });
463
501
  return 0;
464
502
  }