@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.
@@ -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 status of selected) {
1471
- const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === status.id);
1472
- if (!entry) continue;
1473
- const scopes = scopesFor(requested.scope, entry);
1474
- if (scopes.length === 0) {
1475
- steps.push({
1476
- harness: status.id,
1477
- scope: requested.scope === "all" ? "self" : requested.scope,
1478
- command: status.command ?? entry.commands[0]!,
1479
- args: [],
1480
- outcome: "skipped",
1481
- detail: "no_updater",
1482
- });
1483
- continue;
1484
- }
1485
- if (!status.detected || !status.command) {
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: scopes[0]!,
1489
- command: entry.commands[0]!,
1490
- args: argsFor(entry, scopes[0]!) ?? [],
1491
- outcome: "skipped",
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: "github" };
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 ? "github" : row.source;
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: KxmUpdateSource;
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 --kxm (auto)" : "kxm update --kxm";
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 registry apply is for after the public package exists. npm verifies registry integrity itself; do not duplicate that check here. Git installs use GitHub release tarballs. */
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: KxmUpdateSource,
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.174";
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 = JSON.parse(packed.stdout)[0]?.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,4 @@
1
+ export function npmPackArtifacts(stdout: string): Array<{
2
+ filename?: string;
3
+ files?: Array<{ path: string }>;
4
+ }>;
@@ -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
+ }
@@ -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 result = spawnSync(command, args, {
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: childEnv(),
115
+ env,
75
116
  timeout,
76
117
  windowsHide: true,
118
+ windowsVerbatimArguments: windowsCommandScript,
77
119
  });
78
120
  return {
79
121
  status: result.status ?? 1,