@qping/plugin-bus 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.
@@ -0,0 +1,68 @@
1
+ import { Routes } from "./protocol.ts";
2
+
3
+ export interface MyToolsHostActionDefinition {
4
+ id: string;
5
+ /** Host-localized action display name. */
6
+ name: string;
7
+ /** Host-validated display form, for example `Ctrl+H`; absent means click-only. */
8
+ hotkey?: string | null;
9
+ }
10
+
11
+ export interface MyToolsHostInitializePayload {
12
+ protocolVersion: string;
13
+ pluginId: string;
14
+ version?: string;
15
+ itemId: string;
16
+ query: string;
17
+ keyword: string;
18
+ initialState: unknown;
19
+ locale: string;
20
+ fallbackLocale: string;
21
+ translationRevision: string;
22
+ messages: Record<string, string>;
23
+ actions: MyToolsHostActionDefinition[];
24
+ theme?: string;
25
+ themeTokens?: Record<string, string>;
26
+ }
27
+
28
+ export interface MyToolsLanguageChangedPayload {
29
+ locale: string;
30
+ fallbackLocale: string;
31
+ translationRevision: string;
32
+ messages: Record<string, string>;
33
+ }
34
+
35
+ export interface MyToolsThemeChangedPayload {
36
+ theme: string;
37
+ themeTokens: Record<string, string>;
38
+ }
39
+
40
+ export interface MyToolsHostSearchPayload {
41
+ query: string;
42
+ }
43
+
44
+ export interface MyToolsHostKeyPayload {
45
+ key: string;
46
+ }
47
+
48
+ /** Payload explicitly returned in an action outcome's `web.payload`. */
49
+ export interface MyToolsHostDetailActionPayload {
50
+ actionId?: string;
51
+ action?: string;
52
+ [key: string]: unknown;
53
+ }
54
+
55
+ export interface MyToolsInputActionCapturedPayload {
56
+ requestId: string;
57
+ cancelled?: boolean;
58
+ kind?: "hotkey" | "mouse";
59
+ hotKey?: string | null;
60
+ mouseButton?: string | null;
61
+ }
62
+
63
+ export interface MyToolsThemePayload {
64
+ theme?: string;
65
+ themeTokens?: Record<string, string>;
66
+ }
67
+
68
+ export const HostEvents = Routes.HostEvent;
@@ -1 +0,0 @@
1
- export * from "../src/bootstrap.ts";
@@ -1 +0,0 @@
1
- export * from "../src/protocol.ts";
package/dist/server.d.mts DELETED
@@ -1 +0,0 @@
1
- export * from "../src/server.ts";
package/dist/server.mjs DELETED
@@ -1,484 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res) => function __init() {
4
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
- };
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
-
11
- // src/framing.ts
12
- var framing_exports = {};
13
- __export(framing_exports, {
14
- FrameDecoder: () => FrameDecoder,
15
- MAX_FRAME_BYTES: () => MAX_FRAME_BYTES,
16
- PREFIX_BYTES: () => PREFIX_BYTES,
17
- encodeFrame: () => encodeFrame,
18
- encodeFrameString: () => encodeFrameString
19
- });
20
- function encodeFrame(payload) {
21
- const length = payload.length;
22
- const frame = Buffer.alloc(PREFIX_BYTES + length);
23
- frame[0] = length & 255;
24
- frame[1] = length >> 8 & 255;
25
- frame[2] = length >> 16 & 255;
26
- frame[3] = length >> 24 & 255;
27
- payload.copy(frame, PREFIX_BYTES);
28
- return frame;
29
- }
30
- function encodeFrameString(json) {
31
- return encodeFrame(Buffer.from(json, "utf8"));
32
- }
33
- var MAX_FRAME_BYTES, PREFIX_BYTES, FrameDecoder;
34
- var init_framing = __esm({
35
- "src/framing.ts"() {
36
- MAX_FRAME_BYTES = 4 * 1024 * 1024;
37
- PREFIX_BYTES = 4;
38
- FrameDecoder = class {
39
- prefixBuf = Buffer.alloc(PREFIX_BYTES);
40
- prefixFilled = 0;
41
- payload = null;
42
- payloadFilled = 0;
43
- payloadLength = 0;
44
- fatal = false;
45
- pending = Buffer.alloc(0);
46
- feed(chunk) {
47
- const empty = { hasFrame: false, payload: Buffer.alloc(0), isFatal: false };
48
- if (this.fatal) {
49
- return { hasFrame: false, payload: Buffer.alloc(0), isFatal: true };
50
- }
51
- let current;
52
- if (this.pending.length > 0) {
53
- current = Buffer.concat([this.pending, chunk]);
54
- this.pending = Buffer.alloc(0);
55
- } else {
56
- current = chunk;
57
- }
58
- let offset = 0;
59
- while (offset < current.length) {
60
- if (this.payload === null) {
61
- const need = PREFIX_BYTES - this.prefixFilled;
62
- const take = Math.min(need, current.length - offset);
63
- current.copy(this.prefixBuf, this.prefixFilled, offset, offset + take);
64
- this.prefixFilled += take;
65
- offset += take;
66
- if (this.prefixFilled < PREFIX_BYTES) {
67
- return empty;
68
- }
69
- this.payloadLength = this.prefixBuf[0] | this.prefixBuf[1] << 8 | this.prefixBuf[2] << 16 | this.prefixBuf[3] << 24;
70
- if (this.payloadLength < 0 || this.payloadLength > MAX_FRAME_BYTES) {
71
- this.fatal = true;
72
- return { hasFrame: false, payload: Buffer.alloc(0), isFatal: true };
73
- }
74
- if (this.payloadLength === 0) {
75
- this.reset();
76
- this.bufferLeftover(current, offset);
77
- return { hasFrame: true, payload: Buffer.alloc(0), isFatal: false };
78
- }
79
- this.payload = Buffer.alloc(this.payloadLength);
80
- this.payloadFilled = 0;
81
- }
82
- const payloadNeed = this.payloadLength - this.payloadFilled;
83
- const payloadTake = Math.min(payloadNeed, current.length - offset);
84
- current.copy(this.payload, this.payloadFilled, offset, offset + payloadTake);
85
- this.payloadFilled += payloadTake;
86
- offset += payloadTake;
87
- if (this.payloadFilled >= this.payloadLength) {
88
- const out = this.payload;
89
- this.reset();
90
- this.bufferLeftover(current, offset);
91
- return { hasFrame: true, payload: out, isFatal: false };
92
- }
93
- }
94
- return empty;
95
- }
96
- bufferLeftover(src, offset) {
97
- if (offset < src.length) {
98
- this.pending = src.subarray(offset);
99
- }
100
- }
101
- reset() {
102
- this.prefixFilled = 0;
103
- this.payload = null;
104
- this.payloadFilled = 0;
105
- this.payloadLength = 0;
106
- }
107
- };
108
- }
109
- });
110
-
111
- // src/bootstrap.ts
112
- import readline from "node:readline/promises";
113
-
114
- // src/transport.ts
115
- init_framing();
116
- import { connect as netConnect } from "node:net";
117
-
118
- // src/protocol.ts
119
- function canonicalStringify(value) {
120
- return JSON.stringify(stripNulls(value));
121
- }
122
- function stripNulls(value) {
123
- if (value === null || value === void 0) return void 0;
124
- if (Array.isArray(value)) return value.map(stripNulls);
125
- if (typeof value === "object") {
126
- const out = {};
127
- for (const [k, v] of Object.entries(value)) {
128
- const stripped = stripNulls(v);
129
- if (stripped !== void 0) out[k] = stripped;
130
- }
131
- return out;
132
- }
133
- return value;
134
- }
135
-
136
- // src/transport.ts
137
- var NodeTransport = class {
138
- socket = null;
139
- messageHandlers = /* @__PURE__ */ new Set();
140
- disconnectHandlers = /* @__PURE__ */ new Set();
141
- closed = false;
142
- onMessage(handler) {
143
- this.messageHandlers.add(handler);
144
- }
145
- onDisconnect(handler) {
146
- this.disconnectHandlers.add(handler);
147
- }
148
- get isConnected() {
149
- return this.socket !== null && !this.closed;
150
- }
151
- /** Connects to a Windows named pipe (\\.\pipe\<name>). */
152
- async connect(pipePath) {
153
- this.socket = netConnect(pipePath);
154
- await new Promise((resolve, reject) => {
155
- this.socket.once("connect", () => resolve());
156
- this.socket.once("error", (err) => reject(err));
157
- });
158
- const { FrameDecoder: FrameDecoder2 } = await Promise.resolve().then(() => (init_framing(), framing_exports));
159
- const decoder = new FrameDecoder2();
160
- this.socket.on("data", (chunk) => {
161
- let result = decoder.feed(chunk);
162
- if (result.isFatal) {
163
- this.handleDisconnect();
164
- return;
165
- }
166
- while (result.hasFrame) {
167
- try {
168
- const env = JSON.parse(result.payload.toString("utf8"));
169
- for (const h of this.messageHandlers) h(env);
170
- } catch {
171
- this.handleDisconnect();
172
- return;
173
- }
174
- result = decoder.feed(Buffer.alloc(0));
175
- if (result.isFatal) {
176
- this.handleDisconnect();
177
- return;
178
- }
179
- }
180
- });
181
- this.socket.on("close", () => this.handleDisconnect());
182
- this.socket.on("error", () => this.handleDisconnect());
183
- }
184
- /** Serializes an envelope to a length-prefixed frame and writes it. */
185
- send(env) {
186
- if (!this.socket || this.closed) {
187
- throw new Error("transport is not connected");
188
- }
189
- this.socket.write(encodeFrameString(canonicalStringify(env)));
190
- }
191
- async close() {
192
- this.closed = true;
193
- if (this.socket) {
194
- this.socket.end();
195
- await new Promise((resolve) => {
196
- if (this.socket.destroyed) return resolve();
197
- this.socket.once("close", () => resolve());
198
- });
199
- this.socket = null;
200
- }
201
- }
202
- handleDisconnect() {
203
- if (this.closed) return;
204
- this.closed = true;
205
- for (const h of this.disconnectHandlers) h();
206
- }
207
- };
208
-
209
- // src/router.ts
210
- import { randomBytes } from "node:crypto";
211
- var HandlerRouter = class {
212
- handlers = /* @__PURE__ */ new Map();
213
- pendingHostCalls = /* @__PURE__ */ new Map();
214
- pluginId = "p";
215
- entryId = "e";
216
- sessionId = "s";
217
- endpointId = "node-main";
218
- /** Injected transport send fn; tests can override `router.send` directly. */
219
- send;
220
- constructor(deps) {
221
- this.send = deps.send;
222
- }
223
- /** Sets the bound identity stamped on outbound messages (after handshake). */
224
- setIdentity(ids) {
225
- this.pluginId = ids.pluginId;
226
- this.entryId = ids.entryId;
227
- this.sessionId = ids.sessionId;
228
- this.endpointId = ids.endpointId;
229
- }
230
- handle(route, handler) {
231
- this.handlers.set(route, handler);
232
- }
233
- /** Dispatches an inbound request/response. Returns once handled. */
234
- async dispatch(env) {
235
- if (env.kind === "response") {
236
- this.handleHostResponse(env);
237
- return;
238
- }
239
- if (env.kind !== "request") return;
240
- if (env.route === "bus.ping") {
241
- this.send(this.responseFor(env, { ok: true }));
242
- return;
243
- }
244
- const handler = this.handlers.get(env.route);
245
- if (!handler) {
246
- this.send(this.errorResponseFor(env, "RouteNotFound", `route '${env.route}' has no handler`));
247
- return;
248
- }
249
- try {
250
- const result = await handler(env.payload);
251
- this.send(this.responseFor(env, result ?? {}));
252
- } catch (err) {
253
- const message = err instanceof Error ? err.message : String(err);
254
- this.send(this.errorResponseFor(env, "InternalError", message));
255
- }
256
- }
257
- /** Calls a host.call.* capability and resolves with the response payload. */
258
- callHost(route, payload, timeoutMs = 3e4) {
259
- return new Promise((resolve, reject) => {
260
- const id = randomBytesHex();
261
- const req = {
262
- version: "3.0",
263
- id,
264
- traceId: id,
265
- sessionId: this.sessionId,
266
- pluginId: this.pluginId,
267
- entryId: this.entryId,
268
- endpointId: this.endpointId,
269
- kind: "request",
270
- route,
271
- timeoutMs,
272
- payload
273
- };
274
- const pending = { resolve, reject, route };
275
- this.pendingHostCalls.set(id, pending);
276
- const timer = setTimeout(() => {
277
- if (this.pendingHostCalls.has(id)) {
278
- this.pendingHostCalls.delete(id);
279
- reject(new Error(`host call ${route} timed out after ${timeoutMs}ms`));
280
- }
281
- }, timeoutMs);
282
- const origResolve = pending.resolve;
283
- const origReject = pending.reject;
284
- pending.resolve = (v) => {
285
- clearTimeout(timer);
286
- origResolve(v);
287
- };
288
- pending.reject = (e) => {
289
- clearTimeout(timer);
290
- origReject(e);
291
- };
292
- this.send(req);
293
- });
294
- }
295
- handleHostResponse(env) {
296
- if (!env.correlationId) return;
297
- const pending = this.pendingHostCalls.get(env.correlationId);
298
- if (!pending) return;
299
- this.pendingHostCalls.delete(env.correlationId);
300
- if (env.error) {
301
- pending.reject(new Error(`${env.error.code}: ${env.error.message}`));
302
- } else {
303
- pending.resolve(env.payload);
304
- }
305
- }
306
- responseFor(req, payload) {
307
- return {
308
- version: "3.0",
309
- id: randomBytesHex(),
310
- correlationId: req.id,
311
- traceId: req.traceId,
312
- sessionId: req.sessionId,
313
- pluginId: req.pluginId,
314
- entryId: req.entryId,
315
- endpointId: this.endpointId,
316
- kind: "response",
317
- route: req.route,
318
- payload
319
- };
320
- }
321
- errorResponseFor(req, code, message) {
322
- return {
323
- ...this.responseFor(req, null),
324
- payload: void 0,
325
- error: { code, message, retryable: false }
326
- };
327
- }
328
- };
329
- function randomBytesHex() {
330
- return randomBytes(16).toString("hex");
331
- }
332
-
333
- // src/bootstrap.ts
334
- async function runPlugin(handlers) {
335
- const { pipePath, token } = await readBootstrapLine();
336
- const transport = new NodeTransport();
337
- await transport.connect(pipePath);
338
- const router = new HandlerRouter({ send: (env) => transport.send(env) });
339
- transport.onMessage((env) => {
340
- router.dispatch(env);
341
- });
342
- for (const [route, handler] of Object.entries(handlers)) {
343
- router.handle(route, handler);
344
- }
345
- return {
346
- transport,
347
- router,
348
- close: () => transport.close()
349
- };
350
- }
351
- async function readBootstrapLine() {
352
- const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
353
- try {
354
- const line = await new Promise((resolve, reject) => {
355
- rl.once("line", (l) => resolve(l));
356
- rl.once("close", () => reject(new Error("stdin closed before bootstrap line")));
357
- });
358
- const [pipePath, token] = line.split(" ");
359
- if (!pipePath || !token) {
360
- throw new Error(`malformed bootstrap line: ${JSON.stringify(line)}`);
361
- }
362
- return { pipePath, token };
363
- } finally {
364
- rl.close();
365
- }
366
- }
367
-
368
- // src/server.ts
369
- var NodeTool = class {
370
- #handlers = /* @__PURE__ */ new Map();
371
- #searchHandler = null;
372
- #actionHandler = null;
373
- #initializeHandler = null;
374
- #runtime = null;
375
- initialize(handler) {
376
- this.#initializeHandler = handler;
377
- return this;
378
- }
379
- search(handler) {
380
- this.#searchHandler = handler;
381
- return this;
382
- }
383
- action(handler) {
384
- this.#actionHandler = handler;
385
- return this;
386
- }
387
- handle(action, handler) {
388
- if (!action || typeof action !== "string") {
389
- throw new Error("tool.handle requires an action name.");
390
- }
391
- if (typeof handler !== "function") {
392
- throw new Error("tool.handle requires a handler.");
393
- }
394
- this.#handlers.set(action, handler);
395
- return this;
396
- }
397
- /** Publishes a plugin.event.<subjectId> event to all webviews in the session. */
398
- publish(subjectId, payload = {}) {
399
- if (!this.#runtime) throw new Error("tool not started");
400
- const route = subjectId.startsWith("plugin.event.") ? subjectId : `plugin.event.${subjectId}`;
401
- this.#runtime.transport.send({
402
- version: "3.0",
403
- id: crypto.randomUUID().replace(/-/g, "").slice(0, 32),
404
- traceId: crypto.randomUUID().replace(/-/g, "").slice(0, 32),
405
- sessionId: "",
406
- pluginId: "",
407
- entryId: "",
408
- endpointId: "node-main",
409
- kind: "event",
410
- route,
411
- payload
412
- });
413
- }
414
- /** Calls a host.call.<method> capability and awaits the response. */
415
- hostCall(method, params = {}) {
416
- if (!this.#runtime) return Promise.reject(new Error("tool not started"));
417
- return this.#runtime.router.callHost(`host.call.${method}`, params);
418
- }
419
- /** Connects to the host pipe and begins dispatching. Must be called last. */
420
- async start() {
421
- const routes = this.buildRoutes();
422
- this.#runtime = await runPlugin(routes);
423
- }
424
- /**
425
- * Builds the v3 route map from the fluent registrations. Exposed for unit testing the mapping
426
- * without connecting a pipe.
427
- */
428
- buildRoutes() {
429
- const routes = {};
430
- if (this.#initializeHandler) {
431
- routes["plugin.call.initialize"] = (p) => this.#initializeHandler(p);
432
- }
433
- if (this.#searchHandler) {
434
- routes["plugin.call.search"] = (p) => this.#searchHandler(p);
435
- }
436
- if (this.#actionHandler) {
437
- routes["plugin.call.invokeAction"] = (p) => {
438
- return this.#actionHandler({ ...p, itemId: p.itemId, query: p.query });
439
- };
440
- }
441
- routes["plugin.call.detailCall"] = async (p) => {
442
- const action = p?.action ?? "";
443
- const handler = this.#handlers.get(action);
444
- if (!handler) {
445
- throw new Error(`no handler registered for action '${action}'`);
446
- }
447
- const ctx = extractContext(p);
448
- const result = await handler(p?.payload ?? {}, ctx);
449
- return { result: result ?? {} };
450
- };
451
- routes["plugin.call.detailEvent"] = async (p) => {
452
- return { state: p?.payload ?? {} };
453
- };
454
- for (const [action, handler] of this.#handlers) {
455
- const route = `plugin.call.${action}`;
456
- if (!routes[route]) {
457
- routes[route] = async (p) => {
458
- const ctx = extractContext(p);
459
- return handler(p, ctx);
460
- };
461
- }
462
- }
463
- return routes;
464
- }
465
- async stop() {
466
- if (this.#runtime) await this.#runtime.close();
467
- }
468
- };
469
- function extractContext(p) {
470
- return {
471
- action: p?.action ?? "",
472
- itemId: p?.itemId ?? "",
473
- query: p?.query ?? "",
474
- locale: p?.locale ?? "en-US",
475
- fallbackLocale: p?.fallbackLocale ?? "en-US"
476
- };
477
- }
478
- function createTool() {
479
- return new NodeTool();
480
- }
481
- export {
482
- NodeTool,
483
- createTool
484
- };