@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.
- package/README.md +156 -0
- package/bin/snu.js +8 -0
- package/dist/cli/format.d.ts +14 -0
- package/dist/cli/format.d.ts.map +1 -0
- package/dist/cli/format.js +231 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +296 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/stdin.d.ts +14 -0
- package/dist/cli/stdin.d.ts.map +1 -0
- package/dist/cli/stdin.js +75 -0
- package/dist/cli/stdin.js.map +1 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +350 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +185 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/registry.d.ts +5 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +527 -0
- package/dist/registry.js.map +1 -0
- package/dist/server/canonical.d.ts +10 -0
- package/dist/server/canonical.d.ts.map +1 -0
- package/dist/server/canonical.js +72 -0
- package/dist/server/canonical.js.map +1 -0
- package/dist/server/config.d.ts +9 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +117 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/dispatcher.d.ts +32 -0
- package/dist/server/dispatcher.d.ts.map +1 -0
- package/dist/server/dispatcher.js +631 -0
- package/dist/server/dispatcher.js.map +1 -0
- package/dist/server/httpBridge.d.ts +20 -0
- package/dist/server/httpBridge.d.ts.map +1 -0
- package/dist/server/httpBridge.js +219 -0
- package/dist/server/httpBridge.js.map +1 -0
- package/dist/server/pendingRegistry.d.ts +26 -0
- package/dist/server/pendingRegistry.d.ts.map +1 -0
- package/dist/server/pendingRegistry.js +76 -0
- package/dist/server/pendingRegistry.js.map +1 -0
- package/dist/server/policy.d.ts +18 -0
- package/dist/server/policy.d.ts.map +1 -0
- package/dist/server/policy.js +53 -0
- package/dist/server/policy.js.map +1 -0
- package/dist/server/standalone.d.ts +24 -0
- package/dist/server/standalone.d.ts.map +1 -0
- package/dist/server/standalone.js +152 -0
- package/dist/server/standalone.js.map +1 -0
- package/dist/server/wsBridge.d.ts +45 -0
- package/dist/server/wsBridge.d.ts.map +1 -0
- package/dist/server/wsBridge.js +321 -0
- package/dist/server/wsBridge.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- 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
|