0xray 4.0.25 → 4.0.26

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.25",
3
+ "version": "4.0.26",
4
4
  "description": "0xRay suit for Grok, Hermes, OpenClaw, OpenCode: mill+inspect, Codex, 7 MCP servers. Costume 45/42 is opt-in foundry, not the default wear.",
5
5
  "author": {
6
6
  "name": "0xRay AI",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.25",
3
+ "version": "4.0.26",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",
package/dist/CHANGELOG.md CHANGED
@@ -41,6 +41,22 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
41
41
  - Archive root `integrations/` costume SKILL dump → `docs/archive/integrations-costume/`. Live wear is `src/integrations`.
42
42
  - Archive root `commands/` stale copy → `docs/archive/commands-copy/`. Live wear is `src/opencode/commands`.
43
43
 
44
+ ## [4.0.26] - 2026-09-25
45
+
46
+ ### 🔄 Changes
47
+
48
+ ### 🐛 Bug Fixes
49
+ - fix: store a lesson when the task matches no signal (e3c75b43d)
50
+ - fix: prove a Cursor compact cut with a session log and per-session receipt (bb3a1c68f)
51
+ - fix: mill check accepts a published version; mcp:status runs (a8bd79e23)
52
+
53
+ ### 🔎 Other Changes
54
+ - Merge pull request #116 from 0xRayAI/cursor/mill-ci-mcp-status-fbe0 (ba94508d2)
55
+ - Merge pull request #120 from 0xRayAI/cursor/lesson-recall-fbe0 (8f4a5a62a)
56
+ - Merge pull request #119 from 0xRayAI/cursor/compact-receipt-proof-fbe0 (a911fd17b)
57
+
58
+ ---
59
+
44
60
  ## [4.0.25] - 2026-09-24
45
61
 
46
62
  `0xray@4.0.25` ships the plate schematics in the package. `docs-site/docs/plates/` is on the npm `files` list. A worn suit resolves `memory-recall` and `processor` from `dist/integrations/hooks/plates.cjs`.
@@ -1,3 +1,15 @@
1
+ export type McpInstallerAction = {
2
+ kind: "list" | "status" | "help";
3
+ } | {
4
+ kind: "install" | "remove";
5
+ name?: string;
6
+ } | {
7
+ kind: "unknown";
8
+ command: string;
9
+ };
10
+ /** Colon and hyphen forms documented on `npx 0xray` map to the bare dispatcher verbs. */
11
+ export declare function normalizeMcpInstallerToken(command: string | undefined): string | undefined;
12
+ export declare function classifyMcpInstallerArgs(args: readonly string[]): McpInstallerAction;
1
13
  export declare function listMCPsCommand(): void;
2
14
  export declare function showMCPStatusCommand(): void;
3
15
  export declare function installMCPCommand(name: string): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "fs";
2
- import { join, dirname } from "path";
2
+ import { join, dirname, resolve } from "path";
3
3
  import { execSync } from "child_process";
4
+ import { fileURLToPath } from "url";
4
5
  import { getConfigDir } from "../../core/config-paths.js";
5
6
  function getMCPConfigPath() {
6
7
  return join(process.cwd(), ".xray", "mcp-registry.json");
@@ -186,9 +187,44 @@ function removeMCP(name) {
186
187
  saveInstalledMCPs(installed);
187
188
  console.log(`\n✅ Removed ${name}`);
188
189
  }
190
+ const MCP_INSTALLER_ALIASES = {
191
+ "mcp:list": "list",
192
+ "mcp-list": "list",
193
+ "mcp:status": "status",
194
+ "mcp-status": "status",
195
+ "mcp:install": "install",
196
+ "mcp-install": "install",
197
+ "mcp:remove": "remove",
198
+ "mcp-remove": "remove",
199
+ };
200
+ /** Colon and hyphen forms documented on `npx 0xray` map to the bare dispatcher verbs. */
201
+ export function normalizeMcpInstallerToken(command) {
202
+ if (command === undefined)
203
+ return undefined;
204
+ return MCP_INSTALLER_ALIASES[command] ?? command;
205
+ }
206
+ export function classifyMcpInstallerArgs(args) {
207
+ const raw = args[0];
208
+ const command = normalizeMcpInstallerToken(raw);
209
+ if (command === undefined || command === "status")
210
+ return { kind: "status" };
211
+ if (command === "list")
212
+ return { kind: "list" };
213
+ if (command === "help" || command === "--help" || command === "-h")
214
+ return { kind: "help" };
215
+ if (command === "install" || command === "remove") {
216
+ const name = args[1];
217
+ return name === undefined ? { kind: command } : { kind: command, name };
218
+ }
219
+ return { kind: "unknown", command: raw ?? command };
220
+ }
221
+ const isDirectRun = process.argv[1] !== undefined &&
222
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url);
189
223
  async function main() {
224
+ if (!isDirectRun)
225
+ return;
190
226
  const args = process.argv.slice(2);
191
- const command = args[0];
227
+ const command = normalizeMcpInstallerToken(args[0]);
192
228
  switch (command) {
193
229
  case "list":
194
230
  listMCPs();
@@ -40,7 +40,9 @@ Do **not** prove window pressure with UTF-8 chars÷4. `src/integrations/cursor/h
40
40
  | `cursor-cloud` `run-info` / `get-events` / dashboard (model, bc-id, window) | yes — token MISS is honest |
41
41
  | chars÷4, fill-only, `fillBytes` | **forbidden** (`ok: false`) |
42
42
 
43
- Host `preCompact` fire is counted from `.xray/state/cursor-hook-invoke.log` (`event=preCompact`). Do not hand-invoke `pre-compact.js` to mint that Y.
43
+ Host `preCompact` fire is counted from `.xray/state/cursor-hook-invoke.log` (`event=preCompact`). Do not hand-invoke `pre-compact.js` to mint that Y. That invoke log has no session id.
44
+
45
+ A cut counts only when `.xray/state/cursor-receipts/<sessionId>.json` exists **and** `.xray/state/cursor-hook.log` contains a line with that same session id (timestamp, `context_tokens`, `context_usage_percent`, `context_window_size`, `generation_id`). `probeLogExists` is true only when that append succeeded. `.xray/state/cursor-usage-receipt.json` and `cursor-precompact.json` keep only the newest event. That single latest json is not enough — the next arm overwrites it.
44
46
 
45
47
  Arm S landscape + receipt: `examples/killer-dual/`.
46
48
 
@@ -53,6 +53,11 @@ export {
53
53
  FORBIDDEN_USAGE_METHODS,
54
54
  invokeProbeLogPath,
55
55
  cursorUsageReceiptPath,
56
+ cursorHookLogPath,
57
+ sanitizeSessionReceiptId,
58
+ cursorSessionReceiptPath,
59
+ appendCursorCompactProofLog,
60
+ writeCursorSessionReceipt,
56
61
  parseInvokeProbeLog,
57
62
  hostUsageFromPreCompactEvent,
58
63
  classifyUsageCite,
@@ -28,6 +28,70 @@ export function cursorUsageReceiptPath(root) {
28
28
  return path.join(root, '.xray', 'state', 'cursor-usage-receipt.json');
29
29
  }
30
30
 
31
+ /** Proof log. A cut is greppable here; cursor-hook-invoke.log has no session id. */
32
+ export function cursorHookLogPath(root) {
33
+ return path.join(root, '.xray', 'state', 'cursor-hook.log');
34
+ }
35
+
36
+ function logToken(value) {
37
+ if (value == null) return '';
38
+ return String(value).replace(/[\r\n]+/g, '');
39
+ }
40
+
41
+ /**
42
+ * Safe filename segment for `.xray/state/cursor-receipts/<id>.json`.
43
+ * Empty when the id cannot be kept inside that directory.
44
+ */
45
+ export function sanitizeSessionReceiptId(sessionId) {
46
+ const raw = typeof sessionId === 'string' ? sessionId.trim() : '';
47
+ if (!raw) return '';
48
+ const safe = raw
49
+ .replace(/\0/g, '')
50
+ .replace(/[/\\]/g, '_')
51
+ .replace(/\.\./g, '_')
52
+ .replace(/[^A-Za-z0-9._-]+/g, '_')
53
+ .replace(/^[._]+/, '')
54
+ .replace(/[._]+$/g, '');
55
+ if (!safe || safe === '.' || safe === '..') return '';
56
+ return safe.slice(0, 180);
57
+ }
58
+
59
+ export function cursorSessionReceiptPath(root, sessionId) {
60
+ const safe = sanitizeSessionReceiptId(sessionId);
61
+ if (!safe) return null;
62
+ return path.join(root, '.xray', 'state', 'cursor-receipts', `${safe}.json`);
63
+ }
64
+
65
+ /**
66
+ * Append one preCompact proof line. True only when the append landed and
67
+ * the file contains that session id (grep of *.log can find the cut).
68
+ */
69
+ export function appendCursorCompactProofLog(root, fields = {}) {
70
+ const sessionId = logToken(fields.sessionId);
71
+ const timestamp = logToken(fields.timestamp) || new Date().toISOString();
72
+ const line = [
73
+ `ts=${timestamp}`,
74
+ `session_id=${sessionId}`,
75
+ `context_tokens=${logToken(fields.context_tokens)}`,
76
+ `context_usage_percent=${logToken(fields.context_usage_percent)}`,
77
+ `context_window_size=${logToken(fields.context_window_size)}`,
78
+ `generation_id=${logToken(fields.generation_id)}`,
79
+ ].join(' ');
80
+ const dest = cursorHookLogPath(root);
81
+ try {
82
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
83
+ fs.appendFileSync(dest, `${line}\n`);
84
+ } catch {
85
+ return false;
86
+ }
87
+ if (!sessionId) return false;
88
+ try {
89
+ return fs.readFileSync(dest, 'utf8').includes(`session_id=${sessionId}`);
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
31
95
  export function parseInvokeProbeLog(root) {
32
96
  const dest = invokeProbeLogPath(root);
33
97
  const counts = { preToolUse: 0, preCompact: 0, afterFileEdit: 0, unknown: 0 };
@@ -228,7 +292,7 @@ export function buildCursorUsageReceipt(root, input = {}) {
228
292
  preToolUseCount: probe.counts.preToolUse,
229
293
  afterFileEditCount: probe.counts.afterFileEdit,
230
294
  eventClass,
231
- probeLogExists: probe.exists,
295
+ probeLogExists: input.probeLogExists === true,
232
296
  },
233
297
  usage,
234
298
  repertoire,
@@ -299,3 +363,32 @@ export function writeCursorUsageReceipt(root, input = {}) {
299
363
  return null;
300
364
  }
301
365
  }
366
+
367
+ /**
368
+ * Per-session copy of the latest precompact + usage receipts.
369
+ * The next arm overwrites cursor-precompact.json and cursor-usage-receipt.json.
370
+ */
371
+ export function writeCursorSessionReceipt(root, sessionId, paths = {}) {
372
+ const dest = cursorSessionReceiptPath(root, sessionId);
373
+ if (!dest) return null;
374
+ if (!paths.precompactPath && !paths.usagePath) return null;
375
+ try {
376
+ const payload = {
377
+ sessionId: typeof sessionId === 'string' ? sessionId : null,
378
+ precompact: null,
379
+ usage: null,
380
+ };
381
+ if (paths.precompactPath && fs.existsSync(paths.precompactPath)) {
382
+ payload.precompact = JSON.parse(fs.readFileSync(paths.precompactPath, 'utf8'));
383
+ }
384
+ if (paths.usagePath && fs.existsSync(paths.usagePath)) {
385
+ payload.usage = JSON.parse(fs.readFileSync(paths.usagePath, 'utf8'));
386
+ }
387
+ if (!payload.precompact && !payload.usage) return null;
388
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
389
+ fs.writeFileSync(dest, JSON.stringify(payload, null, 2));
390
+ return dest;
391
+ } catch {
392
+ return null;
393
+ }
394
+ }
@@ -18,7 +18,9 @@ import {
18
18
  writeSessionBoot,
19
19
  } from './cursor-hook-utils.js';
20
20
  import {
21
+ appendCursorCompactProofLog,
21
22
  hostUsageFromPreCompactEvent,
23
+ writeCursorSessionReceipt,
22
24
  writeCursorUsageReceipt,
23
25
  } from './cursor-usage-receipt.js';
24
26
 
@@ -132,6 +134,17 @@ async function main() {
132
134
  });
133
135
  bootPath = writeSessionBoot(root, payload);
134
136
  stationLine = payload.stationLine;
137
+ const stampedAt = new Date().toISOString();
138
+ const proofLogged = appendCursorCompactProofLog(root, {
139
+ sessionId,
140
+ timestamp: stampedAt,
141
+ context_tokens: typeof event.context_tokens === 'number' ? event.context_tokens : null,
142
+ context_usage_percent:
143
+ typeof event.context_usage_percent === 'number' ? event.context_usage_percent : null,
144
+ context_window_size:
145
+ typeof event.context_window_size === 'number' ? event.context_window_size : null,
146
+ generation_id: generationId,
147
+ });
135
148
  receiptPath = writeCursorPrecompactReceipt(root, {
136
149
  event_class: eventClass,
137
150
  hookEvent: 'pre_compact',
@@ -145,13 +158,18 @@ async function main() {
145
158
  messages_to_compact:
146
159
  typeof event.messages_to_compact === 'number' ? event.messages_to_compact : null,
147
160
  bootPath,
148
- timestamp: new Date().toISOString(),
161
+ timestamp: stampedAt,
149
162
  });
150
163
  usagePath = writeCursorUsageReceipt(root, {
151
164
  eventClass,
152
165
  sessionId,
166
+ probeLogExists: proofLogged,
153
167
  usage: hostUsageFromPreCompactEvent(event),
154
168
  });
169
+ writeCursorSessionReceipt(root, sessionId, {
170
+ precompactPath: receiptPath,
171
+ usagePath,
172
+ });
155
173
  if (plates) {
156
174
  try {
157
175
  const named = plates.recallPlate(payload.intent || intent || '');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.25",
3
+ "version": "4.0.26",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -4,13 +4,18 @@ export interface LessonInput {
4
4
  taskId: string;
5
5
  assignedAgent: string;
6
6
  sessionId: string;
7
- /** Signals the finished task already named. When set, text matching is not used. */
7
+ /**
8
+ * Signals the finished task already named. When set, text matching is not used.
9
+ * An empty list with a lesson still stores that lesson under a name taken from the task.
10
+ */
8
11
  signals?: string[];
9
12
  /** Approaches, solutions, or the wrong turn from the sessions that named the law. */
10
13
  lesson?: string;
11
14
  }
12
15
  /**
13
16
  * A finished task names the signals its text actually matched and says
14
- * whether the outcome was good. That is the one-tenth step. No match, no write.
17
+ * whether the outcome was good. That is the one-tenth step.
18
+ * A lesson with no matched signal is stored under a hyphenated name from the
19
+ * task, so the same task text recalls it. No lesson and no match, no write.
15
20
  */
16
21
  export declare function recordLesson(input: LessonInput): string[];
@@ -1,14 +1,48 @@
1
1
  import { ensureMemoryRoutingProviderSync } from "./provider-registry.js";
2
+ /**
3
+ * Hyphenated phrase whose spaced form is still inside the task.
4
+ * A later routing read of that same task hits the name.
5
+ */
6
+ function signalNameForUnmatchedLesson(task) {
7
+ const words = task
8
+ .toLowerCase()
9
+ .split(/[^a-z0-9]+/)
10
+ .filter((word) => word.length >= 3);
11
+ const normalized = task.toLowerCase();
12
+ const kept = [];
13
+ for (const word of words) {
14
+ const next = [...kept, word];
15
+ const phrase = next.join(" ");
16
+ const hyphen = next.join("-");
17
+ if (!normalized.includes(phrase) || hyphen.length > 120)
18
+ break;
19
+ kept.push(word);
20
+ }
21
+ if (kept.length === 0)
22
+ return null;
23
+ return kept.join("-");
24
+ }
2
25
  /**
3
26
  * A finished task names the signals its text actually matched and says
4
- * whether the outcome was good. That is the one-tenth step. No match, no write.
27
+ * whether the outcome was good. That is the one-tenth step.
28
+ * A lesson with no matched signal is stored under a hyphenated name from the
29
+ * task, so the same task text recalls it. No lesson and no match, no write.
5
30
  */
6
31
  export function recordLesson(input) {
7
32
  const provider = ensureMemoryRoutingProviderSync();
8
33
  if (provider.id === "null" || !provider.ingestFeedback)
9
34
  return [];
10
- const named = input.signals ?? provider.buildRoutingContext(input.operation).matchedSignals;
11
- const names = [...new Set(named.filter((name) => name.length > 0))];
35
+ const lesson = (input.lesson ?? "").trim();
36
+ const supplied = input.signals;
37
+ const named = supplied !== undefined
38
+ ? supplied
39
+ : provider.buildRoutingContext(input.operation).matchedSignals;
40
+ let names = [...new Set(named.filter((name) => name.length > 0))];
41
+ if (names.length === 0 && lesson.length > 0) {
42
+ const coined = signalNameForUnmatchedLesson(input.operation);
43
+ if (coined)
44
+ names = [coined];
45
+ }
12
46
  if (names.length === 0)
13
47
  return [];
14
48
  provider.ingestFeedback({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.25",
3
+ "version": "4.0.26",
4
4
  "description": "Multi-agent orchestration and Codex governance for OpenCode, Hermes, Grok Build, and OpenClaw",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",
@@ -11,7 +11,9 @@
11
11
  * npx @0xray/foundry reconcile patch --apply
12
12
  *
13
13
  * --apply writes package.json and the package-lock.json version fields.
14
- * --check fails when the lock does not match package.json.
14
+ * --check passes when package.json is equal to the registry (already published)
15
+ * or greater (the pre-publish bump). It fails when package.json is behind npm,
16
+ * or when the lock does not match package.json.
15
17
  */
16
18
 
17
19
  import { execSync } from "child_process";
@@ -91,6 +93,21 @@ function fail(msg) {
91
93
  process.exit(1);
92
94
  }
93
95
 
96
+ /**
97
+ * Registry gate for --check.
98
+ * Equal to npm is a shipped release. Greater is the pre-publish bump.
99
+ * Only a package.json behind the registry fails.
100
+ * @param {string} local
101
+ * @param {string} npmVersion
102
+ * @returns {string | null}
103
+ */
104
+ export function publishedVersionCheckError(local, npmVersion) {
105
+ if (compare(local, npmVersion) < 0) {
106
+ return `package.json (${local}) must be >= npm (${npmVersion}). Run: npx @0xray/foundry reconcile patch --apply`;
107
+ }
108
+ return null;
109
+ }
110
+
94
111
  function main() {
95
112
  const local = readLocalVersion();
96
113
  const npm = readPublishedVersion();
@@ -138,9 +155,12 @@ function main() {
138
155
  return;
139
156
  }
140
157
 
141
- // Strict checks for release gate / pre-tag
142
- if (compare(local, npm) <= 0) {
143
- fail(`package.json (${local}) must be > npm (${npm}). Run: npx @0xray/foundry reconcile patch --apply`);
158
+ // Strict checks for release gate / pre-tag.
159
+ // `local` is the version read before --apply. Apply still writes
160
+ // bumpVersion(max(npm, tag)) and is not this gate.
161
+ const behindNpm = publishedVersionCheckError(local, npm);
162
+ if (behindNpm) {
163
+ fail(behindNpm);
144
164
  }
145
165
 
146
166
  const pkgVersion = readLocalVersion();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.25",
3
+ "version": "4.0.26",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.25",
3
+ "version": "4.0.26",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",