node-autotask 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/ARCHITECTURE.md +10 -4
  2. package/MCP_TOOL_CATALOG.json +2 -2
  3. package/MCP_TOOL_MANIFEST.md +4 -4
  4. package/N8N_MCP_MANIFEST.json +2 -2
  5. package/N8N_MCP_MANIFEST.md +3 -3
  6. package/README.md +36 -0
  7. package/capabilities.json +1571 -1571
  8. package/dist/capabilities.cjs +2 -2
  9. package/dist/capabilities.js +1 -1
  10. package/dist/{chunk-EWMG3HHU.cjs → chunk-5O5EPYBO.cjs} +1570 -1570
  11. package/dist/{chunk-L3TK4CN5.cjs → chunk-7I3F3AJJ.cjs} +429 -419
  12. package/dist/{chunk-VCGC6YCI.cjs → chunk-G564CHXM.cjs} +135 -135
  13. package/dist/{chunk-NB254T3F.js → chunk-HXOWEYV3.js} +49 -39
  14. package/dist/{chunk-JIDR3NPV.js → chunk-VV2UUW2J.js} +1570 -1570
  15. package/dist/{chunk-U6JUHHMB.js → chunk-VYUZZIEE.js} +66 -32
  16. package/dist/{chunk-GWALLMAM.js → chunk-W2WSZOZA.js} +2 -2
  17. package/dist/{chunk-MAYYTYV5.cjs → chunk-WRWX6WIK.cjs} +68 -34
  18. package/dist/{index-CUsvZndu.d.ts → index-BqPY04ZG.d.ts} +89 -16
  19. package/dist/{index-leMyBJze.d.cts → index-D7x3MkLK.d.cts} +89 -16
  20. package/dist/index.cjs +24 -22
  21. package/dist/index.d.cts +2 -2
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.js +6 -4
  24. package/dist/operations/index.cjs +178 -105
  25. package/dist/operations/index.d.cts +22 -7
  26. package/dist/operations/index.d.ts +22 -7
  27. package/dist/operations/index.js +77 -4
  28. package/dist/pool.cjs +8 -8
  29. package/dist/pool.d.cts +1 -1
  30. package/dist/pool.d.ts +1 -1
  31. package/dist/pool.js +4 -4
  32. package/dist/resources/index.cjs +4 -4
  33. package/dist/resources/index.d.cts +1 -1
  34. package/dist/resources/index.d.ts +1 -1
  35. package/dist/resources/index.js +3 -3
  36. package/docs/API.md +78 -15
  37. package/examples/basic.ts +2 -2
  38. package/examples/mcp-server-http.ts +5 -4
  39. package/examples/mcp-server.ts +3 -3
  40. package/package.json +3 -2
package/docs/API.md CHANGED
@@ -81,15 +81,15 @@ flow.
81
81
  Native global `fetch` (Node >= 24) — zero runtime dependencies across the
82
82
  whole package. `HttpClient.request<T>(opts): Promise<T>` is the single
83
83
  transport primitive (`RequestOptions { method, path, query?, body?, headers? }`):
