@tenonhq/dovetail-servicenow 0.0.23 → 0.0.25

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 CHANGED
@@ -253,6 +253,72 @@ adds diagnostics (app-switch status, resolved column key, assigned sys_id) to th
253
253
  result note. Ground truth (the HAR dissection) lives in the CTO repo's create-table
254
254
  docs.
255
255
 
256
+ ### Set a field on a record
257
+
258
+ Set scalar field value(s) on an **existing** data record, capture the change into
259
+ an update set, then read it back and verify.
260
+
261
+ ```bash
262
+ # Target by sys_id
263
+ npx dove-sn set-field \
264
+ --table x_cadso_core_metric_point_type --sys-id <sys_id> \
265
+ --fields "order=20" --update-set <sys_id> --dry-run --json
266
+
267
+ # Or target by a query that resolves to EXACTLY one row
268
+ npx dove-sn set-field \
269
+ --table x_cadso_core_metric_point_type --query "name=send_size" \
270
+ --fields "order=20,label=Send Size" --update-set <sys_id>
271
+ ```
272
+
273
+ `set-field` wraps the update-set-aware `pushWithUpdateSet` core op (update-set +
274
+ scope switching handled server-side, so no `sys_user_preference` is touched), then
275
+ re-queries the record and verifies each value landed. It **refuses** schema tables
276
+ (`sys_db_object` / `sys_dictionary`) — use `add-column` / `create-table` for those.
277
+ `--fields` is a comma-separated `key=value` map (values are sent as strings;
278
+ ServiceNow coerces); `--update-set` is required so the change is captured;
279
+ `--dry-run` reads the current values and prints the plan without writing. Exit
280
+ codes: `0` applied / dry-run, `1` bad args, `2` write landed but read-back did not
281
+ verify.
282
+
283
+ ### Create a record
284
+
285
+ Insert **one** new data record, owned by an explicit scope and captured into an
286
+ update set, then read it back and verify.
287
+
288
+ ```bash
289
+ npx dove-sn create-record \
290
+ --table x_cadso_core_metric_point_type \
291
+ --fields "name=avg_message_parts,label=Avg. Message Parts,order=35" \
292
+ --scope x_cadso_core --update-set <sys_id> \
293
+ --if-absent "name=avg_message_parts" --dry-run --json
294
+ ```
295
+
296
+ `create-record` wraps the scope- and update-set-aware `createRecord` core op, which
297
+ switches the executing user's app scope + update set server-side, inserts, and
298
+ restores both — so the record is owned by the right app and the insert is captured
299
+ in the right update set. Like `set-field` it **refuses** schema tables and verifies
300
+ via read-back. `--scope` and `--update-set` are required; `--if-absent
301
+ "<encoded-query>"` makes re-runs idempotent (the insert is skipped when the query
302
+ already matches a row). Exit codes: `0` created / skipped-in-sync / dry-run, `1` bad
303
+ args, `2` write landed unverified (or skipped with drift). To **update** an existing
304
+ record instead, use `set-field`.
305
+
306
+ Both verbs are exported for programmatic use:
307
+
308
+ ```ts
309
+ import { createClient, setField, createRecord } from "@tenonhq/dovetail-servicenow";
310
+
311
+ var client = createClient({});
312
+ var r = await setField({
313
+ client: client,
314
+ table: "x_cadso_core_metric_point_type",
315
+ sysId: "<sys_id>",
316
+ fields: { order: "20" },
317
+ updateSetSysId: "<sys_id>"
318
+ });
319
+ console.log(r.status, r.verified); // "applied" true
320
+ ```
321
+
256
322
  `test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
257
323
  declared variables?) that never runs the flow; `--execute --confirm` runs it via
258
324
  the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
@@ -323,8 +389,11 @@ console.log(formatLayoutResult("form layout", result));
323
389
 
324
390
  `dove-sn mcp` runs a self-contained MCP stdio server exposing the tools to
325
391
  Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
326
- `set_related_lists`, `add_choices_to_field`, plus the Flow Designer tools
327
- `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
392
+ `set_related_lists`, `add_choices_to_field`, the schema verbs `create_table` /
393
+ `add_column`, the record-write verbs `set_field` (update scalar fields on an
394
+ existing record) and `create_record` (insert one record) — both update-set-captured
395
+ and read-back-verified — `host_assets` (deploy a built dist/), plus the Flow Designer
396
+ tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
328
397
  type's model), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
329
398
  (copy a flow as an inactive draft), `flow_create` (create a NEW flow from scratch +
330
399
  publish, grafting a template), `flow_test` (validate or run a flow), and
package/dist/cli.js CHANGED
@@ -79,6 +79,8 @@ const editFlow_1 = require("./flowDesigner/editFlow");
79
79
  const editActionType_1 = require("./flowDesigner/editActionType");
80
80
  const testFlow_1 = require("./flowDesigner/testFlow");
