heyspex 0.1.0 → 0.2.0
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/README.md +21 -0
- package/bin/hs.mjs +931 -98
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +231 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +449 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +441 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +819 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +95 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +906 -21
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +660 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +29 -9
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// Install the HeySpex lifecycle hooks into Cursor (`.cursor/hooks.json`), at
|
|
2
|
+
// parity with what `.claude/settings.json` registers for Claude Code.
|
|
3
|
+
//
|
|
4
|
+
// The registration itself lives in scripts/cursor-hooks/manifest.mjs (shared
|
|
5
|
+
// with heyspex-doctor.mjs and the smoke test); this file only knows how to
|
|
6
|
+
// merge it into Cursor's config file without disturbing anything it does not
|
|
7
|
+
// own. Idempotent, and self-heals command-string / timeout drift.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node scripts/install-cursor-hooks.mjs # install / repair
|
|
11
|
+
// node scripts/install-cursor-hooks.mjs --check # exit 1 on drift
|
|
12
|
+
// node scripts/install-cursor-hooks.mjs --uninstall
|
|
13
|
+
// node scripts/install-cursor-hooks.mjs --json # machine-readable report
|
|
14
|
+
//
|
|
15
|
+
// OWNERSHIP. Every entry this script writes runs through the shim
|
|
16
|
+
// (`scripts/cursor-hooks/_run.mjs`), which is also how it recognizes its own
|
|
17
|
+
// work on a re-run. Foreign hooks in the same file are never touched.
|
|
18
|
+
//
|
|
19
|
+
// LEGACY MIGRATION. Earlier versions registered a handful of hooks by pointing
|
|
20
|
+
// Cursor straight at `scripts/claude-hooks/<hook>.mjs`. Those entries are
|
|
21
|
+
// REMOVED on install rather than left alone: the manifest now registers the
|
|
22
|
+
// same hooks through the shim, and leaving both would fire each one twice and
|
|
23
|
+
// bypass the off-switch for the direct copy.
|
|
24
|
+
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
CUSTOMER_LAUNCHER,
|
|
31
|
+
HOOKS,
|
|
32
|
+
SHIM,
|
|
33
|
+
UNPORTABLE,
|
|
34
|
+
commandFor,
|
|
35
|
+
commandForCustomer,
|
|
36
|
+
commandForPlugin,
|
|
37
|
+
identityFor,
|
|
38
|
+
timeoutFor,
|
|
39
|
+
} from './cursor-hooks/manifest.mjs';
|
|
40
|
+
import {
|
|
41
|
+
CUSTOMER_LAUNCHERS,
|
|
42
|
+
hookScope,
|
|
43
|
+
invokedDirectly,
|
|
44
|
+
planCustomerLauncherWrite,
|
|
45
|
+
samePlatformPath,
|
|
46
|
+
} from './lib/customer-kit.mjs';
|
|
47
|
+
|
|
48
|
+
const PLATFORM_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
49
|
+
const REPO_ROOT = process.env.HS_SETUP_TARGET?.trim()
|
|
50
|
+
? path.resolve(process.env.HS_SETUP_TARGET.trim())
|
|
51
|
+
: PLATFORM_ROOT;
|
|
52
|
+
const SETTINGS_PATH = path.join(REPO_ROOT, '.cursor', 'hooks.json');
|
|
53
|
+
|
|
54
|
+
// CUSTOMER TARGET (`hs setup cursor` / `hs doctor --fix` with HS_SETUP_TARGET
|
|
55
|
+
// pointing at a repo other than this checkout, or `hs install`): only the
|
|
56
|
+
// customer-scoped hooks of scripts/lib/customer-kit.mjs ship, addressed
|
|
57
|
+
// through a committed LAUNCHER (`.cursor/heyspex/run-hook.mjs`, written here
|
|
58
|
+
// too) that finds the heyspex package from the repo -- never this checkout's
|
|
59
|
+
// absolute path, which exists on no other machine
|
|
60
|
+
// ([[HS-EN4F-F4V2-DIAG-a-repo-set-up-with-hs-install-gets-no]],
|
|
61
|
+
// [[HS-0AF8-WW9R-TASK-portable-hook-and-command-paths-in]]).
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The hook set + command renderer for a target repo. For a customer target
|
|
65
|
+
* `launcher` names the committed launcher file (relative segments + text) the
|
|
66
|
+
* rendered commands point at; the platform checkout has none.
|
|
67
|
+
* @param {string} targetRoot
|
|
68
|
+
* @param {string} [platformRoot]
|
|
69
|
+
*/
|
|
70
|
+
export function cursorKitFor(targetRoot, platformRoot = PLATFORM_ROOT) {
|
|
71
|
+
const customer = !samePlatformPath(targetRoot, platformRoot);
|
|
72
|
+
return {
|
|
73
|
+
customer,
|
|
74
|
+
hooks: customer ? HOOKS.filter((h) => hookScope(h.hook) === 'customer') : HOOKS,
|
|
75
|
+
commandFor: customer ? commandForCustomer : commandFor,
|
|
76
|
+
launcher: customer ? { rel: CUSTOMER_LAUNCHERS.cursor.rel, plan: planCustomerLauncherWrite } : null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const KIT = cursorKitFor(REPO_ROOT);
|
|
81
|
+
const ACTIVE_HOOKS = KIT.hooks;
|
|
82
|
+
const LAUNCHER_PATH = path.join(REPO_ROOT, ...CUSTOMER_LAUNCHERS.cursor.rel);
|
|
83
|
+
|
|
84
|
+
const LEGACY_MARKER = 'scripts/claude-hooks/';
|
|
85
|
+
|
|
86
|
+
function readSettings() {
|
|
87
|
+
try {
|
|
88
|
+
const raw = fs.readFileSync(SETTINGS_PATH, 'utf8').trim();
|
|
89
|
+
if (!raw) return { version: 1, hooks: {} };
|
|
90
|
+
const parsed = JSON.parse(raw);
|
|
91
|
+
if (parsed && typeof parsed === 'object') {
|
|
92
|
+
parsed.version ??= 1;
|
|
93
|
+
parsed.hooks ??= {};
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err && err.code === 'ENOENT') return { version: 1, hooks: {} };
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
return { version: 1, hooks: {} };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeSettings(settings) {
|
|
104
|
+
fs.mkdirSync(path.dirname(SETTINGS_PATH), { recursive: true });
|
|
105
|
+
fs.writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const commandOf = (entry) => (entry && typeof entry.command === 'string' ? entry.command : '');
|
|
109
|
+
|
|
110
|
+
/** Ours = routed through the shim (any path form) or the committed customer launcher. */
|
|
111
|
+
const isOurRunner = (token) => token.includes(SHIM) || token.includes(CUSTOMER_LAUNCHER);
|
|
112
|
+
const isShimEntry = (raw) => isOurRunner(commandOf(raw));
|
|
113
|
+
|
|
114
|
+
/** A pre-shim registration that the manifest now supersedes. */
|
|
115
|
+
const isLegacyEntry = (raw) => !isShimEntry(raw) && commandOf(raw).includes(LEGACY_MARKER);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Which manifest entry a shim command corresponds to. Matching on the parsed
|
|
119
|
+
* command (hook name + tool filter + forwarded args) rather than on the whole
|
|
120
|
+
* string is what lets a timeout or flag-order change -- or the 2026-10-02
|
|
121
|
+
* absolute platform path -- be REPAIRED instead of duplicated.
|
|
122
|
+
*/
|
|
123
|
+
function identityOfCommand(command) {
|
|
124
|
+
const tokens = command.trim().split(/\s+/);
|
|
125
|
+
const shimAt = tokens.findIndex(isOurRunner);
|
|
126
|
+
if (shimAt === -1) return null;
|
|
127
|
+
const rest = tokens.slice(shimAt + 1);
|
|
128
|
+
const hook = rest[0] ?? '';
|
|
129
|
+
let tool = '';
|
|
130
|
+
const args = [];
|
|
131
|
+
for (const token of rest.slice(1)) {
|
|
132
|
+
// The tool filter is quoted on disk; identityFor() works with the raw
|
|
133
|
+
// regex, so the quotes come off before comparing.
|
|
134
|
+
if (token.startsWith('--hs-tool=')) {
|
|
135
|
+
tool = token.slice('--hs-tool='.length).replace(/^"([\s\S]*)"$/, '$1');
|
|
136
|
+
} else if (token.startsWith('--hs-claude-event=')) continue;
|
|
137
|
+
else args.push(token);
|
|
138
|
+
}
|
|
139
|
+
return `${hook}::${tool}::${args.join(' ')}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function desiredEntry(entry, render = KIT.commandFor) {
|
|
143
|
+
return {
|
|
144
|
+
command: render(entry),
|
|
145
|
+
timeout: timeoutFor(entry),
|
|
146
|
+
...(entry.matcher ? { matcher: entry.matcher } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sameEntry(raw, entry, render = KIT.commandFor) {
|
|
151
|
+
const want = desiredEntry(entry, render);
|
|
152
|
+
return (
|
|
153
|
+
commandOf(raw) === want.command &&
|
|
154
|
+
raw.timeout === want.timeout &&
|
|
155
|
+
(raw.matcher ?? undefined) === (want.matcher ?? undefined)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Plugin mirror
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// plugins/heyspex ships the same hooks for a PLUGIN install (a consumer repo
|
|
163
|
+
// that installs HeySpex from the marketplace rather than checking the platform
|
|
164
|
+
// out). It is generated from the manifest above, not hand-mirrored — the Claude
|
|
165
|
+
// equivalent (plugins/heyspex/hooks/hooks.json) was a dated hand mirror (now
|
|
166
|
+
// generated from scripts/lib/customer-kit.mjs), and that
|
|
167
|
+
// is exactly the drift this avoids.
|
|
168
|
+
const PLUGIN_HOOKS_PATH = path.join(REPO_ROOT, 'plugins', 'heyspex', 'hooks', 'cursor-hooks.json');
|
|
169
|
+
|
|
170
|
+
function pluginHooksDocument() {
|
|
171
|
+
const hooks = {};
|
|
172
|
+
for (const entry of HOOKS) {
|
|
173
|
+
hooks[entry.event] ??= [];
|
|
174
|
+
hooks[entry.event].push({
|
|
175
|
+
command: commandForPlugin(entry),
|
|
176
|
+
timeout: timeoutFor(entry),
|
|
177
|
+
...(entry.matcher ? { matcher: entry.matcher } : {}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
_comment:
|
|
182
|
+
'GENERATED by scripts/install-cursor-hooks.mjs from scripts/cursor-hooks/manifest.mjs — ' +
|
|
183
|
+
'do not edit by hand; run `npm run hooks:install-cursor`. Commands resolve through ' +
|
|
184
|
+
'$CURSOR_PROJECT_DIR because the shim and hook bodies live in the host vault checkout, ' +
|
|
185
|
+
'not the plugin cache. WARNING: enabling this plugin in a repo whose .cursor/hooks.json ' +
|
|
186
|
+
'still registers the same hooks runs every hook twice — validate in a fresh clone, or ' +
|
|
187
|
+
'run `node scripts/install-cursor-hooks.mjs --uninstall` there first.',
|
|
188
|
+
version: 1,
|
|
189
|
+
hooks,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function pluginHooksText() {
|
|
194
|
+
return `${JSON.stringify(pluginHooksDocument(), null, 2)}\n`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function syncPluginHooks() {
|
|
198
|
+
const want = pluginHooksText();
|
|
199
|
+
let have = null;
|
|
200
|
+
try {
|
|
201
|
+
have = fs.readFileSync(PLUGIN_HOOKS_PATH, 'utf8');
|
|
202
|
+
} catch {
|
|
203
|
+
have = null;
|
|
204
|
+
}
|
|
205
|
+
if (have === want) return false;
|
|
206
|
+
fs.mkdirSync(path.dirname(PLUGIN_HOOKS_PATH), { recursive: true });
|
|
207
|
+
fs.writeFileSync(PLUGIN_HOOKS_PATH, want, 'utf8');
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Merge a hook set into a parsed `.cursor/hooks.json` object, in place. Drops
|
|
213
|
+
* superseded direct registrations, adds/repairs every entry, and retires shim
|
|
214
|
+
* entries the set no longer registers (a renamed hook, or a platform-only one
|
|
215
|
+
* in a customer repo). Foreign entries are never touched.
|
|
216
|
+
* @returns {{ added: string[], repaired: string[], removed: string[] }}
|
|
217
|
+
*/
|
|
218
|
+
export function mergeCursorHooks(
|
|
219
|
+
settings,
|
|
220
|
+
{ hooks = ACTIVE_HOOKS, commandFor: render = KIT.commandFor } = {},
|
|
221
|
+
) {
|
|
222
|
+
settings.version = 1;
|
|
223
|
+
settings.hooks ??= {};
|
|
224
|
+
|
|
225
|
+
const added = [];
|
|
226
|
+
const repaired = [];
|
|
227
|
+
const removed = [];
|
|
228
|
+
|
|
229
|
+
// 1. Drop superseded direct registrations.
|
|
230
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
231
|
+
if (!Array.isArray(entries)) continue;
|
|
232
|
+
const keep = entries.filter((raw) => {
|
|
233
|
+
if (!isLegacyEntry(raw)) return true;
|
|
234
|
+
removed.push(`${event}: ${commandOf(raw)}`);
|
|
235
|
+
return false;
|
|
236
|
+
});
|
|
237
|
+
settings.hooks[event] = keep;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 2. Add or repair every manifest entry.
|
|
241
|
+
for (const entry of hooks) {
|
|
242
|
+
settings.hooks[entry.event] ??= [];
|
|
243
|
+
const entries = settings.hooks[entry.event];
|
|
244
|
+
const identity = identityFor(entry);
|
|
245
|
+
const existing = entries.filter(
|
|
246
|
+
(raw) => isShimEntry(raw) && identityOfCommand(commandOf(raw)) === identity,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (existing.length === 0) {
|
|
250
|
+
entries.push(desiredEntry(entry, render));
|
|
251
|
+
added.push(`${entry.event}/${entry.hook}`);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// More than one match means an earlier merge duplicated it; collapse to one.
|
|
256
|
+
for (const extra of existing.slice(1)) {
|
|
257
|
+
entries.splice(entries.indexOf(extra), 1);
|
|
258
|
+
removed.push(`${entry.event}: duplicate ${entry.hook}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const raw = existing[0];
|
|
262
|
+
if (!sameEntry(raw, entry, render)) {
|
|
263
|
+
Object.assign(raw, desiredEntry(entry, render));
|
|
264
|
+
// A matcher removed from the manifest must also disappear from disk.
|
|
265
|
+
if (!entry.matcher && 'matcher' in raw) delete raw.matcher;
|
|
266
|
+
repaired.push(`${entry.event}/${entry.hook}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 3. Retire shim entries we no longer register (a hook renamed or dropped).
|
|
271
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
272
|
+
if (!Array.isArray(entries)) continue;
|
|
273
|
+
const wanted = new Set(hooks.filter((h) => h.event === event).map(identityFor));
|
|
274
|
+
settings.hooks[event] = entries.filter((raw) => {
|
|
275
|
+
if (!isShimEntry(raw)) return true;
|
|
276
|
+
if (wanted.has(identityOfCommand(commandOf(raw)))) return true;
|
|
277
|
+
removed.push(`${event}: retired ${commandOf(raw)}`);
|
|
278
|
+
return false;
|
|
279
|
+
});
|
|
280
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { added, repaired, removed };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The customer launcher's state on disk, without writing. */
|
|
287
|
+
function launcherPlan() {
|
|
288
|
+
let existing = null;
|
|
289
|
+
try {
|
|
290
|
+
existing = fs.readFileSync(LAUNCHER_PATH, 'utf8');
|
|
291
|
+
} catch {
|
|
292
|
+
existing = null;
|
|
293
|
+
}
|
|
294
|
+
return planCustomerLauncherWrite(existing, 'cursor');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function install({ json = false } = {}) {
|
|
298
|
+
const settings = readSettings();
|
|
299
|
+
const { added, repaired, removed } = mergeCursorHooks(settings);
|
|
300
|
+
|
|
301
|
+
const settingsChanged = added.length + repaired.length + removed.length > 0;
|
|
302
|
+
if (settingsChanged) writeSettings(settings);
|
|
303
|
+
// The committed launcher every customer row points at. The platform checkout
|
|
304
|
+
// runs the shim directly and never gets one.
|
|
305
|
+
let launcherWritten = false;
|
|
306
|
+
if (KIT.customer) {
|
|
307
|
+
const plan = launcherPlan();
|
|
308
|
+
if (plan.write) {
|
|
309
|
+
fs.mkdirSync(path.dirname(LAUNCHER_PATH), { recursive: true });
|
|
310
|
+
fs.writeFileSync(LAUNCHER_PATH, plan.text, 'utf8');
|
|
311
|
+
launcherWritten = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// The plugin mirror lives in THIS checkout; a customer target never gets one.
|
|
315
|
+
const pluginChanged = KIT.customer ? false : syncPluginHooks();
|
|
316
|
+
|
|
317
|
+
if (json) {
|
|
318
|
+
console.log(
|
|
319
|
+
JSON.stringify(
|
|
320
|
+
{
|
|
321
|
+
ok: true,
|
|
322
|
+
changed: settingsChanged || pluginChanged || launcherWritten,
|
|
323
|
+
added,
|
|
324
|
+
repaired,
|
|
325
|
+
removed,
|
|
326
|
+
pluginChanged,
|
|
327
|
+
launcherWritten,
|
|
328
|
+
total: ACTIVE_HOOKS.length,
|
|
329
|
+
customer: KIT.customer,
|
|
330
|
+
},
|
|
331
|
+
null,
|
|
332
|
+
2,
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (pluginChanged) {
|
|
339
|
+
console.log(`install-cursor-hooks: regenerated ${path.relative(REPO_ROOT, PLUGIN_HOOKS_PATH)}`);
|
|
340
|
+
}
|
|
341
|
+
if (launcherWritten) {
|
|
342
|
+
console.log(`install-cursor-hooks: wrote ${path.relative(REPO_ROOT, LAUNCHER_PATH)} (resolves node_modules/heyspex)`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!settingsChanged) {
|
|
346
|
+
console.log(`install-cursor-hooks: already installed (${ACTIVE_HOOKS.length} hooks, no changes).`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const parts = [];
|
|
351
|
+
if (added.length) parts.push(`added ${added.length}`);
|
|
352
|
+
if (repaired.length) parts.push(`repaired ${repaired.length}`);
|
|
353
|
+
if (removed.length) parts.push(`removed ${removed.length}`);
|
|
354
|
+
console.log(
|
|
355
|
+
`install-cursor-hooks: ${parts.join('; ')} → ${path.relative(REPO_ROOT, SETTINGS_PATH)} ` +
|
|
356
|
+
`(${ACTIVE_HOOKS.length} hooks registered)`,
|
|
357
|
+
);
|
|
358
|
+
for (const line of removed) console.log(` - ${line}`);
|
|
359
|
+
console.log(
|
|
360
|
+
KIT.customer
|
|
361
|
+
? ` Launcher: ${CUSTOMER_LAUNCHER} → node_modules/heyspex/${SHIM} (HeySpex off-switch + Cursor↔Claude payload translation)`
|
|
362
|
+
: ` Shim: ${SHIM} (HeySpex off-switch + Cursor↔Claude payload translation)`,
|
|
363
|
+
);
|
|
364
|
+
for (const gap of UNPORTABLE) {
|
|
365
|
+
console.log(` Not registerable in Cursor: ${gap.hook} (${gap.reason}) → covered by ${gap.coveredBy}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function check({ json = false } = {}) {
|
|
370
|
+
const settings = readSettings();
|
|
371
|
+
const issues = [];
|
|
372
|
+
const entriesFor = (event) =>
|
|
373
|
+
Array.isArray(settings?.hooks?.[event]) ? settings.hooks[event] : [];
|
|
374
|
+
|
|
375
|
+
for (const entry of ACTIVE_HOOKS) {
|
|
376
|
+
const identity = identityFor(entry);
|
|
377
|
+
const ours = entriesFor(entry.event).filter(
|
|
378
|
+
(raw) => isShimEntry(raw) && identityOfCommand(commandOf(raw)) === identity,
|
|
379
|
+
);
|
|
380
|
+
if (ours.length === 0) {
|
|
381
|
+
issues.push(`${entry.event}: missing ${entry.hook}`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (ours.length > 1) issues.push(`${entry.event}: ${entry.hook} registered ${ours.length}×`);
|
|
385
|
+
for (const raw of ours) {
|
|
386
|
+
if (!sameEntry(raw, entry)) {
|
|
387
|
+
issues.push(
|
|
388
|
+
`${entry.event}/${entry.hook}: drift — expected \`${KIT.commandFor(entry)}\`` +
|
|
389
|
+
` (timeout ${timeoutFor(entry)}, matcher ${entry.matcher ?? 'none'}), found` +
|
|
390
|
+
` \`${commandOf(raw)}\` (timeout ${raw.timeout}, matcher ${raw.matcher ?? 'none'})`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
|
|
397
|
+
if (!Array.isArray(entries)) continue;
|
|
398
|
+
for (const raw of entries) {
|
|
399
|
+
if (isLegacyEntry(raw)) {
|
|
400
|
+
issues.push(`${event}: superseded direct registration — ${commandOf(raw)}`);
|
|
401
|
+
} else if (
|
|
402
|
+
isShimEntry(raw) &&
|
|
403
|
+
!ACTIVE_HOOKS.some((h) => h.event === event && identityFor(h) === identityOfCommand(commandOf(raw)))
|
|
404
|
+
) {
|
|
405
|
+
issues.push(`${event}: not part of this repo's hook set — ${commandOf(raw)}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let pluginOnDisk = null;
|
|
411
|
+
try {
|
|
412
|
+
pluginOnDisk = fs.readFileSync(PLUGIN_HOOKS_PATH, 'utf8');
|
|
413
|
+
} catch {
|
|
414
|
+
pluginOnDisk = null;
|
|
415
|
+
}
|
|
416
|
+
if (!KIT.customer && pluginOnDisk !== pluginHooksText()) {
|
|
417
|
+
issues.push(
|
|
418
|
+
`plugins/heyspex/hooks/cursor-hooks.json: ${pluginOnDisk === null ? 'missing' : 'stale'} — regenerate with \`npm run hooks:install-cursor\``,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
if (KIT.customer) {
|
|
422
|
+
const plan = launcherPlan();
|
|
423
|
+
if (plan.write) {
|
|
424
|
+
issues.push(`${CUSTOMER_LAUNCHER}: ${plan.reason === 'missing' ? 'missing' : 'stale'} — the hook rows point at it`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (json) {
|
|
429
|
+
console.log(JSON.stringify({ ok: issues.length === 0, issues, total: ACTIVE_HOOKS.length, customer: KIT.customer }, null, 2));
|
|
430
|
+
process.exit(issues.length === 0 ? 0 : 1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (issues.length === 0) {
|
|
434
|
+
console.log(`install-cursor-hooks --check: OK (${ACTIVE_HOOKS.length} hooks registered)`);
|
|
435
|
+
process.exit(0);
|
|
436
|
+
}
|
|
437
|
+
console.error('install-cursor-hooks --check: drift detected');
|
|
438
|
+
for (const issue of issues) console.error(` - ${issue}`);
|
|
439
|
+
console.error(' Run `node scripts/install-cursor-hooks.mjs` to repair.');
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function uninstall() {
|
|
444
|
+
const settings = readSettings();
|
|
445
|
+
if (!settings.hooks) {
|
|
446
|
+
console.log('install-cursor-hooks: nothing to uninstall (no hooks block).');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
let removed = 0;
|
|
450
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
451
|
+
if (!Array.isArray(entries)) continue;
|
|
452
|
+
const keep = entries.filter((raw) => !isShimEntry(raw) && !isLegacyEntry(raw));
|
|
453
|
+
removed += entries.length - keep.length;
|
|
454
|
+
if (keep.length === 0) delete settings.hooks[event];
|
|
455
|
+
else settings.hooks[event] = keep;
|
|
456
|
+
}
|
|
457
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
458
|
+
writeSettings(settings);
|
|
459
|
+
console.log(`install-cursor-hooks: uninstalled ${removed} hook entr${removed === 1 ? 'y' : 'ies'}.`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// CLI only when run directly -- `hs install` imports mergeCursorHooks/cursorKitFor,
|
|
463
|
+
// and inside the esbuild CLI bundle a plain `import.meta.url === argv[1]` test
|
|
464
|
+
// is true for EVERY bundled module (see `invokedDirectly` in customer-kit.mjs).
|
|
465
|
+
if (invokedDirectly(import.meta.url, 'install-cursor-hooks.mjs')) {
|
|
466
|
+
const argv = process.argv.slice(2);
|
|
467
|
+
const json = argv.includes('--json');
|
|
468
|
+
if (argv.includes('--uninstall')) uninstall();
|
|
469
|
+
else if (argv.includes('--check')) check({ json });
|
|
470
|
+
else install({ json });
|
|
471
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-env-markers.mjs — the ONE list of environment stamps that mean "an AI
|
|
3
|
+
* coding agent is driving this shell".
|
|
4
|
+
*
|
|
5
|
+
* Lifted out of scripts/guard-agent-commit.mjs (2026-09-19) so the git-identity
|
|
6
|
+
* command (`hs source identity set|clear`, scripts/lib/hs-source-identity.mjs)
|
|
7
|
+
* refuses on exactly the same evidence the commit guard refuses on — one list,
|
|
8
|
+
* two consumers, no drift. guard-agent-commit's behaviour is unchanged: it
|
|
9
|
+
* imports this list and filters it the same way it always did.
|
|
10
|
+
*
|
|
11
|
+
* Shipped in the published package (package.json `files`) because
|
|
12
|
+
* hs-source-identity.mjs is a packed `hs source` entrypoint and imports it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Environment stamps that mean "an AI coding agent is driving this shell". */
|
|
16
|
+
export const AGENT_ENV_MARKERS = [
|
|
17
|
+
'CLAUDECODE', // Claude Code
|
|
18
|
+
'CLAUDE_CODE_ENTRYPOINT',
|
|
19
|
+
'CLAUDE_CODE_SESSION_ID',
|
|
20
|
+
'CURSOR_AGENT', // Cursor's agent / cursor-agent CLI — the actor in the 2026-07-12 incident
|
|
21
|
+
'CURSOR_TRACE_ID',
|
|
22
|
+
'CURSOR_SESSION_ID',
|
|
23
|
+
'COMPOSER_NO_INTERACTION',
|
|
24
|
+
'AIDER_MODEL',
|
|
25
|
+
'OPENAI_CODEX_SESSION',
|
|
26
|
+
'DEVIN_SESSION_ID',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The markers that are present (set to anything, including an empty string
|
|
31
|
+
* is NOT present — matches guard-agent-commit's `process.env[k]` truthiness).
|
|
32
|
+
*
|
|
33
|
+
* @param {Record<string, string | undefined>} [env]
|
|
34
|
+
* @returns {string[]}
|
|
35
|
+
*/
|
|
36
|
+
export function detectAgentMarkers(env = process.env) {
|
|
37
|
+
return AGENT_ENV_MARKERS.filter((k) => env[k]);
|
|
38
|
+
}
|