pinomad 0.1.1 → 0.3.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.
@@ -1,9 +1,14 @@
1
1
  import {
2
- PROTOCOL_VERSION,
2
+ HOST_STREAM_NAMES,
3
+ INVALID_FRAME_CLOSE_CODE,
4
+ PROTOCOL,
3
5
  UNAUTHORIZED_CLOSE_CODE,
4
6
  conversationStream,
5
- docStream
6
- } from "./chunk-wgn3cx5q.js";
7
+ docStream,
8
+ hostStreams,
9
+ hostSupportsCall,
10
+ hostSupportsFeature
11
+ } from "./chunk-c155p0t0.js";
7
12
 
8
13
  // packages/protocol/src/remote-durable.ts
9
14
  import { applyImmutable } from "@earendil-works/chord/delta";
@@ -125,13 +130,19 @@ class UnauthorizedError extends Error {
125
130
  }
126
131
 
127
132
  class ProtocolMismatchError extends Error {
128
- host;
129
- client;
130
- constructor(host, client) {
131
- super(`Host protocol mismatch: host speaks ${host}, this client speaks ${client}`);
133
+ hostMajor;
134
+ clientMajor;
135
+ hostVersion;
136
+ direction;
137
+ constructor(hostMajor, clientMajor, hostVersion) {
138
+ const direction = hostMajor > clientMajor ? "client-older" : "host-older";
139
+ super(direction === "client-older" ? `Client out of date: the host speaks protocol major ${hostMajor}, this client speaks ${clientMajor}` : `Host out of date: this client speaks protocol major ${clientMajor}, the host speaks ${hostMajor}`);
132
140
  this.name = "ProtocolMismatchError";
133
- this.host = host;
134
- this.client = client;
141
+ this.hostMajor = hostMajor;
142
+ this.clientMajor = clientMajor;
143
+ if (hostVersion !== undefined)
144
+ this.hostVersion = hostVersion;
145
+ this.direction = direction;
135
146
  }
136
147
  }
137
148
 
@@ -140,7 +151,8 @@ class RemoteClient {
140
151
  #options;
141
152
  #listeners = new Set;
142
153
  #values = new Map;
143
- #wanted = new Set(["conversations", "index", "devices", "mcp"]);
154
+ #wanted = new Set;
155
+ #fresh = new Set;
144
156
  #docs = [];
145
157
  #toolPresentations = {};
146
158
  #snapshotWaiters = new Map;
@@ -149,8 +161,13 @@ class RemoteClient {
149
161
  #connection;
150
162
  #state;
151
163
  #current;
164
+ #side;
152
165
  #nextCall = 1;
153
166
  #nextNotice = 1;
167
+ #hostMinor = 0;
168
+ #helloModels = [];
169
+ #helloDefaults = {};
170
+ #hostVersion;
154
171
  #attempt = 0;
155
172
  #closed = false;
156
173
  #notifying = false;
@@ -192,10 +209,16 @@ class RemoteClient {
192
209
  return;
193
210
  if (code === UNAUTHORIZED_CLOSE_CODE) {
194
211
  this.#closed = true;
195
- this.#update({ connection: "closed" });
212
+ this.#update({ connection: "closed", unauthorized: true });
196
213
  this.#notice("error", new UnauthorizedError().message);
197
214
  return;
198
215
  }
216
+ if (code === INVALID_FRAME_CLOSE_CODE) {
217
+ this.#closed = true;
218
+ this.#update({ connection: "closed" });
219
+ this.#notice("error", `The host rejected a malformed frame from this client${reason === undefined ? "" : `: ${reason}`}`);
220
+ return;
221
+ }
199
222
  this.#update({ connection: "reconnecting" });
200
223
  const { min, max } = this.#options.reconnectDelayMs ?? { min: 200, max: 2000 };
201
224
  const delay = Math.min(max, min * 2 ** this.#attempt++);
@@ -208,44 +231,76 @@ class RemoteClient {
208
231
  this.#connection?.send(JSON.stringify(frame));
209
232
  }
210
233
  #receive(frame) {
234
+ if (frame === null || typeof frame !== "object")
235
+ return;
211
236
  switch (frame.type) {
212
237
  case "hello": {
213
- if (frame.protocol !== PROTOCOL_VERSION) {
214
- const mismatch = new ProtocolMismatchError(frame.protocol, PROTOCOL_VERSION);
238
+ const announced = frame.protocol;
239
+ const hostMajor = typeof announced === "number" ? announced : announced !== null && typeof announced === "object" ? announced.major : undefined;
240
+ if (hostMajor !== PROTOCOL.major) {
241
+ const mismatch = new ProtocolMismatchError(typeof hostMajor === "number" ? hostMajor : Number.NaN, PROTOCOL.major, frame.hostVersion);
215
242
  this.#closed = true;
216
243
  if (this.#state === undefined) {
217
244
  this.#rejectReady(mismatch);
218
245
  } else {
219
- this.#update({ connection: "outdated" });
246
+ this.#update({
247
+ connection: "outdated",
248
+ protocolMismatch: {
249
+ hostMajor: mismatch.hostMajor,
250
+ clientMajor: mismatch.clientMajor,
251
+ ...mismatch.hostVersion === undefined ? {} : { hostVersion: mismatch.hostVersion },
252
+ direction: mismatch.direction
253
+ }
254
+ });
220
255
  this.#notice("error", mismatch.message);
221
256
  }
222
257
  this.#connection?.close();
223
258
  return;
224
259
  }
260
+ const hostMinor = announced !== null && typeof announced === "object" && typeof announced.minor === "number" ? announced.minor : 0;
261
+ this.#hostMinor = hostMinor;
225
262
  this.#attempt = 0;
226
263
  this.#docs = frame.docs;
227
264
  this.#toolPresentations = frame.toolPresentations;
265
+ this.#helloModels = frame.models;
266
+ this.#helloDefaults = frame.defaults ?? {};
267
+ this.#hostVersion = frame.hostVersion;
268
+ this.#fresh.clear();
269
+ const covered = new Set(hostStreams(hostMinor));
270
+ for (const name of HOST_STREAM_NAMES) {
271
+ if (covered.has(name)) {
272
+ this.#wanted.add(name);
273
+ } else {
274
+ this.#wanted.delete(name);
275
+ this.#values.delete(name);
276
+ this.#fresh.delete(name);
277
+ }
278
+ }
228
279
  if (this.#current !== undefined)
229
280
  for (const stream of this.#conversationStreams(this.#current))
230
281
  this.#wanted.add(stream);
282
+ if (this.#side !== undefined)
283
+ for (const stream of this.#conversationStreams(this.#side))
284
+ this.#wanted.add(stream);
231
285
  for (const stream of this.#wanted)
232
286
  this.#send({ type: "subscribe", stream });
233
287
  if (this.#state !== undefined) {
234
288
  this.#update({
235
289
  session: frame.session,
236
- models: frame.models,
237
290
  toolPresentations: frame.toolPresentations,
238
- defaults: frame.defaults ?? {},
239
- connection: "connected"
291
+ connection: "connected",
292
+ host: this.#hostInfo()
240
293
  });
294
+ this.#refresh();
241
295
  } else
242
- this.#awaitFirstView(frame);
296
+ this.#awaitFirstView(frame, hostMinor);
243
297
  return;
244
298
  }
245
299
  case "snapshot":
246
300
  if (!this.#wanted.has(frame.stream))
247
301
  return;
248
302
  this.#values.set(frame.stream, frame.value);
303
+ this.#fresh.add(frame.stream);
249
304
  this.#refresh();
250
305
  for (const waiter of this.#snapshotWaiters.get(frame.stream)?.splice(0) ?? [])
251
306
  waiter.resolve();
@@ -259,6 +314,7 @@ class RemoteClient {
259
314
  case "ended":
260
315
  this.#wanted.delete(frame.stream);
261
316
  this.#values.delete(frame.stream);
317
+ this.#fresh.delete(frame.stream);
262
318
  for (const waiter of this.#snapshotWaiters.get(frame.stream)?.splice(0) ?? []) {
263
319
  waiter.reject(new Error(`Stream ${frame.stream} ended: ${frame.reason}`));
264
320
  }
@@ -278,19 +334,23 @@ class RemoteClient {
278
334
  return;
279
335
  }
280
336
  }
281
- #awaitFirstView(hello) {
282
- Promise.all([this.#snapshot("conversations"), this.#snapshot("index"), this.#snapshot("devices"), this.#snapshot("mcp")]).then(() => {
337
+ #awaitFirstView(hello, hostMinor) {
338
+ Promise.all(hostStreams(hostMinor).map((stream) => this.#snapshot(stream))).then(() => {
283
339
  this.#state = {
284
340
  session: hello.session,
285
341
  organized: { chats: [], projects: [] },
286
- models: hello.models,
287
- defaults: hello.defaults ?? {},
342
+ models: this.#host("models") ?? hello.models,
343
+ defaults: this.#effectiveDefaults(hello.defaults ?? {}),
288
344
  notices: [],
289
345
  connection: "connected",
290
346
  docs: [],
291
347
  toolPresentations: hello.toolPresentations,
292
348
  devices: [],
293
- mcp: null
349
+ mcp: null,
350
+ auth: null,
351
+ settings: null,
352
+ agents: null,
353
+ host: this.#hostInfo()
294
354
  };
295
355
  this.#refresh();
296
356
  this.#resolveReady({ view: this.#viewSource(), controller: this.#controller(), close: () => this.#close() });
@@ -306,28 +366,91 @@ class RemoteClient {
306
366
  #conversationStreams(id) {
307
367
  return [conversationStream(id), ...this.#docs.map((doc) => docStream(doc.kind, id))];
308
368
  }
369
+ async#acquire(id) {
370
+ const missing = this.#conversationStreams(id).filter((stream) => !this.#wanted.has(stream));
371
+ const stale = this.#conversationStreams(id).filter((stream) => this.#wanted.has(stream) && !this.#fresh.has(stream));
372
+ const shown = Promise.all([...missing, ...stale].map((stream) => this.#snapshot(stream)));
373
+ for (const stream of missing) {
374
+ this.#wanted.add(stream);
375
+ this.#send({ type: "subscribe", stream });
376
+ }
377
+ await shown;
378
+ }
379
+ #release(id, other) {
380
+ const keep = new Set(other === undefined ? [] : this.#conversationStreams(other));
381
+ for (const stream of this.#conversationStreams(id)) {
382
+ if (keep.has(stream))
383
+ continue;
384
+ this.#wanted.delete(stream);
385
+ this.#values.delete(stream);
386
+ this.#fresh.delete(stream);
387
+ this.#send({ type: "unsubscribe", stream });
388
+ }
389
+ }
390
+ #host(name) {
391
+ return this.#values.get(name);
392
+ }
393
+ #hostInfo() {
394
+ return {
395
+ protocol: { major: PROTOCOL.major, minor: this.#hostMinor },
396
+ ...this.#hostVersion === undefined ? {} : { version: this.#hostVersion }
397
+ };
398
+ }
399
+ #effectiveDefaults(fallback) {
400
+ const settings = this.#host("settings");
401
+ if (settings === undefined)
402
+ return fallback;
403
+ return {
404
+ ...settings.defaultModel === null ? {} : { model: settings.defaultModel },
405
+ ...settings.defaultThinkingLevel === null ? {} : { thinkingLevel: settings.defaultThinkingLevel }
406
+ };
407
+ }
408
+ #descendsFrom(id, ancestor) {
409
+ const summaries = this.#host("conversations") ?? [];
410
+ const parentOf = new Map(summaries.map((summary) => [summary.id, summary.parent]));
411
+ let cursor = id;
412
+ for (let depth = 0;cursor !== undefined && depth < 8; depth++) {
413
+ if (cursor === ancestor)
414
+ return true;
415
+ cursor = parentOf.get(cursor);
416
+ }
417
+ return false;
418
+ }
419
+ #docsOf(id) {
420
+ return this.#docs.flatMap((doc) => {
421
+ const stream = docStream(doc.kind, id);
422
+ return this.#values.has(stream) ? [{ kind: doc.kind, presentation: doc.presentation, value: this.#values.get(stream) }] : [];
423
+ });
424
+ }
309
425
  #refresh() {
310
426
  if (this.#state === undefined)
311
427
  return;
312
428
  const current = this.#current;
429
+ const side = this.#side;
313
430
  const conversation = current === undefined ? undefined : this.#values.get(conversationStream(current));
314
- const summaries = this.#values.get("conversations") ?? [];
315
- const index = this.#values.get("index") ?? { projects: [], conversations: [] };
316
- const devices = this.#values.get("devices")?.devices ?? [];
431
+ const summaries = this.#host("conversations") ?? [];
432
+ const index = this.#host("index") ?? { projects: [], conversations: [] };
433
+ const devices = this.#host("devices")?.devices ?? [];
317
434
  const tasks = this.#wanted.has("tasks") ? this.#values.get("tasks") : undefined;
318
- const docs = current === undefined ? [] : this.#docs.flatMap((doc) => {
319
- const stream = docStream(doc.kind, current);
320
- return this.#values.has(stream) ? [{ kind: doc.kind, presentation: doc.presentation, value: this.#values.get(stream) }] : [];
321
- });
322
435
  this.#update({
323
436
  conversation,
437
+ models: this.#host("models") ?? this.#helloModels,
324
438
  organized: organize(index, summaries),
325
439
  home: current === undefined ? undefined : homeOf(index, summaries, current),
326
440
  checkout: current === undefined ? undefined : checkoutOf(index, summaries, current),
327
441
  tasks,
328
- docs,
442
+ docs: current === undefined ? [] : this.#docsOf(current),
443
+ side: side === undefined ? undefined : {
444
+ id: side,
445
+ conversation: this.#values.get(conversationStream(side)),
446
+ docs: this.#docsOf(side)
447
+ },
329
448
  devices,
330
- mcp: this.#values.get("mcp") ?? null
449
+ mcp: this.#host("mcp") ?? null,
450
+ auth: this.#host("auth") ?? null,
451
+ settings: this.#host("settings") ?? null,
452
+ agents: this.#host("agents") ?? null,
453
+ defaults: this.#effectiveDefaults(this.#helloDefaults)
331
454
  });
332
455
  }
333
456
  #update(patch) {
@@ -362,6 +485,11 @@ class RemoteClient {
362
485
  this.#commands = this.#commands.then(operation).then(() => {}, (error) => this.#notice("error", error instanceof Error ? error.message : String(error)));
363
486
  return this.#commands;
364
487
  }
488
+ #request(operation) {
489
+ const request = this.#commands.then(operation);
490
+ this.#commands = request.then(() => {}, () => {});
491
+ return request;
492
+ }
365
493
  #viewSource() {
366
494
  return {
367
495
  current: () => this.#state,
@@ -379,16 +507,33 @@ class RemoteClient {
379
507
  return this.#current;
380
508
  };
381
509
  return {
382
- addProject: (path) => this.#command(() => this.#call("addProject", { path })),
510
+ addProject: (path) => this.#request(() => this.#call("addProject", { path })),
511
+ listFolders: (path) => this.#request(() => {
512
+ if (!hostSupportsCall(this.#hostMinor, "listFolders")) {
513
+ throw new Error("The host is too old for this (needs protocol 5.3); run `pinomad upgrade` on the host");
514
+ }
515
+ return this.#call("listFolders", path === undefined ? {} : { path });
516
+ }),
383
517
  removeProject: (path) => this.#command(() => this.#call("removeProject", { path })),
518
+ renameConversation: (id, title) => this.#request(async () => {
519
+ if (!hostSupportsCall(this.#hostMinor, "renameConversation")) {
520
+ throw new Error("The host is too old for this (needs protocol 5.4); run `pinomad upgrade` on the host");
521
+ }
522
+ await this.#call("renameConversation", { conversationId: id, title });
523
+ }),
384
524
  createConversation: (home, text, options) => this.#command(async () => {
525
+ const images = options?.images ?? [];
526
+ if (images.length > 0 && !hostSupportsFeature(this.#hostMinor, "images")) {
527
+ throw new Error("The host is too old for images (needs protocol 5.5); run `pinomad upgrade` on the host");
528
+ }
385
529
  const { conversationId: conversationId2 } = await this.#call("createConversation", {
386
530
  home,
387
531
  text,
388
532
  requestId: newRequestId(),
389
533
  ...options?.checkout === undefined ? {} : { checkout: options.checkout },
390
534
  ...options?.model === undefined ? {} : { model: options.model },
391
- ...options?.thinkingLevel === undefined ? {} : { thinkingLevel: options.thinkingLevel }
535
+ ...options?.thinkingLevel === undefined ? {} : { thinkingLevel: options.thinkingLevel },
536
+ ...images.length === 0 ? {} : { images }
392
537
  });
393
538
  await this.#switch(conversationId2);
394
539
  }),
@@ -396,15 +541,30 @@ class RemoteClient {
396
541
  await this.#call("archive", { conversationId: id, archived });
397
542
  if (archived && id === this.#current)
398
543
  this.#unshow();
544
+ if (archived && this.#side !== undefined && this.#descendsFrom(this.#side, id))
545
+ await this.#showSide(undefined);
546
+ }),
547
+ submit: (text, whenBusy, options) => this.#command(() => {
548
+ const images = options?.images ?? [];
549
+ if (images.length > 0 && !hostSupportsFeature(this.#hostMinor, "images")) {
550
+ throw new Error("The host is too old for images (needs protocol 5.5); run `pinomad upgrade` on the host");
551
+ }
552
+ return this.#call("submit", {
553
+ conversationId: options?.target ?? conversationId(),
554
+ text,
555
+ whenBusy,
556
+ requestId: options?.requestId ?? newRequestId(),
557
+ ...images.length === 0 ? {} : { images }
558
+ });
399
559
  }),
