pipane 0.1.6 → 0.1.8

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 (58) hide show
  1. package/README.md +14 -3
  2. package/bin/pipane-rendezvous.js +21 -0
  3. package/bin/pipane.js +21 -1
  4. package/dist/build-info.json +5 -0
  5. package/dist/client/assets/__vite-browser-external-DtLbhLGx.js +1 -0
  6. package/dist/client/assets/app-runtime-Rw-O-AwW.js +1 -0
  7. package/dist/client/assets/backend-landing-page-B7SbyIXQ.js +1 -0
  8. package/dist/client/assets/index-C7_1ODks.js +2 -0
  9. package/dist/client/assets/index-DgI_gkjg.css +1 -0
  10. package/dist/client/assets/main-DEfSB8wO.js +2822 -0
  11. package/dist/client/assets/pairing-page-C0YByC2D.js +1 -0
  12. package/dist/client/assets/remote-backend-manager-DCSdS38m.js +1 -0
  13. package/dist/client/assets/rendezvous-trust-api-C2Zob5i4.js +4 -0
  14. package/dist/client/assets/webrtc-frame-transport-DGX5EO_A.js +1 -0
  15. package/dist/client/assets/ws-agent-adapter-DpkkZGPl.js +6 -0
  16. package/dist/client/index.html +2 -2
  17. package/dist/server/rendezvous/rendezvous-hub.js +426 -0
  18. package/dist/server/rendezvous/server.js +247 -0
  19. package/dist/server/rendezvous/trust-store.js +432 -0
  20. package/dist/server/server/auth-guard.js +61 -0
  21. package/dist/server/server/backend-connection-authorizer.js +29 -0
  22. package/dist/server/server/backend-identity.js +167 -0
  23. package/dist/server/server/backend-protocol-handler.js +132 -0
  24. package/dist/server/server/backend-trust-store.js +157 -0
  25. package/dist/server/server/backend-webrtc.js +245 -0
  26. package/dist/server/server/build-info.js +13 -0
  27. package/dist/server/server/conversation-file-access.js +97 -0
  28. package/dist/server/server/frame-connection.js +106 -0
  29. package/dist/server/server/frame-router.js +45 -0
  30. package/dist/server/server/ice-servers.js +24 -0
  31. package/dist/server/server/local-backend-api.js +389 -0
  32. package/dist/server/server/local-settings.js +17 -4
  33. package/dist/server/server/pi-rpc-protocol.js +407 -0
  34. package/dist/server/server/process-pool.js +27 -24
  35. package/dist/server/server/rendezvous-client.js +282 -0
  36. package/dist/server/server/rest-api.js +133 -176
  37. package/dist/server/server/server.js +167 -97
  38. package/dist/server/server/session-index.js +105 -28
  39. package/dist/server/server/session-jsonl.js +82 -5
  40. package/dist/server/server/session-path.js +145 -0
  41. package/dist/server/server/update-api.js +28 -0
  42. package/dist/server/server/update-check.js +33 -13
  43. package/dist/server/server/update-manager.js +233 -0
  44. package/dist/server/server/worktree-name.js +116 -31
  45. package/dist/server/server/ws-handler.js +267 -186
  46. package/dist/server/shared/backend-api.js +1 -0
  47. package/dist/server/shared/backend-protocol.js +164 -0
  48. package/dist/server/shared/data-channel-framing.js +137 -0
  49. package/dist/server/shared/node-trust-crypto.js +61 -0
  50. package/dist/server/shared/rendezvous-protocol.js +243 -0
  51. package/dist/server/shared/tool-runtime.js +1 -0
  52. package/dist/server/shared/trust-protocol.js +97 -0
  53. package/dist/server/shared/updates.js +4 -0
  54. package/dist/server/shared/ws-protocol.js +473 -1
  55. package/docs/protocol.md +129 -0
  56. package/package.json +21 -8
  57. package/dist/client/assets/index-Dl_wdLZH.css +0 -1
  58. package/dist/client/assets/index-hNqbnG06.js +0 -2482
