glasspane-mcp 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -0
  3. package/dist/audit-session.d.ts +22 -0
  4. package/dist/audit-session.d.ts.map +1 -0
  5. package/dist/audit-session.js +64 -0
  6. package/dist/audit-session.js.map +1 -0
  7. package/dist/canonical.d.ts +7 -0
  8. package/dist/canonical.d.ts.map +1 -0
  9. package/dist/canonical.js +23 -0
  10. package/dist/canonical.js.map +1 -0
  11. package/dist/dispatch.d.ts +47 -0
  12. package/dist/dispatch.d.ts.map +1 -0
  13. package/dist/dispatch.js +122 -0
  14. package/dist/dispatch.js.map +1 -0
  15. package/dist/engine-client.d.ts +54 -0
  16. package/dist/engine-client.d.ts.map +1 -0
  17. package/dist/engine-client.js +135 -0
  18. package/dist/engine-client.js.map +1 -0
  19. package/dist/errors.d.ts +23 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +22 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/evidence-report.d.ts +30 -0
  24. package/dist/evidence-report.d.ts.map +1 -0
  25. package/dist/evidence-report.js +161 -0
  26. package/dist/evidence-report.js.map +1 -0
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +1476 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/io.d.ts +51 -0
  32. package/dist/io.d.ts.map +1 -0
  33. package/dist/io.js +74 -0
  34. package/dist/io.js.map +1 -0
  35. package/dist/project-registry.d.ts +76 -0
  36. package/dist/project-registry.d.ts.map +1 -0
  37. package/dist/project-registry.js +144 -0
  38. package/dist/project-registry.js.map +1 -0
  39. package/dist/tools.d.ts +255 -0
  40. package/dist/tools.d.ts.map +1 -0
  41. package/dist/tools.js +556 -0
  42. package/dist/tools.js.map +1 -0
  43. package/package.json +51 -0
  44. package/schemas/decision-log-entry.schema.json +45 -0
  45. package/schemas/evidence-pack.schema.json +359 -0
  46. package/schemas/recipe-config.schema.json +36 -0
  47. package/schemas/schemas/decision-log-entry.schema.json +45 -0
  48. package/schemas/schemas/evidence-pack.schema.json +359 -0
  49. package/schemas/schemas/recipe-config.schema.json +36 -0