400
- submit: (text, whenBusy) => this.#command(() => this.#call("submit", { conversationId: conversationId(), text, whenBusy, requestId: newRequestId() })),
401
560
  compact: (instructions) => this.#command(() => this.#call("compact", { conversationId: conversationId(), ...instructions === undefined ? {} : { instructions } })),
402
- abort: () => {
403
- if (this.#current === undefined) {
561
+ abort: (target) => {
562
+ const targetConversation = target ?? this.#current;
563
+ if (targetConversation === undefined) {
404
564
  this.#notice("error", "No conversation selected");
405
565
  return Promise.resolve();
406
566
  }
407
- return this.#call("abort", { conversationId: conversationId() }).then(() => {}, (error) => this.#notice("error", error instanceof Error ? error.message : String(error)));
567
+ return this.#call("abort", { conversationId: targetConversation }).then(() => {}, (error) => this.#notice("error", error instanceof Error ? error.message : String(error)));
408
568
  },
409
569
  setThinkingLevel: (level) => this.#command(() => this.#call("setThinkingLevel", { conversationId: conversationId(), level })),
410
570
  setModel: (model) => this.#command(() => this.#call("setModel", { conversationId: conversationId(), model })),
@@ -421,46 +581,63 @@ class RemoteClient {
421
581
  await shown;
422
582
  }),
423
583
  switchConversation: (id) => this.#command(() => this.#switch(id)),
424
- fork: (entryId, prompt, removeTools) => this.#command(async () => {
584
+ showSide: (id) => this.#command(() => this.#showSide(id)),
585
+ fork: (entryId, prompt, removeTools, options) => this.#command(async () => {
425
586
  const { conversationId: forked } = await this.#call("fork", {
426
587
  conversationId: conversationId(),
427
588
  entryId,
428
589
  ...removeTools === undefined ? {} : { removeTools }
429
590
  });
