@kangentic/protocol 0.19.0 → 0.20.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.
package/README.md CHANGED
@@ -25,7 +25,9 @@ model, and the honest relay-metadata statement.
25
25
  phone: it is a liveness/intent signal, not the security boundary. It opens only if both peers
26
26
  completed the same handshake transcript, which is exactly the property the SAS comparison
27
27
  already vouches for - the human's comparison remains the actual defense.
28
- - No shell, file, or arbitrary-command verb exists in the protocol.
28
+ - No shell, file, or arbitrary-command verb exists in the protocol. `command-terminal` starts the
29
+ project's agent CLI at the project root with no command, branch, or model named by the phone, and
30
+ the desktop ends the session when that agent exits.
29
31
 
30
32
  This package has not undergone a third-party security audit. Report suspected vulnerabilities
31
33
  per the repository's security policy rather than filing a public issue.
@@ -23,8 +23,16 @@
23
23
  * session exactly as the desktop's Pause button does, without moving the task,
24
24
  * and keeps the conversation for a later `start-session` Resume. Gate it on
25
25
  * `BoardTaskWire.pausable`.
26
+ *
27
+ * `command-terminal` starts or stops a Command Terminal: an AGENT CLI the
28
+ * desktop launches at a project's root, the same session the desktop's
29
+ * Ctrl+Shift+P terminal runs. It is not a shell verb. The phone names no
30
+ * command, branch, model or effort, and the desktop ends the session when its
31
+ * agent exits, so no bare shell is left behind for `interactive-terminal` to
32
+ * reach. Stop accepts only a Command Terminal's session. Gate the phone's
33
+ * Terminals button on `ReadBoardSnapshotResponsePayload.commandTerminalsOffered`.
26
34
  */
27
- export declare const CAPABILITY_VERBS: readonly ["read-stream", "read-board", "read-diff", "send-user-message", "move-task", "answer-permission-prompt", "interactive-terminal", "board-tool-read", "board-tool-write", "register-push", "start-session", "pause-session"];
35
+ export declare const CAPABILITY_VERBS: readonly ["read-stream", "read-board", "read-diff", "send-user-message", "move-task", "answer-permission-prompt", "interactive-terminal", "board-tool-read", "board-tool-write", "register-push", "start-session", "pause-session", "command-terminal"];
28
36
  export type CapabilityVerb = (typeof CAPABILITY_VERBS)[number];
29
37
  export declare function isCapabilityVerb(value: string): value is CapabilityVerb;
30
38
  /** Deny-by-default: a verb absent from the set is not authorized. */
@@ -5,6 +5,15 @@ export declare function isPushCategory(value: unknown): value is PushCategory;
5
5
  export declare const PUSH_ENVELOPE_MAX_AGE_MS: number;
6
6
  /** Tolerated forward clock skew; anything claiming to be further in the future is rejected. */
7
7
  export declare const PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS: number;
8
+ /**
9
+ * What kind of session a notification is about, when it is not a task's
10
+ * agent (protocol 0.20.0). `command-terminal`: a Command Terminal, so the tap
11
+ * opens that terminal's screen by `sessionId` (its `taskId` names no board
12
+ * task) and `taskTitle` carries the terminal's name.
13
+ */
14
+ export declare const PUSH_SESSION_KINDS: readonly ["command-terminal"];
15
+ export type PushSessionKind = (typeof PUSH_SESSION_KINDS)[number];
16
+ export declare function isPushSessionKind(value: unknown): value is PushSessionKind;
8
17
  /**
9
18
  * The real notification content, visible only on the sealing desktop and
10
19
  * the opening device. Everything the OS-visible push payload carries
@@ -15,10 +24,13 @@ export interface PushEnvelopePlaintext {
15
24
  projectId: string;
16
25
  taskId: string;
17
26
  sessionId: string;
27
+ /** The task's title, or for a `command-terminal` session the terminal's name. */
18
28
  taskTitle: string;
19
29
  detail: string;
20
30
  /** Wall-clock ms at seal time; openPushEnvelope enforces the freshness window from it. */
21
31
  sentAt: number;
32
+ /** Absent for a task's agent. See PUSH_SESSION_KINDS. */
33
+ sessionKind?: PushSessionKind;
22
34
  }
23
35
  /** Narrows a decrypted envelope's JSON to PushEnvelopePlaintext. Throws on any malformed field. */
24
36
  export declare function parsePushEnvelopePlaintext(value: unknown): PushEnvelopePlaintext;
@@ -198,8 +198,20 @@ export interface TerminalResizeEvent {
198
198
  /** The PTY's new grid; every terminal byte sent after this event is laid out for it. */
199
199
  payload: TerminalDimensionsWire;
200
200
  }
201
+ /**
202
+ * `command-terminals-changed` (protocol 0.20.0): one of the project's Command
203
+ * Terminals started, ended, or was named. `ids` holds the terminal's session id
204
+ * and the envelope carries no `taskId`, since a Command Terminal belongs to no
205
+ * task. The phone re-reads `commandTerminals` from the next snapshot. A change
206
+ * of activity does not fire it: the terminal's state and reason ride its own
207
+ * `read-stream` activity events, as a task agent's do, and the snapshot's
208
+ * `CommandTerminalWire.activity` covers the first paint.
209
+ *
210
+ * The union is closed, so a phone built before a kind existed drops that event
211
+ * (`isBridgeEvent` returns false) and keeps the rest of its feed.
212
+ */
201
213
  export interface BoardEventPayload {
202
- change: 'task-created' | 'task-updated' | 'task-deleted' | 'swimlane-updated' | 'backlog-changed';
214
+ change: 'task-created' | 'task-updated' | 'task-deleted' | 'swimlane-updated' | 'backlog-changed' | 'command-terminals-changed';
203
215
  ids: string[];
204
216
  }
205
217
  export interface BoardEvent {
@@ -208,9 +220,15 @@ export interface BoardEvent {
208
220
  taskId?: string;
209
221
  payload: BoardEventPayload;
210
222
  }
223
+ /**
224
+ * "Re-read the diff." A task's diff names its `taskId`. A project-folder diff
225
+ * (a `read-diff` request with no `taskId`, protocol 0.20.0, for a Command
226
+ * Terminal's Changes tab) names its `projectId` instead. Exactly one is set.
227
+ */
211
228
  export interface DiffEvent {
212
229
  kind: 'diff';
213
- taskId: string;
230
+ taskId?: string;
231
+ projectId?: string;
214
232
  payload: null;
215
233
  }
216
234
  export type BridgeEvent = TranscriptEvent | ActivityEvent | TerminalEvent | TerminalResizeEvent | BoardEvent | DiffEvent;
@@ -466,6 +466,35 @@ export interface BoardTaskWire {
466
466
  */
467
467
  pausable?: boolean | null;
468
468
  }
469
+ /**
470
+ * One live Command Terminal of a project (protocol 0.20.0): an agent the
471
+ * desktop runs at the project root with no task, started from the desktop's
472
+ * Ctrl+Shift+P layer or by a phone's `command-terminal` start. It is listed
473
+ * beside a project's tasks in a read-board snapshot (`commandTerminals`), and
474
+ * the Agents feed draws it with a "Command Terminal" pill.
475
+ *
476
+ * - `slot` is the terminal's durable number in its project, 1 to 4. It never
477
+ * renumbers while siblings start and stop.
478
+ * - `name` is what to show: the terminal's own name once it has one, else
479
+ * "Command Terminal N". `named` is true once it has its own name (the
480
+ * desktop derives one from the first prompt), which is when the phone's
481
+ * session header adds its pill.
482
+ * - `activity` is the raw state and reason, the same shape `read-stream`
483
+ * reports, so the phone formats the line ("Running Bash", "Awaiting
484
+ * permission") with the formatter it already uses for a task's agent. Both
485
+ * are null until the desktop's activity engine has classified the session.
486
+ */
487
+ export interface CommandTerminalWire {
488
+ sessionId: string;
489
+ projectId: string;
490
+ slot: number;
491
+ name: string;
492
+ named: boolean;
493
+ activity: {
494
+ state: ActivityStateWire | null;
495
+ reason: ActivityReasonWire | null;
496
+ };
497
+ }
469
498
  /** Phone-needed subset of the desktop's BacklogTask row. */
470
499
  export interface BacklogItemWire {
471
500
  id: string;
@@ -535,6 +564,12 @@ export declare function parseSessionEventWire(payload: JsonValue): SessionEventW
535
564
  export declare function parseBoardColumnWire(value: JsonValue): BoardColumnWire;
536
565
  /** Narrows one board-task row. Throws on a malformed required field. */
537
566
  export declare function parseBoardTaskWire(value: JsonValue): BoardTaskWire;
567
+ /**
568
+ * Narrows one Command Terminal row. Throws on a malformed required field. An
569
+ * unrecognized activity state or reason reads as null rather than throwing, so
570
+ * a reason kind a newer desktop adds costs the line, not the whole snapshot.
571
+ */
572
+ export declare function parseCommandTerminalWire(value: JsonValue): CommandTerminalWire;
538
573
  /** Narrows one backlog-item row. Throws on a malformed required field. */
539
574
  export declare function parseBacklogItemWire(value: JsonValue): BacklogItemWire;
540
575
  /** Narrows a read-diff file-list payload. Throws on a malformed required field. */
package/dist/index.cjs CHANGED
@@ -20,10 +20,13 @@ var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: tr
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AGENT_UNAVAILABLE_ERROR_CODE: () => AGENT_UNAVAILABLE_ERROR_CODE,
23
24
  BOARD_COLUMN_ROLES: () => BOARD_COLUMN_ROLES,
24
25
  BOARD_TOOL_READ_NAMES: () => BOARD_TOOL_READ_NAMES,
25
26
  BOARD_TOOL_WRITE_NAMES: () => BOARD_TOOL_WRITE_NAMES,
26
27
  CAPABILITY_VERBS: () => CAPABILITY_VERBS,
28
+ COMMAND_TERMINAL_LIMIT_ERROR_CODE: () => COMMAND_TERMINAL_LIMIT_ERROR_CODE,
29
+ COMMAND_TERMINAL_START_FAILED_ERROR_CODE: () => COMMAND_TERMINAL_START_FAILED_ERROR_CODE,
27
30
  COMPRESSION_THRESHOLD: () => COMPRESSION_THRESHOLD,
28
31
  CipherState: () => CipherState,
29
32
  ED25519_KEY_LENGTH: () => ED25519_KEY_LENGTH,
@@ -34,14 +37,18 @@ __export(index_exports, {
34
37
  MAX_DECODED_LENGTH: () => MAX_DECODED_LENGTH,
35
38
  MAX_FRAME_LENGTH: () => MAX_FRAME_LENGTH,
36
39
  MAX_RELAY_ADDRESS_LENGTH: () => MAX_RELAY_ADDRESS_LENGTH,
40
+ NOT_COMMAND_TERMINAL_ERROR_CODE: () => NOT_COMMAND_TERMINAL_ERROR_CODE,
37
41
  PAIRING_URI_SCHEME: () => PAIRING_URI_SCHEME,
38
42
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
39
43
  PUSH_CATEGORIES: () => PUSH_CATEGORIES,
40
44
  PUSH_ENVELOPE_MAX_AGE_MS: () => PUSH_ENVELOPE_MAX_AGE_MS,
41
45
  PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS: () => PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS,
46
+ PUSH_SESSION_KINDS: () => PUSH_SESSION_KINDS,
42
47
  RESPONSE_TOO_LARGE_ERROR_CODE: () => RESPONSE_TOO_LARGE_ERROR_CODE,
43
48
  SecretstreamState: () => SecretstreamState,
44
49
  SessionFrameKind: () => SessionFrameKind,
50
+ UNKNOWN_PROJECT_ERROR_CODE: () => UNKNOWN_PROJECT_ERROR_CODE,
51
+ UNKNOWN_SESSION_ERROR_CODE: () => UNKNOWN_SESSION_ERROR_CODE,
45
52
  UNSUPPORTED_VERB_ERROR_CODE: () => UNSUPPORTED_VERB_ERROR_CODE,
46
53
  UnsupportedVerbError: () => UnsupportedVerbError,
47
54
  X25519_KEY_LENGTH: () => X25519_KEY_LENGTH,
@@ -77,6 +84,7 @@ __export(index_exports, {
77
84
  isDoneRole: () => isDoneRole,
78
85
  isJsonValue: () => isJsonValue,
79
86
  isPushCategory: () => isPushCategory,
87
+ isPushSessionKind: () => isPushSessionKind,
80
88
  isReadStreamSessionStatusWire: () => isReadStreamSessionStatusWire,
81
89
  isRecord: () => isRecord,
82
90
  isRosterEntryExpired: () => isRosterEntryExpired,
@@ -90,6 +98,8 @@ __export(index_exports, {
90
98
  parseBoardColumnWire: () => parseBoardColumnWire,
91
99
  parseBoardTaskWire: () => parseBoardTaskWire,
92
100
  parseCapabilityRequestPayload: () => parseCapabilityRequestPayload,
101
+ parseCommandTerminalStartResponsePayload: () => parseCommandTerminalStartResponsePayload,
102
+ parseCommandTerminalWire: () => parseCommandTerminalWire,
93
103
  parseDiffFileContentWire: () => parseDiffFileContentWire,
94
104
  parseDiffFileListWire: () => parseDiffFileListWire,
95
105
  parsePauseSessionResponsePayload: () => parsePauseSessionResponsePayload,
@@ -4975,7 +4985,8 @@ var CAPABILITY_VERBS = [
4975
4985
  "board-tool-write",
4976
4986
  "register-push",
4977
4987
  "start-session",
4978
- "pause-session"
4988
+ "pause-session",
4989
+ "command-terminal"
4979
4990
  ];
4980
4991
  function isCapabilityVerb(value) {
4981
4992
  return CAPABILITY_VERBS.includes(value);
@@ -5013,6 +5024,12 @@ var JSON_OPEN_BRACE = 123;
5013
5024
  var DEFLATE_HEADER_LENGTH = 5;
5014
5025
  var UNSUPPORTED_VERB_ERROR_CODE = "unsupported-verb";
5015
5026
  var RESPONSE_TOO_LARGE_ERROR_CODE = "response-too-large";
5027
+ var UNKNOWN_PROJECT_ERROR_CODE = "unknown-project";
5028
+ var UNKNOWN_SESSION_ERROR_CODE = "unknown-session";
5029
+ var COMMAND_TERMINAL_LIMIT_ERROR_CODE = "command-terminal-limit";
5030
+ var NOT_COMMAND_TERMINAL_ERROR_CODE = "not-command-terminal";
5031
+ var AGENT_UNAVAILABLE_ERROR_CODE = "agent-unavailable";
5032
+ var COMMAND_TERMINAL_START_FAILED_ERROR_CODE = "command-terminal-start-failed";
5016
5033
  var UNSUPPORTED_VERB_ERROR_NAME = "UnsupportedVerbError";
5017
5034
  var UnsupportedVerbError = class extends Error {
5018
5035
  requestId;
@@ -5100,7 +5117,11 @@ function validateEvent(event) {
5100
5117
  return event;
5101
5118
  }
5102
5119
  case "diff": {
5103
- if (typeof event.taskId !== "string") throw new Error('"diff" event is missing "taskId"');
5120
+ if (event.taskId !== void 0 && typeof event.taskId !== "string") throw new Error('"diff" event has a non-string "taskId"');
5121
+ if (event.projectId !== void 0 && typeof event.projectId !== "string") throw new Error('"diff" event has a non-string "projectId"');
5122
+ if (event.taskId === void 0 === (event.projectId === void 0)) {
5123
+ throw new Error('"diff" event must carry exactly one of "taskId" and "projectId"');
5124
+ }
5104
5125
  return event;
5105
5126
  }
5106
5127
  default:
@@ -5213,6 +5234,10 @@ function isPushCategory(value) {
5213
5234
  }
5214
5235
  var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5215
5236
  var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5237
+ var PUSH_SESSION_KINDS = ["command-terminal"];
5238
+ function isPushSessionKind(value) {
5239
+ return typeof value === "string" && PUSH_SESSION_KINDS.includes(value);
5240
+ }
5216
5241
  function requireEnvelopeString(record, field) {
5217
5242
  const value = record[field];
5218
5243
  if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
@@ -5231,7 +5256,10 @@ function parsePushEnvelopePlaintext(value) {
5231
5256
  sessionId: requireEnvelopeString(value, "sessionId"),
5232
5257
  taskTitle: requireEnvelopeString(value, "taskTitle"),
5233
5258
  detail: requireEnvelopeString(value, "detail"),
5234
- sentAt: value.sentAt
5259
+ sentAt: value.sentAt,
5260
+ // An unknown kind (from a newer desktop) is dropped, not rejected, so the
5261
+ // notification still opens; the tap then routes as a task's would.
5262
+ ...isPushSessionKind(value.sessionKind) ? { sessionKind: value.sessionKind } : {}
5235
5263
  };
5236
5264
  }
5237
5265
  function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
@@ -5484,6 +5512,21 @@ function parseBoardTaskWire(value) {
5484
5512
  pausable: nullableBoolean(value, "pausable")
5485
5513
  };
5486
5514
  }
5515
+ function parseCommandTerminalWire(value) {
5516
+ if (!isRecord(value)) throw new Error("command terminal must be an object");
5517
+ const activity = isRecord(value.activity) ? value.activity : {};
5518
+ return {
5519
+ sessionId: requireString(value, "sessionId", "command terminal"),
5520
+ projectId: requireString(value, "projectId", "command terminal"),
5521
+ slot: requireNumber(value, "slot", "command terminal"),
5522
+ name: requireString(value, "name", "command terminal"),
5523
+ named: requireBoolean(value, "named", "command terminal"),
5524
+ activity: {
5525
+ state: isActivityStateWire(activity.state) ? activity.state : null,
5526
+ reason: isActivityReasonWire(activity.reason) ? activity.reason : null
5527
+ }
5528
+ };
5529
+ }
5487
5530
  function parseBacklogItemWire(value) {
5488
5531
  if (!isRecord(value)) throw new Error("backlog item must be an object");
5489
5532
  return {
@@ -5676,6 +5719,12 @@ function parseReadBoardResponsePayload(payload) {
5676
5719
  if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
5677
5720
  throw new Error('read-board response has an invalid "view"');
5678
5721
  }
5722
+ if (payload.commandTerminals !== void 0 && !Array.isArray(payload.commandTerminals)) {
5723
+ throw new Error('read-board response has a non-array "commandTerminals"');
5724
+ }
5725
+ if (payload.commandTerminalsOffered !== void 0 && typeof payload.commandTerminalsOffered !== "boolean") {
5726
+ throw new Error('read-board response has a non-boolean "commandTerminalsOffered"');
5727
+ }
5679
5728
  return {
5680
5729
  projectId: payload.projectId,
5681
5730
  columns: payload.columns.map((column) => parseBoardColumnWire(column)),
@@ -5684,7 +5733,9 @@ function parseReadBoardResponsePayload(payload) {
5684
5733
  ...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
5685
5734
  ...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {},
5686
5735
  ...payload.view !== void 0 ? { view: payload.view } : {},
5687
- ...payload.taskCountsByColumnId !== void 0 ? { taskCountsByColumnId: parseTaskCountsByColumnId(payload.taskCountsByColumnId) } : {}
5736
+ ...payload.taskCountsByColumnId !== void 0 ? { taskCountsByColumnId: parseTaskCountsByColumnId(payload.taskCountsByColumnId) } : {},
5737
+ ...payload.commandTerminals !== void 0 ? { commandTerminals: payload.commandTerminals.map((terminal) => parseCommandTerminalWire(terminal)) } : {},
5738
+ ...payload.commandTerminalsOffered !== void 0 ? { commandTerminalsOffered: payload.commandTerminalsOffered } : {}
5688
5739
  };
5689
5740
  }
5690
5741
  function parseTaskCountsByColumnId(value) {
@@ -5738,7 +5789,7 @@ function parseReadDiffResponsePayload(payload) {
5738
5789
  }
5739
5790
  function parseReadDiffRequestPayload(payload) {
5740
5791
  if (!isRecord(payload)) throw new Error("read-diff payload must be an object");
5741
- if (typeof payload.taskId !== "string") throw new Error('read-diff payload missing "taskId"');
5792
+ if (payload.taskId !== void 0 && typeof payload.taskId !== "string") throw new Error('read-diff payload has a non-string "taskId"');
5742
5793
  if (typeof payload.projectId !== "string") throw new Error('read-diff payload missing "projectId"');
5743
5794
  if (payload.filePath !== void 0 && typeof payload.filePath !== "string") {
5744
5795
  throw new Error('read-diff payload has a non-string "filePath"');
@@ -5750,7 +5801,7 @@ function parseReadDiffRequestPayload(payload) {
5750
5801
  throw new Error('read-diff payload has an invalid "action"');
5751
5802
  }
5752
5803
  return {
5753
- taskId: payload.taskId,
5804
+ ...payload.taskId !== void 0 ? { taskId: payload.taskId } : {},
5754
5805
  projectId: payload.projectId,
5755
5806
  filePath: payload.filePath,
5756
5807
  scope: payload.scope,
@@ -5802,6 +5853,28 @@ function parsePauseSessionRequestPayload(payload) {
5802
5853
  projectId: payload.projectId
5803
5854
  };
5804
5855
  }
5856
+ function parseCommandTerminalRequestPayload(payload) {
5857
+ if (!isRecord(payload)) throw new Error("command-terminal payload must be an object");
5858
+ if (payload.action === "start") {
5859
+ if (typeof payload.projectId !== "string") throw new Error('command-terminal start payload missing "projectId"');
5860
+ return { action: "start", projectId: payload.projectId };
5861
+ }
5862
+ if (payload.action === "stop") {
5863
+ if (typeof payload.sessionId !== "string") throw new Error('command-terminal stop payload missing "sessionId"');
5864
+ return { action: "stop", sessionId: payload.sessionId };
5865
+ }
5866
+ throw new Error('command-terminal payload has an invalid "action"');
5867
+ }
5868
+ function parseCommandTerminalStartResponsePayload(payload) {
5869
+ if (!isRecord(payload)) throw new Error("command-terminal start response must be an object");
5870
+ if (payload.ok !== true) throw new Error('command-terminal start response missing "ok"');
5871
+ if (typeof payload.sessionId !== "string") throw new Error('command-terminal start response missing "sessionId"');
5872
+ if (typeof payload.slot !== "number" || !Number.isInteger(payload.slot) || payload.slot < 1) {
5873
+ throw new Error('command-terminal start response has an invalid "slot"');
5874
+ }
5875
+ if (typeof payload.name !== "string") throw new Error('command-terminal start response missing "name"');
5876
+ return { ok: true, sessionId: payload.sessionId, slot: payload.slot, name: payload.name };
5877
+ }
5805
5878
  function parsePauseSessionResponsePayload(payload) {
5806
5879
  if (!isRecord(payload)) throw new Error("pause-session response must be an object");
5807
5880
  if (typeof payload.ok !== "boolean") throw new Error('pause-session response missing "ok"');
@@ -5906,6 +5979,8 @@ function parseCapabilityRequestPayload(verb, payload) {
5906
5979
  return parseStartSessionRequestPayload(payload);
5907
5980
  case "pause-session":
5908
5981
  return parsePauseSessionRequestPayload(payload);
5982
+ case "command-terminal":
5983
+ return parseCommandTerminalRequestPayload(payload);
5909
5984
  default: {
5910
5985
  const exhaustiveCheck = verb;
5911
5986
  throw new Error(`Unknown capability verb: ${String(exhaustiveCheck)}`);
@@ -6136,7 +6211,14 @@ function isBoardToolWriteName(value) {
6136
6211
  }
6137
6212
 
6138
6213
  // src/events/event.ts
6139
- var BOARD_CHANGES = ["task-created", "task-updated", "task-deleted", "swimlane-updated", "backlog-changed"];
6214
+ var BOARD_CHANGES = [
6215
+ "task-created",
6216
+ "task-updated",
6217
+ "task-deleted",
6218
+ "swimlane-updated",
6219
+ "backlog-changed",
6220
+ "command-terminals-changed"
6221
+ ];
6140
6222
  function parseActivityEventPayload(payload) {
6141
6223
  if (!isRecord(payload)) throw new Error("activity payload must be an object");
6142
6224
  switch (payload.type) {
@@ -6231,18 +6313,27 @@ function isBridgeEvent(value) {
6231
6313
  if (typeof change !== "string" || !BOARD_CHANGES.includes(change)) return false;
6232
6314
  return Array.isArray(value.payload.ids) && value.payload.ids.every((id) => typeof id === "string");
6233
6315
  }
6234
- case "diff":
6235
- return typeof value.taskId === "string" && value.payload === null;
6316
+ case "diff": {
6317
+ if (value.payload !== null) return false;
6318
+ const hasTaskId = typeof value.taskId === "string";
6319
+ const hasProjectId = typeof value.projectId === "string";
6320
+ if (value.taskId !== void 0 && !hasTaskId) return false;
6321
+ if (value.projectId !== void 0 && !hasProjectId) return false;
6322
+ return hasTaskId !== hasProjectId;
6323
+ }
6236
6324
  default:
6237
6325
  return false;
6238
6326
  }
6239
6327
  }
6240
6328
  // Annotate the CommonJS export names for ESM import in node:
6241
6329
  0 && (module.exports = {
6330
+ AGENT_UNAVAILABLE_ERROR_CODE,
6242
6331
  BOARD_COLUMN_ROLES,
6243
6332
  BOARD_TOOL_READ_NAMES,
6244
6333
  BOARD_TOOL_WRITE_NAMES,
6245
6334
  CAPABILITY_VERBS,
6335
+ COMMAND_TERMINAL_LIMIT_ERROR_CODE,
6336
+ COMMAND_TERMINAL_START_FAILED_ERROR_CODE,
6246
6337
  COMPRESSION_THRESHOLD,
6247
6338
  CipherState,
6248
6339
  ED25519_KEY_LENGTH,
@@ -6253,14 +6344,18 @@ function isBridgeEvent(value) {
6253
6344
  MAX_DECODED_LENGTH,
6254
6345
  MAX_FRAME_LENGTH,
6255
6346
  MAX_RELAY_ADDRESS_LENGTH,
6347
+ NOT_COMMAND_TERMINAL_ERROR_CODE,
6256
6348
  PAIRING_URI_SCHEME,
6257
6349
  PROTOCOL_VERSION,
6258
6350
  PUSH_CATEGORIES,
6259
6351
  PUSH_ENVELOPE_MAX_AGE_MS,
6260
6352
  PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS,
6353
+ PUSH_SESSION_KINDS,
6261
6354
  RESPONSE_TOO_LARGE_ERROR_CODE,
6262
6355
  SecretstreamState,
6263
6356
  SessionFrameKind,
6357
+ UNKNOWN_PROJECT_ERROR_CODE,
6358
+ UNKNOWN_SESSION_ERROR_CODE,
6264
6359
  UNSUPPORTED_VERB_ERROR_CODE,
6265
6360
  UnsupportedVerbError,
6266
6361
  X25519_KEY_LENGTH,
@@ -6296,6 +6391,7 @@ function isBridgeEvent(value) {
6296
6391
  isDoneRole,
6297
6392
  isJsonValue,
6298
6393
  isPushCategory,
6394
+ isPushSessionKind,
6299
6395
  isReadStreamSessionStatusWire,
6300
6396
  isRecord,
6301
6397
  isRosterEntryExpired,
@@ -6309,6 +6405,8 @@ function isBridgeEvent(value) {
6309
6405
  parseBoardColumnWire,
6310
6406
  parseBoardTaskWire,
6311
6407
  parseCapabilityRequestPayload,
6408
+ parseCommandTerminalStartResponsePayload,
6409
+ parseCommandTerminalWire,
6312
6410
  parseDiffFileContentWire,
6313
6411
  parseDiffFileListWire,
6314
6412
  parsePauseSessionResponsePayload,
package/dist/index.d.ts CHANGED
@@ -9,11 +9,11 @@ export { SecretstreamState, deriveSecretstreamPair, FrameTag, type SecretstreamD
9
9
  export { deriveShortAuthenticationString, type ShortAuthenticationString } from './crypto/sas';
10
10
  export { deriveSessionSlotId, derivePairingSlotId } from './crypto/slot';
11
11
  export type { JsonValue, BridgeMessage, HeartbeatMessage, CapabilityRequestMessage, CapabilityResponseMessage, CapabilityErrorCode, EventMessage } from './wire/messages';
12
- export { encodeMessage, encodeMessageFailure, decodeMessage, UnsupportedVerbError, isUnsupportedVerbError, UNSUPPORTED_VERB_ERROR_CODE, RESPONSE_TOO_LARGE_ERROR_CODE, MAX_FRAME_LENGTH, MAX_DECODED_LENGTH, COMPRESSION_THRESHOLD, } from './wire/framing';
12
+ export { encodeMessage, encodeMessageFailure, decodeMessage, UnsupportedVerbError, isUnsupportedVerbError, UNSUPPORTED_VERB_ERROR_CODE, RESPONSE_TOO_LARGE_ERROR_CODE, UNKNOWN_PROJECT_ERROR_CODE, UNKNOWN_SESSION_ERROR_CODE, COMMAND_TERMINAL_LIMIT_ERROR_CODE, NOT_COMMAND_TERMINAL_ERROR_CODE, AGENT_UNAVAILABLE_ERROR_CODE, COMMAND_TERMINAL_START_FAILED_ERROR_CODE, MAX_FRAME_LENGTH, MAX_DECODED_LENGTH, COMPRESSION_THRESHOLD, } from './wire/framing';
13
13
  export { isJsonValue, isRecord } from './wire/json-value';
14
14
  export { SessionFrameKind, wrapSessionFrame, unwrapSessionFrame } from './wire/session-frame';
15
- export { parseCapabilityRequestPayload, parseReadStreamResponsePayload, parseReadBoardResponsePayload, parseReadDiffResponsePayload, parseTranscriptWindowResponsePayload, parseRegisterPushRequestPayload, parseStartSessionResponsePayload, parsePauseSessionResponsePayload, isReadStreamSessionStatusWire, type CapabilityRequestPayloadMap, type CapabilityResponsePayloadMap, type ReadStreamRequestPayload, type ReadStreamResponsePayload, type ReadStreamSessionStatusWire, type TranscriptWindowResponsePayload, type ReadBoardRequestPayload, type ReadBoardView, type ReadBoardProjectSummary, type ReadBoardProjectGroup, type ReadBoardProjectListResponsePayload, type ReadBoardSnapshotResponsePayload, type ReadBoardArchivedResponsePayload, type ReadBoardResponsePayload, type ReadDiffScope, type ReadDiffRequestPayload, type ReadDiffResponsePayload, type SendUserMessageRequestPayload, type SendUserMessageResponsePayload, type MoveTaskRequestPayload, type MoveTaskResponsePayload, type AnswerPermissionPromptRequestPayload, type AnswerPermissionPromptResponsePayload, type InteractiveTerminalRequestPayload, type InteractiveTerminalResponsePayload, type BoardToolRequestPayload, type BoardToolResponsePayload, type RegisterPushRequestPayload, type RegisterPushResponsePayload, type StartSessionRequestPayload, type StartSessionResponsePayload, type StartSessionOutcome, type PauseSessionRequestPayload, type PauseSessionResponsePayload, } from './wire/payloads';
16
- export { PUSH_CATEGORIES, isPushCategory, parsePushEnvelopePlaintext, sealPushEnvelope, openPushEnvelope, PUSH_ENVELOPE_MAX_AGE_MS, PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS, type PushCategory, type PushEnvelopePlaintext, } from './crypto/push-envelope';
15
+ export { parseCapabilityRequestPayload, parseReadStreamResponsePayload, parseReadBoardResponsePayload, parseReadDiffResponsePayload, parseTranscriptWindowResponsePayload, parseRegisterPushRequestPayload, parseStartSessionResponsePayload, parsePauseSessionResponsePayload, parseCommandTerminalStartResponsePayload, isReadStreamSessionStatusWire, type CapabilityRequestPayloadMap, type CapabilityResponsePayloadMap, type ReadStreamRequestPayload, type ReadStreamResponsePayload, type ReadStreamSessionStatusWire, type TranscriptWindowResponsePayload, type ReadBoardRequestPayload, type ReadBoardView, type ReadBoardProjectSummary, type ReadBoardProjectGroup, type ReadBoardProjectListResponsePayload, type ReadBoardSnapshotResponsePayload, type ReadBoardArchivedResponsePayload, type ReadBoardResponsePayload, type ReadDiffScope, type ReadDiffRequestPayload, type ReadDiffResponsePayload, type SendUserMessageRequestPayload, type SendUserMessageResponsePayload, type MoveTaskRequestPayload, type MoveTaskResponsePayload, type AnswerPermissionPromptRequestPayload, type AnswerPermissionPromptResponsePayload, type InteractiveTerminalRequestPayload, type InteractiveTerminalResponsePayload, type BoardToolRequestPayload, type BoardToolResponsePayload, type RegisterPushRequestPayload, type RegisterPushResponsePayload, type StartSessionRequestPayload, type StartSessionResponsePayload, type StartSessionOutcome, type PauseSessionRequestPayload, type PauseSessionResponsePayload, type CommandTerminalRequestPayload, type CommandTerminalStartResponsePayload, type CommandTerminalStopResponsePayload, type CommandTerminalResponsePayload, } from './wire/payloads';
16
+ export { PUSH_CATEGORIES, isPushCategory, PUSH_SESSION_KINDS, isPushSessionKind, parsePushEnvelopePlaintext, sealPushEnvelope, openPushEnvelope, PUSH_ENVELOPE_MAX_AGE_MS, PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS, type PushCategory, type PushSessionKind, type PushEnvelopePlaintext, } from './crypto/push-envelope';
17
17
  export { PAIRING_URI_SCHEME, encodePairingQrPayload, decodePairingQrPayload, type PairingQrPayload, } from './pairing/qr';
18
18
  export { sealPairingConfirm, openPairingConfirm } from './pairing/confirm';
19
19
  export { MAX_RELAY_ADDRESS_LENGTH, isSecureRelayAddress } from './pairing/relay-address';
@@ -22,5 +22,5 @@ export { formatKeyFingerprint } from './roster/fingerprint';
22
22
  export { CAPABILITY_VERBS, isCapabilityVerb, capabilitySetFromArray, capabilitySetToArray, type CapabilityVerb, type CapabilitySet, } from './capabilities/verbs';
23
23
  export { BOARD_TOOL_READ_NAMES, BOARD_TOOL_WRITE_NAMES, isBoardToolReadName, isBoardToolWriteName, type BoardToolReadName, type BoardToolWriteName, type BoardToolName, } from './capabilities/board-tools';
24
24
  export { isBridgeEvent, parseActivityEventPayload, type BridgeEvent, type TranscriptEvent, type ActivityEvent, type ActivityEventPayload, type TerminalEvent, type TerminalResizeEvent, type BoardEvent, type BoardEventPayload, type DiffEvent, } from './events/event';
25
- export { parseTranscriptEntriesWire, parseTranscriptEventPayload, type TranscriptEventPayload, type TranscriptUpsertWire, isActivityStateWire, isActivityReasonWire, parseSessionUsageWire, parseSessionSummaryWire, parseSessionEventWire, parseTerminalDimensionsWire, parseBoardColumnWire, parseBoardTaskWire, parseBacklogItemWire, parseDiffFileListWire, parseDiffFileContentWire, isTodoRole, isDoneRole, BOARD_COLUMN_ROLES, type TranscriptBlockWire, type TranscriptEntryWire, type TranscriptTurnUsageWire, type TranscriptSystemSubtypeWire, type ActivityStateWire, type ActivityReasonWire, type SessionUsageWire, type SessionSummaryWire, type SessionEventWire, type TerminalDimensionsWire, type BoardColumnWire, type BoardColumnRoleWire, type BoardTaskWire, type BacklogItemWire, type DiffFileStatusWire, type DiffFileWire, type DiffFileListWire, type DiffFileContentWire, } from './events/payloads';
25
+ export { parseTranscriptEntriesWire, parseTranscriptEventPayload, type TranscriptEventPayload, type TranscriptUpsertWire, isActivityStateWire, isActivityReasonWire, parseSessionUsageWire, parseSessionSummaryWire, parseSessionEventWire, parseTerminalDimensionsWire, parseBoardColumnWire, parseBoardTaskWire, parseCommandTerminalWire, parseBacklogItemWire, parseDiffFileListWire, parseDiffFileContentWire, isTodoRole, isDoneRole, BOARD_COLUMN_ROLES, type TranscriptBlockWire, type TranscriptEntryWire, type TranscriptTurnUsageWire, type TranscriptSystemSubtypeWire, type ActivityStateWire, type ActivityReasonWire, type SessionUsageWire, type SessionSummaryWire, type SessionEventWire, type TerminalDimensionsWire, type BoardColumnWire, type BoardColumnRoleWire, type BoardTaskWire, type CommandTerminalWire, type BacklogItemWire, type DiffFileStatusWire, type DiffFileWire, type DiffFileListWire, type DiffFileContentWire, } from './events/payloads';
26
26
  export type { Transport, TransportState, Unsubscribe } from './transport/transport';
package/dist/index.mjs CHANGED
@@ -4846,7 +4846,8 @@ var CAPABILITY_VERBS = [
4846
4846
  "board-tool-write",
4847
4847
  "register-push",
4848
4848
  "start-session",
4849
- "pause-session"
4849
+ "pause-session",
4850
+ "command-terminal"
4850
4851
  ];
4851
4852
  function isCapabilityVerb(value) {
4852
4853
  return CAPABILITY_VERBS.includes(value);
@@ -4884,6 +4885,12 @@ var JSON_OPEN_BRACE = 123;
4884
4885
  var DEFLATE_HEADER_LENGTH = 5;
4885
4886
  var UNSUPPORTED_VERB_ERROR_CODE = "unsupported-verb";
4886
4887
  var RESPONSE_TOO_LARGE_ERROR_CODE = "response-too-large";
4888
+ var UNKNOWN_PROJECT_ERROR_CODE = "unknown-project";
4889
+ var UNKNOWN_SESSION_ERROR_CODE = "unknown-session";
4890
+ var COMMAND_TERMINAL_LIMIT_ERROR_CODE = "command-terminal-limit";
4891
+ var NOT_COMMAND_TERMINAL_ERROR_CODE = "not-command-terminal";
4892
+ var AGENT_UNAVAILABLE_ERROR_CODE = "agent-unavailable";
4893
+ var COMMAND_TERMINAL_START_FAILED_ERROR_CODE = "command-terminal-start-failed";
4887
4894
  var UNSUPPORTED_VERB_ERROR_NAME = "UnsupportedVerbError";
4888
4895
  var UnsupportedVerbError = class extends Error {
4889
4896
  requestId;
@@ -4971,7 +4978,11 @@ function validateEvent(event) {
4971
4978
  return event;
4972
4979
  }
4973
4980
  case "diff": {
4974
- if (typeof event.taskId !== "string") throw new Error('"diff" event is missing "taskId"');
4981
+ if (event.taskId !== void 0 && typeof event.taskId !== "string") throw new Error('"diff" event has a non-string "taskId"');
4982
+ if (event.projectId !== void 0 && typeof event.projectId !== "string") throw new Error('"diff" event has a non-string "projectId"');
4983
+ if (event.taskId === void 0 === (event.projectId === void 0)) {
4984
+ throw new Error('"diff" event must carry exactly one of "taskId" and "projectId"');
4985
+ }
4975
4986
  return event;
4976
4987
  }
4977
4988
  default:
@@ -5084,6 +5095,10 @@ function isPushCategory(value) {
5084
5095
  }
5085
5096
  var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5086
5097
  var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5098
+ var PUSH_SESSION_KINDS = ["command-terminal"];
5099
+ function isPushSessionKind(value) {
5100
+ return typeof value === "string" && PUSH_SESSION_KINDS.includes(value);
5101
+ }
5087
5102
  function requireEnvelopeString(record, field) {
5088
5103
  const value = record[field];
5089
5104
  if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
@@ -5102,7 +5117,10 @@ function parsePushEnvelopePlaintext(value) {
5102
5117
  sessionId: requireEnvelopeString(value, "sessionId"),
5103
5118
  taskTitle: requireEnvelopeString(value, "taskTitle"),
5104
5119
  detail: requireEnvelopeString(value, "detail"),
5105
- sentAt: value.sentAt
5120
+ sentAt: value.sentAt,
5121
+ // An unknown kind (from a newer desktop) is dropped, not rejected, so the
5122
+ // notification still opens; the tap then routes as a task's would.
5123
+ ...isPushSessionKind(value.sessionKind) ? { sessionKind: value.sessionKind } : {}
5106
5124
  };
5107
5125
  }
5108
5126
  function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
@@ -5355,6 +5373,21 @@ function parseBoardTaskWire(value) {
5355
5373
  pausable: nullableBoolean(value, "pausable")
5356
5374
  };
5357
5375
  }
5376
+ function parseCommandTerminalWire(value) {
5377
+ if (!isRecord(value)) throw new Error("command terminal must be an object");
5378
+ const activity = isRecord(value.activity) ? value.activity : {};
5379
+ return {
5380
+ sessionId: requireString(value, "sessionId", "command terminal"),
5381
+ projectId: requireString(value, "projectId", "command terminal"),
5382
+ slot: requireNumber(value, "slot", "command terminal"),
5383
+ name: requireString(value, "name", "command terminal"),
5384
+ named: requireBoolean(value, "named", "command terminal"),
5385
+ activity: {
5386
+ state: isActivityStateWire(activity.state) ? activity.state : null,
5387
+ reason: isActivityReasonWire(activity.reason) ? activity.reason : null
5388
+ }
5389
+ };
5390
+ }
5358
5391
  function parseBacklogItemWire(value) {
5359
5392
  if (!isRecord(value)) throw new Error("backlog item must be an object");
5360
5393
  return {
@@ -5547,6 +5580,12 @@ function parseReadBoardResponsePayload(payload) {
5547
5580
  if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
5548
5581
  throw new Error('read-board response has an invalid "view"');
5549
5582
  }
5583
+ if (payload.commandTerminals !== void 0 && !Array.isArray(payload.commandTerminals)) {
5584
+ throw new Error('read-board response has a non-array "commandTerminals"');
5585
+ }
5586
+ if (payload.commandTerminalsOffered !== void 0 && typeof payload.commandTerminalsOffered !== "boolean") {
5587
+ throw new Error('read-board response has a non-boolean "commandTerminalsOffered"');
5588
+ }
5550
5589
  return {
5551
5590
  projectId: payload.projectId,
5552
5591
  columns: payload.columns.map((column) => parseBoardColumnWire(column)),
@@ -5555,7 +5594,9 @@ function parseReadBoardResponsePayload(payload) {
5555
5594
  ...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
5556
5595
  ...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {},
5557
5596
  ...payload.view !== void 0 ? { view: payload.view } : {},
5558
- ...payload.taskCountsByColumnId !== void 0 ? { taskCountsByColumnId: parseTaskCountsByColumnId(payload.taskCountsByColumnId) } : {}
5597
+ ...payload.taskCountsByColumnId !== void 0 ? { taskCountsByColumnId: parseTaskCountsByColumnId(payload.taskCountsByColumnId) } : {},
5598
+ ...payload.commandTerminals !== void 0 ? { commandTerminals: payload.commandTerminals.map((terminal) => parseCommandTerminalWire(terminal)) } : {},
5599
+ ...payload.commandTerminalsOffered !== void 0 ? { commandTerminalsOffered: payload.commandTerminalsOffered } : {}
5559
5600
  };
5560
5601
  }
5561
5602
  function parseTaskCountsByColumnId(value) {
@@ -5609,7 +5650,7 @@ function parseReadDiffResponsePayload(payload) {
5609
5650
  }
5610
5651
  function parseReadDiffRequestPayload(payload) {
5611
5652
  if (!isRecord(payload)) throw new Error("read-diff payload must be an object");
5612
- if (typeof payload.taskId !== "string") throw new Error('read-diff payload missing "taskId"');
5653
+ if (payload.taskId !== void 0 && typeof payload.taskId !== "string") throw new Error('read-diff payload has a non-string "taskId"');
5613
5654
  if (typeof payload.projectId !== "string") throw new Error('read-diff payload missing "projectId"');
5614
5655
  if (payload.filePath !== void 0 && typeof payload.filePath !== "string") {
5615
5656
  throw new Error('read-diff payload has a non-string "filePath"');
@@ -5621,7 +5662,7 @@ function parseReadDiffRequestPayload(payload) {
5621
5662
  throw new Error('read-diff payload has an invalid "action"');
5622
5663
  }
5623
5664
  return {
5624
- taskId: payload.taskId,
5665
+ ...payload.taskId !== void 0 ? { taskId: payload.taskId } : {},
5625
5666
  projectId: payload.projectId,
5626
5667
  filePath: payload.filePath,
5627
5668
  scope: payload.scope,
@@ -5673,6 +5714,28 @@ function parsePauseSessionRequestPayload(payload) {
5673
5714
  projectId: payload.projectId
5674
5715
  };
5675
5716
  }
5717
+ function parseCommandTerminalRequestPayload(payload) {
5718
+ if (!isRecord(payload)) throw new Error("command-terminal payload must be an object");
5719
+ if (payload.action === "start") {
5720
+ if (typeof payload.projectId !== "string") throw new Error('command-terminal start payload missing "projectId"');
5721
+ return { action: "start", projectId: payload.projectId };
5722
+ }
5723
+ if (payload.action === "stop") {
5724
+ if (typeof payload.sessionId !== "string") throw new Error('command-terminal stop payload missing "sessionId"');
5725
+ return { action: "stop", sessionId: payload.sessionId };
5726
+ }
5727
+ throw new Error('command-terminal payload has an invalid "action"');
5728
+ }
5729
+ function parseCommandTerminalStartResponsePayload(payload) {
5730
+ if (!isRecord(payload)) throw new Error("command-terminal start response must be an object");
5731
+ if (payload.ok !== true) throw new Error('command-terminal start response missing "ok"');
5732
+ if (typeof payload.sessionId !== "string") throw new Error('command-terminal start response missing "sessionId"');
5733
+ if (typeof payload.slot !== "number" || !Number.isInteger(payload.slot) || payload.slot < 1) {
5734
+ throw new Error('command-terminal start response has an invalid "slot"');
5735
+ }
5736
+ if (typeof payload.name !== "string") throw new Error('command-terminal start response missing "name"');
5737
+ return { ok: true, sessionId: payload.sessionId, slot: payload.slot, name: payload.name };
5738
+ }
5676
5739
  function parsePauseSessionResponsePayload(payload) {
5677
5740
  if (!isRecord(payload)) throw new Error("pause-session response must be an object");
5678
5741
  if (typeof payload.ok !== "boolean") throw new Error('pause-session response missing "ok"');
@@ -5777,6 +5840,8 @@ function parseCapabilityRequestPayload(verb, payload) {
5777
5840
  return parseStartSessionRequestPayload(payload);
5778
5841
  case "pause-session":
5779
5842
  return parsePauseSessionRequestPayload(payload);
5843
+ case "command-terminal":
5844
+ return parseCommandTerminalRequestPayload(payload);
5780
5845
  default: {
5781
5846
  const exhaustiveCheck = verb;
5782
5847
  throw new Error(`Unknown capability verb: ${String(exhaustiveCheck)}`);
@@ -6007,7 +6072,14 @@ function isBoardToolWriteName(value) {
6007
6072
  }
6008
6073
 
6009
6074
  // src/events/event.ts
6010
- var BOARD_CHANGES = ["task-created", "task-updated", "task-deleted", "swimlane-updated", "backlog-changed"];
6075
+ var BOARD_CHANGES = [
6076
+ "task-created",
6077
+ "task-updated",
6078
+ "task-deleted",
6079
+ "swimlane-updated",
6080
+ "backlog-changed",
6081
+ "command-terminals-changed"
6082
+ ];
6011
6083
  function parseActivityEventPayload(payload) {
6012
6084
  if (!isRecord(payload)) throw new Error("activity payload must be an object");
6013
6085
  switch (payload.type) {
@@ -6102,17 +6174,26 @@ function isBridgeEvent(value) {
6102
6174
  if (typeof change !== "string" || !BOARD_CHANGES.includes(change)) return false;
6103
6175
  return Array.isArray(value.payload.ids) && value.payload.ids.every((id) => typeof id === "string");
6104
6176
  }
6105
- case "diff":
6106
- return typeof value.taskId === "string" && value.payload === null;
6177
+ case "diff": {
6178
+ if (value.payload !== null) return false;
6179
+ const hasTaskId = typeof value.taskId === "string";
6180
+ const hasProjectId = typeof value.projectId === "string";
6181
+ if (value.taskId !== void 0 && !hasTaskId) return false;
6182
+ if (value.projectId !== void 0 && !hasProjectId) return false;
6183
+ return hasTaskId !== hasProjectId;
6184
+ }
6107
6185
  default:
6108
6186
  return false;
6109
6187
  }
6110
6188
  }
6111
6189
  export {
6190
+ AGENT_UNAVAILABLE_ERROR_CODE,
6112
6191
  BOARD_COLUMN_ROLES,
6113
6192
  BOARD_TOOL_READ_NAMES,
6114
6193
  BOARD_TOOL_WRITE_NAMES,
6115
6194
  CAPABILITY_VERBS,
6195
+ COMMAND_TERMINAL_LIMIT_ERROR_CODE,
6196
+ COMMAND_TERMINAL_START_FAILED_ERROR_CODE,
6116
6197
  COMPRESSION_THRESHOLD,
6117
6198
  CipherState,
6118
6199
  ED25519_KEY_LENGTH,
@@ -6123,14 +6204,18 @@ export {
6123
6204
  MAX_DECODED_LENGTH,
6124
6205
  MAX_FRAME_LENGTH,
6125
6206
  MAX_RELAY_ADDRESS_LENGTH,
6207
+ NOT_COMMAND_TERMINAL_ERROR_CODE,
6126
6208
  PAIRING_URI_SCHEME,
6127
6209
  PROTOCOL_VERSION,
6128
6210
  PUSH_CATEGORIES,
6129
6211
  PUSH_ENVELOPE_MAX_AGE_MS,
6130
6212
  PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS,
6213
+ PUSH_SESSION_KINDS,
6131
6214
  RESPONSE_TOO_LARGE_ERROR_CODE,
6132
6215
  SecretstreamState,
6133
6216
  SessionFrameKind,
6217
+ UNKNOWN_PROJECT_ERROR_CODE,
6218
+ UNKNOWN_SESSION_ERROR_CODE,
6134
6219
  UNSUPPORTED_VERB_ERROR_CODE,
6135
6220
  UnsupportedVerbError,
6136
6221
  X25519_KEY_LENGTH,
@@ -6166,6 +6251,7 @@ export {
6166
6251
  isDoneRole,
6167
6252
  isJsonValue,
6168
6253
  isPushCategory,
6254
+ isPushSessionKind,
6169
6255
  isReadStreamSessionStatusWire,
6170
6256
  isRecord,
6171
6257
  isRosterEntryExpired,
@@ -6179,6 +6265,8 @@ export {
6179
6265
  parseBoardColumnWire,
6180
6266
  parseBoardTaskWire,
6181
6267
  parseCapabilityRequestPayload,
6268
+ parseCommandTerminalStartResponsePayload,
6269
+ parseCommandTerminalWire,
6182
6270
  parseDiffFileContentWire,
6183
6271
  parseDiffFileListWire,
6184
6272
  parsePauseSessionResponsePayload,
@@ -8,6 +8,18 @@ export declare const COMPRESSION_THRESHOLD: number;
8
8
  export declare const UNSUPPORTED_VERB_ERROR_CODE: CapabilityErrorCode;
9
9
  /** The `code` a desktop puts on its refusal of a response that would not fit the frame caps above. */
10
10
  export declare const RESPONSE_TOO_LARGE_ERROR_CODE: CapabilityErrorCode;
11
+ /** The `code` on a refusal that named a project this desktop does not have. */
12
+ export declare const UNKNOWN_PROJECT_ERROR_CODE: CapabilityErrorCode;
13
+ /** The `code` on a refusal that named a session id that is not live. */
14
+ export declare const UNKNOWN_SESSION_ERROR_CODE: CapabilityErrorCode;
15
+ /** The `code` on a `command-terminal` start refused at the per-project cap. */
16
+ export declare const COMMAND_TERMINAL_LIMIT_ERROR_CODE: CapabilityErrorCode;
17
+ /** The `code` on a `command-terminal` stop that named a session which is not a Command Terminal. */
18
+ export declare const NOT_COMMAND_TERMINAL_ERROR_CODE: CapabilityErrorCode;
19
+ /** The `code` on a start refused because the project's agent CLI is not installed. */
20
+ export declare const AGENT_UNAVAILABLE_ERROR_CODE: CapabilityErrorCode;
21
+ /** The `code` on a `command-terminal` start that failed for any other reason. */
22
+ export declare const COMMAND_TERMINAL_START_FAILED_ERROR_CODE: CapabilityErrorCode;
11
23
  /**
12
24
  * Thrown by `decodeMessage` for a capability-request whose envelope is
13
25
  * well-formed (a string `requestId`, a string `verb`, a JSON `payload`) but
@@ -45,8 +45,24 @@ export interface CapabilityRequestMessage {
45
45
  * gone (a session ending, a project removed). It is a backstop. A desktop
46
46
  * shrinks a `read-stream` terminal seed to fit rather than refuse it, so a
47
47
  * phone's terminal subscribe is never answered with this code.
48
+ * - `unknown-project`: the request named a project id this desktop does not
49
+ * have (removed, or never registered).
50
+ * - `unknown-session`: the request named a session id that is not live. For a
51
+ * `command-terminal` stop it means the terminal already ended, so a phone
52
+ * treats it as stopped.
53
+ * - `command-terminal-limit`: the project already runs the most Command
54
+ * Terminals a project may have at once (four).
55
+ * - `not-command-terminal`: a `command-terminal` stop named a session that is
56
+ * not a Command Terminal (a task's agent). Nothing was stopped.
57
+ * - `agent-unavailable`: the project's agent CLI could not be found on the
58
+ * desktop, so nothing could be started.
59
+ * - `command-terminal-start-failed`: a `command-terminal` start failed for any
60
+ * other reason. `error` carries the desktop's text for logs.
61
+ *
62
+ * A peer must treat a code it does not know as a generic failure and show
63
+ * `error` or a plain line of its own: a newer desktop may add codes.
48
64
  */
49
- export type CapabilityErrorCode = 'unsupported-verb' | 'response-too-large';
65
+ export type CapabilityErrorCode = 'unsupported-verb' | 'response-too-large' | 'unknown-project' | 'unknown-session' | 'command-terminal-limit' | 'not-command-terminal' | 'agent-unavailable' | 'command-terminal-start-failed';
50
66
  export interface CapabilityResponseMessage {
51
67
  type: 'capability-response';
52
68
  requestId: string;
@@ -16,7 +16,7 @@
16
16
  import type { CapabilityVerb } from '../capabilities/verbs';
17
17
  import type { JsonValue } from './messages';
18
18
  import { type PushCategory } from '../crypto/push-envelope';
19
- import { type ActivityReasonWire, type ActivityStateWire, type BacklogItemWire, type BoardColumnWire, type BoardTaskWire, type DiffFileContentWire, type DiffFileListWire, type SessionSummaryWire, type SessionUsageWire, type TerminalDimensionsWire, type TranscriptEntryWire } from '../events/payloads';
19
+ import { type ActivityReasonWire, type ActivityStateWire, type BacklogItemWire, type BoardColumnWire, type BoardTaskWire, type CommandTerminalWire, type DiffFileContentWire, type DiffFileListWire, type SessionSummaryWire, type SessionUsageWire, type TerminalDimensionsWire, type TranscriptEntryWire } from '../events/payloads';
20
20
  export interface ReadStreamRequestPayload {
21
21
  sessionId: string;
22
22
  /**
@@ -251,6 +251,20 @@ export interface ReadBoardSnapshotResponsePayload {
251
251
  * card to a column needs the real length.
252
252
  */
253
253
  taskCountsByColumnId?: Record<string, number>;
254
+ /**
255
+ * The project's live Command Terminals, ordered by slot (protocol 0.20.0).
256
+ * Carried by every snapshot view, because the Agents feed lists them beside
257
+ * every project's task agents. Absent from an older desktop, which has no way
258
+ * to report them. A `command-terminals-changed` board event says to re-read it.
259
+ */
260
+ commandTerminals?: CommandTerminalWire[];
261
+ /**
262
+ * True when this desktop serves the `command-terminal` verb AND the device
263
+ * holds it (protocol 0.20.0). The phone keeps no grant list, so it shows its
264
+ * Terminals button on this flag, the way `BoardTaskWire.pausable` gates Pause.
265
+ * Absent from an older desktop: treat that as false.
266
+ */
267
+ commandTerminalsOffered?: boolean;
254
268
  }
255
269
  /**
256
270
  * Returned for `action: 'archived'` - one page of a project's completed tasks
@@ -283,7 +297,14 @@ export declare function parseReadBoardResponsePayload(payload: JsonValue): ReadB
283
297
  /** Mirrors DiffService's GitDiffScope in the desktop app; kept as a literal union here rather than an import, since the protocol package does not depend on the app. */
284
298
  export type ReadDiffScope = 'working' | 'staged' | 'branch';
285
299
  export interface ReadDiffRequestPayload {
286
- taskId: string;
300
+ /**
301
+ * The task whose worktree (or, for a task with none, whose base) to diff.
302
+ * Optional from protocol 0.20.0: with no `taskId` the desktop diffs the
303
+ * PROJECT FOLDER against the project's default base branch, exactly as a
304
+ * Command Terminal window's Changes panel does, and its live pings arrive
305
+ * as a `DiffEvent` carrying `projectId` instead of `taskId`.
306
+ */
307
+ taskId?: string;
287
308
  projectId: string;
288
309
  filePath?: string;
289
310
  scope?: ReadDiffScope;
@@ -363,6 +384,50 @@ export interface PauseSessionRequestPayload {
363
384
  export interface PauseSessionResponsePayload {
364
385
  ok: boolean;
365
386
  }
387
+ /**
388
+ * Start or stop a Command Terminal (protocol 0.20.0). See `command-terminal` in
389
+ * capabilities/verbs.ts for what one is.
390
+ *
391
+ * `start` names only the project. The desktop picks the slot, runs the
392
+ * project's default agent with its default model and effort, and starts on the
393
+ * project's default branch the same way its own New terminal does; there is no
394
+ * branch, model or effort parameter, and the parser below drops any such key.
395
+ * `stop` names the terminal's session and is refused for any other session.
396
+ */
397
+ export type CommandTerminalRequestPayload = {
398
+ action: 'start';
399
+ projectId: string;
400
+ } | {
401
+ action: 'stop';
402
+ sessionId: string;
403
+ };
404
+ /**
405
+ * A `start` is answered once the desktop has the session, before the agent has
406
+ * drawn anything, so the phone opens the terminal's screen at once and its
407
+ * stream shows the agent arriving. `slot` is the terminal's number in its
408
+ * project (1 to 4) and `name` its label, "Command Terminal N" until the
409
+ * desktop names it from its first prompt.
410
+ *
411
+ * Refusals carry a `code` on the capability response: `command-terminal-limit`,
412
+ * `unknown-project`, `agent-unavailable` or `command-terminal-start-failed`.
413
+ */
414
+ export interface CommandTerminalStartResponsePayload {
415
+ ok: true;
416
+ sessionId: string;
417
+ slot: number;
418
+ name: string;
419
+ }
420
+ /**
421
+ * A `stop` is answered once the desktop has ended the terminal. Refusals carry
422
+ * `unknown-session` (it already ended, so treat it as stopped) or
423
+ * `not-command-terminal`.
424
+ */
425
+ export interface CommandTerminalStopResponsePayload {
426
+ ok: true;
427
+ }
428
+ export type CommandTerminalResponsePayload = CommandTerminalStartResponsePayload | CommandTerminalStopResponsePayload;
429
+ /** Phone-side guard: narrows a decoded `command-terminal` start response before the phone opens the terminal. */
430
+ export declare function parseCommandTerminalStartResponsePayload(payload: JsonValue): CommandTerminalStartResponsePayload;
366
431
  /** Phone-side guard: narrows a decoded pause-session response before the phone acts on it. */
367
432
  export declare function parsePauseSessionResponsePayload(payload: JsonValue): PauseSessionResponsePayload;
368
433
  export interface AnswerPermissionPromptRequestPayload {
@@ -452,6 +517,7 @@ export interface CapabilityRequestPayloadMap {
452
517
  'register-push': RegisterPushRequestPayload;
453
518
  'start-session': StartSessionRequestPayload;
454
519
  'pause-session': PauseSessionRequestPayload;
520
+ 'command-terminal': CommandTerminalRequestPayload;
455
521
  }
456
522
  export interface CapabilityResponsePayloadMap {
457
523
  'read-stream': ReadStreamResponsePayload | TranscriptWindowResponsePayload;
@@ -466,6 +532,7 @@ export interface CapabilityResponsePayloadMap {
466
532
  'register-push': RegisterPushResponsePayload;
467
533
  'start-session': StartSessionResponsePayload;
468
534
  'pause-session': PauseSessionResponsePayload;
535
+ 'command-terminal': CommandTerminalResponsePayload;
469
536
  }
470
537
  /**
471
538
  * Validates and narrows a decoded capability-request's generic JsonValue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangentic/protocol",
3
- "version": "0.19.0",
3
+ "version": "0.20.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",