@@ -0,0 +1,97 @@
1
+ export const TRUST_PROTOCOL_VERSION = 1;
2
+ export const CONNECTION_TICKET_VERSION = 1;
3
+ export const PIPANE_DATA_CHANNEL_LABEL = "pipane";
4
+ export const PIPANE_DATA_CHANNEL_PROTOCOL = "pipane.v1";
5
+ export function deviceChallengePayload(challenge) {
6
+ return [
7
+ `pipane-device-challenge-v${TRUST_PROTOCOL_VERSION}`,
8
+ challenge.challengeId,
9
+ challenge.nonce,
10
+ challenge.purpose,
11
+ challenge.deviceId,
12
+ challenge.devicePublicKey,
13
+ challenge.backendId ?? "",
14
+ challenge.connectionId ?? "",
15
+ challenge.pairId ?? "",
16
+ challenge.targetDeviceId ?? "",
17
+ String(challenge.expiresAt),
18
+ ].join("\n");
19
+ }
20
+ export function connectionTicketSignaturePayload(encodedClaims) {
21
+ return `pipane-connection-ticket-v${CONNECTION_TICKET_VERSION}\n${encodedClaims}`;
22
+ }
23
+ export function backendIdentityBindingPayload(binding) {
24
+ return [
25
+ `pipane-backend-binding-v${TRUST_PROTOCOL_VERSION}`,
26
+ binding.backendId,
27
+ binding.publicKey,
28
+ binding.connectionId,
29
+ binding.offerSha256,
30
+ binding.answerSha256,
31
+ binding.dtlsFingerprint,
32
+ String(binding.expiresAt),
33
+ ].join("\n");
34
+ }
35
+ export function deviceConnectionProofPayload(ticket, bindingSignature) {
36
+ return [
37
+ `pipane-device-connection-v${TRUST_PROTOCOL_VERSION}`,
38
+ ticket,
39
+ bindingSignature,
40
+ ].join("\n");
41
+ }
42
+ export function decodeDataChannelAuthenticateFrame(raw) {
43
+ let value;
44
+ try {
45
+ value = JSON.parse(raw);
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ if (!isRecord(value)
51
+ || value.protocolVersion !== TRUST_PROTOCOL_VERSION
52
+ || value.type !== "authenticate"
53
+ || !isNonEmptyString(value.ticket)
54
+ || !isNonEmptyString(value.bindingSignature)
55
+ || !isNonEmptyString(value.deviceSignature)
56
+ || !isOptionalString(value.pairingSecret)) {
57
+ return undefined;
58
+ }
59
+ return value;
60
+ }
61
+ export function parseConnectionTicketClaims(value) {
62
+ if (!isRecord(value)
63
+ || value.version !== CONNECTION_TICKET_VERSION
64
+ || (value.kind !== "pairing" && value.kind !== "connection")
65
+ || !isNonEmptyString(value.ticketId)
66
+ || !isNonEmptyString(value.backendId)
67
+ || !isNonEmptyString(value.connectionId)
68
+ || !isNonEmptyString(value.deviceId)
69
+ || !isNonEmptyString(value.devicePublicKey)
70
+ || !isOptionalString(value.accountId)
71
+ || !isOptionalString(value.pairId)
72
+ || !isNonNegativeInteger(value.issuedAt)
73
+ || !isPositiveInteger(value.expiresAt)
74
+ || value.expiresAt <= value.issuedAt) {
75
+ return undefined;
76
+ }
77
+ if (value.kind === "pairing" && !isNonEmptyString(value.pairId))
78
+ return undefined;
79
+ if (value.kind === "connection" && !isNonEmptyString(value.accountId))
80
+ return undefined;
81
+ return value;
82
+ }
83
+ function isRecord(value) {
84
+ return !!value && typeof value === "object" && !Array.isArray(value);
85
+ }
86
+ function isNonEmptyString(value) {
87
+ return typeof value === "string" && value.length > 0;
88
+ }
89
+ function isOptionalString(value) {
90
+ return value === undefined || isNonEmptyString(value);
91
+ }
92
+ function isNonNegativeInteger(value) {
93
+ return Number.isSafeInteger(value) && value >= 0;
94
+ }
95
+ function isPositiveInteger(value) {
96
+ return Number.isSafeInteger(value) && value > 0;
97
+ }
@@ -0,0 +1,4 @@
1
+ export const UPDATE_TARGETS = ["pipane", "pi", "extensions"];
2
+ export function isUpdateTarget(value) {
3
+ return typeof value === "string" && UPDATE_TARGETS.includes(value);
4
+ }
@@ -1 +1,473 @@
1
- export {};
1
+ /** Increment when a breaking WebSocket wire change is introduced. */
2
+ export const WS_PROTOCOL_VERSION = 1;
3
+ class ValidationFailure extends Error {
4
+ code;
5
+ constructor(code, path, message) {
6
+ super(`${path}: ${message}`);
7
+ this.code = code;
8
+ }
9
+ }
10
+ function fail(path, message, code = "invalid_message") {
11
+ throw new ValidationFailure(code, path, message);
12
+ }
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function record(value, path) {
17
+ if (!isRecord(value))
18
+ fail(path, "expected an object");
19
+ return value;
20
+ }
21
+ function string(value, path, allowEmpty = true) {
22
+ if (typeof value !== "string" || (!allowEmpty && value.length === 0)) {
23
+ fail(path, allowEmpty ? "expected a string" : "expected a non-empty string");
24
+ }
25
+ return value;
26
+ }
27
+ function optionalString(value, path) {
28
+ return value === undefined ? undefined : string(value, path);
29
+ }
30
+ function boolean(value, path) {
31
+ if (typeof value !== "boolean")
32
+ fail(path, "expected a boolean");
33
+ return value;
34
+ }
35
+ function integer(value, path) {
36
+ if (!Number.isSafeInteger(value) || value < 0) {
37
+ fail(path, "expected a non-negative safe integer");
38
+ }
39
+ return value;
40
+ }
41
+ function finiteNumber(value, path) {
42
+ if (typeof value !== "number" || !Number.isFinite(value))
43
+ fail(path, "expected a finite number");
44
+ return value;
45
+ }
46
+ function array(value, path) {
47
+ if (!Array.isArray(value))
48
+ fail(path, "expected an array");
49
+ return value;
50
+ }
51
+ function stringArray(value, path) {
52
+ return array(value, path).map((item, index) => string(item, `${path}[${index}]`));
53
+ }
54
+ function stringRecord(value, path) {
55
+ const source = record(value, path);
56
+ for (const [key, item] of Object.entries(source))
57
+ string(item, `${path}.${key}`);
58
+ return source;
59
+ }
60
+ function statusRecord(value, path) {
61
+ const source = record(value, path);
62
+ for (const [key, item] of Object.entries(source)) {
63
+ if (item !== "running" && item !== "done")
64
+ fail(`${path}.${key}`, "expected 'running' or 'done'");
65
+ }
66
+ return source;
67
+ }
68
+ function queuesRecord(value, path) {
69
+ const source = record(value, path);
70
+ for (const [key, item] of Object.entries(source))
71
+ stringArray(item, `${path}.${key}`);
72
+ return source;
73
+ }
74
+ function thinkingLevel(value, path) {
75
+ if (!["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(value))) {
76
+ fail(path, "expected a supported thinking level");
77
+ }
78
+ return value;
79
+ }
80
+ function compactModel(value, path) {
81
+ const model = record(value, path);
82
+ string(model.provider, `${path}.provider`, false);
83
+ string(model.modelId, `${path}.modelId`, false);
84
+ return model;
85
+ }
86
+ function wireModel(value, path) {
87
+ const model = record(value, path);
88
+ string(model.provider, `${path}.provider`, false);
89
+ const id = optionalString(model.id, `${path}.id`);
90
+ const modelId = optionalString(model.modelId, `${path}.modelId`);
91
+ if (!id && !modelId)
92
+ fail(path, "expected id or modelId");
93
+ return model;
94
+ }
95
+ function images(value, path) {
96
+ return array(value, path).map((item, index) => {
97
+ const image = record(item, `${path}[${index}]`);
98
+ if (image.type !== "image")
99
+ fail(`${path}[${index}].type`, "expected 'image'");
100
+ string(image.data, `${path}[${index}].data`);
101
+ string(image.mimeType, `${path}[${index}].mimeType`, false);
102
+ return image;
103
+ });
104
+ }
105
+ function optionalCommandFields(command, path) {
106
+ if (command.thinkingLevel !== undefined)
107
+ thinkingLevel(command.thinkingLevel, `${path}.thinkingLevel`);
108
+ if (command.controlRevision !== undefined)
109
+ integer(command.controlRevision, `${path}.controlRevision`);
110
+ if (command.images !== undefined)
111
+ images(command.images, `${path}.images`);
112
+ }
113
+ function validateVersion(message, path) {
114
+ if (message.protocolVersion !== WS_PROTOCOL_VERSION) {
115
+ const actual = message.protocolVersion === undefined ? "missing" : JSON.stringify(message.protocolVersion);
116
+ fail(`${path}.protocolVersion`, `unsupported protocol version ${actual}; expected ${WS_PROTOCOL_VERSION}`, "unsupported_version");
117
+ }
118
+ }
119
+ function decodeJson(raw) {
120
+ try {
121
+ return { ok: true, value: JSON.parse(raw) };
122
+ }
123
+ catch (error) {
124
+ return {
125
+ ok: false,
126
+ error: {
127
+ code: "invalid_json",
128
+ message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
129
+ requestId: null,
130
+ command: "parse",
131
+ },
132
+ };
133
+ }
134
+ }
135
+ function decodeFailure(value, failure, fallbackCommand) {
136
+ const source = isRecord(value) ? value : undefined;
137
+ const requestId = typeof source?.id === "string" ? source.id : null;
138
+ const command = typeof source?.type === "string" ? source.type : fallbackCommand;
139
+ const validation = failure instanceof ValidationFailure
140
+ ? failure
141
+ : new ValidationFailure("invalid_message", "$", failure instanceof Error ? failure.message : String(failure));
142
+ return {
143
+ ok: false,
144
+ error: {
145
+ code: validation.code,
146
+ message: validation.message,
147
+ requestId,
148
+ command,
149
+ },
150
+ };
151
+ }
152
+ export function decodeClientCommand(raw) {
153
+ const parsed = decodeJson(raw);
154
+ if (!parsed.ok)
155
+ return parsed;
156
+ const value = parsed.value;
157
+ try {
158
+ const command = record(value, "$command");
159
+ validateVersion(command, "$command");
160
+ string(command.id, "$command.id", false);
161
+ const type = string(command.type, "$command.type", false);
162
+ const sessionPath = () => string(command.sessionPath, "$command.sessionPath");
163
+ switch (type) {
164
+ case "install_pi":
165
+ case "get_available_models":
166
+ case "get_default_model":
167
+ case "get_session_statuses":
168
+ case "reload_processes":
169
+ break;
170
+ case "get_commands":
171
+ optionalString(command.sessionPath, "$command.sessionPath");
172
+ optionalString(command.cwd, "$command.cwd");
173
+ break;
174
+ case "subscribe_session":
175
+ case "abort":
176
+ case "hard_kill":
177
+ case "get_session_stats":
178
+ sessionPath();
179
+ break;
180
+ case "prompt":
181
+ sessionPath();
182
+ string(command.message, "$command.message");
183
+ optionalString(command.cwd, "$command.cwd");
184
+ compactModel(command.model, "$command.model");
185
+ optionalCommandFields(command, "$command");
186
+ break;
187
+ case "steer":
188
+ sessionPath();
189
+ string(command.message, "$command.message");
190
+ break;
191
+ case "remove_steering":
192
+ sessionPath();
193
+ integer(command.index, "$command.index");
194
+ break;
195
+ case "compact":
196
+ sessionPath();
197
+ optionalString(command.customInstructions, "$command.customInstructions");
198
+ break;
199
+ case "fork":
200
+ sessionPath();
201
+ string(command.entryId, "$command.entryId", false);
202
+ break;
203
+ case "fork_prompt":
204
+ sessionPath();
205
+ string(command.message, "$command.message");
206
+ compactModel(command.model, "$command.model");
207
+ optionalCommandFields(command, "$command");
208
+ break;
209
+ case "set_session_name":
210
+ sessionPath();
211
+ string(command.name, "$command.name");
212
+ break;
213
+ default:
214
+ fail("$command.type", `unknown command '${type}'`, "unknown_command");
215
+ }
216
+ return { ok: true, value: command };
217
+ }
218
+ catch (error) {
219
+ return decodeFailure(value, error, "unknown");
220
+ }
221
+ }
222
+ function validateSlashCommands(value, path) {
223
+ array(value, path).forEach((item, index) => {
224
+ const command = record(item, `${path}[${index}]`);
225
+ string(command.name, `${path}[${index}].name`, false);
226
+ optionalString(command.description, `${path}[${index}].description`);
227
+ if (!["extension", "prompt", "skill"].includes(String(command.source))) {
228
+ fail(`${path}[${index}].source`, "expected extension, prompt, or skill");
229
+ }
230
+ if (command.sourceInfo !== undefined)
231
+ record(command.sourceInfo, `${path}[${index}].sourceInfo`);
232
+ optionalString(command.location, `${path}[${index}].location`);
233
+ optionalString(command.path, `${path}[${index}].path`);
234
+ });
235
+ }
236
+ function validateSessionStats(value, path) {
237
+ const stats = record(value, path);
238
+ string(stats.sessionFile, `${path}.sessionFile`, false);
239
+ string(stats.sessionId, `${path}.sessionId`, false);
240
+ for (const field of ["userMessages", "assistantMessages", "toolCalls", "toolResults", "totalMessages"]) {
241
+ integer(stats[field], `${path}.${field}`);
242
+ }
243
+ const tokens = record(stats.tokens, `${path}.tokens`);
244
+ for (const field of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
245
+ integer(tokens[field], `${path}.tokens.${field}`);
246
+ }
247
+ if (finiteNumber(stats.cost, `${path}.cost`) < 0)
248
+ fail(`${path}.cost`, "expected a non-negative number");
249
+ if (stats.contextUsage !== undefined) {
250
+ const context = record(stats.contextUsage, `${path}.contextUsage`);
251
+ if (context.tokens !== null)
252
+ integer(context.tokens, `${path}.contextUsage.tokens`);
253
+ integer(context.contextWindow, `${path}.contextUsage.contextWindow`);
254
+ if (context.percent !== null && finiteNumber(context.percent, `${path}.contextUsage.percent`) < 0) {
255
+ fail(`${path}.contextUsage.percent`, "expected a non-negative number or null");
256
+ }
257
+ }
258
+ }
259
+ function validateSuccessData(command, value, path) {
260
+ const data = record(value, path);
261
+ switch (command) {
262
+ case "install_pi":
263
+ case "subscribe_session":
264
+ case "steer":
265
+ case "remove_steering":
266
+ case "abort":
267
+ case "set_session_name":
268
+ break;
269
+ case "prompt":
270
+ case "fork_prompt":
271
+ string(data.newSessionPath, `${path}.newSessionPath`, false);
272
+ break;
273
+ case "hard_kill":
274
+ boolean(data.killed, `${path}.killed`);
275
+ if (data.reason !== undefined && data.reason !== "signal_failed" && data.reason !== "not_attached") {
276
+ fail(`${path}.reason`, "expected signal_failed or not_attached");
277
+ }
278
+ break;
279
+ case "compact":
280
+ break;
281
+ case "get_available_models":
282
+ array(data.models, `${path}.models`).forEach((model, index) => wireModel(model, `${path}.models[${index}]`));
283
+ break;
284
+ case "get_default_model":
285
+ if (data.model !== null)
286
+ wireModel(data.model, `${path}.model`);
287
+ thinkingLevel(data.thinkingLevel, `${path}.thinkingLevel`);
288
+ break;
289
+ case "get_session_statuses":
290
+ statusRecord(data.statuses, `${path}.statuses`);
291
+ break;
292
+ case "get_session_stats":
293
+ validateSessionStats(data, path);
294
+ break;
295
+ case "fork":
296
+ string(data.text, `${path}.text`);
297
+ boolean(data.cancelled, `${path}.cancelled`);
298
+ if (data.newSessionPath !== null)
299
+ string(data.newSessionPath, `${path}.newSessionPath`, false);
300
+ break;
301
+ case "get_commands":
302
+ validateSlashCommands(data.commands, `${path}.commands`);
303
+ break;
304
+ case "reload_processes":
305
+ integer(data.killed, `${path}.killed`);
306
+ integer(data.draining, `${path}.draining`);
307
+ break;
308
+ }
309
+ }
310
+ const CLIENT_COMMAND_TYPES = new Set([
311
+ "install_pi",
312
+ "subscribe_session",
313
+ "prompt",
314
+ "steer",
315
+ "remove_steering",
316
+ "abort",
317
+ "hard_kill",
318
+ "compact",
319
+ "get_available_models",
320
+ "get_default_model",
321
+ "get_session_statuses",
322
+ "get_session_stats",
323
+ "fork",
324
+ "fork_prompt",
325
+ "set_session_name",
326
+ "get_commands",
327
+ "reload_processes",
328
+ ]);
329
+ function clientCommandType(value, path) {
330
+ const type = string(value, path, false);
331
+ if (!CLIENT_COMMAND_TYPES.has(type))
332
+ fail(path, `unknown response command '${type}'`, "unknown_message");
333
+ return type;
334
+ }
335
+ function validatePatches(value, path) {
336
+ array(value, path).forEach((item, index) => {
337
+ const patch = record(item, `${path}[${index}]`);
338
+ integer(patch.offset, `${path}[${index}].offset`);
339
+ integer(patch.deleteCount, `${path}[${index}].deleteCount`);
340
+ string(patch.insert, `${path}[${index}].insert`);
341
+ });
342
+ }
343
+ export function decodeServerMessage(raw) {
344
+ const parsed = decodeJson(raw);
345
+ if (!parsed.ok)
346
+ return parsed;
347
+ const value = parsed.value;
348
+ try {
349
+ const message = record(value, "$message");
350
+ validateVersion(message, "$message");
351
+ const type = string(message.type, "$message.type", false);
352
+ switch (type) {
353
+ case "response": {
354
+ const success = boolean(message.success, "$message.success");
355
+ if (success) {
356
+ string(message.id, "$message.id", false);
357
+ const command = clientCommandType(message.command, "$message.command");
358
+ validateSuccessData(command, message.data, "$message.data");
359
+ }
360
+ else {
361
+ if (message.id !== null)
362
+ string(message.id, "$message.id", false);
363
+ string(message.command, "$message.command", false);
364
+ string(message.error, "$message.error", false);
365
+ if (!["invalid_json", "invalid_message", "unsupported_version", "unknown_command", "unknown_message", "command_failed"].includes(String(message.code))) {
366
+ fail("$message.code", "expected a protocol error code");
367
+ }
368
+ }
369
+ break;
370
+ }
371
+ case "init":
372
+ statusRecord(message.sessionStatuses, "$message.sessionStatuses");
373
+ queuesRecord(message.steeringQueues, "$message.steeringQueues");
374
+ stringRecord(message.providerUsageStatuses, "$message.providerUsageStatuses");
375
+ break;
376
+ case "pi_install_required":
377
+ string(message.command, "$message.command", false);
378
+ boolean(message.installable, "$message.installable");
379
+ boolean(message.installing, "$message.installing");
380
+ string(message.message, "$message.message", false);
381
+ break;
382
+ case "session_status_change":
383
+ string(message.sessionPath, "$message.sessionPath", false);
384
+ if (message.status !== "running" && message.status !== "done")
385
+ fail("$message.status", "expected running or done");
386
+ break;
387
+ case "provider_usage":
388
+ stringRecord(message.statuses, "$message.statuses");
389
+ break;
390
+ case "extension_status":
391
+ string(message.sessionPath, "$message.sessionPath", false);
392
+ stringRecord(message.statuses, "$message.statuses");
393
+ break;
394
+ case "session_sync":
395
+ string(message.sessionPath, "$message.sessionPath", false);
396
+ integer(message.revision, "$message.revision");
397
+ string(message.hash, "$message.hash", false);
398
+ if (message.op === "full") {
399
+ string(message.data, "$message.data");
400
+ }
401
+ else if (message.op === "delta") {
402
+ string(message.baseHash, "$message.baseHash", false);
403
+ validatePatches(message.patches, "$message.patches");
404
+ }
405
+ else {
406
+ fail("$message.op", "expected full or delta");
407
+ }
408
+ break;
409
+ case "control_state":
410
+ string(message.sessionPath, "$message.sessionPath", false);
411
+ if (message.controlRevision !== undefined)
412
+ integer(message.controlRevision, "$message.controlRevision");
413
+ wireModel(message.model, "$message.model");
414
+ thinkingLevel(message.thinkingLevel, "$message.thinkingLevel");
415
+ break;
416
+ case "session_attached":
417
+ string(message.sessionPath, "$message.sessionPath", false);
418
+ optionalString(message.cwd, "$message.cwd");
419
+ optionalString(message.firstMessage, "$message.firstMessage");
420
+ break;
421
+ case "sessions_changed":
422
+ string(message.file, "$message.file");
423
+ break;
424
+ default:
425
+ fail("$message.type", `unknown server message '${type}'`, "unknown_message");
426
+ }
427
+ return { ok: true, value: message };
428
+ }
429
+ catch (error) {
430
+ return decodeFailure(value, error, "unknown");
431
+ }
432
+ }
433
+ export function decodeSessionStateJson(raw) {
434
+ const parsed = decodeJson(raw);
435
+ if (!parsed.ok)
436
+ return parsed;
437
+ const value = parsed.value;
438
+ try {
439
+ const state = record(value, "$session");
440
+ array(state.messages, "$session.messages").forEach((item, index) => {
441
+ const message = record(item, `$session.messages[${index}]`);
442
+ string(message.role, `$session.messages[${index}].role`, false);
443
+ });
444
+ boolean(state.isStreaming, "$session.isStreaming");
445
+ stringArray(state.pendingToolCalls, "$session.pendingToolCalls");
446
+ const timings = record(state.toolCallTimings, "$session.toolCallTimings");
447
+ for (const [toolCallId, value] of Object.entries(timings)) {
448
+ const timing = record(value, `$session.toolCallTimings.${toolCallId}`);
449
+ finiteNumber(timing.startedAt, `$session.toolCallTimings.${toolCallId}.startedAt`);
450
+ if (timing.completedAt !== undefined) {
451
+ finiteNumber(timing.completedAt, `$session.toolCallTimings.${toolCallId}.completedAt`);
452
+ }
453
+ }
454
+ if (state.model !== null)
455
+ compactModel(state.model, "$session.model");
456
+ thinkingLevel(state.thinkingLevel, "$session.thinkingLevel");
457
+ stringArray(state.steeringQueue, "$session.steeringQueue");
458
+ optionalString(state.error, "$session.error");
459
+ return { ok: true, value: state };
460
+ }
461
+ catch (error) {
462
+ return decodeFailure(value, error, "session_sync");
463
+ }
464
+ }
465
+ export function encodeClientCommand(payload, id) {
466
+ return JSON.stringify({ protocolVersion: WS_PROTOCOL_VERSION, id, ...payload });
467
+ }
468
+ export function encodeServerMessage(payload) {
469
+ return JSON.stringify({ protocolVersion: WS_PROTOCOL_VERSION, ...payload });
470
+ }
471
+ export function assertNever(value) {
472
+ throw new Error(`Unhandled protocol variant: ${JSON.stringify(value)}`);
473
+ }