430
- await this.#switch(forked);
591
+ if (options?.show === "side")
592
+ await this.#showSide(forked);
593
+ else
594
+ await this.#switch(forked);
431
595
  await this.#call("submit", { conversationId: forked, text: prompt, whenBusy: "followUp", requestId: newRequestId() });
432
596
  }),
433
- answer: (kind, requestId, answers) => this.#command(() => this.#call("answer", { conversationId: conversationId(), kind, requestId, answers })),
434
- changes: () => {
435
- const call = this.#commands.then(() => this.#call("changes", { conversationId: conversationId() }));
436
- this.#commands = call.then(() => {}, () => {});
437
- return call;
438
- },
439
- createPairing: () => {
440
- const pairing = this.#commands.then(() => this.#call("createPairing", {}));
441
- this.#commands = pairing.then(() => {}, () => {});
442
- return pairing;
443
- },
444
- revokeDevice: (publicKey) => this.#command(() => this.#call("revokeDevice", { publicKey }))
597
+ answer: (kind, requestId, answers, target) => this.#command(() => this.#call("answer", { conversationId: target ?? conversationId(), kind, requestId, answers })),
598
+ changes: () => this.#request(() => this.#call("changes", { conversationId: conversationId() })),
599
+ createPairing: () => this.#request(() => this.#call("createPairing", {})),
600
+ revokeDevice: (publicKey) => this.#command(() => this.#call("revokeDevice", { publicKey })),
601
+ logout: (providerId) => this.#command(() => {
602
+ if (!hostSupportsCall(this.#hostMinor, "logout")) {
603
+ throw new Error("The host is too old for this (needs protocol 5.1); run `pinomad upgrade` on the host");
604
+ }
605
+ return this.#call("logout", { providerId });
606
+ }),
607
+ updateSettings: (patch) => this.#command(() => {
608
+ if (!hostSupportsCall(this.#hostMinor, "updateSettings")) {
609
+ throw new Error("The host is too old for this (needs protocol 5.2); run `pinomad upgrade` on the host");
610
+ }
611
+ return this.#call("updateSettings", { patch });
612
+ }),
613
+ createAgent: (role) => this.#request(async () => {
614
+ if (!hostSupportsCall(this.#hostMinor, "createAgent")) {
615
+ throw new Error("The host is too old for this (needs protocol 5.6); run `pinomad upgrade` on the host");
616
+ }
617
+ await this.#call("createAgent", { role });
618
+ }),
619
+ updateAgent: (role) => this.#request(async () => {
620
+ if (!hostSupportsCall(this.#hostMinor, "updateAgent")) {
621
+ throw new Error("The host is too old for this (needs protocol 5.6); run `pinomad upgrade` on the host");
622
+ }
623
+ await this.#call("updateAgent", { role });
624
+ }),
625
+ deleteAgent: (name) => this.#request(async () => {
626
+ if (!hostSupportsCall(this.#hostMinor, "deleteAgent")) {
627
+ throw new Error("The host is too old for this (needs protocol 5.6); run `pinomad upgrade` on the host");
628
+ }
629
+ await this.#call("deleteAgent", { name });
630
+ })
445
631
  };
