@tenonhq/dovetail-servicenow 0.0.24 → 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/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;
@@ -12,6 +12,7 @@
12
12
  * create-table / add-column. To UPDATE an existing record, use `set-field`.
13
13
  */
14
14
  import type { ServiceNowClient } from "./client";
15
+ import type { RecordWriteResult } from "./setField";
15
16
  export interface CreateRecordParams {
16
17
  client?: ServiceNowClient;
17
18
  table: string;
@@ -25,15 +26,8 @@ export interface CreateRecordParams {
25
26
  ifAbsentQuery?: string;
26
27
  dryRun?: boolean;
27
28
  }
28
- export interface CreateRecordResult {
29
+ export interface CreateRecordResult extends RecordWriteResult {
29
30
  status: "dry-run" | "created" | "skipped" | "failed";
30
- table: string;
31
- sysId: string;
32
31
  scope: string;
33
- updateSetSysId: string;
34
- fields: Record<string, string>;
35
- after: Record<string, string>;
36
- verified: boolean;
37
- note: string;
38
32
  }
39
33
  export declare function createRecord(params: CreateRecordParams): Promise<CreateRecordResult>;
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
+ });
@@ -23,19 +23,28 @@ export interface SetFieldParams {
23
23
  updateSetSysId?: string;
24
24
  dryRun?: boolean;
25
25
  }
26
- export interface SetFieldResult {
27
- status: "dry-run" | "applied" | "failed";
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 {
28
34
  table: string;
29
35
  sysId: string;
30
36
  updateSetSysId: string;
31
37
  fields: Record<string, string>;
32
- before: Record<string, string>;
33
38
  after: Record<string, string>;
34
39
  verified: boolean;
35
40
  note: string;
36
41
  }
42
+ export interface SetFieldResult extends RecordWriteResult {
43
+ status: "dry-run" | "applied" | "failed";
44
+ before: Record<string, string>;
45
+ }
37
46
  /** Coerce a Table-API field value to a comparable string. Reference/display
38
47
  * fields (sysparm_display_value=false) come back as { link, value } objects. */
39
- export declare function fieldToString(value: any): string;
40
- export declare function pickFields(row: Record<string, any>, names: Array<string>): Record<string, string>;
48
+ export declare function fieldToString(value: unknown): string;
49
+ export declare function pickFields(row: Record<string, unknown>, names: Array<string>): Record<string, string>;
41
50
  export declare function setField(params: SetFieldParams): Promise<SetFieldResult>;
package/dist/setField.js CHANGED
@@ -25,7 +25,10 @@ function fieldToString(value) {
25
25
  if (value === undefined || value === null)
26
26
  return "";
27
27
  if (typeof value === "object") {
28
- return value.value !== undefined && value.value !== null ? String(value.value) : "";
28
+ if ("value" in value && value.value !== undefined && value.value !== null) {
29
+ return String(value.value);
30
+ }
31
+ return "";
29
32
  }
30
33
  return String(value);
31
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },