node-autotask 0.40.0 → 0.42.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 (44) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/MCP_TOOL_CATALOG.json +147 -149
  3. package/MCP_TOOL_MANIFEST.md +9 -9
  4. package/N8N_MCP_MANIFEST.json +4 -43
  5. package/N8N_MCP_MANIFEST.md +6 -7
  6. package/README.md +3 -4
  7. package/capabilities.json +3945 -3405
  8. package/dist/capabilities.cjs +2 -2
  9. package/dist/capabilities.js +1 -1
  10. package/dist/{chunk-VDVBXXJW.js → chunk-AETFO6BO.js} +1765 -1880
  11. package/dist/{chunk-EUGSF3U4.cjs → chunk-HD5BRA3Z.cjs} +285 -285
  12. package/dist/{chunk-E5W24BZB.cjs → chunk-MYL34QU5.cjs} +143 -143
  13. package/dist/{chunk-FEYVLIHD.cjs → chunk-SEFJJY4J.cjs} +2801 -2916
  14. package/dist/{chunk-O32L6GS5.js → chunk-TTPHVFPJ.js} +2 -2
  15. package/dist/{chunk-EVDVESQM.cjs → chunk-VEOSOIU4.cjs} +3944 -3404
  16. package/dist/{chunk-Y6YJYKBZ.js → chunk-WP33PQVM.js} +3944 -3404
  17. package/dist/{chunk-MTFRSD7P.js → chunk-ZR24JGW2.js} +2 -2
  18. package/dist/index-C3KFMnRT.d.cts +6860 -0
  19. package/dist/index-C3KFMnRT.d.ts +6860 -0
  20. package/dist/{index-BKp8_P2L.d.cts → index-C7rMJ4IV.d.ts} +139 -7236
  21. package/dist/{index-BKp8_P2L.d.ts → index-Cm_Xpt7X.d.cts} +139 -7236
  22. package/dist/index.cjs +22 -24
  23. package/dist/index.d.cts +58 -34
  24. package/dist/index.d.ts +58 -34
  25. package/dist/index.js +8 -10
  26. package/dist/operations/index.cjs +363 -242
  27. package/dist/operations/index.d.cts +281 -25
  28. package/dist/operations/index.d.ts +281 -25
  29. package/dist/operations/index.js +178 -57
  30. package/dist/pool.cjs +8 -8
  31. package/dist/pool.d.cts +2 -1
  32. package/dist/pool.d.ts +2 -1
  33. package/dist/pool.js +4 -4
  34. package/dist/resources/index.cjs +4 -4
  35. package/dist/resources/index.d.cts +2 -1
  36. package/dist/resources/index.d.ts +2 -1
  37. package/dist/resources/index.js +3 -3
  38. package/dist/types/index.d.cts +1 -1
  39. package/dist/types/index.d.ts +1 -1
  40. package/docs/API.md +101 -139
  41. package/docs/LIVE-TESTING.md +9 -5
  42. package/docs/N8N-ADAPTER.md +1 -1
  43. package/docs/RELEASING.md +121 -0
  44. package/package.json +3 -2
package/docs/API.md CHANGED
@@ -740,20 +740,11 @@ carry paging parameters.
740
740
  query-verb-ONLY (`queryAll` / `count` bags are unchanged; the raw
741
741
  `.query()` walker is uncapped — section 8's `queryBounded` is the MCP
742
742
  query surface the issue targets).
