mesurer-solid 0.2.1 → 0.2.2-beta.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,421 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { readFile } from "node:fs/promises";
6
+ import { fileURLToPath } from "node:url";
7
+ import { parseArgs } from "node:util";
8
+
9
+ const DEFAULT_BRIDGE = "http://127.0.0.1:47365";
10
+
11
+ const BRIDGE_NAME = "mesurer-codex";
12
+
13
+ const BRIDGE_PROTOCOL_VERSION = 2;
14
+
15
+ const REQUIRED_BRIDGE_CAPABILITIES = [
16
+ "thread-discovery-v1",
17
+ "durable-queue-v1",
18
+ "history-recovery-v2",
19
+ "client-message-correlation-v1",
20
+ "idle-safe-shutdown-v1",
21
+ "nonterminal-retention-v1",
22
+ "instance-binding-v1",
23
+ ];
24
+
25
+ const IDLE_SAFE_SHUTDOWN_CAPABILITY = "idle-safe-shutdown-v1";
26
+
27
+ const START_TIMEOUT_MS = 10_000;
28
+
29
+ const POLL_INTERVAL_MS = 100;
30
+
31
+ const usage = `Usage:
32
+ mesurer-codex-connect [options]
33
+
34
+ Ensures the Mesurer Codex bridge is running, then registers one Codex session
35
+ and makes that session the active Mesurer destination.
36
+
37
+ Options:
38
+ --thread <value> Codex session UUID or exact session name
39
+ --session-start Read a Codex SessionStart hook event from stdin
40
+ --session-end Read a Codex SessionEnd hook event from stdin
41
+ --bridge <url> Bridge URL (default: ${DEFAULT_BRIDGE})
42
+ --codex <path> Codex executable passed to a newly-started bridge
43
+ --once Start a new bridge with --once (primarily useful for tests)
44
+ --quiet Suppress success output
45
+ --help Show this help
46
+
47
+ Without --thread or --session-start, CODEX_THREAD_ID is used when available.
48
+ `;
49
+
50
+ const { values } = parseArgs({
51
+ options: {
52
+ thread: { type: "string" },
53
+ "session-start": { type: "boolean", default: false },
54
+ "session-end": { type: "boolean", default: false },
55
+ bridge: { type: "string", default: DEFAULT_BRIDGE },
56
+ codex: { type: "string" },
57
+ once: { type: "boolean", default: false },
58
+ quiet: { type: "boolean", default: false },
59
+ help: { type: "boolean", default: false },
60
+ },
61
+ allowPositionals: false,
62
+ strict: true,
63
+ });
64
+
65
+ if (values.help) {
66
+ process.stdout.write(usage);
67
+ process.exit(0);
68
+ }
69
+
70
+ const fail = (message, code = 1) => {
71
+ process.stderr.write(`${message}\n`);
72
+ process.exit(code);
73
+ };
74
+
75
+ const readStdin = async () => {
76
+ process.stdin.setEncoding("utf8");
77
+ let input = "";
78
+
79
+ for await (const chunk of process.stdin) input += chunk;
80
+
81
+ return input;
82
+ };
83
+
84
+ const explicitThread = values.thread?.trim() || null;
85
+
86
+ const fromSessionStart = values["session-start"];
87
+
88
+ const fromSessionEnd = values["session-end"];
89
+
90
+ const routingModes = [Boolean(explicitThread), fromSessionStart, fromSessionEnd].filter(Boolean).length;
91
+
92
+ if (routingModes > 1) {
93
+ fail("Use only one of --thread, --session-start, or --session-end.", 2);
94
+ }
95
+
96
+ let thread = explicitThread ?? process.env.CODEX_THREAD_ID?.trim() ?? null;
97
+
98
+ let cwd = process.cwd();
99
+
100
+ if (fromSessionStart || fromSessionEnd) {
101
+ const input = await readStdin();
102
+ let event;
103
+
104
+ try {
105
+ event = JSON.parse(input);
106
+ } catch {
107
+ fail(
108
+ fromSessionStart
109
+ ? "--session-start requires one Codex SessionStart JSON event on stdin."
110
+ : "--session-end requires one Codex SessionEnd JSON event on stdin.",
111
+ 2,
112
+ );
113
+ }
114
+
115
+ const expectedEvent = fromSessionStart ? "SessionStart" : "SessionEnd";
116
+
117
+ if (event?.hook_event_name !== expectedEvent) {
118
+ fail(`Expected hook_event_name ${expectedEvent}, got ${event?.hook_event_name ?? "<missing>"}.`, 2);
119
+ }
120
+
121
+ thread = event?.session_id?.trim?.() || null;
122
+ cwd = event?.cwd?.trim?.() || cwd;
123
+ }
124
+
125
+ if (!thread) {
126
+ fail(
127
+ fromSessionStart
128
+ ? "Codex SessionStart input did not include a session_id."
129
+ : fromSessionEnd
130
+ ? "Codex SessionEnd input did not include a session_id."
131
+ : "mesurer-codex-connect requires --thread, CODEX_THREAD_ID, --session-start, or --session-end.",
132
+ 2,
133
+ );
134
+ }
135
+
136
+ const quiet = values.quiet || fromSessionStart || fromSessionEnd;
137
+
138
+ const bridge = values.bridge?.trim() || DEFAULT_BRIDGE;
139
+
140
+ const bridgeUrl = new URL(bridge.endsWith("/") ? bridge : `${bridge}/`);
141
+
142
+ const isLoopbackHost = (hostname) =>
143
+ hostname === "127.0.0.1"
144
+ || hostname === "localhost"
145
+ || hostname === "::1"
146
+ || hostname === "[::1]";
147
+
148
+ if (bridgeUrl.protocol !== "http:" || !isLoopbackHost(bridgeUrl.hostname)) {
149
+ fail(`Automatic bridge startup requires a loopback HTTP URL, got ${bridge}.`, 2);
150
+ }
151
+
152
+ if (bridgeUrl.pathname !== "/" || bridgeUrl.search || bridgeUrl.hash) {
153
+ fail(`Automatic bridge startup requires a root bridge URL, got ${bridge}.`, 2);
154
+ }
155
+
156
+ const port = bridgeUrl.port ? Number(bridgeUrl.port) : 80;
157
+
158
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
159
+ fail(`Invalid bridge port in ${bridge}.`, 2);
160
+ }
161
+
162
+ const bridgeScript = fileURLToPath(new URL("./codex-bridge.mjs", import.meta.url));
163
+
164
+ const expectedSourceHash = createHash("sha256")
165
+ .update(await readFile(bridgeScript))
166
+ .digest("hex");
167
+
168
+ const readPayload = async (response) => {
169
+ const text = await response.text();
170
+
171
+ if (!text) return {};
172
+
173
+ try {
174
+ return JSON.parse(text);
175
+ } catch {
176
+ return { error: text };
177
+ }
178
+ };
179
+
180
+ const health = async () => {
181
+ let response;
182
+
183
+ try {
184
+ response = await fetch(new URL("health", bridgeUrl));
185
+ } catch {
186
+ return null;
187
+ }
188
+
189
+ const payload = await readPayload(response);
190
+
191
+ if (!response.ok || payload.ok !== true) {
192
+ throw new Error(
193
+ payload.error || `Unexpected service at ${bridgeUrl.origin}: HTTP ${response.status}.`,
194
+ );
195
+ }
196
+
197
+ return payload;
198
+ };
199
+
200
+ const waitForBridge = async () => {
201
+ const deadline = Date.now() + START_TIMEOUT_MS;
202
+
203
+ while (Date.now() < deadline) {
204
+ const payload = await health();
205
+
206
+ if (payload) return payload;
207
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
208
+ }
209
+
210
+ throw new Error(`Mesurer Codex bridge did not become ready at ${bridgeUrl.origin}.`);
211
+ };
212
+
213
+ const bridgeCapabilities = (payload) =>
214
+ Array.isArray(payload?.bridge?.capabilities) ? payload.bridge.capabilities : [];
215
+
216
+ const isCompatibleBridge = (payload) => {
217
+ if (payload?.bridge?.name !== BRIDGE_NAME) return false;
218
+
219
+ if (payload.bridge.protocol !== BRIDGE_PROTOCOL_VERSION) return false;
220
+
221
+ const capabilities = new Set(bridgeCapabilities(payload));
222
+
223
+ return REQUIRED_BRIDGE_CAPABILITIES.every((capability) => capabilities.has(capability));
224
+ };
225
+
226
+ const isMesurerBridge = (payload) => payload?.bridge?.name === BRIDGE_NAME;
227
+
228
+ const isExactBridge = (payload) =>
229
+ isCompatibleBridge(payload)
230
+ && payload.bridge.sourceHash === expectedSourceHash;
231
+
232
+ if (fromSessionEnd) {
233
+ let current;
234
+
235
+ try {
236
+ current = await health();
237
+ } catch {
238
+ process.exit(0);
239
+ }
240
+
241
+ if (!current || !isMesurerBridge(current)) process.exit(0);
242
+
243
+ let response;
244
+
245
+ try {
246
+ response = await fetch(new URL("threads/unregister", bridgeUrl), {
247
+ method: "POST",
248
+ headers: { "Content-Type": "application/json" },
249
+ body: JSON.stringify({ thread }),
250
+ signal: AbortSignal.timeout(750),
251
+ });
252
+ } catch {
253
+ process.exit(0);
254
+ }
255
+
256
+ const payload = await readPayload(response);
257
+
258
+ if (!response.ok || payload.ok === false) process.exit(0);
259
+
260
+ process.exit(0);
261
+ }
262
+
263
+ const waitForBridgeToStop = async () => {
264
+ const deadline = Date.now() + START_TIMEOUT_MS;
265
+ let unavailableChecks = 0;
266
+
267
+ while (Date.now() < deadline) {
268
+ if (await health()) {
269
+ unavailableChecks = 0;
270
+ } else {
271
+ unavailableChecks += 1;
272
+
273
+ if (unavailableChecks >= 3) return;
274
+ }
275
+
276
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
277
+ }
278
+
279
+ throw new Error(`Stale Mesurer Codex bridge did not stop at ${bridgeUrl.origin}.`);
280
+ };
281
+
282
+ const bridgeUsage = (payload) => ({
283
+ registeredThreads: Number(payload?.bridgeState?.registeredThreads) || 0,
284
+ nonTerminalDeliveries: Number(payload?.bridgeState?.nonTerminalDeliveries) || 0,
285
+ idle: payload?.bridgeState?.idle === true,
286
+ });
287
+
288
+ const replaceStaleBridge = async (payload) => {
289
+ if (payload?.bridge?.name !== BRIDGE_NAME || payload.bridge.canShutdown !== true) return false;
290
+
291
+ if (!bridgeCapabilities(payload).includes(IDLE_SAFE_SHUTDOWN_CAPABILITY)) return false;
292
+
293
+ if (!bridgeUsage(payload).idle) return false;
294
+
295
+ let response;
296
+
297
+ try {
298
+ response = await fetch(new URL("shutdown", bridgeUrl), { method: "POST" });
299
+ } catch (cause) {
300
+ const error = cause instanceof Error ? cause.message : String(cause);
301
+ throw new Error(`Could not stop stale Mesurer Codex bridge at ${bridgeUrl.origin}: ${error}`);
302
+ }
303
+
304
+ const body = await readPayload(response);
305
+
306
+ if (!response.ok || body.ok === false) {
307
+ throw new Error(body.error || `Stale Mesurer Codex bridge returned HTTP ${response.status} while stopping.`);
308
+ }
309
+
310
+ await waitForBridgeToStop();
311
+
312
+ return true;
313
+ };
314
+
315
+ let current;
316
+
317
+ try {
318
+ current = await health();
319
+ } catch (cause) {
320
+ fail(cause instanceof Error ? cause.message : String(cause));
321
+ }
322
+
323
+ if (current && !isCompatibleBridge(current)) {
324
+ let replaced = false;
325
+
326
+ try {
327
+ replaced = await replaceStaleBridge(current);
328
+ } catch (cause) {
329
+ fail(cause instanceof Error ? cause.message : String(cause));
330
+ }
331
+
332
+ if (replaced) {
333
+ current = null;
334
+ } else if (current?.bridge?.name === BRIDGE_NAME) {
335
+ const usage = bridgeUsage(current);
336
+ const source = current.bridge.sourceHash?.slice?.(0, 12) ?? "unknown";
337
+ const protocol = current.bridge.protocol ?? "unknown";
338
+
339
+ if (!bridgeCapabilities(current).includes(IDLE_SAFE_SHUTDOWN_CAPABILITY)) {
340
+ fail(
341
+ `An incompatible Mesurer Codex Bridge is already running at ${bridgeUrl.origin} (protocol ${protocol}, source ${source}). It cannot prove idle-safe replacement, so Mesurer will not stop it automatically. Close the Codex sessions using that bridge, let it exit, then retry.`,
342
+ );
343
+ }
344
+
345
+ fail(
346
+ `An incompatible Mesurer Codex Bridge is still in use at ${bridgeUrl.origin} (protocol ${protocol}, source ${source}; ${usage.registeredThreads} registered thread(s), ${usage.nonTerminalDeliveries} queued/working delivery(s)). Mesurer will not interrupt it. Close those Codex sessions or wait for their deliveries to finish, then retry.`,
347
+ );
348
+ } else if (!current.bridge && Array.isArray(current.threads)) {
349
+ fail(
350
+ `An older Mesurer Codex bridge is already running at ${bridgeUrl.origin}. Stop that legacy bridge once, then rerun mesurer-codex-connect. Refusing to reuse it because its source identity cannot be verified.`,
351
+ );
352
+ } else {
353
+ fail(
354
+ `Another local service is already using ${bridgeUrl.origin}. Mesurer will not replace or send data to an unverified service.`,
355
+ );
356
+ }
357
+ }
358
+
359
+ if (!current) {
360
+ const args = [bridgeScript, "--port", String(port), "--thread", thread, "--cwd", cwd];
361
+
362
+ if (values.codex?.trim()) args.push("--codex", values.codex.trim());
363
+
364
+ if (values.once) args.push("--once");
365
+
366
+ const child = spawn(process.execPath, args, {
367
+ detached: true,
368
+ env: process.env,
369
+ shell: false,
370
+ stdio: "ignore",
371
+ windowsHide: true,
372
+ });
373
+
374
+ child.unref();
375
+
376
+ try {
377
+ current = await waitForBridge();
378
+ } catch (cause) {
379
+ fail(cause instanceof Error ? cause.message : String(cause));
380
+ }
381
+
382
+ if (!isExactBridge(current)) {
383
+ fail(`Mesurer Codex bridge at ${bridgeUrl.origin} started with an unexpected source identity.`);
384
+ }
385
+ }
386
+
387
+ const registration = { thread, cwd };
388
+
389
+ const appToolsPipe = process.env.CODEX_APP_TOOLS_PIPE_PATH?.trim();
390
+
391
+ if (appToolsPipe) registration.appToolsPipe = appToolsPipe;
392
+
393
+ const codexHome = process.env.CODEX_HOME?.trim();
394
+
395
+ if (codexHome) registration.codexHome = codexHome;
396
+
397
+ let response;
398
+
399
+ try {
400
+ response = await fetch(new URL("threads/register", bridgeUrl), {
401
+ method: "POST",
402
+ headers: { "Content-Type": "application/json" },
403
+ body: JSON.stringify(registration),
404
+ });
405
+ } catch (cause) {
406
+ const error = cause instanceof Error ? cause.message : String(cause);
407
+ fail(`Could not register Codex thread with ${bridgeUrl.origin}: ${error}`);
408
+ }
409
+
410
+ const payload = await readPayload(response);
411
+
412
+ if (!response.ok || payload.ok === false) {
413
+ fail(payload.error || `Mesurer Codex bridge returned HTTP ${response.status}.`);
414
+ }
415
+
416
+ if (!quiet) {
417
+ console.log(`Mesurer Codex bridge ready: ${bridgeUrl.origin}`);
418
+ console.log(`Registered Codex thread: ${thread}`);
419
+ console.log(`BRIDGE_URL=${bridgeUrl.origin}`);
420
+ console.log(`BRIDGE_THREAD=${thread}`);
421
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from "node:util";
4
+
5
+ const DEFAULT_BRIDGE = "http://127.0.0.1:47365";
6
+
7
+ const { values } = parseArgs({
8
+ options: {
9
+ bridge: { type: "string", default: DEFAULT_BRIDGE },
10
+ },
11
+ allowPositionals: false,
12
+ strict: true,
13
+ });
14
+
15
+ const readStdin = async () => {
16
+ process.stdin.setEncoding("utf8");
17
+ let input = "";
18
+
19
+ for await (const chunk of process.stdin) input += chunk;
20
+
21
+ return input;
22
+ };
23
+
24
+ try {
25
+ const input = await readStdin();
26
+ const event = JSON.parse(input);
27
+ const eventName = event?.hook_event_name;
28
+
29
+ if (!["UserPromptSubmit", "Stop", "Interrupt"].includes(eventName)) process.exit(0);
30
+
31
+ const sessionId = event?.session_id?.trim?.() || null;
32
+ const turnId = event?.turn_id?.trim?.() || null;
33
+
34
+ if (!sessionId || !turnId) process.exit(0);
35
+
36
+ const bridge = values.bridge?.trim() || DEFAULT_BRIDGE;
37
+ const base = bridge.endsWith("/") ? bridge : `${bridge}/`;
38
+ await fetch(new URL("lifecycle", base), {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify({
42
+ event: eventName,
43
+ sessionId,
44
+ turnId,
45
+ prompt: eventName === "UserPromptSubmit" ? event?.prompt ?? "" : undefined,
46
+ stopHookActive: eventName === "Stop" ? event?.stop_hook_active === true : undefined,
47
+ }),
48
+ }).catch(() => undefined);
49
+ } catch {
50
+ // Lifecycle telemetry is best-effort and must never block or inject output into Codex.
51
+ }
package/dist/context.d.ts CHANGED
@@ -314,6 +314,7 @@ export type MesurerWorkspaceContextSource = {
314
314
  position: number;
315
315
  }>;
316
316
  } | null;
317
+ elementRect?(element: Element): MesurerContextRect;
317
318
  currentSelection(): {
318
319
  elements: Element[];
319
320
  region: MesurerContextRect | null;