0xray 4.0.39 → 4.0.41

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 (71) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.xray/features.json +1 -1
  3. package/dist/CHANGELOG.md +29 -0
  4. package/dist/integrations/grok/hooks/grok-hook-utils.js +21 -1
  5. package/dist/integrations/grok/hooks/session-start.js +2 -20
  6. package/dist/integrations/hooks/goggles-mcp.mjs +36 -4
  7. package/dist/integrations/hooks/goggles-pipeline.mjs +55 -15
  8. package/dist/integrations/hooks/goggles-planes.json +10 -4
  9. package/dist/integrations/hooks/plates.cjs +22 -0
  10. package/dist/integrations/hooks/session-note-index.cjs +298 -0
  11. package/dist/integrations/hooks/session-note-index.mjs +22 -0
  12. package/dist/integrations/hooks/sleeve.cjs +335 -0
  13. package/dist/integrations/hooks/sleeve.mjs +18 -0
  14. package/dist/integrations/hooks/station-hook-runtime.cjs +205 -9
  15. package/dist/integrations/hooks/wake-cascade.cjs +102 -0
  16. package/dist/integrations/hooks/wake-cascade.mjs +16 -0
  17. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  18. package/dist/memory-routing/plates.d.ts +1 -1
  19. package/dist/memory-routing/plates.js +1 -0
  20. package/docs-site/docs/plates/activity-log.md +1 -1
  21. package/docs-site/docs/plates/boot.md +2 -0
  22. package/docs-site/docs/plates/centrifuge.md +30 -0
  23. package/docs-site/docs/plates/cursor-compact.md +1 -1
  24. package/docs-site/docs/plates/glossary.md +1 -2
  25. package/docs-site/docs/plates/goggles.md +4 -0
  26. package/docs-site/docs/plates/governance.md +2 -0
  27. package/docs-site/docs/plates/grok-compact.md +1 -1
  28. package/docs-site/docs/plates/grokbot.md +1 -1
  29. package/docs-site/docs/plates/host-pack.md +2 -0
  30. package/docs-site/docs/plates/house.md +1 -1
  31. package/docs-site/docs/plates/index.md +4 -2
  32. package/docs-site/docs/plates/inference-files.md +1 -1
  33. package/docs-site/docs/plates/kept-line.md +1 -1
  34. package/docs-site/docs/plates/kits.md +2 -0
  35. package/docs-site/docs/plates/lens-gate.md +1 -1
  36. package/docs-site/docs/plates/lens-page.md +1 -1
  37. package/docs-site/docs/plates/live-mesh.md +41 -0
  38. package/docs-site/docs/plates/memory-recall.md +4 -0
  39. package/docs-site/docs/plates/notes-page.md +2 -1
  40. package/docs-site/docs/plates/orchestration.md +2 -0
  41. package/docs-site/docs/plates/package-face.md +1 -1
  42. package/docs-site/docs/plates/payload-heat.md +1 -1
  43. package/docs-site/docs/plates/pickup-stamp.md +1 -1
  44. package/docs-site/docs/plates/pre-tool.md +1 -1
  45. package/docs-site/docs/plates/processor.md +2 -0
  46. package/docs-site/docs/plates/record-map.md +2 -1
  47. package/docs-site/docs/plates/reflection-page.md +1 -1
  48. package/docs-site/docs/plates/reporting.md +2 -0
  49. package/docs-site/docs/plates/review.md +1 -1
  50. package/docs-site/docs/plates/routing.md +2 -0
  51. package/docs-site/docs/plates/session-capture.md +1 -1
  52. package/docs-site/docs/plates/site-manual.md +1 -1
  53. package/docs-site/docs/plates/stamp-plate.md +1 -1
  54. package/docs-site/docs/plates/station-card.md +4 -1
  55. package/docs-site/docs/plates/station-heat.md +1 -1
  56. package/docs-site/docs/plates/suit-organs.md +1 -1
  57. package/docs-site/docs/plates/suit-settings.md +1 -1
  58. package/docs-site/docs/plates/suit-wear.md +3 -1
  59. package/docs-site/docs/plates/suit.md +2 -0
  60. package/docs-site/docs/plates/trail-state.md +1 -1
  61. package/docs-site/docs/plates/work-fresh.md +1 -1
  62. package/docs-site/docs/plates/write-home.md +1 -1
  63. package/package.json +2 -1
  64. package/scripts/node/bridge-mcp-wiring.cjs +37 -4
  65. package/scripts/node/install-bridges.cjs +172 -19
  66. package/scripts/node/playwright-smoke-server.cjs +27 -0
  67. package/scripts/node/setup.cjs +2 -46
  68. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  69. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/RepertoireOrchestratorBridge.js +4 -1
  70. package/vendor/@0xray/repertoire/dist/paths.js +3 -0
  71. package/xray/features.json +1 -1
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Session notes beside repertoire laws.
3
+ * Laws stay in curated_signals.json. This file is the index.
4
+ * Cleanup drops a lone note only after STALE_AFTER_MS without another sighting.
5
+ * A replaced note stays on disk and is left out of the short index.
6
+ * The short index is a view of current notes. It does not delete history.
7
+ */
8
+ const { createHash } = require("crypto");
9
+ const { existsSync, mkdirSync, readFileSync, writeFileSync } = require("fs");
10
+ const { join } = require("path");
11
+
12
+ const DAY_MS = 24 * 60 * 60 * 1000;
13
+ const STALE_AFTER_MS = 90 * DAY_MS;
14
+ const SHORT_INDEX_LIMIT = 12;
15
+ const LINE_MAX = 240;
16
+
17
+ function sessionNotesPath(root) {
18
+ return join(root, ".xray", "state", "repertoire", "session-notes.json");
19
+ }
20
+
21
+ function lawsPath(root) {
22
+ return join(root, ".xray", "state", "repertoire", "curated_signals.json");
23
+ }
24
+
25
+ function clipLine(raw) {
26
+ if (raw == null) return null;
27
+ const text = String(raw).replace(/\s+/g, " ").trim();
28
+ if (!text) return null;
29
+ if (text.length <= LINE_MAX) return text;
30
+ return `${text.slice(0, LINE_MAX - 1)}…`;
31
+ }
32
+
33
+ function noteId(text) {
34
+ return createHash("sha256").update(text).digest("hex").slice(0, 12);
35
+ }
36
+
37
+ function loadIndex(root) {
38
+ const dest = sessionNotesPath(root);
39
+ if (!existsSync(dest)) return { notes: [], corrupt: false };
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(readFileSync(dest, "utf8"));
43
+ } catch {
44
+ return { notes: [], corrupt: true };
45
+ }
46
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.notes)) {
47
+ return { notes: [], corrupt: true };
48
+ }
49
+ const notes = [];
50
+ for (const row of parsed.notes) {
51
+ if (!row || typeof row.text !== "string" || !row.text.trim()) continue;
52
+ const text = clipLine(row.text);
53
+ if (!text) continue;
54
+ const added = typeof row.added === "string" ? row.added : "";
55
+ const lastSeen = typeof row.last_seen === "string" && row.last_seen ? row.last_seen : added;
56
+ if (!added || !lastSeen) continue;
57
+ const note = {
58
+ id: typeof row.id === "string" && row.id ? row.id : noteId(text),
59
+ text,
60
+ added,
61
+ last_seen: lastSeen,
62
+ };
63
+ if (typeof row.source === "string") note.source = row.source;
64
+ if (typeof row.slot === "string" && row.slot) note.slot = row.slot;
65
+ if (typeof row.superseded_by === "string" && row.superseded_by) note.superseded_by = row.superseded_by;
66
+ if (typeof row.superseded_at === "string" && row.superseded_at) note.superseded_at = row.superseded_at;
67
+ notes.push(note);
68
+ }
69
+ return { notes, corrupt: false };
70
+ }
71
+
72
+ function writeIndex(root, notes) {
73
+ const dest = sessionNotesPath(root);
74
+ try {
75
+ mkdirSync(join(root, ".xray", "state", "repertoire"), { recursive: true });
76
+ const body = notes.map((note) => {
77
+ const row = {
78
+ id: note.id,
79
+ text: note.text,
80
+ added: note.added,
81
+ last_seen: note.last_seen,
82
+ };
83
+ if (note.source) row.source = note.source;
84
+ if (note.slot) row.slot = note.slot;
85
+ if (note.superseded_by) row.superseded_by = note.superseded_by;
86
+ if (note.superseded_at) row.superseded_at = note.superseded_at;
87
+ return row;
88
+ });
89
+ writeFileSync(dest, `${JSON.stringify({ notes: body }, null, 2)}\n`);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ function nowStamp(opts) {
97
+ return typeof opts.now === "string" && opts.now ? opts.now : new Date().toISOString();
98
+ }
99
+
100
+ function isCurrent(note) {
101
+ return !note.superseded_by;
102
+ }
103
+
104
+ /** The new line becomes current. The previous holder of the same slot stays on disk. */
105
+ function closeSlot(notes, slot, keeperId, now) {
106
+ if (!slot) return 0;
107
+ let closed = 0;
108
+ for (const note of notes) {
109
+ if (note.id === keeperId) continue;
110
+ if (note.slot !== slot) continue;
111
+ if (note.superseded_by) continue;
112
+ note.superseded_by = keeperId;
113
+ note.superseded_at = now;
114
+ closed += 1;
115
+ }
116
+ return closed;
117
+ }
118
+
119
+ function rememberSessionNote(root, text, opts = {}) {
120
+ const line = clipLine(text);
121
+ if (!line) return { kept: false };
122
+ const loaded = loadIndex(root);
123
+ if (loaded.corrupt) return { kept: false, corrupt: true };
124
+ const now = nowStamp(opts);
125
+ const source = typeof opts.source === "string" ? opts.source : undefined;
126
+ const slot = typeof opts.slot === "string" && opts.slot ? opts.slot : undefined;
127
+ const existing = loaded.notes.find((note) => note.text === line);
128
+ if (existing) {
129
+ existing.last_seen = now;
130
+ if (source) existing.source = source;
131
+ // A notes page can still show the old line. That sighting must not
132
+ // revive it or demote the line that replaced it.
133
+ if (slot && !existing.superseded_by) {
134
+ existing.slot = slot;
135
+ closeSlot(loaded.notes, slot, existing.id, now);
136
+ }
137
+ const wrote = writeIndex(root, loaded.notes);
138
+ return { kept: wrote, bumped: true, id: existing.id, corrupt: false };
139
+ }
140
+ const created = {
141
+ id: noteId(line),
142
+ text: line,
143
+ added: now,
144
+ last_seen: now,
145
+ };
146
+ if (source) created.source = source;
147
+ if (slot) created.slot = slot;
148
+ loaded.notes.push(created);
149
+ if (slot) closeSlot(loaded.notes, slot, created.id, now);
150
+ const wrote = writeIndex(root, loaded.notes);
151
+ return { kept: wrote, bumped: false, id: created.id, corrupt: false };
152
+ }
153
+
154
+ function supersedeSessionNote(root, previousText, nextText, opts = {}) {
155
+ const previousLine = clipLine(previousText);
156
+ const nextLine = clipLine(nextText);
157
+ if (!previousLine || !nextLine || previousLine === nextLine) return { kept: false };
158
+ const loaded = loadIndex(root);
159
+ if (loaded.corrupt) return { kept: false, corrupt: true };
160
+ const previous = loaded.notes.find((note) => note.text === previousLine || note.id === previousLine);
161
+ if (!previous) return { kept: false, missing: true };
162
+ const now = nowStamp(opts);
163
+ const source = typeof opts.source === "string" ? opts.source : previous.source;
164
+ let next = loaded.notes.find((note) => note.text === nextLine);
165
+ if (!next) {
166
+ next = {
167
+ id: noteId(nextLine),
168
+ text: nextLine,
169
+ added: now,
170
+ last_seen: now,
171
+ };
172
+ if (source) next.source = source;
173
+ if (previous.slot) next.slot = previous.slot;
174
+ loaded.notes.push(next);
175
+ } else {
176
+ next.last_seen = now;
177
+ delete next.superseded_by;
178
+ delete next.superseded_at;
179
+ if (previous.slot) next.slot = previous.slot;
180
+ }
181
+ if (next.id === previous.id) return { kept: false };
182
+ previous.superseded_by = next.id;
183
+ previous.superseded_at = now;
184
+ if (next.slot) closeSlot(loaded.notes, next.slot, next.id, now);
185
+ const wrote = writeIndex(root, loaded.notes);
186
+ return { kept: wrote, id: next.id, supersededId: previous.id, corrupt: false };
187
+ }
188
+
189
+ function sessionNoteHistory(root, textOrId) {
190
+ const loaded = loadIndex(root);
191
+ const key = clipLine(textOrId) || String(textOrId || "");
192
+ const start = loaded.notes.find((note) => note.text === key || note.id === textOrId);
193
+ if (!start) return [];
194
+ const seen = new Set([start.id]);
195
+ const older = [];
196
+ let cursor = start.id;
197
+ while (cursor && seen.size < loaded.notes.length + 1) {
198
+ const prev = loaded.notes.find((note) => note.superseded_by === cursor && !seen.has(note.id));
199
+ if (!prev) break;
200
+ seen.add(prev.id);
201
+ older.push(prev);
202
+ cursor = prev.id;
203
+ }
204
+ return older;
205
+ }
206
+
207
+ function shortSessionIndex(root, limit = SHORT_INDEX_LIMIT) {
208
+ const loaded = loadIndex(root);
209
+ const cap = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : SHORT_INDEX_LIMIT;
210
+ const ordered = loaded.notes.filter(isCurrent).sort((a, b) => {
211
+ if (a.last_seen === b.last_seen) return a.added < b.added ? 1 : -1;
212
+ return a.last_seen < b.last_seen ? 1 : -1;
213
+ });
214
+ return ordered.slice(0, cap);
215
+ }
216
+
217
+ function dropStaleSessionNotes(root, opts = {}) {
218
+ const loaded = loadIndex(root);
219
+ if (loaded.corrupt) return { kept: 0, dropped: 0, corrupt: true };
220
+ const nowMs = typeof opts.now === "string" ? Date.parse(opts.now) : Date.now();
221
+ const windowMs = Number.isFinite(opts.staleAfterMs) ? opts.staleAfterMs : STALE_AFTER_MS;
222
+ if (!Number.isFinite(nowMs)) return { kept: loaded.notes.length, dropped: 0, corrupt: false };
223
+ const protectedIds = new Set();
224
+ for (const note of loaded.notes) {
225
+ if (!note.superseded_by) continue;
226
+ protectedIds.add(note.id);
227
+ protectedIds.add(note.superseded_by);
228
+ }
229
+ const kept = [];
230
+ let dropped = 0;
231
+ for (const note of loaded.notes) {
232
+ const seen = Date.parse(note.last_seen);
233
+ const stale = Number.isFinite(seen) && nowMs - seen > windowMs;
234
+ if (stale && !protectedIds.has(note.id)) {
235
+ dropped += 1;
236
+ continue;
237
+ }
238
+ kept.push(note);
239
+ }
240
+ if (dropped > 0) writeIndex(root, kept);
241
+ return { kept: kept.length, dropped, corrupt: false };
242
+ }
243
+
244
+ function linesFromNotes(text) {
245
+ const lines = [];
246
+ for (const raw of String(text || "").split("\n")) {
247
+ const trimmed = raw.trim();
248
+ if (!trimmed) continue;
249
+ if (/^#{1,6}\s/.test(trimmed)) continue;
250
+ const pickup = trimmed.match(/^\*\*Pickup line:\*\*\s*(.*)$/);
251
+ if (pickup) {
252
+ if (pickup[1].trim()) lines.push({ text: pickup[1].trim(), slot: "pickup" });
253
+ continue;
254
+ }
255
+ const bullet = trimmed.replace(/^[-*]\s+/, "");
256
+ if (bullet) lines.push({ text: bullet });
257
+ }
258
+ return lines;
259
+ }
260
+
261
+ function retainProjectNotes(root, opts = {}) {
262
+ const dest = join(root, ".xray", "state", "NOTES.md");
263
+ let remembered = 0;
264
+ if (existsSync(dest)) {
265
+ let text = "";
266
+ try {
267
+ text = readFileSync(dest, "utf8");
268
+ } catch {
269
+ text = "";
270
+ }
271
+ for (const line of linesFromNotes(text)) {
272
+ const saved = rememberSessionNote(root, line.text, {
273
+ ...opts,
274
+ source: opts.source || "NOTES.md",
275
+ slot: line.slot,
276
+ });
277
+ if (saved.corrupt) return { remembered, dropped: 0, kept: 0, corrupt: true };
278
+ if (saved.kept) remembered += 1;
279
+ }
280
+ }
281
+ const cleaned = dropStaleSessionNotes(root, opts);
282
+ return { remembered, ...cleaned };
283
+ }
284
+
285
+ module.exports = {
286
+ DAY_MS,
287
+ STALE_AFTER_MS,
288
+ SHORT_INDEX_LIMIT,
289
+ LINE_MAX,
290
+ sessionNotesPath,
291
+ lawsPath,
292
+ rememberSessionNote,
293
+ supersedeSessionNote,
294
+ sessionNoteHistory,
295
+ shortSessionIndex,
296
+ dropStaleSessionNotes,
297
+ retainProjectNotes,
298
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ESM face for session-note-index.cjs.
3
+ */
4
+ import { createRequire } from "node:module";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, join } from "node:path";
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const impl = require(join(dirname(fileURLToPath(import.meta.url)), "session-note-index.cjs"));
10
+
11
+ export const DAY_MS = impl.DAY_MS;
12
+ export const STALE_AFTER_MS = impl.STALE_AFTER_MS;
13
+ export const SHORT_INDEX_LIMIT = impl.SHORT_INDEX_LIMIT;
14
+ export const LINE_MAX = impl.LINE_MAX;
15
+ export const sessionNotesPath = impl.sessionNotesPath;
16
+ export const lawsPath = impl.lawsPath;
17
+ export const rememberSessionNote = impl.rememberSessionNote;
18
+ export const supersedeSessionNote = impl.supersedeSessionNote;
19
+ export const sessionNoteHistory = impl.sessionNoteHistory;
20
+ export const shortSessionIndex = impl.shortSessionIndex;
21
+ export const dropStaleSessionNotes = impl.dropStaleSessionNotes;
22
+ export const retainProjectNotes = impl.retainProjectNotes;
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Sleeve: on means the previous ticket came back, the worn plate already
3
+ * matched the package, that ticket already matched the working record,
4
+ * and inspect has passed for the skills on disk.
5
+ * Session start and compact run the mill. A later prompt reads the report.
6
+ */
7
+ const { execFileSync } = require("child_process");
8
+ const { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } = require("fs");
9
+ const { dirname, join } = require("path");
10
+
11
+ const STITCHES = ["loop", "domain", "state", "foundry"];
12
+ const SLEEVE_PLANES = [
13
+ "record-map",
14
+ "station-card",
15
+ "notes-page",
16
+ "memory-recall",
17
+ "suit-wear",
18
+ "suit-organs",
19
+ "suit-settings",
20
+ "trail-state",
21
+ ];
22
+ const FOUNDRY_FILES = ["cli.mjs", "mill-root.mjs", "inspect.mjs", "reconcile-version.mjs"];
23
+
24
+ function isSleevePlane(id) {
25
+ return SLEEVE_PLANES.includes(String(id || ""));
26
+ }
27
+
28
+ function stitch(name, on, at) {
29
+ return {
30
+ name,
31
+ on: Boolean(on),
32
+ at: Array.isArray(at) ? at.filter((item) => typeof item === "string" && item) : [],
33
+ };
34
+ }
35
+
36
+ function readJson(file) {
37
+ try {
38
+ return JSON.parse(readFileSync(file, "utf8"));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function stationText(root) {
45
+ const file = join(root, ".xray", "state", "STATION.md");
46
+ if (!existsSync(file)) return "";
47
+ try {
48
+ return readFileSync(file, "utf8");
49
+ } catch {
50
+ return "";
51
+ }
52
+ }
53
+
54
+ function fieldValue(text, label) {
55
+ const line = String(text || "").split(/\r?\n/).find((row) => row.startsWith(`${label}: `));
56
+ return line ? line.slice(label.length + 2).trim() : "";
57
+ }
58
+
59
+ function ticketIntent(root) {
60
+ return fieldValue(stationText(root), "Intent");
61
+ }
62
+
63
+ function ticketHost(root) {
64
+ const line = fieldValue(stationText(root), "Host");
65
+ return line.split(/\s+/)[0] || "";
66
+ }
67
+
68
+ function workingFile(root) {
69
+ return readJson(join(root, ".xray", "state", "repertoire-working.json"));
70
+ }
71
+
72
+ function oneLine(value) {
73
+ return String(value || "").replace(/\s+/g, " ").trim().slice(0, 80);
74
+ }
75
+
76
+ function loopStitch(root) {
77
+ const card = ticketIntent(root);
78
+ const working = workingFile(root);
79
+ const prior = working && typeof working.priorIntent === "string" ? working.priorIntent : "";
80
+ const kept = working && typeof working.intent === "string" ? working.intent : "";
81
+ const returned = Boolean(prior && kept && card && prior === kept && card === kept);
82
+ return stitch("loop", returned, returned ? [oneLine(kept)] : []);
83
+ }
84
+
85
+ function plateBody(raw) {
86
+ return String(raw || "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
87
+ }
88
+
89
+ function domainStitch(root) {
90
+ const intent = ticketIntent(root);
91
+ if (!intent) return stitch("domain", false, []);
92
+ let plates = null;
93
+ try {
94
+ plates = require("./plates.cjs");
95
+ } catch {
96
+ return stitch("domain", false, []);
97
+ }
98
+ let plate = null;
99
+ try {
100
+ plate = plates.recallPlate(intent);
101
+ } catch {
102
+ plate = null;
103
+ }
104
+ if (!plate || !plate.id || !plate.body) return stitch("domain", false, []);
105
+ const worn = join(root, ".xray", "state", "plates", `${plate.id}.md`);
106
+ if (!existsSync(worn)) return stitch("domain", false, []);
107
+ let raw = "";
108
+ try {
109
+ raw = readFileSync(worn, "utf8");
110
+ } catch {
111
+ return stitch("domain", false, []);
112
+ }
113
+ if (plateBody(raw) !== plate.body) return stitch("domain", false, []);
114
+ return stitch("domain", true, [plate.id]);
115
+ }
116
+
117
+ function stateStitch(root) {
118
+ const card = ticketIntent(root);
119
+ const host = ticketHost(root);
120
+ const working = workingFile(root);
121
+ const priorCard = working && typeof working.priorIntent === "string" ? working.priorIntent : "";
122
+ const priorHost = working && typeof working.priorHost === "string" ? working.priorHost : "";
123
+ const priorKept = working && typeof working.priorWorkingIntent === "string" ? working.priorWorkingIntent : "";
124
+ const priorHeld = working && typeof working.priorWorkingHost === "string" ? working.priorWorkingHost : "";
125
+ const agreed = Boolean(
126
+ priorCard && priorHost && priorKept && priorHeld
127
+ && priorCard === priorKept
128
+ && priorHost === priorHeld
129
+ && card === priorCard
130
+ && host === priorHost,
131
+ );
132
+ return stitch("state", agreed, agreed ? [".xray/state/STATION.md"] : []);
133
+ }
134
+
135
+ function foundryDir(root) {
136
+ const candidates = [
137
+ join(root, "scripts", "foundry"),
138
+ join(root, "node_modules", "0xray", "scripts", "foundry"),
139
+ ];
140
+ for (const dir of candidates) {
141
+ if (FOUNDRY_FILES.every((name) => existsSync(join(dir, name)))) return dir;
142
+ }
143
+ return "";
144
+ }
145
+
146
+ function millPath(root) {
147
+ return join(root, ".xray", "state", "sleeve-mill.json");
148
+ }
149
+
150
+ function packageVersion(dir) {
151
+ let current = dir;
152
+ for (let depth = 0; depth < 6; depth += 1) {
153
+ const pkg = readJson(join(current, "package.json"));
154
+ if (pkg && typeof pkg.version === "string" && pkg.version) return pkg.version;
155
+ const parent = dirname(current);
156
+ if (parent === current) break;
157
+ current = parent;
158
+ }
159
+ return "";
160
+ }
161
+
162
+ function gitHead(root) {
163
+ try {
164
+ return execFileSync("git", ["rev-parse", "--short", "HEAD"], {
165
+ cwd: root,
166
+ encoding: "utf8",
167
+ timeout: 2000,
168
+ stdio: ["ignore", "pipe", "ignore"],
169
+ }).trim();
170
+ } catch {
171
+ return "";
172
+ }
173
+ }
174
+
175
+ function wornSkillFingerprint(root) {
176
+ const homes = [
177
+ ".opencode/skills",
178
+ ".grok/plugins/0xray/skills",
179
+ ".hermes/plugins/xray-hermes/skills",
180
+ ".openclaw/skills",
181
+ ];
182
+ const names = [];
183
+ for (const rel of homes) {
184
+ const dir = join(root, rel);
185
+ if (!existsSync(dir)) continue;
186
+ let entries = [];
187
+ try {
188
+ entries = readdirSync(dir, { withFileTypes: true });
189
+ } catch {
190
+ continue;
191
+ }
192
+ for (const entry of entries) {
193
+ if (!entry.isDirectory()) continue;
194
+ if (!existsSync(join(dir, entry.name, "SKILL.md"))) continue;
195
+ names.push(`${rel}/${entry.name}`);
196
+ }
197
+ }
198
+ names.sort();
199
+ return names.join(",");
200
+ }
201
+
202
+ function millStamp(root, dir) {
203
+ return `${packageVersion(dir)}|${gitHead(root)}|${wornSkillFingerprint(root)}`;
204
+ }
205
+
206
+ function runInspect(root, cli) {
207
+ let out = "";
208
+ try {
209
+ out = execFileSync(process.execPath, [cli, "inspect", "--skip-live"], {
210
+ cwd: root,
211
+ env: {
212
+ ...process.env,
213
+ FOUNDRY_ROOT: root,
214
+ },
215
+ encoding: "utf8",
216
+ timeout: 20000,
217
+ stdio: ["ignore", "pipe", "pipe"],
218
+ });
219
+ } catch (err) {
220
+ if (err && (err.killed || err.signal === "SIGTERM")) {
221
+ return stitch("foundry", false, ["inspect-timeout"]);
222
+ }
223
+ out = err && err.stdout ? String(err.stdout) : "";
224
+ }
225
+ try {
226
+ return inspectStitch(JSON.parse(out));
227
+ } catch {
228
+ return stitch("foundry", false, ["inspect"]);
229
+ }
230
+ }
231
+
232
+ function inspectStitch(report) {
233
+ if (report && report.ok === true) return stitch("foundry", true, ["inspect --skip-live"]);
234
+ const failed = report && Array.isArray(report.failed) ? report.failed.map(String) : [];
235
+ const blocking = failed.filter((id) => id !== "machinePlugin");
236
+ if (failed.length > 0 && blocking.length === 0) {
237
+ return stitch("foundry", true, ["inspect --skip-live"]);
238
+ }
239
+ if (!blocking.length) return stitch("foundry", false, ["inspect"]);
240
+ return stitch("foundry", false, blocking);
241
+ }
242
+
243
+ function writeMill(root, result, stamp) {
244
+ const dest = millPath(root);
245
+ mkdirSync(dirname(dest), { recursive: true });
246
+ writeFileSync(dest, `${JSON.stringify({
247
+ ok: result.on,
248
+ failed: result.on ? [] : result.at,
249
+ stamp,
250
+ ranAt: new Date().toISOString(),
251
+ })}\n`);
252
+ }
253
+
254
+ function runFoundryMill(root) {
255
+ const dir = foundryDir(root);
256
+ if (!dir) {
257
+ const missing = stitch("foundry", false, []);
258
+ writeMill(root, missing, "");
259
+ return missing;
260
+ }
261
+ const stamp = millStamp(root, dir);
262
+ const result = runInspect(root, join(dir, "cli.mjs"));
263
+ writeMill(root, result, stamp);
264
+ return result;
265
+ }
266
+
267
+ function foundryStitch(root) {
268
+ const saved = readJson(millPath(root));
269
+ if (!saved || typeof saved !== "object") return stitch("foundry", false, []);
270
+ const dir = foundryDir(root);
271
+ const stamp = dir ? millStamp(root, dir) : "";
272
+ if (!stamp || saved.stamp !== stamp) return stitch("foundry", false, saved.stamp ? ["stale"] : []);
273
+ if (saved.ok === true) return stitch("foundry", true, ["inspect --skip-live"]);
274
+ const failed = Array.isArray(saved.failed) ? saved.failed.map(String) : [];
275
+ // The wear leaves ~/.grok/plugins/0xray in place and scrubs it. That check is not the mill.
276
+ const blocking = failed.filter((id) => id !== "machinePlugin");
277
+ if (failed.length > 0 && blocking.length === 0) {
278
+ return stitch("foundry", true, ["inspect --skip-live"]);
279
+ }
280
+ return stitch("foundry", false, blocking.length ? blocking : failed);
281
+ }
282
+
283
+ function readSleeve(root) {
284
+ const stitches = [
285
+ loopStitch(root),
286
+ domainStitch(root),
287
+ stateStitch(root),
288
+ foundryStitch(root),
289
+ ];
290
+ const missing = stitches.filter((row) => !row.on).map((row) => row.name);
291
+ return { on: missing.length === 0, missing, stitches };
292
+ }
293
+
294
+ function rowsOf(sleeve) {
295
+ const listed = sleeve && Array.isArray(sleeve.stitches) ? sleeve.stitches : [];
296
+ return STITCHES.map((name) => {
297
+ const found = listed.find((row) => row && row.name === name);
298
+ return found ? stitch(name, found.on, found.at) : stitch(name, false, []);
299
+ });
300
+ }
301
+
302
+ function formatSleevePointer(sleeve) {
303
+ if (sleeve && sleeve.on) return "Sleeve: on";
304
+ const missing = sleeve && Array.isArray(sleeve.missing) && sleeve.missing.length
305
+ ? STITCHES.filter((name) => sleeve.missing.includes(name))
306
+ : rowsOf(sleeve).filter((row) => !row.on).map((row) => row.name);
307
+ const names = missing.length ? missing : STITCHES.slice();
308
+ return `Sleeve: off ${names.join(", ")}`;
309
+ }
310
+
311
+ function stitchText(row) {
312
+ if (row.at.length) return row.at.join(", ");
313
+ return row.on ? "on" : "off";
314
+ }
315
+
316
+ function labelOf(name) {
317
+ return name ? name.charAt(0).toUpperCase() + name.slice(1) : "";
318
+ }
319
+
320
+ function formatSleeveReading(sleeve) {
321
+ const lines = [formatSleevePointer(sleeve)];
322
+ for (const row of rowsOf(sleeve)) lines.push(`${labelOf(row.name)}: ${stitchText(row)}`);
323
+ return lines.join("\n");
324
+ }
325
+
326
+ module.exports = {
327
+ STITCHES,
328
+ SLEEVE_PLANES,
329
+ isSleevePlane,
330
+ readSleeve,
331
+ runFoundryMill,
332
+ inspectStitch,
333
+ formatSleevePointer,
334
+ formatSleeveReading,
335
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ESM face for sleeve.cjs.
3
+ */
4
+ import { createRequire } from "node:module";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, join } from "node:path";
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const impl = require(join(dirname(fileURLToPath(import.meta.url)), "sleeve.cjs"));
10
+
11
+ export const STITCHES = impl.STITCHES;
12
+ export const SLEEVE_PLANES = impl.SLEEVE_PLANES;
13
+ export const isSleevePlane = impl.isSleevePlane;
14
+ export const readSleeve = impl.readSleeve;
15
+ export const runFoundryMill = impl.runFoundryMill;
16
+ export const inspectStitch = impl.inspectStitch;
17
+ export const formatSleevePointer = impl.formatSleevePointer;
18
+ export const formatSleeveReading = impl.formatSleeveReading;