743
- - **`fieldsPreset: name` (issue #61) — the closed includeFields
744
- presets.** The named preset is resolved PRE-WALK to its `includeFields`
745
- list (the vendor's own column selection rides the existing `IncludeFields`
746
- wire path; `id` is always appended by the id-append — zero extra wire).
747
- v1 ships the one preset the issue names: `billing_join` on
748
- `billing_items` (the 14 join/measure columns — section 11.23). The
749
- closed-bag refusals are pre-request (zero wire): unknown preset name
750
- (the valid names are named), preset/resource mismatch (the preset and
751
- its resource are named), and `fieldsPreset` + an explicit
752
- `includeFields` in the same bag (both are named — no silent
753
- precedence). The preset FIELD EXISTENCE is verified against the LIVE
754
- field catalogue (the live-smoke check — the review correction's
755
- mandate; the vendored spec is a non-authoritative guard only), not
756
- against `MODEL_FIELD_SETS`.
743
+ - **`fieldsPreset` — removed in 0.42.0 (issue #287).** The closed
744
+ includeFields presets are gone; the former `billing_join` column list is
745
+ documented as a plain `includeFields` example in section 11.23. A
746
+ `fieldsPreset` in the bag is refused pre-request as an unknown key
747
+ (the valid keys are named; `includeFields` is the named successor).
757
748
  - Raw SDK reads are unchanged: `queryAll` (deterministic plain `T[]`,
758
749
  unbounded, 1000-page guarded) and `query` (stream, `for await ...`). The
759
750
  MCP surface steers the caller to the bounded `query` op; `queryAll` is for
@@ -1402,9 +1393,10 @@ notes cannot be deleted, so they never use create-then-delete.
1402
1393
  optionally scoped to one company (a `companyID eq` leaf and-joined server-side with the title match) and
1403
1394
  projected to the listed columns (`id` is always returned). `searchWithDetail` takes the same bag. Malformed
1404
1395
  `companyID` / `includeFields` are refused before any request. Other resources' `search` is unchanged.
1405
- - **Deprecated (issue #278, since 0.38.0; removed in the next minor):** `time_entries.recentTickets` is a
1406
- strict subset of an aggregate grouped by ticket now that relation filters (§5) and default labels (§11.27)
1407
- exist. Replacement (1-2 calls instead of 2+):
1396
+ - `search` refuses an unknown option key (a misspelt `companyId` can't silently widen the search), and drops an
1397
+ empty `userDefinedFields` the vendor appends to projected rows.
1398
+ - **Hours per ticket for a resource** (`time_entries.recentTickets` was removed in 0.41.0, issue #290): aggregate
1399
+ the time entries grouped by ticket, using relation filters (§5) and label siblings (§11.27, opt-in):
1408
1400
 
1409
1401
  ```ts
1410
1402
  const res = await aggregate(client, 'time_entries.query', [
@@ -1412,29 +1404,16 @@ notes cannot be deleted, so they never use create-then-delete.
1412
1404
  { field: 'dateWorked', op: 'gte', value: '2026-10-01' },
1413
1405
  { field: 'dateWorked', op: 'lte', value: '2026-10-28' },
1414
1406
  { field: 'ticket.companyID', op: 'eq', value: 123 }, // optional: one company, resolved server-side
1415
- ], { groupBy: ['ticketID'], sum: ['hoursWorked'] });
1407
+ ], { groupBy: ['ticketID'], sum: ['hoursWorked'], names: true });
1416
1408
  // res.groups[i] = { key: { ticketID, ticketIDName: '<untrusted_content>T20261001.0001 Printer down</untrusted_content>' },
1417
1409
  // count, sums: { hoursWorked } }
1418
1410
  ```
1419
1411
 
1420
- For per-ticket status / assignee / company use `operations.fetchByIds` on the group ticket ids (labels
1421
- included: `statusLabel`, `assignedResourceIDName`, `companyIDName`).
1422
- - `time_entries.recentTickets(from, to, { resourceID, companyID?, tz?, limit? })` — the distinct tickets a
1423
- resource logged time against in an inclusive `dateWorked` window (`YYYY-MM-DD`; UTC days, or the local days in
1424
- `tz`). One bounded time-entry read (`dateWorked` + `resourceID`; `limit` 1-500, default 500, bounds the
1425
- **entries** scanned) plus `id in [...]` ticket reads (100 ids each). Result `{ items, entriesRead,
1426
- entriesWithoutTicket, unresolvedTicketIDs, window, truncated }`; each item carries `ticketID`, `ticketNumber`,
1427
- `title`, `status`, `assignedResourceID`, `companyID`, `entryCount`, `totalHours`, `lastWorked`, newest first.
1428
- TimeEntry has no `companyID`, so `companyID` is applied **after the join**, client-side on the ticket rows'
1429
- own `companyID` (the ticket read is not company-filtered on the wire). Entries on other companies' tickets
1430
- still consume `limit`; those tickets are only counted in `excludedTickets` (ids are not returned), and
1431
- `unresolvedTicketIDs` holds only tickets the read did not return at all (deleted or invisible, dropped from
1432
- `items` when `companyID` is set). `truncated: true` (same meaning as `postedWithin`) means the rollup covers
1433
- the scanned entries only; narrow the window. `search` refuses an unknown option key (a misspelt `companyId`
1434
- can't silently widen the search), and drops an empty `userDefinedFields` the vendor appends to projected rows.
1435
- - **`tz` on the `dateWorked` ops** (`recentTickets`, `postedWithin`, `dateWorkedWindow`, `customRange` and the relative
1412
+ For per-ticket status / assignee / company use `operations.fetchByIds` on the group ticket ids
1413
+ (with `names: true`: `statusLabel`, `assignedResourceIDName`, `companyIDName`).
1414
+ - **`tz` on the `dateWorked` ops** (`postedWithin`, `dateWorkedWindow`, `customRange` and the relative
1436
1415
  windows): the vendor stores `dateWorked` as a date-only value at 00:00 UTC, so the wire bounds are always whole UTC days.
1437
- For `recentTickets` / `postedWithin` / `dateWorkedWindow` / `customRange` the `from`/`to` days ARE the days, so `tz` is
1416
+ For `postedWithin` / `dateWorkedWindow` / `customRange` the `from`/`to` days ARE the days, so `tz` is
1438
1417
  validated but never shifts them. For the relative windows (`today`, `last7`, `lastFullMonth`, ...) `tz` picks which
1439
1418
  calendar dates the window covers in that zone and the bounds are whole UTC days of those dates. (Before 0.24.0 a `tz`
1440
1419
  behind UTC, e.g. `America/Los_Angeles`, selected the NEXT UTC day; live-verified 2026-10-23.)
@@ -1499,8 +1478,7 @@ resolver holds a status literal: the move engine's closed-target refusal,
1499
1478
  `tickets.forResource(resourceID, opts?)` lists the tickets assigned to one
1500
1479
  resource, as primary assignee, secondary resource, or both (the `scope` option,
1501
1480
  default `both`; the seam is `Tickets.assignedResourceID` for primary and the
1502
- `TicketSecondaryResources` link table for secondary). It follows
1503
- `time_entries.recentTickets`: two bounded reads (a `Tickets/query` on
1481
+ `TicketSecondaryResources` link table for secondary). Two bounded reads (a `Tickets/query` on
1504
1482
  `assignedResourceID` and a `TicketSecondaryResources/query` on `resourceID`,
1505
1483
  both `id in [...]`-chunked when the link set is large) unioned client-side —
1506
1484
  deliberately not the cross-field grouped `or`, whose live semantics are
@@ -1860,7 +1838,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1860
1838
  | `ticket_secondary_resources` | TicketSecondaryResource | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketSecondaryResources_get |
1861
1839
  | `ticket_webhooks` | TicketWebhook | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketWebhook_get |
1862
1840
  | `tickets` | Ticket | entity | core | no | query, count, get, create, update, resolve, search, getContext | changeStatus, findByTicketNumber, forResource, fullContext, getEntityInfo, getFieldInfo, getUdfInfo, lineOfBusiness, openWithNoteAndAssign, slaHealthCheck, statusSemantics, summary, timeline | autotask_ticket_get |
1863
- | `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve | approve, createForResource, createIfNotExists, customRange, dateWorkedWindow, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, moveToTicket, postedWithin, recentTickets, today, yesterday | autotask_timeEntry_get |
1841
+ | `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve | approve, createForResource, createIfNotExists, customRange, dateWorkedWindow, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, moveToTicket, postedWithin, today, yesterday | autotask_timeEntry_get |
1864
1842
  | `time_entry_attachments` | TimeEntryAttachment | entity | extended | yes (time_entries) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_timeEntryAttachment_get |
1865
1843
  | `time_off_requests` | TimeOffRequest | entity | extended | no | query, count, get, resolve | approve, getEntityInfo, getFieldInfo, getUdfInfo, reject | autotask_timeOffRequest_get |
1866
1844
  | `inventory_products` | InventoryProduct | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo, stockedItemsAdd, stockedItemsRemove, stockedItemsTransfer | autotask_inventoryProduct_get |
@@ -2184,11 +2162,14 @@ Field-validation failures ride on **HTTP 500**, not 4xx: the SDK surfaces a
2184
2162
  typed `ServerError` (code `SERVER_ERROR`, httpStatus 500) with a structured
2185
2163
  redacted `vendorError` naming the offending field and a deterministic
2186
2164
  `suggestedAction` (rw-test.md: 9/9 create probes PASS the contract). **Known
2187
- quirk**: the error contract labels 500s `retryable: true` ("the server error is
2188
- transient") even when the 500 is a *deterministic* missing-field rejection —
2189
- retrying with the same body fails identically; fix the field instead
2190
- (`src/errors/` sets `retryable: true` for `SERVER_ERROR` and for every
2191
- httpStatus >= 500). **Resolution (issue #23):** the *deterministic*
2165
+ quirk**: the error contract labels 500s `retryable: true` even when the 500
2166
+ is a *deterministic* missing-field rejection — retrying with the same body
2167
+ fails identically; fix the field instead (`src/errors/` sets `retryable: true`
2168
+ for `SERVER_ERROR` and for every httpStatus >= 500). The plain-5xx
2169
+ `suggestedAction` is the honest SDK-voice hint (issue #287): the SDK already
2170
+ retried per its policy; a repeated 500 on one entity while others work
2171
+ usually means the API user lacks permission on that entity — ask the Autotask
2172
+ admin to grant it; otherwise retry once more at most. **Resolution (issue #23):** the *deterministic*
2192
2173
  validation 500 is no longer labelled retryable — a 500 whose `errors[]` text
2193
2174
  matches the documented heuristic phrases (`isDeterministicVendor500`:
2194
2175
  `exceeds maximum length`, `Missing Required Field`, `Value does not exist for the required field`,
@@ -2861,24 +2842,20 @@ alone cannot fit) and the result carries `truncated: true` and
2861
2842
  `truncation: { reason: 'maxBytes', maxBytes, fullBytes, omitted: [{ resource,
2862
2843
  kind, count }], note }`, so it is always valid JSON and never over the cap (a
2863
2844
  result whose envelope alone cannot fit, e.g. many resources under a small cap,
2864
- is refused with `CONFIG_ERROR`). Both default off (the
2865
- full rows, no cap); a model-facing server should pass `compact: true` and a
2866
- `maxBytes`.
2867
-
2868
- **Reference targets (issue #219 N1).** Every projected field row carries
2869
- `targetResource: string | null` and `targetEnumerable: boolean`: the plan
2870
- resource key the vendor `referenceEntityType` names, and whether that
2871
- resource has a read (`query`/`get`) path, i.e. whether a caller can
2872
- enumerate valid ids with it. The join is DERIVED from the generated
2873
- registry — `resourceForEntityType(entityType)` in
2874
- `node-autotask/operations` reads each resource's plan `apiEntity` (plus
2875
- the singular entity name where the plan's `apiEntity` is the plural path
2876
- segment) and returns `null` when the plan carries no such resource, so a
2877
- consumer never hand-maps `"CompanyLocation"` -> `company_locations`. A
2878
- row for a target the plan cannot enumerate reads `null` + `false`; the
2879
- row carries no remedy, label or exposure claim (`plan.referenceTargets`
2880
- is the only home for those). The same helper serves reference UDF rows —
2881
- there is no second entity-type table.
2845
+ is refused with `CONFIG_ERROR`). Issue #287: an ABSENT `compact` is now
2846
+ compact BY DEFAULT — compact ONLY when the call passes no `fields` filter AND
2847
+ no `labels: true` (`labels: true` without `fields` keeps the full rows +
2848
+ picklist values — the ~137 KB case above, unchanged); an explicit
2849
+ `compact: false` restores the full rows. `maxBytes` still defaults off; a
2850
+ model-facing server can rely on the compact default and pass a `maxBytes`.
2851
+
2852
+ **Reference targets (issue #219 N1 — removed in issue #287).** The
2853
+ plan-static per-row extras `targetResource` / `targetEnumerable` and the
2854
+ `resourceForEntityType` helper are REMOVED (issue #287, breaking): a
2855
+ reference field row carries the vendor's `isReference` flag and
2856
+ `referenceEntityType` name, and the generated registry (the `apiEntity`
2857
+ join, via `autotask_catalog`) is where a caller resolves which plan
2858
+ resource enumerates a named entity — no second entity-type table ships.
2882
2859
 
2883
2860
  **User-defined fields (issue #219 N7).** `udfCapable` entities carry a
2884
2861
  parallel field system, and the served row states it rather than leaving it
@@ -3328,8 +3305,8 @@ picklist scope (owner-approved list: the ticket fields
3328
3305
  rides ONE `operations.fieldCatalog` `labels: true` call (zero extra wire
3329
3306
  on a warm fieldCache). The separate name-read budget counts COLD wire
3330
3307
  name reads only (cache hits are free of it); over-budget ids serve
3331
- `name: null, capped: true` on the composite's name row + the top-level
3332
- `truncated` marker — never an error. An unresolvable id serves the blind
3308
+ `name: null` on the composite's name row + the top-level `truncated`
3309
+ marker — never an error. An unresolvable id serves the blind
3333
3310
  marker (`name: null, blind: true`); a picklist whose values the vendor
3334
3311
  does not expose to this API user serves `null` + the fieldCatalog
3335
3312
  reason row (the #64 W-A4 wall) — never an error. A picklist that IS served but does not
@@ -3409,12 +3386,12 @@ additive, and mirrors the family:
3409
3386
  wire), and the result is the plain `items[]` array — no counts
3410
3387
  (the time-entry family shape).
3411
3388
 
3412
- ### 11.23 Free-text caps and includeFields presets (issue #61)
3389
+ ### 11.23 Free-text caps (issue #61)
3413
3390
 
3414
3391
  Issue #61 (the library remainder after the server-side split — TSV /
3415
3392
  `output_path` / the untrusted-wrapper opt-out live in the server repo)
3416
- adds two opt-in keys to the bounded-query bag (section 4), both
3417
- query-verb-ONLY and zero-wire or vendor-column-selection-only:
3393
+ added opt-in keys to the bounded-query bag (section 4), both
3394
+ query-verb-ONLY and zero-wire:
3418
3395
 
3419
3396
  - **`noteChars: N` — the free-text cap.** One integer (>= 1) caps EVERY
3420
3397
  string value in the resource's untrusted-class free-text fields (the
@@ -3426,20 +3403,6 @@ query-verb-ONLY and zero-wire or vendor-column-selection-only:
3426
3403
  ordering note). Additive result field: `noteCharsTruncated` (the
3427
3404
  truncated-field count; present only when > 0). Absent = the raw result
3428
3405
  bit-for-bit.
3429
- - **`fieldsPreset: name` — the closed preset list.** A preset is
3430
- `{ resource, fields }` — hand-curated (NOT plan-derived: a curation
3431
- choice, same class as n8n-mcp.curation.json), resolved pre-walk to the
3432
- walk's `includeFields` (the vendor's column selection; `id` always
3433
- appended). The v1 list is CLOSED at the one preset the issue names:
3434
-
3435
- | preset | resource | fields (14) |
3436
- |---|---|---|
3437
- | `billing_join` | `billing_items` | `companyID, ticketID, projectID, timeEntryID, contractServiceID, contractID, billingCodeID, expenseItemID, itemDate, quantity, rate, extendedPrice, totalAmount, nonBillable` |
3438
-
3439
- `billing_join` is the billing-item join read: the FK columns that join a
3440
- billing item to its company / ticket / project / time entry / contract
3441
- service / contract / billing code / expense item, plus the date and the
3442
- amount fields — the "common narrow read" the issue's example targets.
3443
3406
  - **`fieldCheck: false` — the field-NAME checks' opt-out (issues #119b +
3444
3407
  #125).** By default the bounded `query` op validates every `includeFields`
3445
3408
  name AND every `filter` condition field name BEFORE the query POST: the
@@ -3468,31 +3431,25 @@ query-verb-ONLY and zero-wire or vendor-column-selection-only:
3468
3431
  groupBy/sum field names ARE checked; name-checking the aggregate filter
3469
3432
  is a separate follow-up, the op's own field whitelist being the
3470
3433
  different authority).
3471
- - **The live check (the AC test for the presets).** Each preset's fields
3472
- are asserted to EXIST on the live resource against the LIVE field
3473
- catalogue (`operations.fieldCatalog` — the entityInformation/fields
3474
- channel; the #64 fieldCache idiom), NOT against `MODEL_FIELD_SETS`
3475
- (the vendored-spec projection — a cheap NON-AUTHORITATIVE subset guard
3476
- only; the spec can lag the live API). The check runs in the live round
3477
- (`npm run live-smoke` — read-only, one field-catalogue read per preset;
3478
- a missing field FAILS the run), not in `npm test`. The v1 field list's
3479
- LIVE existence is verified there — the offline proof is the spec-subset
3480
- guard only (the review correction's mandate, the `fetchMany.liveFields`
3481
- live-first idiom).
3482
- - **Refusals (pre-request, zero wire, typed):** unknown preset name (the
3483
- valid names are named — the closed-set discipline), preset/resource
3484
- mismatch (the preset and its resource are named), `fieldsPreset` + an
3485
- explicit `includeFields` (both are named — no silent precedence), and
3486
- `noteChars` `0` / negative / non-integer.
3434
+ - **Refusals (pre-request, zero wire, typed):** `noteChars` `0` /
3435
+ negative / non-integer.
3487
3436
  - **Backwards compatibility:** both keys are OPT-IN on a closed bag —
3488
3437
  absent = the pre-#61 path bit-for-bit (the default-path zero-move is
3489
- pinned by the note-chars sidecar tests). New public exports:
3490
- `FIELD_PRESETS`, `FIELD_PRESET_NAMES`, `resolveFieldsPreset` and the
3491
- `FieldsPreset` type (additive; the public-surface gate passes as
3492
- additive-only). `UNTRUSTED_FIELDS_BY_RESOURCE` and
3493
- `noteCharsTruncationMarker` are internal. `src/untrusted.ts` is byte-identical
3494
- (the projection contract is untouched — the cap is a query-path
3495
- mechanism, not a projection change).
3438
+ pinned by the note-chars sidecar tests). `UNTRUSTED_FIELDS_BY_RESOURCE`
3439
+ and `noteCharsTruncationMarker` are internal. `src/untrusted.ts` is
3440
+ byte-identical (the projection contract is untouched — the cap is a
3441
+ query-path mechanism, not a projection change).
3442
+
3443
+ **The `fieldsPreset` presets are removed (issue #287, 0.42.0).** The closed
3444
+ includeFields presets are gone — the `FIELD_PRESETS`, `FIELD_PRESET_NAMES`,
3445
+ `resolveFieldsPreset` and `FieldsPreset` exports and the bag key no longer
3446
+ exist, and a `fieldsPreset` in the bag is refused pre-request as an unknown
3447
+ key (the refusal names `includeFields`). The one preset v1 shipped —
3448
+ `billing_join` on `billing_items` — is now a plain column list:
3449
+ `companyID, ticketID, projectID, timeEntryID, contractServiceID,
3450
+ contractID, billingCodeID, expenseItemID, itemDate, quantity, rate,
3451
+ extendedPrice, totalAmount, nonBillable` — pass it as an explicit
3452
+ `includeFields` (`id` is always appended by the id-append).
3496
3453
 
3497
3454
 
3498
3455
  ### 11.24 The unbilled-work composite and the generic aggregate (issue #62)
@@ -3905,13 +3862,24 @@ returns the contract's reconciled P&L over a date window:
3905
3862
  catch-all/start, the provided-without-rate and fully-missing `missing`
3906
3863
  — never a silent zero — and `internalCost: 0` as a RESOLVED zero).
3907
3864
 
3908
- ### 11.27 Default label siblings on query, aggregate and fetch (issue #276)
3865
+ ### 11.27 Label siblings on query, aggregate and fetch (opt-in, issue #287)
3909
3866
 
3910
3867
  Rows served by the invoke `<resource>.query` op (the bounded query), the group
3911
3868
  keys of `operations.aggregate`, and the found records of `operations.fetchMany`
3912
- / `operations.fetchByIds` carry label siblings **by default**, through the same
3869
+ / `operations.fetchByIds` carry label siblings **when opted in** (issue #287:
3870
+ OFF by default — the label pass costs a field-catalog read plus up to 20 cold
3871
+ name reads per call, so a plain read returns raw ids), through the same
3913
3872
  `resolveCompositeNames` path the full-context composites use:
3914
3873
 
3874
+ - **The client option:** `labels: true` on the client config turns the label
3875
+ siblings on for every read surface of that client (the MCP server sets it,
3876
+ server-side).
3877
+ - **The per-call key:** `names: true` in the query bag / the aggregate opts /
3878
+ the `fetchMany` / `fetchByIds` opts turns them on for that call; `names:
3879
+ false` turns them off (even when the client has `labels: true`); ABSENT =
3880
+ the client's option (the off default). A non-boolean `names` is refused
3881
+ pre-request.
3882
+
3915
3883
  | field on the row | sibling | example |
3916
3884
  |---|---|---|
3917
3885
  | a picklist field of the resource (catalog `isPickList`) | `<field>Label` | `status: 5` -> `statusLabel: "Complete"` |
@@ -3919,25 +3887,24 @@ keys of `operations.aggregate`, and the found records of `operations.fetchMany`
3919
3887
  | `ticketID` | `ticketIDName` = `"<ticketNumber> <title>"` | `"T20261001.0001 Printer down"` |
3920
3888
  | `resourceID` and every `*ResourceID` | `<field>Name` (staff name) | `assignedResourceIDName: "Ada Lovelace"` |
3921
3889
 
3922
- - **Untrusted content:** every `*Name` value (and the alias) is vendor free text
3890
+ - **Untrusted content:** every `*Name` value is vendor free text
3923
3891
  outside the untrusted field registry, so the SDK caps it at 200 chars (the
3924
3892
  noteChars marker `… [+N chars truncated]`) and wraps it in
3925
3893
  `<untrusted_content>…</untrusted_content>` (embedded tags neutralised, the
3926
3894
  same `wrapUntrusted` the projection applies to `title`). Picklist `*Label`
3927
3895
  values stay raw (picklists are outside the untrusted class by the registry
3928
3896
  rule). Example: `contractIDName: "<untrusted_content>Managed Services 2026</untrusted_content>"`.
3929
- - `assignedResourceName` is also written (same value as `assignedResourceIDName`):
3930
- a **deprecated alias**, still written in 0.39.0 (null with it, issue #282);
3931
- its removal is deferred to a later minor.
3897
+ - The `assignedResourceName` alias (0.39.0, issue #282) is removed (issue
3898
+ #287) — read `assignedResourceIDName`.
3932
3899
  - Aggregate: `groups[].key.ticketIDName`, `groups[].key.statusLabel`, … (only the
3933
3900
  returned `top` groups are labelled).
3934
3901
  - A labelled field that is present in the row always gets its sibling (issue
3935
3902
  #282): a `null` value, a `0` id on any FK except `companyID` (never resolved: the node skips it and does not read a record; this is a node rule, not a vendor statement that 0 means "no reference"; `companyID: 0` is the tenant owner's own company and IS resolved, issue #306, verified live on one tenant, no other FK target probed has an id 0), an unknown id
3936
- (the vendor's not-found) or a capped id gets `null`, never a missing key and
3937
- never an error. Only a field absent from the row gets no sibling. Example:
3903
+ (the vendor's not-found) or an id the cold-read cap left unlabelled gets
3904
+ `null`, never a missing key and never an error. Only a field absent from the
3905
+ row gets no sibling. Example:
3938
3906
  `{ contractID: null, contractIDName: null, assignedResourceID: null,
3939
- assignedResourceIDName: null, assignedResourceName: null, status: null,
3940
- statusLabel: null }`.
3907
+ assignedResourceIDName: null, status: null, statusLabel: null }`.
3941
3908
  - A picklist value of `0` is looked up like any other value (issue #284): some
3942
3909
  picklists define it (live: contracts and projects `status` 0 = "Inactive",
3943
3910
  opportunities `status` 0 = "Not Ready To Buy", companies
@@ -3949,38 +3916,33 @@ keys of `operations.aggregate`, and the found records of `operations.fetchMany`
3949
3916
  SDK cannot tell the two apart. On a picklist the vendor does not serve at
3950
3917
  all, a 0 does get the reason row.) Any
3951
3918
  other value the served list lacks keeps its `reasons` row.
3952
- - A failed field-catalog read (issue #284) leaves the picklist labels
3953
- unavailable, not absent, and the result says so: `names.catalogError`
3954
- (`{ code, message }`) plus a note. Picklist fields this client read from an
3955
- earlier catalog for the resource get a `null` `*Label` and a `reasons` row
3956
- (`PICKLIST_CATALOG_UNAVAILABLE_NOTE`). Before any successful read the
3957
- picklist fields are unknown (the live catalog is the only source of
3958
- picklist-ness), so no `*Label` is added and the note says that. When the
3959
- earlier read shows no picklist field is keyed in the rows, the failure cost
3960
- no label and no `names` summary is served. `*Name` siblings do not depend on
3961
- the catalog and are served as usual. Retry the call to label the picklists.
3962
- - The resource `query()` iterator (the server's `output_path` walk) labels
3963
- each page the same way but has no result envelope, so it carries no `names`
3964
- summary: a capped id or a failed catalog read is not reported there. Use the
3965
- invoke `<resource>.query` op when the summary matters.
3966
- - Opt out per call: `names: false` (query bag key, aggregate opts, fetchMany /
3967
- fetchByIds opts). A non-boolean `names` is refused pre-request.
3919
+ - A failed field-catalog read leaves the picklist fields UNKNOWN for the call
3920
+ (the live catalog is the only source of picklist-ness): the `*Label` siblings
3921
+ are simply absent and nothing is reported (the 0.39.x `catalogError` channel
3922
+ was removed in issue #287). `*Name` siblings do not depend on the catalog and
3923
+ are served as usual. Retry the call to label the picklists.
3924
+ - The resource `query()` iterator (the server's `output_path` walk) adds the
3925
+ siblings to each page the same way when opted in but has no result envelope,
3926
+ so it carries no `names` summary: the cold-read cap is not reported there
3927
+ (a budget-exhausted id simply serves `null`). Use the invoke
3928
+ `<resource>.query` op when the summary matters.
3968
3929
  - Caches: picklists ride the field cache, staff and record names the record
3969
3930
  cache (all default-on; invalidated as documented in §3 —
3970
3931
  a write through the client clears the affected resource's entries, TTL
3971
3932
  otherwise). A warm call costs zero extra vendor calls; a cold one costs one
3972
3933
  field-metadata GET for the resource plus one GET per distinct uncached id.
3973
3934
  - Cold-read cap: at most `DEFAULT_MAX_NAME_READS` (20) uncached name reads per
3974
- call (per resource for fetch batches). Ids over the cap get a `null` `*Name`
3975
- and the result carries a `names` summary — never silently unlabelled:
3976
- `names: { truncated: true, capped: N, note: "N id(s) left unlabelled (their *Name is null): the per-call cap of 20 uncached name reads was hit — …", reasons? }`.
3977
- `names` is also present (with `truncated: false`) when a picklist label could
3978
- not be resolved (`reasons` maps the field to why) or the field-catalog read
3979
- failed (`catalogError`). On `fetchMany` / `fetchByIds` `reasons` is keyed
3980
- `<resource>.<field>` (even when every item names one resource) and
3981
- `catalogErrors` lists every failing resource (`catalogError` is the first). Absent `names` = every sibling was resolved.
3982
- On the composite path a failed catalog read serves the same unavailable note
3983
- and a `catalogError` on the section, never the "retrying will not help" note.
3935
+ call (per resource for fetch batches). The `names` summary appears ONLY when
3936
+ something was left unlabelled — the cap was hit and/or a picklist label could
3937
+ not be resolved: `{ note, reasons? }` (issue #287 trimmed the structured
3938
+ `truncated` / `capped` / `catalogError` bookkeeping — the note is the
3939
+ summary). Cap note: `"ids left unlabelled (their *Name is null): the
3940
+ per-call cap of 20 uncached name reads was hit — names already read are
3941
+ cached, so repeating or narrowing the call labels more"`. Unresolvable
3942
+ picklists add a note clause and a `reasons` entry per field (the
3943
+ field-catalog honesty note / the value-not-served note); on `fetchMany` /
3944
+ `fetchByIds` the notes merge across resources and `reasons` keys the plain
3945
+ field name. Absent `names` = every sibling was resolved.
3984
3946
  - Response size (full model, wrapped names): a tickets row grows ~40% (live,
3985
3947
  100 rows, 31 siblings per row, some of them null: 341,178 B -> 477,325 B;
3986
3948
  more when every FK is populated), a time_entries row ~77% (worst case, every
@@ -1,8 +1,12 @@
1
1
  # Live testing (the morning run)
2
2
 
3
- The runbook for the REAL live run against an Autotask tenant. Reads
4
- credentials ONLY from the process environment. Nothing is committed, nothing
5
- is printed (the scripts emit env-var names only — verified by audit LIVE-1).
3
+ The runbook for the REAL live run against an Autotask tenant. Credentials come
4
+ from the process environment: the scripts autoload `./.env` (`AUTOTASK_ENV_FILE`
5
+ overrides the path; variables already in the shell win), so the same values can
6
+ be exported in the shell instead. Nothing is committed, and nothing sensitive
7
+ is printed: the scripts emit env-var names only (verified by audit LIVE-1), and
8
+ the `--dry` preflight prints a value-free summary — a length plus a 2-character
9
+ prefix per value (shorter values print neither).
6
10
  The scripts FAIL CLOSED without credentials (exit 2) — a green exit is never
7
11
  a false pass.
8
12
 
@@ -13,8 +17,8 @@ a false pass.
13
17
  test old bytes; the check is newest-src vs oldest-dist mtimes, the
14
18
  `--allow-stale-dist` escape is explicit):
15
19
  npm run build
16
- - Credentials (values as placeholders — export in the SAME shell you run the
17
- tests):
20
+ - Credentials (values as placeholders — in `./.env`, or exported in the SAME
21
+ shell you run the tests; the shell wins):
18
22
  export AUTOTASK_TRACKING_IDENTIFIER="<your Tracking Identifier — Admin > API User Security>"
19
23
  export AUTOTASK_USERNAME="<your Username — API access must be enabled for that user>"
20
24
  export AUTOTASK_SECRET="<your Password/Secret — Admin > API User Security>"
@@ -151,7 +151,7 @@ Two sources in, markdown table out: the node repo (the ops column is re-derived
151
151
  | `ticketSecondaryResources` | TicketSecondaryResource | yes | create, delete, get, getMany, count | — | count, create, delete, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve |
152
152
  | `ticketWebhooks` | TicketWebhook | yes | get, getMany, delete | — | count, create, delete, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve, search, update |
153
153
  | `tickets` | Ticket | no | create, get, getMany, slaHealthCheck, summary, timeline, update, count | slaHealthCheck → `tickets.slaHealthCheck`; summary → `tickets.summary`; timeline → `tickets.timeline` | changeStatus, count, create, findByTicketNumber, forResource, fullContext, get, getContext, getEntityInfo, getFieldInfo, getUdfInfo, lineOfBusiness, openWithNoteAndAssign, query, resolve, search, slaHealthCheck, statusSemantics, summary, timeline, update |
154
- | `timeEntries` | TimeEntry | no | create, update, get, getMany, getPosted, getUnposted, delete, count, createIfNotExists | createIfNotExists → `time_entries.createIfNotExists`; customRange → `time_entries.customRange`; getPosted → `time_entries.getPosted`; getUnposted → `time_entries.getUnposted`; last14 → `time_entries.last14`; last24h → `time_entries.last24h`; last30 → `time_entries.last30`; last7 → `time_entries.last7`; last90 → `time_entries.last90`; lastFullFortnight → `time_entries.lastFullFortnight`; lastFullMonth → `time_entries.lastFullMonth`; lastFullQuarter → `time_entries.lastFullQuarter`; lastFullWeek → `time_entries.lastFullWeek`; today → `time_entries.today`; yesterday → `time_entries.yesterday` | approve, count, create, createForResource, createIfNotExists, customRange, dateWorkedWindow, delete, get, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, moveToTicket, postedWithin, query, recentTickets, resolve, today, update, yesterday |
154
+ | `timeEntries` | TimeEntry | no | create, update, get, getMany, getPosted, getUnposted, delete, count, createIfNotExists | createIfNotExists → `time_entries.createIfNotExists`; customRange → `time_entries.customRange`; getPosted → `time_entries.getPosted`; getUnposted → `time_entries.getUnposted`; last14 → `time_entries.last14`; last24h → `time_entries.last24h`; last30 → `time_entries.last30`; last7 → `time_entries.last7`; last90 → `time_entries.last90`; lastFullFortnight → `time_entries.lastFullFortnight`; lastFullMonth → `time_entries.lastFullMonth`; lastFullQuarter → `time_entries.lastFullQuarter`; lastFullWeek → `time_entries.lastFullWeek`; today → `time_entries.today`; yesterday → `time_entries.yesterday` | approve, count, create, createForResource, createIfNotExists, customRange, dateWorkedWindow, delete, get, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, moveToTicket, postedWithin, query, resolve, today, update, yesterday |
155
155
  | `timeEntryAttachments` | TimeEntryAttachment | yes | create, getMany, count, download, delete | download → `time_entry_attachments.download` | count, create, delete, download, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve |
156
156
  | `timeOffRequests` | TimeOffRequest | no | get, getMany, count, approve, reject | approve → `time_off_requests.approve`; reject → `time_off_requests.reject` | approve, count, get, getEntityInfo, getFieldInfo, getUdfInfo, query, reject, resolve |
157
157
  | `(none — NOT IMPLEMENTED in node)` | InventoryProduct (StockedItemsAdd/Remove/Transfer pseudo-entities) | yes | | stockedItemsAdd → `inventory_products.stockedItemsAdd`; stockedItemsRemove → `inventory_products.stockedItemsRemove`; stockedItemsTransfer → `inventory_products.stockedItemsTransfer` | count, create, delete, get, getEntityInfo, getFieldInfo, getUdfInfo, query, resolve, stockedItemsAdd, stockedItemsRemove, stockedItemsTransfer, update |
@@ -0,0 +1,121 @@
1
+ # Releasing node-autotask
2
+
3
+ Publishing happens only through `.github/workflows/release.yml`, which runs when a `v<version>` tag is pushed.
4
+ Nobody (owner or agent) runs `npm publish` by hand. The day-to-day steps (version bump, merge, tag, confirm,
5
+ record the plan hash) are the close-out checklist in [AGENTS.md](../AGENTS.md#5-close-out-autonomous-after-the-gates).
6
+ This page covers the setup only the owner can do, the first CI release, and rollback.
7
+
8
+ ## Safeguards (no approval step)
9
+
10
+ **Pushing a `v*` tag is the release.** The owner has delegated merge, tag and publish to agents: no
11
+ per-release approval is requested. With approval removed, the only safeguards are the cadence gates in
12
+ [AGENTS.md](../AGENTS.md) (green verify and CI, `y-pragent` all-clear on the final head, stronger-model QA,
13
+ verified merge, the hard stops) and this workflow's own checks. A reviewer on the `release` environment, branch
14
+ protection and a `v*` tag ruleset would add a hard gate, but need a GitHub plan that supports them on a private
15
+ repo (see below). If agents are blocked from `gh pr merge` or tag pushes by the permission classifier, add
16
+ Bash allow rules to `.claude/settings.json`: `gh pr merge *`, `git tag *`, `git push origin v*`,
17
+ `gh release create *`.
18
+
19
+ ## What the workflows do
20
+
21
+ - `ci.yml`: `npm ci`, then `npm run verify` with `COVERAGE=1`, so its single test run also enforces the coverage
22
+ thresholds. Runs on every pull request and push to `main`. On a pull request whose `package.json` version
23
+ differs from the base branch it also runs the release-consistency checks (`release:check`, plan hash
24
+ recorded, non-empty CHANGELOG section).
25
+ - `release.yml` (on tag `v*`):
26
+ 1. `verify` job (`contents: read`, no publish rights): `npm ci --ignore-scripts`; checks the tag is
27
+ annotated, equals `v` + `package.json` version, sits on `main`, has its plan hash recorded and a non-empty
28
+ `## [<version>]` CHANGELOG section; runs `release:check`, `clean` and `verify`; checks the tree is
29
+ unchanged; packs the tarball, records its sha256 and uploads it with the release notes.
30
+ 2. `publish` job (`id-token: write` only, environment `release`): no checkout, no project install, no project
31
+ scripts, so no dependency code runs where a publish credential can be minted. Downloads the tarball, checks
32
+ its hash, then guards (public registry, not a prerelease, not below npm `latest`, skip if already on npm)
33
+ and runs `npm publish <tarball> --ignore-scripts --access public` via npm trusted publishing (OIDC).
34
+ 3. `github-release` job (`contents: write`, after `publish`): `gh release create v<version>` with the
35
+ CHANGELOG section as notes.
36
+
37
+ No `--provenance` while the repo is private: npm rejects it (E422), after already writing the signing entry to
38
+ the public Rekor transparency log. When the repo goes public, add `--provenance` to the publish command.
39
+
40
+ ## Owner setup
41
+
42
+ Labels below are as of writing; if a screen differs, check the current npm / GitHub UI and docs.
43
+
44
+ ### 1. npm trusted publishing (required)
45
+
46
+ `release.yml` publishes with npm trusted publishing (OIDC): no long-lived token, so nothing to rotate or leak.
47
+ Before the first CI release, on npmjs.com in the `node-autotask` package settings, add a GitHub Actions trusted
48
+ publisher: user `msoukhomlinov`, repository `node-autotask`, workflow filename `release.yml`, environment
49
+ `release`. npm must be >= 11.5.1 (the workflow installs exactly 11.5.1; bump it deliberately). Trusted publishing from a private repository works;
50
+ only the provenance attestation is unavailable there (check the current npm docs if a screen differs). Until
51
+ this is configured the publish step fails with an auth error before anything is published.
52
+
53
+ After the first release succeeds this way, delete the old `NPM_TOKEN` secret (`gh secret delete NPM_TOKEN`),
54
+ revoke the token on npmjs.com, and set the package's publishing access to disallow tokens.
55
+
56
+ ### 2. The `release` environment (optional, plan-dependent)
57
+
58
+ `environment: release` is harmless when the environment does not exist: GitHub creates it, unprotected, on the
59
+ first run. To make it a second gate, in Settings > Environments > `release`:
60
+
61
+ 1. Add yourself as a required reviewer. Leave "prevent self-review" off: agents push tags with your
62
+ credentials, so the run counts as triggered by you.
63
+ 2. Restrict deployments to tags matching `v*`.
64
+
65
+ On a private repository, environment reviewers and deployment rules may need a paid plan: check the current
66
+ GitHub docs.
67
+
68
+ ### 3. Branch and tag protection (needs GitHub Pro or a public repo)
69
+
70
+ Branch protection and rulesets are not available on the current plan for this private repo (the API answers
71
+ "Upgrade to GitHub Pro or make this repository public"). When they are:
72
+
73
+ 1. `main`: require a pull request and the `verify` check from `ci.yml`; block force pushes and deletion.
74
+ 2. Tags `v*`: restrict creation, update and deletion to yourself.
75
+ 3. `y-pragent` cannot be a required check (it posts comments, not an approving review or status); its
76
+ final-head all-clear stays a process rule (AGENTS.md step 3).
77
+
78
+ **Residual risk until then:** anyone with write access can push a `v*` tag on a non-`main` commit and run
79
+ *that commit's* `release.yml`, which can mint an OIDC publish credential (the on-`main` check lives in the
80
+ workflow file, so a modified workflow can drop it). The npm trusted publisher is bound to the `release.yml`
81
+ filename and the `release` environment, not to a branch, so this stays open until a deployment tag restriction or
82
+ required reviewers exist on that environment. Keep write access to people and agents bound by AGENTS.md.
83
+
84
+ ### 4. GitHub Actions minutes
85
+
86
+ CI runs on every PR and push to `main`; on a private repository that consumes the account's Actions minutes.
87
+
88
+ ## The first CI-driven release
89
+
90
+ 1. Confirm the npm trusted publisher is configured (setup 1); optionally set up the `release` environment.
91
+ 2. Run the cadence to close-out: release PR with the version bump and CHANGELOG heading, `y-pragent`
92
+ all-clear on the final head, stronger-model QA SHIP, merged; then the annotated tag on the merge commit is pushed.
93
+ 3. Watch the run (`gh run list --workflow release.yml`, `gh run watch`); approve the deployment if reviewers
94
+ are configured.
95
+ 4. Check `npm view node-autotask@<version> version` and `gh release view v<version>`.
96
+
97
+ If the run fails:
98
+
99
+ - **Before `npm publish`** (nothing published): a re-run uses the workflow file *at the tagged commit*. A
100
+ transient failure can be re-run as is. If the fix is in the workflow or the code, merge it to `main`, delete
101
+ the unused tag (`git push origin :refs/tags/v<version>` and `git tag -d v<version>`), and tag the new merge
102
+ commit with the same version. Only ever do this when `npm view node-autotask@<version>` shows nothing.
103
+ - **After `npm publish`** (npm has the version but the run is red): re-run failed jobs; a re-run publish step sees the
104
+ version on npm and skips, then `github-release` runs. Or create the release by hand:
105
+ `gh release create v<version> --verify-tag --title v<version> --notes-file <CHANGELOG section file>`.
106
+ - **A cancelled run** (a newer tag queued behind it): re-run it.
107
+ - **Publish fails with an auth error**: the trusted publisher is not configured or does not match this repo,
108
+ workflow filename and environment (setup 1).
109
+
110
+ ## Rollback
111
+
112
+ npm versions are immutable: never `npm unpublish` (it is restricted after 72 hours anyway, and the MCP server
113
+ pins exact versions). Instead:
114
+
115
+ 1. Deprecate the bad version: `npm deprecate node-autotask@<bad> "<reason>; use <good>"`.
116
+ 2. If `latest` points at it, move the tag back: `npm dist-tag add node-autotask@<good> latest`.
117
+ 3. Ship the fix as a new version through the normal cadence (minor if the plan changed).
118
+ 4. Edit the GitHub release notes to point at the fix; do not delete a published version's git tag.
119
+ 5. Tell the MCP server repo (`msoukhomlinov/iit-mcp-autotask`) which version to pin.
120
+
121
+ `npm deprecate` and `npm dist-tag` need an authenticated npm session with 2FA; they are owner actions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-autotask",
3
- "version": "0.40.0",
3
+ "version": "0.42.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,
@@ -114,7 +114,7 @@
114
114
  "coverage": "vitest run --coverage",
115
115
  "lint": "eslint src test",
116
116
  "lint:fix": "eslint src test --fix",
117
- "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",
117
+ "verify": "npm run lockfile:check && npm run typecheck && npm run lint && npm run generate:check && npm run docs:render: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",
118
118
  "plan:derive": "node scripts/derive-plan.mjs",
119
119
  "generate:check": "node scripts/generate-batch.mjs --check && node scripts/generate-batch.mjs --check --group B",
120
120
  "capabilities:build": "node scripts/generate-capabilities.mjs",
@@ -128,6 +128,7 @@
128
128
  "catalog:build": "node scripts/build-tool-catalog.mjs",
129
129
  "catalog:check": "node scripts/build-tool-catalog.mjs --check",
130
130
  "docs:render": "node scripts/render-docs.mjs",
131
+ "docs:render:check": "node scripts/render-docs.mjs --check",
131
132
  "mcp:check-surface": "node scripts/check-mcp-surface.mjs",
132
133
  "n8n:project": "node scripts/project-n8n-mcp.mjs",
133
134
  "n8n:project:check": "node scripts/project-n8n-mcp.mjs --check",