package/dist/index.js ADDED
@@ -0,0 +1,1476 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import process2 from "node:process";
5
+
6
+ // src/canonical.ts
7
+ function canonicalJson(value) {
8
+ return JSON.stringify(sortKeys(value));
9
+ }
10
+ function sortKeys(value) {
11
+ if (Array.isArray(value)) {
12
+ return value.map((entry) => sortKeys(entry));
13
+ }
14
+ if (value !== null && typeof value === "object") {
15
+ const record = value;
16
+ const sorted = {};
17
+ for (const key of Object.keys(record).sort()) {
18
+ sorted[key] = sortKeys(record[key]);
19
+ }
20
+ return sorted;
21
+ }
22
+ return value;
23
+ }
24
+
25
+ // src/tools.ts
26
+ import { z as z5 } from "zod";
27
+
28
+ // ../kernel/dist/schemas.js
29
+ import { createRequire } from "node:module";
30
+ var nodeRequire = createRequire(import.meta.url);
31
+ function loadSchema(relativePath) {
32
+ return nodeRequire(relativePath);
33
+ }
34
+ var EVIDENCE_PACK_JSON_SCHEMA = loadSchema("../schemas/evidence-pack.schema.json");
35
+ var DECISION_LOG_ENTRY_JSON_SCHEMA = loadSchema("../schemas/decision-log-entry.schema.json");
36
+ var RECIPE_CONFIG_JSON_SCHEMA = loadSchema("../schemas/recipe-config.schema.json");
37
+
38
+ // ../kernel/dist/evidence-pack.js
39
+ import { z } from "zod";
40
+ var OPERATION_ID_PATTERN = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
41
+ var ISO_MILLIS_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
42
+ var TREE_DIGEST_PATTERN = /^[0-9a-f]{32}$/;
43
+ var SELECTOR_MAX_LENGTH = 512;
44
+ var EVIDENCE_SCHEMA_VERSION = "glasspane.evidence/0.1-draft";
45
+ var AttributionLevelSchema = z.enum(["soft", "strong", "weak"]);
46
+ var CircuitBreakerLevelSchema = z.number().int().min(0).max(3);
47
+ var SelectorSchema = z.strictObject({
48
+ role: z.string().max(SELECTOR_MAX_LENGTH),
49
+ title: z.string().max(SELECTOR_MAX_LENGTH).optional(),
50
+ identifier: z.string().max(SELECTOR_MAX_LENGTH).optional()
51
+ });
52
+ var ActionSchema = z.enum([
53
+ "press",
54
+ "increment",
55
+ "decrement",
56
+ "showMenu",
57
+ "confirm",
58
+ "cancel",
59
+ "pick"
60
+ ]);
61
+ var AssertionPropertySchema = z.enum([
62
+ "title",
63
+ "value",
64
+ "role",
65
+ "enabled",
66
+ "focused"
67
+ ]);
68
+ var StringOrBoolSchema = z.union([z.string(), z.boolean()]);
69
+ var BoundsSchema = z.strictObject({
70
+ x: z.number(),
71
+ y: z.number(),
72
+ width: z.number(),
73
+ height: z.number()
74
+ });
75
+ var AxEventSignalSchema = z.strictObject({
76
+ treeDigestBefore: z.string().regex(TREE_DIGEST_PATTERN),
77
+ treeDigestAfter: z.string().regex(TREE_DIGEST_PATTERN),
78
+ nodeCount: z.number().int().min(0),
79
+ axChanged: z.boolean(),
80
+ latencyMs: z.number().min(0)
81
+ });
82
+ var PixelDiffSignalSchema = z.strictObject({
83
+ changedPixelRatio: z.number().min(0).max(1),
84
+ bounds: z.union([BoundsSchema, z.null()]),
85
+ windowId: z.number().int().min(0)
86
+ });
87
+ var ResponsivenessSignalSchema = z.strictObject({
88
+ responsive: z.boolean(),
89
+ pingMs: z.number().min(0)
90
+ });
91
+ var CrashSignalSchema = z.strictObject({
92
+ processAliveBefore: z.boolean(),
93
+ processAliveAfter: z.boolean()
94
+ });
95
+ var ActSignalSchema = z.strictObject({
96
+ selector: SelectorSchema,
97
+ action: ActionSchema,
98
+ actConfirmed: z.boolean()
99
+ });
100
+ var HandlerRefSchema = z.strictObject({
101
+ file: z.string().min(1).max(512),
102
+ line: z.number().int().min(0)
103
+ });
104
+ var HandlerProbeSignalSchema = z.strictObject({
105
+ probeVersion: z.string().min(1).max(64),
106
+ hitCount: z.number().int().min(0),
107
+ handlers: z.array(HandlerRefSchema).max(32),
108
+ lateCount: z.number().int().min(0)
109
+ });
110
+ var StateEntrySchema = z.strictObject({
111
+ key: z.string().min(1).max(512),
112
+ before: z.string().max(1024),
113
+ after: z.string().max(1024)
114
+ });
115
+ var StateDiffSignalSchema = z.strictObject({
116
+ source: z.enum(["z1-macro", "z2-mirror", "z3-kvc"]),
117
+ changed: z.boolean(),
118
+ entries: z.array(StateEntrySchema).max(64)
119
+ });
120
+ var SignalsSchema = z.strictObject({
121
+ act: ActSignalSchema,
122
+ axEvent: AxEventSignalSchema.optional(),
123
+ // Z5 channel has no in-process probes: explicit null is the honest boundary;
124
+ // P6 §3.1 opens the object alternative once a GlassPaneProbe connects.
125
+ handlerProbe: z.union([HandlerProbeSignalSchema, z.null()]),
126
+ stateDiff: z.union([StateDiffSignalSchema, z.null()]),
127
+ pixelDiff: PixelDiffSignalSchema.optional(),
128
+ responsiveness: ResponsivenessSignalSchema.optional(),
129
+ crash: CrashSignalSchema.optional()
130
+ });
131
+ var AttributionSchema = z.strictObject({
132
+ level: AttributionLevelSchema,
133
+ contaminated: z.boolean()
134
+ });
135
+ var CircuitBreakerSchema = z.strictObject({
136
+ level: CircuitBreakerLevelSchema,
137
+ reason: z.string().optional()
138
+ });
139
+ var AssertionSchema = z.strictObject({
140
+ kind: z.literal("element_property"),
141
+ selector: SelectorSchema,
142
+ property: AssertionPropertySchema,
143
+ expected: StringOrBoolSchema,
144
+ actual: StringOrBoolSchema,
145
+ passed: z.boolean()
146
+ });
147
+ var DiagnosisClassSchema = z.enum([
148
+ "T0",
149
+ "T1",
150
+ "T2",
151
+ "T3",
152
+ "T4",
153
+ "T5",
154
+ "T6",
155
+ "T7",
156
+ "T8",
157
+ "T9",
158
+ "NO_ANOMALY",
159
+ "INCONCLUSIVE"
160
+ ]);
161
+ var DiagnosisReportSchema = z.strictObject({
162
+ path: z.string(),
163
+ anomaly: z.string(),
164
+ evidence: z.string(),
165
+ next: z.string()
166
+ });
167
+ var DiagnosisSchema = z.strictObject({
168
+ class: DiagnosisClassSchema,
169
+ report: DiagnosisReportSchema
170
+ });
171
+ var EvidencePackSchema = z.strictObject({
172
+ schemaVersion: z.literal(EVIDENCE_SCHEMA_VERSION),
173
+ operationId: z.string().regex(OPERATION_ID_PATTERN),
174
+ createdAt: z.string().regex(ISO_MILLIS_PATTERN),
175
+ attribution: AttributionSchema,
176
+ circuitBreaker: CircuitBreakerSchema,
177
+ signals: SignalsSchema,
178
+ assertion: z.union([AssertionSchema, z.null()]).optional(),
179
+ diagnosis: z.union([DiagnosisSchema, z.null()]).optional()
180
+ });
181
+
182
+ // ../kernel/dist/decision-log-entry.js
183
+ import { z as z2 } from "zod";
184
+ var DECISION_ENTRY_ID_PATTERN = /^dl_[0-9A-HJKMNP-TV-Z]{26}$/;
185
+ var PREV_ENTRY_HASH_PATTERN = /^([0-9a-f]{64})?$/;
186
+ var SUMMARY_MAX_LENGTH = 2048;
187
+ var DecisionOutcomeSchema = z2.enum(["pass", "fail", "blocked", "inconclusive"]);
188
+ var DecisionLogEntrySchema = z2.strictObject({
189
+ entryId: z2.string().regex(DECISION_ENTRY_ID_PATTERN),
190
+ sequence: z2.number().int().min(0),
191
+ prevEntryHash: z2.string().regex(PREV_ENTRY_HASH_PATTERN),
192
+ operationId: z2.string().regex(OPERATION_ID_PATTERN).optional(),
193
+ summary: z2.string().max(SUMMARY_MAX_LENGTH),
194
+ outcome: DecisionOutcomeSchema,
195
+ createdAt: z2.string().regex(ISO_MILLIS_PATTERN)
196
+ });
197
+
198
+ // ../kernel/dist/recipe-config.js
199
+ import { z as z3 } from "zod";
200
+ var RECIPE_SCHEMA_VERSION = "glasspane.recipe/0.1-draft";
201
+ var RECIPE_NAME_MAX_LENGTH = 256;
202
+ var RECIPE_MIN_STEPS = 1;
203
+ var RECIPE_MAX_STEPS = 64;
204
+ var RecipeStepKindSchema = z3.enum(["act", "observe", "assert", "diagnose"]);
205
+ var RecipeStepSchema = z3.strictObject({
206
+ kind: RecipeStepKindSchema,
207
+ params: z3.record(z3.unknown())
208
+ });
209
+ var RecipeConfigSchema = z3.strictObject({
210
+ schemaVersion: z3.literal(RECIPE_SCHEMA_VERSION),
211
+ name: z3.string().max(RECIPE_NAME_MAX_LENGTH),
212
+ steps: z3.array(RecipeStepSchema).min(RECIPE_MIN_STEPS).max(RECIPE_MAX_STEPS)
213
+ });
214
+
215
+ // ../kernel/dist/errors.js
216
+ var KernelSchemaError = class extends Error {
217
+ code = "KERNEL_E_SCHEMA";
218
+ issues;
219
+ constructor(label, issues) {
220
+ const detail = issues.map((issue) => `${issue.path.length > 0 ? issue.path : "<root>"}: ${issue.message}`).join("; ");
221
+ super(`invalid ${label}: ${detail}`);
222
+ this.name = "KernelSchemaError";
223
+ this.issues = issues;
224
+ }
225
+ };
226
+
227
+ // ../kernel/dist/parse.js
228
+ function parseOrThrow(schema, input, label) {
229
+ const result = schema.safeParse(input);
230
+ if (result.success) {
231
+ return result.data;
232
+ }
233
+ const issues = result.error.issues.map((issue) => ({
234
+ path: issue.path.map((segment) => String(segment)).join("."),
235
+ message: issue.message
236
+ }));
237
+ throw new KernelSchemaError(label, issues);
238
+ }
239
+ function parseEvidencePack(input) {
240
+ return parseOrThrow(EvidencePackSchema, input, "evidence pack");
241
+ }
242
+
243
+ // src/engine-client.ts
244
+ import net from "node:net";
245
+ import os from "node:os";
246
+ import path from "node:path";
247
+ import fs from "node:fs";
248
+ import { fileURLToPath } from "node:url";
249
+
250
+ // src/io.ts
251
+ var MAX_FRAME_BYTES = 4 * 1024 * 1024;
252
+ var LineReader = class {
253
+ constructor(sink) {
254
+ this.sink = sink;
255
+ }
256
+ sink;
257
+ buffer = "";
258
+ /** @param chunk UTF-8 text fragment received from the stream. */
259
+ push(chunk) {
260
+ this.buffer += chunk;
261
+ let nl;
262
+ while ((nl = this.buffer.indexOf("\n")) !== -1) {
263
+ const line = this.buffer.slice(0, nl);
264
+ this.buffer = this.buffer.slice(nl + 1);
265
+ const bytes = Buffer.byteLength(line, "utf8");
266
+ if (bytes > MAX_FRAME_BYTES) {
267
+ this.sink.onOversize(bytes);
268
+ } else {
269
+ this.sink.onLine(line);
270
+ }
271
+ }
272
+ }
273
+ /** Unconsumed partial line (tests / graceful close inspection). */
274
+ pendingText() {
275
+ return this.buffer;
276
+ }
277
+ };
278
+ var StreamLineIo = class {
279
+ constructor(input, output) {
280
+ this.input = input;
281
+ this.output = output;
282
+ this.reader = new LineReader({
283
+ onLine: (line) => this.messageHandler?.(line),
284
+ onOversize: () => this.errorHandler?.(
285
+ new Error("frame exceeds " + MAX_FRAME_BYTES + " bytes")
286
+ )
287
+ });
288
+ input.setEncoding("utf8");
289
+ input.on("data", (chunk) => this.reader.push(chunk));
290
+ input.on("close", () => this.closeHandler?.());
291
+ input.on("error", (error) => this.errorHandler?.(error));
292
+ }
293
+ input;
294
+ output;
295
+ reader;
296
+ messageHandler = null;
297
+ closeHandler = null;
298
+ errorHandler = null;
299
+ writeLine(line) {
300
+ this.output.write(line + "\n");
301
+ }
302
+ onMessage(handler) {
303
+ this.messageHandler = handler;
304
+ }
305
+ onClose(handler) {
306
+ this.closeHandler = handler;
307
+ }
308
+ onError(handler) {
309
+ this.errorHandler = handler;
310
+ }
311
+ close() {
312
+ this.input.destroy();
313
+ }
314
+ };
315
+
316
+ // src/errors.ts
317
+ var GP_E_ENGINE_UNREACHABLE = "GP_E_ENGINE_UNREACHABLE";
318
+ var GP_E_BAD_PARAMS = "GP_E_BAD_PARAMS";
319
+ var GP_E_NO_EVIDENCE = "GP_E_NO_EVIDENCE";
320
+ var GP_E_INTERNAL = "GP_E_INTERNAL";
321
+ var GP_E_PROJECT_LIMIT = "GP_E_PROJECT_LIMIT";
322
+ var GP_E_NOT_FOUND = "GP_E_NOT_FOUND";
323
+ function formatToolError(code, message, remedy) {
324
+ return `${code}: ${message} | remedy: ${remedy}`;
325
+ }
326
+ function formatToolErrorShape(error) {
327
+ return formatToolError(error.code, error.message, error.remedy);
328
+ }
329
+
330
+ // src/engine-client.ts
331
+ function daemonUnreachableRemedy() {
332
+ const here = path.dirname(fileURLToPath(import.meta.url));
333
+ const installerCli = path.resolve(here, "..", "..", "installer", "cli.js");
334
+ if (fs.existsSync(installerCli)) {
335
+ return `run "node ${installerCli} --restore-launchd" (auto-restores and verifies the launchd-managed daemon), then retry`;
336
+ }
337
+ return "ensure the glasspane daemon is running (launchd job com.glasspane.daemon; in a repo checkout run `node installer/cli.js --restore-launchd`), then retry";
338
+ }
339
+ var ENGINE_SOCKET_ENV = "GLASSPANE_ENGINE_SOCK";
340
+ var ENGINE_TIMEOUT_MS = 1e4;
341
+ function defaultSocketPath() {
342
+ return process.env[ENGINE_SOCKET_ENV] ?? path.join(os.homedir(), ".glasspane", "engine.sock");
343
+ }
344
+ var EngineCallError = class extends Error {
345
+ code;
346
+ message;
347
+ remedy;
348
+ constructor(code, message, remedy) {
349
+ super(`${code}: ${message}`);
350
+ this.name = "EngineCallError";
351
+ this.code = code;
352
+ this.message = message;
353
+ this.remedy = remedy;
354
+ }
355
+ toBody() {
356
+ return { code: this.code, message: this.message, remedy: this.remedy };
357
+ }
358
+ };
359
+ var EngineJsonRpcClient = class {
360
+ constructor(io, timeoutMs = ENGINE_TIMEOUT_MS) {
361
+ this.io = io;
362
+ this.timeoutMs = timeoutMs;
363
+ this.io.onMessage((line) => this.handleMessage(line));
364
+ this.io.onError((error) => this.failAll(new EngineCallError(
365
+ GP_E_ENGINE_UNREACHABLE,
366
+ `engine transport error: ${error.message}`,
367
+ daemonUnreachableRemedy()
368
+ )));
369
+ this.io.onClose(() => this.failAll(new EngineCallError(
370
+ GP_E_ENGINE_UNREACHABLE,
371
+ "engine transport closed",
372
+ daemonUnreachableRemedy()
373
+ )));
374
+ }
375
+ io;
376
+ timeoutMs;
377
+ nextId = 0;
378
+ pending = /* @__PURE__ */ new Map();
379
+ /** Send a request and await its matching response frame. */
380
+ call(method, params) {
381
+ const id = this.nextId++;
382
+ const frame = { id, method, ...params === void 0 ? {} : { params } };
383
+ return new Promise((resolve, reject) => {
384
+ const timer = setTimeout(() => {
385
+ this.pending.delete(id);
386
+ reject(new EngineCallError(
387
+ GP_E_ENGINE_UNREACHABLE,
388
+ `engine request timed out after ${this.timeoutMs}ms (method ${method})`,
389
+ daemonUnreachableRemedy()
390
+ ));
391
+ }, this.timeoutMs);
392
+ this.pending.set(id, {
393
+ resolve,
394
+ reject: (reason) => {
395
+ clearTimeout(timer);
396
+ reject(reason);
397
+ }
398
+ });
399
+ this.io.writeLine(JSON.stringify(frame));
400
+ });
401
+ }
402
+ close() {
403
+ this.io.close();
404
+ }
405
+ handleMessage(line) {
406
+ let frame;
407
+ try {
408
+ frame = JSON.parse(line);
409
+ } catch {
410
+ return;
411
+ }
412
+ const entry = this.pending.get(frame.id);
413
+ if (!entry) {
414
+ return;
415
+ }
416
+ this.pending.delete(frame.id);
417
+ if (frame.error) {
418
+ entry.reject(new EngineCallError(frame.error.code, frame.error.message, frame.error.remedy));
419
+ } else {
420
+ entry.resolve(frame.result);
421
+ }
422
+ }
423
+ failAll(reason) {
424
+ for (const entry of this.pending.values()) {
425
+ entry.reject(reason);
426
+ }
427
+ this.pending.clear();
428
+ }
429
+ };
430
+ function unixSocketEngineClient(socketPath) {
431
+ const socket = net.createConnection(socketPath);
432
+ const io = new StreamLineIo(socket, socket);
433
+ return new EngineJsonRpcClient(io);
434
+ }
435
+
436
+ // src/audit-session.ts
437
+ var AUDIT_HISTORY_LIMIT = 20;
438
+ var EvidenceAuditSession = class {
439
+ ids = [];
440
+ /** Record every operationId/evidenceId found in an engine result frame. */
441
+ record(result) {
442
+ for (const id of collectOperationIds(result)) {
443
+ if (!this.ids.includes(id)) {
444
+ this.ids.push(id);
445
+ if (this.ids.length > AUDIT_HISTORY_LIMIT) {
446
+ this.ids.splice(0, this.ids.length - AUDIT_HISTORY_LIMIT);
447
+ }
448
+ }
449
+ }
450
+ }
451
+ /** Clear the trail (called after a successful attach). */
452
+ reset() {
453
+ this.ids = [];
454
+ }
455
+ /** The most recent `limit` recorded operationIds, oldest first. */
456
+ recentIds(limit) {
457
+ return this.ids.slice(-limit);
458
+ }
459
+ };
460
+ function collectOperationIds(value, depth = 0) {
461
+ const out = [];
462
+ if (depth > 3 || value === null || typeof value !== "object") {
463
+ return out;
464
+ }
465
+ if (Array.isArray(value)) {
466
+ for (const item of value) {
467
+ out.push(...collectOperationIds(item, depth + 1));
468
+ }
469
+ return out;
470
+ }
471
+ const record = value;
472
+ for (const key of Object.keys(record)) {
473
+ const entry = record[key];
474
+ if ((key === "operationId" || key === "evidenceId") && typeof entry === "string") {
475
+ out.push(entry);
476
+ } else {
477
+ out.push(...collectOperationIds(entry, depth + 1));
478
+ }
479
+ }
480
+ return out;
481
+ }
482
+
483
+ // src/evidence-report.ts
484
+ var PLACEHOLDER = "\u2014";
485
+ function reportTitle(pack) {
486
+ return `${pack.operationId} \xB7 ${measureLabel(pack)} \xB7 ${levelLabel(pack.circuitBreaker.level)}`;
487
+ }
488
+ function evidenceSummaryLines(pack) {
489
+ const lines = [];
490
+ lines.push(`attribution: ${pack.attribution.level} | contaminated=${pack.attribution.contaminated}`);
491
+ const reason = pack.circuitBreaker.reason === void 0 ? "" : ` | reason: ${pack.circuitBreaker.reason}`;
492
+ lines.push(`circuitBreaker: level ${pack.circuitBreaker.level} (${levelLabel(pack.circuitBreaker.level)})${reason}`);
493
+ if (pack.assertion !== void 0 && pack.assertion !== null) {
494
+ const a = pack.assertion;
495
+ lines.push(
496
+ `assert ${a.property} on ${selectorText(a.selector)}: expected=${valueText(a.expected)} actual=${valueText(a.actual)} passed=${a.passed}`
497
+ );
498
+ }
499
+ const act = pack.signals.act;
500
+ lines.push(`act ${act.action} ${selectorText(act.selector)}: ${act.actConfirmed ? "confirmed" : "rejected"}`);
501
+ if (pack.signals.axEvent !== void 0) {
502
+ const ax = pack.signals.axEvent;
503
+ lines.push(`axEvent: changed=${ax.axChanged} nodes=${ax.nodeCount} latencyMs=${doubleText(ax.latencyMs)}`);
504
+ }
505
+ if (pack.signals.pixelDiff !== void 0) {
506
+ const px = pack.signals.pixelDiff;
507
+ let line = `pixelDiff: changedRatio=${doubleText(px.changedPixelRatio)} windowId=${px.windowId}`;
508
+ if (px.bounds !== null) {
509
+ const b = px.bounds;
510
+ line += ` bounds=${doubleText(b.x)},${doubleText(b.y)},${doubleText(b.width)},${doubleText(b.height)}`;
511
+ }
512
+ lines.push(line);
513
+ }
514
+ if (pack.signals.responsiveness !== void 0) {
515
+ const r = pack.signals.responsiveness;
516
+ lines.push(`responsiveness: responsive=${r.responsive} pingMs=${doubleText(r.pingMs)}`);
517
+ }
518
+ if (pack.signals.crash !== void 0) {
519
+ const c = pack.signals.crash;
520
+ lines.push(`crash: aliveBefore=${c.processAliveBefore} aliveAfter=${c.processAliveAfter}`);
521
+ }
522
+ return lines;
523
+ }
524
+ function renderMarkdown(pack, diagnostics) {
525
+ const out = [];
526
+ out.push(`# ${reportTitle(pack)}`);
527
+ out.push("");
528
+ out.push(`**operationId**: ${pack.operationId}`);
529
+ out.push(`**createdAt**: ${pack.createdAt}`);
530
+ out.push(`**schemaVersion**: ${pack.schemaVersion}`);
531
+ for (const line of evidenceSummaryLines(pack)) {
532
+ out.push(`* ${line}`);
533
+ }
534
+ out.push("");
535
+ for (const section of sections(pack, diagnostics)) {
536
+ out.push(`## ${section.title}`);
537
+ out.push("");
538
+ out.push(section.body);
539
+ out.push("");
540
+ }
541
+ return out.join("\n");
542
+ }
543
+ function renderHTML(pack, diagnostics) {
544
+ const out = [];
545
+ out.push(`<div class="gp-evidence" id="${escapeHTML(pack.operationId)}">`);
546
+ out.push(`<h1>${escapeHTML(reportTitle(pack))}</h1>`);
547
+ out.push(`<dl class="gp-summary">`);
548
+ out.push(`<dt>operationId</dt><dd>${escapeHTML(pack.operationId)}</dd>`);
549
+ out.push(`<dt>createdAt</dt><dd>${escapeHTML(pack.createdAt)}</dd>`);
550
+ out.push(`<dt>schemaVersion</dt><dd>${escapeHTML(pack.schemaVersion)}</dd>`);
551
+ for (const line of evidenceSummaryLines(pack)) {
552
+ out.push(`<dt class="gp-line">\xB7</dt><dd>${escapeHTML(line)}</dd>`);
553
+ }
554
+ out.push(`</dl>`);
555
+ for (const section of sections(pack, diagnostics)) {
556
+ out.push(`<section class="gp-section">`);
557
+ out.push(`<h2>${escapeHTML(section.title)}</h2>`);
558
+ if (section.preformatted) {
559
+ out.push(`<pre>${escapeHTML(section.body)}</pre>`);
560
+ } else {
561
+ out.push(`<p>${escapeHTML(section.body).replace(/\n/g, "<br>")}</p>`);
562
+ }
563
+ out.push(`</section>`);
564
+ }
565
+ out.push(`</div>`);
566
+ return out.join("\n");
567
+ }
568
+ function escapeHTML(value) {
569
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
570
+ }
571
+ function sections(pack, diagnostics) {
572
+ const report = pack.diagnosis === null || pack.diagnosis === void 0 ? void 0 : pack.diagnosis.report;
573
+ const hasDiagnosticsText = diagnostics !== void 0 && diagnostics !== "";
574
+ const reportEvidence = nonEmpty(report?.evidence);
575
+ return [
576
+ { title: "PATH", body: nonEmpty(report?.path) ?? PLACEHOLDER, preformatted: false },
577
+ { title: "ANOMALY", body: nonEmpty(report?.anomaly) ?? PLACEHOLDER, preformatted: false },
578
+ {
579
+ title: "EVIDENCE",
580
+ body: reportEvidence ?? (hasDiagnosticsText && diagnostics !== void 0 ? diagnostics : PLACEHOLDER),
581
+ preformatted: reportEvidence === void 0 && hasDiagnosticsText
582
+ },
583
+ { title: "NEXT", body: nonEmpty(report?.next) ?? PLACEHOLDER, preformatted: false }
584
+ ];
585
+ }
586
+ function nonEmpty(value) {
587
+ return value !== void 0 && value !== "" ? value : void 0;
588
+ }
589
+ function measureLabel(pack) {
590
+ if (pack.diagnosis !== null && pack.diagnosis !== void 0) {
591
+ return pack.diagnosis.class;
592
+ }
593
+ if (pack.assertion !== null && pack.assertion !== void 0) {
594
+ return pack.assertion.property;
595
+ }
596
+ return pack.signals.act.action;
597
+ }
598
+ function levelLabel(level) {
599
+ switch (level) {
600
+ case 0:
601
+ return "normal";
602
+ case 1:
603
+ return "degraded";
604
+ case 2:
605
+ return "act_failed_channel_alive";
606
+ case 3:
607
+ return "channel_fault";
608
+ default:
609
+ return String(level);
610
+ }
611
+ }
612
+ function selectorText(selector) {
613
+ let text = selector.role;
614
+ if (selector.title !== void 0 && selector.title !== "") {
615
+ text += ` "${selector.title}"`;
616
+ }
617
+ if (selector.identifier !== void 0 && selector.identifier !== "") {
618
+ text += ` #${selector.identifier}`;
619
+ }
620
+ return text;
621
+ }
622
+ function valueText(value) {
623
+ return String(value);
624
+ }
625
+ function doubleText(value) {
626
+ return String(value);
627
+ }
628
+
629
+ // src/project-registry.ts
630
+ import fs2 from "node:fs";
631
+ import os2 from "node:os";
632
+ import path2 from "node:path";
633
+ import { z as z4 } from "zod";
634
+ var CROCKFORD_BODY_RE = "[0-9A-HJKMNP-TV-Z]{26}";
635
+ var PROJECT_ID_RE = new RegExp(`^prj_${CROCKFORD_BODY_RE}$`);
636
+ var MAX_PROJECTS = 128;
637
+ var MAX_DISPLAY_NAME_LENGTH = 256;
638
+ var MAX_FIELD_LENGTH = 1024;
639
+ var ProjectListArgs = z4.strictObject({});
640
+ var ProjectGetArgs = z4.strictObject({
641
+ projectId: z4.string().regex(PROJECT_ID_RE, "projectId must match prj_ + 26 Crockford chars")
642
+ });
643
+ var ProjectSetArgs = z4.strictObject({
644
+ projectId: z4.string().regex(PROJECT_ID_RE).optional(),
645
+ displayName: z4.string().min(1).max(MAX_DISPLAY_NAME_LENGTH),
646
+ bundleId: z4.string().max(256).optional(),
647
+ pid: z4.number().int().nonnegative().optional(),
648
+ recipeConfigPath: z4.string().max(MAX_FIELD_LENGTH).optional(),
649
+ calibrationAssetsPath: z4.string().max(MAX_FIELD_LENGTH).optional(),
650
+ evidenceStoragePath: z4.string().max(MAX_FIELD_LENGTH).optional()
651
+ }).refine(
652
+ (v) => v.bundleId !== void 0 !== (v.pid !== void 0),
653
+ { message: "exactly one of bundleId or pid is required" }
654
+ );
655
+ function projectsPath() {
656
+ const envPath = process.env["GLASSPANE_PROJECTS_FILE"];
657
+ if (envPath) return envPath;
658
+ return path2.join(os2.homedir(), ".glasspane", "projects.json");
659
+ }
660
+ function loadProjects(filePath) {
661
+ try {
662
+ const raw = fs2.readFileSync(filePath, "utf8");
663
+ const parsed = JSON.parse(raw);
664
+ if (!Array.isArray(parsed)) return [];
665
+ return parsed;
666
+ } catch {
667
+ return [];
668
+ }
669
+ }
670
+ function saveProjects(filePath, entries) {
671
+ const dir = path2.dirname(filePath);
672
+ fs2.mkdirSync(dir, { recursive: true });
673
+ const tmpPath = `${filePath}.tmp`;
674
+ fs2.writeFileSync(tmpPath, JSON.stringify(entries, null, 2), "utf8");
675
+ fs2.renameSync(tmpPath, filePath);
676
+ }
677
+ function projectList() {
678
+ return loadProjects(projectsPath());
679
+ }
680
+ function projectGet(projectId) {
681
+ const projects = loadProjects(projectsPath());
682
+ return projects.find((p) => p.projectId === projectId);
683
+ }
684
+ function projectSet(args) {
685
+ const filePath = projectsPath();
686
+ const projects = loadProjects(filePath);
687
+ let entry;
688
+ const projectId = args.projectId;
689
+ if (projectId) {
690
+ const idx = projects.findIndex((p) => p.projectId === projectId);
691
+ if (idx === -1) {
692
+ throw new ProjectRegistryError("GP_E_NOT_FOUND", `unknown projectId ${projectId}`);
693
+ }
694
+ const existing = projects[idx];
695
+ entry = {
696
+ ...existing,
697
+ displayName: args.displayName,
698
+ ...args.bundleId !== void 0 ? { bundleId: args.bundleId } : {},
699
+ ...args.pid !== void 0 ? { pid: args.pid } : {},
700
+ ...args.recipeConfigPath !== void 0 ? { recipeConfigPath: args.recipeConfigPath } : {},
701
+ ...args.calibrationAssetsPath !== void 0 ? { calibrationAssetsPath: args.calibrationAssetsPath } : {},
702
+ ...args.evidenceStoragePath !== void 0 ? { evidenceStoragePath: args.evidenceStoragePath } : {}
703
+ };
704
+ projects[idx] = entry;
705
+ } else {
706
+ if (projects.length >= MAX_PROJECTS) {
707
+ throw new ProjectRegistryError("GP_E_PROJECT_LIMIT", `project limit reached (${MAX_PROJECTS})`);
708
+ }
709
+ const projectId2 = generateProjectId();
710
+ entry = {
711
+ projectId: projectId2,
712
+ displayName: args.displayName,
713
+ bundleId: args.bundleId,
714
+ pid: args.pid,
715
+ recipeConfigPath: args.recipeConfigPath,
716
+ calibrationAssetsPath: args.calibrationAssetsPath,
717
+ evidenceStoragePath: args.evidenceStoragePath,
718
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
719
+ };
720
+ projects.push(entry);
721
+ }
722
+ saveProjects(filePath, projects);
723
+ return entry;
724
+ }
725
+ function generateProjectId() {
726
+ const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
727
+ const body = [];
728
+ const bytes = new Uint8Array(26);
729
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
730
+ globalThis.crypto.getRandomValues(bytes);
731
+ } else {
732
+ for (let i = 0; i < 26; i++) bytes[i] = Math.floor(Math.random() * 256);
733
+ }
734
+ for (let i = 0; i < 26; i++) {
735
+ const byte = bytes[i];
736
+ body.push(CROCKFORD.charAt(byte % 32));
737
+ }
738
+ return `prj_${body.join("")}`;
739
+ }
740
+ var ProjectRegistryError = class extends Error {
741
+ code;
742
+ constructor(code, message) {
743
+ super(message);
744
+ this.name = "ProjectRegistryError";
745
+ this.code = code;
746
+ }
747
+ };
748
+
749
+ // src/tools.ts
750
+ var OptionalUintSchema = z5.number().int().nonnegative().optional();
751
+ var OptionalDepthSchema = z5.number().int().min(1).max(10).optional();
752
+ var OptionalRoleSchema = z5.string().max(128).optional();
753
+ var OptionalBundleIdSchema = z5.string().max(256).optional();
754
+ var OptionalOperationIdSchema = z5.string().regex(/^op_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
755
+ var OptionalProjectIdSchema = z5.string().regex(/^prj_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
756
+ var ExpectSchema = z5.union([z5.string().max(512), z5.boolean()]);
757
+ var SnapshotIdSchema = z5.string().regex(/^snap_[0-9A-HJKMNP-TV-Z]{26}$/);
758
+ var ActStepSchema = z5.strictObject({
759
+ selector: SelectorSchema,
760
+ action: ActionSchema
761
+ });
762
+ var RestoreStepsSchema = z5.array(ActStepSchema).min(1).max(64).optional();
763
+ var RestoreModeSchema = z5.string().max(32).optional();
764
+ var AttachArgs = z5.strictObject({
765
+ bundleId: OptionalBundleIdSchema,
766
+ pid: OptionalUintSchema,
767
+ projectId: OptionalProjectIdSchema
768
+ }).refine((v) => v.bundleId !== void 0 || v.pid !== void 0, {
769
+ message: "attach requires either bundleId or pid"
770
+ });
771
+ var ObserveArgs = z5.strictObject({
772
+ maxDepth: OptionalDepthSchema,
773
+ role: OptionalRoleSchema
774
+ });
775
+ var ActArgs = z5.strictObject({
776
+ selector: SelectorSchema,
777
+ action: ActionSchema,
778
+ degrade: z5.boolean().optional()
779
+ });
780
+ var AssertElementArgs = z5.strictObject({
781
+ selector: SelectorSchema,
782
+ property: AssertionPropertySchema,
783
+ expected: ExpectSchema
784
+ });
785
+ var DiagnoseArgs = z5.strictObject({
786
+ operationId: OptionalOperationIdSchema
787
+ });
788
+ var LastEvidenceArgs = z5.strictObject({
789
+ operationId: OptionalOperationIdSchema
790
+ });
791
+ var SnapshotArgs = z5.strictObject({
792
+ maxDepth: OptionalDepthSchema
793
+ });
794
+ var RestoreArgs = z5.strictObject({
795
+ snapshotId: SnapshotIdSchema,
796
+ steps: RestoreStepsSchema,
797
+ mode: RestoreModeSchema
798
+ });
799
+ var ProbeStatusArgs = z5.strictObject({});
800
+ var ReportFormatSchema = z5.enum(["html", "markdown"]).default("markdown");
801
+ var ExportEvidenceArgs = z5.strictObject({
802
+ operationId: z5.string().regex(/^op_[0-9A-HJKMNP-TV-Z]{26}$/),
803
+ format: ReportFormatSchema
804
+ });
805
+ var RecentReportsArgs = z5.strictObject({
806
+ limit: z5.number().int().min(1).max(20).default(5),
807
+ format: ReportFormatSchema
808
+ });
809
+ function zodIssueText(error) {
810
+ return error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
811
+ }
812
+ function zodBridge(schema) {
813
+ return (raw) => {
814
+ const parsed = schema.safeParse(raw);
815
+ if (parsed.success) {
816
+ return { ok: true, value: parsed.data };
817
+ }
818
+ return { ok: false, issues: zodIssueText(parsed.error) };
819
+ };
820
+ }
821
+ var selectorJsonSchema = {
822
+ type: "object",
823
+ additionalProperties: false,
824
+ properties: {
825
+ role: { type: "string", maxLength: 512 },
826
+ title: { type: "string", maxLength: 512 },
827
+ identifier: { type: "string", maxLength: 512 }
828
+ },
829
+ required: ["role"]
830
+ };
831
+ var TOOL_SPECS = [
832
+ {
833
+ name: "gp_attach",
834
+ description: "Attach the engine to a running app by bundleId or pid.",
835
+ engineMethod: "attach",
836
+ inputSchema: {
837
+ type: "object",
838
+ additionalProperties: false,
839
+ properties: {
840
+ bundleId: { type: "string", maxLength: 256 },
841
+ pid: { type: "integer", minimum: 0 },
842
+ projectId: { type: "string", pattern: "^prj_[0-9A-HJKMNP-TV-Z]{26}$" }
843
+ },
844
+ oneOf: [
845
+ { required: ["bundleId"] },
846
+ { required: ["pid"] }
847
+ ]
848
+ },
849
+ validate: zodBridge(AttachArgs)
850
+ },
851
+ {
852
+ name: "gp_observe",
853
+ description: "Snapshot the attached app's accessibility tree.",
854
+ engineMethod: "observe",
855
+ inputSchema: {
856
+ type: "object",
857
+ additionalProperties: false,
858
+ properties: {
859
+ maxDepth: { type: "integer", minimum: 1, maximum: 10 },
860
+ role: { type: "string", maxLength: 128 }
861
+ }
862
+ },
863
+ validate: zodBridge(ObserveArgs)
864
+ },
865
+ {
866
+ name: "gp_act",
867
+ description: "Perform a UI action on the attached app and confirm it. If the daemon reports GP_E_BUSY_INPUT (real user input is contaminating the window), retry later, or set degrade: true to proceed immediately with the contamination recorded in evidence (attribution becomes weak + contaminated=true) \u2014 never silently clean.",
868
+ engineMethod: "act",
869
+ inputSchema: {
870
+ type: "object",
871
+ additionalProperties: false,
872
+ properties: {
873
+ selector: selectorJsonSchema,
874
+ action: {
875
+ type: "string",
876
+ enum: ["press", "increment", "decrement", "showMenu", "confirm", "cancel", "pick"]
877
+ },
878
+ degrade: { type: "boolean" }
879
+ },
880
+ required: ["selector", "action"]
881
+ },
882
+ validate: zodBridge(ActArgs)
883
+ },
884
+ {
885
+ name: "gp_assert_element",
886
+ description: "Assert a property of an element in the attached app.",
887
+ engineMethod: "assert_element",
888
+ inputSchema: {
889
+ type: "object",
890
+ additionalProperties: false,
891
+ properties: {
892
+ selector: selectorJsonSchema,
893
+ property: { type: "string", enum: ["title", "value", "role", "enabled", "focused"] },
894
+ expected: { oneOf: [{ type: "string", maxLength: 512 }, { type: "boolean" }] }
895
+ },
896
+ required: ["selector", "property", "expected"]
897
+ },
898
+ validate: zodBridge(AssertElementArgs)
899
+ },
900
+ {
901
+ name: "gp_diagnose",
902
+ description: "Diagnose the most recent (or the given) operation.",
903
+ engineMethod: "diagnose",
904
+ inputSchema: {
905
+ type: "object",
906
+ additionalProperties: false,
907
+ properties: {
908
+ operationId: { type: "string", pattern: "^op_[0-9A-HJKMNP-TV-Z]{26}$" }
909
+ }
910
+ },
911
+ validate: zodBridge(DiagnoseArgs)
912
+ },
913
+ {
914
+ name: "gp_last_evidence",
915
+ description: "Return the full evidence pack for the most recent (or given) operation.",
916
+ engineMethod: "last_evidence",
917
+ inputSchema: {
918
+ type: "object",
919
+ additionalProperties: false,
920
+ properties: {
921
+ operationId: { type: "string", pattern: "^op_[0-9A-HJKMNP-TV-Z]{26}$" }
922
+ }
923
+ },
924
+ validate: zodBridge(LastEvidenceArgs)
925
+ },
926
+ {
927
+ name: "gp_snapshot",
928
+ description: "Capture a digest-only baseline of the attached app's AX tree for later restore.",
929
+ engineMethod: "snapshot",
930
+ inputSchema: {
931
+ type: "object",
932
+ additionalProperties: false,
933
+ properties: {
934
+ maxDepth: { type: "integer", minimum: 1, maximum: 10 }
935
+ }
936
+ },
937
+ validate: zodBridge(SnapshotArgs)
938
+ },
939
+ {
940
+ name: "gp_restore",
941
+ description: "Restore from a snapshot: replay act steps over the baseline (tier-2 ffwd), or compare the current tree against it when steps are omitted.",
942
+ engineMethod: "restore",
943
+ inputSchema: {
944
+ type: "object",
945
+ additionalProperties: false,
946
+ properties: {
947
+ snapshotId: { type: "string", pattern: "^snap_[0-9A-HJKMNP-TV-Z]{26}$" },
948
+ steps: {
949
+ type: "array",
950
+ minItems: 1,
951
+ maxItems: 64,
952
+ items: {
953
+ type: "object",
954
+ additionalProperties: false,
955
+ properties: {
956
+ selector: selectorJsonSchema,
957
+ action: {
958
+ type: "string",
959
+ enum: ["press", "increment", "decrement", "showMenu", "confirm", "cancel", "pick"]
960
+ }
961
+ },
962
+ required: ["selector", "action"]
963
+ }
964
+ },
965
+ mode: { type: "string", maxLength: 32 }
966
+ },
967
+ required: ["snapshotId"]
968
+ },
969
+ validate: zodBridge(RestoreArgs)
970
+ },
971
+ {
972
+ name: "gp_probe_status",
973
+ description: "List GlassPaneProbe connections (pid, capabilities, events seen) and whether the attached app has a live probe \u2014 probe presence is what makes T4/T5/T7/T8 verdicts and tier-1 checkpoint restores decidable.",
974
+ engineMethod: "probe_status",
975
+ inputSchema: {
976
+ type: "object",
977
+ additionalProperties: false,
978
+ properties: {}
979
+ },
980
+ validate: zodBridge(ProbeStatusArgs)
981
+ },
982
+ {
983
+ name: "gp_export_evidence",
984
+ description: "Render one operation's evidence pack as a human-readable report (HTML or Markdown) for the developer audit view.",
985
+ engineMethod: "export_evidence",
986
+ inputSchema: {
987
+ type: "object",
988
+ additionalProperties: false,
989
+ properties: {
990
+ operationId: { type: "string", pattern: "^op_[0-9A-HJKMNP-TV-Z]{26}$" },
991
+ format: { type: "string", enum: ["html", "markdown"] }
992
+ },
993
+ required: ["operationId"]
994
+ },
995
+ validate: zodBridge(ExportEvidenceArgs),
996
+ execute: exportEvidence
997
+ },
998
+ {
999
+ name: "gp_recent_reports",
1000
+ description: "Aggregate the most recent operations' four-section reports (HTML or Markdown) into one audit view.",
1001
+ engineMethod: "recent_reports",
1002
+ inputSchema: {
1003
+ type: "object",
1004
+ additionalProperties: false,
1005
+ properties: {
1006
+ limit: { type: "integer", minimum: 1, maximum: 20 },
1007
+ format: { type: "string", enum: ["html", "markdown"] }
1008
+ }
1009
+ },
1010
+ validate: zodBridge(RecentReportsArgs),
1011
+ execute: recentReports
1012
+ },
1013
+ {
1014
+ name: "gp_project_list",
1015
+ description: "List all registered GlassPane projects (P1 spec v1.4).",
1016
+ engineMethod: "project_list",
1017
+ inputSchema: {
1018
+ type: "object",
1019
+ additionalProperties: false,
1020
+ properties: {}
1021
+ },
1022
+ validate: zodBridge(ProjectListArgs),
1023
+ execute: projectListTool
1024
+ },
1025
+ {
1026
+ name: "gp_project_set",
1027
+ description: "Create or update a GlassPane project. Omit projectId to register a new project; include it to update an existing one.",
1028
+ engineMethod: "project_set",
1029
+ inputSchema: {
1030
+ type: "object",
1031
+ additionalProperties: false,
1032
+ properties: {
1033
+ projectId: { type: "string", pattern: "^prj_[0-9A-HJKMNP-TV-Z]{26}$" },
1034
+ displayName: { type: "string", minLength: 1, maxLength: 256 },
1035
+ bundleId: { type: "string", maxLength: 256 },
1036
+ pid: { type: "integer", minimum: 0 },
1037
+ recipeConfigPath: { type: "string", maxLength: 1024 },
1038
+ calibrationAssetsPath: { type: "string", maxLength: 1024 },
1039
+ evidenceStoragePath: { type: "string", maxLength: 1024 }
1040
+ },
1041
+ required: ["displayName"],
1042
+ oneOf: [
1043
+ { required: ["bundleId"] },
1044
+ { required: ["pid"] }
1045
+ ]
1046
+ },
1047
+ validate: zodBridge(ProjectSetArgs),
1048
+ execute: projectSetTool
1049
+ },
1050
+ {
1051
+ name: "gp_project_get",
1052
+ description: "Fetch one GlassPane project by its project ID.",
1053
+ engineMethod: "project_get",
1054
+ inputSchema: {
1055
+ type: "object",
1056
+ additionalProperties: false,
1057
+ properties: {
1058
+ projectId: { type: "string", pattern: "^prj_[0-9A-HJKMNP-TV-Z]{26}$" }
1059
+ },
1060
+ required: ["projectId"]
1061
+ },
1062
+ validate: zodBridge(ProjectGetArgs),
1063
+ execute: projectGetTool
1064
+ }
1065
+ ];
1066
+ var TOOL_BY_NAME = new Map(
1067
+ TOOL_SPECS.map((spec) => [spec.name, spec])
1068
+ );
1069
+ async function executeTool(spec, args, engine, session = new EvidenceAuditSession()) {
1070
+ const checked = spec.validate(args);
1071
+ if (!checked.ok) {
1072
+ return {
1073
+ content: [{ type: "text", text: formatToolError(
1074
+ GP_E_BAD_PARAMS,
1075
+ `invalid arguments for ${spec.name}`,
1076
+ `check the tool's input schema; ${checked.issues}`
1077
+ ) }],
1078
+ isError: true
1079
+ };
1080
+ }
1081
+ if (spec.execute !== void 0) {
1082
+ return spec.execute(checked.value, { engine, session });
1083
+ }
1084
+ try {
1085
+ const raw = await engine.call(spec.engineMethod, checked.value);
1086
+ if (spec.name === "gp_attach") {
1087
+ session.reset();
1088
+ }
1089
+ if (spec.engineMethod === "last_evidence") {
1090
+ parseEvidenceFrame(raw);
1091
+ }
1092
+ session.record(raw);
1093
+ return {
1094
+ content: [{ type: "text", text: canonicalJson(raw) }],
1095
+ isError: false
1096
+ };
1097
+ } catch (error) {
1098
+ if (error instanceof EngineCallError) {
1099
+ return {
1100
+ content: [{ type: "text", text: formatToolErrorShape(error.toBody()) }],
1101
+ isError: true
1102
+ };
1103
+ }
1104
+ if (error instanceof KernelSchemaError) {
1105
+ return {
1106
+ content: [{ type: "text", text: formatToolError(
1107
+ GP_E_INTERNAL,
1108
+ `engine returned an invalid evidence pack: ${error.message}`,
1109
+ "engine and kernel schema drifted; fix the common fixtures (assertion C35)"
1110
+ ) }],
1111
+ isError: true
1112
+ };
1113
+ }
1114
+ return {
1115
+ content: [{ type: "text", text: formatToolError(
1116
+ GP_E_INTERNAL,
1117
+ `internal shell error: ${String(error)}`,
1118
+ "see the MCP server logs and retry"
1119
+ ) }],
1120
+ isError: true
1121
+ };
1122
+ }
1123
+ }
1124
+ function parseEvidenceFrame(raw) {
1125
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1126
+ throw new KernelSchemaError("evidence pack", [{
1127
+ path: "evidencePack",
1128
+ message: "expected an object frame with a nested evidencePack field"
1129
+ }]);
1130
+ }
1131
+ const frame = raw;
1132
+ if (typeof frame.evidencePack !== "object" || frame.evidencePack === null) {
1133
+ throw new KernelSchemaError("evidence pack", [{
1134
+ path: "evidencePack",
1135
+ message: "missing evidencePack field in last_evidence result"
1136
+ }]);
1137
+ }
1138
+ return parseEvidencePack(frame.evidencePack);
1139
+ }
1140
+ async function projectListTool() {
1141
+ const projects = projectList();
1142
+ return {
1143
+ content: [{ type: "text", text: canonicalJson({ projects }) }],
1144
+ isError: false
1145
+ };
1146
+ }
1147
+ async function projectSetTool(args) {
1148
+ try {
1149
+ const entry = projectSet(args);
1150
+ return { content: [{ type: "text", text: canonicalJson({ project: entry }) }], isError: false };
1151
+ } catch (error) {
1152
+ return mapProjectError(error);
1153
+ }
1154
+ }
1155
+ async function projectGetTool(args) {
1156
+ const argv = args;
1157
+ const entry = projectGet(argv.projectId);
1158
+ if (entry === void 0) {
1159
+ return {
1160
+ content: [{ type: "text", text: formatToolError(
1161
+ "GP_E_NOT_FOUND",
1162
+ `unknown project ${argv.projectId}`,
1163
+ "check the projectId; use gp_project_list to view available projects"
1164
+ ) }],
1165
+ isError: true
1166
+ };
1167
+ }
1168
+ return { content: [{ type: "text", text: canonicalJson({ project: entry }) }], isError: false };
1169
+ }
1170
+ function mapProjectError(error) {
1171
+ if (error instanceof ProjectRegistryError) {
1172
+ return {
1173
+ content: [{ type: "text", text: formatToolError(
1174
+ error.code,
1175
+ error.message,
1176
+ error.code === GP_E_PROJECT_LIMIT ? "delete unused projects first, then retry" : error.code === GP_E_NOT_FOUND ? "check the projectId; use gp_project_list to view available projects" : "check the tool's input schema and retry"
1177
+ ) }],
1178
+ isError: true
1179
+ };
1180
+ }
1181
+ return {
1182
+ content: [{ type: "text", text: formatToolError(
1183
+ GP_E_INTERNAL,
1184
+ `internal shell error in project registry: ${String(error)}`,
1185
+ "see the MCP server logs and retry"
1186
+ ) }],
1187
+ isError: true
1188
+ };
1189
+ }
1190
+ async function exportEvidence(args, context) {
1191
+ const argv = args;
1192
+ const render = argv.format === "html" ? renderHTML : renderMarkdown;
1193
+ try {
1194
+ const raw = await context.engine.call("last_evidence", { operationId: argv.operationId });
1195
+ const pack = parseEvidenceFrame(raw);
1196
+ context.session.record(raw);
1197
+ return { content: [{ type: "text", text: render(pack, void 0) }], isError: false };
1198
+ } catch (error) {
1199
+ return mapAuditError(error);
1200
+ }
1201
+ }
1202
+ async function recentReports(args, context) {
1203
+ const argv = args;
1204
+ const ids = context.session.recentIds(argv.limit);
1205
+ if (ids.length === 0) {
1206
+ return {
1207
+ content: [{ type: "text", text: formatToolError(
1208
+ GP_E_NO_EVIDENCE,
1209
+ "no operations recorded in this session",
1210
+ "run gp_act / gp_assert_element first, then retry gp_recent_reports"
1211
+ ) }],
1212
+ isError: true
1213
+ };
1214
+ }
1215
+ const packs = [];
1216
+ const skipped = [];
1217
+ for (const id of ids) {
1218
+ try {
1219
+ const raw = await context.engine.call("last_evidence", { operationId: id });
1220
+ packs.push({ id, pack: parseEvidenceFrame(raw) });
1221
+ } catch (error) {
1222
+ if (error instanceof EngineCallError && error.code === GP_E_NO_EVIDENCE) {
1223
+ skipped.push(id);
1224
+ } else {
1225
+ return mapAuditError(error);
1226
+ }
1227
+ }
1228
+ }
1229
+ if (packs.length === 0) {
1230
+ return {
1231
+ content: [{ type: "text", text: formatToolError(
1232
+ GP_E_NO_EVIDENCE,
1233
+ "no recent evidence is reachable in the daemon history",
1234
+ "the engine may have restarted; re-run gp_act / gp_assert_element to regenerate evidence"
1235
+ ) }],
1236
+ isError: true
1237
+ };
1238
+ }
1239
+ const render = argv.format === "html" ? renderHTML : renderMarkdown;
1240
+ const header = argv.format === "html" ? `<h1>GlassPane recent reports (${packs.length})</h1>` : `# GlassPane recent reports (${packs.length})`;
1241
+ const skipNote = skipped.length === 0 ? "" : argv.format === "html" ? `<p class="gp-skipped">skipped ${skipped.length} unreachable ${skipped.length === 1 ? "entry" : "entries"}: ${escapeHTML(skipped.join(", "))}</p>` : `> skipped ${skipped.length} unreachable ${skipped.length === 1 ? "entry" : "entries"}: ${skipped.join(", ")}`;
1242
+ const separator = argv.format === "html" ? "\n<hr>\n" : "\n\n---\n";
1243
+ const body = packs.map(({ pack }) => render(pack, void 0)).join(separator);
1244
+ const sections2 = [header];
1245
+ if (skipNote !== "") {
1246
+ sections2.push(skipNote);
1247
+ }
1248
+ sections2.push(body);
1249
+ return { content: [{ type: "text", text: sections2.join("\n") }], isError: false };
1250
+ }
1251
+ function mapAuditError(error) {
1252
+ if (error instanceof EngineCallError) {
1253
+ return {
1254
+ content: [{ type: "text", text: formatToolErrorShape(error.toBody()) }],
1255
+ isError: true
1256
+ };
1257
+ }
1258
+ if (error instanceof KernelSchemaError) {
1259
+ return {
1260
+ content: [{ type: "text", text: formatToolError(
1261
+ GP_E_INTERNAL,
1262
+ `engine returned an invalid evidence pack: ${error.message}`,
1263
+ "engine and kernel schema drifted; fix the common fixtures (assertion C35)"
1264
+ ) }],
1265
+ isError: true
1266
+ };
1267
+ }
1268
+ return {
1269
+ content: [{ type: "text", text: formatToolError(
1270
+ GP_E_INTERNAL,
1271
+ `internal shell error: ${String(error)}`,
1272
+ "see the MCP server logs and retry"
1273
+ ) }],
1274
+ isError: true
1275
+ };
1276
+ }
1277
+
1278
+ // src/dispatch.ts
1279
+ var JSONRPC = "2.0";
1280
+ var MCP_PROTOCOL_VERSION = "2025-06-18";
1281
+ var SERVER_INFO = { name: "glasspane-mcp", version: "0.1.0" };
1282
+ var PARSE_ERROR = -32700;
1283
+ var INVALID_REQUEST = -32600;
1284
+ var METHOD_NOT_FOUND = -32601;
1285
+ var INVALID_PARAMS = -32602;
1286
+ var INTERNAL_ERROR = -32603;
1287
+ var McpServer = class {
1288
+ constructor(deps) {
1289
+ this.deps = deps;
1290
+ this.specs = deps.specs ?? TOOL_SPECS;
1291
+ this.session = deps.session ?? new EvidenceAuditSession();
1292
+ }
1293
+ deps;
1294
+ specs;
1295
+ session;
1296
+ /** Handle a single newline-delimited frame; returns a response or null. */
1297
+ async handleLine(line) {
1298
+ if (line.trim() === "") {
1299
+ return null;
1300
+ }
1301
+ let parsed;
1302
+ try {
1303
+ parsed = JSON.parse(line);
1304
+ } catch {
1305
+ return this.error(PARSE_ERROR, "Parse error", null);
1306
+ }
1307
+ if (!this.isRequestBody(parsed)) {
1308
+ return this.error(INVALID_REQUEST, "Invalid Request", null);
1309
+ }
1310
+ const request = parsed;
1311
+ if (request.id === void 0) {
1312
+ await this.handleNotification(request);
1313
+ return null;
1314
+ }
1315
+ if (typeof request.id !== "number" && typeof request.id !== "string") {
1316
+ return this.error(INVALID_REQUEST, "Invalid Request", null);
1317
+ }
1318
+ try {
1319
+ return await this.dispatch(request, request.id);
1320
+ } catch (error) {
1321
+ return this.error(INTERNAL_ERROR, `Internal error: ${String(error)}`, request.id);
1322
+ }
1323
+ }
1324
+ async handleNotification(request) {
1325
+ }
1326
+ async dispatch(request, id) {
1327
+ if (typeof request.method !== "string") {
1328
+ return this.error(INVALID_REQUEST, "Invalid Request", id);
1329
+ }
1330
+ switch (request.method) {
1331
+ case "initialize":
1332
+ return this.result(id, {
1333
+ protocolVersion: MCP_PROTOCOL_VERSION,
1334
+ capabilities: { tools: { listChanged: false } },
1335
+ serverInfo: SERVER_INFO
1336
+ });
1337
+ case "ping":
1338
+ return this.result(id, {});
1339
+ case "tools/list":
1340
+ return this.result(id, {
1341
+ tools: this.specs.map((spec) => ({
1342
+ name: spec.name,
1343
+ description: spec.description,
1344
+ inputSchema: spec.inputSchema
1345
+ }))
1346
+ });
1347
+ case "tools/call":
1348
+ return this.callTool(id, request.params);
1349
+ default:
1350
+ return this.error(METHOD_NOT_FOUND, `Method not found: ${request.method}`, id);
1351
+ }
1352
+ }
1353
+ async callTool(id, rawParams) {
1354
+ const params = this.asParams(rawParams);
1355
+ if (!params) {
1356
+ return this.error(INVALID_PARAMS, "tools/call requires an object params", id);
1357
+ }
1358
+ const name = params.name;
1359
+ if (typeof name !== "string") {
1360
+ return this.error(INVALID_PARAMS, "tools/call requires a string name", id);
1361
+ }
1362
+ const spec = this.specs.find((candidate) => candidate.name === name);
1363
+ if (!spec) {
1364
+ return this.error(INVALID_PARAMS, `Unknown tool: ${name}`, id);
1365
+ }
1366
+ const outcome = await executeTool(spec, params.arguments, this.deps.engine, this.session);
1367
+ return this.result(id, { content: outcome.content, isError: outcome.isError });
1368
+ }
1369
+ asParams(rawParams) {
1370
+ if (typeof rawParams !== "object" || rawParams === null || Array.isArray(rawParams)) {
1371
+ return null;
1372
+ }
1373
+ return rawParams;
1374
+ }
1375
+ isRequestBody(value) {
1376
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1377
+ return false;
1378
+ }
1379
+ const candidate = value;
1380
+ return candidate.jsonrpc === JSONRPC;
1381
+ }
1382
+ result(id, result) {
1383
+ return { jsonrpc: JSONRPC, id, result };
1384
+ }
1385
+ error(code, message, id) {
1386
+ return { jsonrpc: JSONRPC, id, error: { code, message } };
1387
+ }
1388
+ };
1389
+
1390
+ // src/index.ts
1391
+ function parseArgs(argv) {
1392
+ const options = { socketPath: defaultSocketPath(), help: false };
1393
+ for (let i = 0; i < argv.length; i++) {
1394
+ const arg = argv[i];
1395
+ if (arg === "--help" || arg === "-h") {
1396
+ options.help = true;
1397
+ } else if (arg === "--socket-path" || arg === "-s") {
1398
+ const value = argv[i + 1];
1399
+ if (value === void 0) {
1400
+ throw new Error("--socket-path requires a value");
1401
+ }
1402
+ options.socketPath = value;
1403
+ i += 1;
1404
+ } else if (arg.startsWith("--socket-path=")) {
1405
+ options.socketPath = arg.slice("--socket-path=".length);
1406
+ }
1407
+ }
1408
+ return options;
1409
+ }
1410
+ var USAGE = `usage: glasspane-mcp [--socket-path <path>]
1411
+
1412
+ Bridges an AI agent (MCP stdio) to the GlassPane engine daemon.
1413
+ The daemon socket defaults to $HOME/.glasspane/engine.sock
1414
+ (override with GLASSPANE_ENGINE_SOCK or --socket-path).
1415
+ `;
1416
+ var ReplyQueue = class {
1417
+ tail = Promise.resolve();
1418
+ push(run) {
1419
+ this.tail = this.tail.then(run);
1420
+ }
1421
+ };
1422
+ function main() {
1423
+ let options;
1424
+ try {
1425
+ options = parseArgs(process2.argv.slice(2));
1426
+ } catch (error) {
1427
+ process2.stderr.write(String(error) + "\n\n" + USAGE);
1428
+ process2.exit(2);
1429
+ }
1430
+ if (options.help) {
1431
+ process2.stdout.write(USAGE);
1432
+ process2.exit(0);
1433
+ }
1434
+ process2.stdin.setEncoding("utf8");
1435
+ const queue = new ReplyQueue();
1436
+ let engine;
1437
+ try {
1438
+ engine = unixSocketEngineClient(options.socketPath);
1439
+ } catch (error) {
1440
+ process2.stderr.write(
1441
+ `failed to create engine client on ${options.socketPath}: ${String(error)}
1442
+ `
1443
+ );
1444
+ process2.exit(2);
1445
+ }
1446
+ const server = new McpServer({ engine });
1447
+ const reader = new LineReader({
1448
+ onLine: (line) => {
1449
+ queue.push(async () => {
1450
+ const response = await server.handleLine(line);
1451
+ if (response !== null) {
1452
+ process2.stdout.write(canonicalJson(response) + "\n");
1453
+ }
1454
+ });
1455
+ },
1456
+ onOversize: (bytes) => {
1457
+ process2.stderr.write(`dropping oversized stdio frame (${bytes} bytes)
1458
+ `);
1459
+ }
1460
+ });
1461
+ process2.stdin.on("data", (chunk) => reader.push(chunk));
1462
+ let shuttingDown = false;
1463
+ const shutdown = () => {
1464
+ if (shuttingDown) {
1465
+ return;
1466
+ }
1467
+ shuttingDown = true;
1468
+ engine.close();
1469
+ process2.stdin.destroy();
1470
+ process2.stdout.end();
1471
+ };
1472
+ process2.stdin.on("close", () => queue.push(async () => shutdown()));
1473
+ process2.on("SIGINT", shutdown);
1474
+ process2.on("SIGTERM", shutdown);
1475
+ }
1476
+ main();