@kontextmind/kxm 0.7.163 → 0.7.165

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 (38) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +2 -0
  4. package/docs/README.md +1 -0
  5. package/docs/architecture/inventory.md +4 -3
  6. package/docs/contributing/harness-routing-internals.md +28 -27
  7. package/docs/glossary.md +3 -3
  8. package/docs/operations/troubleshooting.md +3 -0
  9. package/docs/operations.md +154 -0
  10. package/docs/reference/cli-reference.md +4 -3
  11. package/docs/reference/configuration.md +11 -1
  12. package/docs/reference/harness-routing.md +17 -15
  13. package/package.json +1 -1
  14. package/plugins/kxm/.claude-plugin/plugin.json +2 -2
  15. package/plugins/kxm/README.md +2 -2
  16. package/plugins/kxm/dist/claude-hook.js +120 -16
  17. package/plugins/kxm/dist/cli.js +14929 -14542
  18. package/plugins/kxm/dist/extension.js +299 -122
  19. package/plugins/kxm/dist/mcp-server.js +7690 -106
  20. package/plugins/kxm/dist/runtime-supervisor.js +151 -4
  21. package/plugins/kxm/dist/runtime.js +158 -11
  22. package/plugins/kxm/package.json +1 -1
  23. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +5 -1
  24. package/plugins/kxm/src/cli/context-skills.ts +64 -10
  25. package/plugins/kxm/src/cli/hub.ts +128 -17
  26. package/plugins/kxm/src/cli/project.ts +7 -3
  27. package/plugins/kxm/src/cli/types.ts +6 -0
  28. package/plugins/kxm/src/cli/workflows.ts +12 -1
  29. package/plugins/kxm/src/cli.ts +23 -5
  30. package/plugins/kxm/src/extension.ts +7 -3
  31. package/plugins/kxm/src/hub-autostart.ts +3 -0
  32. package/plugins/kxm/src/hub-binding.ts +182 -2
  33. package/plugins/kxm/src/hub-env.ts +71 -0
  34. package/plugins/kxm/src/mcp-server.ts +3 -2
  35. package/plugins/kxm/src/project-name.ts +34 -7
  36. package/plugins/kxm/src/runtime-supervisor.ts +9 -3
  37. package/plugins/kxm/src/tenant-status.ts +9 -2
  38. package/scripts/build-runtime.mjs +5 -0
@@ -3,20 +3,26 @@ 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 { hasClientHubCredential, resolveClientHubAuthToken } from "../hub-env.ts";
6
+ import { agentSessionEnv, hasClientHubCredential, HubEnvError, resolveClientHubAuthToken } from "../hub-env.ts";
7
7
  import { agentWorker, type Worker } from "../envelope.ts";
8
8
  import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "../tui.ts";
9
9
  import { formatSessionBriefText, loadSessionBriefAsync, readCachedSessionBrief, type SessionHubStatus } from "../session-work.ts";
10
10
  import {
11
11
  HUB_BINDING_SCHEMA,
12
+ CloudTokenError,
12
13
  HubBindingError,
13
14
  hubBindingFile,
14
15
  probeHubHealth,
15
16
  hubBindingScope,
17
+ effectiveHubBindingScope,
18
+ isCloudTokenEnvName,
16
19
  readHubBinding,
17
20
  removeHubBinding,
21
+ splitTokenCommand,
18
22
  validateHubUrl,
19
23
  writeHubBinding,
24
+ resolveCloudHubToken,
25
+ type HubBindingRecord,
20
26
  type HubHealth,
21
27
  } from "../hub-binding.ts";
