@prbe.ai/electron-sdk 0.1.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.
Files changed (54) hide show
  1. package/dist/agent.d.ts +105 -0
  2. package/dist/agent.d.ts.map +1 -0
  3. package/dist/agent.js +861 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/assets/index.d.ts +6 -0
  6. package/dist/assets/index.d.ts.map +1 -0
  7. package/dist/assets/index.js +13 -0
  8. package/dist/assets/index.js.map +1 -0
  9. package/dist/index.d.ts +16 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +62 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/interactions.d.ts +56 -0
  14. package/dist/interactions.d.ts.map +1 -0
  15. package/dist/interactions.js +27 -0
  16. package/dist/interactions.js.map +1 -0
  17. package/dist/models.d.ts +212 -0
  18. package/dist/models.d.ts.map +1 -0
  19. package/dist/models.js +120 -0
  20. package/dist/models.js.map +1 -0
  21. package/dist/serialization.d.ts +49 -0
  22. package/dist/serialization.d.ts.map +1 -0
  23. package/dist/serialization.js +69 -0
  24. package/dist/serialization.js.map +1 -0
  25. package/dist/state.d.ts +67 -0
  26. package/dist/state.d.ts.map +1 -0
  27. package/dist/state.js +270 -0
  28. package/dist/state.js.map +1 -0
  29. package/dist/tools/bash.d.ts +30 -0
  30. package/dist/tools/bash.d.ts.map +1 -0
  31. package/dist/tools/bash.js +247 -0
  32. package/dist/tools/bash.js.map +1 -0
  33. package/dist/tools/filesystem.d.ts +63 -0
  34. package/dist/tools/filesystem.d.ts.map +1 -0
  35. package/dist/tools/filesystem.js +573 -0
  36. package/dist/tools/filesystem.js.map +1 -0
  37. package/dist/tools/index.d.ts +46 -0
  38. package/dist/tools/index.d.ts.map +1 -0
  39. package/dist/tools/index.js +171 -0
  40. package/dist/tools/index.js.map +1 -0
  41. package/dist/tools/interactive.d.ts +15 -0
  42. package/dist/tools/interactive.d.ts.map +1 -0
  43. package/dist/tools/interactive.js +57 -0
  44. package/dist/tools/interactive.js.map +1 -0
  45. package/dist/tools/logs.d.ts +72 -0
  46. package/dist/tools/logs.d.ts.map +1 -0
  47. package/dist/tools/logs.js +366 -0
  48. package/dist/tools/logs.js.map +1 -0
  49. package/dist/types.d.ts +14 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +32 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +35 -0
  54. package/src/assets/probe-mark.svg +5 -0
