@kangentic/protocol 0.8.0 → 0.10.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
@@ -20,7 +20,11 @@ model, and the honest relay-metadata statement.
20
20
  - `Noise_KK_25519_ChaChaPoly_BLAKE2s` for ongoing sessions (mutual authentication by construction,
21
21
  no trust-on-first-use).
22
22
  - A token-bound Noise PSK handshake for the one-time pairing ceremony, confirmed by a Matrix-style
23
- short authentication string (SAS) on both screens.
23
+ short authentication string (SAS) compared on both screens - digits only, no emoji.
24
+ - A sealed pairing-confirm frame (`pairing/confirm.ts`), sent once the human taps Confirm on the
25
+ phone: it is a liveness/intent signal, not the security boundary. It opens only if both peers
26
+ completed the same handshake transcript, which is exactly the property the SAS comparison
27
+ already vouches for - the human's comparison remains the actual defense.
24
28
  - No shell, file, or arbitrary-command verb exists in the protocol.
25
29
 
26
30
  This package has not undergone a third-party security audit. Report suspected vulnerabilities
@@ -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
@@ -58,6 +58,7 @@ __export(index_exports, {
58
58
  encodeProtocolVersion: () => encodeProtocolVersion,
59
59
  encodeRosterEntryForSigning: () => encodeRosterEntryForSigning,
60
60
  findRosterDevice: () => findRosterDevice,
61
+ formatKeyFingerprint: () => formatKeyFingerprint,
61
62
  generateEd25519KeyPair: () => generateEd25519KeyPair,
62
63
  generateX25519KeyPair: () => generateX25519KeyPair,
63
64
  hexToBytes: () => hexToBytes,
@@ -72,6 +73,7 @@ __export(index_exports, {
72
73
  isRecord: () => isRecord,
73
74
  isRosterEntryExpired: () => isRosterEntryExpired,
74
75
  isSecureRelayAddress: () => isSecureRelayAddress,
76
+ openPairingConfirm: () => openPairingConfirm,
75
77
  openPushEnvelope: () => openPushEnvelope,
76
78
  parseActivityEventPayload: () => parseActivityEventPayload,
77
79
  parseBacklogItemWire: () => parseBacklogItemWire,
@@ -86,6 +88,7 @@ __export(index_exports, {
86
88
  parseReadStreamResponsePayload: () => parseReadStreamResponsePayload,
87
89
  parseRegisterPushRequestPayload: () => parseRegisterPushRequestPayload,
88
90
  parseSessionEventWire: () => parseSessionEventWire,
91
+ parseSessionSummaryWire: () => parseSessionSummaryWire,
89
92
  parseSessionUsageWire: () => parseSessionUsageWire,
90
93
  parseTerminalDimensionsWire: () => parseTerminalDimensionsWire,
91
94
  parseTranscriptEntriesWire: () => parseTranscriptEntriesWire,
@@ -93,6 +96,7 @@ __export(index_exports, {
93
96
  parseTranscriptWindowResponsePayload: () => parseTranscriptWindowResponsePayload,
94
97
  randomBytes: () => randomBytes4,
95
98
  rosterDeviceCapabilitySet: () => rosterDeviceCapabilitySet,
99
+ sealPairingConfirm: () => sealPairingConfirm,
96
100
  sealPushEnvelope: () => sealPushEnvelope,
97
101
  signRosterEntry: () => signRosterEntry,
98
102
  unwrapSessionFrame: () => unwrapSessionFrame,
@@ -5361,6 +5365,26 @@ function parseSessionUsageWire(payload) {
5361
5365
  }
5362
5366
  };
5363
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
+ }
5364
5388
  function parseSessionEventWire(payload) {
5365
5389
  if (!isRecord(payload)) throw new Error("session event payload must be an object");
5366
5390
  return {
@@ -5563,6 +5587,18 @@ function parseAccentColor(value, context) {
5563
5587
  }
5564
5588
  function parseReadBoardResponsePayload(payload) {
5565
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
+ }
5566
5602
  if (Array.isArray(payload.projects)) {
5567
5603
  const projects = payload.projects.map((project, index) => {
5568
5604
  if (!isRecord(project) || typeof project.id !== "string" || typeof project.name !== "string") {
@@ -5579,28 +5615,70 @@ function parseReadBoardResponsePayload(payload) {
5579
5615
  if (typeof payload.projectId !== "string") throw new Error('read-board response is missing "projectId"');
5580
5616
  if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
5581
5617
  if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
5582
- if (!Array.isArray(payload.backlog)) throw new Error('read-board response is missing "backlog"');
5618
+ if (payload.backlog !== void 0 && !Array.isArray(payload.backlog)) {
5619
+ throw new Error('read-board response has a non-array "backlog"');
5620
+ }
5583
5621
  if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
5584
5622
  throw new Error('read-board response has a non-boolean "showTicketNumbers"');
5585
5623
  }
5624
+ if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
5625
+ throw new Error('read-board response has an invalid "view"');
5626
+ }
5586
5627
  return {
5587
5628
  projectId: payload.projectId,
5588
5629
  columns: payload.columns.map((column) => parseBoardColumnWire(column)),
5589
5630
  tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
5590
- backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
5631
+ ...payload.backlog !== void 0 ? { backlog: payload.backlog.map((item) => parseBacklogItemWire(item)) } : {},
5591
5632
  ...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
5592
- ...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
5633
+ ...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {},
5634
+ ...payload.view !== void 0 ? { view: payload.view } : {},
5635
+ ...payload.taskCountsByColumnId !== void 0 ? { taskCountsByColumnId: parseTaskCountsByColumnId(payload.taskCountsByColumnId) } : {}
5593
5636
  };
5594
5637
  }
5638
+ function parseTaskCountsByColumnId(value) {
5639
+ if (!isRecord(value)) throw new Error('read-board response has a non-object "taskCountsByColumnId"');
5640
+ const counts = {};
5641
+ for (const [columnId, count] of Object.entries(value)) {
5642
+ if (typeof count === "number" && Number.isInteger(count) && count >= 0) counts[columnId] = count;
5643
+ }
5644
+ return counts;
5645
+ }
5646
+ function parseSummariesByTaskId(value) {
5647
+ if (value === void 0) return {};
5648
+ if (!isRecord(value)) throw new Error('read-board archived response has a non-object "summariesByTaskId"');
5649
+ const summaries = {};
5650
+ for (const [taskId, summary] of Object.entries(value)) {
5651
+ try {
5652
+ summaries[taskId] = parseSessionSummaryWire(summary);
5653
+ } catch {
5654
+ }
5655
+ }
5656
+ return summaries;
5657
+ }
5595
5658
  function parseReadBoardRequestPayload(payload) {
5596
5659
  if (!isRecord(payload)) throw new Error("read-board payload must be an object");
5597
5660
  if (payload.projectId !== void 0 && typeof payload.projectId !== "string") {
5598
5661
  throw new Error('read-board payload has a non-string "projectId"');
5599
5662
  }
5600
- if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe") {
5663
+ if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe" && payload.action !== "archived") {
5601
5664
  throw new Error('read-board payload has an invalid "action"');
5602
5665
  }
5603
- return { projectId: payload.projectId, action: payload.action };
5666
+ if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
5667
+ throw new Error('read-board payload has an invalid "view"');
5668
+ }
5669
+ if (payload.limit !== void 0 && (typeof payload.limit !== "number" || !Number.isInteger(payload.limit) || payload.limit < 1)) {
5670
+ throw new Error('read-board payload has an invalid "limit"');
5671
+ }
5672
+ if (payload.offset !== void 0 && (typeof payload.offset !== "number" || !Number.isInteger(payload.offset) || payload.offset < 0)) {
5673
+ throw new Error('read-board payload has an invalid "offset"');
5674
+ }
5675
+ return {
5676
+ projectId: payload.projectId,
5677
+ action: payload.action,
5678
+ view: payload.view,
5679
+ limit: payload.limit,
5680
+ offset: payload.offset
5681
+ };
5604
5682
  }
5605
5683
  function parseReadDiffResponsePayload(payload) {
5606
5684
  if (!isRecord(payload)) throw new Error("read-diff response must be an object");
@@ -5841,6 +5919,27 @@ function decodePairingQrPayload(uri) {
5841
5919
  return { desktopStaticPublicKey, pairingToken, relayAddress, expiresAt, protocolVersion };
5842
5920
  }
5843
5921
 
5922
+ // src/pairing/confirm.ts
5923
+ var PAIRING_CONFIRM_PLAINTEXT = new TextEncoder().encode("kangentic-pairing-confirm-v1");
5924
+ var EMPTY_ASSOCIATED_DATA = new Uint8Array(0);
5925
+ function sealPairingConfirm(initiatorToResponder) {
5926
+ return initiatorToResponder.encryptWithAd(EMPTY_ASSOCIATED_DATA, PAIRING_CONFIRM_PLAINTEXT);
5927
+ }
5928
+ function openPairingConfirm(initiatorToResponder, frame) {
5929
+ let plaintext;
5930
+ try {
5931
+ plaintext = initiatorToResponder.decryptWithAd(EMPTY_ASSOCIATED_DATA, frame);
5932
+ } catch {
5933
+ return false;
5934
+ }
5935
+ if (plaintext.length !== PAIRING_CONFIRM_PLAINTEXT.length) return false;
5936
+ let mismatch = 0;
5937
+ for (let index = 0; index < plaintext.length; index += 1) {
5938
+ mismatch |= plaintext[index] ^ PAIRING_CONFIRM_PLAINTEXT[index];
5939
+ }
5940
+ return mismatch === 0;
5941
+ }
5942
+
5844
5943
  // src/roster/roster.ts
5845
5944
  function uint32LengthPrefixed(bytes) {
5846
5945
  const prefix = new Uint8Array(4);
@@ -5880,6 +5979,15 @@ function capabilitySetToRosterCapabilities(capabilities) {
5880
5979
  return capabilitySetToArray(capabilities);
5881
5980
  }
5882
5981
 
5982
+ // src/roster/fingerprint.ts
5983
+ function formatKeyFingerprint(publicKeyHex, groups = 4) {
5984
+ const clusters = [];
5985
+ for (let index = 0; index < groups; index += 1) {
5986
+ clusters.push(publicKeyHex.slice(index * 4, index * 4 + 4));
5987
+ }
5988
+ return clusters.join(" ");
5989
+ }
5990
+
5883
5991
  // src/capabilities/board-tools.ts
5884
5992
  var BOARD_TOOL_READ_NAMES = [
5885
5993
  "search_tasks",
@@ -5891,6 +5999,7 @@ var BOARD_TOOL_READ_NAMES = [
5891
5999
  "list_sessions",
5892
6000
  "get_session_history",
5893
6001
  "get_column_detail",
6002
+ "list_board_profiles",
5894
6003
  "get_handoff_context",
5895
6004
  "get_transcript",
5896
6005
  "get_session_files",
@@ -5904,6 +6013,9 @@ var BOARD_TOOL_WRITE_NAMES = [
5904
6013
  "link_pr",
5905
6014
  "remove_attachment",
5906
6015
  "update_column",
6016
+ "create_board_profile",
6017
+ "update_board_profile",
6018
+ "delete_board_profile",
5907
6019
  "create_backlog_task",
5908
6020
  "promote_backlog",
5909
6021
  "update_backlog_item",
@@ -6041,6 +6153,7 @@ function isBridgeEvent(value) {
6041
6153
  encodeProtocolVersion,
6042
6154
  encodeRosterEntryForSigning,
6043
6155
  findRosterDevice,
6156
+ formatKeyFingerprint,
6044
6157
  generateEd25519KeyPair,
6045
6158
  generateX25519KeyPair,
6046
6159
  hexToBytes,
@@ -6055,6 +6168,7 @@ function isBridgeEvent(value) {
6055
6168
  isRecord,
6056
6169
  isRosterEntryExpired,
6057
6170
  isSecureRelayAddress,
6171
+ openPairingConfirm,
6058
6172
  openPushEnvelope,
6059
6173
  parseActivityEventPayload,
6060
6174
  parseBacklogItemWire,
@@ -6069,6 +6183,7 @@ function isBridgeEvent(value) {
6069
6183
  parseReadStreamResponsePayload,
6070
6184
  parseRegisterPushRequestPayload,
6071
6185
  parseSessionEventWire,
6186
+ parseSessionSummaryWire,
6072
6187
  parseSessionUsageWire,
6073
6188
  parseTerminalDimensionsWire,
6074
6189
  parseTranscriptEntriesWire,
@@ -6076,6 +6191,7 @@ function isBridgeEvent(value) {
6076
6191
  parseTranscriptWindowResponsePayload,
6077
6192
  randomBytes,
6078
6193
  rosterDeviceCapabilitySet,
6194
+ sealPairingConfirm,
6079
6195
  sealPushEnvelope,
6080
6196
  signRosterEntry,
6081
6197
  unwrapSessionFrame,
package/dist/index.d.ts CHANGED
@@ -12,13 +12,15 @@ 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 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 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
+ export { sealPairingConfirm, openPairingConfirm } from './pairing/confirm';
18
19
  export { MAX_RELAY_ADDRESS_LENGTH, isSecureRelayAddress } from './pairing/relay-address';
19
20
  export { signRosterEntry, verifyRosterEntry, encodeRosterEntryForSigning, isRosterEntryExpired, findRosterDevice, rosterDeviceCapabilitySet, capabilitySetToRosterCapabilities, type RosterDeviceEntry, type DeviceRoster, } from './roster/roster';
21
+ export { formatKeyFingerprint } from './roster/fingerprint';
20
22
  export { CAPABILITY_VERBS, isCapabilityVerb, capabilitySetFromArray, capabilitySetToArray, type CapabilityVerb, type CapabilitySet, } from './capabilities/verbs';
21
23
  export { BOARD_TOOL_READ_NAMES, BOARD_TOOL_WRITE_NAMES, isBoardToolReadName, isBoardToolWriteName, type BoardToolReadName, type BoardToolWriteName, type BoardToolName, } from './capabilities/board-tools';
22
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';
23
- 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';
24
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") {
@@ -5466,28 +5498,70 @@ function parseReadBoardResponsePayload(payload) {
5466
5498
  if (typeof payload.projectId !== "string") throw new Error('read-board response is missing "projectId"');
5467
5499
  if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
5468
5500
  if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
5469
- if (!Array.isArray(payload.backlog)) throw new Error('read-board response is missing "backlog"');
5501
+ if (payload.backlog !== void 0 && !Array.isArray(payload.backlog)) {
5502
+ throw new Error('read-board response has a non-array "backlog"');
5503
+ }
5470
5504
  if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
5471
5505
  throw new Error('read-board response has a non-boolean "showTicketNumbers"');
5472
5506
  }
5507
+ if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
5508
+ throw new Error('read-board response has an invalid "view"');
5509
+ }
5473
5510
  return {
5474
5511
  projectId: payload.projectId,
5475
5512
  columns: payload.columns.map((column) => parseBoardColumnWire(column)),
5476
5513
  tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
5477
- backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
5514
+ ...payload.backlog !== void 0 ? { backlog: payload.backlog.map((item) => parseBacklogItemWire(item)) } : {},
5478
5515
  ...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
5479
- ...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
5516
+ ...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {},
5517
+ ...payload.view !== void 0 ? { view: payload.view } : {},
5518
+ ...payload.taskCountsByColumnId !== void 0 ? { taskCountsByColumnId: parseTaskCountsByColumnId(payload.taskCountsByColumnId) } : {}
5480
5519
  };
5481
5520
  }
5521
+ function parseTaskCountsByColumnId(value) {
5522
+ if (!isRecord(value)) throw new Error('read-board response has a non-object "taskCountsByColumnId"');
5523
+ const counts = {};
5524
+ for (const [columnId, count] of Object.entries(value)) {
5525
+ if (typeof count === "number" && Number.isInteger(count) && count >= 0) counts[columnId] = count;
5526
+ }
5527
+ return counts;
5528
+ }
5529
+ function parseSummariesByTaskId(value) {
5530
+ if (value === void 0) return {};
5531
+ if (!isRecord(value)) throw new Error('read-board archived response has a non-object "summariesByTaskId"');
5532
+ const summaries = {};
5533
+ for (const [taskId, summary] of Object.entries(value)) {
5534
+ try {
5535
+ summaries[taskId] = parseSessionSummaryWire(summary);
5536
+ } catch {
5537
+ }
5538
+ }
5539
+ return summaries;
5540
+ }
5482
5541
  function parseReadBoardRequestPayload(payload) {
5483
5542
  if (!isRecord(payload)) throw new Error("read-board payload must be an object");
5484
5543
  if (payload.projectId !== void 0 && typeof payload.projectId !== "string") {
5485
5544
  throw new Error('read-board payload has a non-string "projectId"');
5486
5545
  }
5487
- if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe") {
5546
+ if (payload.action !== void 0 && payload.action !== "subscribe" && payload.action !== "unsubscribe" && payload.action !== "archived") {
5488
5547
  throw new Error('read-board payload has an invalid "action"');
5489
5548
  }
5490
- return { projectId: payload.projectId, action: payload.action };
5549
+ if (payload.view !== void 0 && payload.view !== "full" && payload.view !== "sessions") {
5550
+ throw new Error('read-board payload has an invalid "view"');
5551
+ }
5552
+ if (payload.limit !== void 0 && (typeof payload.limit !== "number" || !Number.isInteger(payload.limit) || payload.limit < 1)) {
5553
+ throw new Error('read-board payload has an invalid "limit"');
5554
+ }
5555
+ if (payload.offset !== void 0 && (typeof payload.offset !== "number" || !Number.isInteger(payload.offset) || payload.offset < 0)) {
5556
+ throw new Error('read-board payload has an invalid "offset"');
5557
+ }
5558
+ return {
5559
+ projectId: payload.projectId,
5560
+ action: payload.action,
5561
+ view: payload.view,
5562
+ limit: payload.limit,
5563
+ offset: payload.offset
5564
+ };
5491
5565
  }
5492
5566
  function parseReadDiffResponsePayload(payload) {
5493
5567
  if (!isRecord(payload)) throw new Error("read-diff response must be an object");
@@ -5728,6 +5802,27 @@ function decodePairingQrPayload(uri) {
5728
5802
  return { desktopStaticPublicKey, pairingToken, relayAddress, expiresAt, protocolVersion };
5729
5803
  }
5730
5804
 
5805
+ // src/pairing/confirm.ts
5806
+ var PAIRING_CONFIRM_PLAINTEXT = new TextEncoder().encode("kangentic-pairing-confirm-v1");
5807
+ var EMPTY_ASSOCIATED_DATA = new Uint8Array(0);
5808
+ function sealPairingConfirm(initiatorToResponder) {
5809
+ return initiatorToResponder.encryptWithAd(EMPTY_ASSOCIATED_DATA, PAIRING_CONFIRM_PLAINTEXT);
5810
+ }
5811
+ function openPairingConfirm(initiatorToResponder, frame) {
5812
+ let plaintext;
5813
+ try {
5814
+ plaintext = initiatorToResponder.decryptWithAd(EMPTY_ASSOCIATED_DATA, frame);
5815
+ } catch {
5816
+ return false;
5817
+ }
5818
+ if (plaintext.length !== PAIRING_CONFIRM_PLAINTEXT.length) return false;
5819
+ let mismatch = 0;
5820
+ for (let index = 0; index < plaintext.length; index += 1) {
5821
+ mismatch |= plaintext[index] ^ PAIRING_CONFIRM_PLAINTEXT[index];
5822
+ }
5823
+ return mismatch === 0;
5824
+ }
5825
+
5731
5826
  // src/roster/roster.ts
5732
5827
  function uint32LengthPrefixed(bytes) {
5733
5828
  const prefix = new Uint8Array(4);
@@ -5767,6 +5862,15 @@ function capabilitySetToRosterCapabilities(capabilities) {
5767
5862
  return capabilitySetToArray(capabilities);
5768
5863
  }
5769
5864
 
5865
+ // src/roster/fingerprint.ts
5866
+ function formatKeyFingerprint(publicKeyHex, groups = 4) {
5867
+ const clusters = [];
5868
+ for (let index = 0; index < groups; index += 1) {
5869
+ clusters.push(publicKeyHex.slice(index * 4, index * 4 + 4));
5870
+ }
5871
+ return clusters.join(" ");
5872
+ }
5873
+
5770
5874
  // src/capabilities/board-tools.ts
5771
5875
  var BOARD_TOOL_READ_NAMES = [
5772
5876
  "search_tasks",
@@ -5778,6 +5882,7 @@ var BOARD_TOOL_READ_NAMES = [
5778
5882
  "list_sessions",
5779
5883
  "get_session_history",
5780
5884
  "get_column_detail",
5885
+ "list_board_profiles",
5781
5886
  "get_handoff_context",
5782
5887
  "get_transcript",
5783
5888
  "get_session_files",
@@ -5791,6 +5896,9 @@ var BOARD_TOOL_WRITE_NAMES = [
5791
5896
  "link_pr",
5792
5897
  "remove_attachment",
5793
5898
  "update_column",
5899
+ "create_board_profile",
5900
+ "update_board_profile",
5901
+ "delete_board_profile",
5794
5902
  "create_backlog_task",
5795
5903
  "promote_backlog",
5796
5904
  "update_backlog_item",
@@ -5927,6 +6035,7 @@ export {
5927
6035
  encodeProtocolVersion,
5928
6036
  encodeRosterEntryForSigning,
5929
6037
  findRosterDevice,
6038
+ formatKeyFingerprint,
5930
6039
  generateEd25519KeyPair,
5931
6040
  generateX25519KeyPair,
5932
6041
  hexToBytes,
@@ -5941,6 +6050,7 @@ export {
5941
6050
  isRecord,
5942
6051
  isRosterEntryExpired,
5943
6052
  isSecureRelayAddress,
6053
+ openPairingConfirm,
5944
6054
  openPushEnvelope,
5945
6055
  parseActivityEventPayload,
5946
6056
  parseBacklogItemWire,
@@ -5955,6 +6065,7 @@ export {
5955
6065
  parseReadStreamResponsePayload,
5956
6066
  parseRegisterPushRequestPayload,
5957
6067
  parseSessionEventWire,
6068
+ parseSessionSummaryWire,
5958
6069
  parseSessionUsageWire,
5959
6070
  parseTerminalDimensionsWire,
5960
6071
  parseTranscriptEntriesWire,
@@ -5962,6 +6073,7 @@ export {
5962
6073
  parseTranscriptWindowResponsePayload,
5963
6074
  randomBytes4 as randomBytes,
5964
6075
  rosterDeviceCapabilitySet,
6076
+ sealPairingConfirm,
5965
6077
  sealPushEnvelope,
5966
6078
  signRosterEntry,
5967
6079
  unwrapSessionFrame,
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The pairing-confirm frame: one AEAD-sealed message the phone sends after
3
+ * the human taps Confirm, over the transport key the completed IKpsk0
4
+ * handshake just produced (HandshakeState.writeMessage's `split` result -
5
+ * see pairing-handshake.ts). This is a LIVENESS AND INTENT signal, not the
6
+ * pairing's security boundary: a relay-in-the-middle already holds a valid
7
+ * session with the desktop and could forge any frame it wants. The actual
8
+ * defense is unchanged - the human compares the desktop's SAS digits
9
+ * against the phone's, and only taps Confirm on a match.
10
+ *
11
+ * What this frame buys instead: both peers derive their transport keys
12
+ * from the SAME completed handshake transcript (getHandshakeHash/split are
13
+ * both functions of the transcript), so if the sealed frame opens under the
14
+ * desktop's key, the two transcripts necessarily agree - which means the
15
+ * SAS the human compared necessarily agreed too. The AEAD open IS the
16
+ * desktop's SAS verification; that is why the frame carries no digits of
17
+ * its own, and why a failed open is treated as a mismatch/failure, not a
18
+ * silent no-op.
19
+ *
20
+ * There is no reject frame. Backing out of the ceremony on the phone closes
21
+ * the transport without sending this frame; close-without-confirm is the
22
+ * rejection, observed by the desktop as its pairing timeout (see
23
+ * pairing-service.ts's phase timers).
24
+ */
25
+ import { CipherState } from '../crypto/noise/cipher-state';
26
+ /**
27
+ * Seals the confirm frame under the initiator-to-responder cipher state
28
+ * (index 0 of HandshakeState's `split` pair, by Noise Protocol Framework
29
+ * convention). Called once per ceremony, so nonce reuse under this key is
30
+ * not a concern - the key itself is fresh per handshake transcript.
31
+ */
32
+ export declare function sealPairingConfirm(initiatorToResponder: CipherState): Uint8Array;
33
+ /**
34
+ * Opens a received confirm frame. Returns false (never throws) on a
35
+ * tampered/wrong-key ciphertext or an unexpected plaintext, so the caller
36
+ * can treat "could not verify" uniformly whether the failure was
37
+ * cryptographic or structural.
38
+ */
39
+ export declare function openPairingConfirm(initiatorToResponder: CipherState, frame: Uint8Array): boolean;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Formats a device's static public key as a short, human-comparable
3
+ * fingerprint - the identity the pairing ceremony actually bound and the
4
+ * SAS actually confirmed, unlike a name (not unique) or a hardware id
5
+ * (unavailable on modern Android, permanent PII). Matches
6
+ * kangentic-mobile's src/screens/usePairedDesktopInfo.ts formatKeyFingerprint
7
+ * exactly, so a user can hold the phone next to the desktop and compare
8
+ * directly: front-only slice, no ellipsis, no case change.
9
+ */
10
+ export declare function formatKeyFingerprint(publicKeyHex: string, groups?: number): 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 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
  /**
@@ -99,10 +99,57 @@ export interface TranscriptWindowResponsePayload {
99
99
  }
100
100
  /** Phone-side narrowing of a transcript-window response. Throws on a malformed required field. */
101
101
  export declare function parseTranscriptWindowResponsePayload(payload: JsonValue): TranscriptWindowResponsePayload;
102
+ /**
103
+ * Which projection of a board the phone wants.
104
+ *
105
+ * - 'sessions': columns, per-column task counts, and only the non-archived
106
+ * tasks that carry a `session_id`. This is what an agent feed renders: it
107
+ * watches every project at once but only ever draws the handful of tasks
108
+ * with a live agent on them.
109
+ * - 'full': every non-archived task, for the one project whose board the user
110
+ * actually has open.
111
+ *
112
+ * Neither carries the backlog. Sending the field at all is the signal that
113
+ * this phone tolerates a response without one; a phone that omits it gets the
114
+ * pre-0.9.0 payload unchanged.
115
+ */
116
+ export type ReadBoardView = 'full' | 'sessions';
102
117
  export interface ReadBoardRequestPayload {
103
118
  projectId?: string;
104
- /** 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. */
105
- 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;
137
+ /**
138
+ * subscribe only: the projection wanted (protocol 0.9.0). Omitted means the
139
+ * legacy full board WITH the backlog.
140
+ *
141
+ * Measured on a 15-project desktop, the full boards were 63 kB compressed
142
+ * of a ~96 kB cold start, of which 23 kB was a backlog no phone has ever
143
+ * rendered and another 30 kB was tasks with no session on them. Worse, a
144
+ * board subscription re-snapshots on every board change, so that payload
145
+ * repeats for as long as the phone is connected. The 'sessions' projection
146
+ * is 12 kB for the same 15 projects.
147
+ *
148
+ * A pre-0.9.0 desktop ignores the field and returns a full board with no
149
+ * `view` echo, which is exactly how a 0.9.0 phone detects that it must not
150
+ * treat the snapshot as filtered.
151
+ */
152
+ view?: ReadBoardView;
106
153
  }
107
154
  export interface ReadBoardProjectSummary {
108
155
  id: string;
@@ -123,8 +170,10 @@ export interface ReadBoardProjectListResponsePayload {
123
170
  export interface ReadBoardSnapshotResponsePayload {
124
171
  projectId: string;
125
172
  columns: BoardColumnWire[];
173
+ /** Non-archived tasks; filtered to the ones carrying a `session_id` when `view` is 'sessions'. */
126
174
  tasks: BoardTaskWire[];
127
- backlog: BacklogItemWire[];
175
+ /** Absent whenever the request carried a `view` (protocol 0.9.0) - no phone has ever rendered it. */
176
+ backlog?: BacklogItemWire[];
128
177
  /** The project's accent color ("#rrggbb"); same semantics as ReadBoardProjectSummary.color. Absent from pre-0.5.0 desktops. */
129
178
  projectColor?: string;
130
179
  /**
@@ -133,9 +182,46 @@ export interface ReadBoardSnapshotResponsePayload {
133
182
  * true, the desktop default).
134
183
  */
135
184
  showTicketNumbers?: boolean;
185
+ /**
186
+ * The projection actually applied, echoed back. Absent from a pre-0.9.0
187
+ * desktop, which always sends a full board - so treat an absent `view` as
188
+ * 'full', never as "filtered, unknown how".
189
+ */
190
+ view?: ReadBoardView;
191
+ /**
192
+ * Non-archived task count per column id, sent with a 'sessions' view because
193
+ * `tasks` is filtered and a count taken from it would be wrong. Appending a
194
+ * card to a column needs the real length.
195
+ */
196
+ taskCountsByColumnId?: Record<string, number>;
197
+ }
198
+ /**
199
+ * Returned for `action: 'archived'` - one page of a project's completed tasks
200
+ * (protocol 0.10.0).
201
+ *
202
+ * These never appear in a board snapshot: the desktop builds those from its
203
+ * non-archived task list, so a phone that only ever subscribed has no way to
204
+ * know a completed task exists at all.
205
+ */
206
+ export interface ReadBoardArchivedResponsePayload {
207
+ projectId: string;
208
+ /** Newest-archived first. At most the requested `limit`. */
209
+ archivedTasks: BoardTaskWire[];
210
+ /**
211
+ * Total archived tasks in the project, NOT the length of the page above.
212
+ * Drives the column count and tells the phone whether another page exists.
213
+ */
214
+ archivedTotalCount: number;
215
+ /**
216
+ * Lifetime cost/duration/churn per task id, for the tasks on THIS page that
217
+ * have recorded metrics. Sparse on purpose: a task archived without ever
218
+ * running an agent has nothing to summarize and is simply absent, which is
219
+ * not an error.
220
+ */
221
+ summariesByTaskId: Record<string, SessionSummaryWire>;
136
222
  }
137
- export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload;
138
- /** Phone-side narrowing of a read-board response (project list or board snapshot). Throws on a malformed required field. */
223
+ export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload | ReadBoardArchivedResponsePayload;
224
+ /** Phone-side narrowing of a read-board response (project list, board snapshot, or archived page). Throws on a malformed required field. */
139
225
  export declare function parseReadBoardResponsePayload(payload: JsonValue): ReadBoardResponsePayload;
140
226
  /** 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. */
141
227
  export type ReadDiffScope = 'working' | 'staged' | 'branch';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangentic/protocol",
3
- "version": "0.8.0",
3
+ "version": "0.10.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",