@chances-ai/wire 24.0.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.
Files changed (93) hide show
  1. package/dist/rpc/acp/adapter.d.ts +32 -0
  2. package/dist/rpc/acp/adapter.d.ts.map +1 -0
  3. package/dist/rpc/acp/adapter.js +185 -0
  4. package/dist/rpc/acp/adapter.js.map +1 -0
  5. package/dist/rpc/acp/engine-driver.d.ts +128 -0
  6. package/dist/rpc/acp/engine-driver.d.ts.map +1 -0
  7. package/dist/rpc/acp/engine-driver.js +550 -0
  8. package/dist/rpc/acp/engine-driver.js.map +1 -0
  9. package/dist/rpc/acp/event-map.d.ts +22 -0
  10. package/dist/rpc/acp/event-map.d.ts.map +1 -0
  11. package/dist/rpc/acp/event-map.js +205 -0
  12. package/dist/rpc/acp/event-map.js.map +1 -0
  13. package/dist/rpc/acp/load-sdk.d.ts +3 -0
  14. package/dist/rpc/acp/load-sdk.d.ts.map +1 -0
  15. package/dist/rpc/acp/load-sdk.js +24 -0
  16. package/dist/rpc/acp/load-sdk.js.map +1 -0
  17. package/dist/rpc/acp/workspace-query.d.ts +41 -0
  18. package/dist/rpc/acp/workspace-query.d.ts.map +1 -0
  19. package/dist/rpc/acp/workspace-query.js +89 -0
  20. package/dist/rpc/acp/workspace-query.js.map +1 -0
  21. package/dist/rpc/driver.d.ts +42 -0
  22. package/dist/rpc/driver.d.ts.map +1 -0
  23. package/dist/rpc/driver.js +7 -0
  24. package/dist/rpc/driver.js.map +1 -0
  25. package/dist/rpc/event-map.d.ts +8 -0
  26. package/dist/rpc/event-map.d.ts.map +1 -0
  27. package/dist/rpc/event-map.js +91 -0
  28. package/dist/rpc/event-map.js.map +1 -0
  29. package/dist/rpc/index.d.ts +13 -0
  30. package/dist/rpc/index.d.ts.map +1 -0
  31. package/dist/rpc/index.js +18 -0
  32. package/dist/rpc/index.js.map +1 -0
  33. package/dist/rpc/lines.d.ts +2 -0
  34. package/dist/rpc/lines.d.ts.map +1 -0
  35. package/dist/rpc/lines.js +24 -0
  36. package/dist/rpc/lines.js.map +1 -0
  37. package/dist/rpc/protocol.d.ts +315 -0
  38. package/dist/rpc/protocol.d.ts.map +1 -0
  39. package/dist/rpc/protocol.js +70 -0
  40. package/dist/rpc/protocol.js.map +1 -0
  41. package/dist/rpc/rpc-server.d.ts +56 -0
  42. package/dist/rpc/rpc-server.d.ts.map +1 -0
  43. package/dist/rpc/rpc-server.js +305 -0
  44. package/dist/rpc/rpc-server.js.map +1 -0
  45. package/dist/rpc/stdout-guard.d.ts +5 -0
  46. package/dist/rpc/stdout-guard.d.ts.map +1 -0
  47. package/dist/rpc/stdout-guard.js +31 -0
  48. package/dist/rpc/stdout-guard.js.map +1 -0
  49. package/dist/rpc/writer.d.ts +34 -0
  50. package/dist/rpc/writer.d.ts.map +1 -0
  51. package/dist/rpc/writer.js +85 -0
  52. package/dist/rpc/writer.js.map +1 -0
  53. package/dist/serve/acp-session-host.d.ts +120 -0
  54. package/dist/serve/acp-session-host.d.ts.map +1 -0
  55. package/dist/serve/acp-session-host.js +276 -0
  56. package/dist/serve/acp-session-host.js.map +1 -0
  57. package/dist/serve/auth.d.ts +21 -0
  58. package/dist/serve/auth.d.ts.map +1 -0
  59. package/dist/serve/auth.js +58 -0
  60. package/dist/serve/auth.js.map +1 -0
  61. package/dist/serve/highlight.d.ts +25 -0
  62. package/dist/serve/highlight.d.ts.map +1 -0
  63. package/dist/serve/highlight.js +28 -0
  64. package/dist/serve/highlight.js.map +1 -0
  65. package/dist/serve/index.d.ts +14 -0
  66. package/dist/serve/index.d.ts.map +1 -0
  67. package/dist/serve/index.js +23 -0
  68. package/dist/serve/index.js.map +1 -0
  69. package/dist/serve/pairing.d.ts +25 -0
  70. package/dist/serve/pairing.d.ts.map +1 -0
  71. package/dist/serve/pairing.js +10 -0
  72. package/dist/serve/pairing.js.map +1 -0
  73. package/dist/serve/relay-frames.d.ts +29 -0
  74. package/dist/serve/relay-frames.d.ts.map +1 -0
  75. package/dist/serve/relay-frames.js +54 -0
  76. package/dist/serve/relay-frames.js.map +1 -0
  77. package/dist/serve/relay.d.ts +146 -0
  78. package/dist/serve/relay.d.ts.map +1 -0
  79. package/dist/serve/relay.js +475 -0
  80. package/dist/serve/relay.js.map +1 -0
  81. package/dist/serve/replay-hub.d.ts +102 -0
  82. package/dist/serve/replay-hub.d.ts.map +1 -0
  83. package/dist/serve/replay-hub.js +176 -0
  84. package/dist/serve/replay-hub.js.map +1 -0
  85. package/dist/serve/tls.d.ts +20 -0
  86. package/dist/serve/tls.d.ts.map +1 -0
  87. package/dist/serve/tls.js +64 -0
  88. package/dist/serve/tls.js.map +1 -0
  89. package/dist/serve/ws-transport.d.ts +64 -0
  90. package/dist/serve/ws-transport.d.ts.map +1 -0
  91. package/dist/serve/ws-transport.js +92 -0
  92. package/dist/serve/ws-transport.js.map +1 -0
  93. package/package.json +42 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * (v19 M2 / docs/6.2 §3.2; v20 M3) `ReplayHub` — the seq-stamping, fan-out
