@bettsnation/paperclip-plugin-servicenow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @bettsnation/paperclip-plugin-servicenow
2
+
3
+ Paperclip plugin for bidirectional ServiceNow incident sync.
4
+
5
+ ## Inbound (SN → Paperclip)
6
+ - SN Business Rule fires webhook on incident creation
7
+ - Plugin creates Paperclip issue assigned to target agent
8
+ - Sets correlation_id on SN incident for back-reference
9
+
10
+ ## Outbound (Paperclip → SN)
11
+ - Issue completed → auto-resolve SN incident
12
+ - Issue status changes → SN work notes
13
+ - Agent comments → SN work notes
14
+
15
+ ## Install
16
+
17
+ ```
18
+ npm install @bettsnation/paperclip-plugin-servicenow
19
+ ```
20
+
21
+ Or: Instance Settings → Plugins → Install Plugin → `@bettsnation/paperclip-plugin-servicenow`
22
+
23
+ ## Configuration
24
+
25
+ | Setting | Required | Description |
26
+ |---------|----------|-------------|
27
+ | `snInstanceUrl` | Yes | e.g. `https://dev199497.service-now.com` |
28
+ | `snUsernameRef` | Yes | Secret ref for SN username |
29
+ | `snPasswordRef` | Yes | Secret ref for SN password |
30
+ | `snWebhookSecretRef` | Yes | Secret ref for inbound webhook verification |
31
+ | `targetCompanyId` | Yes | Paperclip company for new issues |
32
+ | `targetAgentId` | Yes | Agent to assign new issues to |
33
+ | `targetProjectId` | Yes | Project for new issues |
34
+ | `autoResolve` | No | Auto-resolve SN incidents (default: true) |
35
+ | `syncWorkNotes` | No | Sync comments to SN (default: true) |
36
+
37
+ ## SN Business Rule
38
+
39
+ Create on `incident` table, after insert:
40
+ ```javascript
41
+ (function executeRule(current, previous) {
42
+ var rm = new sn_ws.RESTMessageV2();
43
+ rm.setEndpoint('<PLUGIN_WEBHOOK_URL>');
44
+ rm.setHttpMethod('POST');
45
+ rm.setRequestHeader('Content-Type', 'application/json');
46
+ rm.setRequestHeader('X-SN-Webhook-Secret', gs.getProperty('x_pc.webhook_secret'));
47
+ rm.setRequestBody(JSON.stringify({
48
+ number: current.getValue('number'),
49
+ sys_id: current.getValue('sys_id'),
50
+ short_description: current.getValue('short_description'),
51
+ description: current.getValue('description'),
52
+ priority: current.getValue('priority'),
53
+ caller_id: current.getDisplayValue('caller_id'),
54
+ assignment_group: current.getDisplayValue('assignment_group'),
55
+ category: current.getValue('category'),
56
+ subcategory: current.getValue('subcategory')
57
+ }));
58
+ rm.executeAsync();
59
+ })(current, previous);
60
+ ```
61
+
62
+ ## License
63
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
2
+ declare const manifest: PaperclipPluginManifestV1;
3
+ export default manifest;
@@ -0,0 +1,83 @@
1
+ const manifest = {
2
+ id: "bettsnation.servicenow",
3
+ apiVersion: 1,
4
+ version: "0.1.0",
5
+ displayName: "ServiceNow Integration",
6
+ description: "Bidirectional ServiceNow incident sync. Receives SN webhooks to create Paperclip issues, auto-updates SN incidents as issues progress.",
7
+ author: "BettsNation",
8
+ categories: ["connector"],
9
+ capabilities: [
10
+ "events.subscribe",
11
+ "webhooks.receive",
12
+ "issues.create",
13
+ "issues.read",
14
+ "http.outbound",
15
+ "secrets.read-ref",
16
+ ],
17
+ entrypoints: {
18
+ worker: "./dist/worker.js",
19
+ },
20
+ webhooks: [
21
+ {
22
+ endpointKey: "servicenow-incident",
23
+ displayName: "ServiceNow Incident Webhook",
24
+ description: "Receives incident create/update webhooks from ServiceNow Business Rules",
25
+ },
26
+ ],
27
+ instanceConfigSchema: {
28
+ type: "object",
29
+ properties: {
30
+ snInstanceUrl: {
31
+ type: "string",
32
+ title: "ServiceNow Instance URL",
33
+ description: "e.g. https://dev199497.service-now.com",
34
+ },
35
+ snUsernameRef: {
36
+ type: "string",
37
+ title: "SN Username (Secret Ref)",
38
+ },
39
+ snPasswordRef: {
40
+ type: "string",
41
+ title: "SN Password (Secret Ref)",
42
+ },
43
+ snWebhookSecretRef: {
44
+ type: "string",
45
+ title: "Inbound Webhook Secret (Secret Ref)",
46
+ },
47
+ targetCompanyId: {
48
+ type: "string",
49
+ title: "Target Company ID",
50
+ description: "Paperclip company to create issues in",
51
+ },
52
+ targetAgentId: {
53
+ type: "string",
54
+ title: "Target Agent ID",
55
+ description: "Agent to assign new issues to",
56
+ },
57
+ targetProjectId: {
58
+ type: "string",
59
+ title: "Target Project ID",
60
+ },
61
+ autoResolve: {
62
+ type: "boolean",
63
+ title: "Auto-resolve SN incidents on issue completion",
64
+ default: true,
65
+ },
66
+ syncWorkNotes: {
67
+ type: "boolean",
68
+ title: "Sync agent comments to SN work notes",
69
+ default: true,
70
+ },
71
+ },
72
+ required: [
73
+ "snInstanceUrl",
74
+ "snUsernameRef",
75
+ "snPasswordRef",
76
+ "snWebhookSecretRef",
77
+ "targetCompanyId",
78
+ "targetAgentId",
79
+ "targetProjectId",
80
+ ],
81
+ },
82
+ };
83
+ export default manifest;
@@ -0,0 +1,2 @@
1
+ declare const plugin: import("@paperclipai/plugin-sdk").PaperclipPlugin;
2
+ export default plugin;
package/dist/worker.js ADDED
@@ -0,0 +1,168 @@
1
+ import { definePlugin, runWorker, } from "@paperclipai/plugin-sdk";
2
+ const PRIORITY_MAP = {
3
+ "1": "critical",
4
+ "2": "high",
5
+ "3": "medium",
6
+ "4": "low",
7
+ };
8
+ async function snRequest(ctx, config, method, path, body) {
9
+ const username = await ctx.secrets.resolve(config.snUsernameRef);
10
+ const password = await ctx.secrets.resolve(config.snPasswordRef);
11
+ const auth = Buffer.from(`${username}:${password}`).toString("base64");
12
+ return ctx.http.fetch(`${config.snInstanceUrl}${path}`, {
13
+ method,
14
+ headers: {
15
+ "Content-Type": "application/json",
16
+ Accept: "application/json",
17
+ Authorization: `Basic ${auth}`,
18
+ },
19
+ body: body ? JSON.stringify(body) : undefined,
20
+ });
21
+ }
22
+ function isSNSysId(value) {
23
+ return typeof value === "string" && /^[a-f0-9]{32}$/.test(value);
24
+ }
25
+ let _config = null;
26
+ let _ctx = null;
27
+ const plugin = definePlugin({
28
+ async setup(ctx) {
29
+ const config = (await ctx.config.get());
30
+ _config = config;
31
+ _ctx = ctx;
32
+ // =========================================================
33
+ // OUTBOUND: Paperclip events → update SN incident
34
+ // =========================================================
35
+ if (config.autoResolve !== false) {
36
+ // Auto-resolve SN incident when Paperclip issue completes
37
+ ctx.events.on("issue.updated", async (event) => {
38
+ const payload = event.payload;
39
+ const changes = payload?.changes;
40
+ const statusChange = changes?.status;
41
+ const newStatus = statusChange?.to ?? payload?.status;
42
+ if (newStatus !== "done")
43
+ return;
44
+ const originId = payload?.originId;
45
+ if (!isSNSysId(originId))
46
+ return;
47
+ const agentName = payload?.assigneeAgentName ?? "Paperclip agent";
48
+ const issueTag = payload?.issueTag ?? event.entityId;
49
+ const response = await snRequest(ctx, config, "PATCH", `/api/now/table/incident/${originId}`, {
50
+ state: "6",
51
+ close_code: "Solved (Permanently)",
52
+ close_notes: `Resolved by ${agentName}. Issue: ${issueTag}`,
53
+ });
54
+ if (response.ok) {
55
+ ctx.logger.info(`Resolved SN incident for ${issueTag}`);
56
+ }
57
+ else {
58
+ ctx.logger.error(`Failed to resolve SN incident: ${response.status}`);
59
+ }
60
+ });
61
+ // Sync status changes to SN work notes
62
+ ctx.events.on("issue.updated", async (event) => {
63
+ const payload = event.payload;
64
+ const changes = payload?.changes;
65
+ const statusChange = changes?.status;
66
+ const newStatus = statusChange?.to;
67
+ if (!newStatus || newStatus === "done")
68
+ return;
69
+ const originId = payload?.originId;
70
+ if (!isSNSysId(originId))
71
+ return;
72
+ const issueTag = payload?.issueTag ?? event.entityId;
73
+ const agentName = payload?.assigneeAgentName ?? "Agent";
74
+ const messages = {
75
+ in_progress: `${agentName} is working on this. Issue: ${issueTag}`,
76
+ in_review: `Code review in progress. Issue: ${issueTag}`,
77
+ blocked: `Issue ${issueTag} is blocked — awaiting board approval.`,
78
+ cancelled: `Issue ${issueTag} was cancelled.`,
79
+ };
80
+ const message = messages[newStatus];
81
+ if (!message)
82
+ return;
83
+ await snRequest(ctx, config, "PATCH", `/api/now/table/incident/${originId}`, {
84
+ work_notes: `[Paperclip] ${message}`,
85
+ });
86
+ });
87
+ }
88
+ // Sync agent comments to SN work notes
89
+ if (config.syncWorkNotes !== false) {
90
+ ctx.events.on("issue.comment.created", async (event) => {
91
+ const payload = event.payload;
92
+ const issue = payload?.issue;
93
+ const originId = issue?.originId;
94
+ if (!isSNSysId(originId))
95
+ return;
96
+ const agentName = payload?.authorAgentName ?? payload?.authorName ?? "Board";
97
+ const body = payload?.body ?? "";
98
+ const truncated = body.length > 3500 ? body.slice(0, 3500) + "\n\n[truncated]" : body;
99
+ await snRequest(ctx, config, "PATCH", `/api/now/table/incident/${originId}`, {
100
+ work_notes: `[Paperclip — ${agentName}] ${truncated}`,
101
+ });
102
+ });
103
+ }
104
+ ctx.logger.info("ServiceNow plugin initialized (outbound events active)");
105
+ },
106
+ // =========================================================
107
+ // INBOUND: SN webhook → create Paperclip issue
108
+ // =========================================================
109
+ async onWebhook(input) {
110
+ if (input.endpointKey !== "servicenow-incident")
111
+ return;
112
+ const ctx = _ctx;
113
+ const config = _config;
114
+ // Verify webhook secret
115
+ const providedSecret = Array.isArray(input.headers["x-sn-webhook-secret"])
116
+ ? input.headers["x-sn-webhook-secret"][0]
117
+ : input.headers["x-sn-webhook-secret"];
118
+ const expectedSecret = await ctx.secrets.resolve(config.snWebhookSecretRef);
119
+ if (providedSecret !== expectedSecret) {
120
+ ctx.logger.warn("SN webhook rejected: invalid secret");
121
+ return;
122
+ }
123
+ const incident = input.parsedBody;
124
+ if (!incident?.number || !incident?.sys_id) {
125
+ ctx.logger.warn("SN webhook missing required fields");
126
+ return;
127
+ }
128
+ // Create Paperclip issue
129
+ const description = [
130
+ `## ServiceNow Incident`,
131
+ ``,
132
+ `**Number:** ${incident.number}`,
133
+ `**Priority:** ${incident.priority ?? "unknown"}`,
134
+ `**Category:** ${incident.category ?? ""}${incident.subcategory ? ` / ${incident.subcategory}` : ""}`,
135
+ `**Caller:** ${incident.caller_id ?? "unknown"}`,
136
+ `**Group:** ${incident.assignment_group ?? ""}`,
137
+ ``,
138
+ `### Description`,
139
+ incident.description || "_No description provided_",
140
+ ``,
141
+ `---`,
142
+ `_SN sys_id: ${incident.sys_id}_`,
143
+ ].join("\n");
144
+ const issue = await ctx.issues.create({
145
+ companyId: config.targetCompanyId,
146
+ title: `[${incident.number}] ${incident.short_description}`,
147
+ description,
148
+ status: "todo",
149
+ priority: PRIORITY_MAP[incident.priority ?? "3"] ?? "medium",
150
+ assigneeAgentId: config.targetAgentId,
151
+ projectId: config.targetProjectId,
152
+ });
153
+ // Set correlation_id on SN incident
154
+ await snRequest(ctx, config, "PATCH", `/api/now/table/incident/${incident.sys_id}`, {
155
+ correlation_id: issue.id ?? "",
156
+ });
157
+ ctx.logger.info(`Created issue for ${incident.number}`);
158
+ },
159
+ async onHealth() {
160
+ return { status: "ok" };
161
+ },
162
+ async onShutdown() {
163
+ _config = null;
164
+ _ctx = null;
165
+ },
166
+ });
167
+ export default plugin;
168
+ runWorker(plugin, import.meta.url);
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@bettsnation/paperclip-plugin-servicenow",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Paperclip plugin for bidirectional ServiceNow incident sync — webhook intake and auto-resolve",
6
+ "paperclipPlugin": {
7
+ "manifest": "./dist/manifest.js",
8
+ "worker": "./dist/worker.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "peerDependencies": {
15
+ "@paperclipai/plugin-sdk": ">=0.1.0"
16
+ },
17
+ "devDependencies": {
18
+ "@paperclipai/plugin-sdk": "^2026.403.0",
19
+ "@types/node": "^22.0.0",
20
+ "typescript": "^5.7.0"
21
+ },
22
+ "files": [
23
+ "dist/",
24
+ "README.md"
25
+ ],
26
+ "license": "MIT",
27
+ "author": "BettsNation",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/bettsnation/paperclip-plugin-servicenow.git"
31
+ }
32
+ }