@dieulc/pi-office-protocol 0.1.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
@@ -17,7 +17,13 @@
17
17
  * session and streams the assistant's reply back as `agent_message`.
18
18
  *
19
19
  * Every connection starts with a `hello` (client) / `welcome` (server) pair so
20
- * both sides know which Office host app is attached.
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).
21
27
  */
22
28
 
23
29
  /** Default TCP port the bridge server listens on (loopback only). */
@@ -35,109 +41,135 @@ export type OfficeToolId = string;
35
41
  /* ── Client → Server ─────────────────────────────────────────────────── */
36
42
 
37
43
  export interface HelloMessage {
38
- type: "hello";
39
- protocolVersion: number;
40
- host: OfficeHostApp;
41
- clientName: string;
42
- paneId: string;
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;
43
57
  }
44
58
 
45
59
  export interface PingMessage {
46
- type: "ping";
47
- ts: number;
60
+ type: "ping";
61
+ ts: number;
48
62
  }
49
63
 
50
64
  /** Result of a proxied Office.js tool call executed by the pane. */
51
65
  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;
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;
60
74
  }
61
75
 
62
76
  /** User typed a prompt in the add-in sidebar; ask Pi to answer it. */
63
77
  export interface UserMessageMessage {
64
- type: "user_message";
65
- text: string;
78
+ type: "user_message";
79
+ text: string;
66
80
  }
67
81
 
68
82
  /** Pane notifies the server it finished applying a message (optional). */
69
83
  export interface ClientStatusMessage {
70
- type: "status";
71
- model?: string;
72
- provider?: string;
84
+ type: "status";
85
+ model?: string;
86
+ provider?: string;
73
87
  }
74
88
 
75
89
  export type ClientMessage =
76
- | HelloMessage
77
- | PingMessage
78
- | ToolResultMessage
79
- | UserMessageMessage
80
- | ClientStatusMessage;
90
+ | HelloMessage
91
+ | PingMessage
92
+ | ToolResultMessage
93
+ | UserMessageMessage
94
+ | ClientStatusMessage;
81
95
 
82
96
  /* ── Server → Client ─────────────────────────────────────────────────── */
83
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
+
84
106
  export interface WelcomeMessage {
85
- type: "welcome";
86
- protocolVersion: number;
87
- piVersion: string | null;
88
- serverName: string;
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[];
89
121
  }
90
122
 
91
123
  export interface PongMessage {
92
- type: "pong";
93
- ts: number;
124
+ type: "pong";
125
+ ts: number;
94
126
  }
95
127
 
96
128
  /** Pi agent asks the pane to execute one Office.js operation. */
97
129
  export interface ToolCallMessage {
98
- type: "tool_call";
99
- id: string;
100
- tool: OfficeToolId;
101
- args: Record<string, unknown>;
130
+ type: "tool_call";
131
+ id: string;
132
+ tool: OfficeToolId;
133
+ args: Record<string, unknown>;
102
134
  }
103
135
 
104
136
  /** A chunk of the assistant's reply (streamed, display-only). */
105
137
  export interface AgentDeltaMessage {
106
- type: "agent_message";
107
- kind: "delta";
108
- text: string;
138
+ type: "agent_message";
139
+ kind: "delta";
140
+ text: string;
109
141
  }
110
142
 
111
143
  /** Final assistant reply for a pane-initiated prompt. */
112
144
  export interface AgentFinalMessage {
113
- type: "agent_message";
114
- kind: "final";
115
- text: string;
116
- messageId?: string;
145
+ type: "agent_message";
146
+ kind: "final";
147
+ text: string;
148
+ messageId?: string;
117
149
  }
118
150
 
119
151
  /** Non-office tool activity (bash, read, …) so the pane can show progress. */
120
152
  export interface ToolActivityMessage {
121
- type: "tool_activity";
122
- tool: string;
123
- status: "start" | "end" | "error";
124
- summary?: string;
153
+ type: "tool_activity";
154
+ tool: string;
155
+ status: "start" | "end" | "error";
156
+ summary?: string;
125
157
  }
126
158
 
127
159
  export interface ServerErrorMessage {
128
- type: "error";
129
- code: string;
130
- message: string;
160
+ type: "error";
161
+ code: string;
162
+ message: string;
131
163
  }
132
164
 
133
165
  export type ServerMessage =
134
- | WelcomeMessage
135
- | PongMessage
136
- | ToolCallMessage
137
- | AgentDeltaMessage
138
- | AgentFinalMessage
139
- | ToolActivityMessage
140
- | ServerErrorMessage;
166
+ | WelcomeMessage
167
+ | PongMessage
168
+ | ToolCallMessage
169
+ | AgentDeltaMessage
170
+ | AgentFinalMessage
171
+ | ToolActivityMessage
172
+ | ServerErrorMessage;
141
173
 
142
174
  /* ── Utilities ───────────────────────────────────────────────────────── */
143
175
 
@@ -145,34 +177,59 @@ let callCounter = 0;
145
177
 
146
178
  /** Monotonic id generator for tool_call / tool_result correlation. */
147
179
  export function nextCallId(prefix = "c"): string {
148
- callCounter += 1;
149
- return `${prefix}-${Date.now().toString(36)}-${callCounter.toString(36)}`;
180
+ callCounter += 1;
181
+ return `${prefix}-${Date.now().toString(36)}-${callCounter.toString(36)}`;
150
182
  }
151
183
 
152
184
  /** Validate an incoming client frame. Returns the parsed message or throws. */
153
185
  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;
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
+ );
169
223
  }
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)}'`);
224
+ }
225
+ return m;
177
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
+ }
178
235
  }