0xray 4.0.12 → 4.0.14
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/.grok-plugin/plugin.json +1 -1
- package/.xray/features.json +1 -1
- package/.xray/features.schema.json +1 -1
- package/AGENTS-consumer.md +4 -2
- package/AGENTS.md +8 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +8 -1
- package/dist/CHANGELOG.md +25 -1
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/integrations/cursor/README.md +61 -0
- package/dist/integrations/cursor/hooks/after-file-edit.js +38 -0
- package/dist/integrations/cursor/hooks/cursor-hook-utils.js +170 -0
- package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +298 -0
- package/dist/integrations/cursor/hooks/hooks.json +20 -0
- package/dist/integrations/cursor/hooks/pre-compact.js +88 -0
- package/dist/integrations/cursor/hooks/pre-tool-use.js +105 -0
- package/dist/integrations/hooks/station-hook-runtime.cjs +89 -1
- package/dist/integrations/hooks/station-hook-runtime.mjs +2 -0
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/nucleus/suit-temperament.d.ts +2 -2
- package/dist/nucleus/suit-temperament.js +3 -1
- package/llms.txt +6 -4
- package/package.json +4 -2
- package/scripts/foundry/CHANGELOG.md +37 -1
- package/scripts/foundry/README.md +13 -1
- package/scripts/foundry/assert-packed-dist-cli.mjs +97 -0
- package/scripts/foundry/blip-render.cjs +1237 -0
- package/scripts/foundry/blip-rippel.cjs +1963 -0
- package/scripts/foundry/blip.mjs +109 -0
- package/scripts/foundry/cli.mjs +8 -4
- package/scripts/foundry/inspect.mjs +117 -7
- package/scripts/foundry/mint-suit.cjs +215 -14
- package/scripts/foundry/package.json +7 -1
- package/scripts/foundry/plant/agents/blip-inspect.yml +5 -0
- package/scripts/foundry/plant/agents/blip.yml +5 -0
- package/scripts/foundry/plant/agents/inspect.yml +1 -1
- package/scripts/foundry/plant/agents/mill.yml +1 -1
- package/scripts/foundry/plant/agents/sound-inspect.yml +5 -0
- package/scripts/foundry/plant/agents/sound.yml +5 -0
- package/scripts/foundry/plant/motions/MOTION-REGISTRY.md +35 -0
- package/scripts/foundry/plant/motions/registry.json +81 -0
- package/scripts/foundry/plant/skills/blip/SKILL.md +39 -0
- package/scripts/foundry/plant/skills/blip-inspect/SKILL.md +32 -0
- package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -2
- package/scripts/foundry/plant/skills/sound/SKILL.md +29 -0
- package/scripts/foundry/plant/skills/sound-inspect/SKILL.md +28 -0
- package/scripts/foundry/release-gate.mjs +18 -10
- package/scripts/foundry/release.mjs +3 -0
- package/scripts/foundry/sound-bed.cjs +454 -0
- package/scripts/foundry/sound-rippel.cjs +1434 -0
- package/scripts/foundry/sound.mjs +93 -0
- package/scripts/node/assert-packed-dist-cli.mjs +4 -0
- package/scripts/node/consumer-install-smoke.mjs +20 -1
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/xray/features.json +1 -1
- package/xray/features.schema.json +1 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Cloud real-usage receipt — no delegation-gate / dist required.
|
|
3
|
+
* FILL / chars÷4 is never usage proof. Host fields or Cloud MCP / dashboard only.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { buildRepertoireResume } from '../../hooks/station-hook-runtime.mjs';
|
|
9
|
+
|
|
10
|
+
export const EVENT_CLASS_HOST = 'cursor-host-precompact';
|
|
11
|
+
export const EVENT_CLASS_FAIL = 'cursor-host-precompact-FAIL';
|
|
12
|
+
|
|
13
|
+
export const FORBIDDEN_USAGE_METHODS = new Set([
|
|
14
|
+
'chars-div-4',
|
|
15
|
+
'chars/4',
|
|
16
|
+
'chars÷4',
|
|
17
|
+
'fill-only',
|
|
18
|
+
'fill',
|
|
19
|
+
'utf8-bytes-div-4',
|
|
20
|
+
'bytes-div-4',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export function invokeProbeLogPath(root) {
|
|
24
|
+
return path.join(root, '.xray', 'state', 'cursor-hook-invoke.log');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function cursorUsageReceiptPath(root) {
|
|
28
|
+
return path.join(root, '.xray', 'state', 'cursor-usage-receipt.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseInvokeProbeLog(root) {
|
|
32
|
+
const dest = invokeProbeLogPath(root);
|
|
33
|
+
const counts = { preToolUse: 0, preCompact: 0, afterFileEdit: 0, unknown: 0 };
|
|
34
|
+
if (!fs.existsSync(dest)) {
|
|
35
|
+
return { exists: false, path: dest, counts, hostPreCompactFired: false };
|
|
36
|
+
}
|
|
37
|
+
let text = '';
|
|
38
|
+
try {
|
|
39
|
+
text = fs.readFileSync(dest, 'utf8');
|
|
40
|
+
} catch {
|
|
41
|
+
return { exists: false, path: dest, counts, hostPreCompactFired: false };
|
|
42
|
+
}
|
|
43
|
+
for (const line of text.split(/\r?\n/)) {
|
|
44
|
+
if (!line.trim()) continue;
|
|
45
|
+
const match = /(?:^|\s)event=([A-Za-z]+)/.exec(line);
|
|
46
|
+
const event = match ? match[1] : 'unknown';
|
|
47
|
+
if (event === 'preToolUse') counts.preToolUse += 1;
|
|
48
|
+
else if (event === 'preCompact') counts.preCompact += 1;
|
|
49
|
+
else if (event === 'afterFileEdit') counts.afterFileEdit += 1;
|
|
50
|
+
else counts.unknown += 1;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
exists: true,
|
|
54
|
+
path: dest,
|
|
55
|
+
counts,
|
|
56
|
+
hostPreCompactFired: counts.preCompact > 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function hostUsageFromPreCompactEvent(event = {}) {
|
|
61
|
+
const usage = { source: 'precompact-stdin', method: 'host-field' };
|
|
62
|
+
if (typeof event.context_tokens === 'number') usage.context_tokens = event.context_tokens;
|
|
63
|
+
if (typeof event.context_usage_percent === 'number') {
|
|
64
|
+
usage.context_usage_percent = event.context_usage_percent;
|
|
65
|
+
}
|
|
66
|
+
if (typeof event.context_window_size === 'number') {
|
|
67
|
+
usage.context_window_size = event.context_window_size;
|
|
68
|
+
}
|
|
69
|
+
if (typeof event.windowCite === 'string' && event.windowCite) usage.windowCite = event.windowCite;
|
|
70
|
+
if (typeof event.model === 'string' && event.model) usage.model = event.model;
|
|
71
|
+
if (typeof event.bcId === 'string' && event.bcId) usage.bcId = event.bcId;
|
|
72
|
+
return usage;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function looksLikeFillCite(cite = {}) {
|
|
76
|
+
const method = String(cite.method || '').trim().toLowerCase();
|
|
77
|
+
if (FORBIDDEN_USAGE_METHODS.has(method)) return true;
|
|
78
|
+
if (cite.fillBytes != null || cite.charsDiv4 != null || cite.fillOnly === true) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasHostTokenFields(cite = {}) {
|
|
83
|
+
return (
|
|
84
|
+
typeof cite.context_tokens === 'number' ||
|
|
85
|
+
typeof cite.context_usage_percent === 'number' ||
|
|
86
|
+
typeof cite.context_window_size === 'number'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasMcpIdentity(cite = {}) {
|
|
91
|
+
const source = String(cite.source || '').trim();
|
|
92
|
+
if (source === 'cursor-cloud-run-info' || source === 'cursor-cloud-events' || source === 'dashboard') {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return Boolean(cite.model || cite.bcId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function classifyUsageCite(cite = {}) {
|
|
99
|
+
const method = String(cite.method || '').trim().toLowerCase();
|
|
100
|
+
const source = String(cite.source || '').trim() || null;
|
|
101
|
+
const windowCite = typeof cite.windowCite === 'string' && cite.windowCite ? cite.windowCite : null;
|
|
102
|
+
const model = typeof cite.model === 'string' && cite.model ? cite.model : null;
|
|
103
|
+
const bcId = typeof cite.bcId === 'string' && cite.bcId ? cite.bcId : null;
|
|
104
|
+
const tokens = typeof cite.context_tokens === 'number' ? cite.context_tokens : null;
|
|
105
|
+
const percent = typeof cite.context_usage_percent === 'number' ? cite.context_usage_percent : null;
|
|
106
|
+
const windowSize = typeof cite.context_window_size === 'number' ? cite.context_window_size : null;
|
|
107
|
+
|
|
108
|
+
if (looksLikeFillCite(cite)) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
forbidden: true,
|
|
112
|
+
miss: false,
|
|
113
|
+
reason: 'FILL / chars÷4 is forbidden as usage proof',
|
|
114
|
+
method: method || 'fill',
|
|
115
|
+
source,
|
|
116
|
+
context_tokens: null,
|
|
117
|
+
context_usage_percent: null,
|
|
118
|
+
context_window_size: null,
|
|
119
|
+
windowCite,
|
|
120
|
+
model,
|
|
121
|
+
bcId,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (hasHostTokenFields(cite)) {
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
forbidden: false,
|
|
129
|
+
miss: false,
|
|
130
|
+
reason: null,
|
|
131
|
+
method: method || 'host-field',
|
|
132
|
+
source: source || 'precompact-stdin',
|
|
133
|
+
context_tokens: tokens,
|
|
134
|
+
context_usage_percent: percent,
|
|
135
|
+
context_window_size: windowSize,
|
|
136
|
+
windowCite,
|
|
137
|
+
model,
|
|
138
|
+
bcId,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (hasMcpIdentity(cite)) {
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
forbidden: false,
|
|
146
|
+
miss: true,
|
|
147
|
+
reason: 'MCP/dashboard identity present; token counts MISS (do not fill)',
|
|
148
|
+
method: method || source || 'mcp',
|
|
149
|
+
source,
|
|
150
|
+
context_tokens: null,
|
|
151
|
+
context_usage_percent: null,
|
|
152
|
+
context_window_size: null,
|
|
153
|
+
windowCite,
|
|
154
|
+
model,
|
|
155
|
+
bcId,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
forbidden: false,
|
|
162
|
+
miss: true,
|
|
163
|
+
reason: 'no host/MCP usage fields; do not fill',
|
|
164
|
+
method: method || null,
|
|
165
|
+
source,
|
|
166
|
+
context_tokens: null,
|
|
167
|
+
context_usage_percent: null,
|
|
168
|
+
context_window_size: null,
|
|
169
|
+
windowCite,
|
|
170
|
+
model,
|
|
171
|
+
bcId,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function buildRepertoireFastenedProof(root) {
|
|
176
|
+
const resume = buildRepertoireResume(root);
|
|
177
|
+
const npmPath = path.join(root, 'node_modules', '@0xray', 'repertoire');
|
|
178
|
+
const vendorPath = path.join(root, 'vendor', '@0xray', 'repertoire');
|
|
179
|
+
const pkgFile = fs.existsSync(path.join(npmPath, 'package.json'))
|
|
180
|
+
? path.join(npmPath, 'package.json')
|
|
181
|
+
: path.join(vendorPath, 'package.json');
|
|
182
|
+
let name = null;
|
|
183
|
+
let version = null;
|
|
184
|
+
try {
|
|
185
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
|
|
186
|
+
name = pkg.name || null;
|
|
187
|
+
version = pkg.version || null;
|
|
188
|
+
} catch {
|
|
189
|
+
/* organ absent */
|
|
190
|
+
}
|
|
191
|
+
const providerExists = fs.existsSync(
|
|
192
|
+
path.join(npmPath, 'dist', 'provider', 'memory-routing-provider.js'),
|
|
193
|
+
);
|
|
194
|
+
const signalsExists = fs.existsSync(path.join(npmPath, 'data', 'curated_signals.json'));
|
|
195
|
+
return {
|
|
196
|
+
fastened: name === '@0xray/repertoire' && providerExists,
|
|
197
|
+
resume,
|
|
198
|
+
name,
|
|
199
|
+
version,
|
|
200
|
+
npmPath: fs.existsSync(npmPath) ? npmPath : null,
|
|
201
|
+
vendorPath: fs.existsSync(vendorPath) ? vendorPath : null,
|
|
202
|
+
providerExists,
|
|
203
|
+
signalsExists,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function buildCursorUsageReceipt(root, input = {}) {
|
|
208
|
+
const probe = parseInvokeProbeLog(root);
|
|
209
|
+
const usage = classifyUsageCite(input.usage || {});
|
|
210
|
+
const repertoire = buildRepertoireFastenedProof(root);
|
|
211
|
+
const eventClass =
|
|
212
|
+
typeof input.eventClass === 'string'
|
|
213
|
+
? input.eventClass
|
|
214
|
+
: probe.hostPreCompactFired
|
|
215
|
+
? EVENT_CLASS_HOST
|
|
216
|
+
: EVENT_CLASS_FAIL;
|
|
217
|
+
return {
|
|
218
|
+
ok: usage.ok && !usage.forbidden,
|
|
219
|
+
protocol: 'real-work-ab',
|
|
220
|
+
timestamp: new Date().toISOString(),
|
|
221
|
+
sessionId: input.sessionId || null,
|
|
222
|
+
compact: {
|
|
223
|
+
hostFired: probe.hostPreCompactFired,
|
|
224
|
+
preCompactCount: probe.counts.preCompact,
|
|
225
|
+
preToolUseCount: probe.counts.preToolUse,
|
|
226
|
+
afterFileEditCount: probe.counts.afterFileEdit,
|
|
227
|
+
eventClass,
|
|
228
|
+
probeLogExists: probe.exists,
|
|
229
|
+
},
|
|
230
|
+
usage,
|
|
231
|
+
repertoire,
|
|
232
|
+
hooksAtBoot: input.hooksAtBoot === true || input.hooksAtBoot === false ? input.hooksAtBoot : null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function upsertStationKeyedLine(card, key, value) {
|
|
237
|
+
const line = `${key}: ${value}`;
|
|
238
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
239
|
+
const re = new RegExp(`^${escaped}:.*$`, 'm');
|
|
240
|
+
if (re.test(card)) return card.replace(re, line);
|
|
241
|
+
const marker = 'Continue this card.';
|
|
242
|
+
const idx = card.lastIndexOf(marker);
|
|
243
|
+
if (idx >= 0) return `${card.slice(0, idx)}${line}\n\n${card.slice(idx)}`;
|
|
244
|
+
return `${card.trimEnd()}\n${line}\n`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Host numeric window first. Ticket `windowCite` is not a substitute. Do not derive from tokens÷percent. */
|
|
248
|
+
export function stationUsageWindowLabel(usage = {}) {
|
|
249
|
+
if (typeof usage.context_window_size === 'number') return String(usage.context_window_size);
|
|
250
|
+
if (typeof usage.windowCite === 'string' && usage.windowCite.trim()) return usage.windowCite.trim();
|
|
251
|
+
return 'MISS';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function upsertStationCompactRow(root, receipt) {
|
|
255
|
+
const dest = path.join(root, '.xray', 'state', 'STATION.md');
|
|
256
|
+
if (!fs.existsSync(dest)) return null;
|
|
257
|
+
try {
|
|
258
|
+
const fired = receipt.compact?.hostFired ? 'Y' : 'N';
|
|
259
|
+
const count = receipt.compact?.preCompactCount ?? 0;
|
|
260
|
+
const usageBit = receipt.usage?.forbidden
|
|
261
|
+
? 'FORBIDDEN-FILL'
|
|
262
|
+
: receipt.usage?.miss
|
|
263
|
+
? 'MISS'
|
|
264
|
+
: 'host-field';
|
|
265
|
+
const tokens =
|
|
266
|
+
typeof receipt.usage?.context_tokens === 'number' ? String(receipt.usage.context_tokens) : 'MISS';
|
|
267
|
+
const heat = receipt.repertoire?.fastened ? 'fastened' : 'absent';
|
|
268
|
+
const windowLabel = stationUsageWindowLabel(receipt.usage || {});
|
|
269
|
+
let card = fs.readFileSync(dest, 'utf8');
|
|
270
|
+
card = upsertStationKeyedLine(
|
|
271
|
+
card,
|
|
272
|
+
'Compact',
|
|
273
|
+
`preCompact ${fired} (count=${count}) · usage ${usageBit} · repertoire ${heat}`,
|
|
274
|
+
);
|
|
275
|
+
card = upsertStationKeyedLine(
|
|
276
|
+
card,
|
|
277
|
+
'Usage',
|
|
278
|
+
`source=${receipt.usage?.source || 'none'} model=${receipt.usage?.model || 'MISS'} window=${windowLabel} tokens=${tokens}`,
|
|
279
|
+
);
|
|
280
|
+
fs.writeFileSync(dest, card);
|
|
281
|
+
return dest;
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function writeCursorUsageReceipt(root, input = {}) {
|
|
288
|
+
try {
|
|
289
|
+
const receipt = buildCursorUsageReceipt(root, input);
|
|
290
|
+
const dest = cursorUsageReceiptPath(root);
|
|
291
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
292
|
+
fs.writeFileSync(dest, JSON.stringify(receipt, null, 2));
|
|
293
|
+
upsertStationCompactRow(root, receipt);
|
|
294
|
+
return dest;
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"preToolUse": [
|
|
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\""
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"preCompact": [
|
|
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\""
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"afterFileEdit": [
|
|
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\""
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cursor preCompact — Station writer. Observational; cannot block compaction.
|
|
4
|
+
* Host fire → event_class: cursor-host-precompact
|
|
5
|
+
* Manual/script invoke → pass --event-class=cursor-precompact-synthetic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
appendHookActivity,
|
|
10
|
+
buildSessionBootPayload,
|
|
11
|
+
classifyPreCompactEvent,
|
|
12
|
+
cursorSessionId,
|
|
13
|
+
cursorWorkspaceRoot,
|
|
14
|
+
readStdinJson,
|
|
15
|
+
writeCursorPrecompactReceipt,
|
|
16
|
+
writeSessionBoot,
|
|
17
|
+
} from './cursor-hook-utils.js';
|
|
18
|
+
import {
|
|
19
|
+
hostUsageFromPreCompactEvent,
|
|
20
|
+
writeCursorUsageReceipt,
|
|
21
|
+
} from './cursor-usage-receipt.js';
|
|
22
|
+
|
|
23
|
+
function extractIntent(event) {
|
|
24
|
+
return (
|
|
25
|
+
event.prompt ||
|
|
26
|
+
event.userMessage ||
|
|
27
|
+
event.user_prompt ||
|
|
28
|
+
event.compactContext ||
|
|
29
|
+
event.user_message ||
|
|
30
|
+
null
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
const fallbackRoot = cursorWorkspaceRoot();
|
|
36
|
+
let eventClass = 'cursor-precompact-synthetic';
|
|
37
|
+
try {
|
|
38
|
+
const event = await readStdinJson();
|
|
39
|
+
eventClass = classifyPreCompactEvent(event);
|
|
40
|
+
const eventRoot = cursorWorkspaceRoot(event);
|
|
41
|
+
const sessionId = cursorSessionId(event);
|
|
42
|
+
const intent = extractIntent(event);
|
|
43
|
+
const payload = buildSessionBootPayload(eventRoot, '0xray/cursor-compact', {
|
|
44
|
+
host: 'cursor',
|
|
45
|
+
hookEvent: 'pre_compact',
|
|
46
|
+
event_class: eventClass,
|
|
47
|
+
sessionId,
|
|
48
|
+
...(intent ? { intent } : {}),
|
|
49
|
+
});
|
|
50
|
+
const bootPath = writeSessionBoot(eventRoot, payload);
|
|
51
|
+
const receiptPath = writeCursorPrecompactReceipt(eventRoot, {
|
|
52
|
+
event_class: eventClass,
|
|
53
|
+
hookEvent: 'pre_compact',
|
|
54
|
+
trigger: event.trigger || 'synthetic',
|
|
55
|
+
sessionId,
|
|
56
|
+
bootPath,
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
});
|
|
59
|
+
const usagePath = writeCursorUsageReceipt(eventRoot, {
|
|
60
|
+
eventClass,
|
|
61
|
+
sessionId,
|
|
62
|
+
usage: hostUsageFromPreCompactEvent(event),
|
|
63
|
+
});
|
|
64
|
+
appendHookActivity(eventRoot, 'cursor-pre-compact', 'station-written', 'success', {
|
|
65
|
+
bootPath,
|
|
66
|
+
receiptPath,
|
|
67
|
+
usagePath,
|
|
68
|
+
event_class: eventClass,
|
|
69
|
+
stationLine: payload.stationLine,
|
|
70
|
+
});
|
|
71
|
+
console.log(
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
user_message:
|
|
74
|
+
`0xRay Station written. Read .xray/state/STATION.md — do not cold-start. event_class=${eventClass}`,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
appendHookActivity(fallbackRoot, 'cursor-pre-compact', 'station-error', 'error', {
|
|
80
|
+
error: err.message,
|
|
81
|
+
event_class: eventClass,
|
|
82
|
+
});
|
|
83
|
+
console.log(JSON.stringify({ user_message: '0xRay Station write failed; Read STATION.md if present.' }));
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
main();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cursor preToolUse — 0xRay constitution gate.
|
|
4
|
+
* Contract: stdin JSON → stdout {"permission":"allow"|"deny"}
|
|
5
|
+
* Does not rely on sessionStart (unavailable on managed cloud).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
appendHookActivity,
|
|
10
|
+
checkCodexPatterns,
|
|
11
|
+
checkFullTestSuite,
|
|
12
|
+
cursorSessionId,
|
|
13
|
+
cursorToolContext,
|
|
14
|
+
cursorWorkspaceRoot,
|
|
15
|
+
ensureCursorSessionBoot,
|
|
16
|
+
evaluatePreToolGate,
|
|
17
|
+
isShellTool,
|
|
18
|
+
isWriteTool,
|
|
19
|
+
loadDelegationGateFeatures,
|
|
20
|
+
loadFeatures,
|
|
21
|
+
readStdinJson,
|
|
22
|
+
} from './cursor-hook-utils.js';
|
|
23
|
+
|
|
24
|
+
function finish(root, permission, reason, hint, toolName, extra = {}) {
|
|
25
|
+
const out = { permission };
|
|
26
|
+
if (reason) {
|
|
27
|
+
out.user_message = reason;
|
|
28
|
+
out.agent_message = reason;
|
|
29
|
+
}
|
|
30
|
+
if (hint && permission === 'allow') {
|
|
31
|
+
out.user_message = hint;
|
|
32
|
+
}
|
|
33
|
+
console.log(JSON.stringify(out));
|
|
34
|
+
appendHookActivity(root, 'cursor-pre-tool-use', permission, permission === 'deny' ? 'error' : 'info', {
|
|
35
|
+
tool: toolName,
|
|
36
|
+
reason: reason || hint || null,
|
|
37
|
+
gate: extra.gate || null,
|
|
38
|
+
livePath: true,
|
|
39
|
+
});
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function main() {
|
|
44
|
+
const fallbackRoot = cursorWorkspaceRoot();
|
|
45
|
+
let toolName = 'unknown';
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const event = await readStdinJson();
|
|
49
|
+
const eventRoot = cursorWorkspaceRoot(event);
|
|
50
|
+
ensureCursorSessionBoot(eventRoot, '0xray/cursor-pre-tool-use-boot', {
|
|
51
|
+
sessionId: cursorSessionId(event),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const features = loadFeatures(eventRoot);
|
|
55
|
+
const gateFeatures = loadDelegationGateFeatures(eventRoot, 'cursor');
|
|
56
|
+
const ctx = cursorToolContext(event);
|
|
57
|
+
toolName = ctx.toolName;
|
|
58
|
+
const { content, cmd, toolInput } = ctx;
|
|
59
|
+
const sessionId = cursorSessionId(event);
|
|
60
|
+
|
|
61
|
+
const gateBlock = evaluatePreToolGate(toolName, toolInput, {
|
|
62
|
+
projectRoot: eventRoot,
|
|
63
|
+
sessionId,
|
|
64
|
+
features: gateFeatures,
|
|
65
|
+
host: 'cursor',
|
|
66
|
+
});
|
|
67
|
+
if (!gateBlock.allow) {
|
|
68
|
+
finish(eventRoot, 'deny', gateBlock.reason, gateBlock.hint, toolName, { gate: gateBlock.gate });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isWriteTool(toolName) && content) {
|
|
72
|
+
const extraBlock = checkCodexPatterns(content, { terms: [11, 29] });
|
|
73
|
+
if (extraBlock) finish(eventRoot, 'deny', extraBlock, null, toolName);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isShellTool(toolName) && cmd) {
|
|
77
|
+
const testHint = checkFullTestSuite(cmd, features);
|
|
78
|
+
if (testHint) finish(eventRoot, 'allow', null, testHint, toolName);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (gateBlock.reason) {
|
|
82
|
+
finish(eventRoot, 'allow', gateBlock.reason, gateBlock.hint, toolName, {
|
|
83
|
+
gate: gateBlock.gate,
|
|
84
|
+
warn: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
finish(eventRoot, 'allow', null, null, toolName);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
appendHookActivity(fallbackRoot, 'cursor-pre-tool-use', 'hook-error', 'error', {
|
|
91
|
+
tool: toolName,
|
|
92
|
+
error: err.message,
|
|
93
|
+
});
|
|
94
|
+
finish(
|
|
95
|
+
fallbackRoot,
|
|
96
|
+
'deny',
|
|
97
|
+
`preToolUse hook error — blocked for safety: ${err.message}`,
|
|
98
|
+
null,
|
|
99
|
+
toolName,
|
|
100
|
+
{ gate: 'hook-error' },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Station heat — compaction / host-swap card.
|
|
3
3
|
* SSOT remains session-boot.json. STATION.md is the projection the model Reads.
|
|
4
4
|
* Grok ignores SessionStart/UserPromptSubmit stdout; disk + AGENTS.md is the contract.
|
|
5
|
+
* Heat writers merge stock fields; unknown keys and ## Durable / ## Seed survive.
|
|
5
6
|
*/
|
|
6
7
|
const { execFileSync } = require("child_process");
|
|
7
8
|
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require("fs");
|
|
@@ -363,6 +364,80 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
363
364
|
};
|
|
364
365
|
}
|
|
365
366
|
|
|
367
|
+
const STOCK_STATION_PREFIXES = [
|
|
368
|
+
"hot-swap:",
|
|
369
|
+
"host:",
|
|
370
|
+
"intent:",
|
|
371
|
+
"plan:",
|
|
372
|
+
"git:",
|
|
373
|
+
"repertoire:",
|
|
374
|
+
"working:",
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
const STOCK_STATION_FOOTERS = [
|
|
378
|
+
"continue this card. compaction and host change are the same cut. do not cold-start.",
|
|
379
|
+
"grok does not inject this file — read it. opencode injects. do not thicken the grok exo.",
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
function isDurableStationHeading(line) {
|
|
383
|
+
return /^##\s+(durable|seed)\b/i.test(String(line || "").trim());
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function isStockStationLine(line) {
|
|
387
|
+
const trimmed = String(line || "").trim();
|
|
388
|
+
if (!trimmed) return true;
|
|
389
|
+
if (/^#\s+station\s*$/i.test(trimmed)) return true;
|
|
390
|
+
const lower = trimmed.toLowerCase();
|
|
391
|
+
if (STOCK_STATION_FOOTERS.includes(lower)) return true;
|
|
392
|
+
return STOCK_STATION_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Keep unknown keys and ## Durable / ## Seed blocks across heat rewrites. */
|
|
396
|
+
function extractPreservedStationLines(existing) {
|
|
397
|
+
if (!existing || typeof existing !== "string") return [];
|
|
398
|
+
const preserved = [];
|
|
399
|
+
let inDurable = false;
|
|
400
|
+
for (const line of existing.split(/\r?\n/)) {
|
|
401
|
+
if (isDurableStationHeading(line)) {
|
|
402
|
+
inDurable = true;
|
|
403
|
+
preserved.push(line);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (inDurable) {
|
|
407
|
+
const trimmed = String(line || "").trim();
|
|
408
|
+
const otherHeading = /^##\s+\S/.test(trimmed) && !isDurableStationHeading(line);
|
|
409
|
+
// Stock prefixes, blank lines, and stock footers close the section.
|
|
410
|
+
// Otherwise Seed swallows Compact/Usage/footers and each heat pastes another footer.
|
|
411
|
+
if (otherHeading || isStockStationLine(line)) {
|
|
412
|
+
inDurable = false;
|
|
413
|
+
} else {
|
|
414
|
+
preserved.push(line);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (isStockStationLine(line)) continue;
|
|
419
|
+
preserved.push(line);
|
|
420
|
+
}
|
|
421
|
+
while (preserved.length && !preserved[0].trim()) preserved.shift();
|
|
422
|
+
while (preserved.length && !preserved[preserved.length - 1].trim()) preserved.pop();
|
|
423
|
+
return preserved;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function mergeStationMarkdown(stockMd, existingMd) {
|
|
427
|
+
const preserved = extractPreservedStationLines(existingMd);
|
|
428
|
+
if (!preserved.length) return stockMd;
|
|
429
|
+
const stock = String(stockMd || "");
|
|
430
|
+
const block = preserved.join("\n");
|
|
431
|
+
const marker = "Continue this card.";
|
|
432
|
+
const idx = stock.indexOf(marker);
|
|
433
|
+
if (idx === -1) {
|
|
434
|
+
return `${stock.replace(/\s*$/, "")}\n\n${block}\n`;
|
|
435
|
+
}
|
|
436
|
+
const head = stock.slice(0, idx).replace(/\s*$/, "");
|
|
437
|
+
const foot = stock.slice(idx);
|
|
438
|
+
return `${head}\n\n${block}\n\n${foot}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
366
441
|
function formatStationMarkdown(fields) {
|
|
367
442
|
const host = fields.host || "unknown";
|
|
368
443
|
const profile = fields.suit_profile || "guided";
|
|
@@ -389,12 +464,23 @@ function formatStationMarkdown(fields) {
|
|
|
389
464
|
return lines.join("\n");
|
|
390
465
|
}
|
|
391
466
|
|
|
467
|
+
function readExistingStationMarkdown(root) {
|
|
468
|
+
const dest = stationMarkdownPath(root);
|
|
469
|
+
if (!existsSync(dest)) return "";
|
|
470
|
+
try {
|
|
471
|
+
return readFileSync(dest, "utf8");
|
|
472
|
+
} catch {
|
|
473
|
+
return "";
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
392
477
|
function writeStationMarkdown(root, fields) {
|
|
393
478
|
try {
|
|
394
479
|
const dir = join(root, ".xray", "state");
|
|
395
480
|
mkdirSync(dir, { recursive: true });
|
|
396
481
|
const dest = stationMarkdownPath(root);
|
|
397
|
-
|
|
482
|
+
const next = mergeStationMarkdown(formatStationMarkdown(fields), readExistingStationMarkdown(root));
|
|
483
|
+
writeFileSync(dest, next);
|
|
398
484
|
return dest;
|
|
399
485
|
} catch {
|
|
400
486
|
return null;
|
|
@@ -416,6 +502,8 @@ module.exports = {
|
|
|
416
502
|
readRepertoireWorking,
|
|
417
503
|
formatWorkingLine,
|
|
418
504
|
applyStationHeat,
|
|
505
|
+
extractPreservedStationLines,
|
|
506
|
+
mergeStationMarkdown,
|
|
419
507
|
formatStationMarkdown,
|
|
420
508
|
writeStationMarkdown,
|
|
421
509
|
};
|
|
@@ -20,5 +20,7 @@ export const persistRepertoireWorking = impl.persistRepertoireWorking;
|
|
|
20
20
|
export const readRepertoireWorking = impl.readRepertoireWorking;
|
|
21
21
|
export const formatWorkingLine = impl.formatWorkingLine;
|
|
22
22
|
export const applyStationHeat = impl.applyStationHeat;
|
|
23
|
+
export const extractPreservedStationLines = impl.extractPreservedStationLines;
|
|
24
|
+
export const mergeStationMarkdown = impl.mergeStationMarkdown;
|
|
23
25
|
export const formatStationMarkdown = impl.formatStationMarkdown;
|
|
24
26
|
export const writeStationMarkdown = impl.writeStationMarkdown;
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* This module does not replace thinDispatch or Codex. It only scales *ceremony*
|
|
6
6
|
* (spawn-plan deny, synthesis, confer) by host strength.
|
|
7
7
|
*
|
|
8
|
-
* Frontier (Grok 4.6 / Grok Build): engine available, spawn warns if no plan.
|
|
8
|
+
* Frontier (Grok 4.6 / Grok Build / Cursor cloud): engine available, spawn warns if no plan.
|
|
9
9
|
* Guided (OpenCode / Hermes / OpenClaw, including free models): full v2 ceremony.
|
|
10
10
|
* Missing config → guided (existing consumers unchanged).
|
|
11
11
|
*/
|
|
12
|
-
export type SuitHost = 'grok' | 'hermes' | 'opencode' | 'openclaw' | 'generic';
|
|
12
|
+
export type SuitHost = 'grok' | 'cursor' | 'hermes' | 'opencode' | 'openclaw' | 'generic';
|
|
13
13
|
export type SuitProfile = 'frontier' | 'guided' | 'strict';
|
|
14
14
|
export type SuitProfileOrAuto = 'auto' | SuitProfile;
|
|
15
15
|
export type CeremonyLevel = 'full' | 'lite';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This module does not replace thinDispatch or Codex. It only scales *ceremony*
|
|
6
6
|
* (spawn-plan deny, synthesis, confer) by host strength.
|
|
7
7
|
*
|
|
8
|
-
* Frontier (Grok 4.6 / Grok Build): engine available, spawn warns if no plan.
|
|
8
|
+
* Frontier (Grok 4.6 / Grok Build / Cursor cloud): engine available, spawn warns if no plan.
|
|
9
9
|
* Guided (OpenCode / Hermes / OpenClaw, including free models): full v2 ceremony.
|
|
10
10
|
* Missing config → guided (existing consumers unchanged).
|
|
11
11
|
*/
|
|
@@ -16,6 +16,7 @@ const requireCjs = createRequire(import.meta.url);
|
|
|
16
16
|
const stationHeat = requireCjs('../integrations/hooks/station-hook-runtime.cjs');
|
|
17
17
|
export const DEFAULT_HOST_PROFILES = {
|
|
18
18
|
grok: 'frontier',
|
|
19
|
+
cursor: 'frontier',
|
|
19
20
|
hermes: 'guided',
|
|
20
21
|
opencode: 'guided',
|
|
21
22
|
openclaw: 'guided',
|
|
@@ -38,6 +39,7 @@ export function loadSuitTemperamentRaw(projectRoot) {
|
|
|
38
39
|
}
|
|
39
40
|
function isSuitHost(value) {
|
|
40
41
|
return (value === 'grok' ||
|
|
42
|
+
value === 'cursor' ||
|
|
41
43
|
value === 'hermes' ||
|
|
42
44
|
value === 'opencode' ||
|
|
43
45
|
value === 'openclaw' ||
|