@hydra-acp/budgeter 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sam Magnuson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # hydra-acp-budgeter
2
+
3
+ Cost-budget transformer extension for [hydra-acp](https://github.com/smagnuso/hydra-acp). Watches every session's `usage_update` events, warns attached clients when total spend crosses a soft limit, and rejects further prompts once it crosses a hard limit — with a human-readable reason returned to the client.
4
+
5
+ Runs as a daemon-managed *transformer* (not a client extension): it connects once, declares its intercepts via `transformer/initialize`, and sits inside the daemon's message pipeline for every live session.
6
+
7
+ ## Install
8
+
9
+ From npm (recommended once published):
10
+
11
+ ```sh
12
+ npm install -g @hydra-acp/budgeter
13
+ ```
14
+
15
+ This drops a `hydra-acp-budgeter` binary on your PATH.
16
+
17
+ Or from source:
18
+
19
+ ```sh
20
+ git clone git@github.com:smagnuso/hydra-acp-budgeter.git ~/dev/hydra-acp-budgeter
21
+ cd ~/dev/hydra-acp-budgeter
22
+ npm install
23
+ npm run build
24
+ ```
25
+
26
+ Register the transformer with hydra. If installed via npm:
27
+
28
+ ```sh
29
+ hydra-acp transformers add hydra-acp-budgeter --command hydra-acp-budgeter
30
+ ```
31
+
32
+ Or pointed at a local build:
33
+
34
+ ```sh
35
+ hydra-acp transformers add hydra-acp-budgeter \
36
+ --command node \
37
+ --args ~/dev/hydra-acp-budgeter/dist/index.js
38
+ ```
39
+
40
+ That registers the transformer but does **not** wire it into sessions yet. You also need to add it to `defaultTransformers` in `~/.hydra-acp/config.json` — there's no CLI command for this, edit the file directly:
41
+
42
+ ```json
43
+ {
44
+ "transformers": {
45
+ "hydra-acp-budgeter": {
46
+ "command": ["node"],
47
+ "args": ["/home/you/dev/hydra-acp-budgeter/dist/index.js"]
48
+ }
49
+ },
50
+ "defaultTransformers": ["hydra-acp-budgeter"]
51
+ }
52
+ ```
53
+
54
+ Without `defaultTransformers`, the transformer process runs but no sessions route through it.
55
+
56
+ On `hydra-acp daemon start`, hydra spawns hydra-acp-budgeter with these env
57
+ vars set: `HYDRA_ACP_DAEMON_URL`, `HYDRA_ACP_TOKEN`, `HYDRA_ACP_WS_URL`,
58
+ `HYDRA_ACP_HOME`, `HYDRA_ACP_TRANSFORMER_NAME`. Stdout/stderr land in
59
+ `~/.hydra-acp/transformers/hydra-acp-budgeter.log`. Lifecycle is managed with
60
+ `hydra-acp transformers start|stop|restart hydra-acp-budgeter` and
61
+ `hydra-acp transformers log hydra-acp-budgeter -f` to tail.
62
+
63
+ ## Behavior
64
+
65
+ Tracks each session's running cost from `usage_update` events (the `cost.amount` the agent reports, or `_meta.hydra-acp.cumulativeCost` when present), sums across sessions, and acts at two thresholds:
66
+
67
+ **1. Soft limit crossed** (`total ≥ HYDRA_ACP_BUDGETER_SOFT`)
68
+ - Emits a single warning `session/update` (`agent_message_chunk`) to every attached client on the session that triggered the cross.
69
+ - Body: `Spent $X.XX of $S.SS soft (hard: $H.HH). Heads up — prompts will be rejected at the hard limit.`
70
+ - Fires **once per upward transition**, not on every tick.
71
+
72
+ **2. Hard limit crossed** (`total ≥ HYDRA_ACP_BUDGETER_HARD`)
73
+ - Same one-shot warning, this time on the session that pushed us over.
74
+ - Body: `Spent $X.XX ≥ $H.HH hard limit. Further prompts will be rejected until the budget is reset.`
75
+
76
+ **3. Prompts while over hard limit**
77
+ - Any `session/prompt` from any session is intercepted at `request:session/prompt` and replaced with a stop response:
78
+ ```json
79
+ {
80
+ "stopReason": "refusal",
81
+ "_meta": {
82
+ "hydra-acp": {
83
+ "budgeter": {
84
+ "message": "Budget exceeded: spent $X.XX ≥ $H.HH hard limit. Reset the budget or raise HYDRA_ACP_BUDGETER_HARD to continue."
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+ - The agent never sees the prompt. The client's pending `session/prompt` resolves with the stop payload; well-behaved renderers (TUI, Zed, agent-shell) will surface the `stopReason: refusal` and the `_meta.hydra-acp.budgeter.message` body.
91
+
92
+ **4. New session opens while over hard limit**
93
+ - The budgeter fires the same warning style on `session.opened` so the user knows their next prompt will bounce, even before they send it.
94
+
95
+ ### State and reset
96
+
97
+ The per-session cost map is persisted to `~/.hydra-acp/budgeter-cost.json`, atomically rewritten on every `usage_update`. The running budgeter reads it on startup and `fs.watch`es it for external mutations — so daemon restarts preserve the running total, and a reset from elsewhere is picked up live without restarting.
98
+
99
+ Spend is sticky across `session.closed`: a closed session's cost stays in the total until you reset.
100
+
101
+ To zero the budget:
102
+
103
+ ```sh
104
+ hydra-acp-budgeter reset
105
+ ```
106
+
107
+ That deletes the state file. If the transformer is running, its watcher adopts the deletion and the in-memory total drops to zero on the next tick (≤50ms). If it isn't running, the file is just gone and the next start begins at zero.
108
+
109
+ ## Configuration
110
+
111
+ Create `~/.hydra-acp/budgeter.conf` (override path via `HYDRA_ACP_BUDGETER_CONF`):
112
+
113
+ ```
114
+ # ~/.hydra-acp/budgeter.conf
115
+ SOFT=5
116
+ HARD=10
117
+ CURRENCY=USD
118
+ DEBUG=false
119
+ ```
120
+
121
+ The file is optional — all keys have defaults and the transformer works without it. Environment variables always win over file values, so you can temporarily override a limit without editing the file.
122
+
123
+ | Key / env var | Default | Purpose |
124
+ |---|---|---|
125
+ | `SOFT` / `HYDRA_ACP_BUDGETER_SOFT` | `5` | Soft limit (warning threshold) |
126
+ | `HARD` / `HYDRA_ACP_BUDGETER_HARD` | `10` | Hard limit (rejection threshold). Must be ≥ soft. |
127
+ | `CURRENCY` / `HYDRA_ACP_BUDGETER_CURRENCY` | `USD` | ISO-3 currency code for formatted messages |
128
+ | `DEBUG` | `false` | Verbose logging |
129
+ | `HYDRA_ACP_DAEMON_URL` | `http://127.0.0.1:8765` | Daemon HTTP endpoint (injected by hydra) |
130
+ | `HYDRA_ACP_TOKEN` | *(required)* | Daemon auth token (injected by hydra) |
131
+ | `HYDRA_ACP_WS_URL` | derived | Override WS endpoint |
132
+
133
+ ## How it works
134
+
135
+ - Connects via WebSocket to `/acp`, calls `initialize`, then `transformer/initialize` declaring the intercepts:
136
+ - `response:session/update` — observe `usage_update` to track cost
137
+ - `request:session/prompt` — reject when over hard limit
138
+ - `lifecycle:session.opened` — warn brand-new sessions that are already over budget
139
+ - `lifecycle:session.closed` — fires session_closed rule event (cost stays sticky)
140
+ - For every `transformer/message` the daemon dispatches, the budgeter responds with `{ action: "continue" }` (observe-only on response side, allow on request side when under budget) or `{ action: "stop", payload: { stopReason: "refusal", _meta: ... } }` (when over hard limit).
141
+ - Warnings are emitted via `hydra-acp/emit_message` with `route: "chain"` and `method: "session/update"` so they flow back through the daemon's broadcast machinery and reach every attached client.
142
+ - All cost state is in-memory; restart the transformer to reset.
143
+
144
+ For a working example of the transformer protocol the budgeter speaks, see [`hydra-acp/cli/examples/transformer-observe.mjs`](https://github.com/smagnuso/hydra-acp/blob/main/cli/examples/transformer-observe.mjs).
@@ -0,0 +1,13 @@
1
+ // ACP wire protocol version this transformer speaks. Single source of
2
+ // truth for the initialize handshake; never a literal at the callsite.
3
+ export const ACP_PROTOCOL_VERSION = 1;
4
+ export function isRequest(m) {
5
+ return "method" in m && "id" in m;
6
+ }
7
+ export function isNotification(m) {
8
+ return "method" in m && !("id" in m);
9
+ }
10
+ export function isResponse(m) {
11
+ return !("method" in m) && "id" in m;
12
+ }
13
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/acp/protocol.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,uEAAuE;AACvE,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AA6BtC,MAAM,UAAU,SAAS,CAAC,CAAiB;IACzC,OAAO,QAAQ,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,CAAiB;IAC9C,OAAO,QAAQ,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,CAAiB;IAC1C,OAAO,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AACvC,CAAC"}
@@ -0,0 +1,188 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { readFileSync } from "node:fs";
3
+ import { WebSocket } from "ws";
4
+ import { logger } from "../util/log.js";
5
+ import { ACP_PROTOCOL_VERSION, isNotification, isRequest, isResponse, } from "./protocol.js";
6
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
7
+ const log = logger("acp");
8
+ // One WebSocket to the daemon for the entire transformer process. Unlike
9
+ // the notifier's AcpAttach (one WS per session), the daemon multiplexes
10
+ // every session's transformer/message + transformer/session_event over
11
+ // this single connection, keyed by sessionId in the params.
12
+ export class TransformerClient extends EventEmitter {
13
+ opts;
14
+ ws;
15
+ nextId = 1;
16
+ pending = new Map();
17
+ connected = false;
18
+ constructor(opts) {
19
+ super();
20
+ this.opts = opts;
21
+ }
22
+ get isConnected() {
23
+ return this.connected;
24
+ }
25
+ start() {
26
+ log.debug(`connecting ${this.opts.daemonWsUrl}`);
27
+ const subprotocols = ["acp.v1", `hydra-acp-token.${this.opts.token}`];
28
+ let ws;
29
+ try {
30
+ ws = new WebSocket(this.opts.daemonWsUrl, subprotocols);
31
+ }
32
+ catch (err) {
33
+ this.emit("error", err);
34
+ return;
35
+ }
36
+ this.ws = ws;
37
+ ws.on("open", () => {
38
+ this.connected = true;
39
+ log.info(`ws open`);
40
+ void this.handshake()
41
+ .then(() => {
42
+ this.emit("open");
43
+ })
44
+ .catch((err) => {
45
+ this.emit("error", err);
46
+ try {
47
+ this.ws?.close();
48
+ }
49
+ catch {
50
+ void 0;
51
+ }
52
+ });
53
+ });
54
+ ws.on("message", (data, isBinary) => {
55
+ if (isBinary) {
56
+ return;
57
+ }
58
+ const text = data.toString("utf8");
59
+ try {
60
+ const parsed = JSON.parse(text);
61
+ this.onMessage(parsed);
62
+ }
63
+ catch (err) {
64
+ log.warn(`parse error: ${err.message}; raw=${text.slice(0, 200)}`);
65
+ }
66
+ });
67
+ ws.on("error", (err) => {
68
+ log.warn(`ws error: ${err.message}`);
69
+ this.emit("error", err);
70
+ });
71
+ ws.on("close", (code, reason) => {
72
+ const hadError = code >= 4000 || code === 1006 || code === 1011;
73
+ const reasonText = reason.toString("utf8");
74
+ this.connected = false;
75
+ log.info(`ws closed code=${code}${reasonText ? ` reason=${reasonText}` : ""}`);
76
+ for (const [, p] of this.pending) {
77
+ p.reject(new Error("ws closed"));
78
+ }
79
+ this.pending.clear();
80
+ this.emit("close", { hadError });
81
+ });
82
+ }
83
+ stop() {
84
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
85
+ try {
86
+ this.ws.close();
87
+ }
88
+ catch {
89
+ void 0;
90
+ }
91
+ }
92
+ }
93
+ async request(method, params) {
94
+ const id = this.nextId++;
95
+ const msg = {
96
+ jsonrpc: "2.0",
97
+ id,
98
+ method,
99
+ ...(params !== undefined ? { params } : {}),
100
+ };
101
+ this.write(msg);
102
+ return new Promise((resolve, reject) => {
103
+ this.pending.set(id, {
104
+ resolve: (resp) => {
105
+ if (resp.error) {
106
+ reject(new Error(`${resp.error.code}: ${resp.error.message}`));
107
+ }
108
+ else {
109
+ resolve(resp.result);
110
+ }
111
+ },
112
+ reject,
113
+ });
114
+ });
115
+ }
116
+ notify(method, params) {
117
+ const msg = {
118
+ jsonrpc: "2.0",
119
+ method,
120
+ ...(params !== undefined ? { params } : {}),
121
+ };
122
+ this.write(msg);
123
+ }
124
+ reply(id, result) {
125
+ const msg = { jsonrpc: "2.0", id, result };
126
+ this.write(msg);
127
+ }
128
+ replyError(id, code, message) {
129
+ const msg = {
130
+ jsonrpc: "2.0",
131
+ id,
132
+ error: { code, message },
133
+ };
134
+ this.write(msg);
135
+ }
136
+ async handshake() {
137
+ try {
138
+ await this.request("initialize", {
139
+ protocolVersion: ACP_PROTOCOL_VERSION,
140
+ clientCapabilities: {
141
+ fs: { readTextFile: false, writeTextFile: false },
142
+ terminal: false,
143
+ },
144
+ clientInfo: { name: "hydra-acp-budgeter", version: pkg.version },
145
+ });
146
+ }
147
+ catch (err) {
148
+ log.warn(`initialize failed: ${err.message}`);
149
+ }
150
+ try {
151
+ await this.request("transformer/initialize", {
152
+ intercepts: this.opts.intercepts,
153
+ });
154
+ log.info(`transformer/initialize ok intercepts=${this.opts.intercepts.join(",")}`);
155
+ }
156
+ catch (err) {
157
+ log.warn(`transformer/initialize failed: ${err.message}`);
158
+ throw err;
159
+ }
160
+ }
161
+ write(msg) {
162
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
163
+ log.warn(`drop write to closed ws: ${JSON.stringify(msg)}`);
164
+ return;
165
+ }
166
+ this.ws.send(JSON.stringify(msg));
167
+ }
168
+ onMessage(m) {
169
+ if (isResponse(m)) {
170
+ const p = this.pending.get(m.id);
171
+ if (p) {
172
+ this.pending.delete(m.id);
173
+ p.resolve(m);
174
+ }
175
+ else {
176
+ log.debug(`unmatched response id=${String(m.id)}`);
177
+ }
178
+ this.emit("response", m);
179
+ }
180
+ else if (isRequest(m)) {
181
+ this.emit("request", m);
182
+ }
183
+ else if (isNotification(m)) {
184
+ this.emit("notification", m);
185
+ }
186
+ }
187
+ }
188
+ //# sourceMappingURL=transformer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformer.js","sourceRoot":"","sources":["../../src/acp/transformer.ts"],"names":[],"mappings":"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"}
package/dist/bridge.js ADDED
@@ -0,0 +1,196 @@
1
+ import { watch } from "node:fs";
2
+ import { basename, dirname } from "node:path";
3
+ import { TransformerClient } from "./acp/transformer.js";
4
+ import { Enforcer } from "./enforce.js";
5
+ import { EventRouter } from "./router.js";
6
+ import { CostTracker } from "./tracker.js";
7
+ import { logger } from "./util/log.js";
8
+ const log = logger("bridge");
9
+ // The set of intercepts the budgeter declares to the daemon. Kept in one
10
+ // place so the rule, the router, and the README stay in agreement.
11
+ //
12
+ // response:session/update — observe usage_update for cost tracking
13
+ // request:session/prompt — reject when over hard limit
14
+ // lifecycle:session.opened — warn on new sessions while over budget
15
+ // lifecycle:session.closed — drop per-session cost state
16
+ const BUDGETER_INTERCEPTS = [
17
+ "response:session/update",
18
+ "request:session/prompt",
19
+ "lifecycle:session.opened",
20
+ "lifecycle:session.closed",
21
+ ];
22
+ // One bridge per budgeter process. Owns the WS connection, the cost
23
+ // tracker, the enforcer, and the router. The mirror of NotifierBridge,
24
+ // except scoped to the whole process rather than to a single session.
25
+ export class BudgeterBridge {
26
+ opts;
27
+ client;
28
+ tracker;
29
+ enforcer;
30
+ router;
31
+ watcher;
32
+ watchTimer;
33
+ stopped = false;
34
+ constructor(opts) {
35
+ this.opts = opts;
36
+ this.client = new TransformerClient({
37
+ daemonWsUrl: opts.daemonWsUrl,
38
+ token: opts.token,
39
+ intercepts: BUDGETER_INTERCEPTS,
40
+ });
41
+ this.tracker = new CostTracker({
42
+ softLimit: opts.softLimit,
43
+ hardLimit: opts.hardLimit,
44
+ currency: opts.currency,
45
+ statePath: opts.statePath,
46
+ });
47
+ this.enforcer = new Enforcer(this.client, log);
48
+ this.router = new EventRouter(opts.rule, this.tracker, this.enforcer, log);
49
+ }
50
+ start() {
51
+ this.client.on("request", (r) => this.onRequest(r));
52
+ this.client.on("notification", (n) => this.onNotification(n));
53
+ this.client.on("error", (err) => {
54
+ log.warn(`client error: ${err.message}`);
55
+ });
56
+ this.client.start();
57
+ this.startWatcher();
58
+ }
59
+ stop() {
60
+ if (this.stopped) {
61
+ return;
62
+ }
63
+ this.stopped = true;
64
+ if (this.watcher) {
65
+ this.watcher.close();
66
+ this.watcher = undefined;
67
+ }
68
+ if (this.watchTimer) {
69
+ clearTimeout(this.watchTimer);
70
+ this.watchTimer = undefined;
71
+ }
72
+ this.client.stop();
73
+ }
74
+ // Watch the state file's parent directory so we still see events when
75
+ // the file is created later (e.g. first usage_update of a fresh run)
76
+ // or deleted entirely (the reset subcommand). fs.watch on a missing
77
+ // file throws on some platforms, but the parent dir is created by
78
+ // CostTracker.persist before any write happens, and the daemon writes
79
+ // the .pid file there even sooner — so the dir reliably exists by
80
+ // the time start() runs. We still try/catch in case it doesn't.
81
+ startWatcher() {
82
+ if (!this.opts.statePath) {
83
+ return;
84
+ }
85
+ const dir = dirname(this.opts.statePath);
86
+ const file = basename(this.opts.statePath);
87
+ try {
88
+ this.watcher = watch(dir, (eventType, filename) => {
89
+ if (filename && filename !== file) {
90
+ return;
91
+ }
92
+ // fs.watch can fire 1–N times per logical change (especially
93
+ // when our own atomic-rename hits it). Debounce briefly so we
94
+ // do at most one re-read per burst.
95
+ if (this.watchTimer) {
96
+ return;
97
+ }
98
+ this.watchTimer = setTimeout(() => {
99
+ this.watchTimer = undefined;
100
+ try {
101
+ this.tracker.adoptFromDisk();
102
+ }
103
+ catch (err) {
104
+ log.warn(`adoptFromDisk failed: ${err.message}`);
105
+ }
106
+ }, 50);
107
+ });
108
+ log.debug(`watching ${this.opts.statePath}`);
109
+ }
110
+ catch (err) {
111
+ log.warn(`fs.watch failed for ${dir}: ${err.message}`);
112
+ }
113
+ }
114
+ onRequest(r) {
115
+ if (r.method !== "transformer/message") {
116
+ // The daemon only sends transformer/message requests to us. Anything
117
+ // else is an error on the daemon side or a future protocol kind we
118
+ // don't yet understand. Continue rather than guessing.
119
+ this.client.reply(r.id, { action: "continue" });
120
+ return;
121
+ }
122
+ const params = (r.params ?? {});
123
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : "";
124
+ const phase = params.phase;
125
+ const method = params.method;
126
+ if (!sessionId || !phase || !method) {
127
+ this.client.reply(r.id, { action: "continue" });
128
+ return;
129
+ }
130
+ if (phase === "response" && method === "session/update") {
131
+ // Response side is observe-only — we always continue and run the
132
+ // tracker update asynchronously. The daemon proceeds with the
133
+ // original envelope; any warning we want to emit goes out via a
134
+ // separate emit_message call.
135
+ this.client.reply(r.id, { action: "continue" });
136
+ const envelope = params.envelope;
137
+ const update = envelope && typeof envelope === "object" && !Array.isArray(envelope)
138
+ ? envelope.update
139
+ : undefined;
140
+ if (update) {
141
+ void this.router
142
+ .onResponseUpdate(sessionId, update)
143
+ .catch((err) => log.warn(`response update error: ${err.message}`));
144
+ }
145
+ return;
146
+ }
147
+ if (phase === "request" && method === "session/prompt") {
148
+ // Request side: ask the rule. If it returns a reject payload, stop;
149
+ // otherwise let the prompt continue. The reply waits on the rule —
150
+ // the daemon's forwardRequest is awaiting our response, so a slow
151
+ // rule pauses the prompt, which is acceptable for the rare reject
152
+ // path.
153
+ void this.router
154
+ .onPromptRequest(sessionId, params.envelope)
155
+ .then((rejectPayload) => {
156
+ if (rejectPayload) {
157
+ this.client.reply(r.id, { action: "stop", payload: rejectPayload });
158
+ }
159
+ else {
160
+ this.client.reply(r.id, { action: "continue" });
161
+ }
162
+ })
163
+ .catch((err) => {
164
+ log.warn(`prompt request error: ${err.message}`);
165
+ this.client.reply(r.id, { action: "continue" });
166
+ });
167
+ return;
168
+ }
169
+ // Unknown phase/method — declare nothing and let the daemon proceed.
170
+ this.client.reply(r.id, { action: "continue" });
171
+ }
172
+ onNotification(n) {
173
+ if (n.method !== "transformer/session_event") {
174
+ return;
175
+ }
176
+ const params = (n.params ?? {});
177
+ const event = typeof params.event === "string" ? params.event : "";
178
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : "";
179
+ if (!event || !sessionId) {
180
+ return;
181
+ }
182
+ if (event === "session.opened") {
183
+ void this.router
184
+ .onLifecycle("session.opened", sessionId)
185
+ .catch((err) => log.warn(`session.opened error: ${err.message}`));
186
+ return;
187
+ }
188
+ if (event === "session.closed") {
189
+ void this.router
190
+ .onLifecycle("session.closed", sessionId)
191
+ .catch((err) => log.warn(`session.closed error: ${err.message}`));
192
+ return;
193
+ }
194
+ }
195
+ }
196
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAkB,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAOzD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;AAgB7B,yEAAyE;AACzE,mEAAmE;AACnE,EAAE;AACF,wEAAwE;AACxE,6DAA6D;AAC7D,wEAAwE;AACxE,6DAA6D;AAC7D,MAAM,mBAAmB,GAAG;IAC1B,yBAAyB;IACzB,wBAAwB;IACxB,0BAA0B;IAC1B,0BAA0B;CAC3B,CAAC;AAEF,oEAAoE;AACpE,uEAAuE;AACvE,sEAAsE;AACtE,MAAM,OAAO,cAAc;IASI;IARZ,MAAM,CAAoB;IAC1B,OAAO,CAAc;IACrB,QAAQ,CAAW;IACnB,MAAM,CAAc;IAC7B,OAAO,CAAwB;IAC/B,UAAU,CAA6B;IACvC,OAAO,GAAG,KAAK,CAAC;IAExB,YAA6B,IAAmB;QAAnB,SAAI,GAAJ,IAAI,CAAe;QAC9C,IAAI,CAAC,MAAM,GAAG,IAAI,iBAAiB,CAAC;YAClC,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU,EAAE,mBAAmB;SAChC,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,GAAG,IAAI,WAAW,CAAC;YAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM,GAAG,IAAI,WAAW,CAC3B,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,QAAQ,EACb,GAAG,CACJ,CAAC;IACJ,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YAC9B,GAAG,CAAC,IAAI,CAAC,iBAAiB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpB,IAAI,CAAC,YAAY,EAAE,CAAC;IACtB,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;QAC3B,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC9B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC9B,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACrB,CAAC;IAED,sEAAsE;IACtE,qEAAqE;IACrE,oEAAoE;IACpE,kEAAkE;IAClE,sEAAsE;IACtE,kEAAkE;IAClE,gEAAgE;IACxD,YAAY;QAClB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACzC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,CAAC;YACH,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE;gBAChD,IAAI,QAAQ,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;oBAClC,OAAO;gBACT,CAAC;gBACD,6DAA6D;gBAC7D,8DAA8D;gBAC9D,oCAAoC;gBACpC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;oBACpB,OAAO;gBACT,CAAC;gBACD,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE;oBAChC,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;oBAC5B,IAAI,CAAC;wBACH,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;oBAC/B,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,GAAG,CAAC,IAAI,CAAC,yBAA0B,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;oBAC9D,CAAC;gBACH,CAAC,EAAE,EAAE,CAAC,CAAC;YACT,CAAC,CAAC,CAAC;YACH,GAAG,CAAC,KAAK,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAC/C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,IAAI,CAAC,uBAAuB,GAAG,KAAM,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IAEO,SAAS,CAAC,CAAiB;QACjC,IAAI,CAAC,CAAC,MAAM,KAAK,qBAAqB,EAAE,CAAC;YACvC,qEAAqE;YACrE,mEAAmE;YACnE,uDAAuD;YACvD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;YAChD,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAsC,CAAC;QACrE,MAAM,SAAS,GAAG,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,SAAS,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;YACpC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;YAChD,OAAO;QACT,CAAC;QAED,IAAI,KAAK,KAAK,UAAU,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;YACxD,iEAAiE;YACjE,8DAA8D;YAC9D,gEAAgE;YAChE,8BAA8B;YAC9B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;YACjC,MAAM,MAAM,GACV,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;gBAClE,CAAC,CAAG,QAAiC,CAAC,MAEtB;gBAChB,CAAC,CAAC,SAAS,CAAC;YAChB,IAAI,MAAM,EAAE,CAAC;gBACX,KAAK,IAAI,CAAC,MAAM;qBACb,gBAAgB,CAAC,SAAS,EAAE,MAAM,CAAC;qBACnC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,0BAA2B,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAClF,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;YACvD,oEAAoE;YACpE,mEAAmE;YACnE,kEAAkE;YAClE,kEAAkE;YAClE,QAAQ;YACR,KAAK,IAAI,CAAC,MAAM;iBACb,eAAe,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC;iBAC3C,IAAI,CAAC,CAAC,aAAa,EAAE,EAAE;gBACtB,IAAI,aAAa,EAAE,CAAC;oBAClB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;gBACtE,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;gBAClD,CAAC;YACH,CAAC,CAAC;iBACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACb,GAAG,CAAC,IAAI,CAAC,yBAA0B,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC5D,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;YAClD,CAAC,CAAC,CAAC;YACL,OAAO;QACT,CAAC;QAED,qEAAqE;QACrE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAClD,CAAC;IAEO,cAAc,CAAC,CAAsB;QAC3C,IAAI,CAAC,CAAC,MAAM,KAAK,2BAA2B,EAAE,CAAC;YAC7C,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAqC,CAAC;QACpE,MAAM,KAAK,GAAG,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,MAAM,SAAS,GAAG,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,KAAK,KAAK,gBAAgB,EAAE,CAAC;YAC/B,KAAK,IAAI,CAAC,MAAM;iBACb,WAAW,CAAC,gBAAgB,EAAE,SAAS,CAAC;iBACxC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,yBAA0B,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC/E,OAAO;QACT,CAAC;QACD,IAAI,KAAK,KAAK,gBAAgB,EAAE,CAAC;YAC/B,KAAK,IAAI,CAAC,MAAM;iBACb,WAAW,CAAC,gBAAgB,EAAE,SAAS,CAAC;iBACxC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,yBAA0B,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC/E,OAAO;QACT,CAAC;IACH,CAAC;CACF"}