pipane 0.1.5 → 0.1.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 (138) hide show
  1. package/README.md +45 -1
  2. package/THIRD_PARTY_NOTICES.md +29 -0
  3. package/bin/pipane-rendezvous.js +21 -0
  4. package/bin/pipane.js +21 -1
  5. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/dist/client/assets/__vite-browser-external-DtLbhLGx.js +1 -0
  65. package/dist/client/assets/app-runtime-Rw-O-AwW.js +1 -0
  66. package/dist/client/assets/backend-landing-page-B7SbyIXQ.js +1 -0
  67. package/dist/client/assets/index-DwbBcYUf.js +2 -0
  68. package/dist/client/assets/index-iblQYBAl.css +1 -0
  69. package/dist/client/assets/main-LGItV9Aj.js +2807 -0
  70. package/dist/client/assets/pairing-page-D-WxlWZR.js +1 -0
  71. package/dist/client/assets/remote-backend-manager-BWuGh30I.js +1 -0
  72. package/dist/client/assets/rendezvous-trust-api-C2Zob5i4.js +4 -0
  73. package/dist/client/assets/webrtc-frame-transport-BijNnA7N.js +1 -0
  74. package/dist/client/assets/ws-agent-adapter-BRysf-Y7.js +6 -0
  75. package/dist/client/index.html +2 -2
  76. package/dist/server/rendezvous/rendezvous-hub.js +426 -0
  77. package/dist/server/rendezvous/server.js +247 -0
  78. package/dist/server/rendezvous/trust-store.js +432 -0
  79. package/dist/server/server/auth-guard.js +61 -0
  80. package/dist/server/server/backend-connection-authorizer.js +29 -0
  81. package/dist/server/server/backend-identity.js +167 -0
  82. package/dist/server/server/backend-protocol-handler.js +132 -0
  83. package/dist/server/server/backend-trust-store.js +157 -0
  84. package/dist/server/server/backend-webrtc.js +239 -0
  85. package/dist/server/server/bundled-extensions.js +6 -0
  86. package/dist/server/server/conversation-file-access.js +97 -0
  87. package/dist/server/server/extension-status.js +31 -0
  88. package/dist/server/server/frame-connection.js +48 -0
  89. package/dist/server/server/frame-router.js +45 -0
  90. package/dist/server/server/ice-servers.js +24 -0
  91. package/dist/server/server/local-backend-api.js +389 -0
  92. package/dist/server/server/local-settings.js +28 -0
  93. package/dist/server/server/pi-rpc-protocol.js +407 -0
  94. package/dist/server/server/pi-runtime.js +1 -1
  95. package/dist/server/server/process-pool.js +179 -136
  96. package/dist/server/server/rendezvous-client.js +282 -0
  97. package/dist/server/server/rest-api.js +121 -231
  98. package/dist/server/server/server.js +188 -134
  99. package/dist/server/server/session-actor.js +202 -0
  100. package/dist/server/server/session-index.js +111 -18
  101. package/dist/server/server/session-jsonl.js +128 -8
  102. package/dist/server/server/session-path.js +145 -0
  103. package/dist/server/server/session-registry.js +67 -0
  104. package/dist/server/server/update-api.js +28 -0
  105. package/dist/server/server/update-check.js +33 -13
  106. package/dist/server/server/update-manager.js +231 -0
  107. package/dist/server/server/worktree-name.js +124 -0
  108. package/dist/server/server/ws-handler.js +784 -478
  109. package/dist/server/shared/backend-api.js +1 -0
  110. package/dist/server/shared/backend-protocol.js +164 -0
  111. package/dist/server/shared/jsonl-sync.js +41 -0
  112. package/dist/server/shared/node-trust-crypto.js +61 -0
  113. package/dist/server/shared/rendezvous-protocol.js +243 -0
  114. package/dist/server/shared/rpc-timeouts.js +4 -0
  115. package/dist/server/shared/thinking-levels.js +94 -0
  116. package/dist/server/shared/tool-runtime.js +1 -0
  117. package/dist/server/shared/trust-protocol.js +97 -0
  118. package/dist/server/shared/updates.js +4 -0
  119. package/dist/server/shared/ws-protocol.js +473 -0
  120. package/docs/protocol.md +127 -0
  121. package/extensions/canvas.ts +1 -1
  122. package/package.json +42 -17
  123. package/dist/client/assets/_node-stub_node_crypto-C_7epg3G.js +0 -1
  124. package/dist/client/assets/_node-stub_node_fs-B-VOzeXW.js +0 -1
  125. package/dist/client/assets/_node-stub_node_http-CROXaVGJ.js +0 -1
  126. package/dist/client/assets/_node-stub_node_os-TTKJha15.js +0 -1
  127. package/dist/client/assets/_node-stub_node_path-DJCJiKv7.js +0 -1
  128. package/dist/client/assets/_node-stub_stream-DDexIdn_.js +0 -1
  129. package/dist/client/assets/index-BtDAXAo1.css +0 -1
  130. package/dist/client/assets/index-CAQqlK61.js +0 -131
  131. package/dist/client/assets/index-CJEeBmBj.js +0 -3
  132. package/dist/client/assets/index-DUEhKmPQ.js +0 -1
  133. package/dist/client/assets/index-XEVKscP-.js +0 -4443
  134. package/dist/client/assets/index-woI4agCy.js +0 -45
  135. package/dist/server/server/attached-session.js +0 -209
  136. package/dist/server/server/load-trace-store.js +0 -48
  137. package/dist/server/server/session-lifecycle.js +0 -155
  138. package/patches/@mariozechner+pi-web-ui+0.55.3.patch +0 -3437
