@snutils/snu 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 (67) hide show
  1. package/README.md +156 -0
  2. package/bin/snu.js +8 -0
  3. package/dist/cli/format.d.ts +14 -0
  4. package/dist/cli/format.d.ts.map +1 -0
  5. package/dist/cli/format.js +231 -0
  6. package/dist/cli/format.js.map +1 -0
  7. package/dist/cli/index.d.ts +4 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +296 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli/stdin.d.ts +14 -0
  12. package/dist/cli/stdin.d.ts.map +1 -0
  13. package/dist/cli/stdin.js +75 -0
  14. package/dist/cli/stdin.js.map +1 -0
  15. package/dist/client.d.ts +43 -0
  16. package/dist/client.d.ts.map +1 -0
  17. package/dist/client.js +350 -0
  18. package/dist/client.js.map +1 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +28 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/mcp/index.d.ts +4 -0
  24. package/dist/mcp/index.d.ts.map +1 -0
  25. package/dist/mcp/index.js +185 -0
  26. package/dist/mcp/index.js.map +1 -0
  27. package/dist/registry.d.ts +5 -0
  28. package/dist/registry.d.ts.map +1 -0
  29. package/dist/registry.js +527 -0
  30. package/dist/registry.js.map +1 -0
  31. package/dist/server/canonical.d.ts +10 -0
  32. package/dist/server/canonical.d.ts.map +1 -0
  33. package/dist/server/canonical.js +72 -0
  34. package/dist/server/canonical.js.map +1 -0
  35. package/dist/server/config.d.ts +9 -0
  36. package/dist/server/config.d.ts.map +1 -0
  37. package/dist/server/config.js +117 -0
  38. package/dist/server/config.js.map +1 -0
  39. package/dist/server/dispatcher.d.ts +32 -0
  40. package/dist/server/dispatcher.d.ts.map +1 -0
  41. package/dist/server/dispatcher.js +631 -0
  42. package/dist/server/dispatcher.js.map +1 -0
  43. package/dist/server/httpBridge.d.ts +20 -0
  44. package/dist/server/httpBridge.d.ts.map +1 -0
  45. package/dist/server/httpBridge.js +219 -0
  46. package/dist/server/httpBridge.js.map +1 -0
  47. package/dist/server/pendingRegistry.d.ts +26 -0
  48. package/dist/server/pendingRegistry.d.ts.map +1 -0
  49. package/dist/server/pendingRegistry.js +76 -0
  50. package/dist/server/pendingRegistry.js.map +1 -0
  51. package/dist/server/policy.d.ts +18 -0
  52. package/dist/server/policy.d.ts.map +1 -0
  53. package/dist/server/policy.js +53 -0
  54. package/dist/server/policy.js.map +1 -0
  55. package/dist/server/standalone.d.ts +24 -0
  56. package/dist/server/standalone.d.ts.map +1 -0
  57. package/dist/server/standalone.js +152 -0
  58. package/dist/server/standalone.js.map +1 -0
  59. package/dist/server/wsBridge.d.ts +45 -0
  60. package/dist/server/wsBridge.d.ts.map +1 -0
  61. package/dist/server/wsBridge.js +321 -0
  62. package/dist/server/wsBridge.js.map +1 -0
  63. package/dist/types.d.ts +123 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +6 -0
  66. package/dist/types.js.map +1 -0
  67. package/package.json +51 -0
