kojee-mcp 0.5.4 → 0.5.7

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.
Files changed (30) hide show
  1. package/README.md +24 -5
  2. package/dist/{chunk-36L3GCU3.js → chunk-3XDJOHMZ.js} +12 -2
  3. package/dist/chunk-6SK6ITFE.js +142 -0
  4. package/dist/{chunk-62KH6VNQ.js → chunk-GATXJ6UT.js} +122 -190
  5. package/dist/{control-token-TYDAL477.js → chunk-GI2CKKBL.js} +13 -2
  6. package/dist/{resubscribe-SLZNA76S.js → chunk-OT2GILXC.js} +1 -0
  7. package/dist/{chunk-YVUXQ4Z2.js → chunk-UEGQGXPY.js} +53 -8
  8. package/dist/{chunk-OSKHA5DS.js → chunk-V5VZPYMZ.js} +2 -2
  9. package/dist/cli.js +19 -24
  10. package/dist/control-token-4BUCTYQB.js +13 -0
  11. package/dist/{doctor-TXWMMSRC.js → doctor-QCQDFLEH.js} +29 -16
  12. package/dist/{doctor-codex-3A7KYOVX.js → doctor-codex-NZ53ROQA.js} +3 -3
  13. package/dist/ensure-join-7AEDJMPE.js +96 -0
  14. package/dist/gateway-client-93P1E0CZ.d.ts +92 -0
  15. package/dist/{hook-server-NDJSV22J.js → hook-server-37E2LUKJ.js} +6 -0
  16. package/dist/index.d.ts +18 -15
  17. package/dist/index.js +7 -4
  18. package/dist/lib.d.ts +427 -0
  19. package/dist/lib.js +44 -0
  20. package/dist/parent-watchdog-RZLHYP7T.js +65 -0
  21. package/dist/reconnect-scheduler-ARV6JIWK.js +36 -0
  22. package/dist/resubscribe-G5OGDZJD.js +6 -0
  23. package/dist/{send-cli-7QJ36YY7.js → send-cli-C2F4WTBN.js} +1 -1
  24. package/dist/{stop-hook-GO363SMD.js → stop-hook-46BJD55B.js} +15 -7
  25. package/dist/{tail-stream-U436QL2X.js → tail-stream-VUZBYKXS.js} +4 -4
  26. package/dist/{user-prompt-submit-hook-ARPEO6FF.js → user-prompt-submit-hook-ZD2XKN7U.js} +7 -1
  27. package/dist/{webhook-config-UKUSI2FE.js → webhook-config-O4WMQ532.js} +1 -1
  28. package/dist/{webhook-sink-GCLL2S6S.js → webhook-sink-NWGCUDGY.js} +17 -3
  29. package/dist/{wizard-Z5JA3YPV.js → wizard-UOXQYJLP.js} +7 -7
  30. package/package.json +11 -2
