@kontextmind/kxm 0.7.174 → 0.7.176
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 +13 -0
- package/docs/contributing/decisions.md +22 -1
- package/docs/contributing/test-matrix.md +1 -1
- package/docs/operations/upgrade.md +5 -5
- package/docs/reference/cli-reference.md +8 -7
- package/docs/reference/config-reference.md +2 -2
- package/docs/start/install.md +3 -3
- package/package.json +6 -6
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +128 -52
- package/plugins/kxm/dist/extension.js +74 -11
- package/plugins/kxm/dist/mcp-server.js +33 -10
- package/plugins/kxm/dist/runtime.js +44 -27
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +8 -3
- package/plugins/kxm/src/cli/system.ts +74 -20
- package/plugins/kxm/src/cli.ts +4 -2
- package/plugins/kxm/src/harness.ts +50 -29
- package/plugins/kxm/src/kxm-install-kind.ts +6 -0
- package/plugins/kxm/src/kxm-update-config.ts +2 -2
- package/plugins/kxm/src/kxm-update.ts +11 -4
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/scripts/docker-install-smoke.mjs +2 -1
- package/scripts/npm-pack-json.d.mts +4 -0
- package/scripts/npm-pack-json.mjs +15 -0
- package/scripts/pr-land.mjs +45 -3
|
@@ -1449,13 +1449,24 @@ function argsFor(entry: HarnessCatalogEntry, scope: Exclude<HarnessUpdateScope,
|
|
|
1449
1449
|
return entry.update.models;
|
|
1450
1450
|
}
|
|
1451
1451
|
|
|
1452
|
+
function detectedHarnesses(
|
|
1453
|
+
inventory: HarnessInventory,
|
|
1454
|
+
requestedHarness?: string,
|
|
1455
|
+
): HarnessStatus[] {
|
|
1456
|
+
if (requestedHarness) return inventory.harnesses.filter((entry) => entry.id === requestedHarness);
|
|
1457
|
+
const detected = inventory.harnesses.filter((entry) => entry.detected);
|
|
1458
|
+
const preferred = inventory.defaultHarness;
|
|
1459
|
+
return [
|
|
1460
|
+
...detected.filter((entry) => entry.id === preferred),
|
|
1461
|
+
...detected.filter((entry) => entry.id !== preferred),
|
|
1462
|
+
];
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1452
1465
|
export function planHarnessUpdate(
|
|
1453
1466
|
inventory: HarnessInventory,
|
|
1454
1467
|
requested: { harness?: string; scope: HarnessUpdateScope },
|
|
1455
1468
|
): HarnessUpdateStep[] {
|
|
1456
|
-
const selected = requested.harness
|
|
1457
|
-
? inventory.harnesses.filter((entry) => entry.id === requested.harness)
|
|
1458
|
-
: inventory.harnesses.filter((entry) => entry.detected);
|
|
1469
|
+
const selected = detectedHarnesses(inventory, requested.harness);
|
|
1459
1470
|
if (requested.harness && selected.length === 0) {
|
|
1460
1471
|
return [{
|
|
1461
1472
|
harness: requested.harness,
|
|
@@ -1466,36 +1477,46 @@ export function planHarnessUpdate(
|
|
|
1466
1477
|
detail: isKnownHarnessId(requested.harness) ? "not_detected" : "unknown_harness",
|
|
1467
1478
|
}];
|
|
1468
1479
|
}
|
|
1480
|
+
const waves: Exclude<HarnessUpdateScope, "all">[] = requested.scope === "all"
|
|
1481
|
+
? ["self", "extensions", "models"]
|
|
1482
|
+
: [requested.scope];
|
|
1469
1483
|
const steps: HarnessUpdateStep[] = [];
|
|
1470
|
-
for (const
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1484
|
+
for (const scope of waves) {
|
|
1485
|
+
for (const status of selected) {
|
|
1486
|
+
const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === status.id);
|
|
1487
|
+
if (!entry) continue;
|
|
1488
|
+
const supported = scopesFor(requested.scope === "all" ? "all" : scope, entry);
|
|
1489
|
+
if (!supported.includes(scope)) {
|
|
1490
|
+
if (requested.scope !== "all") {
|
|
1491
|
+
steps.push({
|
|
1492
|
+
harness: status.id,
|
|
1493
|
+
scope,
|
|
1494
|
+
command: status.command ?? entry.commands[0]!,
|
|
1495
|
+
args: [],
|
|
1496
|
+
outcome: "skipped",
|
|
1497
|
+
detail: "no_updater",
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (!status.detected || !status.command) {
|
|
1503
|
+
steps.push({
|
|
1504
|
+
harness: status.id,
|
|
1505
|
+
scope,
|
|
1506
|
+
command: entry.commands[0]!,
|
|
1507
|
+
args: argsFor(entry, scope) ?? [],
|
|
1508
|
+
outcome: "skipped",
|
|
1509
|
+
detail: "not_detected",
|
|
1510
|
+
});
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1486
1513
|
steps.push({
|
|
1487
1514
|
harness: status.id,
|
|
1488
|
-
scope
|
|
1489
|
-
command:
|
|
1490
|
-
args: argsFor(entry,
|
|
1491
|
-
outcome: "
|
|
1492
|
-
detail: "not_detected",
|
|
1515
|
+
scope,
|
|
1516
|
+
command: status.command,
|
|
1517
|
+
args: argsFor(entry, scope) ?? [],
|
|
1518
|
+
outcome: "would",
|
|
1493
1519
|
});
|
|
1494
|
-
continue;
|
|
1495
|
-
}
|
|
1496
|
-
for (const scope of scopes) {
|
|
1497
|
-
const args = argsFor(entry, scope) ?? [];
|
|
1498
|
-
steps.push({ harness: status.id, scope, command: status.command, args, outcome: "would" });
|
|
1499
1520
|
}
|
|
1500
1521
|
}
|
|
1501
1522
|
return steps;
|
|
@@ -19,6 +19,12 @@ export interface InstallKindReport {
|
|
|
19
19
|
instruction: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export function kxmSourceCheckoutRoot(dir: string): string | undefined {
|
|
23
|
+
if (!existsSync(join(dir, ".git"))) return undefined;
|
|
24
|
+
if (!packageIsKxm(dir)) return undefined;
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
function packageIsKxm(root: string): boolean {
|
|
23
29
|
try {
|
|
24
30
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { name?: unknown };
|
|
@@ -10,7 +10,7 @@ import { kxmUserStateRoot } from "./bindings.ts";
|
|
|
10
10
|
|
|
11
11
|
export function loadKxmUpdateConfig(env: NodeJS.ProcessEnv = process.env): KxmUpdateConfig {
|
|
12
12
|
const path = join(kxmUserStateRoot({ env }), "update.yaml");
|
|
13
|
-
if (!existsSync(path)) return { schema: KXM_UPDATE_SCHEMA, auto: false, source: "
|
|
13
|
+
if (!existsSync(path)) return { schema: KXM_UPDATE_SCHEMA, auto: false, source: "npm" };
|
|
14
14
|
let parsed: unknown;
|
|
15
15
|
try {
|
|
16
16
|
parsed = parseYaml(readFileSync(path, "utf8"));
|
|
@@ -31,7 +31,7 @@ export function loadKxmUpdateConfig(env: NodeJS.ProcessEnv = process.env): KxmUp
|
|
|
31
31
|
if (typeof row.auto !== "boolean") {
|
|
32
32
|
throw new KxmUpdateConfigError("update.yaml auto must be a boolean");
|
|
33
33
|
}
|
|
34
|
-
const source = row.source === undefined ? "
|
|
34
|
+
const source = row.source === undefined ? "npm" : row.source;
|
|
35
35
|
if (source !== "npm" && source !== "github") {
|
|
36
36
|
throw new KxmUpdateConfigError("update.yaml source must be npm or github");
|
|
37
37
|
}
|
|
@@ -10,6 +10,7 @@ const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
|
10
10
|
const SHA256_DIGEST = /^sha256:([0-9a-fA-F]{64})$/;
|
|
11
11
|
|
|
12
12
|
export type KxmUpdateSource = "npm" | "github";
|
|
13
|
+
export type KxmApplySource = KxmUpdateSource | "local";
|
|
13
14
|
|
|
14
15
|
export interface KxmReleaseAsset {
|
|
15
16
|
name: string;
|
|
@@ -27,9 +28,10 @@ export interface KxmUpdateNotice {
|
|
|
27
28
|
latest?: string;
|
|
28
29
|
available: boolean;
|
|
29
30
|
auto: boolean;
|
|
30
|
-
source:
|
|
31
|
+
source: KxmApplySource;
|
|
31
32
|
message: string;
|
|
32
33
|
asset?: KxmReleaseAsset;
|
|
34
|
+
localRoot?: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export class KxmUpdateConfigError extends Error {
|
|
@@ -100,7 +102,7 @@ export function compareSemver(left: string, right: string): number | undefined {
|
|
|
100
102
|
|
|
101
103
|
export function formatKxmUpdateNotice(notice: KxmUpdateNotice): string {
|
|
102
104
|
if (!notice.available) return notice.message;
|
|
103
|
-
const apply = notice.auto ? "kxm update
|
|
105
|
+
const apply = notice.auto ? "kxm update (auto)" : "kxm update";
|
|
104
106
|
return `kxm ${notice.current} → ${notice.latest} available · ${apply}`;
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -243,14 +245,19 @@ export function verifyReleaseAssetDigest(path: string, sha256: string): boolean
|
|
|
243
245
|
}
|
|
244
246
|
}
|
|
245
247
|
|
|
246
|
-
/** npm
|
|
248
|
+
/** npm is the default source and npm verifies registry integrity itself. "local" installs this repository's checkout. "github" downloads the release tarball (gh, or curl without gh) and verifies its sha256. */
|
|
247
249
|
export function planKxmPackageUpdate(
|
|
248
|
-
source:
|
|
250
|
+
source: KxmApplySource,
|
|
249
251
|
latest: string,
|
|
250
252
|
releaseDir: string,
|
|
251
253
|
asset?: KxmReleaseAsset,
|
|
252
254
|
downloader: "gh" | "curl" = "gh",
|
|
255
|
+
localRoot?: string,
|
|
253
256
|
): KxmPackageUpdateStep[] {
|
|
257
|
+
if (source === "local") {
|
|
258
|
+
if (!localRoot) return [];
|
|
259
|
+
return [{ kind: "install", command: "npm", args: ["install", "--global", "--omit=peer", localRoot] }];
|
|
260
|
+
}
|
|
254
261
|
if (source === "npm") {
|
|
255
262
|
return [{ kind: "install", command: "npm", args: ["install", "--global", "--omit=peer", `@kontextmind/kxm@${latest}`] }];
|
|
256
263
|
}
|
|
@@ -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.176";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -14,6 +14,7 @@ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
|
|
|
14
14
|
import { homedir, tmpdir } from "node:os";
|
|
15
15
|
import { join, resolve } from "node:path";
|
|
16
16
|
|
|
17
|
+
import { npmPackArtifacts } from "./npm-pack-json.mjs";
|
|
17
18
|
const repoRoot = resolve(import.meta.dirname, "..");
|
|
18
19
|
|
|
19
20
|
const vault = process.env.KXM_SMOKE_VAULT ?? "AI Provider Keys";
|
|
@@ -92,7 +93,7 @@ function main() {
|
|
|
92
93
|
// commit gate (`npm run verify` -> check:generated) enforces that.
|
|
93
94
|
process.stdout.write("==> npm pack\n");
|
|
94
95
|
const packed = run("npm", ["pack", "--pack-destination", scratch, "--json"], { cwd: repoRoot });
|
|
95
|
-
const filename =
|
|
96
|
+
const filename = npmPackArtifacts(packed.stdout)[0]?.filename;
|
|
96
97
|
if (!filename) die("npm pack produced no tarball");
|
|
97
98
|
const tarball = join(scratch, filename);
|
|
98
99
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm pack --json` is a one-element array through npm 11. npm 12 returns
|
|
3
|
+
* one object keyed by package name. Both shapes carry `filename` and `files`.
|
|
4
|
+
*/
|
|
5
|
+
export function npmPackArtifacts(stdout) {
|
|
6
|
+
const parsed = JSON.parse(stdout);
|
|
7
|
+
if (Array.isArray(parsed)) return parsed;
|
|
8
|
+
if (parsed && typeof parsed === "object") {
|
|
9
|
+
const values = Object.values(parsed);
|
|
10
|
+
if (values.length > 0 && values.every((value) => value && typeof value === "object" && !Array.isArray(value))) {
|
|
11
|
+
return values;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
throw new Error(`unexpected npm pack JSON: ${String(stdout).slice(0, 200)}`);
|
|
15
|
+
}
|
package/scripts/pr-land.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { join, resolve } from "node:path";
|
|
11
|
+
import { delimiter, join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
14
|
const STAGES = Object.freeze([
|
|
@@ -67,13 +67,55 @@ function childEnv() {
|
|
|
67
67
|
return env;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function pathValue(env) {
|
|
71
|
+
const key = Object.keys(env).find((name) => name.toUpperCase() === "PATH");
|
|
72
|
+
return key ? env[key] : "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function comSpec(env) {
|
|
76
|
+
const key = Object.keys(env).find((name) => name.toUpperCase() === "COMSPEC");
|
|
77
|
+
return (key && env[key].trim()) || "cmd.exe";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** CreateProcess does not run `npm.cmd`. Prefer a real `.exe`; otherwise launch the `.cmd`/`.bat` through cmd. */
|
|
81
|
+
function windowsScriptOnPath(command, env) {
|
|
82
|
+
if (process.platform !== "win32") return undefined;
|
|
83
|
+
if (/\.(?:exe|com)$/i.test(command) || /[\\/]/.test(command)) return undefined;
|
|
84
|
+
if (/\.(?:cmd|bat)$/i.test(command)) return command;
|
|
85
|
+
for (const dir of pathValue(env).split(delimiter)) {
|
|
86
|
+
if (!dir) continue;
|
|
87
|
+
if (existsSync(join(dir, `${command}.exe`))) return undefined;
|
|
88
|
+
const cmd = join(dir, `${command}.cmd`);
|
|
89
|
+
if (existsSync(cmd)) return cmd;
|
|
90
|
+
const bat = join(dir, `${command}.bat`);
|
|
91
|
+
if (existsSync(bat)) return bat;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function quoteCmdArg(value) {
|
|
97
|
+
const text = String(value);
|
|
98
|
+
if (/[\0\r\n"%!]/.test(text)) {
|
|
99
|
+
throw new Error(`cannot pass ${JSON.stringify(text)} to a Windows command script`);
|
|
100
|
+
}
|
|
101
|
+
return `"${text}"`;
|
|
102
|
+
}
|
|
103
|
+
|
|
70
104
|
function run(command, args, timeout = 120_000) {
|
|
71
|
-
const
|
|
105
|
+
const env = childEnv();
|
|
106
|
+
const script = windowsScriptOnPath(command, env);
|
|
107
|
+
const windowsCommandScript = Boolean(script);
|
|
108
|
+
const launchCommand = windowsCommandScript ? comSpec(env) : command;
|
|
109
|
+
const launchArgs = windowsCommandScript
|
|
110
|
+
? ["/d", "/s", "/v:off", "/c", `"${[script, ...args].map(quoteCmdArg).join(" ")}"`]
|
|
111
|
+
: args;
|
|
112
|
+
const result = spawnSync(launchCommand, launchArgs, {
|
|
72
113
|
cwd: root,
|
|
73
114
|
encoding: "utf8",
|
|
74
|
-
env
|
|
115
|
+
env,
|
|
75
116
|
timeout,
|
|
76
117
|
windowsHide: true,
|
|
118
|
+
windowsVerbatimArguments: windowsCommandScript,
|
|
77
119
|
});
|
|
78
120
|
return {
|
|
79
121
|
status: result.status ?? 1,
|