@kangentic/protocol 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The board-tool names a phone may pass in a board-tool-read /
3
+ * board-tool-write request's `tool` field. Each name is the desktop's
4
+ * INTERNAL command-registry key (src/main/agent/commands/index.ts
5
+ * `commandHandlers`), not a public MCP tool name, and this is NOT the MCP
6
+ * protocol - see the desktop's board-tool-allowlist.ts doc comment for the
7
+ * full rationale and the deliberate exclusions (query_db, move_task,
8
+ * list_tasks, list_columns, list_backlog).
9
+ *
10
+ * The desktop builds its enforcement allowlist FROM these tuples, so its
11
+ * allowlist parity test (board-tool-allowlist.test.ts, which diffs the
12
+ * allowlist against the real `commandHandlers` key set) transitively keeps
13
+ * this protocol surface honest: a tool renamed or removed desktop-side
14
+ * fails that test instead of silently breaking phones.
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"];
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"];
18
+ export type BoardToolReadName = (typeof BOARD_TOOL_READ_NAMES)[number];
19
+ export type BoardToolWriteName = (typeof BOARD_TOOL_WRITE_NAMES)[number];
20
+ export type BoardToolName = BoardToolReadName | BoardToolWriteName;
21
+ export declare function isBoardToolReadName(value: string): value is BoardToolReadName;
22
+ export declare function isBoardToolWriteName(value: string): value is BoardToolWriteName;
@@ -9,7 +9,7 @@
9
9
  * wire a handler, and a device's roster entry still has to include it in
10
10
  * its CapabilitySet.
11
11
  */
12
- export declare const CAPABILITY_VERBS: readonly ["read-stream", "read-board", "read-diff", "send-user-message", "move-task", "answer-permission-prompt"];
12
+ 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"];
13
13
  export type CapabilityVerb = (typeof CAPABILITY_VERBS)[number];
14
14
  export declare function isCapabilityVerb(value: string): value is CapabilityVerb;
15
15
  /** Deny-by-default: a verb absent from the set is not authorized. */
@@ -0,0 +1 @@
1
+ export declare function deriveSessionSlotId(desktopStaticPublicKey: Uint8Array, phoneStaticPublicKey: Uint8Array): string;
@@ -1,29 +1,90 @@
1
1
  /**
2
- * The event contract skeleton the phone consumes. Phase 1 establishes the
3
- * three event FAMILIES (transcript, board, activity) and their envelope
4
- * shape; Phase 2 (data feeds & interactive control) is what actually maps
5
- * desktop internals - SessionManager's `data`/`activity` events,
6
- * transcript-service output, repository/DiffService mutations - onto
7
- * these envelopes. Keeping `payload` as JsonValue here rather than a
8
- * fully-typed shape per event avoids guessing at Phase 2's real mapping
9
- * before that integration exists.
2
+ * The event contract the phone consumes, pushed as EventMessage.event over
3
+ * an established BridgeSession once Phase 2's capability handlers (desktop
4
+ * bridge module) subscribe SessionManager/transcript-service/DiffService to
5
+ * a session's live state. Five kinds:
6
+ *
7
+ * - transcript: a revision-gated delta from resolveTaskTranscript, for the
8
+ * phone's transcript-styled conversation view.
9
+ * - activity: a discriminated union consolidating SessionManager's
10
+ * separate `activity`/`usage`/`event` emissions plus permission state
11
+ * (carrying the synthesized prompt id `answer-permission-prompt` binds
12
+ * to), so one event kind covers session telemetry instead of three.
13
+ * - terminal: raw PTY output from the unfiltered data-tap, for the phone's
14
+ * raw-terminal-mirror view (a distinct consumer from the parsed
15
+ * transcript).
16
+ * - board: a board-mutation notification, filterable by project and
17
+ * (optionally) task.
18
+ * - diff: a payload-less "something under this task's worktree changed,
19
+ * re-fetch via read-diff" signal, mirroring the desktop's GIT_DIFF_CHANGED.
20
+ *
21
+ * Payload contents are typed by the wire mirrors in events/payloads.ts
22
+ * (protocol Phase 2); the desktop's wire-mappers produce them and
23
+ * isBridgeEvent() is the phone's structural trust boundary for a decoded
24
+ * event before its router dispatches on `kind`.
10
25
  */
11
26
  import type { JsonValue } from '../wire/messages';
27
+ import { type ActivityReasonWire, type ActivityStateWire, type SessionEventWire, type SessionUsageWire, type TranscriptEventPayload } from './payloads';
12
28
  export interface TranscriptEvent {
13
29
  kind: 'transcript';
14
30
  sessionId: string;
15
31
  taskId: string;
16
- payload: JsonValue;
17
- }
18
- export interface BoardEvent {
19
- kind: 'board';
20
- taskId: string;
21
- payload: JsonValue;
32
+ /** Incremental (protocol v2): indexed upserts or a reset signal, never the whole transcript. See TranscriptEventPayload. */
33
+ payload: TranscriptEventPayload;
22
34
  }
