@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/README.md +15 -2
- package/package.json +16 -6
- package/src/office-catalog.ts +1342 -0
- package/src/protocol.ts +138 -81
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
type: "status";
|
|
85
|
+
model?: string;
|
|
86
|
+
provider?: string;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
export type ClientMessage =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
160
|
+
type: "error";
|
|
161
|
+
code: string;
|
|
162
|
+
message: string;
|
|
131
163
|
}
|
|
132
164
|
|
|
133
165
|
export type ServerMessage =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
}
|