@fusengine/harness 0.1.89 → 0.1.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.
- package/dist/adapters/claude/index.d.mts +1 -1
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +2 -2
- package/dist/adapters/codex/index.mjs +5 -4
- package/dist/adapters/cursor/index.d.mts +15 -28
- package/dist/adapters/cursor/index.mjs +35 -46
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.d.mts +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.d.mts +1 -1
- package/dist/adapters/kimi/index.mjs +1 -1
- package/dist/{claude-5bC8TDAz.mjs → claude-Ckv2_TgP.mjs} +3 -43
- package/dist/cli/bin.mjs +429 -16
- package/dist/cli/index.mjs +1 -1
- package/dist/command-string-CALMTnwN.mjs +42 -0
- package/dist/{evaluate-C1VSI5pb.mjs → evaluate-CMiIqHeH.mjs} +261 -13
- package/dist/{handle-DHhukRdh.mjs → handle-C43gA-Pr.mjs} +737 -150
- package/dist/{hermes-kP6NUQlG.mjs → hermes-B9-p_3IF.mjs} +3 -2
- package/dist/{index-DBgnIZn9.d.mts → index-B8PG82su.d.mts} +1 -1
- package/dist/{index-D4C2-IIc.d.mts → index-SmKYkk2N.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/init/index.d.mts +1 -1
- package/dist/init/index.mjs +1 -1
- package/dist/{kimi-D6X7b8wF.mjs → kimi-G2wcSh5-.mjs} +3 -2
- package/dist/normalize-BjG6unTj.mjs +317 -0
- package/dist/permission-mode-B-SFyR0X.mjs +226 -0
- package/dist/policy/index.d.mts +1 -1
- package/dist/policy/index.mjs +2 -2
- package/dist/prompt/index.d.mts +1 -1
- package/dist/{run-CdIlUtjv.mjs → run-DkrzC0gb.mjs} +1 -1
- package/dist/{run-TAXNRSpD.mjs → run-GHAMqcOn.mjs} +26 -3
- package/dist/runtime/index.d.mts +27 -16
- package/dist/runtime/index.mjs +2 -2
- package/dist/{types-C0-igRKr.d.mts → types-Bh0jEF3_.d.mts} +2 -0
- package/dist/{validate-DZFtAbLP.mjs → validate-KjZ1X9tH.mjs} +1 -1
- package/package.json +3 -2
- package/src/adapters/claude/index.ts +92 -0
- package/src/adapters/cline/index.ts +36 -0
- package/src/adapters/codex/apply-patch.ts +94 -0
- package/src/adapters/codex/index.ts +84 -0
- package/src/adapters/codex/permission-mode.ts +16 -0
- package/src/adapters/cursor/context.ts +46 -0
- package/src/adapters/cursor/events.ts +38 -0
- package/src/adapters/cursor/index.ts +43 -0
- package/src/adapters/cursor/interfaces/types.ts +88 -0
- package/src/adapters/cursor/native-response.ts +158 -0
- package/src/adapters/cursor/normalize.ts +123 -0
- package/src/adapters/cursor/respond.ts +123 -0
- package/src/adapters/gemini/index.ts +36 -0
- package/src/adapters/hermes/index.ts +55 -0
- package/src/adapters/hermes/interfaces/types.ts +26 -0
- package/src/adapters/kimi/index.ts +96 -0
- package/src/adapters/kimi/interfaces/types.ts +55 -0
- package/src/cache/compact.ts +55 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/io.ts +38 -0
- package/src/cache/mcp-response.ts +39 -0
- package/src/cache/mcp-store.ts +70 -0
- package/src/cache/store.ts +93 -0
- package/src/changelog/fetch.ts +79 -0
- package/src/cli/bin.ts +100 -0
- package/src/cli/cursor-event-scanner.ts +175 -0
- package/src/cli/cursor-stdin-reader.ts +65 -0
- package/src/cli/doctor.ts +96 -0
- package/src/cli/hook-io.ts +130 -0
- package/src/cli/hook-sound.ts +49 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/json-primitive-scanner.ts +83 -0
- package/src/cli/run.ts +36 -0
- package/src/cli/scope.ts +31 -0
- package/src/codex-rules/build.ts +63 -0
- package/src/codex-rules/index.ts +8 -0
- package/src/codex-rules/mapping.ts +42 -0
- package/src/codex-rules/rules/absolute-paths.ts +52 -0
- package/src/codex-rules/rules/ask.ts +44 -0
- package/src/codex-rules/rules/chmod-ask.ts +57 -0
- package/src/codex-rules/rules/dd.ts +23 -0
- package/src/codex-rules/rules/diskutil.ts +33 -0
- package/src/codex-rules/rules/mkfs.ts +42 -0
- package/src/codex-rules/rules/privilege.ts +49 -0
- package/src/codex-rules/rules/remote-fetch.ts +31 -0
- package/src/codex-rules/rules/rm-variants.ts +58 -0
- package/src/codex-rules/rules/single-commands.ts +57 -0
- package/src/codex-rules/skip-list.ts +25 -0
- package/src/codex-rules/starlark.ts +46 -0
- package/src/codex-rules/types.ts +36 -0
- package/src/config/dotenv.ts +55 -0
- package/src/config/env.ts +10 -0
- package/src/config/home-dir.ts +36 -0
- package/src/config/index.ts +5 -0
- package/src/config/layout.ts +52 -0
- package/src/config/limits.ts +56 -0
- package/src/config/ttl.ts +30 -0
- package/src/detect/harness.ts +73 -0
- package/src/detect/index.ts +1 -0
- package/src/detect/interfaces/types.ts +18 -0
- package/src/freshness/agent-evidence-record.ts +95 -0
- package/src/freshness/agent-evidence.ts +95 -0
- package/src/freshness/codex-spawn-evidence.ts +91 -0
- package/src/freshness/doc-helpers.ts +90 -0
- package/src/freshness/evidence-harvest-io.ts +72 -0
- package/src/freshness/evidence-harvest.ts +61 -0
- package/src/freshness/explore-tools.ts +95 -0
- package/src/freshness/index.ts +2 -0
- package/src/freshness/query-framework.ts +73 -0
- package/src/freshness/ref-evidence.ts +49 -0
- package/src/freshness/ref-journal.ts +98 -0
- package/src/freshness/trivial-edit-counter.ts +30 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +2 -0
- package/src/init/run.ts +32 -0
- package/src/init/templates.ts +75 -0
- package/src/memory/gitignore.ts +14 -0
- package/src/memory/index.ts +3 -0
- package/src/memory/registry.ts +39 -0
- package/src/memory/session-roots.ts +99 -0
- package/src/memory/state.ts +58 -0
- package/src/policy/apex-authorization.ts +100 -0
- package/src/policy/apex-gates.ts +91 -0
- package/src/policy/apex-target-steps.ts +73 -0
- package/src/policy/apex-target.ts +49 -0
- package/src/policy/apex-task-context.ts +81 -0
- package/src/policy/apex.ts +94 -0
- package/src/policy/cartographer/build-tree.ts +69 -0
- package/src/policy/cartographer/describe.ts +53 -0
- package/src/policy/cartographer/entry.ts +38 -0
- package/src/policy/cartographer/frontmatter.ts +48 -0
- package/src/policy/cartographer/indicators.ts +34 -0
- package/src/policy/claude-md-context.ts +66 -0
- package/src/policy/conventions/index.ts +11 -0
- package/src/policy/conventions/interfaces.ts +71 -0
- package/src/policy/conventions/langs.ts +53 -0
- package/src/policy/conventions/query.ts +49 -0
- package/src/policy/conventions/react-hooks.ts +18 -0
- package/src/policy/conventions/store-or-query.ts +21 -0
- package/src/policy/conventions/stores.ts +56 -0
- package/src/policy/conventions/strip.ts +87 -0
- package/src/policy/conventions/verdict.ts +38 -0
- package/src/policy/creation-intent.ts +11 -0
- package/src/policy/deny-loop.ts +88 -0
- package/src/policy/design/allowed-write.ts +28 -0
- package/src/policy/design/content-checks.ts +45 -0
- package/src/policy/design/corpus-fs.ts +21 -0
- package/src/policy/design/corpus-resolve-cache.ts +49 -0
- package/src/policy/design/corpus-resolve.ts +99 -0
- package/src/policy/design/corpus.ts +92 -0
- package/src/policy/design/design-system-rules.ts +45 -0
- package/src/policy/design/flag.ts +30 -0
- package/src/policy/design/gates-pipeline.ts +105 -0
- package/src/policy/design/gates.ts +87 -0
- package/src/policy/design/screenshot-tools.ts +24 -0
- package/src/policy/design/skill-evidence.ts +34 -0
- package/src/policy/design/skill-gate.ts +81 -0
- package/src/policy/design/skill-triggers.ts +51 -0
- package/src/policy/design/state.ts +92 -0
- package/src/policy/design/template-urls.ts +54 -0
- package/src/policy/design/transitions.ts +68 -0
- package/src/policy/detect-claude-md-project-type.ts +32 -0
- package/src/policy/detect-framework.ts +99 -0
- package/src/policy/detect-primitive-checks.ts +100 -0
- package/src/policy/detect-primitive-lib.ts +55 -0
- package/src/policy/detect-project.ts +99 -0
- package/src/policy/edit-outcome.ts +53 -0
- package/src/policy/evaluate.ts +81 -0
- package/src/policy/expert-agents.ts +60 -0
- package/src/policy/file-size-scope.ts +32 -0
- package/src/policy/file-size.ts +99 -0
- package/src/policy/framework-solid-exclude.ts +52 -0
- package/src/policy/framework-solid-extended.ts +93 -0
- package/src/policy/framework-solid-gates-systems.ts +99 -0
- package/src/policy/framework-solid-gates.ts +98 -0
- package/src/policy/framework-solid.ts +88 -0
- package/src/policy/gemini-mcp-gate.ts +83 -0
- package/src/policy/git-gates.ts +40 -0
- package/src/policy/guards/bash-command-anchor.ts +40 -0
- package/src/policy/guards/bash-write-patterns.ts +140 -0
- package/src/policy/guards/bash-write-redirects.ts +201 -0
- package/src/policy/guards/bash-write-safe-paths.ts +62 -0
- package/src/policy/guards/bash-write.ts +76 -0
- package/src/policy/guards/context.ts +12 -0
- package/src/policy/guards/index.ts +61 -0
- package/src/policy/guards/install.ts +22 -0
- package/src/policy/guards/interface-separation-ext.ts +41 -0
- package/src/policy/guards/interface-separation.ts +100 -0
- package/src/policy/guards/protected-path.ts +100 -0
- package/src/policy/guards/security.ts +97 -0
- package/src/policy/index.ts +19 -0
- package/src/policy/interfaces/types.ts +46 -0
- package/src/policy/js-gate-route.ts +51 -0
- package/src/policy/lessons/lesson-gate.ts +0 -0
- package/src/policy/lessons/trigger-index.ts +84 -0
- package/src/policy/lessons/types.ts +31 -0
- package/src/policy/module-layout.ts +31 -0
- package/src/policy/nearest-manifest.ts +75 -0
- package/src/policy/never-approval.ts +95 -0
- package/src/policy/patterns.ts +80 -0
- package/src/policy/seo/validate.ts +51 -0
- package/src/policy/shadcn-project.ts +22 -0
- package/src/policy/shadcn-skill-gate.ts +97 -0
- package/src/policy/shell-read-refs.ts +96 -0
- package/src/policy/skill-path.ts +46 -0
- package/src/policy/skill-patterns/laravel.ts +40 -0
- package/src/policy/skill-patterns/nextjs.ts +29 -0
- package/src/policy/skill-patterns/react.ts +27 -0
- package/src/policy/skill-patterns/shadcn-subskills.ts +33 -0
- package/src/policy/skill-patterns/shadcn.ts +66 -0
- package/src/policy/skill-patterns/swift.ts +30 -0
- package/src/policy/skill-patterns/tailwind.ts +40 -0
- package/src/policy/skill-source.ts +52 -0
- package/src/policy/skill-trigger-patterns.ts +33 -0
- package/src/policy/skill-triggers.ts +108 -0
- package/src/policy/tailwind-skill-gate.ts +63 -0
- package/src/policy/trivial-edits.ts +52 -0
- package/src/policy/verbosity.ts +39 -0
- package/src/prompt/index.ts +1 -0
- package/src/prompt/types.ts +36 -0
- package/src/refs/discover.ts +69 -0
- package/src/refs/frontmatter.ts +24 -0
- package/src/refs/index.ts +4 -0
- package/src/refs/loader.ts +59 -0
- package/src/refs/ref-key.ts +41 -0
- package/src/refs/router.ts +63 -0
- package/src/refs/types.ts +25 -0
- package/src/runtime/activity.ts +99 -0
- package/src/runtime/apply-patch-gate.ts +41 -0
- package/src/runtime/burst-window.ts +21 -0
- package/src/runtime/codex-shell-tool.ts +37 -0
- package/src/runtime/command-string.ts +37 -0
- package/src/runtime/confirm/codex-confirm.ts +126 -0
- package/src/runtime/confirm/codex-prompt-origin.ts +16 -0
- package/src/runtime/confirm/confirm-code.ts +33 -0
- package/src/runtime/confirm/confirm-gate.ts +65 -0
- package/src/runtime/confirm/confirm-irreversible.ts +32 -0
- package/src/runtime/confirm/confirm-pending.ts +34 -0
- package/src/runtime/confirm/confirm-state.ts +56 -0
- package/src/runtime/confirm/confirm-subagent.ts +112 -0
- package/src/runtime/confirm/confirm-submit.ts +38 -0
- package/src/runtime/core-guards-active.ts +84 -0
- package/src/runtime/core-guards-wired.ts +47 -0
- package/src/runtime/deny-loop-store.ts +94 -0
- package/src/runtime/deny-notice.ts +52 -0
- package/src/runtime/design-cache-resolve.ts +51 -0
- package/src/runtime/design-content-gate.ts +95 -0
- package/src/runtime/design-files-gate.ts +114 -0
- package/src/runtime/design-helpers.ts +93 -0
- package/src/runtime/design-lifecycle.ts +39 -0
- package/src/runtime/design-read-credit.ts +28 -0
- package/src/runtime/design.ts +116 -0
- package/src/runtime/dev-context.ts +43 -0
- package/src/runtime/dry-patterns.ts +32 -0
- package/src/runtime/dry.ts +96 -0
- package/src/runtime/fragment-registry.ts +33 -0
- package/src/runtime/framework-skill-gate.ts +79 -0
- package/src/runtime/fs-cleanup.ts +71 -0
- package/src/runtime/gate-apex.ts +81 -0
- package/src/runtime/gate-helpers.ts +52 -0
- package/src/runtime/gate-input.ts +26 -0
- package/src/runtime/gate.ts +128 -0
- package/src/runtime/handle-post.ts +104 -0
- package/src/runtime/handle-pre.ts +137 -0
- package/src/runtime/handle-scope-async.ts +29 -0
- package/src/runtime/handle-types.ts +27 -0
- package/src/runtime/handle.ts +116 -0
- package/src/runtime/home-state.ts +55 -0
- package/src/runtime/index.ts +18 -0
- package/src/runtime/inform.ts +28 -0
- package/src/runtime/inject-budget-recap.ts +47 -0
- package/src/runtime/inject-budget.ts +81 -0
- package/src/runtime/inject-context.ts +85 -0
- package/src/runtime/inject-dedup-exclusive.ts +98 -0
- package/src/runtime/inject-dedup.ts +89 -0
- package/src/runtime/is-agent-tool.ts +19 -0
- package/src/runtime/lifecycle/agent-files.ts +49 -0
- package/src/runtime/lifecycle/agent-memory.ts +82 -0
- package/src/runtime/lifecycle/agent-transcript.ts +80 -0
- package/src/runtime/lifecycle/aipilot/analytics.ts +99 -0
- package/src/runtime/lifecycle/aipilot/apex-task-store.ts +61 -0
- package/src/runtime/lifecycle/aipilot/cache-base.ts +48 -0
- package/src/runtime/lifecycle/aipilot/cache-doc.ts +100 -0
- package/src/runtime/lifecycle/aipilot/cache-lessons.ts +58 -0
- package/src/runtime/lifecycle/aipilot/cache-test.ts +70 -0
- package/src/runtime/lifecycle/aipilot/curate-lessons.ts +64 -0
- package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +100 -0
- package/src/runtime/lifecycle/aipilot/doc-cache-gate.ts +69 -0
- package/src/runtime/lifecycle/aipilot/inject-apex.ts +98 -0
- package/src/runtime/lifecycle/aipilot/inject-doc.ts +62 -0
- package/src/runtime/lifecycle/aipilot/inject-explore.ts +64 -0
- package/src/runtime/lifecycle/aipilot/inject-lessons.ts +61 -0
- package/src/runtime/lifecycle/aipilot/inject-test.ts +48 -0
- package/src/runtime/lifecycle/aipilot/lesson-archive.ts +60 -0
- package/src/runtime/lifecycle/aipilot/lesson-inject.ts +98 -0
- package/src/runtime/lifecycle/aipilot/lesson-parse.ts +74 -0
- package/src/runtime/lifecycle/aipilot/lessons.ts +72 -0
- package/src/runtime/lifecycle/aipilot/promote-global-lessons.ts +92 -0
- package/src/runtime/lifecycle/aipilot/solid-compliance.ts +69 -0
- package/src/runtime/lifecycle/aipilot/solid-notice.ts +47 -0
- package/src/runtime/lifecycle/aipilot/solid-transcript.ts +60 -0
- package/src/runtime/lifecycle/aipilot/source-scan.ts +56 -0
- package/src/runtime/lifecycle/aipilot/sync-task.ts +75 -0
- package/src/runtime/lifecycle/aipilot/transcript.ts +88 -0
- package/src/runtime/lifecycle/aipilot/types.ts +72 -0
- package/src/runtime/lifecycle/auto-document-reads.ts +73 -0
- package/src/runtime/lifecycle/cartographer/detect.ts +57 -0
- package/src/runtime/lifecycle/cartographer/ecosystem-map.ts +56 -0
- package/src/runtime/lifecycle/cartographer/fs-util.ts +64 -0
- package/src/runtime/lifecycle/cartographer/merge.ts +51 -0
- package/src/runtime/lifecycle/cartographer/project-map.ts +49 -0
- package/src/runtime/lifecycle/cartographer/scan-hooks.ts +48 -0
- package/src/runtime/lifecycle/cartographer/scan.ts +81 -0
- package/src/runtime/lifecycle/cartographer/session-start.ts +33 -0
- package/src/runtime/lifecycle/cartographer/track-enrichment.ts +45 -0
- package/src/runtime/lifecycle/cartographer/write-plugin-map.ts +43 -0
- package/src/runtime/lifecycle/cartographer/write-tree.ts +44 -0
- package/src/runtime/lifecycle/changelog-research.ts +43 -0
- package/src/runtime/lifecycle/check-file-size.ts +61 -0
- package/src/runtime/lifecycle/codex-resync/fingerprint.ts +63 -0
- package/src/runtime/lifecycle/codex-resync/lock.ts +46 -0
- package/src/runtime/lifecycle/codex-resync/managed-destination.ts +39 -0
- package/src/runtime/lifecycle/codex-resync/materialize-agents.ts +57 -0
- package/src/runtime/lifecycle/codex-resync/plugin-files.ts +65 -0
- package/src/runtime/lifecycle/codex-resync/plugin-roots.ts +62 -0
- package/src/runtime/lifecycle/codex-resync/resync.ts +45 -0
- package/src/runtime/lifecycle/codex-resync/symlink-commands.ts +25 -0
- package/src/runtime/lifecycle/dispatch.ts +109 -0
- package/src/runtime/lifecycle/failure-lesson.ts +51 -0
- package/src/runtime/lifecycle/index.ts +34 -0
- package/src/runtime/lifecycle/inject-rules.ts +50 -0
- package/src/runtime/lifecycle/instructions-loaded.ts +22 -0
- package/src/runtime/lifecycle/kimi-rules-native.ts +44 -0
- package/src/runtime/lifecycle/lessons/dispatch.ts +88 -0
- package/src/runtime/lifecycle/lessons/reminder.ts +89 -0
- package/src/runtime/lifecycle/lessons/state.ts +23 -0
- package/src/runtime/lifecycle/memory/agent-lesson.ts +35 -0
- package/src/runtime/lifecycle/memory/capture-error.ts +41 -0
- package/src/runtime/lifecycle/memory/client.ts +65 -0
- package/src/runtime/lifecycle/memory/dispatch.ts +42 -0
- package/src/runtime/lifecycle/memory/recall.ts +30 -0
- package/src/runtime/lifecycle/memory/salience.ts +31 -0
- package/src/runtime/lifecycle/memory/state.ts +49 -0
- package/src/runtime/lifecycle/memory/track-ops.ts +15 -0
- package/src/runtime/lifecycle/post-compact.ts +38 -0
- package/src/runtime/lifecycle/post-edit-ts.ts +49 -0
- package/src/runtime/lifecycle/post-tracking.ts +40 -0
- package/src/runtime/lifecycle/pre-compact.ts +38 -0
- package/src/runtime/lifecycle/rules-root.ts +83 -0
- package/src/runtime/lifecycle/security/check-skill.ts +63 -0
- package/src/runtime/lifecycle/security/scan-patterns.ts +53 -0
- package/src/runtime/lifecycle/security/scan.ts +76 -0
- package/src/runtime/lifecycle/security/skill-state.ts +47 -0
- package/src/runtime/lifecycle/security/track-mcp.ts +29 -0
- package/src/runtime/lifecycle/security/track-skill-read.ts +31 -0
- package/src/runtime/lifecycle/seo/post-tool-use.ts +45 -0
- package/src/runtime/lifecycle/session-end.ts +17 -0
- package/src/runtime/lifecycle/session-start.ts +57 -0
- package/src/runtime/lifecycle/snapshot/board.ts +27 -0
- package/src/runtime/lifecycle/snapshot/format.ts +60 -0
- package/src/runtime/lifecycle/snapshot/git.ts +71 -0
- package/src/runtime/lifecycle/snapshot/index.ts +52 -0
- package/src/runtime/lifecycle/snapshot/version.ts +34 -0
- package/src/runtime/lifecycle/solid-detect.ts +56 -0
- package/src/runtime/lifecycle/stop-core.ts +35 -0
- package/src/runtime/lifecycle/subagent-cache.ts +61 -0
- package/src/runtime/lifecycle/task-completed.ts +94 -0
- package/src/runtime/lifecycle/teammate-idle-check.ts +63 -0
- package/src/runtime/lifecycle/teammate-idle.ts +28 -0
- package/src/runtime/lifecycle/tool-failure.ts +24 -0
- package/src/runtime/lifecycle/track-changes.ts +61 -0
- package/src/runtime/lifecycle/validate-solid.ts +70 -0
- package/src/runtime/lifecycle/validate-tailwind.ts +80 -0
- package/src/runtime/lifecycle-bridge.ts +74 -0
- package/src/runtime/mcp-key.ts +44 -0
- package/src/runtime/mcp-tool-name.ts +92 -0
- package/src/runtime/mcp.ts +91 -0
- package/src/runtime/modular.ts +77 -0
- package/src/runtime/normalize.ts +107 -0
- package/src/runtime/notices.ts +91 -0
- package/src/runtime/notification-sound.ts +49 -0
- package/src/runtime/notifications.ts +77 -0
- package/src/runtime/paths.ts +59 -0
- package/src/runtime/post-fanout.ts +41 -0
- package/src/runtime/post-outcome.ts +61 -0
- package/src/runtime/pre-allow.ts +97 -0
- package/src/runtime/precommit.ts +60 -0
- package/src/runtime/prompt-text.ts +38 -0
- package/src/runtime/receipt-capture.ts +34 -0
- package/src/runtime/record.ts +26 -0
- package/src/runtime/respond.ts +92 -0
- package/src/runtime/solid-file-size-gate.ts +58 -0
- package/src/runtime/solid-pre.ts +53 -0
- package/src/runtime/storage.ts +9 -0
- package/src/statusline/colors.ts +31 -0
- package/src/statusline/constants.ts +17 -0
- package/src/statusline/formatters.ts +54 -0
- package/src/statusline/index.ts +4 -0
- package/src/statusline/progress-bar.ts +46 -0
- package/src/tracking/codex-post-failure.ts +73 -0
- package/src/tracking/index.ts +2 -0
- package/src/tracking/integrity.ts +99 -0
- package/src/tracking/one-shot-dedup.ts +34 -0
- package/src/tracking/one-shot-failure.ts +42 -0
- package/src/tracking/one-shot-store.ts +95 -0
- package/src/tracking/one-shot.ts +93 -0
- package/src/tracking/receipts.ts +95 -0
- package/src/tracking/session-state.ts +94 -0
- package/src/tracking/store.ts +96 -0
- package/src/tracking/track-compact.ts +100 -0
- package/src/tracking/track-diff.ts +50 -0
- package/src/tracking/track-journal.ts +97 -0
- package/src/tracking/track-lock-sync.ts +83 -0
- package/src/tracking/track-lock.ts +67 -0
- package/src/util/compact-json.ts +23 -0
- package/src/util/index.ts +2 -0
- package/src/util/json-io.ts +47 -0
- package/src/util/project-root.ts +41 -0
- package/src/util/runtime-io.ts +85 -0
- package/src/util/semver.ts +30 -0
- package/dist/permission-mode-B0MUl7FT.mjs +0 -136
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CursorEventContract } from "./interfaces/types";
|
|
2
|
+
|
|
3
|
+
const EVENT_CONTRACTS = {
|
|
4
|
+
sessionStart: { phase: "pre", lifecycle: "SessionStart", response: "session-context", blockable: false, known: true },
|
|
5
|
+
sessionEnd: { phase: "post", lifecycle: "SessionEnd", response: "neutral", blockable: false, known: true },
|
|
6
|
+
beforeSubmitPrompt: { phase: "pre", lifecycle: "UserPromptSubmit", response: "submit-control", blockable: true, known: true },
|
|
7
|
+
preCompact: { phase: "pre", lifecycle: "PreCompact", response: "compact-notice", blockable: false, known: true },
|
|
8
|
+
subagentStart: { phase: "pre", lifecycle: "SubagentStart", response: "permission", blockable: true, known: true },
|
|
9
|
+
subagentStop: { phase: "post", lifecycle: "SubagentStop", response: "followup", blockable: false, known: true },
|
|
10
|
+
preToolUse: { phase: "pre", lifecycle: "PreToolUse", response: "permission", blockable: true, known: true },
|
|
11
|
+
postToolUse: { phase: "post", lifecycle: "PostToolUse", response: "post-context", blockable: false, known: true },
|
|
12
|
+
postToolUseFailure: { phase: "post", lifecycle: "PostToolUseFailure", response: "neutral", blockable: false, known: true },
|
|
13
|
+
beforeShellExecution: { phase: "pre", lifecycle: "BeforeShellExecution", response: "permission", blockable: true, known: true },
|
|
14
|
+
afterShellExecution: { phase: "post", lifecycle: "AfterShellExecution", response: "neutral", blockable: false, known: true },
|
|
15
|
+
beforeMCPExecution: { phase: "pre", lifecycle: "BeforeMCPExecution", response: "permission", blockable: true, known: true },
|
|
16
|
+
afterMCPExecution: { phase: "post", lifecycle: "AfterMCPExecution", response: "neutral", blockable: false, known: true },
|
|
17
|
+
beforeReadFile: { phase: "pre", lifecycle: "BeforeReadFile", response: "permission", blockable: true, known: true },
|
|
18
|
+
afterFileEdit: { phase: "post", lifecycle: "AfterFileEdit", response: "neutral", blockable: false, known: true },
|
|
19
|
+
beforeTabFileRead: { phase: "pre", lifecycle: "BeforeTabFileRead", response: "permission", blockable: true, known: true },
|
|
20
|
+
afterTabFileEdit: { phase: "post", lifecycle: "AfterTabFileEdit", response: "neutral", blockable: false, known: true },
|
|
21
|
+
afterAgentResponse: { phase: "post", lifecycle: "AfterAgentResponse", response: "neutral", blockable: false, known: true },
|
|
22
|
+
afterAgentThought: { phase: "post", lifecycle: "AfterAgentThought", response: "neutral", blockable: false, known: true },
|
|
23
|
+
stop: { phase: "post", lifecycle: "Stop", response: "followup", blockable: false, known: true },
|
|
24
|
+
workspaceOpen: { phase: "pre", lifecycle: "WorkspaceOpen", response: "plugin-paths", blockable: false, known: true },
|
|
25
|
+
} as const satisfies Record<string, CursorEventContract>;
|
|
26
|
+
|
|
27
|
+
const UNKNOWN_EVENT: CursorEventContract = {
|
|
28
|
+
phase: "post",
|
|
29
|
+
lifecycle: null,
|
|
30
|
+
response: "neutral",
|
|
31
|
+
blockable: false,
|
|
32
|
+
known: false,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Return explicit routing and response metadata for a Cursor event name. */
|
|
36
|
+
export function cursorEventContract(eventName: string): CursorEventContract {
|
|
37
|
+
return EVENT_CONTRACTS[eventName as keyof typeof EVENT_CONTRACTS] ?? UNKNOWN_EVENT;
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Cursor hook adapter; post-edit handling remains observe-only. */
|
|
2
|
+
import { evaluate } from "../../policy/evaluate";
|
|
3
|
+
import { formatPrompt } from "../../prompt/types";
|
|
4
|
+
import { extractCursorEvent } from "./normalize";
|
|
5
|
+
import type { CursorShellPayload, CursorToolPayload, CursorEditPayload, CursorResponse, CursorEditResponse } from "./interfaces/types";
|
|
6
|
+
|
|
7
|
+
export type { CursorShellPayload, CursorToolPayload, CursorEditPayload, CursorResponse, CursorEditResponse } from "./interfaces/types";
|
|
8
|
+
|
|
9
|
+
function guardCursor(payload: object): CursorResponse {
|
|
10
|
+
const event = extractCursorEvent(payload);
|
|
11
|
+
const result = evaluate({ tool: event.tool, filePath: event.filePath, content: event.content, oldString: event.oldString, command: event.command });
|
|
12
|
+
if (result.decision === "allow" || !result.prompt) return { permission: "allow" };
|
|
13
|
+
const message = formatPrompt(result.prompt);
|
|
14
|
+
const userMessage = result.prompt.kind === "ask"
|
|
15
|
+
? `[downgraded from ask — Cursor preToolUse does not reliably enforce ask]\n${message}`
|
|
16
|
+
: message;
|
|
17
|
+
return { permission: "deny", user_message: userMessage, agent_message: message };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function namedPayload(payload: object, eventName: string): object {
|
|
21
|
+
return Object.hasOwn(payload, "hook_event_name") ? payload : { ...payload, hook_event_name: eventName };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Guard a shell command (git/install policies). */
|
|
25
|
+
export function beforeShellExecution(payload: CursorShellPayload): CursorResponse {
|
|
26
|
+
return guardCursor(namedPayload(payload, "beforeShellExecution"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Guard a generic Cursor tool call using the same extraction as the runtime. */
|
|
30
|
+
export function preToolUse(payload: CursorToolPayload): CursorResponse {
|
|
31
|
+
return guardCursor(namedPayload(payload, "preToolUse"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Complete an observed file edit without emitting pre-execution permission
|
|
36
|
+
* fields. Cursor does not define a callback schema for this post hook.
|
|
37
|
+
* @param payload - The `afterFileEdit` stdin payload.
|
|
38
|
+
* @returns An empty successful response.
|
|
39
|
+
*/
|
|
40
|
+
export function afterFileEdit(payload: CursorEditPayload): CursorEditResponse {
|
|
41
|
+
void payload;
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** `beforeShellExecution` stdin payload (subset). */
|
|
2
|
+
export interface CursorShellPayload {
|
|
3
|
+
command?: string;
|
|
4
|
+
cwd?: string;
|
|
5
|
+
sandbox?: boolean;
|
|
6
|
+
workspace_roots?: string[];
|
|
7
|
+
hook_event_name?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** `preToolUse` stdin payload subset consumed by the adapter. */
|
|
11
|
+
export interface CursorToolPayload {
|
|
12
|
+
tool_name?: string;
|
|
13
|
+
tool_input?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `afterFileEdit` stdin payload (subset). */
|
|
17
|
+
export interface CursorEditPayload {
|
|
18
|
+
file_path?: string;
|
|
19
|
+
edits?: { old_string: string; new_string: string }[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One extracted Cursor file edit used by runtime post fan-out. */
|
|
23
|
+
export interface CursorExtractedFile {
|
|
24
|
+
filePath: string;
|
|
25
|
+
oldString?: string;
|
|
26
|
+
content: string;
|
|
27
|
+
op: "update";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Shared Cursor extraction result consumed by runtime and public adapter. */
|
|
31
|
+
export interface CursorExtractedEvent {
|
|
32
|
+
eventName: string;
|
|
33
|
+
lifecycleEvent: string | null;
|
|
34
|
+
responseKind: CursorResponseKind;
|
|
35
|
+
blockable: boolean;
|
|
36
|
+
cwd?: string;
|
|
37
|
+
workspaceRoots?: string[];
|
|
38
|
+
phase: "pre" | "post";
|
|
39
|
+
tool: string;
|
|
40
|
+
input: Record<string, unknown>;
|
|
41
|
+
filePath?: string;
|
|
42
|
+
content?: string;
|
|
43
|
+
oldString?: string;
|
|
44
|
+
command?: string;
|
|
45
|
+
/** Distinct beforeMCPExecution root and nested commands, in wire order. */
|
|
46
|
+
commandCandidates?: string[];
|
|
47
|
+
files?: CursorExtractedFile[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Native Cursor stdout contract selected for one hook event. */
|
|
51
|
+
export type CursorResponseKind =
|
|
52
|
+
| "permission"
|
|
53
|
+
| "post-context"
|
|
54
|
+
| "session-context"
|
|
55
|
+
| "submit-control"
|
|
56
|
+
| "followup"
|
|
57
|
+
| "compact-notice"
|
|
58
|
+
| "plugin-paths"
|
|
59
|
+
| "neutral";
|
|
60
|
+
|
|
61
|
+
/** Routing metadata for a documented Cursor lifecycle event. */
|
|
62
|
+
export interface CursorEventContract {
|
|
63
|
+
phase: "pre" | "post";
|
|
64
|
+
lifecycle: string | null;
|
|
65
|
+
response: CursorResponseKind;
|
|
66
|
+
blockable: boolean;
|
|
67
|
+
known: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Empty afterFileEdit callback. Cursor documents no output fields for this
|
|
72
|
+
* post hook, so pre-execution permission fields are intentionally impossible.
|
|
73
|
+
*/
|
|
74
|
+
export type CursorEditResponse = Record<string, never>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `beforeShellExecution` stdout response. Message keys are snake_case:
|
|
78
|
+
* Cursor silently ignores camelCase `userMessage`/`agentMessage` (#141516,
|
|
79
|
+
* regression persists through v2.0.77+ — forum #142589), matching the
|
|
80
|
+
* schema respond.ts emits for the cursor harness.
|
|
81
|
+
*/
|
|
82
|
+
export interface CursorResponse {
|
|
83
|
+
permission: "allow" | "deny" | "ask";
|
|
84
|
+
/** User-visible message — snake_case required (#141516, #142589). */
|
|
85
|
+
user_message?: string;
|
|
86
|
+
/** Agent-visible message — snake_case required (#141516, #142589). */
|
|
87
|
+
agent_message?: string;
|
|
88
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
type FieldValidator = (value: unknown) => boolean;
|
|
2
|
+
|
|
3
|
+
interface NativeSchema {
|
|
4
|
+
fields: Readonly<Record<string, FieldValidator>>;
|
|
5
|
+
required?: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const stringValue: FieldValidator = (value) => typeof value === "string";
|
|
9
|
+
const booleanValue: FieldValidator = (value) => typeof value === "boolean";
|
|
10
|
+
const plainRecord = (value: unknown): value is Record<string, unknown> => {
|
|
11
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
12
|
+
try {
|
|
13
|
+
const prototype = Object.getPrototypeOf(value);
|
|
14
|
+
return prototype === Object.prototype || prototype === null;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type JsonFrame = { value: unknown; leave?: false } | { value: object; leave: true };
|
|
21
|
+
|
|
22
|
+
function jsonChildren(value: object): unknown[] | null {
|
|
23
|
+
const keys = Reflect.ownKeys(value);
|
|
24
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) return null;
|
|
27
|
+
const length = descriptors.length;
|
|
28
|
+
if (!length || !("value" in length) || !Number.isSafeInteger(length.value) || length.value < 0) return null;
|
|
29
|
+
if (keys.length !== length.value + 1 || keys.some((key) => typeof key === "symbol")) return null;
|
|
30
|
+
const children: unknown[] = [];
|
|
31
|
+
for (let index = 0; index < length.value; index += 1) {
|
|
32
|
+
const descriptor = descriptors[String(index)];
|
|
33
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) return null;
|
|
34
|
+
children.push(descriptor.value);
|
|
35
|
+
}
|
|
36
|
+
return children;
|
|
37
|
+
}
|
|
38
|
+
if (!plainRecord(value) || keys.some((key) => typeof key === "symbol")) return null;
|
|
39
|
+
const children: unknown[] = [];
|
|
40
|
+
for (const key of keys) {
|
|
41
|
+
const descriptor = descriptors[key as string];
|
|
42
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) return null;
|
|
43
|
+
children.push(descriptor.value);
|
|
44
|
+
}
|
|
45
|
+
return children;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function jsonValue(root: unknown): boolean {
|
|
49
|
+
const active = new WeakSet<object>();
|
|
50
|
+
const stack: JsonFrame[] = [{ value: root }];
|
|
51
|
+
while (stack.length > 0) {
|
|
52
|
+
const frame = stack.pop()!;
|
|
53
|
+
if (frame.leave) {
|
|
54
|
+
active.delete(frame.value);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const { value } = frame;
|
|
58
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") continue;
|
|
59
|
+
if (typeof value === "number") {
|
|
60
|
+
if (!Number.isFinite(value)) return false;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (typeof value !== "object" || active.has(value)) return false;
|
|
64
|
+
let children: unknown[] | null;
|
|
65
|
+
try {
|
|
66
|
+
children = jsonChildren(value);
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (!children) return false;
|
|
71
|
+
active.add(value);
|
|
72
|
+
stack.push({ value, leave: true });
|
|
73
|
+
for (let index = children.length - 1; index >= 0; index -= 1) stack.push({ value: children[index] });
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const recordValue: FieldValidator = (value) => plainRecord(value) && jsonValue(value);
|
|
79
|
+
const stringRecord: FieldValidator = (value) => {
|
|
80
|
+
if (!recordValue(value)) return false;
|
|
81
|
+
try {
|
|
82
|
+
return Object.values(Object.getOwnPropertyDescriptors(value as object))
|
|
83
|
+
.every((descriptor) => "value" in descriptor && typeof descriptor.value === "string");
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const stringArray: FieldValidator = (value) => Array.isArray(value) && value.every(stringValue);
|
|
89
|
+
const permission = (...values: string[]): FieldValidator => (value) => typeof value === "string" && values.includes(value);
|
|
90
|
+
|
|
91
|
+
const EMPTY: NativeSchema = { fields: {} };
|
|
92
|
+
const FOLLOWUP: NativeSchema = { fields: { followup_message: stringValue } };
|
|
93
|
+
const PERMISSION_ASK: NativeSchema = {
|
|
94
|
+
fields: { permission: permission("allow", "deny", "ask"), user_message: stringValue, agent_message: stringValue },
|
|
95
|
+
required: ["permission"],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const NATIVE_SCHEMAS = {
|
|
99
|
+
sessionStart: {
|
|
100
|
+
fields: { env: stringRecord, additional_context: stringValue, continue: booleanValue, user_message: stringValue },
|
|
101
|
+
},
|
|
102
|
+
sessionEnd: EMPTY,
|
|
103
|
+
beforeSubmitPrompt: { fields: { continue: booleanValue, user_message: stringValue }, required: ["continue"] },
|
|
104
|
+
preCompact: { fields: { user_message: stringValue } },
|
|
105
|
+
subagentStart: {
|
|
106
|
+
fields: { permission: permission("allow", "deny"), user_message: stringValue }, required: ["permission"],
|
|
107
|
+
},
|
|
108
|
+
subagentStop: FOLLOWUP,
|
|
109
|
+
preToolUse: {
|
|
110
|
+
fields: { ...PERMISSION_ASK.fields, updated_input: recordValue }, required: ["permission"],
|
|
111
|
+
},
|
|
112
|
+
postToolUse: { fields: { updated_mcp_tool_output: recordValue, additional_context: stringValue } },
|
|
113
|
+
postToolUseFailure: EMPTY,
|
|
114
|
+
beforeShellExecution: PERMISSION_ASK,
|
|
115
|
+
afterShellExecution: EMPTY,
|
|
116
|
+
beforeMCPExecution: PERMISSION_ASK,
|
|
117
|
+
afterMCPExecution: EMPTY,
|
|
118
|
+
beforeReadFile: {
|
|
119
|
+
fields: { permission: permission("allow", "deny"), user_message: stringValue }, required: ["permission"],
|
|
120
|
+
},
|
|
121
|
+
afterFileEdit: EMPTY,
|
|
122
|
+
beforeTabFileRead: { fields: { permission: permission("allow", "deny") }, required: ["permission"] },
|
|
123
|
+
afterTabFileEdit: EMPTY,
|
|
124
|
+
afterAgentResponse: EMPTY,
|
|
125
|
+
afterAgentThought: EMPTY,
|
|
126
|
+
stop: FOLLOWUP,
|
|
127
|
+
workspaceOpen: { fields: { pluginPaths: stringArray } },
|
|
128
|
+
} as const satisfies Record<string, NativeSchema>;
|
|
129
|
+
|
|
130
|
+
function isNativeCursorResponse(value: unknown, eventName: string): boolean {
|
|
131
|
+
try {
|
|
132
|
+
if (!recordValue(value)) return false;
|
|
133
|
+
const schema = NATIVE_SCHEMAS[eventName as keyof typeof NATIVE_SCHEMAS] as NativeSchema | undefined;
|
|
134
|
+
if (!schema) return false;
|
|
135
|
+
const descriptors = Object.getOwnPropertyDescriptors(value as object);
|
|
136
|
+
if (schema.required?.some((field) => !Object.hasOwn(descriptors, field))) return false;
|
|
137
|
+
return Reflect.ownKeys(descriptors).every((field) => {
|
|
138
|
+
if (typeof field !== "string") return false;
|
|
139
|
+
const descriptor = descriptors[field];
|
|
140
|
+
if (!Object.hasOwn(schema.fields, field)) return false;
|
|
141
|
+
const validate = schema.fields[field];
|
|
142
|
+
return descriptor?.enumerable === true && "value" in descriptor
|
|
143
|
+
&& validate !== undefined && validate(descriptor.value);
|
|
144
|
+
});
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Preserve raw stdout only when JSON.parse proves a documented native Cursor response. */
|
|
151
|
+
export function parseNativeCursorStdout(stdout: string, eventName: string): string | null {
|
|
152
|
+
if (typeof stdout !== "string") return null;
|
|
153
|
+
try {
|
|
154
|
+
return isNativeCursorResponse(JSON.parse(stdout), eventName) ? stdout : null;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { commandToString } from "../../runtime/command-string";
|
|
2
|
+
import { cursorEventContract } from "./events";
|
|
3
|
+
import { cursorAbsolutePath, cursorPath, cursorWorkspaceRoots } from "./context";
|
|
4
|
+
import type { CursorExtractedEvent } from "./interfaces/types";
|
|
5
|
+
|
|
6
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
8
|
+
? value as Record<string, unknown>
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function str(value: unknown): string | undefined {
|
|
13
|
+
return typeof value === "string" ? value : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function inputRecord(value: unknown): Record<string, unknown> | undefined {
|
|
17
|
+
const direct = record(value);
|
|
18
|
+
if (direct || typeof value !== "string") return direct;
|
|
19
|
+
try {
|
|
20
|
+
return record(JSON.parse(value));
|
|
21
|
+
} catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MCP_ROOT_FIELDS = ["mcp_server_name", "mcp_server_url", "url", "result_json", "duration"] as const;
|
|
27
|
+
|
|
28
|
+
function cursorMcpInput(raw: Record<string, unknown>, input: Record<string, unknown>): Record<string, unknown> {
|
|
29
|
+
const merged = { ...input };
|
|
30
|
+
for (const field of MCP_ROOT_FIELDS) {
|
|
31
|
+
if (Object.hasOwn(raw, field)) merged[field] = raw[field];
|
|
32
|
+
}
|
|
33
|
+
return merged;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sanitizedCursorInput(input: Record<string, unknown>): Record<string, unknown> {
|
|
37
|
+
const safe = { ...input };
|
|
38
|
+
if (typeof safe.file_path === "string" && cursorPath(safe.file_path) === undefined) delete safe.file_path;
|
|
39
|
+
if (typeof safe.path === "string" && cursorPath(safe.path) === undefined) delete safe.path;
|
|
40
|
+
if (Object.hasOwn(safe, "cwd")) {
|
|
41
|
+
const cwd = cursorAbsolutePath(safe.cwd);
|
|
42
|
+
if (cwd) safe.cwd = cwd;
|
|
43
|
+
else delete safe.cwd;
|
|
44
|
+
}
|
|
45
|
+
if (Object.hasOwn(safe, "workspace_roots")) {
|
|
46
|
+
if (Array.isArray(safe.workspace_roots)) safe.workspace_roots = cursorWorkspaceRoots(safe.workspace_roots);
|
|
47
|
+
else delete safe.workspace_roots;
|
|
48
|
+
}
|
|
49
|
+
return safe;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cursorToolName(raw: Record<string, unknown>, event: string, tool: string | undefined, hasCommand: boolean): string {
|
|
53
|
+
if (hasCommand) return "Bash";
|
|
54
|
+
const server = str(raw.mcp_server_name)?.trim().replace(/[^A-Za-z0-9_-]+/g, "_");
|
|
55
|
+
if (/^(before|after)MCPExecution$/i.test(event) && server && tool && !tool.startsWith("mcp__")) {
|
|
56
|
+
return `mcp__${server}__${tool}`;
|
|
57
|
+
}
|
|
58
|
+
if (tool === "Write") return "Edit";
|
|
59
|
+
return tool ?? "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cursorCommands(raw: Record<string, unknown>, input: Record<string, unknown>): string[] {
|
|
63
|
+
const candidates = [commandToString(raw.command), commandToString(input.command)]
|
|
64
|
+
.filter((candidate): candidate is string => candidate !== undefined);
|
|
65
|
+
return [...new Set(candidates)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract one Cursor hook payload into the shared runtime shape.
|
|
70
|
+
*
|
|
71
|
+
* @param payload - Cursor hook stdin payload.
|
|
72
|
+
* @returns Cursor-specific phase, tool, command, and edit fields.
|
|
73
|
+
*/
|
|
74
|
+
export function extractCursorEvent(payload: object): CursorExtractedEvent {
|
|
75
|
+
const raw = payload as Record<string, unknown>;
|
|
76
|
+
const hookEvent = str(raw.hook_event_name) ?? "";
|
|
77
|
+
const parsedInput = inputRecord(raw.tool_input);
|
|
78
|
+
const mcpInput = parsedInput && /^(before|after)MCPExecution$/i.test(hookEvent)
|
|
79
|
+
? cursorMcpInput(raw, parsedInput)
|
|
80
|
+
: parsedInput;
|
|
81
|
+
const input = sanitizedCursorInput(mcpInput ?? raw);
|
|
82
|
+
const contract = cursorEventContract(hookEvent);
|
|
83
|
+
const metadata = {
|
|
84
|
+
eventName: hookEvent,
|
|
85
|
+
lifecycleEvent: contract.lifecycle,
|
|
86
|
+
responseKind: contract.response,
|
|
87
|
+
blockable: contract.blockable,
|
|
88
|
+
cwd: cursorAbsolutePath(raw.cwd),
|
|
89
|
+
workspaceRoots: cursorWorkspaceRoots(raw.workspace_roots),
|
|
90
|
+
};
|
|
91
|
+
if (!contract.known) {
|
|
92
|
+
return { ...metadata, phase: contract.phase, tool: "", input };
|
|
93
|
+
}
|
|
94
|
+
const afterFileEdit = /^afterFileEdit$/i.test(hookEvent);
|
|
95
|
+
const beforeReadFile = /^beforeReadFile$/i.test(hookEvent);
|
|
96
|
+
const beforeTabFileRead = /^beforeTabFileRead$/i.test(hookEvent);
|
|
97
|
+
const edits = Array.isArray(raw.edits)
|
|
98
|
+
? raw.edits.map(record).filter((edit): edit is Record<string, unknown> => edit !== undefined)
|
|
99
|
+
: [];
|
|
100
|
+
const filePath = cursorPath(raw.file_path);
|
|
101
|
+
if (filePath && edits.length > 0) {
|
|
102
|
+
const files = edits.map((edit) => ({
|
|
103
|
+
filePath,
|
|
104
|
+
oldString: str(edit.old_string),
|
|
105
|
+
content: str(edit.new_string) ?? "",
|
|
106
|
+
op: "update" as const,
|
|
107
|
+
}));
|
|
108
|
+
return { ...metadata, phase: contract.phase, tool: "Edit", input, filePath, content: files.map((file) => file.content).join("\n"), files };
|
|
109
|
+
}
|
|
110
|
+
const commands = cursorCommands(raw, input);
|
|
111
|
+
const command = commands[0];
|
|
112
|
+
return {
|
|
113
|
+
...metadata,
|
|
114
|
+
phase: contract.phase,
|
|
115
|
+
tool: afterFileEdit ? "Edit" : beforeReadFile || beforeTabFileRead ? "Read" : cursorToolName(raw, hookEvent, str(raw.tool_name), command !== undefined),
|
|
116
|
+
input,
|
|
117
|
+
filePath: cursorPath(input.file_path) ?? cursorPath(input.path),
|
|
118
|
+
content: str(input.content) ?? str(input.new_string),
|
|
119
|
+
oldString: str(input.old_string),
|
|
120
|
+
command,
|
|
121
|
+
commandCandidates: /^beforeMCPExecution$/i.test(hookEvent) && commands.length > 0 ? commands : undefined,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { formatPrompt, type Prompt } from "../../prompt/types";
|
|
2
|
+
import { cursorEventContract } from "./events";
|
|
3
|
+
import { parseNativeCursorStdout } from "./native-response";
|
|
4
|
+
|
|
5
|
+
const AGENT_MESSAGE_EVENTS = new Set([
|
|
6
|
+
"preToolUse",
|
|
7
|
+
"beforeShellExecution",
|
|
8
|
+
"beforeMCPExecution",
|
|
9
|
+
]);
|
|
10
|
+
const USER_MESSAGE_EVENTS = new Set([
|
|
11
|
+
"preToolUse",
|
|
12
|
+
"beforeShellExecution",
|
|
13
|
+
"beforeMCPExecution",
|
|
14
|
+
"beforeReadFile",
|
|
15
|
+
"subagentStart",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function permissionMessages(eventName: string, userMessage?: string, agentMessage?: string): Record<string, string> {
|
|
19
|
+
return {
|
|
20
|
+
...(userMessage && USER_MESSAGE_EVENTS.has(eventName) ? { user_message: userMessage } : {}),
|
|
21
|
+
...(agentMessage && AGENT_MESSAGE_EVENTS.has(eventName) ? { agent_message: agentMessage } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function joinMessages(...values: unknown[]): string {
|
|
26
|
+
return values.filter((value): value is string => typeof value === "string" && value.length > 0).join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Render a portable policy prompt using the native Cursor event contract. */
|
|
34
|
+
export function toCursorResponse(prompt: Prompt, eventName: string): string {
|
|
35
|
+
const contract = cursorEventContract(eventName);
|
|
36
|
+
const message = formatPrompt(prompt);
|
|
37
|
+
if (!contract.known || contract.response === "neutral" || contract.response === "plugin-paths") return "{}";
|
|
38
|
+
if (contract.response === "post-context" || contract.response === "session-context") {
|
|
39
|
+
return JSON.stringify({ additional_context: message });
|
|
40
|
+
}
|
|
41
|
+
if (contract.response === "followup") return JSON.stringify({ followup_message: message });
|
|
42
|
+
if (contract.response === "compact-notice") return JSON.stringify({ user_message: prompt.userMessage ?? message });
|
|
43
|
+
if (contract.response === "submit-control") {
|
|
44
|
+
return JSON.stringify({ continue: prompt.kind !== "block", user_message: prompt.userMessage ?? message });
|
|
45
|
+
}
|
|
46
|
+
if (prompt.kind === "inform") {
|
|
47
|
+
return JSON.stringify({
|
|
48
|
+
permission: "allow",
|
|
49
|
+
...permissionMessages(eventName, prompt.userMessage, prompt.reason ? message : undefined),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const userMessage = prompt.kind === "ask"
|
|
53
|
+
? `[downgraded from ask — Cursor does not enforce approval for this event]\n${message}`
|
|
54
|
+
: message;
|
|
55
|
+
return JSON.stringify({
|
|
56
|
+
permission: "deny",
|
|
57
|
+
...permissionMessages(eventName, userMessage, userMessage),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Convert a shared lifecycle handler's output to the native Cursor envelope. */
|
|
62
|
+
export function toCursorLifecycleResponse(stdout: string, eventName: string): string {
|
|
63
|
+
const contract = cursorEventContract(eventName);
|
|
64
|
+
const native = parseNativeCursorStdout(stdout, eventName);
|
|
65
|
+
if (native !== null) return native;
|
|
66
|
+
let text = stdout;
|
|
67
|
+
let decision: "allow" | "deny" | "ask" | undefined;
|
|
68
|
+
let userMessage = "";
|
|
69
|
+
let agentMessage = "";
|
|
70
|
+
let decisionMessage = "";
|
|
71
|
+
let structured = false;
|
|
72
|
+
try {
|
|
73
|
+
const parsed: unknown = JSON.parse(stdout);
|
|
74
|
+
const subagentAsk = eventName === "subagentStart" && isRecord(parsed)
|
|
75
|
+
&& Object.hasOwn(parsed, "permission") && parsed.permission === "ask";
|
|
76
|
+
const shared = (isRecord(parsed) ? parsed : {}) as {
|
|
77
|
+
hookSpecificOutput?: {
|
|
78
|
+
additionalContext?: string;
|
|
79
|
+
permissionDecision?: "allow" | "deny" | "ask";
|
|
80
|
+
permissionDecisionReason?: string;
|
|
81
|
+
};
|
|
82
|
+
systemMessage?: string;
|
|
83
|
+
user_message?: string;
|
|
84
|
+
reason?: string;
|
|
85
|
+
followup_message?: string;
|
|
86
|
+
};
|
|
87
|
+
structured = true;
|
|
88
|
+
decision = shared.hookSpecificOutput?.permissionDecision ?? (subagentAsk ? "ask" : undefined);
|
|
89
|
+
userMessage = shared.systemMessage
|
|
90
|
+
?? (subagentAsk && Object.hasOwn(shared, "user_message") && typeof shared.user_message === "string"
|
|
91
|
+
? shared.user_message : "");
|
|
92
|
+
decisionMessage = joinMessages(shared.hookSpecificOutput?.permissionDecisionReason, shared.reason);
|
|
93
|
+
agentMessage = joinMessages(shared.hookSpecificOutput?.additionalContext, decisionMessage);
|
|
94
|
+
text = joinMessages(agentMessage, userMessage, shared.followup_message);
|
|
95
|
+
} catch {
|
|
96
|
+
text = stdout;
|
|
97
|
+
}
|
|
98
|
+
if (contract.response === "neutral") return "{}";
|
|
99
|
+
if (!text) return contract.response === "permission" ? '{"permission":"allow"}' : "{}";
|
|
100
|
+
if (contract.response === "session-context" || contract.response === "post-context") {
|
|
101
|
+
return JSON.stringify({ additional_context: text });
|
|
102
|
+
}
|
|
103
|
+
if (contract.response === "permission") {
|
|
104
|
+
const permission = decision === "deny" || decision === "ask" ? "deny" : "allow";
|
|
105
|
+
const denied = permission === "deny";
|
|
106
|
+
// Cursor subagentStart can gate creation but has no model-context channel.
|
|
107
|
+
// Drop shared context and its "injected" notice on allow: preserving either
|
|
108
|
+
// would claim delivery the native event contract cannot perform.
|
|
109
|
+
if (eventName === "subagentStart" && !denied) return '{"permission":"allow"}';
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
permission,
|
|
112
|
+
...permissionMessages(
|
|
113
|
+
eventName,
|
|
114
|
+
userMessage || (denied ? decisionMessage || agentMessage : ""),
|
|
115
|
+
agentMessage || (denied ? decisionMessage || userMessage : structured ? "" : text),
|
|
116
|
+
),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (contract.response === "followup") return JSON.stringify({ followup_message: text });
|
|
120
|
+
if (contract.response === "compact-notice") return JSON.stringify({ user_message: text });
|
|
121
|
+
if (contract.response === "submit-control") return JSON.stringify({ continue: true, user_message: text });
|
|
122
|
+
return "{}";
|
|
123
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI adapter (hook-mode). Schema per google-gemini/gemini-cli docs/hooks (2026):
|
|
3
|
+
* `BeforeTool` blocks via `{ decision: "deny", reason }` (or exit 2).
|
|
4
|
+
*/
|
|
5
|
+
import { evaluate } from "../../policy/evaluate";
|
|
6
|
+
import { formatPrompt } from "../../prompt/types";
|
|
7
|
+
|
|
8
|
+
/** `BeforeTool` stdin payload (subset). */
|
|
9
|
+
export interface GeminiHookInput {
|
|
10
|
+
tool_name?: string;
|
|
11
|
+
tool_input?: { command?: string; path?: string; content?: string };
|
|
12
|
+
hook_event_name?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Hook stdout response. */
|
|
16
|
+
export interface GeminiResponse {
|
|
17
|
+
decision?: "allow" | "deny";
|
|
18
|
+
reason?: string;
|
|
19
|
+
hookSpecificOutput?: { additionalContext?: string };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Evaluate a tool use; deny on a hard block, otherwise inject context. */
|
|
23
|
+
export function beforeTool(input: GeminiHookInput): GeminiResponse {
|
|
24
|
+
const i = input.tool_input;
|
|
25
|
+
const r = evaluate({
|
|
26
|
+
tool: input.tool_name ?? "write_file",
|
|
27
|
+
filePath: i?.path,
|
|
28
|
+
content: i?.content,
|
|
29
|
+
command: i?.command,
|
|
30
|
+
});
|
|
31
|
+
if (r.decision === "allow" || !r.prompt) return {};
|
|
32
|
+
const msg = formatPrompt(r.prompt);
|
|
33
|
+
return r.prompt.kind === "block"
|
|
34
|
+
? { decision: "deny", reason: msg }
|
|
35
|
+
: { hookSpecificOutput: { additionalContext: msg } };
|
|
36
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermes Agent adapter (hook-mode). Nous Research's hermes-agent (2026) pipes a
|
|
3
|
+
* JSON payload to shell hooks on stdin and reads stdout back as JSON — config
|
|
4
|
+
* `~/.hermes/config.yaml` under `hooks:`, event `pre_tool_call`, scripts in
|
|
5
|
+
* `~/.hermes/agent-hooks/`. The INPUT shape matches Claude Code
|
|
6
|
+
* (`hook_event_name`/`tool_name`/`tool_input`/`cwd`), so the Claude reader is
|
|
7
|
+
* reused; the OUTPUT does not: a block is `{ decision: "block", reason }`
|
|
8
|
+
* (no `permissionDecision`, no interactive "ask"), `{}` allows, and
|
|
9
|
+
* `{ context }` injects LLM context. Tool fields verified: `terminal` takes
|
|
10
|
+
* `command`; `write_file` takes `path` + `content`.
|
|
11
|
+
*/
|
|
12
|
+
import { evaluate } from "../../policy/evaluate";
|
|
13
|
+
import { formatPrompt, type Prompt } from "../../prompt/types";
|
|
14
|
+
import { readClaudeInput } from "../claude";
|
|
15
|
+
import { commandToString } from "../../runtime/command-string";
|
|
16
|
+
import type { HermesHookInput, HermesResponse } from "./interfaces/types";
|
|
17
|
+
|
|
18
|
+
export type { HermesHookInput, HermesResponse } from "./interfaces/types";
|
|
19
|
+
|
|
20
|
+
/** Read & parse the Hermes hook stdin payload (same wire shape as Claude). */
|
|
21
|
+
export async function readHermesInput(): Promise<HermesHookInput> {
|
|
22
|
+
return await readClaudeInput();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render a portable {@link Prompt} as a Hermes hook response string:
|
|
27
|
+
* `block` -> `{decision:"block",reason}`; `ask`/`inform` -> non-blocking
|
|
28
|
+
* `{context}` — Hermes has no interactive "ask" state, so both degrade to
|
|
29
|
+
* context injection, mirroring the gemini-cli/cline routing in `respond()`.
|
|
30
|
+
* @param prompt - The portable policy prompt.
|
|
31
|
+
* @returns The native Hermes JSON response string.
|
|
32
|
+
*/
|
|
33
|
+
export function toHermesResponse(prompt: Prompt): string {
|
|
34
|
+
const msg = formatPrompt(prompt);
|
|
35
|
+
const res: HermesResponse = prompt.kind === "block" ? { decision: "block", reason: msg } : { context: msg };
|
|
36
|
+
return JSON.stringify(res);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run the bundled policy over a Hermes `pre_tool_call` payload and return the
|
|
41
|
+
* native response string, or null to allow (the hook then prints `{}`).
|
|
42
|
+
* @param input - Parsed `pre_tool_call` stdin payload.
|
|
43
|
+
* @returns Native response JSON, or null when the tool use is allowed.
|
|
44
|
+
*/
|
|
45
|
+
export function guard(input: HermesHookInput): string | null {
|
|
46
|
+
const t = input.tool_input;
|
|
47
|
+
const r = evaluate({
|
|
48
|
+
tool: input.tool_name ?? "write_file",
|
|
49
|
+
filePath: t?.path ?? t?.file_path,
|
|
50
|
+
content: t?.content,
|
|
51
|
+
command: commandToString(t?.command),
|
|
52
|
+
});
|
|
53
|
+
if (r.decision === "allow" || !r.prompt) return null;
|
|
54
|
+
return toHermesResponse(r.prompt);
|
|
55
|
+
}
|