35
+ export type ActivityEventPayload = {
36
+ type: 'activity';
37
+ state: ActivityStateWire;
38
+ reason: ActivityReasonWire;
39
+ } | {
40
+ type: 'usage';
41
+ usage: SessionUsageWire;
42
+ } | {
43
+ type: 'event';
44
+ event: SessionEventWire;
45
+ } | {
46
+ type: 'permission';
47
+ promptId: string;
48
+ pending: boolean;
49
+ };
23
50
  export interface ActivityEvent {
24
51
  kind: 'activity';
25
52
  sessionId: string;
26
53
  taskId: string;
27
- payload: JsonValue;
54
+ payload: ActivityEventPayload;
28
55
  }
29
- export type BridgeEvent = TranscriptEvent | BoardEvent | ActivityEvent;
56
+ export interface TerminalEvent {
57
+ kind: 'terminal';
58
+ sessionId: string;
59
+ taskId: string;
60
+ payload: {
61
+ data: string;
62
+ };
63
+ }
64
+ export interface BoardEventPayload {
65
+ change: 'task-created' | 'task-updated' | 'task-deleted' | 'swimlane-updated' | 'backlog-changed';
66
+ ids: string[];
67
+ }
68
+ export interface BoardEvent {
69
+ kind: 'board';
70
+ projectId: string;
71
+ taskId?: string;
72
+ payload: BoardEventPayload;
73
+ }
74
+ export interface DiffEvent {
75
+ kind: 'diff';
76
+ taskId: string;
77
+ payload: null;
78
+ }
79
+ export type BridgeEvent = TranscriptEvent | ActivityEvent | TerminalEvent | BoardEvent | DiffEvent;
80
+ /**
81
+ * Narrows an activity event's payload to its typed union. Throws on a
82
+ * malformed required field so the caller can drop the event cleanly.
83
+ */
84
+ export declare function parseActivityEventPayload(payload: JsonValue): ActivityEventPayload;
85
+ /**
86
+ * Full structural validation of a decoded event - envelope ids AND payload
87
+ * shape per kind. This is the phone-side trust boundary its feed router
88
+ * runs before dispatching; a false return means "drop the event".
89
+ */
90
+ export declare function isBridgeEvent(value: unknown): value is BridgeEvent;
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Wire-level mirrors of the desktop-app shapes that flow to a phone inside
3
+ * feed events and read-* responses, plus the runtime guards a phone runs
4
+ * before trusting any field. Phase 2 of the protocol: the envelope
5
+ * (messages.ts) and event skeleton (event.ts) were final in Phase 1; these
6
+ * types pin down the payload CONTENTS that were deferred as JsonValue.
7
+ *
8
+ * These are deliberate structural MIRRORS, not imports: this package is a
9
+ * dependency-light leaf shared by desktop and phone, so it cannot import
10
+ * the desktop app's internal types (src/shared/types.ts). The desktop's
11
+ * wire-mappers (src/main/mobile-bridge/handlers/wire-mappers.ts) are the
12
+ * one place each mirror meets its source shape, so a drift shows up there
13
+ * as a compile error instead of a silent phone-side parse failure.
14
+ *
15
+ * Guard philosophy: validate the discriminants and required scalars a
16
+ * consumer dispatches on, pass unrecognized extra fields through untouched
17
+ * (lenient forward compatibility - an older phone must tolerate fields a
18
+ * newer desktop adds), and throw on a malformed required field so the
19
+ * caller can drop the payload cleanly.
20
+ */
21
+ import type { JsonValue } from '../wire/messages';
22
+ /** Mirrors the desktop's TranscriptBlock. `tool_use.input` is JSON-sanitized by the desktop mapper before it reaches the wire. */
23
+ export type TranscriptBlockWire = {
24
+ type: 'text';
25
+ text: string;
26
+ } | {
27
+ type: 'thinking';
28
+ text: string;
29
+ } | {
30
+ type: 'tool_use';
31
+ id: string;
32
+ name: string;
33
+ input: JsonValue;
34
+ };
35
+ /** Mirrors the desktop's per-turn TranscriptTurnUsage token counts. */
36
+ export interface TranscriptTurnUsageWire {
37
+ inputTokens: number;
38
+ outputTokens: number;
39
+ cacheCreationInputTokens: number;
40
+ cacheReadInputTokens: number;
41
+ }
42
+ export type TranscriptSystemSubtypeWire = 'compaction' | 'command' | 'command_output' | 'session_boundary';
43
+ /** Mirrors the desktop's TranscriptEntry union - the parsed conversation the phone's transcript-terminal renders. */
44
+ export type TranscriptEntryWire = {
45
+ kind: 'user';
46
+ uuid: string;
47
+ ts: number;
48
+ text: string;
49
+ } | {
50
+ kind: 'assistant';
51
+ uuid: string;
52
+ ts: number;
53
+ model?: string;
54
+ agentName?: string;
55
+ usage?: TranscriptTurnUsageWire;
56
+ blocks: TranscriptBlockWire[];
57
+ } | {
58
+ kind: 'tool_result';
59
+ uuid: string;
60
+ ts: number;
61
+ toolUseId: string;
62
+ content: string;
63
+ isError?: boolean;
64
+ } | {
65
+ kind: 'system';
66
+ uuid: string;
67
+ ts: number;
68
+ subtype: TranscriptSystemSubtypeWire;
69
+ text: string;
70
+ };
71
+ /** One transcript entry at its absolute position in the full transcript at the delta's revision. */
72
+ export interface TranscriptUpsertWire {
73
+ index: number;
74
+ entry: TranscriptEntryWire;
75
+ }
76
+ /**
77
+ * Protocol v2 transcript payload: incremental, never the whole transcript.
78
+ *
79
+ * - 'delta' carries only the entries that changed or appeared since the
80
+ * last delta the desktop sent on this subscription, as absolute-indexed
81
+ * upserts in ascending order. A large delta is split across several
82
+ * events (same revision) so every frame stays well under the transport
83
+ * caps and the first chunk renders without waiting for the rest. The
84
+ * phone applies upserts inside or adjacent to its loaded window and
85
+ * re-requests a window when it detects a gap.
86
+ * - 'reset' tells the phone its local state is unreconstructable from
87
+ * deltas (entries removed or reordered, or a degraded transcript source
88
+ * whose uuids are unstable) - drop local entries and re-request a
89
+ * window via the read-stream 'transcript-window' action.
90
+ *
91
+ * `revision` is the desktop's monotonically increasing whole-transcript
92
+ * version; `totalEntries` is the full transcript length at that revision,
93
+ * which is how the phone knows history extends above its window.
94
+ */
95
+ export type TranscriptEventPayload = {
96
+ mode: 'delta';
97
+ revision: number;
98
+ totalEntries: number;
99
+ upserts: TranscriptUpsertWire[];
100
+ } | {
101
+ mode: 'reset';
102
+ revision: number;
103
+ totalEntries: number;
104
+ };
105
+ /** Mirrors the desktop's ActivityState. 'permission' means the agent paused for user approval (incl. AskUserQuestion / ExitPlanMode pauses). */
106
+ export type ActivityStateWire = 'thinking' | 'idle' | 'permission';
107
+ /** Mirrors the desktop's ActivityReason discriminated union. */
108
+ export type ActivityReasonWire = {
109
+ kind: 'idle';
110
+ } | {
111
+ kind: 'permission';
112
+ } | {
113
+ kind: 'tool';
114
+ pendingCount: number;
115
+ currentTool: string | null;
116
+ } | {
117
+ kind: 'subagent';
118
+ depth: number;
119
+ } | {
120
+ kind: 'background-shell';
121
+ count: number;
122
+ ids: string[];
123
+ } | {
124
+ kind: 'turn-active';
125
+ };
126
+ /**
127
+ * Phone-needed subset of the desktop's SessionUsage. The desktop mapper
128
+ * explicitly picks these fields; renderer-only extras (transcriptPath,
129
+ * rateLimits, sessionId echo) never reach the wire.
130
+ */
131
+ export interface SessionUsageWire {
132
+ contextWindow: {
133
+ usedPercentage: number;
134
+ usedTokens: number;
135
+ cacheTokens: number;
136
+ totalInputTokens: number;
137
+ totalOutputTokens: number;
138
+ contextWindowSize: number;
139
+ };
140
+ cost: {
141
+ totalCostUsd: number;
142
+ totalDurationMs: number;
143
+ };
144
+ toolCallCount?: number;
145
+ model: {
146
+ id: string;
147
+ displayName: string;
148
+ effort?: string;
149
+ };
150
+ }
151
+ /**
152
+ * Loose mirror of the desktop's SessionEvent telemetry entry. `type` is
153
+ * deliberately a plain string, not the desktop's EventType enum, so a new
154
+ * desktop event type does not break an older phone's parser.
155
+ */
156
+ export interface SessionEventWire {
157
+ ts: number;
158
+ type: string;
159
+ tool?: string;
160
+ toolId?: string;
161
+ detail?: string;
162
+ }
163
+ /** Phone-needed subset of the desktop's Swimlane row (snake_case preserved so the desktop mapper is a mechanical pick). */
164
+ export interface BoardColumnWire {
165
+ id: string;
166
+ name: string;
167
+ description: string | null;
168
+ role: string | null;
169
+ position: number;
170
+ color: string;
171
+ icon: string | null;
172
+ is_archived: boolean;
173
+ is_ghost: boolean;
174
+ }
175
+ /** Phone-needed subset of the desktop's Task row. A non-null `session_id` is the live-session signal the phone's triage view keys on. */
176
+ export interface BoardTaskWire {
177
+ id: string;
178
+ display_id: number;
179
+ title: string;
180
+ description: string;
181
+ swimlane_id: string;
182
+ position: number;
183
+ agent: string | null;
184
+ session_id: string | null;
185
+ worktree_path: string | null;
186
+ branch_name: string | null;
187
+ pr_number: number | null;
188
+ pr_url: string | null;
189
+ pr_state: string | null;
190
+ base_branch: string | null;
191
+ labels: string[];
192
+ priority: number;
193
+ attachment_count: number;
194
+ archived_at: string | null;
195
+ created_at: string;
196
+ updated_at: string;
197
+ }
198
+ /** Phone-needed subset of the desktop's BacklogTask row. */
199
+ export interface BacklogItemWire {
200
+ id: string;
201
+ title: string;
202
+ description: string;
203
+ priority: number;
204
+ labels: string[];
205
+ position: number;
206
+ item_type: string | null;
207
+ external_url: string | null;
208
+ attachment_count: number;
209
+ created_at: string;
210
+ updated_at: string;
211
+ }
212
+ /** Mirrors the desktop's GitDiffStatus. */
213
+ export type DiffFileStatusWire = 'A' | 'M' | 'D' | 'R' | 'C' | 'U';
214
+ /** Exact mirror of the desktop's GitDiffFileEntry. */
215
+ export interface DiffFileWire {
216
+ path: string;
217
+ status: DiffFileStatusWire;
218
+ insertions: number;
219
+ deletions: number;
220
+ oldPath?: string;
221
+ binary: boolean;
222
+ }
223
+ /** Exact mirror of the desktop's GitDiffFilesResult - the read-diff file-list response. */
224
+ export interface DiffFileListWire {
225
+ files: DiffFileWire[];
226
+ totalInsertions: number;
227
+ totalDeletions: number;
228
+ }
229
+ /** Exact mirror of the desktop's GitFileContentResult - the read-diff single-file response. */
230
+ export interface DiffFileContentWire {
231
+ original: string;
232
+ modified: string;
233
+ language: string;
234
+ }
235
+ /** Narrows a transcript entry array (windowed-history pages). Throws on a malformed entry so the caller can drop the payload. */
236
+ export declare function parseTranscriptEntriesWire(payload: JsonValue): TranscriptEntryWire[];
237
+ /** Narrows a transcript event's delta/reset payload. Throws on a malformed required field so the caller can drop the push. */
238
+ export declare function parseTranscriptEventPayload(payload: JsonValue): TranscriptEventPayload;
239
+ /** True for a well-formed ActivityStateWire value. */
240
+ export declare function isActivityStateWire(value: unknown): value is ActivityStateWire;
241
+ /** True for a well-formed ActivityReasonWire value. */
242
+ export declare function isActivityReasonWire(value: unknown): value is ActivityReasonWire;
243
+ /** Narrows a usage payload to the SessionUsageWire subset. Throws on a malformed required field. */
244
+ export declare function parseSessionUsageWire(payload: JsonValue): SessionUsageWire;
245
+ /** Narrows a session-event payload to SessionEventWire. Throws on a malformed required field. */
246
+ export declare function parseSessionEventWire(payload: JsonValue): SessionEventWire;
247
+ /** Narrows one board-column row. Throws on a malformed required field. */
248
+ export declare function parseBoardColumnWire(value: JsonValue): BoardColumnWire;
249
+ /** Narrows one board-task row. Throws on a malformed required field. */
250
+ export declare function parseBoardTaskWire(value: JsonValue): BoardTaskWire;
251
+ /** Narrows one backlog-item row. Throws on a malformed required field. */
252
+ export declare function parseBacklogItemWire(value: JsonValue): BacklogItemWire;
253
+ /** Narrows a read-diff file-list payload. Throws on a malformed required field. */
254
+ export declare function parseDiffFileListWire(payload: JsonValue): DiffFileListWire;
255
+ /** Narrows a read-diff single-file payload. Throws on a malformed required field. */
256
+ export declare function parseDiffFileContentWire(payload: JsonValue): DiffFileContentWire;