package/dist/lib.d.ts ADDED
@@ -0,0 +1,427 @@
1
+ import { KeyLike, JWK } from 'jose';
2
+ import { L as LoadedKeyPair, K as KeystoreData, G as GatewayClient } from './gateway-client-93P1E0CZ.js';
3
+ export { P as ProxyConfig, T as ToolCallResult } from './gateway-client-93P1E0CZ.js';
4
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
+
6
+ /**
7
+ * Wire shape of a single SSE event from /api/v2/tandems/stream.
8
+ * Matches A2A spec §6.1 + impl plan B2 SSE event payload.
9
+ */
10
+ interface TandemEvent {
11
+ type: "message" | "state_change";
12
+ id: string;
13
+ tandem_id: string;
14
+ cursor: number;
15
+ time: string;
16
+ from: {
17
+ member_id: string;
18
+ principal: string;
19
+ agent_id?: string;
20
+ session_id?: string;
21
+ displayname: string;
22
+ };
23
+ kind: "tell" | "ask" | "ack" | "status" | "message" | "system";
24
+ content: {
25
+ body: string;
26
+ format?: string;
27
+ };
28
+ mentions?: Array<{
29
+ type: "principal";
30
+ member_id: string;
31
+ displayname_at_mention: string;
32
+ } | {
33
+ type: "class";
34
+ class: "all" | "humans" | "agents" | "businesses";
35
+ } | {
36
+ type: "unresolved";
37
+ raw: string;
38
+ }>;
39
+ reply_to?: string | null;
40
+ severity?: string;
41
+ }
42
+
43
+ /**
44
+ * Orchestrates key enrollment: checks keystore, generates keys if needed,
45
+ * runs register -> confirm flow.
46
+ */
47
+ declare class AuthModule {
48
+ private readonly token;
49
+ private readonly brokerUrl;
50
+ private readonly keystorePath;
51
+ private privateKey;
52
+ private publicJwk;
53
+ private kid;
54
+ constructor(token: string, brokerUrl: string, keystorePath: string);
55
+ /**
56
+ * Ensure we have an enrolled keypair. Either loads from disk or
57
+ * performs the full enrollment flow.
58
+ */
59
+ ensureEnrolled(): Promise<LoadedKeyPair>;
60
+ getPrivateKey(): KeyLike;
61
+ getPublicJwk(): JWK;
62
+ getKid(): string;
63
+ }
64
+
65
+ /**
66
+ * Create a DPoP proof JWT for authenticating requests to the Kojee gateway.
67
+ *
68
+ * The JWT header must contain:
69
+ * - typ: "dpop+jwt"
70
+ * - alg: "ES256"
71
+ * - jwk: { kid: <bot_key_id> } (server reads kid from the jwk sub-object)
72
+ *
73
+ * The JWT payload must contain:
74
+ * - htm: HTTP method
75
+ * - htu: request URL
76
+ * - iat: issued-at (unix seconds)
77
+ * - jti: unique identifier (UUID) for replay protection
78
+ * - nonce: server-issued DPoP nonce (when available)
79
+ * - ath: base64url(SHA-256(access_token)) — binds proof to the token
80
+ *
81
+ * @param privateKey - ES256 private key for signing
82
+ * @param kid - bot_key_id from enrollment
83
+ * @param method - HTTP method (e.g. "POST")
84
+ * @param url - Full URL of the request endpoint
85
+ * @param nonce - Server-issued DPoP nonce (optional on first request)
86
+ * @param accessToken - Gateway token for ath claim
87
+ * @returns Signed DPoP proof JWT string
88
+ */
89
+ declare function createDPoPProof(privateKey: KeyLike, kid: string, method: string, url: string, nonce?: string, accessToken?: string): Promise<string>;
90
+
91
+ /**
92
+ * Canonical keystore path for PAIRED mode (`kojee-mcp pair`):
93
+ * ~/.kojee/keypair.json.
94
+ *
95
+ * The backend keeps exactly ONE DPoP key per gateway token (register_bot_key
96
+ * REPLACES; verification is exact-kid-match) — so every client of the same
97
+ * credential on a box MUST share one keystore or they enroll over each other
98
+ * in a lockout ping-pong. These two functions ARE that contract; out-of-repo
99
+ * consumers (the OpenClaw/Hermes plugins via `kojee-mcp/lib`) reuse them
100
+ * rather than reimplementing the paths.
101
+ */
102
+ declare function defaultPairedKeystorePath(): string;
103
+ /**
104
+ * Canonical keystore path for TOKEN mode (explicit `--token` / env token):
105
+ * ~/.kojee/keypair-<sha256(token)[:12]>.json — one slot per distinct token.
106
+ */
107
+ declare function deriveKeystorePath(token: string): string;
108
+ /**
109
+ * Load an existing keypair from disk.
110
+ * Returns null if the file does not exist or the broker URL does not match.
111
+ */
112
+ declare function loadKeystore(keystorePath?: string, expectedBrokerUrl?: string): Promise<(LoadedKeyPair & {
113
+ data: KeystoreData;
114
+ }) | null>;
115
+ /**
116
+ * Save a keypair to disk with restrictive file permissions.
117
+ */
118
+ declare function saveKeystore(privateKey: KeyLike, publicJwk: JWK, kid: string, brokerUrl: string, keystorePath?: string): Promise<void>;
119
+ /**
120
+ * Generate a new ES256 key pair and export the public JWK.
121
+ */
122
+ declare function generateES256KeyPair(): Promise<{
123
+ privateKey: KeyLike;
124
+ publicJwk: JWK;
125
+ }>;
126
+
127
+ interface PairedConfig {
128
+ token: string;
129
+ broker_url: string;
130
+ paired_at: string;
131
+ principal_id?: string;
132
+ agent_id?: string;
133
+ }
134
+ declare function pairedConfigPath(): string;
135
+ declare function loadPairedConfig(filePath?: string): PairedConfig | null;
136
+
137
+ type Runtime = "claude-code" | "codex" | "unknown";
138
+
139
+ /**
140
+ * Per-harness adapter. Today: minimal — only what's needed for the
141
+ * Channels wake-path. Grows as Codex/Cursor/Openclaw adapters land.
142
+ */
143
+ interface HarnessAdapter {
144
+ readonly runtime: Runtime;
145
+ readonly supportsChannels: boolean;
146
+ /**
147
+ * Format a Tandem event into the {content, meta} pair that becomes
148
+ * a <channel source="..." key=value ...>content</channel> tag in
149
+ * the agent's context.
150
+ *
151
+ * Meta keys must match /^[a-z_][a-z0-9_]*$/ (CC docs requirement);
152
+ * values are free-form strings.
153
+ *
154
+ * Adapters with supportsChannels=false MUST throw if this is called;
155
+ * server.ts gates the call site on supportsChannels.
156
+ */
157
+ formatTandemEvent(event: TandemEvent): {
158
+ content: string;
159
+ meta: Record<string, string>;
160
+ };
161
+ }
162
+
163
+ interface QueueEntry {
164
+ event: TandemEvent;
165
+ receivedAt: number;
166
+ deliveredViaChannel: boolean;
167
+ deliveredViaMonitor: boolean;
168
+ deliveredViaHook: boolean;
169
+ }
170
+ interface EventQueueOptions {
171
+ capacity?: number;
172
+ maxAgeMs?: number;
173
+ }
174
+ /**
175
+ * In-memory queue of Tandem events with per-event delivery-tracking metadata.
176
+ *
177
+ * Dedup rule (locked in spec §0 decision 3): hook-deliverable subset is
178
+ * entries where !deliveredViaChannel && !deliveredViaHook. Channel-delivered
179
+ * events are suppressed from the hook path to avoid double-delivery.
180
+ *
181
+ * Eviction: by maxAgeMs on read of push (default 10min), by capacity FIFO
182
+ * (default 200). Bounds memory under "agent isn't paying attention" cases.
183
+ */
184
+ declare class EventQueue {
185
+ private entries;
186
+ private readonly capacity;
187
+ private readonly maxAgeMs;
188
+ constructor(opts?: EventQueueOptions);
189
+ push(event: TandemEvent): void;
190
+ markChannelDelivered(eventId: string): void;
191
+ markMonitorDelivered(eventId: string): void;
192
+ takeForHook(): QueueEntry[];
193
+ snapshot(): QueueEntry[];
194
+ }
195
+
196
+ interface EventLog {
197
+ /** The MESSAGES-ONLY log (byte-compatible with the old deployed recipe). */
198
+ path: string;
199
+ /** The SEPARATE status/telemetry sibling, next to `path`. */
200
+ statusPath: string;
201
+ append(event: TandemEvent): Promise<void>;
202
+ /**
203
+ * Append a raw STATUS line (round-2: SPLIT THE STREAMS). Status/heartbeat
204
+ * lines describe stream health, not Tandem messages — and they NEVER touch
205
+ * the messages log. They go to a SEPARATE sibling file (`statusPath`) so the
206
+ * messages log stays byte-compatible with every OLD deployed Monitor (which
207
+ * runs raw `tail -n +1 -F <messages-log>` and wakes on every line it sees).
208
+ * Status lines bypass the per-event-id dedup set (they have no event.id) and
209
+ * are NOT subject to body truncation. Callers pass the field body (e.g.
210
+ * "status=connected", "status=heartbeat", "status=subscribed n=3"); this
211
+ * method prefixes the timestamp + sentinel.
212
+ */
213
+ appendStatus(fields: string): Promise<void>;
214
+ cleanup(): void;
215
+ }
216
+
217
+ interface WebhookSinkStats {
218
+ /** Events successfully delivered (a 2xx response). */
219
+ delivered: number;
220
+ /**
221
+ * Events whose attempt TIMED OUT: the receiver may well have processed them
222
+ * (a session-spawning receiver answers late), so they are NOT retried and
223
+ * NOT counted as dropped — delivery is unconfirmed, not failed (0.5.6).
224
+ */
225
+ deliveredUnconfirmed: number;
226
+ /** Events dropped after a permanent failure or exhausted retries. */
227
+ dropped: number;
228
+ /** Events dropped because the bounded queue was full at enqueue time. */
229
+ overflowDropped: number;
230
+ /** Current depth = buffered + (in-flight ? 1 : 0). */
231
+ queueDepth: number;
232
+ /** Last delivery outcome, for doctor/status. */
233
+ lastOutcome: "delivered" | "delivered-unconfirmed" | "dropped" | "none";
234
+ /** Epoch-ms of the last delivery attempt's completion (success or final drop). */
235
+ lastAttemptAt: number | null;
236
+ }
237
+ interface WebhookSink {
238
+ /**
239
+ * Fire-and-forget. Push an event for delivery. SYNCHRONOUS — never blocks,
240
+ * never throws, never awaits the in-flight POST. This is what consumeSse
241
+ * calls AFTER the channel + event-log sinks, so a webhook can't delay a wake.
242
+ */
243
+ enqueue(event: TandemEvent): void;
244
+ /** Snapshot of delivery counters (for /status + doctor). */
245
+ stats(): WebhookSinkStats;
246
+ /** A log-safe summary that NEVER contains the secret. */
247
+ configSummary(): string;
248
+ /** Resolves when the queue is fully drained (no buffered, none in flight). */
249
+ idle(): Promise<void>;
250
+ /** Stop the drainer and drop any buffered events (best-effort, on shutdown). */
251
+ stop(): Promise<void>;
252
+ }
253
+
254
+ interface EventStreamOptions {
255
+ brokerUrl: string;
256
+ token: string;
257
+ gateway: GatewayClient;
258
+ adapter: HarnessAdapter;
259
+ server: Server;
260
+ queue?: EventQueue;
261
+ eventLog?: EventLog;
262
+ /**
263
+ * Optional WEBHOOK SINK — a third, isolated delivery sink. When wired, every
264
+ * normalized TandemEvent is also pushed (fire-and-forget) to a configured
265
+ * HTTP endpoint so a daemonized proxy can wake a runtime via a local receiver.
266
+ * The push is SYNCHRONOUS and runs LAST in the fan-out, AFTER the event-log
267
+ * (Monitor wake) and channel sinks, so a slow/hanging/failing webhook can
268
+ * never delay or break those wake paths (see webhook-sink.ts). Unset ⇒ no
269
+ * webhook delivery, zero behavior change.
270
+ */
271
+ webhookSink?: WebhookSink;
272
+ /**
273
+ * Resubscribe-on-start hook (P0 audit item #2). Called once after each
274
+ * successful (re)connect with the live `Response`. Implementations touch
275
+ * the agent's memberships so a backend that scopes the stream by
276
+ * session-touch self-heals on every backend restart / scope reset, and
277
+ * write a `status=subscribed n=<count>` line to the event log. Injected so
278
+ * the proxy startup wires it (it owns the tandem_list result + gateway) and
279
+ * the unit tests can assert it fires per-connect without a live backend.
280
+ *
281
+ * CAVEAT (documented in resubscribeMemberships): which tool call actually
282
+ * registers a session for streaming is an UNVERIFIED backend contract — see
283
+ * docs/forward-to-backend. Until the backend defines it, the touch is a
284
+ * best-effort no-op-safe read and the status line is the load-bearing part.
285
+ */
286
+ onConnected?: () => void | Promise<void>;
287
+ }
288
+ /**
289
+ * Live stream state, surfaced by the hook-server's GET /status so `kojee-mcp
290
+ * doctor` can give an authoritative verdict instead of guessing from file
291
+ * mtimes (audit P2 doctor item). All timestamps are epoch ms, or null when the
292
+ * event hasn't happened yet.
293
+ */
294
+ interface StreamState {
295
+ /** True between a successful connect and the next disconnect. */
296
+ connected: boolean;
297
+ /** When the current connection opened (null if never connected). */
298
+ connectedSince: number | null;
299
+ /** Last time ANY byte arrived on the stream (null if never). */
300
+ lastEventAt: number | null;
301
+ /** Last time a backend heartbeat arrived (null if never). */
302
+ lastHeartbeatAt: number | null;
303
+ /** Per-room high-water cursors (tandemId → cursor). */
304
+ cursors: Record<string, number>;
305
+ /** How many times the stream has (re)connected. */
306
+ reconnectCount: number;
307
+ /**
308
+ * The LEARNED stale-stream threshold in ms (3× the observed heartbeat
309
+ * interval, ≥90s floor), or null when the watchdog hasn't yet observed ≥2
310
+ * heartbeats. null ⇒ no cadence learned ⇒ the proxy will NOT abort on
311
+ * silence, and `/status` must NOT declare the stream stale (idle ≠ dead).
312
+ */
313
+ staleAfterMs: number | null;
314
+ }
315
+ /**
316
+ * A cancel function that also exposes the live stream state for /status and a
317
+ * graceful `reconnect()` (0.5.4 wake continuity): the subscription is a
318
+ * connect-time snapshot of the caller's memberships, so a seat acquired AFTER
319
+ * connect (tandem_join via the proxy) is invisible until the next reconnect.
320
+ * `reconnect()` closes the current connection and lets the existing
321
+ * backoff/jitter machinery re-open it — the new snapshot then includes the
322
+ * just-acquired seats. No-op after cancel.
323
+ */
324
+ type StreamHandle = (() => void) & {
325
+ getState: () => StreamState;
326
+ reconnect: () => void;
327
+ };
328
+ /**
329
+ * Long-lived SSE subscription to /api/v2/tandems/stream.
330
+ * Returns a cancel function the caller can invoke on shutdown.
331
+ *
332
+ * Reconnect strategy: exponential backoff (1s → 30s) with full jitter.
333
+ * Resume via a PER-ROOM cursor map: ?since=<tandemId>:<cursor>,<tandemId>:<cursor>,…
334
+ * Cursors are per-tandem, so a single global `since` would skip/dupe events
335
+ * across rooms on a multi-room reconnect (H3). We track each room's high-water
336
+ * mark and resume each independently. Against a backend that predates H3 (which
337
+ * 400s the map), the proxy falls back to a bare global `?since=<n>` so it stays
338
+ * compatible with ANY backend version regardless of deploy order — see
339
+ * connectAndConsume.
340
+ *
341
+ * SSE failure never crashes the proxy — connector tool traffic
342
+ * continues uninterrupted.
343
+ */
344
+ declare function startEventStream(opts: EventStreamOptions): Promise<StreamHandle>;
345
+ /**
346
+ * Sanitize a wire-controlled displayname for terminal-bound output.
347
+ *
348
+ * `sender.display` lands in the line-oriented event log (`from=<name>`) AND in
349
+ * channel content/meta, both of which reach a terminal AND are read back by a
350
+ * Claude Code agent (a prompt-injection channel). Without sanitization an
351
+ * interior `\n` forges an entire fake wake-line (attacker-chosen
352
+ * `tandem=`/`cursor=`/`msg=`) that the Monitor delivers as a separate wake,
353
+ * ANSI escapes reach the terminal, and invisible code points (the TAG block,
354
+ * variation selectors, zero-width/bidi) smuggle hidden bytes into the agent's
355
+ * reading context.
356
+ *
357
+ * Defense-in-depth: we do NOT trust the backend to sanitize terminal-bound
358
+ * output. Same policy as the backend naming funnel (apps/tandems/naming.py
359
+ * `sanitize_display_name`), byte for byte:
360
+ * 1. NFC-normalize first so a decomposed base+combining input folds to its
361
+ * precomposed form (`e` + combining acute → `é`) before filtering.
362
+ * 2. DROP every code point in the strip categories (Cc/Cf/Cs/Co/Cn) or the
363
+ * invisible-Mn set. Allowlist-leaning: letters, marks, numbers,
364
+ * punctuation, symbols, and spaces survive.
365
+ * 3. Cap any CONSECUTIVE combining-mark (Mn) run at MAX_COMBINING_RUN
366
+ * (zalgo defense — legitimate single accents survive).
367
+ * 4. Collapse whitespace runs (incl. LS/PS and exotic Zs spaces) to single
368
+ * ASCII spaces, trim.
369
+ * 5. Cap at MAX_DISPLAYNAME_CHARS without splitting a surrogate pair, then
370
+ * trim any trailing space the cut exposed — so the function is
371
+ * idempotent.
372
+ * Returns "" if nothing survives (all-hostile input) so the caller can fall
373
+ * back to the historical `principal:<prefix>` synthesis.
374
+ */
375
+ declare function sanitizeDisplayname(name: string): string;
376
+ declare function normalizeBackendEvent(raw: unknown, sseEventType: string): TandemEvent;
377
+
378
+ /** Mutable debounce cursor shared across reconnects (one per proxy). */
379
+ interface ResubscribeDebounceState {
380
+ /** Epoch-ms of the last SUCCESSFUL (non-skipped) resubscribe run. */
381
+ lastRunAt: number;
382
+ }
383
+ interface ResubscribeOptions {
384
+ gateway: GatewayClient;
385
+ eventLog?: EventLog;
386
+ /**
387
+ * The tandem_ids to touch. Provide EITHER `tandemIds` (a fixed snapshot) OR
388
+ * `listTandems` (a fresh fetch per call). ROUND-2 MINOR 6: the proxy should
389
+ * pass `listTandems` so the membership list is RE-LISTED on every reconnect —
390
+ * a tandem joined mid-session is then touched on the next reconnect instead
391
+ * of being frozen at boot. `tandemIds` remains for tests and simple callers.
392
+ * Empty result ⇒ nothing to touch; we still log `status=subscribed n=0`.
393
+ */
394
+ tandemIds?: string[];
395
+ /** Fresh membership fetch, called once per resubscribe (per reconnect). */
396
+ listTandems?: () => Promise<string[]>;
397
+ /** Per-touch timeout budget (AbortSignal). Default 10s. */
398
+ perCallTimeoutMs?: number;
399
+ /** Bounded touch concurrency. Default 4. */
400
+ concurrency?: number;
401
+ /**
402
+ * Flap-damping window (MINOR E). When `debounceState` is provided and the
403
+ * last successful run was < `debounceMs` ago, this resubscribe is SKIPPED
404
+ * (returns 0, no touches, no status line). Default 30s. With no
405
+ * `debounceState` the debounce is inert (every call runs) — preserving the
406
+ * existing test/simple-caller behavior.
407
+ */
408
+ debounceMs?: number;
409
+ /** Shared debounce cursor (one per proxy). Omit to disable debouncing. */
410
+ debounceState?: ResubscribeDebounceState;
411
+ /** Injectable clock for tests. Defaults to Date.now. */
412
+ now?: () => number;
413
+ }
414
+ /**
415
+ * Touch each membership (best-effort, in parallel with a bounded pool and a
416
+ * per-call AbortSignal timeout) and write a `status=subscribed n=<count>` line.
417
+ * Returns the number of tandems for which the touch call succeeded. Never
418
+ * throws — a failed/timed-out touch is folded into the status line's `touched=`
419
+ * count and to stderr, but must not break the stream loop that calls it.
420
+ *
421
+ * MINOR 6: this runs CONCURRENTLY with consumeSse (the caller does not await it
422
+ * before consuming the stream), so a hung gateway call can never stall
423
+ * first-event delivery; the per-call timeout caps each touch independently.
424
+ */
425
+ declare function resubscribeMemberships(opts: ResubscribeOptions): Promise<number>;
426
+
427
+ export { AuthModule, type EventLog, type EventStreamOptions, GatewayClient, type HarnessAdapter, type PairedConfig, type Runtime, type StreamHandle, type StreamState, type TandemEvent, type WebhookSink, createDPoPProof, defaultPairedKeystorePath, deriveKeystorePath, generateES256KeyPair, loadKeystore, loadPairedConfig, normalizeBackendEvent, pairedConfigPath, resubscribeMemberships, sanitizeDisplayname, saveKeystore, startEventStream };
package/dist/lib.js ADDED
@@ -0,0 +1,44 @@
1
+ import {
2
+ resubscribeMemberships
3
+ } from "./chunk-OT2GILXC.js";
4
+ import {
5
+ loadPairedConfig,
6
+ pairedConfigPath
7
+ } from "./chunk-YH27B6SW.js";
8
+ import {
9
+ AuthModule
10
+ } from "./chunk-6SK6ITFE.js";
11
+ import {
12
+ GatewayClient,
13
+ defaultPairedKeystorePath,
14
+ deriveKeystorePath,
15
+ generateES256KeyPair,
16
+ loadKeystore,
17
+ saveKeystore
18
+ } from "./chunk-3XDJOHMZ.js";
19
+ import {
20
+ normalizeBackendEvent,
21
+ sanitizeDisplayname,
22
+ startEventStream
23
+ } from "./chunk-UEGQGXPY.js";
24
+ import {
25
+ createDPoPProof
26
+ } from "./chunk-2MIISF2W.js";
27
+ import "./chunk-LDZXU3DW.js";
28
+ import "./chunk-BLEGIR35.js";
29
+ export {
30
+ AuthModule,
31
+ GatewayClient,
32
+ createDPoPProof,
33
+ defaultPairedKeystorePath,
34
+ deriveKeystorePath,
35
+ generateES256KeyPair,
36
+ loadKeystore,
37
+ loadPairedConfig,
38
+ normalizeBackendEvent,
39
+ pairedConfigPath,
40
+ resubscribeMemberships,
41
+ sanitizeDisplayname,
42
+ saveKeystore,
43
+ startEventStream
44
+ };
@@ -0,0 +1,65 @@
1
+ import {
2
+ findClaudeAncestorPid
3
+ } from "./chunk-BJMASMKX.js";
4
+
5
+ // src/runtime/parent-watchdog.ts
6
+ function defaultIsPidAlive(pid) {
7
+ try {
8
+ process.kill(pid, 0);
9
+ return true;
10
+ } catch (err) {
11
+ if (err.code === "EPERM") return true;
12
+ return false;
13
+ }
14
+ }
15
+ function createParentWatchdog(opts) {
16
+ const intervalMs = opts.intervalMs ?? 7e3;
17
+ const isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
18
+ const resolveAncestor = opts.resolveAncestor ?? (() => findClaudeAncestorPid());
19
+ let timer = null;
20
+ let fired = false;
21
+ let checking = false;
22
+ function stop() {
23
+ if (timer !== null) {
24
+ clearInterval(timer);
25
+ timer = null;
26
+ }
27
+ }
28
+ async function check() {
29
+ if (fired || checking) return;
30
+ checking = true;
31
+ try {
32
+ const ccPidAlive = isPidAlive(opts.ccPid);
33
+ if (ccPidAlive) return;
34
+ let ancestor = null;
35
+ try {
36
+ ancestor = await resolveAncestor();
37
+ } catch {
38
+ ancestor = null;
39
+ }
40
+ if (ancestor !== null) return;
41
+ fired = true;
42
+ stop();
43
+ try {
44
+ opts.onParentGone();
45
+ } catch {
46
+ }
47
+ } finally {
48
+ checking = false;
49
+ }
50
+ }
51
+ return {
52
+ start() {
53
+ if (timer !== null || fired) return;
54
+ timer = setInterval(() => {
55
+ void check();
56
+ }, intervalMs);
57
+ timer.unref?.();
58
+ },
59
+ stop
60
+ };
61
+ }
62
+ export {
63
+ createParentWatchdog,
64
+ defaultIsPidAlive
65
+ };
@@ -0,0 +1,36 @@
1
+ // src/tandem/reconnect-scheduler.ts
2
+ var DEFAULT_DEBOUNCE_MS = 1e3;
3
+ function createJoinReconnectScheduler(opts) {
4
+ const debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
5
+ let pending = null;
6
+ let queued = false;
7
+ function fire() {
8
+ try {
9
+ return opts.reconnect() !== false;
10
+ } catch (err) {
11
+ console.error(
12
+ "[join-reconnect] reconnect action failed:",
13
+ err?.message ?? String(err)
14
+ );
15
+ return true;
16
+ }
17
+ }
18
+ return {
19
+ requestReconnect() {
20
+ if (pending !== null) return;
21
+ pending = setTimeout(() => {
22
+ pending = null;
23
+ if (!fire()) queued = true;
24
+ }, debounceMs);
25
+ pending.unref?.();
26
+ },
27
+ notifyReady() {
28
+ if (!queued) return;
29
+ queued = false;
30
+ if (!fire()) queued = true;
31
+ }
32
+ };
33
+ }
34
+ export {
35
+ createJoinReconnectScheduler
36
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ resubscribeMemberships
3
+ } from "./chunk-OT2GILXC.js";
4
+ export {
5
+ resubscribeMemberships
6
+ };
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  GatewayClient,
6
6
  loadKeystore
