@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.
- package/README.md +15 -2
- package/package.json +5 -2
- 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
|
-
|
|
18
|
-
|
|
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.
|
|
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": [
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
}
|