@kangentic/protocol 0.5.0 → 0.7.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,4 +1,4 @@
1
- export declare const PUSH_CATEGORIES: readonly ["permission-needed", "agent-question", "turn-complete", "session-failed"];
1
+ export declare const PUSH_CATEGORIES: readonly ["input-required", "turn-complete", "session-failed", "plan-complete", "spawn-stalled"];
2
2
  export type PushCategory = (typeof PUSH_CATEGORIES)[number];
3
3
  export declare function isPushCategory(value: unknown): value is PushCategory;
4
4
  /** Reject an opened envelope whose sentAt is older than this (stale replay). */
@@ -45,10 +45,19 @@ export type ActivityEventPayload = {
45
45
  } | {
46
46
  type: 'event';
47
47
  event: SessionEventWire;
48
- } | {
48
+ }
49
+ /**
50
+ * `options`, when present on a pending push, carries the prompt dialog's
51
+ * numbered option labels in keystroke order (options[0] is answered with
52
+ * "1\r"). Absent from pre-0.6.0 desktops and whenever no numbered dialog
53
+ * could be parsed from the PTY frame; the phone then falls back to its
54
+ * blind approve/deny keystrokes.
55
+ */
56
+ | {
49
57
  type: 'permission';
50
58
  promptId: string;
51
59
  pending: boolean;
60
+ options?: string[];
52
61
  }