84
- JSON bodies, retries with backoff (budget `maxRetries`) in two legs (issue
85
- #105): a 429 (a pre-processing refusal — the vendor rejected the call before
86
- executing it) is retried per `Retry-After` on the pre-#105 retriable set
84
+ JSON bodies, retries with backoff (budget `maxRetries`) in three legs
85
+ (issues #105/#109): a 429 (a pre-processing refusal — the vendor rejected
86
+ the call before executing it) is retried per `Retry-After` on the pre-#105 retriable set
87
87
  (GET/PUT/DELETE + idempotent-flagged calls — an unflagged POST/PATCH surfaces
88
88
  a 429 at once, as before #105), while 5xx/network errors are auto-retried only
89
89
  for GET and idempotent-flagged calls: a mutation makes exactly one send on a
90
90
  5xx or network error (the write-outcome is unknown once the body was written);
91
91
  an explicit `idempotent: true` on a mutation opts back into the pre-#105
92
- auto-retry on both legs — plus a concurrency guard (`maxConcurrent`, default 10 — the API
92
+ auto-retry on all legs — plus a concurrency guard (`maxConcurrent`, default 10 — the API
93
93
  allows <= 10 concurrent), one correlation id per request (surfaced on every
94
94
  error and to `onAudit`), and the opt-in client-side hourly budget tracker
95
95
  (`rateLimit.perHour`, default off — a sliding one-hour window that waits for a slot within the call deadline, else throws `RateLimitError` with `retryAfter`; the API allows 10,000 req/hour). The tracker is PER PROCESS: it counts only this client's attempts and cannot see other instances or integrations on the tenant (`thresholdInformation.get()` is the only cross-integration signal; the local count is `client.hourlyBudget()`, section 3.2.1). Under the `agent` config profile the exhaustion `suggestedAction` says stop or narrow the query — never raise the limit.
@@ -107,7 +107,22 @@ always, personal/business-sensitive data by default.
107
107
  `code` (stable SCREAMING_SNAKE machine code), `category`, `retryable`,
108
108
  `operation`, `httpStatus`, `vendorError` (raw vendor body under a documented
109
109
  key), `resourceIds`, `suggestedAction` (deterministic next step only),
110
- `correlationId`, `url`. The vendor documents no error bodies; the shape
110
+ `correlationId`, `url`.
111
+ **`resourceIds` is the LEGACY, unlabeled id list (issue #116).** The stamp
112
+ sites name records of SEVERAL vendor entities — the failed record's own id
113
+ on a `get` miss (any entity), ticket / company / contact ids on the special
114
+ ops, a billing-code id on the `time_entries` allocation failures, staff ids
115
+ on the assignee/owner prechecks — and the field never says which. Every
116
+ SDK stamp site that sets `resourceIds` also sets `details.idRefs` — the
117
+ same ids, each labeled `{ entity, id }` with the vendor entity name
118
+ (singular
119
+ snake_case — `company`, `ticket`, `contact`, `billing_code`, `resource`
120
+ (staff), the same word the error message uses; the pairing is a guarantee
121
+ of the SDK stamp sites — a consumer-constructed error may set `resourceIds`
122
+ without `idRefs`). Machine consumers key on `idRefs`; `resourceIds` is kept
123
+ for compatibility, and neither is ever the input to `expandResourceNames`
124
+ (staff ids only — a colliding non-staff id resolves to an unrelated
125
+ person's name, section 7.7). The vendor documents no error bodies; the shape
111
126
  `{ errors: [{ message, field?, code? }] }` is pinned from the node's
112
127
  errorHandler and parsed defensively (`parseVendorErrors`,
113
128
  `formatVendorDetail`). `errorFromStatus(status, body, url?, operation?,
@@ -151,8 +166,10 @@ served payload verbatim).
151
166
 
152
167
  `buildFilter(conditions)` wraps caller conditions (multiple top-level
153
168
  conditions are `and`-wrapped); `validateFilter` enforces the vendor caps
154
- (section 5 below). `buildQueryRequest` produces the `QueryModel` wire body
155
- (`{ MaxRecords, IncludeFields?, filter }`). `normalizedConditions` is the
169
+ (section 5 below). `buildQueryBody` produces the `QueryModel` wire body
170
+ (`{ MaxRecords, IncludeFields?, filter }`, `id` appended to a caller
171
+ `includeFields`), shared by page 1 (`buildQueryRequest`) and every cursor
172
+ continuation of a walk (issue #118). `normalizedConditions` is the
156
173
  shared filter-shape gate of the query family (every query-family entry
157
174
  point routes through it pre-request): the `filter` param accepts a
158
175
  conditions array or `{ filter: [...] }`, and `undefined`/`null` are the
@@ -245,7 +262,7 @@ status → what the vendor does that triggers it.
245
262
  | `UnexpectedContentTypeError` | `UNEXPECTED_CONTENT_TYPE` | server | 2xx (non-JSON body + a non-JSON-ish `Content-Type`) | the edge serves a non-JSON page (maintenance) — the vendor never processed the request; `details = { httpStatus, contentType, bodySnippet (256 B) }` — retryable (advisory). A maintenance-notice page is waited out up to `maintenanceMaxWaitMs` (default 1 h) for reads AND writes; on exhaustion `details.maintenance` reports the wait (section 2.1) |
246
263
  | `EmptyResponseError` | `EMPTY_RESPONSE` | server | 2xx (empty body, `expectBody`) | a blank 2xx on a call that expects a result (non-DELETE by default — section 2.1); `details = { httpStatus, method }` — retryable for non-DELETE (advisory: the loop never retries 2xx) |
247
264
  | `CircuitBreakerError` | `CIRCUIT_BREAKER_OPEN` | policy | — (call not sent) | the host circuit breaker (issue #26, opt-in) is open — refused LOCALLY before the request (zero upstream calls, no hourly stamp) — `notSent: true`, `retryAfter` = remaining open seconds — **retryable: true** (the deliberate divergence from #6's not-retryable cooldown refusal: the open period is bounded and advertised; section 3.2.2) |
248
- | `ResolutionError` | `RESOLUTION_AMBIGUOUS` / `RESOLUTION_TRUNCATED` / `NOT_FOUND` | resolution | — | `resolve`: multiple exact matches (candidates in `resourceIds`); a cap-tripped bounded client scan (`resolution.scanOnMiss` only; never a silent `null`); a numeric-id miss |
265
+ | `ResolutionError` | `RESOLUTION_AMBIGUOUS` / `RESOLUTION_TRUNCATED` / `NOT_FOUND` | resolution | — | `resolve`: multiple exact matches (candidates in `resourceIds`, labeled in `details.idRefs`); a cap-tripped bounded client scan (`resolution.scanOnMiss` only; never a silent `null`); a numeric-id miss |
249
266
  | `PaginationLimitError` | `PAGINATION_LIMIT` | policy | — | a page walk that would exceed the 1000-page guard (`suggestedAction` names the tighter filter) |
250
267
  | `QueryTooLongError` | `QUERY_TOO_LONG` | policy | — | caller-requested `via: 'get'` whose serialized URL would exceed the 2048-char GET cap — the SDK never silently degrades to POST |
251
268
  | `StaleObjectError` | `STALE_OBJECT` | conflict | — | the caller-supplied `lastModifiedDate` no longer matches the vendor's current value (no `If-Match`/ETag exists) |
@@ -371,7 +388,10 @@ write that answered nothing is now a typed failure (strict default: no
371
388
 
372
389
  **Deterministic validation 500 (#23).** A 500 whose `errors[]` entries match
373
390
  the documented heuristic phrases (`isDeterministicVendor500` — `exceeds
374
- maximum length` to start) is PERMANENT: the transport makes exactly ONE
391
+ maximum length`, `Missing Required Field`, `Value does not exist for the
392
+ required field`, and since issue #119 `Unable to find <field> in the <entity>
393
+ Entity.` — the live field-existence signature of an unknown `includeFields`
394
+ name) is PERMANENT: the transport makes exactly ONE
375
395
  upstream call (it breaks out before the retry decision — the parsed body is
376
396
  already in hand at the retry site), and the mapped error keeps the
377
397
  `ServerError` class + `SERVER_ERROR` code + status 500 but carries
@@ -470,7 +490,7 @@ Host form: `https://<name>.autotask.net/atservicesrest` (lowercase
470
490
  |---------|---------|-----|-------|
471
491
  | `pageSize` | 10 | 500 (vendor max) | page size for query walks; `maxRecords` on the wire |
472
492
  | `timeoutMs` | 30 000 | — | whole-call budget, incl. retries |
473
- | `maxRetries` | 3 | — | the client's retry budget, overridable per call (`InvokeOptions.maxRetries` / `MutationOpts.maxRetries`; `0` = exactly one send per request the call performs). The budget caps two legs (issue #105): the 429 leg (retried on GET/PUT/DELETE + flagged calls — unchanged) and the 5xx/network leg (GET + flagged only) |
493
+ | `maxRetries` | 3 | — | the client's retry budget, overridable per call (`InvokeOptions.maxRetries` / `MutationOpts.maxRetries`; `0` = exactly one send per request the call performs). The budget caps three legs (issues #105/#109): the 429 leg (retried on GET/PUT/DELETE + flagged calls — unchanged) and the 5xx/network + non-JSON 2xx legs (GET + flagged only) |
474
494
  | `maintenanceMaxWaitMs` | 3 600 000 (1 h) | — | **maintenance-window auto-retry** (weekly Autotask window): when the edge answers a non-JSON HTML page that reads as a maintenance notice, the transport waits `maintenancePollMs` and retries for up to this long from the FIRST such page — for reads AND writes (the edge answered before the vendor application saw the request; the weekly window is typically under 30 min). Does not consume `maxRetries`; each post-wait attempt gets a fresh `timeoutMs`. `0` disables the wait. A page that does not look like maintenance (e.g. a wrong `baseUrl`) never enters the loop |
475
495
  | `maintenancePollMs` | 30 000 | — | poll interval while waiting out a maintenance page |
476
496
  | `maxConcurrent` | 10 | 10 (vendor) | concurrency guard |
@@ -660,9 +680,16 @@ carry paging parameters.
660
680
 
661
681
  - **Page walk — cursor-follow on `pageDetails.nextPageUrl`** (node-style): page 1
662
682
  POSTs the `QueryModel` body (`MaxRecords = pageSize`) to `/V1.0/<E>/query`;
663
- later pages POST the preserved filter to the ABSOLUTE `nextPageUrl`, rebased
664
- to the client base URL (same-origin cursors verbatim; cross-origin re-based —
665
- the node's custom-zone behaviour).
683
+ later pages POST the SAME body (`buildQueryBody` — `MaxRecords`,
684
+ `IncludeFields` with the `id` append, the preserved `filter`) to the ABSOLUTE
685
+ `nextPageUrl`, rebased to the client base URL (same-origin cursors verbatim;
686
+ cross-origin re-based — the node's custom-zone behaviour). Issue #118: before
687
+ the fix later pages POSTed `{ filter }` alone and returned the FULL vendor
688
+ model (live-reproduced 2026-10-19: page 1 of a three-field subset walk —
689
+ `id`, `title`, `status` — carried 4 keys, page 2 carried 76). The vendor HONOURS `IncludeFields` on a continuation and
690
+ IGNORES `MaxRecords` (it serves its own 500-row page; the walk slices to its
691
+ limit, so the RESULT is exact and the wire page size on page 2+ is the
692
+ vendor's).
666
693
  - `Page<T>`: `{ items, page (1-based), pageSize, requestCount (the vendor
667
694
  value verbatim — the MaxRecords echo, not a total), hasMore (nextPageUrl
668
695
  !== null), totalPages }`. `totalPages` is the pages KNOWN so far —
@@ -911,6 +938,12 @@ the mode pinned (§7.4). The governor rules (pinned):
911
938
  the SDK; the caller passes `limit` as the TOTAL cap (default 500, max
912
939
  2500) and never owns a stream. `queryAll` keeps its raw behavior (the
913
940
  unbounded walk; the catalog text steers the caller away for normal work);
941
+ - a query's `includeFields` NAMES are validated **pre-request** (issue
942
+ #119b): the live field metadata is the authority (the vendored spec when
943
+ that read faults), every unknown name is named in one typed
944
+ `CONFIG_ERROR` before the query POST, and `fieldCheck: false` in the query
945
+ bag disables the name check (the `string[]` / non-empty shape check always
946
+ runs);
914
947
  - `dryRun` must be a real boolean: anything else (`'true'`, `0`, `null`) is
915
948
  refused before any request — only an explicit `false` applies a write;
916
949
  - `<resource>.createIfNotExists` takes its match fields as an optional
@@ -1097,6 +1130,18 @@ can be blind to the API user while per-id reads work, so an `id in`
1097
1130
  lookup on a blind table would silently return nothing; the per-id GET is
1098
1131
  the authority read (section 11.2).
1099
1132
 
1133
+ **The input is Resource (staff) ids only (issue #116).** Because the op's
1134
+ one wire verb is `GET /V1.0/Resources/{id}`, an id of any other vendor
1135
+ entity (a ticket / company / contact / billing-code id) resolves to an
1136
+ unrelated person's name when it collides with a live staff id (and the
1137
+ wrong name then sticks in the default-on `resourceNameCache` under that id
1138
+ until TTL — a one-off mis-feed keeps serving it; section 11.21), or to a
1139
+ misleading `blind: true` marker when it does not. An `AutotaskError`'s
1140
+ legacy `resourceIds` may carry exactly those (the field is unlabeled —
1141
+ section 1.5): a consumer must filter the error's `details.idRefs`
1142
+ (`{ entity, id }`) on `entity: 'resource'` before feeding ids here — never
1143
+ the unlabeled field.
1144
+
1100
1145
  - **Input**: `ids: (number | string)[]` — vendor numeric resource ids
1101
1146
  (safe integers, or the canonical integer string for ids above 2^53 —
1102
1147
  the exact digits ride the wire URL verbatim). Non-numeric string ids
@@ -1621,7 +1666,8 @@ retrying with the same body fails identically; fix the field instead
1621
1666
  httpStatus >= 500). **Resolution (issue #23):** the *deterministic*
1622
1667
  validation 500 is no longer labelled retryable — a 500 whose `errors[]` text
1623
1668
  matches the documented heuristic phrases (`isDeterministicVendor500`:
1624
- `exceeds maximum length`, `Missing Required Field`, `Value does not exist for the required field`) makes exactly ONE upstream call (the
1669
+ `exceeds maximum length`, `Missing Required Field`, `Value does not exist for the required field`,
1670
+ `Unable to find <field> in the <entity> Entity.` — issue #119, live-verified 2026-10-19) makes exactly ONE upstream call (the
1625
1671
  transport skips the retry), keeps the `ServerError` class + `SERVER_ERROR`
1626
1672
  code + status 500, but carries `retryable: false`, `details.deterministic:
1627
1673
  true`, and a `suggestedAction` pointing at the field catalog
@@ -2000,7 +2046,11 @@ the TTL is the other expiry path.
2000
2046
 
2001
2047
  **The on-demand op:** `operations.fieldCatalog(client, opts?)`
2002
2048
  (`{ resources?, include?, fields?, labels?, fresh? }` — `fields` filters the
2003
- fields-kind rows to the named fields and reports `unmatchedFields`; `labels`
2049
+ fields-kind rows to the named fields and reports `unmatchedFields`; a
2050
+ projected row is marked `projected: true` (issue #121 — the four derived
2051
+ lists then describe the PROJECTED set only, so a consumer can never read a
2052
+ projected `required`/`queryable` list as the resource's full picture; a full
2053
+ row omits the key); `labels`
2004
2054
  adds the picklist labels; default: the five workflow
2005
2055
  resources, `include: ['fields']`; the closed opts bag is documented in
2006
2056
  the registry record). One bounded entityInformation GET per resource ×
@@ -2603,6 +2653,19 @@ query-verb-ONLY and zero-wire or vendor-column-selection-only:
2603
2653
  billing item to its company / ticket / project / time entry / contract
2604
2654
  service / contract / billing code / expense item, plus the date and the
2605
2655
  amount fields — the "common narrow read" the issue's example targets.
2656
+ - **`fieldCheck: false` — the includeFields NAME check's opt-out (issue
2657
+ #119b).** By default the bounded `query` op validates every `includeFields`
2658
+ name BEFORE the query POST: the authority is the entity's LIVE field
2659
+ metadata (`getFieldInfo()` through the cache-aware channel of section
2660
+ 11.14 — warm = zero wire), degrading to the vendored spec
2661
+ (`src/field-sets.ts`) when the metadata read faults; with neither an
2662
+ authority the check is skipped. The live set wins when present — a
2663
+ live-only field (`time_entries.postedDate`) is accepted and a
2664
+ vendored-only field the tenant no longer has is refused. Every unknown
2665
+ name is named in ONE typed `CONFIG_ERROR` (zero query wire), with
2666
+ `fieldCheck: false` as the escape hatch and `operations.fieldCatalog` as
2667
+ the discovery pointer. The shape check (`string[]`, non-empty entries)
2668
+ always runs.
2606
2669
  - **The live check (the AC test for the presets).** Each preset's fields
2607
2670
  are asserted to EXIST on the live resource against the LIVE field
2608
2671
  catalogue (`operations.fieldCatalog` — the entityInformation/fields
package/examples/basic.ts CHANGED
@@ -32,8 +32,8 @@ import { AutotaskClient, type ZoneName } from 'node-autotask';
32
32
  const apiCode = process.env.AUTOTASK_TRACKING_IDENTIFIER ?? process.env.AUTOTASK_API_CODE ?? process.env.AUTOTASK_API_INTEGRATION_CODE;
33
33
  const user = process.env.AUTOTASK_USERNAME ?? process.env.AUTOTASK_USER;
34
34
  const secret = process.env.AUTOTASK_SECRET;
35
- const zone = process.env.AUTOTASK_ZONE;
36
- const impersonation = process.env.AUTOTASK_IMPERSONATION_ID;
35
+ const zone = process.env.AUTOTASK_ZONE?.trim() || undefined;
36
+ const impersonation = process.env.AUTOTASK_IMPERSONATION_ID?.trim() || undefined;
37
37
 
38
38
  const DRY = !(apiCode && user && secret);
39
39
 
@@ -95,13 +95,14 @@ import {
95
95
  GATEWAY_WARMUP_BUDGET_MS,
96
96
  } from './mcp-gateway.js';
97
97
 
98
- const PORT = Number(process.env.PORT ?? 7437);
98
+ const PORT_RAW = process.env.PORT?.trim();
99
+ const PORT = PORT_RAW !== undefined && PORT_RAW !== '' ? Number(PORT_RAW) : 7437;
99
100
  const apiCode = process.env.AUTOTASK_TRACKING_IDENTIFIER ?? process.env.AUTOTASK_API_CODE ?? process.env.AUTOTASK_API_INTEGRATION_CODE;
100
101
  const user = process.env.AUTOTASK_USERNAME ?? process.env.AUTOTASK_USER;
101
102
  const secret = process.env.AUTOTASK_SECRET;
102
- const zone = process.env.AUTOTASK_ZONE;
103
- const impersonation = process.env.AUTOTASK_IMPERSONATION_ID;
104
- const baseUrl = process.env.AUTOTASK_BASE_URL;
103
+ const zone = process.env.AUTOTASK_ZONE?.trim() || undefined;
104
+ const impersonation = process.env.AUTOTASK_IMPERSONATION_ID?.trim() || undefined;
105
+ const baseUrl = process.env.AUTOTASK_BASE_URL?.trim() || undefined;
105
106
  /** The explicit env-vs-gateway switch (issue #5): "on" = credentials per request, headers only. */
106
107
  const GATEWAY = (process.env.AUTOTASK_GATEWAY_MODE ?? 'off').toLowerCase() === 'on';
107
108
  /** Env mode: DRY when no credentials. Gateway mode is never "DRY" — a request
@@ -64,9 +64,9 @@ import { withEnvOverrides } from './mcp-env-config.js';
64
64
  const apiCode = process.env.AUTOTASK_TRACKING_IDENTIFIER ?? process.env.AUTOTASK_API_CODE ?? process.env.AUTOTASK_API_INTEGRATION_CODE;
65
65
  const user = process.env.AUTOTASK_USERNAME ?? process.env.AUTOTASK_USER;
66
66
  const secret = process.env.AUTOTASK_SECRET;
67
- const zone = process.env.AUTOTASK_ZONE;
68
- const impersonation = process.env.AUTOTASK_IMPERSONATION_ID;
69
- const baseUrl = process.env.AUTOTASK_BASE_URL;
67
+ const zone = process.env.AUTOTASK_ZONE?.trim() || undefined;
68
+ const impersonation = process.env.AUTOTASK_IMPERSONATION_ID?.trim() || undefined;
69
+ const baseUrl = process.env.AUTOTASK_BASE_URL?.trim() || undefined;
70
70
  const DRY = !(apiCode && user && secret);
71
71
 
72
72
  /** One client for the whole process (fail-closed construction; null in DRY mode). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-autotask",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Fully-typed TypeScript SDK for the Autotask (Datto) PSA REST API. Zero runtime dependencies. Built for building MCP servers and integrations.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -112,7 +112,7 @@
112
112
  "coverage": "vitest run --coverage",
113
113
  "lint": "eslint src test",
114
114
  "lint:fix": "eslint src test --fix",
115
- "verify": "npm run typecheck && npm run lint && npm run generate:check && npm run build && node scripts/check-capabilities.mjs --ship && npm run mcp:project:check && npm run n8n:project:check && npm run catalog:check && npm run mcp:check-surface && npm run public-surface && npm run verify:pack && npm test",
115
+ "verify": "npm run lockfile:check && npm run typecheck && npm run lint && npm run generate:check && npm run build && node scripts/check-capabilities.mjs --ship && npm run mcp:project:check && npm run n8n:project:check && npm run catalog:check && npm run mcp:check-surface && npm run public-surface && npm run verify:pack && npm test",
116
116
  "plan:derive": "node scripts/derive-plan.mjs",
117
117
  "generate:check": "node scripts/generate-batch.mjs --check && node scripts/generate-batch.mjs --check --group B",
118
118
  "capabilities:build": "node scripts/generate-capabilities.mjs",
@@ -131,6 +131,7 @@
131
131
  "n8n:project:check": "node scripts/project-n8n-mcp.mjs --check",
132
132
  "verify:pack": "node scripts/verify-pack.mjs",
133
133
  "release:check": "node scripts/check-release-plan-hash.mjs",
134
+ "lockfile:check": "node scripts/check-lockfile-sync.mjs",
134
135
  "capabilities:check:ship": "node scripts/check-capabilities.mjs --ship",
135
136
  "prepack": "npm run build",
136
137
  "prepublishOnly": "npm run release:check && npm run clean && npm run verify && npm run capabilities:check:ship",