package/dist/agent.js ADDED
@@ -0,0 +1,861 @@
1
+ "use strict";
2
+ /**
3
+ * agent.ts — PRBEAgent class
4
+ *
5
+ * Main entry point for the PRBE debug agent SDK.
6
+ * Handles WebSocket connection to middleware, investigation lifecycle,
7
+ * background polling for context requests, file uploads, and persistence.
8
+ *
9
+ * Mirrors PRBEAgent.swift behavior.
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.PRBEAgent = void 0;
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const os = __importStar(require("os"));
49
+ const crypto_1 = require("crypto");
50
+ const models_1 = require("./models");
51
+ const models_2 = require("./models");
52
+ const state_1 = require("./state");
53
+ const index_1 = require("./tools/index");
54
+ const filesystem_1 = require("./tools/filesystem");
55
+ const logs_1 = require("./tools/logs");
56
+ const interactive_1 = require("./tools/interactive");
57
+ const bash_1 = require("./tools/bash");
58
+ const interactions_1 = require("./interactions");
59
+ function getPersistencePath() {
60
+ // Use platform-appropriate app data directory
61
+ const appData = process.env["APPDATA"] ||
62
+ (process.platform === "darwin"
63
+ ? path.join(os.homedir(), "Library", "Application Support")
64
+ : path.join(os.homedir(), ".local", "share"));
65
+ const dir = path.join(appData, "prbe-agent");
66
+ if (!fs.existsSync(dir)) {
67
+ fs.mkdirSync(dir, { recursive: true });
68
+ }
69
+ return path.join(dir, "agent-state.json");
70
+ }
71
+ function loadPersistedData() {
72
+ try {
73
+ const filePath = getPersistencePath();
74
+ if (fs.existsSync(filePath)) {
75
+ const raw = fs.readFileSync(filePath, "utf-8");
76
+ return JSON.parse(raw);
77
+ }
78
+ }
79
+ catch {
80
+ // Ignore read errors, return defaults
81
+ }
82
+ return {};
83
+ }
84
+ function savePersistedData(data) {
85
+ try {
86
+ const filePath = getPersistencePath();
87
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
88
+ }
89
+ catch {
90
+ console.error("[PRBEAgent] Failed to save persisted data");
91
+ }
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // PRBEAgent
95
+ // ---------------------------------------------------------------------------
96
+ class PRBEAgent {
97
+ state;
98
+ logCapture;
99
+ config;
100
+ interactionHandler;
101
+ registry = new index_1.PRBEToolRegistry();
102
+ grantedPaths = new Set();
103
+ userCancelled = false;
104
+ activeWS = null;
105
+ pollingTimer = null;
106
+ persistedData;
107
+ fetchAbortController = null;
108
+ currentInvestigationSource = interactions_1.InvestigationSource.USER;
109
+ currentCRId = null;
110
+ /** Files flagged during the current tool call — uploaded immediately after the tool returns. */
111
+ pendingFlaggedFiles = [];
112
+ // ---------- Persistence ----------
113
+ get agentID() {
114
+ if (this.persistedData.agentId) {
115
+ return this.persistedData.agentId;
116
+ }
117
+ const newID = (0, crypto_1.randomUUID)();
118
+ this.persistedData.agentId = newID;
119
+ savePersistedData(this.persistedData);
120
+ return newID;
121
+ }
122
+ get trackedTicketIDs() {
123
+ return this.persistedData.ticketIds ?? [];
124
+ }
125
+ set trackedTicketIDs(ids) {
126
+ this.persistedData.ticketIds = ids;
127
+ savePersistedData(this.persistedData);
128
+ this.state.updateTrackedTicketIDs(ids);
129
+ this.syncPolling(ids.length > 0);
130
+ }
131
+ get respondedCRIDs() {
132
+ return new Set(this.persistedData.respondedCRIds ?? []);
133
+ }
134
+ set respondedCRIDs(ids) {
135
+ this.persistedData.respondedCRIds = Array.from(ids);
136
+ savePersistedData(this.persistedData);
137
+ }
138
+ syncPolling(hasTickets) {
139
+ if (this.config.backgroundPolling && hasTickets) {
140
+ if (this.pollingTimer === null) {
141
+ this.startPolling();
142
+ }
143
+ }
144
+ else if (!hasTickets) {
145
+ this.stopPolling();
146
+ }
147
+ }
148
+ addTrackedTicket(id) {
149
+ const ids = this.trackedTicketIDs;
150
+ if (!ids.includes(id)) {
151
+ this.trackedTicketIDs = [...ids, id];
152
+ }
153
+ }
154
+ // ---------- Constructor ----------
155
+ constructor(config) {
156
+ this.config = {
157
+ apiKey: config.apiKey,
158
+ allowedRoots: config.allowedRoots,
159
+ pollingInterval: config.pollingInterval ?? 600_000,
160
+ maxLogEntries: config.maxLogEntries ?? 10_000,
161
+ captureConsole: config.captureConsole ?? true,
162
+ backgroundPolling: config.backgroundPolling ?? true,
163
+ };
164
+ this.interactionHandler = config.interactionHandler;
165
+ this.state = new state_1.PRBEAgentState();
166
+ this.logCapture = new logs_1.PRBELogCapture(this.config.maxLogEntries);
167
+ this.persistedData = loadPersistedData();
168
+ const roots = this.config.allowedRoots;
169
+ const requester = this.interactionHandler ? this : undefined;
170
+ const grantedPaths = this.grantedPaths;
171
+ // Register built-in filesystem tools
172
+ this.registry.register(new filesystem_1.ListDirectoryTool(roots, requester, grantedPaths));
173
+ this.registry.register(new filesystem_1.ReadFileTool(roots, requester, grantedPaths));
174
+ this.registry.register(new filesystem_1.SearchContentTool(roots, requester, grantedPaths));
175
+ this.registry.register(new filesystem_1.FindFilesTool(roots, requester, grantedPaths));
176
+ this.registry.register(new filesystem_1.FlagFileTool(roots, (file) => {
177
+ this.pendingFlaggedFiles.push(file);
178
+ }, requester, grantedPaths));
179
+ // Register built-in log tools
180
+ this.registry.register(new logs_1.ReadAppLogsTool(this.logCapture));
181
+ this.registry.register(new logs_1.SearchAppLogsTool(this.logCapture));
182
+ this.registry.register(new logs_1.ClearAppLogsTool(this.logCapture));
183
+ this.registry.register(new logs_1.FlagAppLogsTool(this.logCapture, (file) => {
184
+ this.pendingFlaggedFiles.push(file);
185
+ }));
186
+ // Register interactive tools (only when handler is available)
187
+ if (requester) {
188
+ this.registry.register(new interactive_1.AskUserTool(requester));
189
+ this.registry.register(new bash_1.BashExecuteTool(requester, roots, grantedPaths));
190
+ }
191
+ // Start console capture if configured
192
+ if (this.config.captureConsole) {
193
+ this.logCapture.startCapturing();
194
+ }
195
+ // Hook electron-log if provided
196
+ if (config.electronLog) {
197
+ this.hookElectronLog(config.electronLog);
198
+ }
199
+ // Listen for renderer log forwarding via IPC if provided
200
+ if (config.ipcMain) {
201
+ this.hookRendererLogs(config.ipcMain, config.rendererLogChannel ?? "prbe-renderer-log");
202
+ }
203
+ // Bootstrap: trigger syncPolling + state update for existing tickets
204
+ const existingTickets = this.trackedTicketIDs;
205
+ if (existingTickets.length > 0) {
206
+ this.trackedTicketIDs = existingTickets;
207
+ }
208
+ }
209
+ // ---------- Log integration ----------
210
+ hookElectronLog(electronLog) {
211
+ try {
212
+ electronLog.hooks.push((message, _transportFn, transportName) => {
213
+ if (transportName !== "file")
214
+ return message;
215
+ try {
216
+ const text = message.data
217
+ .map((d) => (typeof d === "string" ? d : JSON.stringify(d)))
218
+ .join(" ");
219
+ const level = PRBEAgent.mapElectronLogLevel(message.level);
220
+ this.logCapture.log(text, level, `electron-log.${message.level}`);
221
+ }
222
+ catch {
223
+ // ignore serialization errors
224
+ }
225
+ return message;
226
+ });
227
+ }
228
+ catch {
229
+ // electron-log not available or incompatible
230
+ }
231
+ }
232
+ hookRendererLogs(ipcMain, channel) {
233
+ ipcMain.on(channel, (_event, entry) => {
234
+ this.logCapture.log(entry.message, entry.level, `renderer.${entry.category}`);
235
+ });
236
+ }
237
+ static mapElectronLogLevel(level) {
238
+ switch (level) {
239
+ case "error": return "ERROR";
240
+ case "warn": return "WARNING";
241
+ case "debug":
242
+ case "verbose":
243
+ case "silly": return "DEBUG";
244
+ default: return "INFO";
245
+ }
246
+ }
247
+ // ---------- PRBEInteractionRequester implementation ----------
248
+ get investigationSource() {
249
+ return this.currentInvestigationSource;
250
+ }
251
+ async requestUserInteraction(payload) {
252
+ if (!this.interactionHandler) {
253
+ throw new models_2.PRBEAgentError(models_2.PRBEAgentErrorType.SERVER_ERROR, "No interaction handler configured");
254
+ }
255
+ // Update state to show pending interaction
256
+ if (this.currentInvestigationSource === interactions_1.InvestigationSource.CONTEXT_REQUEST && this.currentCRId) {
257
+ this.state.setCRPendingInteraction(this.currentCRId, payload);
258
+ }
259
+ else {
260
+ this.state.setPendingInteraction(payload);
261
+ }
262
+ try {
263
+ const response = await this.interactionHandler.handleInteraction(payload);
264
+ return response;
265
+ }
266
+ finally {
267
+ if (this.currentInvestigationSource === interactions_1.InvestigationSource.CONTEXT_REQUEST && this.currentCRId) {
268
+ this.state.clearCRPendingInteraction(this.currentCRId);
269
+ }
270
+ else {
271
+ this.state.clearPendingInteraction();
272
+ }
273
+ }
274
+ }
275
+ /**
276
+ * Add an additional root directory to the runtime allowed roots.
277
+ */
278
+ addAllowedRoot(rootPath) {
279
+ if (!this.config.allowedRoots.includes(rootPath)) {
280
+ this.config.allowedRoots.push(rootPath);
281
+ }
282
+ }
283
+ // ---------- Public API ----------
284
+ /**
285
+ * Register a custom tool that the middleware can invoke during investigations.
286
+ */
287
+ registerTool(name, description, parameters, handler) {
288
+ this.registry.register(new index_1.PRBEClosureTool(name, description, parameters, handler));
289
+ }
290
+ /**
291
+ * User-initiated investigation. Updates `state` events/report directly.
292
+ */
293
+ async investigate(query, contextRequestID) {
294
+ this.userCancelled = false;
295
+ this.currentInvestigationSource = interactions_1.InvestigationSource.USER;
296
+ this.currentCRId = null;
297
+ this.state.beginInvestigation(query);
298
+ const emitter = (status) => {
299
+ // Update state based on status
300
+ switch (status.type) {
301
+ case models_2.PRBEAgentStatusType.STARTED:
302
+ this.state.appendEvent("Starting investigation...");
303
+ break;
304
+ case models_2.PRBEAgentStatusType.THINKING:
305
+ break;
306
+ case models_2.PRBEAgentStatusType.TOOL_CALL:
307
+ this.state.appendEvent(status.label);
308
+ break;
309
+ case models_2.PRBEAgentStatusType.OBSERVATION:
310
+ this.state.attachObservation(status.text);
311
+ break;
312
+ case models_2.PRBEAgentStatusType.THOUGHT:
313
+ this.state.appendEvent("Thinking", status.text);
314
+ break;
315
+ case models_2.PRBEAgentStatusType.COMPLETED:
316
+ this.state.completeInvestigation(status.report, status.userSummary);
317
+ break;
318
+ case models_2.PRBEAgentStatusType.ERROR:
319
+ this.state.failInvestigation(status.message);
320
+ break;
321
+ }
322
+ };
323
+ const result = await this.connectToProxy(query, contextRequestID, emitter, () => this.userCancelled);
324
+ this.currentInvestigationSource = interactions_1.InvestigationSource.USER;
325
+ this.currentCRId = null;
326
+ if (result?.ticketId) {
327
+ this.addTrackedTicket(result.ticketId);
328
+ }
329
+ else if (!result) {
330
+ // No result — either cancelled or errored
331
+ if (this.state.isInvestigating) {
332
+ const message = this.userCancelled
333
+ ? "Investigation cancelled"
334
+ : "Investigation ended unexpectedly";
335
+ this.state.failInvestigation(message);
336
+ }
337
+ }
338
+ }
339
+ /**
340
+ * Cancel user-initiated investigation only (does not cancel background CRs).
341
+ */
342
+ cancelInvestigation() {
343
+ this.userCancelled = true;
344
+ if (this.activeWS) {
345
+ this.activeWS.close(1000, "User cancelled");
346
+ this.activeWS = null;
347
+ }
348
+ }
349
+ /**
350
+ * Cancel everything — user investigation and polling.
351
+ */
352
+ cancel() {
353
+ this.userCancelled = true;
354
+ if (this.activeWS) {
355
+ this.activeWS.close(1000, "User cancelled");
356
+ this.activeWS = null;
357
+ }
358
+ this.abortInFlightRequests();
359
+ this.stopPolling();
360
+ }
361
+ /**
362
+ * Poll the backend for context requests on tracked tickets.
363
+ */
364
+ async poll() {
365
+ const ticketIDs = this.trackedTicketIDs;
366
+ if (ticketIDs.length === 0)
367
+ return null;
368
+ const request = {
369
+ agent_id: this.agentID,
370
+ ticket_ids: ticketIDs,
371
+ };
372
+ try {
373
+ const response = await this.post("/api/agent/poll", request);
374
+ // Discard ticket IDs the backend didn't return (deleted/unknown)
375
+ const returnedIDs = new Set(response.tickets.map((t) => t.ticket_id));
376
+ const orphaned = ticketIDs.filter((id) => !returnedIDs.has(id));
377
+ if (orphaned.length > 0) {
378
+ this.trackedTicketIDs = this.trackedTicketIDs.filter((id) => !orphaned.includes(id));
379
+ }
380
+ // Collect all CR IDs the backend knows about for pruning
381
+ const knownCRIDs = new Set();
382
+ for (const ticket of response.tickets) {
383
+ if (ticket.status === "resolved") {
384
+ this.trackedTicketIDs = this.trackedTicketIDs.filter((id) => id !== ticket.ticket_id);
385
+ }
386
+ for (const cr of ticket.context_requests) {
387
+ knownCRIDs.add(cr.id);
388
+ // Skip inactive or already-responded CRs
389
+ if (!cr.is_active || this.respondedCRIDs.has(cr.id))
390
+ continue;
391
+ await this.investigateForCR(cr, ticket.ticket_id);
392
+ const ids = this.respondedCRIDs;
393
+ ids.add(cr.id);
394
+ this.respondedCRIDs = ids;
395
+ }
396
+ }
397
+ // Prune responded CR IDs for CRs the backend no longer returns
398
+ const currentRespondedIDs = this.respondedCRIDs;
399
+ const stale = new Set([...currentRespondedIDs].filter((id) => !knownCRIDs.has(id)));
400
+ if (stale.size > 0) {
401
+ const pruned = new Set([...currentRespondedIDs].filter((id) => !stale.has(id)));
402
+ this.respondedCRIDs = pruned;
403
+ }
404
+ return response;
405
+ }
406
+ catch {
407
+ return null;
408
+ }
409
+ }
410
+ /**
411
+ * Fetch ticket display info for all tracked tickets.
412
+ */
413
+ async fetchTicketInfo() {
414
+ const ticketIDs = this.trackedTicketIDs;
415
+ if (ticketIDs.length === 0)
416
+ return [];
417
+ const request = { ticket_ids: ticketIDs };
418
+ try {
419
+ const response = await this.post("/api/agent/tickets", request);
420
+ this.state.updateTicketInfo(response.tickets);
421
+ return response.tickets;
422
+ }
423
+ catch {
424
+ return [];
425
+ }
426
+ }
427
+ stopPolling() {
428
+ if (this.pollingTimer !== null) {
429
+ clearInterval(this.pollingTimer);
430
+ this.pollingTimer = null;
431
+ }
432
+ }
433
+ resumePolling() {
434
+ if (this.trackedTicketIDs.length === 0)
435
+ return;
436
+ this.startPolling();
437
+ }
438
+ /**
439
+ * Destroy the agent — stops polling, stops console capture, closes WS.
440
+ */
441
+ destroy() {
442
+ this.cancel();
443
+ this.logCapture.stopCapturing();
444
+ }
445
+ /**
446
+ * Reset all persisted data (agent ID, tracked tickets, responded CRs).
447
+ * Also clears in-memory state.
448
+ */
449
+ resetPersistedData() {
450
+ this.stopPolling();
451
+ this.persistedData = {};
452
+ savePersistedData(this.persistedData);
453
+ this.state.resetInvestigation();
454
+ this.state.completedInvestigations = [];
455
+ this.state.activeCRs.clear();
456
+ this.state.completedCRs = [];
457
+ this.state.trackedTicketIDs = [];
458
+ this.state.ticketInfo = [];
459
+ this.state.emit(state_1.PRBEStateEvent.STATUS);
460
+ }
461
+ /**
462
+ * Delete the persisted data file. Can be called without an agent instance.
463
+ */
464
+ static clearPersistedData() {
465
+ try {
466
+ const filePath = getPersistencePath();
467
+ if (fs.existsSync(filePath)) {
468
+ fs.unlinkSync(filePath);
469
+ }
470
+ }
471
+ catch {
472
+ // Ignore
473
+ }
474
+ }
475
+ // ---------- CR Investigation ----------
476
+ async investigateForCR(cr, ticketId) {
477
+ const crID = cr.id;
478
+ this.currentInvestigationSource = interactions_1.InvestigationSource.CONTEXT_REQUEST;
479
+ this.currentCRId = crID;
480
+ this.state.beginCR(crID, cr.query, cr.slug ?? undefined);
481
+ const emitter = (status) => {
482
+ switch (status.type) {
483
+ case models_2.PRBEAgentStatusType.STARTED:
484
+ this.state.appendCREvent(crID, "Starting investigation...");
485
+ break;
486
+ case models_2.PRBEAgentStatusType.THINKING:
487
+ break;
488
+ case models_2.PRBEAgentStatusType.TOOL_CALL:
489
+ this.state.appendCREvent(crID, status.label);
490
+ break;
491
+ case models_2.PRBEAgentStatusType.OBSERVATION:
492
+ this.state.attachCRObservation(crID, status.text);
493
+ break;
494
+ case models_2.PRBEAgentStatusType.THOUGHT:
495
+ this.state.appendCREvent(crID, "Thinking", status.text);
496
+ break;
497
+ case models_2.PRBEAgentStatusType.COMPLETED:
498
+ this.state.completeCR(crID, status.report, status.userSummary);
499
+ break;
500
+ case models_2.PRBEAgentStatusType.ERROR:
501
+ this.state.failCR(crID, status.message);
502
+ break;
503
+ }
504
+ };
505
+ const result = await this.connectToProxy(cr.query, crID, emitter, () => false, // CRs are not user-cancellable
506
+ ticketId);
507
+ this.currentInvestigationSource = interactions_1.InvestigationSource.USER;
508
+ this.currentCRId = null;
509
+ if (result?.ticketId) {
510
+ this.addTrackedTicket(result.ticketId);
511
+ }
512
+ }
513
+ // ---------- WebSocket Investigation ----------
514
+ connectToProxy(query, contextRequestID, emit, isCancelled, ticketId) {
515
+ return new Promise((resolve) => {
516
+ const wsUrl = `${models_1.MIDDLEWARE_URL}/api/agent/client/ws`;
517
+ let ws;
518
+ try {
519
+ ws = new WebSocket(wsUrl, {
520
+ headers: {
521
+ "X-API-Key": this.config.apiKey,
522
+ },
523
+ });
524
+ }
525
+ catch (err) {
526
+ emit({
527
+ type: models_2.PRBEAgentStatusType.ERROR,
528
+ message: `Failed to create WebSocket: ${err}`,
529
+ });
530
+ resolve(null);
531
+ return;
532
+ }
533
+ this.activeWS = ws;
534
+ let uploadBaseUrl;
535
+ let resolved = false;
536
+ const finish = (result) => {
537
+ if (resolved)
538
+ return;
539
+ resolved = true;
540
+ this.activeWS = null;
541
+ resolve(result);
542
+ };
543
+ ws.onopen = () => {
544
+ // Build tool declarations for the start message
545
+ const toolDeclarations = this.registry
546
+ .allDeclarations()
547
+ .map((decl) => ({
548
+ name: decl.name,
549
+ description: decl.description,
550
+ parameters: decl.parameters.map((param) => ({
551
+ name: param.name,
552
+ type: param.type,
553
+ description: param.description,
554
+ required: param.required,
555
+ })),
556
+ }));
557
+ const startMetadata = {
558
+ agent_id: this.agentID,
559
+ custom_tools: toolDeclarations,
560
+ };
561
+ if (contextRequestID) {
562
+ startMetadata["context_request_id"] = contextRequestID;
563
+ }
564
+ if (ticketId) {
565
+ startMetadata["ticket_id"] = ticketId;
566
+ }
567
+ const startMsg = {
568
+ type: models_1.WSMessageType.START,
569
+ content: query,
570
+ metadata: startMetadata,
571
+ };
572
+ try {
573
+ ws.send(JSON.stringify(startMsg));
574
+ }
575
+ catch (err) {
576
+ emit({
577
+ type: models_2.PRBEAgentStatusType.ERROR,
578
+ message: `Failed to send start message: ${err}`,
579
+ });
580
+ finish(null);
581
+ return;
582
+ }
583
+ emit({ type: models_2.PRBEAgentStatusType.STARTED });
584
+ this.pendingFlaggedFiles = [];
585
+ };
586
+ ws.onmessage = async (event) => {
587
+ if (isCancelled()) {
588
+ this.sendCancel(ws);
589
+ ws.close(1000, "Cancelled");
590
+ finish(null);
591
+ return;
592
+ }
593
+ let raw;
594
+ if (typeof event.data === "string") {
595
+ raw = event.data;
596
+ }
597
+ else if (event.data instanceof Buffer) {
598
+ raw = event.data.toString("utf-8");
599
+ }
600
+ else {
601
+ return;
602
+ }
603
+ let msg;
604
+ try {
605
+ msg = JSON.parse(raw);
606
+ }
607
+ catch {
608
+ return;
609
+ }
610
+ switch (msg.type) {
611
+ case models_1.WSMessageType.THOUGHT:
612
+ emit({
613
+ type: models_2.PRBEAgentStatusType.THOUGHT,
614
+ text: msg.content ?? "",
615
+ });
616
+ break;
617
+ case models_1.WSMessageType.TOOL_CALL: {
618
+ const toolName = msg.name ?? "";
619
+ const callId = msg.id ?? "";
620
+ const args = this.extractArgs(msg.metadata);
621
+ emit({
622
+ type: models_2.PRBEAgentStatusType.TOOL_CALL,
623
+ name: toolName,
624
+ label: msg.content ?? `Running ${toolName}`,
625
+ });
626
+ // Clear before execution so we only capture files flagged by this tool call
627
+ this.pendingFlaggedFiles = [];
628
+ const toolResult = (0, models_1.redactPII)(await this.registry.execute(toolName, args));
629
+ emit({
630
+ type: models_2.PRBEAgentStatusType.OBSERVATION,
631
+ text: toolResult.substring(0, 200),
632
+ });
633
+ // Build refs for any files flagged during this tool call
634
+ let resultMetadata;
635
+ if (this.pendingFlaggedFiles.length > 0 && uploadBaseUrl) {
636
+ const uploadPrefix = this.extractUploadPrefix(uploadBaseUrl);
637
+ const uploadedRefs = [];
638
+ for (const file of this.pendingFlaggedFiles) {
639
+ const filename = path.basename(file.originalPath);
640
+ const safeName = encodeURIComponent(filename);
641
+ const storagePath = `${uploadPrefix}/${safeName}`;
642
+ uploadedRefs.push({
643
+ original_path: file.originalPath,
644
+ reason: file.reason ?? "",
645
+ storage_path: storagePath,
646
+ file_size_bytes: file.data.length,
647
+ });
648
+ // Background upload — don't block the tool_result
649
+ const uploadUrl = `${uploadBaseUrl}/${safeName}`;
650
+ const fileData = file.data;
651
+ const contentType = file.isText
652
+ ? "text/plain"
653
+ : "application/octet-stream";
654
+ void PRBEAgent.backgroundUpload(fileData, uploadUrl, this.config.apiKey, contentType, this.getFetchSignal());
655
+ }
656
+ resultMetadata = { flagged_files: uploadedRefs };
657
+ this.pendingFlaggedFiles = [];
658
+ }
659
+ const resultMsg = {
660
+ type: models_1.WSMessageType.TOOL_RESULT,
661
+ id: callId,
662
+ name: toolName,
663
+ content: toolResult,
664
+ metadata: resultMetadata,
665
+ };
666
+ try {
667
+ ws.send(JSON.stringify(resultMsg));
668
+ }
669
+ catch {
670
+ // Send failed — will be caught by onerror/onclose
671
+ }
672
+ break;
673
+ }
674
+ case models_1.WSMessageType.SERVER_TOOL_CALL:
675
+ emit({
676
+ type: models_2.PRBEAgentStatusType.TOOL_CALL,
677
+ name: msg.name ?? "",
678
+ label: msg.content ?? "",
679
+ });
680
+ break;
681
+ case models_1.WSMessageType.SERVER_OBSERVATION:
682
+ emit({
683
+ type: models_2.PRBEAgentStatusType.OBSERVATION,
684
+ text: (msg.content ?? "").substring(0, 200),
685
+ });
686
+ break;
687
+ case models_1.WSMessageType.COMPLETE: {
688
+ const report = msg.content ?? "";
689
+ const userSummary = msg.metadata?.["user_summary"] ?? "";
690
+ const ticketId = msg.metadata?.["ticket_id"];
691
+ emit({
692
+ type: models_2.PRBEAgentStatusType.COMPLETED,
693
+ report,
694
+ userSummary,
695
+ });
696
+ ws.close(1000, "Complete");
697
+ finish({ report, userSummary, ticketId });
698
+ break;
699
+ }
700
+ case models_1.WSMessageType.ERROR:
701
+ emit({
702
+ type: models_2.PRBEAgentStatusType.ERROR,
703
+ message: msg.content || "Unknown error",
704
+ });
705
+ ws.close(1000, "Error received");
706
+ finish(null);
707
+ break;
708
+ case models_1.WSMessageType.SESSION_CONFIG:
709
+ uploadBaseUrl = msg.metadata?.["upload_url"];
710
+ break;
711
+ case models_1.WSMessageType.PING: {
712
+ const pongMsg = { type: models_1.WSMessageType.PONG };
713
+ try {
714
+ ws.send(JSON.stringify(pongMsg));
715
+ }
716
+ catch {
717
+ // Ignore pong send failures
718
+ }
719
+ break;
720
+ }
721
+ // SDK-originated types and upload_url — ignore
722
+ case models_1.WSMessageType.START:
723
+ case models_1.WSMessageType.TOOL_RESULT:
724
+ case models_1.WSMessageType.UPLOAD_REQUEST:
725
+ case models_1.WSMessageType.CANCEL:
726
+ case models_1.WSMessageType.PONG:
727
+ case models_1.WSMessageType.UPLOAD_URL:
728
+ break;
729
+ }
730
+ };
731
+ ws.onerror = (event) => {
732
+ if (isCancelled()) {
733
+ finish(null);
734
+ return;
735
+ }
736
+ const errorEvent = event;
737
+ const message = errorEvent.message || "WebSocket connection error";
738
+ emit({ type: models_2.PRBEAgentStatusType.ERROR, message });
739
+ finish(null);
740
+ };
741
+ ws.onclose = (_event) => {
742
+ // If we haven't resolved yet, treat as unexpected close
743
+ if (!resolved) {
744
+ if (isCancelled()) {
745
+ finish(null);
746
+ }
747
+ else {
748
+ emit({
749
+ type: models_2.PRBEAgentStatusType.ERROR,
750
+ message: "WebSocket connection closed unexpectedly",
751
+ });
752
+ finish(null);
753
+ }
754
+ }
755
+ };
756
+ });
757
+ }
758
+ sendCancel(ws) {
759
+ try {
760
+ const cancelMsg = { type: models_1.WSMessageType.CANCEL };
761
+ ws.send(JSON.stringify(cancelMsg));
762
+ }
763
+ catch {
764
+ // Ignore
765
+ }
766
+ }
767
+ extractArgs(metadata) {
768
+ if (!metadata)
769
+ return {};
770
+ const argsVal = metadata["args"];
771
+ if (argsVal && typeof argsVal === "object" && !Array.isArray(argsVal)) {
772
+ return argsVal;
773
+ }
774
+ return {};
775
+ }
776
+ extractUploadPrefix(baseUrl) {
777
+ try {
778
+ const url = new URL(baseUrl);
779
+ const urlPath = url.pathname; // "/api/agent/upload/{tenant}/{uuid}/files"
780
+ const marker = "/api/agent/upload/";
781
+ const idx = urlPath.indexOf(marker);
782
+ if (idx !== -1) {
783
+ return urlPath.substring(idx + marker.length);
784
+ }
785
+ }
786
+ catch {
787
+ // Ignore URL parse errors
788
+ }
789
+ return "";
790
+ }
791
+ // ---------- File Upload ----------
792
+ static async backgroundUpload(data, uploadUrl, apiKey, contentType, signal) {
793
+ try {
794
+ const response = await fetch(uploadUrl, {
795
+ method: "PUT",
796
+ headers: {
797
+ "Content-Type": contentType,
798
+ "X-API-Key": apiKey,
799
+ },
800
+ body: data,
801
+ signal,
802
+ });
803
+ if (!response.ok) {
804
+ console.error(`[PRBEAgent] file upload failed: HTTP ${response.status} for ${uploadUrl}`);
805
+ }
806
+ }
807
+ catch (err) {
808
+ console.error(`[PRBEAgent] file upload error: ${err} for ${uploadUrl}`);
809
+ }
810
+ }
811
+ // ---------- Polling ----------
812
+ startPolling() {
813
+ this.stopPolling();
814
+ this.pollingTimer = setInterval(() => {
815
+ void this.poll().then(() => {
816
+ if (this.trackedTicketIDs.length === 0) {
817
+ this.stopPolling();
818
+ }
819
+ });
820
+ }, this.config.pollingInterval);
821
+ }
822
+ // ---------- Networking ----------
823
+ /**
824
+ * Abort all in-flight fetch requests to prevent orphaned DNS lookups
825
+ * that can crash the c-ares resolver during process shutdown.
826
+ */
827
+ abortInFlightRequests() {
828
+ if (this.fetchAbortController) {
829
+ this.fetchAbortController.abort();
830
+ this.fetchAbortController = null;
831
+ }
832
+ }
833
+ /**
834
+ * Get an AbortSignal for fetch requests. Creates a new controller if needed.
835
+ */
836
+ getFetchSignal() {
837
+ if (!this.fetchAbortController) {
838
+ this.fetchAbortController = new AbortController();
839
+ }
840
+ return this.fetchAbortController.signal;
841
+ }
842
+ async post(urlPath, body) {
843
+ const url = `${models_1.API_URL}${urlPath}`;
844
+ const response = await fetch(url, {
845
+ method: "POST",
846
+ headers: {
847
+ "Content-Type": "application/json",
848
+ "X-API-Key": this.config.apiKey,
849
+ },
850
+ body: JSON.stringify(body),
851
+ signal: this.getFetchSignal(),
852
+ });
853
+ if (!response.ok) {
854
+ const message = await response.text().catch(() => "Unknown error");
855
+ throw new models_2.PRBEAgentError(models_2.PRBEAgentErrorType.SERVER_ERROR, `Server error ${response.status}: ${message}`, response.status);
856
+ }
857
+ return (await response.json());
858
+ }
859
+ }
860
+ exports.PRBEAgent = PRBEAgent;
861
+ //# sourceMappingURL=agent.js.map