0xray 4.0.20 → 4.0.21

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 (43) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/orchestrator/SKILL.md +1 -1
  3. package/.xray/features.json +1 -1
  4. package/AGENTS-consumer.md +4 -4
  5. package/AGENTS.md +4 -4
  6. package/README.md +14 -14
  7. package/SKILLS.md +3 -3
  8. package/dist/AGENTS.md +4 -4
  9. package/dist/CHANGELOG.md +16 -8
  10. package/dist/README.md +14 -14
  11. package/dist/SKILLS.md +3 -3
  12. package/dist/cli/commands/validate.d.ts +1 -1
  13. package/dist/cli/commands/validate.js +7 -1
  14. package/dist/integrations/cursor/README.md +6 -5
  15. package/dist/integrations/cursor/hooks/after-file-edit.sh +3 -0
  16. package/dist/integrations/cursor/hooks/before-read-file.sh +3 -0
  17. package/dist/integrations/cursor/hooks/before-shell-execution.sh +3 -0
  18. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +93 -12
  19. package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +3 -0
  20. package/dist/integrations/cursor/hooks/hooks.json +13 -3
  21. package/dist/integrations/cursor/hooks/pre-compact.js +9 -0
  22. package/dist/integrations/cursor/hooks/pre-compact.sh +3 -0
  23. package/dist/integrations/cursor/hooks/pre-tool-use.js +16 -12
  24. package/dist/integrations/cursor/hooks/pre-tool-use.sh +3 -0
  25. package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +63 -0
  26. package/dist/integrations/grok/hooks/grok-hook-utils.js +3 -2
  27. package/dist/integrations/hooks/station-hook-runtime.cjs +222 -70
  28. package/dist/integrations/hooks/station-hook-runtime.mjs +9 -0
  29. package/dist/integrations/hooks/station-memory-ingest.mjs +1 -2
  30. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  31. package/dist/nucleus/suit-temperament.d.ts +0 -5
  32. package/dist/nucleus/suit-temperament.js +13 -1
  33. package/dist/skills/orchestrator/SKILL.md +1 -1
  34. package/llms.txt +1 -1
  35. package/package.json +2 -2
  36. package/scripts/foundry/README.md +2 -2
  37. package/scripts/foundry/assert-packed-dist-cli.mjs +4 -0
  38. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  39. package/scripts/foundry/validate-release-docs.mjs +44 -0
  40. package/scripts/node/install-bridges.cjs +256 -10
  41. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  42. package/src/skills/orchestrator/SKILL.md +1 -1
  43. package/xray/features.json +1 -1
@@ -84,29 +84,101 @@ function addHeatRoot(out, seen, value) {
84
84
  out.push(resolved);
85
85
  }
86
86
 