3
+ * `ByteSink` with a bounded replay ring. The engine driver (M3: `AcpEngineDriver`)
4
+ * writes its outbound frames through a `BoundedNdjsonWriter` whose sink is this
5
+ * hub. The hub:
6
+ *
7
+ * 1. stamps a relay-authoritative monotonic `rseq` on every frame (the
8
+ * reconnect cursor — `stampSeq`, a payload-agnostic byte splice);
9
+ * 2. keeps the last N stamped frames in a fixed-capacity ring so a
10
+ * reconnecting client can replay the gap (`replaySince`); a cursor below
11
+ * the ring floor gets a typed `reset` (cold reload — never a silent gap);
12
+ * 3. fans the stamped frame out to EVERY attached socket (no exclusive
13
+ * controller — the goose/OpenHands model), guarding each send so a
14
+ * closed/wedged socket is isolated and never blocks the others or the
15
+ * engine.
16
+ *
17
+ * Session state (`busy` + open `request_permission` ids) is read straight off
18
+ * the `AcpEngineDriver` (authoritative) by `AcpSessionHost`, NOT snooped from the
19
+ * fan-out bytes — so the hub stays a pure payload-agnostic seq/replay/fan-out
20
+ * layer. (A byte-snoop lived here in the chances-rpc era; it was removed with the
21
+ * M3 hard cutover, since ACP frames' first key is `rseq`/`jsonrpc`, never `type`.)
22
+ *
23
+ * **The engine never blocks on a slow remote.** `write()` ALWAYS returns `true`:
24
+ * the ring is the durable buffer, so there is no reason to park the writer (and
25
+ * parking on the slowest of many fan-out sockets would let one stalled tab
26
+ * throttle everyone — the exact failure goose avoids by never blocking the
27
+ * producer and dropping a lagging subscriber instead). A socket that stops
28
+ * draining is dropped past a hard byte cap; it reconnects and replays from the
29
+ * ring. JS's single thread makes seq-assign + ring-push naturally atomic — no
30
+ * lock (unlike goose's Rust `Mutex`).
31
+ *
32
+ * Socket-free by construction (fake {@link ServerSocket}s in tests).
33
+ */
34
+ import { type ByteSink } from "../rpc/index.js";
35
+ import type { ServerSocket } from "./ws-transport.js";
36
+ /** Default ring depth (frames). Generous vs goose's 512 / claude-code's 5000;
37
+ * a normal turn is well under this, so a transient drop replays in full. */
38
+ export declare const DEFAULT_RING_CAPACITY = 4096;
39
+ export interface ReplayHubOptions {
40
+ /** Ring depth in frames. Default {@link DEFAULT_RING_CAPACITY}. */
41
+ capacity?: number;
42
+ }
43
+ export interface ReplaySlice {
44
+ /** True ⇒ the client's cursor predates the ring floor; it must discard its
45
+ * transcript and take `frames` (the whole live ring) as a cold reload. */
46
+ reset: boolean;
47
+ /** The stamped frame lines to replay, in ascending `rseq` order. */
48
+ frames: string[];
49
+ }
50
+ export declare class ReplayHub implements ByteSink {
51
+ private readonly capacity;
52
+ /** Fixed-size circular buffer of stamped frames (O(1) push + evict-oldest, no
53
+ * per-frame `Array.shift`). `start` is the oldest slot; `count` ≤ capacity. */
54
+ private readonly buf;
55
+ private start;
56
+ private count;
57
+ private counter;
58
+ private readonly sockets;
59
+ private drainWaiters;
60
+ constructor(opts?: ReplayHubOptions);
61
+ /**
62
+ * Stamp + ring + fan-out. ALWAYS returns `true` — the engine/writer
63
+ * never parks on a slow remote (the ring is the durable buffer). Receives the
64
+ * writer's `JSON.stringify(frame) + "\n"`; `stampSeq` splices `rseq` after the
65
+ * leading `{`, leaving the trailing newline untouched.
66
+ */
67
+ write(chunk: string): boolean;
68
+ /** Dormant in M2: `write` never returns `false`, so the writer never awaits a
69
+ * drain. Kept faithful to the `ByteSink` contract (and woken by
70
+ * {@link signalDrain}) in case a future path parks. */
71
+ once(_event: "drain", listener: () => void): void;
72
+ /** Release any (dormant) parked writer — e.g. a socket became writable. */
73
+ signalDrain(): void;
74
+ private ringPush;
75
+ /** The highest `rseq` assigned (0 ⇒ nothing written yet). */
76
+ get headSeq(): number;
77
+ /** The oldest `rseq` still replayable (0 ⇒ ring empty). */
78
+ get floorSeq(): number;
79
+ /**
80
+ * Frames a client at `lastSeq` has missed. `reset` ⇒ its cursor is below the
81
+ * ring floor (unrecoverable) — the caller cold-reloads it with the whole ring.
82
+ * Mirrors goose `subscribe`: `last_id > 0 && floor > 0 && last_id < floor`
83
+ * ⇒ too far behind.
84
+ */
85
+ replaySince(lastSeq: number): ReplaySlice;
86
+ /** Add a socket to the live fan-out set. Caller (SessionHost) has already sent
87
+ * the welcome + replay synchronously, so the first live frame this socket
88
+ * sees is strictly newer than everything replayed (disjoint, in order). */
89
+ addSocket(socket: ServerSocket): void;
90
+ removeSocket(socket: ServerSocket): void;
91
+ get socketCount(): number;
92
+ /**
93
+ * Send one stamped frame to a single socket, guarded: a dead socket
94
+ * (`send===0`) or a wedged one (buffer past the hard cap) is dropped + closed,
95
+ * never throwing, never blocking the others. Returns whether the socket stays
96
+ * attached. Reused by {@link SessionHost} for the synchronous replay path so
97
+ * replay + live share one robust send.
98
+ */
99
+ sendTo(socket: ServerSocket, line: string): boolean;
100
+ private fanout;
101
+ }
102
+ //# sourceMappingURL=replay-hub.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-hub.d.ts","sourceRoot":"","sources":["../../src/serve/replay-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;6EAC6E;AAC7E,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAY1C,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B;+EAC2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,oEAAoE;IACpE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,qBAAa,SAAU,YAAW,QAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC;oFACgF;IAChF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA2B;IAC/C,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,KAAK,CAAK;IAElB,OAAO,CAAC,OAAO,CAAK;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IACnD,OAAO,CAAC,YAAY,CAAyB;gBAEjC,IAAI,GAAE,gBAAqB;IAOvC;;;;;OAKG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAQ7B;;4DAEwD;IACxD,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAIjD,2EAA2E;IAC3E,WAAW,IAAI,IAAI;IAQnB,OAAO,CAAC,QAAQ;IAWhB,6DAA6D;IAC7D,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,2DAA2D;IAC3D,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;;;;OAKG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW;IAezC;;gFAE4E;IAC5E,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAIrC,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAIxC,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;;;;;OAMG;IACH,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAyBnD,OAAO,CAAC,MAAM;CAKf"}
@@ -0,0 +1,176 @@
1
+ /**
2
+ * (v19 M2 / docs/6.2 §3.2; v20 M3) `ReplayHub` — the seq-stamping, fan-out
3
+ * `ByteSink` with a bounded replay ring. The engine driver (M3: `AcpEngineDriver`)
4
+ * writes its outbound frames through a `BoundedNdjsonWriter` whose sink is this
5
+ * hub. The hub:
6
+ *
7
+ * 1. stamps a relay-authoritative monotonic `rseq` on every frame (the
8
+ * reconnect cursor — `stampSeq`, a payload-agnostic byte splice);
9
+ * 2. keeps the last N stamped frames in a fixed-capacity ring so a
10
+ * reconnecting client can replay the gap (`replaySince`); a cursor below
11
+ * the ring floor gets a typed `reset` (cold reload — never a silent gap);
12
+ * 3. fans the stamped frame out to EVERY attached socket (no exclusive
13
+ * controller — the goose/OpenHands model), guarding each send so a
14
+ * closed/wedged socket is isolated and never blocks the others or the
15
+ * engine.
16
+ *
17
+ * Session state (`busy` + open `request_permission` ids) is read straight off
18
+ * the `AcpEngineDriver` (authoritative) by `AcpSessionHost`, NOT snooped from the
19
+ * fan-out bytes — so the hub stays a pure payload-agnostic seq/replay/fan-out
20
+ * layer. (A byte-snoop lived here in the chances-rpc era; it was removed with the
21
+ * M3 hard cutover, since ACP frames' first key is `rseq`/`jsonrpc`, never `type`.)
22
+ *
23
+ * **The engine never blocks on a slow remote.** `write()` ALWAYS returns `true`:
24
+ * the ring is the durable buffer, so there is no reason to park the writer (and
25
+ * parking on the slowest of many fan-out sockets would let one stalled tab
26
+ * throttle everyone — the exact failure goose avoids by never blocking the
27
+ * producer and dropping a lagging subscriber instead). A socket that stops
28
+ * draining is dropped past a hard byte cap; it reconnects and replays from the
29
+ * ring. JS's single thread makes seq-assign + ring-push naturally atomic — no
30
+ * lock (unlike goose's Rust `Mutex`).
31
+ *
32
+ * Socket-free by construction (fake {@link ServerSocket}s in tests).
33
+ */
34
+ import { stampSeq } from "../rpc/index.js";
35
+ /** Default ring depth (frames). Generous vs goose's 512 / claude-code's 5000;
36
+ * a normal turn is well under this, so a transient drop replays in full. */
37
+ export const DEFAULT_RING_CAPACITY = 4096;
38
+ /** Drop a socket whose own send buffer grows past this (a wedged reader). 16 MiB
39
+ * is far above any loopback steady state — it only catches a truly stuck client
40
+ * so relay memory stays bounded. The dropped client reconnects + replays. */
41
+ const WEDGED_SOCKET_BYTES = 16 * 1024 * 1024;
42
+ export class ReplayHub {
43
+ capacity;
44
+ /** Fixed-size circular buffer of stamped frames (O(1) push + evict-oldest, no
45
+ * per-frame `Array.shift`). `start` is the oldest slot; `count` ≤ capacity. */
46
+ buf;
47
+ start = 0;
48
+ count = 0;
49
+ counter = 0; // last assigned rseq (monotonic, from 1)
50
+ sockets = new Set();
51
+ drainWaiters = [];
52
+ constructor(opts = {}) {
53
+ this.capacity = Math.max(1, opts.capacity ?? DEFAULT_RING_CAPACITY);
54
+ this.buf = new Array(this.capacity);
55
+ }
56
+ // -- ByteSink (the BoundedNdjsonWriter writes through this) -----------------
57
+ /**
58
+ * Stamp + ring + fan-out. ALWAYS returns `true` — the engine/writer
59
+ * never parks on a slow remote (the ring is the durable buffer). Receives the
60
+ * writer's `JSON.stringify(frame) + "\n"`; `stampSeq` splices `rseq` after the
61
+ * leading `{`, leaving the trailing newline untouched.
62
+ */
63
+ write(chunk) {
64
+ const seq = ++this.counter;
65
+ const stamped = stampSeq(chunk, seq);
66
+ this.ringPush(seq, stamped);
67
+ this.fanout(stamped);
68
+ return true;
69
+ }
70
+ /** Dormant in M2: `write` never returns `false`, so the writer never awaits a
71
+ * drain. Kept faithful to the `ByteSink` contract (and woken by
72
+ * {@link signalDrain}) in case a future path parks. */
73
+ once(_event, listener) {
74
+ this.drainWaiters.push(listener);
75
+ }
76
+ /** Release any (dormant) parked writer — e.g. a socket became writable. */
77
+ signalDrain() {
78
+ const waiters = this.drainWaiters;
79
+ this.drainWaiters = [];
80
+ for (const fn of waiters)
81
+ fn();
82
+ }
83
+ // -- ring -------------------------------------------------------------------
84
+ ringPush(seq, line) {
85
+ if (this.count < this.capacity) {
86
+ this.buf[(this.start + this.count) % this.capacity] = { seq, line };
87
+ this.count++;
88
+ }
89
+ else {
90
+ // Full: overwrite the oldest slot and advance the floor. O(1).
91
+ this.buf[this.start] = { seq, line };
92
+ this.start = (this.start + 1) % this.capacity;
93
+ }
94
+ }
95
+ /** The highest `rseq` assigned (0 ⇒ nothing written yet). */
96
+ get headSeq() {
97
+ return this.counter;
98
+ }
99
+ /** The oldest `rseq` still replayable (0 ⇒ ring empty). */
100
+ get floorSeq() {
101
+ return this.count === 0 ? 0 : this.buf[this.start].seq;
102
+ }
103
+ /**
104
+ * Frames a client at `lastSeq` has missed. `reset` ⇒ its cursor is below the
105
+ * ring floor (unrecoverable) — the caller cold-reloads it with the whole ring.
106
+ * Mirrors goose `subscribe`: `last_id > 0 && floor > 0 && last_id < floor`
107
+ * ⇒ too far behind.
108
+ */
109
+ replaySince(lastSeq) {
110
+ const floor = this.floorSeq;
111
+ const reset = lastSeq > 0 && floor > 0 && lastSeq < floor;
112
+ const since = reset ? 0 : lastSeq;
113
+ const frames = [];
114
+ // Ring order is strictly ascending in `seq`; collect the suffix `seq > since`.
115
+ for (let i = 0; i < this.count; i++) {
116
+ const slot = this.buf[(this.start + i) % this.capacity];
117
+ if (slot.seq > since)
118
+ frames.push(slot.line);
119
+ }
120
+ return { reset, frames };
121
+ }
122
+ // -- fan-out ----------------------------------------------------------------
123
+ /** Add a socket to the live fan-out set. Caller (SessionHost) has already sent
124
+ * the welcome + replay synchronously, so the first live frame this socket
125
+ * sees is strictly newer than everything replayed (disjoint, in order). */
126
+ addSocket(socket) {
127
+ this.sockets.add(socket);
128
+ }
129
+ removeSocket(socket) {
130
+ this.sockets.delete(socket);
131
+ }
132
+ get socketCount() {
133
+ return this.sockets.size;
134
+ }
135
+ /**
136
+ * Send one stamped frame to a single socket, guarded: a dead socket
137
+ * (`send===0`) or a wedged one (buffer past the hard cap) is dropped + closed,
138
+ * never throwing, never blocking the others. Returns whether the socket stays
139
+ * attached. Reused by {@link SessionHost} for the synchronous replay path so
140
+ * replay + live share one robust send.
141
+ */
142
+ sendTo(socket, line) {
143
+ let sent;
144
+ try {
145
+ sent = socket.send(line);
146
+ }
147
+ catch {
148
+ this.sockets.delete(socket);
149
+ return false; // socket threw mid-send (closing) — isolate it
150
+ }
151
+ if (sent === 0) {
152
+ this.sockets.delete(socket);
153
+ return false; // dropped/dead
154
+ }
155
+ const buffered = socket.bufferedAmount;
156
+ if (buffered !== undefined && buffered > WEDGED_SOCKET_BYTES) {
157
+ this.sockets.delete(socket);
158
+ try {
159
+ socket.close(1013, "relay: client too slow");
160
+ }
161
+ catch {
162
+ /* already gone */
163
+ }
164
+ return false;
165
+ }
166
+ return true; // sent or backpressured-but-enqueued (-1) — stays attached
167
+ }
168
+ fanout(stamped) {
169
+ if (this.sockets.size === 0)
170
+ return;
171
+ // Snapshot so a removal during iteration (a guard-drop) is safe.
172
+ for (const socket of [...this.sockets])
173
+ this.sendTo(socket, stamped);
174
+ }
175
+ }
176
+ //# sourceMappingURL=replay-hub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-hub.js","sourceRoot":"","sources":["../../src/serve/replay-hub.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export interface SelfSignedCert {
2
+ /** PEM certificate. */
3
+ cert: string;
4
+ /** PEM private key. */
5
+ key: string;
6
+ /** SHA-256 fingerprint of the DER cert: uppercase colon-separated hex (goose's
7
+ * format — exactly what a client pins). */
8
+ fingerprint: string;
9
+ }
10
+ /**
11
+ * Generate a self-signed EC (P-256, ECDSA-SHA256) cert valid for `hosts` (always
12
+ * plus `localhost` + `127.0.0.1`). `now` is injected so the validity window is
13
+ * deterministic in tests. Async — WebCrypto under the hood (proven to survive
14
+ * `bun --compile` in the Stage 0 spike).
15
+ */
16
+ export declare function generateSelfSignedCert(hosts: string[], now?: number, validityDays?: number): Promise<SelfSignedCert>;
17
+ /** SHA-256 fingerprint of a PEM cert (over its DER bytes), uppercase colon hex —
18
+ * the value printed to stderr + embedded in the QR + pinned by clients. */
19
+ export declare function fingerprintOf(certPem: string): string;
20
+ //# sourceMappingURL=tls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tls.d.ts","sourceRoot":"","sources":["../../src/serve/tls.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC7B,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ;gDAC4C;IAC5C,WAAW,EAAE,MAAM,CAAC;CACrB;AAMD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAmB,EAAE,YAAY,GAAE,MAA8B,GAAG,OAAO,CAAC,cAAc,CAAC,CAa7J;AAED;4EAC4E;AAC5E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * (v21 M4 / docs/6.5 §3) Self-signed TLS for a non-loopback `chances serve`,
3
+ * mirroring goose's model (`crates/goose-server/src/tls.rs`). On a LAN bind we
4
+ * generate a self-signed cert (SAN = the bound host + `localhost` + `127.0.0.1`),
5
+ * print its SHA-256 fingerprint, and clients PIN it (TOFU — defeats LAN MITM even
6
+ * with a self-signed cert). The CLI CACHES the cert (in the vault) so the
7
+ * fingerprint is stable across restarts — a client pairs once.
8
+ *
9
+ * This module is PURE cert generation + fingerprinting; caching/storage is the
10
+ * CLI's job (the relay stays storage-free). The fingerprint is SHA-256 of the DER
11
+ * cert, derived here via node:crypto's `X509Certificate.fingerprint256` — NOT
12
+ * selfsigned's own `.fingerprint`, which is SHA-1 (the Stage 0 spike caught this).
13
+ */
14
+ import { X509Certificate } from "node:crypto";
15
+ import { generate } from "selfsigned";
16
+ /** Default cert validity. 825 days = the CA/Browser-forum max for leaf certs;
17
+ * ample for a long-lived local pairing, re-mintable any time. */
18
+ const DEFAULT_VALIDITY_DAYS = 825;
19
+ /**
20
+ * Generate a self-signed EC (P-256, ECDSA-SHA256) cert valid for `hosts` (always
21
+ * plus `localhost` + `127.0.0.1`). `now` is injected so the validity window is
22
+ * deterministic in tests. Async — WebCrypto under the hood (proven to survive
23
+ * `bun --compile` in the Stage 0 spike).
24
+ */
25
+ export async function generateSelfSignedCert(hosts, now = Date.now(), validityDays = DEFAULT_VALIDITY_DAYS) {
26
+ const pems = await generate([{ name: "commonName", value: "chances serve" }], {
27
+ keyType: "ec",
28
+ curve: "P-256",
29
+ algorithm: "sha256",
30
+ notBeforeDate: new Date(now),
31
+ notAfterDate: new Date(now + validityDays * 24 * 3600 * 1000),
32
+ extensions: [
33
+ { name: "basicConstraints", cA: false, critical: true },
34
+ { name: "subjectAltName", altNames: buildAltNames(hosts) },
35
+ ],
36
+ });
37
+ return { cert: pems.cert, key: pems.private, fingerprint: fingerprintOf(pems.cert) };
38
+ }
39
+ /** SHA-256 fingerprint of a PEM cert (over its DER bytes), uppercase colon hex —
40
+ * the value printed to stderr + embedded in the QR + pinned by clients. */
41
+ export function fingerprintOf(certPem) {
42
+ return new X509Certificate(certPem).fingerprint256;
43
+ }
44
+ /** SAN list: `localhost` + `127.0.0.1` are always present (the loopback aliases),
45
+ * plus each requested host classified as DNS (type 2) or IP (type 7). Deduped. */
46
+ function buildAltNames(hosts) {
47
+ const dns = new Set(["localhost"]);
48
+ const ips = new Set(["127.0.0.1"]);
49
+ for (const h of hosts) {
50
+ if (h === "0.0.0.0" || h === "::")
51
+ continue; // wildcard binds are not a SAN
52
+ if (isIpLiteral(h))
53
+ ips.add(h);
54
+ else
55
+ dns.add(h);
56
+ }
57
+ return [...[...dns].map((value) => ({ type: 2, value })), ...[...ips].map((ip) => ({ type: 7, ip }))];
58
+ }
59
+ /** Crude IPv4/IPv6 literal check (enough to route a host into the right SAN
60
+ * slot; a hostname with letters is DNS, a dotted-quad or colon form is an IP). */
61
+ function isIpLiteral(h) {
62
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(h) || h.includes(":");
63
+ }
64
+ //# sourceMappingURL=tls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tls.js","sourceRoot":"","sources":["../../src/serve/tls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAYtC;kEACkE;AAClE,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAElC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,KAAe,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE,EAAE,eAAuB,qBAAqB;IAClI,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,EAAE;QAC5E,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,OAAO;QACd,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC;QAC5B,YAAY,EAAE,IAAI,IAAI,CAAC,GAAG,GAAG,YAAY,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;QAC7D,UAAU,EAAE;YACV,EAAE,IAAI,EAAE,kBAAkB,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE;YACvD,EAAE,IAAI,EAAE,gBAAgB,EAAE,QAAQ,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE;SAC3D;KACF,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AACvF,CAAC;AAED;4EAC4E;AAC5E,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,OAAO,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC;AACrD,CAAC;AAED;mFACmF;AACnF,SAAS,aAAa,CAAC,KAAe;IACpC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAS,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAS,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;YAAE,SAAS,CAAC,+BAA+B;QAC5E,IAAI,WAAW,CAAC,CAAC,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;;YAC1B,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAU,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAU,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AAC1H,CAAC;AAED;mFACmF;AACnF,SAAS,WAAW,CAAC,CAAS;IAC5B,OAAO,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * (v17 M1 / v20 M3) Relay transport primitives shared by the ACP control
3
+ * channel. M1's `RpcServer`-per-socket binding (`RpcSocketSession` + `WsByteSink`)
4
+ * was retired in the M3 hard cutover: the relay now runs ONE persistent
5
+ * `AcpSessionHost` whose `AcpEngineDriver` writes through the `ReplayHub`, so the
6
+ * only pieces left here are the transport-shape primitives the relay +
7
+ * `AcpSessionHost` reuse:
8
+ * - {@link ServerSocket}: the minimal server-socket surface the fan-out sends through.
9
+ * - {@link MessageQueue}: a push-driven `AsyncIterable<string>` feeding the driver's input.
10
+ * - {@link perConnectionHost}: makes per-connection shutdown a no-op so the
11
+ * shared runtime outlives individual sockets.
12
+ *
13
+ * Socket-free by construction (tests inject a recording fake `ServerSocket`).
14
+ */
15
+ import type { EngineHost } from "../rpc/index.js";
16
+ /**
17
+ * The minimal server-socket surface the {@link ReplayHub} fan-out sends through.
18
+ * A `ws` WebSocket (adapted by the relay's `nodeServerSocket`) satisfies it; tests
19
+ * inject a recording fake. `send` returns a tri-state: `>0` bytes written, `-1`
20
+ * enqueued under backpressure, `0` dropped (connection gone).
21
+ */
22
+ export interface ServerSocket {
23
+ send(data: string): number;
24
+ close(code?: number, reason?: string): void;
25
+ /** Bytes queued in the socket's own send buffer, if the transport exposes it
26
+ * (`ws.bufferedAmount`). The {@link ReplayHub} fan-out reads it to drop a
27
+ * WEDGED socket past a hard cap so a single dead remote can't grow relay
28
+ * memory unbounded — the dropped client reconnects + replays from the ring.
29
+ * Absent on test fakes (the cap is then simply never tripped). */
30
+ readonly bufferedAmount?: number;
31
+ }
32
+ /**
33
+ * A push-driven `AsyncIterable<string>` feeding `AcpEngineDriver.run`. WebSocket
34
+ * preserves message boundaries, so each inbound message is one (or a few
35
+ * LF-batched) ACP JSON-RPC line(s); {@link AcpSessionHost.onMessage} splits and
36
+ * {@link push}es each. Single-consumer (the driver iterates once); items pushed
37
+ * before the consumer awaits are buffered in order.
38
+ */
39
+ export declare class MessageQueue implements AsyncIterable<string> {
40
+ private readonly buffer;
41
+ private waiting;
42
+ private ended;
43
+ push(line: string): void;
44
+ /** No more input (process shutdown) — drains the buffer, then ends the iterator. */
45
+ end(): void;
46
+ [Symbol.asyncIterator](): AsyncIterator<string>;
47
+ }
48
+ /**
49
+ * Wrap an `EngineHost` so per-connection / per-session shutdown does NOT tear
50
+ * down the shared runtime. The CLI's `makeEngineHost(ctx).dispose()` runs the
51
+ * whole-process `disposeRuntime(ctx)` — correct for `chances rpc` (one stdio
52
+ * client = process lifetime) but wrong for a relay that holds a PERSISTENT
53
+ * session outliving individual sockets. The real teardown happens once when
54
+ * `chances serve` exits (`main()`'s `finally`, idempotent). `build`/`listModels`
55
+ * delegate unchanged; only `dispose` becomes a no-op.
56
+ *
57
+ * (v23 M5) The optional read-only workspace queries are FORWARDED when present —
58
+ * the relay reads them off its host to advertise `workspaceQueries` and answer
59
+ * `list_files`/`read_file`/`git_*`. Dropping them here (as the original wrapper
60
+ * did) silently disabled the 3-pane IDE over the relay even though the CLI host
61
+ * supported it; the desktop's real-socket test catches that regression.
62
+ */
63
+ export declare function perConnectionHost(host: EngineHost): EngineHost;
64
+ //# sourceMappingURL=ws-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-transport.d.ts","sourceRoot":"","sources":["../../src/serve/ws-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C;;;;uEAImE;IACnE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;GAMG;AACH,qBAAa,YAAa,YAAW,aAAa,CAAC,MAAM,CAAC;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,OAAO,CAAsD;IACrE,OAAO,CAAC,KAAK,CAAS;IAEtB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAWxB,oFAAoF;IACpF,GAAG,IAAI,IAAI;IAUJ,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,MAAM,CAAC;CAcvD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAU9D"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * (v17 M1 / v20 M3) Relay transport primitives shared by the ACP control
3
+ * channel. M1's `RpcServer`-per-socket binding (`RpcSocketSession` + `WsByteSink`)
4
+ * was retired in the M3 hard cutover: the relay now runs ONE persistent
5
+ * `AcpSessionHost` whose `AcpEngineDriver` writes through the `ReplayHub`, so the
6
+ * only pieces left here are the transport-shape primitives the relay +
7
+ * `AcpSessionHost` reuse:
8
+ * - {@link ServerSocket}: the minimal server-socket surface the fan-out sends through.
9
+ * - {@link MessageQueue}: a push-driven `AsyncIterable<string>` feeding the driver's input.
10
+ * - {@link perConnectionHost}: makes per-connection shutdown a no-op so the
11
+ * shared runtime outlives individual sockets.
12
+ *
13
+ * Socket-free by construction (tests inject a recording fake `ServerSocket`).
14
+ */
15
+ /**
16
+ * A push-driven `AsyncIterable<string>` feeding `AcpEngineDriver.run`. WebSocket
17
+ * preserves message boundaries, so each inbound message is one (or a few
18
+ * LF-batched) ACP JSON-RPC line(s); {@link AcpSessionHost.onMessage} splits and
19
+ * {@link push}es each. Single-consumer (the driver iterates once); items pushed
20
+ * before the consumer awaits are buffered in order.
21
+ */
22
+ export class MessageQueue {
23
+ buffer = [];
24
+ waiting = null;
25
+ ended = false;
26
+ push(line) {
27
+ if (this.ended)
28
+ return;
29
+ if (this.waiting) {
30
+ const resolve = this.waiting;
31
+ this.waiting = null;
32
+ resolve({ value: line, done: false });
33
+ }
34
+ else {
35
+ this.buffer.push(line);
36
+ }
37
+ }
38
+ /** No more input (process shutdown) — drains the buffer, then ends the iterator. */
39
+ end() {
40
+ if (this.ended)
41
+ return;
42
+ this.ended = true;
43
+ if (this.waiting) {
44
+ const resolve = this.waiting;
45
+ this.waiting = null;
46
+ resolve({ value: undefined, done: true });
47
+ }
48
+ }
49
+ async *[Symbol.asyncIterator]() {
50
+ while (true) {
51
+ if (this.buffer.length > 0) {
52
+ yield this.buffer.shift();
53
+ continue;
54
+ }
55
+ if (this.ended)
56
+ return;
57
+ const next = await new Promise((resolve) => {
58
+ this.waiting = resolve;
59
+ });
60
+ if (next.done)
61
+ return;
62
+ yield next.value;
63
+ }
64
+ }
65
+ }
66
+ /**
67
+ * Wrap an `EngineHost` so per-connection / per-session shutdown does NOT tear
68
+ * down the shared runtime. The CLI's `makeEngineHost(ctx).dispose()` runs the
69
+ * whole-process `disposeRuntime(ctx)` — correct for `chances rpc` (one stdio
70
+ * client = process lifetime) but wrong for a relay that holds a PERSISTENT
71
+ * session outliving individual sockets. The real teardown happens once when
72
+ * `chances serve` exits (`main()`'s `finally`, idempotent). `build`/`listModels`
73
+ * delegate unchanged; only `dispose` becomes a no-op.
74
+ *
75
+ * (v23 M5) The optional read-only workspace queries are FORWARDED when present —
76
+ * the relay reads them off its host to advertise `workspaceQueries` and answer
77
+ * `list_files`/`read_file`/`git_*`. Dropping them here (as the original wrapper
78
+ * did) silently disabled the 3-pane IDE over the relay even though the CLI host
79
+ * supported it; the desktop's real-socket test catches that regression.
80
+ */
81
+ export function perConnectionHost(host) {
82
+ return {
83
+ build: (resolver) => host.build(resolver),
84
+ listModels: () => host.listModels(),
85
+ dispose: async () => { },
86
+ ...(host.queryListFiles ? { queryListFiles: host.queryListFiles.bind(host) } : {}),
87
+ ...(host.queryReadFile ? { queryReadFile: host.queryReadFile.bind(host) } : {}),
88
+ ...(host.queryGitStatus ? { queryGitStatus: host.queryGitStatus.bind(host) } : {}),
89
+ ...(host.queryGitDiff ? { queryGitDiff: host.queryGitDiff.bind(host) } : {}),
90
+ };
91
+ }
92
+ //# sourceMappingURL=ws-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-transport.js","sourceRoot":"","sources":["../../src/serve/ws-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAqBH;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IACN,MAAM,GAAa,EAAE,CAAC;IAC/B,OAAO,GAAiD,IAAI,CAAC;IAC7D,KAAK,GAAG,KAAK,CAAC;IAEtB,IAAI,CAAC,IAAY;QACf,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;YAC7B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACxC,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,oFAAoF;IACpF,GAAG;QACD,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;YAC7B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,OAAO,CAAC,EAAE,KAAK,EAAE,SAAkB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;QAC3B,OAAO,IAAI,EAAE,CAAC;YACZ,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAG,CAAC;gBAC3B,SAAS;YACX,CAAC;YACD,IAAI,IAAI,CAAC,KAAK;gBAAE,OAAO;YACvB,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAyB,CAAC,OAAO,EAAE,EAAE;gBACjE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;YACzB,CAAC,CAAC,CAAC;YACH,IAAI,IAAI,CAAC,IAAI;gBAAE,OAAO;YACtB,MAAM,IAAI,CAAC,KAAK,CAAC;QACnB,CAAC;IACH,CAAC;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAgB;IAChD,OAAO;QACL,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QACzC,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;QACnC,OAAO,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;QACvB,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7E,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@chances-ai/wire",
3
+ "version": "24.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ "./rpc": {
7
+ "types": "./dist/rpc/index.d.ts",
8
+ "import": "./dist/rpc/index.js"
9
+ },
10
+ "./serve": {
11
+ "types": "./dist/serve/index.d.ts",
12
+ "import": "./dist/serve/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@chances-ai/engine": "24.0.0",
20
+ "@chances-ai/runtime": "24.0.0",
21
+ "@chances-ai/ui-core": "24.0.0",
22
+ "selfsigned": "^5.5.0",
23
+ "ws": "^8.18.0",
24
+ "zod": "^3.23.8"
25
+ },
26
+ "peerDependencies": {
27
+ "@agentclientprotocol/sdk": "^0.22.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "@agentclientprotocol/sdk": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "devDependencies": {
35
+ "@agentclientprotocol/sdk": "^0.22.0"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -b",
39
+ "check": "tsc -b",
40
+ "test": "bun test --timeout 30000"
41
+ }
42
+ }