@alessandroraffa/tangyr 0.21.1 → 0.21.2

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/index.js CHANGED
@@ -25741,8 +25741,8 @@ function compileCursorHooks(sourceHooksJson, mappings, lossReport, sourceLabel)
25741
25741
  sourceEvent
25742
25742
  );
25743
25743
  if (mapping.status === "mapped" && mapping.target_event) {
25744
- const extracted = extractHookCommand(payload);
25745
- if (!extracted.command) {
25744
+ const entries = extractHookCommands(payload);
25745
+ if (entries.length === 0) {
25746
25746
  droppedEvents.push(sourceEvent);
25747
25747
  addHookLoss(
25748
25748
  lossReport,
@@ -25755,26 +25755,24 @@ function compileCursorHooks(sourceHooksJson, mappings, lossReport, sourceLabel)
25755
25755
  dropped += 1;
25756
25756
  continue;
25757
25757
  }
25758
- hooks[mapping.target_event] = { command: extracted.command };
25758
+ const untranslatable = /* @__PURE__ */ new Set();
25759
+ hooks[mapping.target_event] = entries.map((entry) => {
25760
+ const matcher = entry.matcher === void 0 ? void 0 : translateCursorMatcher(entry.matcher, untranslatable);
25761
+ return {
25762
+ type: "command",
25763
+ command: entry.command,
25764
+ ...matcher !== void 0 ? { matcher } : {}
25765
+ };
25766
+ });
25759
25767
  translated += 1;
25760
- if (extracted.remaining.length > 0) {
25761
- addHookLoss(
25762
- lossReport,
25763
- "cursor",
25764
- sourceEvent,
25765
- sourceLabel,
25766
- "error",
25767
- `${sourceEvent}: the source registers ${extracted.remaining.length + 1} hooks on this event and Cursor's entry holds one command. Only '${extracted.command}' was written; ${extracted.remaining.map((c) => `'${c}'`).join(", ")} ${extracted.remaining.length === 1 ? "does" : "do"} not run under Cursor. To fix: give this event a single entry point that dispatches to the others, or accept the loss knowingly.`
25768
- );
25769
- }
25770
- if (extracted.matcher) {
25768
+ if (untranslatable.size > 0) {
25771
25769
  addInformational(
25772
25770
  lossReport,
25773
25771
  "cursor",
25774
25772
  "hooks",
25775
25773
  sourceLabel,
25776
25774
  "warning",
25777
- `${sourceEvent}: the source scopes this hook to '${extracted.matcher}', and Cursor's hook entry carries no matcher. It will run for every tool call on this event, not only the matched ones.`
25775
+ `${sourceEvent}: ${[...untranslatable].map((m) => `'${m}'`).join(", ")} ${untranslatable.size === 1 ? "names a tool" : "name tools"} with no known Cursor equivalent and ${untranslatable.size === 1 ? "was" : "were"} written through unchanged. Cursor matches on its own tool names \u2014 measured: Shell, Read, Write, Grep \u2014 so a hook scoped to a name it does not use will not run. To fix: add the name to CURSOR_TOOL_NAMES in the compiler once its Cursor equivalent has been measured.`
25778
25776
  );
25779
25777
  }
25780
25778
  continue;
@@ -25932,7 +25930,35 @@ function compileCopilotHooks(sourceHooksJson, mappings, lossReport, sourceLabel)
25932
25930
  }
25933
25931
  return { payload: { hooks }, translated, dropped, droppedEvents };
25934
25932
  }
25935
- function extractHookCommand(payload) {
25933
+ var CURSOR_TOOL_NAMES = {
25934
+ Bash: "Shell",
25935
+ Edit: "Write",
25936
+ MultiEdit: "Write",
25937
+ // Cursor's own names map to themselves. Without these a kit that already
25938
+ // writes a Cursor matcher — the natural thing to do once this mapping is
25939
+ // documented — would have it reported as untranslatable and warned about,
25940
+ // while it is in fact the only spelling that works.
25941
+ Shell: "Shell",
25942
+ Read: "Read",
25943
+ Write: "Write",
25944
+ Grep: "Grep"
25945
+ };
25946
+ function translateCursorMatcher(matcher, untranslatable) {
25947
+ const translated = [];
25948
+ for (const token of matcher.split("|")) {
25949
+ const trimmed = token.trim();
25950
+ if (!trimmed) continue;
25951
+ const mapped = CURSOR_TOOL_NAMES[trimmed];
25952
+ if (mapped === void 0) {
25953
+ untranslatable.add(trimmed);
25954
+ if (!translated.includes(trimmed)) translated.push(trimmed);
25955
+ continue;
25956
+ }
25957
+ if (!translated.includes(mapped)) translated.push(mapped);
25958
+ }
25959
+ return translated.join("|");
25960
+ }
25961
+ function extractHookCommands(payload) {
25936
25962
  const entries = Array.isArray(payload) ? payload : [payload];
25937
25963
  const found = [];
25938
25964
  for (const entry of entries) {
@@ -25949,15 +25975,7 @@ function extractHookCommand(payload) {
25949
25975
  }
25950
25976
  }
25951
25977
  }
25952
- if (found.length === 0) {
25953
- return { command: "", remaining: [] };
25954
- }
25955
- const [first, ...rest] = found;
25956
- return {
25957
- command: first.command,
25958
- ...first.matcher !== void 0 ? { matcher: first.matcher } : {},
25959
- remaining: rest.map((r) => r.command)
25960
- };
25978
+ return found;
25961
25979
  }
25962
25980
  function normalizeHookEventRule(value, target, sourceEvent) {
25963
25981
  if (typeof value === "string") {
@@ -143,81 +143,61 @@ targets:
143
143
 
144
144
  cursor:
145
145
  delivery: native-versioned-object
146
- # READ THIS BEFORE TRUSTING THE ENTRIES BELOW.
146
+ # Measured on 2026-09-06 against cursor-agent 2026.09.02-c22c1a3, by writing a
147
+ # .cursor/hooks.json, running a real agent turn, and checking whether the hook's
148
+ # script had run. Every entry below is a result, not a reading of the binary.
147
149
  #
148
- # On 2026-09-04 the shipped Cursor binary was inspected at
149
- # /Applications/Cursor.app/Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js.
150
- # Its hook-step enum holds twelve values, all camelCase:
150
+ # The steps are camelCase and come from a 21-value enum: beforeShellExecution,
151
+ # beforeMCPExecution, afterShellExecution, afterMCPExecution, beforeReadFile,
152
+ # afterFileEdit, beforeTabFileRead, afterTabFileEdit, stop, beforeSubmitPrompt,
153
+ # afterAgentResponse, afterAgentThought, sessionStart, sessionEnd, preCompact,
154
+ # subagentStart, subagentStop, preToolUse, postToolUse, postToolUseFailure,
155
+ # workspaceOpen. Every one of this file's source events has an equivalent there.
151
156
  #
152
- # afterFileEdit, afterMCPExecution, afterShellExecution, beforeMCPExecution,
153
- # beforeReadFile, beforeShellExecution, postToolUse, postToolUseFailure,
154
- # preCompact, preToolUse, subagentStart, subagentStop
157
+ # What the 2026-09-04 note above this block got wrong, and how:
155
158
  #
156
- # The target_event values written below are PascalCase `PreToolUse`, not `preToolUse`
157
- # and JSON keys are case sensitive. Three consequences follow, none of them verified:
159
+ # 1. The enum was read from the GUI app and had twelve values. The CLI's has
160
+ # twenty-one, sessionStart and beforeSubmitPrompt among them. Two events
161
+ # recorded here as having no equivalent do have one.
162
+ # 2. PascalCase was suspected of being the whole defect. It is not: a file in
163
+ # Claude Code's own shape with PascalCase keys and { matcher, hooks: [] }
164
+ # entries did not fire either, though the binary carries a detector for
165
+ # that shape. Case and shape had to change together.
166
+ # 3. The entry was thought to hold one command per event, so the compiler
167
+ # reported a collapse. It holds an array: three commands on one step all
168
+ # ran, in parallel. There is nothing to collapse.
158
169
  #
159
- # 1. What this CLI writes to .cursor/hooks.json may be inert. The file exists, names real
160
- # scripts, and may match no key Cursor looks up. That is exactly the failure shape this
161
- # mapping is meant to prevent, so it is recorded here rather than left to be discovered.
162
- # 2. preCompact, subagentStart and subagentStop ARE in that enum, while the entries below
163
- # say they have "no documented equivalent". That claim is wrong, whatever the case
164
- # question turns out to be.
165
- # 3. The same file resolves ~/.claude/settings.json and <project>/.claude/settings.json as
166
- # hook sources alongside ~/.cursor/hooks.json. Cursor may already be reading the
167
- # Claude Code artifact this CLI writes, which would make its own hooks.json redundant.
168
- #
169
- # Nothing below was changed on this evidence. Minified code read without running it is a
170
- # strong hint and not a measurement, and swapping one unverified mapping for another is the
171
- # same mistake in the other direction. To settle it: install this kit for cursor, open the
172
- # Cursor GUI, run one agent turn that executes a shell command, and check whether the hook
173
- # script ran — then repeat with the keys lowercased. Cursor has no CLI on the machine this
174
- # was measured on, which is why it stopped here.
170
+ # One finding has no entry here because it belongs to the compiler: `matcher` is
171
+ # honoured, alternation included, but against Cursor's tool names. A hook scoped
172
+ # to "Bash" never runs; "Shell" does. See CURSOR_TOOL_NAMES in compiler/hooks.ts.
175
173
  source_to_target:
176
174
  SessionStart:
177
- status: probe
178
- reason: >-
179
- Not in the observed enum under any casing, and not probeable without the GUI. See the
180
- note above this block.
175
+ status: mapped
176
+ target_event: sessionStart
181
177
  UserPromptSubmit:
182
- status: probe
183
- reason: >-
184
- Not in the observed enum; the nearest candidate is beforeSubmitPrompt, which is a
185
- different contract. See the note above this block.
186
- PostToolUseFailure:
187
- status: probe
188
- reason: >-
189
- postToolUseFailure is in the observed enum, camelCased. Unverified. See the note above
190
- this block.
178
+ status: mapped
179
+ target_event: beforeSubmitPrompt
191
180
  PreToolUse:
192
181
  status: mapped
193
- target_event: PreToolUse
182
+ target_event: preToolUse
194
183
  PostToolUse:
195
184
  status: mapped
196
- target_event: PostToolUse
185
+ target_event: postToolUse
186
+ PostToolUseFailure:
187
+ status: mapped
188
+ target_event: postToolUseFailure
197
189
  PreCompact:
198
- status: probe
199
- severity: warning
200
- reason: >-
201
- preCompact is in the observed enum, camelCased. The previous entry here said there was
202
- no documented equivalent, which the enum contradicts. See the note above this block.
190
+ status: mapped
191
+ target_event: preCompact
203
192
  SubagentStart:
204
- status: probe
205
- severity: warning
206
- reason: >-
207
- subagentStart is in the observed enum, camelCased. The previous entry here said there was
208
- no documented equivalent, which the enum contradicts. See the note above this block.
193
+ status: mapped
194
+ target_event: subagentStart
209
195
  SubagentStop:
210
- status: probe
211
- severity: warning
212
- reason: >-
213
- subagentStop is in the observed enum, camelCased. The previous entry here said there was
214
- no documented equivalent, which the enum contradicts. See the note above this block.
196
+ status: mapped
197
+ target_event: subagentStop
215
198
  Stop:
216
- status: drop
217
- severity: warning
218
- reason: >-
219
- Not in the observed enum. `stop` appears in that file but as an unrelated identifier, so
220
- this stays a drop rather than becoming a guess. See the note above this block.
199
+ status: mapped
200
+ target_event: stop
221
201
 
222
202
  # OpenCode and Cline exchange no declarative hook file at all: hooks are
223
203
  # plugin code (an OpenCode plugin, a @cline/sdk AgentPlugin), which a kit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alessandroraffa/tangyr",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "description": "CLI for the Tangyr discipline — install and manage operating kits for AI coding tools",
5
5
  "license": "MIT",
6
6
  "engines": {