@kontextmind/kxm 0.7.37 → 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.37",
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.37",
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.37",
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",
@@ -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.37";
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.37",
3
+ "version": "0.7.38",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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.37";
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
+ }