87
+ export function isCursorWorkspaceWrapper(root) {
88
+ if (!root) return false;
89
+ const resolved = path.resolve(String(root));
90
+ return (
91
+ fs.existsSync(path.join(resolved, 'repos', 'xray')) ||
92
+ fs.existsSync(path.join(resolved, 'repos', 'repertoire'))
93
+ );
94
+ }
95
+
96
+ export function heatRootHasCard(root) {
97
+ if (!root) return false;
98
+ const resolved = path.resolve(String(root));
99
+ return (
100
+ fs.existsSync(path.join(resolved, '.xray', 'state', 'STATION.md')) ||
101
+ fs.existsSync(path.join(resolved, '.xray', 'features.json'))
102
+ );
103
+ }
104
+
105
+ export function isHeatRootWritable(root) {
106
+ if (!root) return false;
107
+ const resolved = path.resolve(String(root));
108
+ try {
109
+ const probe = path.join(resolved, '.xray', 'state');
110
+ fs.mkdirSync(probe, { recursive: true });
111
+ fs.accessSync(probe, fs.constants.W_OK);
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ export function shouldHeatRoot(root) {
119
+ if (!root) return false;
120
+ const resolved = path.resolve(String(root));
121
+ if (isCursorWorkspaceWrapper(resolved)) return false;
122
+ if (heatRootHasCard(resolved)) return isHeatRootWritable(resolved);
123
+ return isHeatRootWritable(resolved);
124
+ }
125
+
126
+ export function millRootFromToolPath(filePath) {
127
+ if (!filePath) return null;
128
+ const resolved = path.resolve(String(filePath));
129
+ const marker = `${path.sep}repos${path.sep}`;
130
+ const idx = resolved.indexOf(marker);
131
+ if (idx === -1) return null;
132
+ const after = resolved.slice(idx + marker.length);
133
+ const repo = after.split(path.sep)[0];
134
+ if (!repo) return null;
135
+ return resolved.slice(0, idx + marker.length + repo.length);
136
+ }
137
+
138
+ function addMillCardsUnderWrapper(out, seen, wrapper) {
139
+ if (!wrapper || !isCursorWorkspaceWrapper(wrapper)) return;
140
+ const mill = path.join(path.resolve(String(wrapper)), 'repos', 'xray');
141
+ if (heatRootHasCard(mill) && isHeatRootWritable(mill)) addHeatRoot(out, seen, mill);
142
+ }
143
+
87
144
  /** Every live context that already has a Station card — cwd plus mill when they differ. */
88
145
  export function cursorHeatRoots(event = {}) {
89
146
  const seen = new Set();
90
147
  const out = [];
91
- addHeatRoot(out, seen, event.cwd);
92
- addHeatRoot(out, seen, event.workspaceRoot);
148
+ const candidates = [event.cwd, event.workspaceRoot];
93
149
  const listed = event.workspace_roots || event.workspaceRoots;
94
150
  if (Array.isArray(listed)) {
95
151
  for (const row of listed) {
96
- if (typeof row === 'string') addHeatRoot(out, seen, row);
97
- else if (row && typeof row === 'object' && row.path) addHeatRoot(out, seen, row.path);
152
+ if (typeof row === 'string') candidates.push(row);
153
+ else if (row && typeof row === 'object' && row.path) candidates.push(row.path);
98
154
  }
99
155
  }
100
- addHeatRoot(out, seen, process.env.CURSOR_PROJECT_DIR);
101
- const mill = process.env.XRAY_AI_PATH;
156
+ candidates.push(process.env.CURSOR_PROJECT_DIR);
157
+ for (const value of candidates) {
158
+ if (shouldHeatRoot(value)) addHeatRoot(out, seen, value);
159
+ addMillCardsUnderWrapper(out, seen, value);
160
+ }
161
+ const mill = process.env.XRAY_AI_PATH || process.env.XRAY_ROOT;
102
162
  if (mill) {
103
163
  const resolved = path.resolve(String(mill));
104
- const millHasCard =
105
- fs.existsSync(path.join(resolved, '.xray', 'state', 'STATION.md')) ||
106
- fs.existsSync(path.join(resolved, '.xray', 'features.json'));
107
- if (millHasCard) addHeatRoot(out, seen, resolved);
164
+ if (shouldHeatRoot(resolved)) addHeatRoot(out, seen, resolved);
165
+ }
166
+ const ctx = cursorToolContext(event);
167
+ for (const filePath of ctx.paths) {
168
+ const fromPath = millRootFromToolPath(filePath);
169
+ if (
170
+ fromPath &&
171
+ fs.existsSync(path.join(fromPath, '.xray', 'state', 'STATION.md')) &&
172
+ isHeatRootWritable(fromPath)
173
+ ) {
174
+ addHeatRoot(out, seen, fromPath);
175
+ }
176
+ }
177
+ if (out.length === 0) {
178
+ const fallback = cursorWorkspaceRoot(event);
179
+ if (shouldHeatRoot(fallback)) addHeatRoot(out, seen, fallback);
180
+ addMillCardsUnderWrapper(out, seen, fallback);
108
181
  }
109
- if (out.length === 0) addHeatRoot(out, seen, cursorWorkspaceRoot(event));
110
182
  return out;
111
183
  }
112
184
 
@@ -121,6 +193,10 @@ export function cursorSessionId(event = {}) {
121
193
  );
122
194
  }
123
195
 
196
+ export function cursorGenerationId(event = {}) {
197
+ return event.generation_id || event.generationId || null;
198
+ }
199
+
124
200
  export function cursorToolContext(event = {}) {
125
201
  const toolName = event.tool_name || event.toolName || process.env.TOOL_NAME || 'unknown';
126
202
  const toolInput = event.tool_input ?? event.toolInput ?? {};
@@ -179,11 +255,16 @@ export function cursorBootNeedsRefresh(existing, root) {
179
255
 
180
256
  export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-use-boot', extra = {}) {
181
257
  const bootPath = sessionBootPath(root);
258
+ if (!shouldHeatRoot(root)) return bootPath;
182
259
  if (fs.existsSync(bootPath)) {
183
260
  try {
184
261
  const existing = JSON.parse(fs.readFileSync(bootPath, 'utf8'));
185
262
  if (!cursorBootNeedsRefresh(existing, root)) {
186
- heatLiveMemory(root);
263
+ try {
264
+ heatLiveMemory(root);
265
+ } catch {
266
+ /* dest EACCES must not deny the host tool */
267
+ }
187
268
  return bootPath;
188
269
  }
189
270
  } catch {
@@ -69,6 +69,9 @@ export function hostUsageFromPreCompactEvent(event = {}) {
69
69
  if (typeof event.windowCite === 'string' && event.windowCite) usage.windowCite = event.windowCite;
70
70
  if (typeof event.model === 'string' && event.model) usage.model = event.model;
71
71
  if (typeof event.bcId === 'string' && event.bcId) usage.bcId = event.bcId;
72
+ else if (typeof event.conversation_id === 'string' && event.conversation_id) {
73
+ usage.bcId = event.conversation_id;
74
+ }
72
75
  return usage;
73
76
  }
74
77
 
@@ -3,17 +3,27 @@
3
3
  "hooks": {
4
4
  "preToolUse": [
5
5
  {
6
- "command": "XRAY_AI_PATH=\"${XRAY_AI_PATH:-node_modules/0xray}\" node \"${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/cursor/hooks/pre-tool-use.js\""
6
+ "command": ".cursor/hooks/pre-tool-use.sh"
7
7
  }
8
8
  ],
9
9
  "preCompact": [
10
10
  {
11
- "command": "XRAY_AI_PATH=\"${XRAY_AI_PATH:-node_modules/0xray}\" node \"${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/cursor/hooks/pre-compact.js\""
11
+ "command": ".cursor/hooks/pre-compact.sh"
12
12
  }
13
13
  ],
14
14
  "afterFileEdit": [
15
15
  {
16
- "command": "XRAY_AI_PATH=\"${XRAY_AI_PATH:-node_modules/0xray}\" node \"${XRAY_AI_PATH:-node_modules/0xray}/dist/integrations/cursor/hooks/after-file-edit.js\""
16
+ "command": ".cursor/hooks/after-file-edit.sh"
17
+ }
18
+ ],
19
+ "beforeShellExecution": [
20
+ {
21
+ "command": ".cursor/hooks/before-shell-execution.sh"
22
+ }
23
+ ],
24
+ "beforeReadFile": [
25
+ {
26
+ "command": ".cursor/hooks/before-read-file.sh"
17
27
  }
18
28
  ]
19
29
  }
@@ -9,6 +9,7 @@ import {
9
9
  appendHookActivity,
10
10
  buildSessionBootPayload,
11
11
  classifyPreCompactEvent,
12
+ cursorGenerationId,
12
13
  cursorHeatRoots,
13
14
  cursorSessionId,
14
15
  cursorWorkspaceRoot,
@@ -40,6 +41,7 @@ async function main() {
40
41
  eventClass = classifyPreCompactEvent(event);
41
42
  const eventRoot = cursorWorkspaceRoot(event);
42
43
  const sessionId = cursorSessionId(event);
44
+ const generationId = cursorGenerationId(event);
43
45
  const intent = extractIntent(event);
44
46
  let bootPath = null;
45
47
  let receiptPath = null;
@@ -60,6 +62,13 @@ async function main() {
60
62
  hookEvent: 'pre_compact',
61
63
  trigger: event.trigger || 'synthetic',
62
64
  sessionId,
65
+ conversation_id: sessionId,
66
+ generation_id: generationId,
67
+ is_first_compaction:
68
+ typeof event.is_first_compaction === 'boolean' ? event.is_first_compaction : null,
69
+ message_count: typeof event.message_count === 'number' ? event.message_count : null,
70
+ messages_to_compact:
71
+ typeof event.messages_to_compact === 'number' ? event.messages_to_compact : null,
63
72
  bootPath,
64
73
  timestamp: new Date().toISOString(),
65
74
  });
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ HERE="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
3
+ exec /bin/sh "${HERE}/xray-cloud-hook.sh" preCompact pre-compact.js
@@ -49,56 +49,60 @@ async function main() {
49
49
  const event = await readStdinJson();
50
50
  const eventRoot = cursorWorkspaceRoot(event);
51
51
  const sessionId = cursorSessionId(event);
52
- for (const root of cursorHeatRoots(event)) {
52
+ const heatRoots = cursorHeatRoots(event);
53
+ for (const root of heatRoots) {
53
54
  ensureCursorSessionBoot(root, '0xray/cursor-pre-tool-use-boot', { sessionId });
54
55
  }
56
+ const gateRoot = heatRoots[0] || eventRoot;
55
57
 
56
- const features = loadFeatures(eventRoot);
57
- const gateFeatures = loadDelegationGateFeatures(eventRoot, 'cursor');
58
+ const features = loadFeatures(gateRoot);
59
+ const gateFeatures = loadDelegationGateFeatures(gateRoot, 'cursor');
58
60
  const ctx = cursorToolContext(event);
59
61
  toolName = ctx.toolName;
60
62
  const { content, cmd, toolInput } = ctx;
61
63
 
62
64
  const gateBlock = evaluatePreToolGate(toolName, toolInput, {
63
- projectRoot: eventRoot,
65
+ projectRoot: gateRoot,
64
66
  sessionId,
65
67
  features: gateFeatures,
66
68
  host: 'cursor',
67
69
  });
68
70
  if (!gateBlock.allow) {
69
- finish(eventRoot, 'deny', gateBlock.reason, gateBlock.hint, toolName, { gate: gateBlock.gate });
71
+ finish(gateRoot, 'deny', gateBlock.reason, gateBlock.hint, toolName, { gate: gateBlock.gate });
70
72
  }
71
73
 
72
74
  if (isWriteTool(toolName) && content) {
73
75
  const extraBlock = checkCodexPatterns(content, { terms: [11, 29] });
74
- if (extraBlock) finish(eventRoot, 'deny', extraBlock, null, toolName);
76
+ if (extraBlock) finish(gateRoot, 'deny', extraBlock, null, toolName);
75
77
  }
76
78
 
77
79
  if (isShellTool(toolName) && cmd) {
78
80
  const testHint = checkFullTestSuite(cmd, features);
79
- if (testHint) finish(eventRoot, 'allow', null, testHint, toolName);
81
+ if (testHint) finish(gateRoot, 'allow', null, testHint, toolName);
80
82
  }
81
83
 
82
84
  if (gateBlock.reason) {
83
- finish(eventRoot, 'allow', gateBlock.reason, gateBlock.hint, toolName, {
85
+ finish(gateRoot, 'allow', gateBlock.reason, gateBlock.hint, toolName, {
84
86
  gate: gateBlock.gate,
85
87
  warn: true,
86
88
  });
87
89
  }
88
90
 
89
- finish(eventRoot, 'allow', null, null, toolName);
91
+ finish(gateRoot, 'allow', null, null, toolName);
90
92
  } catch (err) {
91
93
  appendHookActivity(fallbackRoot, 'cursor-pre-tool-use', 'hook-error', 'error', {
92
94
  tool: toolName,
93
95
  error: err.message,
96
+ failOpen: true,
94
97
  });
98
+ // Dest EACCES on a Cloud workspace wrapper must not deny every tool.
95
99
  finish(
96
100
  fallbackRoot,
97
- 'deny',
98
- `preToolUse hook error — blocked for safety: ${err.message}`,
101
+ 'allow',
102
+ `preToolUse hook error — fail open: ${err.message}`,
99
103
  null,
100
104
  toolName,
101
- { gate: 'hook-error' },
105
+ { gate: 'hook-error-fail-open' },
102
106
  );
103
107
  }
104
108
  }
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ HERE="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
3
+ exec /bin/sh "${HERE}/xray-cloud-hook.sh" preToolUse pre-tool-use.js
@@ -0,0 +1,63 @@
1
+ #!/bin/sh
2
+ # Cloud-safe Cursor hook runner. Command in hooks.json must be a relative path.
3
+ # Do not put env-assignment in hooks.json — Cloud execs argv[0] without a shell.
4
+ set -eu
5
+
6
+ EVENT="${1:-preToolUse}"
7
+ JS="${2:-pre-tool-use.js}"
8
+ MILL=""
9
+ REL=""
10
+
11
+ pick_mill() {
12
+ mill="$1"
13
+ rel="$2"
14
+ if [ -f "${mill}/${rel}" ]; then
15
+ MILL="$mill"
16
+ REL="$rel"
17
+ return 0
18
+ fi
19
+ return 1
20
+ }
21
+
22
+ if [ -n "${XRAY_AI_PATH:-}" ]; then
23
+ pick_mill "${XRAY_AI_PATH}" "src/integrations/cursor/hooks/${JS}" || \
24
+ pick_mill "${XRAY_AI_PATH}" "dist/integrations/cursor/hooks/${JS}" || true
25
+ fi
26
+ if [ -z "$MILL" ]; then
27
+ pick_mill "$(pwd)" "src/integrations/cursor/hooks/${JS}" || \
28
+ pick_mill "$(pwd)/../xray" "src/integrations/cursor/hooks/${JS}" || \
29
+ pick_mill "$(pwd)/repos/xray" "src/integrations/cursor/hooks/${JS}" || \
30
+ pick_mill "${CURSOR_PROJECT_DIR:-}/repos/xray" "src/integrations/cursor/hooks/${JS}" || \
31
+ pick_mill "$(pwd)/node_modules/0xray" "dist/integrations/cursor/hooks/${JS}" || \
32
+ pick_mill "$(pwd)/node_modules/0xray" "src/integrations/cursor/hooks/${JS}" || true
33
+ fi
34
+
35
+ if [ -n "$MILL" ] && [ -d "$MILL" ]; then
36
+ MILL="$(cd "$MILL" && pwd)"
37
+ fi
38
+
39
+ NODE_BIN="$(command -v node 2>/dev/null || true)"
40
+
41
+ log_invoke() {
42
+ dest="$1"
43
+ mkdir -p "${dest}/.xray/state" 2>/dev/null || true
44
+ {
45
+ printf 'ts=%s event=%s cwd=%s mill=%s node=%s\n' \
46
+ "$(date -Iseconds)" "$EVENT" "$(pwd)" "${MILL:-MISSING}" "${NODE_BIN:-MISSING}"
47
+ } >> "${dest}/.xray/state/cursor-hook-invoke.log" 2>/dev/null || true
48
+ }
49
+
50
+ log_invoke "$(pwd)"
51
+ if [ -n "$MILL" ]; then
52
+ log_invoke "$MILL"
53
+ fi
54
+
55
+ if [ -z "$NODE_BIN" ] || [ -z "$MILL" ] || [ ! -f "${MILL}/${REL}" ]; then
56
+ printf '{"permission":"allow","user_message":"xray-cloud-hook: mill missing — fail open"}\n'
57
+ exit 0
58
+ fi
59
+
60
+ export XRAY_HOOK_EVENT="$EVENT"
61
+ export XRAY_AI_PATH="$MILL"
62
+ export XRAY_ROOT="$MILL"
63
+ exec "$NODE_BIN" "${MILL}/${REL}"
@@ -25,6 +25,7 @@ import {
25
25
  applyStationHeat,
26
26
  buildRepertoireResume as stationRepertoireResume,
27
27
  readExistingBoot,
28
+ retainCompactFields,
28
29
  writeStationMarkdown,
29
30
  } from '../../hooks/station-hook-runtime.mjs';
30
31
 
@@ -285,8 +286,8 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
285
286
  const frontier = gateFeatures.ceremony === 'lite';
286
287
  const existing = readExistingBoot(root);
287
288
  const heat = applyStationHeat(root, extra.host || 'grok', extra, existing);
289
+ const compactHold = retainCompactFields(existing, extra);
288
290
  return {
289
- hook: source,
290
291
  lead_dev_mode: features.lead_dev_mode,
291
292
  no_new_surface: features.no_new_surface,
292
293
  host: extra.host || 'grok',
@@ -306,9 +307,9 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
306
307
  ...(conferPending ? { conferPending: true, conferTrigger: 'analyze-complexity at synthesis checkpoint' } : {}),
307
308
  ...(userAsideBoot ?? {}),
308
309
  sessionId,
309
- source,
310
310
  ...extra,
311
311
  ...heat,
312
+ ...compactHold,
312
313
  hook: source,
313
314
  source,
314
315
  timestamp: new Date().toISOString(),