@kangentic/protocol 0.1.1 → 0.2.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,89 @@
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 TranscriptEntryWire } 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
+ payload: TranscriptEntryWire[];
22
33
  }
34
+ export type ActivityEventPayload = {
35
+ type: 'activity';
36
+ state: ActivityStateWire;
37
+ reason: ActivityReasonWire;
38
+ } | {
39
+ type: 'usage';
40
+ usage: SessionUsageWire;
41
+ } | {
42
+ type: 'event';
43
+ event: SessionEventWire;
44
+ } | {
45
+ type: 'permission';
46
+ promptId: string;
47
+ pending: boolean;
48
+ };
23
49
  export interface ActivityEvent {
24
50
  kind: 'activity';
25
51
  sessionId: string;
26
52
  taskId: string;
27
- payload: JsonValue;
53
+ payload: ActivityEventPayload;
28
54
  }
29
- export type BridgeEvent = TranscriptEvent | BoardEvent | ActivityEvent;
55
+ export interface TerminalEvent {
56
+ kind: 'terminal';
57
+ sessionId: string;
58
+ taskId: string;
59
+ payload: {
60
+ data: string;
61
+ };
62
+ }
63
+ export interface BoardEventPayload {
64
+ change: 'task-created' | 'task-updated' | 'task-deleted' | 'swimlane-updated' | 'backlog-changed';
65
+ ids: string[];
66
+ }
67
+ export interface BoardEvent {
68
+ kind: 'board';
69
+ projectId: string;
70
+ taskId?: string;
71
+ payload: BoardEventPayload;
72
+ }
73
+ export interface DiffEvent {
74
+ kind: 'diff';
75
+ taskId: string;
76
+ payload: null;
77
+ }
78
+ export type BridgeEvent = TranscriptEvent | ActivityEvent | TerminalEvent | BoardEvent | DiffEvent;
79
+ /**
80
+ * Narrows an activity event's payload to its typed union. Throws on a
81
+ * malformed required field so the caller can drop the event cleanly.
82
+ */
83
+ export declare function parseActivityEventPayload(payload: JsonValue): ActivityEventPayload;
84
+ /**
85
+ * Full structural validation of a decoded event - envelope ids AND payload
86
+ * shape per kind. This is the phone-side trust boundary its feed router
87
+ * runs before dispatching; a false return means "drop the event".
88
+ */
89
+ export declare function isBridgeEvent(value: unknown): value is BridgeEvent;
@@ -0,0 +1,220 @@
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
+ /** Mirrors the desktop's ActivityState. 'permission' means the agent paused for user approval (incl. AskUserQuestion / ExitPlanMode pauses). */
72
+ export type ActivityStateWire = 'thinking' | 'idle' | 'permission';
73
+ /** Mirrors the desktop's ActivityReason discriminated union. */
74
+ export type ActivityReasonWire = {
75
+ kind: 'idle';
76
+ } | {
77
+ kind: 'permission';
78
+ } | {
79
+ kind: 'tool';
80
+ pendingCount: number;
81
+ currentTool: string | null;
82
+ } | {
83
+ kind: 'subagent';
84
+ depth: number;
85
+ } | {
86
+ kind: 'background-shell';
87
+ count: number;
88
+ ids: string[];
89
+ } | {
90
+ kind: 'turn-active';
91
+ };
92
+ /**
93
+ * Phone-needed subset of the desktop's SessionUsage. The desktop mapper
94
+ * explicitly picks these fields; renderer-only extras (transcriptPath,
95
+ * rateLimits, sessionId echo) never reach the wire.
96
+ */
97
+ export interface SessionUsageWire {
98
+ contextWindow: {
99
+ usedPercentage: number;
100
+ usedTokens: number;
101
+ cacheTokens: number;
102
+ totalInputTokens: number;
103
+ totalOutputTokens: number;
104
+ contextWindowSize: number;
105
+ };
106
+ cost: {
107
+ totalCostUsd: number;
108
+ totalDurationMs: number;
109
+ };
110
+ toolCallCount?: number;
111
+ model: {
112
+ id: string;
113
+ displayName: string;
114
+ effort?: string;
115
+ };
116
+ }
117
+ /**
118
+ * Loose mirror of the desktop's SessionEvent telemetry entry. `type` is
119
+ * deliberately a plain string, not the desktop's EventType enum, so a new
120
+ * desktop event type does not break an older phone's parser.
121
+ */
122
+ export interface SessionEventWire {
123
+ ts: number;
124
+ type: string;
125
+ tool?: string;
126
+ toolId?: string;
127
+ detail?: string;
128
+ }
129
+ /** Phone-needed subset of the desktop's Swimlane row (snake_case preserved so the desktop mapper is a mechanical pick). */
130
+ export interface BoardColumnWire {
131
+ id: string;
132
+ name: string;
133
+ description: string | null;
134
+ role: string | null;
135
+ position: number;
136
+ color: string;
137
+ icon: string | null;
138
+ is_archived: boolean;
139
+ is_ghost: boolean;
140
+ }
141
+ /** 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. */
142
+ export interface BoardTaskWire {
143
+ id: string;
144
+ display_id: number;
145
+ title: string;
146
+ description: string;
147
+ swimlane_id: string;
148
+ position: number;
149
+ agent: string | null;
150
+ session_id: string | null;
151
+ worktree_path: string | null;
152
+ branch_name: string | null;
153
+ pr_number: number | null;
154
+ pr_url: string | null;
155
+ pr_state: string | null;
156
+ base_branch: string | null;
157
+ labels: string[];
158
+ priority: number;
159
+ attachment_count: number;
160
+ archived_at: string | null;
161
+ created_at: string;
162
+ updated_at: string;
163
+ }
164
+ /** Phone-needed subset of the desktop's BacklogTask row. */
165
+ export interface BacklogItemWire {
166
+ id: string;
167
+ title: string;
168
+ description: string;
169
+ priority: number;
170
+ labels: string[];
171
+ position: number;
172
+ item_type: string | null;
173
+ external_url: string | null;
174
+ attachment_count: number;
175
+ created_at: string;
176
+ updated_at: string;
177
+ }
178
+ /** Mirrors the desktop's GitDiffStatus. */
179
+ export type DiffFileStatusWire = 'A' | 'M' | 'D' | 'R' | 'C' | 'U';
180
+ /** Exact mirror of the desktop's GitDiffFileEntry. */
181
+ export interface DiffFileWire {
182
+ path: string;
183
+ status: DiffFileStatusWire;
184
+ insertions: number;
185
+ deletions: number;
186
+ oldPath?: string;
187
+ binary: boolean;
188
+ }
189
+ /** Exact mirror of the desktop's GitDiffFilesResult - the read-diff file-list response. */
190
+ export interface DiffFileListWire {
191
+ files: DiffFileWire[];
192
+ totalInsertions: number;
193
+ totalDeletions: number;
194
+ }
195
+ /** Exact mirror of the desktop's GitFileContentResult - the read-diff single-file response. */
196
+ export interface DiffFileContentWire {
197
+ original: string;
198
+ modified: string;
199
+ language: string;
200
+ }
201
+ /** Narrows a transcript event's payload to the entry array the phone renders. Throws on a malformed entry so the caller can drop the push. */
202
+ export declare function parseTranscriptEntriesWire(payload: JsonValue): TranscriptEntryWire[];
203
+ /** True for a well-formed ActivityStateWire value. */
204
+ export declare function isActivityStateWire(value: unknown): value is ActivityStateWire;
205
+ /** True for a well-formed ActivityReasonWire value. */
206
+ export declare function isActivityReasonWire(value: unknown): value is ActivityReasonWire;
207
+ /** Narrows a usage payload to the SessionUsageWire subset. Throws on a malformed required field. */
208
+ export declare function parseSessionUsageWire(payload: JsonValue): SessionUsageWire;
209
+ /** Narrows a session-event payload to SessionEventWire. Throws on a malformed required field. */
210
+ export declare function parseSessionEventWire(payload: JsonValue): SessionEventWire;
211
+ /** Narrows one board-column row. Throws on a malformed required field. */
212
+ export declare function parseBoardColumnWire(value: JsonValue): BoardColumnWire;
213
+ /** Narrows one board-task row. Throws on a malformed required field. */
214
+ export declare function parseBoardTaskWire(value: JsonValue): BoardTaskWire;
215
+ /** Narrows one backlog-item row. Throws on a malformed required field. */
216
+ export declare function parseBacklogItemWire(value: JsonValue): BacklogItemWire;
217
+ /** Narrows a read-diff file-list payload. Throws on a malformed required field. */
218
+ export declare function parseDiffFileListWire(payload: JsonValue): DiffFileListWire;
219
+ /** Narrows a read-diff single-file payload. Throws on a malformed required field. */
220
+ export declare function parseDiffFileContentWire(payload: JsonValue): DiffFileContentWire;