53
62
  /**
54
63
  * The streamed session's PTY exited. Pushed once by the desktop's
package/dist/index.cjs CHANGED
@@ -5186,6 +5186,56 @@ function base64UrlDecode(encoded) {
5186
5186
  return bytes;
5187
5187
  }
5188
5188
 
5189
+ // src/crypto/push-envelope.ts
5190
+ var PUSH_CATEGORIES = ["input-required", "turn-complete", "session-failed", "plan-complete", "spawn-stalled"];
5191
+ function isPushCategory(value) {
5192
+ return typeof value === "string" && PUSH_CATEGORIES.includes(value);
5193
+ }
5194
+ var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5195
+ var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5196
+ function requireEnvelopeString(record, field) {
5197
+ const value = record[field];
5198
+ if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
5199
+ return value;
5200
+ }
5201
+ function parsePushEnvelopePlaintext(value) {
5202
+ if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
5203
+ if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
5204
+ if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
5205
+ throw new Error('push envelope plaintext has an invalid "sentAt"');
5206
+ }
5207
+ return {
5208
+ category: value.category,
5209
+ projectId: requireEnvelopeString(value, "projectId"),
5210
+ taskId: requireEnvelopeString(value, "taskId"),
5211
+ sessionId: requireEnvelopeString(value, "sessionId"),
5212
+ taskTitle: requireEnvelopeString(value, "taskTitle"),
5213
+ detail: requireEnvelopeString(value, "detail"),
5214
+ sentAt: value.sentAt
5215
+ };
5216
+ }
5217
+ function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
5218
+ const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
5219
+ const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
5220
+ const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
5221
+ return base64UrlEncode(concatBytes4(nonce, ciphertext));
5222
+ }
5223
+ function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
5224
+ const bytes = base64UrlDecode(blob);
5225
+ if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
5226
+ throw new Error("push envelope blob is too short");
5227
+ }
5228
+ const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
5229
+ const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
5230
+ const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
5231
+ const decoded = JSON.parse(new TextDecoder().decode(decrypted));
5232
+ const plaintext = parsePushEnvelopePlaintext(decoded);
5233
+ const now = Date.now();
5234
+ if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
5235
+ if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
5236
+ return plaintext;
5237
+ }
5238
+
5189
5239
  // src/events/payloads.ts
5190
5240
  var DIFF_FILE_STATUSES = ["A", "M", "D", "R", "C", "U"];
5191
5241
  var TRANSCRIPT_SYSTEM_SUBTYPES = ["compaction", "command", "command_output", "session_boundary"];
@@ -5437,6 +5487,15 @@ function parseReadStreamResponsePayload(payload) {
5437
5487
  usage: payload.usage === null || payload.usage === void 0 ? null : parseSessionUsageWire(payload.usage),
5438
5488
  awaitedPromptId: payload.awaitedPromptId ?? null
5439
5489
  };
5490
+ if (payload.awaitedPromptOptions !== void 0) {
5491
+ if (payload.awaitedPromptOptions === null) {
5492
+ response.awaitedPromptOptions = null;
5493
+ } else if (Array.isArray(payload.awaitedPromptOptions) && payload.awaitedPromptOptions.every((option) => typeof option === "string")) {
5494
+ response.awaitedPromptOptions = payload.awaitedPromptOptions;
5495
+ } else {
5496
+ throw new Error('read-stream response has an invalid "awaitedPromptOptions"');
5497
+ }
5498
+ }
5440
5499
  if (payload.ptyDimensions !== void 0) {
5441
5500
  response.ptyDimensions = parseTerminalDimensionsWire(payload.ptyDimensions);
5442
5501
  }
@@ -5514,12 +5573,16 @@ function parseReadBoardResponsePayload(payload) {
5514
5573
  if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
5515
5574
  if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
5516
5575
  if (!Array.isArray(payload.backlog)) throw new Error('read-board response is missing "backlog"');
5576
+ if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
5577
+ throw new Error('read-board response has a non-boolean "showTicketNumbers"');
5578
+ }
5517
5579
  return {
5518
5580
  projectId: payload.projectId,
5519
5581
  columns: payload.columns.map((column) => parseBoardColumnWire(column)),
5520
5582
  tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
5521
5583
  backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
5522
- ...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {}
5584
+ ...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
5585
+ ...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
5523
5586
  };
5524
5587
  }
5525
5588
  function parseReadBoardRequestPayload(payload) {
@@ -5638,6 +5701,12 @@ function parseRegisterPushRequestPayload(payload) {
5638
5701
  }
5639
5702
  request.platform = payload.platform;
5640
5703
  }
5704
+ if (payload.categories !== void 0) {
5705
+ if (!Array.isArray(payload.categories) || !payload.categories.every((entry) => typeof entry === "string")) {
5706
+ throw new Error('register-push payload has a malformed "categories"');
5707
+ }
5708
+ request.categories = payload.categories.filter(isPushCategory);
5709
+ }
5641
5710
  if (request.action === "register") {
5642
5711
  if (request.expoPushToken === void 0) throw new Error('register-push register payload missing "expoPushToken"');
5643
5712
  if (request.pushKeyBase64 === void 0) throw new Error('register-push register payload missing "pushKeyBase64"');
@@ -5672,56 +5741,6 @@ function parseCapabilityRequestPayload(verb, payload) {
5672
5741
  }
5673
5742
  }
5674
5743
 
5675
- // src/crypto/push-envelope.ts
5676
- var PUSH_CATEGORIES = ["permission-needed", "agent-question", "turn-complete", "session-failed"];
5677
- function isPushCategory(value) {
5678
- return typeof value === "string" && PUSH_CATEGORIES.includes(value);
5679
- }
5680
- var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5681
- var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5682
- function requireEnvelopeString(record, field) {
5683
- const value = record[field];
5684
- if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
5685
- return value;
5686
- }
5687
- function parsePushEnvelopePlaintext(value) {
5688
- if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
5689
- if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
5690
- if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
5691
- throw new Error('push envelope plaintext has an invalid "sentAt"');
5692
- }
5693
- return {
5694
- category: value.category,
5695
- projectId: requireEnvelopeString(value, "projectId"),
5696
- taskId: requireEnvelopeString(value, "taskId"),
5697
- sessionId: requireEnvelopeString(value, "sessionId"),
5698
- taskTitle: requireEnvelopeString(value, "taskTitle"),
5699
- detail: requireEnvelopeString(value, "detail"),
5700
- sentAt: value.sentAt
5701
- };
5702
- }
5703
- function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
5704
- const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
5705
- const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
5706
- const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
5707
- return base64UrlEncode(concatBytes4(nonce, ciphertext));
5708
- }
5709
- function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
5710
- const bytes = base64UrlDecode(blob);
5711
- if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
5712
- throw new Error("push envelope blob is too short");
5713
- }
5714
- const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
5715
- const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
5716
- const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
5717
- const decoded = JSON.parse(new TextDecoder().decode(decrypted));
5718
- const plaintext = parsePushEnvelopePlaintext(decoded);
5719
- const now = Date.now();
5720
- if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
5721
- if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
5722
- return plaintext;
5723
- }
5724
-
5725
5744
  // src/pairing/qr.ts
5726
5745
  var PAIRING_URI_SCHEME = "kangentic-pair";
5727
5746
  var PAYLOAD_VERSION = 1;
@@ -5895,7 +5914,13 @@ function parseActivityEventPayload(payload) {
5895
5914
  case "permission": {
5896
5915
  if (typeof payload.promptId !== "string") throw new Error('permission payload is missing "promptId"');
5897
5916
  if (typeof payload.pending !== "boolean") throw new Error('permission payload is missing "pending"');
5898
- return { type: "permission", promptId: payload.promptId, pending: payload.pending };
5917
+ if (payload.options === void 0) {
5918
+ return { type: "permission", promptId: payload.promptId, pending: payload.pending };
5919
+ }
5920
+ if (!Array.isArray(payload.options) || !payload.options.every((option) => typeof option === "string")) {
5921
+ throw new Error('permission payload has an invalid "options"');
5922
+ }
5923
+ return { type: "permission", promptId: payload.promptId, pending: payload.pending, options: payload.options };
5899
5924
  }
5900
5925
  case "session-ended": {
5901
5926
  if (typeof payload.intentional !== "boolean") throw new Error('session-ended payload is missing "intentional"');
package/dist/index.mjs CHANGED
@@ -5075,6 +5075,56 @@ function base64UrlDecode(encoded) {
5075
5075
  return bytes;
5076
5076
  }
5077
5077
 
5078
+ // src/crypto/push-envelope.ts
5079
+ var PUSH_CATEGORIES = ["input-required", "turn-complete", "session-failed", "plan-complete", "spawn-stalled"];
5080
+ function isPushCategory(value) {
5081
+ return typeof value === "string" && PUSH_CATEGORIES.includes(value);
5082
+ }
5083
+ var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5084
+ var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5085
+ function requireEnvelopeString(record, field) {
5086
+ const value = record[field];
5087
+ if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
5088
+ return value;
5089
+ }
5090
+ function parsePushEnvelopePlaintext(value) {
5091
+ if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
5092
+ if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
5093
+ if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
5094
+ throw new Error('push envelope plaintext has an invalid "sentAt"');
5095
+ }
5096
+ return {
5097
+ category: value.category,
5098
+ projectId: requireEnvelopeString(value, "projectId"),
5099
+ taskId: requireEnvelopeString(value, "taskId"),
5100
+ sessionId: requireEnvelopeString(value, "sessionId"),
5101
+ taskTitle: requireEnvelopeString(value, "taskTitle"),
5102
+ detail: requireEnvelopeString(value, "detail"),
5103
+ sentAt: value.sentAt
5104
+ };
5105
+ }
5106
+ function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
5107
+ const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
5108
+ const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
5109
+ const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
5110
+ return base64UrlEncode(concatBytes4(nonce, ciphertext));
5111
+ }
5112
+ function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
5113
+ const bytes = base64UrlDecode(blob);
5114
+ if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
5115
+ throw new Error("push envelope blob is too short");
5116
+ }
5117
+ const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
5118
+ const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
5119
+ const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
5120
+ const decoded = JSON.parse(new TextDecoder().decode(decrypted));
5121
+ const plaintext = parsePushEnvelopePlaintext(decoded);
5122
+ const now = Date.now();
5123
+ if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
5124
+ if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
5125
+ return plaintext;
5126
+ }
5127
+
5078
5128
  // src/events/payloads.ts
5079
5129
  var DIFF_FILE_STATUSES = ["A", "M", "D", "R", "C", "U"];
5080
5130
  var TRANSCRIPT_SYSTEM_SUBTYPES = ["compaction", "command", "command_output", "session_boundary"];
@@ -5326,6 +5376,15 @@ function parseReadStreamResponsePayload(payload) {
5326
5376
  usage: payload.usage === null || payload.usage === void 0 ? null : parseSessionUsageWire(payload.usage),
5327
5377
  awaitedPromptId: payload.awaitedPromptId ?? null
5328
5378
  };
5379
+ if (payload.awaitedPromptOptions !== void 0) {
5380
+ if (payload.awaitedPromptOptions === null) {
5381
+ response.awaitedPromptOptions = null;
5382
+ } else if (Array.isArray(payload.awaitedPromptOptions) && payload.awaitedPromptOptions.every((option) => typeof option === "string")) {
5383
+ response.awaitedPromptOptions = payload.awaitedPromptOptions;
5384
+ } else {
5385
+ throw new Error('read-stream response has an invalid "awaitedPromptOptions"');
5386
+ }
5387
+ }
5329
5388
  if (payload.ptyDimensions !== void 0) {
5330
5389
  response.ptyDimensions = parseTerminalDimensionsWire(payload.ptyDimensions);
5331
5390
  }
@@ -5403,12 +5462,16 @@ function parseReadBoardResponsePayload(payload) {
5403
5462
  if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
5404
5463
  if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
5405
5464
  if (!Array.isArray(payload.backlog)) throw new Error('read-board response is missing "backlog"');
5465
+ if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
5466
+ throw new Error('read-board response has a non-boolean "showTicketNumbers"');
5467
+ }
5406
5468
  return {
5407
5469
  projectId: payload.projectId,
5408
5470
  columns: payload.columns.map((column) => parseBoardColumnWire(column)),
5409
5471
  tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
5410
5472
  backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
5411
- ...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {}
5473
+ ...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
5474
+ ...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
5412
5475
  };
5413
5476
  }
5414
5477
  function parseReadBoardRequestPayload(payload) {
@@ -5527,6 +5590,12 @@ function parseRegisterPushRequestPayload(payload) {
5527
5590
  }
5528
5591
  request.platform = payload.platform;
5529
5592
  }
5593
+ if (payload.categories !== void 0) {
5594
+ if (!Array.isArray(payload.categories) || !payload.categories.every((entry) => typeof entry === "string")) {
5595
+ throw new Error('register-push payload has a malformed "categories"');
5596
+ }
5597
+ request.categories = payload.categories.filter(isPushCategory);
5598
+ }
5530
5599
  if (request.action === "register") {
5531
5600
  if (request.expoPushToken === void 0) throw new Error('register-push register payload missing "expoPushToken"');
5532
5601
  if (request.pushKeyBase64 === void 0) throw new Error('register-push register payload missing "pushKeyBase64"');
@@ -5561,56 +5630,6 @@ function parseCapabilityRequestPayload(verb, payload) {
5561
5630
  }
5562
5631
  }
5563
5632
 
5564
- // src/crypto/push-envelope.ts
5565
- var PUSH_CATEGORIES = ["permission-needed", "agent-question", "turn-complete", "session-failed"];
5566
- function isPushCategory(value) {
5567
- return typeof value === "string" && PUSH_CATEGORIES.includes(value);
5568
- }
5569
- var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5570
- var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5571
- function requireEnvelopeString(record, field) {
5572
- const value = record[field];
5573
- if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
5574
- return value;
5575
- }
5576
- function parsePushEnvelopePlaintext(value) {
5577
- if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
5578
- if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
5579
- if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
5580
- throw new Error('push envelope plaintext has an invalid "sentAt"');
5581
- }
5582
- return {
5583
- category: value.category,
5584
- projectId: requireEnvelopeString(value, "projectId"),
5585
- taskId: requireEnvelopeString(value, "taskId"),
5586
- sessionId: requireEnvelopeString(value, "sessionId"),
5587
- taskTitle: requireEnvelopeString(value, "taskTitle"),
5588
- detail: requireEnvelopeString(value, "detail"),
5589
- sentAt: value.sentAt
5590
- };
5591
- }
5592
- function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
5593
- const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
5594
- const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
5595
- const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
5596
- return base64UrlEncode(concatBytes4(nonce, ciphertext));
5597
- }
5598
- function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
5599
- const bytes = base64UrlDecode(blob);
5600
- if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
5601
- throw new Error("push envelope blob is too short");
5602
- }
5603
- const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
5604
- const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
5605
- const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
5606
- const decoded = JSON.parse(new TextDecoder().decode(decrypted));
5607
- const plaintext = parsePushEnvelopePlaintext(decoded);
5608
- const now = Date.now();
5609
- if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
5610
- if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
5611
- return plaintext;
5612
- }
5613
-
5614
5633
  // src/pairing/qr.ts
5615
5634
  var PAIRING_URI_SCHEME = "kangentic-pair";
5616
5635
  var PAYLOAD_VERSION = 1;
@@ -5784,7 +5803,13 @@ function parseActivityEventPayload(payload) {
5784
5803
  case "permission": {
5785
5804
  if (typeof payload.promptId !== "string") throw new Error('permission payload is missing "promptId"');
5786
5805
  if (typeof payload.pending !== "boolean") throw new Error('permission payload is missing "pending"');
5787
- return { type: "permission", promptId: payload.promptId, pending: payload.pending };
5806
+ if (payload.options === void 0) {
5807
+ return { type: "permission", promptId: payload.promptId, pending: payload.pending };
5808
+ }
5809
+ if (!Array.isArray(payload.options) || !payload.options.every((option) => typeof option === "string")) {
5810
+ throw new Error('permission payload has an invalid "options"');
5811
+ }
5812
+ return { type: "permission", promptId: payload.promptId, pending: payload.pending, options: payload.options };
5788
5813
  }
5789
5814
  case "session-ended": {
5790
5815
  if (typeof payload.intentional !== "boolean") throw new Error('session-ended payload is missing "intentional"');
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import type { CapabilityVerb } from '../capabilities/verbs';
17
17
  import type { JsonValue } from './messages';
18
+ import { type PushCategory } from '../crypto/push-envelope';
18
19
  import { type ActivityReasonWire, type ActivityStateWire, type BacklogItemWire, type BoardColumnWire, type BoardTaskWire, type DiffFileContentWire, type DiffFileListWire, type SessionUsageWire, type TerminalDimensionsWire, type TranscriptEntryWire } from '../events/payloads';
19
20
  export interface ReadStreamRequestPayload {
20
21
  sessionId: string;
@@ -48,6 +49,15 @@ export interface ReadStreamResponsePayload {
48
49
  usage: SessionUsageWire | null;
49
50
  /** The live outstanding permission-prompt id (see answer-permission-prompt), or null when none is pending. */
