@kontextmind/kxm 0.7.166 → 0.7.168

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 +18 -0
  3. package/docs/README.md +1 -0
  4. package/docs/architecture/hosted-direction.md +9 -4
  5. package/docs/architecture/inventory.md +1 -0
  6. package/docs/contributing/ci-and-release.md +6 -1
  7. package/docs/contributing/decisions.md +153 -0
  8. package/docs/contributing/learnings.md +54 -53
  9. package/docs/contributing/operating-rules.md +19 -6
  10. package/docs/operations/runtime-sync.md +2 -1
  11. package/docs/operations/troubleshooting.md +4 -0
  12. package/docs/operations/upgrade.md +18 -3
  13. package/docs/operations.md +70 -7
  14. package/docs/reference/cli-reference.md +21 -6
  15. package/docs/reference/config-reference.md +4 -1
  16. package/docs/start/install.md +1 -1
  17. package/package.json +1 -1
  18. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  19. package/plugins/kxm/dist/claude-hook.js +170 -36
  20. package/plugins/kxm/dist/cli.js +332 -129
  21. package/plugins/kxm/dist/extension.js +184 -53
  22. package/plugins/kxm/dist/mcp-server.js +97 -40
  23. package/plugins/kxm/dist/runtime-supervisor.js +113 -42
  24. package/plugins/kxm/dist/runtime.js +113 -42
  25. package/plugins/kxm/dist/server.js +2 -0
  26. package/plugins/kxm/package.json +1 -1
  27. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +4 -2
  28. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -4
  29. package/plugins/kxm/src/cli/hub.ts +42 -7
  30. package/plugins/kxm/src/cli/system.ts +3 -1
  31. package/plugins/kxm/src/cli/types.ts +5 -3
  32. package/plugins/kxm/src/cli.ts +2 -1
  33. package/plugins/kxm/src/hub-binding.ts +301 -55
  34. package/plugins/kxm/src/hub-identity.ts +17 -9
  35. package/plugins/kxm/src/kxm-update.ts +29 -6
  36. package/plugins/kxm/src/mcp-server.ts +1 -1
  37. package/plugins/kxm/src/runtime-supervisor.ts +22 -4
  38. package/scripts/kxm-publish-npm.mjs +47 -1
