@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +18 -0
- package/docs/README.md +1 -0
- package/docs/architecture/hosted-direction.md +9 -4
- package/docs/architecture/inventory.md +1 -0
- package/docs/contributing/ci-and-release.md +6 -1
- package/docs/contributing/decisions.md +153 -0
- package/docs/contributing/learnings.md +54 -53
- package/docs/contributing/operating-rules.md +19 -6
- 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 +4 -1
- 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 +332 -129
- package/plugins/kxm/dist/extension.js +184 -53
- package/plugins/kxm/dist/mcp-server.js +97 -40
- package/plugins/kxm/dist/runtime-supervisor.js +113 -42
- package/plugins/kxm/dist/runtime.js +113 -42
- package/plugins/kxm/dist/server.js +2 -0
- package/plugins/kxm/package.json +1 -1
- 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/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/runtime-supervisor.ts +22 -4
- 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
|
-
|
|
31781
|
-
|
|
31782
|
-
|
|
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
|
-
|
|
31805
|
+
return new HubBindingError(`malformed hub binding at ${file}`);
|
|
31788
31806
|
}
|
|
31789
31807
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
31790
|
-
|
|
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
|
-
|
|
31800
|
-
|
|
31801
|
-
|
|
31802
|
-
|
|
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
|
|
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
|
-
|
|
31963
|
-
|
|
31964
|
-
|
|
31965
|
-
|
|
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
|
-
|
|
31968
|
-
if (
|
|
31969
|
-
|
|
31970
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32529
|
-
|
|
32530
|
-
|
|
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
|
-
|
|
32553
|
+
return new HubBindingError(`malformed hub binding at ${file}`);
|
|
32536
32554
|
}
|
|
32537
32555
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
32538
|
-
|
|
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
|
-
|
|
32548
|
-
|
|
32549
|
-
|
|
32550
|
-
|
|
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
|
|
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
|
-
|
|
32711
|
-
|
|
32712
|
-
|
|
32713
|
-
|
|
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
|
-
|
|
32716
|
-
if (
|
|
32717
|
-
|
|
32718
|
-
|
|
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
|
-
|
|
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;
|
package/plugins/kxm/package.json
CHANGED
|
@@ -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
|
|
43
|
-
|
|
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.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
185
|
-
const ready = await
|
|
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
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)};
|
|
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
|
}
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -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
|
-
.
|
|
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));
|