@arbium/enterprise-agent-connect 0.0.0-stage → 0.1.2-dev.d2aa5ca
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 +202 -0
- package/README.md +264 -2
- package/dist/connectors/dell-techdirect/connector.json +39 -0
- package/dist/connectors/dell-techdirect/index.d.ts +8 -0
- package/dist/connectors/dell-techdirect/index.js +7 -0
- package/dist/connectors/dell-techdirect/index.js.map +1 -0
- package/dist/connectors/dell-techdirect/tools.d.ts +2 -0
- package/dist/connectors/dell-techdirect/tools.js +49 -0
- package/dist/connectors/dell-techdirect/tools.js.map +1 -0
- package/dist/connectors/dell-techdirect/vendor.d.ts +6 -0
- package/dist/connectors/dell-techdirect/vendor.js +57 -0
- package/dist/connectors/dell-techdirect/vendor.js.map +1 -0
- package/dist/connectors/dynamics-365/connector.json +213 -0
- package/dist/connectors/dynamics-365/index.d.ts +11 -0
- package/dist/connectors/dynamics-365/index.js +10 -0
- package/dist/connectors/dynamics-365/index.js.map +1 -0
- package/dist/connectors/dynamics-365/tools.d.ts +35 -0
- package/dist/connectors/dynamics-365/tools.js +806 -0
- package/dist/connectors/dynamics-365/tools.js.map +1 -0
- package/dist/connectors/dynamics-365/vendor.d.ts +9 -0
- package/dist/connectors/dynamics-365/vendor.js +110 -0
- package/dist/connectors/dynamics-365/vendor.js.map +1 -0
- package/dist/connectors/entra/connector.json +85 -0
- package/dist/connectors/entra/index.d.ts +2 -0
- package/dist/connectors/entra/index.js +4 -0
- package/dist/connectors/entra/index.js.map +1 -0
- package/dist/connectors/entra/tools.d.ts +14 -0
- package/dist/connectors/entra/tools.js +262 -0
- package/dist/connectors/entra/tools.js.map +1 -0
- package/dist/connectors/fedex/connector.json +118 -0
- package/dist/connectors/fedex/index.d.ts +8 -0
- package/dist/connectors/fedex/index.js +7 -0
- package/dist/connectors/fedex/index.js.map +1 -0
- package/dist/connectors/fedex/tools.d.ts +3 -0
- package/dist/connectors/fedex/tools.js +152 -0
- package/dist/connectors/fedex/tools.js.map +1 -0
- package/dist/connectors/fedex/vendor.d.ts +7 -0
- package/dist/connectors/fedex/vendor.js +98 -0
- package/dist/connectors/fedex/vendor.js.map +1 -0
- package/dist/connectors/gong/connector.json +127 -0
- package/dist/connectors/gong/index.d.ts +8 -0
- package/dist/connectors/gong/index.js +7 -0
- package/dist/connectors/gong/index.js.map +1 -0
- package/dist/connectors/gong/tools.d.ts +8 -0
- package/dist/connectors/gong/tools.js +424 -0
- package/dist/connectors/gong/tools.js.map +1 -0
- package/dist/connectors/gong/vendor.d.ts +4 -0
- package/dist/connectors/gong/vendor.js +97 -0
- package/dist/connectors/gong/vendor.js.map +1 -0
- package/dist/connectors/http/index.d.ts +38 -0
- package/dist/connectors/http/index.js +322 -0
- package/dist/connectors/http/index.js.map +1 -0
- package/dist/connectors/intune/connector.json +106 -0
- package/dist/connectors/intune/index.d.ts +2 -0
- package/dist/connectors/intune/index.js +4 -0
- package/dist/connectors/intune/index.js.map +1 -0
- package/dist/connectors/intune/tools.d.ts +12 -0
- package/dist/connectors/intune/tools.js +416 -0
- package/dist/connectors/intune/tools.js.map +1 -0
- package/dist/connectors/jamf/connector.json +182 -0
- package/dist/connectors/jamf/index.d.ts +8 -0
- package/dist/connectors/jamf/index.js +7 -0
- package/dist/connectors/jamf/index.js.map +1 -0
- package/dist/connectors/jamf/tools.d.ts +7 -0
- package/dist/connectors/jamf/tools.js +659 -0
- package/dist/connectors/jamf/tools.js.map +1 -0
- package/dist/connectors/jamf/vendor.d.ts +5 -0
- package/dist/connectors/jamf/vendor.js +66 -0
- package/dist/connectors/jamf/vendor.js.map +1 -0
- package/dist/connectors/jira/connector.json +272 -0
- package/dist/connectors/jira/index.d.ts +8 -0
- package/dist/connectors/jira/index.js +7 -0
- package/dist/connectors/jira/index.js.map +1 -0
- package/dist/connectors/jira/jql.d.ts +4 -0
- package/dist/connectors/jira/jql.js +67 -0
- package/dist/connectors/jira/jql.js.map +1 -0
- package/dist/connectors/jira/tools.d.ts +3 -0
- package/dist/connectors/jira/tools.js +464 -0
- package/dist/connectors/jira/tools.js.map +1 -0
- package/dist/connectors/jira/vendor.d.ts +3 -0
- package/dist/connectors/jira/vendor.js +94 -0
- package/dist/connectors/jira/vendor.js.map +1 -0
- package/dist/connectors/microsoft/client.d.ts +58 -0
- package/dist/connectors/microsoft/client.js +434 -0
- package/dist/connectors/microsoft/client.js.map +1 -0
- package/dist/connectors/microsoft/provider.json +118 -0
- package/dist/connectors/netsuite/connector.json +315 -0
- package/dist/connectors/netsuite/index.d.ts +8 -0
- package/dist/connectors/netsuite/index.js +7 -0
- package/dist/connectors/netsuite/index.js.map +1 -0
- package/dist/connectors/netsuite/tools.d.ts +24 -0
- package/dist/connectors/netsuite/tools.js +447 -0
- package/dist/connectors/netsuite/tools.js.map +1 -0
- package/dist/connectors/netsuite/vendor.d.ts +26 -0
- package/dist/connectors/netsuite/vendor.js +248 -0
- package/dist/connectors/netsuite/vendor.js.map +1 -0
- package/dist/connectors/odata/index.d.ts +36 -0
- package/dist/connectors/odata/index.js +460 -0
- package/dist/connectors/odata/index.js.map +1 -0
- package/dist/connectors/okta/connector.json +193 -0
- package/dist/connectors/okta/index.d.ts +8 -0
- package/dist/connectors/okta/index.js +7 -0
- package/dist/connectors/okta/index.js.map +1 -0
- package/dist/connectors/okta/tools.d.ts +6 -0
- package/dist/connectors/okta/tools.js +429 -0
- package/dist/connectors/okta/tools.js.map +1 -0
- package/dist/connectors/okta/vendor.d.ts +14 -0
- package/dist/connectors/okta/vendor.js +203 -0
- package/dist/connectors/okta/vendor.js.map +1 -0
- package/dist/connectors/outlook-calendar/connector.json +136 -0
- package/dist/connectors/outlook-calendar/index.d.ts +2 -0
- package/dist/connectors/outlook-calendar/index.js +4 -0
- package/dist/connectors/outlook-calendar/index.js.map +1 -0
- package/dist/connectors/outlook-calendar/tools.d.ts +65 -0
- package/dist/connectors/outlook-calendar/tools.js +398 -0
- package/dist/connectors/outlook-calendar/tools.js.map +1 -0
- package/dist/connectors/outlook-mail/connector.json +154 -0
- package/dist/connectors/outlook-mail/index.d.ts +2 -0
- package/dist/connectors/outlook-mail/index.js +4 -0
- package/dist/connectors/outlook-mail/index.js.map +1 -0
- package/dist/connectors/outlook-mail/tools.d.ts +8 -0
- package/dist/connectors/outlook-mail/tools.js +236 -0
- package/dist/connectors/outlook-mail/tools.js.map +1 -0
- package/dist/connectors/salesforce/connector.json +122 -0
- package/dist/connectors/salesforce/index.d.ts +8 -0
- package/dist/connectors/salesforce/index.js +7 -0
- package/dist/connectors/salesforce/index.js.map +1 -0
- package/dist/connectors/salesforce/soql.d.ts +3 -0
- package/dist/connectors/salesforce/soql.js +60 -0
- package/dist/connectors/salesforce/soql.js.map +1 -0
- package/dist/connectors/salesforce/tools.d.ts +9 -0
- package/dist/connectors/salesforce/tools.js +234 -0
- package/dist/connectors/salesforce/tools.js.map +1 -0
- package/dist/connectors/salesforce/vendor.d.ts +3 -0
- package/dist/connectors/salesforce/vendor.js +107 -0
- package/dist/connectors/salesforce/vendor.js.map +1 -0
- package/dist/connectors/sap-s4hana/connector.json +263 -0
- package/dist/connectors/sap-s4hana/index.d.ts +8 -0
- package/dist/connectors/sap-s4hana/index.js +7 -0
- package/dist/connectors/sap-s4hana/index.js.map +1 -0
- package/dist/connectors/sap-s4hana/tools.d.ts +4 -0
- package/dist/connectors/sap-s4hana/tools.js +761 -0
- package/dist/connectors/sap-s4hana/tools.js.map +1 -0
- package/dist/connectors/sap-s4hana/vendor.d.ts +5 -0
- package/dist/connectors/sap-s4hana/vendor.js +176 -0
- package/dist/connectors/sap-s4hana/vendor.js.map +1 -0
- package/dist/connectors/servicenow/connector.json +228 -0
- package/dist/connectors/servicenow/index.d.ts +8 -0
- package/dist/connectors/servicenow/index.js +7 -0
- package/dist/connectors/servicenow/index.js.map +1 -0
- package/dist/connectors/servicenow/tools.d.ts +4 -0
- package/dist/connectors/servicenow/tools.js +884 -0
- package/dist/connectors/servicenow/tools.js.map +1 -0
- package/dist/connectors/servicenow/vendor.d.ts +4 -0
- package/dist/connectors/servicenow/vendor.js +109 -0
- package/dist/connectors/servicenow/vendor.js.map +1 -0
- package/dist/connectors/sharepoint-files/connector.json +194 -0
- package/dist/connectors/sharepoint-files/excel.d.ts +16 -0
- package/dist/connectors/sharepoint-files/excel.js +300 -0
- package/dist/connectors/sharepoint-files/excel.js.map +1 -0
- package/dist/connectors/sharepoint-files/index.d.ts +11 -0
- package/dist/connectors/sharepoint-files/index.js +12 -0
- package/dist/connectors/sharepoint-files/index.js.map +1 -0
- package/dist/connectors/sharepoint-files/tools.d.ts +52 -0
- package/dist/connectors/sharepoint-files/tools.js +365 -0
- package/dist/connectors/sharepoint-files/tools.js.map +1 -0
- package/dist/connectors/sharepoint-files/vendor.d.ts +11 -0
- package/dist/connectors/sharepoint-files/vendor.js +167 -0
- package/dist/connectors/sharepoint-files/vendor.js.map +1 -0
- package/dist/connectors/slack/connector.json +148 -0
- package/dist/connectors/slack/index.d.ts +8 -0
- package/dist/connectors/slack/index.js +7 -0
- package/dist/connectors/slack/index.js.map +1 -0
- package/dist/connectors/slack/tools.d.ts +3 -0
- package/dist/connectors/slack/tools.js +355 -0
- package/dist/connectors/slack/tools.js.map +1 -0
- package/dist/connectors/slack/vendor.d.ts +17 -0
- package/dist/connectors/slack/vendor.js +195 -0
- package/dist/connectors/slack/vendor.js.map +1 -0
- package/dist/connectors/soap/index.d.ts +19 -0
- package/dist/connectors/soap/index.js +248 -0
- package/dist/connectors/soap/index.js.map +1 -0
- package/dist/connectors/soap/xml.d.ts +12 -0
- package/dist/connectors/soap/xml.js +126 -0
- package/dist/connectors/soap/xml.js.map +1 -0
- package/dist/connectors/sql/engines.d.ts +58 -0
- package/dist/connectors/sql/engines.js +292 -0
- package/dist/connectors/sql/engines.js.map +1 -0
- package/dist/connectors/sql/index.d.ts +19 -0
- package/dist/connectors/sql/index.js +212 -0
- package/dist/connectors/sql/index.js.map +1 -0
- package/dist/connectors/teams/connector.json +298 -0
- package/dist/connectors/teams/index.d.ts +2 -0
- package/dist/connectors/teams/index.js +5 -0
- package/dist/connectors/teams/index.js.map +1 -0
- package/dist/connectors/teams/tools.d.ts +2 -0
- package/dist/connectors/teams/tools.js +391 -0
- package/dist/connectors/teams/tools.js.map +1 -0
- package/dist/connectors/workday/connector.json +186 -0
- package/dist/connectors/workday/index.d.ts +8 -0
- package/dist/connectors/workday/index.js +7 -0
- package/dist/connectors/workday/index.js.map +1 -0
- package/dist/connectors/workday/tools.d.ts +33 -0
- package/dist/connectors/workday/tools.js +410 -0
- package/dist/connectors/workday/tools.js.map +1 -0
- package/dist/connectors/workday/vendor.d.ts +18 -0
- package/dist/connectors/workday/vendor.js +195 -0
- package/dist/connectors/workday/vendor.js.map +1 -0
- package/dist/packages/connector-kit/src/branding.d.ts +57 -0
- package/dist/packages/connector-kit/src/branding.js +102 -0
- package/dist/packages/connector-kit/src/branding.js.map +1 -0
- package/dist/packages/connector-kit/src/certificate.d.ts +19 -0
- package/dist/packages/connector-kit/src/certificate.js +84 -0
- package/dist/packages/connector-kit/src/certificate.js.map +1 -0
- package/dist/packages/connector-kit/src/common.d.ts +40 -0
- package/dist/packages/connector-kit/src/common.js +107 -0
- package/dist/packages/connector-kit/src/common.js.map +1 -0
- package/dist/packages/connector-kit/src/connector.d.ts +1346 -0
- package/dist/packages/connector-kit/src/connector.js +1034 -0
- package/dist/packages/connector-kit/src/connector.js.map +1 -0
- package/dist/packages/connector-kit/src/index.d.ts +5 -0
- package/dist/packages/connector-kit/src/index.js +5 -0
- package/dist/packages/connector-kit/src/index.js.map +1 -0
- package/dist/packages/connector-kit/src/oauth.d.ts +15 -0
- package/dist/packages/connector-kit/src/oauth.js +96 -0
- package/dist/packages/connector-kit/src/oauth.js.map +1 -0
- package/dist/packages/connector-kit/src/types.d.ts +85 -0
- package/dist/packages/connector-kit/src/types.js +20 -0
- package/dist/packages/connector-kit/src/types.js.map +1 -0
- package/dist/packages/server/migrations/0001_tables.sql +268 -0
- package/dist/packages/server/migrations/0002_organizations.sql +72 -0
- package/dist/packages/server/migrations/0003_drop_unused.sql +3 -0
- package/dist/packages/server/migrations/0004_trigger_runners.sql +15 -0
- package/dist/packages/server/migrations/0005_run_runner.sql +3 -0
- package/dist/packages/server/migrations/0006_approvals.sql +36 -0
- package/dist/packages/server/migrations/0007_app_events.sql +110 -0
- package/dist/packages/server/migrations/rollback/to-legacy.sql +90 -0
- package/dist/packages/server/src/api.d.ts +3714 -0
- package/dist/packages/server/src/api.js +818 -0
- package/dist/packages/server/src/api.js.map +1 -0
- package/dist/packages/server/src/app.d.ts +23 -0
- package/dist/packages/server/src/app.js +467 -0
- package/dist/packages/server/src/app.js.map +1 -0
- package/dist/packages/server/src/builtin.d.ts +4 -0
- package/dist/packages/server/src/builtin.js +42 -0
- package/dist/packages/server/src/builtin.js.map +1 -0
- package/dist/packages/server/src/consent.d.ts +27 -0
- package/dist/packages/server/src/consent.js +36 -0
- package/dist/packages/server/src/consent.js.map +1 -0
- package/dist/packages/server/src/events/admin.d.ts +156 -0
- package/dist/packages/server/src/events/admin.js +255 -0
- package/dist/packages/server/src/events/admin.js.map +1 -0
- package/dist/packages/server/src/events/deliver.d.ts +43 -0
- package/dist/packages/server/src/events/deliver.js +129 -0
- package/dist/packages/server/src/events/deliver.js.map +1 -0
- package/dist/packages/server/src/events/runtime.d.ts +28 -0
- package/dist/packages/server/src/events/runtime.js +397 -0
- package/dist/packages/server/src/events/runtime.js.map +1 -0
- package/dist/packages/server/src/events/store.d.ts +172 -0
- package/dist/packages/server/src/events/store.js +275 -0
- package/dist/packages/server/src/events/store.js.map +1 -0
- package/dist/packages/server/src/index.d.ts +40 -0
- package/dist/packages/server/src/index.js +185 -0
- package/dist/packages/server/src/index.js.map +1 -0
- package/dist/packages/server/src/migrate.d.ts +11 -0
- package/dist/packages/server/src/migrate.js +134 -0
- package/dist/packages/server/src/migrate.js.map +1 -0
- package/dist/packages/server/src/openapi.d.ts +4 -0
- package/dist/packages/server/src/openapi.js +101 -0
- package/dist/packages/server/src/openapi.js.map +1 -0
- package/dist/packages/server/src/queue.d.ts +10 -0
- package/dist/packages/server/src/queue.js +45 -0
- package/dist/packages/server/src/queue.js.map +1 -0
- package/dist/packages/server/src/secrets.d.ts +19 -0
- package/dist/packages/server/src/secrets.js +76 -0
- package/dist/packages/server/src/secrets.js.map +1 -0
- package/dist/packages/server/src/server.d.ts +1 -0
- package/dist/packages/server/src/server.js +82 -0
- package/dist/packages/server/src/server.js.map +1 -0
- package/dist/packages/server/src/service.d.ts +760 -0
- package/dist/packages/server/src/service.js +918 -0
- package/dist/packages/server/src/service.js.map +1 -0
- package/dist/packages/server/src/store.d.ts +158 -0
- package/dist/packages/server/src/store.js +415 -0
- package/dist/packages/server/src/store.js.map +1 -0
- package/dist/packages/server/src/teams/activity.d.ts +123 -0
- package/dist/packages/server/src/teams/activity.js +188 -0
- package/dist/packages/server/src/teams/activity.js.map +1 -0
- package/dist/packages/server/src/teams/admin.d.ts +335 -0
- package/dist/packages/server/src/teams/admin.js +699 -0
- package/dist/packages/server/src/teams/admin.js.map +1 -0
- package/dist/packages/server/src/teams/approval.d.ts +57 -0
- package/dist/packages/server/src/teams/approval.js +118 -0
- package/dist/packages/server/src/teams/approval.js.map +1 -0
- package/dist/packages/server/src/teams/bot.d.ts +25 -0
- package/dist/packages/server/src/teams/bot.js +124 -0
- package/dist/packages/server/src/teams/bot.js.map +1 -0
- package/dist/packages/server/src/teams/claude.d.ts +46 -0
- package/dist/packages/server/src/teams/claude.js +165 -0
- package/dist/packages/server/src/teams/claude.js.map +1 -0
- package/dist/packages/server/src/teams/jwt.d.ts +17 -0
- package/dist/packages/server/src/teams/jwt.js +121 -0
- package/dist/packages/server/src/teams/jwt.js.map +1 -0
- package/dist/packages/server/src/teams/package.d.ts +79 -0
- package/dist/packages/server/src/teams/package.js +319 -0
- package/dist/packages/server/src/teams/package.js.map +1 -0
- package/dist/packages/server/src/teams/provision.d.ts +84 -0
- package/dist/packages/server/src/teams/provision.js +505 -0
- package/dist/packages/server/src/teams/provision.js.map +1 -0
- package/dist/packages/server/src/teams/runner.d.ts +80 -0
- package/dist/packages/server/src/teams/runner.js +187 -0
- package/dist/packages/server/src/teams/runner.js.map +1 -0
- package/dist/packages/server/src/teams/runtime.d.ts +91 -0
- package/dist/packages/server/src/teams/runtime.js +679 -0
- package/dist/packages/server/src/teams/runtime.js.map +1 -0
- package/dist/packages/server/src/teams/store.d.ts +233 -0
- package/dist/packages/server/src/teams/store.js +507 -0
- package/dist/packages/server/src/teams/store.js.map +1 -0
- package/package.json +68 -4
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { audited, Denied, json, listLimit, VendorFailed, } from "../../packages/connector-kit/src/common.js";
|
|
3
|
+
// ServiceNow's Table API (/api/now/table/<table>) and Service Catalog API
|
|
4
|
+
// (/api/sn_sc/servicecatalog). Agents never write an encoded query: each tool builds one from
|
|
5
|
+
// its own parameters and the key's limits, and every value is refused if it carries `^` (which
|
|
6
|
+
// starts a new condition, or with NQ a whole new query) or `javascript:` (which ServiceNow
|
|
7
|
+
// runs when it reads a query value).
|
|
8
|
+
const TABLE = "/api/now/table";
|
|
9
|
+
const CATALOG = "/api/sn_sc/servicecatalog";
|
|
10
|
+
const NUMBER = /^[A-Z]{2,8}[0-9]{4,12}$/;
|
|
11
|
+
const SYS_ID = /^[0-9a-f]{32}$/;
|
|
12
|
+
const CURSOR = /^[0-9]{1,6}$/;
|
|
13
|
+
const JOURNAL_CHARS = 8000;
|
|
14
|
+
const ARTICLE_CHARS = 20000;
|
|
15
|
+
// With sysparm_display_value=all every field comes as { value, display_value }: references
|
|
16
|
+
// and choices are read by their display value (a group's name, "In Progress"), times by their
|
|
17
|
+
// value, which the Table API gives in UTC.
|
|
18
|
+
const text = (f, key) => typeof f?.[key] === "string" && f[key] !== "" ? f[key] : null;
|
|
19
|
+
const shown = (r, field) => text(r[field], "display_value");
|
|
20
|
+
const raw = (r, field) => text(r[field], "value");
|
|
21
|
+
const utc = (r, field) => raw(r, field)?.replace(" ", "T").concat("Z") ?? null;
|
|
22
|
+
export function safe(value, what = "A value") {
|
|
23
|
+
if (!value.trim() || /[\^\r\n]/.test(value) || /javascript:/i.test(value))
|
|
24
|
+
throw new Denied(`${what} cannot be empty or contain ^, line breaks or javascript:`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
// field=a^ORfield=b: ^OR binds to the condition before it, so this stays one condition when
|
|
28
|
+
// joined to others with ^.
|
|
29
|
+
const anyOf = (field, values) => values.map((v, i) => `${i ? "^OR" : ""}${field}=${safe(v)}`).join("");
|
|
30
|
+
const userIs = (field, who) => `${field}.email=${safe(who, "A person")}^OR${field}.user_name=${who}`;
|
|
31
|
+
function scopeQuery(ctx, on) {
|
|
32
|
+
const groups = listLimit(ctx.limits, "groups");
|
|
33
|
+
const companies = listLimit(ctx.limits, "companies");
|
|
34
|
+
return [
|
|
35
|
+
...(groups && on.group ? [anyOf(`${on.group}.name`, groups)] : []),
|
|
36
|
+
...(companies && on.company ? [anyOf(`${on.company}.name`, companies)] : []),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
const within = (values, value) => !values || (value !== null && values.some((v) => v.toLowerCase() === value.toLowerCase()));
|
|
40
|
+
// The same limits, checked again on each record answered.
|
|
41
|
+
function inScope(ctx, on, r) {
|
|
42
|
+
return ((!on.group || within(listLimit(ctx.limits, "groups"), shown(r, on.group))) &&
|
|
43
|
+
(!on.company || within(listLimit(ctx.limits, "companies"), shown(r, on.company))));
|
|
44
|
+
}
|
|
45
|
+
async function ok(ctx, mark, req) {
|
|
46
|
+
const res = await ctx.call(req);
|
|
47
|
+
mark(res.status);
|
|
48
|
+
if (res.status < 200 || res.status >= 300) {
|
|
49
|
+
const e = res.data?.error;
|
|
50
|
+
const detail = [e?.message, e?.detail].filter((x) => typeof x === "string" && x).join(": ");
|
|
51
|
+
throw new VendorFailed(res.status, `ServiceNow answered ${res.status}${detail ? `: ${detail.slice(0, 400)}` : ""}`);
|
|
52
|
+
}
|
|
53
|
+
return res.data;
|
|
54
|
+
}
|
|
55
|
+
function tablePath(table, query, fields, extra = {}) {
|
|
56
|
+
const params = new URLSearchParams({
|
|
57
|
+
...(query.length ? { sysparm_query: query.join("^") } : {}),
|
|
58
|
+
sysparm_fields: fields.join(","),
|
|
59
|
+
sysparm_display_value: "all",
|
|
60
|
+
sysparm_exclude_reference_link: "true",
|
|
61
|
+
...extra,
|
|
62
|
+
});
|
|
63
|
+
return `${TABLE}/${table}?${params}`;
|
|
64
|
+
}
|
|
65
|
+
function offset(cursor) {
|
|
66
|
+
if (cursor === undefined)
|
|
67
|
+
return 0;
|
|
68
|
+
if (!CURSOR.test(cursor))
|
|
69
|
+
throw new Denied("Give the cursor exactly as the last answer's next");
|
|
70
|
+
return Number(cursor);
|
|
71
|
+
}
|
|
72
|
+
// One page: asks for one more row than wanted, so `next` is only given when there is more.
|
|
73
|
+
async function page(ctx, mark, table, query, fields, max, cursor, on) {
|
|
74
|
+
const start = offset(cursor);
|
|
75
|
+
const data = await ok(ctx, mark, {
|
|
76
|
+
method: "GET",
|
|
77
|
+
path: tablePath(table, query, ["sys_id", ...fields], {
|
|
78
|
+
sysparm_limit: String(max + 1),
|
|
79
|
+
sysparm_offset: String(start),
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
const rows = (data.result ?? []);
|
|
83
|
+
return {
|
|
84
|
+
rows: rows.slice(0, max).filter((r) => inScope(ctx, on, r)),
|
|
85
|
+
next: rows.length > max ? String(start + max) : null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// A record by its number. One outside the key's limits is answered like one that does not
|
|
89
|
+
// exist, so a key cannot learn which numbers exist elsewhere.
|
|
90
|
+
async function byNumber(ctx, mark, table, kind, number, fields, on) {
|
|
91
|
+
if (!NUMBER.test(number))
|
|
92
|
+
throw new Denied(`Give the ${kind} number, such as ${example[table]}`);
|
|
93
|
+
const data = await ok(ctx, mark, {
|
|
94
|
+
method: "GET",
|
|
95
|
+
path: tablePath(table, [`number=${number}`, ...scopeQuery(ctx, on)], ["sys_id", ...fields], {
|
|
96
|
+
sysparm_limit: "1",
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
const r = (data.result ?? [])[0];
|
|
100
|
+
if (!r || !inScope(ctx, on, r) || !SYS_ID.test(raw(r, "sys_id") ?? ""))
|
|
101
|
+
throw new Denied(`${number} was not found, or this agent may not use it`);
|
|
102
|
+
return r;
|
|
103
|
+
}
|
|
104
|
+
const example = {
|
|
105
|
+
incident: "INC0010001",
|
|
106
|
+
change_request: "CHG0030001",
|
|
107
|
+
kb_knowledge: "KB0010001",
|
|
108
|
+
};
|
|
109
|
+
// One active user by email or user name. `scoped`: under a companies limit, only a person of
|
|
110
|
+
// those companies (a caller or someone ordered for, not the staff member assigned).
|
|
111
|
+
async function person(ctx, mark, who, scoped) {
|
|
112
|
+
const query = ["active=true", `email=${safe(who, "A person")}^ORuser_name=${who}`];
|
|
113
|
+
if (scoped)
|
|
114
|
+
query.push(...scopeQuery(ctx, { company: "company" }));
|
|
115
|
+
const data = await ok(ctx, mark, {
|
|
116
|
+
method: "GET",
|
|
117
|
+
path: tablePath("sys_user", query, ["sys_id", "name", "company"], {
|
|
118
|
+
sysparm_limit: "2",
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
const rows = (data.result ?? []);
|
|
122
|
+
const r = rows[0];
|
|
123
|
+
if (rows.length !== 1 || (scoped && !inScope(ctx, { company: "company" }, r)))
|
|
124
|
+
throw new Denied(`No single active person ${who} that this agent may act for`);
|
|
125
|
+
return { id: raw(r, "sys_id"), company: raw(r, "company") };
|
|
126
|
+
}
|
|
127
|
+
async function group(ctx, mark, name) {
|
|
128
|
+
if (!within(listLimit(ctx.limits, "groups"), safe(name, "A group name")))
|
|
129
|
+
throw new Denied(`This agent may not use the assignment group ${name}`);
|
|
130
|
+
const data = await ok(ctx, mark, {
|
|
131
|
+
method: "GET",
|
|
132
|
+
path: tablePath("sys_user_group", ["active=true", `name=${name}`], ["sys_id"], {
|
|
133
|
+
sysparm_limit: "2",
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
const rows = (data.result ?? []);
|
|
137
|
+
if (rows.length !== 1)
|
|
138
|
+
throw new Denied(`No single active assignment group is named ${name}`);
|
|
139
|
+
return raw(rows[0], "sys_id");
|
|
140
|
+
}
|
|
141
|
+
const clip = (s, max) => s && s.length > max ? `${s.slice(0, max)}\n[cut at ${max} characters]` : s;
|
|
142
|
+
// Knowledge articles and catalog descriptions are HTML; agents get the text.
|
|
143
|
+
export function htmlText(html) {
|
|
144
|
+
if (!html)
|
|
145
|
+
return "";
|
|
146
|
+
return html
|
|
147
|
+
.replace(/<(script|style)[\s\S]*?<\/\1>/gi, "")
|
|
148
|
+
.replace(/<br\s*\/?>|<\/(p|div|li|h[1-6]|tr)>/gi, "\n")
|
|
149
|
+
.replace(/<[^>]+>/g, "")
|
|
150
|
+
.replace(/ /g, " ")
|
|
151
|
+
.replace(/</g, "<")
|
|
152
|
+
.replace(/>/g, ">")
|
|
153
|
+
.replace(/"/g, '"')
|
|
154
|
+
.replace(/'/g, "'")
|
|
155
|
+
.replace(/&/g, "&")
|
|
156
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
157
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
158
|
+
.trim();
|
|
159
|
+
}
|
|
160
|
+
// Incidents.
|
|
161
|
+
const INCIDENT = { group: "assignment_group", company: "company" };
|
|
162
|
+
const INCIDENT_LIST = [
|
|
163
|
+
"number",
|
|
164
|
+
"short_description",
|
|
165
|
+
"state",
|
|
166
|
+
"priority",
|
|
167
|
+
"assignment_group",
|
|
168
|
+
"assigned_to",
|
|
169
|
+
"caller_id",
|
|
170
|
+
"company",
|
|
171
|
+
"opened_at",
|
|
172
|
+
"sys_updated_on",
|
|
173
|
+
];
|
|
174
|
+
const INCIDENT_FULL = [
|
|
175
|
+
...INCIDENT_LIST,
|
|
176
|
+
"description",
|
|
177
|
+
"category",
|
|
178
|
+
"impact",
|
|
179
|
+
"urgency",
|
|
180
|
+
"cmdb_ci",
|
|
181
|
+
"close_code",
|
|
182
|
+
"close_notes",
|
|
183
|
+
"resolved_at",
|
|
184
|
+
"comments",
|
|
185
|
+
"work_notes",
|
|
186
|
+
];
|
|
187
|
+
// The incident table's state values (out of the box).
|
|
188
|
+
const INCIDENT_STATE = {
|
|
189
|
+
new: "1",
|
|
190
|
+
in_progress: "2",
|
|
191
|
+
on_hold: "3",
|
|
192
|
+
resolved: "6",
|
|
193
|
+
closed: "7",
|
|
194
|
+
};
|
|
195
|
+
const HOLD_REASON = {
|
|
196
|
+
awaiting_caller: "1",
|
|
197
|
+
awaiting_problem: "3",
|
|
198
|
+
awaiting_vendor: "4",
|
|
199
|
+
awaiting_change: "5",
|
|
200
|
+
};
|
|
201
|
+
const incidentSummary = (r) => ({
|
|
202
|
+
number: shown(r, "number"),
|
|
203
|
+
short_description: shown(r, "short_description"),
|
|
204
|
+
state: shown(r, "state"),
|
|
205
|
+
priority: shown(r, "priority"),
|
|
206
|
+
assignment_group: shown(r, "assignment_group"),
|
|
207
|
+
assigned_to: shown(r, "assigned_to"),
|
|
208
|
+
caller: shown(r, "caller_id"),
|
|
209
|
+
company: shown(r, "company"),
|
|
210
|
+
opened: utc(r, "opened_at"),
|
|
211
|
+
updated: utc(r, "sys_updated_on"),
|
|
212
|
+
});
|
|
213
|
+
const incidentDetail = (r) => ({
|
|
214
|
+
...incidentSummary(r),
|
|
215
|
+
description: shown(r, "description"),
|
|
216
|
+
category: shown(r, "category"),
|
|
217
|
+
impact: shown(r, "impact"),
|
|
218
|
+
urgency: shown(r, "urgency"),
|
|
219
|
+
configuration_item: shown(r, "cmdb_ci"),
|
|
220
|
+
close_code: shown(r, "close_code"),
|
|
221
|
+
close_notes: shown(r, "close_notes"),
|
|
222
|
+
resolved: utc(r, "resolved_at"),
|
|
223
|
+
comments: clip(shown(r, "comments"), JOURNAL_CHARS),
|
|
224
|
+
work_notes: clip(shown(r, "work_notes"), JOURNAL_CHARS),
|
|
225
|
+
});
|
|
226
|
+
// Changes.
|
|
227
|
+
const CHANGE = { group: "assignment_group", company: "company" };
|
|
228
|
+
const CHANGE_LIST = [
|
|
229
|
+
"number",
|
|
230
|
+
"short_description",
|
|
231
|
+
"type",
|
|
232
|
+
"state",
|
|
233
|
+
"risk",
|
|
234
|
+
"assignment_group",
|
|
235
|
+
"assigned_to",
|
|
236
|
+
"company",
|
|
237
|
+
"start_date",
|
|
238
|
+
"end_date",
|
|
239
|
+
"sys_updated_on",
|
|
240
|
+
];
|
|
241
|
+
const CHANGE_FULL = [
|
|
242
|
+
...CHANGE_LIST,
|
|
243
|
+
"description",
|
|
244
|
+
"impact",
|
|
245
|
+
"approval",
|
|
246
|
+
"cmdb_ci",
|
|
247
|
+
"justification",
|
|
248
|
+
"implementation_plan",
|
|
249
|
+
"backout_plan",
|
|
250
|
+
"test_plan",
|
|
251
|
+
"close_code",
|
|
252
|
+
"close_notes",
|
|
253
|
+
];
|
|
254
|
+
const changeSummary = (r) => ({
|
|
255
|
+
number: shown(r, "number"),
|
|
256
|
+
short_description: shown(r, "short_description"),
|
|
257
|
+
type: shown(r, "type"),
|
|
258
|
+
state: shown(r, "state"),
|
|
259
|
+
risk: shown(r, "risk"),
|
|
260
|
+
assignment_group: shown(r, "assignment_group"),
|
|
261
|
+
assigned_to: shown(r, "assigned_to"),
|
|
262
|
+
company: shown(r, "company"),
|
|
263
|
+
planned_start: utc(r, "start_date"),
|
|
264
|
+
planned_end: utc(r, "end_date"),
|
|
265
|
+
updated: utc(r, "sys_updated_on"),
|
|
266
|
+
});
|
|
267
|
+
// Configuration items and requested items: only the companies limit applies.
|
|
268
|
+
const CI = { company: "company" };
|
|
269
|
+
const CI_FIELDS = [
|
|
270
|
+
"name",
|
|
271
|
+
"sys_class_name",
|
|
272
|
+
"serial_number",
|
|
273
|
+
"asset_tag",
|
|
274
|
+
"install_status",
|
|
275
|
+
"operational_status",
|
|
276
|
+
"assigned_to",
|
|
277
|
+
"support_group",
|
|
278
|
+
"company",
|
|
279
|
+
"location",
|
|
280
|
+
"ip_address",
|
|
281
|
+
"sys_updated_on",
|
|
282
|
+
];
|
|
283
|
+
const RITM = { company: "requested_for.company" };
|
|
284
|
+
const RITM_FIELDS = [
|
|
285
|
+
"number",
|
|
286
|
+
"request",
|
|
287
|
+
"cat_item",
|
|
288
|
+
"stage",
|
|
289
|
+
"state",
|
|
290
|
+
"requested_for",
|
|
291
|
+
"requested_for.company",
|
|
292
|
+
"assignment_group",
|
|
293
|
+
"opened_at",
|
|
294
|
+
"due_date",
|
|
295
|
+
"sys_updated_on",
|
|
296
|
+
];
|
|
297
|
+
const who = z.string().max(100);
|
|
298
|
+
const max = (top, byDefault) => z.number().int().min(1).max(top).default(byDefault);
|
|
299
|
+
const cursor = z
|
|
300
|
+
.string()
|
|
301
|
+
.optional()
|
|
302
|
+
.describe("The `next` value of the previous answer, to read the following page.");
|
|
303
|
+
const status = z
|
|
304
|
+
.enum(["open", "closed", "any"])
|
|
305
|
+
.default("open")
|
|
306
|
+
.describe("open: still active; closed: resolved, closed or cancelled.");
|
|
307
|
+
const statusQuery = (s) => s === "open" ? ["active=true"] : s === "closed" ? ["active=false"] : [];
|
|
308
|
+
const textQuery = (t) => t ? [`short_descriptionLIKE${safe(t, "Search text")}^ORdescriptionLIKE${t}`] : [];
|
|
309
|
+
export const servicenowTools = {
|
|
310
|
+
names: [
|
|
311
|
+
"servicenow_search_incidents",
|
|
312
|
+
"servicenow_get_incident",
|
|
313
|
+
"servicenow_create_incident",
|
|
314
|
+
"servicenow_update_incident",
|
|
315
|
+
"servicenow_add_incident_note",
|
|
316
|
+
"servicenow_resolve_incident",
|
|
317
|
+
"servicenow_search_changes",
|
|
318
|
+
"servicenow_get_change",
|
|
319
|
+
"servicenow_find_configuration_items",
|
|
320
|
+
"servicenow_search_knowledge",
|
|
321
|
+
"servicenow_get_article",
|
|
322
|
+
"servicenow_search_catalog_items",
|
|
323
|
+
"servicenow_get_catalog_item",
|
|
324
|
+
"servicenow_order_catalog_item",
|
|
325
|
+
"servicenow_search_requests",
|
|
326
|
+
],
|
|
327
|
+
register(server, ctx, allowed) {
|
|
328
|
+
const readOnly = { readOnlyHint: true, openWorldHint: true };
|
|
329
|
+
const write = {
|
|
330
|
+
readOnlyHint: false,
|
|
331
|
+
destructiveHint: false,
|
|
332
|
+
openWorldHint: true,
|
|
333
|
+
};
|
|
334
|
+
const groupNote = listLimit(ctx.limits, "groups")
|
|
335
|
+
? ` This agent may only use these assignment groups: ${listLimit(ctx.limits, "groups").join(", ")}.`
|
|
336
|
+
: "";
|
|
337
|
+
if (allowed.has("servicenow_search_incidents"))
|
|
338
|
+
server.registerTool("servicenow_search_incidents", {
|
|
339
|
+
description: "Search ServiceNow incidents, newest change first. All filters are optional and combine. " +
|
|
340
|
+
"When `next` is set, call again with `cursor` to read more.",
|
|
341
|
+
inputSchema: z.object({
|
|
342
|
+
text: z
|
|
343
|
+
.string()
|
|
344
|
+
.max(200)
|
|
345
|
+
.optional()
|
|
346
|
+
.describe("Words in the short description or description."),
|
|
347
|
+
status,
|
|
348
|
+
priority: z
|
|
349
|
+
.number()
|
|
350
|
+
.int()
|
|
351
|
+
.min(1)
|
|
352
|
+
.max(5)
|
|
353
|
+
.optional()
|
|
354
|
+
.describe("1 Critical to 5 Planning."),
|
|
355
|
+
assignment_group: z
|
|
356
|
+
.string()
|
|
357
|
+
.max(100)
|
|
358
|
+
.optional()
|
|
359
|
+
.describe(`Assignment group name.${groupNote}`),
|
|
360
|
+
caller: who.optional().describe("The caller's email or user name."),
|
|
361
|
+
assigned_to: who.optional().describe("The assignee's email or user name."),
|
|
362
|
+
max_results: max(50, 20),
|
|
363
|
+
cursor,
|
|
364
|
+
}),
|
|
365
|
+
annotations: readOnly,
|
|
366
|
+
}, (args) => audited(ctx, "servicenow_search_incidents", JSON.stringify(args).slice(0, 300), async (mark, retarget) => {
|
|
367
|
+
if (args.assignment_group &&
|
|
368
|
+
!within(listLimit(ctx.limits, "groups"), args.assignment_group))
|
|
369
|
+
throw new Denied(`This agent may not use the assignment group ${args.assignment_group}`);
|
|
370
|
+
const query = [
|
|
371
|
+
...statusQuery(args.status),
|
|
372
|
+
...textQuery(args.text),
|
|
373
|
+
...(args.priority ? [`priority=${args.priority}`] : []),
|
|
374
|
+
...(args.assignment_group
|
|
375
|
+
? [`assignment_group.name=${safe(args.assignment_group)}`]
|
|
376
|
+
: []),
|
|
377
|
+
...(args.caller ? [userIs("caller_id", args.caller)] : []),
|
|
378
|
+
...(args.assigned_to ? [userIs("assigned_to", args.assigned_to)] : []),
|
|
379
|
+
...scopeQuery(ctx, INCIDENT),
|
|
380
|
+
"ORDERBYDESCsys_updated_on",
|
|
381
|
+
];
|
|
382
|
+
retarget(query.join("^").slice(0, 500));
|
|
383
|
+
const { rows, next } = await page(ctx, mark, "incident", query, INCIDENT_LIST, args.max_results, args.cursor, INCIDENT);
|
|
384
|
+
return json({ incidents: rows.map(incidentSummary), next });
|
|
385
|
+
}));
|
|
386
|
+
if (allowed.has("servicenow_get_incident"))
|
|
387
|
+
server.registerTool("servicenow_get_incident", {
|
|
388
|
+
description: "Read one incident by number: its fields, the customer-visible comments and the internal work notes " +
|
|
389
|
+
"(each as ServiceNow's activity text, newest first).",
|
|
390
|
+
inputSchema: z.object({
|
|
391
|
+
number: z.string().describe("Incident number, e.g. INC0010001."),
|
|
392
|
+
}),
|
|
393
|
+
annotations: readOnly,
|
|
394
|
+
}, ({ number }) => audited(ctx, "servicenow_get_incident", number, async (mark) => json(incidentDetail(await byNumber(ctx, mark, "incident", "incident", number, INCIDENT_FULL, INCIDENT)))));
|
|
395
|
+
if (allowed.has("servicenow_create_incident"))
|
|
396
|
+
server.registerTool("servicenow_create_incident", {
|
|
397
|
+
description: "Open a new incident." +
|
|
398
|
+
(listLimit(ctx.limits, "groups")
|
|
399
|
+
? " assignment_group is required for this agent."
|
|
400
|
+
: "") +
|
|
401
|
+
(listLimit(ctx.limits, "companies") ? " caller is required for this agent." : ""),
|
|
402
|
+
inputSchema: z.object({
|
|
403
|
+
short_description: z.string().min(1).max(160),
|
|
404
|
+
description: z.string().max(10000).optional(),
|
|
405
|
+
caller: who.optional().describe("Who the incident is for: email or user name."),
|
|
406
|
+
assignment_group: z
|
|
407
|
+
.string()
|
|
408
|
+
.max(100)
|
|
409
|
+
.optional()
|
|
410
|
+
.describe(`Assignment group name.${groupNote}`),
|
|
411
|
+
impact: z.number().int().min(1).max(3).optional().describe("1 High, 2 Medium, 3 Low."),
|
|
412
|
+
urgency: z.number().int().min(1).max(3).optional().describe("1 High, 2 Medium, 3 Low."),
|
|
413
|
+
category: z
|
|
414
|
+
.string()
|
|
415
|
+
.max(40)
|
|
416
|
+
.optional()
|
|
417
|
+
.describe("Category value, e.g. software, hardware, network."),
|
|
418
|
+
}),
|
|
419
|
+
annotations: write,
|
|
420
|
+
}, (args) => audited(ctx, "servicenow_create_incident", args.short_description.slice(0, 200), async (mark, retarget) => {
|
|
421
|
+
if (listLimit(ctx.limits, "groups") && !args.assignment_group)
|
|
422
|
+
throw new Denied("Give an assignment_group: this agent may only open incidents for its groups");
|
|
423
|
+
if (listLimit(ctx.limits, "companies") && !args.caller)
|
|
424
|
+
throw new Denied("Give a caller: this agent may only open incidents for people of its companies");
|
|
425
|
+
const caller = args.caller ? await person(ctx, mark, args.caller, true) : null;
|
|
426
|
+
const body = {
|
|
427
|
+
short_description: args.short_description,
|
|
428
|
+
};
|
|
429
|
+
if (args.description)
|
|
430
|
+
body.description = args.description;
|
|
431
|
+
if (args.assignment_group)
|
|
432
|
+
body.assignment_group = await group(ctx, mark, args.assignment_group);
|
|
433
|
+
if (caller) {
|
|
434
|
+
body.caller_id = caller.id;
|
|
435
|
+
if (caller.company)
|
|
436
|
+
body.company = caller.company;
|
|
437
|
+
}
|
|
438
|
+
if (args.impact)
|
|
439
|
+
body.impact = String(args.impact);
|
|
440
|
+
if (args.urgency)
|
|
441
|
+
body.urgency = String(args.urgency);
|
|
442
|
+
if (args.category)
|
|
443
|
+
body.category = args.category;
|
|
444
|
+
const data = await ok(ctx, mark, {
|
|
445
|
+
method: "POST",
|
|
446
|
+
path: tablePath("incident", [], INCIDENT_LIST),
|
|
447
|
+
body,
|
|
448
|
+
});
|
|
449
|
+
const r = data.result;
|
|
450
|
+
retarget(`${shown(r, "number")}: ${args.short_description}`.slice(0, 300));
|
|
451
|
+
return json(incidentSummary(r));
|
|
452
|
+
}));
|
|
453
|
+
// The incident read back within the key's limits, then changed by its sys_id.
|
|
454
|
+
const patchIncident = async (mark, number, body) => {
|
|
455
|
+
const current = await byNumber(ctx, mark, "incident", "incident", number, ["number"], INCIDENT);
|
|
456
|
+
const data = await ok(ctx, mark, {
|
|
457
|
+
method: "PATCH",
|
|
458
|
+
path: `${TABLE}/incident/${raw(current, "sys_id")}?${new URLSearchParams({
|
|
459
|
+
sysparm_fields: INCIDENT_LIST.join(","),
|
|
460
|
+
sysparm_display_value: "all",
|
|
461
|
+
sysparm_exclude_reference_link: "true",
|
|
462
|
+
})}`,
|
|
463
|
+
body,
|
|
464
|
+
});
|
|
465
|
+
return incidentSummary(data.result);
|
|
466
|
+
};
|
|
467
|
+
if (allowed.has("servicenow_update_incident"))
|
|
468
|
+
server.registerTool("servicenow_update_incident", {
|
|
469
|
+
description: "Change an incident's fields, reassign it or set it to new, in progress or on hold. " +
|
|
470
|
+
"To resolve it use servicenow_resolve_incident; to add a note, servicenow_add_incident_note.",
|
|
471
|
+
inputSchema: z.object({
|
|
472
|
+
number: z.string().describe("Incident number, e.g. INC0010001."),
|
|
473
|
+
short_description: z.string().min(1).max(160).optional(),
|
|
474
|
+
description: z.string().max(10000).optional(),
|
|
475
|
+
impact: z.number().int().min(1).max(3).optional(),
|
|
476
|
+
urgency: z.number().int().min(1).max(3).optional(),
|
|
477
|
+
category: z.string().max(40).optional(),
|
|
478
|
+
assignment_group: z
|
|
479
|
+
.string()
|
|
480
|
+
.max(100)
|
|
481
|
+
.optional()
|
|
482
|
+
.describe(`New assignment group name.${groupNote}`),
|
|
483
|
+
assigned_to: who.optional().describe("New assignee: email or user name."),
|
|
484
|
+
state: z.enum(["new", "in_progress", "on_hold"]).optional(),
|
|
485
|
+
on_hold_reason: z
|
|
486
|
+
.enum(["awaiting_caller", "awaiting_problem", "awaiting_vendor", "awaiting_change"])
|
|
487
|
+
.optional()
|
|
488
|
+
.describe("Required with state on_hold."),
|
|
489
|
+
}),
|
|
490
|
+
annotations: write,
|
|
491
|
+
}, ({ number, ...args }) => audited(ctx, "servicenow_update_incident", number, async (mark) => {
|
|
492
|
+
if ((args.state === "on_hold") !== Boolean(args.on_hold_reason))
|
|
493
|
+
throw new Denied("Give on_hold_reason exactly when state is on_hold");
|
|
494
|
+
const body = {};
|
|
495
|
+
for (const f of ["short_description", "description", "category"])
|
|
496
|
+
if (args[f] !== undefined)
|
|
497
|
+
body[f] = args[f];
|
|
498
|
+
if (args.impact)
|
|
499
|
+
body.impact = String(args.impact);
|
|
500
|
+
if (args.urgency)
|
|
501
|
+
body.urgency = String(args.urgency);
|
|
502
|
+
if (args.state)
|
|
503
|
+
body.state = INCIDENT_STATE[args.state];
|
|
504
|
+
if (args.on_hold_reason)
|
|
505
|
+
body.hold_reason = HOLD_REASON[args.on_hold_reason];
|
|
506
|
+
if (args.assignment_group)
|
|
507
|
+
body.assignment_group = await group(ctx, mark, args.assignment_group);
|
|
508
|
+
if (args.assigned_to)
|
|
509
|
+
body.assigned_to = (await person(ctx, mark, args.assigned_to, false)).id;
|
|
510
|
+
if (!Object.keys(body).length)
|
|
511
|
+
throw new Denied("Give at least one field to change");
|
|
512
|
+
return json(await patchIncident(mark, number, body));
|
|
513
|
+
}));
|
|
514
|
+
if (allowed.has("servicenow_add_incident_note"))
|
|
515
|
+
server.registerTool("servicenow_add_incident_note", {
|
|
516
|
+
description: "Add a note to an incident. internal (default) is a work note only staff see; customer is a " +
|
|
517
|
+
"comment the caller sees and is usually emailed.",
|
|
518
|
+
inputSchema: z.object({
|
|
519
|
+
number: z.string().describe("Incident number, e.g. INC0010001."),
|
|
520
|
+
text: z.string().min(1).max(10000),
|
|
521
|
+
visibility: z.enum(["internal", "customer"]).default("internal"),
|
|
522
|
+
}),
|
|
523
|
+
annotations: write,
|
|
524
|
+
}, ({ number, text: note, visibility }) => audited(ctx, "servicenow_add_incident_note", `${number} (${visibility})`, async (mark) => {
|
|
525
|
+
const field = visibility === "customer" ? "comments" : "work_notes";
|
|
526
|
+
const incident = await patchIncident(mark, number, {
|
|
527
|
+
[field]: note,
|
|
528
|
+
});
|
|
529
|
+
return json({
|
|
530
|
+
number: incident.number,
|
|
531
|
+
added: visibility === "customer" ? "comment" : "work note",
|
|
532
|
+
});
|
|
533
|
+
}));
|
|
534
|
+
if (allowed.has("servicenow_resolve_incident"))
|
|
535
|
+
server.registerTool("servicenow_resolve_incident", {
|
|
536
|
+
description: "Resolve an incident with a resolution code and notes. The caller is usually told and can reopen it.",
|
|
537
|
+
inputSchema: z.object({
|
|
538
|
+
number: z.string().describe("Incident number, e.g. INC0010001."),
|
|
539
|
+
close_code: z
|
|
540
|
+
.string()
|
|
541
|
+
.min(1)
|
|
542
|
+
.max(80)
|
|
543
|
+
.describe("Resolution code as your instance names it, e.g. Solution provided."),
|
|
544
|
+
close_notes: z.string().min(1).max(4000),
|
|
545
|
+
}),
|
|
546
|
+
annotations: write,
|
|
547
|
+
}, ({ number, close_code, close_notes }) => audited(ctx, "servicenow_resolve_incident", number, async (mark) => json(await patchIncident(mark, number, {
|
|
548
|
+
state: INCIDENT_STATE.resolved,
|
|
549
|
+
close_code,
|
|
550
|
+
close_notes,
|
|
551
|
+
}))));
|
|
552
|
+
if (allowed.has("servicenow_search_changes"))
|
|
553
|
+
server.registerTool("servicenow_search_changes", {
|
|
554
|
+
description: "Search change requests, newest change first. When `next` is set, call again with `cursor` to read more.",
|
|
555
|
+
inputSchema: z.object({
|
|
556
|
+
text: z
|
|
557
|
+
.string()
|
|
558
|
+
.max(200)
|
|
559
|
+
.optional()
|
|
560
|
+
.describe("Words in the short description or description."),
|
|
561
|
+
status,
|
|
562
|
+
assignment_group: z
|
|
563
|
+
.string()
|
|
564
|
+
.max(100)
|
|
565
|
+
.optional()
|
|
566
|
+
.describe(`Assignment group name.${groupNote}`),
|
|
567
|
+
max_results: max(50, 20),
|
|
568
|
+
cursor,
|
|
569
|
+
}),
|
|
570
|
+
annotations: readOnly,
|
|
571
|
+
}, (args) => audited(ctx, "servicenow_search_changes", JSON.stringify(args).slice(0, 300), async (mark, retarget) => {
|
|
572
|
+
if (args.assignment_group &&
|
|
573
|
+
!within(listLimit(ctx.limits, "groups"), args.assignment_group))
|
|
574
|
+
throw new Denied(`This agent may not use the assignment group ${args.assignment_group}`);
|
|
575
|
+
const query = [
|
|
576
|
+
...statusQuery(args.status),
|
|
577
|
+
...textQuery(args.text),
|
|
578
|
+
...(args.assignment_group
|
|
579
|
+
? [`assignment_group.name=${safe(args.assignment_group)}`]
|
|
580
|
+
: []),
|
|
581
|
+
...scopeQuery(ctx, CHANGE),
|
|
582
|
+
"ORDERBYDESCsys_updated_on",
|
|
583
|
+
];
|
|
584
|
+
retarget(query.join("^").slice(0, 500));
|
|
585
|
+
const { rows, next } = await page(ctx, mark, "change_request", query, CHANGE_LIST, args.max_results, args.cursor, CHANGE);
|
|
586
|
+
return json({ changes: rows.map(changeSummary), next });
|
|
587
|
+
}));
|
|
588
|
+
if (allowed.has("servicenow_get_change"))
|
|
589
|
+
server.registerTool("servicenow_get_change", {
|
|
590
|
+
description: "Read one change request by number, with its plans and approval state.",
|
|
591
|
+
inputSchema: z.object({
|
|
592
|
+
number: z.string().describe("Change number, e.g. CHG0030001."),
|
|
593
|
+
}),
|
|
594
|
+
annotations: readOnly,
|
|
595
|
+
}, ({ number }) => audited(ctx, "servicenow_get_change", number, async (mark) => {
|
|
596
|
+
const r = await byNumber(ctx, mark, "change_request", "change", number, CHANGE_FULL, CHANGE);
|
|
597
|
+
return json({
|
|
598
|
+
...changeSummary(r),
|
|
599
|
+
description: shown(r, "description"),
|
|
600
|
+
impact: shown(r, "impact"),
|
|
601
|
+
approval: shown(r, "approval"),
|
|
602
|
+
configuration_item: shown(r, "cmdb_ci"),
|
|
603
|
+
justification: shown(r, "justification"),
|
|
604
|
+
implementation_plan: shown(r, "implementation_plan"),
|
|
605
|
+
backout_plan: shown(r, "backout_plan"),
|
|
606
|
+
test_plan: shown(r, "test_plan"),
|
|
607
|
+
close_code: shown(r, "close_code"),
|
|
608
|
+
close_notes: shown(r, "close_notes"),
|
|
609
|
+
});
|
|
610
|
+
}));
|
|
611
|
+
if (allowed.has("servicenow_find_configuration_items"))
|
|
612
|
+
server.registerTool("servicenow_find_configuration_items", {
|
|
613
|
+
description: "Look up configuration items (computers, servers, applications and so on) in the CMDB by name or serial number.",
|
|
614
|
+
inputSchema: z.object({
|
|
615
|
+
name: z.string().max(100).optional().describe("All or part of the item's name."),
|
|
616
|
+
serial_number: z.string().max(100).optional().describe("The exact serial number."),
|
|
617
|
+
max_results: max(20, 10),
|
|
618
|
+
}),
|
|
619
|
+
annotations: readOnly,
|
|
620
|
+
}, (args) => audited(ctx, "servicenow_find_configuration_items", JSON.stringify(args).slice(0, 300), async (mark) => {
|
|
621
|
+
if (!args.name && !args.serial_number)
|
|
622
|
+
throw new Denied("Give a name or a serial number");
|
|
623
|
+
const query = [
|
|
624
|
+
...(args.name ? [`nameLIKE${safe(args.name, "The name")}`] : []),
|
|
625
|
+
...(args.serial_number
|
|
626
|
+
? [`serial_number=${safe(args.serial_number, "The serial number")}`]
|
|
627
|
+
: []),
|
|
628
|
+
...scopeQuery(ctx, CI),
|
|
629
|
+
"ORDERBYname",
|
|
630
|
+
];
|
|
631
|
+
const { rows, next } = await page(ctx, mark, "cmdb_ci", query, CI_FIELDS, args.max_results, undefined, CI);
|
|
632
|
+
return json({
|
|
633
|
+
items: rows.map((r) => ({
|
|
634
|
+
name: shown(r, "name"),
|
|
635
|
+
class: shown(r, "sys_class_name"),
|
|
636
|
+
serial_number: shown(r, "serial_number"),
|
|
637
|
+
asset_tag: shown(r, "asset_tag"),
|
|
638
|
+
install_status: shown(r, "install_status"),
|
|
639
|
+
operational_status: shown(r, "operational_status"),
|
|
640
|
+
assigned_to: shown(r, "assigned_to"),
|
|
641
|
+
support_group: shown(r, "support_group"),
|
|
642
|
+
company: shown(r, "company"),
|
|
643
|
+
location: shown(r, "location"),
|
|
644
|
+
ip_address: shown(r, "ip_address"),
|
|
645
|
+
updated: utc(r, "sys_updated_on"),
|
|
646
|
+
})),
|
|
647
|
+
more: next !== null,
|
|
648
|
+
});
|
|
649
|
+
}));
|
|
650
|
+
if (allowed.has("servicenow_search_knowledge"))
|
|
651
|
+
server.registerTool("servicenow_search_knowledge", {
|
|
652
|
+
description: "Search published knowledge articles by keywords. Read one with servicenow_get_article. " +
|
|
653
|
+
"When `next` is set, call again with `cursor` to read more.",
|
|
654
|
+
inputSchema: z.object({
|
|
655
|
+
text: z.string().min(1).max(200).describe("Keywords, e.g. vpn will not connect."),
|
|
656
|
+
max_results: max(20, 10),
|
|
657
|
+
cursor,
|
|
658
|
+
}),
|
|
659
|
+
annotations: readOnly,
|
|
660
|
+
}, ({ text: words, max_results, cursor: at }) => audited(ctx, "servicenow_search_knowledge", words, async (mark) => {
|
|
661
|
+
const query = [
|
|
662
|
+
"workflow_state=published",
|
|
663
|
+
`123TEXTQUERY321=${safe(words, "Search text")}`,
|
|
664
|
+
];
|
|
665
|
+
const fields = [
|
|
666
|
+
"number",
|
|
667
|
+
"short_description",
|
|
668
|
+
"kb_knowledge_base",
|
|
669
|
+
"kb_category",
|
|
670
|
+
"sys_updated_on",
|
|
671
|
+
];
|
|
672
|
+
const { rows, next } = await page(ctx, mark, "kb_knowledge", query, fields, max_results, at, {});
|
|
673
|
+
return json({
|
|
674
|
+
articles: rows.map((r) => ({
|
|
675
|
+
number: shown(r, "number"),
|
|
676
|
+
title: shown(r, "short_description"),
|
|
677
|
+
knowledge_base: shown(r, "kb_knowledge_base"),
|
|
678
|
+
category: shown(r, "kb_category"),
|
|
679
|
+
updated: utc(r, "sys_updated_on"),
|
|
680
|
+
})),
|
|
681
|
+
next,
|
|
682
|
+
});
|
|
683
|
+
}));
|
|
684
|
+
if (allowed.has("servicenow_get_article"))
|
|
685
|
+
server.registerTool("servicenow_get_article", {
|
|
686
|
+
description: "Read one published knowledge article by number, as plain text.",
|
|
687
|
+
inputSchema: z.object({
|
|
688
|
+
number: z.string().describe("Article number, e.g. KB0010001."),
|
|
689
|
+
}),
|
|
690
|
+
annotations: readOnly,
|
|
691
|
+
}, ({ number }) => audited(ctx, "servicenow_get_article", number, async (mark) => {
|
|
692
|
+
if (!NUMBER.test(number))
|
|
693
|
+
throw new Denied("Give an article number such as KB0010001");
|
|
694
|
+
const data = await ok(ctx, mark, {
|
|
695
|
+
method: "GET",
|
|
696
|
+
path: tablePath("kb_knowledge", [`number=${number}`, "workflow_state=published"], [
|
|
697
|
+
"number",
|
|
698
|
+
"short_description",
|
|
699
|
+
"text",
|
|
700
|
+
"wiki",
|
|
701
|
+
"kb_knowledge_base",
|
|
702
|
+
"kb_category",
|
|
703
|
+
"sys_updated_on",
|
|
704
|
+
], { sysparm_limit: "1" }),
|
|
705
|
+
});
|
|
706
|
+
const r = (data.result ?? [])[0];
|
|
707
|
+
if (!r)
|
|
708
|
+
throw new Denied(`${number} was not found, or is not published`);
|
|
709
|
+
const body = htmlText(raw(r, "text")) || (raw(r, "wiki") ?? "");
|
|
710
|
+
return json({
|
|
711
|
+
number: shown(r, "number"),
|
|
712
|
+
title: shown(r, "short_description"),
|
|
713
|
+
knowledge_base: shown(r, "kb_knowledge_base"),
|
|
714
|
+
category: shown(r, "kb_category"),
|
|
715
|
+
updated: utc(r, "sys_updated_on"),
|
|
716
|
+
text: clip(body, ARTICLE_CHARS),
|
|
717
|
+
});
|
|
718
|
+
}));
|
|
719
|
+
if (allowed.has("servicenow_search_catalog_items"))
|
|
720
|
+
server.registerTool("servicenow_search_catalog_items", {
|
|
721
|
+
description: "Search the service catalog for things people can request (laptops, software, access). " +
|
|
722
|
+
"Read one with servicenow_get_catalog_item before ordering it.",
|
|
723
|
+
inputSchema: z.object({
|
|
724
|
+
text: z
|
|
725
|
+
.string()
|
|
726
|
+
.max(200)
|
|
727
|
+
.optional()
|
|
728
|
+
.describe("Words in the item's name or description."),
|
|
729
|
+
max_results: max(50, 20),
|
|
730
|
+
cursor,
|
|
731
|
+
}),
|
|
732
|
+
annotations: readOnly,
|
|
733
|
+
}, ({ text: words, max_results, cursor: at }) => audited(ctx, "servicenow_search_catalog_items", words ?? null, async (mark) => {
|
|
734
|
+
const start = offset(at);
|
|
735
|
+
const params = new URLSearchParams({
|
|
736
|
+
...(words ? { sysparm_text: safe(words, "Search text") } : {}),
|
|
737
|
+
sysparm_limit: String(max_results + 1),
|
|
738
|
+
sysparm_offset: String(start),
|
|
739
|
+
});
|
|
740
|
+
const data = await ok(ctx, mark, {
|
|
741
|
+
method: "GET",
|
|
742
|
+
path: `${CATALOG}/items?${params}`,
|
|
743
|
+
});
|
|
744
|
+
const items = (data.result ?? []);
|
|
745
|
+
return json({
|
|
746
|
+
items: items.slice(0, max_results).map((i) => ({
|
|
747
|
+
id: i.sys_id,
|
|
748
|
+
name: i.name,
|
|
749
|
+
short_description: i.short_description || null,
|
|
750
|
+
category: i.category?.title ?? null,
|
|
751
|
+
price: i.price || null,
|
|
752
|
+
})),
|
|
753
|
+
next: items.length > max_results ? String(start + max_results) : null,
|
|
754
|
+
});
|
|
755
|
+
}));
|
|
756
|
+
if (allowed.has("servicenow_get_catalog_item"))
|
|
757
|
+
server.registerTool("servicenow_get_catalog_item", {
|
|
758
|
+
description: "Read one catalog item: what it is and the variables (questions) an order must answer.",
|
|
759
|
+
inputSchema: z.object({
|
|
760
|
+
id: z.string().describe("The item's id from servicenow_search_catalog_items."),
|
|
761
|
+
}),
|
|
762
|
+
annotations: readOnly,
|
|
763
|
+
}, ({ id }) => audited(ctx, "servicenow_get_catalog_item", id, async (mark) => {
|
|
764
|
+
if (!SYS_ID.test(id))
|
|
765
|
+
throw new Denied("Give the item's 32 character id");
|
|
766
|
+
const data = await ok(ctx, mark, {
|
|
767
|
+
method: "GET",
|
|
768
|
+
path: `${CATALOG}/items/${id}`,
|
|
769
|
+
});
|
|
770
|
+
const i = data.result ?? {};
|
|
771
|
+
return json({
|
|
772
|
+
id: i.sys_id,
|
|
773
|
+
name: i.name,
|
|
774
|
+
short_description: i.short_description || null,
|
|
775
|
+
description: clip(htmlText(i.description ?? null), 4000),
|
|
776
|
+
price: i.price || null,
|
|
777
|
+
variables: (i.variables ?? [])
|
|
778
|
+
.filter((v) => v.name)
|
|
779
|
+
.map((v) => ({
|
|
780
|
+
name: v.name,
|
|
781
|
+
label: v.label ?? null,
|
|
782
|
+
type: v.friendly_type ?? v.type ?? null,
|
|
783
|
+
mandatory: Boolean(v.mandatory),
|
|
784
|
+
...(Array.isArray(v.choices) && v.choices.length
|
|
785
|
+
? {
|
|
786
|
+
choices: v.choices.map((c) => ({
|
|
787
|
+
value: c.value,
|
|
788
|
+
label: c.label,
|
|
789
|
+
})),
|
|
790
|
+
}
|
|
791
|
+
: {}),
|
|
792
|
+
})),
|
|
793
|
+
});
|
|
794
|
+
}));
|
|
795
|
+
if (allowed.has("servicenow_order_catalog_item"))
|
|
796
|
+
server.registerTool("servicenow_order_catalog_item", {
|
|
797
|
+
description: "Order a catalog item, answering its variables by name. This starts the item's approvals and fulfillment." +
|
|
798
|
+
(listLimit(ctx.limits, "companies")
|
|
799
|
+
? " requested_for is required for this agent."
|
|
800
|
+
: ""),
|
|
801
|
+
inputSchema: z.object({
|
|
802
|
+
id: z.string().describe("The item's id."),
|
|
803
|
+
variables: z
|
|
804
|
+
.record(z.string(), z.string())
|
|
805
|
+
.default({})
|
|
806
|
+
.describe("Variable names and answers."),
|
|
807
|
+
quantity: z.number().int().min(1).max(10).default(1),
|
|
808
|
+
requested_for: who
|
|
809
|
+
.optional()
|
|
810
|
+
.describe("Who it is for: email or user name. Default: the integration user."),
|
|
811
|
+
}),
|
|
812
|
+
annotations: write,
|
|
813
|
+
}, ({ id, variables, quantity, requested_for }) => audited(ctx, "servicenow_order_catalog_item", id, async (mark, retarget) => {
|
|
814
|
+
if (!SYS_ID.test(id))
|
|
815
|
+
throw new Denied("Give the item's 32 character id");
|
|
816
|
+
if (Object.keys(variables).length > 100)
|
|
817
|
+
throw new Denied("Give at most 100 variables");
|
|
818
|
+
if (listLimit(ctx.limits, "companies") && !requested_for)
|
|
819
|
+
throw new Denied("Give requested_for: this agent may only order for people of its companies");
|
|
820
|
+
const forWhom = requested_for ? await person(ctx, mark, requested_for, true) : null;
|
|
821
|
+
const data = await ok(ctx, mark, {
|
|
822
|
+
method: "POST",
|
|
823
|
+
path: `${CATALOG}/items/${id}/order_now`,
|
|
824
|
+
body: {
|
|
825
|
+
sysparm_quantity: String(quantity),
|
|
826
|
+
variables,
|
|
827
|
+
...(forWhom ? { sysparm_requested_for: forWhom.id } : {}),
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
const request = data.result?.request_number ?? data.result?.number ?? null;
|
|
831
|
+
retarget(`${id} -> ${request}`);
|
|
832
|
+
return json({ request_number: request });
|
|
833
|
+
}));
|
|
834
|
+
if (allowed.has("servicenow_search_requests"))
|
|
835
|
+
server.registerTool("servicenow_search_requests", {
|
|
836
|
+
description: "Track requested items (RITM): their stage and state. Filter by a RITM or REQ number or by who they are for. " +
|
|
837
|
+
"When `next` is set, call again with `cursor` to read more.",
|
|
838
|
+
inputSchema: z.object({
|
|
839
|
+
number: z
|
|
840
|
+
.string()
|
|
841
|
+
.optional()
|
|
842
|
+
.describe("A requested item (RITM...) or request (REQ...) number."),
|
|
843
|
+
requested_for: who.optional().describe("Email or user name."),
|
|
844
|
+
status,
|
|
845
|
+
max_results: max(50, 20),
|
|
846
|
+
cursor,
|
|
847
|
+
}),
|
|
848
|
+
annotations: readOnly,
|
|
849
|
+
}, (args) => audited(ctx, "servicenow_search_requests", JSON.stringify(args).slice(0, 300), async (mark) => {
|
|
850
|
+
if (args.number && !NUMBER.test(args.number))
|
|
851
|
+
throw new Denied("Give a number such as RITM0010001");
|
|
852
|
+
const query = [
|
|
853
|
+
...statusQuery(args.status),
|
|
854
|
+
...(args.number
|
|
855
|
+
? [
|
|
856
|
+
args.number.startsWith("REQ")
|
|
857
|
+
? `request.number=${args.number}`
|
|
858
|
+
: `number=${args.number}`,
|
|
859
|
+
]
|
|
860
|
+
: []),
|
|
861
|
+
...(args.requested_for ? [userIs("requested_for", args.requested_for)] : []),
|
|
862
|
+
...scopeQuery(ctx, RITM),
|
|
863
|
+
"ORDERBYDESCsys_updated_on",
|
|
864
|
+
];
|
|
865
|
+
const { rows, next } = await page(ctx, mark, "sc_req_item", query, RITM_FIELDS, args.max_results, args.cursor, RITM);
|
|
866
|
+
return json({
|
|
867
|
+
requests: rows.map((r) => ({
|
|
868
|
+
number: shown(r, "number"),
|
|
869
|
+
request: shown(r, "request"),
|
|
870
|
+
item: shown(r, "cat_item"),
|
|
871
|
+
stage: shown(r, "stage"),
|
|
872
|
+
state: shown(r, "state"),
|
|
873
|
+
requested_for: shown(r, "requested_for"),
|
|
874
|
+
assignment_group: shown(r, "assignment_group"),
|
|
875
|
+
opened: utc(r, "opened_at"),
|
|
876
|
+
due: utc(r, "due_date"),
|
|
877
|
+
updated: utc(r, "sys_updated_on"),
|
|
878
|
+
})),
|
|
879
|
+
next,
|
|
880
|
+
});
|
|
881
|
+
}));
|
|
882
|
+
},
|
|
883
|
+
};
|
|
884
|
+
//# sourceMappingURL=tools.js.map
|