@tenonhq/dovetail-servicenow 0.0.2 → 0.0.3
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 +107 -6
- package/dist/cli.js +227 -5
- package/dist/client.d.ts +46 -1
- package/dist/client.js +113 -30
- package/dist/flowDesigner/buildFlowOrchestrator.d.ts +67 -0
- package/dist/flowDesigner/buildFlowOrchestrator.js +211 -0
- package/dist/flowDesigner/cloneActionType.d.ts +40 -0
- package/dist/flowDesigner/cloneActionType.js +135 -0
- package/dist/flowDesigner/cloneSubflow.d.ts +53 -0
- package/dist/flowDesigner/cloneSubflow.js +153 -0
- package/dist/flowDesigner/index.d.ts +19 -0
- package/dist/flowDesigner/index.js +29 -0
- package/dist/flowDesigner/listTemplates.d.ts +37 -0
- package/dist/flowDesigner/listTemplates.js +99 -0
- package/dist/flowDesigner/shape.d.ts +41 -0
- package/dist/flowDesigner/shape.js +84 -0
- package/dist/flowDesigner/triggerPublication.d.ts +50 -0
- package/dist/flowDesigner/triggerPublication.js +114 -0
- package/dist/flowDesigner/verifyArtifact.d.ts +47 -0
- package/dist/flowDesigner/verifyArtifact.js +110 -0
- package/dist/flowDesigner/writeOrder.d.ts +66 -0
- package/dist/flowDesigner/writeOrder.js +142 -0
- package/dist/flowDesigner-formatter.d.ts +6 -0
- package/dist/flowDesigner-formatter.js +75 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +22 -1
- package/dist/layout/formLayout.d.ts +20 -0
- package/dist/layout/formLayout.js +439 -0
- package/dist/layout/formatter.d.ts +12 -0
- package/dist/layout/formatter.js +43 -0
- package/dist/layout/layoutCommon.d.ts +94 -0
- package/dist/layout/layoutCommon.js +187 -0
- package/dist/layout/listLayout.d.ts +12 -0
- package/dist/layout/listLayout.js +167 -0
- package/dist/layout/relatedLists.d.ts +13 -0
- package/dist/layout/relatedLists.js +164 -0
- package/dist/layout/views.d.ts +12 -0
- package/dist/layout/views.js +38 -0
- package/dist/mcp/registry.d.ts +25 -0
- package/dist/mcp/registry.js +107 -0
- package/dist/mcp/schemas.d.ts +187 -0
- package/dist/mcp/schemas.js +60 -0
- package/dist/mcp/server.d.ts +20 -0
- package/dist/mcp/server.js +40 -0
- package/dist/types.d.ts +109 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -70,14 +70,14 @@ SN_PASSWORD=...
|
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
# Inline form
|
|
73
|
-
npx
|
|
73
|
+
npx dove-sn add-choices \
|
|
74
74
|
--table x_cadso_core_event \
|
|
75
75
|
--column state \
|
|
76
76
|
--update-set 0083c3bb33d003507b18bc534d5c7b6d \
|
|
77
77
|
--choices "delivered=Delivered,failed=Failed,expired=Expired"
|
|
78
78
|
|
|
79
79
|
# JSON payload form (recommended for >5 choices)
|
|
80
|
-
npx
|
|
80
|
+
npx dove-sn add-choices --from-json ./choices.json
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
JSON payload shape:
|
|
@@ -110,9 +110,110 @@ console.log(result.choices);
|
|
|
110
110
|
// ]
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
## Form, list & view layouts
|
|
114
|
+
|
|
115
|
+
The same query-to-diff, update-set-captured pattern now covers ServiceNow form
|
|
116
|
+
and list layouts. Four declarative, idempotent functions reconcile the `sys_ui_*`
|
|
117
|
+
tables — you describe the layout you want, the function writes only the delta.
|
|
118
|
+
|
|
119
|
+
| Function | What it sets | ServiceNow tables |
|
|
120
|
+
|----------|--------------|-------------------|
|
|
121
|
+
| `createView` | a named custom view | `sys_ui_view` |
|
|
122
|
+
| `setListLayout` | the columns of a list | `sys_ui_list`, `sys_ui_list_element` |
|
|
123
|
+
| `setFormLayout` | the sections + fields of a form | `sys_ui_form`, `sys_ui_form_section`, `sys_ui_section`, `sys_ui_element` |
|
|
124
|
+
| `setRelatedLists` | which related lists appear on a form | `sys_ui_related_list`, `sys_ui_related_list_entry` |
|
|
125
|
+
|
|
126
|
+
All four are **idempotent** (re-running reports every record `unchanged`),
|
|
127
|
+
**update-set-captured** (every create / update / delete lands in the update set
|
|
128
|
+
you pass — deletes pin the session update set first), and support **`dryRun`**
|
|
129
|
+
(plan the writes without performing them) and **`prune`** (default `true` —
|
|
130
|
+
delete records absent from your spec; pass `false` to only add / reorder).
|
|
131
|
+
|
|
132
|
+
An empty or omitted `view` targets the **Default view**. A named `view` that
|
|
133
|
+
does not exist yet is created automatically.
|
|
134
|
+
|
|
135
|
+
### CLI
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Create a custom view
|
|
139
|
+
npx dove-sn create-view --name sales_support --title "Sales Support" \
|
|
140
|
+
--update-set 0083c3bb33d003507b18bc534d5c7b6d
|
|
141
|
+
|
|
142
|
+
# Set a list layout (inline columns, or --from-json)
|
|
143
|
+
npx dove-sn set-list-layout \
|
|
144
|
+
--table x_cadso_automate_audience \
|
|
145
|
+
--columns "number,name,state" \
|
|
146
|
+
--update-set 0083c3bb33d003507b18bc534d5c7b6d \
|
|
147
|
+
--dry-run
|
|
148
|
+
|
|
149
|
+
# Set a form layout (sections are nested — pass a JSON spec)
|
|
150
|
+
npx dove-sn set-form-layout --from-json ./form.json
|
|
151
|
+
|
|
152
|
+
# Set the related lists shown on a form
|
|
153
|
+
npx dove-sn set-related-lists \
|
|
154
|
+
--table x_cadso_automate_audience \
|
|
155
|
+
--related-lists "x_cadso_automate_audience_member.audience" \
|
|
156
|
+
--update-set 0083c3bb33d003507b18bc534d5c7b6d
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`set-form-layout` JSON payload shape:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"table": "x_cadso_automate_audience",
|
|
164
|
+
"view": "",
|
|
165
|
+
"updateSetSysId": "0083c3bb33d003507b18bc534d5c7b6d",
|
|
166
|
+
"prune": true,
|
|
167
|
+
"sections": [
|
|
168
|
+
{ "fields": ["name", "active", "description"] },
|
|
169
|
+
{ "caption": "Meta Data", "fields": ["created_by", "updated_on"] }
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The first section is the **primary section** — omit its `caption`.
|
|
175
|
+
|
|
176
|
+
### Programmatic
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { createClient, setFormLayout, formatLayoutResult } from "@tenonhq/dovetail-servicenow";
|
|
180
|
+
|
|
181
|
+
var client = createClient({});
|
|
182
|
+
var result = await setFormLayout(client, {
|
|
183
|
+
table: "x_cadso_automate_audience",
|
|
184
|
+
view: "",
|
|
185
|
+
updateSetSysId: "0083c3bb33d003507b18bc534d5c7b6d",
|
|
186
|
+
sections: [
|
|
187
|
+
{ fields: ["name", "active"] },
|
|
188
|
+
{ caption: "Meta Data", fields: ["created_by"] }
|
|
189
|
+
]
|
|
190
|
+
});
|
|
191
|
+
console.log(formatLayoutResult("form layout", result));
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## MCP server
|
|
195
|
+
|
|
196
|
+
`dove-sn mcp` runs a self-contained MCP stdio server exposing the write tools to
|
|
197
|
+
Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
|
|
198
|
+
`set_related_lists`, and `add_choices_to_field`. It reads ServiceNow credentials
|
|
199
|
+
from the same env vars as the CLI.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npx dove-sn mcp --smoke # list the registered tools and exit
|
|
203
|
+
npx dove-sn mcp # run the stdio server (wire into .mcp.json)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`.mcp.json` entry:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{ "mcpServers": { "dovetail-servicenow": { "command": "npx", "args": ["dove-sn", "mcp"] } } }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
This server is separate from `@tenonhq/dovetail-mcp` (the read-only cross-system
|
|
213
|
+
aggregator) — `dovetail-servicenow`'s server is the ServiceNow **write** surface.
|
|
214
|
+
|
|
113
215
|
## Roadmap
|
|
114
216
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
`sys_trigger`
|
|
118
|
-
query to diff, write through the Dovetail REST API, report per-row actions.
|
|
217
|
+
The same query-to-diff pattern will continue across the rest of the
|
|
218
|
+
`sinch-dlr-manual-steps` work: indexes (`sys_db_object_ix`), table properties
|
|
219
|
+
(`accessible_from`), `sys_trigger` and `sys_property` creation.
|
package/dist/cli.js
CHANGED
|
@@ -60,6 +60,14 @@ const fs = __importStar(require("fs"));
|
|
|
60
60
|
const client_1 = require("./client");
|
|
61
61
|
const choices_1 = require("./choices");
|
|
62
62
|
const formatter_1 = require("./formatter");
|
|
63
|
+
const views_1 = require("./layout/views");
|
|
64
|
+
const listLayout_1 = require("./layout/listLayout");
|
|
65
|
+
const formLayout_1 = require("./layout/formLayout");
|
|
66
|
+
const relatedLists_1 = require("./layout/relatedLists");
|
|
67
|
+
const formatter_2 = require("./layout/formatter");
|
|
68
|
+
const server_1 = require("./mcp/server");
|
|
69
|
+
const buildFlowOrchestrator_1 = require("./flowDesigner/buildFlowOrchestrator");
|
|
70
|
+
const flowDesigner_formatter_1 = require("./flowDesigner-formatter");
|
|
63
71
|
function parseArgs(argv) {
|
|
64
72
|
var command = argv[0] || "";
|
|
65
73
|
var flags = {};
|
|
@@ -125,24 +133,238 @@ async function runAddChoices(flags) {
|
|
|
125
133
|
}
|
|
126
134
|
process.stdout.write((0, formatter_1.formatAddChoicesResult)(params.table, params.column, result) + "\n");
|
|
127
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* sinc-sn build-flow:
|
|
138
|
+
* --from-json <path> Required. JSON spec for the artifact (clone | create).
|
|
139
|
+
* --update-set <sys_id> Optional. Overrides spec.updateSetSysId at the CLI level.
|
|
140
|
+
* --dry-run Optional. Emit the planned write graph; do nothing.
|
|
141
|
+
* --skip-publish Optional. Skip the publish trigger entirely.
|
|
142
|
+
* --json Optional. Emit the structured BuildFlowResult instead of human text.
|
|
143
|
+
*
|
|
144
|
+
* Exit codes (mirror BuildFlowResult.outcome):
|
|
145
|
+
* 0 — done OR unchanged OR dry-run
|
|
146
|
+
* 2 — needs-ui-publish (writes ok, verify ok, publish degraded)
|
|
147
|
+
* 3 — verify-mismatch (writes ok but verify saw counts that don't match)
|
|
148
|
+
* 4 — write-failed (partial state in update set; discard to roll back)
|
|
149
|
+
* 5 — unrecoverable (spec or auth bug; never reached SN)
|
|
150
|
+
*/
|
|
151
|
+
async function runBuildFlowCmd(flags) {
|
|
152
|
+
if (!flags["from-json"]) {
|
|
153
|
+
process.stderr.write("build-flow: --from-json <path> is required\n");
|
|
154
|
+
return 5;
|
|
155
|
+
}
|
|
156
|
+
var raw;
|
|
157
|
+
try {
|
|
158
|
+
raw = JSON.parse(fs.readFileSync(flags["from-json"], "utf8"));
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
process.stderr.write("build-flow: failed to read/parse spec file: " + err.message + "\n");
|
|
162
|
+
return 5;
|
|
163
|
+
}
|
|
164
|
+
if (flags["update-set"] && raw && typeof raw === "object") {
|
|
165
|
+
raw.updateSetSysId = flags["update-set"];
|
|
166
|
+
}
|
|
167
|
+
var client = (0, client_1.createClient)({});
|
|
168
|
+
var result = await (0, buildFlowOrchestrator_1.runBuildFlow)(client, raw, {
|
|
169
|
+
dryRun: flags["dry-run"] === "true",
|
|
170
|
+
skipPublish: flags["skip-publish"] === "true",
|
|
171
|
+
});
|
|
172
|
+
if (flags.json === "true") {
|
|
173
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
process.stdout.write((0, flowDesigner_formatter_1.formatBuildFlowResult)(result) + "\n");
|
|
177
|
+
}
|
|
178
|
+
return result.exitCode;
|
|
179
|
+
}
|
|
180
|
+
/** Split a comma-separated CLI value into a trimmed, non-empty list. */
|
|
181
|
+
function splitList(raw) {
|
|
182
|
+
return raw
|
|
183
|
+
.split(",")
|
|
184
|
+
.map(function (v) { return v.trim(); })
|
|
185
|
+
.filter(function (v) { return v !== ""; });
|
|
186
|
+
}
|
|
187
|
+
async function runCreateView(flags) {
|
|
188
|
+
var params = {
|
|
189
|
+
name: flags.name,
|
|
190
|
+
updateSetSysId: flags["update-set"] || flags.updateSetSysId
|
|
191
|
+
};
|
|
192
|
+
if (!params.name || !params.updateSetSysId) {
|
|
193
|
+
throw new Error("create-view: --name and --update-set are required");
|
|
194
|
+
}
|
|
195
|
+
if (flags.title) {
|
|
196
|
+
params.title = flags.title;
|
|
197
|
+
}
|
|
198
|
+
if (flags.scope) {
|
|
199
|
+
params.scope = flags.scope;
|
|
200
|
+
}
|
|
201
|
+
if (flags["dry-run"] === "true") {
|
|
202
|
+
params.dryRun = true;
|
|
203
|
+
}
|
|
204
|
+
var result = await (0, views_1.createView)((0, client_1.createClient)({}), params);
|
|
205
|
+
if (flags.json === "true") {
|
|
206
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
process.stdout.write((0, formatter_2.formatCreateViewResult)(result) + "\n");
|
|
210
|
+
}
|
|
211
|
+
async function runSetListLayout(flags) {
|
|
212
|
+
var params;
|
|
213
|
+
if (flags["from-json"]) {
|
|
214
|
+
params = JSON.parse(fs.readFileSync(flags["from-json"], "utf8"));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
var table = flags.table;
|
|
218
|
+
var updateSetSysId = flags["update-set"] || flags.updateSetSysId;
|
|
219
|
+
var columns = flags.columns;
|
|
220
|
+
if (!table || !updateSetSysId || !columns) {
|
|
221
|
+
throw new Error("set-list-layout: --table, --update-set and --columns are required (or use --from-json)");
|
|
222
|
+
}
|
|
223
|
+
params = { table: table, updateSetSysId: updateSetSysId, columns: splitList(columns) };
|
|
224
|
+
if (flags.view) {
|
|
225
|
+
params.view = flags.view;
|
|
226
|
+
}
|
|
227
|
+
if (flags.scope) {
|
|
228
|
+
params.scope = flags.scope;
|
|
229
|
+
}
|
|
230
|
+
if (flags.parent) {
|
|
231
|
+
params.parent = flags.parent;
|
|
232
|
+
}
|
|
233
|
+
if (flags.prune === "false") {
|
|
234
|
+
params.prune = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (flags["dry-run"] === "true") {
|
|
238
|
+
params.dryRun = true;
|
|
239
|
+
}
|
|
240
|
+
var result = await (0, listLayout_1.setListLayout)((0, client_1.createClient)({}), params);
|
|
241
|
+
if (flags.json === "true") {
|
|
242
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
process.stdout.write((0, formatter_2.formatLayoutResult)("list layout", result) + "\n");
|
|
246
|
+
}
|
|
247
|
+
async function runSetFormLayout(flags) {
|
|
248
|
+
if (!flags["from-json"]) {
|
|
249
|
+
throw new Error("set-form-layout: --from-json <path> is required (sections are nested — pass a JSON spec)");
|
|
250
|
+
}
|
|
251
|
+
var params = JSON.parse(fs.readFileSync(flags["from-json"], "utf8"));
|
|
252
|
+
if (flags["update-set"]) {
|
|
253
|
+
params.updateSetSysId = flags["update-set"];
|
|
254
|
+
}
|
|
255
|
+
if (flags["dry-run"] === "true") {
|
|
256
|
+
params.dryRun = true;
|
|
257
|
+
}
|
|
258
|
+
var result = await (0, formLayout_1.setFormLayout)((0, client_1.createClient)({}), params);
|
|
259
|
+
if (flags.json === "true") {
|
|
260
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
process.stdout.write((0, formatter_2.formatLayoutResult)("form layout", result) + "\n");
|
|
264
|
+
}
|
|
265
|
+
async function runSetRelatedLists(flags) {
|
|
266
|
+
var params;
|
|
267
|
+
if (flags["from-json"]) {
|
|
268
|
+
params = JSON.parse(fs.readFileSync(flags["from-json"], "utf8"));
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
var table = flags.table;
|
|
272
|
+
var updateSetSysId = flags["update-set"] || flags.updateSetSysId;
|
|
273
|
+
var relatedLists = flags["related-lists"];
|
|
274
|
+
if (!table || !updateSetSysId || !relatedLists) {
|
|
275
|
+
throw new Error("set-related-lists: --table, --update-set and --related-lists are required (or use --from-json)");
|
|
276
|
+
}
|
|
277
|
+
params = { table: table, updateSetSysId: updateSetSysId, relatedLists: splitList(relatedLists) };
|
|
278
|
+
if (flags.view) {
|
|
279
|
+
params.view = flags.view;
|
|
280
|
+
}
|
|
281
|
+
if (flags.scope) {
|
|
282
|
+
params.scope = flags.scope;
|
|
283
|
+
}
|
|
284
|
+
if (flags.prune === "false") {
|
|
285
|
+
params.prune = false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (flags["dry-run"] === "true") {
|
|
289
|
+
params.dryRun = true;
|
|
290
|
+
}
|
|
291
|
+
var result = await (0, relatedLists_1.setRelatedLists)((0, client_1.createClient)({}), params);
|
|
292
|
+
if (flags.json === "true") {
|
|
293
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
process.stdout.write((0, formatter_2.formatLayoutResult)("related lists", result) + "\n");
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* dove-sn mcp — run the MCP stdio server. With --smoke, list the registered
|
|
300
|
+
* tools and exit. Otherwise the process stays alive until the transport closes.
|
|
301
|
+
*/
|
|
302
|
+
async function runMcp(flags) {
|
|
303
|
+
if (flags.smoke === "true") {
|
|
304
|
+
await (0, server_1.runSmoke)();
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
await (0, server_1.runStdio)();
|
|
308
|
+
await new Promise(function () { });
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
128
311
|
function printHelp() {
|
|
129
|
-
process.stdout.write("
|
|
312
|
+
process.stdout.write("dove-sn — ServiceNow platform helpers\n\n" +
|
|
130
313
|
"Commands:\n" +
|
|
131
|
-
" add-choices
|
|
314
|
+
" add-choices Upsert sys_choice rows for a table.column\n" +
|
|
315
|
+
" create-view Create a custom view (sys_ui_view)\n" +
|
|
316
|
+
" (--name <n> --update-set <sys_id> [--title <t>] [--scope <s>] [--dry-run] [--json])\n" +
|
|
317
|
+
" set-list-layout Set the columns of a list layout\n" +
|
|
318
|
+
" (--from-json <path> OR --table <t> --columns a,b,c --update-set <sys_id>\n" +
|
|
319
|
+
" [--view <v>] [--parent <t>] [--scope <s>] [--prune false] [--dry-run] [--json])\n" +
|
|
320
|
+
" set-form-layout Set the sections + fields of a form layout\n" +
|
|
321
|
+
" (--from-json <path> [--update-set <sys_id>] [--dry-run] [--json])\n" +
|
|
322
|
+
" set-related-lists Set which related lists appear on a form\n" +
|
|
323
|
+
" (--from-json <path> OR --table <t> --related-lists a,b --update-set <sys_id>\n" +
|
|
324
|
+
" [--view <v>] [--scope <s>] [--prune false] [--dry-run] [--json])\n" +
|
|
325
|
+
" build-flow Author Custom Action Types and Subflows from a JSON spec\n" +
|
|
326
|
+
" (--from-json <path> [--update-set <sys_id>] [--dry-run] [--skip-publish] [--json])\n" +
|
|
327
|
+
" mcp Run the MCP stdio server (--smoke lists tools and exits)\n");
|
|
132
328
|
}
|
|
133
329
|
async function main() {
|
|
134
330
|
var parsed = parseArgs(process.argv.slice(2));
|
|
135
331
|
if (parsed.command === "add-choices") {
|
|
136
332
|
await runAddChoices(parsed.flags);
|
|
137
|
-
return;
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
if (parsed.command === "build-flow") {
|
|
336
|
+
return await runBuildFlowCmd(parsed.flags);
|
|
337
|
+
}
|
|
338
|
+
if (parsed.command === "create-view") {
|
|
339
|
+
await runCreateView(parsed.flags);
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
if (parsed.command === "set-list-layout") {
|
|
343
|
+
await runSetListLayout(parsed.flags);
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
if (parsed.command === "set-form-layout") {
|
|
347
|
+
await runSetFormLayout(parsed.flags);
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
if (parsed.command === "set-related-lists") {
|
|
351
|
+
await runSetRelatedLists(parsed.flags);
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
if (parsed.command === "mcp") {
|
|
355
|
+
return await runMcp(parsed.flags);
|
|
138
356
|
}
|
|
139
357
|
if (!parsed.command || parsed.command === "help" || parsed.flags.help === "true") {
|
|
140
358
|
printHelp();
|
|
141
|
-
return;
|
|
359
|
+
return 0;
|
|
142
360
|
}
|
|
143
361
|
throw new Error("Unknown command: " + parsed.command);
|
|
144
362
|
}
|
|
145
|
-
main()
|
|
363
|
+
main()
|
|
364
|
+
.then(function (code) {
|
|
365
|
+
process.exit(code);
|
|
366
|
+
})
|
|
367
|
+
.catch(function (err) {
|
|
146
368
|
process.stderr.write("sinc-sn error: " + (err && err.message ? err.message : String(err)) + "\n");
|
|
147
369
|
process.exit(1);
|
|
148
370
|
});
|
package/dist/client.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ServiceNow REST client for @tenonhq/dovetail-servicenow.
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
4
|
+
* Provides three entry points:
|
|
5
5
|
* - `table.*` — read-only GETs against the native Table API
|
|
6
|
+
* - `buildAgent.*` — reads via the sn_build_agent API, with graceful
|
|
7
|
+
* fallback to Table API / sys_dictionary when the
|
|
8
|
+
* Build Agent plugin is unavailable
|
|
6
9
|
* - `claude.*` — writes via the Dovetail Scripted REST API
|
|
7
10
|
* (/api/cadso/dovetail/*), which handles update-set + scope
|
|
8
11
|
* switching atomically so every write lands in the right
|
|
@@ -21,6 +24,16 @@ export interface TableQueryOptions {
|
|
|
21
24
|
limit?: number;
|
|
22
25
|
fields?: string[];
|
|
23
26
|
}
|
|
27
|
+
export interface TableSchemaField {
|
|
28
|
+
name: string;
|
|
29
|
+
type: string;
|
|
30
|
+
mandatory: boolean;
|
|
31
|
+
reference_table: string | null;
|
|
32
|
+
}
|
|
33
|
+
export interface TableSchema {
|
|
34
|
+
fields: Array<TableSchemaField>;
|
|
35
|
+
primary_key: string;
|
|
36
|
+
}
|
|
24
37
|
export interface ServiceNowClient {
|
|
25
38
|
table: {
|
|
26
39
|
/** GET /api/now/table/<t>?sysparm_query=...&sysparm_limit=N — returns result array. */
|
|
@@ -29,6 +42,25 @@ export interface ServiceNowClient {
|
|
|
29
42
|
<T = Record<string, any>>(table: string, query: string, options: TableQueryOptions): Promise<Array<T>>;
|
|
30
43
|
};
|
|
31
44
|
};
|
|
45
|
+
buildAgent: {
|
|
46
|
+
/**
|
|
47
|
+
* GET /api/sn_build_agent/build_agent_api/runQuery/table/<t>/query/<encoded q>.
|
|
48
|
+
* Same shape as table.query. Falls back to table.query on 403/404 so the skill
|
|
49
|
+
* works on instances where sn_build_agent is not deployed or the caller lacks
|
|
50
|
+
* the Build Agent role. The fallback is transparent to the caller.
|
|
51
|
+
*/
|
|
52
|
+
runQuery: <T = Record<string, any>>(params: {
|
|
53
|
+
table: string;
|
|
54
|
+
query: string;
|
|
55
|
+
limit?: number;
|
|
56
|
+
}) => Promise<Array<T>>;
|
|
57
|
+
/**
|
|
58
|
+
* GET /api/sn_build_agent/build_agent_api/getTableSchema/<t>.
|
|
59
|
+
* On 403/404 falls back to a sys_dictionary query that synthesizes the same
|
|
60
|
+
* shape from element/internal_type/mandatory/reference_table fields.
|
|
61
|
+
*/
|
|
62
|
+
getTableSchema: (table: string) => Promise<TableSchema>;
|
|
63
|
+
};
|
|
32
64
|
claude: {
|
|
33
65
|
/** POST /api/cadso/dovetail/createRecord (legacy path: /api/cadso/claude/createRecord). */
|
|
34
66
|
createRecord: (params: {
|
|
@@ -56,6 +88,19 @@ export interface ServiceNowClient {
|
|
|
56
88
|
sys_id: string;
|
|
57
89
|
name: string;
|
|
58
90
|
}>;
|
|
91
|
+
/** GET /api/cadso/dovetail/changeUpdateSet?sysId=... — pins the REST session's active update set. */
|
|
92
|
+
changeUpdateSet: (params: {
|
|
93
|
+
sysId: string;
|
|
94
|
+
}) => Promise<{
|
|
95
|
+
[k: string]: any;
|
|
96
|
+
}>;
|
|
97
|
+
/** POST /api/cadso/dovetail/deleteRecord — body { table, sys_id }. Returns the deleted record. */
|
|
98
|
+
deleteRecord: (params: {
|
|
99
|
+
table: string;
|
|
100
|
+
sys_id: string;
|
|
101
|
+
}) => Promise<{
|
|
102
|
+
[k: string]: any;
|
|
103
|
+
}>;
|
|
59
104
|
};
|
|
60
105
|
}
|
|
61
106
|
export declare function createClient(config?: ServiceNowClientConfig): ServiceNowClient;
|
package/dist/client.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* ServiceNow REST client for @tenonhq/dovetail-servicenow.
|
|
4
4
|
*
|
|
5
|
-
* Provides
|
|
5
|
+
* Provides three entry points:
|
|
6
6
|
* - `table.*` — read-only GETs against the native Table API
|
|
7
|
+
* - `buildAgent.*` — reads via the sn_build_agent API, with graceful
|
|
8
|
+
* fallback to Table API / sys_dictionary when the
|
|
9
|
+
* Build Agent plugin is unavailable
|
|
7
10
|
* - `claude.*` — writes via the Dovetail Scripted REST API
|
|
8
11
|
* (/api/cadso/dovetail/*), which handles update-set + scope
|
|
9
12
|
* switching atomically so every write lands in the right
|
|
@@ -70,6 +73,18 @@ function sleep(ms) {
|
|
|
70
73
|
setTimeout(resolve, ms);
|
|
71
74
|
});
|
|
72
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Match a thrown error message against the 403/404 patterns produced by request().
|
|
78
|
+
* buildAgent.* uses this to decide when to fall back to the plain Table API.
|
|
79
|
+
*/
|
|
80
|
+
function isAccessOrMissing(err) {
|
|
81
|
+
var msg = err && err.message ? String(err.message) : "";
|
|
82
|
+
if (msg.indexOf("auth error 403") >= 0)
|
|
83
|
+
return true;
|
|
84
|
+
if (msg.indexOf("SN 404") >= 0)
|
|
85
|
+
return true;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
73
88
|
function createClient(config = {}) {
|
|
74
89
|
var host = resolveInstance(config);
|
|
75
90
|
var creds = resolveAuth(config);
|
|
@@ -168,37 +183,97 @@ function createClient(config = {}) {
|
|
|
168
183
|
throw e;
|
|
169
184
|
}
|
|
170
185
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
async function tableQueryHelper(table, query, limitOrOptions) {
|
|
187
|
+
var limit = 100;
|
|
188
|
+
var fields;
|
|
189
|
+
if (typeof limitOrOptions === "number") {
|
|
190
|
+
limit = limitOrOptions;
|
|
191
|
+
}
|
|
192
|
+
else if (limitOrOptions && typeof limitOrOptions === "object") {
|
|
193
|
+
if (typeof limitOrOptions.limit === "number") {
|
|
194
|
+
limit = limitOrOptions.limit;
|
|
195
|
+
}
|
|
196
|
+
if (limitOrOptions.fields && limitOrOptions.fields.length > 0) {
|
|
197
|
+
fields = limitOrOptions.fields;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
var params = {
|
|
201
|
+
sysparm_query: query,
|
|
202
|
+
sysparm_limit: limit,
|
|
203
|
+
sysparm_display_value: false
|
|
204
|
+
};
|
|
205
|
+
if (fields) {
|
|
206
|
+
params.sysparm_fields = fields.join(",");
|
|
207
|
+
}
|
|
208
|
+
var data = await request({
|
|
209
|
+
method: "GET",
|
|
210
|
+
url: "/api/now/table/" + encodeURIComponent(table),
|
|
211
|
+
params: params
|
|
212
|
+
}, "table.query(" + table + ")");
|
|
213
|
+
return data.result || [];
|
|
214
|
+
}
|
|
215
|
+
async function buildAgentRunQuery(params) {
|
|
216
|
+
var lim = params.limit != null ? params.limit : 100;
|
|
217
|
+
var ctx = "buildAgent.runQuery(" + params.table + ")";
|
|
218
|
+
try {
|
|
219
|
+
var data = await request({
|
|
220
|
+
method: "GET",
|
|
221
|
+
url: "/api/sn_build_agent/build_agent_api/runQuery/table/"
|
|
222
|
+
+ encodeURIComponent(params.table)
|
|
223
|
+
+ "/query/" + encodeURIComponent(params.query),
|
|
224
|
+
params: { sysparm_limit: lim }
|
|
225
|
+
}, ctx);
|
|
226
|
+
// build_agent endpoints may return { result: [...] } or [...] directly.
|
|
227
|
+
if (Array.isArray(data))
|
|
228
|
+
return data;
|
|
229
|
+
if (data && Array.isArray(data.result))
|
|
230
|
+
return data.result;
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (!isAccessOrMissing(err))
|
|
235
|
+
throw err;
|
|
236
|
+
return await tableQueryHelper(params.table, params.query, lim);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function buildAgentGetTableSchema(table) {
|
|
240
|
+
var ctx = "buildAgent.getTableSchema(" + table + ")";
|
|
241
|
+
try {
|
|
242
|
+
var data = await request({
|
|
243
|
+
method: "GET",
|
|
244
|
+
url: "/api/sn_build_agent/build_agent_api/getTableSchema/" + encodeURIComponent(table)
|
|
245
|
+
}, ctx);
|
|
246
|
+
var payload = data && data.result ? data.result : data;
|
|
247
|
+
if (payload && Array.isArray(payload.fields)) {
|
|
248
|
+
return {
|
|
249
|
+
fields: payload.fields,
|
|
250
|
+
primary_key: payload.primary_key || "sys_id"
|
|
191
251
|
};
|
|
192
|
-
if (fields) {
|
|
193
|
-
params.sysparm_fields = fields.join(",");
|
|
194
|
-
}
|
|
195
|
-
var data = await request({
|
|
196
|
-
method: "GET",
|
|
197
|
-
url: "/api/now/table/" + encodeURIComponent(table),
|
|
198
|
-
params: params
|
|
199
|
-
}, "table.query(" + table + ")");
|
|
200
|
-
return data.result || [];
|
|
201
252
|
}
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
if (!isAccessOrMissing(err))
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
// Fallback: derive from sys_dictionary.
|
|
259
|
+
var rows = await tableQueryHelper("sys_dictionary", "name=" + table + "^element!=NULL", 500);
|
|
260
|
+
var fields = rows.map(function (r) {
|
|
261
|
+
return {
|
|
262
|
+
name: r.element,
|
|
263
|
+
type: r.internal_type,
|
|
264
|
+
mandatory: r.mandatory === "true" || r.mandatory === true,
|
|
265
|
+
reference_table: r.reference_table || null
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
return { fields: fields, primary_key: "sys_id" };
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
table: {
|
|
272
|
+
query: tableQueryHelper
|
|
273
|
+
},
|
|
274
|
+
buildAgent: {
|
|
275
|
+
runQuery: buildAgentRunQuery,
|
|
276
|
+
getTableSchema: buildAgentGetTableSchema
|
|
202
277
|
},
|
|
203
278
|
claude: {
|
|
204
279
|
createRecord: async function (params) {
|
|
@@ -212,6 +287,14 @@ function createClient(config = {}) {
|
|
|
212
287
|
currentUpdateSet: async function (scope) {
|
|
213
288
|
var data = await dovetailRequest("GET", "currentUpdateSet", null, scope ? { scope: scope } : null, "claude.currentUpdateSet");
|
|
214
289
|
return data.result || data;
|
|
290
|
+
},
|
|
291
|
+
changeUpdateSet: async function (params) {
|
|
292
|
+
var data = await dovetailRequest("GET", "changeUpdateSet", null, { sysId: params.sysId }, "claude.changeUpdateSet");
|
|
293
|
+
return data.result || data;
|
|
294
|
+
},
|
|
295
|
+
deleteRecord: async function (params) {
|
|
296
|
+
var data = await dovetailRequest("POST", "deleteRecord", { table: params.table, sys_id: params.sys_id }, null, "claude.deleteRecord(" + params.table + ")");
|
|
297
|
+
return data.result || data;
|
|
215
298
|
}
|
|
216
299
|
}
|
|
217
300
|
};
|