@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.
Files changed (46) hide show
  1. package/README.md +107 -6
  2. package/dist/cli.js +227 -5
  3. package/dist/client.d.ts +46 -1
  4. package/dist/client.js +113 -30
  5. package/dist/flowDesigner/buildFlowOrchestrator.d.ts +67 -0
  6. package/dist/flowDesigner/buildFlowOrchestrator.js +211 -0
  7. package/dist/flowDesigner/cloneActionType.d.ts +40 -0
  8. package/dist/flowDesigner/cloneActionType.js +135 -0
  9. package/dist/flowDesigner/cloneSubflow.d.ts +53 -0
  10. package/dist/flowDesigner/cloneSubflow.js +153 -0
  11. package/dist/flowDesigner/index.d.ts +19 -0
  12. package/dist/flowDesigner/index.js +29 -0
  13. package/dist/flowDesigner/listTemplates.d.ts +37 -0
  14. package/dist/flowDesigner/listTemplates.js +99 -0
  15. package/dist/flowDesigner/shape.d.ts +41 -0
  16. package/dist/flowDesigner/shape.js +84 -0
  17. package/dist/flowDesigner/triggerPublication.d.ts +50 -0
  18. package/dist/flowDesigner/triggerPublication.js +114 -0
  19. package/dist/flowDesigner/verifyArtifact.d.ts +47 -0
  20. package/dist/flowDesigner/verifyArtifact.js +110 -0
  21. package/dist/flowDesigner/writeOrder.d.ts +66 -0
  22. package/dist/flowDesigner/writeOrder.js +142 -0
  23. package/dist/flowDesigner-formatter.d.ts +6 -0
  24. package/dist/flowDesigner-formatter.js +75 -0
  25. package/dist/index.d.ts +9 -2
  26. package/dist/index.js +22 -1
  27. package/dist/layout/formLayout.d.ts +20 -0
  28. package/dist/layout/formLayout.js +439 -0
  29. package/dist/layout/formatter.d.ts +12 -0
  30. package/dist/layout/formatter.js +43 -0
  31. package/dist/layout/layoutCommon.d.ts +94 -0
  32. package/dist/layout/layoutCommon.js +187 -0
  33. package/dist/layout/listLayout.d.ts +12 -0
  34. package/dist/layout/listLayout.js +167 -0
  35. package/dist/layout/relatedLists.d.ts +13 -0
  36. package/dist/layout/relatedLists.js +164 -0
  37. package/dist/layout/views.d.ts +12 -0
  38. package/dist/layout/views.js +38 -0
  39. package/dist/mcp/registry.d.ts +25 -0
  40. package/dist/mcp/registry.js +107 -0
  41. package/dist/mcp/schemas.d.ts +187 -0
  42. package/dist/mcp/schemas.js +60 -0
  43. package/dist/mcp/server.d.ts +20 -0
  44. package/dist/mcp/server.js +40 -0
  45. package/dist/types.d.ts +109 -0
  46. 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 sinc-sn add-choices \
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 sinc-sn add-choices --from-json ./choices.json
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
- Same package will grow to cover the rest of the `sinch-dlr-manual-steps`
116
- patterns: indexes (`sys_db_object_ix`), table properties (`accessible_from`),
117
- `sys_trigger` creation, `sys_property` creation. Pattern stays identical —
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("sinc-sn — ServiceNow helpers\n\n" +
312
+ process.stdout.write("dove-sn — ServiceNow platform helpers\n\n" +
130
313
  "Commands:\n" +
131
- " add-choices Upsert sys_choice rows for a table.column (see --help in source)\n");
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().catch(function (err) {
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 two entry points:
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 two entry points:
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
- return {
172
- table: {
173
- query: async function (table, query, limitOrOptions) {
174
- var limit = 100;
175
- var fields;
176
- if (typeof limitOrOptions === "number") {
177
- limit = limitOrOptions;
178
- }
179
- else if (limitOrOptions && typeof limitOrOptions === "object") {
180
- if (typeof limitOrOptions.limit === "number") {
181
- limit = limitOrOptions.limit;
182
- }
183
- if (limitOrOptions.fields && limitOrOptions.fields.length > 0) {
184
- fields = limitOrOptions.fields;
185
- }
186
- }
187
- var params = {
188
- sysparm_query: query,
189
- sysparm_limit: limit,
190
- sysparm_display_value: false
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
  };