m365connector 0.3.6 → 0.3.8

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.
@@ -1,16 +1,9 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
- const WHOAMI_TIMEOUT_MS = 60000;
4
- const FIND_PEOPLE_TIMEOUT_MS = 90000;
5
- const COUNT_PEOPLE_TIMEOUT_MS = 120000;
6
-
7
- const SEARCH_FILE_THRESHOLD = 8192;
8
- const OPEN_FILE_THRESHOLD = 4096;
9
-
10
3
  const SEARCH_TOOL = {
11
4
  name: "search",
12
5
  description:
13
- "Search across Microsoft 365 data including emails, files, Teams chats, calendar events, and external connectors. Large result sets are saved to a local temp file — use the returned file_path with your file-reading tool to access the full results.",
6
+ "Search across Microsoft 365 data including emails, files, Teams chats, calendar events, and external connectors.",
14
7
  inputSchema: {
15
8
  type: "object",
16
9
  properties: {
@@ -250,10 +243,9 @@ export function registerTools(registry, context) {
250
243
  const size = Number.isInteger(args.size) ? args.size : 10;
251
244
  const from = Number.isInteger(args.from) ? args.from : 0;
252
245
 
253
- const wsResult = await context.ws.sendRequest(
254
- "search",
255
- { conversation_id: conversationId, query, source, connector, size, from },
256
- { timeoutMs: 30000 }
246
+ await context.substrateTokenProvider.getTokenEntry();
247
+ const wsResult = await context.searchApi.search(
248
+ { conversation_id: conversationId, query, source, connector, size, from }
257
249
  );
258
250
 
259
251
  const resultObject = asObject(wsResult);
@@ -274,7 +266,7 @@ export function registerTools(registry, context) {
274
266
  const readHandle = makeReadHandle(context.readHandleCache, readContext);
275
267
 
276
268
  // Only expose the opaque handle to agents; never return internal read context.
277
- const entry = {
269
+ return {
278
270
  index: Number.isInteger(publicRow.index) ? publicRow.index : index,
279
271
  type: publicRow.type || "",
280
272
  title: publicRow.title || "",
@@ -303,18 +295,9 @@ export function registerTools(registry, context) {
303
295
  email: publicRow.email,
304
296
  _readHandle: readHandle
305
297
  };
306
-
307
- // Strip null/undefined fields to reduce payload size
308
- for (const key of Object.keys(entry)) {
309
- if (entry[key] == null) {
310
- delete entry[key];
311
- }
312
- }
313
-
314
- return entry;
315
298
  });
316
299
 
317
- const response = {
300
+ return {
318
301
  results: transformedResults,
319
302
  totalEstimatedMatches: Number(resultObject.totalEstimatedMatches || 0),
320
303
  moreAvailable: Boolean(resultObject.moreAvailable),
@@ -324,24 +307,6 @@ export function registerTools(registry, context) {
324
307
  : from + transformedResults.length,
325
308
  warnings: Array.isArray(resultObject.warnings) ? resultObject.warnings : []
326
309
  };
327
-
328
- const responseJson = JSON.stringify(response);
329
- const responseBytes = Buffer.byteLength(responseJson, "utf-8");
330
-
331
- if (responseBytes > SEARCH_FILE_THRESHOLD) {
332
- const filePath = context.tempFiles.write(responseJson);
333
- return {
334
- result_count: transformedResults.length,
335
- file_path: filePath,
336
- file_size_bytes: responseBytes,
337
- totalEstimatedMatches: response.totalEstimatedMatches,
338
- moreAvailable: response.moreAvailable,
339
- nextFrom: response.nextFrom,
340
- warnings: response.warnings
341
- };
342
- }
343
-
344
- return response;
345
310
  });
346
311
 
347
312
  registry.addTool(OPEN_TOOL, async (args) => {
@@ -360,10 +325,8 @@ export function registerTools(registry, context) {
360
325
  throw new Error("Invalid or expired read_handle. Run search again.");
361
326
  }
362
327
 
363
- const wsResult = await context.ws.sendRequest(
364
- "open",
365
- { conversation_id: conversationId, context: readContext, fullContent: Boolean(args.fullContent) },
366
- { timeoutMs: args.fullContent ? 90000 : 60000 }
328
+ const wsResult = await context.contentReader.read(
329
+ { conversation_id: conversationId, context: readContext, fullContent: Boolean(args.fullContent) }
367
330
  );
368
331
 
369
332
  const result = asObject(wsResult);
@@ -375,7 +338,8 @@ export function registerTools(registry, context) {
375
338
  : (result.content != null ? JSON.stringify(result.content, null, 2) : "");
376
339
  const contentBytes = Buffer.byteLength(content, "utf-8");
377
340
 
378
- const useFile = contentBytes > OPEN_FILE_THRESHOLD;
341
+ const FILE_THRESHOLD = 4096;
342
+ const useFile = contentBytes > FILE_THRESHOLD;
379
343
 
380
344
  const summary = {
381
345
  type: readContext.type || result.type || "",
@@ -436,16 +400,11 @@ export function registerTools(registry, context) {
436
400
  throw new Error("conversation_id is required");
437
401
  }
438
402
 
439
- const wsResult = await context.ws.sendRequest(
440
- "whoami",
441
- {
442
- conversation_id: conversationId,
443
- direct_reports_size: Number.isInteger(args.direct_reports_size)
444
- ? args.direct_reports_size
445
- : 30
446
- },
447
- { timeoutMs: WHOAMI_TIMEOUT_MS }
448
- );
403
+ const wsResult = await context.contentReader.whoAmI({
404
+ directReportsSize: Number.isInteger(args.direct_reports_size)
405
+ ? args.direct_reports_size
406
+ : 30
407
+ });
449
408
 
450
409
  return asObject(wsResult);
451
410
  });
@@ -469,11 +428,7 @@ export function registerTools(registry, context) {
469
428
  };
470
429
  const size = Number.isInteger(args.size) ? args.size : 20;
471
430
 
472
- const wsResult = await context.ws.sendRequest(
473
- "find_people",
474
- { conversation_id: conversationId, ...requestParams, size },
475
- { timeoutMs: FIND_PEOPLE_TIMEOUT_MS }
476
- );
431
+ const wsResult = await context.contentReader.findPeople({ ...requestParams, size });
477
432
 
478
433
  return asObject(wsResult);
479
434
  });
@@ -494,11 +449,7 @@ export function registerTools(registry, context) {
494
449
  title: typeof args.title === "string" ? args.title.trim() : ""
495
450
  };
496
451
 
497
- const wsResult = await context.ws.sendRequest(
498
- "count_people",
499
- { conversation_id: conversationId, ...requestParams },
500
- { timeoutMs: COUNT_PEOPLE_TIMEOUT_MS }
501
- );
452
+ const wsResult = await context.contentReader.countPeople(requestParams);
502
453
 
503
454
  return asObject(wsResult);
504
455
  });
package/src/ws-server.js DELETED
@@ -1,203 +0,0 @@
1
- import crypto from "node:crypto";
2
- import { WebSocket, WebSocketServer } from "ws";
3
-
4
- export class WsServer {
5
- constructor(options = {}) {
6
- const stderrLogger = {
7
- info: (...args) => console.error(...args),
8
- warn: (...args) => console.warn(...args),
9
- error: (...args) => console.error(...args)
10
- };
11
-
12
- this.host = options.host || "127.0.0.1";
13
- this.port = Number(options.port || 52365);
14
- this.logger = options.logger || stderrLogger;
15
-
16
- this.wss = null;
17
- this.client = null;
18
- this.pending = new Map();
19
- }
20
-
21
- async start() {
22
- if (this.wss) {
23
- return;
24
- }
25
-
26
- this.wss = new WebSocketServer({
27
- host: this.host,
28
- port: this.port
29
- });
30
-
31
- this.wss.on("connection", (socket, req) => {
32
- this.handleConnection(socket, req).catch((err) => {
33
- this.logger.error("[m365connector/ws] connection handling failed:", err);
34
- try {
35
- socket.close(1011, "internal error");
36
- } catch (_err) {
37
- // ignored
38
- }
39
- });
40
- });
41
-
42
- await new Promise((resolve, reject) => {
43
- this.wss.once("listening", resolve);
44
- this.wss.once("error", reject);
45
- });
46
-
47
- this.logger.info(`[m365connector/ws] listening on ws://${this.host}:${this.port}`);
48
- }
49
-
50
- async stop() {
51
- for (const pending of this.pending.values()) {
52
- clearTimeout(pending.timeout);
53
- pending.reject(new Error("WebSocket server stopped"));
54
- }
55
- this.pending.clear();
56
-
57
- if (this.client) {
58
- try {
59
- this.client.close(1001, "server stopping");
60
- } catch (_err) {
61
- // ignored
62
- }
63
- this.client = null;
64
- }
65
-
66
- if (!this.wss) {
67
- return;
68
- }
69
-
70
- const wss = this.wss;
71
- this.wss = null;
72
- await new Promise((resolve) => wss.close(resolve));
73
- }
74
-
75
- isReady() {
76
- return Boolean(this.client && this.client.readyState === WebSocket.OPEN);
77
- }
78
-
79
- getStatus() {
80
- return {
81
- host: this.host,
82
- port: this.port,
83
- connected: this.isReady()
84
- };
85
- }
86
-
87
- async sendRequest(type, params, options = {}) {
88
- const timeoutMs = Number(options.timeoutMs || 30000);
89
-
90
- if (!this.isReady()) {
91
- throw new Error("M365 Connector extension is not connected");
92
- }
93
-
94
- const id = crypto.randomUUID();
95
- const payload = { id, type, params: params || {} };
96
-
97
- return new Promise((resolve, reject) => {
98
- const timeout = setTimeout(() => {
99
- this.logger.warn(`[m365connector/ws] request timed out: type=${type} id=${id}`);
100
- this.pending.delete(id);
101
- reject(new Error(`WebSocket request timed out after ${timeoutMs}ms`));
102
- }, timeoutMs);
103
-
104
- this.pending.set(id, { resolve, reject, timeout });
105
-
106
- try {
107
- this.client.send(JSON.stringify(payload));
108
- } catch (err) {
109
- clearTimeout(timeout);
110
- this.pending.delete(id);
111
- reject(err);
112
- }
113
- });
114
- }
115
-
116
- async handleConnection(socket, req) {
117
- const origin = req.headers.origin;
118
-
119
- if (!origin) {
120
- this.logger.warn("[m365connector/ws] rejected connection: missing origin header");
121
- socket.close(1008, "origin required");
122
- return;
123
- }
124
-
125
- if (!origin.startsWith("chrome-extension://")) {
126
- this.logger.warn(`[m365connector/ws] rejected connection from origin: ${origin}`);
127
- socket.close(1008, "origin not allowed");
128
- return;
129
- }
130
-
131
- if (this.client && this.client.readyState === WebSocket.OPEN) {
132
- // MV3 service workers can die silently without sending a close frame.
133
- // When the extension reconnects, replace the stale connection.
134
- this.logger.info("[m365connector/ws] replacing existing connection with new one");
135
- try { this.client.terminate(); } catch (_e) { /* ignored */ }
136
- this.handleClose(this.client);
137
- }
138
-
139
- // Clean up if client ref exists but is no longer OPEN
140
- if (this.client && this.client.readyState !== WebSocket.OPEN) {
141
- this.client = null;
142
- }
143
-
144
- this.client = socket;
145
-
146
- socket.on("message", async (rawData) => {
147
- await this.handleMessage(socket, rawData);
148
- });
149
-
150
- socket.on("close", (code, reason) => {
151
- this.logger.info(`[m365connector/ws] socket close: code=${code}, reason=${String(reason)}`);
152
- this.handleClose(socket);
153
- });
154
-
155
- socket.on("error", (err) => {
156
- this.logger.warn("[m365connector/ws] client socket error:", err.message);
157
- });
158
- this.logger.info("[m365connector/ws] extension connected");
159
- }
160
-
161
- async handleMessage(socket, rawData) {
162
- let message;
163
- try {
164
- message = JSON.parse(String(rawData));
165
- } catch (_err) {
166
- socket.close(4002, "invalid json");
167
- return;
168
- }
169
-
170
- if (!message?.id) {
171
- return;
172
- }
173
-
174
- const pending = this.pending.get(message.id);
175
- if (!pending) {
176
- return;
177
- }
178
-
179
- clearTimeout(pending.timeout);
180
- this.pending.delete(message.id);
181
-
182
- if (!message.success) {
183
- pending.reject(new Error(message.error || "Unknown extension error"));
184
- return;
185
- }
186
-
187
- pending.resolve(message.data);
188
- }
189
-
190
- handleClose(socket) {
191
- if (this.client === socket) {
192
- this.client = null;
193
- }
194
-
195
- for (const [id, pending] of this.pending.entries()) {
196
- clearTimeout(pending.timeout);
197
- pending.reject(new Error("Extension disconnected"));
198
- this.pending.delete(id);
199
- }
200
-
201
- this.logger.info("[m365connector/ws] extension disconnected");
202
- }
203
- }