glasspane-mcp 1.6.2 → 1.7.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.
- package/dist/dispatch.d.ts +1 -1
- package/dist/engine-client.d.ts +1 -0
- package/dist/evidence-report.d.ts +13 -0
- package/dist/http-gateway-cli.js +78 -12
- package/dist/http-gateway.js +78 -12
- package/dist/index.js +88 -11
- package/dist/io.d.ts +11 -0
- package/dist/project-registry.d.ts +7 -0
- package/package.json +1 -1
package/dist/dispatch.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare const MCP_PROTOCOL_VERSION: "2025-06-18";
|
|
|
26
26
|
export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
|
|
27
27
|
export declare const SERVER_INFO: {
|
|
28
28
|
readonly name: "glasspane-mcp";
|
|
29
|
-
readonly version: "1.
|
|
29
|
+
readonly version: "1.7.0";
|
|
30
30
|
};
|
|
31
31
|
export declare const PARSE_ERROR = -32700;
|
|
32
32
|
export declare const INVALID_REQUEST = -32600;
|
package/dist/engine-client.d.ts
CHANGED
|
@@ -167,6 +167,7 @@ export declare function engineDeadlineMs(method: string, params?: Record<string,
|
|
|
167
167
|
export declare function callerDeadlineMs(method: string, params?: Record<string, unknown>, ceilingMs?: number): number;
|
|
168
168
|
/** Engine protocol version this shell speaks (daemon `hello.protocolVersion`). */
|
|
169
169
|
export declare const ENGINE_PROTOCOL_VERSION = "0";
|
|
170
|
+
export declare const ENGINE_WRITE_BACKLOG_BYTES: number;
|
|
170
171
|
/** Daemon self-report the shell compares a fresh connection against. */
|
|
171
172
|
export interface EngineIdentityExpectation {
|
|
172
173
|
/**
|
|
@@ -37,6 +37,19 @@ export declare function evidenceSummaryLines(pack: EvidencePackReportView): stri
|
|
|
37
37
|
export declare function renderMarkdown(pack: EvidencePackReportView, diagnostics: string | undefined): string;
|
|
38
38
|
/** Full HTML view with an operationId anchor; all values escaped. */
|
|
39
39
|
export declare function renderHTML(pack: EvidencePackReportView, diagnostics: string | undefined): string;
|
|
40
|
+
/**
|
|
41
|
+
* 终端控制字符一律转成可见的 `\uXXXX`。
|
|
42
|
+
*
|
|
43
|
+
* markdown 这一路从前是原文输出的:`escapeHTML` 只护住了 HTML 那半,而
|
|
44
|
+
* `gp_export_evidence` 与报告导出默认交回的就是 markdown——代理会把它直接打到
|
|
45
|
+
* 终端上。报告里的标题、selector 标题、stateDiff 的键值、探针的文件路径全是
|
|
46
|
+
* app 自己写的字符串:一个窗口标题里塞一段 OSC-52 就能改写用户的剪贴板,
|
|
47
|
+
* 一段 ANSI 能把光标移走覆盖掉它说过的话。daemon 那一侧为这件事立过规矩
|
|
48
|
+
* (R5-03:`<0x20` 控制字符一律转义),这里补齐壳这一侧。
|
|
49
|
+
*
|
|
50
|
+
* `\t`(0x09) 与 `\n`(0x0a) 保留——它们是这份报告自己的排版,不是外来字符。
|
|
51
|
+
*/
|
|
52
|
+
export declare function escapeTerminalControl(value: string): string;
|
|
40
53
|
/**
|
|
41
54
|
* Escape HTML metacharacters in values that cross into a report (spec v1.3
|
|
42
55
|
* §13.4, the five-character escape set, no injection). Shared so aggregation
|
package/dist/http-gateway-cli.js
CHANGED
|
@@ -125,6 +125,9 @@ var StreamLineIo = class {
|
|
|
125
125
|
writeLine(line) {
|
|
126
126
|
this.output.write(line + "\n");
|
|
127
127
|
}
|
|
128
|
+
bufferedBytes() {
|
|
129
|
+
return this.output.writableLength;
|
|
130
|
+
}
|
|
128
131
|
onMessage(handler) {
|
|
129
132
|
this.messageHandler = handler;
|
|
130
133
|
}
|
|
@@ -233,6 +236,7 @@ function callerDeadlineMs(method, params, ceilingMs = CALLER_VISIBLE_CEILING_MS)
|
|
|
233
236
|
return Math.min(engineDeadlineMs(method, params), ceilingMs);
|
|
234
237
|
}
|
|
235
238
|
var ENGINE_PROTOCOL_VERSION = "0";
|
|
239
|
+
var ENGINE_WRITE_BACKLOG_BYTES = 8 * 1024 * 1024;
|
|
236
240
|
var EngineCallError = class extends Error {
|
|
237
241
|
code;
|
|
238
242
|
message;
|
|
@@ -418,6 +422,16 @@ var EngineJsonRpcClient = class {
|
|
|
418
422
|
fail: reject
|
|
419
423
|
};
|
|
420
424
|
entry.timer = setTimeout(() => this.onDeadline(entry), deadlineMs);
|
|
425
|
+
const backlog = this.io.bufferedBytes?.() ?? 0;
|
|
426
|
+
if (backlog > ENGINE_WRITE_BACKLOG_BYTES) {
|
|
427
|
+
this.pending.delete(id);
|
|
428
|
+
this.reject(entry, new EngineCallError(
|
|
429
|
+
GP_E_ENGINE_UNREACHABLE,
|
|
430
|
+
`not sending '${method}': ${backlog} bytes are already queued for the engine socket and it has not read them`,
|
|
431
|
+
`the background service is accepting the connection but not reading it \u2014 it is wedged, not busy. Check it with gp_probe_status; if it stays like this, restart it with the command in \`glasspaned --help\` (or the panel's \u91CD\u542F\u540E\u53F0\u670D\u52A1 button). Do not retry this call in a loop: every retry only deepens the queue it is refusing to drain.`
|
|
432
|
+
));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
421
435
|
this.pending.set(id, entry);
|
|
422
436
|
try {
|
|
423
437
|
this.io.writeLine(JSON.stringify(frame));
|
|
@@ -474,13 +488,18 @@ var EngineJsonRpcClient = class {
|
|
|
474
488
|
this.report(`dropped late-reply tracking for '${evicted.method}' (id ${evicted.id}): retention cap ${LATE_REPLY_RETENTION} reached`);
|
|
475
489
|
}
|
|
476
490
|
handleMessage(line) {
|
|
477
|
-
let
|
|
491
|
+
let parsed;
|
|
478
492
|
try {
|
|
479
|
-
|
|
493
|
+
parsed = JSON.parse(line);
|
|
480
494
|
} catch {
|
|
481
495
|
this.report(`engine sent a non-JSON frame, dropped: ${truncate(line)}`);
|
|
482
496
|
return;
|
|
483
497
|
}
|
|
498
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
499
|
+
this.report(`engine sent a frame that is not a JSON object, dropped: ${truncate(line)}`);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const frame = parsed;
|
|
484
503
|
const id = frame.id;
|
|
485
504
|
if (typeof id === "number") {
|
|
486
505
|
const entry = this.pending.get(id);
|
|
@@ -525,9 +544,17 @@ var EngineJsonRpcClient = class {
|
|
|
525
544
|
settleFromFrame(entry, frame) {
|
|
526
545
|
if (frame.error) {
|
|
527
546
|
this.reject(entry, this.engineError(frame.error));
|
|
528
|
-
|
|
529
|
-
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (!("result" in frame)) {
|
|
550
|
+
this.reject(entry, new EngineCallError(
|
|
551
|
+
GP_E_INTERNAL,
|
|
552
|
+
`engine answered '${entry.method}' with a frame carrying neither result nor error`,
|
|
553
|
+
"do not read this call as successful: the engine answered nothing. Retry once, then check the daemon log (stderr) for what it died before answering"
|
|
554
|
+
));
|
|
555
|
+
return;
|
|
530
556
|
}
|
|
557
|
+
this.resolve(entry, frame.result);
|
|
531
558
|
}
|
|
532
559
|
/**
|
|
533
560
|
* A frame over the cap: attribute it to the request it belonged to — from
|
|
@@ -754,6 +781,9 @@ var ReconnectingSocketIo = class {
|
|
|
754
781
|
writeLine(line) {
|
|
755
782
|
this.connect().writeLine(line);
|
|
756
783
|
}
|
|
784
|
+
bufferedBytes() {
|
|
785
|
+
return this.inner?.bufferedBytes() ?? 0;
|
|
786
|
+
}
|
|
757
787
|
onMessage(handler) {
|
|
758
788
|
this.messageHandler = handler;
|
|
759
789
|
}
|
|
@@ -893,7 +923,7 @@ function renderMarkdown(pack, diagnostics) {
|
|
|
893
923
|
out.push(section.body);
|
|
894
924
|
out.push("");
|
|
895
925
|
}
|
|
896
|
-
return out.join("\n");
|
|
926
|
+
return escapeTerminalControl(out.join("\n"));
|
|
897
927
|
}
|
|
898
928
|
function renderHTML(pack, diagnostics) {
|
|
899
929
|
const out = [];
|
|
@@ -918,7 +948,10 @@ function renderHTML(pack, diagnostics) {
|
|
|
918
948
|
out.push(`</section>`);
|
|
919
949
|
}
|
|
920
950
|
out.push(`</div>`);
|
|
921
|
-
return out.join("\n");
|
|
951
|
+
return escapeTerminalControl(out.join("\n"));
|
|
952
|
+
}
|
|
953
|
+
function escapeTerminalControl(value) {
|
|
954
|
+
return value.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
922
955
|
}
|
|
923
956
|
function escapeHTML(value) {
|
|
924
957
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
@@ -1245,7 +1278,11 @@ var RECIPE_MAX_STEPS = 64;
|
|
|
1245
1278
|
var RecipeStepKindSchema = z3.enum(["act", "observe", "assert", "diagnose"]);
|
|
1246
1279
|
var RecipeStepSchema = z3.strictObject({
|
|
1247
1280
|
kind: RecipeStepKindSchema,
|
|
1248
|
-
|
|
1281
|
+
// Two-argument `z.record`: valid on the zod 3 this package pins *and* on zod 4,
|
|
1282
|
+
// which made the key type mandatory. The fork of opencode runs zod 4, and its
|
|
1283
|
+
// typecheck caught this as `TS2554: Expected 2-3 arguments, but got 1` — a
|
|
1284
|
+
// latent break waiting for the day this package's zod is bumped.
|
|
1285
|
+
params: z3.record(z3.string(), z3.unknown())
|
|
1249
1286
|
});
|
|
1250
1287
|
var RecipeConfigSchema = z3.strictObject({
|
|
1251
1288
|
schemaVersion: z3.literal(RECIPE_SCHEMA_VERSION),
|
|
@@ -1281,8 +1318,14 @@ function parseEvidencePackRead(input) {
|
|
|
1281
1318
|
throw schemaError("evidence pack", result.error);
|
|
1282
1319
|
}
|
|
1283
1320
|
|
|
1321
|
+
// ../kernel/dist/decision-log.js
|
|
1322
|
+
var DecisionLogDraftSchema = DecisionLogEntrySchema.omit({
|
|
1323
|
+
sequence: true,
|
|
1324
|
+
prevEntryHash: true
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1284
1327
|
// src/canonical.ts
|
|
1285
|
-
function
|
|
1328
|
+
function canonicalJson2(value) {
|
|
1286
1329
|
return JSON.stringify(sortKeys(value));
|
|
1287
1330
|
}
|
|
1288
1331
|
function sortKeys(value) {
|
|
@@ -1502,6 +1545,27 @@ function tightenMode(target, want, kind) {
|
|
|
1502
1545
|
);
|
|
1503
1546
|
}
|
|
1504
1547
|
}
|
|
1548
|
+
function fileIdentity(filePath) {
|
|
1549
|
+
try {
|
|
1550
|
+
const stat = fs2.statSync(filePath);
|
|
1551
|
+
return `${stat.mtimeMs}:${stat.size}:${stat.ino}`;
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
if (nodeErrorCode(error) === "ENOENT") return null;
|
|
1554
|
+
throw error;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
function registryDriftReason(filePath, openedAs, now) {
|
|
1558
|
+
if (now === openedAs) {
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
return `projects.json at ${filePath} changed between the read that produced this edit and the write that would have published it (was ${openedAs ?? "absent"}, now ${now ?? "absent"}), so this call wrote nothing. Another process \u2014 the background service, its CLI, or the settings panel \u2014 holds a newer version of this table, and renaming ours over theirs would drop their entry while reporting success. Repeat gp_project_set: it reads the file again, so the second attempt carries what the other writer landed. If it keeps failing, something is writing this registry on a timer \u2014 read the file to see what appeared, and do not force the write.`;
|
|
1562
|
+
}
|
|
1563
|
+
function assertNoConcurrentWrite(filePath, openedAs) {
|
|
1564
|
+
const reason = registryDriftReason(filePath, openedAs, fileIdentity(filePath));
|
|
1565
|
+
if (reason !== null) {
|
|
1566
|
+
throw new ProjectRegistryError("registry-changed-underneath", reason);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1505
1569
|
function saveProjects(filePath, entries) {
|
|
1506
1570
|
const dir = path2.dirname(filePath);
|
|
1507
1571
|
const tmpPath = `${filePath}.tmp-${uniqueToken()}`;
|
|
@@ -1548,6 +1612,7 @@ function projectSet(args) {
|
|
|
1548
1612
|
assertStorablePid(args.pid);
|
|
1549
1613
|
const snapshot = loadRegistry(filePath);
|
|
1550
1614
|
const { projects, discarded } = requireUsableRegistry(filePath, snapshot);
|
|
1615
|
+
const openedAs = fileIdentity(filePath);
|
|
1551
1616
|
let entry;
|
|
1552
1617
|
const projectId = args.projectId;
|
|
1553
1618
|
if (projectId) {
|
|
@@ -1573,6 +1638,7 @@ function projectSet(args) {
|
|
|
1573
1638
|
assertSingleIdentity(entry);
|
|
1574
1639
|
projects.push(entry);
|
|
1575
1640
|
}
|
|
1641
|
+
assertNoConcurrentWrite(filePath, openedAs);
|
|
1576
1642
|
saveProjects(filePath, projects);
|
|
1577
1643
|
return {
|
|
1578
1644
|
...entry,
|
|
@@ -2364,7 +2430,7 @@ async function captureView(args, context) {
|
|
|
2364
2430
|
};
|
|
2365
2431
|
}
|
|
2366
2432
|
const mimeType = typeof body.mimeType === "string" && body.mimeType.length > 0 ? body.mimeType : "image/png";
|
|
2367
|
-
const summary =
|
|
2433
|
+
const summary = canonicalJson2({
|
|
2368
2434
|
mimeType,
|
|
2369
2435
|
byteCount: body.byteCount,
|
|
2370
2436
|
pixelWidth: body.pixelWidth,
|
|
@@ -2428,7 +2494,7 @@ async function projectListTool() {
|
|
|
2428
2494
|
try {
|
|
2429
2495
|
const projects = projectList();
|
|
2430
2496
|
return {
|
|
2431
|
-
content: [{ type: "text", text:
|
|
2497
|
+
content: [{ type: "text", text: canonicalJson2({ projects }) }],
|
|
2432
2498
|
isError: false
|
|
2433
2499
|
};
|
|
2434
2500
|
} catch (error) {
|
|
@@ -2438,7 +2504,7 @@ async function projectListTool() {
|
|
|
2438
2504
|
async function projectSetTool(args) {
|
|
2439
2505
|
try {
|
|
2440
2506
|
const entry = projectSet(args);
|
|
2441
|
-
return { content: [{ type: "text", text:
|
|
2507
|
+
return { content: [{ type: "text", text: canonicalJson2({ project: entry }) }], isError: false };
|
|
2442
2508
|
} catch (error) {
|
|
2443
2509
|
return mapProjectError(error);
|
|
2444
2510
|
}
|
|
@@ -2460,7 +2526,7 @@ async function projectGetTool(args) {
|
|
|
2460
2526
|
isError: true
|
|
2461
2527
|
};
|
|
2462
2528
|
}
|
|
2463
|
-
return { content: [{ type: "text", text:
|
|
2529
|
+
return { content: [{ type: "text", text: canonicalJson2({ project: entry }) }], isError: false };
|
|
2464
2530
|
} catch (error) {
|
|
2465
2531
|
return mapProjectError(error);
|
|
2466
2532
|
}
|
package/dist/http-gateway.js
CHANGED
|
@@ -125,6 +125,9 @@ var StreamLineIo = class {
|
|
|
125
125
|
writeLine(line) {
|
|
126
126
|
this.output.write(line + "\n");
|
|
127
127
|
}
|
|
128
|
+
bufferedBytes() {
|
|
129
|
+
return this.output.writableLength;
|
|
130
|
+
}
|
|
128
131
|
onMessage(handler) {
|
|
129
132
|
this.messageHandler = handler;
|
|
130
133
|
}
|
|
@@ -229,6 +232,7 @@ function callerDeadlineMs(method, params, ceilingMs = CALLER_VISIBLE_CEILING_MS)
|
|
|
229
232
|
return Math.min(engineDeadlineMs(method, params), ceilingMs);
|
|
230
233
|
}
|
|
231
234
|
var ENGINE_PROTOCOL_VERSION = "0";
|
|
235
|
+
var ENGINE_WRITE_BACKLOG_BYTES = 8 * 1024 * 1024;
|
|
232
236
|
var EngineCallError = class extends Error {
|
|
233
237
|
code;
|
|
234
238
|
message;
|
|
@@ -414,6 +418,16 @@ var EngineJsonRpcClient = class {
|
|
|
414
418
|
fail: reject
|
|
415
419
|
};
|
|
416
420
|
entry.timer = setTimeout(() => this.onDeadline(entry), deadlineMs);
|
|
421
|
+
const backlog = this.io.bufferedBytes?.() ?? 0;
|
|
422
|
+
if (backlog > ENGINE_WRITE_BACKLOG_BYTES) {
|
|
423
|
+
this.pending.delete(id);
|
|
424
|
+
this.reject(entry, new EngineCallError(
|
|
425
|
+
GP_E_ENGINE_UNREACHABLE,
|
|
426
|
+
`not sending '${method}': ${backlog} bytes are already queued for the engine socket and it has not read them`,
|
|
427
|
+
`the background service is accepting the connection but not reading it \u2014 it is wedged, not busy. Check it with gp_probe_status; if it stays like this, restart it with the command in \`glasspaned --help\` (or the panel's \u91CD\u542F\u540E\u53F0\u670D\u52A1 button). Do not retry this call in a loop: every retry only deepens the queue it is refusing to drain.`
|
|
428
|
+
));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
417
431
|
this.pending.set(id, entry);
|
|
418
432
|
try {
|
|
419
433
|
this.io.writeLine(JSON.stringify(frame));
|
|
@@ -470,13 +484,18 @@ var EngineJsonRpcClient = class {
|
|
|
470
484
|
this.report(`dropped late-reply tracking for '${evicted.method}' (id ${evicted.id}): retention cap ${LATE_REPLY_RETENTION} reached`);
|
|
471
485
|
}
|
|
472
486
|
handleMessage(line) {
|
|
473
|
-
let
|
|
487
|
+
let parsed;
|
|
474
488
|
try {
|
|
475
|
-
|
|
489
|
+
parsed = JSON.parse(line);
|
|
476
490
|
} catch {
|
|
477
491
|
this.report(`engine sent a non-JSON frame, dropped: ${truncate(line)}`);
|
|
478
492
|
return;
|
|
479
493
|
}
|
|
494
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
495
|
+
this.report(`engine sent a frame that is not a JSON object, dropped: ${truncate(line)}`);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const frame = parsed;
|
|
480
499
|
const id = frame.id;
|
|
481
500
|
if (typeof id === "number") {
|
|
482
501
|
const entry = this.pending.get(id);
|
|
@@ -521,9 +540,17 @@ var EngineJsonRpcClient = class {
|
|
|
521
540
|
settleFromFrame(entry, frame) {
|
|
522
541
|
if (frame.error) {
|
|
523
542
|
this.reject(entry, this.engineError(frame.error));
|
|
524
|
-
|
|
525
|
-
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (!("result" in frame)) {
|
|
546
|
+
this.reject(entry, new EngineCallError(
|
|
547
|
+
GP_E_INTERNAL,
|
|
548
|
+
`engine answered '${entry.method}' with a frame carrying neither result nor error`,
|
|
549
|
+
"do not read this call as successful: the engine answered nothing. Retry once, then check the daemon log (stderr) for what it died before answering"
|
|
550
|
+
));
|
|
551
|
+
return;
|
|
526
552
|
}
|
|
553
|
+
this.resolve(entry, frame.result);
|
|
527
554
|
}
|
|
528
555
|
/**
|
|
529
556
|
* A frame over the cap: attribute it to the request it belonged to — from
|
|
@@ -750,6 +777,9 @@ var ReconnectingSocketIo = class {
|
|
|
750
777
|
writeLine(line) {
|
|
751
778
|
this.connect().writeLine(line);
|
|
752
779
|
}
|
|
780
|
+
bufferedBytes() {
|
|
781
|
+
return this.inner?.bufferedBytes() ?? 0;
|
|
782
|
+
}
|
|
753
783
|
onMessage(handler) {
|
|
754
784
|
this.messageHandler = handler;
|
|
755
785
|
}
|
|
@@ -883,7 +913,7 @@ function renderMarkdown(pack, diagnostics) {
|
|
|
883
913
|
out.push(section.body);
|
|
884
914
|
out.push("");
|
|
885
915
|
}
|
|
886
|
-
return out.join("\n");
|
|
916
|
+
return escapeTerminalControl(out.join("\n"));
|
|
887
917
|
}
|
|
888
918
|
function renderHTML(pack, diagnostics) {
|
|
889
919
|
const out = [];
|
|
@@ -908,7 +938,10 @@ function renderHTML(pack, diagnostics) {
|
|
|
908
938
|
out.push(`</section>`);
|
|
909
939
|
}
|
|
910
940
|
out.push(`</div>`);
|
|
911
|
-
return out.join("\n");
|
|
941
|
+
return escapeTerminalControl(out.join("\n"));
|
|
942
|
+
}
|
|
943
|
+
function escapeTerminalControl(value) {
|
|
944
|
+
return value.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
912
945
|
}
|
|
913
946
|
function escapeHTML(value) {
|
|
914
947
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
@@ -1235,7 +1268,11 @@ var RECIPE_MAX_STEPS = 64;
|
|
|
1235
1268
|
var RecipeStepKindSchema = z3.enum(["act", "observe", "assert", "diagnose"]);
|
|
1236
1269
|
var RecipeStepSchema = z3.strictObject({
|
|
1237
1270
|
kind: RecipeStepKindSchema,
|
|
1238
|
-
|
|
1271
|
+
// Two-argument `z.record`: valid on the zod 3 this package pins *and* on zod 4,
|
|
1272
|
+
// which made the key type mandatory. The fork of opencode runs zod 4, and its
|
|
1273
|
+
// typecheck caught this as `TS2554: Expected 2-3 arguments, but got 1` — a
|
|
1274
|
+
// latent break waiting for the day this package's zod is bumped.
|
|
1275
|
+
params: z3.record(z3.string(), z3.unknown())
|
|
1239
1276
|
});
|
|
1240
1277
|
var RecipeConfigSchema = z3.strictObject({
|
|
1241
1278
|
schemaVersion: z3.literal(RECIPE_SCHEMA_VERSION),
|
|
@@ -1271,8 +1308,14 @@ function parseEvidencePackRead(input) {
|
|
|
1271
1308
|
throw schemaError("evidence pack", result.error);
|
|
1272
1309
|
}
|
|
1273
1310
|
|
|
1311
|
+
// ../kernel/dist/decision-log.js
|
|
1312
|
+
var DecisionLogDraftSchema = DecisionLogEntrySchema.omit({
|
|
1313
|
+
sequence: true,
|
|
1314
|
+
prevEntryHash: true
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1274
1317
|
// src/canonical.ts
|
|
1275
|
-
function
|
|
1318
|
+
function canonicalJson2(value) {
|
|
1276
1319
|
return JSON.stringify(sortKeys(value));
|
|
1277
1320
|
}
|
|
1278
1321
|
function sortKeys(value) {
|
|
@@ -1492,6 +1535,27 @@ function tightenMode(target, want, kind) {
|
|
|
1492
1535
|
);
|
|
1493
1536
|
}
|
|
1494
1537
|
}
|
|
1538
|
+
function fileIdentity(filePath) {
|
|
1539
|
+
try {
|
|
1540
|
+
const stat = fs2.statSync(filePath);
|
|
1541
|
+
return `${stat.mtimeMs}:${stat.size}:${stat.ino}`;
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
if (nodeErrorCode(error) === "ENOENT") return null;
|
|
1544
|
+
throw error;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function registryDriftReason(filePath, openedAs, now) {
|
|
1548
|
+
if (now === openedAs) {
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
return `projects.json at ${filePath} changed between the read that produced this edit and the write that would have published it (was ${openedAs ?? "absent"}, now ${now ?? "absent"}), so this call wrote nothing. Another process \u2014 the background service, its CLI, or the settings panel \u2014 holds a newer version of this table, and renaming ours over theirs would drop their entry while reporting success. Repeat gp_project_set: it reads the file again, so the second attempt carries what the other writer landed. If it keeps failing, something is writing this registry on a timer \u2014 read the file to see what appeared, and do not force the write.`;
|
|
1552
|
+
}
|
|
1553
|
+
function assertNoConcurrentWrite(filePath, openedAs) {
|
|
1554
|
+
const reason = registryDriftReason(filePath, openedAs, fileIdentity(filePath));
|
|
1555
|
+
if (reason !== null) {
|
|
1556
|
+
throw new ProjectRegistryError("registry-changed-underneath", reason);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1495
1559
|
function saveProjects(filePath, entries) {
|
|
1496
1560
|
const dir = path2.dirname(filePath);
|
|
1497
1561
|
const tmpPath = `${filePath}.tmp-${uniqueToken()}`;
|
|
@@ -1538,6 +1602,7 @@ function projectSet(args) {
|
|
|
1538
1602
|
assertStorablePid(args.pid);
|
|
1539
1603
|
const snapshot = loadRegistry(filePath);
|
|
1540
1604
|
const { projects, discarded } = requireUsableRegistry(filePath, snapshot);
|
|
1605
|
+
const openedAs = fileIdentity(filePath);
|
|
1541
1606
|
let entry;
|
|
1542
1607
|
const projectId = args.projectId;
|
|
1543
1608
|
if (projectId) {
|
|
@@ -1563,6 +1628,7 @@ function projectSet(args) {
|
|
|
1563
1628
|
assertSingleIdentity(entry);
|
|
1564
1629
|
projects.push(entry);
|
|
1565
1630
|
}
|
|
1631
|
+
assertNoConcurrentWrite(filePath, openedAs);
|
|
1566
1632
|
saveProjects(filePath, projects);
|
|
1567
1633
|
return {
|
|
1568
1634
|
...entry,
|
|
@@ -2354,7 +2420,7 @@ async function captureView(args, context) {
|
|
|
2354
2420
|
};
|
|
2355
2421
|
}
|
|
2356
2422
|
const mimeType = typeof body.mimeType === "string" && body.mimeType.length > 0 ? body.mimeType : "image/png";
|
|
2357
|
-
const summary =
|
|
2423
|
+
const summary = canonicalJson2({
|
|
2358
2424
|
mimeType,
|
|
2359
2425
|
byteCount: body.byteCount,
|
|
2360
2426
|
pixelWidth: body.pixelWidth,
|
|
@@ -2418,7 +2484,7 @@ async function projectListTool() {
|
|
|
2418
2484
|
try {
|
|
2419
2485
|
const projects = projectList();
|
|
2420
2486
|
return {
|
|
2421
|
-
content: [{ type: "text", text:
|
|
2487
|
+
content: [{ type: "text", text: canonicalJson2({ projects }) }],
|
|
2422
2488
|
isError: false
|
|
2423
2489
|
};
|
|
2424
2490
|
} catch (error) {
|
|
@@ -2428,7 +2494,7 @@ async function projectListTool() {
|
|
|
2428
2494
|
async function projectSetTool(args) {
|
|
2429
2495
|
try {
|
|
2430
2496
|
const entry = projectSet(args);
|
|
2431
|
-
return { content: [{ type: "text", text:
|
|
2497
|
+
return { content: [{ type: "text", text: canonicalJson2({ project: entry }) }], isError: false };
|
|
2432
2498
|
} catch (error) {
|
|
2433
2499
|
return mapProjectError(error);
|
|
2434
2500
|
}
|
|
@@ -2450,7 +2516,7 @@ async function projectGetTool(args) {
|
|
|
2450
2516
|
isError: true
|
|
2451
2517
|
};
|
|
2452
2518
|
}
|
|
2453
|
-
return { content: [{ type: "text", text:
|
|
2519
|
+
return { content: [{ type: "text", text: canonicalJson2({ project: entry }) }], isError: false };
|
|
2454
2520
|
} catch (error) {
|
|
2455
2521
|
return mapProjectError(error);
|
|
2456
2522
|
}
|
package/dist/index.js
CHANGED
|
@@ -229,7 +229,11 @@ var RECIPE_MAX_STEPS = 64;
|
|
|
229
229
|
var RecipeStepKindSchema = z3.enum(["act", "observe", "assert", "diagnose"]);
|
|
230
230
|
var RecipeStepSchema = z3.strictObject({
|
|
231
231
|
kind: RecipeStepKindSchema,
|
|
232
|
-
|
|
232
|
+
// Two-argument `z.record`: valid on the zod 3 this package pins *and* on zod 4,
|
|
233
|
+
// which made the key type mandatory. The fork of opencode runs zod 4, and its
|
|
234
|
+
// typecheck caught this as `TS2554: Expected 2-3 arguments, but got 1` — a
|
|
235
|
+
// latent break waiting for the day this package's zod is bumped.
|
|
236
|
+
params: z3.record(z3.string(), z3.unknown())
|
|
233
237
|
});
|
|
234
238
|
var RecipeConfigSchema = z3.strictObject({
|
|
235
239
|
schemaVersion: z3.literal(RECIPE_SCHEMA_VERSION),
|
|
@@ -265,6 +269,12 @@ function parseEvidencePackRead(input) {
|
|
|
265
269
|
throw schemaError("evidence pack", result.error);
|
|
266
270
|
}
|
|
267
271
|
|
|
272
|
+
// ../kernel/dist/decision-log.js
|
|
273
|
+
var DecisionLogDraftSchema = DecisionLogEntrySchema.omit({
|
|
274
|
+
sequence: true,
|
|
275
|
+
prevEntryHash: true
|
|
276
|
+
});
|
|
277
|
+
|
|
268
278
|
// src/update-state.ts
|
|
269
279
|
import fs from "node:fs";
|
|
270
280
|
import os from "node:os";
|
|
@@ -530,11 +540,16 @@ function readUpdateState(env = process.env, stateRootOverride) {
|
|
|
530
540
|
`current=${String(parsed.current ?? "unknown")}`,
|
|
531
541
|
`latest=${String(parsed.latest ?? "none-seen")}`,
|
|
532
542
|
`lastCheckAt=${String(parsed.updatedAt ?? "never")}`,
|
|
533
|
-
`
|
|
543
|
+
// `disabled` 缺失与 `disabled: false` 不是一件事:前者是"这份记录没说",后者是
|
|
544
|
+
// "记录说了:没关"。从前两者都掉进 `on-by-default`,等于替这台机器宣布自动更新
|
|
545
|
+
// 开着——而本文件开头立的规矩恰恰是 absent 不许读成友好态。
|
|
546
|
+
`autoUpdate=${parsed.disabled === true ? "off" : parsed.disabled === false ? "on" : parsed.autoApply === true ? "on" : "unknown(neither disabled nor autoApply is recorded)"}`
|
|
534
547
|
];
|
|
535
548
|
const lastError = parsed.lastError;
|
|
536
|
-
if (lastError && typeof lastError.message === "string") {
|
|
537
|
-
|
|
549
|
+
if (lastError && (typeof lastError.code === "string" || typeof lastError.message === "string")) {
|
|
550
|
+
const code = typeof lastError.code === "string" && lastError.code !== "" ? lastError.code : "no-code";
|
|
551
|
+
const message = typeof lastError.message === "string" && lastError.message !== "" ? lastError.message : "(the record carries a code with no message: the code is all this shell can honestly say)";
|
|
552
|
+
bits.push(`lastError=${code}: ${message}`);
|
|
538
553
|
}
|
|
539
554
|
const staged = parsed.staged;
|
|
540
555
|
if (staged && typeof staged.version === "string") {
|
|
@@ -658,6 +673,9 @@ var StreamLineIo = class {
|
|
|
658
673
|
writeLine(line) {
|
|
659
674
|
this.output.write(line + "\n");
|
|
660
675
|
}
|
|
676
|
+
bufferedBytes() {
|
|
677
|
+
return this.output.writableLength;
|
|
678
|
+
}
|
|
661
679
|
onMessage(handler) {
|
|
662
680
|
this.messageHandler = handler;
|
|
663
681
|
}
|
|
@@ -874,6 +892,7 @@ function callerDeadlineMs(method, params, ceilingMs = CALLER_VISIBLE_CEILING_MS)
|
|
|
874
892
|
return Math.min(engineDeadlineMs(method, params), ceilingMs);
|
|
875
893
|
}
|
|
876
894
|
var ENGINE_PROTOCOL_VERSION = "0";
|
|
895
|
+
var ENGINE_WRITE_BACKLOG_BYTES = 8 * 1024 * 1024;
|
|
877
896
|
var EngineCallError = class extends Error {
|
|
878
897
|
code;
|
|
879
898
|
message;
|
|
@@ -1059,6 +1078,16 @@ var EngineJsonRpcClient = class {
|
|
|
1059
1078
|
fail: reject
|
|
1060
1079
|
};
|
|
1061
1080
|
entry.timer = setTimeout(() => this.onDeadline(entry), deadlineMs);
|
|
1081
|
+
const backlog = this.io.bufferedBytes?.() ?? 0;
|
|
1082
|
+
if (backlog > ENGINE_WRITE_BACKLOG_BYTES) {
|
|
1083
|
+
this.pending.delete(id);
|
|
1084
|
+
this.reject(entry, new EngineCallError(
|
|
1085
|
+
GP_E_ENGINE_UNREACHABLE,
|
|
1086
|
+
`not sending '${method}': ${backlog} bytes are already queued for the engine socket and it has not read them`,
|
|
1087
|
+
`the background service is accepting the connection but not reading it \u2014 it is wedged, not busy. Check it with gp_probe_status; if it stays like this, restart it with the command in \`glasspaned --help\` (or the panel's \u91CD\u542F\u540E\u53F0\u670D\u52A1 button). Do not retry this call in a loop: every retry only deepens the queue it is refusing to drain.`
|
|
1088
|
+
));
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1062
1091
|
this.pending.set(id, entry);
|
|
1063
1092
|
try {
|
|
1064
1093
|
this.io.writeLine(JSON.stringify(frame));
|
|
@@ -1115,13 +1144,18 @@ var EngineJsonRpcClient = class {
|
|
|
1115
1144
|
this.report(`dropped late-reply tracking for '${evicted.method}' (id ${evicted.id}): retention cap ${LATE_REPLY_RETENTION} reached`);
|
|
1116
1145
|
}
|
|
1117
1146
|
handleMessage(line) {
|
|
1118
|
-
let
|
|
1147
|
+
let parsed;
|
|
1119
1148
|
try {
|
|
1120
|
-
|
|
1149
|
+
parsed = JSON.parse(line);
|
|
1121
1150
|
} catch {
|
|
1122
1151
|
this.report(`engine sent a non-JSON frame, dropped: ${truncate(line)}`);
|
|
1123
1152
|
return;
|
|
1124
1153
|
}
|
|
1154
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1155
|
+
this.report(`engine sent a frame that is not a JSON object, dropped: ${truncate(line)}`);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const frame = parsed;
|
|
1125
1159
|
const id = frame.id;
|
|
1126
1160
|
if (typeof id === "number") {
|
|
1127
1161
|
const entry = this.pending.get(id);
|
|
@@ -1166,9 +1200,17 @@ var EngineJsonRpcClient = class {
|
|
|
1166
1200
|
settleFromFrame(entry, frame) {
|
|
1167
1201
|
if (frame.error) {
|
|
1168
1202
|
this.reject(entry, this.engineError(frame.error));
|
|
1169
|
-
|
|
1170
|
-
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (!("result" in frame)) {
|
|
1206
|
+
this.reject(entry, new EngineCallError(
|
|
1207
|
+
GP_E_INTERNAL,
|
|
1208
|
+
`engine answered '${entry.method}' with a frame carrying neither result nor error`,
|
|
1209
|
+
"do not read this call as successful: the engine answered nothing. Retry once, then check the daemon log (stderr) for what it died before answering"
|
|
1210
|
+
));
|
|
1211
|
+
return;
|
|
1171
1212
|
}
|
|
1213
|
+
this.resolve(entry, frame.result);
|
|
1172
1214
|
}
|
|
1173
1215
|
/**
|
|
1174
1216
|
* A frame over the cap: attribute it to the request it belonged to — from
|
|
@@ -1395,6 +1437,9 @@ var ReconnectingSocketIo = class {
|
|
|
1395
1437
|
writeLine(line) {
|
|
1396
1438
|
this.connect().writeLine(line);
|
|
1397
1439
|
}
|
|
1440
|
+
bufferedBytes() {
|
|
1441
|
+
return this.inner?.bufferedBytes() ?? 0;
|
|
1442
|
+
}
|
|
1398
1443
|
onMessage(handler) {
|
|
1399
1444
|
this.messageHandler = handler;
|
|
1400
1445
|
}
|
|
@@ -1765,7 +1810,7 @@ function renderMarkdown(pack, diagnostics) {
|
|
|
1765
1810
|
out.push(section.body);
|
|
1766
1811
|
out.push("");
|
|
1767
1812
|
}
|
|
1768
|
-
return out.join("\n");
|
|
1813
|
+
return escapeTerminalControl(out.join("\n"));
|
|
1769
1814
|
}
|
|
1770
1815
|
function renderHTML(pack, diagnostics) {
|
|
1771
1816
|
const out = [];
|
|
@@ -1790,7 +1835,10 @@ function renderHTML(pack, diagnostics) {
|
|
|
1790
1835
|
out.push(`</section>`);
|
|
1791
1836
|
}
|
|
1792
1837
|
out.push(`</div>`);
|
|
1793
|
-
return out.join("\n");
|
|
1838
|
+
return escapeTerminalControl(out.join("\n"));
|
|
1839
|
+
}
|
|
1840
|
+
function escapeTerminalControl(value) {
|
|
1841
|
+
return value.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
1794
1842
|
}
|
|
1795
1843
|
function escapeHTML(value) {
|
|
1796
1844
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
@@ -2106,6 +2154,27 @@ function tightenMode(target, want, kind) {
|
|
|
2106
2154
|
);
|
|
2107
2155
|
}
|
|
2108
2156
|
}
|
|
2157
|
+
function fileIdentity(filePath) {
|
|
2158
|
+
try {
|
|
2159
|
+
const stat = fs3.statSync(filePath);
|
|
2160
|
+
return `${stat.mtimeMs}:${stat.size}:${stat.ino}`;
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
if (nodeErrorCode(error) === "ENOENT") return null;
|
|
2163
|
+
throw error;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
function registryDriftReason(filePath, openedAs, now) {
|
|
2167
|
+
if (now === openedAs) {
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
return `projects.json at ${filePath} changed between the read that produced this edit and the write that would have published it (was ${openedAs ?? "absent"}, now ${now ?? "absent"}), so this call wrote nothing. Another process \u2014 the background service, its CLI, or the settings panel \u2014 holds a newer version of this table, and renaming ours over theirs would drop their entry while reporting success. Repeat gp_project_set: it reads the file again, so the second attempt carries what the other writer landed. If it keeps failing, something is writing this registry on a timer \u2014 read the file to see what appeared, and do not force the write.`;
|
|
2171
|
+
}
|
|
2172
|
+
function assertNoConcurrentWrite(filePath, openedAs) {
|
|
2173
|
+
const reason = registryDriftReason(filePath, openedAs, fileIdentity(filePath));
|
|
2174
|
+
if (reason !== null) {
|
|
2175
|
+
throw new ProjectRegistryError("registry-changed-underneath", reason);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2109
2178
|
function saveProjects(filePath, entries) {
|
|
2110
2179
|
const dir = path3.dirname(filePath);
|
|
2111
2180
|
const tmpPath = `${filePath}.tmp-${uniqueToken()}`;
|
|
@@ -2152,6 +2221,7 @@ function projectSet(args) {
|
|
|
2152
2221
|
assertStorablePid(args.pid);
|
|
2153
2222
|
const snapshot = loadRegistry(filePath);
|
|
2154
2223
|
const { projects, discarded } = requireUsableRegistry(filePath, snapshot);
|
|
2224
|
+
const openedAs = fileIdentity(filePath);
|
|
2155
2225
|
let entry;
|
|
2156
2226
|
const projectId = args.projectId;
|
|
2157
2227
|
if (projectId) {
|
|
@@ -2177,6 +2247,7 @@ function projectSet(args) {
|
|
|
2177
2247
|
assertSingleIdentity(entry);
|
|
2178
2248
|
projects.push(entry);
|
|
2179
2249
|
}
|
|
2250
|
+
assertNoConcurrentWrite(filePath, openedAs);
|
|
2180
2251
|
saveProjects(filePath, projects);
|
|
2181
2252
|
return {
|
|
2182
2253
|
...entry,
|
|
@@ -3590,7 +3661,7 @@ var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
|
3590
3661
|
"2025-03-26",
|
|
3591
3662
|
MCP_PROTOCOL_VERSION
|
|
3592
3663
|
];
|
|
3593
|
-
var SERVER_INFO = { name: "glasspane-mcp", version: "1.
|
|
3664
|
+
var SERVER_INFO = { name: "glasspane-mcp", version: "1.7.0" };
|
|
3594
3665
|
var PARSE_ERROR = -32700;
|
|
3595
3666
|
var INVALID_REQUEST = -32600;
|
|
3596
3667
|
var METHOD_NOT_FOUND = -32601;
|
|
@@ -3793,6 +3864,12 @@ function parseArgs(argv) {
|
|
|
3793
3864
|
i += 1;
|
|
3794
3865
|
} else if (arg.startsWith("--socket-path=")) {
|
|
3795
3866
|
options.socketPath = arg.slice("--socket-path=".length);
|
|
3867
|
+
} else {
|
|
3868
|
+
throw new Error(
|
|
3869
|
+
`unknown argument: ${arg}
|
|
3870
|
+
${USAGE}
|
|
3871
|
+
This shell accepts only --help/-h and --socket-path/-s <path>. A flag spelled wrong here is not an error the daemon will ever report.`
|
|
3872
|
+
);
|
|
3796
3873
|
}
|
|
3797
3874
|
}
|
|
3798
3875
|
return options;
|
package/dist/io.d.ts
CHANGED
|
@@ -62,6 +62,16 @@ export declare function recoverFrameId(prefix: string): number | string | null;
|
|
|
62
62
|
*/
|
|
63
63
|
export interface LineIo {
|
|
64
64
|
writeLine(line: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* 已经交给内核、还没被对端读走的字节数。可选:只有真的接在一条 socket/pipe 上
|
|
67
|
+
* 的传输量得出这个数。
|
|
68
|
+
*
|
|
69
|
+
* 它存在的理由是 `writeLine` 不看 `output.write()` 的返回值(stdio 那一侧有
|
|
70
|
+
* `DrainAwareWriter` 管背压,引擎这一侧从前没有):daemon 停止读 socket 时,
|
|
71
|
+
* Node 会把写进去的帧无上限地堆在进程内存里,调用方只看到"还没回",
|
|
72
|
+
* 而这一面已经悄悄替它攒了几百 MB。有了这个数,客户端可以选择**不再接活**。
|
|
73
|
+
*/
|
|
74
|
+
bufferedBytes?(): number;
|
|
65
75
|
onMessage(handler: (line: string) => void): void;
|
|
66
76
|
onClose(handler: () => void): void;
|
|
67
77
|
onError(handler: (error: Error) => void): void;
|
|
@@ -86,6 +96,7 @@ export declare class StreamLineIo implements LineIo {
|
|
|
86
96
|
private errorHandler;
|
|
87
97
|
constructor(input: Readable, output: Writable);
|
|
88
98
|
writeLine(line: string): void;
|
|
99
|
+
bufferedBytes(): number;
|
|
89
100
|
onMessage(handler: (line: string) => void): void;
|
|
90
101
|
onClose(handler: () => void): void;
|
|
91
102
|
onError(handler: (error: Error) => void): void;
|
|
@@ -112,6 +112,13 @@ export declare const ProjectSetArgs: z.ZodObject<{
|
|
|
112
112
|
pid?: number | null | undefined;
|
|
113
113
|
}>;
|
|
114
114
|
export type ProjectSetArgs = z.infer<typeof ProjectSetArgs>;
|
|
115
|
+
/**
|
|
116
|
+
* 读—改—写之间盘上换了哪一份:给出这一句拒绝的理由,没换则 null。
|
|
117
|
+
*
|
|
118
|
+
* 单独抽成纯函数是为了能被验红——`projectSet` 是同步的,测试没法在它读到盘与
|
|
119
|
+
* 写回盘之间插进另一次写,所以"守卫真的会拦"这件事只能把判据本身拿出来测。
|
|
120
|
+
*/
|
|
121
|
+
export declare function registryDriftReason(filePath: string, openedAs: string | null, now: string | null): string | null;
|
|
115
122
|
export declare function projectList(): ProjectEntry[];
|
|
116
123
|
export declare function projectGet(projectId: string): ProjectEntry | undefined;
|
|
117
124
|
/**
|
package/package.json
CHANGED