@kontextmind/kxm 0.7.167 → 0.7.169

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 +30 -0
  3. package/docs/contributing/ci-and-release.md +6 -1
  4. package/docs/operations/runtime-sync.md +2 -1
  5. package/docs/operations/troubleshooting.md +4 -0
  6. package/docs/operations/upgrade.md +18 -3
  7. package/docs/operations.md +70 -7
  8. package/docs/reference/cli-reference.md +21 -6
  9. package/docs/reference/config-reference.md +28 -13
  10. package/docs/start/install.md +1 -1
  11. package/package.json +1 -1
  12. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  13. package/plugins/kxm/dist/claude-hook.js +170 -36
  14. package/plugins/kxm/dist/cli.js +451 -132
  15. package/plugins/kxm/dist/extension.js +184 -53
  16. package/plugins/kxm/dist/mcp-server.js +97 -40
  17. package/plugins/kxm/dist/runtime-supervisor.js +240 -47
  18. package/plugins/kxm/dist/runtime.js +240 -47
  19. package/plugins/kxm/dist/server.js +9 -0
  20. package/plugins/kxm/package.json +1 -1
  21. package/plugins/kxm/skills/kxm-definitions/SKILL.md +3 -2
  22. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +4 -2
  23. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -4
  24. package/plugins/kxm/src/cli/hub.ts +42 -7
  25. package/plugins/kxm/src/cli/system.ts +3 -1
  26. package/plugins/kxm/src/cli/types.ts +5 -3
  27. package/plugins/kxm/src/cli.ts +2 -1
  28. package/plugins/kxm/src/engine.ts +105 -8
  29. package/plugins/kxm/src/hub-binding.ts +301 -55
  30. package/plugins/kxm/src/hub-identity.ts +17 -9
  31. package/plugins/kxm/src/kxm-update.ts +29 -6
  32. package/plugins/kxm/src/mcp-server.ts +1 -1
  33. package/plugins/kxm/src/project-config.ts +63 -1
  34. package/plugins/kxm/src/runtime-supervisor.ts +22 -4
  35. package/scripts/kxm-publish-npm.mjs +47 -1
@@ -9409,7 +9409,7 @@ var init_config = __esm({
9409
9409
  });
9410
9410
 
9411
9411
  // plugins/kxm/src/hub-binding.ts
9412
- import { spawnSync } from "node:child_process";
9412
+ import { spawnSync as spawnSync2 } from "node:child_process";
9413
9413
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
9414
9414
  import { homedir as homedir2 } from "node:os";
9415
9415
  import { dirname as dirname3, isAbsolute, join as join4, resolve as resolve2 } from "node:path";