7
- } from "./chunk-36L3GCU3.js";
7
+ } from "./chunk-3XDJOHMZ.js";
8
8
  import "./chunk-2MIISF2W.js";
9
9
  import {
10
10
  executeSend,
@@ -2,16 +2,19 @@ import {
2
2
  readHookStdin
3
3
  } from "./chunk-LSUB6QMP.js";
4
4
  import {
5
- deriveDiscoveryKey,
6
- findClaudeAncestorPid
7
- } from "./chunk-BJMASMKX.js";
5
+ monitorHeartbeatPath,
6
+ nudgeSentinelPath
7
+ } from "./chunk-2TUAFAIW.js";
8
+ import {
9
+ controlTokenAuthHeaders
10
+ } from "./chunk-GI2CKKBL.js";
8
11
  import {
9
12
  buildMonitorNudge
10
13
  } from "./chunk-X672ZN7V.js";
11
14
  import {
12
- monitorHeartbeatPath,
13
- nudgeSentinelPath
14
- } from "./chunk-2TUAFAIW.js";
15
+ deriveDiscoveryKey,
16
+ findClaudeAncestorPid
17
+ } from "./chunk-BJMASMKX.js";
15
18
  import {
16
19
  readSessionDiscoveryByKey
17
20
  } from "./chunk-DO42NPNR.js";
