@kontextmind/kxm 0.7.36 → 0.7.38

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.
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.36",
14
+ "version": "0.7.38",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.36",
3
+ "version": "0.7.38",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "kxm",
4
4
  "displayName": "KXM",
5
- "version": "0.7.36",
5
+ "version": "0.7.38",
6
6
  "description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
7
7
  "author": {
8
8
  "name": "KontextMind",
@@ -33904,6 +33904,12 @@ async function cmdRouteList(runtime) {
33904
33904
  print(runtime.io, runtime.json, { ok: true, command: "routes list", policy }, [...policy.admitted.map((x2) => `admitted ${x2}`), ...policy.disabled.map((x2) => `disabled ${x2}`)].join("\n") || "no route decisions");
33905
33905
  return 0;
33906
33906
  }
33907
+ async function cmdRouteCount(runtime) {
33908
+ const policy = loadRoutePolicy(runtime.dirs.workdir);
33909
+ const summary = `${policy.admitted.length} admitted / ${policy.disabled.length} disabled`;
33910
+ print(runtime.io, runtime.json, { ok: true, command: "routes count", admitted: policy.admitted.length, disabled: policy.disabled.length, summary }, summary);
33911
+ return 0;
33912
+ }
33907
33913
  async function cmdModelInventoryRefresh(runtime) {
33908
33914
  if (runtime.dryRun) {
33909
33915
  print(runtime.io, runtime.json, { ok: true, command: "models inventory refresh", dryRun: true }, "would refresh .kxm/models/inventory.yaml");
@@ -48805,6 +48811,9 @@ function createProgram(ctx, result) {
48805
48811
  addGlobalOptions(routesCmd.command("list").description("List route decisions")).action(async function routesListAction() {
48806
48812
  result.code = await cmdRouteList(runtimeFrom(ctx, this));
48807
48813
  });
48814
+ addGlobalOptions(routesCmd.command("count").description("Count admitted and disabled routes")).action(async function routesCountAction() {
48815
+ result.code = await cmdRouteCount(runtimeFrom(ctx, this));
48816
+ });
48808
48817
  for (const status of ["admit", "disable"]) {
48809
48818
  addGlobalOptions(routesCmd.command(status).description(`${status === "admit" ? "Admit" : "Disable"} a model route from the inventory`)).option("--model <id>", "Exact model id; omit to choose interactively").action(async function routeChangeAction(options) {
48810
48819
  result.code = await cmdRouteChange(runtimeFrom(ctx, this), status === "admit" ? "admitted" : "disabled", options.model);
@@ -49196,8 +49205,10 @@ function removedMeshInvocation(argv) {
49196
49205
  return { invoked, json };
49197
49206
  }
49198
49207
  async function runCli(argv, env = process.env, io = { stdout: (text) => process.stdout.write(text), stderr: (text) => process.stderr.write(text) }, cwd = process.cwd()) {
49199
- const originalStdout = io.stdout;
49200
- const originalStderr = io.stderr;
49208
+ const nested = io.io;
49209
+ const sink = nested && typeof nested.stdout === "function" && typeof nested.stderr === "function" ? nested : io;
49210
+ const originalStdout = sink.stdout;
49211
+ const originalStderr = sink.stderr;
49201
49212
  io = { ...io, stdout: (text) => originalStdout(redactConfiguredValues(text, env)), stderr: (text) => originalStderr(redactConfiguredValues(text, env)) };
49202
49213
  const mesh = removedMeshInvocation(argv);
49203
49214
  if (mesh.invoked) {
@@ -17121,7 +17121,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
17121
17121
  }
17122
17122
 
17123
17123
  // plugins/kxm/src/mcp-server.ts
17124
- var VERSION = "0.7.36";
17124
+ var VERSION = "0.7.38";
17125
17125
  var inbox = /* @__PURE__ */ new Map();
17126
17126
  var notifiedInbox = /* @__PURE__ */ new Set();
17127
17127
  var meshClient;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kxm-claude-plugin",
3
- "version": "0.7.36",
3
+ "version": "0.7.38",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -771,6 +771,13 @@ export async function cmdRouteList(runtime: Runtime): Promise<number> {
771
771
  return 0;
772
772
  }
773
773
 
774
+ export async function cmdRouteCount(runtime: Runtime): Promise<number> {
775
+ const policy = loadRoutePolicy(runtime.dirs.workdir);
776
+ const summary = `${policy.admitted.length} admitted / ${policy.disabled.length} disabled`;
777
+ print(runtime.io, runtime.json, { ok: true, command: "routes count", admitted: policy.admitted.length, disabled: policy.disabled.length, summary }, summary);
778
+ return 0;
779
+ }
780
+
774
781
  export async function cmdModelInventoryRefresh(runtime: Runtime): Promise<number> {
775
782
  if (runtime.dryRun) {
776
783
  print(runtime.io, runtime.json, { ok: true, command: "models inventory refresh", dryRun: true }, "would refresh .kxm/models/inventory.yaml");
@@ -114,6 +114,7 @@ import {
114
114
  cmdRouteChange,
115
115
  cmdModelsScreen,
116
116
  cmdRouteList,
117
+ cmdRouteCount,
117
118
  cmdModelInventoryRefresh,
118
119
  cmdVnextRuntime,
119
120
  } from "./cli/vnext.ts";
@@ -426,6 +427,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
426
427
  const routesCmd = addGlobalOptions(program.command("routes").description("Admit or disable verified model routes"));
427
428
  routesCmd.helpCommand("help", "Show routes help");
428
429
  addGlobalOptions(routesCmd.command("list").description("List route decisions")).action(async function routesListAction(this: Command) { result.code = await cmdRouteList(runtimeFrom(ctx, this)); });
430
+ addGlobalOptions(routesCmd.command("count").description("Count admitted and disabled routes")).action(async function routesCountAction(this: Command) { result.code = await cmdRouteCount(runtimeFrom(ctx, this)); });
429
431
  for (const status of ["admit", "disable"] as const) {
430
432
  addGlobalOptions(routesCmd.command(status).description(`${status === "admit" ? "Admit" : "Disable"} a model route from the inventory`)).option("--model <id>", "Exact model id; omit to choose interactively").action(async function routeChangeAction(this: Command, options: { model?: string }) { result.code = await cmdRouteChange(runtimeFrom(ctx, this), status === "admit" ? "admitted" : "disabled", options.model); });
431
433
  }
@@ -1210,8 +1212,10 @@ export async function runCli(
1210
1212
  io: CliIo = { stdout: (text) => process.stdout.write(text), stderr: (text) => process.stderr.write(text) },
1211
1213
  cwd = process.cwd(),
1212
1214
  ): Promise<number> {
1213
- const originalStdout = io.stdout;
1214
- const originalStderr = io.stderr;
1215
+ const nested = (io as { io?: CliIo }).io;
1216
+ const sink = nested && typeof nested.stdout === "function" && typeof nested.stderr === "function" ? nested : io;
1217
+ const originalStdout = sink.stdout;
1218
+ const originalStderr = sink.stderr;
1215
1219
  io = { ...io, stdout: (text) => originalStdout(redactConfiguredValues(text, env)), stderr: (text) => originalStderr(redactConfiguredValues(text, env)) };
1216
1220
  const mesh = removedMeshInvocation(argv);
1217
1221
  if (mesh.invoked) {
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
8
8
  import { deliverInboxNotification } from "./inbox.ts";
9
9
  import type { HubEvent, MessageRecord } from "./protocol.ts";
10
10
 
11
- const VERSION = "0.7.36";
11
+ const VERSION = "0.7.38";
12
12
  const inbox = new Map<string, MessageRecord>();
13
13
  const notifiedInbox = new Set<string>();
14
14
  let meshClient: HubClient | undefined;
@@ -0,0 +1,94 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { isRouteAdmitted } from "./routes.ts";
4
+
5
+ /** Deterministic vision gate: judge a UI screenshot through an admitted
6
+ * vision route with a bounded prompt and a strict structured verdict. The
7
+ * gate never free-form chats and never fabricates — an unreadable image, an
8
+ * unadmitted route, or an unparsable verdict returns `verdict: null` with a
9
+ * `divergence` reason. Transport is injectable so tests run without
10
+ * network. */
11
+
12
+ export const VISION_GATE_DEFAULT_ROUTE = "zai-coding-cn/glm-5.3-flash";
13
+ const QUESTION_MAX_CHARS = 500;
14
+ const RAW_MAX_CHARS = 2000;
15
+
16
+ export interface VisionGateOptions {
17
+ projectRoot?: string | undefined;
18
+ /** Absolute or project-relative path to the screenshot. */
19
+ imagePath: string;
20
+ /** What to assert about the screenshot, phrased as a yes/no question. */
21
+ question: string;
22
+ /** Producer id as admitted in routes.yaml. Defaults to the verified
23
+ * vision route. */
24
+ route?: string | undefined;
25
+ timeoutMs?: number | undefined;
26
+ /** Injectable transport: receives the composed prompt, returns CLI stdout. */
27
+ transport?: ((prompt: string) => Promise<string>) | undefined;
28
+ }
29
+
30
+ export interface VisionGateResult {
31
+ /** true/false when the verdict parsed; null = fail closed. */
32
+ verdict: boolean | null;
33
+ route: string;
34
+ /** Bounded raw output for audit. */
35
+ raw: string;
36
+ divergence?: string | undefined;
37
+ }
38
+
39
+ export function visionGatePrompt(question: string, imagePath?: string | undefined): string {
40
+ const bounded = question.trim().slice(0, QUESTION_MAX_CHARS);
41
+ const reference = imagePath ? `Look at the image @${imagePath}.` : "Look at the image attached to this prompt.";
42
+ return [
43
+ "You are judging a UI screenshot for an automated verification gate.",
44
+ reference,
45
+ bounded,
46
+ 'Reply with exactly one JSON object and nothing else: {"verdict": true} or {"verdict": false}',
47
+ ].join(" ");
48
+ }
49
+
50
+ export function parseVisionVerdict(raw: string): boolean | null {
51
+ const match = /"verdict"\s*:\s*(true|false)/i.exec(raw);
52
+ return match ? match[1] === "true" : null;
53
+ }
54
+
55
+ function defaultTransport(prompt: string, timeoutMs: number): string {
56
+ return execFileSync(
57
+ "pi",
58
+ ["--provider", "zai-coding-cn", "--model", "glm-5.3-flash", "-p", "--no-session", prompt],
59
+ { encoding: "utf8", timeout: timeoutMs, windowsHide: true },
60
+ );
61
+ }
62
+
63
+ export async function runVisionGate(options: VisionGateOptions): Promise<VisionGateResult> {
64
+ const projectRoot = options.projectRoot ?? process.cwd();
65
+ const route = options.route ?? VISION_GATE_DEFAULT_ROUTE;
66
+ const timeoutMs = options.timeoutMs ?? 90_000;
67
+ const base: VisionGateResult = { verdict: null, route, raw: "" };
68
+
69
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 5_000 || timeoutMs > 300_000) {
70
+ return { ...base, divergence: "timeout_out_of_bounds" };
71
+ }
72
+ if (!options.imagePath || !existsSync(options.imagePath) || !statSync(options.imagePath).isFile()) {
73
+ return { ...base, divergence: "image unreadable" };
74
+ }
75
+ if (!isRouteAdmitted(projectRoot, route)) {
76
+ return { ...base, divergence: "route_not_admitted" };
77
+ }
78
+ const prompt = visionGatePrompt(options.question, options.imagePath);
79
+
80
+ let raw: string;
81
+ try {
82
+ raw = options.transport
83
+ ? await options.transport(prompt)
84
+ : defaultTransport(prompt, timeoutMs);
85
+ } catch (error) {
86
+ return { ...base, divergence: `transport failed: ${String((error as Error)?.message ?? error).slice(0, 200)}` };
87
+ }
88
+ const boundedRaw = raw.slice(0, RAW_MAX_CHARS);
89
+ const verdict = parseVisionVerdict(boundedRaw);
90
+ if (verdict === null) {
91
+ return { ...base, raw: boundedRaw, divergence: "verdict unreadable" };
92
+ }
93
+ return { ...base, verdict, raw: boundedRaw };
94
+ }