446
632
  }
447
633
  async#switch(id) {
448
634
  const previous = this.#current;
449
635
  if (id === previous)
450
636
  return;
451
- const next = this.#conversationStreams(id);
452
- for (const stream of next)
453
- this.#wanted.add(stream);
454
- const shown = Promise.all(next.map((stream) => this.#snapshot(stream)));
455
- for (const stream of next)
456
- this.#send({ type: "subscribe", stream });
457
- await shown;
637
+ await this.#acquire(id);
458
638
  this.#current = id;
459
- for (const stream of previous === undefined ? [] : this.#conversationStreams(previous)) {
460
- this.#wanted.delete(stream);
461
- this.#values.delete(stream);
462
- this.#send({ type: "unsubscribe", stream });
463
- }
639
+ if (previous !== undefined)
640
+ this.#release(previous, this.#side);
464
641
  this.#refresh();
465
642
  }
466
643
  #unshow() {
@@ -468,11 +645,18 @@ class RemoteClient {
468
645
  if (previous === undefined)
469
646
  return;
470
647
  this.#current = undefined;
471
- for (const stream of this.#conversationStreams(previous)) {
472
- this.#wanted.delete(stream);
473
- this.#values.delete(stream);
474
- this.#send({ type: "unsubscribe", stream });
475
- }
648
+ this.#release(previous, this.#side);
649
+ this.#refresh();
650
+ }
651
+ async#showSide(id) {
652
+ const previous = this.#side;
653
+ if (id === previous)
654
+ return;
655
+ if (id !== undefined)
656
+ await this.#acquire(id);
657
+ this.#side = id;
658
+ if (previous !== undefined)
659
+ this.#release(previous, this.#current);
476
660
  this.#refresh();
477
661
  }