@@ -0,0 +1,239 @@
1
+ import nodeDataChannel from "node-datachannel";
2
+ import { PIPANE_DATA_CHANNEL_LABEL, PIPANE_DATA_CHANNEL_PROTOCOL, TRUST_PROTOCOL_VERSION, decodeDataChannelAuthenticateFrame, deviceConnectionProofPayload, } from "../shared/trust-protocol.js";
3
+ import { deriveDeviceId, verifyConnectionTicket, verifyDeviceSignature } from "../shared/node-trust-crypto.js";
4
+ import { signBackendIdentityBinding, } from "./backend-identity.js";
5
+ import { toNodeIceServers } from "./ice-servers.js";
6
+ export { PIPANE_DATA_CHANNEL_LABEL, PIPANE_DATA_CHANNEL_PROTOCOL } from "../shared/trust-protocol.js";
7
+ const AUTHENTICATION_TIMEOUT_MS = 10_000;
8
+ const MAX_AUTHENTICATION_FRAME_BYTES = 32 * 1024;
9
+ /** Answer-side WebRTC peers that expose channels only after ticket and device proof validation. */
10
+ export class BackendWebRtcManager {
11
+ signaling;
12
+ identity;
13
+ ticketPublicKey;
14
+ authorize;
15
+ iceServers;
16
+ iceTransportPolicy;
17
+ bindAddress;
18
+ authenticationTimeoutMs;
19
+ peers = new Map();
20
+ channelListeners = new Set();
21
+ errorListeners = new Set();
22
+ unsubscribers;
23
+ constructor(options) {
24
+ this.signaling = options.signaling;
25
+ this.identity = options.identity;
26
+ this.ticketPublicKey = options.ticketPublicKey;
27
+ this.authorize = options.authorize;
28
+ this.iceServers = options.iceServers ?? [];
29
+ this.iceTransportPolicy = options.iceTransportPolicy ?? "all";
30
+ this.bindAddress = options.bindAddress;
31
+ this.authenticationTimeoutMs = options.authenticationTimeoutMs ?? AUTHENTICATION_TIMEOUT_MS;
32
+ this.unsubscribers = [
33
+ this.signaling.onConnectionRequest((request) => this.createPeer(request)),
34
+ this.signaling.onSignal((connectionId, signal) => this.applySignal(connectionId, signal)),
35
+ this.signaling.onConnectionClosed((connectionId) => this.removePeer(connectionId)),
36
+ ];
37
+ }
38
+ onDataChannel(listener) {
39
+ this.channelListeners.add(listener);
40
+ return () => this.channelListeners.delete(listener);
41
+ }
42
+ onError(listener) {
43
+ this.errorListeners.add(listener);
44
+ return () => this.errorListeners.delete(listener);
45
+ }
46
+ closeAuthorization(accountId, deviceId) {
47
+ for (const [connectionId, state] of this.peers) {
48
+ if (state.authenticated?.accountId !== accountId)
49
+ continue;
50
+ if (deviceId && state.authenticated.deviceId !== deviceId)
51
+ continue;
52
+ // Rendezvous already removed the route before delivering revocation.
53
+ this.removePeer(connectionId);
54
+ }
55
+ }
56
+ close() {
57
+ for (const unsubscribe of this.unsubscribers.splice(0))
58
+ unsubscribe();
59
+ for (const connectionId of [...this.peers.keys()])
60
+ this.removePeer(connectionId);
61
+ }
62
+ createPeer(request) {
63
+ this.removePeer(request.connectionId);
64
+ let claims;
65
+ try {
66
+ claims = verifyConnectionTicket(request.ticket, this.ticketPublicKey());
67
+ if (claims.connectionId !== request.connectionId || claims.backendId !== this.identity.backendId) {
68
+ throw new Error("Connection ticket does not match this backend route");
69
+ }
70
+ if (deriveDeviceId(claims.devicePublicKey) !== claims.deviceId)
71
+ throw new Error("Connection ticket device identity is invalid");
72
+ }
73
+ catch (error) {
74
+ this.rejectRequest(request.connectionId, error);
75
+ return;
76
+ }
77
+ const peer = new nodeDataChannel.PeerConnection(request.connectionId, {
78
+ iceServers: request.iceServers.length > 0 ? toNodeIceServers(request.iceServers) : this.iceServers,
79
+ iceTransportPolicy: this.iceTransportPolicy,
80
+ bindAddress: this.bindAddress,
81
+ });
82
+ const state = { request, claims, peer };
83
+ this.peers.set(request.connectionId, state);
84
+ peer.onLocalDescription((sdp, type) => {
85
+ if (this.peers.get(request.connectionId) !== state || type !== "answer" || !state.offerSdp)
86
+ return;
87
+ try {
88
+ const binding = signBackendIdentityBinding(this.identity, {
89
+ connectionId: request.connectionId,
90
+ offerSdp: state.offerSdp,
91
+ answerSdp: sdp,
92
+ expiresAt: claims.expiresAt,
93
+ });
94
+ state.binding = binding;
95
+ this.signaling.sendSignal(request.connectionId, { kind: "description", type: "answer", sdp });
96
+ this.signaling.sendIdentityBinding(request.connectionId, binding);
97
+ }
98
+ catch (error) {
99
+ this.failPeer(request.connectionId, error);
100
+ }
101
+ });
102
+ peer.onLocalCandidate((candidate, mid) => {
103
+ if (this.peers.get(request.connectionId) !== state)
104
+ return;
105
+ this.safeSendSignal(request.connectionId, {
106
+ kind: "candidate",
107
+ candidate,
108
+ sdpMid: mid || null,
109
+ sdpMLineIndex: null,
110
+ });
111
+ });
112
+ peer.onDataChannel((channel) => this.handleDataChannel(state, channel));
113
+ peer.onStateChange((connectionState) => {
114
+ if (connectionState === "failed" || connectionState === "closed")
115
+ this.removePeer(request.connectionId);
116
+ });
117
+ }
118
+ handleDataChannel(state, channel) {
119
+ if (channel.getLabel() !== PIPANE_DATA_CHANNEL_LABEL || channel.getProtocol() !== PIPANE_DATA_CHANNEL_PROTOCOL) {
120
+ channel.close();
121
+ return;
122
+ }
123
+ state.authenticationTimer = setTimeout(() => this.failPeer(state.request.connectionId, new Error("DataChannel authentication timed out")), this.authenticationTimeoutMs);
124
+ state.authenticationTimer.unref?.();
125
+ channel.onMessage((message) => {
126
+ if (state.authenticated)
127
+ return;
128
+ const raw = message.toString();
129
+ if (Buffer.byteLength(raw) > MAX_AUTHENTICATION_FRAME_BYTES) {
130
+ this.failAuthentication(state, channel, "Authentication frame is too large");
131
+ return;
132
+ }
133
+ const hello = decodeDataChannelAuthenticateFrame(raw);
134
+ if (!hello) {
135
+ this.failAuthentication(state, channel, "First DataChannel frame must authenticate the device");
136
+ return;
137
+ }
138
+ void this.authenticateChannel(state, channel, hello).catch((error) => {
139
+ if (this.peers.get(state.request.connectionId) !== state)
140
+ return;
141
+ this.failAuthentication(state, channel, error instanceof Error ? error.message : String(error));
142
+ });
143
+ });
144
+ }
145
+ async authenticateChannel(state, channel, hello) {
146
+ if (!state.binding)
147
+ throw new Error("Backend identity binding is not ready");
148
+ if (state.claims.expiresAt <= Date.now())
149
+ throw new Error("Connection ticket expired during negotiation");
150
+ if (hello.ticket !== state.request.ticket || hello.bindingSignature !== state.binding.signature) {
151
+ throw new Error("Device proof does not match the negotiated connection");
152
+ }
153
+ if (!verifyDeviceSignature(state.claims.devicePublicKey, deviceConnectionProofPayload(hello.ticket, hello.bindingSignature), hello.deviceSignature)) {
154
+ throw new Error("Invalid device connection signature");
155
+ }
156
+ const authorization = await this.authorize({ claims: state.claims, pairingSecret: hello.pairingSecret });
157
+ if (this.peers.get(state.request.connectionId) !== state)
158
+ throw new Error("Connection closed during authorization");
159
+ if (authorization.deviceId !== state.claims.deviceId)
160
+ throw new Error("Authorized device does not match the ticket");
161
+ state.authenticated = authorization;
162
+ if (state.authenticationTimer)
163
+ clearTimeout(state.authenticationTimer);
164
+ state.authenticationTimer = undefined;
165
+ const response = {
166
+ protocolVersion: TRUST_PROTOCOL_VERSION,
167
+ type: "authenticated",
168
+ accountId: authorization.accountId,
169
+ deviceId: authorization.deviceId,
170
+ };
171
+ channel.sendMessage(JSON.stringify(response));
172
+ for (const listener of this.channelListeners) {
173
+ listener({ connectionId: state.request.connectionId, channel, ...authorization });
174
+ }
175
+ }
176
+ failAuthentication(state, channel, message) {
177
+ const response = {
178
+ protocolVersion: TRUST_PROTOCOL_VERSION,
179
+ type: "authentication_error",
180
+ message,
181
+ };
182
+ channel.sendMessage(JSON.stringify(response));
183
+ this.failPeer(state.request.connectionId, new Error(message));
184
+ }
185
+ applySignal(connectionId, signal) {
186
+ const state = this.peers.get(connectionId);
187
+ if (!state) {
188
+ this.signaling.closeConnection(connectionId, "Unknown backend WebRTC connection");
189
+ return;
190
+ }
191
+ try {
192
+ if (signal.kind === "candidate") {
193
+ state.peer.addRemoteCandidate(signal.candidate, signal.sdpMid ?? "0");
194
+ return;
195
+ }
196
+ if (signal.type !== "offer")
197
+ throw new Error("Backend WebRTC peer expected an offer");
198
+ state.offerSdp = signal.sdp;
199
+ state.peer.setRemoteDescription(signal.sdp, "offer");
200
+ }
201
+ catch (error) {
202
+ this.failPeer(connectionId, error);
203
+ }
204
+ }
205
+ safeSendSignal(connectionId, signal) {
206
+ try {
207
+ this.signaling.sendSignal(connectionId, signal);
208
+ }
209
+ catch (error) {
210
+ this.failPeer(connectionId, error);
211
+ }
212
+ }
213
+ rejectRequest(connectionId, error) {
214
+ const normalized = error instanceof Error ? error : new Error(String(error));
215
+ for (const listener of this.errorListeners)
216
+ listener(connectionId, normalized);
217
+ this.signaling.closeConnection(connectionId, normalized.message);
218
+ }
219
+ failPeer(connectionId, error) {
220
+ const normalized = error instanceof Error ? error : new Error(String(error));
221
+ for (const listener of this.errorListeners)
222
+ listener(connectionId, normalized);
223
+ try {
224
+ this.signaling.closeConnection(connectionId, "WebRTC negotiation failed");
225
+ }
226
+ finally {
227
+ this.removePeer(connectionId);
228
+ }
229
+ }
230
+ removePeer(connectionId) {
231
+ const state = this.peers.get(connectionId);
232
+ if (!state)
233
+ return;
234
+ this.peers.delete(connectionId);
235
+ if (state.authenticationTimer)
236
+ clearTimeout(state.authenticationTimer);
237
+ state.peer.close();
238
+ }
239
+ }
@@ -0,0 +1,6 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ /** Absolute entrypoint for the bundled common-subscription usage extension. */
4
+ export function resolveUsageExtensionPath() {
5
+ return require.resolve("@sreetej510/pi-usage/dist/index.js");
6
+ }
@@ -0,0 +1,97 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { parseSessionEntries } from "@earendil-works/pi-coding-agent";
5
+ import { buildSessionDisplayMessages } from "./session-jsonl.js";
6
+ const PATH_CONTINUATION = /[\p{L}\p{N}_~.%/\\:+-]/u;
7
+ function expandHome(value) {
8
+ return value.replace(/^~/, process.env.HOME || "/");
9
+ }
10
+ function addCandidate(candidates, value) {
11
+ if (!value || value === ".")
12
+ return;
13
+ candidates.add(value);
14
+ try {
15
+ candidates.add(encodeURI(value)
16
+ .replaceAll("(", "%28")
17
+ .replaceAll(")", "%29"));
18
+ }
19
+ catch {
20
+ // Invalid surrogate pairs cannot be represented in a URL, but the raw
21
+ // spelling can still be checked without weakening the exact match.
22
+ }
23
+ }
24
+ function buildMentionCandidates(rawRequestPath, requestedPath, resolvedPath, sessionCwd) {
25
+ const candidates = new Set();
26
+ addCandidate(candidates, rawRequestPath);
27
+ const cwd = path.resolve(expandHome(sessionCwd));
28
+ for (const candidatePath of new Set([path.resolve(requestedPath), path.resolve(resolvedPath)])) {
29
+ addCandidate(candidates, candidatePath);
30
+ addCandidate(candidates, pathToFileURL(candidatePath).href);
31
+ const relative = path.relative(cwd, candidatePath);
32
+ if (relative && !path.isAbsolute(relative)) {
33
+ addCandidate(candidates, relative);
34
+ if (!relative.startsWith(`..${path.sep}`) && relative !== "..") {
35
+ addCandidate(candidates, `.${path.sep}${relative}`);
36
+ }
37
+ }
38
+ }
39
+ return candidates;
40
+ }
41
+ function containsExactPathMention(text, candidate) {
42
+ let index = text.indexOf(candidate);
43
+ while (index >= 0) {
44
+ const before = index > 0 ? text[index - 1] : "";
45
+ const end = index + candidate.length;
46
+ const after = end < text.length ? text[end] : "";
47
+ if ((!before || !PATH_CONTINUATION.test(before))
48
+ && (!after || !PATH_CONTINUATION.test(after))) {
49
+ return true;
50
+ }
51
+ index = text.indexOf(candidate, index + 1);
52
+ }
53
+ return false;
54
+ }
55
+ function conversationText(message) {
56
+ if (message?.role === "compactionSummary" && typeof message.summary === "string") {
57
+ return [message.summary];
58
+ }
59
+ if (message?.role !== "user" && message?.role !== "assistant")
60
+ return [];
61
+ if (typeof message.content === "string")
62
+ return [message.content];
63
+ if (!Array.isArray(message.content))
64
+ return [];
65
+ const text = [];
66
+ for (const chunk of message.content) {
67
+ if (chunk?.type === "text" && typeof chunk.text === "string")
68
+ text.push(chunk.text);
69
+ if (chunk?.type === "thinking" && typeof chunk.thinking === "string")
70
+ text.push(chunk.thinking);
71
+ }
72
+ return text;
73
+ }
74
+ /**
75
+ * Allow an exact path outside the session CWD only when its spelling (absolute,
76
+ * file URL, or CWD-relative) appears in the currently displayed conversation.
77
+ */
78
+ export function conversationMentionsFile(options) {
79
+ try {
80
+ const entries = parseSessionEntries(readFileSync(options.sessionPath, "utf8"));
81
+ const messages = buildSessionDisplayMessages(entries);
82
+ const candidates = buildMentionCandidates(options.rawRequestPath, options.requestedPath, options.resolvedPath, options.sessionCwd);
83
+ for (const message of messages) {
84
+ for (const text of conversationText(message)) {
85
+ for (const candidate of candidates) {
86
+ if (containsExactPathMention(text, candidate))
87
+ return true;
88
+ }
89
+ }
90
+ }
91
+ }
92
+ catch {
93
+ // Authorization failures are deny-by-default. The endpoint reports the
94
+ // same 403 as an ordinary unmentioned CWD escape.
95
+ }
96
+ return false;
97
+ }
@@ -0,0 +1,31 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ export const PROVIDER_USAGE_STATUS_KEY = "provider-usage";
3
+ const MAX_STATUS_KEY_LENGTH = 128;
4
+ const MAX_STATUS_TEXT_LENGTH = 512;
5
+ const KEY_CONTROL_CHARACTER = /[\u0000-\u001f\u007f-\u009f]/;
6
+ const CONTROL_CHARACTERS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
7
+ export function isValidExtensionStatusKey(value) {
8
+ return typeof value === "string"
9
+ && value.length > 0
10
+ && value.length <= MAX_STATUS_KEY_LENGTH
11
+ && !KEY_CONTROL_CHARACTER.test(value);
12
+ }
13
+ export function normalizeExtensionStatusText(value) {
14
+ return stripVTControlCharacters(value)
15
+ .replace(/[\r\n\t]+/g, " ")
16
+ .replace(CONTROL_CHARACTERS, "")
17
+ .replace(/\s{2,}/g, " ")
18
+ .trim()
19
+ .slice(0, MAX_STATUS_TEXT_LENGTH);
20
+ }
21
+ export function extensionStatusSnapshot(statuses) {
22
+ return Object.fromEntries(statuses ?? []);
23
+ }
24
+ /** Identify successful pi-usage status text; transient states such as "checking" do not match. */
25
+ export function providerForUsageStatus(text) {
26
+ if (/^codex(?:\s|$)/i.test(text))
27
+ return "codex";
28
+ if (/^(?:anthropic|claude)(?:\s|$)/i.test(text))
29
+ return "anthropic";
30
+ return undefined;
31
+ }
@@ -0,0 +1,48 @@
1
+ export const FRAME_CONNECTION_OPEN = 1;
2
+ export const FRAME_CONNECTION_CLOSED = 3;
3
+ /** Adapts an authenticated reliable DataChannel to the server's frame connection contract. */
4
+ export class DataChannelFrameConnection {
5
+ channel;
6
+ messageListeners = new Set();
7
+ closeListeners = new Set();
8
+ state = FRAME_CONNECTION_OPEN;
9
+ constructor(channel) {
10
+ this.channel = channel;
11
+ channel.onMessage((message) => {
12
+ for (const listener of this.messageListeners)
13
+ listener(message);
14
+ });
15
+ channel.onClosed(() => {
16
+ if (this.state === FRAME_CONNECTION_CLOSED)
17
+ return;
18
+ this.state = FRAME_CONNECTION_CLOSED;
19
+ for (const listener of this.closeListeners)
20
+ listener();
21
+ });
22
+ }
23
+ get readyState() {
24
+ return this.state === FRAME_CONNECTION_OPEN && this.channel.isOpen()
25
+ ? FRAME_CONNECTION_OPEN
26
+ : FRAME_CONNECTION_CLOSED;
27
+ }
28
+ send(frame) {
29
+ if (this.readyState !== FRAME_CONNECTION_OPEN || !this.channel.sendMessage(frame)) {
30
+ throw new Error("DataChannel frame connection is closed");
31
+ }
32
+ }
33
+ close() {
34
+ if (this.state === FRAME_CONNECTION_CLOSED)
35
+ return;
36
+ this.state = FRAME_CONNECTION_CLOSED;
37
+ this.channel.close();
38
+ for (const listener of this.closeListeners)
39
+ listener();
40
+ }
41
+ on(event, listener) {
42
+ if (event === "message")
43
+ this.messageListeners.add(listener);
44
+ else
45
+ this.closeListeners.add(listener);
46
+ return this;
47
+ }
48
+ }
@@ -0,0 +1,45 @@
1
+ import { isBackendProtocolFrame } from "../shared/backend-protocol.js";
2
+ class RoutedFrameConnection {
3
+ connection;
4
+ messageListeners = new Set();
5
+ closeListeners = new Set();
6
+ constructor(connection) {
7
+ this.connection = connection;
8
+ }
9
+ get readyState() { return this.connection.readyState; }
10
+ send(frame) { return this.connection.send(frame); }
11
+ close(code, reason) { return this.connection.close(code, reason); }
12
+ on(event, listener) {
13
+ if (event === "message")
14
+ this.messageListeners.add(listener);
15
+ else
16
+ this.closeListeners.add(listener);
17
+ return this;
18
+ }
19
+ emitMessage(raw) {
20
+ const frame = { toString: () => raw };
21
+ for (const listener of this.messageListeners)
22
+ listener(frame);
23
+ }
24
+ emitClose() {
25
+ for (const listener of this.closeListeners)
26
+ listener();
27
+ }
28
+ }
29
+ /** Splits application v1 and semantic v2 frames without creating extra DataChannels. */
30
+ export function routeFrameConnection(connection) {
31
+ const application = new RoutedFrameConnection(connection);
32
+ const semantic = new RoutedFrameConnection(connection);
33
+ connection.on("message", (frame) => {
34
+ const raw = frame.toString();
35
+ if (isBackendProtocolFrame(raw))
36
+ semantic.emitMessage(raw);
37
+ else
38
+ application.emitMessage(raw);
39
+ });
40
+ connection.on("close", () => {
41
+ application.emitClose();
42
+ semantic.emitClose();
43
+ });
44
+ return { application, semantic };
45
+ }
@@ -0,0 +1,24 @@
1
+ /** Converts browser ICE server records into node-datachannel's explicit relay shape. */
2
+ export function toNodeIceServers(servers) {
3
+ const result = [];
4
+ for (const server of servers) {
5
+ for (const url of Array.isArray(server.urls) ? server.urls : [server.urls]) {
6
+ if (url.startsWith("stun:")) {
7
+ result.push(url);
8
+ continue;
9
+ }
10
+ const match = /^(turns?):(?:\/\/)?(\[[^\]]+\]|[^:?]+)(?::(\d+))?(?:\?transport=(udp|tcp))?$/iu.exec(url);
11
+ if (!match)
12
+ throw new Error(`Unsupported ICE server URL: ${url}`);
13
+ const secure = match[1].toLowerCase() === "turns";
14
+ result.push({
15
+ hostname: match[2].replace(/^\[|\]$/gu, ""),
16
+ port: match[3] ? Number.parseInt(match[3], 10) : secure ? 5349 : 3478,
17
+ username: server.username,
18
+ password: server.credential,
19
+ relayType: secure ? "TurnTls" : match[4]?.toLowerCase() === "tcp" ? "TurnTcp" : "TurnUdp",
20
+ });
21
+ }
22
+ }
23
+ return result;
24
+ }