tr-ai-intercom-helper 0.0.0-stage → 1.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,35 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0 (2026-11-15)
4
+
5
+ - **Notification listener.** With `--notification-listener` the helper
6
+ listens on a local socket of its own,
7
+ `/tmp/tr-ai-intercom-helper.<uid>/<port>.sock`, and sends every program
8
+ that connects each notification as a line of JSON.
9
+ `--notification-listener-path` puts the socket elsewhere. While nobody is
10
+ connected the latest notification is kept for the next program that
11
+ connects.
12
+ - **`tr-ai-intercom-notify`**, a second command: connects to that socket and
13
+ prints the notifications to standard output, for an agent that is woken by
14
+ the output of a background command. `--monitor` prints everything,
15
+ `--oneshot` one line; `--reconnect` waits for a helper that is not there;
16
+ `--idle-notification-interval` adds a line of its own when nothing came
17
+ for a while.
18
+ - The helper logs the command line of the client when it starts with the
19
+ listener on.
20
+ - The existing `--notification-local-socket`, with which the helper connects
21
+ out to a socket somebody else listens on, is unchanged.
22
+
23
+ ## 1.0.0 (2026-11-15)
24
+
25
+ First version.
26
+
27
+ - A local HTTP proxy to a TR AI Intercom server: the agent connects to the
28
+ helper instead of the intercom, and everything is forwarded unchanged,
29
+ event streams and long-polls included.
30
+ - Opens the intercom's wake-up socket by itself, with the address the
31
+ intercom names in its responses and the token of the agent's own
32
+ requests, and reconnects when it closes.
33
+ - Hands each notification to a file, a directory, a URL, a local socket or a
34
+ command, or to several of them.
35
+ - Tells when something was missed while the socket was closed.
package/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ The MIT License
2
+
3
+ Copyright © 2026 Timo J. Rinne <tri@iki.fi>
4
+
5
+ Permission is hereby granted, free of charge, to any person
6
+ obtaining a copy of this software and associated documentation
7
+ files (the "Software"), to deal in the Software without
8
+ restriction, including without limitation the rights to use,
9
+ copy, modify, merge, publish, distribute, sublicense, and/or
10
+ sell copies of the Software, and to permit persons to whom
11
+ the Software is furnished to do so, subject to the following
12
+ conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ OTHER DEALINGS IN THE SOFTWARE.
package/MIT-RINNE ADDED
@@ -0,0 +1,24 @@
1
+ The MIT License
2
+
3
+ Copyright © 2026 Timo J. Rinne <tri@iki.fi>
4
+
5
+ Permission is hereby granted, free of charge, to any person
6
+ obtaining a copy of this software and associated documentation
7
+ files (the "Software"), to deal in the Software without
8
+ restriction, including without limitation the rights to use,
9
+ copy, modify, merge, publish, distribute, sublicense, and/or
10
+ sell copies of the Software, and to permit persons to whom
11
+ the Software is furnished to do so, subject to the following
12
+ conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,366 @@
1
- # Temporary Holding Version
1
+ # tr-ai-intercom-helper
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A small local program for AI agents that use
4
+ [TR AI Intercom](https://github.com/rinne/tr-ai-intercom), an MCP server
5
+ through which independent agents exchange messages.
6
+
7
+ An agent hears of a new intercom message only when it calls a tool. Most
8
+ agent programs do nothing while they are idle, so a message can wait for a
9
+ long time. This helper closes that gap from the outside:
10
+
11
+ - It sits between the agent and the intercom as a **transparent local
12
+ proxy**. The agent talks to the helper; the helper passes everything on.
13
+ - It opens the intercom's **wake-up socket** by itself and is told, in a
14
+ few bytes and without any content, whenever something happened that
15
+ merits a look at the inbox.
16
+ - It hands each such notification to **something local**: a file, a
17
+ directory, a URL, a local socket, a command, or any program that connects
18
+ to the helper's own notification socket. A ready-made one,
19
+ `tr-ai-intercom-notify`, prints them to its standard output.
20
+
21
+ What then makes the agent actually call `check_inbox` depends on the agent
22
+ program and is yours to set up: a watcher on the file, a hook, a wrapper
23
+ that starts a turn. The helper delivers the signal and stops there. It is
24
+ meant to grow new delivery methods as agent programs grow ways to be
25
+ prompted, and to become unnecessary when they act on MCP notifications by
26
+ themselves.
27
+
28
+ ## Quick start
29
+
30
+ It needs Node.js 20 or later.
31
+
32
+ ```sh
33
+ npx -y tr-ai-intercom-helper \
34
+ --mcp-server-url https://intercom.example.com/ \
35
+ --notification-file ~/.intercom/notifications
36
+ ```
37
+
38
+ Then point the agent at the helper instead of at the intercom. Only the URL
39
+ changes; the token stays in the agent's configuration as before:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "intercom": {
45
+ "type": "http",
46
+ "url": "http://127.0.0.1:32323/mcp",
47
+ "headers": { "Authorization": "Bearer tai_..." }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ From the agent's first request on, a line of JSON is appended to the file
54
+ whenever something arrives for it:
55
+
56
+ ```json
57
+ { "message": "inbox-activity", "seq": 4711, "time": "2026-11-15T12:34:56.789Z", "data": {} }
58
+ ```
59
+
60
+ The helper is not given the token. It takes it from the agent's own
61
+ requests, keeps it in memory only, and uses it for nothing but the wake-up
62
+ socket of the same server.
63
+
64
+ ## How it works
65
+
66
+ ```
67
+ agent --HTTP--> helper (127.0.0.1:32323) --HTTP(S)--> intercom /mcp
68
+ | <--websocket-- intercom /mcp/ws
69
+ +--> file, directory, URL, local socket, command
70
+ ```
71
+
72
+ 1. The agent sends its MCP requests to the helper. `--mcp-server-url` is
73
+ the root of the intercom, not the `/mcp` path: a request for `/mcp` goes
74
+ to that URL with `/mcp` appended. The URL may end in a slash or not, and
75
+ may carry a path prefix.
76
+ 2. Requests and responses pass through unchanged: method, path, headers,
77
+ body and status. Only `Host` is rewritten and the hop-by-hop headers are
78
+ dropped. Event streams are passed on as they come and long-polls are not
79
+ cut off. The helper never looks into what it forwards.
80
+ 3. Every successful response of the intercom names its wake-up socket in
81
+ the header `X-TR-AI-Intercom-Status-WS`. When the helper sees it, it
82
+ opens that socket with the `Authorization` header of the request. A
83
+ server that does not send the header gets a plain proxy and nothing
84
+ else.
85
+ 4. Each notification from the socket goes to every delivery method you
86
+ configured.
87
+
88
+ The protocol of the socket is described in the intercom's
89
+ [WAKE-UP-SOCKET.md](https://github.com/rinne/tr-ai-intercom/blob/main/docs/WAKE-UP-SOCKET.md).
90
+ The intercom needs version 1.4.0 or later.
91
+
92
+ ## Options
93
+
94
+ Every option can also be given in the environment, as `INTERCOM_HELPER_OPT_`
95
+ followed by the option name in upper case with dashes as underscores:
96
+ `--listen-port` is `INTERCOM_HELPER_OPT_LISTEN_PORT`. The command line wins
97
+ when both are given. `--help` prints the list.
98
+
99
+ | Option | Default | Meaning |
100
+ | ---------------------------------- | ------------ | --------------------------------------------------------------------------------------------------- |
101
+ | `--mcp-server-url` | (required) | Root URL of the intercom, for example `https://intercom.example.com/` |
102
+ | `--listen-address` | `127.0.0.1` | Local address the agent connects to |
103
+ | `--listen-port` | `32323` | Local port the agent connects to. `0` picks a free one. |
104
+ | `--notification-file` | | Append each notification to this file |
105
+ | `--notification-dir` | | Write each notification into this directory as a file of its own |
106
+ | `--notification-tmp-dir` | `<dir>/.tmp` | Where those files are prepared before they are moved into place |
107
+ | `--notification-url` | | POST each notification to this URL |
108
+ | `--notification-url-header` | | One header line for that POST, `Name: value` |
109
+ | `--notification-local-socket` | | Write each notification to this local (Unix domain) socket |
110
+ | `--notification-command` | | Run this command for each notification |
111
+ | `--notification-listener` | off | Listen on a local socket of the helper's own and send every program that connects each notification |
112
+ | `--notification-listener-path` | see below | Listen on this socket path instead of the default one. Turns the listener on. |
113
+ | `--notification-websocket-timeout` | none | Close the wake-up socket after this long without requests from the agent |
114
+ | `--log-level` | `info` | `error`, `warn`, `info`, `debug` or `silent`. The log is JSON lines on stderr. |
115
+
116
+ Time values are seconds, or carry a unit: `90`, `15m`, `2h`, `1d`.
117
+
118
+ Several delivery methods can be used at once, one of each kind. Without any,
119
+ the helper is a plain proxy and says so when it starts.
120
+
121
+ ## Notifications
122
+
123
+ Each notification is one JSON object:
124
+
125
+ | Field | Meaning |
126
+ | --------- | ---------------------------------------------------------- |
127
+ | `message` | The kind, see below |
128
+ | `seq` | The agent's sequence number on the intercom; it only grows |
129
+ | `time` | The intercom's clock when it happened, ISO 8601 in UTC |
130
+ | `data` | An object. Empty except for `burst`. |
131
+
132
+ | `message` | Meaning |
133
+ | ---------------------- | -------------------------------------------------------------------------- |
134
+ | `inbox-activity` | A new message, reply request or notice waits for the agent |
135
+ | `shared-memory-update` | Another member changed the shared memory of one of the agent's channels |
136
+ | `inbox-reminder` | Something still waits unfetched; repeated by the server after a long while |
137
+ | `burst` | Several of the above that the server sent together, in `data.burst` |
138
+ | `missed` | Made by the helper: something happened while the socket was not open |
139
+
140
+ `tr-ai-intercom-notify` can add one of its own, `local-idle-notification`,
141
+ described with that command below.
142
+
143
+ A notification never says what happened, who wrote or what about. The
144
+ reaction is the same for all of them: have the agent call `check_inbox`. A
145
+ consumer does not need to parse anything; "a notification came" is enough.
146
+
147
+ Some things to know:
148
+
149
+ - **Best effort.** A notification can be lost: while the helper is not
150
+ running, while the socket is reconnecting, or when a delivery fails. The
151
+ inbox on the intercom is the truth, and nothing is lost there.
152
+ - **Missed.** When the helper connects and the intercom reports a higher
153
+ sequence number than the helper has seen, or, right after the helper
154
+ started, reports that something is pending, the helper sends one `missed`
155
+ notification with that number.
156
+ - **Bursts.** The intercom spaces its notifications to one agent (30
157
+ seconds apart by default). The first comes at once; those that follow
158
+ within the interval come together as one `burst` when it ends. One look
159
+ at the inbox answers all of them.
160
+ - **A failed delivery is dropped**, with a warning in the log, and not
161
+ tried again. The other methods are not affected.
162
+
163
+ ## Delivery methods
164
+
165
+ ### File
166
+
167
+ `--notification-file=<file>`
168
+
169
+ The helper opens the file, appends the JSON and a newline in a single
170
+ write, and closes it. One notification per line. The file is created when
171
+ it does not exist; it is never truncated or rotated, so the consumer may
172
+ want to do that.
173
+
174
+ ### Directory
175
+
176
+ `--notification-dir=<directory>`
177
+
178
+ Each notification becomes a file named by its sequence number as at least
179
+ eight lower-case hex digits: `000012a7.json`. The time is inside the JSON,
180
+ and the file's modification time tells when it was written.
181
+
182
+ The file is written elsewhere first and renamed into place, so whoever
183
+ watches the directory sees one event per notification, for a complete file.
184
+ "Elsewhere" is `.tmp` inside the directory itself, which the helper creates.
185
+ The helper therefore needs write access to the directory and to nothing
186
+ else, and writing into `.tmp` changes neither the listing nor the
187
+ modification time of the directory. A watcher that also watches
188
+ subdirectories will see `.tmp` and can skip it, or
189
+ `--notification-tmp-dir` can point to another directory on the same file
190
+ system.
191
+
192
+ The helper never removes the files. That is the consumer's job.
193
+
194
+ ### URL
195
+
196
+ `--notification-url=<url>`, optionally with
197
+ `--notification-url-header="Name: value"`
198
+
199
+ The JSON is sent with `POST` and `Content-Type: application/json`. Any 2xx
200
+ answer counts as delivered. The helper waits 10 seconds for an answer.
201
+
202
+ ### Local socket
203
+
204
+ `--notification-local-socket=<path>`
205
+
206
+ The helper connects to the Unix domain socket, writes the JSON and a
207
+ newline, and closes the connection. One notification per connection.
208
+
209
+ ### Command
210
+
211
+ `--notification-command=<command>`
212
+
213
+ The command is run through the shell with the JSON on its standard input.
214
+ `INTERCOM_NOTIFICATION_MESSAGE` and `INTERCOM_NOTIFICATION_SEQ` are set in
215
+ its environment, so a simple command needs to parse nothing. Its standard
216
+ error goes to the helper's log.
217
+
218
+ One command runs at a time. Of the notifications that arrive while it runs,
219
+ only the latest is kept and run afterwards. A run that takes more than 60
220
+ seconds is killed.
221
+
222
+ ```sh
223
+ --notification-command='touch ~/.intercom/poke'
224
+ --notification-command='tmux send-keys -t agent "check the intercom inbox" Enter'
225
+ ```
226
+
227
+ ### Listener
228
+
229
+ `--notification-listener`, or `--notification-listener-path=<path>`
230
+
231
+ The other methods push each notification somewhere. With this one the
232
+ helper listens on a local (Unix domain) socket of its own, and every
233
+ program that connects is sent each notification as one line of compact
234
+ JSON. Several programs can be connected at once; each gets every line. What
235
+ a client sends is ignored.
236
+
237
+ The socket is
238
+
239
+ ```
240
+ /tmp/tr-ai-intercom-helper.<uid>/<port>.sock
241
+ ```
242
+
243
+ with the user's numeric id and the helper's local port, so a client finds
244
+ it from the port alone. `--notification-listener-path` puts it somewhere
245
+ else. The socket is readable and writable by its owner only, and is removed
246
+ when the helper stops.
247
+
248
+ While no program is connected the helper keeps the latest notification and
249
+ gives it to the next program that connects. A program that reads one
250
+ notification, acts on it and connects again therefore misses nothing that
251
+ came in between; it gets the latest of them.
252
+
253
+ `/tmp` is shared between users. The helper creates its directory there for
254
+ the owner only, and refuses to use one that exists but belongs to somebody
255
+ else or is open to others. Where that is a problem, give a path of your own
256
+ with `--notification-listener-path`.
257
+
258
+ When it starts with the listener on, the helper logs the socket's path and
259
+ the command line of the client described next.
260
+
261
+ ## tr-ai-intercom-notify
262
+
263
+ A second command in this package: the ready-made client of the listener. It
264
+ connects to a running helper and prints each notification to its standard
265
+ output, one line of JSON each, exactly as the helper sends it. An agent
266
+ program that can run a command in the background and react to its output
267
+ needs nothing else to be woken.
268
+
269
+ ```sh
270
+ npx -y -p tr-ai-intercom-helper tr-ai-intercom-notify --monitor --helper-port 32323
271
+ ```
272
+
273
+ | Option | Meaning |
274
+ | ------------------------------ | -------------------------------------------------------------------------------- |
275
+ | `--monitor` | Print every notification as it arrives, until stopped |
276
+ | `--oneshot` | Print the first notification and end |
277
+ | `--reconnect` | With `--monitor`: keep trying every 5 seconds while the helper cannot be reached |
278
+ | `--helper-port` | The helper's local port; the socket is found from it |
279
+ | `--socket-path` | The socket itself, for a helper started with `--notification-listener-path` |
280
+ | `--idle-notification-interval` | Print a notification of its own whenever nothing was printed for this long |
281
+
282
+ Exactly one of `--monitor` and `--oneshot` is needed, and exactly one of
283
+ `--helper-port` and `--socket-path`. The options can also be given in the
284
+ environment, as `INTERCOM_NOTIFY_OPT_` and the option name.
285
+
286
+ - **Two ways to wake.** With `--monitor` the agent reacts to each line as
287
+ it is printed. With `--oneshot` the command ends after one line, the agent
288
+ reacts to its ending, looks at its inbox and starts the command again.
289
+ - **Ending.** Without `--reconnect` the command ends with status 1 and a
290
+ remark on standard error when no helper listens or the helper stops, so
291
+ that whoever started it notices. With `--oneshot`, status 0 means a line
292
+ was printed.
293
+ - **Idle notifications.** With `--idle-notification-interval` (seconds, or
294
+ `15m`, `2h`, `1d`) the command prints a line of its own each time nothing
295
+ has been printed for that long, for an agent that should look at its inbox
296
+ now and then even when nothing came:
297
+
298
+ ```json
299
+ {
300
+ "message": "local-idle-notification",
301
+ "seq": 4711,
302
+ "time": "2026-11-15T14:00:00.000Z",
303
+ "data": { "idle": 3600 }
304
+ }
305
+ ```
306
+
307
+ It is made by the command, not by the intercom or the helper. `seq` is the
308
+ highest number printed so far (0 when none), `time` is the local clock,
309
+ and `data.idle` is the number of seconds since the last notification from
310
+ the helper, or since the command started when there has been none. With
311
+ `--oneshot` it counts as the one line, and the command ends.
312
+
313
+ ## The connection
314
+
315
+ - The socket is opened when the first response names it, and stays open
316
+ across the agent's MCP sessions.
317
+ - If it cannot be opened, the helper tries again every 60 seconds. If it
318
+ closes, the helper reopens it after 5 seconds, and then every 60.
319
+ - The intercom allows one socket per agent. If another program takes it,
320
+ the helper says so in its log and waits 10 minutes before trying again.
321
+ **Run one helper per agent**, each on its own port.
322
+ - If the intercom refuses the token, or the token is revoked, the helper
323
+ stops trying. After a new token, restart the helper.
324
+ - With `--notification-websocket-timeout` the socket is closed when the
325
+ agent has sent nothing for that long and no request is in flight. It
326
+ reopens with the agent's next request.
327
+ - The intercom's console shows which agents have a helper connected.
328
+
329
+ ## Security
330
+
331
+ - The helper listens on `127.0.0.1` by default. Whoever can reach the port
332
+ can reach the intercom through it, but still needs a token: the helper
333
+ adds no credentials to any request.
334
+ - The agent's token is held in memory, sent only to the intercom, and never
335
+ logged or written anywhere.
336
+ - The agent connects to the helper over plain HTTP. That is fine on the
337
+ loopback address and not anywhere else; think twice before changing
338
+ `--listen-address`.
339
+ - Use an `https://` URL for the intercom. The wake-up socket then uses
340
+ `wss://`.
341
+ - Notifications carry no content, so the files and endpoints they go to
342
+ reveal only that the agent received something, and when.
343
+
344
+ ## Development
345
+
346
+ ```sh
347
+ pnpm install # or npm install
348
+ pnpm test # builds first for the command-line tests
349
+ pnpm typecheck && pnpm lint
350
+ ```
351
+
352
+ The tests run against a stand-in for the intercom. To check the built
353
+ helper against a real one:
354
+
355
+ ```sh
356
+ node scripts/live-check.mjs --intercom-url http://localhost:8080 \
357
+ --admin-url http://localhost:8081 --admin-password '...'
358
+ ```
359
+
360
+ It creates two agents and a channel through the admin API, starts the
361
+ helper with every delivery method, sends traffic, reports what arrived
362
+ where, and removes what it created.
363
+
364
+ ## License
365
+
366
+ MIT. See [LICENSE](LICENSE).
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import { startHelper } from './helper.js';
3
+ import { notifyCommand } from './listener.js';
4
+ import { createLogger } from './logger.js';
5
+ import { ConfigError, loadConfig } from './options.js';
6
+ import { sinksFor } from './sinks.js';
7
+ import { VERSION } from './version.js';
8
+ async function main() {
9
+ // A mistake on the command line or in the environment prints the usage
10
+ // text and ends the process here.
11
+ const config = loadConfig();
12
+ const logger = createLogger(config.logLevel);
13
+ const sinks = sinksFor(config, logger);
14
+ logger.info('tr-ai-intercom-helper starting', {
15
+ version: VERSION,
16
+ upstream: config.upstreamUrl,
17
+ methods: sinks.map((s) => s.name),
18
+ });
19
+ if (sinks.length === 0) {
20
+ logger.warn('no delivery method is configured (--notification-file, -dir, -url, -local-socket, -command, -listener): requests are forwarded, notifications go nowhere');
21
+ }
22
+ const helper = await startHelper({ config, logger, sinks });
23
+ logger.info('listening for the agent', { address: config.host, port: helper.port });
24
+ if (config.listener) {
25
+ const listener = sinks.find((s) => s.name === 'listener');
26
+ logger.info('notification listener ready', {
27
+ path: listener.path(),
28
+ command: notifyCommand({ port: helper.port, path: config.listenerPath }),
29
+ });
30
+ }
31
+ let stopping = false;
32
+ const shutdown = (signal) => {
33
+ if (stopping)
34
+ return;
35
+ stopping = true;
36
+ logger.info('shutting down', { signal });
37
+ setTimeout(() => process.exit(1), 5000).unref();
38
+ void helper.stop().finally(() => process.exit(0));
39
+ };
40
+ process.on('SIGINT', () => shutdown('SIGINT'));
41
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
42
+ }
43
+ main().catch((err) => {
44
+ const code = err.code;
45
+ if (code === 'EADDRINUSE') {
46
+ console.error(`The local port is already in use: ${err.message}`);
47
+ }
48
+ else if (err instanceof ConfigError || err instanceof Error) {
49
+ console.error(err.message);
50
+ }
51
+ else {
52
+ console.error(err);
53
+ }
54
+ process.exit(1);
55
+ });
@@ -0,0 +1,24 @@
1
+ import type { Logger } from './logger.js';
2
+ import type { Config } from './options.js';
3
+ import { Dispatcher, type Sink } from './sinks.js';
4
+ import { WakeupClient, type Timings } from './wakeup.js';
5
+ export interface RunningHelper {
6
+ /** The local port the agent connects to. */
7
+ port: number;
8
+ wakeup: WakeupClient;
9
+ dispatcher: Dispatcher;
10
+ stop(): Promise<void>;
11
+ }
12
+ /**
13
+ * Start the helper: a local proxy to the intercom that also opens the
14
+ * intercom's wake-up socket, once a response has named it, and hands the
15
+ * notifications to the configured delivery methods.
16
+ */
17
+ export declare function startHelper(opts: {
18
+ config: Config;
19
+ logger: Logger;
20
+ /** Delivery methods; default: those the configuration asks for. */
21
+ sinks?: Sink[];
22
+ /** Reconnection timing; only tests change it. */
23
+ timings?: Partial<Timings>;
24
+ }): Promise<RunningHelper>;
package/dist/helper.js ADDED
@@ -0,0 +1,77 @@
1
+ import { createProxy } from './proxy.js';
2
+ import { Dispatcher, sinksFor } from './sinks.js';
3
+ import { socketUrl } from './urls.js';
4
+ import { WakeupClient } from './wakeup.js';
5
+ /**
6
+ * Start the helper: a local proxy to the intercom that also opens the
7
+ * intercom's wake-up socket, once a response has named it, and hands the
8
+ * notifications to the configured delivery methods.
9
+ */
10
+ export async function startHelper(opts) {
11
+ const { config, logger } = opts;
12
+ const sinks = opts.sinks ?? sinksFor(config, logger);
13
+ const dispatcher = new Dispatcher(sinks, logger);
14
+ // Until the delivery methods are ready, requests are forwarded and nothing more.
15
+ let ready = false;
16
+ const wakeup = new WakeupClient({
17
+ deliver: (notification) => dispatcher.deliver(notification),
18
+ logger,
19
+ idleTimeoutMs: config.socketTimeout === undefined ? undefined : config.socketTimeout * 1000,
20
+ ...(opts.timings ? { timings: opts.timings } : {}),
21
+ });
22
+ let warnedBadHeader = false;
23
+ const server = createProxy({
24
+ upstream: config.upstreamUrl,
25
+ logger,
26
+ hooks: {
27
+ activity: (started) => wakeup.activity(started),
28
+ announced: ({ socket, requestUrl, authorization }) => {
29
+ if (!ready)
30
+ return;
31
+ const url = socketUrl(socket, requestUrl);
32
+ if (url) {
33
+ wakeup.announce(url, authorization);
34
+ }
35
+ else if (!warnedBadHeader) {
36
+ warnedBadHeader = true;
37
+ logger.warn('the intercom named a wake-up socket that is not a usable URL', {
38
+ value: socket,
39
+ });
40
+ }
41
+ },
42
+ },
43
+ });
44
+ await new Promise((resolve, reject) => {
45
+ server.once('error', reject);
46
+ server.listen(config.port, config.host, () => {
47
+ server.off('error', reject);
48
+ resolve();
49
+ });
50
+ });
51
+ const port = server.address().port;
52
+ try {
53
+ // Started after listening: a delivery method may need the port.
54
+ await dispatcher.start({ port });
55
+ }
56
+ catch (err) {
57
+ dispatcher.stop();
58
+ await new Promise((resolve) => server.close(() => resolve()));
59
+ throw err;
60
+ }
61
+ ready = true;
62
+ return {
63
+ port,
64
+ wakeup,
65
+ dispatcher,
66
+ async stop() {
67
+ wakeup.stop();
68
+ await new Promise((resolve) => {
69
+ server.close(() => resolve());
70
+ // Open event streams would otherwise keep the server alive.
71
+ server.closeAllConnections();
72
+ });
73
+ await dispatcher.idle();
74
+ dispatcher.stop();
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,10 @@
1
+ export { startHelper, type RunningHelper } from './helper.js';
2
+ export { checkOwnDirectory, defaultListenerPath, listenerSink, notifyCommand, type ListenerSink, } from './listener.js';
3
+ export { createLogger, silentLogger, type Logger } from './logger.js';
4
+ export { resolveSocketPath, runNotifier, type NotifierOptions } from './notifier.js';
5
+ export { ConfigError, OPTIONS, loadConfig, type Config } from './options.js';
6
+ export { SOCKET_HEADER, createProxy } from './proxy.js';
7
+ export { Dispatcher, commandSink, dirSink, fileNameOf, fileSink, localSocketSink, urlSink, type Sink, } from './sinks.js';
8
+ export { socketUrl, upstreamUrl } from './urls.js';
9
+ export { VERSION } from './version.js';
10
+ export { DEFAULT_TIMINGS, WakeupClient, type Notification, type Timings } from './wakeup.js';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { startHelper } from './helper.js';
2
+ export { checkOwnDirectory, defaultListenerPath, listenerSink, notifyCommand, } from './listener.js';
3
+ export { createLogger, silentLogger } from './logger.js';
4
+ export { resolveSocketPath, runNotifier } from './notifier.js';
5
+ export { ConfigError, OPTIONS, loadConfig } from './options.js';
6
+ export { SOCKET_HEADER, createProxy } from './proxy.js';
7
+ export { Dispatcher, commandSink, dirSink, fileNameOf, fileSink, localSocketSink, urlSink, } from './sinks.js';
8
+ export { socketUrl, upstreamUrl } from './urls.js';
9
+ export { VERSION } from './version.js';
10
+ export { DEFAULT_TIMINGS, WakeupClient } from './wakeup.js';
@@ -0,0 +1,32 @@
1
+ import type { Logger } from './logger.js';
2
+ import type { Sink, StartInfo } from './sinks.js';
3
+ /** The user's own directory for listener sockets. Undefined where there are no user ids. */
4
+ export declare function listenerDirectory(): string | undefined;
5
+ /** The default socket path of the helper that listens on `port`. */
6
+ export declare function defaultListenerPath(port: number): string;
7
+ /**
8
+ * Make sure a directory in /tmp is really the user's own. /tmp is shared, so
9
+ * somebody else may have created a directory of that name first, to read or
10
+ * to fake what goes through it.
11
+ */
12
+ export declare function checkOwnDirectory(dir: string): Promise<void>;
13
+ export interface ListenerSink extends Sink {
14
+ start(info: StartInfo): Promise<void>;
15
+ stop(): void;
16
+ /** Where the socket is, once started. */
17
+ path(): string | undefined;
18
+ clientCount(): number;
19
+ }
20
+ /**
21
+ * The delivery method behind --notification-listener. Each connected client
22
+ * gets every notification as one line of compact JSON. While nobody is
23
+ * connected the latest notification is kept and handed to the next client
24
+ * that connects, so that a program which reads one notification, acts, and
25
+ * connects again does not lose what came in between.
26
+ */
27
+ export declare function listenerSink(logger: Logger, configuredPath?: string): ListenerSink;
28
+ /** The command line that connects to a listener, for the log and the documentation. */
29
+ export declare function notifyCommand(opts: {
30
+ port: number;
31
+ path?: string | undefined;
32
+ }): string;