@@ -9432,6 +9432,9 @@ function resolveUserStateRoot(env) {
9432
9432
  function hubBindingFile(env = process.env) {
9433
9433
  return join4(resolveUserStateRoot(env), "hub-binding.json");
9434
9434
  }
9435
+ function hubBindingCloudFile(env = process.env) {
9436
+ return join4(resolveUserStateRoot(env), "hub-binding.cloud.json");
9437
+ }
9435
9438
  function validateHubUrl(raw) {
9436
9439
  let parsed;
9437
9440
  try {
@@ -9517,7 +9520,7 @@ function resolveCloudHubToken(binding, env = process.env) {
9517
9520
  function runTokenCommand(command, env) {
9518
9521
  const argv = splitTokenCommand(command);
9519
9522
  const [program2, ...args] = argv;
9520
- const result = spawnSync(program2, args, {
9523
+ const result = spawnSync2(program2, args, {
9521
9524
  encoding: "utf8",
9522
9525
  shell: false,
9523
9526
  timeout: TOKEN_COMMAND_TIMEOUT_MS,
@@ -9560,33 +9563,35 @@ function isAbortError(error) {
9560
9563
  error && typeof error === "object" && ("name" in error && error.name === "AbortError" || "code" in error && error.code === "ABORT_ERR")
9561
9564
  );
9562
9565
  }
9563
- function readHubBinding(env = process.env) {
9564
- const file = hubBindingFile(env);
9565
- if (!existsSync4(file)) return void 0;
9566
+ function bindingFallbackWarning(file, reason) {
9567
+ return `hub binding at ${file} ${reason}; falling back to the local hub. Restart the Runtime supervisor, the hub, and workers after upgrading and before a cloud bind.`;
9568
+ }
9569
+ function writeBindingJson(file, value) {
9570
+ mkdirSync3(dirname3(file), { recursive: true, mode: 448 });
9571
+ const temporary = join4(dirname3(file), `.${file.endsWith("cloud.json") ? "hub-binding-cloud" : "hub-binding"}-${process.pid}.tmp`);
9572
+ try {
9573
+ writeFileSync3(temporary, `${JSON.stringify(value, null, 2)}
9574
+ `, { encoding: "utf8", mode: 384 });
9575
+ renameSync(temporary, file);
9576
+ } finally {
9577
+ rmSync(temporary, { force: true });
9578
+ }
9579
+ }
9580
+ function parseBindingObject(file) {
9581
+ if (!existsSync4(file)) return new HubBindingError(`missing hub binding at ${file}`);
9566
9582
  let parsed;
9567
9583
  try {
9568
9584
  parsed = JSON.parse(readFileSync3(file, "utf8"));
9569
9585
  } catch {
9570
- throw new HubBindingError(`malformed hub binding at ${file}`);
9586
+ return new HubBindingError(`malformed hub binding at ${file}`);
9571
9587
  }
9572
9588
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9573
- throw new HubBindingError(`malformed hub binding at ${file}`);
9574
- }
9575
- const row = parsed;
9576
- const keys = Object.keys(row);
9577
- const allowed = /* @__PURE__ */ new Set(["schema", "url", "boundAt", "cloud", "tokenEnv", "tokenCommand"]);
9578
- const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
9579
- if (keys.some((key) => !allowed.has(key)) || !cloudish && keys.length !== 3 || row.schema !== HUB_BINDING_SCHEMA || typeof row.url !== "string" || typeof row.boundAt !== "string" || !isIsoTimestamp(row.boundAt)) {
9580
- throw new HubBindingError(`malformed hub binding at ${file}`);
9581
- }
9582
- let url;
9583
- try {
9584
- url = validateHubUrl(row.url);
9585
- } catch {
9586
- throw new HubBindingError(`malformed hub binding at ${file}`);
9589
+ return new HubBindingError(`malformed hub binding at ${file}`);
9587
9590
  }
9588
- if (!cloudish) return { schema: HUB_BINDING_SCHEMA, url, boundAt: row.boundAt };
9589
- if (row.cloud !== true) throw new HubBindingError(`malformed hub binding at ${file}`);
9591
+ return parsed;
9592
+ }
9593
+ function cloudRecordFromRow(file, row, url, boundAt) {
9594
+ if (row.cloud !== true) return void 0;
9590
9595
  let tokenEnv;
9591
9596
  let tokenCommand;
9592
9597
  if (row.tokenEnv !== void 0) {
@@ -9608,33 +9613,203 @@ function readHubBinding(env = process.env) {
9608
9613
  return {
9609
9614
  schema: HUB_BINDING_SCHEMA,
9610
9615
  url,
9611
- boundAt: row.boundAt,
9616
+ boundAt,
9612
9617
  cloud: true,
9613
9618
  ...tokenEnv ? { tokenEnv } : {},
9614
9619
  ...tokenCommand ? { tokenCommand } : {}
9615
9620
  };
9616
9621
  }
9617
- function writeHubBinding(record, env = process.env) {
9622
+ function readLegacyV1HubBinding(env = process.env) {
9618
9623
  const file = hubBindingFile(env);
9619
- mkdirSync3(dirname3(file), { recursive: true, mode: 448 });
9620
- const temporary = join4(dirname3(file), `.hub-binding-${process.pid}.tmp`);
9624
+ if (!existsSync4(file)) return void 0;
9625
+ const parsed = parseBindingObject(file);
9626
+ if (parsed instanceof HubBindingError) throw parsed;
9627
+ const keys = Object.keys(parsed);
9628
+ if (keys.length !== 3 || keys.some((key) => !LEGACY_BINDING_KEYS.includes(key)) || parsed.schema !== HUB_BINDING_SCHEMA || typeof parsed.url !== "string" || typeof parsed.boundAt !== "string" || !isIsoTimestamp(parsed.boundAt)) {
9629
+ throw new HubBindingError(`malformed hub binding at ${file}`);
9630
+ }
9631
+ let url;
9621
9632
  try {
9622
- writeFileSync3(temporary, `${JSON.stringify(record, null, 2)}
9623
- `, { encoding: "utf8", mode: 384 });
9624
- renameSync(temporary, file);
9625
- } finally {
9626
- rmSync(temporary, { force: true });
9633
+ url = validateHubUrl(parsed.url);
9634
+ } catch {
9635
+ throw new HubBindingError(`malformed hub binding at ${file}`);
9636
+ }
9637
+ return { schema: HUB_BINDING_SCHEMA, url, boundAt: parsed.boundAt };
9638
+ }
9639
+ function interpretBindingFile(file) {
9640
+ if (!existsSync4(file)) return {};
9641
+ const parsed = parseBindingObject(file);
9642
+ if (parsed instanceof HubBindingError) throw parsed;
9643
+ const keys = Object.keys(parsed);
9644
+ const unknown = keys.filter((key) => !BINDING_KEYS.has(key));
9645
+ if (parsed.schema !== HUB_BINDING_SCHEMA || unknown.length > 0) {
9646
+ return { warning: bindingFallbackWarning(file, `is newer than this build understands (${String(parsed.schema)})`) };
9647
+ }
9648
+ if (typeof parsed.url !== "string" || typeof parsed.boundAt !== "string" || !isIsoTimestamp(parsed.boundAt)) {
9649
+ throw new HubBindingError(`malformed hub binding at ${file}`);
9650
+ }
9651
+ let url;
9652
+ try {
9653
+ url = validateHubUrl(parsed.url);
9654
+ } catch {
9655
+ throw new HubBindingError(`malformed hub binding at ${file}`);
9656
+ }
9657
+ const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
9658
+ if (!cloudish) {
9659
+ if (keys.length !== 3) throw new HubBindingError(`malformed hub binding at ${file}`);
9660
+ return { record: { schema: HUB_BINDING_SCHEMA, url, boundAt: parsed.boundAt } };
9661
+ }
9662
+ const record = cloudRecordFromRow(file, parsed, url, parsed.boundAt);
9663
+ if (!record) throw new HubBindingError(`malformed hub binding at ${file}`);
9664
+ return { record, legacyCloud: true };
9665
+ }
9666
+ function loadHubBinding(env = process.env) {
9667
+ const cloudFile = hubBindingCloudFile(env);
9668
+ const cloud = interpretBindingFile(cloudFile);
9669
+ if (cloud.warning && !cloud.record) {
9670
+ const local = existsSync4(hubBindingFile(env)) ? interpretBindingFile(hubBindingFile(env)) : {};
9671
+ if (local.record && !local.record.cloud) return { record: local.record, warning: cloud.warning };
9672
+ return { warning: cloud.warning };
9673
+ }
9674
+ if (cloud.record?.cloud) return { record: cloud.record, ...cloud.warning ? { warning: cloud.warning } : {} };
9675
+ const main = interpretBindingFile(hubBindingFile(env));
9676
+ if (main.legacyCloud && main.record) {
9677
+ writeBindingJson(cloudFile, main.record);
9678
+ rmSync(hubBindingFile(env), { force: true });
9679
+ return {
9680
+ record: main.record,
9681
+ warning: bindingFallbackWarning(
9682
+ hubBindingFile(env),
9683
+ "stored a cloud bind in the pre-0.7.160 file"
9684
+ )
9685
+ };
9627
9686
  }
9687
+ return { ...main.record ? { record: main.record } : {}, ...main.warning ? { warning: main.warning } : {} };
9688
+ }
9689
+ function readHubBinding(env = process.env) {
9690
+ return loadHubBinding(env).record;
9691
+ }
9692
+ function writeHubBinding(record, env = process.env) {
9693
+ if (record.cloud) {
9694
+ const file2 = hubBindingCloudFile(env);
9695
+ writeBindingJson(file2, record);
9696
+ const main = hubBindingFile(env);
9697
+ if (existsSync4(main)) {
9698
+ try {
9699
+ const legacy = readLegacyV1HubBinding(env);
9700
+ if (!legacy || legacy.url === record.url) rmSync(main, { force: true });
9701
+ } catch (error) {
9702
+ if (!(error instanceof HubBindingError)) throw error;
9703
+ rmSync(main, { force: true });
9704
+ }
9705
+ }
9706
+ return file2;
9707
+ }
9708
+ rmSync(hubBindingCloudFile(env), { force: true });
9709
+ const file = hubBindingFile(env);
9710
+ writeBindingJson(file, {
9711
+ schema: HUB_BINDING_SCHEMA,
9712
+ url: record.url,
9713
+ boundAt: record.boundAt
9714
+ });
9628
9715
  return file;
9629
9716
  }
9630
9717
  function removeHubBinding(env = process.env) {
9631
- const file = hubBindingFile(env);
9718
+ const files = [hubBindingFile(env), hubBindingCloudFile(env)];
9719
+ let removed = false;
9720
+ for (const file of files) {
9721
+ try {
9722
+ rmSync(file);
9723
+ removed = true;
9724
+ } catch (error) {
9725
+ if (error.code !== "ENOENT") throw error;
9726
+ }
9727
+ }
9728
+ return removed;
9729
+ }
9730
+ function contentTypeBase(header) {
9731
+ return (header ?? "").split(";")[0].trim().toLowerCase();
9732
+ }
9733
+ function looksLikeHtml(contentType, body) {
9734
+ if (contentType === "text/html" || contentType === "application/xhtml+xml") return true;
9735
+ const sample = body.slice(0, 800).trim().toLowerCase();
9736
+ return sample.startsWith("<!doctype html") || sample.startsWith("<html") || sample.includes("<form") && (sample.includes("login") || sample.includes("authentik") || sample.includes("password"));
9737
+ }
9738
+ function jsonObject(text) {
9632
9739
  try {
9633
- rmSync(file);
9634
- return true;
9740
+ const parsed = JSON.parse(text);
9741
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
9742
+ return parsed;
9743
+ } catch {
9744
+ return void 0;
9745
+ }
9746
+ }
9747
+ function hubEndpointShape(role, body) {
9748
+ if (role === "health") return body.ok === true && typeof body.agents === "number";
9749
+ return body.ok === true && (body.storage === "sqlite" || body.storage === "memory");
9750
+ }
9751
+ function classifyHubResponse(input) {
9752
+ const contentType = contentTypeBase(input.contentType);
9753
+ if (input.status >= 300 && input.status < 400) {
9754
+ return {
9755
+ ok: false,
9756
+ status: input.status,
9757
+ kind: "redirect",
9758
+ detail: `HTTP ${input.status} redirect; this looks like an auth proxy or a redirect, not a kxm hub`,
9759
+ body: { error: "auth_proxy", detail: `HTTP ${input.status} redirect` }
9760
+ };
9761
+ }
9762
+ if (looksLikeHtml(contentType, input.bodyText)) {
9763
+ return {
9764
+ ok: false,
9765
+ status: input.status,
9766
+ kind: "auth_proxy",
9767
+ detail: `HTTP ${input.status} returned an HTML login page${contentType ? ` (${contentType})` : ""}; this looks like an auth proxy, not a kxm hub`,
9768
+ body: { error: "auth_proxy", detail: "HTML login page" }
9769
+ };
9770
+ }
9771
+ const jsonType = contentType === "application/json" || contentType === "text/plain" || contentType === "";
9772
+ const body = jsonType ? jsonObject(input.bodyText) : void 0;
9773
+ if (body && hubEndpointShape(input.role, body) && input.status >= 200 && input.status < 300) {
9774
+ if (contentType !== "" && contentType !== "application/json" && contentType !== "text/plain") {
9775
+ return {
9776
+ ok: false,
9777
+ status: input.status,
9778
+ kind: "not_hub",
9779
+ detail: `HTTP ${input.status} content-type ${contentType} is not application/json`,
9780
+ body: { error: "not_hub", detail: contentType }
9781
+ };
9782
+ }
9783
+ return { ok: true, status: input.status, kind: "hub", detail: "kxm hub", body };
9784
+ }
9785
+ const typeNote = contentType && contentType !== "application/json" ? ` content-type ${contentType}` : "";
9786
+ return {
9787
+ ok: false,
9788
+ status: input.status,
9789
+ kind: "not_hub",
9790
+ detail: `HTTP ${input.status}${typeNote} is not a kxm hub ${input.role} response`,
9791
+ body: { error: "not_hub", detail: `HTTP ${input.status}${typeNote}` }
9792
+ };
9793
+ }
9794
+ async function readHubEndpoint(url, role, fetchImpl, init = {}) {
9795
+ try {
9796
+ const response = await fetchImpl(`${url}/${role}`, {
9797
+ ...init.headers ? { headers: init.headers } : {},
9798
+ ...init.signal ? { signal: init.signal } : {},
9799
+ redirect: "manual"
9800
+ });
9801
+ const bodyText = await response.text();
9802
+ return classifyHubResponse({
9803
+ role,
9804
+ status: response.status,
9805
+ contentType: response.headers.get("content-type"),
9806
+ bodyText
9807
+ });
9635
9808
  } catch (error) {
9636
- if (error.code === "ENOENT") return false;
9637
- throw error;
9809
+ if (isAbortError(error)) {
9810
+ return { ok: false, status: 0, kind: "timeout", detail: "no reply within the probe budget", body: { error: "hub_unreachable" } };
9811
+ }
9812
+ return { ok: false, status: 0, kind: "unreachable", detail: "nothing answered", body: { error: "hub_unreachable" } };
9638
9813
  }
9639
9814
  }
9640
9815
  async function probeHubHealth(url, fetchImpl, timeoutMs = HUB_HEALTH_PROBE_MS) {
@@ -9642,25 +9817,14 @@ async function probeHubHealth(url, fetchImpl, timeoutMs = HUB_HEALTH_PROBE_MS) {
9642
9817
  const controller = new AbortController();
9643
9818
  const timer = setTimeout(() => controller.abort(), timeoutMs);
9644
9819
  try {
9645
- const response = await fetchImpl(`${url}/health`, { signal: controller.signal });
9646
- if (!response.ok) return { health: "unknown", probeMs: Date.now() - started };
9647
- let body;
9648
- try {
9649
- body = await response.json();
9650
- } catch {
9651
- return { health: "unknown", probeMs: Date.now() - started };
9652
- }
9653
- if (body && typeof body === "object" && body.ok === true) {
9654
- return { health: "on", probeMs: Date.now() - started };
9655
- }
9656
- return { health: "unknown", probeMs: Date.now() - started };
9657
- } catch (error) {
9658
- return { health: isAbortError(error) ? "unknown" : "off", probeMs: Date.now() - started };
9820
+ const verdict = await readHubEndpoint(url, "health", fetchImpl, { signal: controller.signal });
9821
+ const health = verdict.kind === "hub" ? "on" : verdict.kind === "unreachable" ? "off" : "unknown";
9822
+ return { health, probeMs: Date.now() - started, kind: verdict.kind, detail: verdict.detail, status: verdict.status };
9659
9823
  } finally {
9660
9824
  clearTimeout(timer);
9661
9825
  }
9662
9826
  }
9663
- var HUB_BINDING_SCHEMA, HUB_HEALTH_PROBE_MS, CloudTokenError, TOKEN_ENV_NAME, TOKEN_COMMAND_TIMEOUT_MS, TOKEN_COMMAND_MAX_BUFFER, HubBindingError;
9827
+ var HUB_BINDING_SCHEMA, HUB_HEALTH_PROBE_MS, CloudTokenError, TOKEN_ENV_NAME, TOKEN_COMMAND_TIMEOUT_MS, TOKEN_COMMAND_MAX_BUFFER, HubBindingError, LEGACY_BINDING_KEYS, BINDING_KEYS;
9664
9828
  var init_hub_binding = __esm({
9665
9829
  "plugins/kxm/src/hub-binding.ts"() {
9666
9830
  "use strict";
@@ -9683,11 +9847,13 @@ var init_hub_binding = __esm({
9683
9847
  this.name = "HubBindingError";
9684
9848
  }
9685
9849
  };
9850
+ LEGACY_BINDING_KEYS = ["schema", "url", "boundAt"];
9851
+ BINDING_KEYS = /* @__PURE__ */ new Set([...LEGACY_BINDING_KEYS, "cloud", "tokenEnv", "tokenCommand"]);
9686
9852
  }
9687
9853
  });
9688
9854
 
9689
9855
  // plugins/kxm/src/hub-identity.ts
9690
- import { spawnSync as spawnSync2 } from "node:child_process";
9856
+ import { spawnSync as spawnSync3 } from "node:child_process";
9691
9857
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
9692
9858
  import { basename, dirname as dirname4, join as join5, resolve as resolve3 } from "node:path";
9693
9859
  function normalizeUrl(raw) {
@@ -9768,7 +9934,7 @@ function defaultOpRead(reference, env) {
9768
9934
  if (!isHubOpReference(reference)) {
9769
9935
  throw new CloudTokenError("cloud_token_command_invalid", `refusing to resolve ${reference}; an op:// reference is required`);
9770
9936
  }
9771
- const result = spawnSync2("op", ["read", reference], {
9937
+ const result = spawnSync3("op", ["read", reference], {
9772
9938
  encoding: "utf8",
9773
9939
  shell: false,
9774
9940
  timeout: OP_TIMEOUT_MS,
@@ -9808,18 +9974,24 @@ function resolveKeyReference(ref, env, opRead = defaultOpRead) {
9808
9974
  }
9809
9975
  function keySourceFor(input) {
9810
9976
  const ref = input.endpoint?.key;
9811
- if (input.mode === "local" && input.env.KXM_AUTH_TOKEN?.trim()) return "env:KXM_AUTH_TOKEN";
9812
- if (ref?.env && input.env[ref.env]?.trim()) return `env:${ref.env}`;
9813
- if (ref?.op) return `op:${ref.op}`;
9814
- if (ref?.env) return `env:${ref.env}`;
9977
+ const envName = (name) => {
9978
+ if (!name) return void 0;
9979
+ return input.env[name]?.trim() ? `env:${name}` : void 0;
9980
+ };
9815
9981
  if (input.mode === "cloud") {
9816
- if (input.binding?.tokenEnv) return `env:${input.binding.tokenEnv}`;
9817
- if (input.binding?.tokenCommand) {
9818
- const op = opReferenceFromTokenCommand(input.binding.tokenCommand);
9819
- return op ? `op:${op}` : "command";
9820
- }
9982
+ const fromEnv2 = envName(ref?.env);
9983
+ if (fromEnv2) return fromEnv2;
9984
+ if (ref?.op) return `op:${ref.op}`;
9985
+ const fromBindingEnv = envName(input.binding?.tokenEnv);
9986
+ if (fromBindingEnv) return fromBindingEnv;
9987
+ if (input.binding?.tokenCommand) return "token-command";
9821
9988
  return "missing";
9822
9989
  }
9990
+ const fromAuth = envName("KXM_AUTH_TOKEN");
9991
+ if (fromAuth) return fromAuth;
9992
+ const fromEnv = envName(ref?.env);
9993
+ if (fromEnv) return fromEnv;
9994
+ if (ref?.op) return `op:${ref.op}`;
9823
9995
  if (input.hasSavedProjectToken?.(input.project)) return "hub-env";
9824
9996
  return "missing";
9825
9997
  }
@@ -18919,7 +19091,7 @@ var init_oneshot_process = __esm({
18919
19091
  });
18920
19092
 
18921
19093
  // plugins/kxm/src/harness.ts
18922
- import { spawnSync as spawnSync3 } from "node:child_process";
19094
+ import { spawnSync as spawnSync4 } from "node:child_process";
18923
19095
  import { existsSync as existsSync9 } from "node:fs";
18924
19096
  import { win32 as win32Path } from "node:path";
18925
19097
  function reportedModelId(value) {
@@ -19208,7 +19380,7 @@ function findWinNpmInnerExe(cliId, options = {}) {
19208
19380
  function defaultRunner(env) {
19209
19381
  return (command, args, timeoutMs) => {
19210
19382
  try {
19211
- const result = spawnSync3(command, [...args], {
19383
+ const result = spawnSync4(command, [...args], {
19212
19384
  encoding: "utf8",
19213
19385
  timeout: timeoutMs,
19214
19386
  windowsHide: true,
@@ -19942,7 +20114,7 @@ var init_harness = __esm({
19942
20114
  });
19943
20115
 
19944
20116
  // plugins/kxm/src/project-config.ts
19945
- import { spawnSync as spawnSync4 } from "node:child_process";
20117
+ import { spawnSync as spawnSync5 } from "node:child_process";
19946
20118
  import { createHash as createHash7 } from "node:crypto";
19947
20119
  import { existsSync as existsSync10, lstatSync, readFileSync as readFileSync8, readdirSync as readdirSync2, realpathSync } from "node:fs";
19948
20120
  import { basename as basename2, dirname as dirname7, extname, isAbsolute as isAbsolute3, join as join9, relative, resolve as resolve6, sep } from "node:path";
@@ -19950,6 +20122,21 @@ import { pathToFileURL } from "node:url";
19950
20122
  function defaultKxmSchemaDir() {
19951
20123
  return join9(findKxmRepoRoot(import.meta.url), "schemas");
19952
20124
  }
20125
+ function toolPresetProfile(preset) {
20126
+ if (!Object.hasOwn(TOOL_PRESET_PROFILES, preset)) return void 0;
20127
+ return TOOL_PRESET_PROFILES[preset];
20128
+ }
20129
+ function toolPresetExceedsRoleMessage(preset, roleId, rolePermission, rolePreset) {
20130
+ const profile = toolPresetProfile(preset);
20131
+ if (profile !== "edit") return void 0;
20132
+ if (rolePermission === "read-only") {
20133
+ return `tools.preset ${preset} exceeds role ${roleId} permission read-only`;
20134
+ }
20135
+ if (rolePreset !== void 0 && toolPresetProfile(rolePreset) === "read-only") {
20136
+ return `tools.preset ${preset} exceeds role ${roleId} preset ${rolePreset}`;
20137
+ }
20138
+ return void 0;
20139
+ }
19953
20140
  function compareCodeUnits4(left, right) {
19954
20141
  return left < right ? -1 : left > right ? 1 : 0;
19955
20142
  }
@@ -20235,6 +20422,15 @@ function objectValue(value) {
20235
20422
  function stringValue(value) {
20236
20423
  return typeof value === "string" ? value : void 0;
20237
20424
  }
20425
+ function roleById(roles, roleId) {
20426
+ const direct = roles.get(roleId);
20427
+ if (direct) return direct;
20428
+ const folded = roleId.toLocaleLowerCase("en-US");
20429
+ for (const role of roles.values()) {
20430
+ if ((role.id ?? "").toLocaleLowerCase("en-US") === folded) return role;
20431
+ }
20432
+ return void 0;
20433
+ }
20238
20434
  function numberValue(value, fallback) {
20239
20435
  return typeof value === "number" ? value : fallback;
20240
20436
  }
@@ -20745,10 +20941,29 @@ function validateBundle(project, repositories, agents, models, workflows, enviro
20745
20941
  if (executor && !executors.has(executor)) issues.push(issue3("reference", "executor_unknown", agent.logicalPath, `executor ${executor} is not registered`));
20746
20942
  const preset = stringValue(objectValue(agent.value.tools)?.preset);
20747
20943
  if (preset && !presets.has(preset)) issues.push(issue3("reference", "tool_preset_unknown", agent.logicalPath, `tool preset ${preset} is not registered`));
20944
+ const roleId = stringValue(agent.value.role);
20945
+ const role = roleId ? roleById(roles, roleId) : void 0;
20946
+ const rolePermission = stringValue(role?.value.permission);
20947
+ if (preset && role && (rolePermission === "edit" || rolePermission === "read-only")) {
20948
+ const message = toolPresetExceedsRoleMessage(
20949
+ preset,
20950
+ role.id ?? roleId ?? "role",
20951
+ rolePermission,
20952
+ stringValue(objectValue(role.value.tools)?.preset)
20953
+ );
20954
+ if (message) issues.push(issue3("semantic", "tool_preset_exceeds_role", agent.logicalPath, message));
20955
+ }
20748
20956
  for (const repositoryId of Object.keys(objectValue(agent.value.repositories) ?? {})) {
20749
20957
  if (!repositoryIds.has(repositoryId)) issues.push(issue3("reference", "repository_unknown", agent.logicalPath, `references unknown repository ${repositoryId}`));
20750
20958
  }
20751
20959
  }
20960
+ for (const role of roles.values()) {
20961
+ const permission = stringValue(role.value.permission);
20962
+ const rolePreset = stringValue(objectValue(role.value.tools)?.preset);
20963
+ if (permission === "read-only" && rolePreset && toolPresetProfile(rolePreset) === "edit") {
20964
+ issues.push(issue3("semantic", "tool_preset_exceeds_role", role.logicalPath, `tools.preset ${rolePreset} exceeds permission read-only`));
20965
+ }
20966
+ }
20752
20967
  validateModelReferences(models, issues);
20753
20968
  const defaultWorkflow = stringValue(project.value.defaultWorkflow) ?? "default";
20754
20969
  if (!lookupKxmResource(workflows.values(), defaultWorkflow, "workflow")) issues.push(issue3("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
@@ -20821,7 +21036,7 @@ function developerCeilings() {
20821
21036
  const script = join9(findKxmRepoRoot(import.meta.url), "scripts", "harness-run.mjs");
20822
21037
  let loaded;
20823
21038
  try {
20824
- loaded = spawnSync4(process.execPath, ["--input-type=module", "-e", `const m = await import(${JSON.stringify(pathToFileURL(script).href)}); process.stdout.write(JSON.stringify({ROUTES:m.ROUTES,NATIVE_PI_BRAKE_PROVIDERS:m.NATIVE_PI_BRAKE_PROVIDERS,PI_ALLOWED_PROVIDERS:m.PI_ALLOWED_PROVIDERS,PI_NATIVE_VENDOR_PROVIDERS:m.PI_NATIVE_VENDOR_PROVIDERS}))`], {
21039
+ loaded = spawnSync5(process.execPath, ["--input-type=module", "-e", `const m = await import(${JSON.stringify(pathToFileURL(script).href)}); process.stdout.write(JSON.stringify({ROUTES:m.ROUTES,NATIVE_PI_BRAKE_PROVIDERS:m.NATIVE_PI_BRAKE_PROVIDERS,PI_ALLOWED_PROVIDERS:m.PI_ALLOWED_PROVIDERS,PI_NATIVE_VENDOR_PROVIDERS:m.PI_NATIVE_VENDOR_PROVIDERS}))`], {
20825
21040
  encoding: "utf8",
20826
21041
  timeout: 15e3
20827
21042
  });
@@ -20917,7 +21132,7 @@ function gitEnvironment() {
20917
21132
  function discoverGitRoot(start = process.cwd()) {
20918
21133
  let current = resolve6(start);
20919
21134
  if (existsSync10(current) && !lstatSync(current).isDirectory()) current = dirname7(current);
20920
- const result = spawnSync4("git", ["-C", current, "rev-parse", "--show-toplevel"], {
21135
+ const result = spawnSync5("git", ["-C", current, "rev-parse", "--show-toplevel"], {
20921
21136
  encoding: "utf8",
20922
21137
  env: gitEnvironment(),
20923
21138
  timeout: 5e3,
@@ -21226,7 +21441,7 @@ function planKxmInitialization(start = process.cwd(), options = {}) {
21226
21441
  }
21227
21442
  return { mode: "create", inspectedFrom, projectRoot: candidateRoot, changesRequired: true, issues: [], legacyInputs: [] };
21228
21443
  }
21229
- var import__, KxmConfigError, DEFAULT_SCHEMA_DIR, RESOURCE_SCHEMA, IDENTIFIER2, WINDOWS_RESERVED, BUILTIN_EXECUTORS, BUILTIN_TOOL_PRESETS, SECRET_VALUE_PATTERNS, KxmSchemaRegistry, cachedRunEventRegistry, GATE_STEP_OUTCOMES, LEGACY_CONFIG_FILES;
21444
+ var import__, KxmConfigError, DEFAULT_SCHEMA_DIR, RESOURCE_SCHEMA, IDENTIFIER2, WINDOWS_RESERVED, BUILTIN_EXECUTORS, TOOL_PRESET_PROFILES, BUILTIN_TOOL_PRESETS, SECRET_VALUE_PATTERNS, KxmSchemaRegistry, cachedRunEventRegistry, GATE_STEP_OUTCOMES, LEGACY_CONFIG_FILES;
21230
21445
  var init_project_config = __esm({
21231
21446
  "plugins/kxm/src/project-config.ts"() {
21232
21447
  "use strict";
@@ -21260,7 +21475,13 @@ var init_project_config = __esm({
21260
21475
  IDENTIFIER2 = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
21261
21476
  WINDOWS_RESERVED = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³]|conin\$|conout\$|clock\$)$/i;
21262
21477
  BUILTIN_EXECUTORS = ["local", "ssh", "exe-dev"];
21263
- BUILTIN_TOOL_PRESETS = ["coordinator", "read-only", "workspace-writer", "tests-writer"];
21478
+ TOOL_PRESET_PROFILES = Object.freeze({
21479
+ coordinator: "read-only",
21480
+ "read-only": "read-only",
21481
+ "workspace-writer": "edit",
21482
+ "tests-writer": "edit"
21483
+ });
21484
+ BUILTIN_TOOL_PRESETS = Object.keys(TOOL_PRESET_PROFILES);
21264
21485
  SECRET_VALUE_PATTERNS = [
21265
21486
  /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----/,
21266
21487
  /\b(?:ghp|github_pat)_[A-Za-z0-9_]{20,}\b/,
@@ -21437,7 +21658,7 @@ var init_sqlite = __esm({
21437
21658
  });
21438
21659
 
21439
21660
  // plugins/kxm/src/bindings.ts
21440
- import { spawnSync as spawnSync5 } from "node:child_process";
21661
+ import { spawnSync as spawnSync6 } from "node:child_process";
21441
21662
  import { createHash as createHash8, randomUUID as randomUUID3 } from "node:crypto";
21442
21663
  import {
21443
21664
  chmodSync as chmodSync2,
@@ -21639,7 +21860,7 @@ function projectOperationLockFile(projectRoot) {
21639
21860
  for (const key of Object.keys(env)) {
21640
21861
  if (key.toLocaleUpperCase("en-US").startsWith("GIT_")) delete env[key];
21641
21862
  }
21642
- const result = spawnSync5("git", ["-C", projectRoot, "rev-parse", "--absolute-git-dir"], {
21863
+ const result = spawnSync6("git", ["-C", projectRoot, "rev-parse", "--absolute-git-dir"], {
21643
21864
  encoding: "utf8",
21644
21865
  env,
21645
21866
  timeout: 5e3,
@@ -22816,7 +23037,7 @@ var init_database = __esm({
22816
23037
  });
22817
23038
 
22818
23039
  // plugins/kxm/src/runtime-store.ts
22819
- import { spawnSync as spawnSync6 } from "node:child_process";
23040
+ import { spawnSync as spawnSync7 } from "node:child_process";
22820
23041
  import { createHash as createHash12, randomUUID as randomUUID4 } from "node:crypto";
22821
23042
  import { existsSync as existsSync14, lstatSync as lstatSync4, mkdirSync as mkdirSync8, readFileSync as readFileSync11, realpathSync as realpathSync4, statSync, writeFileSync as writeFileSync8 } from "node:fs";
22822
23043
  import { dirname as dirname10, isAbsolute as isAbsolute6, resolve as resolve11 } from "node:path";
@@ -22856,7 +23077,7 @@ function gitEnv() {
22856
23077
  return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("GIT_")));
22857
23078
  }
22858
23079
  function gitCommonDirectory(projectRoot) {
22859
- const result = spawnSync6("git", ["-C", projectRoot, "rev-parse", "--git-common-dir"], {
23080
+ const result = spawnSync7("git", ["-C", projectRoot, "rev-parse", "--git-common-dir"], {
22860
23081
  encoding: "utf8",
22861
23082
  env: gitEnv(),
22862
23083
  timeout: 3e4,
@@ -27537,6 +27758,68 @@ function unsupportedAgentStepTimeout(step, limitMs) {
27537
27758
  detail: "step timeoutMs is wider than project limits.agentStepTimeoutMs"
27538
27759
  };
27539
27760
  }
27761
+ function narrowerPermission(left, right) {
27762
+ return left === "read-only" || right === "read-only" ? "read-only" : "edit";
27763
+ }
27764
+ function stepRepositoryPermission(step) {
27765
+ return Object.values(step.repositories).some((access) => access === "write") ? "edit" : "read-only";
27766
+ }
27767
+ function stepAccessDetail(step) {
27768
+ const entries = Object.entries(step.repositories);
27769
+ if (entries.length === 0) return "none";
27770
+ return entries.map(([id, access]) => `${id} ${access}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0).join(", ");
27771
+ }
27772
+ function yamlToolsPreset(doc) {
27773
+ const tools = doc?.tools;
27774
+ if (!tools || typeof tools !== "object" || Array.isArray(tools)) return void 0;
27775
+ const preset = tools.preset;
27776
+ return typeof preset === "string" && preset.length > 0 ? preset : void 0;
27777
+ }
27778
+ function resolveToolDispatch(projectRoot, step, agentId) {
27779
+ const stepPermission = stepRepositoryPermission(step);
27780
+ if (step.kind !== "agent" && step.kind !== "moa") return { permission: stepPermission };
27781
+ const agentFile = findYamlBasename(join19(projectRoot, ".kxm", "agents"), agentId, "agent") ?? agentId;
27782
+ const agent = readYamlFile(join19(projectRoot, ".kxm", "agents", `${agentFile}.yaml`));
27783
+ const roleId = typeof agent?.role === "string" ? agent.role : "";
27784
+ if (!roleId) return { permission: stepPermission };
27785
+ const roleDoc = readYamlFile(join19(projectRoot, ".kxm", "roles", `${roleId}.yaml`));
27786
+ const rawPermission = roleDoc?.permission;
27787
+ const rolePermission = rawPermission === "edit" || rawPermission === "read-only" ? rawPermission : void 0;
27788
+ if (!rolePermission) return { permission: stepPermission };
27789
+ const rolePreset = yamlToolsPreset(roleDoc);
27790
+ const agentPreset = yamlToolsPreset(agent);
27791
+ const rolePresetProfile = rolePreset ? toolPresetProfile(rolePreset) : void 0;
27792
+ if (rolePreset && rolePresetProfile === "edit" && rolePermission === "read-only") {
27793
+ return {
27794
+ error: {
27795
+ reason: "step_unsupported",
27796
+ field: "tools.preset",
27797
+ detail: `tools.preset ${rolePreset} exceeds role ${roleId} permission read-only`
27798
+ }
27799
+ };
27800
+ }
27801
+ if (agentPreset) {
27802
+ const exceeds = toolPresetExceedsRoleMessage(agentPreset, roleId, rolePermission, rolePreset);
27803
+ if (exceeds) {
27804
+ return { error: { reason: "step_unsupported", field: "tools.preset", detail: exceeds } };
27805
+ }
27806
+ }
27807
+ let chain = rolePermission;
27808
+ if (rolePresetProfile) chain = narrowerPermission(chain, rolePresetProfile);
27809
+ const agentProfile = agentPreset ? toolPresetProfile(agentPreset) : void 0;
27810
+ if (agentProfile) chain = narrowerPermission(chain, agentProfile);
27811
+ const permission = narrowerPermission(chain, stepPermission);
27812
+ if (stepPermission === "edit" && permission === "read-only") {
27813
+ return {
27814
+ error: {
27815
+ reason: "step_unsupported",
27816
+ field: "permission",
27817
+ detail: `role permission ${rolePermission} resolves to ${chain}; step access ${stepAccessDetail(step)}`
27818
+ }
27819
+ };
27820
+ }
27821
+ return { permission };
27822
+ }
27540
27823
  function kxmLiveRunPrerequisites(bundle, workflowId, projectRoot) {
27541
27824
  const workflow = lookupKxmResource(bundle.workflows.values(), workflowId, "workflow");
27542
27825
  if (!workflow) throw runtimeError("run_workflow_unknown", workflowId, `workflow ${workflowId} does not exist in this project`);
@@ -27564,6 +27847,11 @@ function kxmLiveRunPrerequisites(bundle, workflowId, projectRoot) {
27564
27847
  if (step.kind !== "agent" && step.kind !== "moa") continue;
27565
27848
  const agentIds = step.assignments.allowedAgents.length > 0 ? step.assignments.allowedAgents : [step.agent];
27566
27849
  for (const agentId of agentIds) {
27850
+ const toolDispatch = resolveToolDispatch(projectRoot, step, agentId);
27851
+ if ("error" in toolDispatch) {
27852
+ prerequisites.push({ ...toolDispatch.error, stepId });
27853
+ continue;
27854
+ }
27567
27855
  const route = resolveProducerRoute(projectRoot, step, agentId);
27568
27856
  if ("error" in route) {
27569
27857
  const detail = route.error.detail === "producer_route_unsupported: agent step model is not honored" ? `${agentId}: agent step model is not honored; remove model from the step` : `${agentId}: ${route.error.detail}; set role in .kxm/agents/${agentId}.yaml and admit the route model with kxm routes admit --model <provider/model>`;
@@ -27571,7 +27859,7 @@ function kxmLiveRunPrerequisites(bundle, workflowId, projectRoot) {
27571
27859
  continue;
27572
27860
  }
27573
27861
  const harness = route.harness;
27574
- const permission = Object.values(step.repositories).includes("write") ? "edit" : "read-only";
27862
+ const permission = toolDispatch.permission;
27575
27863
  if (!BUILTIN_HARNESSES.some((entry) => entry.id === harness && entry.oneShot) || !oneShotPermissionArgs(harness, permission)) {
27576
27864
  prerequisites.push({ reason: "step_unsupported", stepId, field: "harness", detail: `${agentId}: ${harness} has no audited ${permission} one-shot profile; use a supported workflow or execute this work directly in ${harness}, without substituting another harness` });
27577
27865
  continue;
@@ -28369,10 +28657,13 @@ function runtimeFrom(ctx, command) {
28369
28657
  const envServerUrl = ctx.env.KXM_SERVER_URL?.trim();
28370
28658
  let boundHubUrl;
28371
28659
  try {
28372
- boundHubUrl = readHubBinding(ctx.env)?.url;
28660
+ const loaded = loadHubBinding(ctx.env);
28661
+ if (loaded.warning) ctx.io.stderr(`kxm: ${loaded.warning}
28662
+ `);
28663
+ boundHubUrl = loaded.record?.url;
28373
28664
  } catch (error) {
28374
28665
  if (error instanceof HubBindingError) {
28375
- ctx.io.stderr(`kxm: ignoring malformed hub binding at ${hubBindingFile(ctx.env)}; run kxm hub bind <url> again
28666
+ ctx.io.stderr(`kxm: ignoring malformed hub binding at ${hubBindingFile(ctx.env)}; falling back to the local hub. Run kxm hub bind <url> again
28376
28667
  `);
28377
28668
  } else {
28378
28669
  throw error;
@@ -28621,7 +28912,7 @@ var init_vision_gate = __esm({
28621
28912
  });
28622
28913
 
28623
28914
  // plugins/kxm/src/permission.ts
28624
- import { spawnSync as spawnSync7 } from "node:child_process";
28915
+ import { spawnSync as spawnSync8 } from "node:child_process";
28625
28916
  import { createHash as createHash17 } from "node:crypto";
28626
28917
  import { existsSync as existsSync27, mkdtempSync, mkdirSync as mkdirSync20, readFileSync as readFileSync25, rmSync as rmSync8, writeFileSync as writeFileSync19 } from "node:fs";
28627
28918
  import { tmpdir as tmpdir2 } from "node:os";
@@ -29095,7 +29386,7 @@ function gitEnvironment2() {
29095
29386
  return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("GIT_")));
29096
29387
  }
29097
29388
  function git(root, args) {
29098
- const result = spawnSync7("git", ["-C", root, ...args], {
29389
+ const result = spawnSync8("git", ["-C", root, ...args], {
29099
29390
  encoding: "utf8",
29100
29391
  env: gitEnvironment2(),
29101
29392
  timeout: 15e3,
@@ -29113,7 +29404,7 @@ function git(root, args) {
29113
29404
  return result.stdout;
29114
29405
  }
29115
29406
  function gitBuffer(root, args) {
29116
- const result = spawnSync7("git", ["-C", root, ...args], {
29407
+ const result = spawnSync8("git", ["-C", root, ...args], {
29117
29408
  env: gitEnvironment2(),
29118
29409
  timeout: 15e3,
29119
29410
  windowsHide: true,
@@ -29311,7 +29602,7 @@ function loadBaseProjectDeclarations(projectFile) {
29311
29602
  return members;
29312
29603
  }
29313
29604
  function initShadowGitRoot(directory) {
29314
- const result = spawnSync7("git", ["-c", "init.defaultBranch=main", "init", "--quiet", directory], {
29605
+ const result = spawnSync8("git", ["-c", "init.defaultBranch=main", "init", "--quiet", directory], {
29315
29606
  encoding: "utf8",
29316
29607
  env: gitEnvironment2(),
29317
29608
  timeout: 1e4,
@@ -35174,6 +35465,7 @@ function useColor() {
35174
35465
  var program = new Command();
35175
35466
 
35176
35467
  // plugins/kxm/src/kxm-update.ts
35468
+ import { spawnSync } from "node:child_process";
35177
35469
  import { createHash } from "node:crypto";
35178
35470
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
35179
35471
  import { join } from "node:path";
@@ -35192,6 +35484,18 @@ var KxmUpdateConfigError = class extends Error {
35192
35484
  function kxmReleaseAssetName(version) {
35193
35485
  return `kxm-${version}.tgz`;
35194
35486
  }
35487
+ function kxmReleaseDownloadUrl(version, name = kxmReleaseAssetName(version)) {
35488
+ return `https://github.com/kontextmind/kxm/releases/download/v${version}/${name}`;
35489
+ }
35490
+ function ghReleaseDownloadAvailable(env = process.env) {
35491
+ const result = spawnSync("gh", ["--version"], {
35492
+ encoding: "utf8",
35493
+ env,
35494
+ windowsHide: true,
35495
+ timeout: 15e3
35496
+ });
35497
+ return !result.error && result.status === 0;
35498
+ }
35195
35499
  function readInstalledKxmVersion(root) {
35196
35500
  const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
35197
35501
  if (typeof pkg.version !== "string" || !parseSemver(pkg.version)) {
@@ -35340,13 +35644,17 @@ function verifyReleaseAssetDigest(path4, sha256) {
35340
35644
  return false;
35341
35645
  }
35342
35646
  }
35343
- function planKxmPackageUpdate(source, latest, releaseDir, asset) {
35647
+ function planKxmPackageUpdate(source, latest, releaseDir, asset, downloader = "gh") {
35344
35648
  if (source === "npm") {
35345
35649
  return [{ kind: "install", command: "npm", args: ["install", "--global", "--omit=peer", `@kontextmind/kxm@${latest}`] }];
35346
35650
  }
35347
35651
  const name = asset?.name ?? kxmReleaseAssetName(latest);
35348
35652
  const steps = [
35349
- {
35653
+ downloader === "curl" ? {
35654
+ kind: "download",
35655
+ command: "curl",
35656
+ args: ["--fail", "--silent", "--show-error", "--location", "--output", join(releaseDir, name), kxmReleaseDownloadUrl(latest, name)]
35657
+ } : {
35350
35658
  kind: "download",
35351
35659
  command: "gh",
35352
35660
  args: ["release", "download", `v${latest}`, "--repo", "kontextmind/kxm", "--pattern", name, "--dir", releaseDir, "--clobber"]
@@ -40076,7 +40384,7 @@ init_project();
40076
40384
  init_repo_root();
40077
40385
  init_harness();
40078
40386
  init_types();
40079
- import { spawnSync as spawnSync8 } from "node:child_process";
40387
+ import { spawnSync as spawnSync9 } from "node:child_process";
40080
40388
  var PLUGIN_SUPPORTED_HARNESSES = Object.freeze(["pi", "omp", "claude"]);
40081
40389
  async function cmdPluginInstall(runtime, options) {
40082
40390
  const repoRoot = findKxmRepoRoot(import.meta.url);
@@ -40115,7 +40423,7 @@ async function cmdPluginInstall(runtime, options) {
40115
40423
  detail: `would install kxm plugin into pi: ${command} ${args.join(" ")}`
40116
40424
  });
40117
40425
  } else {
40118
- const run = runtime.io.spawnSync ? runtime.io.spawnSync(command, args) : spawnSync8(command, args, { encoding: "utf8", windowsHide: true, env: runtime.env });
40426
+ const run = runtime.io.spawnSync ? runtime.io.spawnSync(command, args) : spawnSync9(command, args, { encoding: "utf8", windowsHide: true, env: runtime.env });
40119
40427
  if (run.error || run.status !== 0 && run.status !== null) {
40120
40428
  results.push({
40121
40429
  harness: "pi",
@@ -40146,7 +40454,7 @@ async function cmdPluginInstall(runtime, options) {
40146
40454
  detail: `would install kxm plugin into omp: ${command} ${args.join(" ")}`
40147
40455
  });
40148
40456
  } else {
40149
- const run = runtime.io.spawnSync ? runtime.io.spawnSync(command, args) : spawnSync8(command, args, { encoding: "utf8", windowsHide: true, env: runtime.env });
40457
+ const run = runtime.io.spawnSync ? runtime.io.spawnSync(command, args) : spawnSync9(command, args, { encoding: "utf8", windowsHide: true, env: runtime.env });
40150
40458
  if (run.error || run.status !== 0 && run.status !== null) {
40151
40459
  results.push({
40152
40460
  harness: "omp",
@@ -40181,9 +40489,9 @@ async function cmdPluginInstall(runtime, options) {
40181
40489
  if (runtime.io.spawnSync) {
40182
40490
  runtime.io.spawnSync(command, marketplaceArgs);
40183
40491
  } else {
40184
- spawnSync8(command, marketplaceArgs, { encoding: "utf8", windowsHide: true, env: runtime.env });
40492
+ spawnSync9(command, marketplaceArgs, { encoding: "utf8", windowsHide: true, env: runtime.env });
40185
40493
  }
40186
- const run = runtime.io.spawnSync ? runtime.io.spawnSync(command, installArgs) : spawnSync8(command, installArgs, { encoding: "utf8", windowsHide: true, env: runtime.env });
40494
+ const run = runtime.io.spawnSync ? runtime.io.spawnSync(command, installArgs) : spawnSync9(command, installArgs, { encoding: "utf8", windowsHide: true, env: runtime.env });
40187
40495
  if (run.error || run.status !== 0 && run.status !== null) {
40188
40496
  results.push({
40189
40497
  harness: "claude",
@@ -40307,7 +40615,7 @@ init_project_config();
40307
40615
  init_runtime_store();
40308
40616
  init_runtime_supervisor();
40309
40617
  init_types();
40310
- import { spawnSync as spawnSync9 } from "node:child_process";
40618
+ import { spawnSync as spawnSync10 } from "node:child_process";
40311
40619
  import { chmodSync as chmodSync6, existsSync as existsSync33, mkdirSync as mkdirSync24, readFileSync as readFileSync30, writeFileSync as writeFileSync22 } from "node:fs";
40312
40620
  import { basename as basename8, dirname as dirname18, join as join37, resolve as resolve25 } from "node:path";
40313
40621
  var LANE_SCHEMA = "kxm.lanes.v1";
@@ -40322,7 +40630,7 @@ function gitEnv2() {
40322
40630
  return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("GIT_")));
40323
40631
  }
40324
40632
  function git2(cwd, args) {
40325
- const result = spawnSync9("git", ["-C", cwd, ...args], {
40633
+ const result = spawnSync10("git", ["-C", cwd, ...args], {
40326
40634
  encoding: "utf8",
40327
40635
  env: gitEnv2(),
40328
40636
  timeout: 3e4,
@@ -40897,7 +41205,7 @@ function cmdLand(runtime, options = {}) {
40897
41205
  // plugins/kxm/src/cli/assign.ts
40898
41206
  init_project_config();
40899
41207
  init_types();
40900
- import { spawnSync as spawnSync10 } from "node:child_process";
41208
+ import { spawnSync as spawnSync11 } from "node:child_process";
40901
41209
  import { existsSync as existsSync34 } from "node:fs";
40902
41210
  import { join as join39 } from "node:path";
40903
41211
  var kxmAssignCliSeams = {};
@@ -40983,7 +41291,7 @@ async function cmdAssign(runtime, subcommand, args) {
40983
41291
  return 0;
40984
41292
  }
40985
41293
  const spawn6 = kxmAssignCliSeams.spawn ?? ((commandName, commandArgs, options) => {
40986
- const result2 = spawnSync10(commandName, commandArgs, {
41294
+ const result2 = spawnSync11(commandName, commandArgs, {
40987
41295
  cwd: options.cwd,
40988
41296
  stdio: options.stdio,
40989
41297
  env: options.env,
@@ -49448,7 +49756,7 @@ var MAX_RENDER_WRITE_CHARS = 1024 * 1024;
49448
49756
  // plugins/kxm/src/tui.ts
49449
49757
  import { mkdirSync as mkdirSync25 } from "node:fs";
49450
49758
  import { join as join44, resolve as resolve27, dirname as dirname20 } from "node:path";
49451
- import { spawnSync as spawnSync11 } from "node:child_process";
49759
+ import { spawnSync as spawnSync12 } from "node:child_process";
49452
49760
 
49453
49761
  // packages/core/tui/src/types/surface.ts
49454
49762
  var KXM_TUI_LIMITS = Object.freeze({
@@ -50005,16 +50313,16 @@ function applyMeshTuiKey(view, key, itemCount = 0) {
50005
50313
  function copyToClipboard(text) {
50006
50314
  try {
50007
50315
  if (process.platform === "darwin") {
50008
- const proc = spawnSync11("pbcopy", { input: text, encoding: "utf8", windowsHide: true });
50316
+ const proc = spawnSync12("pbcopy", { input: text, encoding: "utf8", windowsHide: true });
50009
50317
  return proc.status === 0;
50010
50318
  }
50011
50319
  if (process.platform === "win32") {
50012
- const proc = spawnSync11("clip", { input: text, encoding: "utf8", windowsHide: true });
50320
+ const proc = spawnSync12("clip", { input: text, encoding: "utf8", windowsHide: true });
50013
50321
  return proc.status === 0;
50014
50322
  }
50015
- const wl = spawnSync11("wl-copy", [text], { encoding: "utf8", windowsHide: true });
50323
+ const wl = spawnSync12("wl-copy", [text], { encoding: "utf8", windowsHide: true });
50016
50324
  if (wl.status === 0) return true;
50017
- const xclip = spawnSync11("xclip", ["-selection", "clipboard"], { input: text, encoding: "utf8", windowsHide: true });
50325
+ const xclip = spawnSync12("xclip", ["-selection", "clipboard"], { input: text, encoding: "utf8", windowsHide: true });
50018
50326
  return xclip.status === 0;
50019
50327
  } catch {
50020
50328
  return false;
@@ -50022,7 +50330,7 @@ function copyToClipboard(text) {
50022
50330
  }
50023
50331
  function spawnDegradeWorktree(repoRoot, runId, options) {
50024
50332
  const runner = options?.execFn ?? ((cmd, args) => {
50025
- const res = spawnSync11(cmd, args, {
50333
+ const res = spawnSync12(cmd, args, {
50026
50334
  cwd: repoRoot,
50027
50335
  encoding: "utf8",
50028
50336
  windowsHide: true,
@@ -50806,7 +51114,7 @@ async function runMeshTui(input) {
50806
51114
  }
50807
51115
 
50808
51116
  // plugins/kxm/src/session-work.ts
50809
- import { spawnSync as spawnSync12 } from "node:child_process";
51117
+ import { spawnSync as spawnSync13 } from "node:child_process";
50810
51118
  import { randomUUID as randomUUID13 } from "node:crypto";
50811
51119
  import { existsSync as existsSync36, mkdirSync as mkdirSync26, readFileSync as readFileSync32, renameSync as renameSync8, writeFileSync as writeFileSync23 } from "node:fs";
50812
51120
  import { join as join45 } from "node:path";
@@ -50867,10 +51175,10 @@ function formatShipLine(ship) {
50867
51175
  }
50868
51176
  function readGitShip(cwd) {
50869
51177
  try {
50870
- const dirty = spawnSync12("git", ["--no-optional-locks", "-C", cwd, "status", "--porcelain"], { encoding: "utf8", windowsHide: true });
51178
+ const dirty = spawnSync13("git", ["--no-optional-locks", "-C", cwd, "status", "--porcelain"], { encoding: "utf8", windowsHide: true });
50871
51179
  if (dirty.status !== 0) return void 0;
50872
51180
  const isDirty = dirty.stdout.trim().length > 0;
50873
- const upstream = spawnSync12("git", ["-C", cwd, "rev-list", "--count", "@{u}..HEAD"], { encoding: "utf8", windowsHide: true });
51181
+ const upstream = spawnSync13("git", ["-C", cwd, "rev-list", "--count", "@{u}..HEAD"], { encoding: "utf8", windowsHide: true });
50874
51182
  if (upstream.status === 0) {
50875
51183
  return {
50876
51184
  dirty: isDirty,
@@ -50878,9 +51186,9 @@ function readGitShip(cwd) {
50878
51186
  };
50879
51187
  }
50880
51188
  for (const baseRef of ["origin/HEAD", "main", "origin/main", "master", "origin/master"]) {
50881
- const mb = spawnSync12("git", ["-C", cwd, "merge-base", baseRef, "HEAD"], { encoding: "utf8", windowsHide: true });
51189
+ const mb = spawnSync13("git", ["-C", cwd, "merge-base", baseRef, "HEAD"], { encoding: "utf8", windowsHide: true });
50882
51190
  if (mb.status === 0 && mb.stdout.trim()) {
50883
- const count = spawnSync12("git", ["-C", cwd, "rev-list", "--count", `${mb.stdout.trim()}..HEAD`], { encoding: "utf8", windowsHide: true });
51191
+ const count = spawnSync13("git", ["-C", cwd, "rev-list", "--count", `${mb.stdout.trim()}..HEAD`], { encoding: "utf8", windowsHide: true });
50884
51192
  if (count.status === 0) {
50885
51193
  return {
50886
51194
  dirty: isDirty,
@@ -51424,20 +51732,6 @@ init_repo_root();
51424
51732
  import { fileURLToPath as fileURLToPath3 } from "node:url";
51425
51733
  import { dirname as dirname22 } from "node:path";
51426
51734
  import { homedir as homedir7 } from "node:os";
51427
- async function hubGet(url, fetchImpl, headers) {
51428
- try {
51429
- const response = await fetchImpl(url, headers ? { headers } : void 0);
51430
- const text = redactSecrets((await response.text()).slice(0, 8e3));
51431
- let body = text;
51432
- try {
51433
- body = JSON.parse(text);
51434
- } catch {
51435
- }
51436
- return { ok: response.ok, status: response.status, body };
51437
- } catch {
51438
- return { ok: false, status: 0, body: { error: "hub_unreachable" } };
51439
- }
51440
- }
51441
51735
  function installProbeFrom(runtime) {
51442
51736
  const partial = runtime.io.installProbe ?? {};
51443
51737
  const moduleDir = partial.moduleDir ?? dirname22(fileURLToPath3(import.meta.url));
@@ -51512,8 +51806,11 @@ async function cmdStatus(runtime) {
51512
51806
  }
51513
51807
  if (!(error instanceof HubBindingError)) throw error;
51514
51808
  }
51515
- const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl, headers);
51516
- const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl, headers);
51809
+ const health = await readHubEndpoint(runtime.serverUrl, "health", runtime.fetchImpl, headers ? { headers } : {});
51810
+ const ready = await readHubEndpoint(runtime.serverUrl, "ready", runtime.fetchImpl, headers ? { headers } : {});
51811
+ const proxyKind = [health.kind, ready.kind].find((kind) => kind === "redirect" || kind === "auth_proxy");
51812
+ const notHub = !proxyKind && (health.kind === "not_hub" || ready.kind === "not_hub");
51813
+ const probeNote = proxyKind === "redirect" ? " \xB7 auth proxy or redirect" : proxyKind === "auth_proxy" ? " \xB7 auth proxy (HTML login page)" : notHub ? " \xB7 not a kxm hub" : "";
51517
51814
  const effectiveScope = connection.scope;
51518
51815
  const overridden = connection.urlSource === "env";
51519
51816
  const identity = {
@@ -51537,7 +51834,7 @@ async function cmdStatus(runtime) {
51537
51834
  runtime.io,
51538
51835
  runtime.json,
51539
51836
  payload,
51540
- `hub health=${health.ok} ready=${ready.ok} \xB7 ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} \xB7 mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}`
51837
+ `hub health=${health.ok} ready=${ready.ok} \xB7 ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} \xB7 mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}${probeNote}`
51541
51838
  );
51542
51839
  return payload.ok ? 0 : 1;
51543
51840
  }
@@ -51748,7 +52045,7 @@ async function cmdHubBind(runtime, rawUrl, options = {}) {
51748
52045
  return 2;
51749
52046
  }
51750
52047
  }
51751
- const file = hubBindingFile(runtime.env);
52048
+ const file = cloud ? hubBindingCloudFile(runtime.env) : hubBindingFile(runtime.env);
51752
52049
  const bindingCommand = tokenCommand ?? (cloud && keyOp ? `op read ${keyOp}` : void 0);
51753
52050
  const cloudFields = cloud ? { cloud: true, ...tokenEnv ? { tokenEnv } : {}, ...bindingCommand ? { tokenCommand: bindingCommand } : {} } : {};
51754
52051
  const record = {
@@ -51773,6 +52070,27 @@ async function cmdHubBind(runtime, rawUrl, options = {}) {
51773
52070
  );
51774
52071
  return 0;
51775
52072
  }
52073
+ const probe = await probeHubHealth(url, runtime.fetchImpl);
52074
+ const acceptable = probe.kind === "hub" || probe.kind === "timeout" || probe.kind === "unreachable";
52075
+ if (!acceptable && options.force !== true) {
52076
+ print(
52077
+ runtime.io,
52078
+ runtime.json,
52079
+ {
52080
+ ok: false,
52081
+ command: "hub bind",
52082
+ error: "hub_not_kxm",
52083
+ url,
52084
+ scope,
52085
+ kind: probe.kind,
52086
+ status: probe.status,
52087
+ detail: probe.detail,
52088
+ nextAction: "bind_a_kxm_hub_or_pass_force"
52089
+ },
52090
+ `refusing to bind ${url}: ${probe.detail}; no binding was written. Pass --force to bind anyway`
52091
+ );
52092
+ return 2;
52093
+ }
51776
52094
  try {
51777
52095
  writeHubEndpoint(runtime.dirs.workdir, {
51778
52096
  mode: cloud ? "cloud" : "local",
@@ -51793,12 +52111,12 @@ async function cmdHubBind(runtime, rawUrl, options = {}) {
51793
52111
  throw error;
51794
52112
  }
51795
52113
  writeHubBinding({ ...record, boundAt: (/* @__PURE__ */ new Date()).toISOString() }, runtime.env);
51796
- const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
52114
+ const healthText = acceptable ? formatHubBindHealth(probe.health) : `${probe.detail} (--force)`;
51797
52115
  print(
51798
52116
  runtime.io,
51799
52117
  runtime.json,
51800
- { ok: true, command: "hub bind", url, scope, file, configFile, project: identity.project, health, probeMs, ...cloudFields },
51801
- `bound hub ${url} \xB7 ${scope} \xB7 ${formatHubBindHealth(health)}${cloud ? cloudNote : scope === "remote" ? " \xB7 token leaves this machine" : ""}`
52118
+ { ok: true, command: "hub bind", url, scope, file, configFile, project: identity.project, health: probe.health, probeMs: probe.probeMs, kind: probe.kind, ...cloudFields },
52119
+ `bound hub ${url} \xB7 ${scope} \xB7 ${healthText}${cloud ? cloudNote : scope === "remote" ? " \xB7 token leaves this machine" : ""}`
51802
52120
  );
51803
52121
  return 0;
51804
52122
  }
@@ -52231,7 +52549,7 @@ init_redact();
52231
52549
  init_telemetry();
52232
52550
  init_routing();
52233
52551
  init_prices();
52234
- import { spawnSync as spawnSync14 } from "node:child_process";
52552
+ import { spawnSync as spawnSync15 } from "node:child_process";
52235
52553
  import { createHash as createHash19 } from "node:crypto";
52236
52554
  import { existsSync as existsSync47, mkdtempSync as mkdtempSync3, readFileSync as readFileSync41, rmSync as rmSync13 } from "node:fs";
52237
52555
  import { tmpdir as tmpdir3 } from "node:os";
@@ -53110,7 +53428,7 @@ ${divider}
53110
53428
 
53111
53429
  // plugins/kxm/src/ssh-remote.ts
53112
53430
  init_safety_integrity();
53113
- import { spawnSync as spawnSync13 } from "node:child_process";
53431
+ import { spawnSync as spawnSync14 } from "node:child_process";
53114
53432
  import { existsSync as existsSync44, mkdirSync as mkdirSync30, readFileSync as readFileSync39, readdirSync as readdirSync14, rmSync as rmSync12, statSync as statSync7 } from "node:fs";
53115
53433
  import { homedir as homedir8 } from "node:os";
53116
53434
  import { join as join52, resolve as resolve31 } from "node:path";
@@ -53186,7 +53504,7 @@ function parseSshConfig(configPath) {
53186
53504
  return [];
53187
53505
  }
53188
53506
  }
53189
- function resolveSshHostG(host, execFn = spawnSync13) {
53507
+ function resolveSshHostG(host, execFn = spawnSync14) {
53190
53508
  try {
53191
53509
  const result = execFn("ssh", ["-G", host], { encoding: "utf-8" });
53192
53510
  if (result.status !== 0 || !result.stdout) {
@@ -53252,7 +53570,7 @@ function buildSshArgs(options) {
53252
53570
  args.push(options.host);
53253
53571
  return args;
53254
53572
  }
53255
- function checkControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawnSync13) {
53573
+ function checkControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawnSync14) {
53256
53574
  const resolvedDir = ensureSocketDir(socketDir);
53257
53575
  const controlPath = join52(resolvedDir, "%C");
53258
53576
  try {
@@ -53264,7 +53582,7 @@ function checkControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawn
53264
53582
  return false;
53265
53583
  }
53266
53584
  }
53267
- function closeControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawnSync13) {
53585
+ function closeControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawnSync14) {
53268
53586
  const resolvedDir = ensureSocketDir(socketDir);
53269
53587
  const controlPath = join52(resolvedDir, "%C");
53270
53588
  try {
@@ -53278,7 +53596,7 @@ function closeControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawn
53278
53596
  }
53279
53597
  function executeSshRun(params) {
53280
53598
  const startTime = Date.now();
53281
- const execSyncFn = params.execFn ?? spawnSync13;
53599
+ const execSyncFn = params.execFn ?? spawnSync14;
53282
53600
  if (params.action === "info") {
53283
53601
  if (params.host) {
53284
53602
  const hostInfo = resolveSshHostG(params.host, execSyncFn);
@@ -54423,7 +54741,7 @@ init_types();
54423
54741
  init_repo_root();
54424
54742
  function cliSpawn(runtime, command, args, extra) {
54425
54743
  if (runtime.io.spawnSync) return runtime.io.spawnSync(command, args);
54426
- const result = spawnSync14(command, [...args], {
54744
+ const result = spawnSync15(command, [...args], {
54427
54745
  encoding: "utf8",
54428
54746
  windowsHide: true,
54429
54747
  shell: process.platform === "win32",
@@ -54463,7 +54781,8 @@ function applyKxmPackageUpdate(runtime, notice) {
54463
54781
  }
54464
54782
  const releaseDir = mkdtempSync3(join55(tmpdir3(), "kxm-pkg-update-"));
54465
54783
  try {
54466
- const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset);
54784
+ const downloader = notice.source === "github" && !ghReleaseDownloadAvailable(runtime.env) ? "curl" : "gh";
54785
+ const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset, downloader);
54467
54786
  if (runtime.dryRun) {
54468
54787
  return { ok: true, detail: planned.map(formatPackageUpdateStep).join(" && ") };
54469
54788
  }
@@ -56042,7 +56361,7 @@ function createProgram(ctx, result, argv) {
56042
56361
  addGlobalOptions(hub.command("stop").description("Request managed hub and worker shutdown")).option("--wait-ms <ms>", "How long to wait for PID files to clear").action(async function hubStopAction(options) {
56043
56362
  result.code = await cmdStop(runtimeFrom(ctx, this), options.waitMs);
56044
56363
  });
56045
- addGlobalOptions(hub.command("bind").description("Bind this machine to a running hub").argument("<url>", "Hub base URL (http or https)")).option("--cloud", "Mark the hub remote even when the URL is a loopback forward").option("--token-env <name>", "Environment variable that holds the hub token; the token is not stored").option("--token-command <command>", "Program that prints the hub token; the token is not stored").option("--key-op <ref>", "op:// reference for the hub token; resolved with op read, never stored").action(async function hubBindAction(url, options) {
56364
+ addGlobalOptions(hub.command("bind").description("Bind this machine to a running hub").argument("<url>", "Hub base URL (http or https)")).option("--cloud", "Mark the hub remote even when the URL is a loopback forward").option("--token-env <name>", "Environment variable that holds the hub token; the token is not stored").option("--token-command <command>", "Program that prints the hub token; the token is not stored").option("--key-op <ref>", "op:// reference for the hub token; resolved with op read, never stored").option("--force", "Bind even when the URL is not a kxm hub").action(async function hubBindAction(url, options) {
56046
56365
  result.code = await cmdHubBind(runtimeFrom(ctx, this), url, options);
56047
56366
  });
56048
56367
  addGlobalOptions(hub.command("unbind").description("Remove this machine's hub binding")).action(bind(cmdHubUnbind));