@primitive.ai/prim 0.1.0-alpha.90 → 0.1.0-alpha.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +20 -12
  2. package/SKILL.md +6 -6
  3. package/dist/{chunk-LMRYZL3D.js → chunk-5AFRMOPX.js} +1 -1
  4. package/dist/{chunk-RO3F6JYV.js → chunk-5GYQ4J3D.js} +2 -2
  5. package/dist/{chunk-3EEFL6NJ.js → chunk-67VNLLBN.js} +261 -1
  6. package/dist/{chunk-LEMX3IOG.js → chunk-BTNNEH2W.js} +1 -1
  7. package/dist/chunk-EWZMDD44.js +1474 -0
  8. package/dist/{chunk-YWO7O6QG.js → chunk-FBAYCS3X.js} +2 -2
  9. package/dist/{chunk-R26A7TWY.js → chunk-FKXFUUWJ.js} +1 -1
  10. package/dist/{chunk-C7T7B45R.js → chunk-FOLKHXLJ.js} +150 -21
  11. package/dist/chunk-H4UNYFUC.js +196 -0
  12. package/dist/chunk-HU4PW6I6.js +264 -0
  13. package/dist/{chunk-CUE2LKLR.js → chunk-KFXF2V5G.js} +1 -1
  14. package/dist/{chunk-6IWNH776.js → chunk-LFFLHSQO.js} +4 -3
  15. package/dist/{chunk-MAZWKJGL.js → chunk-M6RIJVRE.js} +42 -232
  16. package/dist/{chunk-2JRX7ZR4.js → chunk-NAHHFSLH.js} +16 -5
  17. package/dist/{chunk-FI3LIRCB.js → chunk-OAAP2MY6.js} +2 -2
  18. package/dist/{chunk-Z7YPFWAZ.js → chunk-PCVGBE5B.js} +10 -268
  19. package/dist/{chunk-UZPBB7V4.js → chunk-VW3FDYRT.js} +60 -48
  20. package/dist/chunk-WX52U7Z2.js +112 -0
  21. package/dist/{chunk-PPQYQEIR.js → chunk-ZB4F75SZ.js} +28 -0
  22. package/dist/daemon/server.js +20 -20
  23. package/dist/hook-runtime/daemon/server.js +20 -20
  24. package/dist/hook-runtime/hooks/post-commit.js +20 -20
  25. package/dist/hook-runtime/hooks/post-rewrite.js +20 -20
  26. package/dist/hook-runtime/hooks/post-tool-use.js +651 -132
  27. package/dist/hook-runtime/hooks/pre-tool-use.js +644 -96
  28. package/dist/hook-runtime/hooks/prim-hook.js +612 -96
  29. package/dist/hook-runtime/hooks/session-end.js +610 -27
  30. package/dist/hook-runtime/hooks/session-start.js +1132 -533
  31. package/dist/hook-runtime/index.js +735 -264
  32. package/dist/hook-runtime/statusline-main.js +48 -4
  33. package/dist/hooks/post-commit.js +11 -9
  34. package/dist/hooks/post-rewrite.js +11 -9
  35. package/dist/hooks/post-tool-use.js +73 -48
  36. package/dist/hooks/pre-commit.js +4 -4
  37. package/dist/hooks/pre-tool-use.js +46 -44
  38. package/dist/hooks/prim-hook.js +47 -25
  39. package/dist/hooks/session-end.js +15 -26
  40. package/dist/hooks/session-start.js +59 -32
  41. package/dist/index.js +558 -1436
  42. package/dist/statusline-main.js +48 -4
  43. package/package.json +1 -1
  44. package/dist/chunk-PMRDE2DA.js +0 -310
  45. package/dist/chunk-WB7AUZ4C.js +0 -52
@@ -311,7 +311,7 @@ function daemonRequest(method, params = {}, opts = {}) {
311
311
  ...caller ? { caller } : {}
312
312
  };