@@ -0,0 +1,631 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.StandaloneDispatcher = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const crypto = __importStar(require("crypto"));
40
+ const pendingRegistry_js_1 = require("./pendingRegistry.js");
41
+ const policy_js_1 = require("./policy.js");
42
+ const config_js_1 = require("./config.js");
43
+ const canonical_js_1 = require("./canonical.js");
44
+ class StandaloneDispatcher {
45
+ cwd;
46
+ ws;
47
+ pending;
48
+ config;
49
+ pendingReviewRequests = new Map(); // correlationId/requestId -> reviewId
50
+ requestIdToCorrelationId = new Map(); // requestId -> correlationId
51
+ constructor(opts) {
52
+ this.cwd = opts.cwd || process.cwd();
53
+ this.ws = opts.wsBridge;
54
+ this.pending = opts.pending || pendingRegistry_js_1.defaultPendingRegistry;
55
+ this.config = (0, config_js_1.resolveStandaloneConfig)(opts.cliFlags);
56
+ }
57
+ getWorkspaceRoot() {
58
+ return this.cwd;
59
+ }
60
+ getInstanceSettings(instanceName) {
61
+ const p1 = path.join(this.cwd, instanceName, '_settings.json');
62
+ const p2 = path.join(this.cwd, instanceName, 'settings.json');
63
+ if (fs.existsSync(p1)) {
64
+ try {
65
+ return JSON.parse(fs.readFileSync(p1, 'utf8'));
66
+ }
67
+ catch { }
68
+ }
69
+ if (fs.existsSync(p2)) {
70
+ try {
71
+ return JSON.parse(fs.readFileSync(p2, 'utf8'));
72
+ }
73
+ catch { }
74
+ }
75
+ return null;
76
+ }
77
+ listInstanceFolders() {
78
+ try {
79
+ const entries = fs.readdirSync(this.cwd, { withFileTypes: true });
80
+ const nonInstance = new Set(['.vscode', '.cursor', '.git', 'node_modules', 'dist', 'bin', 'packages', 'screenshots', 'agentrules']);
81
+ return entries
82
+ .filter((d) => d.isDirectory() && !d.name.startsWith('.') && !nonInstance.has(d.name.toLowerCase()))
83
+ .map((d) => path.join(this.cwd, d.name))
84
+ .filter((f) => fs.existsSync(path.join(f, '_settings.json')) || fs.existsSync(path.join(f, 'settings.json')));
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ }
90
+ resolveInstance(requestInstance) {
91
+ const folders = this.listInstanceFolders();
92
+ if (requestInstance) {
93
+ const targetFolder = path.join(this.cwd, requestInstance);
94
+ const settings = this.getInstanceSettings(requestInstance);
95
+ if (!settings) {
96
+ throw Object.assign(new Error(`Instance folder not found or missing _settings.json: ${requestInstance}`), {
97
+ code: 'E_INSTANCE_NOT_FOUND',
98
+ });
99
+ }
100
+ return { name: requestInstance, folder: targetFolder, settings };
101
+ }
102
+ if (folders.length === 1) {
103
+ const name = path.basename(folders[0]);
104
+ return { name, folder: folders[0], settings: this.getInstanceSettings(name) };
105
+ }
106
+ if (folders.length === 0) {
107
+ return { name: 'default', folder: this.cwd, settings: {} };
108
+ }
109
+ // Check freshness to find default instance
110
+ const now = Date.now();
111
+ const ranked = folders.map((f) => {
112
+ const name = path.basename(f);
113
+ let mtime = 0;
114
+ try {
115
+ mtime = fs.statSync(path.join(f, '_settings.json')).mtimeMs;
116
+ }
117
+ catch { }
118
+ return { name, folder: f, settings: this.getInstanceSettings(name), age: now - mtime };
119
+ }).sort((a, b) => a.age - b.age);
120
+ return ranked[0];
121
+ }
122
+ cancel(requestId, reason = 'CANCELLED') {
123
+ const correlationId = this.requestIdToCorrelationId.get(requestId) || requestId;
124
+ const reviewId = this.pendingReviewRequests.get(correlationId) || this.pendingReviewRequests.get(requestId);
125
+ if (reviewId) {
126
+ this.pendingReviewRequests.delete(correlationId);
127
+ this.pendingReviewRequests.delete(requestId);
128
+ this.ws.cancelReview(reviewId, reason);
129
+ }
130
+ this.pending.cancel(correlationId, reason);
131
+ this.pending.cancel(requestId, reason);
132
+ this.requestIdToCorrelationId.delete(requestId);
133
+ }
134
+ async dispatch(req) {
135
+ const correlationId = `snu_req_${req.id}_${Date.now()}`;
136
+ this.requestIdToCorrelationId.set(req.id, correlationId);
137
+ const policy = (0, policy_js_1.getCommandPolicy)(req);
138
+ try {
139
+ // 1. Connection Commands
140
+ if (req.command === 'check_connection') {
141
+ const isRunning = this.ws.isServerRunning();
142
+ const hasBrowser = this.ws.hasBrowserClient();
143
+ const state = this.ws.getHelperState();
144
+ return {
145
+ id: req.id,
146
+ command: req.command,
147
+ status: 'success',
148
+ timestamp: Date.now(),
149
+ result: {
150
+ serverRunning: isRunning,
151
+ browserConnected: hasBrowser,
152
+ helper: hasBrowser
153
+ ? {
154
+ tier: state.tier,
155
+ proFeatures: state.proFeatures,
156
+ cdp: state.cdp,
157
+ capabilities: state.capabilities,
158
+ }
159
+ : null,
160
+ },
161
+ };
162
+ }
163
+ if (req.command === 'list_instances') {
164
+ const folders = this.listInstanceFolders();
165
+ const now = Date.now();
166
+ const instances = folders.map((f) => {
167
+ const name = path.basename(f);
168
+ const settings = this.getInstanceSettings(name) || {};
169
+ let lastActiveAgeMs = null;
170
+ try {
171
+ const mtime = fs.statSync(path.join(f, '_settings.json')).mtimeMs;
172
+ lastActiveAgeMs = Math.max(0, now - mtime);
173
+ }
174
+ catch { }
175
+ return {
176
+ name,
177
+ url: settings.url || null,
178
+ hasSettings: !!settings.url,
179
+ recentlyActive: lastActiveAgeMs !== null && lastActiveAgeMs < 10 * 3600 * 1000,
180
+ lastActiveAgeMs,
181
+ };
182
+ });
183
+ instances.sort((a, b) => {
184
+ if (a.lastActiveAgeMs === null)
185
+ return 1;
186
+ if (b.lastActiveAgeMs === null)
187
+ return -1;
188
+ return a.lastActiveAgeMs - b.lastActiveAgeMs;
189
+ });
190
+ const recent = instances.filter((i) => i.recentlyActive);
191
+ const defaultInstance = recent.length === 1 ? recent[0].name : (instances[0]?.name || null);
192
+ return {
193
+ id: req.id,
194
+ command: req.command,
195
+ status: 'success',
196
+ timestamp: Date.now(),
197
+ result: {
198
+ instances,
199
+ count: instances.length,
200
+ connected: this.ws.hasBrowserClient(),
201
+ defaultInstance,
202
+ needsConfirmation: false,
203
+ },
204
+ };
205
+ }
206
+ if (req.command === 'get_instance_info') {
207
+ const inst = this.resolveInstance(req.instance);
208
+ return {
209
+ id: req.id,
210
+ command: req.command,
211
+ status: 'success',
212
+ timestamp: Date.now(),
213
+ result: {
214
+ instanceName: inst.name,
215
+ hasSettings: !!inst.settings?.url,
216
+ connected: this.ws.hasBrowserClient(),
217
+ },
218
+ };
219
+ }
220
+ if (req.command === 'get_capabilities') {
221
+ const state = this.ws.getHelperState();
222
+ const instanceGatesRecord = {};
223
+ for (const [origin, snap] of state.instanceGates.entries()) {
224
+ instanceGatesRecord[origin] = snap;
225
+ }
226
+ return {
227
+ id: req.id,
228
+ command: req.command,
229
+ status: 'success',
230
+ timestamp: Date.now(),
231
+ result: {
232
+ apiVersion: 8,
233
+ tier: state.tier,
234
+ proFeatures: state.proFeatures,
235
+ cdp: state.cdp,
236
+ gates: this.config.gates,
237
+ instanceGates: instanceGatesRecord,
238
+ capabilities: state.capabilities,
239
+ },
240
+ };
241
+ }
242
+ // Check browser connection for all instance/remote commands
243
+ if (!this.ws.hasBrowserClient()) {
244
+ throw Object.assign(new Error('Browser helper disconnected. Open the SN Utils helper tab via /token in ServiceNow.'), {
245
+ code: 'E_BROWSER_DISCONNECTED',
246
+ });
247
+ }
248
+ const inst = this.resolveInstance(req.instance);
249
+ const instanceUrl = inst.settings?.url || '';
250
+ const instanceOrigin = instanceUrl ? new URL(instanceUrl).origin.toLowerCase() : '';
251
+ // 2. Authoritative Security Gate Evaluation (Deny-Wins)
252
+ for (const gateName of policy.gates) {
253
+ // A. Check Host Gate (Fail-closed standalone authority)
254
+ if (!this.config.gates[gateName]) {
255
+ const label = gateName === 'backgroundScripts' ? 'Background Scripts' : gateName === 'deleteRecords' ? 'Delete Records' : gateName;
256
+ throw Object.assign(new Error(`${label} is disabled in standalone host config. Pass --allow-${gateName.replace(/[A-Z]/g, (l) => '-' + l.toLowerCase())} or set SNU_ALLOW_${gateName.toUpperCase()}=1.`), { code: 'E_DISABLED' });
257
+ }
258
+ // B. Check Helper Instance Gate (if instanceSecurityGates capability is present)
259
+ const helperState = this.ws.getHelperState();
260
+ if (helperState.capabilities?.instanceSecurityGates && instanceOrigin) {
261
+ const helperGateMode = this.ws.getInstanceGate(instanceUrl, gateName);
262
+ if (helperGateMode === 'off' || helperGateMode === false) {
263
+ const label = gateName === 'backgroundScripts' ? 'Background Scripts' : gateName === 'deleteRecords' ? 'Delete Records' : gateName;
264
+ throw Object.assign(new Error(`This instance (${instanceOrigin}) does not permit ${label.toLowerCase()} in SN Utils helper.`), { code: 'E_DISABLED' });
265
+ }
266
+ }
267
+ }
268
+ // 3. Two-Phase Human Review Enforcement (when supported by connected helper)
269
+ const helperState = this.ws.getHelperState();
270
+ let isReviewRequired = policy.review === 'required' && this.config.reviewHighRisk;
271
+ if (helperState.capabilities?.instanceSecurityGates && instanceOrigin) {
272
+ for (const gateName of policy.gates) {
273
+ const gateMode = this.ws.getInstanceGate(instanceUrl, gateName);
274
+ if (gateMode === 'auto') {
275
+ isReviewRequired = false;
276
+ }
277
+ else if (gateMode === 'approve') {
278
+ isReviewRequired = true;
279
+ }
280
+ }
281
+ }
282
+ // Positive capability detection: helpers in the wild predate the review
283
+ // protocol and send no capabilities at all, so only an explicit
284
+ // commandReview: 1 opts into the review round-trip. Everything else falls
285
+ // through to direct execution, still behind the fail-closed host gates.
286
+ const hasCommandReview = this.ws.hasBrowserClient() && helperState.capabilities?.commandReview === 1;
287
+ if (isReviewRequired && hasCommandReview) {
288
+ const reviewId = `rev_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`;
289
+ const serverNonce = crypto.randomBytes(16).toString('hex');
290
+ const payloadHash = (0, canonical_js_1.computePayloadHash)(1, instanceOrigin, req.command, req.params);
291
+ this.ws.registerReview({
292
+ reviewId,
293
+ nonce: serverNonce,
294
+ payloadHash,
295
+ correlationId,
296
+ command: req.command,
297
+ params: req.params,
298
+ instanceOrigin,
299
+ });
300
+ this.pendingReviewRequests.set(correlationId, reviewId);
301
+ this.pendingReviewRequests.set(req.id, reviewId);
302
+ // Send Phase 1 reviewRequest to browser
303
+ const reviewTimeoutMs = 300_000; // 5 minutes
304
+ const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: reviewTimeoutMs });
305
+ this.ws.sendToBrowser({
306
+ action: 'reviewRequest',
307
+ reviewId,
308
+ nonce: serverNonce,
309
+ payloadHash,
310
+ reviewKind: policy.reviewKind,
311
+ risk: policy.risk,
312
+ command: req.command,
313
+ params: req.params,
314
+ instanceOrigin,
315
+ instance: inst.settings,
316
+ client: {
317
+ name: 'SN Utils CLI',
318
+ version: '0.1.0',
319
+ hostKind: 'standalone',
320
+ pid: process.pid,
321
+ },
322
+ expiresIn: Math.floor(reviewTimeoutMs / 1000),
323
+ agentRequestId: correlationId,
324
+ });
325
+ try {
326
+ const res = await pendingPromise;
327
+ // approvedNotExecuted: the helper approved but cannot run this
328
+ // command shape (bulk delete by query, REST DELETE, cascade) — fall
329
+ // through to the direct execution branches below.
330
+ if (res?.approvedNotExecuted !== true) {
331
+ return {
332
+ id: req.id,
333
+ command: req.command,
334
+ status: 'success',
335
+ timestamp: Date.now(),
336
+ result: res.output !== undefined ? { output: res.output } : res.data || res.result || { success: true },
337
+ };
338
+ }
339
+ }
340
+ finally {
341
+ this.pendingReviewRequests.delete(correlationId);
342
+ this.pendingReviewRequests.delete(req.id);
343
+ this.ws.cancelReview(reviewId, 'COMPLETED_OR_TIMED_OUT');
344
+ }
345
+ }
346
+ // 4. Standard Direct Execution (Unreviewed commands + legacy helper fallback)
347
+ // Background Script
348
+ if (req.command === 'run_background_script') {
349
+ const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: 70_000 });
350
+ this.ws.sendToBrowser({
351
+ action: 'agentRunBackgroundScript',
352
+ agentRequestId: correlationId,
353
+ script: req.params?.script,
354
+ instance: inst.settings,
355
+ appName: 'SN Utils CLI',
356
+ });
357
+ const res = await pendingPromise;
358
+ if (res.success === false) {
359
+ throw Object.assign(new Error(res.error || 'Background script failed'), { code: res.code || 'E_COMMAND_FAILED' });
360
+ }
361
+ return {
362
+ id: req.id,
363
+ command: req.command,
364
+ status: 'success',
365
+ timestamp: Date.now(),
366
+ result: { output: res.output || '' },
367
+ };
368
+ }
369
+ // Delete Record
370
+ if (req.command === 'delete_record') {
371
+ const { table, sys_id } = req.params || {};
372
+ const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: 70_000 });
373
+ this.ws.sendToBrowser({
374
+ action: 'agentRestApi',
375
+ agentRequestId: correlationId,
376
+ endpoint: `/api/now/table/${table}/${sys_id}`,
377
+ method: 'DELETE',
378
+ instance: inst.settings,
379
+ appName: 'SN Utils CLI',
380
+ });
381
+ const res = await pendingPromise;
382
+ if (res.success === false) {
383
+ throw Object.assign(new Error(res.error || 'Delete failed'), { code: res.code || 'E_COMMAND_FAILED' });
384
+ }
385
+ return {
386
+ id: req.id,
387
+ command: req.command,
388
+ status: 'success',
389
+ timestamp: Date.now(),
390
+ result: { deleted: true, table, sys_id },
391
+ };
392
+ }
393
+ // Code Search (GraphQL)
394
+ if (req.command === 'code_search') {
395
+ const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: 70_000 });
396
+ this.ws.sendToBrowser({
397
+ action: 'agentCodeSearch',
398
+ agentRequestId: correlationId,
399
+ term: req.params?.term,
400
+ tables: req.params?.tables,
401
+ limit: req.params?.limit ?? 50,
402
+ activeOnly: req.params?.activeOnly === true,
403
+ instance: inst.settings,
404
+ appName: 'SN Utils CLI',
405
+ });
406
+ const res = await pendingPromise;
407
+ if (res.success === false) {
408
+ throw Object.assign(new Error(res.error || 'Code search failed'), { code: res.code || 'E_COMMAND_FAILED' });
409
+ }
410
+ return {
411
+ id: req.id,
412
+ command: req.command,
413
+ status: 'success',
414
+ timestamp: Date.now(),
415
+ result: res,
416
+ };
417
+ }
418
+ // Query Records (Table API)
419
+ if (req.command === 'query_records') {
420
+ const table = req.params?.table;
421
+ const query = req.params?.query || '';
422
+ const fields = req.params?.fields || 'sys_id,number,short_description,sys_created_on';
423
+ const limit = req.params?.limit ?? 10;
424
+ const orderBy = req.params?.orderBy;
425
+ let sysparmQuery = query;
426
+ if (orderBy) {
427
+ sysparmQuery = sysparmQuery ? `${sysparmQuery}` + (sysparmQuery ? '^' : '') + `${orderBy}` : orderBy;
428
+ }
429
+ const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: 70_000 });
430
+ this.ws.sendToBrowser({
431
+ action: 'agentRestApi',
432
+ agentRequestId: correlationId,
433
+ endpoint: `/api/now/table/${table}`,
434
+ method: 'GET',
435
+ queryParams: {
436
+ sysparm_query: sysparmQuery,
437
+ sysparm_fields: fields,
438
+ sysparm_limit: String(limit),
439
+ sysparm_display_value: 'true',
440
+ },
441
+ instance: inst.settings,
442
+ appName: 'SN Utils CLI',
443
+ });
444
+ const res = await pendingPromise;
445
+ if (res.success === false) {
446
+ throw Object.assign(new Error(res.error || 'Query failed'), { code: res.status === 404 ? 'E_NOT_FOUND' : 'E_COMMAND_FAILED' });
447
+ }
448
+ const records = res.data?.result || [];
449
+ return {
450
+ id: req.id,
451
+ command: req.command,
452
+ status: 'success',
453
+ timestamp: Date.now(),
454
+ result: {
455
+ table,
456
+ count: records.length,
457
+ records,
458
+ },
459
+ };
460
+ }
461
+ // Get Record
462
+ if (req.command === 'get_record') {
463
+ const table = req.params?.table;
464
+ const sysId = req.params?.sys_id;
465
+ const fields = req.params?.fields;
466
+ const queryParams = { sysparm_display_value: 'true' };
467
+ if (fields)
468
+ queryParams.sysparm_fields = fields;
469
+ const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: 70_000 });
470
+ this.ws.sendToBrowser({
471
+ action: 'agentRestApi',
472
+ agentRequestId: correlationId,
473
+ endpoint: `/api/now/table/${table}/${sysId}`,
474
+ method: 'GET',
475
+ queryParams,
476
+ instance: inst.settings,
477
+ appName: 'SN Utils CLI',
478
+ });
479
+ const res = await pendingPromise;
480
+ if (res.success === false) {
481
+ throw Object.assign(new Error(res.error || `Record ${sysId} not found`), { code: res.status === 404 ? 'E_NOT_FOUND' : 'E_COMMAND_FAILED' });
482
+ }
483
+ return {
484
+ id: req.id,
485
+ command: req.command,
486
+ status: 'success',
487
+ timestamp: Date.now(),
488
+ result: {
489
+ table,
490
+ sys_id: sysId,
491
+ record: res.data?.result,
492
+ },
493
+ };
494
+ }
495
+ // Update Record
496
+ if (req.command === 'update_record') {
497
+ const { table, sys_id, field, content } = req.params || {};
498
+ const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: 70_000 });
499
+ this.ws.sendToBrowser({
500
+ action: 'agentRestApi',
501
+ agentRequestId: correlationId,
502
+ endpoint: `/api/now/table/${table}/${sys_id}`,
503
+ method: 'PATCH',
504
+ body: { [field]: content },
505
+ instance: inst.settings,
506
+ appName: 'SN Utils CLI',
507
+ });
508
+ const res = await pendingPromise;
509
+ if (res.success === false) {
510
+ throw Object.assign(new Error(res.error || 'Update failed'), { code: 'E_COMMAND_FAILED' });
511
+ }
512
+ return {
513
+ id: req.id,
514
+ command: req.command,
515
+ status: 'success',
516
+ timestamp: Date.now(),
517
+ result: {
518
+ updated: true,
519
+ table,
520
+ sys_id,
521
+ field,
522
+ record: res.data?.result,
523
+ },
524
+ };
525
+ }
526
+ // Create Artifact
527
+ if (req.command === 'create_artifact') {
528
+ const { table, fields } = req.params || {};
529
+ const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: 70_000 });
530
+ this.ws.sendToBrowser({
531
+ action: 'agentRestApi',
532
+ agentRequestId: correlationId,
533
+ endpoint: `/api/now/table/${table}`,
534
+ method: 'POST',
535
+ body: fields,
536
+ instance: inst.settings,
537
+ appName: 'SN Utils CLI',
538
+ });
539
+ const res = await pendingPromise;
540
+ if (res.success === false) {
541
+ throw Object.assign(new Error(res.error || 'Create artifact failed'), { code: 'E_COMMAND_FAILED' });
542
+ }
543
+ return {
544
+ id: req.id,
545
+ command: req.command,
546
+ status: 'success',
547
+ timestamp: Date.now(),
548
+ result: {
549
+ created: true,
550
+ table,
551
+ name: fields?.name,
552
+ sys_id: res.data?.result?.sys_id,
553
+ record: res.data?.result,
554
+ },
555
+ };
556
+ }
557
+ // Schema Metadata
558
+ if (req.command === 'get_table_metadata') {
559
+ const table = req.params?.table;
560
+ const pendingPromise = this.pending.register({ id: correlationId, command: req.command });
561
+ this.ws.sendToBrowser({
562
+ action: 'agentRestApi',
563
+ agentRequestId: correlationId,
564
+ endpoint: `/api/now/doc/table/schema/${table}`,
565
+ method: 'GET',
566
+ instance: inst.settings,
567
+ appName: 'SN Utils CLI',
568
+ });
569
+ const res = await pendingPromise;
570
+ if (res.success === false) {
571
+ throw Object.assign(new Error(res.error || `Schema for table ${table} not found`), { code: 'E_NOT_FOUND' });
572
+ }
573
+ return {
574
+ id: req.id,
575
+ command: req.command,
576
+ status: 'success',
577
+ timestamp: Date.now(),
578
+ result: res.data?.result || res.data,
579
+ };
580
+ }
581
+ // Browser Form & UI Actions
582
+ if (['get_form_state', 'set_field', 'run_ui_action', 'navigate', 'take_screenshot'].includes(req.command)) {
583
+ const actionMap = {
584
+ get_form_state: 'agentGetFormState',
585
+ set_field: 'agentSetField',
586
+ run_ui_action: 'agentRunUiAction',
587
+ navigate: 'agentNavigate',
588
+ take_screenshot: 'agentTakeScreenshot',
589
+ };
590
+ const action = actionMap[req.command];
591
+ const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: 70_000 });
592
+ this.ws.sendToBrowser({
593
+ action,
594
+ agentRequestId: correlationId,
595
+ ...req.params,
596
+ appName: 'SN Utils CLI',
597
+ });
598
+ const res = await pendingPromise;
599
+ if (res.success === false) {
600
+ throw Object.assign(new Error(res.error || `Browser command ${req.command} failed`), { code: res.code || 'E_COMMAND_FAILED' });
601
+ }
602
+ return {
603
+ id: req.id,
604
+ command: req.command,
605
+ status: 'success',
606
+ timestamp: Date.now(),
607
+ result: res,
608
+ };
609
+ }
610
+ throw Object.assign(new Error(`Unknown command: ${req.command}`), { code: 'E_UNKNOWN_COMMAND' });
611
+ }
612
+ catch (err) {
613
+ this.pendingReviewRequests.delete(correlationId);
614
+ this.pendingReviewRequests.delete(req.id);
615
+ return {
616
+ id: req.id,
617
+ command: req.command,
618
+ status: 'error',
619
+ error: err.message || String(err),
620
+ code: err.code || 'E_COMMAND_FAILED',
621
+ timestamp: Date.now(),
622
+ details: err.details,
623
+ };
624
+ }
625
+ finally {
626
+ this.requestIdToCorrelationId.delete(req.id);
627
+ }
628
+ }
629
+ }
630
+ exports.StandaloneDispatcher = StandaloneDispatcher;
631
+ //# sourceMappingURL=dispatcher.js.map