@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 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
- /** Resolve and cap MCP args for the observe-only epoch-writer request. */
59
- function resolveMcpToolArgs(raw) {
60
- let obj;
61
- if (raw && typeof raw === 'object')
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
- buf += chunk;
109
- if (buf.length > 1_048_576)
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. Legacy /context action prose is deliberately
238
- // not merged here, so InjectionBus owns session-start directive timing.
239
- //
240
- // The server no longer delivers the bootstrap digest on this event: Cursor's
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. Delivered here on UserPromptSubmit rather than SessionStart:
341
- // Cursor kills the FIRST hook fire after a fresh IDE open (the sessionStart
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.tool_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. The accepted reflex-recall design is "all tool actions from
499
- * day one … Noise controls are budget, per-session dedup, and cue dedup, not a
500
- * trigger list" (docs/development/memory-adoption.plan.md, "Decisions taken"
501
- * #1), and Cursor DOES emit `Read` as a preToolUse tool type. Recall must keep
502
- * firing there. What must NOT follow the read is the grounding gate above: it
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. A payload naming no existing file returns before any
511
- * network call, so `Task`, `Grep` and friends cost one process that already had
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.tool_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
- // Credits the PREVIOUS shell call's writes: Cursor fires nothing after a
603
- // shell command, so the next one is the earliest observation point. Ahead of
604
- // the empty-command check, which exits the process.
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 args = (input.mcp_args ??
721
- input.tool_input ??
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
- if (config.apiKey && input.mcp_result !== undefined) {
767
- const args = (input.mcp_args ??
768
- input.tool_input ??
769
- {});
770
- await fetchMcpRecord({
771
- server: config.server,
772
- apiKey: config.apiKey,
773
- cwd: cwdOf(input),
774
- projectId: (await findProjectIdentity(cwdOf(input)))?.projectId,
775
- cursorSessionId: sessionId(input),
776
- tool,
777
- args,
778
- result: input.mcp_result,
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: resolveMcpToolArgs(input.mcp_args ?? input.tool_input),
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
  }