478
662
  #close() {
@@ -0,0 +1,154 @@
1
+ // packages/protocol/src/settings.ts
2
+ var HOST_SETTING_KEY_SET = {
3
+ defaultModel: true,
4
+ defaultThinkingLevel: true,
5
+ steeringMode: true,
6
+ followUpMode: true
7
+ };
8
+ var HOST_SETTING_KEYS = Object.keys(HOST_SETTING_KEY_SET);
9
+ var THINKING_LEVEL_KEYS = { off: true, minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
10
+ var THINKING_LEVELS = Object.keys(THINKING_LEVEL_KEYS);
11
+ var isThinkingLevel = (value) => typeof value === "string" && Object.prototype.hasOwnProperty.call(THINKING_LEVEL_KEYS, value);
12
+ var isQueueMode = (value) => value === "all" || value === "one-at-a-time";
13
+
14
+ // packages/protocol/src/frames.ts
15
+ var HOST_STREAMS = { conversations: 0, index: 0, devices: 0, mcp: 0, models: 1, auth: 1, settings: 2, agents: 6 };
16
+ var HOST_STREAM_NAMES = Object.keys(HOST_STREAMS);
17
+ function hostStreams(hostMinor) {
18
+ return HOST_STREAM_NAMES.filter((name) => HOST_STREAMS[name] <= hostMinor);
19
+ }
20
+ var conversationStream = (id) => `conversation:${id}`;
21
+ var docStream = (kind, id) => `doc:${kind}:${id}`;
22
+ var UNAUTHORIZED_CLOSE_CODE = 4401;
23
+ var INVALID_FRAME_CLOSE_CODE = 4400;
24
+ var PROTOCOL = { major: 5, minor: 6 };
25
+ var CALL_SINCE = {
26
+ logout: 1,
27
+ updateSettings: 2,
28
+ listFolders: 3,
29
+ renameConversation: 4,
30
+ createAgent: 6,
31
+ updateAgent: 6,
32
+ deleteAgent: 6
33
+ };
34
+ function hostSupportsCall(hostMinor, method) {
35
+ return hostMinor >= (CALL_SINCE[method] ?? 0);
36
+ }
37
+ var FEATURE_SINCE = { images: 5 };
38
+ function hostSupportsFeature(hostMinor, feature) {
39
+ return hostMinor >= FEATURE_SINCE[feature];
40
+ }
41
+ function parseClientFrame(value) {
42
+ if (!record(value) || typeof value.type !== "string")
43
+ return { kind: "malformed" };
44
+ if (value.type === "subscribe" || value.type === "unsubscribe") {
45
+ const stream = value.stream;
46
+ if (typeof stream !== "string")
47
+ return { kind: "malformed" };
48
+ if (isStreamName(stream)) {
49
+ return { kind: "frame", frame: { type: value.type, stream } };
50
+ }
51
+ const prefix = stream.slice(0, stream.indexOf(":") === -1 ? undefined : stream.indexOf(":"));
52
+ if (STREAM_KINDS.has(prefix))
53
+ return { kind: "malformed" };
54
+ return { kind: "unsupportedStream", type: value.type, stream };
55
+ }
56
+ if (value.type !== "call")
57
+ return { kind: "unknownType" };
58
+ const { id, method, args } = value;
59
+ if (typeof id !== "number" || !Number.isSafeInteger(id) || !record(args) || typeof method !== "string") {
60
+ return { kind: "malformed" };
61
+ }
62
+ if (!isCallMethod(method))
63
+ return { kind: "unsupportedCall", id, method };
64
+ return validCallArgs(method, args) ? { kind: "frame", frame: { type: "call", id, method, args } } : { kind: "malformed" };
65
+ }
66
+ var STREAM_KINDS = new Set(["conversation", "doc", "tasks", ...HOST_STREAM_NAMES]);
67
+ function isStreamName(stream) {
68
+ return stream === "tasks" || HOST_STREAM_NAMES.includes(stream) || /^(?:conversation:|doc:.+:)[1-9]\d*$/.test(stream);
69
+ }
70
+ var isCallMethod = (method) => CALL_METHODS.includes(method);
71
+ var CALL_METHODS = [
72
+ "addProject",
73
+ "removeProject",
74
+ "listFolders",
75
+ "createConversation",
76
+ "archive",
77
+ "renameConversation",
78
+ "submit",
79
+ "abort",
80
+ "compact",
81
+ "setModel",
82
+ "setThinkingLevel",
83
+ "fork",
84
+ "answer",
85
+ "changes",
86
+ "createPairing",
87
+ "revokeDevice",
88
+ "logout",
89
+ "updateSettings",
90
+ "createAgent",
91
+ "updateAgent",
92
+ "deleteAgent"
93
+ ];
94
+ var SETTING_VALIDATORS = {
95
+ defaultModel: (value) => value === null || record(value) && nonempty(value.provider) && nonempty(value.modelId),
96
+ defaultThinkingLevel: (value) => value === null || isThinkingLevel(value),
97
+ steeringMode: (value) => value === null || isQueueMode(value),
98
+ followUpMode: (value) => value === null || isQueueMode(value)
99
+ };
100
+ function validCallArgs(method, args) {
101
+ const conversationId = () => typeof args.conversationId === "number" && Number.isSafeInteger(args.conversationId) && args.conversationId >= 1;
102
+ const images = () => args.images === undefined || Array.isArray(args.images) && args.images.every((image) => record(image) && typeof image.data === "string" && typeof image.mimeType === "string");
103
+ const role = () => record(args.role) && typeof args.role.name === "string" && typeof args.role.description === "string" && typeof args.role.prompt === "string" && (args.role.model === undefined || typeof args.role.model === "string") && (args.role.thinkingLevel === undefined || typeof args.role.thinkingLevel === "string");
104
+ switch (method) {
105
+ case "submit":
106
+ return conversationId() && typeof args.text === "string" && (args.whenBusy === "steer" || args.whenBusy === "followUp") && nonempty(args.requestId) && images();
107
+ case "abort":
108
+ return conversationId();
109
+ case "setThinkingLevel":
110
+ return conversationId() && nonempty(args.level);
111
+ case "compact":
112
+ return conversationId() && (args.instructions === undefined || typeof args.instructions === "string");
113
+ case "setModel":
114
+ return conversationId() && record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId);
115
+ case "fork":
116
+ return conversationId() && typeof args.entryId === "string" && /^[1-9]\d*$/.test(args.entryId) && (args.removeTools === undefined || Array.isArray(args.removeTools) && args.removeTools.every(nonempty));
117
+ case "answer":
118
+ return conversationId() && nonempty(args.kind) && nonempty(args.requestId) && Array.isArray(args.answers) && args.answers.every((answer) => record(answer) && Array.isArray(answer.selected) && answer.selected.every((l) => typeof l === "string") && (answer.other === undefined || typeof answer.other === "string"));
119
+ case "changes":
120
+ return conversationId();
121
+ case "addProject":
122
+ case "removeProject":
123
+ return nonempty(args.path);
124
+ case "listFolders":
125
+ return args.path === undefined || nonempty(args.path);
126
+ case "createConversation":
127
+ return record(args.home) && (args.home.kind === "chat" || args.home.kind === "project" && nonempty(args.home.path)) && typeof args.text === "string" && (nonempty(args.text) || Array.isArray(args.images) && args.images.length > 0) && nonempty(args.requestId) && images() && (args.checkout === undefined || args.checkout === "worktree" || args.checkout === "project") && (args.model === undefined || record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId)) && (args.thinkingLevel === undefined || nonempty(args.thinkingLevel));
128
+ case "archive":
129
+ return conversationId() && typeof args.archived === "boolean";
130
+ case "renameConversation":
131
+ return conversationId() && typeof args.title === "string";
132
+ case "createPairing":
133
+ return Object.keys(args).length === 0;
134
+ case "revokeDevice":
135
+ return nonempty(args.publicKey) && /^[A-Za-z0-9_-]+$/.test(args.publicKey);
136
+ case "logout":
137
+ return nonempty(args.providerId);
138
+ case "createAgent":
139
+ case "updateAgent":
140
+ return role();
141
+ case "deleteAgent":
142
+ return typeof args.name === "string";
143
+ case "updateSettings":
144
+ return record(args.patch) && Object.keys(args.patch).every((key) => HOST_SETTING_KEYS.includes(key)) && Object.entries(args.patch).every(([key, value]) => SETTING_VALIDATORS[key]?.(value) === true);
145
+ }
146
+ }
147
+ function record(value) {
148
+ return value !== null && typeof value === "object" && !Array.isArray(value);
149
+ }
150
+ function nonempty(value) {
151
+ return typeof value === "string" && value.length > 0;
152
+ }
153
+
154
+ export { HOST_SETTING_KEYS, THINKING_LEVELS, isThinkingLevel, isQueueMode, HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame };
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  main,
4
4
  restartService
5
- } from "./chunk-zx77w1nm.js";
6
- import"./chunk-fpb6f9n5.js";
7
- import"./chunk-y73t2jmc.js";
8
- import"./chunk-wgn3cx5q.js";
5
+ } from "./chunk-t9y2wfk5.js";
6
+ import"./chunk-3tf23vjq.js";
7
+ import"./chunk-bggesvnj.js";
8
+ import"./chunk-c155p0t0.js";
9
9
  import"./chunk-cjfmfyjb.js";
10
10
  import"./chunk-df32avw6.js";
11
11
  export {