313
313
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
314
- return new Promise((resolve) => {
314
+ return new Promise((resolve4) => {
315
315
  let settled = false;
316
316
  let socket;
317
317
  let buffer = Buffer.alloc(0);
@@ -326,7 +326,7 @@ function daemonRequest(method, params = {}, opts = {}) {
326
326
  } catch {
327
327
  }
328
328
  }
329
- resolve(value);
329
+ resolve4(value);
330
330
  };
331
331
  const timer = setTimeout(() => settle(null), timeoutMs);
332
332
  timer.unref();
@@ -389,14 +389,494 @@ function daemonRequest(method, params = {}, opts = {}) {
389
389
  function parseAgent(argv) {
390
390
  const i = argv.indexOf("--agent");
391
391
  const value = i !== -1 ? argv[i + 1] : void 0;
392
- if (value === "codex" || value === "hermes") {
392
+ if (value === "codex" || value === "cursor" || value === "hermes") {
393
393
  return value;
394
394
  }
395
395
  return "claude_code";
396
396
  }
397
397
 
398
+ // src/commands/cursor-install.ts
399
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
400
+ import { homedir as homedir3 } from "os";
401
+ import { isAbsolute as isAbsolute5, join as join8 } from "path";
402
+
403
+ // src/lib/atomic-file.ts
404
+ import { randomUUID } from "crypto";
405
+ import {
406
+ constants,
407
+ closeSync,
408
+ fchmodSync,
409
+ fsyncSync,
410
+ mkdirSync,
411
+ openSync,
412
+ renameSync,
413
+ rmSync,
414
+ writeFileSync
415
+ } from "fs";
416
+ import { dirname } from "path";
417
+
418
+ // src/lib/bin-path.ts
419
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
420
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4 } from "path";
421
+ import { fileURLToPath } from "url";
422
+ var PKG_NAME = "@primitive.ai/prim";
423
+ var STABLE_HOOK_LAUNCHER_NAME = "prim-hook-launcher-v1";
424
+ function shellQuote(value) {
425
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
426
+ }
427
+ var STABLE_HOOK_ARGUMENTS_RE = /^[-A-Za-z0-9_ ]*$/u;
428
+ var STABLE_HOOK_BIN_RE = /^[A-Za-z0-9][A-Za-z0-9-]*$/u;
429
+ function stableHookCommand(bin, args = "") {
430
+ if (!STABLE_HOOK_BIN_RE.test(bin) || !STABLE_HOOK_ARGUMENTS_RE.test(args)) {
431
+ throw new Error("invalid stable hook command");
432
+ }
433
+ const resolver = [
434
+ // #242 and earlier identify managed commands by this package marker. Keep
435
+ // it as inert data during the rolling window so an older uninstall or
436
+ // reinstall can remove the stable entry without invoking npm or PATH.
437
+ `prim_legacy_reader='-p ${PKG_NAME}@stable prim-shim.sh ${bin} '; `,
438
+ 'prim_absolute() { case "$1" in [/]* ) ;; *) return 1 ;; esac; case "$1" in [[:space:]]*|*[[:space:]]|*[/][/]*|*[/].[/]*|*[/]..[/]*|*[/].|*[/]..|?*[/]) return 1 ;; esac; }; ',
439
+ 'prim_config=${PRIM_CONFIG_DIR:-}; if ! prim_absolute "$prim_config"; then prim_config=${XDG_CONFIG_HOME:-}; if prim_absolute "$prim_config"; then ',
440
+ 'case "$prim_config" in /) prim_config=/prim ;; *) prim_config="$prim_config/prim" ;; esac; ',
441
+ 'else prim_home=${HOME:-}; prim_absolute "$prim_home" || exit 78; case "$prim_home" in /) prim_config=/.config/prim ;; *) prim_config="$prim_home/.config/prim" ;; esac; ',
442
+ "fi; fi; ",
443
+ `case "$prim_config" in /) prim_launcher=/${STABLE_HOOK_LAUNCHER_NAME} ;; *) prim_launcher="$prim_config/${STABLE_HOOK_LAUNCHER_NAME}" ;; esac; `,
444
+ 'exec "$prim_launcher" "$@"'
445
+ ].join("");
446
+ const suffix = args ? ` ${args}` : "";
447
+ return `/bin/sh -c ${shellQuote(resolver)} ${STABLE_HOOK_LAUNCHER_NAME} ${bin}${suffix}`;
448
+ }
449
+ function detachedHookShimCommand(bin, args = "") {
450
+ return `payload=$(cat); { trap '' HUP; printf '%s' "$payload" | { ${stableHookCommand(bin, args)}; }; } </dev/null >/dev/null 2>&1 &`;
451
+ }
452
+
453
+ // src/lib/hook-runtime.ts
454
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
455
+ import {
456
+ constants as constants2,
457
+ accessSync,
458
+ chmodSync,
459
+ copyFileSync,
460
+ existsSync as existsSync3,
461
+ lstatSync,
462
+ mkdirSync as mkdirSync3,
463
+ mkdtempSync,
464
+ readFileSync as readFileSync4,
465
+ readdirSync,
466
+ realpathSync,
467
+ renameSync as renameSync2,
468
+ rmSync as rmSync3,
469
+ statSync as statSync2,
470
+ utimesSync,
471
+ writeFileSync as writeFileSync3
472
+ } from "fs";
473
+ import { dirname as dirname4, isAbsolute as isAbsolute3, join as join6, resolve } from "path";
474
+
475
+ // src/lib/file-lock.ts
476
+ import { randomBytes } from "crypto";
477
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
478
+ import { dirname as dirname3, join as join5 } from "path";
479
+ var syncSleeper = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
480
+
481
+ // src/lib/hook-runtime.ts
482
+ var RELEASE_PREFIX = "release-";
483
+ var MAX_MANIFEST_BYTES = 64 * 1024;
484
+ var RELEASE_GRACE_MS = 10 * 60 * 1e3;
485
+ var HOOK_RUNTIME_ENTRIES = {
486
+ // SessionStart self-healing resolves this exact CLI entry to run
487
+ // `daemon ensure`; it must travel with the immutable hook release.
488
+ prim: "dist/index.js",
489
+ "prim-hook": "dist/hooks/prim-hook.js",
490
+ "prim-pre-commit": "dist/hooks/pre-commit.js",
491
+ "prim-post-commit": "dist/hooks/post-commit.js",
492
+ "prim-post-rewrite": "dist/hooks/post-rewrite.js",
493
+ "prim-pre-tool-use": "dist/hooks/pre-tool-use.js",
494
+ "prim-post-tool-use": "dist/hooks/post-tool-use.js",
495
+ "prim-session-start": "dist/hooks/session-start.js",
496
+ "prim-session-end": "dist/hooks/session-end.js",
497
+ "prim-daemon-server": "dist/daemon/server.js",
498
+ "prim-statusline": "dist/statusline-main.js"
499
+ };
500
+ var SOURCE_ENTRIES = Object.fromEntries(
501
+ Object.entries(HOOK_RUNTIME_ENTRIES).map(([bin, target]) => [
502
+ bin,
503
+ target.replace(/^dist\//u, "")
504
+ ])
505
+ );
506
+ var STABLE_HOOK_LAUNCHER_CONTENT = `#!/bin/sh
507
+ set -eu
508
+ case "$0" in */*) prim_launcher_dir=\${0%/*} ;; *) exit 78 ;; esac
509
+ IFS= read -r prim_release_name < "$prim_launcher_dir/hook-runtime/current" || exit 69
510
+ case "$prim_release_name" in ${RELEASE_PREFIX}*) ;; *) exit 69 ;; esac
511
+ prim_release_id=\${prim_release_name#${RELEASE_PREFIX}}
512
+ case "$prim_release_id" in *[!0-9a-f]*|"") exit 69 ;; esac
513
+ [ "\${#prim_release_id}" -eq 64 ] || exit 69
514
+ prim_release="$prim_launcher_dir/hook-runtime/releases/$prim_release_name"
515
+ case "\${1-}" in
516
+ ${Object.entries(HOOK_RUNTIME_ENTRIES).map(([bin, entry]) => ` ${bin}) prim_entry="$prim_release/${entry}" ;;`).join("\n")}
517
+ *) exit 64 ;;
518
+ esac
519
+ shift
520
+ IFS= read -r prim_node < "$prim_release/node" || exit 69
521
+ case "$prim_node" in /*) ;; *) exit 69 ;; esac
522
+ [ -x "$prim_node" ] && [ -f "$prim_entry" ] || exit 69
523
+ exec "$prim_node" "$prim_entry" "$@"
524
+ `;
525
+
526
+ // src/commands/claude-install.ts
527
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
528
+ import { homedir as homedir2 } from "os";
529
+ import { join as join7 } from "path";
530
+
531
+ // src/lib/git.ts
532
+ import { execFileSync } from "child_process";
533
+ import { createHash as createHash4 } from "crypto";
534
+ import { lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
535
+ import { basename, dirname as dirname5, isAbsolute as isAbsolute4, relative, resolve as resolve2, sep } from "path";
536
+ var GIT_TIMEOUT_MS = 1e3;
537
+ var GITHUB_OWNER_PATTERN = /^(?![A-Za-z0-9-]*--)[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
538
+ var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]{1,100}$/u;
539
+ function gitToplevel(cwd) {
540
+ try {
541
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
542
+ cwd,
543
+ encoding: "utf-8",
544
+ stdio: ["ignore", "pipe", "ignore"],
545
+ timeout: GIT_TIMEOUT_MS
546
+ }).trim();
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+ function gitValue(cwd, args) {
552
+ try {
553
+ const value = execFileSync("git", args, {
554
+ cwd,
555
+ encoding: "utf-8",
556
+ stdio: ["ignore", "pipe", "ignore"],
557
+ timeout: GIT_TIMEOUT_MS
558
+ }).trim();
559
+ return value || void 0;
560
+ } catch {
561
+ return void 0;
562
+ }
563
+ }
564
+ function githubRepositoryFullName(cwd) {
565
+ const remote = gitValue(cwd, ["config", "--get", "remote.origin.url"]);
566
+ if (!remote || [...remote].some((char) => char < " " || char === "\x7F")) return null;
567
+ const match = /^(?:git@github\.com:([^?#]+)|(?:https|ssh):\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/([^?#]+))$/i.exec(
568
+ remote
569
+ );
570
+ const path = match?.[1] ?? match?.[2];
571
+ if (!path) return null;
572
+ const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
573
+ if (parts.length !== 2 || !GITHUB_OWNER_PATTERN.test(parts[0] ?? "") || !GITHUB_REPOSITORY_PATTERN.test(parts[1] ?? "")) {
574
+ return null;
575
+ }
576
+ return `${parts[0]}/${parts[1]}`;
577
+ }
578
+ function normalizeOriginRemote(remote) {
579
+ const value = remote.trim();
580
+ if (!value) return void 0;
581
+ const scp = !value.includes("://") ? /^(?:[^@/\s]+@)?(?<host>[^:/\s]+):(?<path>.+)$/.exec(value)?.groups : void 0;
582
+ if (scp?.host && scp.path) {
583
+ const path = scp.path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
584
+ return path ? `${scp.host.toLowerCase()}/${path}` : void 0;
585
+ }
586
+ try {
587
+ const url = new URL(value);
588
+ url.username = "";
589
+ url.password = "";
590
+ url.search = "";
591
+ url.hash = "";
592
+ const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
593
+ if (url.host && path) return `${url.host.toLowerCase()}/${path}`;
594
+ return void 0;
595
+ } catch {
596
+ return void 0;
597
+ }
598
+ }
599
+ function opaqueRepoKey(source, value) {
600
+ return `repo_v1_${createHash4("sha256").update(`${source}\0${value}`).digest("hex")}`;
601
+ }
602
+ function resolveRepositoryContext(cwd = process.cwd()) {
603
+ const top = gitToplevel(cwd);
604
+ if (!top) return null;
605
+ let repoRoot;
606
+ try {
607
+ repoRoot = realpathSync2.native(top);
608
+ } catch {
609
+ repoRoot = resolve2(top);
610
+ }
611
+ const origin = gitValue(repoRoot, ["config", "--get", "remote.origin.url"]);
612
+ const repoFullName = githubRepositoryFullName(repoRoot) ?? void 0;
613
+ const normalizedOrigin = origin ? normalizeOriginRemote(origin) : void 0;
614
+ if (normalizedOrigin) {
615
+ return {
616
+ repoRoot,
617
+ repoKey: opaqueRepoKey("origin", normalizedOrigin),
618
+ identitySource: "origin",
619
+ repoFullName
620
+ };
621
+ }
622
+ const roots = gitValue(repoRoot, ["rev-list", "--max-parents=0", "HEAD"])?.split(/\s+/).filter(Boolean).sort();
623
+ if (roots && roots.length > 0) {
624
+ return {
625
+ repoRoot,
626
+ repoKey: opaqueRepoKey("root_commit", roots.join("\n")),
627
+ identitySource: "root_commit",
628
+ repoFullName
629
+ };
630
+ }
631
+ return { repoRoot, repoFullName };
632
+ }
633
+
634
+ // src/commands/claude-install.ts
635
+ var CAPTURE_BIN = "prim-hook";
636
+ var GATE_BIN = "prim-pre-tool-use";
637
+ var POST_TOOL_USE_BIN = "prim-post-tool-use";
638
+ var SESSION_START_BIN = "prim-session-start";
639
+ var SESSION_END_BIN = "prim-session-end";
640
+ var STATUSLINE_BIN = "prim-statusline";
641
+ var STATUSLINE_ARGS = "";
642
+ var STATUSLINE_COMMAND = stableHookCommand(STATUSLINE_BIN, STATUSLINE_ARGS);
643
+ var USER_SCOPE_PATH = join7(homedir2(), ".claude", "settings.json");
644
+ function projectRoot() {
645
+ return gitToplevel() ?? process.cwd();
646
+ }
647
+ var CAPTURE_EVENTS = [
648
+ "SessionStart",
649
+ "UserPromptSubmit",
650
+ "PreToolUse",
651
+ "PostToolUse",
652
+ "PostToolUseFailure",
653
+ "Stop",
654
+ "SessionEnd",
655
+ "SubagentStop"
656
+ ];
657
+ function makeRegistration(event, matcher, bin, args = "") {
658
+ return {
659
+ event,
660
+ matcher,
661
+ bin,
662
+ command: stableHookCommand(bin, args)
663
+ };
664
+ }
665
+ function makeDetachedRegistration(event, matcher, bin, args = "") {
666
+ return { event, matcher, bin, command: detachedHookShimCommand(bin, args) };
667
+ }
668
+ var REGISTRATIONS = [
669
+ ...CAPTURE_EVENTS.map(
670
+ (event) => event === "SessionEnd" ? makeDetachedRegistration(event, "*", CAPTURE_BIN) : makeRegistration(event, "*", CAPTURE_BIN)
671
+ ),
672
+ makeRegistration("PreToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", GATE_BIN),
673
+ makeRegistration("PostToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", POST_TOOL_USE_BIN),
674
+ makeRegistration(
675
+ "PostToolUseFailure",
676
+ "Edit|Write|MultiEdit|NotebookEdit|Bash",
677
+ POST_TOOL_USE_BIN
678
+ ),
679
+ makeRegistration("SessionStart", "*", SESSION_START_BIN),
680
+ makeDetachedRegistration("SessionEnd", "*", SESSION_END_BIN)
681
+ ];
682
+ var PRIM_PERMISSION_RULE = "Bash(npx --yes @primitive.ai/prim*)";
683
+ var LEGACY_PRIM_PERMISSION_RULES = [
684
+ "Bash(npx --yes @primitive.ai/prim@latest:*)",
685
+ "Bash(npx --yes @primitive.ai/prim:*)"
686
+ ];
687
+ var ALL_PRIM_PERMISSION_RULES = /* @__PURE__ */ new Set([
688
+ PRIM_PERMISSION_RULE,
689
+ ...LEGACY_PRIM_PERMISSION_RULES
690
+ ]);
691
+
692
+ // src/commands/cursor-install.ts
693
+ var CAPTURE_BIN2 = "prim-hook";
694
+ var GATE_BIN2 = "prim-pre-tool-use";
695
+ var POST_TOOL_BIN = "prim-post-tool-use";
696
+ var SESSION_START_BIN2 = "prim-session-start";
697
+ var SESSION_END_BIN2 = "prim-session-end";
698
+ var STATUSLINE_BIN2 = "prim-statusline";
699
+ var CURSOR_EVENTS = [
700
+ "sessionStart",
701
+ "beforeSubmitPrompt",
702
+ "preToolUse",
703
+ "postToolUse",
704
+ "postToolUseFailure",
705
+ "afterAgentResponse",
706
+ "subagentStop",
707
+ "stop",
708
+ "sessionEnd"
709
+ ];
710
+ var REGISTRATIONS2 = [
711
+ ...CURSOR_EVENTS.map((event) => ({ event, bin: CAPTURE_BIN2 })),
712
+ { event: "preToolUse", bin: GATE_BIN2, matcher: "Write|Delete|Shell", timeout: 10 },
713
+ { event: "postToolUse", bin: POST_TOOL_BIN },
714
+ { event: "postToolUseFailure", bin: POST_TOOL_BIN },
715
+ { event: "sessionStart", bin: SESSION_START_BIN2 },
716
+ { event: "sessionEnd", bin: SESSION_END_BIN2 }
717
+ ];
718
+ function cursorConfigDirectory(env = process.env, home = homedir3(), platform = process.platform) {
719
+ const explicit = env.CURSOR_CONFIG_DIR?.trim();
720
+ if (explicit && isAbsolute5(explicit)) return explicit;
721
+ const xdg = env.XDG_CONFIG_HOME?.trim();
722
+ if (platform !== "darwin" && xdg && isAbsolute5(xdg)) return join8(xdg, "cursor");
723
+ return join8(home, ".cursor");
724
+ }
725
+ function cursorHooksPath(scope, root = projectRoot()) {
726
+ return scope === "user" ? join8(cursorConfigDirectory(), "hooks.json") : join8(root, ".cursor", "hooks.json");
727
+ }
728
+ function plainObject(value) {
729
+ return typeof value === "object" && value !== null && !Array.isArray(value);
730
+ }
731
+ function readCursorHooks(path) {
732
+ if (!existsSync5(path)) return {};
733
+ let parsed;
734
+ try {
735
+ parsed = JSON.parse(readFileSync6(path, "utf8"));
736
+ } catch {
737
+ throw new Error(`invalid Cursor hooks JSON at ${path}`);
738
+ }
739
+ if (!plainObject(parsed)) throw new Error(`Cursor hooks config must be an object: ${path}`);
740
+ if (parsed.version !== void 0 && parsed.version !== 1) {
741
+ throw new Error(`unsupported Cursor hooks version at ${path} (expected version 1)`);
742
+ }
743
+ if (parsed.hooks !== void 0) {
744
+ if (!plainObject(parsed.hooks)) throw new Error(`Cursor hooks must be an object: ${path}`);
745
+ for (const [event, entries] of Object.entries(parsed.hooks)) {
746
+ if (!Array.isArray(entries) || !entries.every(plainObject)) {
747
+ throw new Error(`Cursor hook list must contain objects: ${event} in ${path}`);
748
+ }
749
+ }
750
+ }
751
+ return parsed;
752
+ }
753
+ function commandFor(registration, scope) {
754
+ return stableHookCommand(
755
+ registration.bin,
756
+ `--agent cursor --event ${registration.event} --scope ${scope}`
757
+ );
758
+ }
759
+ var OWNED_COMMANDS = new Set(
760
+ ["project", "user"].flatMap(
761
+ (scope) => REGISTRATIONS2.map((registration) => commandFor(registration, scope))
762
+ )
763
+ );
764
+ function jsoncUnsafeCommand(command) {
765
+ return command.replace('case "$1" in [/]* ', 'case "$1" in /*').replace("*[/][/]*", "*//*").replace("*[/].[/]*", "*/./*").replace("*[/]..[/]*", "*/../*").replace("*[/].", "*/.").replace("*[/]..", "*/..").replace("?*[/]", "?*/");
766
+ }
767
+ var JSONC_UNSAFE_OWNED_COMMANDS = new Set([...OWNED_COMMANDS].map(jsoncUnsafeCommand));
768
+ function hookFor(registration, scope) {
769
+ return {
770
+ command: commandFor(registration, scope),
771
+ ...registration.matcher ? { matcher: registration.matcher } : {},
772
+ ...registration.timeout ? { timeout: registration.timeout } : {}
773
+ };
774
+ }
775
+ function exactHook(entry, registration, scope) {
776
+ return JSON.stringify(entry) === JSON.stringify(hookFor(registration, scope));
777
+ }
778
+ function hasExactCursorHandler(settings, event, bin, scope) {
779
+ const registration = REGISTRATIONS2.find(
780
+ (candidate) => candidate.event === event && candidate.bin === bin
781
+ );
782
+ return registration ? (settings.hooks?.[event] ?? []).some((entry) => exactHook(entry, registration, scope)) : false;
783
+ }
784
+ var FOOTER = {
785
+ type: "command",
786
+ command: stableHookCommand(STATUSLINE_BIN2),
787
+ updateIntervalMs: 1e3,
788
+ timeoutMs: 2e3
789
+ };
790
+ var JSONC_UNSAFE_FOOTER = {
791
+ ...FOOTER,
792
+ command: jsoncUnsafeCommand(FOOTER.command)
793
+ };
794
+
795
+ // src/hooks/cursor-coexistence.ts
796
+ var CURSOR_EVENTS2 = /* @__PURE__ */ new Set([
797
+ "sessionStart",
798
+ "beforeSubmitPrompt",
799
+ "preToolUse",
800
+ "postToolUse",
801
+ "postToolUseFailure",
802
+ "afterAgentResponse",
803
+ "subagentStop",
804
+ "stop",
805
+ "sessionEnd"
806
+ ]);
807
+ function cursorEvidence(parsed) {
808
+ return typeof parsed.cursor_version === "string" && parsed.cursor_version.length > 0 && typeof parsed.conversation_id === "string" && parsed.conversation_id.length > 0 && typeof parsed.generation_id === "string" && parsed.generation_id.length > 0 && Array.isArray(parsed.workspace_roots) && typeof parsed.hook_event_name === "string" && CURSOR_EVENTS2.has(parsed.hook_event_name);
809
+ }
810
+ function uniqueProjectRoot(parsed) {
811
+ if (!Array.isArray(parsed.workspace_roots)) return;
812
+ const roots = new Set(
813
+ parsed.workspace_roots.filter((value) => typeof value === "string" && value.length > 0).map((value) => resolveRepositoryContext(value)?.repoRoot).filter((value) => !!value)
814
+ );
815
+ return roots.size === 1 ? [...roots][0] : void 0;
816
+ }
817
+ function shouldSuppressImportedCursorHandler(parsed, bin) {
818
+ if (!cursorEvidence(parsed)) return false;
819
+ const project = uniqueProjectRoot(parsed);
820
+ if (project) {
821
+ try {
822
+ if (hasExactCursorHandler(
823
+ readCursorHooks(cursorHooksPath("project", project)),
824
+ parsed.hook_event_name,
825
+ bin,
826
+ "project"
827
+ )) {
828
+ return true;
829
+ }
830
+ } catch {
831
+ }
832
+ }
833
+ try {
834
+ return hasExactCursorHandler(
835
+ readCursorHooks(cursorHooksPath("user")),
836
+ parsed.hook_event_name,
837
+ bin,
838
+ "user"
839
+ );
840
+ } catch {
841
+ return false;
842
+ }
843
+ }
844
+
845
+ // src/hooks/hook-stdin.ts
846
+ var DEFAULT_MAX_BYTES = 1024 * 1024;
847
+ function readHookStdin(timeoutMs = 1e3, maxBytes = DEFAULT_MAX_BYTES) {
848
+ return new Promise((resolve4, reject) => {
849
+ const chunks = [];
850
+ let bytes = 0;
851
+ let settled = false;
852
+ const finish = (error) => {
853
+ if (settled) return;
854
+ settled = true;
855
+ clearTimeout(timer);
856
+ process.stdin.removeAllListeners("data");
857
+ process.stdin.removeAllListeners("end");
858
+ process.stdin.removeAllListeners("error");
859
+ if (error) reject(error);
860
+ else resolve4(Buffer.concat(chunks, bytes).toString("utf-8"));
861
+ };
862
+ const timer = setTimeout(() => finish(new Error("stdin read timeout")), timeoutMs);
863
+ process.stdin.on("data", (chunk) => {
864
+ bytes += chunk.length;
865
+ if (bytes > maxBytes) {
866
+ finish(new Error("stdin envelope exceeds size limit"));
867
+ return;
868
+ }
869
+ chunks.push(chunk);
870
+ });
871
+ process.stdin.on("end", () => finish());
872
+ process.stdin.on("error", (error) => finish(error));
873
+ });
874
+ }
875
+
398
876
  // src/hooks/normalize.ts
399
- import { createHash as createHash3 } from "crypto";
877
+ import { createHash as createHash5 } from "crypto";
878
+ import { realpathSync as realpathSync3 } from "fs";
879
+ import { isAbsolute as isAbsolute6, resolve as resolve3 } from "path";
400
880
  var HERMES_EVENT_MAP = {
401
881
  on_session_start: "SessionStart",
402
882
  on_session_end: "SessionEnd",
@@ -406,7 +886,125 @@ var HERMES_EVENT_MAP = {
406
886
  post_tool_call: "PostToolUse",
407
887
  subagent_stop: "SubagentStop"
408
888
  };
889
+ var CURSOR_EVENT_MAP = {
890
+ sessionStart: "SessionStart",
891
+ beforeSubmitPrompt: "UserPromptSubmit",
892
+ preToolUse: "PreToolUse",
893
+ postToolUse: "PostToolUse",
894
+ postToolUseFailure: "PostToolUseFailure",
895
+ afterAgentResponse: "AssistantResponse",
896
+ subagentStop: "SubagentStop",
897
+ stop: "Stop",
898
+ sessionEnd: "SessionEnd"
899
+ };
900
+ var CURSOR_TOOL_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
901
+ var SAFE_CORRELATION_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/u;
902
+ var CURSOR_LOCAL_ONLY_FIELDS = /* @__PURE__ */ new Set([
903
+ "conversation_id",
904
+ "generation_id",
905
+ "user_email",
906
+ "account_email",
907
+ "transcript_path",
908
+ "transcript",
909
+ "thought",
910
+ "thoughts",
911
+ "thinking",
912
+ "tool_output"
913
+ ]);
914
+ function requiredId(value, field) {
915
+ if (typeof value !== "string" || value.length === 0) {
916
+ throw new TypeError(`Cursor hook envelope is missing ${field}`);
917
+ }
918
+ return value;
919
+ }
920
+ function cursorToolUseId(value) {
921
+ if (SAFE_CORRELATION_ID_RE.test(value)) return value;
922
+ return `cursor:tool:v1:${createHash5("sha256").update(value).digest("hex")}`;
923
+ }
924
+ function workspaceCandidates(value) {
925
+ if (!Array.isArray(value)) return [];
926
+ const roots = value.filter((item) => typeof item === "string" && item.length > 0);
927
+ if (roots.some((root) => !isAbsolute6(root))) {
928
+ throw new TypeError("Cursor workspace roots must be absolute");
929
+ }
930
+ return roots;
931
+ }
932
+ function pathIdentity(path) {
933
+ try {
934
+ return realpathSync3.native(path);
935
+ } catch {
936
+ return resolve3(path);
937
+ }
938
+ }
939
+ function resolveCursorCwd(parsed) {
940
+ const roots = workspaceCandidates(parsed.workspace_roots).map(pathIdentity);
941
+ const rootRepositories = roots.map((root) => resolveRepositoryContext(root)?.repoRoot);
942
+ const repositoryRoots = new Set(rootRepositories.filter((root) => !!root));
943
+ if (repositoryRoots.size > 1) {
944
+ throw new TypeError("Cursor hook envelope spans multiple repositories");
945
+ }
946
+ if (roots.length > 1 && rootRepositories.some((root) => root === void 0)) {
947
+ throw new TypeError("Cursor hook envelope has ambiguous workspace roots");
948
+ }
949
+ const expectedRepo = [...repositoryRoots][0];
950
+ const rawCwd = typeof parsed.cwd === "string" && parsed.cwd.length > 0 ? parsed.cwd : void 0;
951
+ if (rawCwd && !isAbsolute6(rawCwd)) {
952
+ throw new TypeError("Cursor hook cwd must be absolute");
953
+ }
954
+ const envelopeCwd = rawCwd ? pathIdentity(rawCwd) : void 0;
955
+ const input = typeof parsed.tool_input === "object" && parsed.tool_input !== null && !Array.isArray(parsed.tool_input) ? parsed.tool_input : void 0;
956
+ const rawToolCwd = parsed.tool_name === "Shell" && typeof input?.working_directory === "string" ? input.working_directory : void 0;
957
+ const base = envelopeCwd ?? (roots.length === 1 ? roots[0] : expectedRepo);
958
+ if (rawToolCwd && !isAbsolute6(rawToolCwd) && !base) {
959
+ throw new TypeError("relative Cursor tool cwd has no workspace base");
960
+ }
961
+ const toolCwd = rawToolCwd ? pathIdentity(isAbsolute6(rawToolCwd) ? rawToolCwd : resolve3(base, rawToolCwd)) : void 0;
962
+ const selected = toolCwd ?? envelopeCwd ?? (roots.length === 1 ? roots[0] : expectedRepo);
963
+ if (!selected) {
964
+ throw new TypeError("Cursor hook envelope has no unambiguous workspace");
965
+ }
966
+ const selectedRepo = resolveRepositoryContext(selected)?.repoRoot;
967
+ if (expectedRepo && selectedRepo !== expectedRepo) {
968
+ throw new TypeError("Cursor hook cwd conflicts with workspace repository");
969
+ }
970
+ return selected;
971
+ }
972
+ function normalizeCursorEnvelope(parsed) {
973
+ const rawEvent = requiredId(parsed.hook_event_name, "hook_event_name");
974
+ const event = CURSOR_EVENT_MAP[rawEvent];
975
+ if (!event) throw new TypeError(`unsupported Cursor hook event: ${rawEvent}`);
976
+ const sessionId = requiredId(parsed.conversation_id, "conversation_id");
977
+ const turnId = requiredId(parsed.generation_id, "generation_id");
978
+ if (parsed.session_id !== void 0 && parsed.session_id !== sessionId) {
979
+ throw new TypeError("Cursor conversation_id conflicts with session_id");
980
+ }
981
+ if (parsed.turn_id !== void 0 && parsed.turn_id !== turnId) {
982
+ throw new TypeError("Cursor generation_id conflicts with turn_id");
983
+ }
984
+ const rawToolUseId = CURSOR_TOOL_EVENTS.has(event) ? requiredId(parsed.tool_use_id, "tool_use_id") : void 0;
985
+ const normalized = {
986
+ ...Object.fromEntries(
987
+ Object.entries(parsed).filter(([key]) => !CURSOR_LOCAL_ONLY_FIELDS.has(key))
988
+ ),
989
+ hook_event_name: event,
990
+ session_id: sessionId,
991
+ turn_id: turnId,
992
+ cwd: resolveCursorCwd(parsed),
993
+ ...rawToolUseId === void 0 ? {} : { tool_use_id: cursorToolUseId(rawToolUseId) }
994
+ };
995
+ if (typeof parsed.tool_output === "string") {
996
+ try {
997
+ normalized.tool_response = JSON.parse(parsed.tool_output);
998
+ } catch {
999
+ normalized.tool_response = parsed.tool_output;
1000
+ }
1001
+ }
1002
+ return normalized;
1003
+ }
409
1004
  function normalizeEnvelope(parsed, agent) {
1005
+ if (agent === "cursor") {
1006
+ return normalizeCursorEnvelope(parsed);
1007
+ }
410
1008
  if (agent === "codex") {
411
1009
  const event = parsed.hook_event_name;
412
1010
  if ((event === "PreToolUse" || event === "PostToolUse") && (typeof parsed.tool_use_id !== "string" || parsed.tool_use_id.length === 0)) {
@@ -418,7 +1016,7 @@ function normalizeEnvelope(parsed, agent) {
418
1016
  ]);
419
1017
  return {
420
1018
  ...parsed,
421
- tool_use_id: `codex:fallback:v1:${createHash3("sha256").update(stableToolIdentity).digest("hex")}`
1019
+ tool_use_id: `codex:fallback:v1:${createHash5("sha256").update(stableToolIdentity).digest("hex")}`
422
1020
  };
423
1021
  }
424
1022
  return parsed;
@@ -436,23 +1034,6 @@ function normalizeEnvelope(parsed, agent) {
436
1034
  // src/hooks/session-end.ts
437
1035
  var STDIN_TIMEOUT_MS = 1e3;
438
1036
  var DAEMON_TIMEOUT_MS = 250;
439
- function readStdin() {
440
- return new Promise((resolve, reject) => {
441
- const chunks = [];
442
- const timer = setTimeout(() => {
443
- reject(new Error("stdin read timeout"));
444
- }, STDIN_TIMEOUT_MS);
445
- process.stdin.on("data", (chunk) => chunks.push(chunk));
446
- process.stdin.on("end", () => {
447
- clearTimeout(timer);
448
- resolve(Buffer.concat(chunks).toString("utf-8"));
449
- });
450
- process.stdin.on("error", (err) => {
451
- clearTimeout(timer);
452
- reject(err);
453
- });
454
- });
455
- }
456
1037
  function emit() {
457
1038
  process.stdout.write("{}\n");
458
1039
  }
@@ -460,17 +1041,19 @@ async function main() {
460
1041
  const agent = parseAgent(process.argv);
461
1042
  let raw;
462
1043
  try {
463
- raw = await readStdin();
1044
+ raw = await readHookStdin(STDIN_TIMEOUT_MS);
464
1045
  } catch {
465
1046
  emit();
466
1047
  return;
467
1048
  }
468
1049
  let envelope;
469
1050
  try {
470
- envelope = normalizeEnvelope(
471
- JSON.parse(raw),
472
- agent
473
- );
1051
+ const incoming = JSON.parse(raw);
1052
+ if (agent !== "cursor" && shouldSuppressImportedCursorHandler(incoming, "prim-session-end")) {
1053
+ emit();
1054
+ return;
1055
+ }
1056
+ envelope = normalizeEnvelope(incoming, agent);
474
1057
  } catch {
475
1058
  emit();
476
1059
  return;