@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -43,6 +44,21 @@ export function kxmReleaseAssetName(version: string): string {
|
|
|
43
44
|
return `kxm-${version}.tgz`;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export function kxmReleaseDownloadUrl(version: string, name = kxmReleaseAssetName(version)): string {
|
|
48
|
+
return `https://github.com/kontextmind/kxm/releases/download/v${version}/${name}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `gh` is optional. Hosts without it download the same asset with curl and still verify the digest. */
|
|
52
|
+
export function ghReleaseDownloadAvailable(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
53
|
+
const result = spawnSync("gh", ["--version"], {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
env,
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
timeout: 15_000,
|
|
58
|
+
});
|
|
59
|
+
return !result.error && result.status === 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
export function readInstalledKxmVersion(root: string): string {
|
|
47
63
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version?: unknown };
|
|
48
64
|
if (typeof pkg.version !== "string" || !parseSemver(pkg.version)) {
|
|
@@ -213,7 +229,7 @@ export async function fetchLatestKxmVersion(
|
|
|
213
229
|
}
|
|
214
230
|
|
|
215
231
|
export type KxmPackageUpdateStep =
|
|
216
|
-
| { kind: "download"; command: "gh"; args: string[] }
|
|
232
|
+
| { kind: "download"; command: "gh" | "curl"; args: string[] }
|
|
217
233
|
| { kind: "verify"; path: string; sha256: string }
|
|
218
234
|
| { kind: "install"; command: "npm"; args: string[] };
|
|
219
235
|
|
|
@@ -233,17 +249,24 @@ export function planKxmPackageUpdate(
|
|
|
233
249
|
latest: string,
|
|
234
250
|
releaseDir: string,
|
|
235
251
|
asset?: KxmReleaseAsset,
|
|
252
|
+
downloader: "gh" | "curl" = "gh",
|
|
236
253
|
): KxmPackageUpdateStep[] {
|
|
237
254
|
if (source === "npm") {
|
|
238
255
|
return [{ kind: "install", command: "npm", args: ["install", "--global", "--omit=peer", `@kontextmind/kxm@${latest}`] }];
|
|
239
256
|
}
|
|
240
257
|
const name = asset?.name ?? kxmReleaseAssetName(latest);
|
|
241
258
|
const steps: KxmPackageUpdateStep[] = [
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
259
|
+
downloader === "curl"
|
|
260
|
+
? {
|
|
261
|
+
kind: "download",
|
|
262
|
+
command: "curl",
|
|
263
|
+
args: ["--fail", "--silent", "--show-error", "--location", "--output", join(releaseDir, name), kxmReleaseDownloadUrl(latest, name)],
|
|
264
|
+
}
|
|
265
|
+
: {
|
|
266
|
+
kind: "download",
|
|
267
|
+
command: "gh",
|
|
268
|
+
args: ["release", "download", `v${latest}`, "--repo", "kontextmind/kxm", "--pattern", name, "--dir", releaseDir, "--clobber"],
|
|
269
|
+
},
|
|
247
270
|
];
|
|
248
271
|
if (asset?.sha256) {
|
|
249
272
|
steps.push({ kind: "verify", path: join(releaseDir, name), sha256: asset.sha256 });
|
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.169";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -104,7 +104,40 @@ const RESOURCE_SCHEMA: Readonly<Record<KxmResourceKind, { identity: string; file
|
|
|
104
104
|
const IDENTIFIER = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
105
105
|
const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³]|conin\$|conout\$|clock\$)$/i;
|
|
106
106
|
const BUILTIN_EXECUTORS = ["local", "ssh", "exe-dev"];
|
|
107
|
-
|
|
107
|
+
/** Builtin preset to the harness one-shot profile. An agent preset may only narrow. */
|
|
108
|
+
export const TOOL_PRESET_PROFILES = Object.freeze({
|
|
109
|
+
coordinator: "read-only",
|
|
110
|
+
"read-only": "read-only",
|
|
111
|
+
"workspace-writer": "edit",
|
|
112
|
+
"tests-writer": "edit",
|
|
113
|
+
} as const);
|
|
114
|
+
const BUILTIN_TOOL_PRESETS = Object.keys(TOOL_PRESET_PROFILES);
|
|
115
|
+
|
|
116
|
+
export function toolPresetProfile(preset: string): "read-only" | "edit" | undefined {
|
|
117
|
+
if (!Object.hasOwn(TOOL_PRESET_PROFILES, preset)) return undefined;
|
|
118
|
+
return TOOL_PRESET_PROFILES[preset as keyof typeof TOOL_PRESET_PROFILES];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Set when `preset` is wider than the role permission, or wider than a
|
|
123
|
+
* registered role preset. Same text at load and at dispatch.
|
|
124
|
+
*/
|
|
125
|
+
export function toolPresetExceedsRoleMessage(
|
|
126
|
+
preset: string,
|
|
127
|
+
roleId: string,
|
|
128
|
+
rolePermission: "edit" | "read-only",
|
|
129
|
+
rolePreset: string | undefined,
|
|
130
|
+
): string | undefined {
|
|
131
|
+
const profile = toolPresetProfile(preset);
|
|
132
|
+
if (profile !== "edit") return undefined;
|
|
133
|
+
if (rolePermission === "read-only") {
|
|
134
|
+
return `tools.preset ${preset} exceeds role ${roleId} permission read-only`;
|
|
135
|
+
}
|
|
136
|
+
if (rolePreset !== undefined && toolPresetProfile(rolePreset) === "read-only") {
|
|
137
|
+
return `tools.preset ${preset} exceeds role ${roleId} preset ${rolePreset}`;
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
108
141
|
const SECRET_VALUE_PATTERNS: readonly RegExp[] = [
|
|
109
142
|
/-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----/,
|
|
110
143
|
/\b(?:ghp|github_pat)_[A-Za-z0-9_]{20,}\b/,
|
|
@@ -583,6 +616,16 @@ function stringValue(value: JsonValue | undefined): string | undefined {
|
|
|
583
616
|
return typeof value === "string" ? value : undefined;
|
|
584
617
|
}
|
|
585
618
|
|
|
619
|
+
function roleById(roles: ReadonlyMap<string, KxmResource>, roleId: string): KxmResource | undefined {
|
|
620
|
+
const direct = roles.get(roleId);
|
|
621
|
+
if (direct) return direct;
|
|
622
|
+
const folded = roleId.toLocaleLowerCase("en-US");
|
|
623
|
+
for (const role of roles.values()) {
|
|
624
|
+
if ((role.id ?? "").toLocaleLowerCase("en-US") === folded) return role;
|
|
625
|
+
}
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
|
|
586
629
|
function numberValue(value: JsonValue | undefined, fallback: number): number {
|
|
587
630
|
return typeof value === "number" ? value : fallback;
|
|
588
631
|
}
|
|
@@ -1224,10 +1267,29 @@ function validateBundle(
|
|
|
1224
1267
|
if (executor && !executors.has(executor)) issues.push(issue("reference", "executor_unknown", agent.logicalPath, `executor ${executor} is not registered`));
|
|
1225
1268
|
const preset = stringValue(objectValue(agent.value.tools)?.preset);
|
|
1226
1269
|
if (preset && !presets.has(preset)) issues.push(issue("reference", "tool_preset_unknown", agent.logicalPath, `tool preset ${preset} is not registered`));
|
|
1270
|
+
const roleId = stringValue(agent.value.role);
|
|
1271
|
+
const role = roleId ? roleById(roles, roleId) : undefined;
|
|
1272
|
+
const rolePermission = stringValue(role?.value.permission);
|
|
1273
|
+
if (preset && role && (rolePermission === "edit" || rolePermission === "read-only")) {
|
|
1274
|
+
const message = toolPresetExceedsRoleMessage(
|
|
1275
|
+
preset,
|
|
1276
|
+
role.id ?? roleId ?? "role",
|
|
1277
|
+
rolePermission,
|
|
1278
|
+
stringValue(objectValue(role.value.tools)?.preset),
|
|
1279
|
+
);
|
|
1280
|
+
if (message) issues.push(issue("semantic", "tool_preset_exceeds_role", agent.logicalPath, message));
|
|
1281
|
+
}
|
|
1227
1282
|
for (const repositoryId of Object.keys(objectValue(agent.value.repositories) ?? {})) {
|
|
1228
1283
|
if (!repositoryIds.has(repositoryId)) issues.push(issue("reference", "repository_unknown", agent.logicalPath, `references unknown repository ${repositoryId}`));
|
|
1229
1284
|
}
|
|
1230
1285
|
}
|
|
1286
|
+
for (const role of roles.values()) {
|
|
1287
|
+
const permission = stringValue(role.value.permission);
|
|
1288
|
+
const rolePreset = stringValue(objectValue(role.value.tools)?.preset);
|
|
1289
|
+
if (permission === "read-only" && rolePreset && toolPresetProfile(rolePreset) === "edit") {
|
|
1290
|
+
issues.push(issue("semantic", "tool_preset_exceeds_role", role.logicalPath, `tools.preset ${rolePreset} exceeds permission read-only`));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1231
1293
|
validateModelReferences(models, issues);
|
|
1232
1294
|
|
|
1233
1295
|
const defaultWorkflow = stringValue(project.value.defaultWorkflow) ?? "default";
|
|
@@ -31,7 +31,7 @@ import { createKxmOneShotProducer } from "./oneshot-producer.ts";
|
|
|
31
31
|
import { KxmRunScheduler, createKxmSimulatedProducer, recordDriveReceipt, recoverKxmRun, kxmDrivePollProjection } from "./engine.ts";
|
|
32
32
|
import { kxmDriveSession, kxmOpenDriveSessions } from "./runtime-owner.ts";
|
|
33
33
|
import { RuntimeHubClient, HubHttpError, type SyncPushResponse } from "./client.ts";
|
|
34
|
-
import { CloudTokenError,
|
|
34
|
+
import { CloudTokenError, HubBindingError, loadHubBinding } from "./hub-binding.ts";
|
|
35
35
|
import { resolveClientHubAuthToken } from "./hub-env.ts";
|
|
36
36
|
import { createLogger } from "./logger.ts";
|
|
37
37
|
import type { KxmOutboxRow, KxmOutboxStatus } from "./runtime-store.ts";
|
|
@@ -572,8 +572,21 @@ export async function syncKxmOutbox(
|
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
/** Where this Runtime reports one project, or nothing when no hub is bound. */
|
|
575
|
-
function runtimeHubClientFor(
|
|
576
|
-
|
|
575
|
+
function runtimeHubClientFor(
|
|
576
|
+
context: KxmRuntimeContext,
|
|
577
|
+
env: NodeJS.ProcessEnv,
|
|
578
|
+
onBindingWarning?: (message: string) => void,
|
|
579
|
+
): RuntimeHubClient | undefined {
|
|
580
|
+
let recordUrl: string | undefined;
|
|
581
|
+
try {
|
|
582
|
+
const loaded = loadHubBinding(env);
|
|
583
|
+
if (loaded.warning) onBindingWarning?.(loaded.warning);
|
|
584
|
+
recordUrl = loaded.record?.url;
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (!(error instanceof HubBindingError)) throw error;
|
|
587
|
+
onBindingWarning?.(`${error.message}; falling back to the local hub so sync can continue`);
|
|
588
|
+
}
|
|
589
|
+
const serverUrl = env.KXM_SERVER_URL?.trim() || recordUrl;
|
|
577
590
|
if (!serverUrl) return undefined;
|
|
578
591
|
// Use the context's actual projectId — the same identity sync events carry — so the
|
|
579
592
|
// ops snapshot can join runs to their home Runtime. The npm package name (what an
|
|
@@ -1271,6 +1284,7 @@ async function startKxmRuntimeSupervisorInner(
|
|
|
1271
1284
|
};
|
|
1272
1285
|
|
|
1273
1286
|
let syncing = false;
|
|
1287
|
+
let bindingWarning: string | undefined;
|
|
1274
1288
|
const syncTimer = setInterval(() => {
|
|
1275
1289
|
if (syncing || stopping) return;
|
|
1276
1290
|
syncing = true;
|
|
@@ -1292,7 +1306,11 @@ async function startKxmRuntimeSupervisorInner(
|
|
|
1292
1306
|
consecutiveFailures: 0,
|
|
1293
1307
|
};
|
|
1294
1308
|
try {
|
|
1295
|
-
const client = runtimeHubClientFor(context, env)
|
|
1309
|
+
const client = runtimeHubClientFor(context, env, (message) => {
|
|
1310
|
+
if (bindingWarning === message) return;
|
|
1311
|
+
bindingWarning = message;
|
|
1312
|
+
logger.warn({ event: "hub_binding_fallback", message });
|
|
1313
|
+
});
|
|
1296
1314
|
if (!client) {
|
|
1297
1315
|
// Not a failure: a box with no bound hub simply keeps its rows.
|
|
1298
1316
|
recordSyncStatus(context, { ...base, state: "no_hub" });
|
|
@@ -271,6 +271,36 @@ export async function runKxmNpmPublish(input) {
|
|
|
271
271
|
};
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* npm can accept a publish minutes before `npm view <pkg>@<version>` answers.
|
|
276
|
+
* Callers that report success should wait until this returns visible.
|
|
277
|
+
*/
|
|
278
|
+
export function npmViewVersion(version, exec = execFile) {
|
|
279
|
+
return new Promise((resolve) => {
|
|
280
|
+
exec("npm", ["view", `@kontextmind/kxm@${version}`, "version"], { encoding: "utf8", timeout: 30_000 }, (error, stdout) => {
|
|
281
|
+
if (error) resolve("");
|
|
282
|
+
else resolve(String(stdout ?? "").trim());
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function waitForNpmVersion(version, options = {}) {
|
|
288
|
+
const waitMs = Math.max(0, options.waitMs ?? 0);
|
|
289
|
+
const intervalMs = Math.max(250, options.intervalMs ?? 15_000);
|
|
290
|
+
const exec = options.npmView ?? ((value) => npmViewVersion(value));
|
|
291
|
+
const deadline = Date.now() + waitMs;
|
|
292
|
+
let last = "";
|
|
293
|
+
for (;;) {
|
|
294
|
+
const seen = String(await exec(version) ?? "").trim();
|
|
295
|
+
if (seen === version) return { visible: true, version };
|
|
296
|
+
last = seen || last;
|
|
297
|
+
if (Date.now() >= deadline) return { visible: false, version, detail: last };
|
|
298
|
+
const pause = Math.min(intervalMs, Math.max(0, deadline - Date.now()));
|
|
299
|
+
if (pause === 0) return { visible: false, version, detail: last };
|
|
300
|
+
await new Promise((resolve) => setTimeout(resolve, pause));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
274
304
|
export async function main(env = process.env, stdout = process.stdout, stderr = process.stderr) {
|
|
275
305
|
const token = env.GITHUB_TOKEN || env.GH_TOKEN;
|
|
276
306
|
const npmToken = env.NODE_AUTH_TOKEN || env.NPM_TOKEN;
|
|
@@ -313,7 +343,23 @@ export async function main(env = process.env, stdout = process.stdout, stderr =
|
|
|
313
343
|
stdout.write(`release ${tag} is currently a draft; waiting for publish (${sec}s remaining)...\n`);
|
|
314
344
|
},
|
|
315
345
|
});
|
|
316
|
-
|
|
346
|
+
const version = String(tag).replace(/^v/, "");
|
|
347
|
+
const visibleWaitRaw = env.KXM_NPM_VISIBLE_WAIT_MS;
|
|
348
|
+
const visibleWaitMs = visibleWaitRaw == null || visibleWaitRaw === ""
|
|
349
|
+
? 20 * 60 * 1000
|
|
350
|
+
: Math.max(0, Number(visibleWaitRaw) || 0);
|
|
351
|
+
const intervalRaw = env.KXM_NPM_VISIBLE_INTERVAL_MS;
|
|
352
|
+
const intervalMs = intervalRaw == null || intervalRaw === "" ? 15_000 : Math.max(250, Number(intervalRaw) || 15_000);
|
|
353
|
+
if (visibleWaitMs > 0) {
|
|
354
|
+
stdout.write(`npm publish submitted for @kontextmind/kxm@${version}; waiting until npm view shows it\n`);
|
|
355
|
+
const visible = await waitForNpmVersion(version, { waitMs: visibleWaitMs, intervalMs });
|
|
356
|
+
if (!visible.visible) {
|
|
357
|
+
stderr.write(`npm_not_visible: npm view @kontextmind/kxm@${version} version did not appear within ${visibleWaitMs}ms\n`);
|
|
358
|
+
return 1;
|
|
359
|
+
}
|
|
360
|
+
stdout.write(`npm view @kontextmind/kxm@${version} version is visible\n`);
|
|
361
|
+
}
|
|
362
|
+
stdout.write(`${JSON.stringify({ ...result, npmVisible: visibleWaitMs > 0 })}\n`);
|
|
317
363
|
return 0;
|
|
318
364
|
} catch (error) {
|
|
319
365
|
const code = error instanceof KxmPublishNpmError ? error.code : "publish_failed";
|