@dieulc/pi-office-protocol 0.2.0 → 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.
package/src/protocol.ts CHANGED
@@ -1,204 +1,235 @@
1
- /**
2
- * Bridge protocol — shared wire format between the pi-for-office task pane
3
- * (client) and this Pi extension (server).
4
- *
5
- * The server lives inside a local Pi process. The task pane connects to it
6
- * over WebSocket at `ws://127.0.0.1:<port>`.
7
- *
8
- * Two flows are supported:
9
- *
10
- * 1. **Tool proxy (Pi → pane)** — the Pi agent calls an `office_*` tool; the
11
- * extension forwards a `tool_call` to the attached pane for the matching
12
- * host app; the pane executes the Office.js operation and answers with a
13
- * `tool_result`.
14
- *
15
- * 2. **Pane-driven chat (pane → Pi)** — the user types in the add-in sidebar;
16
- * the pane forwards a `user_message`; the extension injects it into the Pi
17
- * session and streams the assistant's reply back as `agent_message`.
18
- *
19
- * Every connection starts with a `hello` (client) / `welcome` (server) pair so
20
- * both sides know which Office host app is attached.
21
- */
22
-
23
- /** Default TCP port the bridge server listens on (loopback only). */
24
- export const BRIDGE_DEFAULT_PORT = 38617;
25
-
26
- /** Protocol version. Bump on breaking message-shape changes. */
27
- export const BRIDGE_PROTOCOL_VERSION = 1;
28
-
29
- /** Office host applications the add-in can attach. */
30
- export type OfficeHostApp = "excel" | "word" | "powerpoint";
31
-
32
- /** Every office tool name is namespaced by host: "excel.read_range" etc. */
33
- export type OfficeToolId = string;
34
-
35
- /* ── Client Server ─────────────────────────────────────────────────── */
36
-
37
- export interface HelloMessage {
38
- type: "hello";
39
- protocolVersion: number;
40
- host: OfficeHostApp;
41
- clientName: string;
42
- paneId: string;
43
- }
44
-
45
- export interface PingMessage {
46
- type: "ping";
47
- ts: number;
48
- }
49
-
50
- /** Result of a proxied Office.js tool call executed by the pane. */
51
- export interface ToolResultMessage {
52
- type: "tool_result";
53
- id: string;
54
- ok: boolean;
55
- /** Markdown/text shown to the LLM. */
56
- text: string;
57
- /** Structured details (may be large; used by the agent tool result). */
58
- details?: unknown;
59
- error?: string;
60
- }
61
-
62
- /** User typed a prompt in the add-in sidebar; ask Pi to answer it. */
63
- export interface UserMessageMessage {
64
- type: "user_message";
65
- text: string;
66
- }
67
-
68
- /** Pane notifies the server it finished applying a message (optional). */
69
- export interface ClientStatusMessage {
70
- type: "status";
71
- model?: string;
72
- provider?: string;
73
- }
74
-
75
- export type ClientMessage =
76
- | HelloMessage
77
- | PingMessage
78
- | ToolResultMessage
79
- | UserMessageMessage
80
- | ClientStatusMessage;
81
-
82
- /* ── Server Client ─────────────────────────────────────────────────── */
83
-
84
- /**
85
- * Optional server capabilities advertised in `welcome`. Clients must treat an
86
- * absent `capabilities` array as "legacy bridge" and degrade gracefully.
87
- *
88
- * - `http-health` — the server answers `GET /health` with JSON metadata.
89
- */
90
- export type BridgeCapability = "http-health";
91
-
92
- export interface WelcomeMessage {
93
- type: "welcome";
94
- protocolVersion: number;
95
- piVersion: string | null;
96
- serverName: string;
97
- /**
98
- * The bridge extension's own package version (e.g. `"0.2.0"`). Optional so
99
- * older servers (0.1.0) keep working absent means "unknown/legacy".
100
- */
101
- serverVersion?: string;
102
- /**
103
- * Optional server capabilities. Additive: absent means the server predates
104
- * capability advertisement (legacy 0.1.0 bridge).
105
- */
106
- capabilities?: BridgeCapability[];
107
- }
108
-
109
- export interface PongMessage {
110
- type: "pong";
111
- ts: number;
112
- }
113
-
114
- /** Pi agent asks the pane to execute one Office.js operation. */
115
- export interface ToolCallMessage {
116
- type: "tool_call";
117
- id: string;
118
- tool: OfficeToolId;
119
- args: Record<string, unknown>;
120
- }
121
-
122
- /** A chunk of the assistant's reply (streamed, display-only). */
123
- export interface AgentDeltaMessage {
124
- type: "agent_message";
125
- kind: "delta";
126
- text: string;
127
- }
128
-
129
- /** Final assistant reply for a pane-initiated prompt. */
130
- export interface AgentFinalMessage {
131
- type: "agent_message";
132
- kind: "final";
133
- text: string;
134
- messageId?: string;
135
- }
136
-
137
- /** Non-office tool activity (bash, read, …) so the pane can show progress. */
138
- export interface ToolActivityMessage {
139
- type: "tool_activity";
140
- tool: string;
141
- status: "start" | "end" | "error";
142
- summary?: string;
143
- }
144
-
145
- export interface ServerErrorMessage {
146
- type: "error";
147
- code: string;
148
- message: string;
149
- }
150
-
151
- export type ServerMessage =
152
- | WelcomeMessage
153
- | PongMessage
154
- | ToolCallMessage
155
- | AgentDeltaMessage
156
- | AgentFinalMessage
157
- | ToolActivityMessage
158
- | ServerErrorMessage;
159
-
160
- /* ── Utilities ───────────────────────────────────────────────────────── */
161
-
162
- let callCounter = 0;
163
-
164
- /** Monotonic id generator for tool_call / tool_result correlation. */
165
- export function nextCallId(prefix = "c"): string {
166
- callCounter += 1;
167
- return `${prefix}-${Date.now().toString(36)}-${callCounter.toString(36)}`;
168
- }
169
-
170
- /** Validate an incoming client frame. Returns the parsed message or throws. */
171
- export function parseClientMessage(raw: string): ClientMessage {
172
- // JSON.parse throws on malformed input — wrap it at this boundary so the
173
- // caller always sees a descriptive bridge error instead of a SyntaxError.
174
- let data: unknown;
175
- try {
176
- data = JSON.parse(raw);
177
- } catch (error) {
178
- const detail = error instanceof Error ? error.message : String(error);
179
- throw new Error(`bridge: message is not valid JSON (${detail})`);
180
- }
181
- if (typeof data !== "object" || data === null) {
182
- throw new Error("bridge: expected a JSON object message");
183
- }
184
- const msg = data as Record<string, unknown>;
185
- if (typeof msg.type !== "string") {
186
- throw new Error("bridge: message missing 'type'");
187
- }
188
- switch (msg.type) {
189
- case "hello": {
190
- const m = data as HelloMessage;
191
- if (!["excel", "word", "powerpoint"].includes(m.host)) {
192
- throw new Error(`bridge: hello with unknown host '${String(m.host)}'`);
193
- }
194
- return m;
195
- }
196
- case "ping":
197
- case "tool_result":
198
- case "user_message":
199
- case "status":
200
- return data as ClientMessage;
201
- default:
202
- throw new Error(`bridge: unknown client message type '${String(msg.type)}'`);
203
- }
204
- }
1
+ /**
2
+ * Bridge protocol — shared wire format between the pi-for-office task pane
3
+ * (client) and this Pi extension (server).
4
+ *
5
+ * The server lives inside a local Pi process. The task pane connects to it
6
+ * over WebSocket at `ws://127.0.0.1:<port>`.
7
+ *
8
+ * Two flows are supported:
9
+ *
10
+ * 1. **Tool proxy (Pi → pane)** — the Pi agent calls an `office_*` tool; the
11
+ * extension forwards a `tool_call` to the attached pane for the matching
12
+ * host app; the pane executes the Office.js operation and answers with a
13
+ * `tool_result`.
14
+ *
15
+ * 2. **Pane-driven chat (pane → Pi)** — the user types in the add-in sidebar;
16
+ * the pane forwards a `user_message`; the extension injects it into the Pi
17
+ * session and streams the assistant's reply back as `agent_message`.
18
+ *
19
+ * Every connection starts with a `hello` (client) / `welcome` (server) pair so
20
+ * both sides know which Office host app is attached. The client may also
21
+ * advertise the op ids it can execute (`hello.ops`) plus the catalog version
22
+ * they were derived from; the server validates them against its own catalog.
23
+ *
24
+ * The office op catalog is exported via the `@dieulc/pi-office-protocol/office-catalog`
25
+ * subpath (kept out of this entry so Node loads this raw-TS file without a
26
+ * `.js`→`.ts` rewrite).
27
+ */
28
+
29
+ /** Default TCP port the bridge server listens on (loopback only). */
30
+ export const BRIDGE_DEFAULT_PORT = 38617;
31
+
32
+ /** Protocol version. Bump on breaking message-shape changes. */
33
+ export const BRIDGE_PROTOCOL_VERSION = 1;
34
+
35
+ /** Office host applications the add-in can attach. */
36
+ export type OfficeHostApp = "excel" | "word" | "powerpoint";
37
+
38
+ /** Every office tool name is namespaced by host: "excel.read_range" etc. */
39
+ export type OfficeToolId = string;
40
+
41
+ /* ── Client → Server ─────────────────────────────────────────────────── */
42
+
43
+ export interface HelloMessage {
44
+ type: "hello";
45
+ protocolVersion: number;
46
+ host: OfficeHostApp;
47
+ clientName: string;
48
+ paneId: string;
49
+ /**
50
+ * Op ids this pane can execute (optional; additive since protocol v1).
51
+ * When absent the pane is treated as a legacy 0.2.x client and only the
52
+ * v1 op set is activated server-side.
53
+ */
54
+ ops?: string[];
55
+ /** Catalog version the pane's ops were derived from (see office-catalog.ts). */
56
+ catalogVersion?: number;
57
+ }
58
+
59
+ export interface PingMessage {
60
+ type: "ping";
61
+ ts: number;
62
+ }
63
+
64
+ /** Result of a proxied Office.js tool call executed by the pane. */
65
+ export interface ToolResultMessage {
66
+ type: "tool_result";
67
+ id: string;
68
+ ok: boolean;
69
+ /** Markdown/text shown to the LLM. */
70
+ text: string;
71
+ /** Structured details (may be large; used by the agent tool result). */
72
+ details?: unknown;
73
+ error?: string;
74
+ }
75
+
76
+ /** User typed a prompt in the add-in sidebar; ask Pi to answer it. */
77
+ export interface UserMessageMessage {
78
+ type: "user_message";
79
+ text: string;
80
+ }
81
+
82
+ /** Pane notifies the server it finished applying a message (optional). */
83
+ export interface ClientStatusMessage {
84
+ type: "status";
85
+ model?: string;
86
+ provider?: string;
87
+ }
88
+
89
+ export type ClientMessage =
90
+ | HelloMessage
91
+ | PingMessage
92
+ | ToolResultMessage
93
+ | UserMessageMessage
94
+ | ClientStatusMessage;
95
+
96
+ /* ── Server → Client ─────────────────────────────────────────────────── */
97
+
98
+ /**
99
+ * Optional server capabilities advertised in `welcome`. Clients must treat an
100
+ * absent `capabilities` array as "legacy bridge" and degrade gracefully.
101
+ *
102
+ * - `http-health` — the server answers `GET /health` with JSON metadata.
103
+ */
104
+ export type BridgeCapability = "http-health";
105
+
106
+ export interface WelcomeMessage {
107
+ type: "welcome";
108
+ protocolVersion: number;
109
+ piVersion: string | null;
110
+ serverName: string;
111
+ /**
112
+ * The bridge extension's own package version (e.g. `"0.2.0"`). Optional so
113
+ * older servers (0.1.0) keep working — absent means "unknown/legacy".
114
+ */
115
+ serverVersion?: string;
116
+ /**
117
+ * Optional server capabilities. Additive: absent means the server predates
118
+ * capability advertisement (legacy 0.1.0 bridge).
119
+ */
120
+ capabilities?: BridgeCapability[];
121
+ }
122
+
123
+ export interface PongMessage {
124
+ type: "pong";
125
+ ts: number;
126
+ }
127
+
128
+ /** Pi agent asks the pane to execute one Office.js operation. */
129
+ export interface ToolCallMessage {
130
+ type: "tool_call";
131
+ id: string;
132
+ tool: OfficeToolId;
133
+ args: Record<string, unknown>;
134
+ }
135
+
136
+ /** A chunk of the assistant's reply (streamed, display-only). */
137
+ export interface AgentDeltaMessage {
138
+ type: "agent_message";
139
+ kind: "delta";
140
+ text: string;
141
+ }
142
+
143
+ /** Final assistant reply for a pane-initiated prompt. */
144
+ export interface AgentFinalMessage {
145
+ type: "agent_message";
146
+ kind: "final";
147
+ text: string;
148
+ messageId?: string;
149
+ }
150
+
151
+ /** Non-office tool activity (bash, read, …) so the pane can show progress. */
152
+ export interface ToolActivityMessage {
153
+ type: "tool_activity";
154
+ tool: string;
155
+ status: "start" | "end" | "error";
156
+ summary?: string;
157
+ }
158
+
159
+ export interface ServerErrorMessage {
160
+ type: "error";
161
+ code: string;
162
+ message: string;
163
+ }
164
+
165
+ export type ServerMessage =
166
+ | WelcomeMessage
167
+ | PongMessage
168
+ | ToolCallMessage
169
+ | AgentDeltaMessage
170
+ | AgentFinalMessage
171
+ | ToolActivityMessage
172
+ | ServerErrorMessage;
173
+
174
+ /* ── Utilities ───────────────────────────────────────────────────────── */
175
+
176
+ let callCounter = 0;
177
+
178
+ /** Monotonic id generator for tool_call / tool_result correlation. */
179
+ export function nextCallId(prefix = "c"): string {
180
+ callCounter += 1;
181
+ return `${prefix}-${Date.now().toString(36)}-${callCounter.toString(36)}`;
182
+ }
183
+
184
+ /** Validate an incoming client frame. Returns the parsed message or throws. */
185
+ export function parseClientMessage(raw: string): ClientMessage {
186
+ // JSON.parse throws on malformed input — wrap it at this boundary so the
187
+ // caller always sees a descriptive bridge error instead of a SyntaxError.
188
+ let data: unknown;
189
+ try {
190
+ data = JSON.parse(raw);
191
+ } catch (error) {
192
+ const detail = error instanceof Error ? error.message : String(error);
193
+ throw new Error(`bridge: message is not valid JSON (${detail})`);
194
+ }
195
+ if (typeof data !== "object" || data === null) {
196
+ throw new Error("bridge: expected a JSON object message");
197
+ }
198
+ const msg = data as Record<string, unknown>;
199
+ if (typeof msg.type !== "string") {
200
+ throw new Error("bridge: message missing 'type'");
201
+ }
202
+ switch (msg.type) {
203
+ case "hello": {
204
+ const m = data as HelloMessage;
205
+ if (!["excel", "word", "powerpoint"].includes(m.host)) {
206
+ throw new Error(`bridge: hello with unknown host '${String(m.host)}'`);
207
+ }
208
+ if (m.ops !== undefined) {
209
+ if (!Array.isArray(m.ops) || m.ops.some((op) => typeof op !== "string")) {
210
+ throw new Error("bridge: hello with malformed 'ops' (expected string[])");
211
+ }
212
+ }
213
+ if (m.catalogVersion !== undefined) {
214
+ if (
215
+ typeof m.catalogVersion !== "number" ||
216
+ !Number.isFinite(m.catalogVersion) ||
217
+ !Number.isInteger(m.catalogVersion) ||
218
+ m.catalogVersion < 1
219
+ ) {
220
+ throw new Error(
221
+ `bridge: hello with malformed 'catalogVersion' (${String(m.catalogVersion)})`,
222
+ );
223
+ }
224
+ }
225
+ return m;
226
+ }
227
+ case "ping":
228
+ case "tool_result":
229
+ case "user_message":
230
+ case "status":
231
+ return data as ClientMessage;
232
+ default:
233
+ throw new Error(`bridge: unknown client message type '${String(msg.type)}'`);
234
+ }
235
+ }