glasspane-mcp 1.5.0 → 1.6.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/http-gateway-cli.js +2 -0
- package/dist/http-gateway.js +2 -0
- package/dist/index.js +155 -3
- package/dist/update-state.d.ts +55 -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.6.0";
|
|
30
30
|
};
|
|
31
31
|
export declare const PARSE_ERROR = -32700;
|
|
32
32
|
export declare const INVALID_REQUEST = -32600;
|
package/dist/http-gateway-cli.js
CHANGED
|
@@ -1303,6 +1303,8 @@ function sortKeys(value) {
|
|
|
1303
1303
|
// src/update-state.ts
|
|
1304
1304
|
var CA_ROOTS_STATUSES = ["ok", "probe-failed", "empty", "unavailable", "write-unverified"];
|
|
1305
1305
|
var KNOWN_CA_ROOTS_STATUSES = new Set(CA_ROOTS_STATUSES);
|
|
1306
|
+
var RUNTIME_STATUSES = ["refreshed", "failed", "kept", "skipped"];
|
|
1307
|
+
var KNOWN_RUNTIME_STATUSES = new Set(RUNTIME_STATUSES);
|
|
1306
1308
|
|
|
1307
1309
|
// src/project-registry.ts
|
|
1308
1310
|
import fs2 from "node:fs";
|
package/dist/http-gateway.js
CHANGED
|
@@ -1293,6 +1293,8 @@ function sortKeys(value) {
|
|
|
1293
1293
|
// src/update-state.ts
|
|
1294
1294
|
var CA_ROOTS_STATUSES = ["ok", "probe-failed", "empty", "unavailable", "write-unverified"];
|
|
1295
1295
|
var KNOWN_CA_ROOTS_STATUSES = new Set(CA_ROOTS_STATUSES);
|
|
1296
|
+
var RUNTIME_STATUSES = ["refreshed", "failed", "kept", "skipped"];
|
|
1297
|
+
var KNOWN_RUNTIME_STATUSES = new Set(RUNTIME_STATUSES);
|
|
1296
1298
|
|
|
1297
1299
|
// src/project-registry.ts
|
|
1298
1300
|
import fs2 from "node:fs";
|
package/dist/index.js
CHANGED
|
@@ -367,6 +367,104 @@ function decodeCaRoots(value, bundlePath) {
|
|
|
367
367
|
}
|
|
368
368
|
};
|
|
369
369
|
}
|
|
370
|
+
var RUNTIME_STATUSES = ["refreshed", "failed", "kept", "skipped"];
|
|
371
|
+
var KNOWN_RUNTIME_STATUSES = new Set(RUNTIME_STATUSES);
|
|
372
|
+
function isRuntimeStatus(value) {
|
|
373
|
+
return KNOWN_RUNTIME_STATUSES.has(value);
|
|
374
|
+
}
|
|
375
|
+
var RUNTIME_KEYS = [
|
|
376
|
+
"status",
|
|
377
|
+
"version",
|
|
378
|
+
"cliPath",
|
|
379
|
+
"previousCliPath",
|
|
380
|
+
"agentVerified",
|
|
381
|
+
"at",
|
|
382
|
+
"code",
|
|
383
|
+
"detail"
|
|
384
|
+
];
|
|
385
|
+
var RUNTIME_VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
386
|
+
var RUNTIME_CODE_RE = /^[a-z0-9][a-z0-9.-]*$/;
|
|
387
|
+
function runtimeTextSlot(record, field, purpose) {
|
|
388
|
+
const raw = record[field];
|
|
389
|
+
if (raw === void 0 || raw === null) return { ok: true, value: null };
|
|
390
|
+
if (typeof raw !== "string") {
|
|
391
|
+
return { ok: false, reason: `runtime.${field} is ${typeof raw} instead of ${purpose} or null` };
|
|
392
|
+
}
|
|
393
|
+
if (raw.trim() === "") {
|
|
394
|
+
return { ok: false, reason: `runtime.${field} is blank, so ${purpose} cannot be named` };
|
|
395
|
+
}
|
|
396
|
+
return { ok: true, value: raw };
|
|
397
|
+
}
|
|
398
|
+
function decodeRuntime(value) {
|
|
399
|
+
if (value === void 0 || value === null) {
|
|
400
|
+
return { ok: true, runtime: { recorded: false, writtenAsNull: value === null } };
|
|
401
|
+
}
|
|
402
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
reason: `runtime is ${Array.isArray(value) ? "an array" : typeof value} instead of the object the updater writes`
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const record = value;
|
|
409
|
+
const stray = Object.keys(record).filter((key) => !RUNTIME_KEYS.includes(key));
|
|
410
|
+
if (stray.length > 0) {
|
|
411
|
+
return {
|
|
412
|
+
ok: false,
|
|
413
|
+
reason: `runtime carries ${stray.map((key) => JSON.stringify(key)).join(", ")}, a key the state schema does not declare (the object is closed with additionalProperties: false)`
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const rawStatus = record.status;
|
|
417
|
+
if (typeof rawStatus !== "string" || !isRuntimeStatus(rawStatus)) {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
reason: `runtime.status ${JSON.stringify(rawStatus ?? null)} is not one of ${RUNTIME_STATUSES.join(", ")}`
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const version = runtimeTextSlot(record, "version", "a version");
|
|
424
|
+
if (!version.ok) return version;
|
|
425
|
+
if (version.value !== null && !RUNTIME_VERSION_RE.test(version.value)) {
|
|
426
|
+
return { ok: false, reason: `runtime.version ${JSON.stringify(version.value)} is not a version this shell can name` };
|
|
427
|
+
}
|
|
428
|
+
const cliPath = runtimeTextSlot(record, "cliPath", "the installed copy");
|
|
429
|
+
if (!cliPath.ok) return cliPath;
|
|
430
|
+
const previousCliPath = runtimeTextSlot(record, "previousCliPath", "the copy it came from");
|
|
431
|
+
if (!previousCliPath.ok) return previousCliPath;
|
|
432
|
+
const at = runtimeTextSlot(record, "at", "an instant");
|
|
433
|
+
if (!at.ok) return at;
|
|
434
|
+
const code = runtimeTextSlot(record, "code", "a gate code");
|
|
435
|
+
if (!code.ok) return code;
|
|
436
|
+
if (code.value !== null && !RUNTIME_CODE_RE.test(code.value)) {
|
|
437
|
+
return { ok: false, reason: `runtime.code ${JSON.stringify(code.value)} is not a gate code this shell can quote` };
|
|
438
|
+
}
|
|
439
|
+
const rawAgent = record.agentVerified;
|
|
440
|
+
let agentVerified = null;
|
|
441
|
+
if (typeof rawAgent === "boolean") {
|
|
442
|
+
agentVerified = rawAgent;
|
|
443
|
+
} else if (rawAgent !== void 0 && rawAgent !== null) {
|
|
444
|
+
return { ok: false, reason: `runtime.agentVerified is ${typeof rawAgent} instead of true, false or null` };
|
|
445
|
+
}
|
|
446
|
+
const rawDetail = record.detail;
|
|
447
|
+
return {
|
|
448
|
+
ok: true,
|
|
449
|
+
runtime: {
|
|
450
|
+
recorded: true,
|
|
451
|
+
status: rawStatus,
|
|
452
|
+
version: version.value,
|
|
453
|
+
cliPath: cliPath.value,
|
|
454
|
+
previousCliPath: previousCliPath.value,
|
|
455
|
+
agentVerified,
|
|
456
|
+
at: at.value,
|
|
457
|
+
code: code.value,
|
|
458
|
+
detail: typeof rawDetail === "string" && rawDetail.trim() !== "" ? rawDetail : null
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function runtimeIsUsable(runtime) {
|
|
463
|
+
return runtime.recorded && runtime.status === "refreshed" && runtime.agentVerified === true;
|
|
464
|
+
}
|
|
465
|
+
function stateRootOf(stateFile) {
|
|
466
|
+
return path.dirname(stateFile);
|
|
467
|
+
}
|
|
370
468
|
function readUpdateState(env = process.env, stateRootOverride) {
|
|
371
469
|
const stateFile = updateStateFile(env, stateRootOverride);
|
|
372
470
|
if (stateFile === null) {
|
|
@@ -419,6 +517,14 @@ function readUpdateState(env = process.env, stateRootOverride) {
|
|
|
419
517
|
summary: `update state: ${stateFile} carries a CA trust bundle record this shell cannot read (${ca.reason}); whether this install can reach the release endpoint at all is unknown, so nothing below it is trusted either \u2014 re-run the installer or \`node updater/cli.js enable\` to write the record again`
|
|
420
518
|
};
|
|
421
519
|
}
|
|
520
|
+
const runtime = decodeRuntime(parsed.runtime);
|
|
521
|
+
if (!runtime.ok) {
|
|
522
|
+
return {
|
|
523
|
+
kind: "unreadable",
|
|
524
|
+
stateFile,
|
|
525
|
+
summary: `update state: ${stateFile} carries an updater self-update record this shell cannot read (${runtime.reason}); which copy of the updater this machine's daily job will run next is unknown, so nothing below it is trusted either \u2014 re-run the GlassPane installer, or \`node updater/cli.js enable\`, to write the record again`
|
|
526
|
+
};
|
|
527
|
+
}
|
|
422
528
|
const bits = [
|
|
423
529
|
`status=${status}`,
|
|
424
530
|
`current=${String(parsed.current ?? "unknown")}`,
|
|
@@ -434,7 +540,7 @@ function readUpdateState(env = process.env, stateRootOverride) {
|
|
|
434
540
|
if (staged && typeof staged.version === "string") {
|
|
435
541
|
bits.push(`staged=${staged.version}${staged.digest ? ` (${String(staged.digest).slice(0, 12)}\u2026)` : ""}`);
|
|
436
542
|
}
|
|
437
|
-
return { kind: "read", stateFile, summary: `update state: ${bits.join("; ")}`, ca: ca.ca };
|
|
543
|
+
return { kind: "read", stateFile, summary: `update state: ${bits.join("; ")}`, ca: ca.ca, runtime: runtime.runtime };
|
|
438
544
|
}
|
|
439
545
|
|
|
440
546
|
// src/io.ts
|
|
@@ -2913,7 +3019,8 @@ async function runValidatedTool(spec, value, engine, session, turn) {
|
|
|
2913
3019
|
content.push({
|
|
2914
3020
|
type: "text",
|
|
2915
3021
|
text: reading.kind === "read" ? `${reading.summary}
|
|
2916
|
-
${caBundleDiagnosis(reading.ca)}
|
|
3022
|
+
${caBundleDiagnosis(reading.ca)}
|
|
3023
|
+
${runtimeSelfDiagnosis(reading.runtime, reading.stateFile)}` : reading.summary
|
|
2917
3024
|
});
|
|
2918
3025
|
}
|
|
2919
3026
|
return {
|
|
@@ -2978,6 +3085,51 @@ function caBundleDiagnosis(ca) {
|
|
|
2978
3085
|
].filter((part) => part !== null);
|
|
2979
3086
|
return provenance.length === 0 ? diagnosis : `${diagnosis} The record itself says: ${provenance.join("; ")}.`;
|
|
2980
3087
|
}
|
|
3088
|
+
function runtimeSelfDiagnosis(runtime, stateFile) {
|
|
3089
|
+
const installerCmd = "`node installer/cli.js` (or the `install.sh` this machine was installed with)";
|
|
3090
|
+
const enableCmd = (cliPath) => cliPath === null ? null : `node ${cliPath} enable --state-dir ${stateRootOf(stateFile)} --json`;
|
|
3091
|
+
const copy = (version, cliPath) => {
|
|
3092
|
+
if (cliPath === null) {
|
|
3093
|
+
return version === null ? "the record names neither a version nor a path for it" : `version ${version}, and the record names no path for it`;
|
|
3094
|
+
}
|
|
3095
|
+
return version === null ? `${cliPath} (no version recorded)` : `${version} at ${cliPath}`;
|
|
3096
|
+
};
|
|
3097
|
+
if (!runtime.recorded) {
|
|
3098
|
+
return `updater self-update: this install has never recorded moving the updater's own copy \u2014 the state file ${runtime.writtenAsNull ? 'carries "runtime": null' : 'carries no "runtime" record at all'}, which is NOT the same as "the installed updater is current". An install that predates the self-swap looks exactly like this, and its daily job still runs the copy the installer first cloned: a release whose content is "fix the updater" can never reach this machine by itself. Remedy: re-run the GlassPane installer once, ${installerCmd}. That is a manual step this install needs before self-updating is possible; after it, a successful apply records this line as refreshed.`;
|
|
3099
|
+
}
|
|
3100
|
+
const location = copy(runtime.version, runtime.cliPath);
|
|
3101
|
+
let diagnosis;
|
|
3102
|
+
switch (runtime.status) {
|
|
3103
|
+
case "refreshed": {
|
|
3104
|
+
if (!runtimeIsUsable(runtime)) {
|
|
3105
|
+
diagnosis = `updater self-update: refreshed, but not confirmed \u2014 the record claims the updater's own copy (${location}) is installed, yet the loaded launchd job was never read back going through that entry, so this is not evidence the new copy runs: tomorrow's check may still execute the previous one. Remedy: ` + (enableCmd(runtime.cliPath) === null ? `the record names no script to register, so re-run the GlassPane installer, ${installerCmd}` : `have that copy register itself and read the job back with \`${enableCmd(runtime.cliPath)}\`, or re-run the GlassPane installer, ${installerCmd}`) + ". Until this line says refreshed with the job read back, do not treat a release that fixes the updater as delivered here.";
|
|
3106
|
+
break;
|
|
3107
|
+
}
|
|
3108
|
+
diagnosis = `updater self-update: refreshed \u2014 the updater's own installed copy (${location}) is in place and the loaded launchd job was read back and seen to go through the stable entry that resolves this pointer, so this machine's daily check runs the same code the last release shipped. Nothing to do: a future release that changes the updater reaches this install without anyone reinstalling.`;
|
|
3109
|
+
break;
|
|
3110
|
+
}
|
|
3111
|
+
case "failed":
|
|
3112
|
+
diagnosis = `updater self-update: failed \u2014 the update itself installed (the .app bundles and the npm packages did change), but the updater's own copy did not move: the copy this swap tried to install (${location}) is not what the job runs, and the previous copy still does. This is NOT "the update failed", and it is not harmless either: the scheduled job keeps running the previous updater with that version's gates, so a release that fixes the updater cannot land here while the line above still reads up-to-date. Remedy: re-run the GlassPane installer, ${installerCmd}, which rewrites the installer pointer and puts a current copy in place` + (enableCmd(runtime.cliPath) === null ? " \u2014 this record names no new copy at all, so the installer is the only way out" : `; if the tree already on disk is the one you want running, \`${enableCmd(runtime.cliPath)}\` registers that copy directly`) + ". Do not count the next check as running new code until this line says refreshed.";
|
|
3113
|
+
break;
|
|
3114
|
+
case "kept":
|
|
3115
|
+
diagnosis = `updater self-update: kept \u2014 automatic update is switched off on this machine, so the updater's code and the installer pointer moved (${location}) but no launchd job was registered. This is the switch doing what it was set to, not a failure, and not a self-updating install either: nothing will move here by itself, so the settings panel and a hand-run check are the only ways this copy advances. Remedy: switch automatic ` + (enableCmd(runtime.cliPath) === null ? `update back on in the settings panel, or re-run the GlassPane installer, ${installerCmd}, which registers the job as it installs` : `update back on in the settings panel, or let that copy register itself with \`${enableCmd(runtime.cliPath)}\`; the installer, ${installerCmd}, does the same as it installs`) + ".";
|
|
3116
|
+
break;
|
|
3117
|
+
case "skipped":
|
|
3118
|
+
diagnosis = "updater self-update: skipped \u2014 the updater's code, the installer pointer and the stable agent entry all moved, but the registered launchd job still names a versioned script instead of that entry, so this machine owes one migration. Every scheduled apply stops here until it is done, because re-registering from inside the daily job would unregister the job that is running it" + (runtime.cliPath === null ? "" : `; the copy that would run next is ${location}`) + `. So treat the updater as whatever the loaded job actually runs, rather than as current. Remedy (one time): switch automatic update off and on again in the settings panel, or run \`updater enable\` from a terminal \u2014 after that a version move is a single file write and no future release will ask for this step; if a machine has only ever shown skipped, re-run the GlassPane installer, ${installerCmd}, once to put a self-updating copy down.`;
|
|
3119
|
+
break;
|
|
3120
|
+
default: {
|
|
3121
|
+
const unhandled = runtime.status;
|
|
3122
|
+
diagnosis = `updater self-update: the state file records ${JSON.stringify(unhandled)}, a state this shell has no sentence for; treat the updater's own copy as unverified and re-run the GlassPane installer`;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
const provenance = [
|
|
3126
|
+
runtime.at === null ? null : `recorded at ${runtime.at}`,
|
|
3127
|
+
runtime.code === null ? null : `gate code: ${runtime.code}`,
|
|
3128
|
+
runtime.previousCliPath === null ? null : `previous copy: ${runtime.previousCliPath}`,
|
|
3129
|
+
runtime.detail === null ? null : `recorded reason: ${runtime.detail}`
|
|
3130
|
+
].filter((part) => part !== null);
|
|
3131
|
+
return provenance.length === 0 ? diagnosis : `${diagnosis} The record itself says: ${provenance.join("; ")}.`;
|
|
3132
|
+
}
|
|
2981
3133
|
var EvidenceFrameShapeError = class extends Error {
|
|
2982
3134
|
constructor(detail) {
|
|
2983
3135
|
super(`engine returned a last_evidence frame without an evidencePack object: ${detail}`);
|
|
@@ -3438,7 +3590,7 @@ var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
|
3438
3590
|
"2025-03-26",
|
|
3439
3591
|
MCP_PROTOCOL_VERSION
|
|
3440
3592
|
];
|
|
3441
|
-
var SERVER_INFO = { name: "glasspane-mcp", version: "1.
|
|
3593
|
+
var SERVER_INFO = { name: "glasspane-mcp", version: "1.6.0" };
|
|
3442
3594
|
var PARSE_ERROR = -32700;
|
|
3443
3595
|
var INVALID_REQUEST = -32600;
|
|
3444
3596
|
var METHOD_NOT_FOUND = -32601;
|
package/dist/update-state.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export type UpdateStateReading = {
|
|
|
17
17
|
summary: string;
|
|
18
18
|
/** Decoded `caRoots`, present exactly when the file was read and understood. */
|
|
19
19
|
ca: CaRootsReading;
|
|
20
|
+
/** Decoded `runtime`, present exactly when the file was read and understood. */
|
|
21
|
+
runtime: RuntimeReading;
|
|
20
22
|
} | {
|
|
21
23
|
kind: "absent" | "unreadable" | "unresolvable";
|
|
22
24
|
stateFile: string | null;
|
|
@@ -51,6 +53,59 @@ export type CaRootsReading = {
|
|
|
51
53
|
recorded: false;
|
|
52
54
|
bundlePath: string;
|
|
53
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* The four states the updater can record for **its own installed copy**, as a closed set:
|
|
58
|
+
* `refreshed` (the new copy landed, the pointer names it, and the loaded launchd job was read back and
|
|
59
|
+
* seen to go through the stable agent entry that resolves that pointer — with the entry in place the job
|
|
60
|
+
* no longer names a version, so `agentVerified` is a claim about the entry, not about this version),
|
|
61
|
+
* `failed` (the updater's own copy did not move — the `.app`/npm swap may still have
|
|
62
|
+
* succeeded, so this is a stated degradation and emphatically not "the update failed"),
|
|
63
|
+
* `kept` (automatic update is switched off on this machine, so the code and the pointer
|
|
64
|
+
* moved but no job was registered: neither a failure nor something to brag about),
|
|
65
|
+
* `skipped` (the code, the pointer and the entry all moved, but the registered job still names a
|
|
66
|
+
* versioned script, so this machine owes the one migration that puts it on the entry — after which no
|
|
67
|
+
* version asks again). Only `refreshed` with `agentVerified === true` is
|
|
68
|
+
* usable; folding any other state into it is how a machine that has run the same stale
|
|
69
|
+
* updater since install reads as one that fixes itself.
|
|
70
|
+
*/
|
|
71
|
+
export declare const RUNTIME_STATUSES: readonly ["refreshed", "failed", "kept", "skipped"];
|
|
72
|
+
export type RuntimeStatus = (typeof RUNTIME_STATUSES)[number];
|
|
73
|
+
export type RuntimeReading = {
|
|
74
|
+
recorded: true;
|
|
75
|
+
status: RuntimeStatus;
|
|
76
|
+
/** Version the updater's own copy was moved to; `null` when the record did not say. */
|
|
77
|
+
version: string | null;
|
|
78
|
+
/** The installed copy this record is about — a real path, never a template. */
|
|
79
|
+
cliPath: string | null;
|
|
80
|
+
/** The copy it came from, when the record kept one (what a rollback points back at). */
|
|
81
|
+
previousCliPath: string | null;
|
|
82
|
+
/** `true` only after the loaded job was read back and names `cliPath`. */
|
|
83
|
+
agentVerified: boolean | null;
|
|
84
|
+
at: string | null;
|
|
85
|
+
/** Stable code of the gate that refused (`runtime-stale`, `auto-disabled`, …), or null. */
|
|
86
|
+
code: string | null;
|
|
87
|
+
/** The updater's own reason text, when it wrote one. */
|
|
88
|
+
detail: string | null;
|
|
89
|
+
} | {
|
|
90
|
+
recorded: false;
|
|
91
|
+
/**
|
|
92
|
+
* Which route left nothing recorded: `true` is a file that carries `runtime: null`,
|
|
93
|
+
* `false` is a file with no such key at all — the shape an install predating the
|
|
94
|
+
* feature has. Both mean "this swap never tried", and they stay apart because a
|
|
95
|
+
* reader says them differently.
|
|
96
|
+
*/
|
|
97
|
+
writtenAsNull: boolean;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Does this record say the updater's own copy is current? Only `refreshed` *with*
|
|
101
|
+
* `agentVerified === true` qualifies — that pair is the one moment where the code on disk
|
|
102
|
+
* and the code launchd has loaded were both looked at. `kept` is deliberately not usable:
|
|
103
|
+
* the machine is switched off for automatic updates, so nothing was registered and nothing
|
|
104
|
+
* will move by itself.
|
|
105
|
+
*/
|
|
106
|
+
export declare function runtimeIsUsable(runtime: RuntimeReading): boolean;
|
|
107
|
+
/** The state root a state file lives in: one author, so a remedy command never names a directory that is not the root. */
|
|
108
|
+
export declare function stateRootOf(stateFile: string): string;
|
|
54
109
|
/**
|
|
55
110
|
* Read and render. Never throws: `gp_diagnose` answers about an operation, and a
|
|
56
111
|
* missing or broken update file is a fact about the machine, not a reason to fail
|
package/package.json
CHANGED