81
81
  const table_1 = require("./table");
82
+ const setField_1 = require("./setField");
83
+ const createRecord_1 = require("./createRecord");
82
84
  const hostAssets_1 = require("./hostAssets");
83
85
  const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
84
86
  function parseArgs(argv) {
@@ -711,6 +713,12 @@ function printHelp() {
711
713
  " (--table <name|sys_id> --label <l> --type <t>\n" +
712
714
  " [--name <element>] [--max-length <n>] [--reference <table>]\n" +
713
715
  " [--scope <s>] [--update-set <sys_id>] [--dry-run] [--json])\n" +
716
+ " set-field Set scalar field value(s) on an EXISTING record, into an update set, then verify\n" +
717
+ " (--table <t> --sys-id <id>|--query <q> --fields \"k=v,k2=v2\"\n" +
718
+ " --update-set <sys_id> [--dry-run] [--json])\n" +
719
+ " create-record Create ONE NEW record in a data table, into an update set, then verify\n" +
720
+ " (--table <t> --fields \"k=v,k2=v2\" --scope <s> --update-set <sys_id>\n" +
721
+ " [--if-absent <encoded-query>] [--dry-run] [--json])\n" +
714
722
  " host-assets Deploy a built dist/ to ServiceNow (carrier sys_ui_script + attachment + m2m)\n" +
715
723
  " (--dir <dist> --app <sys_id> --scope <namespace>\n" +
716
724
  " [--update-set <sys_id>] [--max-bytes <n>] [--allow-oversize] [--dry-run] [--json])\n" +
@@ -875,6 +883,111 @@ async function runAddColumn(flags) {
875
883
  return 2;
876
884
  return 0;
877
885
  }
886
+ /** Parse inline `--fields "k=v, k2=v2"` into a field map. */
887
+ function parseFieldsInline(input) {
888
+ var out = {};
889
+ if (!input)
890
+ return out;
891
+ var parts = input.split(",");
892
+ for (var i = 0; i < parts.length; i += 1) {
893
+ var piece = parts[i].trim();
894
+ if (!piece)
895
+ continue;
896
+ var eq = piece.indexOf("=");
897
+ if (eq === -1)
898
+ continue;
899
+ var key = piece.slice(0, eq).trim();
900
+ if (key)
901
+ out[key] = piece.slice(eq + 1).trim();
902
+ }
903
+ return out;
904
+ }
905
+ /**
906
+ * dove-sn set-field:
907
+ * --table x_cadso_core_metric_point_type
908
+ * --sys-id <id> | --query "name=send_size" (query must resolve to exactly 1 row)
909
+ * --fields "order=20" (comma-separated key=value pairs)
910
+ * --update-set <sys_id> (required — the change is captured here)
911
+ * [--dry-run] [--json]
912
+ * Exit codes: 0 applied/dry-run, 1 bad args, 2 write landed but read-back unverified.
913
+ */
914
+ async function runSetField(flags) {
915
+ var table = flags.table;
916
+ var fields = parseFieldsInline(flags.fields || "");
917
+ var hasTarget = Boolean(flags["sys-id"] || flags.query);
918
+ if (!table || Object.keys(fields).length === 0 || !hasTarget || !flags["update-set"]) {
919
+ process.stderr.write("set-field: --table, --fields \"k=v\", one of --sys-id/--query, and --update-set are required\n");
920
+ return 1;
921
+ }
922
+ var params = {
923
+ client: (0, client_1.createClient)({}),
924
+ table: table,
925
+ fields: fields,
926
+ updateSetSysId: flags["update-set"]
927
+ };
928
+ if (flags["sys-id"])
929
+ params.sysId = flags["sys-id"];
930
+ if (flags.query)
931
+ params.query = flags.query;
932
+ if (flags["dry-run"] === "true")
933
+ params.dryRun = true;
934
+ var result = await (0, setField_1.setField)(params);
935
+ if (flags.json === "true") {
936
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
937
+ }
938
+ else {
939
+ process.stdout.write("[" + result.status + "] " + result.table + "/" + result.sysId + " "
940
+ + JSON.stringify(result.fields) + (result.verified ? " — verified" : "")
941
+ + "\n" + result.note + "\n");
942
+ }
943
+ if (result.status === "failed")
944
+ return 2;
945
+ return 0;
946
+ }
947
+ /**
948
+ * dove-sn create-record:
949
+ * --table x_cadso_core_metric_point_type
950
+ * --fields "name=avg_message_parts,label=Avg. Message Parts,order=35"
951
+ * --scope x_cadso_core (the app that owns the new record)
952
+ * --update-set <sys_id> (required — the insert is captured here)
953
+ * [--if-absent "name=avg_message_parts"] (skip the insert when this query already matches)
954
+ * [--dry-run] [--json]
955
+ * Exit codes: 0 created/skipped-in-sync/dry-run, 1 bad args, 2 write landed but read-back unverified
956
+ * (or skipped with drift).
957
+ */
958
+ async function runCreateRecord(flags) {
959
+ var table = flags.table;
960
+ var fields = parseFieldsInline(flags.fields || "");
961
+ if (!table || Object.keys(fields).length === 0 || !flags.scope || !flags["update-set"]) {
962
+ process.stderr.write("create-record: --table, --fields \"k=v\", --scope and --update-set are required\n");
963
+ return 1;
964
+ }
965
+ var params = {
966
+ client: (0, client_1.createClient)({}),
967
+ table: table,
968
+ fields: fields,
969
+ scope: flags.scope,
970
+ updateSetSysId: flags["update-set"]
971
+ };
972
+ if (flags["if-absent"])
973
+ params.ifAbsentQuery = flags["if-absent"];
974
+ if (flags["dry-run"] === "true")
975
+ params.dryRun = true;
976
+ var result = await (0, createRecord_1.createRecord)(params);
977
+ if (flags.json === "true") {
978
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
979
+ }
980
+ else {
981
+ process.stdout.write("[" + result.status + "] " + result.table + "/" + (result.sysId || "(new)") + " "
982
+ + JSON.stringify(result.fields) + (result.verified ? " — verified" : "")
983
+ + "\n" + result.note + "\n");
984
+ }
985
+ if (result.status === "failed")
986
+ return 2;
987
+ if (result.status === "skipped" && !result.verified)
988
+ return 2;
989
+ return 0;
990
+ }
878
991
  /**
879
992
  * dove-sn host-assets:
880
993
  * --dir <dist> Required. Path to the pre-built dist/ directory.
@@ -951,6 +1064,12 @@ async function main() {
951
1064
  if (parsed.command === "add-column") {
952
1065
  return await runAddColumn(parsed.flags);
953
1066
  }
1067
+ if (parsed.command === "set-field") {
1068
+ return await runSetField(parsed.flags);
1069
+ }
1070
+ if (parsed.command === "create-record") {
1071
+ return await runCreateRecord(parsed.flags);
1072
+ }
954
1073
  if (parsed.command === "host-assets") {
955
1074
  return await runHostAssets(parsed.flags);
956
1075
  }
package/dist/client.d.ts CHANGED
@@ -74,7 +74,7 @@ export interface ServiceNowClient {
74
74
  /** POST /api/cadso/dovetail_core/createRecord (legacy: /api/cadso/dovetail/createRecord). */
75
75
  createRecord: (params: {
76
76
  table: string;
77
- fields: Record<string, any>;
77
+ fields: Record<string, unknown>;
78
78
  scope?: string;
79
79
  update_set_sys_id?: string;
80
80
  sys_id?: string;
@@ -87,7 +87,7 @@ export interface ServiceNowClient {
87
87
  update_set_sys_id: string;
88
88
  table: string;
89
89
  record_sys_id: string;
90
- fields: Record<string, any>;
90
+ fields: Record<string, unknown>;
91
91
  }) => Promise<{
92
92
  sys_id: string;
93
93
  [k: string]: any;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * dove-sn create-record — create ONE new record in a data table, captured
3
+ * into a specified update set, then read back and verify.
4
+ *
5
+ * Wraps the Dovetail core Scripted REST `createRecord` op, which switches the
6
+ * executing user's app scope and update set server-side, inserts, and
7
+ * restores both — so the record is owned by the right app and the insert is
8
+ * captured in the right update set without touching sys_user_preference.
9
+ * The INSERT counterpart to `set-field`.
10
+ *
11
+ * NOT for schema tables (sys_db_object / sys_dictionary) — that's
12
+ * create-table / add-column. To UPDATE an existing record, use `set-field`.
13
+ */
14
+ import type { ServiceNowClient } from "./client";
15
+ import type { RecordWriteResult } from "./setField";
16
+ export interface CreateRecordParams {
17
+ client?: ServiceNowClient;
18
+ table: string;
19
+ /** Field name -> value for the new record. Values are sent as strings; ServiceNow coerces. */
20
+ fields: Record<string, string>;
21
+ /** App scope that will own the record. Required — session-scope stamping is the #1 wrong-scope cause. */
22
+ scope?: string;
23
+ /** Update set to capture the insert into. Required for a tracked write. */
24
+ updateSetSysId?: string;
25
+ /** Encoded query; when it already matches a row the insert is skipped (idempotent re-runs). */
26
+ ifAbsentQuery?: string;
27
+ dryRun?: boolean;
28
+ }
29
+ export interface CreateRecordResult extends RecordWriteResult {
30
+ status: "dry-run" | "created" | "skipped" | "failed";
31
+ scope: string;
32
+ }
33
+ export declare function createRecord(params: CreateRecordParams): Promise<CreateRecordResult>;
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * dove-sn create-record — create ONE new record in a data table, captured
4
+ * into a specified update set, then read back and verify.
5
+ *
6
+ * Wraps the Dovetail core Scripted REST `createRecord` op, which switches the
7
+ * executing user's app scope and update set server-side, inserts, and
8
+ * restores both — so the record is owned by the right app and the insert is
9
+ * captured in the right update set without touching sys_user_preference.
10
+ * The INSERT counterpart to `set-field`.
11
+ *
12
+ * NOT for schema tables (sys_db_object / sys_dictionary) — that's
13
+ * create-table / add-column. To UPDATE an existing record, use `set-field`.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.createRecord = createRecord;
17
+ const client_1 = require("./client");
18
+ const setField_1 = require("./setField");
19
+ // Same guard as set-field: schema tables are never written as data — routed to
20
+ // the dedicated schema verbs instead so we never orphan or corrupt metadata.
21
+ var REFUSED_TABLES = ["sys_db_object", "sys_dictionary"];
22
+ async function createRecord(params) {
23
+ var client = params.client || (0, client_1.createClient)({});
24
+ var table = params.table;
25
+ if (!table) {
26
+ throw new Error("create-record: --table is required.");
27
+ }
28
+ if (REFUSED_TABLES.indexOf(table) !== -1) {
29
+ throw new Error("create-record: refusing to write " + table + " as data — it is a schema table. "
30
+ + "Use add-column / create-table for schema changes.");
31
+ }
32
+ var fieldNames = params.fields ? Object.keys(params.fields) : [];
33
+ if (fieldNames.length === 0) {
34
+ throw new Error("create-record: at least one field (--fields key=value) is required.");
35
+ }
36
+ if (!params.scope) {
37
+ throw new Error("create-record: --scope is required so the record is owned by the right app.");
38
+ }
39
+ if (!params.updateSetSysId) {
40
+ throw new Error("create-record: --update-set <sys_id> is required so the insert is captured.");
41
+ }
42
+ var readFields = ["sys_id"].concat(fieldNames);
43
+ // Idempotent re-runs: skip the insert when the guard query already matches.
44
+ if (params.ifAbsentQuery) {
45
+ var existing = await client.table.query(table, params.ifAbsentQuery, {
46
+ limit: 1,
47
+ fields: readFields
48
+ });
49
+ if (existing.length > 0) {
50
+ var existingSysId = (0, setField_1.fieldToString)(existing[0].sys_id);
51
+ var existingValues = (0, setField_1.pickFields)(existing[0], fieldNames);
52
+ var matches = true;
53
+ for (var i = 0; i < fieldNames.length; i += 1) {
54
+ if (existingValues[fieldNames[i]] !== (0, setField_1.fieldToString)(params.fields[fieldNames[i]])) {
55
+ matches = false;
56
+ }
57
+ }
58
+ return {
59
+ status: "skipped",
60
+ table: table,
61
+ sysId: existingSysId,
62
+ scope: params.scope,
63
+ updateSetSysId: params.updateSetSysId,
64
+ fields: params.fields,
65
+ after: existingValues,
66
+ verified: matches,
67
+ note: matches
68
+ ? "skipped: --if-absent matched " + table + "/" + existingSysId
69
+ + " and its values already match."
70
+ : "skipped: --if-absent matched " + table + "/" + existingSysId
71
+ + " but its values differ from the requested fields — use set-field to update it."
72
+ };
73
+ }
74
+ }
75
+ if (params.dryRun) {
76
+ return {
77
+ status: "dry-run",
78
+ table: table,
79
+ sysId: "",
80
+ scope: params.scope,
81
+ updateSetSysId: params.updateSetSysId,
82
+ fields: params.fields,
83
+ after: {},
84
+ verified: false,
85
+ note: "dry-run: no write. Would create a " + table + " record with "
86
+ + JSON.stringify(params.fields) + " in scope " + params.scope
87
+ + ", captured into update set " + params.updateSetSysId + "."
88
+ };
89
+ }
90
+ // Insert via the scope- and update-set-aware core REST op.
91
+ var created = await client.claude.createRecord({
92
+ table: table,
93
+ fields: params.fields,
94
+ scope: params.scope,
95
+ update_set_sys_id: params.updateSetSysId
96
+ });
97
+ var sysId = (0, setField_1.fieldToString)(created && created.sys_id);
98
+ if (!sysId) {
99
+ return {
100
+ status: "failed",
101
+ table: table,
102
+ sysId: "",
103
+ scope: params.scope,
104
+ updateSetSysId: params.updateSetSysId,
105
+ fields: params.fields,
106
+ after: {},
107
+ verified: false,
108
+ note: "createRecord returned no sys_id — the insert may not have landed; check the instance."
109
+ };
110
+ }
111
+ // Read back and verify each field equals what we sent.
112
+ var afterRows = await client.table.query(table, "sys_id=" + sysId, {
113
+ limit: 1,
114
+ fields: readFields
115
+ });
116
+ var after = (0, setField_1.pickFields)(afterRows[0] || {}, fieldNames);
117
+ var verified = afterRows.length > 0;
118
+ for (var j = 0; j < fieldNames.length; j += 1) {
119
+ if (after[fieldNames[j]] !== (0, setField_1.fieldToString)(params.fields[fieldNames[j]])) {
120
+ verified = false;
121
+ }
122
+ }
123
+ return {
124
+ status: verified ? "created" : "failed",
125
+ table: table,
126
+ sysId: sysId,
127
+ scope: params.scope,
128
+ updateSetSysId: params.updateSetSysId,
129
+ fields: params.fields,
130
+ after: after,
131
+ verified: verified,
132
+ note: verified
133
+ ? "Created " + table + "/" + sysId + " and verified via read-back."
134
+ : "Insert landed as " + sysId
135
+ + " but read-back does not match the requested values — check field types / ACLs."
136
+ };
137
+ }
package/dist/index.d.ts CHANGED
@@ -21,3 +21,7 @@ export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFo
21
21
  export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams, ChunkRole, ChunkInfo, ChunkResult, PrunedResult, HostAssetsParams, HostAssetsResult } from "./types";
22
22
  export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, applyAddColumnOverlay } from "./table";
23
23
  export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec } from "./table";
24
+ export { setField } from "./setField";
25
+ export { createRecord } from "./createRecord";
26
+ export type { RecordWriteResult, SetFieldParams, SetFieldResult } from "./setField";
27
+ export type { CreateRecordParams, CreateRecordResult } from "./createRecord";
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * REST API so every change lands in the target update set and scope.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.applyAddColumnOverlay = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.createClient = void 0;
9
+ exports.createRecord = exports.setField = exports.applyAddColumnOverlay = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.createClient = void 0;
10
10
  var client_1 = require("./client");
11
11
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
12
12
  var createClientFromEnvFile_1 = require("./createClientFromEnvFile");
@@ -68,3 +68,7 @@ Object.defineProperty(exports, "DEFAULT_SAVE_ACTION", { enumerable: true, get: f
68
68
  Object.defineProperty(exports, "addColumn", { enumerable: true, get: function () { return table_1.addColumn; } });
69
69
  Object.defineProperty(exports, "deriveElement", { enumerable: true, get: function () { return table_1.deriveElement; } });
70
70
  Object.defineProperty(exports, "applyAddColumnOverlay", { enumerable: true, get: function () { return table_1.applyAddColumnOverlay; } });
71
+ var setField_1 = require("./setField");
72
+ Object.defineProperty(exports, "setField", { enumerable: true, get: function () { return setField_1.setField; } });
73
+ var createRecord_1 = require("./createRecord");
74
+ Object.defineProperty(exports, "createRecord", { enumerable: true, get: function () { return createRecord_1.createRecord; } });
@@ -10,7 +10,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import type { ToolAnnotations } from "@tenonhq/dovetail-mcp-kit";
12
12
  import type { ServiceNowClient } from "../client";
13
- export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "host_assets"];
13
+ export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_field", "create_record", "host_assets"];
14
14
  export type ToolName = typeof TOOL_NAMES[number];
15
15
  export interface RegistryDeps {
16
16
  /** Optional client injection for tests; defaults to createClient({}). */
@@ -27,6 +27,8 @@ const editFlow_1 = require("../flowDesigner/editFlow");
27
27
  const testFlow_1 = require("../flowDesigner/testFlow");
28
28
  const table_1 = require("../table");
29
29
  const hostAssets_1 = require("../hostAssets");
30
+ const setField_1 = require("../setField");
31
+ const createRecord_1 = require("../createRecord");
30
32
  const schemas_1 = require("./schemas");
31
33
  exports.TOOL_NAMES = [
32
34
  "create_view",
@@ -43,6 +45,8 @@ exports.TOOL_NAMES = [
43
45
  "flow_edit",
44
46
  "create_table",
45
47
  "add_column",
48
+ "set_field",
49
+ "create_record",
46
50
  "host_assets"
47
51
  ];
48
52
  // Annotation presets (READ_ONLY / WRITE_ADDITIVE_IDEMPOTENT / WRITE_CREATE /
@@ -296,6 +300,56 @@ function buildDescriptors(deps = {}) {
296
300
  });
297
301
  }
298
302
  },
303
+ {
304
+ name: "set_field",
305
+ annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
306
+ description: "Set scalar field value(s) on an EXISTING ServiceNow data record, captured into a specified "
307
+ + "update set, then READ BACK to verify each value landed. Wraps the update-set-aware "
308
+ + "pushWithUpdateSet core op (no sys_user_preference mutation). Target the record by sysId, or "
309
+ + "by a query that resolves to EXACTLY one row. REFUSES schema tables (sys_db_object / "
310
+ + "sys_dictionary) — use add_column / create_table for those. fields is a flat name->string map "
311
+ + "(sent as strings; ServiceNow coerces); updateSetSysId is required so the change is tracked; "
312
+ + "dryRun:true reads the current values and returns the plan without writing. To INSERT a new "
313
+ + "record use create_record.",
314
+ shape: schemas_1.setFieldSchema.shape,
315
+ handler: async function (args) {
316
+ var p = schemas_1.setFieldSchema.parse(args);
317
+ return (0, setField_1.setField)({
318
+ client: client(),
319
+ table: p.table,
320
+ sysId: p.sysId,
321
+ query: p.query,
322
+ fields: p.fields,
323
+ updateSetSysId: p.updateSetSysId,
324
+ dryRun: p.dryRun
325
+ });
326
+ }
327
+ },
328
+ {
329
+ name: "create_record",
330
+ annotations: dovetail_mcp_kit_1.WRITE_CREATE,
331
+ description: "Create ONE new ServiceNow data record, owned by an explicit app scope and captured into a "
332
+ + "specified update set, then READ BACK to verify. Wraps the scope- and update-set-aware "
333
+ + "createRecord core op (switches app scope + update set server-side, inserts, restores both — "
334
+ + "so the record lands in the right scope without sys_user_preference mutation). REFUSES schema "
335
+ + "tables (sys_db_object / sys_dictionary) — use create_table / add_column for those. fields is a "
336
+ + "flat name->string map; scope and updateSetSysId are required; ifAbsentQuery makes re-runs "
337
+ + "idempotent (skips the insert when it already matches a row); dryRun:true returns the plan "
338
+ + "without writing. To UPDATE an existing record use set_field.",
339
+ shape: schemas_1.createRecordSchema.shape,
340
+ handler: async function (args) {
341
+ var p = schemas_1.createRecordSchema.parse(args);
342
+ return (0, createRecord_1.createRecord)({
343
+ client: client(),
344
+ table: p.table,
345
+ fields: p.fields,
346
+ scope: p.scope,
347
+ updateSetSysId: p.updateSetSysId,
348
+ ifAbsentQuery: p.ifAbsentQuery,
349
+ dryRun: p.dryRun
350
+ });
351
+ }
352
+ },
299
353
  {
300
354
  name: "host_assets",
301
355
  annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
@@ -563,3 +563,47 @@ export declare var addColumnSchema: z.ZodObject<{
563
563
  saveActionSysId?: string | undefined;
564
564
  columnsRelId?: string | undefined;
565
565
  }>;
566
+ export declare var setFieldSchema: z.ZodObject<{
567
+ table: z.ZodString;
568
+ sysId: z.ZodOptional<z.ZodString>;
569
+ query: z.ZodOptional<z.ZodString>;
570
+ fields: z.ZodRecord<z.ZodString, z.ZodString>;
571
+ updateSetSysId: z.ZodString;
572
+ dryRun: z.ZodOptional<z.ZodBoolean>;
573
+ }, "strip", z.ZodTypeAny, {
574
+ table: string;
575
+ fields: Record<string, string>;
576
+ updateSetSysId: string;
577
+ query?: string | undefined;
578
+ sysId?: string | undefined;
579
+ dryRun?: boolean | undefined;
580
+ }, {
581
+ table: string;
582
+ fields: Record<string, string>;
583
+ updateSetSysId: string;
584
+ query?: string | undefined;
585
+ sysId?: string | undefined;
586
+ dryRun?: boolean | undefined;
587
+ }>;
588
+ export declare var createRecordSchema: z.ZodObject<{
589
+ table: z.ZodString;
590
+ fields: z.ZodRecord<z.ZodString, z.ZodString>;
591
+ scope: z.ZodString;
592
+ updateSetSysId: z.ZodString;
593
+ ifAbsentQuery: z.ZodOptional<z.ZodString>;
594
+ dryRun: z.ZodOptional<z.ZodBoolean>;
595
+ }, "strip", z.ZodTypeAny, {
596
+ table: string;
597
+ fields: Record<string, string>;
598
+ scope: string;
599
+ updateSetSysId: string;
600
+ dryRun?: boolean | undefined;
601
+ ifAbsentQuery?: string | undefined;
602
+ }, {
603
+ table: string;
604
+ fields: Record<string, string>;
605
+ scope: string;
606
+ updateSetSysId: string;
607
+ dryRun?: boolean | undefined;
608
+ ifAbsentQuery?: string | undefined;
609
+ }>;
@@ -4,7 +4,7 @@
4
4
  * own file so registry.ts stays focused on wiring.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
7
+ exports.createRecordSchema = exports.setFieldSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
8
8
  const zod_1 = require("zod");
9
9
  exports.createViewSchema = zod_1.z.object({
10
10
  name: zod_1.z.string().min(1),
@@ -156,3 +156,23 @@ exports.addColumnSchema = zod_1.z.object({
156
156
  dryRun: zod_1.z.boolean().optional(),
157
157
  debug: zod_1.z.boolean().optional()
158
158
  });
159
+ // Data-record write verbs. Kept as plain z.object (no .refine wrapper) so
160
+ // registry.ts can read `.shape`; the deeper rules — one of sysId/query, at
161
+ // least one field, the schema-table refusal — are enforced by the core
162
+ // setField / createRecord functions, which throw clear errors.
163
+ exports.setFieldSchema = zod_1.z.object({
164
+ table: zod_1.z.string().min(1),
165
+ sysId: zod_1.z.string().optional(),
166
+ query: zod_1.z.string().optional(),
167
+ fields: zod_1.z.record(zod_1.z.string()),
168
+ updateSetSysId: zod_1.z.string().min(1),
169
+ dryRun: zod_1.z.boolean().optional()
170
+ });
171
+ exports.createRecordSchema = zod_1.z.object({
172
+ table: zod_1.z.string().min(1),
173
+ fields: zod_1.z.record(zod_1.z.string()),
174
+ scope: zod_1.z.string().min(1),
175
+ updateSetSysId: zod_1.z.string().min(1),
176
+ ifAbsentQuery: zod_1.z.string().optional(),
177
+ dryRun: zod_1.z.boolean().optional()
178
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * dove-sn set-field — set scalar field value(s) on an EXISTING ServiceNow
3
+ * record, captured into a specified update set, then read back and verify.
4
+ *
5
+ * Wraps the Dovetail core Scripted REST `pushWithUpdateSet` op (update-set +
6
+ * scope switching handled atomically server-side), so the change lands in the
7
+ * right update set without touching sys_user_preference. This is the
8
+ * change-and-KEEP counterpart to sn-capture-fields' change-and-revert capture.
9
+ *
10
+ * NOT for schema tables (sys_db_object / sys_dictionary) — that's add-column /
11
+ * create-table. To INSERT a new record, use `dove create`.
12
+ */
13
+ import type { ServiceNowClient } from "./client";
14
+ export interface SetFieldParams {
15
+ client?: ServiceNowClient;
16
+ table: string;
17
+ /** Target record by sys_id, OR by a query that resolves to EXACTLY one row. */
18
+ sysId?: string;
19
+ query?: string;
20
+ /** Field name -> value to set. Values are sent as strings; ServiceNow coerces. */
21
+ fields: Record<string, string>;
22
+ /** Update set to capture the change into. Required for a tracked write. */
23
+ updateSetSysId?: string;
24
+ dryRun?: boolean;
25
+ }
26
+ /**
27
+ * Fields common to every record-write verb's result: the target, the update set
28
+ * the change was captured into, the requested field map, the read-back values,
29
+ * whether the read-back matched, and a human note. `set-field` and
30
+ * `create-record` each extend this with their own `status` union (and
31
+ * set-field's `before` snapshot / create-record's owning `scope`).
32
+ */
33
+ export interface RecordWriteResult {
34
+ table: string;
35
+ sysId: string;
36
+ updateSetSysId: string;
37
+ fields: Record<string, string>;
38
+ after: Record<string, string>;
39
+ verified: boolean;
40
+ note: string;
41
+ }
42
+ export interface SetFieldResult extends RecordWriteResult {
43
+ status: "dry-run" | "applied" | "failed";
44
+ before: Record<string, string>;
45
+ }
46
+ /** Coerce a Table-API field value to a comparable string. Reference/display
47
+ * fields (sysparm_display_value=false) come back as { link, value } objects. */
48
+ export declare function fieldToString(value: unknown): string;
49
+ export declare function pickFields(row: Record<string, unknown>, names: Array<string>): Record<string, string>;
50
+ export declare function setField(params: SetFieldParams): Promise<SetFieldResult>;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /**
3
+ * dove-sn set-field — set scalar field value(s) on an EXISTING ServiceNow
4
+ * record, captured into a specified update set, then read back and verify.
5
+ *
6
+ * Wraps the Dovetail core Scripted REST `pushWithUpdateSet` op (update-set +
7
+ * scope switching handled atomically server-side), so the change lands in the
8
+ * right update set without touching sys_user_preference. This is the
9
+ * change-and-KEEP counterpart to sn-capture-fields' change-and-revert capture.
10
+ *
11
+ * NOT for schema tables (sys_db_object / sys_dictionary) — that's add-column /
12
+ * create-table. To INSERT a new record, use `dove create`.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.fieldToString = fieldToString;
16
+ exports.pickFields = pickFields;
17
+ exports.setField = setField;
18
+ const client_1 = require("./client");
19
+ // Platform/schema tables that must not be written as data — routed to the
20
+ // dedicated schema verbs instead so we never orphan or corrupt metadata.
21
+ var REFUSED_TABLES = ["sys_db_object", "sys_dictionary"];
22
+ /** Coerce a Table-API field value to a comparable string. Reference/display
23
+ * fields (sysparm_display_value=false) come back as { link, value } objects. */
24
+ function fieldToString(value) {
25
+ if (value === undefined || value === null)
26
+ return "";
27
+ if (typeof value === "object") {
28
+ if ("value" in value && value.value !== undefined && value.value !== null) {
29
+ return String(value.value);
30
+ }
31
+ return "";
32
+ }
33
+ return String(value);
34
+ }
35
+ function pickFields(row, names) {
36
+ var out = {};
37
+ for (var i = 0; i < names.length; i += 1) {
38
+ out[names[i]] = fieldToString(row ? row[names[i]] : undefined);
39
+ }
40
+ return out;
41
+ }
42
+ async function setField(params) {
43
+ var client = params.client || (0, client_1.createClient)({});
44
+ var table = params.table;
45
+ if (!table) {
46
+ throw new Error("set-field: --table is required.");
47
+ }
48
+ if (REFUSED_TABLES.indexOf(table) !== -1) {
49
+ throw new Error("set-field: refusing to write " + table + " as data — it is a schema table. "
50
+ + "Use add-column / create-table for schema changes.");
51
+ }
52
+ var fieldNames = params.fields ? Object.keys(params.fields) : [];
53
+ if (fieldNames.length === 0) {
54
+ throw new Error("set-field: at least one field (--fields key=value) is required.");
55
+ }
56
+ if (!params.updateSetSysId) {
57
+ throw new Error("set-field: --update-set <sys_id> is required so the change is captured.");
58
+ }
59
+ // Resolve the target sys_id (explicit, or a single-match query).
60
+ var sysId = params.sysId;
61
+ if (!sysId) {
62
+ if (!params.query) {
63
+ throw new Error("set-field: one of --sys-id or --query is required.");
64
+ }
65
+ var matches = await client.table.query(table, params.query, { limit: 2, fields: ["sys_id"] });
66
+ if (matches.length === 0) {
67
+ throw new Error("set-field: --query matched no rows on " + table + ".");
68
+ }
69
+ if (matches.length > 1) {
70
+ throw new Error("set-field: --query matched 2+ rows on " + table + " — refine to exactly one.");
71
+ }
72
+ sysId = fieldToString(matches[0].sys_id);
73
+ }
74
+ // Read current values (also confirms the record exists).
75
+ var readFields = ["sys_id"].concat(fieldNames);
76
+ var beforeRows = await client.table.query(table, "sys_id=" + sysId, { limit: 1, fields: readFields });
77
+ if (beforeRows.length === 0) {
78
+ throw new Error("set-field: no record " + sysId + " found on " + table + ".");
79
+ }
80
+ var before = pickFields(beforeRows[0], fieldNames);
81
+ if (params.dryRun) {
82
+ return {
83
+ status: "dry-run",
84
+ table: table,
85
+ sysId: sysId,
86
+ updateSetSysId: params.updateSetSysId,
87
+ fields: params.fields,
88
+ before: before,
89
+ after: before,
90
+ verified: false,
91
+ note: "dry-run: no write. Would set " + JSON.stringify(params.fields)
92
+ + " on " + table + "/" + sysId + " into update set " + params.updateSetSysId + "."
93
+ };
94
+ }
95
+ // Write via the update-set-aware core REST op.
96
+ await client.claude.pushWithUpdateSet({
97
+ update_set_sys_id: params.updateSetSysId,
98
+ table: table,
99
+ record_sys_id: sysId,
100
+ fields: params.fields
101
+ });
102
+ // Read back and verify each field equals what we set.
103
+ var afterRows = await client.table.query(table, "sys_id=" + sysId, { limit: 1, fields: readFields });
104
+ var after = pickFields(afterRows[0] || {}, fieldNames);
105
+ var verified = true;
106
+ for (var j = 0; j < fieldNames.length; j += 1) {
107
+ if (after[fieldNames[j]] !== fieldToString(params.fields[fieldNames[j]])) {
108
+ verified = false;
109
+ }
110
+ }
111
+ return {
112
+ status: verified ? "applied" : "failed",
113
+ table: table,
114
+ sysId: sysId,
115
+ updateSetSysId: params.updateSetSysId,
116
+ fields: params.fields,
117
+ before: before,
118
+ after: after,
119
+ verified: verified,
120
+ note: verified
121
+ ? "Set " + fieldNames.join(", ") + " on " + table + "/" + sysId + " and verified via read-back."
122
+ : "Write landed but read-back does not match the requested values — check field types / ACLs."
123
+ };
124
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },