@kangentic/protocol 0.9.0 → 0.11.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.
@@ -13,8 +13,8 @@
13
13
  * this protocol surface honest: a tool renamed or removed desktop-side
14
14
  * fails that test instead of silently breaking phones.
15
15
  */
16
- export declare const BOARD_TOOL_READ_NAMES: readonly ["search_tasks", "find_task", "get_current_task", "get_task_stats", "get_usage_stats", "board_summary", "list_sessions", "get_session_history", "get_column_detail", "get_handoff_context", "get_transcript", "get_session_files", "get_session_events", "get_activity_intervals"];
17
- export declare const BOARD_TOOL_WRITE_NAMES: readonly ["create_task", "update_task", "delete_task", "link_pr", "remove_attachment", "update_column", "create_backlog_task", "promote_backlog", "update_backlog_item", "delete_backlog_item"];
16
+ export declare const BOARD_TOOL_READ_NAMES: readonly ["search_tasks", "find_task", "get_current_task", "get_task_stats", "get_usage_stats", "board_summary", "list_sessions", "get_session_history", "get_column_detail", "list_board_profiles", "get_handoff_context", "get_transcript", "get_session_files", "get_session_events", "get_activity_intervals"];
17
+ export declare const BOARD_TOOL_WRITE_NAMES: readonly ["create_task", "update_task", "delete_task", "link_pr", "remove_attachment", "update_column", "create_board_profile", "update_board_profile", "delete_board_profile", "create_backlog_task", "promote_backlog", "update_backlog_item", "delete_backlog_item"];
18
18
  export type BoardToolReadName = (typeof BOARD_TOOL_READ_NAMES)[number];
19
19
  export type BoardToolWriteName = (typeof BOARD_TOOL_WRITE_NAMES)[number];
20
20
  export type BoardToolName = BoardToolReadName | BoardToolWriteName;
@@ -167,6 +167,44 @@ export interface SessionUsageWire {
167
167
  effort?: string;
168
168
  };
169
169
  }
170
+ /**
171
+ * Phone-needed subset of the desktop's SessionSummary: what a task COST over
172
+ * its whole life, as opposed to SessionUsageWire's live snapshot of one
173
+ * running agent.
174
+ *
175
+ * Every figure is a lifetime aggregate across every session record of the
176
+ * task, which is what makes it meaningful for a completed one: a task worked
177
+ * across five `--resume` legs has five session rows, and reporting only the
178
+ * final leg would under-report everything. The desktop's getSummaryForTask
179
+ * owns those aggregation rules (SUM cost/duration/tool calls/lines, MAX files
180
+ * changed, and tokens taken as the latest row per session lineage so a
181
+ * resumed session is not double-counted) - they are not re-derived here.
182
+ *
183
+ * `sessionId` is the transcript ANCHOR, not a live session: it resolves
184
+ * through the desktop's session records, which outlive the agent, so it reads
185
+ * a finished task's conversation with nothing running. An archived task's
186
+ * `session_id` on the board is null by then, so this is the only handle to it.
187
+ */
188
+ export interface SessionSummaryWire {
189
+ sessionId: string;
190
+ totalCostUsd: number;
191
+ totalInputTokens: number;
192
+ totalOutputTokens: number;
193
+ modelDisplayName: string;
194
+ durationMs: number;
195
+ toolCallCount: number;
196
+ compactionCount: number;
197
+ linesAdded: number;
198
+ linesRemoved: number;
199
+ filesChanged: number;
200
+ /** ISO 8601. The task's creation, which is where its timeline starts. */
201
+ taskCreatedAt: string;
202
+ /** ISO 8601. Earliest session start across every leg. */
203
+ startedAt: string;
204
+ /** ISO 8601, or null while a leg is still open. Latest exit or suspend. */
205
+ exitedAt: string | null;
206
+ exitCode: number | null;
207
+ }
170
208
  /**
171
209
  * Loose mirror of the desktop's SessionEvent telemetry entry. `type` is
172
210
  * deliberately a plain string, not the desktop's EventType enum, so a new
@@ -267,6 +305,16 @@ export declare function isActivityStateWire(value: unknown): value is ActivitySt
267
305
  export declare function isActivityReasonWire(value: unknown): value is ActivityReasonWire;
268
306
  /** Narrows a usage payload to the SessionUsageWire subset. Throws on a malformed required field. */
269
307
  export declare function parseSessionUsageWire(payload: JsonValue): SessionUsageWire;
308
+ /**
309
+ * Narrows a session-summary payload to SessionSummaryWire. Throws on a
310
+ * malformed required field.
311
+ *
312
+ * The numeric metrics are required rather than optional because the desktop
313
+ * COALESCEs each to 0 before it sends them, so "no data" arrives as a real
314
+ * zero. Making them optional here would invite a phone to render an em-dash
315
+ * where the honest answer is 0.
316
+ */
317
+ export declare function parseSessionSummaryWire(payload: JsonValue): SessionSummaryWire;
270
318
  /** Narrows a session-event payload to SessionEventWire. Throws on a malformed required field. */
271
319
  export declare function parseSessionEventWire(payload: JsonValue): SessionEventWire;
272
320
  /** Narrows one board-column row. Throws on a malformed required field. */
package/dist/index.cjs CHANGED
@@ -88,6 +88,7 @@ __export(index_exports, {
88
88
  parseReadStreamResponsePayload: () => parseReadStreamResponsePayload,
89
89
  parseRegisterPushRequestPayload: () => parseRegisterPushRequestPayload,
90
90
  parseSessionEventWire: () => parseSessionEventWire,
91
+ parseSessionSummaryWire: () => parseSessionSummaryWire,
91
92
  parseSessionUsageWire: () => parseSessionUsageWire,
92
93
  parseTerminalDimensionsWire: () => parseTerminalDimensionsWire,
93
94
  parseTranscriptEntriesWire: () => parseTranscriptEntriesWire,
@@ -5364,6 +5365,26 @@ function parseSessionUsageWire(payload) {
5364
5365
  }
5365
5366
  };
5366
5367
  }
5368
+ function parseSessionSummaryWire(payload) {
5369
+ if (!isRecord(payload)) throw new Error("session summary payload must be an object");
5370
+ return {
5371
+ sessionId: requireString(payload, "sessionId", "session summary"),
5372
+ totalCostUsd: requireNumber(payload, "totalCostUsd", "session summary"),
5373
+ totalInputTokens: requireNumber(payload, "totalInputTokens", "session summary"),
5374
+ totalOutputTokens: requireNumber(payload, "totalOutputTokens", "session summary"),
5375
+ modelDisplayName: requireString(payload, "modelDisplayName", "session summary"),
5376
+ durationMs: requireNumber(payload, "durationMs", "session summary"),
5377
+ toolCallCount: requireNumber(payload, "toolCallCount", "session summary"),
5378
+ compactionCount: requireNumber(payload, "compactionCount", "session summary"),
5379
+ linesAdded: requireNumber(payload, "linesAdded", "session summary"),
5380
+ linesRemoved: requireNumber(payload, "linesRemoved", "session summary"),
5381
+ filesChanged: requireNumber(payload, "filesChanged", "session summary"),
5382
+ taskCreatedAt: requireString(payload, "taskCreatedAt", "session summary"),
5383
+ startedAt: requireString(payload, "startedAt", "session summary"),
5384
+ exitedAt: typeof payload.exitedAt === "string" ? payload.exitedAt : null,
5385
+ exitCode: typeof payload.exitCode === "number" ? payload.exitCode : null
5386
+ };
5387
+ }
5367
5388
  function parseSessionEventWire(payload) {
5368
5389
  if (!isRecord(payload)) throw new Error("session event payload must be an object");
5369
5390
  return {
@@ -5566,6 +5587,18 @@ function parseAccentColor(value, context) {
5566
5587
  }
5567
5588
  function parseReadBoardResponsePayload(payload) {
5568
5589
  if (!isRecord(payload)) throw new Error("read-board response must be an object");
5590
+ if (Array.isArray(payload.archivedTasks)) {
5591
+ if (typeof payload.projectId !== "string") throw new Error('read-board archived response is missing "projectId"');
5592
+ if (typeof payload.archivedTotalCount !== "number") {
5593
+ throw new Error('read-board archived response is missing "archivedTotalCount"');
5594
+ }
5595
+ return {
5596
+ projectId: payload.projectId,
5597
+ archivedTasks: payload.archivedTasks.map((task) => parseBoardTaskWire(task)),
5598
+ archivedTotalCount: payload.archivedTotalCount,
5599
+ summariesByTaskId: parseSummariesByTaskId(payload.summariesByTaskId)
5600
+ };
5601
+ }
5569
5602
  if (Array.isArray(payload.projects)) {
5570
5603
  const projects = payload.projects.map((project, index) => {
5571
5604
  if (!isRecord(project) || typeof project.id !== "string" || typeof project.name !== "string") {
@@ -5574,10 +5607,15 @@ function parseReadBoardResponsePayload(payload) {
5574
5607
  return {
5575
5608
  id: project.id,
5576
5609
  name: project.name,
5577
- ...project.color !== void 0 ? { color: parseAccentColor(project.color, `read-board project ${index}`) } : {}
5610
+ ...project.color !== void 0 ? { color: parseAccentColor(project.color, `read-board project ${index}`) } : {},
5611
+ ...typeof project.groupId === "string" ? { groupId: project.groupId } : {},
5612
+ ...typeof project.position === "number" ? { position: project.position } : {}
5578
5613
  };
5579
5614
  });
5580
- return { projects };
5615
+ const groups = Array.isArray(payload.groups) ? payload.groups.flatMap(
5616
+ (group) => isRecord(group) && typeof group.id === "string" && typeof group.name === "string" && typeof group.position === "number" ? [{ id: group.id, name: group.name, position: group.position }] : []
5617
+ ) : void 0;
5618
+ return { projects, ...groups !== void 0 ? { groups } : {} };
5581
5619
  }
5582
5620
  if (typeof payload.projectId !== "string") throw new Error('read-board response is missing "projectId"');
5583
5621
  if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
@@ -5610,18 +5648,42 @@ function parseTaskCountsByColumnId(value) {
5610
5648
  }
5611
5649
  return counts;
5612
5650
  }
5651
+ function parseSummariesByTaskId(value) {
5652
+ if (value === void 0) return {};
5653
+ if (!isRecord(value)) throw new Error('read-board archived response has a non-object "summariesByTaskId"');
5654
+ const summaries = {};
5655
+ for (const [taskId, summary] of Object.entries(value)) {
5656
+ try {
5657
+ summaries[taskId] = parseSessionSummaryWire(summary);
5658
+ } catch {
5659
+ }
5660
+ }
5661
+ return summaries;
5662
+ }
5613
5663
  function parseReadBoardRequestPayload(payload) {
5614
5664
  if (!isRecord(payload)) throw new Error("read-board payload must be an object");
5615
5665
  if (payload.projectId !== void 0 && typeof payload.projectId !== "string") {
5616
5666
  throw new Error('read-board payload has a non-string "projectId"');
5617
5667
  }
5618
- if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe") {
5668
+ if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe" && payload.action !== "archived") {
5619
5669
  throw new Error('read-board payload has an invalid "action"');
5620
5670
  }
5621
5671
  if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
5622
5672
  throw new Error('read-board payload has an invalid "view"');
5623
5673
  }
5624
- return { projectId: payload.projectId, action: payload.action, view: payload.view };
5674
+ if (payload.limit !== void 0 && (typeof payload.limit !== "number" || !Number.isInteger(payload.limit) || payload.limit < 1)) {
5675
+ throw new Error('read-board payload has an invalid "limit"');
5676
+ }
5677
+ if (payload.offset !== void 0 && (typeof payload.offset !== "number" || !Number.isInteger(payload.offset) || payload.offset < 0)) {
5678
+ throw new Error('read-board payload has an invalid "offset"');
5679
+ }
5680
+ return {
5681
+ projectId: payload.projectId,
5682
+ action: payload.action,
5683
+ view: payload.view,
5684
+ limit: payload.limit,
5685
+ offset: payload.offset
5686
+ };
5625
5687
  }
5626
5688
  function parseReadDiffResponsePayload(payload) {
5627
5689
  if (!isRecord(payload)) throw new Error("read-diff response must be an object");
@@ -5942,6 +6004,7 @@ var BOARD_TOOL_READ_NAMES = [
5942
6004
  "list_sessions",
5943
6005
  "get_session_history",
5944
6006
  "get_column_detail",
6007
+ "list_board_profiles",
5945
6008
  "get_handoff_context",
5946
6009
  "get_transcript",
5947
6010
  "get_session_files",
@@ -5955,6 +6018,9 @@ var BOARD_TOOL_WRITE_NAMES = [
5955
6018
  "link_pr",
5956
6019
  "remove_attachment",
5957
6020
  "update_column",
6021
+ "create_board_profile",
6022
+ "update_board_profile",
6023
+ "delete_board_profile",
5958
6024
  "create_backlog_task",
5959
6025
  "promote_backlog",
5960
6026
  "update_backlog_item",
@@ -6122,6 +6188,7 @@ function isBridgeEvent(value) {
6122
6188
  parseReadStreamResponsePayload,
6123
6189
  parseRegisterPushRequestPayload,
6124
6190
  parseSessionEventWire,
6191
+ parseSessionSummaryWire,
6125
6192
  parseSessionUsageWire,
6126
6193
  parseTerminalDimensionsWire,
6127
6194
  parseTranscriptEntriesWire,
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export type { JsonValue, BridgeMessage, HeartbeatMessage, CapabilityRequestMessa
12
12
  export { encodeMessage, decodeMessage, 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, type CapabilityRequestPayloadMap, type CapabilityResponsePayloadMap, type ReadStreamRequestPayload, type ReadStreamResponsePayload, type ReadStreamSessionStatusWire, type TranscriptWindowResponsePayload, type ReadBoardRequestPayload, type ReadBoardView, type ReadBoardProjectSummary, type ReadBoardProjectListResponsePayload, type ReadBoardSnapshotResponsePayload, 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, } from './wire/payloads';
15
+ export { parseCapabilityRequestPayload, parseReadStreamResponsePayload, parseReadBoardResponsePayload, parseReadDiffResponsePayload, parseTranscriptWindowResponsePayload, parseRegisterPushRequestPayload, 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, } from './wire/payloads';
16
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';
17
17
  export { PAIRING_URI_SCHEME, encodePairingQrPayload, decodePairingQrPayload, type PairingQrPayload, } from './pairing/qr';
18
18
  export { sealPairingConfirm, openPairingConfirm } from './pairing/confirm';
@@ -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, parseSessionEventWire, parseTerminalDimensionsWire, parseBoardColumnWire, parseBoardTaskWire, parseBacklogItemWire, parseDiffFileListWire, parseDiffFileContentWire, type TranscriptBlockWire, type TranscriptEntryWire, type TranscriptTurnUsageWire, type TranscriptSystemSubtypeWire, type ActivityStateWire, type ActivityReasonWire, type SessionUsageWire, type SessionEventWire, type TerminalDimensionsWire, type BoardColumnWire, 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, parseBacklogItemWire, parseDiffFileListWire, parseDiffFileContentWire, type TranscriptBlockWire, type TranscriptEntryWire, type TranscriptTurnUsageWire, type TranscriptSystemSubtypeWire, type ActivityStateWire, type ActivityReasonWire, type SessionUsageWire, type SessionSummaryWire, type SessionEventWire, type TerminalDimensionsWire, type BoardColumnWire, type BoardTaskWire, 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
@@ -5248,6 +5248,26 @@ function parseSessionUsageWire(payload) {
5248
5248
  }
5249
5249
  };
5250
5250
  }
5251
+ function parseSessionSummaryWire(payload) {
5252
+ if (!isRecord(payload)) throw new Error("session summary payload must be an object");
5253
+ return {
5254
+ sessionId: requireString(payload, "sessionId", "session summary"),
5255
+ totalCostUsd: requireNumber(payload, "totalCostUsd", "session summary"),
5256
+ totalInputTokens: requireNumber(payload, "totalInputTokens", "session summary"),
5257
+ totalOutputTokens: requireNumber(payload, "totalOutputTokens", "session summary"),
5258
+ modelDisplayName: requireString(payload, "modelDisplayName", "session summary"),
5259
+ durationMs: requireNumber(payload, "durationMs", "session summary"),
5260
+ toolCallCount: requireNumber(payload, "toolCallCount", "session summary"),
5261
+ compactionCount: requireNumber(payload, "compactionCount", "session summary"),
5262
+ linesAdded: requireNumber(payload, "linesAdded", "session summary"),
5263
+ linesRemoved: requireNumber(payload, "linesRemoved", "session summary"),
5264
+ filesChanged: requireNumber(payload, "filesChanged", "session summary"),
5265
+ taskCreatedAt: requireString(payload, "taskCreatedAt", "session summary"),
5266
+ startedAt: requireString(payload, "startedAt", "session summary"),
5267
+ exitedAt: typeof payload.exitedAt === "string" ? payload.exitedAt : null,
5268
+ exitCode: typeof payload.exitCode === "number" ? payload.exitCode : null
5269
+ };
5270
+ }
5251
5271
  function parseSessionEventWire(payload) {
5252
5272
  if (!isRecord(payload)) throw new Error("session event payload must be an object");
5253
5273
  return {
@@ -5450,6 +5470,18 @@ function parseAccentColor(value, context) {
5450
5470
  }
5451
5471
  function parseReadBoardResponsePayload(payload) {
5452
5472
  if (!isRecord(payload)) throw new Error("read-board response must be an object");
5473
+ if (Array.isArray(payload.archivedTasks)) {
5474
+ if (typeof payload.projectId !== "string") throw new Error('read-board archived response is missing "projectId"');
5475
+ if (typeof payload.archivedTotalCount !== "number") {
5476
+ throw new Error('read-board archived response is missing "archivedTotalCount"');
5477
+ }
5478
+ return {
5479
+ projectId: payload.projectId,
5480
+ archivedTasks: payload.archivedTasks.map((task) => parseBoardTaskWire(task)),
5481
+ archivedTotalCount: payload.archivedTotalCount,
5482
+ summariesByTaskId: parseSummariesByTaskId(payload.summariesByTaskId)
5483
+ };
5484
+ }
5453
5485
  if (Array.isArray(payload.projects)) {
5454
5486
  const projects = payload.projects.map((project, index) => {
5455
5487
  if (!isRecord(project) || typeof project.id !== "string" || typeof project.name !== "string") {
@@ -5458,10 +5490,15 @@ function parseReadBoardResponsePayload(payload) {
5458
5490
  return {
5459
5491
  id: project.id,
5460
5492
  name: project.name,
5461
- ...project.color !== void 0 ? { color: parseAccentColor(project.color, `read-board project ${index}`) } : {}
5493
+ ...project.color !== void 0 ? { color: parseAccentColor(project.color, `read-board project ${index}`) } : {},
5494
+ ...typeof project.groupId === "string" ? { groupId: project.groupId } : {},
5495
+ ...typeof project.position === "number" ? { position: project.position } : {}
5462
5496
  };
5463
5497
  });
5464
- return { projects };
5498
+ const groups = Array.isArray(payload.groups) ? payload.groups.flatMap(
5499
+ (group) => isRecord(group) && typeof group.id === "string" && typeof group.name === "string" && typeof group.position === "number" ? [{ id: group.id, name: group.name, position: group.position }] : []
5500
+ ) : void 0;
5501
+ return { projects, ...groups !== void 0 ? { groups } : {} };
5465
5502
  }
5466
5503
  if (typeof payload.projectId !== "string") throw new Error('read-board response is missing "projectId"');
5467
5504
  if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
@@ -5494,18 +5531,42 @@ function parseTaskCountsByColumnId(value) {
5494
5531
  }
5495
5532
  return counts;
5496
5533
  }
5534
+ function parseSummariesByTaskId(value) {
5535
+ if (value === void 0) return {};
5536
+ if (!isRecord(value)) throw new Error('read-board archived response has a non-object "summariesByTaskId"');
5537
+ const summaries = {};
5538
+ for (const [taskId, summary] of Object.entries(value)) {
5539
+ try {
5540
+ summaries[taskId] = parseSessionSummaryWire(summary);
5541
+ } catch {
5542
+ }
5543
+ }
5544
+ return summaries;
5545
+ }
5497
5546
  function parseReadBoardRequestPayload(payload) {
5498
5547
  if (!isRecord(payload)) throw new Error("read-board payload must be an object");
5499
5548
  if (payload.projectId !== void 0 && typeof payload.projectId !== "string") {
5500
5549
  throw new Error('read-board payload has a non-string "projectId"');
5501
5550
  }
5502
- if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe") {
5551
+ if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe" && payload.action !== "archived") {
5503
5552
  throw new Error('read-board payload has an invalid "action"');
5504
5553
  }
5505
5554
  if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
5506
5555
  throw new Error('read-board payload has an invalid "view"');
5507
5556
  }
5508
- return { projectId: payload.projectId, action: payload.action, view: payload.view };
5557
+ if (payload.limit !== void 0 && (typeof payload.limit !== "number" || !Number.isInteger(payload.limit) || payload.limit < 1)) {
5558
+ throw new Error('read-board payload has an invalid "limit"');
5559
+ }
5560
+ if (payload.offset !== void 0 && (typeof payload.offset !== "number" || !Number.isInteger(payload.offset) || payload.offset < 0)) {
5561
+ throw new Error('read-board payload has an invalid "offset"');
5562
+ }
5563
+ return {
5564
+ projectId: payload.projectId,
5565
+ action: payload.action,
5566
+ view: payload.view,
5567
+ limit: payload.limit,
5568
+ offset: payload.offset
5569
+ };
5509
5570
  }
5510
5571
  function parseReadDiffResponsePayload(payload) {
5511
5572
  if (!isRecord(payload)) throw new Error("read-diff response must be an object");
@@ -5826,6 +5887,7 @@ var BOARD_TOOL_READ_NAMES = [
5826
5887
  "list_sessions",
5827
5888
  "get_session_history",
5828
5889
  "get_column_detail",
5890
+ "list_board_profiles",
5829
5891
  "get_handoff_context",
5830
5892
  "get_transcript",
5831
5893
  "get_session_files",
@@ -5839,6 +5901,9 @@ var BOARD_TOOL_WRITE_NAMES = [
5839
5901
  "link_pr",
5840
5902
  "remove_attachment",
5841
5903
  "update_column",
5904
+ "create_board_profile",
5905
+ "update_board_profile",
5906
+ "delete_board_profile",
5842
5907
  "create_backlog_task",
5843
5908
  "promote_backlog",
5844
5909
  "update_backlog_item",
@@ -6005,6 +6070,7 @@ export {
6005
6070
  parseReadStreamResponsePayload,
6006
6071
  parseRegisterPushRequestPayload,
6007
6072
  parseSessionEventWire,
6073
+ parseSessionSummaryWire,
6008
6074
  parseSessionUsageWire,
6009
6075
  parseTerminalDimensionsWire,
6010
6076
  parseTranscriptEntriesWire,
@@ -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 SessionUsageWire, type TerminalDimensionsWire, type TranscriptEntryWire } from '../events/payloads';
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';
20
20
  export interface ReadStreamRequestPayload {
21
21
  sessionId: string;
22
22
  /**
@@ -116,8 +116,24 @@ export declare function parseTranscriptWindowResponsePayload(payload: JsonValue)
116
116
  export type ReadBoardView = 'full' | 'sessions';
117
117
  export interface ReadBoardRequestPayload {
118
118
  projectId?: string;
119
- /** Defaults to 'subscribe' when omitted. 'unsubscribe' only has an effect when projectId is set - the no-projectId project list is a one-shot read with no live feed to tear down. */
120
- action?: 'subscribe' | 'unsubscribe';
119
+ /**
120
+ * Defaults to 'subscribe' when omitted. 'unsubscribe' only has an effect
121
+ * when projectId is set - the no-projectId project list is a one-shot read
122
+ * with no live feed to tear down.
123
+ *
124
+ * 'archived' (protocol 0.10.0) is a one-shot page of the project's COMPLETED
125
+ * tasks, which no other projection carries: both 'full' and 'sessions' are
126
+ * built from the desktop's non-archived task list. It is deliberately not a
127
+ * field on the snapshot, because a board subscription re-snapshots on every
128
+ * board change - archived tasks would then repeat down the wire forever,
129
+ * which is the exact bloat the 0.9.0 projections removed. Completed work
130
+ * changes rarely and is read on demand.
131
+ */
132
+ action?: 'subscribe' | 'unsubscribe' | 'archived';
133
+ /** 'archived' only: page size, newest-archived first. The desktop clamps it. */
134
+ limit?: number;
135
+ /** 'archived' only: how many newest to skip, for paging. Defaults to 0. */
136
+ offset?: number;
121
137
  /**
122
138
  * subscribe only: the projection wanted (protocol 0.9.0). Omitted means the
123
139
  * legacy full board WITH the backlog.
@@ -135,9 +151,28 @@ export interface ReadBoardRequestPayload {
135
151
  */
136
152
  view?: ReadBoardView;
137
153
  }
154
+ /**
155
+ * A desktop project group ("KANGENTIC", "TROY WEB"), for a phone rendering
156
+ * its project list with the same structure the desktop sidebar shows
157
+ * (protocol 0.11.0).
158
+ */
159
+ export interface ReadBoardProjectGroup {
160
+ id: string;
161
+ name: string;
162
+ /** Display order among groups. */
163
+ position: number;
164
+ }
138
165
  export interface ReadBoardProjectSummary {
139
166
  id: string;
140
167
  name: string;
168
+ /**
169
+ * The group this project belongs to, or null/absent for an ungrouped one
170
+ * (protocol 0.11.0). Absent from an older desktop, which is indistinguishable
171
+ * from ungrouped and renders the same flat list as before.
172
+ */
173
+ groupId?: string | null;
174
+ /** Display order within the group, mirroring the desktop's own ordering (protocol 0.11.0). */
175
+ position?: number;
141
176
  /**
142
177
  * Accent color for this project ("#rrggbb"). Today the desktop derives
143
178
  * it deterministically from the project id; a user-set project color
@@ -149,6 +184,12 @@ export interface ReadBoardProjectSummary {
149
184
  /** Returned when the request omits projectId - the phone's project-bootstrap listing. */
150
185
  export interface ReadBoardProjectListResponsePayload {
151
186
  projects: ReadBoardProjectSummary[];
187
+ /**
188
+ * The desktop's project groups, in display order (protocol 0.11.0). Absent
189
+ * from an older desktop; a phone that gets none renders one flat list, which
190
+ * is exactly the pre-0.11.0 behaviour.
191
+ */
192
+ groups?: ReadBoardProjectGroup[];
152
193
  }
153
194
  /** Returned when the request carries a projectId - a snapshot of that project's board. */
154
195
  export interface ReadBoardSnapshotResponsePayload {
@@ -179,8 +220,33 @@ export interface ReadBoardSnapshotResponsePayload {
179
220
  */
180
221
  taskCountsByColumnId?: Record<string, number>;
181
222
  }
182
- export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload;
183
- /** Phone-side narrowing of a read-board response (project list or board snapshot). Throws on a malformed required field. */
223
+ /**
224
+ * Returned for `action: 'archived'` - one page of a project's completed tasks
225
+ * (protocol 0.10.0).
226
+ *
227
+ * These never appear in a board snapshot: the desktop builds those from its
228
+ * non-archived task list, so a phone that only ever subscribed has no way to
229
+ * know a completed task exists at all.
230
+ */
231
+ export interface ReadBoardArchivedResponsePayload {
232
+ projectId: string;
233
+ /** Newest-archived first. At most the requested `limit`. */
234
+ archivedTasks: BoardTaskWire[];
235
+ /**
236
+ * Total archived tasks in the project, NOT the length of the page above.
237
+ * Drives the column count and tells the phone whether another page exists.
238
+ */
239
+ archivedTotalCount: number;
240
+ /**
241
+ * Lifetime cost/duration/churn per task id, for the tasks on THIS page that
242
+ * have recorded metrics. Sparse on purpose: a task archived without ever
243
+ * running an agent has nothing to summarize and is simply absent, which is
244
+ * not an error.
245
+ */
246
+ summariesByTaskId: Record<string, SessionSummaryWire>;
247
+ }
248
+ export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload | ReadBoardArchivedResponsePayload;
249
+ /** Phone-side narrowing of a read-board response (project list, board snapshot, or archived page). Throws on a malformed required field. */
184
250
  export declare function parseReadBoardResponsePayload(payload: JsonValue): ReadBoardResponsePayload;
185
251
  /** 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. */
186
252
  export type ReadDiffScope = 'working' | 'staged' | 'branch';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangentic/protocol",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",
@@ -25,6 +25,7 @@
25
25
  ],
26
26
  "author": {
27
27
  "name": "Kangentic",
28
+ "email": "hello@kangentic.com",
28
29
  "url": "https://github.com/Kangentic"
29
30
  },
30
31
  "license": "AGPL-3.0-only",