@dieulc/pi-office-protocol 0.1.0 → 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.
Files changed (3) hide show
  1. package/README.md +15 -2
  2. package/package.json +5 -2
  3. package/src/protocol.ts +204 -178
package/README.md CHANGED
@@ -14,5 +14,18 @@ same format at runtime.
14
14
  - **Client → Server:** `hello`, `ping`, `tool_result`, `user_message`, `status`
15
15
  - **Server → Client:** `welcome`, `pong`, `tool_call`, `agent_message` (delta/final), `tool_activity`, `error`
16
16
 
17
- See `src/protocol.ts` for the typed shapes. Bump `BRIDGE_PROTOCOL_VERSION` on
18
- breaking changes.
17
+ ## Version & capabilities
18
+
19
+ `BRIDGE_PROTOCOL_VERSION` is bumped only on **breaking** message-shape changes.
20
+ Additive fields keep both halves compatible:
21
+
22
+ - `welcome.serverVersion?: string` — the bridge extension's package version.
23
+ - `welcome.capabilities?: BridgeCapability[]` — currently `"http-health"`,
24
+ meaning the server answers `GET /health` with JSON metadata.
25
+
26
+ Both are **optional**. An absent `capabilities` array (or absent
27
+ `serverVersion`) means the peer is a legacy 0.1.0 bridge: clients must degrade
28
+ gracefully (the live WebSocket status is still authoritative) instead of
29
+ assuming the server is unreachable.
30
+
31
+ See `src/protocol.ts` for the typed shapes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dieulc/pi-office-protocol",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Shared bridge wire protocol between the pi-office-bridge Pi extension and the pi-for-office task-pane add-in.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,10 @@
9
9
  "exports": {
10
10
  ".": "./src/protocol.ts"
11
11
  },
12
- "files": ["src/protocol.ts", "README.md"],
12
+ "files": [
13
+ "src/protocol.ts",
14
+ "README.md"
15
+ ],
13
16
  "publishConfig": {
14
17
  "access": "public"
15
18
  },
package/src/protocol.ts CHANGED
@@ -1,178 +1,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.
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
- export interface WelcomeMessage {
85
- type: "welcome";
86
- protocolVersion: number;
87
- piVersion: string | null;
88
- serverName: string;
89
- }
90
-
91
- export interface PongMessage {
92
- type: "pong";
93
- ts: number;
94
- }
95
-
96
- /** Pi agent asks the pane to execute one Office.js operation. */
97
- export interface ToolCallMessage {
98
- type: "tool_call";
99
- id: string;
100
- tool: OfficeToolId;
101
- args: Record<string, unknown>;
102
- }
103
-
104
- /** A chunk of the assistant's reply (streamed, display-only). */
105
- export interface AgentDeltaMessage {
106
- type: "agent_message";
107
- kind: "delta";
108
- text: string;
109
- }
110
-
111
- /** Final assistant reply for a pane-initiated prompt. */
112
- export interface AgentFinalMessage {
113
- type: "agent_message";
114
- kind: "final";
115
- text: string;
116
- messageId?: string;
117
- }
118
-
119
- /** Non-office tool activity (bash, read, …) so the pane can show progress. */
120
- export interface ToolActivityMessage {
121
- type: "tool_activity";
122
- tool: string;
123
- status: "start" | "end" | "error";
124
- summary?: string;
125
- }
126
-
127
- export interface ServerErrorMessage {
128
- type: "error";
129
- code: string;
130
- message: string;
131
- }
132
-
133
- export type ServerMessage =
134
- | WelcomeMessage
135
- | PongMessage
136
- | ToolCallMessage
137
- | AgentDeltaMessage
138
- | AgentFinalMessage
139
- | ToolActivityMessage
140
- | ServerErrorMessage;
141
-
142
- /* ── Utilities ───────────────────────────────────────────────────────── */
143
-
144
- let callCounter = 0;
145
-
146
- /** Monotonic id generator for tool_call / tool_result correlation. */
147
- export function nextCallId(prefix = "c"): string {
148
- callCounter += 1;
149
- return `${prefix}-${Date.now().toString(36)}-${callCounter.toString(36)}`;
150
- }
151
-
152
- /** Validate an incoming client frame. Returns the parsed message or throws. */
153
- export function parseClientMessage(raw: string): ClientMessage {
154
- const data: unknown = JSON.parse(raw);
155
- if (typeof data !== "object" || data === null) {
156
- throw new Error("bridge: expected a JSON object message");
157
- }
158
- const msg = data as Record<string, unknown>;
159
- if (typeof msg.type !== "string") {
160
- throw new Error("bridge: message missing 'type'");
161
- }
162
- switch (msg.type) {
163
- case "hello": {
164
- const m = data as HelloMessage;
165
- if (!["excel", "word", "powerpoint"].includes(m.host)) {
166
- throw new Error(`bridge: hello with unknown host '${String(m.host)}'`);
167
- }
168
- return m;
169
- }
170
- case "ping":
171
- case "tool_result":
172
- case "user_message":
173
- case "status":
174
- return data as ClientMessage;
175
- default:
176
- throw new Error(`bridge: unknown client message type '${String(msg.type)}'`);
177
- }
178
- }
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
+ }