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.
- package/.grok-plugin/plugin.json +1 -1
- package/.opencode/skills/orchestrator/SKILL.md +1 -1
- package/.xray/features.json +1 -1
- package/AGENTS-consumer.md +4 -4
- package/AGENTS.md +4 -4
- package/README.md +14 -14
- package/SKILLS.md +3 -3
- package/dist/AGENTS.md +4 -4
- package/dist/CHANGELOG.md +16 -8
- package/dist/README.md +14 -14
- package/dist/SKILLS.md +3 -3
- package/dist/cli/commands/validate.d.ts +1 -1
- package/dist/cli/commands/validate.js +7 -1
- package/dist/integrations/cursor/README.md +6 -5
- package/dist/integrations/cursor/hooks/after-file-edit.sh +3 -0
- package/dist/integrations/cursor/hooks/before-read-file.sh +3 -0
- package/dist/integrations/cursor/hooks/before-shell-execution.sh +3 -0
- package/dist/integrations/cursor/hooks/cursor-hook-utils.js +93 -12
- package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +3 -0
- package/dist/integrations/cursor/hooks/hooks.json +13 -3
- package/dist/integrations/cursor/hooks/pre-compact.js +9 -0
- package/dist/integrations/cursor/hooks/pre-compact.sh +3 -0
- package/dist/integrations/cursor/hooks/pre-tool-use.js +16 -12
- package/dist/integrations/cursor/hooks/pre-tool-use.sh +3 -0
- package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +63 -0
- package/dist/integrations/grok/hooks/grok-hook-utils.js +3 -2
- package/dist/integrations/hooks/station-hook-runtime.cjs +222 -70
- package/dist/integrations/hooks/station-hook-runtime.mjs +9 -0
- package/dist/integrations/hooks/station-memory-ingest.mjs +1 -2
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/nucleus/suit-temperament.d.ts +0 -5
- package/dist/nucleus/suit-temperament.js +13 -1
- package/dist/skills/orchestrator/SKILL.md +1 -1
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/scripts/foundry/README.md +2 -2
- package/scripts/foundry/assert-packed-dist-cli.mjs +4 -0
- package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
- package/scripts/foundry/validate-release-docs.mjs +44 -0
- package/scripts/node/install-bridges.cjs +256 -10
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/skills/orchestrator/SKILL.md +1 -1
- 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
|
-
|
|
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')
|
|
97
|
-
else if (row && typeof row === 'object' && 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
|
-
|
|
101
|
-
const
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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": "
|
|
6
|
+
"command": ".cursor/hooks/pre-tool-use.sh"
|
|
7
7
|
}
|
|
8
8
|
],
|
|
9
9
|
"preCompact": [
|
|
10
10
|
{
|
|
11
|
-
"command": "
|
|
11
|
+
"command": ".cursor/hooks/pre-compact.sh"
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
14
|
"afterFileEdit": [
|
|
15
15
|
{
|
|
16
|
-
"command": "
|
|
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
|
});
|
|
@@ -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
|
-
|
|
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(
|
|
57
|
-
const gateFeatures = loadDelegationGateFeatures(
|
|
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:
|
|
65
|
+
projectRoot: gateRoot,
|
|
64
66
|
sessionId,
|
|
65
67
|
features: gateFeatures,
|
|
66
68
|
host: 'cursor',
|
|
67
69
|
});
|
|
68
70
|
if (!gateBlock.allow) {
|
|
69
|
-
finish(
|
|
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(
|
|
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(
|
|
81
|
+
if (testHint) finish(gateRoot, 'allow', null, testHint, toolName);
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
if (gateBlock.reason) {
|
|
83
|
-
finish(
|
|
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(
|
|
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
|
-
'
|
|
98
|
-
`preToolUse hook error —
|
|
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,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(),
|