@kontextmind/kxm 0.7.165 → 0.7.166

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.
Files changed (35) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/docs/concepts/trust-model.md +1 -1
  4. package/docs/glossary.md +2 -2
  5. package/docs/operations/troubleshooting.md +2 -2
  6. package/docs/operations.md +62 -8
  7. package/docs/reference/cli-reference.md +11 -11
  8. package/docs/reference/config-reference.md +36 -11
  9. package/docs/reference/configuration.md +2 -2
  10. package/docs/reference/tools.md +1 -1
  11. package/docs/start/quickstart-claude-code.md +2 -2
  12. package/package.json +1 -1
  13. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  14. package/plugins/kxm/README.md +3 -3
  15. package/plugins/kxm/dist/claude-hook.js +417 -66
  16. package/plugins/kxm/dist/cli.js +1399 -900
  17. package/plugins/kxm/dist/extension.js +399 -64
  18. package/plugins/kxm/dist/mcp-server.js +622 -135
  19. package/plugins/kxm/dist/runtime-supervisor.js +668 -166
  20. package/plugins/kxm/dist/runtime.js +713 -211
  21. package/plugins/kxm/dist/server.js +466 -58
  22. package/plugins/kxm/package.json +1 -1
  23. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +15 -7
  24. package/plugins/kxm/src/cli/hub.ts +80 -21
  25. package/plugins/kxm/src/cli/types.ts +8 -1
  26. package/plugins/kxm/src/cli.ts +41 -7
  27. package/plugins/kxm/src/config.ts +246 -3
  28. package/plugins/kxm/src/extension.ts +8 -7
  29. package/plugins/kxm/src/hub-autostart.ts +20 -0
  30. package/plugins/kxm/src/hub-env.ts +83 -31
  31. package/plugins/kxm/src/hub-identity.ts +337 -0
  32. package/plugins/kxm/src/mcp-server.ts +17 -15
  33. package/plugins/kxm/src/project-name.ts +6 -47
  34. package/plugins/kxm/src/server.ts +22 -4
  35. package/scripts/kxm-hub.mjs +5 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.165",
3
+ "version": "0.7.166",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -46,7 +46,7 @@ the hub admin token or a project token.
46
46
  |---|---|---|
47
47
  | `kxm hub start` | Start the hub in the foreground | `--json` |
48
48
  | `kxm hub stop` | Request managed hub and worker shutdown | `--wait-ms <ms>` |
49
- | `kxm hub bind <url>` | Bind this machine to a running hub | http or https URL. `--cloud` with `--token-env` or `--token-command` for an SSH forward to loopback. The token is not stored |
49
+ | `kxm hub bind <url>` | Bind this machine to a running hub and record the non-secret endpoint in `.kxm/config.yaml` | http or https URL. `--token-env` and `--key-op` for either mode. `--cloud` with `--token-env`, `--token-command`, or `--key-op` for an SSH forward to loopback. The token is not stored |
50
50
  | `kxm hub unbind` | Remove this machine's hub binding | `--json` |
51
51
  | `kxm restore <manifest>` | Replace this project's live stores from a verified backup manifest | `--all-projects`, `--json` |
52
52
 
@@ -60,19 +60,27 @@ kxm restore .kxm/backups/pre-upgrade/manifest.json
60
60
  `KXM_AUTH_TOKEN` is unset it generates a long random admin token once and
61
61
  persists it in `hub-env.json` (schema `kxm.hub-env.v1`, mode `0600`) under
62
62
  the user state root, then reuses it on every restart.
63
- - `KXM_PROJECT_TOKENS` must list every project's token. It replaces the saved
64
- token map rather than merging with it, and the replacement is saved. When a
65
- hub already serves other projects, build the full map with the merge
66
- command in the KXM documentation's Claude Code quick start
63
+ - `KXM_PROJECT_TOKENS` must list every project's token when it is set. It
64
+ replaces the saved token map rather than merging with it, and the
65
+ replacement is saved. The same map can instead live in `.kxm/config.yaml`
66
+ as `hub.projects`, with `op://` references or environment variable names.
67
+ That map is resolved in memory and is not saved. When a hub already serves
68
+ other projects, build the full map with the merge command in the KXM
69
+ documentation's Claude Code quick start
67
70
  (docs/start/quickstart-claude-code.md, section "Start the hub") before