22
28
  import {
@@ -57,9 +63,13 @@ import {
57
63
  type Runtime,
58
64
  } from "./types.ts";
59
65
 
60
- export async function hubGet(url: string, fetchImpl: typeof fetch): Promise<{ ok: boolean; status: number; body: unknown }> {
66
+ export async function hubGet(
67
+ url: string,
68
+ fetchImpl: typeof fetch,
69
+ headers?: Record<string, string>,
70
+ ): Promise<{ ok: boolean; status: number; body: unknown }> {
61
71
  try {
62
- const response = await fetchImpl(url);
72
+ const response = await fetchImpl(url, headers ? { headers } : undefined);
63
73
  const text = redactSecrets((await response.text()).slice(0, 8_000));
64
74
  let body: unknown = text;
65
75
  try {
@@ -147,14 +157,29 @@ export async function refreshKxmUpdateNotice(
147
157
  }
148
158
 
149
159
  export async function cmdStatus(runtime: Runtime): Promise<number> {
150
- const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl);
151
- const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl);
160
+ let headers: Record<string, string> | undefined;
161
+ try {
162
+ const binding = readHubBinding(runtime.env);
163
+ if (binding?.cloud) {
164
+ headers = { authorization: `Bearer ${resolveCloudHubToken(binding, runtime.env)}` };
165
+ }
166
+ } catch (error) {
167
+ if (error instanceof CloudTokenError) {
168
+ print(runtime.io, runtime.json, { ok: false, command: "hub view", error: error.code, detail: error.message }, error.message);
169
+ return 2;
170
+ }
171
+ if (!(error instanceof HubBindingError)) throw error;
172
+ }
173
+ const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl, headers);
174
+ const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl, headers);
152
175
  // Scope on the status line deliberately: "attached across a network" and "attached on
153
176
  // this box" are otherwise indistinguishable, and only one of them ships a token.
154
177
  // Scope is a property of the URL actually contacted, not of whichever file the
155
178
  // binding came from: KXM_SERVER_URL overrides the binding, and labelling the binding
156
179
  // while probing an override would report "loopback" about a remote request.
157
- const effectiveScope = hubBindingScope(runtime.serverUrl);
180
+ // A cloud binding is remote when the contacted URL is that binding, including a
181
+ // loopback SSH forward.
182
+ const effectiveScope = effectiveHubBindingScope(runtime.serverUrl, runtime.env);
158
183
  const overridden = Boolean(runtime.boundHubUrl && runtime.boundHubUrl !== runtime.serverUrl);
159
184
  const payload = {
160
185
  ok: health.ok && ready.ok,
@@ -192,7 +217,16 @@ export async function cmdDash(runtime: Runtime, options: { screen?: string | und
192
217
  return 2;
193
218
  }
194
219
  const project = defaultProjectName(runtime.dirs.workdir, runtime.env) || "project";
195
- const authToken = resolveClientHubAuthToken(runtime.env, project);
220
+ let authToken: string | undefined;
221
+ try {
222
+ authToken = resolveClientHubAuthToken(runtime.env, project);
223
+ } catch (error) {
224
+ if (error instanceof CloudTokenError) {
225
+ print(runtime.io, runtime.json, { ok: false, command: "dash", error: error.code, detail: error.message }, error.message);
226
+ return 2;
227
+ }
228
+ throw error;
229
+ }
196
230
  return await runMeshTui({
197
231
  serverUrl: runtime.serverUrl,
198
232
  dataPath,
@@ -254,7 +288,13 @@ export function formatHubBindHealth(health: HubHealth): string {
254
288
  const HUB_BIND_UNAUTHENTICATED_HINT =
255
289
  "export KXM_AUTH_TOKEN (or point KXM_STATE_HOME at the hub-env record that already holds one), then re-run; the hub itself requires a token beyond loopback";
256
290
 
257
- export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<number> {
291
+ export interface HubBindOptions {
292
+ cloud?: boolean | undefined;
293
+ tokenEnv?: string | undefined;
294
+ tokenCommand?: string | undefined;
295
+ }
296
+
297
+ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubBindOptions = {}): Promise<number> {
258
298
  let url: string;
259
299
  try {
260
300
  url = validateHubUrl(rawUrl);
@@ -270,7 +310,51 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<numb
270
310
  }
271
311
  throw error;
272
312
  }
273
- const scope = hubBindingScope(url);
313
+ const cloud = options.cloud === true;
314
+ const tokenEnv = options.tokenEnv?.trim() || undefined;
315
+ const tokenCommand = options.tokenCommand?.trim() || undefined;
316
+ if ((tokenEnv || tokenCommand) && !cloud) {
317
+ print(
318
+ runtime.io,
319
+ runtime.json,
320
+ { ok: false, command: "hub bind", error: "cloud_flag_required", url },
321
+ "--token-env and --token-command require --cloud; no binding was written",
322
+ );
323
+ return 2;
324
+ }
325
+ if (cloud && !tokenEnv && !tokenCommand) {
326
+ print(
327
+ runtime.io,
328
+ runtime.json,
329
+ { 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",
331
+ );
332
+ return 2;
333
+ }
334
+ if (cloud && tokenEnv && !isCloudTokenEnvName(tokenEnv)) {
335
+ print(
336
+ runtime.io,
337
+ runtime.json,
338
+ { ok: false, command: "hub bind", error: "cloud_token_env_invalid", url },
339
+ "--token-env must be an environment variable name; no binding was written",
340
+ );
341
+ return 2;
342
+ }
343
+ if (cloud && tokenCommand) {
344
+ try {
345
+ splitTokenCommand(tokenCommand);
346
+ } catch (error) {
347
+ const detail = error instanceof Error ? error.message : String(error);
348
+ print(
349
+ runtime.io,
350
+ runtime.json,
351
+ { ok: false, command: "hub bind", error: "cloud_token_command_invalid", url, detail },
352
+ `${detail}; no binding was written`,
353
+ );
354
+ return 2;
355
+ }
356
+ }
357
+ const scope = cloud ? "remote" : hubBindingScope(url);
274
358
  // Scope-scoped on purpose, and only for **this command**. A remote binding puts a
275
359
  // bearer on a network path, so it is refused when nothing can authenticate it; a
276
360
  // stored-but-unusable URL otherwise reads later like a network fault and gets debugged
@@ -280,7 +364,7 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<numb
280
364
  // fail `kxm peer list` on loopback. Narrowing the claim is the point; the first version
281
365
  // of this guard checked the record *before* the scope and refused loopback binds that had
282
366
  // always worked, which is a regression against behaviour predating this slice.
283
- if (scope === "remote") {
367
+ if (!cloud && scope === "remote") {
284
368
  // The project that will actually authenticate: a record holding only another
285
369
  // project's token cannot authorise this one.
286
370
  const bindProject = defaultProjectName(runtime.dirs.workdir, runtime.env) || "project";
@@ -329,14 +413,29 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<numb
329
413
  }
330
414
  }
331
415
  const file = hubBindingFile(runtime.env);
416
+ const cloudFields = cloud
417
+ ? { cloud: true as const, ...(tokenEnv ? { tokenEnv } : {}), ...(tokenCommand ? { tokenCommand } : {}) }
418
+ : {};
419
+ const record: HubBindingRecord = {
420
+ schema: HUB_BINDING_SCHEMA,
421
+ url,
422
+ boundAt: new Date().toISOString(),
423
+ ...cloudFields,
424
+ };
425
+ const cloudNote = cloud ? " · token is not stored" : "";
332
426
  if (runtime.dryRun) {
333
- print(runtime.io, runtime.json, { ok: true, command: "hub bind", dryRun: true, url, scope, file }, `would bind hub ${url} (${scope})`);
427
+ print(
428
+ runtime.io,
429
+ runtime.json,
430
+ { ok: true, command: "hub bind", dryRun: true, url, scope, file, ...cloudFields },
431
+ `would bind hub ${url} (${scope})${cloudNote}`,
432
+ );
334
433
  return 0;
335
434
  }
336
- writeHubBinding({ schema: HUB_BINDING_SCHEMA, url, boundAt: new Date().toISOString() }, runtime.env);
435
+ writeHubBinding({ ...record, boundAt: new Date().toISOString() }, runtime.env);
337
436
  const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
338
- print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, health, probeMs },
339
- `bound hub ${url} · ${scope} · ${formatHubBindHealth(health)}${scope === "remote" ? " · token leaves this machine" : ""}`);
437
+ print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, health, probeMs, ...cloudFields },
438
+ `bound hub ${url} · ${scope} · ${formatHubBindHealth(health)}${cloud ? cloudNote : scope === "remote" ? " · token leaves this machine" : ""}`);
340
439
  return 0;
341
440
  }
342
441
 
@@ -383,7 +482,7 @@ export async function cmdWorker(runtime: Runtime, options: {
383
482
  freshStart?: boolean | undefined;
384
483
  }): Promise<number> {
385
484
  const name = options.name?.trim() || runtime.env.KXM_AGENT_NAME?.trim();
386
- const project = options.project?.trim() || runtime.env.KXM_PROJECT?.trim();
485
+ const project = defaultProjectName(runtime.dirs.workdir, runtime.env, options.project);
387
486
  const model = options.model?.trim() || runtime.env.KXM_WORKER_MODEL?.trim();
388
487
  const fallbackModels = options.fallbackModels?.trim() || runtime.env.KXM_WORKER_FALLBACK_MODELS?.trim();
389
488
  const tools = options.tools?.trim() || runtime.env.KXM_WORKER_TOOLS?.trim();
@@ -392,10 +491,22 @@ export async function cmdWorker(runtime: Runtime, options: {
392
491
  runtime.io.stderr("worker --session-isolation must be workflow or off\n");
393
492
  return 2;
394
493
  }
494
+ let sessionEnv: NodeJS.ProcessEnv = { KXM_PROJECT: project };
495
+ if (!runtime.dryRun) {
496
+ try {
497
+ sessionEnv = agentSessionEnv(runtime.dirs.workdir, runtime.env, options.project);
498
+ } catch (error) {
499
+ if (error instanceof CloudTokenError || error instanceof HubEnvError) {
500
+ runtime.io.stderr(`${error.message}\n`);
501
+ return 2;
502
+ }
503
+ throw error;
504
+ }
505
+ }
395
506
  const extraEnv = {
396
507
  ...workspaceEnv(runtime),
508
+ ...sessionEnv,
397
509
  ...(name ? { KXM_AGENT_NAME: name } : {}),
398
- ...(project ? { KXM_PROJECT: project } : {}),
399
510
  ...(model ? { KXM_WORKER_MODEL: model } : {}),
400
511
  ...(fallbackModels ? { KXM_WORKER_FALLBACK_MODELS: fallbackModels } : {}),
401
512
  ...(tools ? { KXM_WORKER_TOOLS: tools } : {}),
@@ -675,7 +786,7 @@ export async function cmdSessionBrief(runtime: Runtime, options: { status?: bool
675
786
  evidence: health === "unknown" ? "timeout" : "probed",
676
787
  online: health === "on",
677
788
  url: targetUrl,
678
- scope: hubBindingScope(targetUrl),
789
+ scope: effectiveHubBindingScope(targetUrl, runtime.env),
679
790
  };
680
791
  } else {
681
792
  hub = { state: "off", evidence: "unconfigured", online: false };
@@ -29,6 +29,8 @@ import {
29
29
  import { kxmRuntimePaths, runtimeError } from "../runtime-store.ts";
30
30
  import { assembleTenantStatus, formatTenantStatus } from "../tenant-status.ts";
31
31
  import { resolveClientAdminAuthToken } from "../hub-env.ts";
32
+ import { defaultProjectName } from "../project-name.ts";
33
+ import { effectiveHubBindingScope } from "../hub-binding.ts";
32
34
  import {
33
35
  formatHarnessInventory,
34
36
  probeHarnessesAsync,
@@ -779,7 +781,7 @@ export async function cmdKxmRunList(runtime: Runtime): Promise<number> {
779
781
  }
780
782
  }
781
783
 
782
- export async function cmdTenantStatus(runtime: Runtime): Promise<number> {
784
+ export async function cmdTenantStatus(runtime: Runtime, options: { project?: string | undefined } = {}): Promise<number> {
783
785
  let projectRoot: string | undefined;
784
786
  let projectId: string;
785
787
  try {
@@ -789,7 +791,8 @@ export async function cmdTenantStatus(runtime: Runtime): Promise<number> {
789
791
  return 1;
790
792
  }
791
793
  const bundle = loadKxmProject(projectRoot, {});
792
- projectId = String(bundle.project.value.id);
794
+ const yamlProjectId = String(bundle.project.value.id);
795
+ projectId = defaultProjectName(runtime.cwd, runtime.env, options.project);
793
796
  const root = projectRoot;
794
797
 
795
798
  const payload = await assembleTenantStatus({
@@ -799,6 +802,7 @@ export async function cmdTenantStatus(runtime: Runtime): Promise<number> {
799
802
  // and resolve it inside the hub source so a malformed persisted record degrades that
800
803
  // one source instead of aborting the Runtime read with it.
801
804
  resolveAdminToken: () => resolveClientAdminAuthToken(runtime.env),
805
+ bindingScope: effectiveHubBindingScope(runtime.serverUrl, runtime.env),
802
806
  fetchImpl: runtime.fetchImpl,
803
807
  runtime: {
804
808
  listRuns: async () => {
@@ -808,7 +812,7 @@ export async function cmdTenantStatus(runtime: Runtime): Promise<number> {
808
812
  if (!handle) {
809
813
  throw runtimeError("runtime_supervisor_not_running", "runtime", "no live runtime supervisor on this box");
810
814
  }
811
- const result = await kxmRuntimeRequest(handle, "GET", `/v1/projects/${encodeURIComponent(projectId)}/runs?projectRoot=${encodeURIComponent(root)}`);
815
+ const result = await kxmRuntimeRequest(handle, "GET", `/v1/projects/${encodeURIComponent(yamlProjectId)}/runs?projectRoot=${encodeURIComponent(root)}`);
812
816
  const runs = (result.runs ?? []) as Array<Record<string, unknown>>;
813
817
  return runs.map((run) => ({
814
818
  runId: String(run.runId ?? ""),
@@ -8,6 +8,7 @@ import { gateWorker, workerResult, type Worker, type WorkerOutcome } from "../en
8
8
  import { canonicalWorkflowEvidenceKey, type WorkflowEvidenceInput } from "../workflow.ts";
9
9
  import {
10
10
  HubBindingError,
11
+ effectiveHubBindingScope,
11
12
  hubBindingFile,
12
13
  readHubBinding,
13
14
  } from "../hub-binding.ts";
@@ -165,6 +166,11 @@ export function printWorker(
165
166
  }
166
167
 
167
168
  export function hostMode(runtime: Runtime): "local" | "hub" {
169
+ try {
170
+ if (effectiveHubBindingScope(runtime.serverUrl, runtime.env) === "remote") return "hub";
171
+ } catch {
172
+ // A malformed binding is classified from the URL below.
173
+ }
168
174
  try {
169
175
  const hostname = new URL(runtime.serverUrl).hostname;
170
176
  if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") return "local";
@@ -37,6 +37,8 @@ import {
37
37
  type Runtime,
38
38
  } from "./types.ts";
39
39
  import { resolvePickItem, type PickCandidate } from "./roles.ts";
40
+ import { CloudTokenError } from "../hub-binding.ts";
41
+ import { resolveControlPlaneAuthToken } from "../hub-env.ts";
40
42
 
41
43
  const CLI_NAME = "kxm";
42
44
 
@@ -480,7 +482,16 @@ export async function cmdWorkflowStart(runtime: Runtime, definitionIdArg: string
480
482
  export async function cmdWorkflowDegrade(runtime: Runtime, runId: string, stageId: string, options: { requirement?: string | undefined; reason?: string | undefined }): Promise<number> {
481
483
  const requirementKey = options.requirement?.trim() ?? "";
482
484
  const reason = options.reason?.trim() ?? "";
483
- const adminToken = runtime.env.KXM_AUTH_TOKEN?.trim();
485
+ let adminToken: string | undefined;
486
+ try {
487
+ adminToken = resolveControlPlaneAuthToken(runtime.env);
488
+ } catch (error) {
489
+ if (error instanceof CloudTokenError) {
490
+ runtime.io.stderr(`${error.message}\n`);
491
+ return 2;
492
+ }
493
+ throw error;
494
+ }
484
495
  if (!runId || !stageId || !requirementKey || !reason || !adminToken) {
485
496
  runtime.io.stderr("workflow degrade requires <runId> <stageId>, --requirement, --reason, and KXM_AUTH_TOKEN\n");
486
497
  return 2;
@@ -19,6 +19,7 @@ import { readInstalledKxmVersion } from "./kxm-update.ts";
19
19
  import { findKxmRepoRoot } from "./repo-root.ts";
20
20
  import { HubClient } from "./client.ts";
21
21
  import { AgentProjectTokenMissingError, resolveAgentHubAuthToken } from "./hub-env.ts";
22
+ import { CloudTokenError } from "./hub-binding.ts";
22
23
  import { defaultProjectName } from "./project-name.ts";
23
24
  import {
24
25
  AGENT_COMMANDS_MAP,
@@ -310,6 +311,9 @@ async function dispatchAgentCliCommand(
310
311
  return 2;
311
312
  }
312
313
 
314
+ const explicitProject = typeof rawArgs.project === "string" ? rawArgs.project.trim() : "";
315
+ if (explicitProject) runtime = { ...runtime, env: { ...runtime.env, KXM_PROJECT: explicitProject } };
316
+
313
317
  let args: Record<string, unknown> = {};
314
318
  if (typeof rawArgs.payload === "string" && rawArgs.payload.trim()) {
315
319
  try {
@@ -324,7 +328,7 @@ async function dispatchAgentCliCommand(
324
328
  }
325
329
 
326
330
  for (const [key, val] of Object.entries(rawArgs)) {
327
- if (val !== undefined && key !== "payload") {
331
+ if (val !== undefined && key !== "payload" && key !== "project") {
328
332
  if (key === "timeoutMs" || key === "ttlMs" || key === "attempt") {
329
333
  args[key] = Number(val);
330
334
  } else if (key === "targets" && typeof val === "string") {
@@ -388,6 +392,15 @@ async function dispatchAgentCliCommand(
388
392
  );
389
393
  return 2;
390
394
  }
395
+ if (error instanceof CloudTokenError) {
396
+ print(
397
+ runtime.io,
398
+ runtime.json,
399
+ { ok: false, error: error.code, detail: message },
400
+ message,
401
+ );
402
+ return 2;
403
+ }
391
404
  print(runtime.io, runtime.json, { ok: false, error: "command_failed", detail: message }, `command failed: ${message}`);
392
405
  return 1;
393
406
  } finally {
@@ -649,8 +662,9 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
649
662
  const tenantCmd = addGlobalOptions(program.command("tenant").description("Composed tenant reads for machine clients (portal)"));
650
663
  tenantCmd.helpCommand("help", "Show tenant help");
651
664
  addGlobalOptions(tenantCmd.command("status").description("Read hub metadata and authoritative Runtime run state as one labeled view"))
652
- .action(async function tenantStatusAction(this: Command) {
653
- result.code = await cmdTenantStatus(runtimeFrom(ctx, this));
665
+ .option("--project <id>", "Hub project; overrides KXM_PROJECT for this command")
666
+ .action(async function tenantStatusAction(this: Command, options: { project?: string }) {
667
+ result.code = await cmdTenantStatus(runtimeFrom(ctx, this), options);
654
668
  });
655
669
 
656
670
  const pricesCmd = addGlobalOptions(program.command("prices").description("Stamp the local list-price catalog. Estimates stay unknown until today's stamp"));
@@ -816,6 +830,7 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
816
830
  peer.helpCommand("help", "Show peer help");
817
831
 
818
832
  addGlobalOptions(peer.command("list").description("List peer agents in this project's hub pool with host and presence"))
833
+ .option("--project <id>", "Hub project; overrides KXM_PROJECT for this command")
819
834
  .option("--include-offline", "Also list registered peers whose hub lease has expired")
820
835
  .option("--payload <json>", "JSON payload")
821
836
  .action(async function peerListAction(this: Command, opts?: Record<string, unknown>) {
@@ -1326,8 +1341,11 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
1326
1341
  result.code = await cmdStop(runtimeFrom(ctx, this), options.waitMs);
1327
1342
  });
1328
1343
  addGlobalOptions(hub.command("bind").description("Bind this machine to a running hub").argument("<url>", "Hub base URL (http or https)"))
1329
- .action(async function hubBindAction(this: Command, url: string) {
1330
- result.code = await cmdHubBind(runtimeFrom(ctx, this), url);
1344
+ .option("--cloud", "Mark the hub remote even when the URL is a loopback forward")
1345
+ .option("--token-env <name>", "Environment variable that holds the hub token; the token is not stored")
1346
+ .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 }) {
1348
+ result.code = await cmdHubBind(runtimeFrom(ctx, this), url, options);
1331
1349
  });
1332
1350
  addGlobalOptions(hub.command("unbind").description("Remove this machine's hub binding")).action(bind(cmdHubUnbind));
1333
1351
  addGlobalOptions(program.command("dash").description("Live screens for headless agents, tasks, workflows, and plans")
@@ -7,6 +7,7 @@ import { HubClient, HubHttpError } from "./client.ts";
7
7
  import { loadKxmConfig } from "./config.ts";
8
8
  import { ensureHubRunning, hubAutoStartMode } from "./hub-autostart.ts";
9
9
  import { AgentProjectTokenMissingError, resolveAgentHubAuthToken } from "./hub-env.ts";
10
+ import { CloudTokenError, resolveHubServerUrl } from "./hub-binding.ts";
10
11
  import { defaultProjectName } from "./project-name.ts";
11
12
  import { nousFactoryWork, type NousRegistrationReport } from "./nous-pi.ts";
12
13
  import {
@@ -346,7 +347,7 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
346
347
  }
347
348
 
348
349
  async function showKxmHub(ctx: { ui: { notify: (message: string, type: "info" | "warning" | "error") => void } }): Promise<void> {
349
- const url = (process.env.KXM_SERVER_URL ?? "http://127.0.0.1:7331").replace(/\/$/, "");
350
+ const url = resolveHubServerUrl(process.env);
350
351
  let health = "unreachable";
351
352
  try {
352
353
  const response = await fetch(`${url}/health`);
@@ -676,7 +677,7 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
676
677
  return;
677
678
  }
678
679
  currentPiSessionId = ctx.sessionManager?.getSessionId();
679
- const serverUrl = process.env.KXM_SERVER_URL ?? "http://127.0.0.1:7331";
680
+ const serverUrl = resolveHubServerUrl(process.env);
680
681
  const project = defaultProjectName(ctx.cwd, process.env);
681
682
  const name = process.env.KXM_AGENT_NAME ?? pi.getSessionName() ?? `pi-${process.pid}`;
682
683
  agentName = name;
@@ -710,7 +711,10 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
710
711
  try {
711
712
  hubAuthToken = resolveAgentHubAuthToken(process.env, project);
712
713
  } catch (error) {
713
- ctx.ui.notify(`kxm could not read persisted hub credentials: ${error instanceof Error ? error.message : String(error)}`, "error");
714
+ const detail = error instanceof CloudTokenError
715
+ ? error.message
716
+ : `kxm could not read persisted hub credentials: ${error instanceof Error ? error.message : String(error)}`;
717
+ ctx.ui.notify(detail, "error");
714
718
  await applySessionChrome(ctx, event, true);
715
719
  return;
716
720
  }
@@ -133,6 +133,9 @@ export async function ensureHubRunning(options: EnsureHubOptions): Promise<Ensur
133
133
  if (binding) {
134
134
  const { health } = await probeHubHealth(binding.url, fetchImpl);
135
135
  if (health === "on") return { status: "bound-healthy", url: binding.url };
136
+ // A cloud binding names a remote hub. A dead forward must not start a local hub
137
+ // that would then authenticate with this machine's hub-env token.
138
+ if (binding.cloud) return { status: "failed", reason: "cloud_hub_unreachable", logPath: "" };
136
139
  }
137
140
 
138
141
  // 2. A live local claim means a hub (started any way) already owns this