mesurer-solid 0.2.0-beta.9 → 0.2.0

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.
@@ -0,0 +1,155 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const DESKTOP_COMMAND_TIMEOUT_MS = 10_000;
4
+
5
+ const normalizeString = (value) => {
6
+ if (value?.constructor !== String) return null;
7
+
8
+ const text = value.trim();
9
+
10
+ return text || null;
11
+ };
12
+
13
+ export const desktopSessionFromEnvironment = (env = process.env) => {
14
+ const thread = normalizeString(env.CODEX_THREAD_ID);
15
+ const appToolsPipe = normalizeString(env.CODEX_APP_TOOLS_PIPE_PATH);
16
+
17
+ if (!thread || !appToolsPipe) return null;
18
+
19
+ return {
20
+ thread,
21
+ appToolsPipe,
22
+ };
23
+ };
24
+
25
+ const run = (
26
+ command,
27
+ args,
28
+ env,
29
+ timeoutMs,
30
+ label,
31
+ ) => new Promise((resolve, reject) => {
32
+ const child = spawn(command, args, {
33
+ env,
34
+ shell: false,
35
+ stdio: ["ignore", "pipe", "pipe"],
36
+ windowsHide: true,
37
+ });
38
+
39
+ let stdout = "";
40
+ let stderr = "";
41
+ let settled = false;
42
+
43
+ const finish = (error, output) => {
44
+ if (settled) return;
45
+ settled = true;
46
+ clearTimeout(timeout);
47
+
48
+ if (error) reject(error);
49
+ else resolve(output);
50
+ };
51
+
52
+ const timeout = setTimeout(() => {
53
+ if (child.exitCode === null) child.kill("SIGTERM");
54
+
55
+ finish(new Error(`${label} timed out after ${timeoutMs}ms.`));
56
+ }, timeoutMs);
57
+
58
+ child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
59
+ child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
60
+ child.on("error", (error) => finish(error));
61
+ child.on("close", (code, signal) => {
62
+ if (code === 0) {
63
+ finish(null, stdout.trim());
64
+
65
+ return;
66
+ }
67
+
68
+ const detail = stderr.trim()
69
+ || stdout.trim()
70
+ || `exit code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`;
71
+
72
+ finish(new Error(`${label} failed: ${detail}`));
73
+ });
74
+ });
75
+
76
+ const queuedSubmissionId = (output, thread) => {
77
+ const escapedThread = thread.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
78
+
79
+ const exact = output.match(new RegExp(
80
+ `Queued message\\s+(\\S+)\\s+for thread\\s+${escapedThread}\\.?`,
81
+ ));
82
+
83
+ if (exact?.[1]) return exact[1];
84
+
85
+ const generic = output.match(/Queued message\s+(\S+)\s+for thread\s+\S+\.?/);
86
+
87
+ return generic?.[1] ?? null;
88
+ };
89
+
90
+ export const queueDesktopThread = async ({
91
+ command,
92
+ thread,
93
+ message,
94
+ env = process.env,
95
+ }) => {
96
+ const output = await run(
97
+ command,
98
+ ["queue", "--thread", thread, "--message", message],
99
+ env,
100
+ DESKTOP_COMMAND_TIMEOUT_MS,
101
+ "Codex Desktop queue",
102
+ );
103
+
104
+ const submissionId = queuedSubmissionId(output, thread);
105
+
106
+ if (!submissionId) {
107
+ throw new Error("Codex queued the Desktop message without a recognizable submission id.");
108
+ }
109
+
110
+ return {
111
+ output,
112
+ queuedSubmissionId: submissionId,
113
+ };
114
+ };
115
+
116
+ export const openDesktopThread = async ({
117
+ thread,
118
+ env = process.env,
119
+ platform = process.platform,
120
+ }) => {
121
+ const url = `codex://threads/${thread}`;
122
+ const override = normalizeString(env.MESURER_CODEX_DESKTOP_OPEN_BIN);
123
+
124
+ let command;
125
+ let args;
126
+
127
+ if (override) {
128
+ command = override;
129
+ args = [url];
130
+ } else if (platform === "darwin") {
131
+ command = "/usr/bin/open";
132
+ args = [url];
133
+ } else if (platform === "win32") {
134
+ command = "powershell.exe";
135
+ const quotedUrl = url.replaceAll("'", "''");
136
+
137
+ args = [
138
+ "-NoProfile",
139
+ "-Command",
140
+ `Start-Process -FilePath '${quotedUrl}'`,
141
+ ];
142
+ } else {
143
+ throw new Error(`Codex Desktop thread wake is unsupported on platform ${platform}.`);
144
+ }
145
+
146
+ await run(
147
+ command,
148
+ args,
149
+ env,
150
+ DESKTOP_COMMAND_TIMEOUT_MS,
151
+ "Codex Desktop thread wake",
152
+ );
153
+
154
+ return url;
155
+ };
@@ -14,9 +14,6 @@ if (command !== "install") {
14
14
  } else {
15
15
  const source = new URL("../skills/mesurer-ui/", import.meta.url);
16
16
  const injector = new URL("../dist/inject-script.js", import.meta.url);
17
- const codexBridge = new URL("../codex/codex-bridge.mjs", import.meta.url);
18
- const codexConnect = new URL("../codex/codex-connect.mjs", import.meta.url);
19
- const codexLifecycle = new URL("../codex/codex-lifecycle.mjs", import.meta.url);
20
17
  const destination = resolve(process.cwd(), ".agents/skills/mesurer-ui");
21
18
  const assets = resolve(destination, "assets");
22
19
 
@@ -26,18 +23,12 @@ if (command !== "install") {
26
23
  } else if (!existsSync(injector)) {
27
24
  console.error("Mesurer inject-script asset is missing. Reinstall mesurer-solid and retry.");
28
25
  process.exitCode = 1;
29
- } else if (!existsSync(codexBridge) || !existsSync(codexConnect) || !existsSync(codexLifecycle)) {
30
- console.error("Mesurer Codex companion assets are missing. Reinstall mesurer-solid and retry.");
31
- process.exitCode = 1;
32
26
  } else {
33
27
  if (force) rmSync(destination, { recursive: true, force: true });
34
28
  mkdirSync(resolve(destination, ".."), { recursive: true });
35
29
  cpSync(source, destination, { recursive: true });
36
30
  mkdirSync(assets, { recursive: true });
37
31
  cpSync(injector, resolve(assets, "inject-script.js"));
38
- cpSync(codexBridge, resolve(assets, "codex-bridge.mjs"));
39
- cpSync(codexConnect, resolve(assets, "codex-connect.mjs"));
40
- cpSync(codexLifecycle, resolve(assets, "codex-lifecycle.mjs"));
41
- console.log(`Installed Mesurer Agent Skill and assets at ${destination}`);
32
+ console.log(`Installed Mesurer Agent Skill and injector at ${destination}`);
42
33
  }
43
34
  }
@@ -7,9 +7,9 @@ description: Use Mesurer for frontend UI implementation, review, debugging, layo
7
7
 
8
8
  Mesurer is shared visual state between the person reviewing a page and the coding agent editing it. The rendered page is the integration boundary.
9
9
 
10
- The normal coding-agent workflow requires no Mesurer MCP server, chat-delivery daemon, or Send-to-agent callback. Use the browser/evaluation channel the browser controller already provides and read `window.__MESURER__` directly. The optional `codex()` plugin and local Codex companion are a separate human-initiated convenience for routing Context feedback to explicitly registered Codex threads.
10
+ The normal coding-agent workflow requires no Mesurer MCP server, chat-delivery daemon, or Send-to-agent callback. Use the browser/evaluation channel the browser controller already provides and read `window.__MESURER__` directly. The first-party `codex()` plugin is a separate human-initiated convenience for routing Context feedback through the application's native Codex host capability.
11
11
 
12
- When the current agent is Codex and the user has explicitly asked for Codex delivery, or the live Mesurer instance already has `mesurer.codex` enabled, ensure the packaged local companion is ready from the Codex process. Do not expect browser JavaScript to spawn a local executable.
12
+ Do not add a second Mesurer process for Codex delivery. Native applications expose the narrow `window.__MESURER_HOST__.codexBridge(request)` capability. Shared sessions use Codex's local app-server. When the native host inherited both `CODEX_THREAD_ID` and `CODEX_APP_TOOLS_PIPE_PATH` from Codex Desktop, Mesurer may queue only to that exact thread and wake it through `codex://threads/<id>`. Treat the app-tools pipe as an ownership signal only; never connect to, proxy, or invoke it. A Desktop-bundled Codex executable must never be used to bootstrap the shared daemon.
13
13
 
14
14
  A meaningful Mesurer step must return evidence the agent actually uses.
15
15
 
@@ -290,39 +290,22 @@ The optional Screenshot plugin is a separate human camera workflow. Native host
290
290
 
291
291
  ## Optional Codex thread handoff
292
292
 
293
- Use this only when the user asked for Mesurer-to-Codex delivery or the live Mesurer instance already has `mesurer.codex` enabled. Normal agent work still uses `window.__MESURER__` and browser verification.
293
+ Use this only when the user asked for Mesurer-to-Codex delivery or the live Mesurer instance has `mesurer.codex` enabled. Normal agent work still uses `window.__MESURER__` and browser verification.
294
294
 
295
- When this task is running inside Codex, check whether the live page already has the plugin:
296
-
297
- ```js
298
- const codexEnabled = Boolean(
299
- window.__MESURER_INSTANCE__?.pluginHost?.has("mesurer.codex")
300
- )
301
- ```
302
-
303
- If delivery was requested or `codexEnabled` is true, start or reuse the packaged companion from the Codex shell or tool environment:
304
-
305
- ```bash
306
- node .agents/skills/mesurer-ui/assets/codex-connect.mjs
307
- ```
308
-
309
- Inside the Mesurer Solid repository, use:
310
-
311
- ```bash
312
- bun run mesurer-codex-connect
313
- ```
295
+ Codex delivery uses the native host's in-process Codex Bridge. There is no Mesurer Codex server, bridge port, marketplace plugin, SessionStart hook, or thread-registration command.
314
296
 
315
297
  Keep these delivery rules:
316
298
 
317
299
  - Mesurer uses Queue, never `turn/steer`.
318
- - Codex's native durable queue is the message source of truth.
319
- - Desktop delivery queues once, keeps the queued-submission id, and opens the existing `codex://threads/<threadId>` destination. Do not use the Desktop app-tools pipe as a second delivery path.
320
- - Do not delete and resend a queued item after a bridge restart. The bridge persistence file tracks lifecycle; it is not another message queue.
321
- - The browser plugin connects lazily when the human first queues feedback or opens the thread picker. Do not add mount-time loopback polling.
322
- - Each page keeps its originating or explicitly chosen thread in per-tab state. If routing is ambiguous, require a human choice.
300
+ - Codex's shared app-server and durable queue are the message source of truth.
301
+ - Only threads reported as loaded by Codex are sendable.
302
+ - The host-side bridge uses `thread/queue/add` directly. Do not shell through `codex queue` or start a parallel app-server.
303
+ - Mesurer's persistence file tracks correlation and lifecycle; it is not another message queue.
304
+ - Each page keeps its explicitly chosen thread in per-tab state. If routing is ambiguous, require a human choice.
323
305
  - Queue submission is single-flight. Do not bypass duplicate suppression with a second request.
324
- - A completed matched turn may remove only the annotation ids included in that delivery. Interrupted, failed, or uncertain work keeps them.
325
- - Mesurer does not create Codex threads. Create or open the thread in Codex and let the trusted `SessionStart` connector register it.
306
+ - A completed matched turn may remove only the annotation ids included in that delivery. Interrupted, failed, ambiguous, or unreadable work keeps them.
307
+ - Mesurer does not create Codex threads. Create or open the thread in Codex first, then use it once Codex reports it as loaded.
308
+ - Do not stop Codex's shared app-server daemon when Mesurer closes.
326
309
 
327
310
  The typed `codex:v1` service supports `health()`, `listThreads()`, `useThread(thread)`, `delivery(deliveryId)`, and `queue({ thread })`. `send()` is retained only as a compatibility alias. Generic automation uses `codex.queue`; `codex.send` remains its compatibility alias.
328
311