nowaikit 4.12.0 → 4.15.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 (51) hide show
  1. package/README.md +12 -5
  2. package/dist/cli/auth.d.ts +13 -0
  3. package/dist/cli/auth.d.ts.map +1 -1
  4. package/dist/cli/auth.js +237 -50
  5. package/dist/cli/auth.js.map +1 -1
  6. package/dist/cli/detect-clients.d.ts.map +1 -1
  7. package/dist/cli/detect-clients.js +78 -2
  8. package/dist/cli/detect-clients.js.map +1 -1
  9. package/dist/cli/setup.d.ts.map +1 -1
  10. package/dist/cli/setup.js +218 -9
  11. package/dist/cli/setup.js.map +1 -1
  12. package/dist/servicenow/client.d.ts +49 -1
  13. package/dist/servicenow/client.d.ts.map +1 -1
  14. package/dist/servicenow/client.js +367 -46
  15. package/dist/servicenow/client.js.map +1 -1
  16. package/dist/servicenow/instances.d.ts.map +1 -1
  17. package/dist/servicenow/instances.js +56 -4
  18. package/dist/servicenow/instances.js.map +1 -1
  19. package/dist/servicenow/types.d.ts +17 -0
  20. package/dist/servicenow/types.d.ts.map +1 -1
  21. package/dist/tools/app-studio.d.ts.map +1 -1
  22. package/dist/tools/app-studio.js +80 -9
  23. package/dist/tools/app-studio.js.map +1 -1
  24. package/dist/tools/change.d.ts.map +1 -1
  25. package/dist/tools/change.js +3 -1
  26. package/dist/tools/change.js.map +1 -1
  27. package/dist/tools/core.d.ts.map +1 -1
  28. package/dist/tools/core.js +28 -2
  29. package/dist/tools/core.js.map +1 -1
  30. package/dist/tools/incident.d.ts.map +1 -1
  31. package/dist/tools/incident.js +5 -3
  32. package/dist/tools/incident.js.map +1 -1
  33. package/dist/tools/index.d.ts +159 -0
  34. package/dist/tools/index.d.ts.map +1 -1
  35. package/dist/tools/index.js +2 -2
  36. package/dist/tools/index.js.map +1 -1
  37. package/dist/tools/knowledge.d.ts.map +1 -1
  38. package/dist/tools/knowledge.js +12 -1
  39. package/dist/tools/knowledge.js.map +1 -1
  40. package/dist/tools/notification.d.ts +169 -0
  41. package/dist/tools/notification.d.ts.map +1 -1
  42. package/dist/tools/notification.js +88 -9
  43. package/dist/tools/notification.js.map +1 -1
  44. package/dist/tools/problem.d.ts.map +1 -1
  45. package/dist/tools/problem.js +3 -1
  46. package/dist/tools/problem.js.map +1 -1
  47. package/dist/tools/updateset.d.ts.map +1 -1
  48. package/dist/tools/updateset.js +62 -38
  49. package/dist/tools/updateset.js.map +1 -1
  50. package/dist/tools-manifest.json +82 -11
  51. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/servicenow/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/servicenow/client.ts"],"names":[],"mappings":"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"}
