@dieulc/pi-office-bridge 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 +180 -97
- package/package.json +64 -60
- package/src/active-tools.ts +79 -0
- package/src/bridge-server.ts +283 -16
- package/src/index.ts +165 -16
- package/src/office-tools.ts +39 -238
- package/src/protocol.ts +5 -2
package/src/index.ts
CHANGED
|
@@ -21,8 +21,11 @@ import type {
|
|
|
21
21
|
RegisteredCommand,
|
|
22
22
|
} from "@earendil-works/pi-coding-agent";
|
|
23
23
|
import { Type, type Static } from "typebox";
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { dirname, resolve } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
24
27
|
|
|
25
|
-
import { BRIDGE_DEFAULT_PORT } from "./protocol.js";
|
|
28
|
+
import { CATALOG_VERSION, BRIDGE_DEFAULT_PORT } from "./protocol.js";
|
|
26
29
|
import type { AttachedPane } from "./bridge-server.js";
|
|
27
30
|
import { OfficeBridgeServer } from "./bridge-server.js";
|
|
28
31
|
import {
|
|
@@ -32,6 +35,10 @@ import {
|
|
|
32
35
|
HOST_APP_LABEL,
|
|
33
36
|
} from "./office-tools.js";
|
|
34
37
|
import type { OfficeToolDescriptor } from "./office-tools.js";
|
|
38
|
+
import {
|
|
39
|
+
reconcileOfficeToolActivation,
|
|
40
|
+
type PaneCapability,
|
|
41
|
+
} from "./active-tools.js";
|
|
35
42
|
|
|
36
43
|
const FLAG_PORT = "office-bridge-port";
|
|
37
44
|
|
|
@@ -46,6 +53,24 @@ function piVersion(): string | null {
|
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The bridge extension's own package version, read from the `package.json`
|
|
58
|
+
* that ships next to this module. Falls back to `"unknown"` so a bundled or
|
|
59
|
+
* relocated install never breaks the `welcome` frame.
|
|
60
|
+
*/
|
|
61
|
+
function serverVersion(): string {
|
|
62
|
+
try {
|
|
63
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
64
|
+
const raw = readFileSync(resolve(here, "..", "package.json"), "utf8");
|
|
65
|
+
const pkg = JSON.parse(raw) as { version?: unknown };
|
|
66
|
+
return typeof pkg.version === "string" && pkg.version.length > 0
|
|
67
|
+
? pkg.version
|
|
68
|
+
: "unknown";
|
|
69
|
+
} catch {
|
|
70
|
+
return "unknown";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
export default function (pi: ExtensionAPI): void {
|
|
50
75
|
let server: OfficeBridgeServer | null = null;
|
|
51
76
|
let currentCtx: ExtensionContext | null = null;
|
|
@@ -62,12 +87,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
62
87
|
const raw = pi.getFlag(FLAG_PORT);
|
|
63
88
|
if (typeof raw === "string" && raw.trim() !== "") {
|
|
64
89
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
65
|
-
if (Number.isFinite(parsed) && parsed > 0 && parsed < 65536)
|
|
90
|
+
if (Number.isFinite(parsed) && parsed > 0 && parsed < 65536)
|
|
91
|
+
return parsed;
|
|
66
92
|
}
|
|
67
93
|
const env = process.env.PI_OFFICE_BRIDGE_PORT;
|
|
68
94
|
if (env) {
|
|
69
95
|
const parsed = Number.parseInt(env, 10);
|
|
70
|
-
if (Number.isFinite(parsed) && parsed > 0 && parsed < 65536)
|
|
96
|
+
if (Number.isFinite(parsed) && parsed > 0 && parsed < 65536)
|
|
97
|
+
return parsed;
|
|
71
98
|
}
|
|
72
99
|
return BRIDGE_DEFAULT_PORT;
|
|
73
100
|
}
|
|
@@ -82,11 +109,28 @@ export default function (pi: ExtensionAPI): void {
|
|
|
82
109
|
}
|
|
83
110
|
const port = server.actualPort ?? resolvePort();
|
|
84
111
|
if (panes.length === 0) {
|
|
85
|
-
ui.setStatus(
|
|
112
|
+
ui.setStatus(
|
|
113
|
+
"office-bridge",
|
|
114
|
+
`office bridge on :${port} — no app attached`,
|
|
115
|
+
);
|
|
86
116
|
return;
|
|
87
117
|
}
|
|
88
118
|
const labels = panes.map((p) => HOST_APP_LABEL[p.host]).join(", ");
|
|
89
|
-
ui.setStatus(
|
|
119
|
+
ui.setStatus(
|
|
120
|
+
"office-bridge",
|
|
121
|
+
`office: ${labels} attached (bridge :${port})`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let panesChangedTimer: ReturnType<typeof setTimeout> | null = null;
|
|
126
|
+
|
|
127
|
+
/** Debounced status refresh triggered by pane attach/detach (~100 ms). */
|
|
128
|
+
function scheduleStatusUpdate(): void {
|
|
129
|
+
if (panesChangedTimer !== null) clearTimeout(panesChangedTimer);
|
|
130
|
+
panesChangedTimer = setTimeout(() => {
|
|
131
|
+
panesChangedTimer = null;
|
|
132
|
+
updateStatus();
|
|
133
|
+
}, 100);
|
|
90
134
|
}
|
|
91
135
|
|
|
92
136
|
/** Extract display text from an assistant message content payload. */
|
|
@@ -98,7 +142,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
98
142
|
if (typeof part !== "object" || part === null) continue;
|
|
99
143
|
const p = part as { type?: unknown; text?: unknown };
|
|
100
144
|
if (p.type === "text" && typeof p.text === "string") parts.push(p.text);
|
|
101
|
-
if (p.type === "thinking" && typeof p.text === "string")
|
|
145
|
+
if (p.type === "thinking" && typeof p.text === "string")
|
|
146
|
+
parts.push(p.text);
|
|
102
147
|
}
|
|
103
148
|
return parts.join("\n").trim();
|
|
104
149
|
}
|
|
@@ -110,6 +155,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
110
155
|
name: descriptor.name,
|
|
111
156
|
label: descriptor.label,
|
|
112
157
|
description: descriptor.description,
|
|
158
|
+
promptSnippet: descriptor.promptSnippet,
|
|
113
159
|
promptGuidelines: descriptor.promptGuidelines,
|
|
114
160
|
parameters: descriptor.parameters,
|
|
115
161
|
executionMode: "sequential",
|
|
@@ -120,6 +166,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
120
166
|
_onUpdate,
|
|
121
167
|
_ctx,
|
|
122
168
|
): Promise<AgentToolResult<unknown>> {
|
|
169
|
+
// SAFETY: `Params` is the TypeBox-derived shape of
|
|
170
|
+
// `descriptor.parameters` (always an object); the bridge forwards it
|
|
171
|
+
// verbatim as the Office.js args record, so this widening is sound.
|
|
123
172
|
const args = params as unknown as Record<string, unknown>;
|
|
124
173
|
const active = server;
|
|
125
174
|
if (!active?.isRunning) {
|
|
@@ -127,8 +176,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
127
176
|
`office-bridge: server is not running. Check the Pi extension loaded, then open the add-in.`,
|
|
128
177
|
);
|
|
129
178
|
}
|
|
130
|
-
const result = await active.callOfficeTool(
|
|
131
|
-
|
|
179
|
+
const result = await active.callOfficeTool(
|
|
180
|
+
descriptor.host,
|
|
181
|
+
descriptor.op,
|
|
182
|
+
args,
|
|
183
|
+
signal,
|
|
184
|
+
);
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: "text", text: result.text }],
|
|
187
|
+
details: result.details,
|
|
188
|
+
};
|
|
132
189
|
},
|
|
133
190
|
});
|
|
134
191
|
}
|
|
@@ -139,6 +196,46 @@ export default function (pi: ExtensionAPI): void {
|
|
|
139
196
|
}
|
|
140
197
|
}
|
|
141
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Reconcile the Pi active tool set with the currently attached panes: only
|
|
201
|
+
* the office ops the attached panes advertise stay active; everything else
|
|
202
|
+
* (user/other-extension tools) is preserved. Called on attach/detach.
|
|
203
|
+
*/
|
|
204
|
+
function reconcileActiveTools(panes: readonly AttachedPane[]): void {
|
|
205
|
+
const capabilities: PaneCapability[] = panes.map((pane) => ({
|
|
206
|
+
host: pane.host,
|
|
207
|
+
ops: pane.ops,
|
|
208
|
+
}));
|
|
209
|
+
const next = reconcileOfficeToolActivation(
|
|
210
|
+
pi.getActiveTools(),
|
|
211
|
+
capabilities,
|
|
212
|
+
);
|
|
213
|
+
pi.setActiveTools(next);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Stable per-turn pane-awareness block appended to the system prompt. */
|
|
217
|
+
function paneContextBlock(): string | null {
|
|
218
|
+
const active = server;
|
|
219
|
+
if (!active?.isRunning) return null;
|
|
220
|
+
const panes = active.attachedPanes();
|
|
221
|
+
if (panes.length === 0) return null;
|
|
222
|
+
|
|
223
|
+
const lines = panes.map((p) => {
|
|
224
|
+
const legacy =
|
|
225
|
+
p.ops === null
|
|
226
|
+
? " (legacy client: only v1 ops — update the add-in for newer tools)"
|
|
227
|
+
: "";
|
|
228
|
+
return `- ${HOST_APP_LABEL[p.host]} is attached via the office bridge${legacy}. The open document is live: the office_${p.host}_* tools edit it directly.`;
|
|
229
|
+
});
|
|
230
|
+
return (
|
|
231
|
+
"\n\n## Attached Office documents\n" +
|
|
232
|
+
lines.join("\n") +
|
|
233
|
+
"\n\nEditing these documents with the office_* tools is fully supported, including formatting and " +
|
|
234
|
+
"structured documents (titles, headings, lists, tables, alignment, indents). Never claim formatting is " +
|
|
235
|
+
"unavailable, and never emit HTML for Word — use the office_word_* tools."
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
142
239
|
/* ── lifecycle ─────────────────────────────────────────────────────── */
|
|
143
240
|
|
|
144
241
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -154,6 +251,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
154
251
|
const bridge = new OfficeBridgeServer({
|
|
155
252
|
port,
|
|
156
253
|
serverName: "pi-office-bridge",
|
|
254
|
+
serverVersion: serverVersion(),
|
|
157
255
|
piVersion: piVersion(),
|
|
158
256
|
handlers: {
|
|
159
257
|
onUserMessage: (text, pane) => {
|
|
@@ -163,16 +261,36 @@ export default function (pi: ExtensionAPI): void {
|
|
|
163
261
|
// triggers a turn; if a turn is running it is queued until it settles.
|
|
164
262
|
pi.sendUserMessage(text, { deliverAs: "followUp" });
|
|
165
263
|
},
|
|
264
|
+
// Keep the TUI status line live and the active tool set in sync:
|
|
265
|
+
// opening an app activates that host's office tools; closing it
|
|
266
|
+
// deactivates them.
|
|
267
|
+
onPanesChanged: (panes) => {
|
|
268
|
+
scheduleStatusUpdate();
|
|
269
|
+
reconcileActiveTools(panes);
|
|
270
|
+
},
|
|
166
271
|
},
|
|
167
272
|
});
|
|
168
273
|
|
|
169
274
|
try {
|
|
170
275
|
await bridge.start();
|
|
171
276
|
server = bridge;
|
|
172
|
-
ctx.ui.notify(
|
|
277
|
+
ctx.ui.notify(
|
|
278
|
+
`Office bridge listening on ws://127.0.0.1:${port}`,
|
|
279
|
+
"info",
|
|
280
|
+
);
|
|
173
281
|
} catch (error) {
|
|
174
282
|
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
-
|
|
283
|
+
const code = (error as NodeJS.ErrnoException | null)?.code;
|
|
284
|
+
if (code === "EADDRINUSE") {
|
|
285
|
+
ctx.ui.notify(
|
|
286
|
+
`Office bridge: port ${port} is already in use — another Pi process ` +
|
|
287
|
+
`is running the bridge. Start this one on a free port with ` +
|
|
288
|
+
`--${FLAG_PORT} <port> or PI_OFFICE_BRIDGE_PORT=<port>.`,
|
|
289
|
+
"error",
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
ctx.ui.notify(`Office bridge failed to start: ${message}`, "error");
|
|
293
|
+
}
|
|
176
294
|
console.error(`[office-bridge] start failed: ${message}`);
|
|
177
295
|
}
|
|
178
296
|
updateStatus();
|
|
@@ -181,6 +299,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
181
299
|
pi.on("session_shutdown", async () => {
|
|
182
300
|
currentCtx = null;
|
|
183
301
|
pendingReplyTargets.length = 0;
|
|
302
|
+
if (panesChangedTimer !== null) {
|
|
303
|
+
clearTimeout(panesChangedTimer);
|
|
304
|
+
panesChangedTimer = null;
|
|
305
|
+
}
|
|
184
306
|
const active = server;
|
|
185
307
|
server = null;
|
|
186
308
|
if (active) {
|
|
@@ -190,6 +312,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
190
312
|
|
|
191
313
|
/* ── agent → pane forwarding ───────────────────────────────────────── */
|
|
192
314
|
|
|
315
|
+
// Tell the agent which Office apps are attached before each turn so it uses
|
|
316
|
+
// the office_* tools directly instead of claiming capabilities are missing.
|
|
317
|
+
pi.on("before_agent_start", (event, _ctx) => {
|
|
318
|
+
const block = paneContextBlock();
|
|
319
|
+
if (block === null) return undefined;
|
|
320
|
+
return { systemPrompt: event.systemPrompt + block };
|
|
321
|
+
});
|
|
322
|
+
|
|
193
323
|
// Forward the final assistant reply to the pane that prompted it.
|
|
194
324
|
pi.on("message_end", async (event, _ctx) => {
|
|
195
325
|
if (event.message.role !== "assistant") return;
|
|
@@ -199,6 +329,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
199
329
|
|
|
200
330
|
const text = flattenAssistantText(event.message.content);
|
|
201
331
|
if (!text) return;
|
|
332
|
+
// SAFETY: assistant messages may carry an id that the Pi event type does
|
|
333
|
+
// not surface; read it through a narrow shape check before using it.
|
|
202
334
|
const maybeId = (event.message as unknown as { id?: unknown }).id;
|
|
203
335
|
server.broadcast({
|
|
204
336
|
type: "agent_message",
|
|
@@ -230,7 +362,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
230
362
|
type: "tool_activity",
|
|
231
363
|
tool: event.toolName,
|
|
232
364
|
status: event.isError ? "error" : "end",
|
|
233
|
-
summary: event.isError
|
|
365
|
+
summary: event.isError
|
|
366
|
+
? String(event.result ?? "tool failed")
|
|
367
|
+
: undefined,
|
|
234
368
|
});
|
|
235
369
|
});
|
|
236
370
|
|
|
@@ -248,16 +382,30 @@ export default function (pi: ExtensionAPI): void {
|
|
|
248
382
|
const port = active.actualPort ?? resolvePort();
|
|
249
383
|
const panes = active.attachedPanes();
|
|
250
384
|
if (panes.length === 0) {
|
|
251
|
-
ctx.ui.notify(
|
|
385
|
+
ctx.ui.notify(
|
|
386
|
+
`Office bridge is listening on :${port} — no app attached yet.`,
|
|
387
|
+
"info",
|
|
388
|
+
);
|
|
252
389
|
return;
|
|
253
390
|
}
|
|
254
391
|
const lines = panes.map((p) => {
|
|
255
392
|
const model = p.model ? `, model=${p.model}` : "";
|
|
256
393
|
const provider = p.provider ? `, provider=${p.provider}` : "";
|
|
257
394
|
const ago = Math.max(0, Math.round((Date.now() - p.lastSeen) / 1000));
|
|
258
|
-
|
|
395
|
+
const ops =
|
|
396
|
+
p.ops !== null
|
|
397
|
+
? `${p.ops.length} ops${p.catalogVersion ? ` (catalog v${p.catalogVersion})` : ""}`
|
|
398
|
+
: "legacy client (v1 ops)";
|
|
399
|
+
const ignored =
|
|
400
|
+
p.opsIgnoredCount > 0
|
|
401
|
+
? `, ${p.opsIgnoredCount} advertised op(s) ignored (unknown to this server)`
|
|
402
|
+
: "";
|
|
403
|
+
return `- ${HOST_APP_LABEL[p.host]} (${p.clientName}, pane ${p.paneId.slice(0, 8)})${model}${provider}, seen ${ago}s ago — ${ops}${ignored}`;
|
|
259
404
|
});
|
|
260
|
-
ctx.ui.notify(
|
|
405
|
+
ctx.ui.notify(
|
|
406
|
+
`Office bridge on :${port} (catalog v${CATALOG_VERSION})\n${lines.join("\n")}`,
|
|
407
|
+
"info",
|
|
408
|
+
);
|
|
261
409
|
},
|
|
262
410
|
};
|
|
263
411
|
|
|
@@ -265,10 +413,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
265
413
|
|
|
266
414
|
// Also list every office tool we exposed so users can confirm them:
|
|
267
415
|
pi.registerCommand("office-tools", {
|
|
268
|
-
description:
|
|
416
|
+
description:
|
|
417
|
+
"List the office tools registered by the pi-office bridge extension.",
|
|
269
418
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
270
419
|
ctx.ui.notify(
|
|
271
|
-
`Office tools (${OFFICE_TOOL_NAMES.length}):\n${OFFICE_TOOL_NAMES.join("\n")}`,
|
|
420
|
+
`Office tools (${OFFICE_TOOL_NAMES.length}, catalog v${CATALOG_VERSION}):\n${OFFICE_TOOL_NAMES.join("\n")}`,
|
|
272
421
|
"info",
|
|
273
422
|
);
|
|
274
423
|
},
|
package/src/office-tools.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Office tool catalog —
|
|
3
|
-
* through the bridge.
|
|
2
|
+
* Office tool catalog adapter — thin wrapper over the shared catalog.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Keep `op` values in sync with the add-in's `bridge/tool-registry.ts` (same
|
|
10
|
-
* repository, `packages/add-in`). The pane validates args again at runtime, so
|
|
11
|
-
* this file is the *contract*, not the enforcement point.
|
|
4
|
+
* The authoritative op definitions (names, schemas, descriptions) live in
|
|
5
|
+
* `@dieulc/pi-office-protocol` (`office-catalog.ts`). This module only maps
|
|
6
|
+
* catalog entries to the descriptors this extension registers with Pi, so the
|
|
7
|
+
* Pi side can never drift from the add-in's pane-side registry.
|
|
12
8
|
*/
|
|
13
9
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
10
|
+
import type { TSchema } from "typebox";
|
|
11
|
+
import {
|
|
12
|
+
CATALOG_VERSION,
|
|
13
|
+
HOST_APP_LABEL,
|
|
14
|
+
OFFICE_CATALOG,
|
|
15
|
+
hostForToolName,
|
|
16
|
+
} from "./protocol.js";
|
|
16
17
|
import type { OfficeHostApp } from "./protocol.js";
|
|
17
18
|
|
|
18
19
|
export interface OfficeToolDescriptor {
|
|
@@ -24,239 +25,39 @@ export interface OfficeToolDescriptor {
|
|
|
24
25
|
name: string;
|
|
25
26
|
label: string;
|
|
26
27
|
description: string;
|
|
28
|
+
promptSnippet?: string;
|
|
27
29
|
promptGuidelines?: string[];
|
|
28
30
|
parameters: TSchema;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
/**
|
|
32
|
-
export const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
50
|
-
mode: Type.Optional(
|
|
51
|
-
StringEnum(["compact", "csv", "detailed"], {
|
|
52
|
-
description:
|
|
53
|
-
'"compact" (default): markdown table. "csv": raw values. "detailed": with formulas/formats.',
|
|
54
|
-
}),
|
|
55
|
-
),
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
/* ── Word ────────────────────────────────────────────────────────────── */
|
|
59
|
-
|
|
60
|
-
const WORD_READ_SCOPE = StringEnum(["all", "selection"], {
|
|
61
|
-
description: '"all": whole document. "selection": currently selected text only.',
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
/* ── PowerPoint ──────────────────────────────────────────────────────── */
|
|
65
|
-
|
|
66
|
-
const PPT_SLIDE_INDEX = Type.Integer({
|
|
67
|
-
minimum: 1,
|
|
68
|
-
description: "1-based slide index.",
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
/* ── Catalog ─────────────────────────────────────────────────────────── */
|
|
72
|
-
|
|
73
|
-
export const OFFICE_TOOL_DESCRIPTORS: OfficeToolDescriptor[] = [
|
|
74
|
-
/* Excel */
|
|
75
|
-
{
|
|
76
|
-
host: "excel",
|
|
77
|
-
op: "get_overview",
|
|
78
|
-
name: officeToolName("excel", "get_overview"),
|
|
79
|
-
label: "Excel Workbook Overview",
|
|
80
|
-
description:
|
|
81
|
-
"Read a compact overview of the attached Excel workbook: sheet names, used ranges, " +
|
|
82
|
-
"table names, and named ranges. Call this first before any range operation.",
|
|
83
|
-
promptGuidelines: [
|
|
84
|
-
"Call office_excel_get_overview before office_excel_read_range to learn the workbook structure.",
|
|
85
|
-
],
|
|
86
|
-
parameters: Type.Object({}),
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
host: "excel",
|
|
90
|
-
op: "read_range",
|
|
91
|
-
name: officeToolName("excel", "read_range"),
|
|
92
|
-
label: "Excel Read Range",
|
|
93
|
-
description:
|
|
94
|
-
"Read cell values (and optionally formulas/formatting) from a range in the attached Excel workbook.",
|
|
95
|
-
parameters: EXCEL_READ_RANGE_SCHEMA,
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
host: "excel",
|
|
99
|
-
op: "write_cells",
|
|
100
|
-
name: officeToolName("excel", "write_cells"),
|
|
101
|
-
label: "Excel Write Cells",
|
|
102
|
-
description:
|
|
103
|
-
"Write a 2D array of values into the attached Excel workbook, starting at a top-left cell. " +
|
|
104
|
-
"values[row][col]; the array is written down and to the right from start_cell.",
|
|
105
|
-
promptGuidelines: [
|
|
106
|
-
"Prefer office_excel_write_cells in a single batched call instead of many small edits.",
|
|
107
|
-
"Always verify with office_excel_read_range after office_excel_write_cells when the change is user-visible.",
|
|
108
|
-
],
|
|
109
|
-
parameters: Type.Object({
|
|
110
|
-
start_cell: Type.String({
|
|
111
|
-
description: 'Top-left cell to write from, e.g. "A1" or "Sheet2!B3".',
|
|
112
|
-
}),
|
|
113
|
-
values: Type.Array(Type.Array(Type.Any()), {
|
|
114
|
-
description: "2D array of cell values (rows × cols).",
|
|
115
|
-
}),
|
|
116
|
-
}),
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
host: "excel",
|
|
120
|
-
op: "fill_formula",
|
|
121
|
-
name: officeToolName("excel", "fill_formula"),
|
|
122
|
-
label: "Excel Fill Formula",
|
|
123
|
-
description:
|
|
124
|
-
"Write a formula into a single contiguous range of the attached Excel workbook. " +
|
|
125
|
-
"Relative references adjust as the formula fills.",
|
|
126
|
-
parameters: Type.Object({
|
|
127
|
-
range: Type.String({ description: 'Target range, e.g. "B2:B20" or "Sheet1!C3:F20".' }),
|
|
128
|
-
formula: Type.String({
|
|
129
|
-
description: 'Formula starting with "=", e.g. "=SUM(B2:B10)".',
|
|
130
|
-
}),
|
|
131
|
-
}),
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
/* Word */
|
|
135
|
-
{
|
|
136
|
-
host: "word",
|
|
137
|
-
op: "get_overview",
|
|
138
|
-
name: officeToolName("word", "get_overview"),
|
|
139
|
-
label: "Word Document Overview",
|
|
140
|
-
description:
|
|
141
|
-
"Read a compact overview of the attached Word document: heading outline, paragraph count, " +
|
|
142
|
-
"table count, and word count. Call this first before editing.",
|
|
143
|
-
promptGuidelines: [
|
|
144
|
-
"Call office_word_get_overview before office_word_insert_text or office_word_replace_text.",
|
|
145
|
-
],
|
|
146
|
-
parameters: Type.Object({}),
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
host: "word",
|
|
150
|
-
op: "read_document",
|
|
151
|
-
name: officeToolName("word", "read_document"),
|
|
152
|
-
label: "Word Read Document",
|
|
153
|
-
description:
|
|
154
|
-
"Read text from the attached Word document: the whole body or the current selection.",
|
|
155
|
-
parameters: Type.Object({
|
|
156
|
-
scope: Type.Optional(WORD_READ_SCOPE),
|
|
157
|
-
maxChars: Type.Optional(
|
|
158
|
-
Type.Integer({
|
|
159
|
-
minimum: 100,
|
|
160
|
-
maximum: 200000,
|
|
161
|
-
description: "Cap on characters returned (default 20000).",
|
|
162
|
-
}),
|
|
163
|
-
),
|
|
164
|
-
}),
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
host: "word",
|
|
168
|
-
op: "insert_text",
|
|
169
|
-
name: officeToolName("word", "insert_text"),
|
|
170
|
-
label: "Word Insert Text",
|
|
171
|
-
description:
|
|
172
|
-
"Insert text at the start or end of the attached Word document, or replace the current selection.",
|
|
173
|
-
parameters: Type.Object({
|
|
174
|
-
text: Type.String({ description: "Text to insert." }),
|
|
175
|
-
location: Type.Optional(
|
|
176
|
-
StringEnum(["start", "end", "replace_selection"], {
|
|
177
|
-
description: '"end" (default) appends to the document. "replace_selection" overwrites the selection.',
|
|
178
|
-
}),
|
|
179
|
-
),
|
|
180
|
-
}),
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
host: "word",
|
|
184
|
-
op: "replace_text",
|
|
185
|
-
name: officeToolName("word", "replace_text"),
|
|
186
|
-
label: "Word Replace Text",
|
|
187
|
-
description:
|
|
188
|
-
"Find and replace literal text in the attached Word document. Returns how many occurrences were replaced.",
|
|
189
|
-
parameters: Type.Object({
|
|
190
|
-
find: Type.String({ description: "Literal text to find." }),
|
|
191
|
-
replace: Type.String({ description: "Replacement text." }),
|
|
192
|
-
matchCase: Type.Optional(Type.Boolean({ description: "Case-sensitive match (default false)." })),
|
|
193
|
-
}),
|
|
194
|
-
},
|
|
195
|
-
|
|
196
|
-
/* PowerPoint */
|
|
197
|
-
{
|
|
198
|
-
host: "powerpoint",
|
|
199
|
-
op: "get_overview",
|
|
200
|
-
name: officeToolName("powerpoint", "get_overview"),
|
|
201
|
-
label: "PowerPoint Overview",
|
|
202
|
-
description:
|
|
203
|
-
"Read a compact overview of the attached presentation: slide count, each slide's title and " +
|
|
204
|
-
"shape count. Call this first before any slide operation.",
|
|
205
|
-
promptGuidelines: [
|
|
206
|
-
"Call office_powerpoint_get_overview before office_powerpoint_read_slide or office_powerpoint_add_slide.",
|
|
207
|
-
],
|
|
208
|
-
parameters: Type.Object({}),
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
host: "powerpoint",
|
|
212
|
-
op: "read_slide",
|
|
213
|
-
name: officeToolName("powerpoint", "read_slide"),
|
|
214
|
-
label: "PowerPoint Read Slide",
|
|
215
|
-
description:
|
|
216
|
-
"Read all text content of one slide in the attached presentation (shapes, text frames, notes).",
|
|
217
|
-
parameters: Type.Object({
|
|
218
|
-
slideIndex: PPT_SLIDE_INDEX,
|
|
219
|
-
}),
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
host: "powerpoint",
|
|
223
|
-
op: "add_slide",
|
|
224
|
-
name: officeToolName("powerpoint", "add_slide"),
|
|
225
|
-
label: "PowerPoint Add Slide",
|
|
226
|
-
description:
|
|
227
|
-
"Append a new slide to the attached presentation and navigate to it. Uses the default layout.",
|
|
228
|
-
parameters: Type.Object({}),
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
host: "powerpoint",
|
|
232
|
-
op: "add_text_box",
|
|
233
|
-
name: officeToolName("powerpoint", "add_text_box"),
|
|
234
|
-
label: "PowerPoint Add Text Box",
|
|
235
|
-
description:
|
|
236
|
-
"Add a text box with the given text to a slide. Coordinates/geometry are in points.",
|
|
237
|
-
parameters: Type.Object({
|
|
238
|
-
slideIndex: PPT_SLIDE_INDEX,
|
|
239
|
-
text: Type.String({ description: "Text box content." }),
|
|
240
|
-
x: Type.Optional(Type.Number({ description: "Left edge in points (default centered)." })),
|
|
241
|
-
y: Type.Optional(Type.Number({ description: "Top edge in points (default centered)." })),
|
|
242
|
-
width: Type.Optional(Type.Number({ description: "Width in points (default 400)." })),
|
|
243
|
-
height: Type.Optional(Type.Number({ description: "Height in points (default 60)." })),
|
|
244
|
-
}),
|
|
245
|
-
},
|
|
246
|
-
];
|
|
33
|
+
/** The catalog version this extension's tool surface was built from. */
|
|
34
|
+
export const TOOL_CATALOG_VERSION = CATALOG_VERSION;
|
|
35
|
+
|
|
36
|
+
/** All descriptors built from the shared catalog (deterministic order). */
|
|
37
|
+
export const OFFICE_TOOL_DESCRIPTORS: readonly OfficeToolDescriptor[] =
|
|
38
|
+
OFFICE_CATALOG.map((entry) => ({
|
|
39
|
+
host: entry.host,
|
|
40
|
+
op: entry.op,
|
|
41
|
+
name: entry.name,
|
|
42
|
+
label: entry.label,
|
|
43
|
+
description: entry.description,
|
|
44
|
+
...(entry.promptSnippet === undefined
|
|
45
|
+
? null
|
|
46
|
+
: { promptSnippet: entry.promptSnippet }),
|
|
47
|
+
...(entry.promptGuidelines === undefined
|
|
48
|
+
? null
|
|
49
|
+
: { promptGuidelines: entry.promptGuidelines }),
|
|
50
|
+
parameters: entry.parameters,
|
|
51
|
+
}));
|
|
247
52
|
|
|
248
53
|
/** Index by op id for fast lookup. */
|
|
249
|
-
export const OFFICE_TOOL_BY_OP: ReadonlyMap<string, OfficeToolDescriptor> =
|
|
250
|
-
OFFICE_TOOL_DESCRIPTORS.map((d) => [`${d.host}.${d.op}`, d])
|
|
251
|
-
);
|
|
54
|
+
export const OFFICE_TOOL_BY_OP: ReadonlyMap<string, OfficeToolDescriptor> =
|
|
55
|
+
new Map(OFFICE_TOOL_DESCRIPTORS.map((d) => [`${d.host}.${d.op}`, d]));
|
|
252
56
|
|
|
253
57
|
/** All tool names registered by this extension. */
|
|
254
|
-
export const OFFICE_TOOL_NAMES: readonly string[] = OFFICE_TOOL_DESCRIPTORS.map(
|
|
58
|
+
export const OFFICE_TOOL_NAMES: readonly string[] = OFFICE_TOOL_DESCRIPTORS.map(
|
|
59
|
+
(d) => d.name,
|
|
60
|
+
);
|
|
255
61
|
|
|
256
|
-
/** The office host this tool name drives, or null when unknown. */
|
|
257
|
-
export
|
|
258
|
-
for (const d of OFFICE_TOOL_DESCRIPTORS) {
|
|
259
|
-
if (d.name === name) return d.host;
|
|
260
|
-
}
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
62
|
+
/** The office host this tool name drives, or null when unknown. (from catalog) */
|
|
63
|
+
export { HOST_APP_LABEL, hostForToolName };
|
package/src/protocol.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Re-export of the shared bridge protocol.
|
|
2
|
+
* Re-export of the shared bridge protocol + office op catalog.
|
|
3
3
|
*
|
|
4
4
|
* The single source of truth lives in `@dieulc/pi-office-protocol` so the Pi
|
|
5
|
-
* extension (server) and the add-in task pane (client) stay in lockstep.
|
|
5
|
+
* extension (server) and the add-in task pane (client) stay in lockstep. The
|
|
6
|
+
* catalog is re-exported from its subpath (the main entry keeps Node-loadable
|
|
7
|
+
* raw TS without a `.js`→`.ts` rewrite).
|
|
6
8
|
*/
|
|
7
9
|
export * from "@dieulc/pi-office-protocol";
|
|
10
|
+
export * from "@dieulc/pi-office-protocol/office-catalog";
|