@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +30 -0
- package/docs/contributing/ci-and-release.md +6 -1
- package/docs/operations/runtime-sync.md +2 -1
- package/docs/operations/troubleshooting.md +4 -0
- package/docs/operations/upgrade.md +18 -3
- package/docs/operations.md +70 -7
- package/docs/reference/cli-reference.md +21 -6
- package/docs/reference/config-reference.md +28 -13
- package/docs/start/install.md +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/claude-hook.js +170 -36
- package/plugins/kxm/dist/cli.js +451 -132
- package/plugins/kxm/dist/extension.js +184 -53
- package/plugins/kxm/dist/mcp-server.js +97 -40
- package/plugins/kxm/dist/runtime-supervisor.js +240 -47
- package/plugins/kxm/dist/runtime.js +240 -47
- package/plugins/kxm/dist/server.js +9 -0
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +3 -2
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +4 -2
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -4
- package/plugins/kxm/src/cli/hub.ts +42 -7
- package/plugins/kxm/src/cli/system.ts +3 -1
- package/plugins/kxm/src/cli/types.ts +5 -3
- package/plugins/kxm/src/cli.ts +2 -1
- package/plugins/kxm/src/engine.ts +105 -8
- package/plugins/kxm/src/hub-binding.ts +301 -55
- package/plugins/kxm/src/hub-identity.ts +17 -9
- package/plugins/kxm/src/kxm-update.ts +29 -6
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/project-config.ts +63 -1
- package/plugins/kxm/src/runtime-supervisor.ts +22 -4
- package/scripts/kxm-publish-npm.mjs +47 -1
|
@@ -556,6 +556,9 @@ function resolveUserStateRoot(env) {
|
|
|
556
556
|
function hubBindingFile(env = process.env) {
|
|
557
557
|
return join3(resolveUserStateRoot(env), "hub-binding.json");
|
|
558
558
|
}
|
|
559
|
+
function hubBindingCloudFile(env = process.env) {
|
|
560
|
+
return join3(resolveUserStateRoot(env), "hub-binding.cloud.json");
|
|
561
|
+
}
|
|
559
562
|
function validateHubUrl(raw) {
|
|
560
563
|
let parsed;
|
|
561
564
|
try {
|
|
@@ -618,33 +621,35 @@ function isAbortError(error) {
|
|
|
618
621
|
error && typeof error === "object" && ("name" in error && error.name === "AbortError" || "code" in error && error.code === "ABORT_ERR")
|
|
619
622
|
);
|
|
620
623
|
}
|
|
621
|
-
function
|
|
622
|
-
|
|
623
|
-
|
|
624
|
+
function bindingFallbackWarning(file, reason) {
|
|
625
|
+
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.`;
|
|
626
|
+
}
|
|
627
|
+
function writeBindingJson(file, value) {
|
|
628
|
+
mkdirSync3(dirname(file), { recursive: true, mode: 448 });
|
|
629
|
+
const temporary = join3(dirname(file), `.${file.endsWith("cloud.json") ? "hub-binding-cloud" : "hub-binding"}-${process.pid}.tmp`);
|
|
630
|
+
try {
|
|
631
|
+
writeFileSync2(temporary, `${JSON.stringify(value, null, 2)}
|
|
632
|
+
`, { encoding: "utf8", mode: 384 });
|
|
633
|
+
renameSync(temporary, file);
|
|
634
|
+
} finally {
|
|
635
|
+
rmSync(temporary, { force: true });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function parseBindingObject(file) {
|
|
639
|
+
if (!existsSync4(file)) return new HubBindingError(`missing hub binding at ${file}`);
|
|
624
640
|
let parsed;
|
|
625
641
|
try {
|
|
626
642
|
parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
627
643
|
} catch {
|
|
628
|
-
|
|
644
|
+
return new HubBindingError(`malformed hub binding at ${file}`);
|
|
629
645
|
}
|
|
630
646
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
const row = parsed;
|
|
634
|
-
const keys = Object.keys(row);
|
|
635
|
-
const allowed = /* @__PURE__ */ new Set(["schema", "url", "boundAt", "cloud", "tokenEnv", "tokenCommand"]);
|
|
636
|
-
const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
|
|
637
|
-
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)) {
|
|
638
|
-
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
639
|
-
}
|
|
640
|
-
let url;
|
|
641
|
-
try {
|
|
642
|
-
url = validateHubUrl(row.url);
|
|
643
|
-
} catch {
|
|
644
|
-
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
647
|
+
return new HubBindingError(`malformed hub binding at ${file}`);
|
|
645
648
|
}
|
|
646
|
-
|
|
647
|
-
|
|
649
|
+
return parsed;
|
|
650
|
+
}
|
|
651
|
+
function cloudRecordFromRow(file, row, url, boundAt) {
|
|
652
|
+
if (row.cloud !== true) return void 0;
|
|
648
653
|
let tokenEnv;
|
|
649
654
|
let tokenCommand;
|
|
650
655
|
if (row.tokenEnv !== void 0) {
|
|
@@ -666,36 +671,163 @@ function readHubBinding(env = process.env) {
|
|
|
666
671
|
return {
|
|
667
672
|
schema: HUB_BINDING_SCHEMA,
|
|
668
673
|
url,
|
|
669
|
-
boundAt
|
|
674
|
+
boundAt,
|
|
670
675
|
cloud: true,
|
|
671
676
|
...tokenEnv ? { tokenEnv } : {},
|
|
672
677
|
...tokenCommand ? { tokenCommand } : {}
|
|
673
678
|
};
|
|
674
679
|
}
|
|
680
|
+
function interpretBindingFile(file) {
|
|
681
|
+
if (!existsSync4(file)) return {};
|
|
682
|
+
const parsed = parseBindingObject(file);
|
|
683
|
+
if (parsed instanceof HubBindingError) throw parsed;
|
|
684
|
+
const keys = Object.keys(parsed);
|
|
685
|
+
const unknown = keys.filter((key) => !BINDING_KEYS.has(key));
|
|
686
|
+
if (parsed.schema !== HUB_BINDING_SCHEMA || unknown.length > 0) {
|
|
687
|
+
return { warning: bindingFallbackWarning(file, `is newer than this build understands (${String(parsed.schema)})`) };
|
|
688
|
+
}
|
|
689
|
+
if (typeof parsed.url !== "string" || typeof parsed.boundAt !== "string" || !isIsoTimestamp(parsed.boundAt)) {
|
|
690
|
+
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
691
|
+
}
|
|
692
|
+
let url;
|
|
693
|
+
try {
|
|
694
|
+
url = validateHubUrl(parsed.url);
|
|
695
|
+
} catch {
|
|
696
|
+
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
697
|
+
}
|
|
698
|
+
const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
|
|
699
|
+
if (!cloudish) {
|
|
700
|
+
if (keys.length !== 3) throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
701
|
+
return { record: { schema: HUB_BINDING_SCHEMA, url, boundAt: parsed.boundAt } };
|
|
702
|
+
}
|
|
703
|
+
const record = cloudRecordFromRow(file, parsed, url, parsed.boundAt);
|
|
704
|
+
if (!record) throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
705
|
+
return { record, legacyCloud: true };
|
|
706
|
+
}
|
|
707
|
+
function loadHubBinding(env = process.env) {
|
|
708
|
+
const cloudFile = hubBindingCloudFile(env);
|
|
709
|
+
const cloud = interpretBindingFile(cloudFile);
|
|
710
|
+
if (cloud.warning && !cloud.record) {
|
|
711
|
+
const local = existsSync4(hubBindingFile(env)) ? interpretBindingFile(hubBindingFile(env)) : {};
|
|
712
|
+
if (local.record && !local.record.cloud) return { record: local.record, warning: cloud.warning };
|
|
713
|
+
return { warning: cloud.warning };
|
|
714
|
+
}
|
|
715
|
+
if (cloud.record?.cloud) return { record: cloud.record, ...cloud.warning ? { warning: cloud.warning } : {} };
|
|
716
|
+
const main2 = interpretBindingFile(hubBindingFile(env));
|
|
717
|
+
if (main2.legacyCloud && main2.record) {
|
|
718
|
+
writeBindingJson(cloudFile, main2.record);
|
|
719
|
+
rmSync(hubBindingFile(env), { force: true });
|
|
720
|
+
return {
|
|
721
|
+
record: main2.record,
|
|
722
|
+
warning: bindingFallbackWarning(
|
|
723
|
+
hubBindingFile(env),
|
|
724
|
+
"stored a cloud bind in the pre-0.7.160 file"
|
|
725
|
+
)
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return { ...main2.record ? { record: main2.record } : {}, ...main2.warning ? { warning: main2.warning } : {} };
|
|
729
|
+
}
|
|
730
|
+
function readHubBinding(env = process.env) {
|
|
731
|
+
return loadHubBinding(env).record;
|
|
732
|
+
}
|
|
733
|
+
function contentTypeBase(header) {
|
|
734
|
+
return (header ?? "").split(";")[0].trim().toLowerCase();
|
|
735
|
+
}
|
|
736
|
+
function looksLikeHtml(contentType, body) {
|
|
737
|
+
if (contentType === "text/html" || contentType === "application/xhtml+xml") return true;
|
|
738
|
+
const sample = body.slice(0, 800).trim().toLowerCase();
|
|
739
|
+
return sample.startsWith("<!doctype html") || sample.startsWith("<html") || sample.includes("<form") && (sample.includes("login") || sample.includes("authentik") || sample.includes("password"));
|
|
740
|
+
}
|
|
741
|
+
function jsonObject(text) {
|
|
742
|
+
try {
|
|
743
|
+
const parsed = JSON.parse(text);
|
|
744
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
745
|
+
return parsed;
|
|
746
|
+
} catch {
|
|
747
|
+
return void 0;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function hubEndpointShape(role, body) {
|
|
751
|
+
if (role === "health") return body.ok === true && typeof body.agents === "number";
|
|
752
|
+
return body.ok === true && (body.storage === "sqlite" || body.storage === "memory");
|
|
753
|
+
}
|
|
754
|
+
function classifyHubResponse(input) {
|
|
755
|
+
const contentType = contentTypeBase(input.contentType);
|
|
756
|
+
if (input.status >= 300 && input.status < 400) {
|
|
757
|
+
return {
|
|
758
|
+
ok: false,
|
|
759
|
+
status: input.status,
|
|
760
|
+
kind: "redirect",
|
|
761
|
+
detail: `HTTP ${input.status} redirect; this looks like an auth proxy or a redirect, not a kxm hub`,
|
|
762
|
+
body: { error: "auth_proxy", detail: `HTTP ${input.status} redirect` }
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
if (looksLikeHtml(contentType, input.bodyText)) {
|
|
766
|
+
return {
|
|
767
|
+
ok: false,
|
|
768
|
+
status: input.status,
|
|
769
|
+
kind: "auth_proxy",
|
|
770
|
+
detail: `HTTP ${input.status} returned an HTML login page${contentType ? ` (${contentType})` : ""}; this looks like an auth proxy, not a kxm hub`,
|
|
771
|
+
body: { error: "auth_proxy", detail: "HTML login page" }
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const jsonType = contentType === "application/json" || contentType === "text/plain" || contentType === "";
|
|
775
|
+
const body = jsonType ? jsonObject(input.bodyText) : void 0;
|
|
776
|
+
if (body && hubEndpointShape(input.role, body) && input.status >= 200 && input.status < 300) {
|
|
777
|
+
if (contentType !== "" && contentType !== "application/json" && contentType !== "text/plain") {
|
|
778
|
+
return {
|
|
779
|
+
ok: false,
|
|
780
|
+
status: input.status,
|
|
781
|
+
kind: "not_hub",
|
|
782
|
+
detail: `HTTP ${input.status} content-type ${contentType} is not application/json`,
|
|
783
|
+
body: { error: "not_hub", detail: contentType }
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
return { ok: true, status: input.status, kind: "hub", detail: "kxm hub", body };
|
|
787
|
+
}
|
|
788
|
+
const typeNote = contentType && contentType !== "application/json" ? ` content-type ${contentType}` : "";
|
|
789
|
+
return {
|
|
790
|
+
ok: false,
|
|
791
|
+
status: input.status,
|
|
792
|
+
kind: "not_hub",
|
|
793
|
+
detail: `HTTP ${input.status}${typeNote} is not a kxm hub ${input.role} response`,
|
|
794
|
+
body: { error: "not_hub", detail: `HTTP ${input.status}${typeNote}` }
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
async function readHubEndpoint(url, role, fetchImpl, init = {}) {
|
|
798
|
+
try {
|
|
799
|
+
const response = await fetchImpl(`${url}/${role}`, {
|
|
800
|
+
...init.headers ? { headers: init.headers } : {},
|
|
801
|
+
...init.signal ? { signal: init.signal } : {},
|
|
802
|
+
redirect: "manual"
|
|
803
|
+
});
|
|
804
|
+
const bodyText = await response.text();
|
|
805
|
+
return classifyHubResponse({
|
|
806
|
+
role,
|
|
807
|
+
status: response.status,
|
|
808
|
+
contentType: response.headers.get("content-type"),
|
|
809
|
+
bodyText
|
|
810
|
+
});
|
|
811
|
+
} catch (error) {
|
|
812
|
+
if (isAbortError(error)) {
|
|
813
|
+
return { ok: false, status: 0, kind: "timeout", detail: "no reply within the probe budget", body: { error: "hub_unreachable" } };
|
|
814
|
+
}
|
|
815
|
+
return { ok: false, status: 0, kind: "unreachable", detail: "nothing answered", body: { error: "hub_unreachable" } };
|
|
816
|
+
}
|
|
817
|
+
}
|
|
675
818
|
async function probeHubHealth(url, fetchImpl, timeoutMs = HUB_HEALTH_PROBE_MS) {
|
|
676
819
|
const started = Date.now();
|
|
677
820
|
const controller = new AbortController();
|
|
678
821
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
679
822
|
try {
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
try {
|
|
684
|
-
body = await response.json();
|
|
685
|
-
} catch {
|
|
686
|
-
return { health: "unknown", probeMs: Date.now() - started };
|
|
687
|
-
}
|
|
688
|
-
if (body && typeof body === "object" && body.ok === true) {
|
|
689
|
-
return { health: "on", probeMs: Date.now() - started };
|
|
690
|
-
}
|
|
691
|
-
return { health: "unknown", probeMs: Date.now() - started };
|
|
692
|
-
} catch (error) {
|
|
693
|
-
return { health: isAbortError(error) ? "unknown" : "off", probeMs: Date.now() - started };
|
|
823
|
+
const verdict = await readHubEndpoint(url, "health", fetchImpl, { signal: controller.signal });
|
|
824
|
+
const health = verdict.kind === "hub" ? "on" : verdict.kind === "unreachable" ? "off" : "unknown";
|
|
825
|
+
return { health, probeMs: Date.now() - started, kind: verdict.kind, detail: verdict.detail, status: verdict.status };
|
|
694
826
|
} finally {
|
|
695
827
|
clearTimeout(timer);
|
|
696
828
|
}
|
|
697
829
|
}
|
|
698
|
-
var HUB_BINDING_SCHEMA, HUB_HEALTH_PROBE_MS, CloudTokenError, TOKEN_ENV_NAME, HubBindingError;
|
|
830
|
+
var HUB_BINDING_SCHEMA, HUB_HEALTH_PROBE_MS, CloudTokenError, TOKEN_ENV_NAME, HubBindingError, LEGACY_BINDING_KEYS, BINDING_KEYS;
|
|
699
831
|
var init_hub_binding = __esm({
|
|
700
832
|
"plugins/kxm/src/hub-binding.ts"() {
|
|
701
833
|
"use strict";
|
|
@@ -716,6 +848,8 @@ var init_hub_binding = __esm({
|
|
|
716
848
|
this.name = "HubBindingError";
|
|
717
849
|
}
|
|
718
850
|
};
|
|
851
|
+
LEGACY_BINDING_KEYS = ["schema", "url", "boundAt"];
|
|
852
|
+
BINDING_KEYS = /* @__PURE__ */ new Set([...LEGACY_BINDING_KEYS, "cloud", "tokenEnv", "tokenCommand"]);
|
|
719
853
|
}
|
|
720
854
|
});
|
|
721
855
|
|