m365connector 0.3.6 → 0.3.7
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 +29 -103
- package/package.json +4 -3
- package/src/browser-session.js +619 -0
- package/src/index.js +31 -14
- package/src/lib/content-reader.js +2746 -0
- package/src/lib/search-api.js +989 -0
- package/src/lib/vendor/jszip.min.js +13 -0
- package/src/token-providers.js +739 -0
- package/src/tools/search.js +17 -66
- package/src/ws-server.js +0 -203
package/src/tools/search.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
364
|
-
|
|
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
|
|
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.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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.
|
|
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.
|
|
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
|
-
}
|