@@ -31668,6 +31668,9 @@ function resolveUserStateRoot(env) {
31668
31668
  function hubBindingFile(env = process.env) {
31669
31669
  return join19(resolveUserStateRoot(env), "hub-binding.json");
31670
31670
  }
31671
+ function hubBindingCloudFile(env = process.env) {
31672
+ return join19(resolveUserStateRoot(env), "hub-binding.cloud.json");
31673
+ }
31671
31674
  function validateHubUrl(raw) {
31672
31675
  let parsed;
31673
31676
  try {
@@ -31777,33 +31780,37 @@ function isIsoTimestamp(value) {
31777
31780
  if (Number.isNaN(Date.parse(value))) return false;
31778
31781
  return value === new Date(value).toISOString();
31779
31782
  }
31780
- function readHubBinding(env = process.env) {
31781
- const file = hubBindingFile(env);
31782
- if (!existsSync16(file)) return void 0;
31783
+ var LEGACY_BINDING_KEYS = ["schema", "url", "boundAt"];
31784
+ var BINDING_KEYS = /* @__PURE__ */ new Set([...LEGACY_BINDING_KEYS, "cloud", "tokenEnv", "tokenCommand"]);
31785
+ function bindingFallbackWarning(file, reason) {
31786
+ 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.`;
31787
+ }
31788
+ function writeBindingJson(file, value) {
31789
+ mkdirSync7(dirname9(file), { recursive: true, mode: 448 });
31790
+ const temporary = join19(dirname9(file), `.${file.endsWith("cloud.json") ? "hub-binding-cloud" : "hub-binding"}-${process.pid}.tmp`);
31791
+ try {
31792
+ writeFileSync8(temporary, `${JSON.stringify(value, null, 2)}
31793
+ `, { encoding: "utf8", mode: 384 });
31794
+ renameSync2(temporary, file);
31795
+ } finally {
31796
+ rmSync2(temporary, { force: true });
31797
+ }
31798
+ }
31799
+ function parseBindingObject(file) {
31800
+ if (!existsSync16(file)) return new HubBindingError(`missing hub binding at ${file}`);
31783
31801
  let parsed;
31784
31802
  try {
31785
31803
  parsed = JSON.parse(readFileSync14(file, "utf8"));
31786
31804
  } catch {
31787
- throw new HubBindingError(`malformed hub binding at ${file}`);
31805
+ return new HubBindingError(`malformed hub binding at ${file}`);
31788
31806
  }
31789
31807
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
31790
- throw new HubBindingError(`malformed hub binding at ${file}`);
31791
- }
31792
- const row = parsed;
31793
- const keys = Object.keys(row);
31794
- const allowed = /* @__PURE__ */ new Set(["schema", "url", "boundAt", "cloud", "tokenEnv", "tokenCommand"]);
31795
- const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
31796
- 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)) {
31797
- throw new HubBindingError(`malformed hub binding at ${file}`);
31808
+ return new HubBindingError(`malformed hub binding at ${file}`);
31798
31809
  }
31799
- let url;
31800
- try {
31801
- url = validateHubUrl(row.url);
31802
- } catch {
31803
- throw new HubBindingError(`malformed hub binding at ${file}`);
31804
- }
31805
- if (!cloudish) return { schema: HUB_BINDING_SCHEMA, url, boundAt: row.boundAt };
31806
- if (row.cloud !== true) throw new HubBindingError(`malformed hub binding at ${file}`);
31810
+ return parsed;
31811
+ }
31812
+ function cloudRecordFromRow(file, row, url, boundAt) {
31813
+ if (row.cloud !== true) return void 0;
31807
31814
  let tokenEnv;
31808
31815
  let tokenCommand;
31809
31816
  if (row.tokenEnv !== void 0) {
@@ -31825,12 +31832,65 @@ function readHubBinding(env = process.env) {
31825
31832
  return {
31826
31833
  schema: HUB_BINDING_SCHEMA,
31827
31834
  url,
31828
- boundAt: row.boundAt,
31835
+ boundAt,
31829
31836
  cloud: true,
31830
31837
  ...tokenEnv ? { tokenEnv } : {},
31831
31838
  ...tokenCommand ? { tokenCommand } : {}
31832
31839
  };
31833
31840
  }
31841
+ function interpretBindingFile(file) {
31842
+ if (!existsSync16(file)) return {};
31843
+ const parsed = parseBindingObject(file);
31844
+ if (parsed instanceof HubBindingError) throw parsed;
31845
+ const keys = Object.keys(parsed);
31846
+ const unknown = keys.filter((key) => !BINDING_KEYS.has(key));
31847
+ if (parsed.schema !== HUB_BINDING_SCHEMA || unknown.length > 0) {
31848
+ return { warning: bindingFallbackWarning(file, `is newer than this build understands (${String(parsed.schema)})`) };
31849
+ }
31850
+ if (typeof parsed.url !== "string" || typeof parsed.boundAt !== "string" || !isIsoTimestamp(parsed.boundAt)) {
31851
+ throw new HubBindingError(`malformed hub binding at ${file}`);
31852
+ }
31853
+ let url;
31854
+ try {
31855
+ url = validateHubUrl(parsed.url);
31856
+ } catch {
31857
+ throw new HubBindingError(`malformed hub binding at ${file}`);
31858
+ }
31859
+ const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
31860
+ if (!cloudish) {
31861
+ if (keys.length !== 3) throw new HubBindingError(`malformed hub binding at ${file}`);
31862
+ return { record: { schema: HUB_BINDING_SCHEMA, url, boundAt: parsed.boundAt } };
31863
+ }
31864
+ const record2 = cloudRecordFromRow(file, parsed, url, parsed.boundAt);
31865
+ if (!record2) throw new HubBindingError(`malformed hub binding at ${file}`);
31866
+ return { record: record2, legacyCloud: true };
31867
+ }
31868
+ function loadHubBinding(env = process.env) {
31869
+ const cloudFile = hubBindingCloudFile(env);
31870
+ const cloud = interpretBindingFile(cloudFile);
31871
+ if (cloud.warning && !cloud.record) {
31872
+ const local = existsSync16(hubBindingFile(env)) ? interpretBindingFile(hubBindingFile(env)) : {};
31873
+ if (local.record && !local.record.cloud) return { record: local.record, warning: cloud.warning };
31874
+ return { warning: cloud.warning };
31875
+ }
31876
+ if (cloud.record?.cloud) return { record: cloud.record, ...cloud.warning ? { warning: cloud.warning } : {} };
31877
+ const main = interpretBindingFile(hubBindingFile(env));
31878
+ if (main.legacyCloud && main.record) {
31879
+ writeBindingJson(cloudFile, main.record);
31880
+ rmSync2(hubBindingFile(env), { force: true });
31881
+ return {
31882
+ record: main.record,
31883
+ warning: bindingFallbackWarning(
31884
+ hubBindingFile(env),
31885
+ "stored a cloud bind in the pre-0.7.160 file"
31886
+ )
31887
+ };
31888
+ }
31889
+ return { ...main.record ? { record: main.record } : {}, ...main.warning ? { warning: main.warning } : {} };
31890
+ }
31891
+ function readHubBinding(env = process.env) {
31892
+ return loadHubBinding(env).record;
31893
+ }
31834
31894
 
31835
31895
  // plugins/kxm/src/hub-env.ts
31836
31896
  import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync16, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync9 } from "node:fs";
@@ -31906,15 +31966,6 @@ function resolveProjectIdentity(cwd, env = process.env, explicit) {
31906
31966
  if (fromPackage) return { project: fromPackage, source: "package.json", sourceLabel: "package.json name" };
31907
31967
  return { project: basename4(cwd), source: "directory", sourceLabel: "directory name" };
31908
31968
  }
31909
- function opReferenceFromTokenCommand(command) {
31910
- try {
31911
- const args = splitTokenCommand(command);
31912
- if (args[0] === "op" && args[1] === "read" && args[2] && isHubOpReference(args[2])) return args[2];
31913
- } catch {
31914
- return void 0;
31915
- }
31916
- return void 0;
31917
- }
31918
31969
  function defaultOpRead(reference, env) {
31919
31970
  if (!isHubOpReference(reference)) {
31920
31971
  throw new CloudTokenError("cloud_token_command_invalid", `refusing to resolve ${reference}; an op:// reference is required`);
@@ -31959,18 +32010,24 @@ function resolveKeyReference(ref, env, opRead = defaultOpRead) {
31959
32010
  }
31960
32011
  function keySourceFor(input) {
31961
32012
  const ref = input.endpoint?.key;
31962
- if (input.mode === "local" && input.env.KXM_AUTH_TOKEN?.trim()) return "env:KXM_AUTH_TOKEN";
31963
- if (ref?.env && input.env[ref.env]?.trim()) return `env:${ref.env}`;
31964
- if (ref?.op) return `op:${ref.op}`;
31965
- if (ref?.env) return `env:${ref.env}`;
32013
+ const envName = (name) => {
32014
+ if (!name) return void 0;
32015
+ return input.env[name]?.trim() ? `env:${name}` : void 0;
32016
+ };
31966
32017
  if (input.mode === "cloud") {
31967
- if (input.binding?.tokenEnv) return `env:${input.binding.tokenEnv}`;
31968
- if (input.binding?.tokenCommand) {
31969
- const op = opReferenceFromTokenCommand(input.binding.tokenCommand);
31970
- return op ? `op:${op}` : "command";
31971
- }
32018
+ const fromEnv2 = envName(ref?.env);
32019
+ if (fromEnv2) return fromEnv2;
32020
+ if (ref?.op) return `op:${ref.op}`;
32021
+ const fromBindingEnv = envName(input.binding?.tokenEnv);
32022
+ if (fromBindingEnv) return fromBindingEnv;
32023
+ if (input.binding?.tokenCommand) return "token-command";
31972
32024
  return "missing";
31973
32025
  }
32026
+ const fromAuth = envName("KXM_AUTH_TOKEN");
32027
+ if (fromAuth) return fromAuth;
32028
+ const fromEnv = envName(ref?.env);
32029
+ if (fromEnv) return fromEnv;
32030
+ if (ref?.op) return `op:${ref.op}`;
31974
32031
  if (input.hasSavedProjectToken?.(input.project)) return "hub-env";
31975
32032
  return "missing";
31976
32033
  }
@@ -32631,8 +32688,17 @@ async function syncKxmOutbox(eventStore, client, options = {}) {
32631
32688
  }
32632
32689
  }
32633
32690
  }
32634
- function runtimeHubClientFor(context, env) {
32635
- const serverUrl = env.KXM_SERVER_URL?.trim() || readHubBinding(env)?.url;
32691
+ function runtimeHubClientFor(context, env, onBindingWarning) {
32692
+ let recordUrl;
32693
+ try {
32694
+ const loaded = loadHubBinding(env);
32695
+ if (loaded.warning) onBindingWarning?.(loaded.warning);
32696
+ recordUrl = loaded.record?.url;
32697
+ } catch (error) {
32698
+ if (!(error instanceof HubBindingError)) throw error;
32699
+ onBindingWarning?.(`${error.message}; falling back to the local hub so sync can continue`);
32700
+ }
32701
+ const serverUrl = env.KXM_SERVER_URL?.trim() || recordUrl;
32636
32702
  if (!serverUrl) return void 0;
32637
32703
  const project = context.projectId;
32638
32704
  const authToken = resolveClientHubAuthToken(env, project);
@@ -33160,6 +33226,7 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
33160
33226
  else logger.info(entry);
33161
33227
  };
33162
33228
  let syncing = false;
33229
+ let bindingWarning;
33163
33230
  const syncTimer = setInterval(() => {
33164
33231
  if (syncing || stopping) return;
33165
33232
  syncing = true;
@@ -33177,7 +33244,11 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
33177
33244
  consecutiveFailures: 0
33178
33245
  };
33179
33246
  try {
33180
- const client = runtimeHubClientFor(context, env);
33247
+ const client = runtimeHubClientFor(context, env, (message) => {
33248
+ if (bindingWarning === message) return;
33249
+ bindingWarning = message;
33250
+ logger.warn({ event: "hub_binding_fallback", message });
33251
+ });
33181
33252
  if (!client) {
33182
33253
  recordSyncStatus(context, { ...base, state: "no_hub" });
33183
33254
  continue;
@@ -32416,6 +32416,9 @@ function resolveUserStateRoot(env) {
32416
32416
  function hubBindingFile(env = process.env) {
32417
32417
  return join19(resolveUserStateRoot(env), "hub-binding.json");
32418
32418
  }
32419
+ function hubBindingCloudFile(env = process.env) {
32420
+ return join19(resolveUserStateRoot(env), "hub-binding.cloud.json");
32421
+ }
32419
32422
  function validateHubUrl(raw) {
32420
32423
  let parsed;
32421
32424
  try {
@@ -32525,33 +32528,37 @@ function isIsoTimestamp(value) {
32525
32528
  if (Number.isNaN(Date.parse(value))) return false;
32526
32529
  return value === new Date(value).toISOString();
32527
32530
  }
32528
- function readHubBinding(env = process.env) {
32529
- const file = hubBindingFile(env);
32530
- if (!existsSync16(file)) return void 0;
32531
+ var LEGACY_BINDING_KEYS = ["schema", "url", "boundAt"];
32532
+ var BINDING_KEYS = /* @__PURE__ */ new Set([...LEGACY_BINDING_KEYS, "cloud", "tokenEnv", "tokenCommand"]);
32533
+ function bindingFallbackWarning(file, reason) {
32534
+ 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.`;
32535
+ }
32536
+ function writeBindingJson(file, value) {
32537
+ mkdirSync7(dirname9(file), { recursive: true, mode: 448 });
32538
+ const temporary = join19(dirname9(file), `.${file.endsWith("cloud.json") ? "hub-binding-cloud" : "hub-binding"}-${process.pid}.tmp`);
32539
+ try {
32540
+ writeFileSync8(temporary, `${JSON.stringify(value, null, 2)}
32541
+ `, { encoding: "utf8", mode: 384 });
32542
+ renameSync2(temporary, file);
32543
+ } finally {
32544
+ rmSync2(temporary, { force: true });
32545
+ }
32546
+ }
32547
+ function parseBindingObject(file) {
32548
+ if (!existsSync16(file)) return new HubBindingError(`missing hub binding at ${file}`);
32531
32549
  let parsed;
32532
32550
  try {
32533
32551
  parsed = JSON.parse(readFileSync14(file, "utf8"));
32534
32552
  } catch {
32535
- throw new HubBindingError(`malformed hub binding at ${file}`);
32553
+ return new HubBindingError(`malformed hub binding at ${file}`);
32536
32554
  }
32537
32555
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
32538
- throw new HubBindingError(`malformed hub binding at ${file}`);
32539
- }
32540
- const row = parsed;
32541
- const keys = Object.keys(row);
32542
- const allowed = /* @__PURE__ */ new Set(["schema", "url", "boundAt", "cloud", "tokenEnv", "tokenCommand"]);
32543
- const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
32544
- 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)) {
32545
- throw new HubBindingError(`malformed hub binding at ${file}`);
32556
+ return new HubBindingError(`malformed hub binding at ${file}`);
32546
32557
  }
32547
- let url;
32548
- try {
32549
- url = validateHubUrl(row.url);
32550
- } catch {
32551
- throw new HubBindingError(`malformed hub binding at ${file}`);
32552
- }
32553
- if (!cloudish) return { schema: HUB_BINDING_SCHEMA, url, boundAt: row.boundAt };
32554
- if (row.cloud !== true) throw new HubBindingError(`malformed hub binding at ${file}`);
32558
+ return parsed;
32559
+ }
32560
+ function cloudRecordFromRow(file, row, url, boundAt) {
32561
+ if (row.cloud !== true) return void 0;
32555
32562
  let tokenEnv;
32556
32563
  let tokenCommand;
32557
32564
  if (row.tokenEnv !== void 0) {
@@ -32573,12 +32580,65 @@ function readHubBinding(env = process.env) {
32573
32580
  return {
32574
32581
  schema: HUB_BINDING_SCHEMA,
32575
32582
  url,
32576
- boundAt: row.boundAt,
32583
+ boundAt,
32577
32584
  cloud: true,
32578
32585
  ...tokenEnv ? { tokenEnv } : {},
32579
32586
  ...tokenCommand ? { tokenCommand } : {}
32580
32587
  };
32581
32588
  }
32589
+ function interpretBindingFile(file) {
32590
+ if (!existsSync16(file)) return {};
32591
+ const parsed = parseBindingObject(file);
32592
+ if (parsed instanceof HubBindingError) throw parsed;
32593
+ const keys = Object.keys(parsed);
32594
+ const unknown = keys.filter((key) => !BINDING_KEYS.has(key));
32595
+ if (parsed.schema !== HUB_BINDING_SCHEMA || unknown.length > 0) {
32596
+ return { warning: bindingFallbackWarning(file, `is newer than this build understands (${String(parsed.schema)})`) };
32597
+ }
32598
+ if (typeof parsed.url !== "string" || typeof parsed.boundAt !== "string" || !isIsoTimestamp(parsed.boundAt)) {
32599
+ throw new HubBindingError(`malformed hub binding at ${file}`);
32600
+ }
32601
+ let url;
32602
+ try {
32603
+ url = validateHubUrl(parsed.url);
32604
+ } catch {
32605
+ throw new HubBindingError(`malformed hub binding at ${file}`);
32606
+ }
32607
+ const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
32608
+ if (!cloudish) {
32609
+ if (keys.length !== 3) throw new HubBindingError(`malformed hub binding at ${file}`);
32610
+ return { record: { schema: HUB_BINDING_SCHEMA, url, boundAt: parsed.boundAt } };
32611
+ }
32612
+ const record2 = cloudRecordFromRow(file, parsed, url, parsed.boundAt);
32613
+ if (!record2) throw new HubBindingError(`malformed hub binding at ${file}`);
32614
+ return { record: record2, legacyCloud: true };
32615
+ }
32616
+ function loadHubBinding(env = process.env) {
32617
+ const cloudFile = hubBindingCloudFile(env);
32618
+ const cloud = interpretBindingFile(cloudFile);
32619
+ if (cloud.warning && !cloud.record) {
32620
+ const local = existsSync16(hubBindingFile(env)) ? interpretBindingFile(hubBindingFile(env)) : {};
32621
+ if (local.record && !local.record.cloud) return { record: local.record, warning: cloud.warning };
32622
+ return { warning: cloud.warning };
32623
+ }
32624
+ if (cloud.record?.cloud) return { record: cloud.record, ...cloud.warning ? { warning: cloud.warning } : {} };
32625
+ const main = interpretBindingFile(hubBindingFile(env));
32626
+ if (main.legacyCloud && main.record) {
32627
+ writeBindingJson(cloudFile, main.record);
32628
+ rmSync2(hubBindingFile(env), { force: true });
32629
+ return {
32630
+ record: main.record,
32631
+ warning: bindingFallbackWarning(
32632
+ hubBindingFile(env),
32633
+ "stored a cloud bind in the pre-0.7.160 file"
32634
+ )
32635
+ };
32636
+ }
32637
+ return { ...main.record ? { record: main.record } : {}, ...main.warning ? { warning: main.warning } : {} };
32638
+ }
32639
+ function readHubBinding(env = process.env) {
32640
+ return loadHubBinding(env).record;
32641
+ }
32582
32642
 
32583
32643
  // plugins/kxm/src/hub-env.ts
32584
32644
  import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync16, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync9 } from "node:fs";
@@ -32654,15 +32714,6 @@ function resolveProjectIdentity(cwd, env = process.env, explicit) {
32654
32714
  if (fromPackage) return { project: fromPackage, source: "package.json", sourceLabel: "package.json name" };
32655
32715
  return { project: basename4(cwd), source: "directory", sourceLabel: "directory name" };
32656
32716
  }
32657
- function opReferenceFromTokenCommand(command) {
32658
- try {
32659
- const args = splitTokenCommand(command);
32660
- if (args[0] === "op" && args[1] === "read" && args[2] && isHubOpReference(args[2])) return args[2];
32661
- } catch {
32662
- return void 0;
32663
- }
32664
- return void 0;
32665
- }
32666
32717
  function defaultOpRead(reference, env) {
32667
32718
  if (!isHubOpReference(reference)) {
32668
32719
  throw new CloudTokenError("cloud_token_command_invalid", `refusing to resolve ${reference}; an op:// reference is required`);
@@ -32707,18 +32758,24 @@ function resolveKeyReference(ref, env, opRead = defaultOpRead) {
32707
32758
  }
32708
32759
  function keySourceFor(input) {
32709
32760
  const ref = input.endpoint?.key;
32710
- if (input.mode === "local" && input.env.KXM_AUTH_TOKEN?.trim()) return "env:KXM_AUTH_TOKEN";
32711
- if (ref?.env && input.env[ref.env]?.trim()) return `env:${ref.env}`;
32712
- if (ref?.op) return `op:${ref.op}`;
32713
- if (ref?.env) return `env:${ref.env}`;
32761
+ const envName = (name) => {
32762
+ if (!name) return void 0;
32763
+ return input.env[name]?.trim() ? `env:${name}` : void 0;
32764
+ };
32714
32765
  if (input.mode === "cloud") {
32715
- if (input.binding?.tokenEnv) return `env:${input.binding.tokenEnv}`;
32716
- if (input.binding?.tokenCommand) {
32717
- const op = opReferenceFromTokenCommand(input.binding.tokenCommand);
32718
- return op ? `op:${op}` : "command";
32719
- }
32766
+ const fromEnv2 = envName(ref?.env);
32767
+ if (fromEnv2) return fromEnv2;
32768
+ if (ref?.op) return `op:${ref.op}`;
32769
+ const fromBindingEnv = envName(input.binding?.tokenEnv);
32770
+ if (fromBindingEnv) return fromBindingEnv;
32771
+ if (input.binding?.tokenCommand) return "token-command";
32720
32772
  return "missing";
32721
32773
  }
32774
+ const fromAuth = envName("KXM_AUTH_TOKEN");
32775
+ if (fromAuth) return fromAuth;
32776
+ const fromEnv = envName(ref?.env);
32777
+ if (fromEnv) return fromEnv;
32778
+ if (ref?.op) return `op:${ref.op}`;
32722
32779
  if (input.hasSavedProjectToken?.(input.project)) return "hub-env";
32723
32780
  return "missing";
32724
32781
  }
@@ -33379,8 +33436,17 @@ async function syncKxmOutbox(eventStore, client, options = {}) {
33379
33436
  }
33380
33437
  }
33381
33438
  }
33382
- function runtimeHubClientFor(context, env) {
33383
- const serverUrl = env.KXM_SERVER_URL?.trim() || readHubBinding(env)?.url;
33439
+ function runtimeHubClientFor(context, env, onBindingWarning) {
33440
+ let recordUrl;
33441
+ try {
33442
+ const loaded = loadHubBinding(env);
33443
+ if (loaded.warning) onBindingWarning?.(loaded.warning);
33444
+ recordUrl = loaded.record?.url;
33445
+ } catch (error) {
33446
+ if (!(error instanceof HubBindingError)) throw error;
33447
+ onBindingWarning?.(`${error.message}; falling back to the local hub so sync can continue`);
33448
+ }
33449
+ const serverUrl = env.KXM_SERVER_URL?.trim() || recordUrl;
33384
33450
  if (!serverUrl) return void 0;
33385
33451
  const project = context.projectId;
33386
33452
  const authToken = resolveClientHubAuthToken(env, project);
@@ -33908,6 +33974,7 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
33908
33974
  else logger.info(entry);
33909
33975
  };
33910
33976
  let syncing = false;
33977
+ let bindingWarning;
33911
33978
  const syncTimer = setInterval(() => {
33912
33979
  if (syncing || stopping) return;
33913
33980
  syncing = true;
@@ -33925,7 +33992,11 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
33925
33992
  consecutiveFailures: 0
33926
33993
  };
33927
33994
  try {
33928
- const client = runtimeHubClientFor(context, env);
33995
+ const client = runtimeHubClientFor(context, env, (message) => {
33996
+ if (bindingWarning === message) return;
33997
+ bindingWarning = message;
33998
+ logger.warn({ event: "hub_binding_fallback", message });
33999
+ });
33929
34000
  if (!client) {
33930
34001
  recordSyncStatus(context, { ...base, state: "no_hub" });
33931
34002
  continue;
@@ -23491,6 +23491,8 @@ var CloudTokenError = class extends Error {
23491
23491
  this.code = code;
23492
23492
  }
23493
23493
  };
23494
+ var LEGACY_BINDING_KEYS = ["schema", "url", "boundAt"];
23495
+ var BINDING_KEYS = /* @__PURE__ */ new Set([...LEGACY_BINDING_KEYS, "cloud", "tokenEnv", "tokenCommand"]);
23494
23496
 
23495
23497
  // plugins/kxm/src/hub-identity.ts
23496
23498
  var OP_TIMEOUT_MS = 15e3;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.166",
3
+ "version": "0.7.168",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -39,8 +39,10 @@ still blocks the tools.
39
39
  | `kxm update [harness] --extensions` | Update only extensions and plugins (Pi packages, the Claude kxm plugin) |
40
40
  | `kxm update [harness] --models` | Refresh model catalogs where the harness supports it |
41
41
 
42
- - `kxm update --kxm` downloads the GitHub release tarball by default, so it
43
- needs an authenticated `gh`.
42
+ - `kxm update --kxm` downloads the GitHub release tarball by default. It uses
43
+ `gh` when that CLI answers `gh --version`, and otherwise curl with the same
44
+ sha256 check. A host with neither can follow the curl commands in
45
+ docs/operations/upgrade.md.
44
46
  - `kxm update claude --extensions` updates only a user-scope plugin install;
45
47
  its dry run prints `claude extensions: would claude plugin update kxm -y`.
46
48
  A project-scope install is the operator's `claude plugin update kxm@kxm --scope project`.
@@ -77,10 +77,15 @@ kxm restore .kxm/backups/pre-upgrade/manifest.json
77
77
  `hub_bind_unauthenticated` unless a credential for the current project
78
78
  resolves from `KXM_AUTH_TOKEN` or the persisted `hub-env.json`.
79
79
  - `kxm hub bind --cloud` is the bind for a loopback SSH forward of a remote
80
- hub. It never falls back to this machine's hub-env token. Pass
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".
80
+ hub (`http://127.0.0.1:17331`). `https://hub.kxmd.dev` is an Authentik login
81
+ page, not a client URL. Bind refuses an HTML 200 or a 30x (`hub_not_kxm`)
82
+ unless `--force` is set. It never falls back to this machine's hub-env
83
+ token. Pass `--token-env KXMD_HUB_TOKEN`, `--key-op op://Private/kxmd/project-token`,
84
+ or `--token-command "op read op://..."`. `--token-command` is stored only in
85
+ the machine-wide `hub-binding.cloud.json`, not in `.kxm/config.yaml`.
86
+ `kxm hub view` names the source that would supply the token. See
87
+ docs/operations.md, section "Cross-box peer attach". Restart the supervisor,
88
+ hub, and workers after an upgrade and before a cloud bind.
84
89
  - Hub PID claims record the wrapper and server child PID. A dead wrapper's
85
90
  claim is reclaimed automatically, an orphaned server is terminated first,
86
91
  and `kxm hub stop` recovers such orphans directly.
@@ -13,8 +13,10 @@ import {
13
13
  HUB_BINDING_SCHEMA,
14
14
  CloudTokenError,
15
15
  HubBindingError,
16
+ hubBindingCloudFile,
16
17
  hubBindingFile,
17
18
  probeHubHealth,
19
+ readHubEndpoint,
18
20
  hubBindingScope,
19
21
  effectiveHubBindingScope,
20
22
  isCloudTokenEnvName,
@@ -181,8 +183,17 @@ export async function cmdStatus(runtime: Runtime): Promise<number> {
181
183
  }
182
184
  if (!(error instanceof HubBindingError)) throw error;
183
185
  }
184
- const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl, headers);
185
- const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl, headers);
186
+ const health = await readHubEndpoint(runtime.serverUrl, "health", runtime.fetchImpl, headers ? { headers } : {});
187
+ const ready = await readHubEndpoint(runtime.serverUrl, "ready", runtime.fetchImpl, headers ? { headers } : {});
188
+ const proxyKind = [health.kind, ready.kind].find((kind) => kind === "redirect" || kind === "auth_proxy");
189
+ const notHub = !proxyKind && (health.kind === "not_hub" || ready.kind === "not_hub");
190
+ const probeNote = proxyKind === "redirect"
191
+ ? " · auth proxy or redirect"
192
+ : proxyKind === "auth_proxy"
193
+ ? " · auth proxy (HTML login page)"
194
+ : notHub
195
+ ? " · not a kxm hub"
196
+ : "";
186
197
  // Scope on the status line deliberately: "attached across a network" and "attached on
187
198
  // this box" are otherwise indistinguishable, and only one of them ships a token.
188
199
  // Scope is a property of the URL actually contacted, not of whichever file the
@@ -210,7 +221,7 @@ export async function cmdStatus(runtime: Runtime): Promise<number> {
210
221
  ready: ready.body,
211
222
  };
212
223
  print(runtime.io, runtime.json, payload,
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}`);
224
+ `hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} · mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}${probeNote}`);
214
225
  return payload.ok ? 0 : 1;
215
226
  }
216
227
 
@@ -314,6 +325,7 @@ export interface HubBindOptions {
314
325
  tokenEnv?: string | undefined;
315
326
  tokenCommand?: string | undefined;
316
327
  keyOp?: string | undefined;
328
+ force?: boolean | undefined;
317
329
  }
318
330
 
319
331
  export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubBindOptions = {}): Promise<number> {
@@ -444,7 +456,7 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
444
456
  return 2;
445
457
  }
446
458
  }
447
- const file = hubBindingFile(runtime.env);
459
+ const file = cloud ? hubBindingCloudFile(runtime.env) : hubBindingFile(runtime.env);
448
460
  const bindingCommand = tokenCommand ?? (cloud && keyOp ? `op read ${keyOp}` : undefined);
449
461
  const cloudFields = cloud
450
462
  ? { cloud: true as const, ...(tokenEnv ? { tokenEnv } : {}), ...(bindingCommand ? { tokenCommand: bindingCommand } : {}) }
@@ -471,6 +483,27 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
471
483
  );
472
484
  return 0;
473
485
  }
486
+ const probe = await probeHubHealth(url, runtime.fetchImpl);
487
+ const acceptable = probe.kind === "hub" || probe.kind === "timeout" || probe.kind === "unreachable";
488
+ if (!acceptable && options.force !== true) {
489
+ print(
490
+ runtime.io,
491
+ runtime.json,
492
+ {
493
+ ok: false,
494
+ command: "hub bind",
495
+ error: "hub_not_kxm",
496
+ url,
497
+ scope,
498
+ kind: probe.kind,
499
+ status: probe.status,
500
+ detail: probe.detail,
501
+ nextAction: "bind_a_kxm_hub_or_pass_force",
502
+ },
503
+ `refusing to bind ${url}: ${probe.detail}; no binding was written. Pass --force to bind anyway`,
504
+ );
505
+ return 2;
506
+ }
474
507
  try {
475
508
  writeHubEndpoint(runtime.dirs.workdir, {
476
509
  mode: cloud ? "cloud" : "local",
@@ -491,9 +524,11 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
491
524
  throw error;
492
525
  }
493
526
  writeHubBinding({ ...record, boundAt: new Date().toISOString() }, runtime.env);
494
- const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
495
- print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, configFile, project: identity.project, health, probeMs, ...cloudFields },
496
- `bound hub ${url} · ${scope} · ${formatHubBindHealth(health)}${cloud ? cloudNote : scope === "remote" ? " · token leaves this machine" : ""}`);
527
+ const healthText = acceptable
528
+ ? formatHubBindHealth(probe.health)
529
+ : `${probe.detail} (--force)`;
530
+ print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, configFile, project: identity.project, health: probe.health, probeMs: probe.probeMs, kind: probe.kind, ...cloudFields },
531
+ `bound hub ${url} · ${scope} · ${healthText}${cloud ? cloudNote : scope === "remote" ? " · token leaves this machine" : ""}`);
497
532
  return 0;
498
533
  }
499
534
 
@@ -75,6 +75,7 @@ import {
75
75
  import {
76
76
  installedKxmVersion,
77
77
  fetchLatestKxmVersion,
78
+ ghReleaseDownloadAvailable,
78
79
  kxmReleaseAssetName,
79
80
  noticeFromVersions,
80
81
  planKxmPackageUpdate,
@@ -159,7 +160,8 @@ export function applyKxmPackageUpdate(runtime: Runtime, notice: KxmUpdateNotice)
159
160
  }
160
161
  const releaseDir = mkdtempSync(join(tmpdir(), "kxm-pkg-update-"));
161
162
  try {
162
- const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset);
163
+ const downloader = notice.source === "github" && !ghReleaseDownloadAvailable(runtime.env) ? "curl" : "gh";
164
+ const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset, downloader);
163
165
  if (runtime.dryRun) {
164
166
  return { ok: true, detail: planned.map(formatPackageUpdateStep).join(" && ") };
165
167
  }
@@ -10,7 +10,7 @@ import {
10
10
  HubBindingError,
11
11
  effectiveHubBindingScope,
12
12
  hubBindingFile,
13
- readHubBinding,
13
+ loadHubBinding,
14
14
  } from "../hub-binding.ts";
15
15
  import { describeHubConnection } from "../hub-identity.ts";
16
16
  import type { InstallProbe } from "../kxm-install-kind.ts";
@@ -280,10 +280,12 @@ export function runtimeFrom(ctx: CliContext, command: Command): Runtime {
280
280
  const envServerUrl = ctx.env.KXM_SERVER_URL?.trim();
281
281
  let boundHubUrl: string | undefined;
282
282
  try {
283
- boundHubUrl = readHubBinding(ctx.env)?.url;
283
+ const loaded = loadHubBinding(ctx.env);
284
+ if (loaded.warning) ctx.io.stderr(`kxm: ${loaded.warning}\n`);
285
+ boundHubUrl = loaded.record?.url;
284
286
  } catch (error) {
285
287
  if (error instanceof HubBindingError) {
286
- ctx.io.stderr(`kxm: ignoring malformed hub binding at ${hubBindingFile(ctx.env)}; run kxm hub bind <url> again\n`);
288
+ ctx.io.stderr(`kxm: ignoring malformed hub binding at ${hubBindingFile(ctx.env)}; falling back to the local hub. Run kxm hub bind <url> again\n`);
287
289
  } else {
288
290
  throw error;
289
291
  }
@@ -1374,7 +1374,8 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
1374
1374
  .option("--token-env <name>", "Environment variable that holds the hub token; the token is not stored")
1375
1375
  .option("--token-command <command>", "Program that prints the hub token; the token is not stored")
1376
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 }) {
1377
+ .option("--force", "Bind even when the URL is not a kxm hub")
1378
+ .action(async function hubBindAction(this: Command, url: string, options: { cloud?: boolean; tokenEnv?: string; tokenCommand?: string; keyOp?: string; force?: boolean }) {
1378
1379
  result.code = await cmdHubBind(runtimeFrom(ctx, this), url, options);
1379
1380
  });
1380
1381
  addGlobalOptions(hub.command("unbind").description("Remove this machine's hub binding")).action(bind(cmdHubUnbind));