68
71
  restarting the hub.
72
+ - `kxm hub view` prints the mode, URL, project id, and key source. It does
73
+ not print the token. The project id is the flag, then `KXM_PROJECT`, then
74
+ `hub.local.project` or `hub.cloud.project`, then `.kxm/project.yaml`, then
75
+ the package name, then the directory name.
69
76
  - `kxm hub bind` to a remote (non-loopback) URL fails closed with
70
77
  `hub_bind_unauthenticated` unless a credential for the current project
71
78
  resolves from `KXM_AUTH_TOKEN` or the persisted `hub-env.json`.
72
79
  - `kxm hub bind --cloud` is the bind for a loopback SSH forward of a remote
73
80
  hub. It never falls back to this machine's hub-env token. Pass
74
- `--token-env KXMD_HUB_TOKEN` or `--token-command "op read op://..."`.
75
- See docs/operations.md, section "Cross-box peer attach".
81
+ `--token-env KXMD_HUB_TOKEN`, `--key-op op://Private/kxmd/project-token`,
82
+ or `--token-command "op read op://..."`. The non-secret fields are written
83
+ to `.kxm/config.yaml`. See docs/operations.md, section "Cross-box peer attach".
76
84
  - Hub PID claims record the wrapper and server child PID. A dead wrapper's
77
85
  claim is reclaimed automatically, an orphaned server is terminated first,
78
86
  and `kxm hub stop` recovers such orphans directly.
@@ -3,7 +3,9 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
3
3
  import { join, resolve } from "node:path";
4
4
  import { redactSecrets } from "../redact.ts";
5
5
  import { defaultProjectName } from "../project-name.ts";
6
- import { agentSessionEnv, hasClientHubCredential, HubEnvError, resolveClientHubAuthToken } from "../hub-env.ts";
6
+ import { agentSessionEnv, hasClientHubCredential, HubEnvError, readHubEnvRecord, resolveClientHubAuthToken } from "../hub-env.ts";
7
+ import { deleteKxmConfigValue, findConfigRoot, isHubOpReference, KxmHubConfigError, writeHubEndpoint } from "../config.ts";
8
+ import { describeHubConnection, opReferenceFromTokenCommand, resolveProjectIdentity } from "../hub-identity.ts";
7
9
  import { agentWorker, type Worker } from "../envelope.ts";
8
10
  import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "../tui.ts";
9
11
  import { formatSessionBriefText, loadSessionBriefAsync, readCachedSessionBrief, type SessionHubStatus } from "../session-work.ts";
@@ -21,7 +23,6 @@ import {
21
23
  splitTokenCommand,
22
24
  validateHubUrl,
23
25
  writeHubBinding,
24
- resolveCloudHubToken,
25
26
  type HubBindingRecord,
26
27
  type HubHealth,
27
28
  } from "../hub-binding.ts";
@@ -157,11 +158,21 @@ export async function refreshKxmUpdateNotice(
157
158
  }
158
159
 