@@ -74,7 +77,12 @@ async function runStopHook() {
74
77
  discovery: discovery ? { port: discovery.port, eventLogPath: discovery.eventLogPath } : null,
75
78
  pollEvents: async () => {
76
79
  const res = await fetch(
77
- `http://127.0.0.1:${discovery.port}/poll?type=stop&timeout_ms=${STOP_POLL_TIMEOUT_MS}`
80
+ `http://127.0.0.1:${discovery.port}/poll?type=stop&timeout_ms=${STOP_POLL_TIMEOUT_MS}`,
81
+ // 0.5.4: GET /poll is gated by the control token. Same-user hook —
82
+ // read the bearer from the path the discovery file advertises. A
83
+ // missing token yields no header and the !res.ok branch degrades to
84
+ // "no events" (fail-open, the Monitor file path is the primary wake).
85
+ { headers: controlTokenAuthHeaders(discovery.controlTokenPath) }
78
86
  );
79
87
  if (!res.ok) return { events: [], count: 0 };
80
88
  return await res.json();
@@ -1,12 +1,12 @@
1
- import {
2
- createAdaptiveWatchdog
3
- } from "./chunk-YVUXQ4Z2.js";
4
- import "./chunk-2MIISF2W.js";
5
1
  import {
6
2
  STATUS_LINE_PREFIX,
7
3
  monitorHeartbeatPath,
8
4
  statusLogPath
9
5
  } from "./chunk-2TUAFAIW.js";
6
+ import {
7
+ createAdaptiveWatchdog
8
+ } from "./chunk-UEGQGXPY.js";
9
+ import "./chunk-2MIISF2W.js";
10
10
  import "./chunk-DO42NPNR.js";
11
11
  import "./chunk-BLEGIR35.js";
12
12
 
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  readHookStdin
3
3
  } from "./chunk-LSUB6QMP.js";
4
+ import {
5
+ controlTokenAuthHeaders
6
+ } from "./chunk-GI2CKKBL.js";
4
7
  import {
5
8
  deriveDiscoveryKey,
6
9
  findClaudeAncestorPid
@@ -22,7 +25,10 @@ async function runUserPromptSubmitHook() {
22
25
  }
23
26
  let body;
24
27
  try {
25
- const res = await fetch(`http://127.0.0.1:${discovery.port}/poll?type=user-prompt-submit&timeout_ms=0`);
28
+ const res = await fetch(
29
+ `http://127.0.0.1:${discovery.port}/poll?type=user-prompt-submit&timeout_ms=0`,
30
+ { headers: controlTokenAuthHeaders(discovery.controlTokenPath) }
31
+ );
26
32
  if (!res.ok) {
27
33
  process.stdout.write("{}");
28
34
  return;
@@ -7,7 +7,7 @@ import {
7
7
  emissionRejectionReason,
8
8
  resolveSignatureEmission,
9
9
  resolveWebhookConfig
10
- } from "./chunk-OSKHA5DS.js";
10
+ } from "./chunk-V5VZPYMZ.js";
11
11
  export {
12
12
  WEBHOOK_DEFAULT_MAX_RETRIES,
13
13
  WEBHOOK_DEFAULT_SIGNATURE_HEADER,