@@ -28,16 +28,45 @@ export function basicAuthDiagnostic() {
28
28
  */
29
29
  export function forbiddenDiagnostic(authMethod) {
30
30
  const lines = [
31
- 'Authenticated, but not authorized for this operation (HTTP 403). Common causes:',
32
- ' 1. The account is missing the roles this needs (e.g. itil for ITSM, or admin for config tables).',
31
+ 'Authenticated, but not authorized (HTTP 403). If ServiceNow returned a specific reason above',
32
+ '(e.g. a "Data Policy Exception" or an ACL detail), that IS the actual cause — read it first.',
33
+ 'Otherwise, common causes are:',
34
+ ' 1. A data policy or field-level ACL is rejecting a field in the request (e.g. a conditionally',
35
+ ' mandatory or read-only field). This can fail some requests and succeed on others depending on',
36
+ ' the exact fields sent, even for the same user.',
37
+ ' 2. The account is missing a role this needs (e.g. itil for ITSM, admin for config tables).',
33
38
  ];
34
39
  if (authMethod === 'oauth') {
35
- lines.push(' 2. Your OAuth app/token is missing API scope. In the OAuth application registry, grant the', ' required scope (e.g. "useraccount"), and make sure the token\'s user actually holds the roles.');
40
+ lines.push(' 3. Your OAuth app/token is missing API scope. In the OAuth application registry, grant the', ' required scope (e.g. "useraccount"), and make sure the token\'s user actually holds the roles.');
36
41
  }
37
- lines.push(' 3. Writes need WRITE_ENABLED=true AND the user\'s write roles; some tools also need scripting/CMDB/ATF flags.', ' 4. An ACL on the specific table may be denying access even with the role.');
42
+ lines.push(' 4. Writes need WRITE_ENABLED=true AND the user\'s write roles; some tools also need scripting/CMDB/ATF flags.');
38
43
  return lines.join('\n');
39
44
  }
40
45
  // ─── Input validation helpers ────────────────────────────────────────────────
46
+ /** Best-effort MIME type from a file extension, for attachments fetched without a content type. */
47
+ function guessContentType(fileName) {
48
+ const ext = (fileName.split('.').pop() || '').toLowerCase();
49
+ const map = {
50
+ pdf: 'application/pdf',
51
+ doc: 'application/msword',
52
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
53
+ xls: 'application/vnd.ms-excel',
54
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
55
+ ppt: 'application/vnd.ms-powerpoint',
56
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
57
+ csv: 'text/csv',
58
+ txt: 'text/plain',
59
+ json: 'application/json',
60
+ xml: 'application/xml',
61
+ html: 'text/html',
62
+ png: 'image/png',
63
+ jpg: 'image/jpeg',
64
+ jpeg: 'image/jpeg',
65
+ gif: 'image/gif',
66
+ zip: 'application/zip',
67
+ };
68
+ return map[ext] || 'application/octet-stream';
69
+ }
41
70
  /** Validate and sanitize ServiceNow table names (alphanumeric + underscores only) */
42
71
  function validateTableName(table) {
43
72
  if (!table || !/^[a-zA-Z][a-zA-Z0-9_]*$/.test(table)) {
@@ -86,6 +115,10 @@ export class ServiceNowClient {
86
115
  impersonateUserSysId;
87
116
  /** For per-user mode: pre-loaded token overrides service-account auth */
88
117
  perUserBearerToken;
118
+ /** Local per-user OAuth only: refresh token + expiry + persist callback (unset for the gateway). */
119
+ perUserRefreshToken;
120
+ perUserTokenExpiry;
121
+ onTokenRefreshed;
89
122
  accessToken;
90
123
  tokenExpiry;
91
124
  constructor(config) {
@@ -99,6 +132,49 @@ export class ServiceNowClient {
99
132
  this.requestTimeoutMs = config.requestTimeoutMs || 30000;
100
133
  this.impersonateUserSysId = config.impersonateUserSysId;
101
134
  this.perUserBearerToken = config.perUserBearerToken;
135
+ this.perUserRefreshToken = config.perUserRefreshToken;
136
+ this.perUserTokenExpiry = config.perUserTokenExpiry;
137
+ this.onTokenRefreshed = config.onTokenRefreshed;
138
+ }
139
+ /**
140
+ * True only for LOCAL per-user OAuth that owns a refresh token + client id (i.e. can renew its
141
+ * own token). The multi-tenant gateway's per-user mode has no refresh token, so this is false
142
+ * there and the injected token is used as-is — the gateway path is unchanged.
143
+ */
144
+ hasPerUserRefresh() {
145
+ return this.authMode === 'per-user' && !!this.perUserRefreshToken && !!this.oauthConfig?.clientId;
146
+ }
147
+ /** Renew the per-user access token using the stored refresh token (refresh_token grant). */
148
+ async refreshPerUserToken() {
149
+ if (!this.perUserRefreshToken || !this.oauthConfig?.clientId) {
150
+ throw new ServiceNowError('Cannot refresh: no refresh token or OAuth client configured', 'AUTHENTICATION_FAILED');
151
+ }
152
+ const body = new URLSearchParams({
153
+ grant_type: 'refresh_token',
154
+ client_id: this.oauthConfig.clientId,
155
+ refresh_token: this.perUserRefreshToken,
156
+ });
157
+ // Confidential clients send the secret; public (PKCE) clients omit it.
158
+ if (this.oauthConfig.clientSecret)
159
+ body.set('client_secret', this.oauthConfig.clientSecret);
160
+ const resp = await fetch(`${this.baseUrl}/oauth_token.do`, {
161
+ method: 'POST',
162
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
163
+ body: body.toString(),
164
+ });
165
+ if (!resp.ok) {
166
+ throw new ServiceNowError(`OAuth token refresh failed: ${resp.status} ${resp.statusText}`, 'AUTHENTICATION_FAILED');
167
+ }
168
+ const data = await resp.json();
169
+ this.perUserBearerToken = data.access_token;
170
+ if (data.refresh_token)
171
+ this.perUserRefreshToken = data.refresh_token; // honour rotation
172
+ const expiresAt = Date.now() + (data.expires_in ? data.expires_in * 1000 * 0.9 : 25 * 60 * 1000);
173
+ this.perUserTokenExpiry = expiresAt;
174
+ try {
175
+ this.onTokenRefreshed?.({ accessToken: this.perUserBearerToken, refreshToken: this.perUserRefreshToken, expiresAt });
176
+ }
177
+ catch { /* persistence is best-effort; never fail the request over it */ }
102
178
  }
103
179
  /**
104
180
  * The ServiceNow instance host this client targets (e.g. "acme.service-now.com").
@@ -112,6 +188,56 @@ export class ServiceNowClient {
112
188
  return this.baseUrl.toLowerCase();
113
189
  }
114
190
  }
191
+ /** Public accessor for the resolved instance base URL (no trailing slash), for building UI links. */
192
+ get instanceUrl() {
193
+ return this.baseUrl;
194
+ }
195
+ /** Human-facing record URL (opens the form in ServiceNow), not the /api/now REST link. */
196
+ recordUrl(table, sysId) {
197
+ return `${this.baseUrl}/${encodeURIComponent(table)}.do?sys_id=${encodeURIComponent(sysId)}`;
198
+ }
199
+ /**
200
+ * Return the fields that ServiceNow Data Policies make mandatory on a table, so an agent can
201
+ * collect them BEFORE a create/update and avoid the "Data Policy Exception: mandatory fields"
202
+ * error. Reads sys_data_policy_rule (the mandatory rules) and sys_data_policy2 (the condition
203
+ * under which each applies). All server-side, read-only.
204
+ */
205
+ async getMandatoryFields(table) {
206
+ const t = validateTableName(table);
207
+ const rules = await this.queryRecords({
208
+ table: 'sys_data_policy_rule',
209
+ query: `table=${t}^mandatory=true`,
210
+ fields: 'field,sys_data_policy',
211
+ limit: 100,
212
+ });
213
+ const rows = rules.records ?? rules ?? [];
214
+ // Resolve the parent policy conditions once.
215
+ const policyIds = Array.from(new Set(rows.map((r) => (typeof r.sys_data_policy === 'object' ? r.sys_data_policy?.value : r.sys_data_policy)).filter(Boolean)));
216
+ const condByPolicy = new Map();
217
+ if (policyIds.length) {
218
+ const pol = await this.queryRecords({
219
+ table: 'sys_data_policy2',
220
+ query: `sys_idIN${policyIds.join(',')}`,
221
+ fields: 'sys_id,short_description,conditions,active',
222
+ limit: 100,
223
+ });
224
+ for (const p of (pol.records ?? pol ?? [])) {
225
+ condByPolicy.set(p.sys_id, { desc: p.short_description, conditions: p.conditions, active: p.active });
226
+ }
227
+ }
228
+ const fields = rows.map((r) => {
229
+ const pid = typeof r.sys_data_policy === 'object' ? r.sys_data_policy?.value : r.sys_data_policy;
230
+ const meta = condByPolicy.get(pid) || {};
231
+ return { field: r.field, applies_when: meta.conditions || 'always', policy: meta.desc, active: meta.active };
232
+ }).filter((f) => f.active !== 'false');
233
+ return {
234
+ table: t,
235
+ mandatory_fields: fields,
236
+ summary: fields.length
237
+ ? `On ${t}, these fields are made mandatory by data policy: ${fields.map((f) => f.field).join(', ')}. Collect them before creating or updating.`
238
+ : `No data-policy mandatory fields found on ${t} (dictionary-level mandatory fields may still apply).`,
239
+ };
240
+ }
115
241
  /**
116
242
  * Return a copy of this client configured to run as a specific user.
117
243
  * Used for per-request user context switching without mutating the shared client.
@@ -149,6 +275,11 @@ export class ServiceNowClient {
149
275
  if (options.bearerToken) {
150
276
  copy.authMode = 'per-user';
151
277
  copy.perUserBearerToken = options.bearerToken;
278
+ // A per-request injected token (the gateway path) is not ours to refresh — never carry over
279
+ // any refresh state from the base client. Keeps hasPerUserRefresh() false for the gateway.
280
+ copy.perUserRefreshToken = undefined;
281
+ copy.perUserTokenExpiry = undefined;
282
+ copy.onTokenRefreshed = undefined;
152
283
  }
153
284
  return copy;
154
285
  }
@@ -159,6 +290,11 @@ export class ServiceNowClient {
159
290
  // Per-user mode carries the user's own bearer token (delegated auth) —
160
291
  // no service-account token acquisition needed.
161
292
  if (this.authMode === 'per-user' && this.perUserBearerToken) {
293
+ // Local stored-session OAuth: proactively refresh a token that is expired or within 60s of it.
294
+ // The gateway path has no refresh token (hasPerUserRefresh() === false), so it is untouched.
295
+ if (this.hasPerUserRefresh() && this.perUserTokenExpiry && Date.now() >= this.perUserTokenExpiry - 60_000) {
296
+ await this.refreshPerUserToken();
297
+ }
162
298
  return;
163
299
  }
164
300
  if (this.authMethod === 'basic') {
@@ -277,6 +413,11 @@ export class ServiceNowClient {
277
413
  const errorJson = JSON.parse(errorText);
278
414
  if (errorJson.error?.message) {
279
415
  errorMessage = errorJson.error.message;
416
+ // ServiceNow puts the ACTUAL reason (data policy, ACL, mandatory/read-only field) in
417
+ // `detail`; `message` is often just the generic "Operation Failed". Surface both.
418
+ if (errorJson.error.detail) {
419
+ errorMessage += `: ${String(errorJson.error.detail).replace(/\s+/g, ' ').trim()}`;
420
+ }
280
421
  }
281
422
  }
282
423
  catch {
@@ -298,6 +439,17 @@ export class ServiceNowClient {
298
439
  catch { /* fall through to the throw below */ }
299
440
  continue;
300
441
  }
442
+ // Local per-user OAuth that owns a refresh token: renew once and retry. The multi-tenant
443
+ // gateway's injected-token per-user mode has NO refresh token, so hasPerUserRefresh() is
444
+ // false there and this is skipped — the live Copilot path is unchanged.
445
+ if (this.hasPerUserRefresh() && !reauthed) {
446
+ reauthed = true;
447
+ try {
448
+ await this.refreshPerUserToken();
449
+ }
450
+ catch { /* fall through to the throw below */ }
451
+ continue;
452
+ }
301
453
  // Attach the actionable checklist so a bare 401 isn't a dead end.
302
454
  if (this.authMethod === 'basic') {
303
455
  errorMessage = `${errorMessage}\n\n${basicAuthDiagnostic()}`;
@@ -322,7 +474,9 @@ export class ServiceNowClient {
322
474
  lastError = error instanceof Error ? error : new Error('Unknown error');
323
475
  // Don't retry on auth errors or invalid requests
324
476
  if (error instanceof ServiceNowError) {
325
- if (['AUTHENTICATION_FAILED', 'INVALID_REQUEST', 'NOT_FOUND'].includes(error.code)) {
477
+ // 403 is not transient (a role/ACL/data-policy denial won't clear in 4s), so fail fast
478
+ // instead of burning ~7s on retries.
479
+ if (['AUTHENTICATION_FAILED', 'INVALID_REQUEST', 'NOT_FOUND', 'INSUFFICIENT_PRIVILEGES'].includes(error.code)) {
326
480
  throw error;
327
481
  }
328
482
  }
@@ -363,6 +517,11 @@ export class ServiceNowClient {
363
517
  if (params.fields) {
364
518
  queryParams.set('sysparm_fields', params.fields);
365
519
  }
520
+ // Return reference fields as readable names, not raw sys_ids. 'all' keeps the sys_id too
521
+ // (as {display_value, value}) so the agent can still act on the record.
522
+ if (params.displayValue) {
523
+ queryParams.set('sysparm_display_value', params.displayValue === 'all' ? 'all' : 'true');
524
+ }
366
525
  if (params.limit !== undefined && params.limit > 0) {
367
526
  queryParams.set('sysparm_limit', Math.min(params.limit, 1000).toString());
368
527
  }
@@ -453,7 +612,7 @@ export class ServiceNowClient {
453
612
  /**
454
613
  * Get a single record by sys_id
455
614
  */
456
- async getRecord(table, sysId, fields) {
615
+ async getRecord(table, sysId, fields, displayValue) {
457
616
  validateTableName(table);
458
617
  validateSysId(sysId);
459
618
  await this.authenticate();
@@ -461,6 +620,9 @@ export class ServiceNowClient {
461
620
  if (fields) {
462
621
  queryParams.set('sysparm_fields', fields);
463
622
  }
623
+ if (displayValue) {
624
+ queryParams.set('sysparm_display_value', displayValue === 'all' ? 'all' : 'true');
625
+ }
464
626
  const url = `${this.baseUrl}/api/now/table/${table}/${sysId}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
465
627
  logger.info(`Getting record from ${table}: ${sysId}`);
466
628
  try {
@@ -971,12 +1133,73 @@ export class ServiceNowClient {
971
1133
  * Accepts base64-encoded content and uploads it as a multipart form.
972
1134
  */
973
1135
  async uploadAttachment(table, recordSysId, fileName, contentType, contentBase64) {
1136
+ // Decode base64 to binary, then hand off to the shared binary uploader.
1137
+ const binary = Buffer.from(contentBase64, 'base64');
1138
+ return this.uploadAttachmentBuffer(table, recordSysId, fileName, contentType, binary);
1139
+ }
1140
+ /**
1141
+ * Attach a file to a record by fetching its bytes server-side from a URL, so the file content
1142
+ * never has to travel through the LLM/tool call as a base64 argument. This is the reliable path
1143
+ * for anything larger than a few hundred KB. `sourceHeaders` lets the caller pass auth (e.g. a
1144
+ * bearer/basic credential) for any protected URL. Content type is taken from the caller, then
1145
+ * the response's Content-Type, then guessed from the file extension.
1146
+ */
1147
+ async uploadAttachmentFromUrl(table, recordSysId, fileName, sourceUrl, contentType, sourceHeaders) {
1148
+ const MAX_FETCH_BYTES = 50 * 1024 * 1024; // 50MB safety cap on server-side fetch
1149
+ let src;
1150
+ try {
1151
+ src = new URL(sourceUrl);
1152
+ }
1153
+ catch {
1154
+ throw new ServiceNowError(`source_url is not a valid URL: ${sourceUrl}`, 'INVALID_REQUEST');
1155
+ }
1156
+ if (src.protocol !== 'https:' && src.protocol !== 'http:') {
1157
+ throw new ServiceNowError(`source_url must be http(s), got ${src.protocol}`, 'INVALID_REQUEST');
1158
+ }
1159
+ logger.info(`Fetching attachment "${fileName}" from source URL for ${table}:${recordSysId}`);
1160
+ let fetched;
1161
+ try {
1162
+ fetched = await fetch(src, { headers: sourceHeaders ?? {} });
1163
+ }
1164
+ catch (error) {
1165
+ throw new ServiceNowError(`Failed to fetch source_url: ${error instanceof Error ? error.message : 'Unknown error'}`, 'ATTACHMENT_SOURCE_FETCH_FAILED');
1166
+ }
1167
+ if (!fetched.ok) {
1168
+ throw new ServiceNowError(`source_url returned HTTP ${fetched.status} ${fetched.statusText}`, 'ATTACHMENT_SOURCE_FETCH_FAILED');
1169
+ }
1170
+ const arrayBuf = await fetched.arrayBuffer();
1171
+ const binary = Buffer.from(arrayBuf);
1172
+ if (binary.length > MAX_FETCH_BYTES) {
1173
+ throw new ServiceNowError(`Fetched file is ${(binary.length / 1024 / 1024).toFixed(1)}MB, over the ${MAX_FETCH_BYTES / 1024 / 1024}MB limit`, 'ATTACHMENT_TOO_LARGE');
1174
+ }
1175
+ const responseType = fetched.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() || '';
1176
+ const resolvedType = contentType || responseType || guessContentType(fileName);
1177
+ // Phantom-attachment guard: a protected/expired link (e.g. SharePoint) often returns a tiny
1178
+ // HTML/JSON error page with HTTP 200. Uploading that as "document.pdf" creates an empty, broken
1179
+ // attachment that looks successful. Reject the obvious cases instead of attaching junk.
1180
+ const expectedType = (contentType || guessContentType(fileName)).toLowerCase();
1181
+ const expectsBinary = !expectedType.startsWith('text/') && !/json|xml|csv|html/.test(expectedType);
1182
+ const looksLikeErrorPage = /text\/html|application\/json|application\/xml/.test(responseType);
1183
+ const head = binary.subarray(0, 5).toString('latin1');
1184
+ const magicOk = !expectedType.includes('pdf') ? true : head.startsWith('%PDF'); // PDFs must start with %PDF
1185
+ if (binary.length < 100 && expectsBinary) {
1186
+ throw new ServiceNowError(`source_url returned only ${binary.length} bytes for "${fileName}" — this is almost certainly an error/redirect page, not the file. ` +
1187
+ 'The link is likely protected or expired (for SharePoint, use the pre-authorised @microsoft.graph.downloadUrl). Nothing was attached.', 'ATTACHMENT_SOURCE_NOT_A_FILE');
1188
+ }
1189
+ if (expectsBinary && looksLikeErrorPage) {
1190
+ throw new ServiceNowError(`source_url returned ${responseType} but "${fileName}" was expected to be a binary file — this looks like an error page, not the document. Nothing was attached.`, 'ATTACHMENT_SOURCE_NOT_A_FILE');
1191
+ }
1192
+ if (!magicOk) {
1193
+ throw new ServiceNowError(`source_url content is not a valid PDF (missing %PDF header) for "${fileName}". The link likely returned an error page. Nothing was attached.`, 'ATTACHMENT_SOURCE_NOT_A_FILE');
1194
+ }
1195
+ return this.uploadAttachmentBuffer(table, recordSysId, fileName, resolvedType, binary);
1196
+ }
1197
+ /** Shared core: POST raw bytes to ServiceNow's native binary attachment endpoint. */
1198
+ async uploadAttachmentBuffer(table, recordSysId, fileName, contentType, binary) {
974
1199
  await this.authenticate();
975
1200
  const url = `${this.baseUrl}/api/now/attachment/file?table_name=${encodeURIComponent(table)}&table_sys_id=${encodeURIComponent(recordSysId)}&file_name=${encodeURIComponent(fileName)}`;
976
- logger.info(`Uploading attachment "${fileName}" to ${table}:${recordSysId}`);
1201
+ logger.info(`Uploading attachment "${fileName}" (${binary.length} bytes) to ${table}:${recordSysId}`);
977
1202
  try {
978
- // Decode base64 to binary
979
- const binary = Buffer.from(contentBase64, 'base64');
980
1203
  const response = await fetch(url, {
981
1204
  method: 'POST',
982
1205
  headers: {
@@ -991,8 +1214,11 @@ export class ServiceNowClient {
991
1214
  let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
992
1215
  try {
993
1216
  const errorJson = JSON.parse(errorText);
994
- if (errorJson.error?.message)
1217
+ if (errorJson.error?.message) {
995
1218
  errorMessage = errorJson.error.message;
1219
+ if (errorJson.error.detail)
1220
+ errorMessage += `: ${String(errorJson.error.detail).replace(/\s+/g, ' ').trim()}`;
1221
+ }
996
1222
  }
997
1223
  catch {
998
1224
  // ignore parse error
@@ -1008,6 +1234,76 @@ export class ServiceNowClient {
1008
1234
  throw new ServiceNowError(`Failed to upload attachment: ${error instanceof Error ? error.message : 'Unknown error'}`, 'ATTACHMENT_UPLOAD_FAILED');
1009
1235
  }
1010
1236
  }
1237
+ /** Fetch an existing attachment's metadata + raw bytes, server-side (no LLM involvement). */
1238
+ async fetchAttachment(attachmentSysId) {
1239
+ await this.authenticate();
1240
+ const id = validateSysId(attachmentSysId);
1241
+ // Metadata (file_name, content_type, size) via the attachment record.
1242
+ const metaRes = await fetch(`${this.baseUrl}/api/now/attachment/${encodeURIComponent(id)}`, {
1243
+ headers: { Authorization: this.getAuthHeader(), Accept: 'application/json' },
1244
+ });
1245
+ if (!metaRes.ok) {
1246
+ throw new ServiceNowError(metaRes.status === 404 ? `Attachment not found: ${id}` : `HTTP ${metaRes.status}: ${metaRes.statusText}`, metaRes.status === 404 ? 'NOT_FOUND' : 'ATTACHMENT_READ_FAILED');
1247
+ }
1248
+ const meta = (await metaRes.json()).result ?? {};
1249
+ // Raw bytes via the binary file endpoint.
1250
+ const fileRes = await fetch(`${this.baseUrl}/api/now/attachment/${encodeURIComponent(id)}/file`, {
1251
+ headers: { Authorization: this.getAuthHeader() },
1252
+ });
1253
+ if (!fileRes.ok) {
1254
+ throw new ServiceNowError(`HTTP ${fileRes.status}: ${fileRes.statusText}`, 'ATTACHMENT_READ_FAILED');
1255
+ }
1256
+ const bytes = Buffer.from(await fileRes.arrayBuffer());
1257
+ return { meta, bytes };
1258
+ }
1259
+ /**
1260
+ * Copy an existing ServiceNow attachment onto another record, entirely server-side. The bytes
1261
+ * are fetched from the source attachment and re-posted to the target, so nothing passes through
1262
+ * the LLM/tool call. Great for "attach the same document from the RITM onto the KB".
1263
+ */
1264
+ async copyAttachment(attachmentSysId, targetTable, targetRecordSysId, newFileName) {
1265
+ const { meta, bytes } = await this.fetchAttachment(attachmentSysId);
1266
+ const fileName = newFileName || meta.file_name || `attachment-${attachmentSysId}`;
1267
+ const contentType = meta.content_type || guessContentType(fileName);
1268
+ logger.info(`Copying attachment ${attachmentSysId} (${bytes.length} bytes) to ${targetTable}:${targetRecordSysId}`);
1269
+ return this.uploadAttachmentBuffer(targetTable, targetRecordSysId, fileName, contentType, bytes);
1270
+ }
1271
+ /**
1272
+ * Read an existing ServiceNow attachment and return its text content when the file is text-based
1273
+ * (txt/csv/json/xml/html/md). For binary formats (PDF/DOCX/images) it returns metadata plus a note
1274
+ * rather than dumping bytes, since those need a dedicated extractor. All server-side.
1275
+ */
1276
+ async readAttachment(attachmentSysId, maxChars = 200_000) {
1277
+ const { meta, bytes } = await this.fetchAttachment(attachmentSysId);
1278
+ const ct = String(meta.content_type || guessContentType(meta.file_name || '')).toLowerCase();
1279
+ const name = String(meta.file_name || '').toLowerCase();
1280
+ const isTextual = ct.startsWith('text/') ||
1281
+ /json|xml|csv|html|yaml|markdown|javascript|x-www-form-urlencoded/.test(ct) ||
1282
+ /\.(txt|csv|json|xml|html?|md|log|yaml|yml|tsv)$/.test(name);
1283
+ const base = {
1284
+ attachment_sys_id: attachmentSysId,
1285
+ file_name: meta.file_name,
1286
+ content_type: meta.content_type,
1287
+ size_bytes: bytes.length,
1288
+ };
1289
+ if (isTextual) {
1290
+ const text = bytes.toString('utf8');
1291
+ const truncated = text.length > maxChars;
1292
+ return {
1293
+ ...base,
1294
+ text: truncated ? text.slice(0, maxChars) : text,
1295
+ truncated,
1296
+ summary: `Read ${bytes.length} bytes of text from "${meta.file_name}"${truncated ? ` (truncated to ${maxChars} chars)` : ''}`,
1297
+ };
1298
+ }
1299
+ return {
1300
+ ...base,
1301
+ text: null,
1302
+ extractable: false,
1303
+ note: `"${meta.file_name}" is a binary format (${meta.content_type}) that needs a dedicated text extractor. ` +
1304
+ 'Use copy_attachment to move it between records, or process the file where it was uploaded.',
1305
+ };
1306
+ }
1011
1307
  /**
1012
1308
  * Execute multiple REST API operations in a single HTTP call (Batch API).
1013
1309
  * Uses /api/now/v1/batch endpoint. Up to 50 operations per batch.
@@ -1070,50 +1366,75 @@ export class ServiceNowClient {
1070
1366
  */
1071
1367
  async executeScript(script, scope) {
1072
1368
  await this.authenticate();
1073
- logger.info('Executing server-side script');
1074
- try {
1075
- // Use the standard script execution endpoint
1076
- const response = await this.request(`${this.baseUrl}/api/now/v1/batch`, {
1369
+ // ServiceNow has NO supported REST endpoint for running arbitrary background scripts. The only
1370
+ // reliable server-side path is an optional helper the customer installs and reviews: a scoped,
1371
+ // secured Scripted REST API. Point us at it with SCRIPT_EXEC_ENDPOINT (e.g. "/api/x_nowaikit/exec")
1372
+ // and this executes through it; otherwise we fail with a clear, actionable message rather than
1373
+ // silently hitting a non-existent endpoint (the previous implementation targeted a bogus
1374
+ // sys_script_execution table wrapped in a malformed Batch request and never worked).
1375
+ // 1) Optional customer-installed helper (a scoped, secured Scripted REST API) if configured.
1376
+ const endpoint = process.env.SCRIPT_EXEC_ENDPOINT;
1377
+ if (endpoint) {
1378
+ logger.info('Executing server-side script via SCRIPT_EXEC_ENDPOINT');
1379
+ const url = `${this.baseUrl}${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`;
1380
+ const response = await this.request(url, {
1077
1381
  method: 'POST',
1078
- body: JSON.stringify({
1079
- batch_request_id: `script_${Date.now()}`,
1080
- rest_requests: [{
1081
- id: 'script_exec',
1082
- method: 'POST',
1083
- url: '/api/now/table/sys_script_execution',
1084
- headers: [
1085
- { name: 'Content-Type', value: 'application/json' },
1086
- { name: 'Accept', value: 'application/json' },
1087
- ],
1088
- body: JSON.stringify({
1089
- script,
1090
- scope: scope || 'global',
1091
- }),
1092
- }],
1093
- }),
1382
+ body: JSON.stringify({ script, scope: scope || 'global' }),
1094
1383
  });
1095
- const results = response.serviced_requests || [];
1096
- if (results.length > 0) {
1097
- let body;
1384
+ return { status: 200, output: (response && response.result !== undefined) ? response.result : response, scope: scope || 'global' };
1385
+ }
1386
+ // 2) Default (no install): run via a one-time, self-terminating scheduled job. ServiceNow has no
1387
+ // REST endpoint to run background scripts, but the SCHEDULER is a separate path — and the job record
1388
+ // is created with the Table API, which works. The wrapper captures the result to a temp property,
1389
+ // then deactivates and deletes the job.
1390
+ return await this.executeViaScheduledJob(script);
1391
+ }
1392
+ async executeViaScheduledJob(script) {
1393
+ const runId = `nwk_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e9).toString(36)}`;
1394
+ const jobName = `NowAIKit exec ${runId}`;
1395
+ const prop = `nowaikit.exec.${runId}`;
1396
+ const wrapper = `var __r, __e = null;\n` +
1397
+ `try { __r = (function(){\n${script}\n})(); } catch (e) { __e = '' + e; }\n` +
1398
+ `try { gs.setProperty(${JSON.stringify(prop)}, JSON.stringify({ ok: __e === null, result: (__r === undefined ? null : __r), error: __e })); } catch (ig) {}\n` +
1399
+ `try { var __j = new GlideRecord('sysauto_script'); if (__j.get('name', ${JSON.stringify(jobName)})) { __j.active = false; __j.deleteRecord(); } } catch (ig) {}\n`;
1400
+ const nowGmt = new Date().toISOString().replace('T', ' ').slice(0, 19);
1401
+ let jobSysId;
1402
+ try {
1403
+ const job = await this.createRecord('sysauto_script', {
1404
+ name: jobName, script: wrapper, active: 'true', run_type: 'once', run_start: nowGmt, next_action: nowGmt,
1405
+ });
1406
+ jobSysId = job?.sys_id;
1407
+ }
1408
+ catch (e) {
1409
+ throw new ServiceNowError(`Server-side script execution needs to create a one-time scheduled job (sysauto_script), but that ` +
1410
+ `write was rejected: ${e instanceof Error ? e.message : String(e)}. ` +
1411
+ `Alternatives: use the Table API tools for data changes, run it in ServiceNow (Scripts - Background), ` +
1412
+ `or install the optional script-exec helper and set SCRIPT_EXEC_ENDPOINT.`, 'SCRIPT_EXEC_UNAVAILABLE');
1413
+ }
1414
+ // Poll for the result property (scheduler latency is typically a few to ~60s).
1415
+ const deadline = Date.now() + 90_000;
1416
+ while (Date.now() < deadline) {
1417
+ await new Promise((r) => setTimeout(r, 3000));
1418
+ const res = await this.queryRecords({ table: 'sys_properties', query: `name=${prop}`, fields: 'sys_id,value', limit: 1 });
1419
+ if (res.count > 0) {
1420
+ const rec = res.records[0];
1421
+ try {
1422
+ await this.deleteRecord('sys_properties', rec.sys_id);
1423
+ }
1424
+ catch { /* leave it */ }
1425
+ let parsed;
1098
1426
  try {
1099
- body = typeof results[0].body === 'string' ? JSON.parse(results[0].body) : results[0].body;
1427
+ parsed = JSON.parse(rec.value);
1100
1428
  }
1101
1429
  catch {
1102
- body = results[0].body;
1430
+ parsed = { ok: true, result: rec.value };
1103
1431
  }
1104
- return {
1105
- status: results[0].status_code,
1106
- output: body,
1107
- scope: scope || 'global',
1108
- };
1432
+ return { status: 200, ...parsed, via: 'scheduled-job' };
1109
1433
  }
1110
- return { status: 200, output: 'Script executed (no output captured)', scope: scope || 'global' };
1111
- }
1112
- catch (error) {
1113
- if (error instanceof ServiceNowError)
1114
- throw error;
1115
- throw new ServiceNowError(`Script execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'SCRIPT_FAILED');
1116
1434
  }
1435
+ throw new ServiceNowError(`Script was scheduled (sysauto_script "${jobName}"${jobSysId ? ' ' + jobSysId : ''}) but did not report a ` +
1436
+ `result within 90s — the scheduler may be backed up or the script was blocked by instance hardening. ` +
1437
+ `Check the job and its output on the instance.`, 'SCRIPT_EXEC_TIMEOUT');
1117
1438
  }
1118
1439
  /**
1119
1440
  * Natural language update (simplified implementation)