159
160
  export async function cmdStatus(runtime: Runtime): Promise<number> {
161
+ const connection = describeHubConnection(runtime.dirs.workdir, runtime.env, {
162
+ hasSavedProjectToken: (project) => {
163
+ try {
164
+ const token = readHubEnvRecord(runtime.env)?.projectTokens?.[project];
165
+ return typeof token === "string" && token.trim().length > 0;
166
+ } catch {
167
+ return false;
168
+ }
169
+ },
170
+ });
160
171
  let headers: Record<string, string> | undefined;
161
172
  try {
162
- const binding = readHubBinding(runtime.env);
163
- if (binding?.cloud) {
164
- headers = { authorization: `Bearer ${resolveCloudHubToken(binding, runtime.env)}` };
173
+ if (connection.mode === "cloud") {
174
+ const token = resolveClientHubAuthToken(runtime.env, connection.project, runtime.dirs.workdir);
175
+ if (token) headers = { authorization: `Bearer ${token}` };
165
176
  }
166
177
  } catch (error) {
167
178
  if (error instanceof CloudTokenError) {
@@ -177,19 +188,29 @@ export async function cmdStatus(runtime: Runtime): Promise<number> {
177
188
  // Scope is a property of the URL actually contacted, not of whichever file the
178
189
  // binding came from: KXM_SERVER_URL overrides the binding, and labelling the binding
179
190
  // while probing an override would report "loopback" about a remote request.
180
- // A cloud binding is remote when the contacted URL is that binding, including a
191
+ // A cloud mode is remote when the contacted URL is that endpoint, including a
181
192
  // loopback SSH forward.
182
- const effectiveScope = effectiveHubBindingScope(runtime.serverUrl, runtime.env);
183
- const overridden = Boolean(runtime.boundHubUrl && runtime.boundHubUrl !== runtime.serverUrl);
193
+ const effectiveScope = connection.scope;
194
+ const overridden = connection.urlSource === "env";
195
+ const identity = {
196
+ mode: connection.mode,
197
+ modeSource: connection.modeSource,
198
+ url: connection.url,
199
+ urlSource: connection.urlSource,
200
+ project: connection.project,
201
+ projectSource: connection.source,
202
+ keySource: connection.keySource,
203
+ };
184
204
  const payload = {
185
205
  ok: health.ok && ready.ok,
186
206
  command: "hub view",
187
207
  target: { url: runtime.serverUrl, scope: effectiveScope, ...(overridden ? { source: "env" } : {}) },
208
+ identity,
188
209
  health: health.body,
189
210
  ready: ready.body,
190
211
  };
191
212
  print(runtime.io, runtime.json, payload,
192
- `hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""}`);
213
+ `hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} · mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}`);
193
214
  return payload.ok ? 0 : 1;
194
215
  }
195
216
 
@@ -216,10 +237,10 @@ export async function cmdDash(runtime: Runtime, options: { screen?: string | und
216
237
  runtime.io.stderr("kxm dash does not support --json; use kxm hub view\n");
217
238
  return 2;
218
239
  }
219
- const project = defaultProjectName(runtime.dirs.workdir, runtime.env) || "project";
240
+ const project = resolveProjectIdentity(runtime.dirs.workdir, runtime.env).project || "project";
220
241
  let authToken: string | undefined;
221
242
  try {
222
- authToken = resolveClientHubAuthToken(runtime.env, project);
243
+ authToken = resolveClientHubAuthToken(runtime.env, project, runtime.dirs.workdir);
223
244
  } catch (error) {
224
245
  if (error instanceof CloudTokenError) {
225
246
  print(runtime.io, runtime.json, { ok: false, command: "dash", error: error.code, detail: error.message }, error.message);
@@ -292,6 +313,7 @@ export interface HubBindOptions {
292
313
  cloud?: boolean | undefined;
293
314
  tokenEnv?: string | undefined;
294
315
  tokenCommand?: string | undefined;
316
+ keyOp?: string | undefined;
295
317
  }
296
318
 
297
319
  export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubBindOptions = {}): Promise<number> {
@@ -313,25 +335,35 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
313
335
  const cloud = options.cloud === true;
314
336
  const tokenEnv = options.tokenEnv?.trim() || undefined;
315
337
  const tokenCommand = options.tokenCommand?.trim() || undefined;
316
- if ((tokenEnv || tokenCommand) && !cloud) {
338
+ const keyOp = options.keyOp?.trim() || opReferenceFromTokenCommand(tokenCommand ?? "") || undefined;
339
+ if (options.keyOp?.trim() && !isHubOpReference(options.keyOp.trim())) {
340
+ print(
341
+ runtime.io,
342
+ runtime.json,
343
+ { ok: false, command: "hub bind", error: "hub_config_invalid", url },
344
+ "--key-op must be an op://vault/item/field reference, not a token; no binding was written",
345
+ );
346
+ return 2;
347
+ }
348
+ if (tokenCommand && !cloud) {
317
349
  print(
318
350
  runtime.io,
319
351
  runtime.json,
320
352
  { ok: false, command: "hub bind", error: "cloud_flag_required", url },
321
- "--token-env and --token-command require --cloud; no binding was written",
353
+ "--token-command requires --cloud; no binding was written",
322
354
  );
323
355
  return 2;
324
356
  }
325
- if (cloud && !tokenEnv && !tokenCommand) {
357
+ if (cloud && !tokenEnv && !tokenCommand && !keyOp) {
326
358
  print(
327
359
  runtime.io,
328
360
  runtime.json,
329
361
  { ok: false, command: "hub bind", error: "cloud_token_source_required", url },
330
- "--cloud needs --token-env, --token-command, or both; the token is not stored in the binding",
362
+ "--cloud needs --token-env, --token-command, --key-op, or a combination; the token is not stored",
331
363
  );
332
364
  return 2;
333
365
  }
334
- if (cloud && tokenEnv && !isCloudTokenEnvName(tokenEnv)) {
366
+ if (tokenEnv && !isCloudTokenEnvName(tokenEnv)) {
335
367
  print(
336
368
  runtime.io,
337
369
  runtime.json,
@@ -367,10 +399,10 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
367
399
  if (!cloud && scope === "remote") {
368
400
  // The project that will actually authenticate: a record holding only another
369
401
  // project's token cannot authorise this one.
370
- const bindProject = defaultProjectName(runtime.dirs.workdir, runtime.env) || "project";
402
+ const bindProject = resolveProjectIdentity(runtime.dirs.workdir, runtime.env).project || "project";
371
403
  let credentialReady = false;
372
404
  try {
373
- credentialReady = hasClientHubCredential(runtime.env, bindProject);
405
+ credentialReady = hasClientHubCredential(runtime.env, bindProject, runtime.dirs.workdir);
374
406
  } catch (error) {
375
407
  // A malformed record is a readable configuration failure, not an uncaught throw
376
408
  // past a user-facing entry point.
@@ -413,8 +445,9 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
413
445
  }
414
446
  }
415
447
  const file = hubBindingFile(runtime.env);
448
+ const bindingCommand = tokenCommand ?? (cloud && keyOp ? `op read ${keyOp}` : undefined);
416
449
  const cloudFields = cloud
417
- ? { cloud: true as const, ...(tokenEnv ? { tokenEnv } : {}), ...(tokenCommand ? { tokenCommand } : {}) }
450
+ ? { cloud: true as const, ...(tokenEnv ? { tokenEnv } : {}), ...(bindingCommand ? { tokenCommand: bindingCommand } : {}) }
418
451
  : {};
419
452
  const record: HubBindingRecord = {
420
453
  schema: HUB_BINDING_SCHEMA,
@@ -422,19 +455,44 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
422
455
  boundAt: new Date().toISOString(),
423
456
  ...cloudFields,
424
457
  };
458
+ const identity = resolveProjectIdentity(runtime.dirs.workdir, runtime.env);
459
+ const configFile = join(findConfigRoot(runtime.dirs.workdir), ".kxm", "config.yaml");
460
+ const key = {
461
+ ...(keyOp ? { op: keyOp } : {}),
462
+ ...(tokenEnv ? { env: tokenEnv } : {}),
463
+ };
425
464
  const cloudNote = cloud ? " · token is not stored" : "";
426
465
  if (runtime.dryRun) {
427
466
  print(
428
467
  runtime.io,
429
468
  runtime.json,
430
- { ok: true, command: "hub bind", dryRun: true, url, scope, file, ...cloudFields },
469
+ { ok: true, command: "hub bind", dryRun: true, url, scope, file, configFile, project: identity.project, ...cloudFields },
431
470
  `would bind hub ${url} (${scope})${cloudNote}`,
432
471
  );
433
472
  return 0;
434
473
  }
474
+ try {
475
+ writeHubEndpoint(runtime.dirs.workdir, {
476
+ mode: cloud ? "cloud" : "local",
477
+ url,
478
+ project: identity.project,
479
+ ...(key.op || key.env ? { key } : {}),
480
+ });
481
+ } catch (error) {
482
+ if (error instanceof KxmHubConfigError) {
483
+ print(
484
+ runtime.io,
485
+ runtime.json,
486
+ { ok: false, command: "hub bind", error: error.code, detail: error.message },
487
+ `${error.message}; no binding was written`,
488
+ );
489
+ return 2;
490
+ }
491
+ throw error;
492
+ }
435
493
  writeHubBinding({ ...record, boundAt: new Date().toISOString() }, runtime.env);
436
494
  const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
437
- print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, health, probeMs, ...cloudFields },
495
+ print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, configFile, project: identity.project, health, probeMs, ...cloudFields },
438
496
  `bound hub ${url} · ${scope} · ${formatHubBindHealth(health)}${cloud ? cloudNote : scope === "remote" ? " · token leaves this machine" : ""}`);
439
497
  return 0;
440
498
  }
@@ -463,6 +521,7 @@ export async function cmdHubUnbind(runtime: Runtime): Promise<number> {
463
521
  return 1;
464
522
  }
465
523
  removeHubBinding(runtime.env);
524
+ deleteKxmConfigValue(findConfigRoot(runtime.dirs.workdir), "hub.mode");
466
525
  if (malformed) {
467
526
  print(runtime.io, runtime.json, { ok: true, command: "hub unbind", file }, "unbound hub (record was malformed)");
468
527
  return 0;
@@ -12,6 +12,7 @@ import {
12
12
  hubBindingFile,
13
13
  readHubBinding,
14
14
  } from "../hub-binding.ts";
15
+ import { describeHubConnection } from "../hub-identity.ts";
15
16
  import type { InstallProbe } from "../kxm-install-kind.ts";
16
17
 
17
18
  export interface CliSpawnResult {
@@ -166,6 +167,11 @@ export function printWorker(
166
167
  }
167
168
 
168
169
  export function hostMode(runtime: Runtime): "local" | "hub" {
170
+ try {
171
+ if (describeHubConnection(runtime.cwd, runtime.env).scope === "remote") return "hub";
172
+ } catch {
173
+ // A malformed binding is classified from the URL below.
174
+ }
169
175
  try {
170
176
  if (effectiveHubBindingScope(runtime.serverUrl, runtime.env) === "remote") return "hub";
171
177
  } catch {
@@ -282,13 +288,14 @@ export function runtimeFrom(ctx: CliContext, command: Command): Runtime {
282
288
  throw error;
283
289
  }
284
290
  }
291
+ const connection = describeHubConnection(ctx.cwd, ctx.env);
285
292
  return {
286
293
  ...ctx,
287
294
  json: Boolean(opts.json),
288
295
  dryRun: Boolean(opts.dryRun),
289
296
  ...(opts.workspace === undefined ? {} : { workspaceFlag: opts.workspace }),
290
297
  dirs: workspaceDirs(ctx.cwd, opts.workspace, ctx.env),
291
- serverUrl: envServerUrl || boundHubUrl || "http://127.0.0.1:7331",
298
+ serverUrl: connection.url || envServerUrl || boundHubUrl || "http://127.0.0.1:7331",
292
299
  ...(boundHubUrl ? { boundHubUrl } : {}),
293
300
  fetchImpl: ctx.io.fetchImpl ?? fetch,
294
301
  };
@@ -17,10 +17,11 @@ import { fileURLToPath } from "node:url";
17
17
  import { Command, CommanderError } from "commander";
18
18
  import { readInstalledKxmVersion } from "./kxm-update.ts";
19
19
  import { findKxmRepoRoot } from "./repo-root.ts";
20
- import { HubClient } from "./client.ts";
20
+ import { HubClient, HubHttpError } from "./client.ts";
21
21
  import { AgentProjectTokenMissingError, resolveAgentHubAuthToken } from "./hub-env.ts";
22
22
  import { CloudTokenError } from "./hub-binding.ts";
23
- import { defaultProjectName } from "./project-name.ts";
23
+ import { KxmHubConfigError } from "./config.ts";
24
+ import { resolveProjectIdentity } from "./hub-identity.ts";
24
25
  import {
25
26
  AGENT_COMMANDS_MAP,
26
27
  enforceToolPolicy,
@@ -256,13 +257,14 @@ function addGlobalOptions(command: Command): Command {
256
257
 
257
258
  async function ensureCliClient(runtime: Runtime): Promise<HubClient> {
258
259
  const serverUrl = runtime.serverUrl;
259
- const project = defaultProjectName(runtime.cwd, runtime.env);
260
+ const identity = resolveProjectIdentity(runtime.cwd, runtime.env);
261
+ const project = identity.project;
260
262
  const name = runtime.env.KXM_AGENT_NAME?.trim() || `cli-${process.pid}`;
261
263
  const purpose = runtime.env.KXM_AGENT_PURPOSE?.trim() || "CLI agent client";
262
264
  // These commands act as a peer agent, so they take the agent credential: never the
263
265
  // persisted admin token, which the hub accepts for any project without its own token.
264
- const authToken = resolveAgentHubAuthToken(runtime.env, project);
265
- if (!authToken) throw new AgentProjectTokenMissingError(project);
266
+ const authToken = resolveAgentHubAuthToken(runtime.env, project, runtime.cwd);
267
+ if (!authToken) throw new AgentProjectTokenMissingError(identity);
266
268
  const client = new HubClient({
267
269
  serverUrl,
268
270
  name,
@@ -387,11 +389,38 @@ async function dispatchAgentCliCommand(
387
389
  print(
388
390
  runtime.io,
389
391
  runtime.json,
390
- { ok: false, error: error.code, project: error.project, nextAction: "export_kxm_auth_token", detail: message },
392
+ {
393
+ ok: false,
394
+ error: error.code,
395
+ project: error.project,
396
+ projectSource: error.projectSource,
397
+ nextAction: "configure_hub_project",
398
+ hint: error.hint,
399
+ detail: message,
400
+ },
391
401
  message,
392
402
  );
393
403
  return 2;
394
404
  }
405
+ if (error instanceof HubHttpError && error.code === "project_token_missing") {
406
+ const identity = resolveProjectIdentity(runtime.cwd, runtime.env);
407
+ const missing = new AgentProjectTokenMissingError(identity);
408
+ print(
409
+ runtime.io,
410
+ runtime.json,
411
+ {
412
+ ok: false,
413
+ error: missing.code,
414
+ project: identity.project,
415
+ projectSource: identity.source,
416
+ nextAction: "configure_hub_project",
417
+ hint: missing.hint,
418
+ detail: missing.message,
419
+ },
420
+ missing.message,
421
+ );
422
+ return 2;
423
+ }
395
424
  if (error instanceof CloudTokenError) {
396
425
  print(
397
426
  runtime.io,
@@ -1344,7 +1373,8 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
1344
1373
  .option("--cloud", "Mark the hub remote even when the URL is a loopback forward")
1345
1374
  .option("--token-env <name>", "Environment variable that holds the hub token; the token is not stored")
1346
1375
  .option("--token-command <command>", "Program that prints the hub token; the token is not stored")
1347
- .action(async function hubBindAction(this: Command, url: string, options: { cloud?: boolean; tokenEnv?: string; tokenCommand?: string }) {
1376
+ .option("--key-op <ref>", "op:// reference for the hub token; resolved with op read, never stored")
1377
+ .action(async function hubBindAction(this: Command, url: string, options: { cloud?: boolean; tokenEnv?: string; tokenCommand?: string; keyOp?: string }) {
1348
1378
  result.code = await cmdHubBind(runtimeFrom(ctx, this), url, options);
1349
1379
  });
1350
1380
  addGlobalOptions(hub.command("unbind").description("Remove this machine's hub binding")).action(bind(cmdHubUnbind));
@@ -1562,6 +1592,10 @@ export async function runCli(
1562
1592
  } catch (error) {
1563
1593
  if (error instanceof CommanderError) return mapCommanderError(error, argv, program, io);
1564
1594
  if (error instanceof DryRunRefused) return error.code;
1595
+ if (error instanceof KxmHubConfigError) {
1596
+ print(io, hasJsonFlag(argv), { ok: false, error: error.code, detail: error.message }, error.message);
1597
+ return 2;
1598
+ }
1565
1599
  throw error;
1566
1600
  }
1567
1601
  }