nowaikit-utils 1.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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/ai-window.html +598 -0
- package/background/service-worker.js +398 -0
- package/cli.mjs +65 -0
- package/content/ai-sidebar.js +1198 -0
- package/content/code-templates.js +843 -0
- package/content/content.js +2527 -0
- package/content/integration-bridge.js +627 -0
- package/content/main-panel.js +592 -0
- package/content/styles.css +1609 -0
- package/icons/README.txt +1 -0
- package/icons/icon-128.png +0 -0
- package/icons/icon-16.png +0 -0
- package/icons/icon-48.png +0 -0
- package/icons/icon.svg +16 -0
- package/manifest.json +63 -0
- package/options/options.html +434 -0
- package/package.json +49 -0
- package/popup/popup.html +663 -0
- package/popup/popup.js +414 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NowAIKit Utils — Integration Bridge
|
|
3
|
+
*
|
|
4
|
+
* WebSocket client that connects the browser extension to the NowAIKit Builder
|
|
5
|
+
* VS Code extension, turning the browser extension into part of an integrated
|
|
6
|
+
* product suite.
|
|
7
|
+
*
|
|
8
|
+
* Protocol:
|
|
9
|
+
* Browser -> Builder: { type: 'api-request', id, method, params }
|
|
10
|
+
* Builder -> Browser: { type: 'api-response', id, success, result|error }
|
|
11
|
+
* Browser -> Builder: { type: 'script-change', table, sys_id, field, name, scope, content }
|
|
12
|
+
* Builder -> Browser: { type: 'script-save', table, sys_id, field, name, scope, content }
|
|
13
|
+
*
|
|
14
|
+
* Not wrapped in IIFE — functions are exposed globally for content.js and ai-sidebar.js.
|
|
15
|
+
* Uses `var` and top-level `function` declarations for content script compatibility.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ─── Bridge State ──────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** @type {WebSocket|null} */
|
|
21
|
+
var _bridgeSocket = null;
|
|
22
|
+
|
|
23
|
+
/** @type {boolean} */
|
|
24
|
+
var _bridgeConnected = false;
|
|
25
|
+
|
|
26
|
+
/** @type {string} */
|
|
27
|
+
var _bridgeInstanceName = '';
|
|
28
|
+
|
|
29
|
+
/** @type {string} */
|
|
30
|
+
var _bridgeInstanceUrl = '';
|
|
31
|
+
|
|
32
|
+
/** @type {number} */
|
|
33
|
+
var _bridgeReconnectAttempts = 0;
|
|
34
|
+
|
|
35
|
+
/** @type {number|null} Timer ID for reconnection */
|
|
36
|
+
var _bridgeReconnectTimer = null;
|
|
37
|
+
|
|
38
|
+
/** @type {number} Configured port (loaded from storage, default 8765) */
|
|
39
|
+
var _bridgePort = 8765;
|
|
40
|
+
|
|
41
|
+
/** @type {boolean} Whether auto-connect is enabled */
|
|
42
|
+
var _bridgeAutoConnect = true;
|
|
43
|
+
|
|
44
|
+
/** @type {Map<string, {resolve: Function, reject: Function, timer: number}>} */
|
|
45
|
+
var _bridgePendingRequests = new Map();
|
|
46
|
+
|
|
47
|
+
/** @type {Function[]} Callbacks fired on connection status changes */
|
|
48
|
+
var _bridgeStatusCallbacks = [];
|
|
49
|
+
|
|
50
|
+
/** @type {Function[]} Callbacks fired when a script-save message arrives */
|
|
51
|
+
var _scriptSaveCallbacks = [];
|
|
52
|
+
|
|
53
|
+
/** @type {number} Request timeout in milliseconds */
|
|
54
|
+
var BRIDGE_REQUEST_TIMEOUT = 30000;
|
|
55
|
+
|
|
56
|
+
/** @type {number} Max fast reconnect attempts before backing off */
|
|
57
|
+
var BRIDGE_MAX_FAST_RECONNECTS = 3;
|
|
58
|
+
|
|
59
|
+
/** @type {number} Fast reconnect interval in milliseconds */
|
|
60
|
+
var BRIDGE_RECONNECT_FAST = 10000;
|
|
61
|
+
|
|
62
|
+
/** @type {number} Slow (backoff) reconnect interval in milliseconds */
|
|
63
|
+
var BRIDGE_RECONNECT_SLOW = 30000;
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// ─── ID Generation ─────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate a unique request ID for correlating request/response pairs.
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function _bridgeGenerateId() {
|
|
73
|
+
return Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
// ─── Status Change Notifications ───────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Fire all registered status change callbacks with the current state.
|
|
81
|
+
*/
|
|
82
|
+
function _bridgeFireStatusChange() {
|
|
83
|
+
var status = getBridgeStatus();
|
|
84
|
+
for (var i = 0; i < _bridgeStatusCallbacks.length; i++) {
|
|
85
|
+
try {
|
|
86
|
+
_bridgeStatusCallbacks[i](status);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// Don't let a bad callback break the bridge
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
// ─── Connection Management ─────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initialize the integration bridge.
|
|
98
|
+
*
|
|
99
|
+
* Loads configuration from chrome.storage and optionally auto-connects
|
|
100
|
+
* to the NowAIKit Builder VS Code extension WebSocket server.
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} [options]
|
|
103
|
+
* @param {number} [options.port] - WebSocket port (default: from storage or 8765)
|
|
104
|
+
* @param {boolean} [options.autoConnect] - Connect immediately (default: from storage or true)
|
|
105
|
+
* @param {Function} [options.onStatusChange] - Callback for connection status changes
|
|
106
|
+
*/
|
|
107
|
+
function initIntegrationBridge(options) {
|
|
108
|
+
options = options || {};
|
|
109
|
+
|
|
110
|
+
// Register status change callback if provided
|
|
111
|
+
if (typeof options.onStatusChange === 'function') {
|
|
112
|
+
_bridgeStatusCallbacks.push(options.onStatusChange);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Load configuration from chrome.storage, then connect if enabled
|
|
116
|
+
chrome.storage.local.get({ bridgePort: 8765 }, function(localData) {
|
|
117
|
+
_bridgePort = options.port || localData.bridgePort || 8765;
|
|
118
|
+
|
|
119
|
+
chrome.storage.sync.get({ enableBridgeAutoConnect: true }, function(syncData) {
|
|
120
|
+
_bridgeAutoConnect = (options.autoConnect !== undefined)
|
|
121
|
+
? options.autoConnect
|
|
122
|
+
: syncData.enableBridgeAutoConnect;
|
|
123
|
+
|
|
124
|
+
if (_bridgeAutoConnect) {
|
|
125
|
+
connectToBridge();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Connect (or reconnect) to the NowAIKit Builder WebSocket server.
|
|
133
|
+
*
|
|
134
|
+
* Safe to call multiple times — if already connected, this is a no-op.
|
|
135
|
+
* On successful connection, immediately queries the Builder for instance info.
|
|
136
|
+
*/
|
|
137
|
+
function connectToBridge() {
|
|
138
|
+
// Don't open a second socket
|
|
139
|
+
if (_bridgeSocket && (_bridgeSocket.readyState === WebSocket.CONNECTING || _bridgeSocket.readyState === WebSocket.OPEN)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Clear any pending reconnect timer
|
|
144
|
+
if (_bridgeReconnectTimer !== null) {
|
|
145
|
+
clearTimeout(_bridgeReconnectTimer);
|
|
146
|
+
_bridgeReconnectTimer = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
var url = 'ws://localhost:' + _bridgePort;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
_bridgeSocket = new WebSocket(url);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
// WebSocket constructor can throw if URL is invalid or localhost blocked
|
|
155
|
+
_bridgeConnected = false;
|
|
156
|
+
_bridgeFireStatusChange();
|
|
157
|
+
_bridgeScheduleReconnect();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── onopen ──
|
|
162
|
+
|
|
163
|
+
_bridgeSocket.onopen = function() {
|
|
164
|
+
_bridgeConnected = true;
|
|
165
|
+
_bridgeReconnectAttempts = 0;
|
|
166
|
+
_bridgeFireStatusChange();
|
|
167
|
+
|
|
168
|
+
// Immediately fetch instance info from Builder
|
|
169
|
+
_bridgeRequest('get_instance_info', {}).then(function(result) {
|
|
170
|
+
if (result) {
|
|
171
|
+
_bridgeInstanceName = result.name || result.instanceName || '';
|
|
172
|
+
_bridgeInstanceUrl = result.url || result.instanceUrl || '';
|
|
173
|
+
_bridgeFireStatusChange();
|
|
174
|
+
}
|
|
175
|
+
}).catch(function() {
|
|
176
|
+
// Non-fatal — Builder might not have instance info yet
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// ── onmessage ──
|
|
181
|
+
|
|
182
|
+
_bridgeSocket.onmessage = function(event) {
|
|
183
|
+
var msg;
|
|
184
|
+
try {
|
|
185
|
+
msg = JSON.parse(event.data);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
return; // Ignore non-JSON messages
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (msg.type === 'api-response') {
|
|
191
|
+
_bridgeHandleResponse(msg);
|
|
192
|
+
} else if (msg.type === 'script-save') {
|
|
193
|
+
_bridgeHandleScriptSave(msg);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// ── onerror ──
|
|
198
|
+
|
|
199
|
+
_bridgeSocket.onerror = function() {
|
|
200
|
+
// Error usually followed by close — actual handling happens in onclose
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ── onclose ──
|
|
204
|
+
|
|
205
|
+
_bridgeSocket.onclose = function() {
|
|
206
|
+
var wasConnected = _bridgeConnected;
|
|
207
|
+
_bridgeConnected = false;
|
|
208
|
+
_bridgeInstanceName = '';
|
|
209
|
+
_bridgeInstanceUrl = '';
|
|
210
|
+
_bridgeSocket = null;
|
|
211
|
+
|
|
212
|
+
// Reject all pending requests
|
|
213
|
+
_bridgePendingRequests.forEach(function(entry) {
|
|
214
|
+
clearTimeout(entry.timer);
|
|
215
|
+
entry.reject(new Error('Bridge connection closed'));
|
|
216
|
+
});
|
|
217
|
+
_bridgePendingRequests.clear();
|
|
218
|
+
|
|
219
|
+
if (wasConnected) {
|
|
220
|
+
_bridgeFireStatusChange();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_bridgeScheduleReconnect();
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Manually disconnect from the Builder WebSocket server.
|
|
229
|
+
*
|
|
230
|
+
* Cancels any pending reconnect timer so the bridge stays disconnected
|
|
231
|
+
* until `connectToBridge()` is called again.
|
|
232
|
+
*/
|
|
233
|
+
function disconnectBridge() {
|
|
234
|
+
// Cancel reconnect
|
|
235
|
+
if (_bridgeReconnectTimer !== null) {
|
|
236
|
+
clearTimeout(_bridgeReconnectTimer);
|
|
237
|
+
_bridgeReconnectTimer = null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_bridgeReconnectAttempts = 0;
|
|
241
|
+
|
|
242
|
+
if (_bridgeSocket) {
|
|
243
|
+
// Prevent onclose from triggering reconnect
|
|
244
|
+
_bridgeSocket.onclose = null;
|
|
245
|
+
_bridgeSocket.onerror = null;
|
|
246
|
+
_bridgeSocket.close();
|
|
247
|
+
_bridgeSocket = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var wasConnected = _bridgeConnected;
|
|
251
|
+
_bridgeConnected = false;
|
|
252
|
+
_bridgeInstanceName = '';
|
|
253
|
+
_bridgeInstanceUrl = '';
|
|
254
|
+
|
|
255
|
+
// Reject pending requests
|
|
256
|
+
_bridgePendingRequests.forEach(function(entry) {
|
|
257
|
+
clearTimeout(entry.timer);
|
|
258
|
+
entry.reject(new Error('Bridge manually disconnected'));
|
|
259
|
+
});
|
|
260
|
+
_bridgePendingRequests.clear();
|
|
261
|
+
|
|
262
|
+
if (wasConnected) {
|
|
263
|
+
_bridgeFireStatusChange();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get the current bridge connection status.
|
|
269
|
+
*
|
|
270
|
+
* @returns {{ connected: boolean, instanceName: string, instanceUrl: string }}
|
|
271
|
+
*/
|
|
272
|
+
function getBridgeStatus() {
|
|
273
|
+
return {
|
|
274
|
+
connected: _bridgeConnected,
|
|
275
|
+
instanceName: _bridgeInstanceName,
|
|
276
|
+
instanceUrl: _bridgeInstanceUrl,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
// ─── Reconnection Logic ────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Schedule a reconnection attempt with exponential backoff.
|
|
285
|
+
*
|
|
286
|
+
* First 3 attempts use a 10-second interval, then backs off to 30 seconds.
|
|
287
|
+
*/
|
|
288
|
+
function _bridgeScheduleReconnect() {
|
|
289
|
+
if (_bridgeReconnectTimer !== null) return; // Already scheduled
|
|
290
|
+
|
|
291
|
+
_bridgeReconnectAttempts++;
|
|
292
|
+
|
|
293
|
+
var delay = (_bridgeReconnectAttempts <= BRIDGE_MAX_FAST_RECONNECTS)
|
|
294
|
+
? BRIDGE_RECONNECT_FAST
|
|
295
|
+
: BRIDGE_RECONNECT_SLOW;
|
|
296
|
+
|
|
297
|
+
_bridgeReconnectTimer = setTimeout(function() {
|
|
298
|
+
_bridgeReconnectTimer = null;
|
|
299
|
+
connectToBridge();
|
|
300
|
+
}, delay);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
// ─── Request / Response Correlation ────────────────────────────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Handle an incoming api-response message from the Builder.
|
|
308
|
+
*
|
|
309
|
+
* Matches the response to a pending request by ID, resolves or rejects
|
|
310
|
+
* the associated Promise, and cleans up the timeout timer.
|
|
311
|
+
*
|
|
312
|
+
* @param {{ id: string, success: boolean, result?: *, error?: string }} msg
|
|
313
|
+
*/
|
|
314
|
+
function _bridgeHandleResponse(msg) {
|
|
315
|
+
var id = msg.id;
|
|
316
|
+
if (!id || !_bridgePendingRequests.has(id)) return;
|
|
317
|
+
|
|
318
|
+
var entry = _bridgePendingRequests.get(id);
|
|
319
|
+
_bridgePendingRequests.delete(id);
|
|
320
|
+
clearTimeout(entry.timer);
|
|
321
|
+
|
|
322
|
+
if (msg.success) {
|
|
323
|
+
entry.resolve(msg.result);
|
|
324
|
+
} else {
|
|
325
|
+
entry.reject(new Error(msg.error || 'Unknown bridge error'));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Send an API request to the Builder and return a Promise for the response.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} method - InstanceService method name
|
|
333
|
+
* @param {Object} params - Method parameters
|
|
334
|
+
* @returns {Promise<*>} - Resolves with the result payload
|
|
335
|
+
*/
|
|
336
|
+
function _bridgeRequest(method, params) {
|
|
337
|
+
return new Promise(function(resolve, reject) {
|
|
338
|
+
if (!_bridgeSocket || _bridgeSocket.readyState !== WebSocket.OPEN) {
|
|
339
|
+
reject(new Error('Bridge not connected'));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
var id = _bridgeGenerateId();
|
|
344
|
+
|
|
345
|
+
var timer = setTimeout(function() {
|
|
346
|
+
if (_bridgePendingRequests.has(id)) {
|
|
347
|
+
_bridgePendingRequests.delete(id);
|
|
348
|
+
reject(new Error('Bridge request timed out (' + method + ')'));
|
|
349
|
+
}
|
|
350
|
+
}, BRIDGE_REQUEST_TIMEOUT);
|
|
351
|
+
|
|
352
|
+
_bridgePendingRequests.set(id, {
|
|
353
|
+
resolve: resolve,
|
|
354
|
+
reject: reject,
|
|
355
|
+
timer: timer,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
var message = {
|
|
359
|
+
type: 'api-request',
|
|
360
|
+
id: id,
|
|
361
|
+
method: method,
|
|
362
|
+
params: params || {},
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
_bridgeSocket.send(JSON.stringify(message));
|
|
367
|
+
} catch (e) {
|
|
368
|
+
_bridgePendingRequests.delete(id);
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
reject(new Error('Failed to send bridge request: ' + e.message));
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
// ─── Script Sync Handling ──────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Handle an incoming script-save message from the Builder.
|
|
380
|
+
*
|
|
381
|
+
* Attempts to update the corresponding form field in the ServiceNow page
|
|
382
|
+
* if we are viewing the matching record, then notifies all registered
|
|
383
|
+
* script-save callbacks.
|
|
384
|
+
*
|
|
385
|
+
* @param {{ table: string, sys_id: string, field: string, name: string, scope: string, content: string }} msg
|
|
386
|
+
*/
|
|
387
|
+
function _bridgeHandleScriptSave(msg) {
|
|
388
|
+
// Try to update the ServiceNow form if we're on the right page
|
|
389
|
+
_bridgeApplyScriptToPage(msg);
|
|
390
|
+
|
|
391
|
+
// Fire registered callbacks
|
|
392
|
+
for (var i = 0; i < _scriptSaveCallbacks.length; i++) {
|
|
393
|
+
try {
|
|
394
|
+
_scriptSaveCallbacks[i](msg);
|
|
395
|
+
} catch (e) {
|
|
396
|
+
// Don't let a bad callback break the handler
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Attempt to apply script content to the current ServiceNow page.
|
|
403
|
+
*
|
|
404
|
+
* Checks if we are on a form for the matching table/sys_id, then updates
|
|
405
|
+
* the field via g_form.setValue(), CodeMirror, or textarea fallback.
|
|
406
|
+
*
|
|
407
|
+
* @param {{ table: string, sys_id: string, field: string, content: string }} msg
|
|
408
|
+
*/
|
|
409
|
+
function _bridgeApplyScriptToPage(msg) {
|
|
410
|
+
// Check if we're on the matching record page
|
|
411
|
+
// currentTable and currentSysId are defined in content.js
|
|
412
|
+
if (typeof currentTable === 'undefined' || typeof currentSysId === 'undefined') return;
|
|
413
|
+
if (!msg.table || !msg.sys_id || !msg.field || typeof msg.content !== 'string') return;
|
|
414
|
+
if (msg.table !== currentTable || msg.sys_id !== currentSysId) return;
|
|
415
|
+
|
|
416
|
+
// Validate field name is a safe identifier (prevent CSS selector injection)
|
|
417
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(msg.field)) return;
|
|
418
|
+
|
|
419
|
+
// Method 1: Use g_form.setValue() if available (most reliable)
|
|
420
|
+
if (typeof g_form !== 'undefined' && typeof g_form.setValue === 'function') {
|
|
421
|
+
try {
|
|
422
|
+
g_form.setValue(msg.field, msg.content);
|
|
423
|
+
return;
|
|
424
|
+
} catch (e) {
|
|
425
|
+
// Fall through to other methods
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Method 2: Find a CodeMirror editor for the field
|
|
430
|
+
var fieldElement = document.getElementById(msg.field) || document.querySelector('[name="' + msg.field + '"]');
|
|
431
|
+
if (fieldElement) {
|
|
432
|
+
// Check if the field has a CodeMirror instance
|
|
433
|
+
var cmWrapper = fieldElement.closest('.CodeMirror') || (fieldElement.nextElementSibling && fieldElement.nextElementSibling.classList.contains('CodeMirror') ? fieldElement.nextElementSibling : null);
|
|
434
|
+
if (!cmWrapper) {
|
|
435
|
+
// Some ServiceNow forms wrap the textarea, and the CM is a sibling
|
|
436
|
+
var parent = fieldElement.parentElement;
|
|
437
|
+
if (parent) {
|
|
438
|
+
cmWrapper = parent.querySelector('.CodeMirror');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (cmWrapper && cmWrapper.CodeMirror) {
|
|
443
|
+
try {
|
|
444
|
+
cmWrapper.CodeMirror.setValue(msg.content);
|
|
445
|
+
return;
|
|
446
|
+
} catch (e) {
|
|
447
|
+
// Fall through
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Method 3: Direct textarea update
|
|
452
|
+
if (fieldElement.tagName === 'TEXTAREA' || fieldElement.tagName === 'INPUT') {
|
|
453
|
+
fieldElement.value = msg.content;
|
|
454
|
+
fieldElement.dispatchEvent(new Event('input', { bubbles: true }));
|
|
455
|
+
fieldElement.dispatchEvent(new Event('change', { bubbles: true }));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
// ─── Public API: ServiceNow Instance Queries ───────────────────
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Query records from a ServiceNow table via the Builder's InstanceService.
|
|
466
|
+
*
|
|
467
|
+
* @param {string} table - Table name (e.g. 'incident', 'sys_script')
|
|
468
|
+
* @param {string} [query] - Encoded query string (e.g. 'active=true^priority=1')
|
|
469
|
+
* @param {string[]} [fields] - Array of field names to return
|
|
470
|
+
* @param {number} [limit] - Maximum records to return
|
|
471
|
+
* @returns {Promise<Object[]>}
|
|
472
|
+
*/
|
|
473
|
+
function bridgeQueryRecords(table, query, fields, limit) {
|
|
474
|
+
var params = { table: table };
|
|
475
|
+
if (query) params.query = query;
|
|
476
|
+
if (fields) params.fields = fields;
|
|
477
|
+
if (limit) params.limit = limit;
|
|
478
|
+
return _bridgeRequest('query_records', params);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get a single record by sys_id via the Builder's InstanceService.
|
|
483
|
+
*
|
|
484
|
+
* @param {string} table - Table name
|
|
485
|
+
* @param {string} sysId - Record sys_id
|
|
486
|
+
* @param {string[]} [fields] - Array of field names to return
|
|
487
|
+
* @returns {Promise<Object>}
|
|
488
|
+
*/
|
|
489
|
+
function bridgeGetRecord(table, sysId, fields) {
|
|
490
|
+
var params = { table: table, sys_id: sysId };
|
|
491
|
+
if (fields) params.fields = fields;
|
|
492
|
+
return _bridgeRequest('get_record', params);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Get the schema (field definitions) for a ServiceNow table.
|
|
497
|
+
*
|
|
498
|
+
* @param {string} table - Table name
|
|
499
|
+
* @returns {Promise<Object>}
|
|
500
|
+
*/
|
|
501
|
+
function bridgeGetTableSchema(table) {
|
|
502
|
+
return _bridgeRequest('get_table_schema', { table: table });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Get update sets, optionally filtered by state.
|
|
507
|
+
*
|
|
508
|
+
* @param {string} [state] - Filter by state (e.g. 'in progress', 'complete')
|
|
509
|
+
* @returns {Promise<Object[]>}
|
|
510
|
+
*/
|
|
511
|
+
function bridgeGetUpdateSets(state) {
|
|
512
|
+
var params = {};
|
|
513
|
+
if (state) params.state = state;
|
|
514
|
+
return _bridgeRequest('get_update_sets', params);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Get scripts from the instance, filtered by table type and scope.
|
|
519
|
+
*
|
|
520
|
+
* @param {string} [tableType] - Script table (e.g. 'sys_script', 'sys_script_include')
|
|
521
|
+
* @param {string} [scope] - Application scope
|
|
522
|
+
* @returns {Promise<Object[]>}
|
|
523
|
+
*/
|
|
524
|
+
function bridgeGetScripts(tableType, scope) {
|
|
525
|
+
var params = {};
|
|
526
|
+
if (tableType) params.table_type = tableType;
|
|
527
|
+
if (scope) params.scope = scope;
|
|
528
|
+
return _bridgeRequest('get_scripts', params);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Execute a server-side script on the instance (background script).
|
|
533
|
+
*
|
|
534
|
+
* @param {string} script - Script body to execute
|
|
535
|
+
* @param {string} [scope] - Application scope to run in
|
|
536
|
+
* @returns {Promise<Object>}
|
|
537
|
+
*/
|
|
538
|
+
function bridgeExecuteScript(script, scope) {
|
|
539
|
+
var params = { script: script };
|
|
540
|
+
if (scope) params.scope = scope;
|
|
541
|
+
return _bridgeRequest('execute_script', params);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Get instance health information (node status, stats, etc.).
|
|
546
|
+
*
|
|
547
|
+
* @returns {Promise<Object>}
|
|
548
|
+
*/
|
|
549
|
+
function bridgeGetInstanceHealth() {
|
|
550
|
+
return _bridgeRequest('get_instance_health', {});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Get instance identification info (name, URL, version, etc.).
|
|
555
|
+
*
|
|
556
|
+
* @returns {Promise<Object>}
|
|
557
|
+
*/
|
|
558
|
+
function bridgeGetInstanceInfo() {
|
|
559
|
+
return _bridgeRequest('get_instance_info', {});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Search for scripts matching a query across one or more script tables.
|
|
564
|
+
*
|
|
565
|
+
* @param {string} [table] - Script table to search (e.g. 'sys_script_include')
|
|
566
|
+
* @param {string} [query] - Search text or encoded query
|
|
567
|
+
* @param {string[]} [fields] - Fields to return
|
|
568
|
+
* @param {number} [limit] - Maximum results
|
|
569
|
+
* @returns {Promise<Object[]>}
|
|
570
|
+
*/
|
|
571
|
+
function bridgeSearchScripts(table, query, fields, limit) {
|
|
572
|
+
var params = {};
|
|
573
|
+
if (table) params.table = table;
|
|
574
|
+
if (query) params.query = query;
|
|
575
|
+
if (fields) params.fields = fields;
|
|
576
|
+
if (limit) params.limit = limit;
|
|
577
|
+
return _bridgeRequest('search_scripts', params);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
// ─── Public API: Script Sync ───────────────────────────────────
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Send a script-change message to the Builder, notifying it that
|
|
585
|
+
* a script has been modified in the browser.
|
|
586
|
+
*
|
|
587
|
+
* @param {string} table - Table name (e.g. 'sys_script_include')
|
|
588
|
+
* @param {string} sysId - Record sys_id
|
|
589
|
+
* @param {string} field - Field name (e.g. 'script')
|
|
590
|
+
* @param {string} name - Record display name
|
|
591
|
+
* @param {string} scope - Application scope
|
|
592
|
+
* @param {string} content - Updated script content
|
|
593
|
+
*/
|
|
594
|
+
function bridgeSendScriptChange(table, sysId, field, name, scope, content) {
|
|
595
|
+
if (!_bridgeSocket || _bridgeSocket.readyState !== WebSocket.OPEN) {
|
|
596
|
+
return; // Silently skip if not connected
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
var message = {
|
|
600
|
+
type: 'script-change',
|
|
601
|
+
table: table,
|
|
602
|
+
sys_id: sysId,
|
|
603
|
+
field: field,
|
|
604
|
+
name: name,
|
|
605
|
+
scope: scope,
|
|
606
|
+
content: content,
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
_bridgeSocket.send(JSON.stringify(message));
|
|
611
|
+
} catch (e) {
|
|
612
|
+
// Silently fail — connection may have just dropped
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Register a callback to be invoked when a script-save message arrives
|
|
618
|
+
* from the Builder (indicating a script was saved in VS Code).
|
|
619
|
+
*
|
|
620
|
+
* @param {Function} callback - Called with the script-save message object:
|
|
621
|
+
* { table, sys_id, field, name, scope, content }
|
|
622
|
+
*/
|
|
623
|
+
function onBridgeScriptSave(callback) {
|
|
624
|
+
if (typeof callback === 'function') {
|
|
625
|
+
_scriptSaveCallbacks.push(callback);
|
|
626
|
+
}
|
|
627
|
+
}
|