50
51
  awaitedPromptId: string | null;
52
+ /**
53
+ * The awaited prompt's numbered option labels, parsed by the desktop from
54
+ * the pending dialog's PTY frame, in keystroke order: options[0] is the
55
+ * row answered with "1\r", options[1] with "2\r", and so on. Absent from
56
+ * pre-0.6.0 desktops; absent or null means unknown (no numbered dialog
57
+ * could be parsed), and the phone falls back to its blind
58
+ * approve/deny keystrokes. Only meaningful while awaitedPromptId is set.
59
+ */
60
+ awaitedPromptOptions?: string[] | null;
51
61
  /**
52
62
  * The PTY grid the scrollback bytes are laid out for. Absent from
53
63
  * pre-0.4.0 desktops; the phone then falls back to inferring a width
@@ -103,6 +113,12 @@ export interface ReadBoardSnapshotResponsePayload {
103
113
  backlog: BacklogItemWire[];
104
114
  /** The project's accent color ("#rrggbb"); same semantics as ReadBoardProjectSummary.color. Absent from pre-0.5.0 desktops. */
105
115
  projectColor?: string;
116
+ /**
117
+ * The desktop's Layout "Ticket Numbers" setting: whether task cards
118
+ * display their #N number. Absent from pre-0.6.0 desktops (treat as
119
+ * true, the desktop default).
120
+ */
121
+ showTicketNumbers?: boolean;
106
122
  }
107
123
  export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload;
108
124
  /** Phone-side narrowing of a read-board response (project list or board snapshot). Throws on a malformed required field. */
@@ -192,12 +208,20 @@ export interface BoardToolResponsePayload {
192
208
  * notification envelope with (see crypto/push-envelope.ts); 'unregister'
193
209
  * carries neither. The desktop keys the registration by the requesting
194
210
  * device's roster identity, never by anything in this payload.
211
+ *
212
+ * `categories`, when present, is the device's push preferences: the desktop
213
+ * filters outgoing notifications to this set before sending, so preference
214
+ * enforcement never depends on the receiving device (a future iOS
215
+ * Notification Service Extension stays preference-free). Absent means every
216
+ * category (the default, and what an older device that predates this field
217
+ * implicitly requests); an explicit empty array means none.
195
218
  */
196
219
  export interface RegisterPushRequestPayload {
197
220
  action: 'register' | 'unregister';
198
221
  expoPushToken?: string;
199
222
  pushKeyBase64?: string;
200
223
  platform?: 'android' | 'ios';
224
+ categories?: PushCategory[];
201
225
  }
202
226
  export interface RegisterPushResponsePayload {
203
227
  registered: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangentic/protocol",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Kangentic mobile bridge wire protocol: framing, Noise-based pairing and session handshakes, signed device roster, and capability verbs shared between desktop and mobile.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",