@pinta-ai/pinta-musecode 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,71 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ Initial scaffold — stage 1 (observation) of the MuseCode adapter plan.
6
+
7
+ ### Added
8
+ - Hook dispatch for all twelve Muse Code lifecycle events, with event-name
9
+ resolution from **both** argv and the stdin payload (argv wins).
10
+ - OTLP export with the `musecode` ingest discriminator and `muse.*` attributes,
11
+ reusing `@pinta-ai/core` for redaction, transport, retry queue, and traces.
12
+ - Per-turn trace rotation on `UserPromptSubmit`.
13
+ - Guard evaluation on `PreToolUse` and `PermissionRequest`, with the
14
+ enforce-before-telemetry ordering invariant covered by tests.
15
+ - Exemption for the six documented subagent control tools.
16
+ - `tools/doctor.ts` — environment preflight; fails on Linux without Node.
17
+ - `tools/capture-hook.mjs` + `tools/spike.ts` — stage-0 spike harness that
18
+ captures real hook invocations and reports payload shapes, event-name source,
19
+ undocumented events, and the surviving env allowlist.
20
+
21
+ ### Verified
22
+
23
+ Stage 0 was executed against **`muse 0.1.0-R708.1`**. The adapter ran as a real
24
+ managed hook and emitted OTLP spans end to end. See README §Confirmed contracts.
25
+ Notable corrections this produced:
26
+
27
+ - The managed hooks file needs the `hooks` wrapper **and** a matcher-group level;
28
+ the previous template had neither and would have been ignored silently.
29
+ `schema_version` and `matcher` turned out to be optional. Now guarded by
30
+ `tests/managed-hooks.test.ts`.
31
+ - **Group and handler objects are deserialized strictly, and rejection is silent.**
32
+ One unrecognised key skips the whole event while the rest of the file keeps
33
+ working, so a typo removes monitoring from one event and leaves the adapter
34
+ looking healthy. Accepted handler keys: `type`, `command`, `commandWindows`,
35
+ `timeout`, `statusMessage`, `silent`, `async`.
36
+ - **The per-handler `env` block does not work** — it is one of the rejected keys,
37
+ despite appearing in the binary's strings. This promotes the env file from the
38
+ preferable channel to the only one.
39
+ - `timeout` is in **seconds**, not milliseconds.
40
+ - `managed_hooks_path` is a `settings.json` key, not an environment variable.
41
+ - The hook environment is filtered to 13 variables, confirming the env file as
42
+ the only usable config channel — and revealing that `XDG_CONFIG_HOME` is
43
+ stripped, so the env file must be written under `$HOME/.config/muse/`.
44
+ - `PreLLMCall`/`PostLLMCall` carry the full `messages` array, which justifies
45
+ keeping them opt-in.
46
+ - `SubagentStart` reports the child's `session_id`, not the parent's.
47
+ - The six subagent control tool names in `INTERNAL_TOOLS` are confirmed exactly
48
+ as listed, including `subagent_status`.
49
+ - `session.jsonl` records the hook events too, alongside token counts and
50
+ parent/child session links that the hook payload omits — so it is a workable
51
+ backstop for stage 3. Tool arguments were not observed and remain open.
52
+ - Hook cost is ~45 ms per invocation, essentially all Node start-up, and hooks
53
+ for different subagents run in parallel — 16 concurrent invocations finish in
54
+ 123 ms. Five hooks fire on a turn that does nothing, two of them
55
+ `SubagentStart`, so subagent traffic is the volume driver rather than tools.
56
+
57
+ ### Fixed
58
+
59
+ - **The deny contract was wrong.** `hookSpecificOutput.permissionDecision` is
60
+ ignored for `UserPromptSubmit` — the turn just proceeds. The working shape is
61
+ the top-level `{"decision":"block","reason":…}`. `renderDenyJson()` now
62
+ branches on the event family, and `PINTA_MUSE_DENY_FORMAT` defaults to `auto`,
63
+ pairing the JSON with exit 2 (measured to block on its own).
64
+ Verified end to end: the adapter's own output cancelled a live turn.
65
+ - Ruled out Muse Code's native OTLP export as a cheaper telemetry path — it
66
+ refuses to send to a non-Meta destination by design.
67
+
68
+ ### Notes
69
+ - Enforcement is **off by default** (shadow mode). The deny wire contract is
70
+ unconfirmed and isolated in `src/core/decision.ts`; see README "Spike-pending".
71
+ - `PreLLMCall` / `PostLLMCall` are opt-in via `PINTA_MUSE_LLM_EVENTS=1`.
package/LICENSE ADDED
@@ -0,0 +1,136 @@
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ ## Acceptance
6
+
7
+ In order to get any license under these terms, you must agree
8
+ to them as both strict obligations and conditions to all
9
+ your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the
14
+ software to do everything you might do with the software
15
+ that would otherwise infringe the licensor's copyright
16
+ in it for any permitted purpose. However, you may
17
+ only distribute the software according to [Distribution
18
+ License](#distribution-license) and make changes or new works
19
+ based on the software according to [Changes and New Works
20
+ License](#changes-and-new-works-license).
21
+
22
+ ## Distribution License
23
+
24
+ The licensor grants you an additional copyright license to
25
+ distribute copies of the software. Your license to distribute
26
+ covers distributing the software with changes and new works
27
+ permitted by [Changes and New Works
28
+ License](#changes-and-new-works-license).
29
+
30
+ ## Notices
31
+
32
+ You must ensure that anyone who gets a copy of any part of
33
+ the software from you also gets a copy of these terms or the
34
+ URL for them above, as well as copies of any plain-text lines
35
+ beginning with `Required Notice:` that the licensor provided
36
+ with the software. For example:
37
+
38
+ > Required Notice: Copyright Pinta AI (https://pinta.sh)
39
+
40
+ ## Changes and New Works License
41
+
42
+ The licensor grants you an additional copyright license to make
43
+ changes and new works based on the software for any permitted
44
+ purpose.
45
+
46
+ ## Patent License
47
+
48
+ The licensor grants you a patent license for the software that
49
+ covers patent claims the licensor can license, or becomes able
50
+ to license, that you would infringe by using the software.
51
+
52
+ ## Noncommercial Purposes
53
+
54
+ Any noncommercial purpose is a permitted purpose.
55
+
56
+ ## Personal Uses
57
+
58
+ Personal use for research, experiment, and testing for
59
+ the benefit of public knowledge, personal study, private
60
+ entertainment, hobby projects, amateur pursuits, or religious
61
+ observance, without any anticipated commercial application,
62
+ is use for a permitted purpose.
63
+
64
+ ## Noncommercial Organizations
65
+
66
+ Use by any charitable organization, educational institution,
67
+ public research organization, public safety or health
68
+ organization, environmental protection organization, or
69
+ government institution is use for a permitted purpose
70
+ regardless of the source of funding or obligations resulting
71
+ from the funding.
72
+
73
+ ## Fair Use
74
+
75
+ You may have "fair use" rights for the software under the
76
+ law. These terms do not limit them.
77
+
78
+ ## No Other Rights
79
+
80
+ These terms do not allow you to sublicense or transfer any of
81
+ your licenses to anyone else, or prevent the licensor from
82
+ granting licenses to anyone else. These terms do not imply
83
+ any other licenses.
84
+
85
+ ## Patent Defense
86
+
87
+ If you make any written claim that the software infringes or
88
+ contributes to infringement of any patent, your patent license
89
+ for the software granted under these terms ends immediately. If
90
+ your company makes such a claim, your patent license ends
91
+ immediately for work on behalf of your company.
92
+
93
+ ## Violations
94
+
95
+ The first time you are notified in writing that you have
96
+ violated any of these terms, or done anything with the software
97
+ not covered by your licenses, your licenses can nonetheless
98
+ continue if you come into full compliance with these terms,
99
+ and take practical steps to correct past violations, within
100
+ 32 days of receiving notice. Otherwise, all your licenses
101
+ end immediately.
102
+
103
+ ## No Liability
104
+
105
+ ***As far as the law allows, the software comes as is, without
106
+ any warranty or condition, and the licensor will not be liable
107
+ to you for any damages arising out of these terms or the use
108
+ or nature of the software, under any kind of legal claim.***
109
+
110
+ ## Definitions
111
+
112
+ The **licensor** is the individual or entity offering these
113
+ terms, and the **software** is the software the licensor makes
114
+ available under these terms.
115
+
116
+ **You** refers to the individual or entity agreeing to these
117
+ terms.
118
+
119
+ **Your company** is any legal entity, sole proprietorship,
120
+ or other kind of organization that you work for, plus all
121
+ organizations that have control over, are under the control
122
+ of, or are under common control with that organization.
123
+ **Control** means ownership of substantially all the assets of
124
+ an entity, or the power to direct its management and policies
125
+ by vote, contract, or otherwise. Control can be direct or
126
+ indirect.
127
+
128
+ **Your licenses** are all the licenses granted to you for the
129
+ software under these terms.
130
+
131
+ **Use** means anything you do with the software requiring one
132
+ of your licenses.
133
+
134
+ ---
135
+
136
+ Required Notice: Copyright (c) 2026 Pinta AI
package/README.md ADDED
@@ -0,0 +1,431 @@
1
+ # pinta-musecode
2
+
3
+ Pinta AI adapter for **Meta Muse Code** — forwards Muse Code lifecycle hook
4
+ events to an OTLP collector and, optionally, enforces guard decisions.
5
+
6
+ > **Status: stage 1, verified end to end against `muse 0.1.0-R708.1`.**
7
+ > Telemetry and blocking are both confirmed against a real binary — the
8
+ > adapter's own deny output cancelled a live turn. Enforcement is still **off by
9
+ > default**, because the *tool-side* deny shape could not be exercised (the test
10
+ > account hit a billing error before any tool call) and because stage 2 wants
11
+ > false-positive data first. See [Confirmed contracts](#confirmed-contracts).
12
+
13
+ Implementation plan: [🎼 Meta MuseCode 어댑터 구현 계획](https://app.notion.com/p/3b7dcc09b89a8197b782e2d5ab96d1ea)
14
+
15
+ ---
16
+
17
+ ## Platform support
18
+
19
+ **macOS and Linux. Both are supported; there is nothing mac-specific in `src/`.**
20
+
21
+ Windows is not a gap in this adapter — Muse Code itself has no Windows build.
22
+ Meta ships *"a native binary on your path for macOS and Linux"*, and the sandbox
23
+ exists in exactly two forms (Seatbelt on macOS, bundled bubblewrap on Linux). So
24
+ there is no host to hook on Windows, and none of the `.cmd` wrapper handling the
25
+ other Pinta adapters carry applies here.
26
+
27
+ | OS | Muse Code | Node available | Result |
28
+ | --- | --- | --- | --- |
29
+ | macOS | yes | pinta-manager bundles it | works |
30
+ | Linux | yes | **no bundled Node** — literal `node` | works **iff** system Node is present |
31
+ | Windows | no | — | not applicable |
32
+
33
+ The only platform variable is therefore the **Node runtime on Linux**, not the
34
+ adapter code. See [Linux requires Node](#-linux-requires-node).
35
+
36
+ ---
37
+
38
+ ## How it works
39
+
40
+ Muse Code binds a shell command to exactly one of its twelve lifecycle events.
41
+ The manager installs this adapter as a **managed hook**, which Muse Code treats
42
+ as pre-approved and runs without a trust step.
43
+
44
+ ```
45
+ muse ──spawn──> node dist/index.js <EventName> ──stdin──> payload JSON
46
+ │
47
+ ├─ PreToolUse / PermissionRequest ─> POST /guard/evaluate ─> DENY? ─> stdout
48
+ └─ everything else ────────────────> OTLP span
49
+ ```
50
+
51
+ Two properties are load-bearing and both are covered by tests:
52
+
53
+ - **Enforce before telemetry.** A DENY is written to stdout *before* any
54
+ telemetry work, so a telemetry failure can never bubble into the top-level
55
+ fail-open catch and silently allow a blocked tool.
56
+ - **Fail open, always.** No endpoint, a timeout, a non-200, a malformed payload,
57
+ or an unhandled throw all resolve to ALLOW. A security gate must not be able
58
+ to wedge the agent.
59
+
60
+ ## Event coverage
61
+
62
+ All twelve documented events are routed.
63
+
64
+ | Event | Handling |
65
+ | --- | --- |
66
+ | `PreToolUse`, `PermissionRequest` | guard evaluation (+ optional deny) |
67
+ | `UserPromptSubmit` | opens a **new trace** — the per-turn boundary |
68
+ | `SessionStart`, `Stop` | observation |
69
+ | `PostToolUse` | observation |
70
+ | `SubagentStart`, `SubagentStop` | observation |
71
+ | `PreCompact`, `PostCompact` | observation |
72
+ | `PreLLMCall`, `PostLLMCall` | **skipped unless `PINTA_MUSE_LLM_EVENTS=1`** |
73
+
74
+ `PostLLMCall` is expected to fire per response chunk. `pinta-gemini` deliberately
75
+ skips the equivalent `AfterModel` for the same reason, so it is opt-in here until
76
+ the volume has actually been measured.
77
+
78
+ Unrecognised events are **not dropped** — they are forwarded as observations.
79
+ Muse Code is 0.1.x beta whose shim self-updates hourly, so parsing stays
80
+ envelope-open: known keys are validated, everything else rides through.
81
+
82
+ ### Internal tool exemption
83
+
84
+ The lead agent drives its subagent team through native tools
85
+ (`subagent_spawn`, `subagent_status`, `subagent_send_message`, `subagent_cancel`,
86
+ `subagent_wait`, `subagent_read_result`). Blocking one of those does not stop a
87
+ risky action — it breaks the agent's own control loop and kills the turn. They
88
+ skip guard evaluation but are still recorded.
89
+
90
+ Only names confirmed in the official docs are exempt. **Do not add speculative
91
+ entries**: an over-broad exemption is a hole in the guard.
92
+
93
+ ## Configuration
94
+
95
+ Muse Code runs hook commands with *"a cleared environment with a small
96
+ allowlist"*, so inherited process env cannot be relied on. The manager therefore
97
+ writes `~/.config/muse/pinta-musecode.env` (`KEY=VALUE` per line), which the
98
+ adapter loads before anything else reads `process.env`. This is the same
99
+ mechanism every other Pinta hook adapter uses.
100
+
101
+ `$XDG_CONFIG_HOME/muse` takes precedence over `~/.config/muse` when set.
102
+
103
+ | Variable | Meaning |
104
+ | --- | --- |
105
+ | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | OTLP/HTTP traces URL. Unset ⇒ telemetry silently disabled. |
106
+ | `OTEL_EXPORTER_OTLP_HEADERS` | Extra headers, e.g. `x-pinta-relay-token=…` |
107
+ | `PINTA_GUARD_ENDPOINT` | Guard URL. Unset ⇒ guard silently disabled. |
108
+ | `PINTA_RELAY_TOKEN` | Sent as `x-pinta-relay-token` on guard calls. |
109
+ | `PINTA_GUARD_DISABLED=1` | Force-disable the guard even with an endpoint set. |
110
+ | `PINTA_MUSE_ENFORCE=1` | **Leave a DENY in effect.** Default off = shadow mode. |
111
+ | `PINTA_MUSE_DENY_FORMAT` | `auto` (default), `json`, or `exit2`. See below. |
112
+ | `PINTA_MUSE_LLM_EVENTS=1` | Opt into `PreLLMCall` / `PostLLMCall`. |
113
+ | `PINTA_MUSECODE_DATA` | State dir. Default `~/.pinta/adaptors/pinta-musecode`. |
114
+
115
+ State deliberately does **not** live in the workspace: Muse Code treats `.muse`,
116
+ `.git` and `.agents` as read-only even inside an otherwise writable workspace.
117
+
118
+ ### Shadow mode
119
+
120
+ Enforcement is off by default. The guard is still queried and its verdict still
121
+ rides on the span, but nothing is written back to the host. That is the plan's
122
+ staged rollout expressed in code:
123
+
124
+ > Muse Code's default approval mode catches only three dangerous commands, so
125
+ > **our DENY is the first block a user ever experiences.** One false positive and
126
+ > the hook gets deleted. Measure the false-positive rate in shadow first.
127
+
128
+ ## Confirmed contracts
129
+
130
+ Verified against **`muse 0.1.0-R708.1`** on macOS by running the adapter as a
131
+ real managed hook. Anything not listed here is still inference.
132
+
133
+ Muse Code documents no hook API — `muse --help` does not mention hooks at all —
134
+ so everything below is **observed behaviour of one build**, established by
135
+ running the host and watching what it did. None of it is a supported interface,
136
+ and any of it can change without notice. The tests in `tests/` exist to catch
137
+ that: the host rejects a malformed hook file **silently**, so a contract change
138
+ would otherwise show up as telemetry quietly going missing rather than as an
139
+ error.
140
+
141
+ ### Registration
142
+
143
+ `managed_hooks_path` is a key in Muse Code's **`settings.json`**, not an
144
+ environment variable. `TBH_MANAGED_HOOKS_PATH` exists in the binary but does not
145
+ register hooks. Inline `hooks` in `settings.json` did **not** fire in testing;
146
+ only the managed file did.
147
+
148
+ The file needs the `hooks` wrapper and the matcher-**group** level. Each was
149
+ tested on its own, holding everything else constant:
150
+
151
+ ```json
152
+ {
153
+ "schema_version": 1,
154
+ "hooks": {
155
+ "PreToolUse": [
156
+ { "matcher": "*", "hooks": [{ "type": "command", "command": "…" }] }
157
+ ]
158
+ }
159
+ }
160
+ ```
161
+
162
+ | Variant | Result |
163
+ | --- | --- |
164
+ | As above | fires |
165
+ | Handlers listed straight under the event name | **never fires** |
166
+ | Event names hoisted to the top level, no `hooks` wrapper | **never fires** |
167
+ | `schema_version` omitted | fires — it is optional |
168
+ | `matcher` omitted | fires — it is optional |
169
+
170
+ > ⚠️ **A malformed or missing managed hooks file is ignored in total silence** —
171
+ > no warning, no exit code, no log. `tests/managed-hooks.test.ts` guards the
172
+ > template against exactly this, because nothing else would catch it.
173
+
174
+ Worse, group and handler objects are **deserialized strictly, and a rejection is
175
+ also silent**. One unrecognised key and the whole event is skipped while the rest
176
+ of the file keeps working — so a typo removes monitoring from one event and
177
+ leaves the adapter looking healthy:
178
+
179
+ | Handler key | |
180
+ | --- | --- |
181
+ | `type` `command` `commandWindows` `timeout` `statusMessage` `silent` `async` | accepted |
182
+ | **`env`** | **rejected — event skipped** |
183
+ | `shell` | rejected — event skipped |
184
+ | `enabled` on a matcher group | rejected — event skipped |
185
+ | any unknown key | rejected — event skipped |
186
+
187
+ Unknown keys at the *top* level are tolerated, which is why the template can
188
+ carry `_note`-style documentation.
189
+
190
+ `timeout` is in **seconds**: a hook sleeping 3s under `timeout: 1` was killed at
191
+ ~1s wall.
192
+
193
+ The rejected `env` key is the reason configuration goes through an env file.
194
+ The binary's strings advertise it, but there is no working in-file way to pass a
195
+ variable to a hook.
196
+
197
+ ### The hook environment
198
+
199
+ Hook commands get an environment filtered to **thirteen** variables:
200
+
201
+ ```
202
+ HOME LANG LOGNAME OLDPWD PATH PWD SHELL SHLVL TERM TMPDIR USER _ __CF_USER_TEXT_ENCODING
203
+ ```
204
+
205
+ Consequences, all load-bearing:
206
+
207
+ - **No `PINTA_*` or `OTEL_*` shell export reaches the hook.** The env file is the
208
+ only configuration channel that works.
209
+ - **`XDG_CONFIG_HOME` is stripped too**, so inside a hook the config dir can only
210
+ resolve to `$HOME/.config/muse`. pinta-manager must write the env file there
211
+ even for users who set `XDG_CONFIG_HOME`, or the adapter silently no-ops.
212
+ - **`PATH` is inherited in full**, so a bare `node` resolves fine.
213
+
214
+ ### Payload
215
+
216
+ `hook_event_name` **is** present in the payload on every event, and the event
217
+ name also arrives on argv. The adapter prefers argv and falls back to the
218
+ payload.
219
+
220
+ Present on every event: `hook_event_name`, `session_id`, `cwd`, `model`,
221
+ `permission_mode`, `transcript_path` (which may be `null`; `model` may be
222
+ `"unknown"`). `turn_id` rides on everything except `SessionStart`.
223
+
224
+ | Event | Additional keys |
225
+ | --- | --- |
226
+ | `SessionStart` | `source` |
227
+ | `UserPromptSubmit` | `prompt` |
228
+ | `PreToolUse` | `tool_name`, `tool_input`, `tool_use_id` |
229
+ | `PostToolUse` | + `tool_response` |
230
+ | `PreLLMCall` | `attempt`, `provider`, `request_id`, `step`, `messages`, `tools`, `message_count`, `tool_count` |
231
+ | `PostLLMCall` | + `response_id`, `finish_reason`, `output_text_preview`, `status`, `error`, `tool_call_count`, `usage` |
232
+ | `SubagentStart` | `subagent_id`, `child_session_id` |
233
+ | `Stop` | `last_assistant_message`, `stop_hook_active` |
234
+
235
+ > ⚠️ **`PreLLMCall`/`PostLLMCall` carry the full `messages` array and tool
236
+ > schemas** — the entire conversation, not a preview. This is why they are
237
+ > opt-in and absent from the template.
238
+
239
+ > ⚠️ **On `SubagentStart`, `session_id` is the *child's* session id**, not the
240
+ > parent's, and no parent id is present. Correlate subagents through the trace
241
+ > file (which the adapter does) rather than through `session_id`.
242
+
243
+ ### Deny contract — measured
244
+
245
+ There is **no single deny shape**, and the plan's warning not to assume one was
246
+ right. Verified with a live managed `UserPromptSubmit` hook:
247
+
248
+ | Hook output | Result |
249
+ | --- | --- |
250
+ | `{"decision":"block","reason":…}` | **BLOCKED** |
251
+ | `{"hookSpecificOutput":{…,"permissionDecision":"deny"}}` | ignored, turn proceeded |
252
+ | `{"hookSpecificOutput":{…,"decision":"deny"}}` | ignored, turn proceeded |
253
+ | `{"continue":false,"stopReason":…}` | ignored, turn proceeded |
254
+ | exit code **2** | **BLOCKED** |
255
+ | exit code 1 | proceeded |
256
+ | unparseable stdout | proceeded |
257
+ | missing hook binary | proceeded |
258
+
259
+ So the host is **fail-open on everything except exit 2**. A crashed adapter
260
+ cannot wedge the agent — but it also cannot enforce, which is why stage 3 needs
261
+ bypass visibility rather than trusting the hook to always run.
262
+
263
+ `PINTA_MUSE_DENY_FORMAT` now defaults to `auto`, which picks the shape by event
264
+ family and pairs it with exit 2 (the two channels are independent, so a deny
265
+ still lands even if a host update rejects the JSON). `json` and `exit2` force a
266
+ single channel.
267
+
268
+ > ⚠️ Sending the wrong family's shape is **ignored silently** — no error, turn
269
+ > proceeds. A security control that fails open without saying so is the worst
270
+ > case, so `renderDenyJson()` branches on the event and
271
+ > `tests/core/decision.test.ts` pins both branches.
272
+
273
+ The tool-side shape (`PreToolUse` / `PermissionRequest`) could **not** be
274
+ exercised — the account hit `402 billing_error` before any tool call. It comes
275
+ from the binary's own validation strings, which require `hookSpecificOutput` to
276
+ carry a `hookEventName` matching the firing event alongside `permissionDecision`
277
+ / `permissionDecisionReason`, and which mention "pre-tool hook blocked tool use"
278
+ and "permission hook denied tool use". Strong evidence, not measurement.
279
+
280
+ ### Native OTLP is not an alternative
281
+
282
+ Muse Code ships its own OTLP telemetry, so "just point Muse Code at our
283
+ collector and skip the hooks" looks attractive. It does not work. `telemetry.destination`
284
+ is an enum (`legacy` | `consolidated` | `edge` | `external`), not a URL, and:
285
+
286
+ - `external` + `OTEL_EXPORTER_OTLP_ENDPOINT` at our own host → **export DISABLED**;
287
+ the binary detects the non-baseline destination and withholds the bearer
288
+ credential on purpose.
289
+ - `edge` / `consolidated` → internal-network destinations, **not available in
290
+ public builds**.
291
+
292
+ The hook adapter is therefore the only viable telemetry path, not merely the
293
+ chosen one.
294
+
295
+ ### Cost
296
+
297
+ Measured on an 18-core M-series Mac, `--provider echo`, adapter built to `dist/`:
298
+
299
+ | | |
300
+ | --- | --- |
301
+ | Hook invocations on a trivial turn | **5** — `SessionStart`, `UserPromptSubmit`, `SubagentStart` ×2, `Stop` |
302
+ | One adapter invocation, cold | **~45 ms** (p50 44, max 49) — dominated by Node start-up |
303
+ | Turn wall-clock, no hooks → 5 hooks | 0.28 s → 0.47 s |
304
+ | 16 concurrent invocations | **123 ms wall** (7.7 ms/hook amortised) |
305
+
306
+ Hooks for different subagents run **in parallel**, not serially: two
307
+ `SubagentStart` handlers each sleeping 1s cost 1.5s of wall clock, not 2.5s. So
308
+ the 16-subagent case is bounded by the slowest hook rather than by their sum.
309
+
310
+ Two things follow. Node start-up is essentially the entire per-hook cost, which
311
+ is the strongest argument for a native build if `PreToolUse` is ever enabled on
312
+ a hot loop. And `SubagentStart` fires twice even on a turn that does nothing —
313
+ subagent traffic is the volume driver here, not tool calls.
314
+
315
+ ### The session log is a viable backstop
316
+
317
+ `~/.local/share/muse/sessions/YYYY/MM/DD/<session-id>/session.jsonl`, with
318
+ subagents nested underneath as `subagent/<child-id>/session.jsonl`. Every line is
319
+ a JSON envelope (`record_type: "event"`, plus `sequence`, `recorded_at`,
320
+ `payload_type`). Surveyed 73 files / 1351 lines, reading keys only:
321
+
322
+ - **Hook events are recorded here too** — `SessionStart`, `UserPromptSubmit`,
323
+ `Stop`, `SubagentStart`, `PreLLMCall`, `PostLLMCall` all appear as
324
+ `payload.event.event`. The log is a superset of the hook stream, so stage 3 can
325
+ reconcile against it to detect a bypassed or crashed adapter.
326
+ - **Token counts are present**: `model_completed.usage.{input,output,cached,reasoning}_tokens`,
327
+ and again under `goal_usage_attribution.record.quantity.*`.
328
+ - **Byte accounting is present but coarse** — `model_input_trace_recorded` reports
329
+ lane and sample byte counts, not per-edit diff sizes.
330
+ - **Parent/child correlation exists here**, unlike in the hook payload:
331
+ `parent_session_id`, `parent_run_id` and `child_session_log_path`. The directory
332
+ layout alone recovers the parent that `SubagentStart` omits.
333
+ - Tool arguments were **not** observed — no session in the sample reached a tool
334
+ call, so this remains open alongside the tool-side deny shape.
335
+
336
+ ### Still unconfirmed
337
+
338
+ - The tool-side deny shape, on a live tool call (needs a working account).
339
+ - Whether tool arguments and per-edit diff bytes appear in `session.jsonl`.
340
+ - Whether the TUI and `muse exec` emit identical payload shapes.
341
+ - Whether `read_skill` must be exempted. It is a **real tool** (confirmed in the
342
+ binary, with a `skill_read_ledger` behind it), not the speculative name it was
343
+ assumed to be — but its argument shape is unknown, so it is deliberately still
344
+ NOT exempted. Exempting it would widen the unguarded surface.
345
+
346
+ ### Running the spike
347
+
348
+ `tools/` ships a harness that answers most of the above in one pass. Point Muse
349
+ Code's hooks at the capture script instead of the adapter, use Muse Code
350
+ normally, then read the report.
351
+
352
+ ```bash
353
+ npx tsx tools/spike.ts hooks > ~/.config/muse/pinta-capture-hooks.json
354
+ # then add to ~/.config/muse/settings.json:
355
+ # "managed_hooks_path": "/Users/<you>/.config/muse/pinta-capture-hooks.json"
356
+ # ... use Muse Code for a while ...
357
+ npx tsx tools/spike.ts report
358
+ ```
359
+
360
+ There is no `muse hooks validate` command, and a bad file is ignored silently,
361
+ so confirm registration by checking that the capture file actually appears at
362
+ `~/.pinta/spike/capture.jsonl` after one session.
363
+
364
+ The report gives, per event: the union of payload keys, **which keys are not
365
+ always present** (the ones that silently break a transformer later), whether the
366
+ event name came from argv or the payload, parse failures, the slowest
367
+ invocation, any undocumented event the host has started emitting, and the env
368
+ keys that survived on every single capture — that last set *is* the allowlist.
369
+
370
+ `tools/capture-hook.mjs` is dependency-free plain ESM (no build step) and always
371
+ exits 0, so it cannot disturb the session it observes. Setting
372
+ `PINTA_SPIKE_DENY=<Event>` makes it emit a probe denial for that one event, which
373
+ is how the deny wire contract gets confirmed against a real session — use a
374
+ throwaway workspace, because it really does try to block.
375
+
376
+ ## ⚠️ Linux requires Node
377
+
378
+ Muse Code installs as a **single native binary** and runs on macOS and Linux
379
+ only, so its users have no reason to have Node installed. pinta-manager bundles
380
+ a Node binary for macOS and Windows but **not for Linux**, where it invokes the
381
+ literal `node` token.
382
+
383
+ Those two facts collide exactly on Linux: enrollment succeeds and the hooks then
384
+ **silently never run** — a machine that looks protected and is not.
385
+
386
+ This is a runtime-availability problem, **not a portability one**: the adapter
387
+ runs on Linux perfectly well once Node is present. Until the manager ships a
388
+ Linux Node bundle or a native adapter build, the rule is simply that
389
+ **enrollment must fail loudly when Node is missing on Linux**, rather than leave
390
+ a silently unprotected machine. Run the doctor before enrolling:
391
+
392
+ ```bash
393
+ npx tsx tools/doctor.ts # exits non-zero on a blocking problem
394
+ ```
395
+
396
+ ## Development
397
+
398
+ ```bash
399
+ npm install # @pinta-ai/core comes from GitHub Packages (needs read:packages)
400
+ npm run typecheck
401
+ npm test
402
+ npm run build # dist/index.js (CJS hook) + dist/index.mjs (ESM, sidecar-importable)
403
+ npm run doctor
404
+ ```
405
+
406
+ `dist/index.mjs` guards its direct-exec path, so the pinta-manager sidecar can
407
+ `import()` it without reading stdin, dispatching a hook, or exiting the process.
408
+
409
+ ## Release and distribution
410
+
411
+ Not published yet. `@pinta-ai/pinta-musecode` is not on npmjs and the adaptor is
412
+ not listed in `pinta-catalog`, so pinta-manager cannot install it — CI being
413
+ green is necessary but not sufficient.
414
+
415
+ Two things have to happen, in order, and neither can be done from this
416
+ repository alone:
417
+
418
+ 1. **First publish by hand.** `publish.yml` uses OIDC trusted publishing, which
419
+ cannot create a package that does not exist yet
420
+ ([npm/cli#8544](https://github.com/npm/cli/issues/8544)). Someone with
421
+ publish rights on the `@pinta-ai` scope runs `npm publish --access public`
422
+ once; every tag after that goes through the workflow.
423
+ 2. **Register in `pinta-catalog`**, which pins the tarball by sha256 — so the
424
+ manifest cannot be written before step 1.
425
+
426
+ Full commands, the manifest, and why this adaptor's manifest differs from the
427
+ other hook adaptors are in [`PUBLISHING.md`](PUBLISHING.md).
428
+
429
+ ## License
430
+
431
+ PolyForm Noncommercial 1.0.0
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";var pt=Object.create;var ue=Object.defineProperty;var dt=Object.getOwnPropertyDescriptor;var gt=Object.getOwnPropertyNames;var _t=Object.getPrototypeOf,ht=Object.prototype.hasOwnProperty;var m=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var mt=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of gt(t))!ht.call(e,o)&&o!==r&&ue(e,o,{get:()=>t[o],enumerable:!(n=dt(t,o))||n.enumerable});return e};var y=(e,t,r)=>(r=e!=null?pt(_t(e)):{},mt(t||!e||!e.__esModule?ue(r,"default",{value:e,enumerable:!0}):r,e));var V=m(p=>{"use strict";Object.defineProperty(p,"__esModule",{value:!0});p.PATTERNS=p.MAX_BYTES=void 0;p.truncateTo=ce;p.truncate=yt;p.collectMatches=le;p.resolveOverlaps=fe;p.applyMatches=pe;p.redact=Pt;p.MAX_BYTES=102400;function ce(e,t){let r=Buffer.from(e,"utf-8");return r.length<=t?e:`${r.subarray(0,Math.max(0,t)).toString("utf-8")}\u2026[TRUNCATED:${r.length}]`}function yt(e){return ce(e,p.MAX_BYTES)}p.PATTERNS=[{type:"aws_access_key",regex:/AKIA[0-9A-Z]{16}/g},{type:"aws_secret_key",regex:/(?:aws[_-]?secret(?:[_-]?(?:access)?[_-]?key)?)\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])/gi,captureGroup:1},{type:"gcp_service_account",regex:/\{[\s\S]{0,200}?"type"\s*:\s*"service_account"[\s\S]*?\}/g},{type:"github_token",regex:/gh[pousr]_[A-Za-z0-9]{36,}/g},{type:"gitlab_token",regex:/glpat-[A-Za-z0-9_-]{20}/g},{type:"slack_token",regex:/xox[abrsp]-[0-9A-Za-z-]{10,}/g},{type:"openai_key",regex:/sk-(?:proj-)?[A-Za-z0-9_-]{40,}/g},{type:"anthropic_key",regex:/sk-ant-[A-Za-z0-9_-]{50,}/g},{type:"stripe_key",regex:/(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g},{type:"jwt",regex:/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g},{type:"private_key_block",regex:/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g},{type:"bearer_token",regex:/bearer\s+([A-Za-z0-9._~+/=-]{12,})/gi,captureGroup:1},{type:"basic_auth",regex:/basic\s+([A-Za-z0-9+/=]{12,})/gi,captureGroup:1},{type:"db_url_password",regex:/\b(?:postgres|postgresql|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/]+:([^@\s]+)@/gi,captureGroup:1},{type:"cli_password_flag",regex:/(?:--password|--pass|--pwd)[=\s]([^\s'"]+)/g,captureGroup:1},{type:"cli_password_short",regex:/\s-p([^\s'"]+)/g,captureGroup:1,requireContext:"bash"},{type:"env_var_secret",regex:/^(?:export\s+)?([A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|API_KEY)[A-Z0-9_]*)\s*=\s*["']?([^\s"'\n]+)/gm,captureGroup:2}];function le(e,t){let r=[];for(let n of p.PATTERNS){if(n.requireContext&&n.requireContext!==t.context)continue;let o=new RegExp(n.regex.source,n.regex.flags),i;for(;(i=o.exec(e))!==null;){let a=n.captureGroup??0,u=i[a];if(u===void 0){i.index===o.lastIndex&&o.lastIndex++;continue}let c=i.index,h=i.index+i[0].length,w=c+i[0].indexOf(u),ft=w+u.length;r.push({start:c,end:h,replaceStart:w,replaceEnd:ft,type:n.type}),i.index===o.lastIndex&&o.lastIndex++}}return r}function fe(e){let t=[...e].sort((o,i)=>o.start!==i.start?o.start-i.start:i.end-i.start-(o.end-o.start)),r=[],n=-1;for(let o of t)o.start<n||(r.push(o),n=o.end);return r}function pe(e,t){let r=[...t].sort((o,i)=>i.replaceStart-o.replaceStart),n=e;for(let o of r)n=n.slice(0,o.replaceStart)+`[REDACTED:${o.type}]`+n.slice(o.replaceEnd);return n}function Pt(e,t={}){if(e.length===0)return e;let r=le(e,t);if(r.length===0)return e;let n=fe(r);return pe(e,n)}});var U=m(f=>{"use strict";var Et=f&&f.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(f,"__esModule",{value:!0});f.MAX_SPAN_BYTES=void 0;f.ulidToTraceId=he;f.newSpanId=me;f.snakeCase=Tt;f.toOtlpValue=ye;f.attrsFromRecord=bt;f.guardAttrs=Pe;f.clientAttrs=Ee;f.enforceSpanBudget=ve;f.buildPayload=Ot;f.mergeBatch=kt;var vt=Et(require("crypto")),G=V(),St="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function he(e){if(e.length!==26)throw new Error(`ulidToTraceId: expected 26 chars, got ${e.length}`);let t=0n;for(let n of e){let o=St.indexOf(n);if(o<0)throw new Error(`ulidToTraceId: invalid Crockford char "${n}"`);t=t<<5n|BigInt(o)}let r=(1n<<128n)-1n;return t&=r,t.toString(16).padStart(32,"0")}function me(){return vt.default.randomBytes(8).toString("hex")}function Tt(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2").toLowerCase()}var de=new Set;function R(e,t,r){let n=(0,G.truncate)(t);if((r.skipRedactKeys??de).has(e))return n;let o=(r.bashContextKeys??de).has(e)?"bash":void 0;return(0,G.redact)(n,{context:o})}function ye(e,t,r={}){if(t==null)return null;switch(typeof t){case"string":return{stringValue:R(e,t,r)};case"boolean":return{boolValue:t};case"number":return Number.isInteger(t)?{intValue:t}:{doubleValue:t};case"object":try{return{stringValue:R(e,JSON.stringify(t),r)}}catch{return{stringValue:R(e,String(t),r)}}default:return{stringValue:R(e,String(t),r)}}}function bt(e,t,r={}){let n=[];for(let[o,i]of Object.entries(e)){let a=`${t}.${o}`,u=ye(a,i,r);u!==null&&n.push({key:a,value:u})}return n}function Pe(e){let t=[{key:"pinta.guard.decision",value:{stringValue:e.decision.toLowerCase()}},{key:"pinta.guard.duration_ms",value:{intValue:e.durationMs}}];return e.reason&&t.push({key:"pinta.guard.matched_rule",value:{stringValue:e.reason}}),e.failOpenReason&&t.push({key:"pinta.guard.fail_open_reason",value:{stringValue:e.failOpenReason}}),t}function Ee(e){return[{key:"pinta.client.rtt_ms",value:Number.isInteger(e.rttMs)?{intValue:e.rttMs}:{doubleValue:e.rttMs}},{key:"pinta.client.op",value:{stringValue:e.op}}]}f.MAX_SPAN_BYTES=800*1024;function ge(e){let t=JSON.stringify(e);return t===void 0?0:Buffer.byteLength(t,"utf-8")}var _e=1024;function ve(e,t=f.MAX_SPAN_BYTES){let r=ge(e);for(let n=0;n<100&&r>t;n++){let o=null,i=0;for(let c of e.attributes){if(!("stringValue"in c.value))continue;let h=Buffer.byteLength(c.value.stringValue,"utf-8");h>i&&(i=h,o=c.value)}if(o===null||i<=_e)break;let a=r-t,u=Math.max(_e,i-a-64);o.stringValue=(0,G.truncateTo)(o.stringValue,u),r=ge(e)}return e}function Ot(e){let t=e.now??Date.now(),r=(BigInt(t)*1000000n).toString(),n=[...e.attributes];e.guard&&n.push(...Pe(e.guard));let o=e.clientTiming??(e.guard?.clientRttMs!==void 0?{op:"guard",rttMs:e.guard.clientRttMs}:null);o&&n.push(...Ee(o));let i={traceId:he(e.traceId),spanId:me(),name:e.spanName,kind:e.spanKind??1,startTimeUnixNano:r,endTimeUnixNano:r,attributes:n};return ve(i),{resourceSpans:[{resource:{attributes:e.resource},scopeSpans:[{scope:e.scope,spans:[i]}]}]}}function kt(e){let t=[];for(let r of e)t.push(...r.resourceSpans);return{resourceSpans:t}}});var Se=m(X=>{"use strict";Object.defineProperty(X,"__esModule",{value:!0});X.evaluateGuard=xt;var At=1e4,wt="pinta-core";function Y(e,t){return{decision:"ALLOW",reason:null,userMessage:null,durationMs:e,clientRttMs:e,failOpenReason:t}}var Mt=new Set(["ALLOW","DENY","REVIEW"]);async function xt(e,t,r={}){if(!t||r.disabled)return null;let n=r.timeoutMs??At,o=Date.now(),i=new AbortController,a=setTimeout(()=>i.abort(),n);try{let u=await fetch(t,{method:"POST",headers:{"content-type":"application/json","user-agent":r.userAgent??wt,"x-pinta-relay-token":r.token??""},body:JSON.stringify({input:e}),signal:i.signal});if(u.status!==200)return Y(Date.now()-o,"error");let c=await u.json(),h=c?.durationMs===void 0||typeof c.durationMs=="number"&&Number.isFinite(c.durationMs);if(!c||!Mt.has(c.decision)||!h)return Y(Date.now()-o,"error");let w=Date.now()-o;return{decision:c.decision,reason:c.reason??null,userMessage:c.userMessage??null,durationMs:c.durationMs??w,clientRttMs:w}}catch(u){let c=u.name,h=c==="AbortError"||c==="TimeoutError"?"timeout":"error";return Y(Date.now()-o,h)}finally{clearTimeout(a)}}});var j=m(M=>{"use strict";var Rt=M&&M.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(M,"__esModule",{value:!0});M.atomicWriteFileSync=jt;var Te=Rt(require("fs"));function jt(e,t){let r=`${e}.${process.pid}.tmp`;Te.default.writeFileSync(r,t),Te.default.renameSync(r,e)}});var Z=m(P=>{"use strict";var be=P&&P.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(P,"__esModule",{value:!0});P.MemoryRetryQueue=P.DiskRetryQueue=void 0;var d=be(require("fs")),D=be(require("path")),Dt=j(),N=1e3,Nt=50,It=5,z=class{filePath;lockPath;logPrefix;constructor(t,r){this.filePath=D.default.join(t,"failed-spans.jsonl"),this.lockPath=this.filePath+".lock",this.logPrefix=r}enqueue(t){try{d.default.mkdirSync(D.default.dirname(this.filePath),{recursive:!0});let r=JSON.stringify({savedAt:new Date().toISOString(),payload:t})+`
2
+ `;d.default.appendFileSync(this.filePath,r),this.trim()}catch(r){process.stderr.write(`[${this.logPrefix}] retry-queue enqueue failed: ${r}
3
+ `)}}readAll(){try{let t=d.default.readFileSync(this.filePath,"utf-8"),r=[];for(let n of t.split(`
4
+ `))if(n.trim())try{let o=JSON.parse(n);Array.isArray(o?.payload?.resourceSpans)&&r.push(o)}catch{}return r}catch{return[]}}rewrite(t){try{if(t.length===0){d.default.existsSync(this.filePath)&&d.default.unlinkSync(this.filePath);return}d.default.mkdirSync(D.default.dirname(this.filePath),{recursive:!0}),(0,Dt.atomicWriteFileSync)(this.filePath,t.map(r=>JSON.stringify(r)).join(`
5
+ `)+`
6
+ `)}catch(r){process.stderr.write(`[${this.logPrefix}] retry-queue rewrite failed: ${r}
7
+ `)}}tryAcquireLock(){let t=Date.now();try{d.default.mkdirSync(D.default.dirname(this.lockPath),{recursive:!0})}catch(r){return process.stderr.write(`[${this.logPrefix}] retry-queue lock mkdir failed: ${r}
8
+ `),!1}for(;Date.now()-t<Nt;)try{let r=d.default.openSync(this.lockPath,"wx");return d.default.writeSync(r,String(process.pid)),d.default.closeSync(r),!0}catch(r){if(r?.code!=="EEXIST")return process.stderr.write(`[${this.logPrefix}] retry-queue lock open failed: ${r}
9
+ `),!1;try{let i=d.default.statSync(this.lockPath);if(Date.now()-i.mtimeMs>3e4){d.default.unlinkSync(this.lockPath);continue}}catch{}let n=It,o=Date.now()+n;for(;Date.now()<o;);}return!1}release(){try{d.default.unlinkSync(this.lockPath)}catch{}}trim(){let t=this.readAll();if(t.length<=N)return;let r=t.length-N;process.stderr.write(`[${this.logPrefix}] retry-queue full, dropping ${r} oldest entries
10
+ `),this.rewrite(t.slice(r))}};P.DiskRetryQueue=z;var K=class{entries=[];enqueue(t){this.entries.push(t),this.entries.length>N&&this.entries.splice(0,this.entries.length-N)}drain(){let t=this.entries;return this.entries=[],t}get size(){return this.entries.length}};P.MemoryRetryQueue=K});var xe=m(l=>{"use strict";Object.defineProperty(l,"__esModule",{value:!0});l.MemoryTransport=l.DiskTransport=l.MAX_POST_BYTES=void 0;l.parseHeadersEnv=we;l.envOptionsResolver=Me;l.postOtlp=x;var Oe=Z(),ke=U(),Ct=5e3;l.MAX_POST_BYTES=900*1024;function I(e){let t=JSON.stringify(e);return t===void 0?0:Buffer.byteLength(t,"utf-8")}function Ae(e,t,r){let n=[],o=[],i=[],a=0;for(let u of e){let c=t(u);if(c>r){o.push(u);continue}i.length>0&&a+c>r&&(n.push(i),i=[],a=0),i.push(u),a+=c}return i.length>0&&n.push(i),{chunks:n,oversize:o}}function we(e){if(!e)return{};if(typeof e=="object")return{...e};let t={};for(let r of e.split(",")){let[n,...o]=r.split("=");n&&o.length>0&&(t[n.trim()]=o.join("=").trim())}return t}function Me(){let e=process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,t=process.env.OTEL_EXPORTER_OTLP_ENDPOINT,r;return e?r=e.replace(/\/+$/,""):t&&(r=t.replace(/\/+$/,"")+"/v1/traces"),r?{endpoint:r,headers:we(process.env.OTEL_EXPORTER_OTLP_HEADERS)}:null}async function x(e,t,r){let n=new AbortController,o=setTimeout(()=>n.abort(),Ct);try{let i=await fetch(t.endpoint,{method:"POST",headers:{"Content-Type":"application/json",...t.headers},body:JSON.stringify(e),signal:n.signal});if(!i.ok){let a="";try{a=(await i.text()).slice(0,200)}catch{}let u=i.status===401||i.status===403?" \u2014 check OTEL_EXPORTER_OTLP_HEADERS (relay token)":i.status===404?" \u2014 check OTEL_EXPORTER_OTLP_TRACES_ENDPOINT path":i.status>=500?" \u2014 collector may be down":"";return process.stderr.write(`[${r}] OTLP POST ${i.status} ${t.endpoint}${u}${a?` body=${a}`:""}
11
+ `),!1}return!0}catch(i){return process.stderr.write(`[${r}] OTLP POST failed: ${i.message??String(i)}
12
+ `),!1}finally{clearTimeout(o)}}var W=class{queue;logPrefix;resolveOptions;constructor(t){this.queue=new Oe.DiskRetryQueue(t.pluginData,t.logPrefix),this.logPrefix=t.logPrefix,this.resolveOptions=t.resolveOptions??Me}async send(t){let r=this.resolveOptions();if(!r||this.dropIfOversized(t))return;await x(t,r,this.logPrefix)||this.queue.enqueue(t)}async flush(){let t=this.resolveOptions();if(t&&this.queue.tryAcquireLock())try{let r=this.queue.readAll();if(r.length===0)return;let{chunks:n,oversize:o}=Ae(r,u=>I(u.payload),l.MAX_POST_BYTES);o.length>0&&process.stderr.write(`[${this.logPrefix}] dropping ${o.length} oversized retry ${o.length===1?"entry":"entries"} (each > ${l.MAX_POST_BYTES} bytes \u2014 the backend caps the request body, so they can never be delivered)
13
+ `);let i=[].concat(...n),a=0;for(let u of n){if(!await x((0,ke.mergeBatch)(u.map(h=>h.payload)),t,this.logPrefix))break;a+=u.length}this.queue.rewrite(i.slice(a))}catch(r){process.stderr.write(`[${this.logPrefix}] flush failed: ${r?.message??String(r)}
14
+ `)}finally{this.queue.release()}}dropIfOversized(t){let r=I(t);return r<=l.MAX_POST_BYTES?!1:(process.stderr.write(`[${this.logPrefix}] dropping oversized span payload (${r} > ${l.MAX_POST_BYTES} bytes) \u2014 undeliverable, not queued
15
+ `),!0)}};l.DiskTransport=W;var J=class{queue=new Oe.MemoryRetryQueue;logPrefix;resolveOptions;constructor(t){this.logPrefix=t.logPrefix,this.resolveOptions=t.resolveOptions}async send(t){let r=this.resolveOptions();if(!r)return;if(I(t)>l.MAX_POST_BYTES){process.stderr.write(`[${this.logPrefix}] dropping oversized span payload (> ${l.MAX_POST_BYTES} bytes) \u2014 undeliverable, not buffered
16
+ `);return}await x(t,r,this.logPrefix)||this.queue.enqueue(t)}async flush(){let t=this.resolveOptions();if(!t)return;let r=this.queue.drain();if(r.length===0)return;let{chunks:n,oversize:o}=Ae(r,I,l.MAX_POST_BYTES);o.length>0&&process.stderr.write(`[${this.logPrefix}] dropping ${o.length} oversized buffered ${o.length===1?"payload":"payloads"} (each > ${l.MAX_POST_BYTES} bytes \u2014 undeliverable)
17
+ `);let i=[].concat(...n),a=0;for(let u of n){if(!await x((0,ke.mergeBatch)(u),t,this.logPrefix))break;a+=u.length}for(let u of i.slice(a))this.queue.enqueue(u)}};l.MemoryTransport=J});var te=m(S=>{"use strict";var ee=S&&S.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(S,"__esModule",{value:!0});S.TraceManager=void 0;S.generateUlid=je;var Re=ee(require("fs")),Bt=ee(require("path")),Lt=ee(require("crypto")),$t=j(),H="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function je(){let e=Date.now(),t="",r=e;for(let i=0;i<10;i++)t=H[r&31]+t,r=Math.floor(r/32);let n=Lt.default.randomBytes(10),o="";for(let i=0;i<10;i++)o+=H[n[i]&31];for(;o.length<16;)o+=H[0];return t+o}var Q=class{tracePath;constructor(t){this.tracePath=t}newTrace(){let t=je();return this.save(t),t}currentTrace(){try{let t=Re.default.readFileSync(this.tracePath,"utf-8"),{traceId:r}=JSON.parse(t);if(r)return r}catch{}return this.newTrace()}save(t){try{Re.default.mkdirSync(Bt.default.dirname(this.tracePath),{recursive:!0}),(0,$t.atomicWriteFileSync)(this.tracePath,JSON.stringify({traceId:t}))}catch(r){process.stderr.write(`[pinta-core] trace persistence failed: ${r}
18
+ `)}}};S.TraceManager=Q});var Be=m(E=>{"use strict";var Ne=E&&E.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(E,"__esModule",{value:!0});E.MemorySessionTraceManager=E.DiskSessionTraceManager=void 0;var De=Ne(require("fs")),qt=Ne(require("path")),Ie=te(),Ft=j(),Ce=200,re=class{tracePath;maxSessions;constructor(t,r={}){this.tracePath=t,this.maxSessions=r.maxSessions??Ce}read(){try{let t=JSON.parse(De.default.readFileSync(this.tracePath,"utf-8"));if(t&&typeof t=="object"&&!("traceId"in t))return t}catch{}return{}}write(t){try{De.default.mkdirSync(qt.default.dirname(this.tracePath),{recursive:!0});let r=Object.entries(t),n=r.length>this.maxSessions?Object.fromEntries(r.slice(-this.maxSessions)):t;(0,Ft.atomicWriteFileSync)(this.tracePath,JSON.stringify(n))}catch{}}newTrace(t){let r=t||"default",n=(0,Ie.generateUlid)(),o=this.read();return o[r]=n,this.write(o),n}currentTrace(t){let r=t||"default",n=this.read()[r];return n||this.newTrace(r)}};E.DiskSessionTraceManager=re;var ne=class{map=new Map;maxSessions;constructor(t={}){this.maxSessions=t.maxSessions??Ce}newTrace(t){let r=t||"default",n=(0,Ie.generateUlid)();if(this.map.set(r,n),this.map.size>this.maxSessions){let o=this.map.keys().next().value;o!==void 0&&this.map.delete(o)}return n}currentTrace(t){let r=t||"default";return this.map.get(r)??this.newTrace(r)}};E.MemorySessionTraceManager=ne});var qe=m(T=>{"use strict";var oe=T&&T.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(T,"__esModule",{value:!0});T.envFilePath=Ut;T.parseEnvFile=$e;T.loadEnvFile=Yt;var Vt=oe(require("node:fs")),Gt=oe(require("node:os")),Le=oe(require("node:path"));function Ut(e,t,r){let n=r?process.env[r]:void 0,o=n&&n.length>0?n:Le.default.join(Gt.default.homedir(),e);return Le.default.join(o,t)}function $e(e){let t={};for(let r of e.split(`
19
+ `)){let n=r.trim();if(!n||n.startsWith("#"))continue;let o=n.indexOf("=");if(o<0)continue;let i=n.slice(0,o).trim(),a=n.slice(o+1).trim();(a.startsWith('"')&&a.endsWith('"')||a.startsWith("'")&&a.endsWith("'"))&&(a=a.slice(1,-1)),i&&(t[i]=a)}return t}function Yt(e){let t;try{t=Vt.default.readFileSync(e,"utf-8")}catch{return}let r=$e(t);for(let[n,o]of Object.entries(r))process.env[n]===void 0&&(process.env[n]=o)}});var O=m(s=>{"use strict";Object.defineProperty(s,"__esModule",{value:!0});s.loadEnvFile=s.parseEnvFile=s.envFilePath=s.MemorySessionTraceManager=s.DiskSessionTraceManager=s.generateUlid=s.TraceManager=s.MAX_POST_BYTES=s.parseHeadersEnv=s.envOptionsResolver=s.postOtlp=s.MemoryTransport=s.DiskTransport=s.MemoryRetryQueue=s.DiskRetryQueue=s.evaluateGuard=s.MAX_SPAN_BYTES=s.enforceSpanBudget=s.mergeBatch=s.buildPayload=s.clientAttrs=s.guardAttrs=s.attrsFromRecord=s.toOtlpValue=s.snakeCase=s.newSpanId=s.ulidToTraceId=s.MAX_BYTES=s.PATTERNS=s.applyMatches=s.resolveOverlaps=s.collectMatches=s.truncateTo=s.truncate=s.redact=void 0;var v=V();Object.defineProperty(s,"redact",{enumerable:!0,get:function(){return v.redact}});Object.defineProperty(s,"truncate",{enumerable:!0,get:function(){return v.truncate}});Object.defineProperty(s,"truncateTo",{enumerable:!0,get:function(){return v.truncateTo}});Object.defineProperty(s,"collectMatches",{enumerable:!0,get:function(){return v.collectMatches}});Object.defineProperty(s,"resolveOverlaps",{enumerable:!0,get:function(){return v.resolveOverlaps}});Object.defineProperty(s,"applyMatches",{enumerable:!0,get:function(){return v.applyMatches}});Object.defineProperty(s,"PATTERNS",{enumerable:!0,get:function(){return v.PATTERNS}});Object.defineProperty(s,"MAX_BYTES",{enumerable:!0,get:function(){return v.MAX_BYTES}});var _=U();Object.defineProperty(s,"ulidToTraceId",{enumerable:!0,get:function(){return _.ulidToTraceId}});Object.defineProperty(s,"newSpanId",{enumerable:!0,get:function(){return _.newSpanId}});Object.defineProperty(s,"snakeCase",{enumerable:!0,get:function(){return _.snakeCase}});Object.defineProperty(s,"toOtlpValue",{enumerable:!0,get:function(){return _.toOtlpValue}});Object.defineProperty(s,"attrsFromRecord",{enumerable:!0,get:function(){return _.attrsFromRecord}});Object.defineProperty(s,"guardAttrs",{enumerable:!0,get:function(){return _.guardAttrs}});Object.defineProperty(s,"clientAttrs",{enumerable:!0,get:function(){return _.clientAttrs}});Object.defineProperty(s,"buildPayload",{enumerable:!0,get:function(){return _.buildPayload}});Object.defineProperty(s,"mergeBatch",{enumerable:!0,get:function(){return _.mergeBatch}});Object.defineProperty(s,"enforceSpanBudget",{enumerable:!0,get:function(){return _.enforceSpanBudget}});Object.defineProperty(s,"MAX_SPAN_BYTES",{enumerable:!0,get:function(){return _.MAX_SPAN_BYTES}});var Xt=Se();Object.defineProperty(s,"evaluateGuard",{enumerable:!0,get:function(){return Xt.evaluateGuard}});var Fe=Z();Object.defineProperty(s,"DiskRetryQueue",{enumerable:!0,get:function(){return Fe.DiskRetryQueue}});Object.defineProperty(s,"MemoryRetryQueue",{enumerable:!0,get:function(){return Fe.MemoryRetryQueue}});var b=xe();Object.defineProperty(s,"DiskTransport",{enumerable:!0,get:function(){return b.DiskTransport}});Object.defineProperty(s,"MemoryTransport",{enumerable:!0,get:function(){return b.MemoryTransport}});Object.defineProperty(s,"postOtlp",{enumerable:!0,get:function(){return b.postOtlp}});Object.defineProperty(s,"envOptionsResolver",{enumerable:!0,get:function(){return b.envOptionsResolver}});Object.defineProperty(s,"parseHeadersEnv",{enumerable:!0,get:function(){return b.parseHeadersEnv}});Object.defineProperty(s,"MAX_POST_BYTES",{enumerable:!0,get:function(){return b.MAX_POST_BYTES}});var Ve=te();Object.defineProperty(s,"TraceManager",{enumerable:!0,get:function(){return Ve.TraceManager}});Object.defineProperty(s,"generateUlid",{enumerable:!0,get:function(){return Ve.generateUlid}});var Ge=Be();Object.defineProperty(s,"DiskSessionTraceManager",{enumerable:!0,get:function(){return Ge.DiskSessionTraceManager}});Object.defineProperty(s,"MemorySessionTraceManager",{enumerable:!0,get:function(){return Ge.MemorySessionTraceManager}});var se=qe();Object.defineProperty(s,"envFilePath",{enumerable:!0,get:function(){return se.envFilePath}});Object.defineProperty(s,"parseEnvFile",{enumerable:!0,get:function(){return se.parseEnvFile}});Object.defineProperty(s,"loadEnvFile",{enumerable:!0,get:function(){return se.loadEnvFile}})});var Ue=y(require("node:os")),C=y(require("node:path")),ie=y(O());var zt="pinta-musecode.env";function Kt(){let e=process.env.XDG_CONFIG_HOME;return e&&e.length>0?C.default.join(e,"muse"):C.default.join(Ue.default.homedir(),".config","muse")}function Zt(){return C.default.join(Kt(),zt)}function Ye(e=Zt()){(0,ie.loadEnvFile)(e)}var Xe=y(require("node:os")),ae=y(require("node:path"));function Wt(){return process.env.PINTA_MUSECODE_DATA||process.env.PINTA_PLUGIN_DATA||ae.default.join(Xe.default.homedir(),".pinta","adaptors","pinta-musecode")}function ze(){let e=Wt();return{pluginData:e,tracePath:ae.default.join(e,"trace.json")}}function Ke(){return process.env.PINTA_MUSE_ENFORCE==="1"}function Ze(){return process.env.PINTA_MUSE_LLM_EVENTS==="1"}var Jt=["SessionStart","UserPromptSubmit","PreToolUse","PermissionRequest","PostToolUse","PreLLMCall","PostLLMCall","PreCompact","PostCompact","SubagentStart","SubagentStop","Stop"],Ht=new Set(Jt);function Qt(e){return Ht.has(e)}var er=["hook_event_name","event","hook","hook_event","eventName"];function We(e,t){for(let r of e){let n=r.startsWith("--event=")?r.slice(8):r;if(Qt(n))return n}for(let r of er){let n=t[r];if(typeof n=="string"&&n.length>0)return n}return null}function B(e){return e==="PreToolUse"||e==="PermissionRequest"}function Je(e){return e==="PreLLMCall"||e==="PostLLMCall"}var tr=new Set(["subagent_spawn","subagent_status","subagent_send_message","subagent_cancel","subagent_wait","subagent_read_result"]);function He(e){return e!==void 0&&tr.has(e)}var Qe=y(O()),rr=1e4,nr="pinta-musecode/0.1.0";function et(e,t){return(0,Qe.evaluateGuard)(e,t,{timeoutMs:rr,token:process.env.PINTA_RELAY_TOKEN??"",disabled:process.env.PINTA_GUARD_DISABLED==="1",userAgent:nr})}function or(){let e=process.env.PINTA_MUSE_DENY_FORMAT;return e==="exit2"||e==="json"?e:"auto"}function sr(e,t){return B(e)?{hookSpecificOutput:{hookEventName:e,permissionDecision:"deny",permissionDecisionReason:t}}:{decision:"block",reason:t}}function tt(e,t,r={}){let n=r.stdout??(a=>process.stdout.write(a)),o=r.stderr??(a=>process.stderr.write(a)),i=or();return i==="exit2"?(o(`${t}
20
+ `),{written:!0,exitCode:2}):(n(JSON.stringify(sr(e,t))+`
21
+ `),i==="json"?{written:!0,exitCode:0}:(o(`${t}
22
+ `),{written:!0,exitCode:2}))}var rt=y(O()),L=class extends rt.DiskTransport{constructor(t){super({pluginData:t.pluginData,logPrefix:"pinta-musecode"})}};var nt=y(O()),$=class extends nt.TraceManager{constructor(t){super(t.tracePath)}};var F=y(require("node:os")),k=y(O()),ot="0.1.0",ir="musecode",g="muse",q;function ar(){if(q===void 0)try{q=F.default.userInfo().username}catch{q=process.env.USER??process.env.LOGNAME??(typeof process.getuid=="function"?String(process.getuid()):"unknown")}return q}function ur(e){for(let t of["muse_version","version","cli_version"]){let r=e[t];if(typeof r=="string"&&r.length>0)return r}return process.env.MUSE_VERSION??"unknown"}var cr=new Set([`${g}.hook`,`${g}.tool_name`,`${g}.tool_use_id`,`${g}.session_id`,`${g}.cwd`,`${g}.permission_mode`,`${g}.agent_id`,`${g}.agent_type`]),lr=new Set([`${g}.tool_input`,`${g}.tool_response`]),fr={skipRedactKeys:cr,bashContextKeys:lr};function pr(e){let t=[];t.push({key:"ingest.type",value:{stringValue:ir}}),t.push({key:`${g}.hook`,value:{stringValue:e.hook_event_name}});let r={};for(let[n,o]of Object.entries(e))n!=="hook_event_name"&&(r[n]=o);return t.push(...(0,k.attrsFromRecord)(r,g,fr)),t}function dr(e){return[{key:"service.name",value:{stringValue:"muse-code"}},{key:"service.version",value:{stringValue:ur(e)}},{key:"telemetry.sdk.name",value:{stringValue:"pinta-musecode"}},{key:"telemetry.sdk.language",value:{stringValue:"nodejs"}},{key:"telemetry.sdk.version",value:{stringValue:ot}},{key:"process.pid",value:{intValue:process.pid}},{key:"process.owner",value:{stringValue:ar()}},{key:"host.name",value:{stringValue:F.default.hostname()}},{key:"host.arch",value:{stringValue:F.default.arch()}}]}function st(e){return(0,k.buildPayload)({traceId:e.traceId,spanName:`${g}.${(0,k.snakeCase)(e.event.hook_event_name)}`,attributes:pr(e.event),resource:dr(e.event),scope:{name:"pinta-musecode",version:ot},now:e.now,guard:e.guard})}async function gr(e,t,r={}){let n=new L(t);await n.flush();let o=new $(t),i=r.traceMode==="new"?o.newTrace():o.currentTrace(),a=st({event:e,traceId:i,guard:r.guard});await n.send(a)}async function A(e,t,r={}){try{await gr(e,t,r)}catch(n){process.stderr.write(`[pinta-musecode] telemetry emit failed: ${n}
23
+ `)}}async function it(e,t){if(He(e.tool_name))return await A(e,t),0;let r=typeof e.tool_input=="string"?e.tool_input:JSON.stringify(e.tool_input),n=await et({spanId:e.session_id??"unknown",toolName:e.tool_name,toolInput:e.tool_input,rawTextFields:{toolInput:r??""}},process.env.PINTA_GUARD_ENDPOINT),o=0;if(n?.decision==="DENY"&&Ke()){let i=n.userMessage??n.reason??"guard_deny";o=tt(e.hook_event_name,i).exitCode}return await A(e,t,{guard:n}),o}async function at(e,t){return await A(e,t),0}async function ut(e,t){return await A(e,t,{traceMode:"new"}),0}async function ct(e){return 0}async function _r(){let e=[];for await(let t of process.stdin)e.push(t);return Buffer.concat(e).toString("utf-8")}function hr(e){let t=e.trim();if(t.length===0)return{};try{let r=JSON.parse(t);if(r!==null&&typeof r=="object"&&!Array.isArray(r))return r}catch{}return{}}async function mr(e,t){let r=e.hook_event_name;return B(r)?it(e,t):r==="UserPromptSubmit"?ut(e,t):Je(r)&&!Ze()?ct(e):at(e,t)}async function lt(e=process.argv.slice(2)){let t=0;try{let r=ze(),n=hr(await _r()),o=We(e,n);if(o===null)return process.stderr.write(`[pinta-musecode] could not resolve hook event name; skipping
24
+ `),0;let i={...n,hook_event_name:o};t=await mr(i,r)}catch(r){process.stderr.write(`[pinta-musecode] error: ${r}
25
+ `),t=0}return t}Ye();async function yr(){let e=await lt();process.exit(e)}yr();
package/dist/index.mjs ADDED
@@ -0,0 +1,26 @@
1
+ import{createRequire as __pintaCreateRequire}from 'node:module';const require=__pintaCreateRequire(import.meta.url);
2
+ var ft=Object.create;var ue=Object.defineProperty;var pt=Object.getOwnPropertyDescriptor;var dt=Object.getOwnPropertyNames;var gt=Object.getPrototypeOf,_t=Object.prototype.hasOwnProperty;var _=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,r)=>(typeof require<"u"?require:t)[r]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var y=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var ht=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of dt(t))!_t.call(e,o)&&o!==r&&ue(e,o,{get:()=>t[o],enumerable:!(n=pt(t,o))||n.enumerable});return e};var M=(e,t,r)=>(r=e!=null?ft(gt(e)):{},ht(t||!e||!e.__esModule?ue(r,"default",{value:e,enumerable:!0}):r,e));var F=y(p=>{"use strict";Object.defineProperty(p,"__esModule",{value:!0});p.PATTERNS=p.MAX_BYTES=void 0;p.truncateTo=ce;p.truncate=mt;p.collectMatches=le;p.resolveOverlaps=fe;p.applyMatches=pe;p.redact=yt;p.MAX_BYTES=102400;function ce(e,t){let r=Buffer.from(e,"utf-8");return r.length<=t?e:`${r.subarray(0,Math.max(0,t)).toString("utf-8")}\u2026[TRUNCATED:${r.length}]`}function mt(e){return ce(e,p.MAX_BYTES)}p.PATTERNS=[{type:"aws_access_key",regex:/AKIA[0-9A-Z]{16}/g},{type:"aws_secret_key",regex:/(?:aws[_-]?secret(?:[_-]?(?:access)?[_-]?key)?)\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])/gi,captureGroup:1},{type:"gcp_service_account",regex:/\{[\s\S]{0,200}?"type"\s*:\s*"service_account"[\s\S]*?\}/g},{type:"github_token",regex:/gh[pousr]_[A-Za-z0-9]{36,}/g},{type:"gitlab_token",regex:/glpat-[A-Za-z0-9_-]{20}/g},{type:"slack_token",regex:/xox[abrsp]-[0-9A-Za-z-]{10,}/g},{type:"openai_key",regex:/sk-(?:proj-)?[A-Za-z0-9_-]{40,}/g},{type:"anthropic_key",regex:/sk-ant-[A-Za-z0-9_-]{50,}/g},{type:"stripe_key",regex:/(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g},{type:"jwt",regex:/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g},{type:"private_key_block",regex:/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g},{type:"bearer_token",regex:/bearer\s+([A-Za-z0-9._~+/=-]{12,})/gi,captureGroup:1},{type:"basic_auth",regex:/basic\s+([A-Za-z0-9+/=]{12,})/gi,captureGroup:1},{type:"db_url_password",regex:/\b(?:postgres|postgresql|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/]+:([^@\s]+)@/gi,captureGroup:1},{type:"cli_password_flag",regex:/(?:--password|--pass|--pwd)[=\s]([^\s'"]+)/g,captureGroup:1},{type:"cli_password_short",regex:/\s-p([^\s'"]+)/g,captureGroup:1,requireContext:"bash"},{type:"env_var_secret",regex:/^(?:export\s+)?([A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|API_KEY)[A-Z0-9_]*)\s*=\s*["']?([^\s"'\n]+)/gm,captureGroup:2}];function le(e,t){let r=[];for(let n of p.PATTERNS){if(n.requireContext&&n.requireContext!==t.context)continue;let o=new RegExp(n.regex.source,n.regex.flags),i;for(;(i=o.exec(e))!==null;){let a=n.captureGroup??0,u=i[a];if(u===void 0){i.index===o.lastIndex&&o.lastIndex++;continue}let c=i.index,m=i.index+i[0].length,w=c+i[0].indexOf(u),lt=w+u.length;r.push({start:c,end:m,replaceStart:w,replaceEnd:lt,type:n.type}),i.index===o.lastIndex&&o.lastIndex++}}return r}function fe(e){let t=[...e].sort((o,i)=>o.start!==i.start?o.start-i.start:i.end-i.start-(o.end-o.start)),r=[],n=-1;for(let o of t)o.start<n||(r.push(o),n=o.end);return r}function pe(e,t){let r=[...t].sort((o,i)=>i.replaceStart-o.replaceStart),n=e;for(let o of r)n=n.slice(0,o.replaceStart)+`[REDACTED:${o.type}]`+n.slice(o.replaceEnd);return n}function yt(e,t={}){if(e.length===0)return e;let r=le(e,t);if(r.length===0)return e;let n=fe(r);return pe(e,n)}});var G=y(f=>{"use strict";var Et=f&&f.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(f,"__esModule",{value:!0});f.MAX_SPAN_BYTES=void 0;f.ulidToTraceId=he;f.newSpanId=me;f.snakeCase=St;f.toOtlpValue=ye;f.attrsFromRecord=Tt;f.guardAttrs=Ee;f.clientAttrs=Pe;f.enforceSpanBudget=ve;f.buildPayload=bt;f.mergeBatch=Ot;var Pt=Et(_("crypto")),V=F(),vt="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function he(e){if(e.length!==26)throw new Error(`ulidToTraceId: expected 26 chars, got ${e.length}`);let t=0n;for(let n of e){let o=vt.indexOf(n);if(o<0)throw new Error(`ulidToTraceId: invalid Crockford char "${n}"`);t=t<<5n|BigInt(o)}let r=(1n<<128n)-1n;return t&=r,t.toString(16).padStart(32,"0")}function me(){return Pt.default.randomBytes(8).toString("hex")}function St(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2").toLowerCase()}var de=new Set;function j(e,t,r){let n=(0,V.truncate)(t);if((r.skipRedactKeys??de).has(e))return n;let o=(r.bashContextKeys??de).has(e)?"bash":void 0;return(0,V.redact)(n,{context:o})}function ye(e,t,r={}){if(t==null)return null;switch(typeof t){case"string":return{stringValue:j(e,t,r)};case"boolean":return{boolValue:t};case"number":return Number.isInteger(t)?{intValue:t}:{doubleValue:t};case"object":try{return{stringValue:j(e,JSON.stringify(t),r)}}catch{return{stringValue:j(e,String(t),r)}}default:return{stringValue:j(e,String(t),r)}}}function Tt(e,t,r={}){let n=[];for(let[o,i]of Object.entries(e)){let a=`${t}.${o}`,u=ye(a,i,r);u!==null&&n.push({key:a,value:u})}return n}function Ee(e){let t=[{key:"pinta.guard.decision",value:{stringValue:e.decision.toLowerCase()}},{key:"pinta.guard.duration_ms",value:{intValue:e.durationMs}}];return e.reason&&t.push({key:"pinta.guard.matched_rule",value:{stringValue:e.reason}}),e.failOpenReason&&t.push({key:"pinta.guard.fail_open_reason",value:{stringValue:e.failOpenReason}}),t}function Pe(e){return[{key:"pinta.client.rtt_ms",value:Number.isInteger(e.rttMs)?{intValue:e.rttMs}:{doubleValue:e.rttMs}},{key:"pinta.client.op",value:{stringValue:e.op}}]}f.MAX_SPAN_BYTES=800*1024;function ge(e){let t=JSON.stringify(e);return t===void 0?0:Buffer.byteLength(t,"utf-8")}var _e=1024;function ve(e,t=f.MAX_SPAN_BYTES){let r=ge(e);for(let n=0;n<100&&r>t;n++){let o=null,i=0;for(let c of e.attributes){if(!("stringValue"in c.value))continue;let m=Buffer.byteLength(c.value.stringValue,"utf-8");m>i&&(i=m,o=c.value)}if(o===null||i<=_e)break;let a=r-t,u=Math.max(_e,i-a-64);o.stringValue=(0,V.truncateTo)(o.stringValue,u),r=ge(e)}return e}function bt(e){let t=e.now??Date.now(),r=(BigInt(t)*1000000n).toString(),n=[...e.attributes];e.guard&&n.push(...Ee(e.guard));let o=e.clientTiming??(e.guard?.clientRttMs!==void 0?{op:"guard",rttMs:e.guard.clientRttMs}:null);o&&n.push(...Pe(o));let i={traceId:he(e.traceId),spanId:me(),name:e.spanName,kind:e.spanKind??1,startTimeUnixNano:r,endTimeUnixNano:r,attributes:n};return ve(i),{resourceSpans:[{resource:{attributes:e.resource},scopeSpans:[{scope:e.scope,spans:[i]}]}]}}function Ot(e){let t=[];for(let r of e)t.push(...r.resourceSpans);return{resourceSpans:t}}});var Se=y(Y=>{"use strict";Object.defineProperty(Y,"__esModule",{value:!0});Y.evaluateGuard=Mt;var kt=1e4,At="pinta-core";function U(e,t){return{decision:"ALLOW",reason:null,userMessage:null,durationMs:e,clientRttMs:e,failOpenReason:t}}var wt=new Set(["ALLOW","DENY","REVIEW"]);async function Mt(e,t,r={}){if(!t||r.disabled)return null;let n=r.timeoutMs??kt,o=Date.now(),i=new AbortController,a=setTimeout(()=>i.abort(),n);try{let u=await fetch(t,{method:"POST",headers:{"content-type":"application/json","user-agent":r.userAgent??At,"x-pinta-relay-token":r.token??""},body:JSON.stringify({input:e}),signal:i.signal});if(u.status!==200)return U(Date.now()-o,"error");let c=await u.json(),m=c?.durationMs===void 0||typeof c.durationMs=="number"&&Number.isFinite(c.durationMs);if(!c||!wt.has(c.decision)||!m)return U(Date.now()-o,"error");let w=Date.now()-o;return{decision:c.decision,reason:c.reason??null,userMessage:c.userMessage??null,durationMs:c.durationMs??w,clientRttMs:w}}catch(u){let c=u.name,m=c==="AbortError"||c==="TimeoutError"?"timeout":"error";return U(Date.now()-o,m)}finally{clearTimeout(a)}}});var D=y(x=>{"use strict";var xt=x&&x.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(x,"__esModule",{value:!0});x.atomicWriteFileSync=Rt;var Te=xt(_("fs"));function Rt(e,t){let r=`${e}.${process.pid}.tmp`;Te.default.writeFileSync(r,t),Te.default.renameSync(r,e)}});var K=y(E=>{"use strict";var be=E&&E.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(E,"__esModule",{value:!0});E.MemoryRetryQueue=E.DiskRetryQueue=void 0;var d=be(_("fs")),N=be(_("path")),jt=D(),I=1e3,Dt=50,Nt=5,X=class{filePath;lockPath;logPrefix;constructor(t,r){this.filePath=N.default.join(t,"failed-spans.jsonl"),this.lockPath=this.filePath+".lock",this.logPrefix=r}enqueue(t){try{d.default.mkdirSync(N.default.dirname(this.filePath),{recursive:!0});let r=JSON.stringify({savedAt:new Date().toISOString(),payload:t})+`
3
+ `;d.default.appendFileSync(this.filePath,r),this.trim()}catch(r){process.stderr.write(`[${this.logPrefix}] retry-queue enqueue failed: ${r}
4
+ `)}}readAll(){try{let t=d.default.readFileSync(this.filePath,"utf-8"),r=[];for(let n of t.split(`
5
+ `))if(n.trim())try{let o=JSON.parse(n);Array.isArray(o?.payload?.resourceSpans)&&r.push(o)}catch{}return r}catch{return[]}}rewrite(t){try{if(t.length===0){d.default.existsSync(this.filePath)&&d.default.unlinkSync(this.filePath);return}d.default.mkdirSync(N.default.dirname(this.filePath),{recursive:!0}),(0,jt.atomicWriteFileSync)(this.filePath,t.map(r=>JSON.stringify(r)).join(`
6
+ `)+`
7
+ `)}catch(r){process.stderr.write(`[${this.logPrefix}] retry-queue rewrite failed: ${r}
8
+ `)}}tryAcquireLock(){let t=Date.now();try{d.default.mkdirSync(N.default.dirname(this.lockPath),{recursive:!0})}catch(r){return process.stderr.write(`[${this.logPrefix}] retry-queue lock mkdir failed: ${r}
9
+ `),!1}for(;Date.now()-t<Dt;)try{let r=d.default.openSync(this.lockPath,"wx");return d.default.writeSync(r,String(process.pid)),d.default.closeSync(r),!0}catch(r){if(r?.code!=="EEXIST")return process.stderr.write(`[${this.logPrefix}] retry-queue lock open failed: ${r}
10
+ `),!1;try{let i=d.default.statSync(this.lockPath);if(Date.now()-i.mtimeMs>3e4){d.default.unlinkSync(this.lockPath);continue}}catch{}let n=Nt,o=Date.now()+n;for(;Date.now()<o;);}return!1}release(){try{d.default.unlinkSync(this.lockPath)}catch{}}trim(){let t=this.readAll();if(t.length<=I)return;let r=t.length-I;process.stderr.write(`[${this.logPrefix}] retry-queue full, dropping ${r} oldest entries
11
+ `),this.rewrite(t.slice(r))}};E.DiskRetryQueue=X;var z=class{entries=[];enqueue(t){this.entries.push(t),this.entries.length>I&&this.entries.splice(0,this.entries.length-I)}drain(){let t=this.entries;return this.entries=[],t}get size(){return this.entries.length}};E.MemoryRetryQueue=z});var xe=y(l=>{"use strict";Object.defineProperty(l,"__esModule",{value:!0});l.MemoryTransport=l.DiskTransport=l.MAX_POST_BYTES=void 0;l.parseHeadersEnv=we;l.envOptionsResolver=Me;l.postOtlp=R;var Oe=K(),ke=G(),It=5e3;l.MAX_POST_BYTES=900*1024;function C(e){let t=JSON.stringify(e);return t===void 0?0:Buffer.byteLength(t,"utf-8")}function Ae(e,t,r){let n=[],o=[],i=[],a=0;for(let u of e){let c=t(u);if(c>r){o.push(u);continue}i.length>0&&a+c>r&&(n.push(i),i=[],a=0),i.push(u),a+=c}return i.length>0&&n.push(i),{chunks:n,oversize:o}}function we(e){if(!e)return{};if(typeof e=="object")return{...e};let t={};for(let r of e.split(",")){let[n,...o]=r.split("=");n&&o.length>0&&(t[n.trim()]=o.join("=").trim())}return t}function Me(){let e=process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,t=process.env.OTEL_EXPORTER_OTLP_ENDPOINT,r;return e?r=e.replace(/\/+$/,""):t&&(r=t.replace(/\/+$/,"")+"/v1/traces"),r?{endpoint:r,headers:we(process.env.OTEL_EXPORTER_OTLP_HEADERS)}:null}async function R(e,t,r){let n=new AbortController,o=setTimeout(()=>n.abort(),It);try{let i=await fetch(t.endpoint,{method:"POST",headers:{"Content-Type":"application/json",...t.headers},body:JSON.stringify(e),signal:n.signal});if(!i.ok){let a="";try{a=(await i.text()).slice(0,200)}catch{}let u=i.status===401||i.status===403?" \u2014 check OTEL_EXPORTER_OTLP_HEADERS (relay token)":i.status===404?" \u2014 check OTEL_EXPORTER_OTLP_TRACES_ENDPOINT path":i.status>=500?" \u2014 collector may be down":"";return process.stderr.write(`[${r}] OTLP POST ${i.status} ${t.endpoint}${u}${a?` body=${a}`:""}
12
+ `),!1}return!0}catch(i){return process.stderr.write(`[${r}] OTLP POST failed: ${i.message??String(i)}
13
+ `),!1}finally{clearTimeout(o)}}var Z=class{queue;logPrefix;resolveOptions;constructor(t){this.queue=new Oe.DiskRetryQueue(t.pluginData,t.logPrefix),this.logPrefix=t.logPrefix,this.resolveOptions=t.resolveOptions??Me}async send(t){let r=this.resolveOptions();if(!r||this.dropIfOversized(t))return;await R(t,r,this.logPrefix)||this.queue.enqueue(t)}async flush(){let t=this.resolveOptions();if(t&&this.queue.tryAcquireLock())try{let r=this.queue.readAll();if(r.length===0)return;let{chunks:n,oversize:o}=Ae(r,u=>C(u.payload),l.MAX_POST_BYTES);o.length>0&&process.stderr.write(`[${this.logPrefix}] dropping ${o.length} oversized retry ${o.length===1?"entry":"entries"} (each > ${l.MAX_POST_BYTES} bytes \u2014 the backend caps the request body, so they can never be delivered)
14
+ `);let i=[].concat(...n),a=0;for(let u of n){if(!await R((0,ke.mergeBatch)(u.map(m=>m.payload)),t,this.logPrefix))break;a+=u.length}this.queue.rewrite(i.slice(a))}catch(r){process.stderr.write(`[${this.logPrefix}] flush failed: ${r?.message??String(r)}
15
+ `)}finally{this.queue.release()}}dropIfOversized(t){let r=C(t);return r<=l.MAX_POST_BYTES?!1:(process.stderr.write(`[${this.logPrefix}] dropping oversized span payload (${r} > ${l.MAX_POST_BYTES} bytes) \u2014 undeliverable, not queued
16
+ `),!0)}};l.DiskTransport=Z;var W=class{queue=new Oe.MemoryRetryQueue;logPrefix;resolveOptions;constructor(t){this.logPrefix=t.logPrefix,this.resolveOptions=t.resolveOptions}async send(t){let r=this.resolveOptions();if(!r)return;if(C(t)>l.MAX_POST_BYTES){process.stderr.write(`[${this.logPrefix}] dropping oversized span payload (> ${l.MAX_POST_BYTES} bytes) \u2014 undeliverable, not buffered
17
+ `);return}await R(t,r,this.logPrefix)||this.queue.enqueue(t)}async flush(){let t=this.resolveOptions();if(!t)return;let r=this.queue.drain();if(r.length===0)return;let{chunks:n,oversize:o}=Ae(r,C,l.MAX_POST_BYTES);o.length>0&&process.stderr.write(`[${this.logPrefix}] dropping ${o.length} oversized buffered ${o.length===1?"payload":"payloads"} (each > ${l.MAX_POST_BYTES} bytes \u2014 undeliverable)
18
+ `);let i=[].concat(...n),a=0;for(let u of n){if(!await R((0,ke.mergeBatch)(u),t,this.logPrefix))break;a+=u.length}for(let u of i.slice(a))this.queue.enqueue(u)}};l.MemoryTransport=W});var ee=y(S=>{"use strict";var Q=S&&S.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(S,"__esModule",{value:!0});S.TraceManager=void 0;S.generateUlid=je;var Re=Q(_("fs")),Ct=Q(_("path")),Bt=Q(_("crypto")),Lt=D(),J="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function je(){let e=Date.now(),t="",r=e;for(let i=0;i<10;i++)t=J[r&31]+t,r=Math.floor(r/32);let n=Bt.default.randomBytes(10),o="";for(let i=0;i<10;i++)o+=J[n[i]&31];for(;o.length<16;)o+=J[0];return t+o}var H=class{tracePath;constructor(t){this.tracePath=t}newTrace(){let t=je();return this.save(t),t}currentTrace(){try{let t=Re.default.readFileSync(this.tracePath,"utf-8"),{traceId:r}=JSON.parse(t);if(r)return r}catch{}return this.newTrace()}save(t){try{Re.default.mkdirSync(Ct.default.dirname(this.tracePath),{recursive:!0}),(0,Lt.atomicWriteFileSync)(this.tracePath,JSON.stringify({traceId:t}))}catch(r){process.stderr.write(`[pinta-core] trace persistence failed: ${r}
19
+ `)}}};S.TraceManager=H});var Be=y(P=>{"use strict";var Ne=P&&P.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(P,"__esModule",{value:!0});P.MemorySessionTraceManager=P.DiskSessionTraceManager=void 0;var De=Ne(_("fs")),$t=Ne(_("path")),Ie=ee(),qt=D(),Ce=200,te=class{tracePath;maxSessions;constructor(t,r={}){this.tracePath=t,this.maxSessions=r.maxSessions??Ce}read(){try{let t=JSON.parse(De.default.readFileSync(this.tracePath,"utf-8"));if(t&&typeof t=="object"&&!("traceId"in t))return t}catch{}return{}}write(t){try{De.default.mkdirSync($t.default.dirname(this.tracePath),{recursive:!0});let r=Object.entries(t),n=r.length>this.maxSessions?Object.fromEntries(r.slice(-this.maxSessions)):t;(0,qt.atomicWriteFileSync)(this.tracePath,JSON.stringify(n))}catch{}}newTrace(t){let r=t||"default",n=(0,Ie.generateUlid)(),o=this.read();return o[r]=n,this.write(o),n}currentTrace(t){let r=t||"default",n=this.read()[r];return n||this.newTrace(r)}};P.DiskSessionTraceManager=te;var re=class{map=new Map;maxSessions;constructor(t={}){this.maxSessions=t.maxSessions??Ce}newTrace(t){let r=t||"default",n=(0,Ie.generateUlid)();if(this.map.set(r,n),this.map.size>this.maxSessions){let o=this.map.keys().next().value;o!==void 0&&this.map.delete(o)}return n}currentTrace(t){let r=t||"default";return this.map.get(r)??this.newTrace(r)}};P.MemorySessionTraceManager=re});var qe=y(T=>{"use strict";var ne=T&&T.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(T,"__esModule",{value:!0});T.envFilePath=Gt;T.parseEnvFile=$e;T.loadEnvFile=Ut;var Ft=ne(_("node:fs")),Vt=ne(_("node:os")),Le=ne(_("node:path"));function Gt(e,t,r){let n=r?process.env[r]:void 0,o=n&&n.length>0?n:Le.default.join(Vt.default.homedir(),e);return Le.default.join(o,t)}function $e(e){let t={};for(let r of e.split(`
20
+ `)){let n=r.trim();if(!n||n.startsWith("#"))continue;let o=n.indexOf("=");if(o<0)continue;let i=n.slice(0,o).trim(),a=n.slice(o+1).trim();(a.startsWith('"')&&a.endsWith('"')||a.startsWith("'")&&a.endsWith("'"))&&(a=a.slice(1,-1)),i&&(t[i]=a)}return t}function Ut(e){let t;try{t=Ft.default.readFileSync(e,"utf-8")}catch{return}let r=$e(t);for(let[n,o]of Object.entries(r))process.env[n]===void 0&&(process.env[n]=o)}});var O=y(s=>{"use strict";Object.defineProperty(s,"__esModule",{value:!0});s.loadEnvFile=s.parseEnvFile=s.envFilePath=s.MemorySessionTraceManager=s.DiskSessionTraceManager=s.generateUlid=s.TraceManager=s.MAX_POST_BYTES=s.parseHeadersEnv=s.envOptionsResolver=s.postOtlp=s.MemoryTransport=s.DiskTransport=s.MemoryRetryQueue=s.DiskRetryQueue=s.evaluateGuard=s.MAX_SPAN_BYTES=s.enforceSpanBudget=s.mergeBatch=s.buildPayload=s.clientAttrs=s.guardAttrs=s.attrsFromRecord=s.toOtlpValue=s.snakeCase=s.newSpanId=s.ulidToTraceId=s.MAX_BYTES=s.PATTERNS=s.applyMatches=s.resolveOverlaps=s.collectMatches=s.truncateTo=s.truncate=s.redact=void 0;var v=F();Object.defineProperty(s,"redact",{enumerable:!0,get:function(){return v.redact}});Object.defineProperty(s,"truncate",{enumerable:!0,get:function(){return v.truncate}});Object.defineProperty(s,"truncateTo",{enumerable:!0,get:function(){return v.truncateTo}});Object.defineProperty(s,"collectMatches",{enumerable:!0,get:function(){return v.collectMatches}});Object.defineProperty(s,"resolveOverlaps",{enumerable:!0,get:function(){return v.resolveOverlaps}});Object.defineProperty(s,"applyMatches",{enumerable:!0,get:function(){return v.applyMatches}});Object.defineProperty(s,"PATTERNS",{enumerable:!0,get:function(){return v.PATTERNS}});Object.defineProperty(s,"MAX_BYTES",{enumerable:!0,get:function(){return v.MAX_BYTES}});var h=G();Object.defineProperty(s,"ulidToTraceId",{enumerable:!0,get:function(){return h.ulidToTraceId}});Object.defineProperty(s,"newSpanId",{enumerable:!0,get:function(){return h.newSpanId}});Object.defineProperty(s,"snakeCase",{enumerable:!0,get:function(){return h.snakeCase}});Object.defineProperty(s,"toOtlpValue",{enumerable:!0,get:function(){return h.toOtlpValue}});Object.defineProperty(s,"attrsFromRecord",{enumerable:!0,get:function(){return h.attrsFromRecord}});Object.defineProperty(s,"guardAttrs",{enumerable:!0,get:function(){return h.guardAttrs}});Object.defineProperty(s,"clientAttrs",{enumerable:!0,get:function(){return h.clientAttrs}});Object.defineProperty(s,"buildPayload",{enumerable:!0,get:function(){return h.buildPayload}});Object.defineProperty(s,"mergeBatch",{enumerable:!0,get:function(){return h.mergeBatch}});Object.defineProperty(s,"enforceSpanBudget",{enumerable:!0,get:function(){return h.enforceSpanBudget}});Object.defineProperty(s,"MAX_SPAN_BYTES",{enumerable:!0,get:function(){return h.MAX_SPAN_BYTES}});var Yt=Se();Object.defineProperty(s,"evaluateGuard",{enumerable:!0,get:function(){return Yt.evaluateGuard}});var Fe=K();Object.defineProperty(s,"DiskRetryQueue",{enumerable:!0,get:function(){return Fe.DiskRetryQueue}});Object.defineProperty(s,"MemoryRetryQueue",{enumerable:!0,get:function(){return Fe.MemoryRetryQueue}});var b=xe();Object.defineProperty(s,"DiskTransport",{enumerable:!0,get:function(){return b.DiskTransport}});Object.defineProperty(s,"MemoryTransport",{enumerable:!0,get:function(){return b.MemoryTransport}});Object.defineProperty(s,"postOtlp",{enumerable:!0,get:function(){return b.postOtlp}});Object.defineProperty(s,"envOptionsResolver",{enumerable:!0,get:function(){return b.envOptionsResolver}});Object.defineProperty(s,"parseHeadersEnv",{enumerable:!0,get:function(){return b.parseHeadersEnv}});Object.defineProperty(s,"MAX_POST_BYTES",{enumerable:!0,get:function(){return b.MAX_POST_BYTES}});var Ve=ee();Object.defineProperty(s,"TraceManager",{enumerable:!0,get:function(){return Ve.TraceManager}});Object.defineProperty(s,"generateUlid",{enumerable:!0,get:function(){return Ve.generateUlid}});var Ge=Be();Object.defineProperty(s,"DiskSessionTraceManager",{enumerable:!0,get:function(){return Ge.DiskSessionTraceManager}});Object.defineProperty(s,"MemorySessionTraceManager",{enumerable:!0,get:function(){return Ge.MemorySessionTraceManager}});var oe=qe();Object.defineProperty(s,"envFilePath",{enumerable:!0,get:function(){return oe.envFilePath}});Object.defineProperty(s,"parseEnvFile",{enumerable:!0,get:function(){return oe.parseEnvFile}});Object.defineProperty(s,"loadEnvFile",{enumerable:!0,get:function(){return oe.loadEnvFile}})});import{realpathSync as Er}from"node:fs";import{pathToFileURL as Pr}from"node:url";var ie=M(O());import Xt from"node:os";import se from"node:path";var zt="pinta-musecode.env";function Kt(){let e=process.env.XDG_CONFIG_HOME;return e&&e.length>0?se.join(e,"muse"):se.join(Xt.homedir(),".config","muse")}function Zt(){return se.join(Kt(),zt)}function Ue(e=Zt()){(0,ie.loadEnvFile)(e)}import Wt from"node:os";import Ye from"node:path";function Jt(){return process.env.PINTA_MUSECODE_DATA||process.env.PINTA_PLUGIN_DATA||Ye.join(Wt.homedir(),".pinta","adaptors","pinta-musecode")}function Xe(){let e=Jt();return{pluginData:e,tracePath:Ye.join(e,"trace.json")}}function ze(){return process.env.PINTA_MUSE_ENFORCE==="1"}function Ke(){return process.env.PINTA_MUSE_LLM_EVENTS==="1"}var Ht=["SessionStart","UserPromptSubmit","PreToolUse","PermissionRequest","PostToolUse","PreLLMCall","PostLLMCall","PreCompact","PostCompact","SubagentStart","SubagentStop","Stop"],Qt=new Set(Ht);function er(e){return Qt.has(e)}var tr=["hook_event_name","event","hook","hook_event","eventName"];function Ze(e,t){for(let r of e){let n=r.startsWith("--event=")?r.slice(8):r;if(er(n))return n}for(let r of tr){let n=t[r];if(typeof n=="string"&&n.length>0)return n}return null}function B(e){return e==="PreToolUse"||e==="PermissionRequest"}function We(e){return e==="PreLLMCall"||e==="PostLLMCall"}var rr=new Set(["subagent_spawn","subagent_status","subagent_send_message","subagent_cancel","subagent_wait","subagent_read_result"]);function Je(e){return e!==void 0&&rr.has(e)}var He=M(O()),nr=1e4,or="pinta-musecode/0.1.0";function Qe(e,t){return(0,He.evaluateGuard)(e,t,{timeoutMs:nr,token:process.env.PINTA_RELAY_TOKEN??"",disabled:process.env.PINTA_GUARD_DISABLED==="1",userAgent:or})}function sr(){let e=process.env.PINTA_MUSE_DENY_FORMAT;return e==="exit2"||e==="json"?e:"auto"}function ir(e,t){return B(e)?{hookSpecificOutput:{hookEventName:e,permissionDecision:"deny",permissionDecisionReason:t}}:{decision:"block",reason:t}}function et(e,t,r={}){let n=r.stdout??(a=>process.stdout.write(a)),o=r.stderr??(a=>process.stderr.write(a)),i=sr();return i==="exit2"?(o(`${t}
21
+ `),{written:!0,exitCode:2}):(n(JSON.stringify(ir(e,t))+`
22
+ `),i==="json"?{written:!0,exitCode:0}:(o(`${t}
23
+ `),{written:!0,exitCode:2}))}var tt=M(O()),L=class extends tt.DiskTransport{constructor(t){super({pluginData:t.pluginData,logPrefix:"pinta-musecode"})}};var rt=M(O()),$=class extends rt.TraceManager{constructor(t){super(t.tracePath)}};var k=M(O());import ae from"node:os";var nt="0.1.0",ar="musecode",g="muse",q;function ur(){if(q===void 0)try{q=ae.userInfo().username}catch{q=process.env.USER??process.env.LOGNAME??(typeof process.getuid=="function"?String(process.getuid()):"unknown")}return q}function cr(e){for(let t of["muse_version","version","cli_version"]){let r=e[t];if(typeof r=="string"&&r.length>0)return r}return process.env.MUSE_VERSION??"unknown"}var lr=new Set([`${g}.hook`,`${g}.tool_name`,`${g}.tool_use_id`,`${g}.session_id`,`${g}.cwd`,`${g}.permission_mode`,`${g}.agent_id`,`${g}.agent_type`]),fr=new Set([`${g}.tool_input`,`${g}.tool_response`]),pr={skipRedactKeys:lr,bashContextKeys:fr};function dr(e){let t=[];t.push({key:"ingest.type",value:{stringValue:ar}}),t.push({key:`${g}.hook`,value:{stringValue:e.hook_event_name}});let r={};for(let[n,o]of Object.entries(e))n!=="hook_event_name"&&(r[n]=o);return t.push(...(0,k.attrsFromRecord)(r,g,pr)),t}function gr(e){return[{key:"service.name",value:{stringValue:"muse-code"}},{key:"service.version",value:{stringValue:cr(e)}},{key:"telemetry.sdk.name",value:{stringValue:"pinta-musecode"}},{key:"telemetry.sdk.language",value:{stringValue:"nodejs"}},{key:"telemetry.sdk.version",value:{stringValue:nt}},{key:"process.pid",value:{intValue:process.pid}},{key:"process.owner",value:{stringValue:ur()}},{key:"host.name",value:{stringValue:ae.hostname()}},{key:"host.arch",value:{stringValue:ae.arch()}}]}function ot(e){return(0,k.buildPayload)({traceId:e.traceId,spanName:`${g}.${(0,k.snakeCase)(e.event.hook_event_name)}`,attributes:dr(e.event),resource:gr(e.event),scope:{name:"pinta-musecode",version:nt},now:e.now,guard:e.guard})}async function _r(e,t,r={}){let n=new L(t);await n.flush();let o=new $(t),i=r.traceMode==="new"?o.newTrace():o.currentTrace(),a=ot({event:e,traceId:i,guard:r.guard});await n.send(a)}async function A(e,t,r={}){try{await _r(e,t,r)}catch(n){process.stderr.write(`[pinta-musecode] telemetry emit failed: ${n}
24
+ `)}}async function st(e,t){if(Je(e.tool_name))return await A(e,t),0;let r=typeof e.tool_input=="string"?e.tool_input:JSON.stringify(e.tool_input),n=await Qe({spanId:e.session_id??"unknown",toolName:e.tool_name,toolInput:e.tool_input,rawTextFields:{toolInput:r??""}},process.env.PINTA_GUARD_ENDPOINT),o=0;if(n?.decision==="DENY"&&ze()){let i=n.userMessage??n.reason??"guard_deny";o=et(e.hook_event_name,i).exitCode}return await A(e,t,{guard:n}),o}async function it(e,t){return await A(e,t),0}async function at(e,t){return await A(e,t,{traceMode:"new"}),0}async function ut(e){return 0}async function hr(){let e=[];for await(let t of process.stdin)e.push(t);return Buffer.concat(e).toString("utf-8")}function mr(e){let t=e.trim();if(t.length===0)return{};try{let r=JSON.parse(t);if(r!==null&&typeof r=="object"&&!Array.isArray(r))return r}catch{}return{}}async function yr(e,t){let r=e.hook_event_name;return B(r)?st(e,t):r==="UserPromptSubmit"?at(e,t):We(r)&&!Ke()?ut(e):it(e,t)}async function ct(e=process.argv.slice(2)){let t=0;try{let r=Xe(),n=mr(await hr()),o=Ze(e,n);if(o===null)return process.stderr.write(`[pinta-musecode] could not resolve hook event name; skipping
25
+ `),0;let i={...n,hook_event_name:o};t=await yr(i,r)}catch(r){process.stderr.write(`[pinta-musecode] error: ${r}
26
+ `),t=0}return t}Ue();function vr(){let e=process.argv[1];if(!e)return!1;try{return import.meta.url===Pr(Er(e)).href}catch{return!1}}async function Sr(){let e=await ct();process.exit(e)}vr()&&Sr();
@@ -0,0 +1,11 @@
1
+ {
2
+ "_note": "TEMPLATE — a fixture for `muse hooks run <key> --fixture ./fixture.json`. The two top-level keys are the only part confirmed by the official docs: a required `event` matching the hook's event, and a required `stdin` object holding the payload the hook receives. The CONTENTS of `stdin` below are a guess and are exactly what the stage-0 spike must replace with a real capture.",
3
+ "event": "PreToolUse",
4
+ "stdin": {
5
+ "session_id": "00000000-0000-0000-0000-000000000000",
6
+ "cwd": "/path/to/workspace",
7
+ "tool_name": "bash",
8
+ "tool_use_id": "tool-1",
9
+ "tool_input": { "command": "rm -rf /tmp/example" }
10
+ }
11
+ }
@@ -0,0 +1,101 @@
1
+ {
2
+ "_note": "Managed hooks file. The path is set by the `managed_hooks_path` key in Muse Code's settings.json ($XDG_CONFIG_HOME/muse/settings.json, else ~/.config/muse/settings.json). Managed hooks are pre-approved and need no `trust` step. Verified against muse 0.1.0-R708.1.",
3
+ "_schema": "Two nesting levels are mandatory, measured one at a time against muse 0.1.0-R708.1: the `hooks` wrapper, and the matcher-GROUP level between the event name and the handlers. Hoisting event names to the top level, or listing handlers directly under an event name, makes the file ignored SILENTLY. `schema_version` and `matcher` are both optional; they are kept here because they cost nothing and document intent.",
4
+ "_strict": "Unknown keys are tolerated at the TOP level (hence these underscore notes) but are FATAL inside a matcher group or a handler — and fatal means the whole event is skipped with no error, no exit code and no log. Accepted handler keys: type, command, commandWindows, timeout, statusMessage, silent, async. REJECTED: env, shell (and `enabled` on a matcher group). Do not add a key here without probing it first; a typo silently disables monitoring for that event.",
5
+ "_env": "The rejection of the handler-level `env` key is why the adaptor is configured through an env FILE. It is not a stylistic preference — there is no in-file way to pass a variable to a hook, and Muse Code filters the inherited environment down to a fixed allowlist that no PINTA_* or OTEL_* name survives.",
6
+ "_timeout": "`timeout` is in SECONDS. Measured: a hook sleeping 3s under `timeout: 1` was killed at ~1s wall. Left unset here so the host default applies; pinta-manager may set it per deployment.",
7
+ "_placeholder": "${PINTA_MUSECODE_ROOT} is substituted by pinta-manager at enroll time with the adaptor install directory. PATH is inherited in full by hook commands, so a bare `node` resolves; pinta-manager still writes an absolute interpreter path so the hook cannot break when the user's PATH changes.",
8
+ "_omitted": "PreLLMCall and PostLLMCall are intentionally absent. Both carry the FULL `messages` array and tool schemas, so enabling them ships conversation content to the collector — opt in with PINTA_MUSE_LLM_EVENTS=1 only after a redaction review.",
9
+ "schema_version": 1,
10
+ "hooks": {
11
+ "SessionStart": [
12
+ {
13
+ "matcher": "*",
14
+ "hooks": [
15
+ { "type": "command", "command": "node ${PINTA_MUSECODE_ROOT}/dist/index.js SessionStart" }
16
+ ]
17
+ }
18
+ ],
19
+ "UserPromptSubmit": [
20
+ {
21
+ "matcher": "*",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "node ${PINTA_MUSECODE_ROOT}/dist/index.js UserPromptSubmit"
26
+ }
27
+ ]
28
+ }
29
+ ],
30
+ "PreToolUse": [
31
+ {
32
+ "matcher": "*",
33
+ "hooks": [
34
+ { "type": "command", "command": "node ${PINTA_MUSECODE_ROOT}/dist/index.js PreToolUse" }
35
+ ]
36
+ }
37
+ ],
38
+ "PermissionRequest": [
39
+ {
40
+ "matcher": "*",
41
+ "hooks": [
42
+ {
43
+ "type": "command",
44
+ "command": "node ${PINTA_MUSECODE_ROOT}/dist/index.js PermissionRequest"
45
+ }
46
+ ]
47
+ }
48
+ ],
49
+ "PostToolUse": [
50
+ {
51
+ "matcher": "*",
52
+ "hooks": [
53
+ { "type": "command", "command": "node ${PINTA_MUSECODE_ROOT}/dist/index.js PostToolUse" }
54
+ ]
55
+ }
56
+ ],
57
+ "PreCompact": [
58
+ {
59
+ "matcher": "*",
60
+ "hooks": [
61
+ { "type": "command", "command": "node ${PINTA_MUSECODE_ROOT}/dist/index.js PreCompact" }
62
+ ]
63
+ }
64
+ ],
65
+ "PostCompact": [
66
+ {
67
+ "matcher": "*",
68
+ "hooks": [
69
+ { "type": "command", "command": "node ${PINTA_MUSECODE_ROOT}/dist/index.js PostCompact" }
70
+ ]
71
+ }
72
+ ],
73
+ "SubagentStart": [
74
+ {
75
+ "matcher": "*",
76
+ "hooks": [
77
+ {
78
+ "type": "command",
79
+ "command": "node ${PINTA_MUSECODE_ROOT}/dist/index.js SubagentStart"
80
+ }
81
+ ]
82
+ }
83
+ ],
84
+ "SubagentStop": [
85
+ {
86
+ "matcher": "*",
87
+ "hooks": [
88
+ { "type": "command", "command": "node ${PINTA_MUSECODE_ROOT}/dist/index.js SubagentStop" }
89
+ ]
90
+ }
91
+ ],
92
+ "Stop": [
93
+ {
94
+ "matcher": "*",
95
+ "hooks": [
96
+ { "type": "command", "command": "node ${PINTA_MUSECODE_ROOT}/dist/index.js Stop" }
97
+ ]
98
+ }
99
+ ]
100
+ }
101
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@pinta-ai/pinta-musecode",
3
+ "version": "0.1.0",
4
+ "description": "Generic OTLP forwarder for Meta Muse Code hook events",
5
+ "main": "dist/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.mjs",
9
+ "require": "./dist/index.js"
10
+ }
11
+ },
12
+ "license": "PolyForm-Noncommercial-1.0.0",
13
+ "files": [
14
+ "dist",
15
+ "hooks",
16
+ "LICENSE",
17
+ "README.md",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/pinta-ai/pinta-musecode.git"
23
+ },
24
+ "homepage": "https://github.com/pinta-ai/pinta-musecode#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/pinta-ai/pinta-musecode/issues"
27
+ },
28
+ "scripts": {
29
+ "typecheck": "tsc --noEmit",
30
+ "build": "tsc --noEmit && npm run build:cjs && npm run build:esm",
31
+ "build:cjs": "esbuild src/index.ts --bundle --platform=node --format=cjs --target=node18 --minify --outfile=dist/index.js",
32
+ "build:esm": "esbuild src/index.mts --bundle --platform=node --format=esm --target=node18 --minify --banner:js=\"import{createRequire as __pintaCreateRequire}from 'node:module';const require=__pintaCreateRequire(import.meta.url);\" --outfile=dist/index.mjs",
33
+ "dev": "tsc --watch",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest",
36
+ "doctor": "tsx tools/doctor.ts",
37
+ "spike:hooks": "tsx tools/spike.ts hooks",
38
+ "spike:report": "tsx tools/spike.ts report",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ "devDependencies": {
42
+ "@pinta-ai/core": "^0.5.0",
43
+ "@types/node": "^22.0.0",
44
+ "esbuild": "^0.24.0",
45
+ "tsx": "^4.0.0",
46
+ "typescript": "^5.7.0",
47
+ "vitest": "^2.1.0"
48
+ },
49
+ "publishConfig": {
50
+ "registry": "https://registry.npmjs.org",
51
+ "access": "public"
52
+ }
53
+ }