@mnemonik/cursor-hooks 0.7.66 → 0.7.71
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/dist/hook.js +230 -87
- package/dist/hook.js.map +1 -1
- package/dist/install.js +4 -5
- package/dist/install.js.map +1 -1
- package/dist/lib/http.d.ts +22 -0
- package/dist/lib/http.js +127 -0
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/mcpPayload.d.ts +30 -0
- package/dist/lib/mcpPayload.js +73 -0
- package/dist/lib/mcpPayload.js.map +1 -0
- package/dist/lib/resultCue.d.ts +37 -0
- package/dist/lib/resultCue.js +356 -0
- package/dist/lib/resultCue.js.map +1 -0
- package/dist/lib/sessionState.d.ts +6 -0
- package/dist/lib/sessionState.js +29 -1
- package/dist/lib/sessionState.js.map +1 -1
- package/dist/lib/types.d.ts +31 -14
- package/dist/lib/types.js.map +1 -1
- package/package.json +2 -7
package/dist/hook.js
CHANGED
|
@@ -5,18 +5,20 @@ import { exit, stderr, stdin, stdout } from 'node:process';
|
|
|
5
5
|
import { PRE_BASH_GATE, PRE_EDIT_GATE, PRE_MCP_GATE, PROMPT_CONTEXT, CHECKPOINT_PRESSURE, PRECOMPACT_OBSERVE, SUBAGENT_TRACK, } from './lib/behavior.js';
|
|
6
6
|
import { additionalContext, ask, continueWith, deny, fitCursorContextItems, identifiedContextItems, renderVisibleMnemonikText, } from './lib/cursorOutput.js';
|
|
7
7
|
import { isNonInteractive, parseBypass, resolveConfig } from './lib/env.js';
|
|
8
|
-
import { fetchAmbient, fetchInjections, fetchJit, fetchMcpPrecheck, fetchMcpRecord, reportBypass, reportGateFired, reportSubagent, trackIdeEdit, } from './lib/http.js';
|
|
8
|
+
import { fetchAmbient, fetchInjections, fetchJit, fetchMcpPrecheck, fetchMcpRecord, createPreCompactSnapshot, postContextStats, postProxySuppressionClear, postSessionEnd, reportBypass, reportGateFired, reportSubagent, trackIdeEdit, } from './lib/http.js';
|
|
9
9
|
import { writeProjectFiles } from './lib/files.js';
|
|
10
10
|
import { CURSOR_INLINE_CONTEXT_LIMIT, deliverContext } from './lib/contextOverflow.js';
|
|
11
11
|
import { claimFire } from './lib/dedupGuard.js';
|
|
12
12
|
import { readGitHead } from './lib/git.js';
|
|
13
13
|
import { matchIrreducibleDeny } from './lib/shellTokens.js';
|
|
14
14
|
import { addPathsToGitDirtySnapshot, captureGitDirtyBaseline, diffGitDirtySnapshot, } from '@mnemonik/shared/git-edit-detector';
|
|
15
|
-
import { sessionStateFilePath, sweepStaleSessionDirs } from './lib/sessionState.js';
|
|
15
|
+
import { removeSessionState, sessionStateFilePath, sweepStaleSessionDirs, } from './lib/sessionState.js';
|
|
16
16
|
import { findProjectIdentity } from '@mnemonik/shared/hook-runtime';
|
|
17
17
|
import { cwdOf } from './lib/cwd.js';
|
|
18
18
|
import { isMnemonikToolName } from './lib/mnemonikTool.js';
|
|
19
|
+
import { normalizeMcpArgs, normalizeMcpResult, projectMcpArgsForObservation, } from './lib/mcpPayload.js';
|
|
19
20
|
import { appendProbeLog, buildProbeOutput, probeMode, probeNonce } from './lib/probe.js';
|
|
21
|
+
import { attachPendingResultCue, discardPendingResultCue, extractCursorResultExcerpt, isReadResultTool, rememberPendingResultCue, safeCursorResultText, takePendingResultCue, validCursorToolUseId, } from './lib/resultCue.js';
|
|
20
22
|
import { persistJitDeliveryRef, readPendingJitDeliveryRefs, } from '@mnemonik/shared/jit-delivery-ack';
|
|
21
23
|
import { HOOK_VERSION } from './lib/version.js';
|
|
22
24
|
/**
|
|
@@ -55,35 +57,10 @@ function isCursorEditTool(tool) {
|
|
|
55
57
|
* `toolArgs.code` (regexed for `mnemonik.code_search(`); an oversized body
|
|
56
58
|
* must never break the POST. */
|
|
57
59
|
const MAX_TOOL_ARGS_BYTES = 8 * 1024;
|
|
58
|
-
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
obj = raw;
|
|
63
|
-
else if (typeof raw === 'string') {
|
|
64
|
-
try {
|
|
65
|
-
const parsed = JSON.parse(raw);
|
|
66
|
-
if (parsed && typeof parsed === 'object')
|
|
67
|
-
obj = parsed;
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (!obj)
|
|
74
|
-
return undefined;
|
|
75
|
-
let size;
|
|
76
|
-
try {
|
|
77
|
-
size = JSON.stringify(obj).length;
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
if (size <= MAX_TOOL_ARGS_BYTES)
|
|
83
|
-
return obj;
|
|
84
|
-
if (typeof obj.code === 'string')
|
|
85
|
-
return { code: obj.code.slice(0, MAX_TOOL_ARGS_BYTES) };
|
|
86
|
-
return undefined;
|
|
60
|
+
const MAX_STDIN_BYTES = 1_048_576;
|
|
61
|
+
function objectToolInput(input) {
|
|
62
|
+
const value = input.tool_input;
|
|
63
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
87
64
|
}
|
|
88
65
|
function captureHostTz() {
|
|
89
66
|
try {
|
|
@@ -96,6 +73,7 @@ function captureHostTz() {
|
|
|
96
73
|
async function readStdin() {
|
|
97
74
|
return new Promise((res) => {
|
|
98
75
|
let buf = '';
|
|
76
|
+
let bytes = 0;
|
|
99
77
|
let settled = false;
|
|
100
78
|
const finish = (value) => {
|
|
101
79
|
if (settled)
|
|
@@ -105,9 +83,12 @@ async function readStdin() {
|
|
|
105
83
|
};
|
|
106
84
|
stdin.setEncoding('utf8');
|
|
107
85
|
stdin.on('data', (chunk) => {
|
|
108
|
-
|
|
109
|
-
if (
|
|
86
|
+
bytes += Buffer.byteLength(chunk, 'utf8');
|
|
87
|
+
if (bytes > MAX_STDIN_BYTES) {
|
|
110
88
|
finish(null);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
buf += chunk;
|
|
111
92
|
});
|
|
112
93
|
stdin.on('end', () => {
|
|
113
94
|
try {
|
|
@@ -202,7 +183,7 @@ async function emitGateFired(input, config, tool, mode, outcome) {
|
|
|
202
183
|
});
|
|
203
184
|
}
|
|
204
185
|
function fileTargetsFromToolInput(toolName, toolInput, cwd) {
|
|
205
|
-
if (!toolInput)
|
|
186
|
+
if (!toolInput || typeof toolInput !== 'object' || Array.isArray(toolInput))
|
|
206
187
|
return [];
|
|
207
188
|
const candidates = [];
|
|
208
189
|
const path = toolInput.path ?? toolInput.file_path ?? toolInput.filePath;
|
|
@@ -217,6 +198,33 @@ function fileTargetsFromToolInput(toolName, toolInput, cwd) {
|
|
|
217
198
|
}
|
|
218
199
|
return candidates.map((p) => (p.startsWith('/') ? p : resolve(cwd, p)));
|
|
219
200
|
}
|
|
201
|
+
function pendingResultCueTarget(input) {
|
|
202
|
+
const tool = input.tool_name ?? '';
|
|
203
|
+
if (tool === 'Read') {
|
|
204
|
+
const target = fileTargetsFromToolInput(tool, input.tool_input, cwdOf(input))[0];
|
|
205
|
+
return target ? `file:${target}` : undefined;
|
|
206
|
+
}
|
|
207
|
+
if (tool === 'Shell') {
|
|
208
|
+
const args = objectToolInput(input);
|
|
209
|
+
const command = args?.command;
|
|
210
|
+
if (typeof command === 'string' && isReadResultTool('Shell', args)) {
|
|
211
|
+
return `command:${command.trim()}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
function dedicatedReadTarget(input) {
|
|
217
|
+
const path = input.file_path;
|
|
218
|
+
if (typeof path !== 'string' || !path.trim())
|
|
219
|
+
return undefined;
|
|
220
|
+
return `file:${path.startsWith('/') ? path : resolve(cwdOf(input), path)}`;
|
|
221
|
+
}
|
|
222
|
+
function dedicatedShellTarget(input) {
|
|
223
|
+
const command = input.command;
|
|
224
|
+
if (typeof command !== 'string' || !isReadResultTool('Shell', { command }))
|
|
225
|
+
return undefined;
|
|
226
|
+
return `command:${command.trim()}`;
|
|
227
|
+
}
|
|
220
228
|
async function handleSessionStart(input, config) {
|
|
221
229
|
// Ahead of every early return below: the dirty-set baseline is what makes
|
|
222
230
|
// later shell writes classifiable, and it must be taken even when prompt
|
|
@@ -234,21 +242,10 @@ async function handleSessionStart(input, config) {
|
|
|
234
242
|
});
|
|
235
243
|
if (!isFirstFire)
|
|
236
244
|
writeAllow();
|
|
237
|
-
// Bus-driven injections only.
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// SessionStart `additional_context` field is NOT model-visible (operator
|
|
242
|
-
// probe 2026-07-02, Cursor 3.9.16; Cursor-acknowledged upstream bug —
|
|
243
|
-
// composer-handle timing, forum #158452), so a digest emitted here would be
|
|
244
|
-
// marked "delivered" server-side while the model never saw it. This call now
|
|
245
|
-
// only WARMS the server's digest cache for the project resolved from the
|
|
246
|
-
// SessionStart payload's workspace_roots (see cwdOf); the digest itself is
|
|
247
|
-
// delivered — and the bootstrap obligation satisfied — on the first
|
|
248
|
-
// `beforeSubmitPrompt` below, which probe-verified as model-visible 9/9. This
|
|
249
|
-
// handler's own behavior is otherwise unchanged: it fails open (`writeAllow`)
|
|
250
|
-
// when the server returns no pieces, and still forwards any non-bootstrap
|
|
251
|
-
// injections the server chooses to send on SessionStart.
|
|
245
|
+
// Bus-driven injections only. Current Cursor makes SessionStart
|
|
246
|
+
// additional_context model-visible, so this is the primary bootstrap
|
|
247
|
+
// carrier. UserPromptSubmit and PostToolUse remain visible late fallbacks
|
|
248
|
+
// while the server-side obligation is genuinely open.
|
|
252
249
|
const cwd = cwdOf(input);
|
|
253
250
|
const projectId = (await findProjectIdentity(cwd))?.projectId;
|
|
254
251
|
const tz = captureHostTz();
|
|
@@ -261,6 +258,17 @@ async function handleSessionStart(input, config) {
|
|
|
261
258
|
event: 'SessionStart',
|
|
262
259
|
tz,
|
|
263
260
|
});
|
|
261
|
+
// The compatibility rule is offered on SessionStart and UserPromptSubmit.
|
|
262
|
+
// Writes are idempotent and fail-open, so an older event sequence can land
|
|
263
|
+
// the same file at the prompt boundary without churning it.
|
|
264
|
+
if (injectionsResp?.filesToWrite?.length) {
|
|
265
|
+
try {
|
|
266
|
+
await writeProjectFiles(cwd, injectionsResp.filesToWrite);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
/* fail-open — a rule-file write must never break the session */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
264
272
|
const pieces = [];
|
|
265
273
|
if (injectionsResp?.ok) {
|
|
266
274
|
for (const inj of injectionsResp.injections)
|
|
@@ -337,11 +345,8 @@ async function handleBeforeSubmitPrompt(input, config) {
|
|
|
337
345
|
: Promise.resolve(null),
|
|
338
346
|
]);
|
|
339
347
|
// Write any server-provided workspace files (the memory-tools rule file)
|
|
340
|
-
// idempotently.
|
|
341
|
-
//
|
|
342
|
-
// bug), so a SessionStart write never lands on the first session. This fire is
|
|
343
|
-
// the prompt boundary, which Cursor runs to completion, so the write lands.
|
|
344
|
-
// Never breaks the session: writeProjectFiles is fail-open and we wrap it too.
|
|
348
|
+
// idempotently. SessionStart is primary; this remains the compatibility
|
|
349
|
+
// fallback for older Cursor event sequences. Never breaks the session.
|
|
345
350
|
if (injectionsResp?.filesToWrite?.length) {
|
|
346
351
|
try {
|
|
347
352
|
await writeProjectFiles(cwdOf(input), injectionsResp.filesToWrite);
|
|
@@ -446,7 +451,7 @@ async function handlePreToolUse(input, config) {
|
|
|
446
451
|
cursorSessionId: sessionId(input),
|
|
447
452
|
event: 'PreToolUse',
|
|
448
453
|
toolName: input.tool_name ?? '',
|
|
449
|
-
toolArgs: input
|
|
454
|
+
toolArgs: objectToolInput(input),
|
|
450
455
|
filePaths: existingTargets,
|
|
451
456
|
});
|
|
452
457
|
if (!injectionsResp?.ok) {
|
|
@@ -495,27 +500,38 @@ async function handlePreToolUse(input, config) {
|
|
|
495
500
|
* PreToolUse for every OTHER tool — a read, a search, an MCP file tool,
|
|
496
501
|
* whatever Cursor adds next.
|
|
497
502
|
*
|
|
498
|
-
* WHY THIS EXISTS.
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* would report a mutation-shaped outcome for an action that mutates nothing.
|
|
503
|
+
* WHY THIS EXISTS. Cursor emits Read/Grep/Shell through this generic event.
|
|
504
|
+
* Those read-result actions record bounded local intent and defer recall to
|
|
505
|
+
* model-visible PostToolUse; other generic file actions retain pre-tool recall.
|
|
506
|
+
* None may enter the grounding gate above: it would report a mutation-shaped
|
|
507
|
+
* outcome for an action that mutates nothing.
|
|
504
508
|
*
|
|
505
509
|
* ADVISORY BY CONSTRUCTION. This branch can add recall to an allow and can
|
|
506
510
|
* never deny — grounding stays with the edit gate, policy with the shell gate.
|
|
507
511
|
* It emits NO `gate_fired` telemetry, exactly like the codex and claude-code
|
|
508
512
|
* generic branches: there is no gate here to report the firing of.
|
|
509
513
|
*
|
|
510
|
-
* CHEAP BY CONSTRUCTION.
|
|
511
|
-
*
|
|
512
|
-
* to start.
|
|
514
|
+
* CHEAP BY CONSTRUCTION. Result-cue actions return after a local bounded state
|
|
515
|
+
* write; another payload naming no existing file returns before any network.
|
|
513
516
|
*/
|
|
514
517
|
async function handlePreGenericTool(input, config) {
|
|
515
518
|
if (!config.apiKey)
|
|
516
519
|
writeAllow();
|
|
517
520
|
const cwd = cwdOf(input);
|
|
518
521
|
const tool = input.tool_name ?? '';
|
|
522
|
+
// Read-result recall belongs to the visible PostToolUse carrier. Remember
|
|
523
|
+
// only the bounded action intent here; an invisible PreToolUse fetch could
|
|
524
|
+
// consume server dedup before Cursor has a channel the model can see.
|
|
525
|
+
const cueTarget = pendingResultCueTarget(input);
|
|
526
|
+
if (cueTarget && sessionId(input) !== UNKNOWN_SESSION) {
|
|
527
|
+
rememberPendingResultCue({
|
|
528
|
+
sessionId: sessionId(input),
|
|
529
|
+
toolUseId: input.tool_use_id ?? '',
|
|
530
|
+
target: cueTarget,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (tool === 'Read' || tool === 'Grep' || cueTarget)
|
|
534
|
+
writeAllow();
|
|
519
535
|
const filePaths = fileTargetsFromToolInput(tool, input.tool_input, cwd).filter((path) => existsSync(path));
|
|
520
536
|
if (filePaths.length === 0)
|
|
521
537
|
writeAllow();
|
|
@@ -527,7 +543,7 @@ async function handlePreGenericTool(input, config) {
|
|
|
527
543
|
cursorSessionId: sessionId(input),
|
|
528
544
|
event: 'PreToolUse',
|
|
529
545
|
toolName: tool,
|
|
530
|
-
toolArgs: input
|
|
546
|
+
toolArgs: objectToolInput(input),
|
|
531
547
|
filePaths,
|
|
532
548
|
});
|
|
533
549
|
if (!injectionsResp?.ok)
|
|
@@ -550,6 +566,23 @@ async function handlePreGenericTool(input, config) {
|
|
|
550
566
|
async function handlePostToolUse(input, config) {
|
|
551
567
|
const tool = input.tool_name ?? '';
|
|
552
568
|
const cwd = cwdOf(input);
|
|
569
|
+
const cueTarget = pendingResultCueTarget(input);
|
|
570
|
+
const toolUseId = validCursorToolUseId(input.tool_use_id);
|
|
571
|
+
const directExcerpt = extractCursorResultExcerpt({
|
|
572
|
+
toolName: tool,
|
|
573
|
+
toolInput: objectToolInput(input),
|
|
574
|
+
toolOutput: input.tool_output,
|
|
575
|
+
});
|
|
576
|
+
const pendingCue = cueTarget && toolUseId && sessionId(input) !== UNKNOWN_SESSION
|
|
577
|
+
? takePendingResultCue({
|
|
578
|
+
sessionId: sessionId(input),
|
|
579
|
+
toolUseId,
|
|
580
|
+
target: cueTarget,
|
|
581
|
+
})
|
|
582
|
+
: undefined;
|
|
583
|
+
// Current direct result text wins; BeforeReadFile/AfterShellExecution state
|
|
584
|
+
// exists only for metadata-only PostToolUse payloads.
|
|
585
|
+
const resultExcerpt = sessionId(input) === UNKNOWN_SESSION ? undefined : (directExcerpt ?? pendingCue?.excerpt);
|
|
553
586
|
const targets = fileTargetsFromToolInput(tool, input.tool_input, cwd);
|
|
554
587
|
for (const target of targets) {
|
|
555
588
|
await trackIdeEdit({
|
|
@@ -577,6 +610,9 @@ async function handlePostToolUse(input, config) {
|
|
|
577
610
|
cursorSessionId: sessionId(input),
|
|
578
611
|
event: 'PostToolUse',
|
|
579
612
|
toolName: tool,
|
|
613
|
+
toolArgs: resultExcerpt ? objectToolInput(input) : undefined,
|
|
614
|
+
toolResultExcerpt: resultExcerpt,
|
|
615
|
+
toolUseId: resultExcerpt ? toolUseId : undefined,
|
|
580
616
|
// Post-edit grounding (cursor-only server behavior): the server returns
|
|
581
617
|
// the edited file's memory summary as a file_context_required envelope,
|
|
582
618
|
// delivered on postToolUse additional_context — Cursor's only working
|
|
@@ -598,10 +634,42 @@ async function handlePostToolUse(input, config) {
|
|
|
598
634
|
const context = fitInlineContext(pieces);
|
|
599
635
|
writeOutput(additionalContext(await deliverContext(context, sessionId(input))));
|
|
600
636
|
}
|
|
637
|
+
async function handleBeforeReadFile(input, config) {
|
|
638
|
+
const target = dedicatedReadTarget(input);
|
|
639
|
+
const excerpt = input.attachments?.length ? undefined : safeCursorResultText(input.content);
|
|
640
|
+
if (config.apiKey && target && excerpt && sessionId(input) !== UNKNOWN_SESSION) {
|
|
641
|
+
attachPendingResultCue({ sessionId: sessionId(input), target, excerpt });
|
|
642
|
+
}
|
|
643
|
+
writeAllow();
|
|
644
|
+
}
|
|
645
|
+
async function handleAfterShellExecution(input, config) {
|
|
646
|
+
// Phase-0 order is AfterShellExecution before generic PostToolUse. Observe
|
|
647
|
+
// writes immediately, then prepare content only; this event has no visible
|
|
648
|
+
// output field and therefore never marks a cue delivered.
|
|
649
|
+
await reportShellWrites(input, config);
|
|
650
|
+
const target = dedicatedShellTarget(input);
|
|
651
|
+
const excerpt = safeCursorResultText(input.output);
|
|
652
|
+
if (config.apiKey && target && excerpt && sessionId(input) !== UNKNOWN_SESSION) {
|
|
653
|
+
attachPendingResultCue({ sessionId: sessionId(input), target, excerpt });
|
|
654
|
+
}
|
|
655
|
+
writeAllow();
|
|
656
|
+
}
|
|
657
|
+
async function handlePostToolUseFailure(input, config) {
|
|
658
|
+
if (input.tool_name === 'Shell')
|
|
659
|
+
await reportShellWrites(input, config);
|
|
660
|
+
if (sessionId(input) !== UNKNOWN_SESSION) {
|
|
661
|
+
discardPendingResultCue({
|
|
662
|
+
sessionId: sessionId(input),
|
|
663
|
+
toolUseId: input.tool_use_id,
|
|
664
|
+
target: pendingResultCueTarget(input),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
writeAllow();
|
|
668
|
+
}
|
|
601
669
|
async function handleBeforeShellExecution(input, config) {
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
670
|
+
// Compatibility carrier for a previous shell call when AfterShellExecution
|
|
671
|
+
// did not fire. The rolling snapshot keeps this idempotent with the current
|
|
672
|
+
// immediate post-shell observer. Ahead of the empty-command early exit.
|
|
605
673
|
await reportShellWrites(input, config);
|
|
606
674
|
const command = (input.command ?? '').toString();
|
|
607
675
|
if (!command.trim())
|
|
@@ -613,6 +681,10 @@ async function handleBeforeShellExecution(input, config) {
|
|
|
613
681
|
// precedes the floor).
|
|
614
682
|
const denyName = matchIrreducibleDeny(command);
|
|
615
683
|
if (denyName) {
|
|
684
|
+
discardPendingResultCue({
|
|
685
|
+
sessionId: sessionId(input),
|
|
686
|
+
target: dedicatedShellTarget(input),
|
|
687
|
+
});
|
|
616
688
|
await emitGateFired(input, config, 'Shell', 'enforce', `irreducible_deny_${denyName}`);
|
|
617
689
|
writeOutput(deny([
|
|
618
690
|
'MNEMONIK_IRREDUCIBLE_DENY',
|
|
@@ -680,6 +752,10 @@ async function handleBeforeShellExecution(input, config) {
|
|
|
680
752
|
lines.push(fitInjection(checkpointInjection, 4_000));
|
|
681
753
|
}
|
|
682
754
|
if (policyCheck) {
|
|
755
|
+
discardPendingResultCue({
|
|
756
|
+
sessionId: sessionId(input),
|
|
757
|
+
target: dedicatedShellTarget(input),
|
|
758
|
+
});
|
|
683
759
|
lines.push(fitInjection(policyCheck, 4_000));
|
|
684
760
|
}
|
|
685
761
|
for (const nudge of policyNudges) {
|
|
@@ -717,9 +793,11 @@ async function handleBeforeMCPExecution(input, config) {
|
|
|
717
793
|
await emitGateFired(input, config, tool, 'advisory', 'no_api_key');
|
|
718
794
|
writeAllow();
|
|
719
795
|
}
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
796
|
+
const normalizedArgs = normalizeMcpArgs(input);
|
|
797
|
+
if (!normalizedArgs.ok) {
|
|
798
|
+
await emitGateFired(input, config, tool, 'advisory', 'invalid_args');
|
|
799
|
+
writeAllow();
|
|
800
|
+
}
|
|
723
801
|
// Read .git/HEAD up-front so the `git_head_already_mined` evaluator can
|
|
724
802
|
// compare against `git_mine_history.last_mined_sha`. Soft-fails to undefined
|
|
725
803
|
// when the cwd isn't a git tree or HEAD is unreadable — the server-side
|
|
@@ -733,7 +811,7 @@ async function handleBeforeMCPExecution(input, config) {
|
|
|
733
811
|
projectId: (await findProjectIdentity(cwdOf(input)))?.projectId,
|
|
734
812
|
cursorSessionId: sessionId(input),
|
|
735
813
|
tool,
|
|
736
|
-
args,
|
|
814
|
+
args: normalizedArgs.value,
|
|
737
815
|
model: input.model,
|
|
738
816
|
...(gitHeadSha ? { gitHeadSha } : {}),
|
|
739
817
|
});
|
|
@@ -763,20 +841,21 @@ async function handleAfterMCPExecution(input, config) {
|
|
|
763
841
|
// so the server can populate the retrieved-memory log + embedding cache
|
|
764
842
|
// that backs four precheck evaluators (Cat 2/3/8). Fire-and-forget; any
|
|
765
843
|
// failure leaves the corresponding evaluator fail-open at most.
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
844
|
+
const normalizedArgs = normalizeMcpArgs(input);
|
|
845
|
+
const normalizedResult = normalizeMcpResult(input);
|
|
846
|
+
if (config.apiKey) {
|
|
847
|
+
if (normalizedArgs.ok && normalizedResult.present) {
|
|
848
|
+
await fetchMcpRecord({
|
|
849
|
+
server: config.server,
|
|
850
|
+
apiKey: config.apiKey,
|
|
851
|
+
cwd: cwdOf(input),
|
|
852
|
+
projectId: (await findProjectIdentity(cwdOf(input)))?.projectId,
|
|
853
|
+
cursorSessionId: sessionId(input),
|
|
854
|
+
tool,
|
|
855
|
+
args: normalizedArgs.value,
|
|
856
|
+
result: normalizedResult.value,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
780
859
|
// Cursor cannot render afterMCPExecution output. Keep the epoch and
|
|
781
860
|
// obligation writes, but tell the server to skip injection candidates.
|
|
782
861
|
await fetchInjections({
|
|
@@ -787,7 +866,7 @@ async function handleAfterMCPExecution(input, config) {
|
|
|
787
866
|
cursorSessionId: sessionId(input),
|
|
788
867
|
event: 'PostToolUse',
|
|
789
868
|
toolName: tool,
|
|
790
|
-
toolArgs:
|
|
869
|
+
toolArgs: projectMcpArgsForObservation(input, MAX_TOOL_ARGS_BYTES),
|
|
791
870
|
observeOnly: true,
|
|
792
871
|
});
|
|
793
872
|
}
|
|
@@ -844,12 +923,64 @@ async function handleSubagentLifecycle(input, config, event) {
|
|
|
844
923
|
async function handlePreCompact(input, config) {
|
|
845
924
|
if (PRECOMPACT_OBSERVE === 'off')
|
|
846
925
|
writeAllow();
|
|
926
|
+
if (!config.apiKey)
|
|
927
|
+
writeAllow();
|
|
928
|
+
const cwd = cwdOf(input);
|
|
929
|
+
const projectId = (await findProjectIdentity(cwd))?.projectId;
|
|
930
|
+
const cursorSessionId = sessionId(input);
|
|
931
|
+
if (cursorSessionId !== UNKNOWN_SESSION &&
|
|
932
|
+
typeof input.context_tokens === 'number' &&
|
|
933
|
+
Number.isInteger(input.context_tokens) &&
|
|
934
|
+
input.context_tokens >= 0) {
|
|
935
|
+
await postContextStats({
|
|
936
|
+
server: config.server,
|
|
937
|
+
apiKey: config.apiKey,
|
|
938
|
+
cwd,
|
|
939
|
+
projectId,
|
|
940
|
+
cursorSessionId,
|
|
941
|
+
contextTokens: input.context_tokens,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
if (cursorSessionId !== UNKNOWN_SESSION) {
|
|
945
|
+
await createPreCompactSnapshot({
|
|
946
|
+
server: config.server,
|
|
947
|
+
apiKey: config.apiKey,
|
|
948
|
+
cwd,
|
|
949
|
+
projectId,
|
|
950
|
+
cursorSessionId,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
847
953
|
const usage = typeof input.context_usage_percent === 'number' ? Math.round(input.context_usage_percent) : -1;
|
|
848
954
|
const msgs = typeof input.messages_to_compact === 'number' ? input.messages_to_compact : -1;
|
|
849
955
|
const first = input.is_first_compaction ? 'first' : 'subsequent';
|
|
850
956
|
await emitGateFired(input, config, 'preCompact', 'advisory', `compact_${first}_u${usage}_m${msgs}`);
|
|
851
957
|
writeAllow();
|
|
852
958
|
}
|
|
959
|
+
async function handleSessionEnd(input, config) {
|
|
960
|
+
const cursorSessionId = sessionId(input);
|
|
961
|
+
if (config.apiKey && cursorSessionId !== UNKNOWN_SESSION) {
|
|
962
|
+
const cwd = cwdOf(input);
|
|
963
|
+
const projectId = (await findProjectIdentity(cwd))?.projectId;
|
|
964
|
+
await postProxySuppressionClear({
|
|
965
|
+
server: config.server,
|
|
966
|
+
apiKey: config.apiKey,
|
|
967
|
+
cwd,
|
|
968
|
+
projectId,
|
|
969
|
+
cursorSessionId,
|
|
970
|
+
});
|
|
971
|
+
await postSessionEnd({
|
|
972
|
+
server: config.server,
|
|
973
|
+
apiKey: config.apiKey,
|
|
974
|
+
cwd,
|
|
975
|
+
projectId,
|
|
976
|
+
cursorSessionId,
|
|
977
|
+
reason: input.reason ?? input.final_status,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
if (cursorSessionId !== UNKNOWN_SESSION)
|
|
981
|
+
removeSessionState(cursorSessionId);
|
|
982
|
+
writeAllow();
|
|
983
|
+
}
|
|
853
984
|
async function dispatch(input) {
|
|
854
985
|
if (isNonInteractive())
|
|
855
986
|
writeAllow();
|
|
@@ -912,15 +1043,24 @@ async function dispatch(input) {
|
|
|
912
1043
|
case 'postToolUse':
|
|
913
1044
|
await handlePostToolUse(input, config);
|
|
914
1045
|
break;
|
|
1046
|
+
case 'postToolUseFailure':
|
|
1047
|
+
await handlePostToolUseFailure(input, config);
|
|
1048
|
+
break;
|
|
915
1049
|
case 'beforeShellExecution':
|
|
916
1050
|
await handleBeforeShellExecution(input, config);
|
|
917
1051
|
break;
|
|
1052
|
+
case 'afterShellExecution':
|
|
1053
|
+
await handleAfterShellExecution(input, config);
|
|
1054
|
+
break;
|
|
918
1055
|
case 'beforeMCPExecution':
|
|
919
1056
|
await handleBeforeMCPExecution(input, config);
|
|
920
1057
|
break;
|
|
921
1058
|
case 'afterMCPExecution':
|
|
922
1059
|
await handleAfterMCPExecution(input, config);
|
|
923
1060
|
break;
|
|
1061
|
+
case 'beforeReadFile':
|
|
1062
|
+
await handleBeforeReadFile(input, config);
|
|
1063
|
+
break;
|
|
924
1064
|
case 'afterFileEdit':
|
|
925
1065
|
await handleAfterFileEdit(input, config);
|
|
926
1066
|
break;
|
|
@@ -936,6 +1076,9 @@ async function dispatch(input) {
|
|
|
936
1076
|
case 'preCompact':
|
|
937
1077
|
await handlePreCompact(input, config);
|
|
938
1078
|
break;
|
|
1079
|
+
case 'sessionEnd':
|
|
1080
|
+
await handleSessionEnd(input, config);
|
|
1081
|
+
break;
|
|
939
1082
|
default:
|
|
940
1083
|
writeAllow();
|
|
941
1084
|
}
|